1pub mod builder;
13pub mod generation;
14#[doc(hidden)]
15pub mod registry;
16pub mod validation;
17
18use crate::response::AiGenerator;
19use crate::response_rewriter::ResponseRewriter;
20use crate::{OpenApiOperation, OpenApiRoute, OpenApiSchema, OpenApiSpec};
21use axum::extract::{DefaultBodyLimit, Path as AxumPath, RawQuery};
22use axum::http::HeaderMap;
23use axum::response::IntoResponse;
24use axum::routing::*;
25use axum::{Json, Router};
26pub use builder::*;
27use chrono::Utc;
28pub use generation::*;
29use mockforge_foundation::ai_response::RequestContext;
30use mockforge_foundation::error::{Error, Result};
31use mockforge_foundation::latency::LatencyInjector;
32use mockforge_foundation::response_generation_trace::ResponseGenerationTrace;
33use mockforge_foundation::schema_diff::validation_diff;
34use once_cell::sync::Lazy;
35use openapiv3::ParameterSchemaOrContent;
36use serde_json::{json, Map, Value};
37use std::collections::{HashMap, HashSet, VecDeque};
38use std::sync::{Arc, Mutex};
39use tracing;
40pub use validation::*;
41
42#[derive(Clone)]
44pub struct OpenApiRouteRegistry {
45 spec: Arc<OpenApiSpec>,
47 routes: Vec<OpenApiRoute>,
49 options: ValidationOptions,
51 custom_fixture_loader: Option<Arc<crate::custom_fixture::CustomFixtureLoader>>,
53}
54
55#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default)]
57pub enum ValidationMode {
58 Disabled,
60 #[default]
62 Warn,
63 Enforce,
65}
66
67#[derive(Debug, Clone)]
69pub struct ValidationOptions {
70 pub request_mode: ValidationMode,
72 pub aggregate_errors: bool,
74 pub validate_responses: bool,
76 pub overrides: HashMap<String, ValidationMode>,
78 pub admin_skip_prefixes: Vec<String>,
80 pub response_template_expand: bool,
82 pub validation_status: Option<u16>,
84}
85
86impl Default for ValidationOptions {
87 fn default() -> Self {
88 Self {
89 request_mode: ValidationMode::Enforce,
90 aggregate_errors: true,
91 validate_responses: false,
92 overrides: HashMap::new(),
93 admin_skip_prefixes: Vec::new(),
94 response_template_expand: false,
95 validation_status: None,
96 }
97 }
98}
99
100#[derive(Clone)]
105pub struct RouterContext {
106 pub custom_fixture_loader: Option<Arc<crate::custom_fixture::CustomFixtureLoader>>,
108 pub latency_injector: Option<LatencyInjector>,
110 pub failure_injector: Option<mockforge_foundation::failure_injection::FailureInjector>,
112 pub response_rewriter: Option<Arc<dyn ResponseRewriter>>,
116 pub overrides_enabled: bool,
119 pub ai_generator: Option<Arc<dyn AiGenerator + Send + Sync>>,
121 pub mockai: Option<
125 Arc<
126 tokio::sync::RwLock<
127 dyn mockforge_foundation::intelligent_behavior::MockAiBehavior + Send + Sync,
128 >,
129 >,
130 >,
131 pub enable_full_validation: bool,
133 pub enable_template_expand: bool,
135 pub add_spec_endpoint: bool,
137}
138
139impl Default for RouterContext {
140 fn default() -> Self {
141 Self {
142 custom_fixture_loader: None,
143 latency_injector: None,
144 failure_injector: None,
145 response_rewriter: None,
146 overrides_enabled: false,
147 ai_generator: None,
148 mockai: None,
149 enable_full_validation: false,
150 enable_template_expand: false,
151 add_spec_endpoint: true,
152 }
153 }
154}
155
156fn openapi_body_limit_bytes() -> usize {
171 const DEFAULT_MB: usize = 50;
172 std::env::var("MOCKFORGE_HTTP_BODY_LIMIT_MB")
173 .ok()
174 .and_then(|v| v.parse::<usize>().ok())
175 .unwrap_or(DEFAULT_MB)
176 .saturating_mul(1024 * 1024)
177}
178
179impl OpenApiRouteRegistry {
180 pub fn new(spec: OpenApiSpec) -> Self {
182 Self::new_with_env(spec)
183 }
184
185 pub fn new_with_env(spec: OpenApiSpec) -> Self {
194 Self::new_with_env_and_persona(spec, None)
195 }
196
197 pub fn new_with_env_and_persona(
199 spec: OpenApiSpec,
200 persona: Option<Arc<mockforge_foundation::intelligent_behavior::Persona>>,
201 ) -> Self {
202 tracing::debug!("Creating OpenAPI route registry");
203 let spec = Arc::new(spec);
204 let routes = Self::generate_routes_with_persona(&spec, persona);
205 let options = ValidationOptions {
206 request_mode: match std::env::var("MOCKFORGE_REQUEST_VALIDATION")
207 .unwrap_or_else(|_| "enforce".into())
208 .to_ascii_lowercase()
209 .as_str()
210 {
211 "off" | "disable" | "disabled" => ValidationMode::Disabled,
212 "warn" | "warning" => ValidationMode::Warn,
213 _ => ValidationMode::Enforce,
214 },
215 aggregate_errors: std::env::var("MOCKFORGE_AGGREGATE_ERRORS")
216 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
217 .unwrap_or(true),
218 validate_responses: std::env::var("MOCKFORGE_RESPONSE_VALIDATION")
219 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
220 .unwrap_or(false),
221 overrides: HashMap::new(),
222 admin_skip_prefixes: Vec::new(),
223 response_template_expand: std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
224 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
225 .unwrap_or(false),
226 validation_status: std::env::var("MOCKFORGE_VALIDATION_STATUS")
227 .ok()
228 .and_then(|s| s.parse::<u16>().ok()),
229 };
230 Self {
231 spec,
232 routes,
233 options,
234 custom_fixture_loader: None,
235 }
236 }
237
238 pub fn new_with_options(spec: OpenApiSpec, options: ValidationOptions) -> Self {
240 Self::new_with_options_and_persona(spec, options, None)
241 }
242
243 pub fn new_with_options_and_persona(
245 spec: OpenApiSpec,
246 options: ValidationOptions,
247 persona: Option<Arc<mockforge_foundation::intelligent_behavior::Persona>>,
248 ) -> Self {
249 tracing::debug!("Creating OpenAPI route registry with custom options");
250 let spec = Arc::new(spec);
251 let routes = Self::generate_routes_with_persona(&spec, persona);
252 Self {
253 spec,
254 routes,
255 options,
256 custom_fixture_loader: None,
257 }
258 }
259
260 pub fn with_custom_fixture_loader(
262 mut self,
263 loader: Arc<crate::custom_fixture::CustomFixtureLoader>,
264 ) -> Self {
265 self.custom_fixture_loader = Some(loader);
266 self
267 }
268
269 pub fn clone_for_validation(&self) -> Self {
274 OpenApiRouteRegistry {
275 spec: self.spec.clone(),
276 routes: self.routes.clone(),
277 options: self.options.clone(),
278 custom_fixture_loader: self.custom_fixture_loader.clone(),
279 }
280 }
281
282 fn generate_routes_with_persona(
284 spec: &Arc<OpenApiSpec>,
285 persona: Option<Arc<mockforge_foundation::intelligent_behavior::Persona>>,
286 ) -> Vec<OpenApiRoute> {
287 let mut routes = Vec::new();
288
289 let all_paths_ops = spec.all_paths_and_operations();
290 tracing::debug!("Generating routes from OpenAPI spec with {} paths", all_paths_ops.len());
291
292 for (path, operations) in all_paths_ops {
293 tracing::debug!("Processing path: {}", path);
294 for (method, operation) in operations {
295 routes.push(OpenApiRoute::from_operation_with_persona(
296 &method,
297 path.clone(),
298 &operation,
299 spec.clone(),
300 persona.clone(),
301 ));
302 }
303 }
304
305 tracing::debug!("Generated {} total routes from OpenAPI spec", routes.len());
306 routes
307 }
308
309 pub fn routes(&self) -> &[OpenApiRoute] {
311 &self.routes
312 }
313
314 pub fn spec(&self) -> &OpenApiSpec {
316 &self.spec
317 }
318
319 fn normalize_path_for_dedup(path: &str) -> String {
323 let mut result = String::with_capacity(path.len());
324 let mut in_brace = false;
325 for ch in path.chars() {
326 if ch == '{' {
327 in_brace = true;
328 result.push_str("{_}");
329 } else if ch == '}' {
330 in_brace = false;
331 } else if !in_brace {
332 result.push(ch);
333 }
334 }
335 result
336 }
337
338 fn deduplicated_routes(&self) -> Vec<(String, &OpenApiRoute)> {
344 let mut result = Vec::new();
345 let mut registered_routes: HashSet<(String, String)> = HashSet::new();
346 let mut canonical_paths: HashMap<String, String> = HashMap::new();
347
348 for route in &self.routes {
349 if !route.is_valid_axum_path() {
350 tracing::warn!(
351 "Skipping route with unsupported path syntax: {} {}",
352 route.method,
353 route.path
354 );
355 continue;
356 }
357 let axum_path = route.axum_path();
358 let normalized = Self::normalize_path_for_dedup(&axum_path);
359 let axum_path = canonical_paths
360 .entry(normalized.clone())
361 .or_insert_with(|| axum_path.clone())
362 .clone();
363 let route_key = (route.method.clone(), normalized);
364 if !registered_routes.insert(route_key) {
365 tracing::debug!(
366 "Skipping duplicate route: {} {} (axum path: {})",
367 route.method,
368 route.path,
369 axum_path
370 );
371 continue;
372 }
373 result.push((axum_path, route));
374 }
375 result
376 }
377
378 fn route_for_method<H, T>(router: Router, path: &str, method: &str, handler: H) -> Router
382 where
383 H: axum::handler::Handler<T, ()>,
384 T: 'static,
385 {
386 match method {
387 "GET" => router.route(path, get(handler)),
388 "POST" => router.route(path, post(handler)),
389 "PUT" => router.route(path, put(handler)),
390 "DELETE" => router.route(path, delete(handler)),
391 "PATCH" => router.route(path, patch(handler)),
392 "HEAD" => router.route(path, head(handler)),
393 "OPTIONS" => router.route(path, options(handler)),
394 _ => router,
395 }
396 }
397
398 pub fn build_router(self) -> Router {
400 let ctx = RouterContext {
401 custom_fixture_loader: self.custom_fixture_loader.clone(),
402 enable_full_validation: true,
403 enable_template_expand: true,
404 add_spec_endpoint: true,
405 ..Default::default()
406 };
407 self.build_router_with_context(ctx)
408 }
409
410 fn build_router_with_context(self, ctx: RouterContext) -> Router {
415 let mut router = Router::new();
416 tracing::debug!("Building router from {} routes", self.routes.len());
417
418 let deduped = self.deduplicated_routes();
419 let ctx = Arc::new(ctx);
420 for (axum_path, route) in &deduped {
421 tracing::debug!("Adding route: {} {}", route.method, route.path);
422 let operation = route.operation.clone();
423 let method = route.method.clone();
424 let path_template = route.path.clone();
425 let validator = self.clone_for_validation();
426 let route_clone = (*route).clone();
427 let ctx = ctx.clone();
428
429 let mut operation_tags = operation.tags.clone();
431 if let Some(operation_id) = &operation.operation_id {
432 operation_tags.push(operation_id.clone());
433 }
434
435 let handler = move |AxumPath(path_params): AxumPath<HashMap<String, String>>,
437 RawQuery(raw_query): RawQuery,
438 headers: HeaderMap,
439 body: axum::body::Bytes| async move {
440 tracing::debug!("Handling OpenAPI request: {} {}", method, path_template);
441
442 if let Some(ref loader) = ctx.custom_fixture_loader {
444 use crate::request_fingerprint::RequestFingerprint;
445 use axum::http::{Method, Uri};
446
447 let mut request_path = path_template.clone();
449 for (key, value) in &path_params {
450 request_path = request_path.replace(&format!("{{{}}}", key), value);
451 }
452
453 let normalized_request_path =
455 crate::custom_fixture::CustomFixtureLoader::normalize_path(&request_path);
456
457 let query_string =
459 raw_query.as_ref().map(|q| q.to_string()).unwrap_or_default();
460
461 let uri_str = if query_string.is_empty() {
464 normalized_request_path.clone()
465 } else {
466 format!("{}?{}", normalized_request_path, query_string)
467 };
468
469 if let Ok(uri) = uri_str.parse::<Uri>() {
470 let http_method =
471 Method::from_bytes(method.as_bytes()).unwrap_or(Method::GET);
472 let body_slice = if body.is_empty() {
473 None
474 } else {
475 Some(body.as_ref())
476 };
477 let fingerprint =
478 RequestFingerprint::new(http_method, &uri, &headers, body_slice);
479
480 tracing::debug!(
482 "Checking fixture for {} {} (template: '{}', request_path: '{}', normalized: '{}', fingerprint.path: '{}')",
483 method,
484 path_template,
485 path_template,
486 request_path,
487 normalized_request_path,
488 fingerprint.path
489 );
490
491 if let Some(custom_fixture) = loader.load_fixture(&fingerprint) {
492 tracing::debug!(
493 "Using custom fixture for {} {}",
494 method,
495 path_template
496 );
497
498 if custom_fixture.delay_ms > 0 {
500 tokio::time::sleep(tokio::time::Duration::from_millis(
501 custom_fixture.delay_ms,
502 ))
503 .await;
504 }
505
506 let response_body = if custom_fixture.response.is_string() {
508 custom_fixture.response.as_str().unwrap().to_string()
509 } else {
510 serde_json::to_string(&custom_fixture.response)
511 .unwrap_or_else(|_| "{}".to_string())
512 };
513
514 let json_value: Value = serde_json::from_str(&response_body)
516 .unwrap_or_else(|_| serde_json::json!({}));
517
518 let status = axum::http::StatusCode::from_u16(custom_fixture.status)
520 .unwrap_or(axum::http::StatusCode::OK);
521
522 let mut response = (status, Json(json_value)).into_response();
523
524 let response_headers = response.headers_mut();
526 for (key, value) in &custom_fixture.headers {
527 if let (Ok(header_name), Ok(header_value)) = (
528 axum::http::HeaderName::from_bytes(key.as_bytes()),
529 axum::http::HeaderValue::from_str(value),
530 ) {
531 response_headers.insert(header_name, header_value);
532 }
533 }
534
535 if !custom_fixture.headers.contains_key("content-type") {
537 response_headers.insert(
538 axum::http::header::CONTENT_TYPE,
539 axum::http::HeaderValue::from_static("application/json"),
540 );
541 }
542
543 return response;
544 }
545 }
546 }
547
548 if let Some(ref failure_injector) = ctx.failure_injector {
550 if let Some((status_code, error_message)) =
551 failure_injector.process_request(&operation_tags)
552 {
553 let payload = serde_json::json!({
554 "error": error_message,
555 "injected_failure": true
556 });
557 let body_bytes = serde_json::to_vec(&payload)
558 .unwrap_or_else(|_| br#"{"error":"injected failure"}"#.to_vec());
559 return axum::http::Response::builder()
560 .status(
561 axum::http::StatusCode::from_u16(status_code)
562 .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR),
563 )
564 .header(axum::http::header::CONTENT_TYPE, "application/json")
565 .body(axum::body::Body::from(body_bytes))
566 .expect("Response builder should create valid response");
567 }
568 }
569
570 if let Some(ref injector) = ctx.latency_injector {
572 if let Err(e) = injector.inject_latency(&operation_tags).await {
573 tracing::warn!("Failed to inject latency: {}", e);
574 }
575 }
576
577 let scenario = headers
579 .get("X-Mockforge-Scenario")
580 .and_then(|v| v.to_str().ok())
581 .map(|s| s.to_string())
582 .or_else(|| std::env::var("MOCKFORGE_HTTP_SCENARIO").ok());
583
584 let status_override = headers
585 .get("X-Mockforge-Response-Status")
586 .and_then(|v| v.to_str().ok())
587 .and_then(|s| s.parse::<u16>().ok());
588
589 let (selected_status, mock_response) = route_clone
591 .mock_response_with_status_and_scenario_and_override(
592 scenario.as_deref(),
593 status_override,
594 );
595
596 if ctx.enable_full_validation {
598 let mut path_map = Map::new();
600 for (k, v) in &path_params {
601 path_map.insert(k.clone(), Value::String(v.clone()));
602 }
603
604 let mut query_map = Map::new();
606 if let Some(ref q) = raw_query {
607 for (k, v) in url::form_urlencoded::parse(q.as_bytes()) {
608 query_map.insert(k.to_string(), Value::String(v.to_string()));
609 }
610 }
611
612 let mut header_map = Map::new();
614 for p_ref in &operation.parameters {
615 if let Some(openapiv3::Parameter::Header { parameter_data, .. }) =
616 p_ref.as_item()
617 {
618 let name_lc = parameter_data.name.to_ascii_lowercase();
619 if let Ok(hn) = axum::http::HeaderName::from_bytes(name_lc.as_bytes()) {
620 if let Some(val) = headers.get(hn) {
621 if let Ok(s) = val.to_str() {
622 header_map.insert(
623 parameter_data.name.clone(),
624 Value::String(s.to_string()),
625 );
626 }
627 }
628 }
629 }
630 }
631
632 let mut cookie_map = Map::new();
634 if let Some(val) = headers.get(axum::http::header::COOKIE) {
635 if let Ok(s) = val.to_str() {
636 for part in s.split(';') {
637 let part = part.trim();
638 if let Some((k, v)) = part.split_once('=') {
639 cookie_map.insert(k.to_string(), Value::String(v.to_string()));
640 }
641 }
642 }
643 }
644
645 let is_multipart = headers
647 .get(axum::http::header::CONTENT_TYPE)
648 .and_then(|v| v.to_str().ok())
649 .map(|ct| ct.starts_with("multipart/form-data"))
650 .unwrap_or(false);
651
652 #[allow(unused_assignments)]
654 let mut multipart_fields = HashMap::new();
655 let mut _multipart_files = HashMap::new();
656 let mut body_json: Option<Value> = None;
657
658 if is_multipart {
659 match extract_multipart_from_bytes(&body, &headers).await {
661 Ok((fields, files)) => {
662 multipart_fields = fields;
663 _multipart_files = files;
664 let mut body_obj = Map::new();
666 for (k, v) in &multipart_fields {
667 body_obj.insert(k.clone(), v.clone());
668 }
669 if !body_obj.is_empty() {
670 body_json = Some(Value::Object(body_obj));
671 }
672 }
673 Err(e) => {
674 tracing::warn!("Failed to parse multipart data: {}", e);
675 }
676 }
677 } else {
678 body_json = if !body.is_empty() {
680 serde_json::from_slice(&body).ok()
681 } else {
682 None
683 };
684 }
685
686 if let Err(e) = validator.validate_request_with_all(
687 &path_template,
688 &method,
689 &path_map,
690 &query_map,
691 &header_map,
692 &cookie_map,
693 body_json.as_ref(),
694 ) {
695 let status_code =
697 validator.options.validation_status.unwrap_or_else(|| {
698 std::env::var("MOCKFORGE_VALIDATION_STATUS")
699 .ok()
700 .and_then(|s| s.parse::<u16>().ok())
701 .unwrap_or(400)
702 });
703
704 let payload = if status_code == 422 {
705 generate_enhanced_422_response(
707 &validator,
708 &path_template,
709 &method,
710 body_json.as_ref(),
711 &path_map,
712 &query_map,
713 &header_map,
714 &cookie_map,
715 )
716 } else {
717 let msg = format!("{}", e);
719 let detail_val = serde_json::from_str::<Value>(&msg)
720 .unwrap_or(serde_json::json!(msg));
721 json!({
722 "error": "request validation failed",
723 "detail": detail_val,
724 "method": method,
725 "path": path_template,
726 "timestamp": Utc::now().to_rfc3339(),
727 })
728 };
729
730 record_validation_error(&payload);
731 let status = axum::http::StatusCode::from_u16(status_code)
732 .unwrap_or(axum::http::StatusCode::BAD_REQUEST);
733
734 let body_bytes = serde_json::to_vec(&payload)
736 .unwrap_or_else(|_| br#"{"error":"Serialization failed"}"#.to_vec());
737
738 return axum::http::Response::builder()
739 .status(status)
740 .header(axum::http::header::CONTENT_TYPE, "application/json")
741 .body(axum::body::Body::from(body_bytes))
742 .expect("Response builder should create valid response with valid headers and body");
743 }
744 }
745
746 let mut final_response = mock_response.clone();
755 let env_expand: Option<bool> = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
756 .ok()
757 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"));
758 let expand = match env_expand {
759 Some(v) => v,
760 None => {
761 ctx.enable_template_expand || validator.options.response_template_expand
762 }
763 };
764 if expand {
765 if let Some(ref rewriter) = ctx.response_rewriter {
766 rewriter.expand_tokens(&mut final_response);
767 }
768 }
769
770 if ctx.overrides_enabled {
772 if let Some(ref rewriter) = ctx.response_rewriter {
773 let op_tags =
774 operation.operation_id.clone().map(|id| vec![id]).unwrap_or_default();
775 rewriter.apply_overrides(
776 &operation.operation_id.clone().unwrap_or_default(),
777 &op_tags,
778 &path_template,
779 &mut final_response,
780 );
781 }
782 }
783
784 if ctx.enable_full_validation {
786 if validator.options.validate_responses {
788 if let Some((status_code, _response)) = operation
790 .responses
791 .responses
792 .iter()
793 .filter_map(|(status, resp)| match status {
794 openapiv3::StatusCode::Code(code)
795 if *code >= 200 && *code < 300 =>
796 {
797 resp.as_item().map(|r| ((*code), r))
798 }
799 openapiv3::StatusCode::Range(range)
800 if *range >= 200 && *range < 300 =>
801 {
802 resp.as_item().map(|r| (200, r))
803 }
804 _ => None,
805 })
806 .next()
807 {
808 if serde_json::from_value::<Value>(final_response.clone()).is_err() {
810 tracing::warn!(
811 "Response validation failed: invalid JSON for status {}",
812 status_code
813 );
814 }
815 }
816 }
817
818 let mut trace = ResponseGenerationTrace::new();
820 trace.set_final_payload(final_response.clone());
821
822 if let Some((_status_code, response_ref)) = operation
824 .responses
825 .responses
826 .iter()
827 .filter_map(|(status, resp)| match status {
828 openapiv3::StatusCode::Code(code) if *code == selected_status => {
829 resp.as_item().map(|r| ((*code), r))
830 }
831 openapiv3::StatusCode::Range(range)
832 if *range >= 200 && *range < 300 =>
833 {
834 resp.as_item().map(|r| (200, r))
835 }
836 _ => None,
837 })
838 .next()
839 .or_else(|| {
840 operation
842 .responses
843 .responses
844 .iter()
845 .filter_map(|(status, resp)| match status {
846 openapiv3::StatusCode::Code(code)
847 if *code >= 200 && *code < 300 =>
848 {
849 resp.as_item().map(|r| ((*code), r))
850 }
851 _ => None,
852 })
853 .next()
854 })
855 {
856 let response_item = response_ref;
858 if let Some(content) = response_item.content.get("application/json") {
860 if let Some(schema_ref) = &content.schema {
861 if let Some(schema) = schema_ref.as_item() {
863 if let Ok(schema_json) = serde_json::to_value(schema) {
864 let validation_errors =
866 validation_diff(&schema_json, &final_response);
867 trace.set_schema_validation_diff(validation_errors);
868 }
869 }
870 }
871 }
872 }
873
874 let mut response = Json(final_response).into_response();
876 response.extensions_mut().insert(trace);
877 *response.status_mut() = axum::http::StatusCode::from_u16(selected_status)
878 .unwrap_or(axum::http::StatusCode::OK);
879 return response;
880 }
881
882 let mut response = Json(final_response).into_response();
884 *response.status_mut() = axum::http::StatusCode::from_u16(selected_status)
885 .unwrap_or(axum::http::StatusCode::OK);
886 response
887 };
888
889 router = Self::route_for_method(router, axum_path, &route.method, handler);
890 }
891
892 if ctx.add_spec_endpoint {
894 let spec_json = serde_json::to_value(&self.spec.spec).unwrap_or(Value::Null);
895 router = router.route("/openapi.json", get(move || async move { Json(spec_json) }));
896 }
897
898 router.layer(DefaultBodyLimit::max(openapi_body_limit_bytes()))
902 }
903
904 pub fn build_router_with_latency(self, latency_injector: LatencyInjector) -> Router {
906 self.build_router_with_injectors(latency_injector, None)
907 }
908
909 pub fn build_router_with_injectors(
911 self,
912 latency_injector: LatencyInjector,
913 failure_injector: Option<mockforge_foundation::failure_injection::FailureInjector>,
914 ) -> Router {
915 self.build_router_with_injectors_and_overrides(
916 latency_injector,
917 failure_injector,
918 None,
919 false,
920 )
921 }
922
923 pub fn build_router_with_injectors_and_overrides(
927 self,
928 latency_injector: LatencyInjector,
929 failure_injector: Option<mockforge_foundation::failure_injection::FailureInjector>,
930 response_rewriter: Option<Arc<dyn ResponseRewriter>>,
931 overrides_enabled: bool,
932 ) -> Router {
933 let ctx = RouterContext {
934 custom_fixture_loader: self.custom_fixture_loader.clone(),
935 latency_injector: Some(latency_injector),
936 failure_injector,
937 response_rewriter,
938 overrides_enabled,
939 enable_full_validation: true,
940 enable_template_expand: true,
941 add_spec_endpoint: true,
942 ..Default::default()
943 };
944 self.build_router_with_context(ctx)
945 }
946
947 pub fn get_route(&self, path: &str, method: &str) -> Option<&OpenApiRoute> {
949 self.routes.iter().find(|route| route.path == path && route.method == method)
950 }
951
952 pub fn get_routes_for_path(&self, path: &str) -> Vec<&OpenApiRoute> {
954 self.routes.iter().filter(|route| route.path == path).collect()
955 }
956
957 pub fn validate_request(&self, path: &str, method: &str, body: Option<&Value>) -> Result<()> {
959 self.validate_request_with(path, method, &Map::new(), &Map::new(), body)
960 }
961
962 pub fn validate_request_with(
964 &self,
965 path: &str,
966 method: &str,
967 path_params: &Map<String, Value>,
968 query_params: &Map<String, Value>,
969 body: Option<&Value>,
970 ) -> Result<()> {
971 self.validate_request_with_all(
972 path,
973 method,
974 path_params,
975 query_params,
976 &Map::new(),
977 &Map::new(),
978 body,
979 )
980 }
981
982 #[allow(clippy::too_many_arguments)]
984 pub fn validate_request_with_all(
985 &self,
986 path: &str,
987 method: &str,
988 path_params: &Map<String, Value>,
989 query_params: &Map<String, Value>,
990 header_params: &Map<String, Value>,
991 cookie_params: &Map<String, Value>,
992 body: Option<&Value>,
993 ) -> Result<()> {
994 for pref in &self.options.admin_skip_prefixes {
996 if !pref.is_empty() && path.starts_with(pref) {
997 return Ok(());
998 }
999 }
1000 let env_mode = std::env::var("MOCKFORGE_REQUEST_VALIDATION").ok().map(|v| {
1002 match v.to_ascii_lowercase().as_str() {
1003 "off" | "disable" | "disabled" => ValidationMode::Disabled,
1004 "warn" | "warning" => ValidationMode::Warn,
1005 _ => ValidationMode::Enforce,
1006 }
1007 });
1008 let aggregate = std::env::var("MOCKFORGE_AGGREGATE_ERRORS")
1009 .ok()
1010 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1011 .unwrap_or(self.options.aggregate_errors);
1012 let env_overrides: Option<Map<String, Value>> =
1014 std::env::var("MOCKFORGE_VALIDATION_OVERRIDES_JSON")
1015 .ok()
1016 .and_then(|s| serde_json::from_str::<Value>(&s).ok())
1017 .and_then(|v| v.as_object().cloned());
1018 let mut effective_mode = env_mode.unwrap_or(self.options.request_mode.clone());
1020 if let Some(map) = &env_overrides {
1022 if let Some(v) = map.get(&format!("{} {}", method, path)) {
1023 if let Some(m) = v.as_str() {
1024 effective_mode = match m {
1025 "off" => ValidationMode::Disabled,
1026 "warn" => ValidationMode::Warn,
1027 _ => ValidationMode::Enforce,
1028 };
1029 }
1030 }
1031 }
1032 if let Some(override_mode) = self.options.overrides.get(&format!("{} {}", method, path)) {
1034 effective_mode = override_mode.clone();
1035 }
1036 if matches!(effective_mode, ValidationMode::Disabled) {
1037 return Ok(());
1038 }
1039 if let Some(route) = self.get_route(path, method) {
1040 if matches!(effective_mode, ValidationMode::Disabled) {
1041 return Ok(());
1042 }
1043 let mut errors: Vec<String> = Vec::new();
1044 let mut details: Vec<Value> = Vec::new();
1045 if let Some(schema) = &route.operation.request_body {
1047 if let Some(value) = body {
1048 let request_body = match schema {
1050 openapiv3::ReferenceOr::Item(rb) => Some(rb),
1051 openapiv3::ReferenceOr::Reference { reference } => {
1052 self.spec
1054 .spec
1055 .components
1056 .as_ref()
1057 .and_then(|components| {
1058 components.request_bodies.get(
1059 reference.trim_start_matches("#/components/requestBodies/"),
1060 )
1061 })
1062 .and_then(|rb_ref| rb_ref.as_item())
1063 }
1064 };
1065
1066 if let Some(rb) = request_body {
1067 if let Some(content) = rb.content.get("application/json") {
1068 if let Some(schema_ref) = &content.schema {
1069 match schema_ref {
1071 openapiv3::ReferenceOr::Item(schema) => {
1072 if let Err(validation_error) =
1074 OpenApiSchema::new(schema.clone()).validate(value)
1075 {
1076 let error_msg = validation_error.to_string();
1077 errors.push(format!(
1078 "body validation failed: {}",
1079 error_msg
1080 ));
1081 if aggregate {
1082 details.push(serde_json::json!({"path":"body","code":"schema_validation","message":error_msg}));
1083 }
1084 }
1085 }
1086 openapiv3::ReferenceOr::Reference { reference } => {
1087 if let Some(resolved_schema_ref) =
1089 self.spec.get_schema(reference)
1090 {
1091 if let Err(validation_error) = OpenApiSchema::new(
1092 resolved_schema_ref.schema.clone(),
1093 )
1094 .validate(value)
1095 {
1096 let error_msg = validation_error.to_string();
1097 errors.push(format!(
1098 "body validation failed: {}",
1099 error_msg
1100 ));
1101 if aggregate {
1102 details.push(serde_json::json!({"path":"body","code":"schema_validation","message":error_msg}));
1103 }
1104 }
1105 } else {
1106 errors.push(format!("body validation failed: could not resolve schema reference {}", reference));
1108 if aggregate {
1109 details.push(serde_json::json!({"path":"body","code":"reference_error","message":"Could not resolve schema reference"}));
1110 }
1111 }
1112 }
1113 }
1114 }
1115 }
1116 } else {
1117 errors.push("body validation failed: could not resolve request body or no application/json content".to_string());
1119 if aggregate {
1120 details.push(serde_json::json!({"path":"body","code":"reference_error","message":"Could not resolve request body reference"}));
1121 }
1122 }
1123 } else {
1124 errors.push("body: Request body is required but not provided".to_string());
1125 details.push(serde_json::json!({"path":"body","code":"required","message":"Request body is required"}));
1126 }
1127 } else if body.is_some() {
1128 tracing::debug!("Body provided for operation without requestBody; accepting");
1130 }
1131
1132 for p_ref in &route.operation.parameters {
1134 if let Some(p) = p_ref.as_item() {
1135 match p {
1136 openapiv3::Parameter::Path { parameter_data, .. } => {
1137 validate_parameter(
1138 parameter_data,
1139 path_params,
1140 "path",
1141 aggregate,
1142 &mut errors,
1143 &mut details,
1144 );
1145 }
1146 openapiv3::Parameter::Query {
1147 parameter_data,
1148 style,
1149 ..
1150 } => {
1151 let deep_value = if matches!(style, openapiv3::QueryStyle::DeepObject) {
1154 let prefix_bracket = format!("{}[", parameter_data.name);
1155 let mut obj = Map::new();
1156 for (key, val) in query_params.iter() {
1157 if let Some(rest) = key.strip_prefix(&prefix_bracket) {
1158 if let Some(prop) = rest.strip_suffix(']') {
1159 obj.insert(prop.to_string(), val.clone());
1160 }
1161 }
1162 }
1163 if obj.is_empty() {
1164 None
1165 } else {
1166 Some(Value::Object(obj))
1167 }
1168 } else {
1169 None
1170 };
1171 let style_str = match style {
1172 openapiv3::QueryStyle::Form => Some("form"),
1173 openapiv3::QueryStyle::SpaceDelimited => Some("spaceDelimited"),
1174 openapiv3::QueryStyle::PipeDelimited => Some("pipeDelimited"),
1175 openapiv3::QueryStyle::DeepObject => Some("deepObject"),
1176 };
1177 validate_parameter_with_deep_object(
1178 parameter_data,
1179 query_params,
1180 "query",
1181 deep_value,
1182 style_str,
1183 aggregate,
1184 &mut errors,
1185 &mut details,
1186 );
1187 }
1188 openapiv3::Parameter::Header { parameter_data, .. } => {
1189 validate_parameter(
1190 parameter_data,
1191 header_params,
1192 "header",
1193 aggregate,
1194 &mut errors,
1195 &mut details,
1196 );
1197 }
1198 openapiv3::Parameter::Cookie { parameter_data, .. } => {
1199 validate_parameter(
1200 parameter_data,
1201 cookie_params,
1202 "cookie",
1203 aggregate,
1204 &mut errors,
1205 &mut details,
1206 );
1207 }
1208 }
1209 }
1210 }
1211 if errors.is_empty() {
1212 return Ok(());
1213 }
1214 match effective_mode {
1215 ValidationMode::Disabled => Ok(()),
1216 ValidationMode::Warn => {
1217 tracing::warn!("Request validation warnings: {:?}", errors);
1218 Ok(())
1219 }
1220 ValidationMode::Enforce => Err(Error::validation(
1221 serde_json::json!({"errors": errors, "details": details}).to_string(),
1222 )),
1223 }
1224 } else {
1225 Err(Error::internal(format!("Route {} {} not found in OpenAPI spec", method, path)))
1226 }
1227 }
1228
1229 pub fn paths(&self) -> Vec<String> {
1233 let mut paths: Vec<String> = self.routes.iter().map(|route| route.path.clone()).collect();
1234 paths.sort();
1235 paths.dedup();
1236 paths
1237 }
1238
1239 pub fn methods(&self) -> Vec<String> {
1241 let mut methods: Vec<String> =
1242 self.routes.iter().map(|route| route.method.clone()).collect();
1243 methods.sort();
1244 methods.dedup();
1245 methods
1246 }
1247
1248 pub fn get_operation(&self, path: &str, method: &str) -> Option<OpenApiOperation> {
1250 self.get_route(path, method).map(|route| {
1251 OpenApiOperation::from_operation(
1252 &route.method,
1253 route.path.clone(),
1254 &route.operation,
1255 &self.spec,
1256 )
1257 })
1258 }
1259
1260 pub fn extract_path_parameters(&self, path: &str, method: &str) -> HashMap<String, String> {
1262 for route in &self.routes {
1263 if route.method != method {
1264 continue;
1265 }
1266
1267 if let Some(params) = self.match_path_to_route(path, &route.path) {
1268 return params;
1269 }
1270 }
1271 HashMap::new()
1272 }
1273
1274 fn match_path_to_route(
1276 &self,
1277 request_path: &str,
1278 route_pattern: &str,
1279 ) -> Option<HashMap<String, String>> {
1280 let mut params = HashMap::new();
1281
1282 let request_segments: Vec<&str> = request_path.trim_start_matches('/').split('/').collect();
1284 let pattern_segments: Vec<&str> =
1285 route_pattern.trim_start_matches('/').split('/').collect();
1286
1287 if request_segments.len() != pattern_segments.len() {
1288 return None;
1289 }
1290
1291 for (req_seg, pat_seg) in request_segments.iter().zip(pattern_segments.iter()) {
1292 if pat_seg.starts_with('{') && pat_seg.ends_with('}') {
1293 let param_name = &pat_seg[1..pat_seg.len() - 1];
1295 params.insert(param_name.to_string(), req_seg.to_string());
1296 } else if req_seg != pat_seg {
1297 return None;
1299 }
1300 }
1301
1302 Some(params)
1303 }
1304
1305 pub fn convert_path_to_axum(openapi_path: &str) -> String {
1308 openapi_path.to_string()
1310 }
1311
1312 pub fn build_router_with_ai(
1314 &self,
1315 ai_generator: Option<Arc<dyn AiGenerator + Send + Sync>>,
1316 ) -> Router {
1317 let mut router = Router::new();
1318 let deduped = self.deduplicated_routes();
1319 tracing::debug!("Building router with AI support from {} routes", self.routes.len());
1320
1321 for (axum_path, route) in &deduped {
1322 tracing::debug!("Adding AI-enabled route: {} {}", route.method, route.path);
1323
1324 let route_clone = (*route).clone();
1325 let ai_generator_clone = ai_generator.clone();
1326
1327 let handler = move |headers: HeaderMap, body: Option<Json<Value>>| {
1329 let route = route_clone.clone();
1330 let ai_generator = ai_generator_clone.clone();
1331
1332 async move {
1333 tracing::debug!(
1334 "Handling AI request for route: {} {}",
1335 route.method,
1336 route.path
1337 );
1338
1339 let mut context = RequestContext::new(route.method.clone(), route.path.clone());
1341
1342 context.headers = headers
1344 .iter()
1345 .map(|(k, v)| {
1346 (k.to_string(), Value::String(v.to_str().unwrap_or("").to_string()))
1347 })
1348 .collect();
1349
1350 context.body = body.map(|Json(b)| b);
1352
1353 let (status, response) = if let (Some(generator), Some(_ai_config)) =
1355 (ai_generator, &route.ai_config)
1356 {
1357 route
1358 .mock_response_with_status_async(&context, Some(generator.as_ref()))
1359 .await
1360 } else {
1361 route.mock_response_with_status()
1363 };
1364
1365 (
1366 axum::http::StatusCode::from_u16(status)
1367 .unwrap_or(axum::http::StatusCode::OK),
1368 Json(response),
1369 )
1370 }
1371 };
1372
1373 router = Self::route_for_method(router, axum_path, &route.method, handler);
1374 }
1375
1376 router.layer(DefaultBodyLimit::max(openapi_body_limit_bytes()))
1380 }
1381
1382 pub fn build_router_with_mockai(
1393 &self,
1394 mockai: Option<
1395 Arc<
1396 tokio::sync::RwLock<
1397 dyn mockforge_foundation::intelligent_behavior::MockAiBehavior + Send + Sync,
1398 >,
1399 >,
1400 >,
1401 ) -> Router {
1402 use mockforge_foundation::intelligent_behavior::Request as MockAIRequest;
1403
1404 let mut router = Router::new();
1405 let deduped = self.deduplicated_routes();
1406 tracing::debug!("Building router with MockAI support from {} routes", self.routes.len());
1407
1408 let custom_loader = self.custom_fixture_loader.clone();
1409 for (axum_path, route) in &deduped {
1410 tracing::debug!("Adding MockAI-enabled route: {} {}", route.method, route.path);
1411
1412 let route_clone = (*route).clone();
1413 let mockai_clone = mockai.clone();
1414 let custom_loader_clone = custom_loader.clone();
1415
1416 let handler = move |query: axum::extract::Query<HashMap<String, String>>,
1420 headers: HeaderMap,
1421 body: Option<Json<Value>>| {
1422 let route = route_clone.clone();
1423 let mockai = mockai_clone.clone();
1424
1425 async move {
1426 tracing::info!(
1427 "[FIXTURE DEBUG] Starting fixture check for {} {} (custom_loader available: {})",
1428 route.method,
1429 route.path,
1430 custom_loader_clone.is_some()
1431 );
1432
1433 if let Some(ref loader) = custom_loader_clone {
1435 use crate::request_fingerprint::RequestFingerprint;
1436 use axum::http::{Method, Uri};
1437
1438 let query_string = if query.0.is_empty() {
1440 String::new()
1441 } else {
1442 query
1443 .0
1444 .iter()
1445 .map(|(k, v)| format!("{}={}", k, v))
1446 .collect::<Vec<_>>()
1447 .join("&")
1448 };
1449
1450 let normalized_request_path =
1452 crate::custom_fixture::CustomFixtureLoader::normalize_path(&route.path);
1453
1454 tracing::info!(
1455 "[FIXTURE DEBUG] Path normalization: original='{}', normalized='{}'",
1456 route.path,
1457 normalized_request_path
1458 );
1459
1460 let uri_str = if query_string.is_empty() {
1462 normalized_request_path.clone()
1463 } else {
1464 format!("{}?{}", normalized_request_path, query_string)
1465 };
1466
1467 tracing::info!(
1468 "[FIXTURE DEBUG] URI construction: uri_str='{}', query_string='{}'",
1469 uri_str,
1470 query_string
1471 );
1472
1473 if let Ok(uri) = uri_str.parse::<Uri>() {
1474 let http_method =
1475 Method::from_bytes(route.method.as_bytes()).unwrap_or(Method::GET);
1476
1477 let body_bytes =
1479 body.as_ref().and_then(|Json(b)| serde_json::to_vec(b).ok());
1480 let body_slice = body_bytes.as_deref();
1481
1482 let fingerprint =
1483 RequestFingerprint::new(http_method, &uri, &headers, body_slice);
1484
1485 tracing::info!(
1486 "[FIXTURE DEBUG] RequestFingerprint created: method='{}', path='{}', query='{}', body_hash={:?}",
1487 fingerprint.method,
1488 fingerprint.path,
1489 fingerprint.query,
1490 fingerprint.body_hash
1491 );
1492
1493 let available_fixtures = loader.has_fixture(&fingerprint);
1495 tracing::info!(
1496 "[FIXTURE DEBUG] Fixture check result: has_fixture={}",
1497 available_fixtures
1498 );
1499
1500 if let Some(custom_fixture) = loader.load_fixture(&fingerprint) {
1501 tracing::info!(
1502 "[FIXTURE DEBUG] ✅ FIXTURE MATCHED! Using custom fixture for {} {} (status: {}, path: '{}')",
1503 route.method,
1504 route.path,
1505 custom_fixture.status,
1506 custom_fixture.path
1507 );
1508
1509 if custom_fixture.delay_ms > 0 {
1511 tokio::time::sleep(tokio::time::Duration::from_millis(
1512 custom_fixture.delay_ms,
1513 ))
1514 .await;
1515 }
1516
1517 let response_body = if custom_fixture.response.is_string() {
1519 custom_fixture.response.as_str().unwrap().to_string()
1520 } else {
1521 serde_json::to_string(&custom_fixture.response)
1522 .unwrap_or_else(|_| "{}".to_string())
1523 };
1524
1525 let json_value: Value = serde_json::from_str(&response_body)
1527 .unwrap_or_else(|_| serde_json::json!({}));
1528
1529 let status =
1531 axum::http::StatusCode::from_u16(custom_fixture.status)
1532 .unwrap_or(axum::http::StatusCode::OK);
1533
1534 return (status, Json(json_value));
1536 } else {
1537 tracing::warn!(
1538 "[FIXTURE DEBUG] ❌ No fixture match found for {} {} (fingerprint.path='{}', normalized='{}')",
1539 route.method,
1540 route.path,
1541 fingerprint.path,
1542 normalized_request_path
1543 );
1544 }
1545 } else {
1546 tracing::warn!("[FIXTURE DEBUG] Failed to parse URI: '{}'", uri_str);
1547 }
1548 } else {
1549 tracing::warn!(
1550 "[FIXTURE DEBUG] Custom fixture loader not available for {} {}",
1551 route.method,
1552 route.path
1553 );
1554 }
1555
1556 tracing::debug!(
1557 "Handling MockAI request for route: {} {}",
1558 route.method,
1559 route.path
1560 );
1561
1562 let mockai_query = query.0;
1564
1565 let method_upper = route.method.to_uppercase();
1570 let should_use_mockai =
1571 matches!(method_upper.as_str(), "POST" | "PUT" | "PATCH" | "DELETE");
1572
1573 if should_use_mockai {
1574 if let Some(mockai_arc) = mockai {
1575 let mockai_guard = mockai_arc.read().await;
1576
1577 let mut mockai_headers = HashMap::new();
1579 for (k, v) in headers.iter() {
1580 mockai_headers
1581 .insert(k.to_string(), v.to_str().unwrap_or("").to_string());
1582 }
1583
1584 let mockai_request = MockAIRequest {
1585 method: route.method.clone(),
1586 path: route.path.clone(),
1587 body: body.as_ref().map(|Json(b)| b.clone()),
1588 query_params: mockai_query,
1589 headers: mockai_headers,
1590 };
1591
1592 match mockai_guard.process_request(&mockai_request).await {
1594 Ok(mockai_response) => {
1595 let is_empty = mockai_response.body.is_object()
1597 && mockai_response
1598 .body
1599 .as_object()
1600 .map(|obj| obj.is_empty())
1601 .unwrap_or(false);
1602
1603 if is_empty {
1604 tracing::debug!(
1605 "MockAI returned empty object for {} {}, falling back to OpenAPI response generation",
1606 route.method,
1607 route.path
1608 );
1609 } else {
1611 let spec_status = route.find_first_available_status_code();
1615 tracing::debug!(
1616 "MockAI generated response for {} {}, using spec status: {} (MockAI suggested: {})",
1617 route.method,
1618 route.path,
1619 spec_status,
1620 mockai_response.status_code
1621 );
1622 return (
1623 axum::http::StatusCode::from_u16(spec_status)
1624 .unwrap_or(axum::http::StatusCode::OK),
1625 Json(mockai_response.body),
1626 );
1627 }
1628 }
1629 Err(e) => {
1630 tracing::warn!(
1631 "MockAI processing failed for {} {}: {}, falling back to standard response",
1632 route.method,
1633 route.path,
1634 e
1635 );
1636 }
1638 }
1639 }
1640 } else {
1641 tracing::debug!(
1642 "Skipping MockAI for {} request {} - using OpenAPI response generation",
1643 method_upper,
1644 route.path
1645 );
1646 }
1647
1648 let status_override = headers
1650 .get("X-Mockforge-Response-Status")
1651 .and_then(|v| v.to_str().ok())
1652 .and_then(|s| s.parse::<u16>().ok());
1653
1654 let scenario = headers
1656 .get("X-Mockforge-Scenario")
1657 .and_then(|v| v.to_str().ok())
1658 .map(|s| s.to_string())
1659 .or_else(|| std::env::var("MOCKFORGE_HTTP_SCENARIO").ok());
1660
1661 let (status, response) = route
1663 .mock_response_with_status_and_scenario_and_override(
1664 scenario.as_deref(),
1665 status_override,
1666 );
1667 (
1668 axum::http::StatusCode::from_u16(status)
1669 .unwrap_or(axum::http::StatusCode::OK),
1670 Json(response),
1671 )
1672 }
1673 };
1674
1675 router = Self::route_for_method(router, axum_path, &route.method, handler);
1676 }
1677
1678 router.layer(DefaultBodyLimit::max(openapi_body_limit_bytes()))
1681 }
1682}
1683
1684async fn extract_multipart_from_bytes(
1689 body: &axum::body::Bytes,
1690 headers: &HeaderMap,
1691) -> Result<(HashMap<String, Value>, HashMap<String, String>)> {
1692 let boundary = headers
1694 .get(axum::http::header::CONTENT_TYPE)
1695 .and_then(|v| v.to_str().ok())
1696 .and_then(|ct| {
1697 ct.split(';').find_map(|part| {
1698 let part = part.trim();
1699 if part.starts_with("boundary=") {
1700 Some(part.strip_prefix("boundary=").unwrap_or("").trim_matches('"'))
1701 } else {
1702 None
1703 }
1704 })
1705 })
1706 .ok_or_else(|| Error::internal("Missing boundary in Content-Type header"))?;
1707
1708 let mut fields = HashMap::new();
1709 let mut files = HashMap::new();
1710
1711 let boundary_prefix = format!("--{}", boundary).into_bytes();
1714 let boundary_line = format!("\r\n--{}\r\n", boundary).into_bytes();
1715 let end_boundary = format!("\r\n--{}--\r\n", boundary).into_bytes();
1716
1717 let mut pos = 0;
1719 let mut parts = Vec::new();
1720
1721 if body.starts_with(&boundary_prefix) {
1723 if let Some(first_crlf) = body.iter().position(|&b| b == b'\r') {
1724 pos = first_crlf + 2; }
1726 }
1727
1728 while let Some(boundary_pos) = body[pos..]
1730 .windows(boundary_line.len())
1731 .position(|window| window == boundary_line.as_slice())
1732 {
1733 let actual_pos = pos + boundary_pos;
1734 if actual_pos > pos {
1735 parts.push((pos, actual_pos));
1736 }
1737 pos = actual_pos + boundary_line.len();
1738 }
1739
1740 if let Some(end_pos) = body[pos..]
1742 .windows(end_boundary.len())
1743 .position(|window| window == end_boundary.as_slice())
1744 {
1745 let actual_end = pos + end_pos;
1746 if actual_end > pos {
1747 parts.push((pos, actual_end));
1748 }
1749 } else if pos < body.len() {
1750 parts.push((pos, body.len()));
1752 }
1753
1754 for (start, end) in parts {
1756 let part_data = &body[start..end];
1757
1758 let separator = b"\r\n\r\n";
1760 if let Some(sep_pos) =
1761 part_data.windows(separator.len()).position(|window| window == separator)
1762 {
1763 let header_bytes = &part_data[..sep_pos];
1764 let body_start = sep_pos + separator.len();
1765 let body_data = &part_data[body_start..];
1766
1767 let header_str = String::from_utf8_lossy(header_bytes);
1769 let mut field_name = None;
1770 let mut filename = None;
1771
1772 for header_line in header_str.lines() {
1773 if header_line.starts_with("Content-Disposition:") {
1774 if let Some(name_start) = header_line.find("name=\"") {
1776 let name_start = name_start + 6;
1777 if let Some(name_end) = header_line[name_start..].find('"') {
1778 field_name =
1779 Some(header_line[name_start..name_start + name_end].to_string());
1780 }
1781 }
1782
1783 if let Some(file_start) = header_line.find("filename=\"") {
1785 let file_start = file_start + 10;
1786 if let Some(file_end) = header_line[file_start..].find('"') {
1787 filename =
1788 Some(header_line[file_start..file_start + file_end].to_string());
1789 }
1790 }
1791 }
1792 }
1793
1794 if let Some(name) = field_name {
1795 if let Some(file) = filename {
1796 let temp_dir = std::env::temp_dir().join("mockforge-uploads");
1798 std::fs::create_dir_all(&temp_dir)
1799 .map_err(|e| Error::io_with_context("temp directory", e.to_string()))?;
1800
1801 let file_path = temp_dir.join(format!("{}_{}", uuid::Uuid::new_v4(), file));
1802 std::fs::write(&file_path, body_data)
1803 .map_err(|e| Error::io_with_context("file", e.to_string()))?;
1804
1805 let file_path_str = file_path.to_string_lossy().to_string();
1806 files.insert(name.clone(), file_path_str.clone());
1807 fields.insert(name, Value::String(file_path_str));
1808 } else {
1809 let body_str = body_data
1812 .strip_suffix(b"\r\n")
1813 .or_else(|| body_data.strip_suffix(b"\n"))
1814 .unwrap_or(body_data);
1815
1816 if let Ok(field_value) = String::from_utf8(body_str.to_vec()) {
1817 fields.insert(name, Value::String(field_value.trim().to_string()));
1818 } else {
1819 use base64::{engine::general_purpose, Engine as _};
1821 fields.insert(
1822 name,
1823 Value::String(general_purpose::STANDARD.encode(body_str)),
1824 );
1825 }
1826 }
1827 }
1828 }
1829 }
1830
1831 Ok((fields, files))
1832}
1833
1834static LAST_ERRORS: Lazy<Mutex<VecDeque<Value>>> =
1835 Lazy::new(|| Mutex::new(VecDeque::with_capacity(20)));
1836
1837pub fn record_validation_error(v: &Value) {
1839 if let Ok(mut q) = LAST_ERRORS.lock() {
1840 if q.len() >= 20 {
1841 q.pop_front();
1842 }
1843 q.push_back(v.clone());
1844 }
1845 }
1847
1848pub fn get_last_validation_error() -> Option<Value> {
1850 LAST_ERRORS.lock().ok()?.back().cloned()
1851}
1852
1853pub fn get_validation_errors() -> Vec<Value> {
1855 LAST_ERRORS.lock().map(|q| q.iter().cloned().collect()).unwrap_or_default()
1856}
1857
1858fn coerce_value_for_schema(value: &Value, schema: &openapiv3::Schema) -> Value {
1863 match value {
1865 Value::String(s) => {
1866 if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
1868 &schema.schema_kind
1869 {
1870 if s.contains(',') {
1871 let parts: Vec<&str> = s.split(',').map(|s| s.trim()).collect();
1873 let mut array_values = Vec::new();
1874
1875 for part in parts {
1876 if let Some(items_schema) = &array_type.items {
1878 if let Some(items_schema_obj) = items_schema.as_item() {
1879 let part_value = Value::String(part.to_string());
1880 let coerced_part =
1881 coerce_value_for_schema(&part_value, items_schema_obj);
1882 array_values.push(coerced_part);
1883 } else {
1884 array_values.push(Value::String(part.to_string()));
1886 }
1887 } else {
1888 array_values.push(Value::String(part.to_string()));
1890 }
1891 }
1892 return Value::Array(array_values);
1893 }
1894 }
1895
1896 match &schema.schema_kind {
1898 openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
1899 value.clone()
1901 }
1902 openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => {
1903 if let Ok(n) = s.parse::<f64>() {
1905 if let Some(num) = serde_json::Number::from_f64(n) {
1906 return Value::Number(num);
1907 }
1908 }
1909 value.clone()
1910 }
1911 openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => {
1912 if let Ok(n) = s.parse::<i64>() {
1914 if let Some(num) = serde_json::Number::from_f64(n as f64) {
1915 return Value::Number(num);
1916 }
1917 }
1918 value.clone()
1919 }
1920 openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => {
1921 match s.to_lowercase().as_str() {
1923 "true" | "1" | "yes" | "on" => Value::Bool(true),
1924 "false" | "0" | "no" | "off" => Value::Bool(false),
1925 _ => value.clone(),
1926 }
1927 }
1928 _ => {
1929 value.clone()
1931 }
1932 }
1933 }
1934 _ => value.clone(),
1935 }
1936}
1937
1938fn coerce_by_style(value: &Value, schema: &openapiv3::Schema, style: Option<&str>) -> Value {
1940 match value {
1942 Value::String(s) => {
1943 if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
1945 &schema.schema_kind
1946 {
1947 let delimiter = match style {
1948 Some("spaceDelimited") => " ",
1949 Some("pipeDelimited") => "|",
1950 Some("form") | None => ",", _ => ",", };
1953
1954 if s.contains(delimiter) {
1955 let parts: Vec<&str> = s.split(delimiter).map(|s| s.trim()).collect();
1957 let mut array_values = Vec::new();
1958
1959 for part in parts {
1960 if let Some(items_schema) = &array_type.items {
1962 if let Some(items_schema_obj) = items_schema.as_item() {
1963 let part_value = Value::String(part.to_string());
1964 let coerced_part =
1965 coerce_by_style(&part_value, items_schema_obj, style);
1966 array_values.push(coerced_part);
1967 } else {
1968 array_values.push(Value::String(part.to_string()));
1970 }
1971 } else {
1972 array_values.push(Value::String(part.to_string()));
1974 }
1975 }
1976 return Value::Array(array_values);
1977 }
1978 }
1979
1980 if let Ok(n) = s.parse::<f64>() {
1982 if let Some(num) = serde_json::Number::from_f64(n) {
1983 return Value::Number(num);
1984 }
1985 }
1986 match s.to_lowercase().as_str() {
1988 "true" | "1" | "yes" | "on" => return Value::Bool(true),
1989 "false" | "0" | "no" | "off" => return Value::Bool(false),
1990 _ => {}
1991 }
1992 value.clone()
1994 }
1995 _ => value.clone(),
1996 }
1997}
1998
1999fn build_deep_object(name: &str, params: &Map<String, Value>) -> Option<Value> {
2001 let prefix = format!("{}[", name);
2002 let mut obj = Map::new();
2003 for (k, v) in params.iter() {
2004 if let Some(rest) = k.strip_prefix(&prefix) {
2005 if let Some(key) = rest.strip_suffix(']') {
2006 obj.insert(key.to_string(), v.clone());
2007 }
2008 }
2009 }
2010 if obj.is_empty() {
2011 None
2012 } else {
2013 Some(Value::Object(obj))
2014 }
2015}
2016
2017#[allow(clippy::too_many_arguments)]
2023fn generate_enhanced_422_response(
2024 validator: &OpenApiRouteRegistry,
2025 path_template: &str,
2026 method: &str,
2027 body: Option<&Value>,
2028 path_params: &Map<String, Value>,
2029 query_params: &Map<String, Value>,
2030 header_params: &Map<String, Value>,
2031 cookie_params: &Map<String, Value>,
2032) -> Value {
2033 let mut field_errors = Vec::new();
2034
2035 if let Some(route) = validator.get_route(path_template, method) {
2037 if let Some(schema) = &route.operation.request_body {
2039 if let Some(value) = body {
2040 if let Some(content) =
2041 schema.as_item().and_then(|rb| rb.content.get("application/json"))
2042 {
2043 if let Some(_schema_ref) = &content.schema {
2044 if serde_json::from_value::<Value>(value.clone()).is_err() {
2046 field_errors.push(json!({
2047 "path": "body",
2048 "message": "invalid JSON"
2049 }));
2050 }
2051 }
2052 }
2053 } else {
2054 field_errors.push(json!({
2055 "path": "body",
2056 "expected": "object",
2057 "found": "missing",
2058 "message": "Request body is required but not provided"
2059 }));
2060 }
2061 }
2062
2063 for param_ref in &route.operation.parameters {
2065 if let Some(param) = param_ref.as_item() {
2066 match param {
2067 openapiv3::Parameter::Path { parameter_data, .. } => {
2068 validate_parameter_detailed(
2069 parameter_data,
2070 path_params,
2071 "path",
2072 "path parameter",
2073 &mut field_errors,
2074 );
2075 }
2076 openapiv3::Parameter::Query { parameter_data, .. } => {
2077 let deep_value = if Some("form") == Some("deepObject") {
2078 build_deep_object(¶meter_data.name, query_params)
2079 } else {
2080 None
2081 };
2082 validate_parameter_detailed_with_deep(
2083 parameter_data,
2084 query_params,
2085 "query",
2086 "query parameter",
2087 deep_value,
2088 &mut field_errors,
2089 );
2090 }
2091 openapiv3::Parameter::Header { parameter_data, .. } => {
2092 validate_parameter_detailed(
2093 parameter_data,
2094 header_params,
2095 "header",
2096 "header parameter",
2097 &mut field_errors,
2098 );
2099 }
2100 openapiv3::Parameter::Cookie { parameter_data, .. } => {
2101 validate_parameter_detailed(
2102 parameter_data,
2103 cookie_params,
2104 "cookie",
2105 "cookie parameter",
2106 &mut field_errors,
2107 );
2108 }
2109 }
2110 }
2111 }
2112 }
2113
2114 json!({
2116 "error": "Schema validation failed",
2117 "details": field_errors,
2118 "method": method,
2119 "path": path_template,
2120 "timestamp": Utc::now().to_rfc3339(),
2121 "validation_type": "openapi_schema"
2122 })
2123}
2124
2125fn validate_parameter(
2127 parameter_data: &openapiv3::ParameterData,
2128 params_map: &Map<String, Value>,
2129 prefix: &str,
2130 aggregate: bool,
2131 errors: &mut Vec<String>,
2132 details: &mut Vec<Value>,
2133) {
2134 match params_map.get(¶meter_data.name) {
2135 Some(v) => {
2136 if let ParameterSchemaOrContent::Schema(s) = ¶meter_data.format {
2137 if let Some(schema) = s.as_item() {
2138 let coerced = coerce_value_for_schema(v, schema);
2139 if let Err(validation_error) =
2141 OpenApiSchema::new(schema.clone()).validate(&coerced)
2142 {
2143 let error_msg = validation_error.to_string();
2144 errors.push(format!(
2145 "{} parameter '{}' validation failed: {}",
2146 prefix, parameter_data.name, error_msg
2147 ));
2148 if aggregate {
2149 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
2150 }
2151 }
2152 }
2153 }
2154 }
2155 None => {
2156 if parameter_data.required {
2157 errors.push(format!(
2158 "missing required {} parameter '{}'",
2159 prefix, parameter_data.name
2160 ));
2161 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
2162 }
2163 }
2164 }
2165}
2166
2167#[allow(clippy::too_many_arguments)]
2169fn validate_parameter_with_deep_object(
2170 parameter_data: &openapiv3::ParameterData,
2171 params_map: &Map<String, Value>,
2172 prefix: &str,
2173 deep_value: Option<Value>,
2174 style: Option<&str>,
2175 aggregate: bool,
2176 errors: &mut Vec<String>,
2177 details: &mut Vec<Value>,
2178) {
2179 match deep_value.as_ref().or_else(|| params_map.get(¶meter_data.name)) {
2180 Some(v) => {
2181 if let ParameterSchemaOrContent::Schema(s) = ¶meter_data.format {
2182 if let Some(schema) = s.as_item() {
2183 let coerced = coerce_by_style(v, schema, style); if let Err(validation_error) =
2186 OpenApiSchema::new(schema.clone()).validate(&coerced)
2187 {
2188 let error_msg = validation_error.to_string();
2189 errors.push(format!(
2190 "{} parameter '{}' validation failed: {}",
2191 prefix, parameter_data.name, error_msg
2192 ));
2193 if aggregate {
2194 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
2195 }
2196 }
2197 }
2198 }
2199 }
2200 None => {
2201 if parameter_data.required {
2202 errors.push(format!(
2203 "missing required {} parameter '{}'",
2204 prefix, parameter_data.name
2205 ));
2206 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
2207 }
2208 }
2209 }
2210}
2211
2212fn validate_parameter_detailed(
2214 parameter_data: &openapiv3::ParameterData,
2215 params_map: &Map<String, Value>,
2216 location: &str,
2217 value_type: &str,
2218 field_errors: &mut Vec<Value>,
2219) {
2220 match params_map.get(¶meter_data.name) {
2221 Some(value) => {
2222 if let ParameterSchemaOrContent::Schema(schema) = ¶meter_data.format {
2223 let details: Vec<Value> = Vec::new();
2225 let param_path = format!("{}.{}", location, parameter_data.name);
2226
2227 if let Some(schema_ref) = schema.as_item() {
2229 let coerced_value = coerce_value_for_schema(value, schema_ref);
2230 if let Err(validation_error) =
2232 OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
2233 {
2234 field_errors.push(json!({
2235 "path": param_path,
2236 "expected": "valid according to schema",
2237 "found": coerced_value,
2238 "message": validation_error.to_string()
2239 }));
2240 }
2241 }
2242
2243 for detail in details {
2244 field_errors.push(json!({
2245 "path": detail["path"],
2246 "expected": detail["expected_type"],
2247 "found": detail["value"],
2248 "message": detail["message"]
2249 }));
2250 }
2251 }
2252 }
2253 None => {
2254 if parameter_data.required {
2255 field_errors.push(json!({
2256 "path": format!("{}.{}", location, parameter_data.name),
2257 "expected": "value",
2258 "found": "missing",
2259 "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
2260 }));
2261 }
2262 }
2263 }
2264}
2265
2266fn validate_parameter_detailed_with_deep(
2268 parameter_data: &openapiv3::ParameterData,
2269 params_map: &Map<String, Value>,
2270 location: &str,
2271 value_type: &str,
2272 deep_value: Option<Value>,
2273 field_errors: &mut Vec<Value>,
2274) {
2275 match deep_value.as_ref().or_else(|| params_map.get(¶meter_data.name)) {
2276 Some(value) => {
2277 if let ParameterSchemaOrContent::Schema(schema) = ¶meter_data.format {
2278 let details: Vec<Value> = Vec::new();
2280 let param_path = format!("{}.{}", location, parameter_data.name);
2281
2282 if let Some(schema_ref) = schema.as_item() {
2284 let coerced_value = coerce_by_style(value, schema_ref, Some("form")); if let Err(validation_error) =
2287 OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
2288 {
2289 field_errors.push(json!({
2290 "path": param_path,
2291 "expected": "valid according to schema",
2292 "found": coerced_value,
2293 "message": validation_error.to_string()
2294 }));
2295 }
2296 }
2297
2298 for detail in details {
2299 field_errors.push(json!({
2300 "path": detail["path"],
2301 "expected": detail["expected_type"],
2302 "found": detail["value"],
2303 "message": detail["message"]
2304 }));
2305 }
2306 }
2307 }
2308 None => {
2309 if parameter_data.required {
2310 field_errors.push(json!({
2311 "path": format!("{}.{}", location, parameter_data.name),
2312 "expected": "value",
2313 "found": "missing",
2314 "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
2315 }));
2316 }
2317 }
2318 }
2319}
2320
2321pub async fn create_registry_from_file<P: AsRef<std::path::Path>>(
2323 path: P,
2324) -> Result<OpenApiRouteRegistry> {
2325 let spec = OpenApiSpec::from_file(path).await?;
2326 spec.validate()?;
2327 Ok(OpenApiRouteRegistry::new(spec))
2328}
2329
2330pub fn create_registry_from_json(json: Value) -> Result<OpenApiRouteRegistry> {
2332 let spec = OpenApiSpec::from_json(json)?;
2333 spec.validate()?;
2334 Ok(OpenApiRouteRegistry::new(spec))
2335}
2336
2337#[cfg(test)]
2338mod tests {
2339 use super::*;
2340 use serde_json::json;
2341 use tempfile::TempDir;
2342
2343 #[tokio::test]
2344 async fn test_registry_creation() {
2345 let spec_json = json!({
2346 "openapi": "3.0.0",
2347 "info": {
2348 "title": "Test API",
2349 "version": "1.0.0"
2350 },
2351 "paths": {
2352 "/users": {
2353 "get": {
2354 "summary": "Get users",
2355 "responses": {
2356 "200": {
2357 "description": "Success",
2358 "content": {
2359 "application/json": {
2360 "schema": {
2361 "type": "array",
2362 "items": {
2363 "type": "object",
2364 "properties": {
2365 "id": {"type": "integer"},
2366 "name": {"type": "string"}
2367 }
2368 }
2369 }
2370 }
2371 }
2372 }
2373 }
2374 },
2375 "post": {
2376 "summary": "Create user",
2377 "requestBody": {
2378 "content": {
2379 "application/json": {
2380 "schema": {
2381 "type": "object",
2382 "properties": {
2383 "name": {"type": "string"}
2384 },
2385 "required": ["name"]
2386 }
2387 }
2388 }
2389 },
2390 "responses": {
2391 "201": {
2392 "description": "Created",
2393 "content": {
2394 "application/json": {
2395 "schema": {
2396 "type": "object",
2397 "properties": {
2398 "id": {"type": "integer"},
2399 "name": {"type": "string"}
2400 }
2401 }
2402 }
2403 }
2404 }
2405 }
2406 }
2407 },
2408 "/users/{id}": {
2409 "get": {
2410 "summary": "Get user by ID",
2411 "parameters": [
2412 {
2413 "name": "id",
2414 "in": "path",
2415 "required": true,
2416 "schema": {"type": "integer"}
2417 }
2418 ],
2419 "responses": {
2420 "200": {
2421 "description": "Success",
2422 "content": {
2423 "application/json": {
2424 "schema": {
2425 "type": "object",
2426 "properties": {
2427 "id": {"type": "integer"},
2428 "name": {"type": "string"}
2429 }
2430 }
2431 }
2432 }
2433 }
2434 }
2435 }
2436 }
2437 }
2438 });
2439
2440 let registry = create_registry_from_json(spec_json).unwrap();
2441
2442 assert_eq!(registry.paths().len(), 2);
2444 assert!(registry.paths().contains(&"/users".to_string()));
2445 assert!(registry.paths().contains(&"/users/{id}".to_string()));
2446
2447 assert_eq!(registry.methods().len(), 2);
2448 assert!(registry.methods().contains(&"GET".to_string()));
2449 assert!(registry.methods().contains(&"POST".to_string()));
2450
2451 let get_users_route = registry.get_route("/users", "GET").unwrap();
2453 assert_eq!(get_users_route.method, "GET");
2454 assert_eq!(get_users_route.path, "/users");
2455
2456 let post_users_route = registry.get_route("/users", "POST").unwrap();
2457 assert_eq!(post_users_route.method, "POST");
2458 assert!(post_users_route.operation.request_body.is_some());
2459
2460 let user_by_id_route = registry.get_route("/users/{id}", "GET").unwrap();
2462 assert_eq!(user_by_id_route.axum_path(), "/users/{id}");
2463 }
2464
2465 #[tokio::test]
2466 async fn test_validate_request_with_params_and_formats() {
2467 let spec_json = json!({
2468 "openapi": "3.0.0",
2469 "info": { "title": "Test API", "version": "1.0.0" },
2470 "paths": {
2471 "/users/{id}": {
2472 "post": {
2473 "parameters": [
2474 { "name": "id", "in": "path", "required": true, "schema": {"type": "string"} },
2475 { "name": "q", "in": "query", "required": false, "schema": {"type": "integer"} }
2476 ],
2477 "requestBody": {
2478 "content": {
2479 "application/json": {
2480 "schema": {
2481 "type": "object",
2482 "required": ["email", "website"],
2483 "properties": {
2484 "email": {"type": "string", "format": "email"},
2485 "website": {"type": "string", "format": "uri"}
2486 }
2487 }
2488 }
2489 }
2490 },
2491 "responses": {"200": {"description": "ok"}}
2492 }
2493 }
2494 }
2495 });
2496
2497 let registry = create_registry_from_json(spec_json).unwrap();
2498 let mut path_params = Map::new();
2499 path_params.insert("id".to_string(), json!("abc"));
2500 let mut query_params = Map::new();
2501 query_params.insert("q".to_string(), json!(123));
2502
2503 let body = json!({"email":"a@b.co","website":"https://example.com"});
2505 assert!(registry
2506 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2507 .is_ok());
2508
2509 let bad_email = json!({"email":"not-an-email","website":"https://example.com"});
2511 assert!(registry
2512 .validate_request_with(
2513 "/users/{id}",
2514 "POST",
2515 &path_params,
2516 &query_params,
2517 Some(&bad_email)
2518 )
2519 .is_err());
2520
2521 let empty_path_params = Map::new();
2523 assert!(registry
2524 .validate_request_with(
2525 "/users/{id}",
2526 "POST",
2527 &empty_path_params,
2528 &query_params,
2529 Some(&body)
2530 )
2531 .is_err());
2532 }
2533
2534 #[tokio::test]
2535 async fn test_ref_resolution_for_params_and_body() {
2536 let spec_json = json!({
2537 "openapi": "3.0.0",
2538 "info": { "title": "Ref API", "version": "1.0.0" },
2539 "components": {
2540 "schemas": {
2541 "EmailWebsite": {
2542 "type": "object",
2543 "required": ["email", "website"],
2544 "properties": {
2545 "email": {"type": "string", "format": "email"},
2546 "website": {"type": "string", "format": "uri"}
2547 }
2548 }
2549 },
2550 "parameters": {
2551 "PathId": {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}},
2552 "QueryQ": {"name": "q", "in": "query", "required": false, "schema": {"type": "integer"}}
2553 },
2554 "requestBodies": {
2555 "CreateUser": {
2556 "content": {
2557 "application/json": {
2558 "schema": {"$ref": "#/components/schemas/EmailWebsite"}
2559 }
2560 }
2561 }
2562 }
2563 },
2564 "paths": {
2565 "/users/{id}": {
2566 "post": {
2567 "parameters": [
2568 {"$ref": "#/components/parameters/PathId"},
2569 {"$ref": "#/components/parameters/QueryQ"}
2570 ],
2571 "requestBody": {"$ref": "#/components/requestBodies/CreateUser"},
2572 "responses": {"200": {"description": "ok"}}
2573 }
2574 }
2575 }
2576 });
2577
2578 let registry = create_registry_from_json(spec_json).unwrap();
2579 let mut path_params = Map::new();
2580 path_params.insert("id".to_string(), json!("abc"));
2581 let mut query_params = Map::new();
2582 query_params.insert("q".to_string(), json!(7));
2583
2584 let body = json!({"email":"user@example.com","website":"https://example.com"});
2585 assert!(registry
2586 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2587 .is_ok());
2588
2589 let bad = json!({"email":"nope","website":"https://example.com"});
2590 assert!(registry
2591 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&bad))
2592 .is_err());
2593 }
2594
2595 #[tokio::test]
2596 async fn test_header_cookie_and_query_coercion() {
2597 let spec_json = json!({
2598 "openapi": "3.0.0",
2599 "info": { "title": "Params API", "version": "1.0.0" },
2600 "paths": {
2601 "/items": {
2602 "get": {
2603 "parameters": [
2604 {"name": "X-Flag", "in": "header", "required": true, "schema": {"type": "boolean"}},
2605 {"name": "session", "in": "cookie", "required": true, "schema": {"type": "string"}},
2606 {"name": "ids", "in": "query", "required": false, "schema": {"type": "array", "items": {"type": "integer"}}}
2607 ],
2608 "responses": {"200": {"description": "ok"}}
2609 }
2610 }
2611 }
2612 });
2613
2614 let registry = create_registry_from_json(spec_json).unwrap();
2615
2616 let path_params = Map::new();
2617 let mut query_params = Map::new();
2618 query_params.insert("ids".to_string(), json!("1,2,3"));
2620 let mut header_params = Map::new();
2621 header_params.insert("X-Flag".to_string(), json!("true"));
2622 let mut cookie_params = Map::new();
2623 cookie_params.insert("session".to_string(), json!("abc123"));
2624
2625 assert!(registry
2626 .validate_request_with_all(
2627 "/items",
2628 "GET",
2629 &path_params,
2630 &query_params,
2631 &header_params,
2632 &cookie_params,
2633 None
2634 )
2635 .is_ok());
2636
2637 let empty_cookie = Map::new();
2639 assert!(registry
2640 .validate_request_with_all(
2641 "/items",
2642 "GET",
2643 &path_params,
2644 &query_params,
2645 &header_params,
2646 &empty_cookie,
2647 None
2648 )
2649 .is_err());
2650
2651 let mut bad_header = Map::new();
2653 bad_header.insert("X-Flag".to_string(), json!("notabool"));
2654 assert!(registry
2655 .validate_request_with_all(
2656 "/items",
2657 "GET",
2658 &path_params,
2659 &query_params,
2660 &bad_header,
2661 &cookie_params,
2662 None
2663 )
2664 .is_err());
2665 }
2666
2667 #[tokio::test]
2668 async fn test_query_styles_space_pipe_deepobject() {
2669 let spec_json = json!({
2670 "openapi": "3.0.0",
2671 "info": { "title": "Query Styles API", "version": "1.0.0" },
2672 "paths": {"/search": {"get": {
2673 "parameters": [
2674 {"name":"tags","in":"query","style":"spaceDelimited","schema":{"type":"array","items":{"type":"string"}}},
2675 {"name":"ids","in":"query","style":"pipeDelimited","schema":{"type":"array","items":{"type":"integer"}}},
2676 {"name":"filter","in":"query","style":"deepObject","schema":{"type":"object","properties":{"color":{"type":"string"}},"required":["color"]}}
2677 ],
2678 "responses": {"200": {"description":"ok"}}
2679 }} }
2680 });
2681
2682 let registry = create_registry_from_json(spec_json).unwrap();
2683
2684 let path_params = Map::new();
2685 let mut query = Map::new();
2686 query.insert("tags".into(), json!("alpha beta gamma"));
2687 query.insert("ids".into(), json!("1|2|3"));
2688 query.insert("filter[color]".into(), json!("red"));
2689
2690 assert!(registry
2691 .validate_request_with("/search", "GET", &path_params, &query, None)
2692 .is_ok());
2693 }
2694
2695 #[tokio::test]
2696 async fn test_oneof_anyof_allof_validation() {
2697 let spec_json = json!({
2698 "openapi": "3.0.0",
2699 "info": { "title": "Composite API", "version": "1.0.0" },
2700 "paths": {
2701 "/composite": {
2702 "post": {
2703 "requestBody": {
2704 "content": {
2705 "application/json": {
2706 "schema": {
2707 "allOf": [
2708 {"type": "object", "required": ["base"], "properties": {"base": {"type": "string"}}}
2709 ],
2710 "oneOf": [
2711 {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"], "not": {"required": ["b"]}},
2712 {"type": "object", "properties": {"b": {"type": "integer"}}, "required": ["b"], "not": {"required": ["a"]}}
2713 ],
2714 "anyOf": [
2715 {"type": "object", "properties": {"flag": {"type": "boolean"}}, "required": ["flag"]},
2716 {"type": "object", "properties": {"extra": {"type": "string"}}, "required": ["extra"]}
2717 ]
2718 }
2719 }
2720 }
2721 },
2722 "responses": {"200": {"description": "ok"}}
2723 }
2724 }
2725 }
2726 });
2727
2728 let registry = create_registry_from_json(spec_json).unwrap();
2729 let ok = json!({"base": "x", "a": 1, "flag": true});
2731 assert!(registry
2732 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&ok))
2733 .is_ok());
2734
2735 let bad_oneof = json!({"base": "x", "a": 1, "b": 2, "flag": false});
2737 assert!(registry
2738 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_oneof))
2739 .is_err());
2740
2741 let bad_anyof = json!({"base": "x", "a": 1});
2743 assert!(registry
2744 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_anyof))
2745 .is_err());
2746
2747 let bad_allof = json!({"a": 1, "flag": true});
2749 assert!(registry
2750 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_allof))
2751 .is_err());
2752 }
2753
2754 #[tokio::test]
2755 async fn test_overrides_warn_mode_allows_invalid() {
2756 let spec_json = json!({
2758 "openapi": "3.0.0",
2759 "info": { "title": "Overrides API", "version": "1.0.0" },
2760 "paths": {"/things": {"post": {
2761 "parameters": [{"name":"q","in":"query","required":true,"schema":{"type":"integer"}}],
2762 "responses": {"200": {"description":"ok"}}
2763 }}}
2764 });
2765
2766 let spec = OpenApiSpec::from_json(spec_json).unwrap();
2767 let mut overrides = HashMap::new();
2768 overrides.insert("POST /things".to_string(), ValidationMode::Warn);
2769 let registry = OpenApiRouteRegistry::new_with_options(
2770 spec,
2771 ValidationOptions {
2772 request_mode: ValidationMode::Enforce,
2773 aggregate_errors: true,
2774 validate_responses: false,
2775 overrides,
2776 admin_skip_prefixes: vec![],
2777 response_template_expand: false,
2778 validation_status: None,
2779 },
2780 );
2781
2782 let ok = registry.validate_request_with("/things", "POST", &Map::new(), &Map::new(), None);
2784 assert!(ok.is_ok());
2785 }
2786
2787 #[tokio::test]
2788 async fn test_admin_skip_prefix_short_circuit() {
2789 let spec_json = json!({
2790 "openapi": "3.0.0",
2791 "info": { "title": "Skip API", "version": "1.0.0" },
2792 "paths": {}
2793 });
2794 let spec = OpenApiSpec::from_json(spec_json).unwrap();
2795 let registry = OpenApiRouteRegistry::new_with_options(
2796 spec,
2797 ValidationOptions {
2798 request_mode: ValidationMode::Enforce,
2799 aggregate_errors: true,
2800 validate_responses: false,
2801 overrides: HashMap::new(),
2802 admin_skip_prefixes: vec!["/admin".into()],
2803 response_template_expand: false,
2804 validation_status: None,
2805 },
2806 );
2807
2808 let res = registry.validate_request_with_all(
2810 "/admin/__mockforge/health",
2811 "GET",
2812 &Map::new(),
2813 &Map::new(),
2814 &Map::new(),
2815 &Map::new(),
2816 None,
2817 );
2818 assert!(res.is_ok());
2819 }
2820
2821 #[test]
2822 fn test_path_conversion() {
2823 assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users"), "/users");
2824 assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users/{id}"), "/users/{id}");
2825 assert_eq!(
2826 OpenApiRouteRegistry::convert_path_to_axum("/users/{id}/posts/{postId}"),
2827 "/users/{id}/posts/{postId}"
2828 );
2829 }
2830
2831 #[test]
2832 fn test_validation_options_default() {
2833 let options = ValidationOptions::default();
2834 assert!(matches!(options.request_mode, ValidationMode::Enforce));
2835 assert!(options.aggregate_errors);
2836 assert!(!options.validate_responses);
2837 assert!(options.overrides.is_empty());
2838 assert!(options.admin_skip_prefixes.is_empty());
2839 assert!(!options.response_template_expand);
2840 assert!(options.validation_status.is_none());
2841 }
2842
2843 #[test]
2844 fn test_validation_mode_variants() {
2845 let disabled = ValidationMode::Disabled;
2847 let warn = ValidationMode::Warn;
2848 let enforce = ValidationMode::Enforce;
2849 let default = ValidationMode::default();
2850
2851 assert!(matches!(default, ValidationMode::Warn));
2853
2854 assert!(!matches!(disabled, ValidationMode::Warn));
2856 assert!(!matches!(warn, ValidationMode::Enforce));
2857 assert!(!matches!(enforce, ValidationMode::Disabled));
2858 }
2859
2860 #[test]
2861 fn test_registry_spec_accessor() {
2862 let spec_json = json!({
2863 "openapi": "3.0.0",
2864 "info": {
2865 "title": "Test API",
2866 "version": "1.0.0"
2867 },
2868 "paths": {}
2869 });
2870 let spec = OpenApiSpec::from_json(spec_json).unwrap();
2871 let registry = OpenApiRouteRegistry::new(spec.clone());
2872
2873 let accessed_spec = registry.spec();
2875 assert_eq!(accessed_spec.title(), "Test API");
2876 }
2877
2878 #[test]
2879 fn test_clone_for_validation() {
2880 let spec_json = json!({
2881 "openapi": "3.0.0",
2882 "info": {
2883 "title": "Test API",
2884 "version": "1.0.0"
2885 },
2886 "paths": {
2887 "/users": {
2888 "get": {
2889 "responses": {
2890 "200": {
2891 "description": "Success"
2892 }
2893 }
2894 }
2895 }
2896 }
2897 });
2898 let spec = OpenApiSpec::from_json(spec_json).unwrap();
2899 let registry = OpenApiRouteRegistry::new(spec);
2900
2901 let cloned = registry.clone_for_validation();
2903 assert_eq!(cloned.routes().len(), registry.routes().len());
2904 assert_eq!(cloned.spec().title(), registry.spec().title());
2905 }
2906
2907 #[test]
2908 fn test_with_custom_fixture_loader() {
2909 let temp_dir = TempDir::new().unwrap();
2910 let spec_json = json!({
2911 "openapi": "3.0.0",
2912 "info": {
2913 "title": "Test API",
2914 "version": "1.0.0"
2915 },
2916 "paths": {}
2917 });
2918 let spec = OpenApiSpec::from_json(spec_json).unwrap();
2919 let registry = OpenApiRouteRegistry::new(spec);
2920 let original_routes_len = registry.routes().len();
2921
2922 let custom_loader = Arc::new(crate::custom_fixture::CustomFixtureLoader::new(
2924 temp_dir.path().to_path_buf(),
2925 true,
2926 ));
2927 let registry_with_loader = registry.with_custom_fixture_loader(custom_loader);
2928
2929 assert_eq!(registry_with_loader.routes().len(), original_routes_len);
2931 }
2932
2933 #[test]
2934 fn test_get_route() {
2935 let spec_json = json!({
2936 "openapi": "3.0.0",
2937 "info": {
2938 "title": "Test API",
2939 "version": "1.0.0"
2940 },
2941 "paths": {
2942 "/users": {
2943 "get": {
2944 "operationId": "getUsers",
2945 "responses": {
2946 "200": {
2947 "description": "Success"
2948 }
2949 }
2950 },
2951 "post": {
2952 "operationId": "createUser",
2953 "responses": {
2954 "201": {
2955 "description": "Created"
2956 }
2957 }
2958 }
2959 }
2960 }
2961 });
2962 let spec = OpenApiSpec::from_json(spec_json).unwrap();
2963 let registry = OpenApiRouteRegistry::new(spec);
2964
2965 let route = registry.get_route("/users", "GET");
2967 assert!(route.is_some());
2968 assert_eq!(route.unwrap().method, "GET");
2969 assert_eq!(route.unwrap().path, "/users");
2970
2971 let route = registry.get_route("/nonexistent", "GET");
2973 assert!(route.is_none());
2974
2975 let route = registry.get_route("/users", "POST");
2977 assert!(route.is_some());
2978 assert_eq!(route.unwrap().method, "POST");
2979 }
2980
2981 #[test]
2982 fn test_get_routes_for_path() {
2983 let spec_json = json!({
2984 "openapi": "3.0.0",
2985 "info": {
2986 "title": "Test API",
2987 "version": "1.0.0"
2988 },
2989 "paths": {
2990 "/users": {
2991 "get": {
2992 "responses": {
2993 "200": {
2994 "description": "Success"
2995 }
2996 }
2997 },
2998 "post": {
2999 "responses": {
3000 "201": {
3001 "description": "Created"
3002 }
3003 }
3004 },
3005 "put": {
3006 "responses": {
3007 "200": {
3008 "description": "Success"
3009 }
3010 }
3011 }
3012 },
3013 "/posts": {
3014 "get": {
3015 "responses": {
3016 "200": {
3017 "description": "Success"
3018 }
3019 }
3020 }
3021 }
3022 }
3023 });
3024 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3025 let registry = OpenApiRouteRegistry::new(spec);
3026
3027 let routes = registry.get_routes_for_path("/users");
3029 assert_eq!(routes.len(), 3);
3030 let methods: Vec<&str> = routes.iter().map(|r| r.method.as_str()).collect();
3031 assert!(methods.contains(&"GET"));
3032 assert!(methods.contains(&"POST"));
3033 assert!(methods.contains(&"PUT"));
3034
3035 let routes = registry.get_routes_for_path("/posts");
3037 assert_eq!(routes.len(), 1);
3038 assert_eq!(routes[0].method, "GET");
3039
3040 let routes = registry.get_routes_for_path("/nonexistent");
3042 assert!(routes.is_empty());
3043 }
3044
3045 #[test]
3046 fn test_new_vs_new_with_options() {
3047 let spec_json = json!({
3048 "openapi": "3.0.0",
3049 "info": {
3050 "title": "Test API",
3051 "version": "1.0.0"
3052 },
3053 "paths": {}
3054 });
3055 let spec1 = OpenApiSpec::from_json(spec_json.clone()).unwrap();
3056 let spec2 = OpenApiSpec::from_json(spec_json).unwrap();
3057
3058 let registry1 = OpenApiRouteRegistry::new(spec1);
3060 assert_eq!(registry1.spec().title(), "Test API");
3061
3062 let options = ValidationOptions {
3064 request_mode: ValidationMode::Disabled,
3065 aggregate_errors: false,
3066 validate_responses: true,
3067 overrides: HashMap::new(),
3068 admin_skip_prefixes: vec!["/admin".to_string()],
3069 response_template_expand: true,
3070 validation_status: Some(422),
3071 };
3072 let registry2 = OpenApiRouteRegistry::new_with_options(spec2, options);
3073 assert_eq!(registry2.spec().title(), "Test API");
3074 }
3075
3076 #[test]
3077 fn test_new_with_env_vs_new() {
3078 let spec_json = json!({
3079 "openapi": "3.0.0",
3080 "info": {
3081 "title": "Test API",
3082 "version": "1.0.0"
3083 },
3084 "paths": {}
3085 });
3086 let spec1 = OpenApiSpec::from_json(spec_json.clone()).unwrap();
3087 let spec2 = OpenApiSpec::from_json(spec_json).unwrap();
3088
3089 let registry1 = OpenApiRouteRegistry::new(spec1);
3091
3092 let registry2 = OpenApiRouteRegistry::new_with_env(spec2);
3094
3095 assert_eq!(registry1.spec().title(), "Test API");
3097 assert_eq!(registry2.spec().title(), "Test API");
3098 }
3099
3100 #[test]
3101 fn test_validation_options_custom() {
3102 let options = ValidationOptions {
3103 request_mode: ValidationMode::Warn,
3104 aggregate_errors: false,
3105 validate_responses: true,
3106 overrides: {
3107 let mut map = HashMap::new();
3108 map.insert("getUsers".to_string(), ValidationMode::Disabled);
3109 map
3110 },
3111 admin_skip_prefixes: vec!["/admin".to_string(), "/internal".to_string()],
3112 response_template_expand: true,
3113 validation_status: Some(422),
3114 };
3115
3116 assert!(matches!(options.request_mode, ValidationMode::Warn));
3117 assert!(!options.aggregate_errors);
3118 assert!(options.validate_responses);
3119 assert_eq!(options.overrides.len(), 1);
3120 assert_eq!(options.admin_skip_prefixes.len(), 2);
3121 assert!(options.response_template_expand);
3122 assert_eq!(options.validation_status, Some(422));
3123 }
3124
3125 #[test]
3126 fn test_validation_mode_default_standalone() {
3127 let mode = ValidationMode::default();
3128 assert!(matches!(mode, ValidationMode::Warn));
3129 }
3130
3131 #[test]
3132 fn test_validation_mode_clone() {
3133 let mode1 = ValidationMode::Enforce;
3134 let mode2 = mode1.clone();
3135 assert!(matches!(mode1, ValidationMode::Enforce));
3136 assert!(matches!(mode2, ValidationMode::Enforce));
3137 }
3138
3139 #[test]
3140 fn test_validation_mode_debug() {
3141 let mode = ValidationMode::Disabled;
3142 let debug_str = format!("{:?}", mode);
3143 assert!(debug_str.contains("Disabled") || debug_str.contains("ValidationMode"));
3144 }
3145
3146 #[test]
3147 fn test_validation_options_clone() {
3148 let options1 = ValidationOptions {
3149 request_mode: ValidationMode::Warn,
3150 aggregate_errors: true,
3151 validate_responses: false,
3152 overrides: HashMap::new(),
3153 admin_skip_prefixes: vec![],
3154 response_template_expand: false,
3155 validation_status: None,
3156 };
3157 let options2 = options1.clone();
3158 assert!(matches!(options2.request_mode, ValidationMode::Warn));
3159 assert_eq!(options1.aggregate_errors, options2.aggregate_errors);
3160 }
3161
3162 #[test]
3163 fn test_validation_options_debug() {
3164 let options = ValidationOptions::default();
3165 let debug_str = format!("{:?}", options);
3166 assert!(debug_str.contains("ValidationOptions"));
3167 }
3168
3169 #[test]
3170 fn test_validation_options_with_all_fields() {
3171 let mut overrides = HashMap::new();
3172 overrides.insert("op1".to_string(), ValidationMode::Disabled);
3173 overrides.insert("op2".to_string(), ValidationMode::Warn);
3174
3175 let options = ValidationOptions {
3176 request_mode: ValidationMode::Enforce,
3177 aggregate_errors: false,
3178 validate_responses: true,
3179 overrides: overrides.clone(),
3180 admin_skip_prefixes: vec!["/admin".to_string(), "/internal".to_string()],
3181 response_template_expand: true,
3182 validation_status: Some(422),
3183 };
3184
3185 assert!(matches!(options.request_mode, ValidationMode::Enforce));
3186 assert!(!options.aggregate_errors);
3187 assert!(options.validate_responses);
3188 assert_eq!(options.overrides.len(), 2);
3189 assert_eq!(options.admin_skip_prefixes.len(), 2);
3190 assert!(options.response_template_expand);
3191 assert_eq!(options.validation_status, Some(422));
3192 }
3193
3194 #[test]
3195 fn test_openapi_route_registry_clone() {
3196 let spec_json = json!({
3197 "openapi": "3.0.0",
3198 "info": { "title": "Test API", "version": "1.0.0" },
3199 "paths": {}
3200 });
3201 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3202 let registry1 = OpenApiRouteRegistry::new(spec);
3203 let registry2 = registry1.clone();
3204 assert_eq!(registry1.spec().title(), registry2.spec().title());
3205 }
3206
3207 #[test]
3208 fn test_validation_mode_serialization() {
3209 let mode = ValidationMode::Enforce;
3210 let json = serde_json::to_string(&mode).unwrap();
3211 assert!(json.contains("Enforce") || json.contains("enforce"));
3212 }
3213
3214 #[test]
3215 fn test_validation_mode_deserialization() {
3216 let json = r#""Disabled""#;
3217 let mode: ValidationMode = serde_json::from_str(json).unwrap();
3218 assert!(matches!(mode, ValidationMode::Disabled));
3219 }
3220
3221 #[test]
3222 fn test_validation_options_default_values() {
3223 let options = ValidationOptions::default();
3224 assert!(matches!(options.request_mode, ValidationMode::Enforce));
3225 assert!(options.aggregate_errors);
3226 assert!(!options.validate_responses);
3227 assert!(options.overrides.is_empty());
3228 assert!(options.admin_skip_prefixes.is_empty());
3229 assert!(!options.response_template_expand);
3230 assert_eq!(options.validation_status, None);
3231 }
3232
3233 #[test]
3234 fn test_validation_mode_all_variants() {
3235 let disabled = ValidationMode::Disabled;
3236 let warn = ValidationMode::Warn;
3237 let enforce = ValidationMode::Enforce;
3238
3239 assert!(matches!(disabled, ValidationMode::Disabled));
3240 assert!(matches!(warn, ValidationMode::Warn));
3241 assert!(matches!(enforce, ValidationMode::Enforce));
3242 }
3243
3244 #[test]
3245 fn test_validation_options_with_overrides() {
3246 let mut overrides = HashMap::new();
3247 overrides.insert("operation1".to_string(), ValidationMode::Disabled);
3248 overrides.insert("operation2".to_string(), ValidationMode::Warn);
3249
3250 let options = ValidationOptions {
3251 request_mode: ValidationMode::Enforce,
3252 aggregate_errors: true,
3253 validate_responses: false,
3254 overrides,
3255 admin_skip_prefixes: vec![],
3256 response_template_expand: false,
3257 validation_status: None,
3258 };
3259
3260 assert_eq!(options.overrides.len(), 2);
3261 assert!(matches!(options.overrides.get("operation1"), Some(ValidationMode::Disabled)));
3262 assert!(matches!(options.overrides.get("operation2"), Some(ValidationMode::Warn)));
3263 }
3264
3265 #[test]
3266 fn test_validation_options_with_admin_skip_prefixes() {
3267 let options = ValidationOptions {
3268 request_mode: ValidationMode::Enforce,
3269 aggregate_errors: true,
3270 validate_responses: false,
3271 overrides: HashMap::new(),
3272 admin_skip_prefixes: vec![
3273 "/admin".to_string(),
3274 "/internal".to_string(),
3275 "/debug".to_string(),
3276 ],
3277 response_template_expand: false,
3278 validation_status: None,
3279 };
3280
3281 assert_eq!(options.admin_skip_prefixes.len(), 3);
3282 assert!(options.admin_skip_prefixes.contains(&"/admin".to_string()));
3283 assert!(options.admin_skip_prefixes.contains(&"/internal".to_string()));
3284 assert!(options.admin_skip_prefixes.contains(&"/debug".to_string()));
3285 }
3286
3287 #[test]
3288 fn test_validation_options_with_validation_status() {
3289 let options1 = ValidationOptions {
3290 request_mode: ValidationMode::Enforce,
3291 aggregate_errors: true,
3292 validate_responses: false,
3293 overrides: HashMap::new(),
3294 admin_skip_prefixes: vec![],
3295 response_template_expand: false,
3296 validation_status: Some(400),
3297 };
3298
3299 let options2 = ValidationOptions {
3300 request_mode: ValidationMode::Enforce,
3301 aggregate_errors: true,
3302 validate_responses: false,
3303 overrides: HashMap::new(),
3304 admin_skip_prefixes: vec![],
3305 response_template_expand: false,
3306 validation_status: Some(422),
3307 };
3308
3309 assert_eq!(options1.validation_status, Some(400));
3310 assert_eq!(options2.validation_status, Some(422));
3311 }
3312
3313 #[test]
3314 fn test_validate_request_with_disabled_mode() {
3315 let spec_json = json!({
3317 "openapi": "3.0.0",
3318 "info": {"title": "Test API", "version": "1.0.0"},
3319 "paths": {
3320 "/users": {
3321 "get": {
3322 "responses": {"200": {"description": "OK"}}
3323 }
3324 }
3325 }
3326 });
3327 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3328 let options = ValidationOptions {
3329 request_mode: ValidationMode::Disabled,
3330 ..Default::default()
3331 };
3332 let registry = OpenApiRouteRegistry::new_with_options(spec, options);
3333
3334 let result = registry.validate_request_with_all(
3336 "/users",
3337 "GET",
3338 &Map::new(),
3339 &Map::new(),
3340 &Map::new(),
3341 &Map::new(),
3342 None,
3343 );
3344 assert!(result.is_ok());
3345 }
3346
3347 #[test]
3348 fn test_validate_request_with_warn_mode() {
3349 let spec_json = json!({
3351 "openapi": "3.0.0",
3352 "info": {"title": "Test API", "version": "1.0.0"},
3353 "paths": {
3354 "/users": {
3355 "post": {
3356 "requestBody": {
3357 "required": true,
3358 "content": {
3359 "application/json": {
3360 "schema": {
3361 "type": "object",
3362 "required": ["name"],
3363 "properties": {
3364 "name": {"type": "string"}
3365 }
3366 }
3367 }
3368 }
3369 },
3370 "responses": {"200": {"description": "OK"}}
3371 }
3372 }
3373 }
3374 });
3375 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3376 let options = ValidationOptions {
3377 request_mode: ValidationMode::Warn,
3378 ..Default::default()
3379 };
3380 let registry = OpenApiRouteRegistry::new_with_options(spec, options);
3381
3382 let result = registry.validate_request_with_all(
3384 "/users",
3385 "POST",
3386 &Map::new(),
3387 &Map::new(),
3388 &Map::new(),
3389 &Map::new(),
3390 None, );
3392 assert!(result.is_ok()); }
3394
3395 #[test]
3396 fn test_validate_request_body_validation_error() {
3397 let spec_json = json!({
3399 "openapi": "3.0.0",
3400 "info": {"title": "Test API", "version": "1.0.0"},
3401 "paths": {
3402 "/users": {
3403 "post": {
3404 "requestBody": {
3405 "required": true,
3406 "content": {
3407 "application/json": {
3408 "schema": {
3409 "type": "object",
3410 "required": ["name"],
3411 "properties": {
3412 "name": {"type": "string"}
3413 }
3414 }
3415 }
3416 }
3417 },
3418 "responses": {"200": {"description": "OK"}}
3419 }
3420 }
3421 }
3422 });
3423 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3424 let registry = OpenApiRouteRegistry::new(spec);
3425
3426 let result = registry.validate_request_with_all(
3428 "/users",
3429 "POST",
3430 &Map::new(),
3431 &Map::new(),
3432 &Map::new(),
3433 &Map::new(),
3434 None, );
3436 assert!(result.is_err());
3437 }
3438
3439 #[test]
3440 fn test_validate_request_body_schema_validation_error() {
3441 let spec_json = json!({
3443 "openapi": "3.0.0",
3444 "info": {"title": "Test API", "version": "1.0.0"},
3445 "paths": {
3446 "/users": {
3447 "post": {
3448 "requestBody": {
3449 "required": true,
3450 "content": {
3451 "application/json": {
3452 "schema": {
3453 "type": "object",
3454 "required": ["name"],
3455 "properties": {
3456 "name": {"type": "string"}
3457 }
3458 }
3459 }
3460 }
3461 },
3462 "responses": {"200": {"description": "OK"}}
3463 }
3464 }
3465 }
3466 });
3467 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3468 let registry = OpenApiRouteRegistry::new(spec);
3469
3470 let invalid_body = json!({}); let result = registry.validate_request_with_all(
3473 "/users",
3474 "POST",
3475 &Map::new(),
3476 &Map::new(),
3477 &Map::new(),
3478 &Map::new(),
3479 Some(&invalid_body),
3480 );
3481 assert!(result.is_err());
3482 }
3483
3484 #[test]
3485 fn test_validate_request_body_referenced_schema_error() {
3486 let spec_json = json!({
3488 "openapi": "3.0.0",
3489 "info": {"title": "Test API", "version": "1.0.0"},
3490 "paths": {
3491 "/users": {
3492 "post": {
3493 "requestBody": {
3494 "required": true,
3495 "content": {
3496 "application/json": {
3497 "schema": {
3498 "$ref": "#/components/schemas/NonExistentSchema"
3499 }
3500 }
3501 }
3502 },
3503 "responses": {"200": {"description": "OK"}}
3504 }
3505 }
3506 },
3507 "components": {}
3508 });
3509 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3510 let registry = OpenApiRouteRegistry::new(spec);
3511
3512 let body = json!({"name": "test"});
3514 let result = registry.validate_request_with_all(
3515 "/users",
3516 "POST",
3517 &Map::new(),
3518 &Map::new(),
3519 &Map::new(),
3520 &Map::new(),
3521 Some(&body),
3522 );
3523 assert!(result.is_err());
3524 }
3525
3526 #[test]
3527 fn test_validate_request_body_referenced_request_body_error() {
3528 let spec_json = json!({
3530 "openapi": "3.0.0",
3531 "info": {"title": "Test API", "version": "1.0.0"},
3532 "paths": {
3533 "/users": {
3534 "post": {
3535 "requestBody": {
3536 "$ref": "#/components/requestBodies/NonExistentRequestBody"
3537 },
3538 "responses": {"200": {"description": "OK"}}
3539 }
3540 }
3541 },
3542 "components": {}
3543 });
3544 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3545 let registry = OpenApiRouteRegistry::new(spec);
3546
3547 let body = json!({"name": "test"});
3549 let result = registry.validate_request_with_all(
3550 "/users",
3551 "POST",
3552 &Map::new(),
3553 &Map::new(),
3554 &Map::new(),
3555 &Map::new(),
3556 Some(&body),
3557 );
3558 assert!(result.is_err());
3559 }
3560
3561 #[test]
3562 fn test_validate_request_body_provided_when_not_expected() {
3563 let spec_json = json!({
3565 "openapi": "3.0.0",
3566 "info": {"title": "Test API", "version": "1.0.0"},
3567 "paths": {
3568 "/users": {
3569 "get": {
3570 "responses": {"200": {"description": "OK"}}
3571 }
3572 }
3573 }
3574 });
3575 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3576 let registry = OpenApiRouteRegistry::new(spec);
3577
3578 let body = json!({"extra": "data"});
3580 let result = registry.validate_request_with_all(
3581 "/users",
3582 "GET",
3583 &Map::new(),
3584 &Map::new(),
3585 &Map::new(),
3586 &Map::new(),
3587 Some(&body),
3588 );
3589 assert!(result.is_ok());
3591 }
3592
3593 #[test]
3594 fn test_get_operation() {
3595 let spec_json = json!({
3597 "openapi": "3.0.0",
3598 "info": {"title": "Test API", "version": "1.0.0"},
3599 "paths": {
3600 "/users": {
3601 "get": {
3602 "operationId": "getUsers",
3603 "summary": "Get users",
3604 "responses": {"200": {"description": "OK"}}
3605 }
3606 }
3607 }
3608 });
3609 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3610 let registry = OpenApiRouteRegistry::new(spec);
3611
3612 let operation = registry.get_operation("/users", "GET");
3614 assert!(operation.is_some());
3615 assert_eq!(operation.unwrap().method, "GET");
3616
3617 assert!(registry.get_operation("/nonexistent", "GET").is_none());
3619 }
3620
3621 #[test]
3622 fn test_extract_path_parameters() {
3623 let spec_json = json!({
3625 "openapi": "3.0.0",
3626 "info": {"title": "Test API", "version": "1.0.0"},
3627 "paths": {
3628 "/users/{id}": {
3629 "get": {
3630 "parameters": [
3631 {
3632 "name": "id",
3633 "in": "path",
3634 "required": true,
3635 "schema": {"type": "string"}
3636 }
3637 ],
3638 "responses": {"200": {"description": "OK"}}
3639 }
3640 }
3641 }
3642 });
3643 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3644 let registry = OpenApiRouteRegistry::new(spec);
3645
3646 let params = registry.extract_path_parameters("/users/123", "GET");
3648 assert_eq!(params.get("id"), Some(&"123".to_string()));
3649
3650 let empty_params = registry.extract_path_parameters("/users", "GET");
3652 assert!(empty_params.is_empty());
3653 }
3654
3655 #[test]
3656 fn test_extract_path_parameters_multiple_params() {
3657 let spec_json = json!({
3659 "openapi": "3.0.0",
3660 "info": {"title": "Test API", "version": "1.0.0"},
3661 "paths": {
3662 "/users/{userId}/posts/{postId}": {
3663 "get": {
3664 "parameters": [
3665 {
3666 "name": "userId",
3667 "in": "path",
3668 "required": true,
3669 "schema": {"type": "string"}
3670 },
3671 {
3672 "name": "postId",
3673 "in": "path",
3674 "required": true,
3675 "schema": {"type": "string"}
3676 }
3677 ],
3678 "responses": {"200": {"description": "OK"}}
3679 }
3680 }
3681 }
3682 });
3683 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3684 let registry = OpenApiRouteRegistry::new(spec);
3685
3686 let params = registry.extract_path_parameters("/users/123/posts/456", "GET");
3688 assert_eq!(params.get("userId"), Some(&"123".to_string()));
3689 assert_eq!(params.get("postId"), Some(&"456".to_string()));
3690 }
3691
3692 #[test]
3693 fn test_validate_request_route_not_found() {
3694 let spec_json = json!({
3696 "openapi": "3.0.0",
3697 "info": {"title": "Test API", "version": "1.0.0"},
3698 "paths": {
3699 "/users": {
3700 "get": {
3701 "responses": {"200": {"description": "OK"}}
3702 }
3703 }
3704 }
3705 });
3706 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3707 let registry = OpenApiRouteRegistry::new(spec);
3708
3709 let result = registry.validate_request_with_all(
3711 "/nonexistent",
3712 "GET",
3713 &Map::new(),
3714 &Map::new(),
3715 &Map::new(),
3716 &Map::new(),
3717 None,
3718 );
3719 assert!(result.is_err());
3720 assert!(result.unwrap_err().to_string().contains("not found"));
3721 }
3722
3723 #[test]
3724 fn test_validate_request_with_path_parameters() {
3725 let spec_json = json!({
3727 "openapi": "3.0.0",
3728 "info": {"title": "Test API", "version": "1.0.0"},
3729 "paths": {
3730 "/users/{id}": {
3731 "get": {
3732 "parameters": [
3733 {
3734 "name": "id",
3735 "in": "path",
3736 "required": true,
3737 "schema": {"type": "string", "minLength": 1}
3738 }
3739 ],
3740 "responses": {"200": {"description": "OK"}}
3741 }
3742 }
3743 }
3744 });
3745 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3746 let registry = OpenApiRouteRegistry::new(spec);
3747
3748 let mut path_params = Map::new();
3750 path_params.insert("id".to_string(), json!("123"));
3751 let result = registry.validate_request_with_all(
3752 "/users/{id}",
3753 "GET",
3754 &path_params,
3755 &Map::new(),
3756 &Map::new(),
3757 &Map::new(),
3758 None,
3759 );
3760 assert!(result.is_ok());
3761 }
3762
3763 #[test]
3764 fn test_validate_request_with_query_parameters() {
3765 let spec_json = json!({
3767 "openapi": "3.0.0",
3768 "info": {"title": "Test API", "version": "1.0.0"},
3769 "paths": {
3770 "/users": {
3771 "get": {
3772 "parameters": [
3773 {
3774 "name": "page",
3775 "in": "query",
3776 "required": true,
3777 "schema": {"type": "integer", "minimum": 1}
3778 }
3779 ],
3780 "responses": {"200": {"description": "OK"}}
3781 }
3782 }
3783 }
3784 });
3785 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3786 let registry = OpenApiRouteRegistry::new(spec);
3787
3788 let mut query_params = Map::new();
3790 query_params.insert("page".to_string(), json!(1));
3791 let result = registry.validate_request_with_all(
3792 "/users",
3793 "GET",
3794 &Map::new(),
3795 &query_params,
3796 &Map::new(),
3797 &Map::new(),
3798 None,
3799 );
3800 assert!(result.is_ok());
3801 }
3802
3803 #[test]
3804 fn test_validate_request_with_header_parameters() {
3805 let spec_json = json!({
3807 "openapi": "3.0.0",
3808 "info": {"title": "Test API", "version": "1.0.0"},
3809 "paths": {
3810 "/users": {
3811 "get": {
3812 "parameters": [
3813 {
3814 "name": "X-API-Key",
3815 "in": "header",
3816 "required": true,
3817 "schema": {"type": "string"}
3818 }
3819 ],
3820 "responses": {"200": {"description": "OK"}}
3821 }
3822 }
3823 }
3824 });
3825 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3826 let registry = OpenApiRouteRegistry::new(spec);
3827
3828 let mut header_params = Map::new();
3830 header_params.insert("X-API-Key".to_string(), json!("secret-key"));
3831 let result = registry.validate_request_with_all(
3832 "/users",
3833 "GET",
3834 &Map::new(),
3835 &Map::new(),
3836 &header_params,
3837 &Map::new(),
3838 None,
3839 );
3840 assert!(result.is_ok());
3841 }
3842
3843 #[test]
3844 fn test_validate_request_with_cookie_parameters() {
3845 let spec_json = json!({
3847 "openapi": "3.0.0",
3848 "info": {"title": "Test API", "version": "1.0.0"},
3849 "paths": {
3850 "/users": {
3851 "get": {
3852 "parameters": [
3853 {
3854 "name": "sessionId",
3855 "in": "cookie",
3856 "required": true,
3857 "schema": {"type": "string"}
3858 }
3859 ],
3860 "responses": {"200": {"description": "OK"}}
3861 }
3862 }
3863 }
3864 });
3865 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3866 let registry = OpenApiRouteRegistry::new(spec);
3867
3868 let mut cookie_params = Map::new();
3870 cookie_params.insert("sessionId".to_string(), json!("abc123"));
3871 let result = registry.validate_request_with_all(
3872 "/users",
3873 "GET",
3874 &Map::new(),
3875 &Map::new(),
3876 &Map::new(),
3877 &cookie_params,
3878 None,
3879 );
3880 assert!(result.is_ok());
3881 }
3882
3883 #[test]
3884 fn test_validate_request_no_errors_early_return() {
3885 let spec_json = json!({
3887 "openapi": "3.0.0",
3888 "info": {"title": "Test API", "version": "1.0.0"},
3889 "paths": {
3890 "/users": {
3891 "get": {
3892 "responses": {"200": {"description": "OK"}}
3893 }
3894 }
3895 }
3896 });
3897 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3898 let registry = OpenApiRouteRegistry::new(spec);
3899
3900 let result = registry.validate_request_with_all(
3902 "/users",
3903 "GET",
3904 &Map::new(),
3905 &Map::new(),
3906 &Map::new(),
3907 &Map::new(),
3908 None,
3909 );
3910 assert!(result.is_ok());
3911 }
3912
3913 #[test]
3914 fn test_validate_request_query_parameter_different_styles() {
3915 let spec_json = json!({
3917 "openapi": "3.0.0",
3918 "info": {"title": "Test API", "version": "1.0.0"},
3919 "paths": {
3920 "/users": {
3921 "get": {
3922 "parameters": [
3923 {
3924 "name": "tags",
3925 "in": "query",
3926 "style": "pipeDelimited",
3927 "schema": {
3928 "type": "array",
3929 "items": {"type": "string"}
3930 }
3931 }
3932 ],
3933 "responses": {"200": {"description": "OK"}}
3934 }
3935 }
3936 }
3937 });
3938 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3939 let registry = OpenApiRouteRegistry::new(spec);
3940
3941 let mut query_params = Map::new();
3943 query_params.insert("tags".to_string(), json!(["tag1", "tag2"]));
3944 let result = registry.validate_request_with_all(
3945 "/users",
3946 "GET",
3947 &Map::new(),
3948 &query_params,
3949 &Map::new(),
3950 &Map::new(),
3951 None,
3952 );
3953 assert!(result.is_ok() || result.is_err()); }
3956}