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 let validator = Arc::new(self.clone_for_validation());
428 for (axum_path, route) in &deduped {
429 tracing::debug!("Adding route: {} {}", route.method, route.path);
430 let operation = route.operation.clone();
431 let method = route.method.clone();
432 let path_template = route.path.clone();
433 let validator = validator.clone();
434 let route_clone = (*route).clone();
435 let ctx = ctx.clone();
436
437 let mut operation_tags = operation.tags.clone();
439 if let Some(operation_id) = &operation.operation_id {
440 operation_tags.push(operation_id.clone());
441 }
442
443 let handler = move |AxumPath(path_params): AxumPath<HashMap<String, String>>,
445 RawQuery(raw_query): RawQuery,
446 headers: HeaderMap,
447 body: axum::body::Bytes| async move {
448 tracing::debug!("Handling OpenAPI request: {} {}", method, path_template);
449
450 if let Some(ref loader) = ctx.custom_fixture_loader {
452 use crate::request_fingerprint::RequestFingerprint;
453 use axum::http::{Method, Uri};
454
455 let mut request_path = path_template.clone();
457 for (key, value) in &path_params {
458 request_path = request_path.replace(&format!("{{{}}}", key), value);
459 }
460
461 let normalized_request_path =
463 crate::custom_fixture::CustomFixtureLoader::normalize_path(&request_path);
464
465 let query_string =
467 raw_query.as_ref().map(|q| q.to_string()).unwrap_or_default();
468
469 let uri_str = if query_string.is_empty() {
472 normalized_request_path.clone()
473 } else {
474 format!("{}?{}", normalized_request_path, query_string)
475 };
476
477 if let Ok(uri) = uri_str.parse::<Uri>() {
478 let http_method =
479 Method::from_bytes(method.as_bytes()).unwrap_or(Method::GET);
480 let body_slice = if body.is_empty() {
481 None
482 } else {
483 Some(body.as_ref())
484 };
485 let fingerprint =
486 RequestFingerprint::new(http_method, &uri, &headers, body_slice);
487
488 tracing::debug!(
490 "Checking fixture for {} {} (template: '{}', request_path: '{}', normalized: '{}', fingerprint.path: '{}')",
491 method,
492 path_template,
493 path_template,
494 request_path,
495 normalized_request_path,
496 fingerprint.path
497 );
498
499 if let Some(custom_fixture) = loader.load_fixture(&fingerprint) {
500 tracing::debug!(
501 "Using custom fixture for {} {}",
502 method,
503 path_template
504 );
505
506 if custom_fixture.delay_ms > 0 {
508 tokio::time::sleep(tokio::time::Duration::from_millis(
509 custom_fixture.delay_ms,
510 ))
511 .await;
512 }
513
514 let response_body = if custom_fixture.response.is_string() {
516 custom_fixture.response.as_str().unwrap().to_string()
517 } else {
518 serde_json::to_string(&custom_fixture.response)
519 .unwrap_or_else(|_| "{}".to_string())
520 };
521
522 let json_value: Value = serde_json::from_str(&response_body)
524 .unwrap_or_else(|_| serde_json::json!({}));
525
526 let status = axum::http::StatusCode::from_u16(custom_fixture.status)
528 .unwrap_or(axum::http::StatusCode::OK);
529
530 let mut response = (status, Json(json_value)).into_response();
531
532 let response_headers = response.headers_mut();
534 for (key, value) in &custom_fixture.headers {
535 if let (Ok(header_name), Ok(header_value)) = (
536 axum::http::HeaderName::from_bytes(key.as_bytes()),
537 axum::http::HeaderValue::from_str(value),
538 ) {
539 response_headers.insert(header_name, header_value);
540 }
541 }
542
543 if !custom_fixture.headers.contains_key("content-type") {
545 response_headers.insert(
546 axum::http::header::CONTENT_TYPE,
547 axum::http::HeaderValue::from_static("application/json"),
548 );
549 }
550
551 return response;
552 }
553 }
554 }
555
556 if let Some(ref failure_injector) = ctx.failure_injector {
558 if let Some((status_code, error_message)) =
559 failure_injector.process_request(&operation_tags)
560 {
561 let payload = serde_json::json!({
562 "error": error_message,
563 "injected_failure": true
564 });
565 let body_bytes = serde_json::to_vec(&payload)
566 .unwrap_or_else(|_| br#"{"error":"injected failure"}"#.to_vec());
567 return axum::http::Response::builder()
568 .status(
569 axum::http::StatusCode::from_u16(status_code)
570 .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR),
571 )
572 .header(axum::http::header::CONTENT_TYPE, "application/json")
573 .body(axum::body::Body::from(body_bytes))
574 .expect("Response builder should create valid response");
575 }
576 }
577
578 if let Some(ref injector) = ctx.latency_injector {
580 if let Err(e) = injector.inject_latency(&operation_tags).await {
581 tracing::warn!("Failed to inject latency: {}", e);
582 }
583 }
584
585 let scenario = headers
587 .get("X-Mockforge-Scenario")
588 .and_then(|v| v.to_str().ok())
589 .map(|s| s.to_string())
590 .or_else(|| std::env::var("MOCKFORGE_HTTP_SCENARIO").ok());
591
592 let status_override = headers
593 .get("X-Mockforge-Response-Status")
594 .and_then(|v| v.to_str().ok())
595 .and_then(|s| s.parse::<u16>().ok());
596
597 let (selected_status, mock_response) = route_clone
599 .mock_response_with_status_and_scenario_and_override(
600 scenario.as_deref(),
601 status_override,
602 );
603
604 if ctx.enable_full_validation {
606 let mut path_map = Map::new();
608 for (k, v) in &path_params {
609 path_map.insert(k.clone(), Value::String(v.clone()));
610 }
611
612 let mut query_map = Map::new();
614 if let Some(ref q) = raw_query {
615 for (k, v) in url::form_urlencoded::parse(q.as_bytes()) {
616 query_map.insert(k.to_string(), Value::String(v.to_string()));
617 }
618 }
619
620 let mut header_map = Map::new();
622 for p_ref in &operation.parameters {
623 if let Some(openapiv3::Parameter::Header { parameter_data, .. }) =
624 p_ref.as_item()
625 {
626 let name_lc = parameter_data.name.to_ascii_lowercase();
627 if let Ok(hn) = axum::http::HeaderName::from_bytes(name_lc.as_bytes()) {
628 if let Some(val) = headers.get(hn) {
629 if let Ok(s) = val.to_str() {
630 header_map.insert(
631 parameter_data.name.clone(),
632 Value::String(s.to_string()),
633 );
634 }
635 }
636 }
637 }
638 }
639
640 let mut cookie_map = Map::new();
642 if let Some(val) = headers.get(axum::http::header::COOKIE) {
643 if let Ok(s) = val.to_str() {
644 for part in s.split(';') {
645 let part = part.trim();
646 if let Some((k, v)) = part.split_once('=') {
647 cookie_map.insert(k.to_string(), Value::String(v.to_string()));
648 }
649 }
650 }
651 }
652
653 let is_multipart = headers
655 .get(axum::http::header::CONTENT_TYPE)
656 .and_then(|v| v.to_str().ok())
657 .map(|ct| ct.starts_with("multipart/form-data"))
658 .unwrap_or(false);
659
660 #[allow(unused_assignments)]
662 let mut multipart_fields = HashMap::new();
663 let mut _multipart_files = HashMap::new();
664 let mut body_json: Option<Value> = None;
665
666 if is_multipart {
667 match extract_multipart_from_bytes(&body, &headers).await {
669 Ok((fields, files)) => {
670 multipart_fields = fields;
671 _multipart_files = files;
672 let mut body_obj = Map::new();
674 for (k, v) in &multipart_fields {
675 body_obj.insert(k.clone(), v.clone());
676 }
677 if !body_obj.is_empty() {
678 body_json = Some(Value::Object(body_obj));
679 }
680 }
681 Err(e) => {
682 tracing::warn!("Failed to parse multipart data: {}", e);
683 }
684 }
685 } else {
686 body_json = if !body.is_empty() {
688 serde_json::from_slice(&body).ok()
689 } else {
690 None
691 };
692 }
693
694 if let Err(e) = validator.validate_request_with_all(
695 &path_template,
696 &method,
697 &path_map,
698 &query_map,
699 &header_map,
700 &cookie_map,
701 body_json.as_ref(),
702 ) {
703 let status_code =
705 validator.options.validation_status.unwrap_or_else(|| {
706 std::env::var("MOCKFORGE_VALIDATION_STATUS")
707 .ok()
708 .and_then(|s| s.parse::<u16>().ok())
709 .unwrap_or(400)
710 });
711
712 let payload = if status_code == 422 {
713 generate_enhanced_422_response(
715 &validator,
716 &path_template,
717 &method,
718 body_json.as_ref(),
719 &path_map,
720 &query_map,
721 &header_map,
722 &cookie_map,
723 )
724 } else {
725 let msg = format!("{}", e);
727 let detail_val = serde_json::from_str::<Value>(&msg)
728 .unwrap_or(serde_json::json!(msg));
729 json!({
730 "error": "request validation failed",
731 "detail": detail_val,
732 "method": method,
733 "path": path_template,
734 "timestamp": Utc::now().to_rfc3339(),
735 })
736 };
737
738 record_validation_error(&payload);
739
740 let reason = payload
747 .get("detail")
748 .and_then(|d| {
749 if d.is_string() {
750 d.as_str().map(|s| s.to_string())
751 } else {
752 serde_json::to_string(d).ok()
753 }
754 })
755 .unwrap_or_else(|| {
756 payload
757 .get("error")
758 .and_then(|v| v.as_str())
759 .unwrap_or("request validation failed")
760 .to_string()
761 });
762 let category = classify_validation_reason(&reason);
763 mockforge_foundation::conformance_violations::record(
764 mockforge_foundation::conformance_violations::ServerConformanceViolation {
765 timestamp: Utc::now(),
766 method: method.to_string(),
767 path: path_template.clone(),
768 client_ip: "unknown".to_string(),
769 status: status_code,
770 reason,
771 category,
772 },
773 );
774
775 let status = axum::http::StatusCode::from_u16(status_code)
776 .unwrap_or(axum::http::StatusCode::BAD_REQUEST);
777
778 let body_bytes = serde_json::to_vec(&payload)
780 .unwrap_or_else(|_| br#"{"error":"Serialization failed"}"#.to_vec());
781
782 return axum::http::Response::builder()
783 .status(status)
784 .header(axum::http::header::CONTENT_TYPE, "application/json")
785 .body(axum::body::Body::from(body_bytes))
786 .expect("Response builder should create valid response with valid headers and body");
787 }
788 }
789
790 let mut final_response = mock_response.clone();
799 let env_expand: Option<bool> = std::env::var("MOCKFORGE_RESPONSE_TEMPLATE_EXPAND")
800 .ok()
801 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"));
802 let expand = match env_expand {
803 Some(v) => v,
804 None => {
805 ctx.enable_template_expand || validator.options.response_template_expand
806 }
807 };
808 if expand {
809 if let Some(ref rewriter) = ctx.response_rewriter {
810 rewriter.expand_tokens(&mut final_response);
811 }
812 }
813
814 if ctx.overrides_enabled {
816 if let Some(ref rewriter) = ctx.response_rewriter {
817 let op_tags =
818 operation.operation_id.clone().map(|id| vec![id]).unwrap_or_default();
819 rewriter.apply_overrides(
820 &operation.operation_id.clone().unwrap_or_default(),
821 &op_tags,
822 &path_template,
823 &mut final_response,
824 );
825 }
826 }
827
828 if ctx.enable_full_validation {
830 if validator.options.validate_responses {
832 if let Some((status_code, _response)) = operation
834 .responses
835 .responses
836 .iter()
837 .filter_map(|(status, resp)| match status {
838 openapiv3::StatusCode::Code(code)
839 if *code >= 200 && *code < 300 =>
840 {
841 resp.as_item().map(|r| ((*code), r))
842 }
843 openapiv3::StatusCode::Range(range)
844 if *range >= 200 && *range < 300 =>
845 {
846 resp.as_item().map(|r| (200, r))
847 }
848 _ => None,
849 })
850 .next()
851 {
852 if serde_json::from_value::<Value>(final_response.clone()).is_err() {
854 tracing::warn!(
855 "Response validation failed: invalid JSON for status {}",
856 status_code
857 );
858 }
859 }
860 }
861
862 let mut trace = ResponseGenerationTrace::new();
864 trace.set_final_payload(final_response.clone());
865
866 if let Some((_status_code, response_ref)) = operation
868 .responses
869 .responses
870 .iter()
871 .filter_map(|(status, resp)| match status {
872 openapiv3::StatusCode::Code(code) if *code == selected_status => {
873 resp.as_item().map(|r| ((*code), r))
874 }
875 openapiv3::StatusCode::Range(range)
876 if *range >= 200 && *range < 300 =>
877 {
878 resp.as_item().map(|r| (200, r))
879 }
880 _ => None,
881 })
882 .next()
883 .or_else(|| {
884 operation
886 .responses
887 .responses
888 .iter()
889 .filter_map(|(status, resp)| match status {
890 openapiv3::StatusCode::Code(code)
891 if *code >= 200 && *code < 300 =>
892 {
893 resp.as_item().map(|r| ((*code), r))
894 }
895 _ => None,
896 })
897 .next()
898 })
899 {
900 let response_item = response_ref;
902 if let Some(content) = response_item.content.get("application/json") {
904 if let Some(schema_ref) = &content.schema {
905 if let Some(schema) = schema_ref.as_item() {
907 if let Ok(schema_json) = serde_json::to_value(schema) {
908 let validation_errors =
910 validation_diff(&schema_json, &final_response);
911 trace.set_schema_validation_diff(validation_errors);
912 }
913 }
914 }
915 }
916 }
917
918 let mut response = Json(final_response).into_response();
920 response.extensions_mut().insert(trace);
921 *response.status_mut() = axum::http::StatusCode::from_u16(selected_status)
922 .unwrap_or(axum::http::StatusCode::OK);
923 return response;
924 }
925
926 let mut response = Json(final_response).into_response();
928 *response.status_mut() = axum::http::StatusCode::from_u16(selected_status)
929 .unwrap_or(axum::http::StatusCode::OK);
930 response
931 };
932
933 router = Self::route_for_method(router, axum_path, &route.method, handler);
934 }
935
936 if ctx.add_spec_endpoint {
938 let spec_json = serde_json::to_value(&self.spec.spec).unwrap_or(Value::Null);
939 router = router.route("/openapi.json", get(move || async move { Json(spec_json) }));
940 }
941
942 router.layer(DefaultBodyLimit::max(openapi_body_limit_bytes()))
946 }
947
948 pub fn build_router_with_latency(self, latency_injector: LatencyInjector) -> Router {
950 self.build_router_with_injectors(latency_injector, None)
951 }
952
953 pub fn build_router_with_injectors(
955 self,
956 latency_injector: LatencyInjector,
957 failure_injector: Option<mockforge_foundation::failure_injection::FailureInjector>,
958 ) -> Router {
959 self.build_router_with_injectors_and_overrides(
960 latency_injector,
961 failure_injector,
962 None,
963 false,
964 )
965 }
966
967 pub fn build_router_with_injectors_and_overrides(
971 self,
972 latency_injector: LatencyInjector,
973 failure_injector: Option<mockforge_foundation::failure_injection::FailureInjector>,
974 response_rewriter: Option<Arc<dyn ResponseRewriter>>,
975 overrides_enabled: bool,
976 ) -> Router {
977 let ctx = RouterContext {
978 custom_fixture_loader: self.custom_fixture_loader.clone(),
979 latency_injector: Some(latency_injector),
980 failure_injector,
981 response_rewriter,
982 overrides_enabled,
983 enable_full_validation: true,
984 enable_template_expand: true,
985 add_spec_endpoint: true,
986 ..Default::default()
987 };
988 self.build_router_with_context(ctx)
989 }
990
991 pub fn get_route(&self, path: &str, method: &str) -> Option<&OpenApiRoute> {
993 self.routes.iter().find(|route| route.path == path && route.method == method)
994 }
995
996 pub fn get_routes_for_path(&self, path: &str) -> Vec<&OpenApiRoute> {
998 self.routes.iter().filter(|route| route.path == path).collect()
999 }
1000
1001 pub fn validate_request(&self, path: &str, method: &str, body: Option<&Value>) -> Result<()> {
1003 self.validate_request_with(path, method, &Map::new(), &Map::new(), body)
1004 }
1005
1006 pub fn validate_request_with(
1008 &self,
1009 path: &str,
1010 method: &str,
1011 path_params: &Map<String, Value>,
1012 query_params: &Map<String, Value>,
1013 body: Option<&Value>,
1014 ) -> Result<()> {
1015 self.validate_request_with_all(
1016 path,
1017 method,
1018 path_params,
1019 query_params,
1020 &Map::new(),
1021 &Map::new(),
1022 body,
1023 )
1024 }
1025
1026 #[allow(clippy::too_many_arguments)]
1039 pub fn run_validation_with_recording(
1040 &self,
1041 path_template: &str,
1042 method: &str,
1043 path_params: &Map<String, Value>,
1044 query_params: &Map<String, Value>,
1045 header_map: &Map<String, Value>,
1046 cookie_map: &Map<String, Value>,
1047 body: Option<&Value>,
1048 ) -> std::result::Result<(), (u16, Value)> {
1049 let e = match self.validate_request_with_all(
1050 path_template,
1051 method,
1052 path_params,
1053 query_params,
1054 header_map,
1055 cookie_map,
1056 body,
1057 ) {
1058 Ok(()) => return Ok(()),
1059 Err(e) => e,
1060 };
1061
1062 let status_code = self.options.validation_status.unwrap_or_else(|| {
1063 std::env::var("MOCKFORGE_VALIDATION_STATUS")
1064 .ok()
1065 .and_then(|s| s.parse::<u16>().ok())
1066 .unwrap_or(400)
1067 });
1068
1069 let payload = if status_code == 422 {
1070 generate_enhanced_422_response(
1071 self,
1072 path_template,
1073 method,
1074 body,
1075 path_params,
1076 query_params,
1077 header_map,
1078 cookie_map,
1079 )
1080 } else {
1081 let msg = format!("{}", e);
1082 let detail_val = serde_json::from_str::<Value>(&msg).unwrap_or(serde_json::json!(msg));
1083 json!({
1084 "error": "request validation failed",
1085 "detail": detail_val,
1086 "method": method,
1087 "path": path_template,
1088 "timestamp": Utc::now().to_rfc3339(),
1089 })
1090 };
1091
1092 record_validation_error(&payload);
1093
1094 let reason = payload
1095 .get("detail")
1096 .and_then(|d| {
1097 if d.is_string() {
1098 d.as_str().map(|s| s.to_string())
1099 } else {
1100 serde_json::to_string(d).ok()
1101 }
1102 })
1103 .unwrap_or_else(|| {
1104 payload
1105 .get("error")
1106 .and_then(|v| v.as_str())
1107 .unwrap_or("request validation failed")
1108 .to_string()
1109 });
1110 let category = classify_validation_reason(&reason);
1111 mockforge_foundation::conformance_violations::record(
1112 mockforge_foundation::conformance_violations::ServerConformanceViolation {
1113 timestamp: Utc::now(),
1114 method: method.to_string(),
1115 path: path_template.to_string(),
1116 client_ip: "unknown".to_string(),
1117 status: status_code,
1118 reason,
1119 category,
1120 },
1121 );
1122
1123 if mockforge_foundation::unknown_paths::shadow_mode_enabled() {
1130 return Ok(());
1131 }
1132
1133 Err((status_code, payload))
1134 }
1135
1136 #[allow(clippy::too_many_arguments)]
1138 pub fn validate_request_with_all(
1139 &self,
1140 path: &str,
1141 method: &str,
1142 path_params: &Map<String, Value>,
1143 query_params: &Map<String, Value>,
1144 header_params: &Map<String, Value>,
1145 cookie_params: &Map<String, Value>,
1146 body: Option<&Value>,
1147 ) -> Result<()> {
1148 for pref in &self.options.admin_skip_prefixes {
1150 if !pref.is_empty() && path.starts_with(pref) {
1151 return Ok(());
1152 }
1153 }
1154 let env_mode = std::env::var("MOCKFORGE_REQUEST_VALIDATION").ok().map(|v| {
1156 match v.to_ascii_lowercase().as_str() {
1157 "off" | "disable" | "disabled" => ValidationMode::Disabled,
1158 "warn" | "warning" => ValidationMode::Warn,
1159 _ => ValidationMode::Enforce,
1160 }
1161 });
1162 let aggregate = std::env::var("MOCKFORGE_AGGREGATE_ERRORS")
1163 .ok()
1164 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
1165 .unwrap_or(self.options.aggregate_errors);
1166 let env_overrides: Option<Map<String, Value>> =
1168 std::env::var("MOCKFORGE_VALIDATION_OVERRIDES_JSON")
1169 .ok()
1170 .and_then(|s| serde_json::from_str::<Value>(&s).ok())
1171 .and_then(|v| v.as_object().cloned());
1172 let mut effective_mode = env_mode.unwrap_or(self.options.request_mode.clone());
1174 if let Some(map) = &env_overrides {
1176 if let Some(v) = map.get(&format!("{} {}", method, path)) {
1177 if let Some(m) = v.as_str() {
1178 effective_mode = match m {
1179 "off" => ValidationMode::Disabled,
1180 "warn" => ValidationMode::Warn,
1181 _ => ValidationMode::Enforce,
1182 };
1183 }
1184 }
1185 }
1186 if let Some(override_mode) = self.options.overrides.get(&format!("{} {}", method, path)) {
1188 effective_mode = override_mode.clone();
1189 }
1190 if matches!(effective_mode, ValidationMode::Disabled) {
1191 return Ok(());
1192 }
1193 if let Some(route) = self.get_route(path, method) {
1194 if matches!(effective_mode, ValidationMode::Disabled) {
1195 return Ok(());
1196 }
1197 let mut errors: Vec<String> = Vec::new();
1198 let mut details: Vec<Value> = Vec::new();
1199 if let Some(schema) = &route.operation.request_body {
1201 if let Some(value) = body {
1202 let request_body = match schema {
1204 openapiv3::ReferenceOr::Item(rb) => Some(rb),
1205 openapiv3::ReferenceOr::Reference { reference } => {
1206 self.spec
1208 .spec
1209 .components
1210 .as_ref()
1211 .and_then(|components| {
1212 components.request_bodies.get(
1213 reference.trim_start_matches("#/components/requestBodies/"),
1214 )
1215 })
1216 .and_then(|rb_ref| rb_ref.as_item())
1217 }
1218 };
1219
1220 if let Some(rb) = request_body {
1221 if let Some(content) = rb.content.get("application/json") {
1222 if let Some(schema_ref) = &content.schema {
1223 match schema_ref {
1225 openapiv3::ReferenceOr::Item(schema) => {
1226 if let Err(validation_error) =
1228 OpenApiSchema::new(schema.clone()).validate(value)
1229 {
1230 let error_msg = validation_error.to_string();
1231 errors.push(format!(
1232 "body validation failed: {}",
1233 error_msg
1234 ));
1235 if aggregate {
1236 details.push(serde_json::json!({"path":"body","code":"schema_validation","message":error_msg}));
1237 }
1238 }
1239 }
1240 openapiv3::ReferenceOr::Reference { reference } => {
1241 if let Some(resolved_schema_ref) =
1243 self.spec.get_schema(reference)
1244 {
1245 if let Err(validation_error) = OpenApiSchema::new(
1246 resolved_schema_ref.schema.clone(),
1247 )
1248 .validate(value)
1249 {
1250 let error_msg = validation_error.to_string();
1251 errors.push(format!(
1252 "body validation failed: {}",
1253 error_msg
1254 ));
1255 if aggregate {
1256 details.push(serde_json::json!({"path":"body","code":"schema_validation","message":error_msg}));
1257 }
1258 }
1259 } else {
1260 errors.push(format!("body validation failed: could not resolve schema reference {}", reference));
1262 if aggregate {
1263 details.push(serde_json::json!({"path":"body","code":"reference_error","message":"Could not resolve schema reference"}));
1264 }
1265 }
1266 }
1267 }
1268 }
1269 }
1270 } else {
1271 errors.push("body validation failed: could not resolve request body or no application/json content".to_string());
1273 if aggregate {
1274 details.push(serde_json::json!({"path":"body","code":"reference_error","message":"Could not resolve request body reference"}));
1275 }
1276 }
1277 } else {
1278 errors.push("body: Request body is required but not provided".to_string());
1279 details.push(serde_json::json!({"path":"body","code":"required","message":"Request body is required"}));
1280 }
1281 } else if body.is_some() {
1282 tracing::debug!("Body provided for operation without requestBody; accepting");
1284 }
1285
1286 for p_ref in &route.operation.parameters {
1288 if let Some(p) = p_ref.as_item() {
1289 match p {
1290 openapiv3::Parameter::Path { parameter_data, .. } => {
1291 validate_parameter(
1292 parameter_data,
1293 path_params,
1294 "path",
1295 aggregate,
1296 &mut errors,
1297 &mut details,
1298 );
1299 }
1300 openapiv3::Parameter::Query {
1301 parameter_data,
1302 style,
1303 ..
1304 } => {
1305 let deep_value = if matches!(style, openapiv3::QueryStyle::DeepObject) {
1308 let prefix_bracket = format!("{}[", parameter_data.name);
1309 let mut obj = Map::new();
1310 for (key, val) in query_params.iter() {
1311 if let Some(rest) = key.strip_prefix(&prefix_bracket) {
1312 if let Some(prop) = rest.strip_suffix(']') {
1313 obj.insert(prop.to_string(), val.clone());
1314 }
1315 }
1316 }
1317 if obj.is_empty() {
1318 None
1319 } else {
1320 Some(Value::Object(obj))
1321 }
1322 } else {
1323 None
1324 };
1325 let style_str = match style {
1326 openapiv3::QueryStyle::Form => Some("form"),
1327 openapiv3::QueryStyle::SpaceDelimited => Some("spaceDelimited"),
1328 openapiv3::QueryStyle::PipeDelimited => Some("pipeDelimited"),
1329 openapiv3::QueryStyle::DeepObject => Some("deepObject"),
1330 };
1331 validate_parameter_with_deep_object(
1332 parameter_data,
1333 query_params,
1334 "query",
1335 deep_value,
1336 style_str,
1337 aggregate,
1338 &mut errors,
1339 &mut details,
1340 );
1341 }
1342 openapiv3::Parameter::Header { parameter_data, .. } => {
1343 validate_parameter(
1344 parameter_data,
1345 header_params,
1346 "header",
1347 aggregate,
1348 &mut errors,
1349 &mut details,
1350 );
1351 }
1352 openapiv3::Parameter::Cookie { parameter_data, .. } => {
1353 validate_parameter(
1354 parameter_data,
1355 cookie_params,
1356 "cookie",
1357 aggregate,
1358 &mut errors,
1359 &mut details,
1360 );
1361 }
1362 }
1363 }
1364 }
1365 if errors.is_empty() {
1366 return Ok(());
1367 }
1368 match effective_mode {
1369 ValidationMode::Disabled => Ok(()),
1370 ValidationMode::Warn => {
1371 tracing::warn!("Request validation warnings: {:?}", errors);
1372 Ok(())
1373 }
1374 ValidationMode::Enforce => Err(Error::validation(
1375 serde_json::json!({"errors": errors, "details": details}).to_string(),
1376 )),
1377 }
1378 } else {
1379 Err(Error::internal(format!("Route {} {} not found in OpenAPI spec", method, path)))
1380 }
1381 }
1382
1383 pub fn paths(&self) -> Vec<String> {
1387 let mut paths: Vec<String> = self.routes.iter().map(|route| route.path.clone()).collect();
1388 paths.sort();
1389 paths.dedup();
1390 paths
1391 }
1392
1393 pub fn methods(&self) -> Vec<String> {
1395 let mut methods: Vec<String> =
1396 self.routes.iter().map(|route| route.method.clone()).collect();
1397 methods.sort();
1398 methods.dedup();
1399 methods
1400 }
1401
1402 pub fn get_operation(&self, path: &str, method: &str) -> Option<OpenApiOperation> {
1404 self.get_route(path, method).map(|route| {
1405 OpenApiOperation::from_operation(
1406 &route.method,
1407 route.path.clone(),
1408 &route.operation,
1409 &self.spec,
1410 )
1411 })
1412 }
1413
1414 pub fn extract_path_parameters(&self, path: &str, method: &str) -> HashMap<String, String> {
1416 for route in &self.routes {
1417 if route.method != method {
1418 continue;
1419 }
1420
1421 if let Some(params) = self.match_path_to_route(path, &route.path) {
1422 return params;
1423 }
1424 }
1425 HashMap::new()
1426 }
1427
1428 fn match_path_to_route(
1430 &self,
1431 request_path: &str,
1432 route_pattern: &str,
1433 ) -> Option<HashMap<String, String>> {
1434 let mut params = HashMap::new();
1435
1436 let request_segments: Vec<&str> = request_path.trim_start_matches('/').split('/').collect();
1438 let pattern_segments: Vec<&str> =
1439 route_pattern.trim_start_matches('/').split('/').collect();
1440
1441 if request_segments.len() != pattern_segments.len() {
1442 return None;
1443 }
1444
1445 for (req_seg, pat_seg) in request_segments.iter().zip(pattern_segments.iter()) {
1446 if pat_seg.starts_with('{') && pat_seg.ends_with('}') {
1447 let param_name = &pat_seg[1..pat_seg.len() - 1];
1449 params.insert(param_name.to_string(), req_seg.to_string());
1450 } else if req_seg != pat_seg {
1451 return None;
1453 }
1454 }
1455
1456 Some(params)
1457 }
1458
1459 pub fn convert_path_to_axum(openapi_path: &str) -> String {
1462 openapi_path.to_string()
1464 }
1465
1466 pub fn build_router_with_ai(
1468 &self,
1469 ai_generator: Option<Arc<dyn AiGenerator + Send + Sync>>,
1470 ) -> Router {
1471 let mut router = Router::new();
1472 let deduped = self.deduplicated_routes();
1473 tracing::debug!("Building router with AI support from {} routes", self.routes.len());
1474
1475 let validator = Arc::new(self.clone_for_validation());
1479 for (axum_path, route) in &deduped {
1480 tracing::debug!("Adding AI-enabled route: {} {}", route.method, route.path);
1481
1482 let route_clone = (*route).clone();
1483 let ai_generator_clone = ai_generator.clone();
1484 let validator_clone = validator.clone();
1488
1489 let handler = move |AxumPath(path_params): AxumPath<HashMap<String, String>>,
1491 axum::extract::Query(query_params): axum::extract::Query<
1492 HashMap<String, String>,
1493 >,
1494 headers: HeaderMap,
1495 body: Option<Json<Value>>| {
1496 let route = route_clone.clone();
1497 let ai_generator = ai_generator_clone.clone();
1498 let validator = validator_clone.clone();
1499
1500 async move {
1501 let mut path_map = Map::new();
1506 for (k, v) in &path_params {
1507 path_map.insert(k.clone(), Value::String(v.clone()));
1508 }
1509 let mut query_map = Map::new();
1510 for (k, v) in &query_params {
1511 query_map.insert(k.clone(), Value::String(v.clone()));
1512 }
1513 let mut header_map = Map::new();
1514 for (k, v) in headers.iter() {
1515 if let Ok(s) = v.to_str() {
1516 header_map.insert(k.to_string(), Value::String(s.to_string()));
1517 }
1518 }
1519 let body_val: Option<&Value> = body.as_ref().map(|Json(b)| b);
1520 if let Err((status_code, payload)) = validator.run_validation_with_recording(
1521 &route.path,
1522 &route.method,
1523 &path_map,
1524 &query_map,
1525 &header_map,
1526 &Map::new(),
1527 body_val,
1528 ) {
1529 let status = axum::http::StatusCode::from_u16(status_code)
1530 .unwrap_or(axum::http::StatusCode::BAD_REQUEST);
1531 return (status, Json(payload));
1532 }
1533
1534 tracing::debug!(
1535 "Handling AI request for route: {} {}",
1536 route.method,
1537 route.path
1538 );
1539
1540 let mut context = RequestContext::new(route.method.clone(), route.path.clone());
1542
1543 context.headers = headers
1545 .iter()
1546 .map(|(k, v)| {
1547 (k.to_string(), Value::String(v.to_str().unwrap_or("").to_string()))
1548 })
1549 .collect();
1550
1551 context.body = body.map(|Json(b)| b);
1553
1554 let (status, response) = if let (Some(generator), Some(_ai_config)) =
1556 (ai_generator, &route.ai_config)
1557 {
1558 route
1559 .mock_response_with_status_async(&context, Some(generator.as_ref()))
1560 .await
1561 } else {
1562 route.mock_response_with_status()
1564 };
1565
1566 (
1567 axum::http::StatusCode::from_u16(status)
1568 .unwrap_or(axum::http::StatusCode::OK),
1569 Json(response),
1570 )
1571 }
1572 };
1573
1574 router = Self::route_for_method(router, axum_path, &route.method, handler);
1575 }
1576
1577 router.layer(DefaultBodyLimit::max(openapi_body_limit_bytes()))
1581 }
1582
1583 pub fn build_router_with_mockai(
1594 &self,
1595 mockai: Option<
1596 Arc<
1597 tokio::sync::RwLock<
1598 dyn mockforge_foundation::intelligent_behavior::MockAiBehavior + Send + Sync,
1599 >,
1600 >,
1601 >,
1602 ) -> Router {
1603 use mockforge_foundation::intelligent_behavior::Request as MockAIRequest;
1604
1605 let mut router = Router::new();
1606 let deduped = self.deduplicated_routes();
1607 tracing::debug!("Building router with MockAI support from {} routes", self.routes.len());
1608
1609 let custom_loader = self.custom_fixture_loader.clone();
1610 let validator = Arc::new(self.clone_for_validation());
1614 for (axum_path, route) in &deduped {
1615 tracing::debug!("Adding MockAI-enabled route: {} {}", route.method, route.path);
1616
1617 let route_clone = (*route).clone();
1618 let mockai_clone = mockai.clone();
1619 let custom_loader_clone = custom_loader.clone();
1620 let validator_clone = validator.clone();
1626
1627 let handler = move |AxumPath(path_params): AxumPath<HashMap<String, String>>,
1631 query: axum::extract::Query<HashMap<String, String>>,
1632 headers: HeaderMap,
1633 body: Option<Json<Value>>| {
1634 let route = route_clone.clone();
1635 let mockai = mockai_clone.clone();
1636 let validator = validator_clone.clone();
1637
1638 async move {
1639 let mut path_map = Map::new();
1644 for (k, v) in &path_params {
1645 path_map.insert(k.clone(), Value::String(v.clone()));
1646 }
1647 let mut query_map = Map::new();
1648 for (k, v) in &query.0 {
1649 query_map.insert(k.clone(), Value::String(v.clone()));
1650 }
1651 let mut header_map = Map::new();
1652 for (k, v) in headers.iter() {
1653 if let Ok(s) = v.to_str() {
1654 header_map.insert(k.to_string(), Value::String(s.to_string()));
1655 }
1656 }
1657 let body_val: Option<&Value> = body.as_ref().map(|Json(b)| b);
1658 if let Err((status_code, payload)) = validator.run_validation_with_recording(
1659 &route.path,
1660 &route.method,
1661 &path_map,
1662 &query_map,
1663 &header_map,
1664 &Map::new(),
1665 body_val,
1666 ) {
1667 let status = axum::http::StatusCode::from_u16(status_code)
1668 .unwrap_or(axum::http::StatusCode::BAD_REQUEST);
1669 return (status, Json(payload));
1670 }
1671
1672 tracing::info!(
1673 "[FIXTURE DEBUG] Starting fixture check for {} {} (custom_loader available: {})",
1674 route.method,
1675 route.path,
1676 custom_loader_clone.is_some()
1677 );
1678
1679 if let Some(ref loader) = custom_loader_clone {
1681 use crate::request_fingerprint::RequestFingerprint;
1682 use axum::http::{Method, Uri};
1683
1684 let query_string = if query.0.is_empty() {
1686 String::new()
1687 } else {
1688 query
1689 .0
1690 .iter()
1691 .map(|(k, v)| format!("{}={}", k, v))
1692 .collect::<Vec<_>>()
1693 .join("&")
1694 };
1695
1696 let normalized_request_path =
1698 crate::custom_fixture::CustomFixtureLoader::normalize_path(&route.path);
1699
1700 tracing::info!(
1701 "[FIXTURE DEBUG] Path normalization: original='{}', normalized='{}'",
1702 route.path,
1703 normalized_request_path
1704 );
1705
1706 let uri_str = if query_string.is_empty() {
1708 normalized_request_path.clone()
1709 } else {
1710 format!("{}?{}", normalized_request_path, query_string)
1711 };
1712
1713 tracing::info!(
1714 "[FIXTURE DEBUG] URI construction: uri_str='{}', query_string='{}'",
1715 uri_str,
1716 query_string
1717 );
1718
1719 if let Ok(uri) = uri_str.parse::<Uri>() {
1720 let http_method =
1721 Method::from_bytes(route.method.as_bytes()).unwrap_or(Method::GET);
1722
1723 let body_bytes =
1725 body.as_ref().and_then(|Json(b)| serde_json::to_vec(b).ok());
1726 let body_slice = body_bytes.as_deref();
1727
1728 let fingerprint =
1729 RequestFingerprint::new(http_method, &uri, &headers, body_slice);
1730
1731 tracing::info!(
1732 "[FIXTURE DEBUG] RequestFingerprint created: method='{}', path='{}', query='{}', body_hash={:?}",
1733 fingerprint.method,
1734 fingerprint.path,
1735 fingerprint.query,
1736 fingerprint.body_hash
1737 );
1738
1739 let available_fixtures = loader.has_fixture(&fingerprint);
1741 tracing::info!(
1742 "[FIXTURE DEBUG] Fixture check result: has_fixture={}",
1743 available_fixtures
1744 );
1745
1746 if let Some(custom_fixture) = loader.load_fixture(&fingerprint) {
1747 tracing::info!(
1748 "[FIXTURE DEBUG] ✅ FIXTURE MATCHED! Using custom fixture for {} {} (status: {}, path: '{}')",
1749 route.method,
1750 route.path,
1751 custom_fixture.status,
1752 custom_fixture.path
1753 );
1754
1755 if custom_fixture.delay_ms > 0 {
1757 tokio::time::sleep(tokio::time::Duration::from_millis(
1758 custom_fixture.delay_ms,
1759 ))
1760 .await;
1761 }
1762
1763 let response_body = if custom_fixture.response.is_string() {
1765 custom_fixture.response.as_str().unwrap().to_string()
1766 } else {
1767 serde_json::to_string(&custom_fixture.response)
1768 .unwrap_or_else(|_| "{}".to_string())
1769 };
1770
1771 let json_value: Value = serde_json::from_str(&response_body)
1773 .unwrap_or_else(|_| serde_json::json!({}));
1774
1775 let status =
1777 axum::http::StatusCode::from_u16(custom_fixture.status)
1778 .unwrap_or(axum::http::StatusCode::OK);
1779
1780 return (status, Json(json_value));
1782 } else {
1783 tracing::warn!(
1784 "[FIXTURE DEBUG] ❌ No fixture match found for {} {} (fingerprint.path='{}', normalized='{}')",
1785 route.method,
1786 route.path,
1787 fingerprint.path,
1788 normalized_request_path
1789 );
1790 }
1791 } else {
1792 tracing::warn!("[FIXTURE DEBUG] Failed to parse URI: '{}'", uri_str);
1793 }
1794 } else {
1795 tracing::warn!(
1796 "[FIXTURE DEBUG] Custom fixture loader not available for {} {}",
1797 route.method,
1798 route.path
1799 );
1800 }
1801
1802 tracing::debug!(
1803 "Handling MockAI request for route: {} {}",
1804 route.method,
1805 route.path
1806 );
1807
1808 let mockai_query = query.0;
1810
1811 let method_upper = route.method.to_uppercase();
1816 let should_use_mockai =
1817 matches!(method_upper.as_str(), "POST" | "PUT" | "PATCH" | "DELETE");
1818
1819 if should_use_mockai {
1820 if let Some(mockai_arc) = mockai {
1821 let mockai_guard = mockai_arc.read().await;
1822
1823 let mut mockai_headers = HashMap::new();
1825 for (k, v) in headers.iter() {
1826 mockai_headers
1827 .insert(k.to_string(), v.to_str().unwrap_or("").to_string());
1828 }
1829
1830 let mockai_request = MockAIRequest {
1831 method: route.method.clone(),
1832 path: route.path.clone(),
1833 body: body.as_ref().map(|Json(b)| b.clone()),
1834 query_params: mockai_query,
1835 headers: mockai_headers,
1836 };
1837
1838 match mockai_guard.process_request(&mockai_request).await {
1840 Ok(mockai_response) => {
1841 let is_empty = mockai_response.body.is_object()
1843 && mockai_response
1844 .body
1845 .as_object()
1846 .map(|obj| obj.is_empty())
1847 .unwrap_or(false);
1848
1849 if is_empty {
1850 tracing::debug!(
1851 "MockAI returned empty object for {} {}, falling back to OpenAPI response generation",
1852 route.method,
1853 route.path
1854 );
1855 } else {
1857 let spec_status = route.find_first_available_status_code();
1861 tracing::debug!(
1862 "MockAI generated response for {} {}, using spec status: {} (MockAI suggested: {})",
1863 route.method,
1864 route.path,
1865 spec_status,
1866 mockai_response.status_code
1867 );
1868 return (
1869 axum::http::StatusCode::from_u16(spec_status)
1870 .unwrap_or(axum::http::StatusCode::OK),
1871 Json(mockai_response.body),
1872 );
1873 }
1874 }
1875 Err(e) => {
1876 tracing::warn!(
1877 "MockAI processing failed for {} {}: {}, falling back to standard response",
1878 route.method,
1879 route.path,
1880 e
1881 );
1882 }
1884 }
1885 }
1886 } else {
1887 tracing::debug!(
1888 "Skipping MockAI for {} request {} - using OpenAPI response generation",
1889 method_upper,
1890 route.path
1891 );
1892 }
1893
1894 let status_override = headers
1896 .get("X-Mockforge-Response-Status")
1897 .and_then(|v| v.to_str().ok())
1898 .and_then(|s| s.parse::<u16>().ok());
1899
1900 let scenario = headers
1902 .get("X-Mockforge-Scenario")
1903 .and_then(|v| v.to_str().ok())
1904 .map(|s| s.to_string())
1905 .or_else(|| std::env::var("MOCKFORGE_HTTP_SCENARIO").ok());
1906
1907 let (status, response) = route
1909 .mock_response_with_status_and_scenario_and_override(
1910 scenario.as_deref(),
1911 status_override,
1912 );
1913 (
1914 axum::http::StatusCode::from_u16(status)
1915 .unwrap_or(axum::http::StatusCode::OK),
1916 Json(response),
1917 )
1918 }
1919 };
1920
1921 router = Self::route_for_method(router, axum_path, &route.method, handler);
1922 }
1923
1924 router.layer(DefaultBodyLimit::max(openapi_body_limit_bytes()))
1927 }
1928}
1929
1930async fn extract_multipart_from_bytes(
1935 body: &axum::body::Bytes,
1936 headers: &HeaderMap,
1937) -> Result<(HashMap<String, Value>, HashMap<String, String>)> {
1938 let boundary = headers
1940 .get(axum::http::header::CONTENT_TYPE)
1941 .and_then(|v| v.to_str().ok())
1942 .and_then(|ct| {
1943 ct.split(';').find_map(|part| {
1944 let part = part.trim();
1945 if part.starts_with("boundary=") {
1946 Some(part.strip_prefix("boundary=").unwrap_or("").trim_matches('"'))
1947 } else {
1948 None
1949 }
1950 })
1951 })
1952 .ok_or_else(|| Error::internal("Missing boundary in Content-Type header"))?;
1953
1954 let mut fields = HashMap::new();
1955 let mut files = HashMap::new();
1956
1957 let boundary_prefix = format!("--{}", boundary).into_bytes();
1960 let boundary_line = format!("\r\n--{}\r\n", boundary).into_bytes();
1961 let end_boundary = format!("\r\n--{}--\r\n", boundary).into_bytes();
1962
1963 let mut pos = 0;
1965 let mut parts = Vec::new();
1966
1967 if body.starts_with(&boundary_prefix) {
1969 if let Some(first_crlf) = body.iter().position(|&b| b == b'\r') {
1970 pos = first_crlf + 2; }
1972 }
1973
1974 while let Some(boundary_pos) = body[pos..]
1976 .windows(boundary_line.len())
1977 .position(|window| window == boundary_line.as_slice())
1978 {
1979 let actual_pos = pos + boundary_pos;
1980 if actual_pos > pos {
1981 parts.push((pos, actual_pos));
1982 }
1983 pos = actual_pos + boundary_line.len();
1984 }
1985
1986 if let Some(end_pos) = body[pos..]
1988 .windows(end_boundary.len())
1989 .position(|window| window == end_boundary.as_slice())
1990 {
1991 let actual_end = pos + end_pos;
1992 if actual_end > pos {
1993 parts.push((pos, actual_end));
1994 }
1995 } else if pos < body.len() {
1996 parts.push((pos, body.len()));
1998 }
1999
2000 for (start, end) in parts {
2002 let part_data = &body[start..end];
2003
2004 let separator = b"\r\n\r\n";
2006 if let Some(sep_pos) =
2007 part_data.windows(separator.len()).position(|window| window == separator)
2008 {
2009 let header_bytes = &part_data[..sep_pos];
2010 let body_start = sep_pos + separator.len();
2011 let body_data = &part_data[body_start..];
2012
2013 let header_str = String::from_utf8_lossy(header_bytes);
2015 let mut field_name = None;
2016 let mut filename = None;
2017
2018 for header_line in header_str.lines() {
2019 if header_line.starts_with("Content-Disposition:") {
2020 if let Some(name_start) = header_line.find("name=\"") {
2022 let name_start = name_start + 6;
2023 if let Some(name_end) = header_line[name_start..].find('"') {
2024 field_name =
2025 Some(header_line[name_start..name_start + name_end].to_string());
2026 }
2027 }
2028
2029 if let Some(file_start) = header_line.find("filename=\"") {
2031 let file_start = file_start + 10;
2032 if let Some(file_end) = header_line[file_start..].find('"') {
2033 filename =
2034 Some(header_line[file_start..file_start + file_end].to_string());
2035 }
2036 }
2037 }
2038 }
2039
2040 if let Some(name) = field_name {
2041 if let Some(file) = filename {
2042 let temp_dir = std::env::temp_dir().join("mockforge-uploads");
2044 std::fs::create_dir_all(&temp_dir)
2045 .map_err(|e| Error::io_with_context("temp directory", e.to_string()))?;
2046
2047 let file_path = temp_dir.join(format!("{}_{}", uuid::Uuid::new_v4(), file));
2048 std::fs::write(&file_path, body_data)
2049 .map_err(|e| Error::io_with_context("file", e.to_string()))?;
2050
2051 let file_path_str = file_path.to_string_lossy().to_string();
2052 files.insert(name.clone(), file_path_str.clone());
2053 fields.insert(name, Value::String(file_path_str));
2054 } else {
2055 let body_str = body_data
2058 .strip_suffix(b"\r\n")
2059 .or_else(|| body_data.strip_suffix(b"\n"))
2060 .unwrap_or(body_data);
2061
2062 if let Ok(field_value) = String::from_utf8(body_str.to_vec()) {
2063 fields.insert(name, Value::String(field_value.trim().to_string()));
2064 } else {
2065 use base64::{engine::general_purpose, Engine as _};
2067 fields.insert(
2068 name,
2069 Value::String(general_purpose::STANDARD.encode(body_str)),
2070 );
2071 }
2072 }
2073 }
2074 }
2075 }
2076
2077 Ok((fields, files))
2078}
2079
2080static LAST_ERRORS: Lazy<Mutex<VecDeque<Value>>> =
2081 Lazy::new(|| Mutex::new(VecDeque::with_capacity(20)));
2082
2083pub fn classify_validation_reason(reason: &str) -> String {
2091 let r = reason.to_ascii_lowercase();
2092 if r.contains("required")
2093 && (r.contains("param") || r.contains("query") || r.contains("header"))
2094 {
2095 return "parameters".into();
2096 }
2097 if r.contains("schema") || r.contains("body") || r.contains("json") {
2098 return "request-body".into();
2099 }
2100 if r.contains("content-type") || r.contains("content type") {
2101 return "content-types".into();
2102 }
2103 if r.contains("header") {
2104 return "headers".into();
2105 }
2106 if r.contains("cookie") {
2107 return "cookies".into();
2108 }
2109 if r.contains("method") {
2110 return "http-methods".into();
2111 }
2112 if r.contains("auth") || r.contains("security") {
2113 return "security".into();
2114 }
2115 if r.contains("enum") || r.contains("min") || r.contains("max") || r.contains("pattern") {
2116 return "constraints".into();
2117 }
2118 String::new()
2119}
2120
2121pub fn record_validation_error(v: &Value) {
2123 if let Ok(mut q) = LAST_ERRORS.lock() {
2124 if q.len() >= 20 {
2125 q.pop_front();
2126 }
2127 q.push_back(v.clone());
2128 }
2129 }
2131
2132pub fn get_last_validation_error() -> Option<Value> {
2134 LAST_ERRORS.lock().ok()?.back().cloned()
2135}
2136
2137pub fn get_validation_errors() -> Vec<Value> {
2139 LAST_ERRORS.lock().map(|q| q.iter().cloned().collect()).unwrap_or_default()
2140}
2141
2142fn coerce_value_for_schema(value: &Value, schema: &openapiv3::Schema) -> Value {
2147 match value {
2149 Value::String(s) => {
2150 if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
2152 &schema.schema_kind
2153 {
2154 if s.contains(',') {
2155 let parts: Vec<&str> = s.split(',').map(|s| s.trim()).collect();
2157 let mut array_values = Vec::new();
2158
2159 for part in parts {
2160 if let Some(items_schema) = &array_type.items {
2162 if let Some(items_schema_obj) = items_schema.as_item() {
2163 let part_value = Value::String(part.to_string());
2164 let coerced_part =
2165 coerce_value_for_schema(&part_value, items_schema_obj);
2166 array_values.push(coerced_part);
2167 } else {
2168 array_values.push(Value::String(part.to_string()));
2170 }
2171 } else {
2172 array_values.push(Value::String(part.to_string()));
2174 }
2175 }
2176 return Value::Array(array_values);
2177 }
2178 }
2179
2180 match &schema.schema_kind {
2182 openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
2183 value.clone()
2185 }
2186 openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => {
2187 if let Ok(n) = s.parse::<f64>() {
2189 if let Some(num) = serde_json::Number::from_f64(n) {
2190 return Value::Number(num);
2191 }
2192 }
2193 value.clone()
2194 }
2195 openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => {
2196 if let Ok(n) = s.parse::<i64>() {
2198 if let Some(num) = serde_json::Number::from_f64(n as f64) {
2199 return Value::Number(num);
2200 }
2201 }
2202 value.clone()
2203 }
2204 openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => {
2205 match s.to_lowercase().as_str() {
2207 "true" | "1" | "yes" | "on" => Value::Bool(true),
2208 "false" | "0" | "no" | "off" => Value::Bool(false),
2209 _ => value.clone(),
2210 }
2211 }
2212 _ => {
2213 value.clone()
2215 }
2216 }
2217 }
2218 _ => value.clone(),
2219 }
2220}
2221
2222fn coerce_by_style(value: &Value, schema: &openapiv3::Schema, style: Option<&str>) -> Value {
2224 match value {
2226 Value::String(s) => {
2227 if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
2229 &schema.schema_kind
2230 {
2231 let delimiter = match style {
2232 Some("spaceDelimited") => " ",
2233 Some("pipeDelimited") => "|",
2234 Some("form") | None => ",", _ => ",", };
2237
2238 if s.contains(delimiter) {
2239 let parts: Vec<&str> = s.split(delimiter).map(|s| s.trim()).collect();
2241 let mut array_values = Vec::new();
2242
2243 for part in parts {
2244 if let Some(items_schema) = &array_type.items {
2246 if let Some(items_schema_obj) = items_schema.as_item() {
2247 let part_value = Value::String(part.to_string());
2248 let coerced_part =
2249 coerce_by_style(&part_value, items_schema_obj, style);
2250 array_values.push(coerced_part);
2251 } else {
2252 array_values.push(Value::String(part.to_string()));
2254 }
2255 } else {
2256 array_values.push(Value::String(part.to_string()));
2258 }
2259 }
2260 return Value::Array(array_values);
2261 }
2262 }
2263
2264 if let Ok(n) = s.parse::<f64>() {
2266 if let Some(num) = serde_json::Number::from_f64(n) {
2267 return Value::Number(num);
2268 }
2269 }
2270 match s.to_lowercase().as_str() {
2272 "true" | "1" | "yes" | "on" => return Value::Bool(true),
2273 "false" | "0" | "no" | "off" => return Value::Bool(false),
2274 _ => {}
2275 }
2276 value.clone()
2278 }
2279 _ => value.clone(),
2280 }
2281}
2282
2283fn build_deep_object(name: &str, params: &Map<String, Value>) -> Option<Value> {
2285 let prefix = format!("{}[", name);
2286 let mut obj = Map::new();
2287 for (k, v) in params.iter() {
2288 if let Some(rest) = k.strip_prefix(&prefix) {
2289 if let Some(key) = rest.strip_suffix(']') {
2290 obj.insert(key.to_string(), v.clone());
2291 }
2292 }
2293 }
2294 if obj.is_empty() {
2295 None
2296 } else {
2297 Some(Value::Object(obj))
2298 }
2299}
2300
2301#[allow(clippy::too_many_arguments)]
2307fn generate_enhanced_422_response(
2308 validator: &OpenApiRouteRegistry,
2309 path_template: &str,
2310 method: &str,
2311 body: Option<&Value>,
2312 path_params: &Map<String, Value>,
2313 query_params: &Map<String, Value>,
2314 header_params: &Map<String, Value>,
2315 cookie_params: &Map<String, Value>,
2316) -> Value {
2317 let mut field_errors = Vec::new();
2318
2319 if let Some(route) = validator.get_route(path_template, method) {
2321 if let Some(schema) = &route.operation.request_body {
2323 if let Some(value) = body {
2324 if let Some(content) =
2325 schema.as_item().and_then(|rb| rb.content.get("application/json"))
2326 {
2327 if let Some(_schema_ref) = &content.schema {
2328 if serde_json::from_value::<Value>(value.clone()).is_err() {
2330 field_errors.push(json!({
2331 "path": "body",
2332 "message": "invalid JSON"
2333 }));
2334 }
2335 }
2336 }
2337 } else {
2338 field_errors.push(json!({
2339 "path": "body",
2340 "expected": "object",
2341 "found": "missing",
2342 "message": "Request body is required but not provided"
2343 }));
2344 }
2345 }
2346
2347 for param_ref in &route.operation.parameters {
2349 if let Some(param) = param_ref.as_item() {
2350 match param {
2351 openapiv3::Parameter::Path { parameter_data, .. } => {
2352 validate_parameter_detailed(
2353 parameter_data,
2354 path_params,
2355 "path",
2356 "path parameter",
2357 &mut field_errors,
2358 );
2359 }
2360 openapiv3::Parameter::Query { parameter_data, .. } => {
2361 let deep_value = if Some("form") == Some("deepObject") {
2362 build_deep_object(¶meter_data.name, query_params)
2363 } else {
2364 None
2365 };
2366 validate_parameter_detailed_with_deep(
2367 parameter_data,
2368 query_params,
2369 "query",
2370 "query parameter",
2371 deep_value,
2372 &mut field_errors,
2373 );
2374 }
2375 openapiv3::Parameter::Header { parameter_data, .. } => {
2376 validate_parameter_detailed(
2377 parameter_data,
2378 header_params,
2379 "header",
2380 "header parameter",
2381 &mut field_errors,
2382 );
2383 }
2384 openapiv3::Parameter::Cookie { parameter_data, .. } => {
2385 validate_parameter_detailed(
2386 parameter_data,
2387 cookie_params,
2388 "cookie",
2389 "cookie parameter",
2390 &mut field_errors,
2391 );
2392 }
2393 }
2394 }
2395 }
2396 }
2397
2398 json!({
2400 "error": "Schema validation failed",
2401 "details": field_errors,
2402 "method": method,
2403 "path": path_template,
2404 "timestamp": Utc::now().to_rfc3339(),
2405 "validation_type": "openapi_schema"
2406 })
2407}
2408
2409fn validate_parameter(
2411 parameter_data: &openapiv3::ParameterData,
2412 params_map: &Map<String, Value>,
2413 prefix: &str,
2414 aggregate: bool,
2415 errors: &mut Vec<String>,
2416 details: &mut Vec<Value>,
2417) {
2418 match params_map.get(¶meter_data.name) {
2419 Some(v) => {
2420 if let ParameterSchemaOrContent::Schema(s) = ¶meter_data.format {
2421 if let Some(schema) = s.as_item() {
2422 let coerced = coerce_value_for_schema(v, schema);
2423 if let Err(validation_error) =
2425 OpenApiSchema::new(schema.clone()).validate(&coerced)
2426 {
2427 let error_msg = validation_error.to_string();
2428 errors.push(format!(
2429 "{} parameter '{}' validation failed: {}",
2430 prefix, parameter_data.name, error_msg
2431 ));
2432 if aggregate {
2433 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
2434 }
2435 }
2436 }
2437 }
2438 }
2439 None => {
2440 if parameter_data.required {
2441 errors.push(format!(
2442 "missing required {} parameter '{}'",
2443 prefix, parameter_data.name
2444 ));
2445 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
2446 }
2447 }
2448 }
2449}
2450
2451#[allow(clippy::too_many_arguments)]
2453fn validate_parameter_with_deep_object(
2454 parameter_data: &openapiv3::ParameterData,
2455 params_map: &Map<String, Value>,
2456 prefix: &str,
2457 deep_value: Option<Value>,
2458 style: Option<&str>,
2459 aggregate: bool,
2460 errors: &mut Vec<String>,
2461 details: &mut Vec<Value>,
2462) {
2463 match deep_value.as_ref().or_else(|| params_map.get(¶meter_data.name)) {
2464 Some(v) => {
2465 if let ParameterSchemaOrContent::Schema(s) = ¶meter_data.format {
2466 if let Some(schema) = s.as_item() {
2467 let coerced = coerce_by_style(v, schema, style); if let Err(validation_error) =
2470 OpenApiSchema::new(schema.clone()).validate(&coerced)
2471 {
2472 let error_msg = validation_error.to_string();
2473 errors.push(format!(
2474 "{} parameter '{}' validation failed: {}",
2475 prefix, parameter_data.name, error_msg
2476 ));
2477 if aggregate {
2478 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
2479 }
2480 }
2481 }
2482 }
2483 }
2484 None => {
2485 if parameter_data.required {
2486 errors.push(format!(
2487 "missing required {} parameter '{}'",
2488 prefix, parameter_data.name
2489 ));
2490 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
2491 }
2492 }
2493 }
2494}
2495
2496fn validate_parameter_detailed(
2498 parameter_data: &openapiv3::ParameterData,
2499 params_map: &Map<String, Value>,
2500 location: &str,
2501 value_type: &str,
2502 field_errors: &mut Vec<Value>,
2503) {
2504 match params_map.get(¶meter_data.name) {
2505 Some(value) => {
2506 if let ParameterSchemaOrContent::Schema(schema) = ¶meter_data.format {
2507 let details: Vec<Value> = Vec::new();
2509 let param_path = format!("{}.{}", location, parameter_data.name);
2510
2511 if let Some(schema_ref) = schema.as_item() {
2513 let coerced_value = coerce_value_for_schema(value, schema_ref);
2514 if let Err(validation_error) =
2516 OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
2517 {
2518 field_errors.push(json!({
2519 "path": param_path,
2520 "expected": "valid according to schema",
2521 "found": coerced_value,
2522 "message": validation_error.to_string()
2523 }));
2524 }
2525 }
2526
2527 for detail in details {
2528 field_errors.push(json!({
2529 "path": detail["path"],
2530 "expected": detail["expected_type"],
2531 "found": detail["value"],
2532 "message": detail["message"]
2533 }));
2534 }
2535 }
2536 }
2537 None => {
2538 if parameter_data.required {
2539 field_errors.push(json!({
2540 "path": format!("{}.{}", location, parameter_data.name),
2541 "expected": "value",
2542 "found": "missing",
2543 "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
2544 }));
2545 }
2546 }
2547 }
2548}
2549
2550fn validate_parameter_detailed_with_deep(
2552 parameter_data: &openapiv3::ParameterData,
2553 params_map: &Map<String, Value>,
2554 location: &str,
2555 value_type: &str,
2556 deep_value: Option<Value>,
2557 field_errors: &mut Vec<Value>,
2558) {
2559 match deep_value.as_ref().or_else(|| params_map.get(¶meter_data.name)) {
2560 Some(value) => {
2561 if let ParameterSchemaOrContent::Schema(schema) = ¶meter_data.format {
2562 let details: Vec<Value> = Vec::new();
2564 let param_path = format!("{}.{}", location, parameter_data.name);
2565
2566 if let Some(schema_ref) = schema.as_item() {
2568 let coerced_value = coerce_by_style(value, schema_ref, Some("form")); if let Err(validation_error) =
2571 OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
2572 {
2573 field_errors.push(json!({
2574 "path": param_path,
2575 "expected": "valid according to schema",
2576 "found": coerced_value,
2577 "message": validation_error.to_string()
2578 }));
2579 }
2580 }
2581
2582 for detail in details {
2583 field_errors.push(json!({
2584 "path": detail["path"],
2585 "expected": detail["expected_type"],
2586 "found": detail["value"],
2587 "message": detail["message"]
2588 }));
2589 }
2590 }
2591 }
2592 None => {
2593 if parameter_data.required {
2594 field_errors.push(json!({
2595 "path": format!("{}.{}", location, parameter_data.name),
2596 "expected": "value",
2597 "found": "missing",
2598 "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
2599 }));
2600 }
2601 }
2602 }
2603}
2604
2605pub async fn create_registry_from_file<P: AsRef<std::path::Path>>(
2607 path: P,
2608) -> Result<OpenApiRouteRegistry> {
2609 let spec = OpenApiSpec::from_file(path).await?;
2610 spec.validate()?;
2611 Ok(OpenApiRouteRegistry::new(spec))
2612}
2613
2614pub fn create_registry_from_json(json: Value) -> Result<OpenApiRouteRegistry> {
2616 let spec = OpenApiSpec::from_json(json)?;
2617 spec.validate()?;
2618 Ok(OpenApiRouteRegistry::new(spec))
2619}
2620
2621#[cfg(test)]
2622mod tests {
2623 use super::*;
2624 use serde_json::json;
2625 use tempfile::TempDir;
2626
2627 #[tokio::test]
2628 async fn test_registry_creation() {
2629 let spec_json = json!({
2630 "openapi": "3.0.0",
2631 "info": {
2632 "title": "Test API",
2633 "version": "1.0.0"
2634 },
2635 "paths": {
2636 "/users": {
2637 "get": {
2638 "summary": "Get users",
2639 "responses": {
2640 "200": {
2641 "description": "Success",
2642 "content": {
2643 "application/json": {
2644 "schema": {
2645 "type": "array",
2646 "items": {
2647 "type": "object",
2648 "properties": {
2649 "id": {"type": "integer"},
2650 "name": {"type": "string"}
2651 }
2652 }
2653 }
2654 }
2655 }
2656 }
2657 }
2658 },
2659 "post": {
2660 "summary": "Create user",
2661 "requestBody": {
2662 "content": {
2663 "application/json": {
2664 "schema": {
2665 "type": "object",
2666 "properties": {
2667 "name": {"type": "string"}
2668 },
2669 "required": ["name"]
2670 }
2671 }
2672 }
2673 },
2674 "responses": {
2675 "201": {
2676 "description": "Created",
2677 "content": {
2678 "application/json": {
2679 "schema": {
2680 "type": "object",
2681 "properties": {
2682 "id": {"type": "integer"},
2683 "name": {"type": "string"}
2684 }
2685 }
2686 }
2687 }
2688 }
2689 }
2690 }
2691 },
2692 "/users/{id}": {
2693 "get": {
2694 "summary": "Get user by ID",
2695 "parameters": [
2696 {
2697 "name": "id",
2698 "in": "path",
2699 "required": true,
2700 "schema": {"type": "integer"}
2701 }
2702 ],
2703 "responses": {
2704 "200": {
2705 "description": "Success",
2706 "content": {
2707 "application/json": {
2708 "schema": {
2709 "type": "object",
2710 "properties": {
2711 "id": {"type": "integer"},
2712 "name": {"type": "string"}
2713 }
2714 }
2715 }
2716 }
2717 }
2718 }
2719 }
2720 }
2721 }
2722 });
2723
2724 let registry = create_registry_from_json(spec_json).unwrap();
2725
2726 assert_eq!(registry.paths().len(), 2);
2728 assert!(registry.paths().contains(&"/users".to_string()));
2729 assert!(registry.paths().contains(&"/users/{id}".to_string()));
2730
2731 assert_eq!(registry.methods().len(), 2);
2732 assert!(registry.methods().contains(&"GET".to_string()));
2733 assert!(registry.methods().contains(&"POST".to_string()));
2734
2735 let get_users_route = registry.get_route("/users", "GET").unwrap();
2737 assert_eq!(get_users_route.method, "GET");
2738 assert_eq!(get_users_route.path, "/users");
2739
2740 let post_users_route = registry.get_route("/users", "POST").unwrap();
2741 assert_eq!(post_users_route.method, "POST");
2742 assert!(post_users_route.operation.request_body.is_some());
2743
2744 let user_by_id_route = registry.get_route("/users/{id}", "GET").unwrap();
2746 assert_eq!(user_by_id_route.axum_path(), "/users/{id}");
2747 }
2748
2749 #[tokio::test]
2750 async fn test_validate_request_with_params_and_formats() {
2751 let spec_json = json!({
2752 "openapi": "3.0.0",
2753 "info": { "title": "Test API", "version": "1.0.0" },
2754 "paths": {
2755 "/users/{id}": {
2756 "post": {
2757 "parameters": [
2758 { "name": "id", "in": "path", "required": true, "schema": {"type": "string"} },
2759 { "name": "q", "in": "query", "required": false, "schema": {"type": "integer"} }
2760 ],
2761 "requestBody": {
2762 "content": {
2763 "application/json": {
2764 "schema": {
2765 "type": "object",
2766 "required": ["email", "website"],
2767 "properties": {
2768 "email": {"type": "string", "format": "email"},
2769 "website": {"type": "string", "format": "uri"}
2770 }
2771 }
2772 }
2773 }
2774 },
2775 "responses": {"200": {"description": "ok"}}
2776 }
2777 }
2778 }
2779 });
2780
2781 let registry = create_registry_from_json(spec_json).unwrap();
2782 let mut path_params = Map::new();
2783 path_params.insert("id".to_string(), json!("abc"));
2784 let mut query_params = Map::new();
2785 query_params.insert("q".to_string(), json!(123));
2786
2787 let body = json!({"email":"a@b.co","website":"https://example.com"});
2789 assert!(registry
2790 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2791 .is_ok());
2792
2793 let bad_email = json!({"email":"not-an-email","website":"https://example.com"});
2795 assert!(registry
2796 .validate_request_with(
2797 "/users/{id}",
2798 "POST",
2799 &path_params,
2800 &query_params,
2801 Some(&bad_email)
2802 )
2803 .is_err());
2804
2805 let empty_path_params = Map::new();
2807 assert!(registry
2808 .validate_request_with(
2809 "/users/{id}",
2810 "POST",
2811 &empty_path_params,
2812 &query_params,
2813 Some(&body)
2814 )
2815 .is_err());
2816 }
2817
2818 #[tokio::test]
2819 async fn test_ref_resolution_for_params_and_body() {
2820 let spec_json = json!({
2821 "openapi": "3.0.0",
2822 "info": { "title": "Ref API", "version": "1.0.0" },
2823 "components": {
2824 "schemas": {
2825 "EmailWebsite": {
2826 "type": "object",
2827 "required": ["email", "website"],
2828 "properties": {
2829 "email": {"type": "string", "format": "email"},
2830 "website": {"type": "string", "format": "uri"}
2831 }
2832 }
2833 },
2834 "parameters": {
2835 "PathId": {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}},
2836 "QueryQ": {"name": "q", "in": "query", "required": false, "schema": {"type": "integer"}}
2837 },
2838 "requestBodies": {
2839 "CreateUser": {
2840 "content": {
2841 "application/json": {
2842 "schema": {"$ref": "#/components/schemas/EmailWebsite"}
2843 }
2844 }
2845 }
2846 }
2847 },
2848 "paths": {
2849 "/users/{id}": {
2850 "post": {
2851 "parameters": [
2852 {"$ref": "#/components/parameters/PathId"},
2853 {"$ref": "#/components/parameters/QueryQ"}
2854 ],
2855 "requestBody": {"$ref": "#/components/requestBodies/CreateUser"},
2856 "responses": {"200": {"description": "ok"}}
2857 }
2858 }
2859 }
2860 });
2861
2862 let registry = create_registry_from_json(spec_json).unwrap();
2863 let mut path_params = Map::new();
2864 path_params.insert("id".to_string(), json!("abc"));
2865 let mut query_params = Map::new();
2866 query_params.insert("q".to_string(), json!(7));
2867
2868 let body = json!({"email":"user@example.com","website":"https://example.com"});
2869 assert!(registry
2870 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2871 .is_ok());
2872
2873 let bad = json!({"email":"nope","website":"https://example.com"});
2874 assert!(registry
2875 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&bad))
2876 .is_err());
2877 }
2878
2879 #[tokio::test]
2880 async fn test_header_cookie_and_query_coercion() {
2881 let spec_json = json!({
2882 "openapi": "3.0.0",
2883 "info": { "title": "Params API", "version": "1.0.0" },
2884 "paths": {
2885 "/items": {
2886 "get": {
2887 "parameters": [
2888 {"name": "X-Flag", "in": "header", "required": true, "schema": {"type": "boolean"}},
2889 {"name": "session", "in": "cookie", "required": true, "schema": {"type": "string"}},
2890 {"name": "ids", "in": "query", "required": false, "schema": {"type": "array", "items": {"type": "integer"}}}
2891 ],
2892 "responses": {"200": {"description": "ok"}}
2893 }
2894 }
2895 }
2896 });
2897
2898 let registry = create_registry_from_json(spec_json).unwrap();
2899
2900 let path_params = Map::new();
2901 let mut query_params = Map::new();
2902 query_params.insert("ids".to_string(), json!("1,2,3"));
2904 let mut header_params = Map::new();
2905 header_params.insert("X-Flag".to_string(), json!("true"));
2906 let mut cookie_params = Map::new();
2907 cookie_params.insert("session".to_string(), json!("abc123"));
2908
2909 assert!(registry
2910 .validate_request_with_all(
2911 "/items",
2912 "GET",
2913 &path_params,
2914 &query_params,
2915 &header_params,
2916 &cookie_params,
2917 None
2918 )
2919 .is_ok());
2920
2921 let empty_cookie = Map::new();
2923 assert!(registry
2924 .validate_request_with_all(
2925 "/items",
2926 "GET",
2927 &path_params,
2928 &query_params,
2929 &header_params,
2930 &empty_cookie,
2931 None
2932 )
2933 .is_err());
2934
2935 let mut bad_header = Map::new();
2937 bad_header.insert("X-Flag".to_string(), json!("notabool"));
2938 assert!(registry
2939 .validate_request_with_all(
2940 "/items",
2941 "GET",
2942 &path_params,
2943 &query_params,
2944 &bad_header,
2945 &cookie_params,
2946 None
2947 )
2948 .is_err());
2949 }
2950
2951 #[tokio::test]
2952 async fn test_query_styles_space_pipe_deepobject() {
2953 let spec_json = json!({
2954 "openapi": "3.0.0",
2955 "info": { "title": "Query Styles API", "version": "1.0.0" },
2956 "paths": {"/search": {"get": {
2957 "parameters": [
2958 {"name":"tags","in":"query","style":"spaceDelimited","schema":{"type":"array","items":{"type":"string"}}},
2959 {"name":"ids","in":"query","style":"pipeDelimited","schema":{"type":"array","items":{"type":"integer"}}},
2960 {"name":"filter","in":"query","style":"deepObject","schema":{"type":"object","properties":{"color":{"type":"string"}},"required":["color"]}}
2961 ],
2962 "responses": {"200": {"description":"ok"}}
2963 }} }
2964 });
2965
2966 let registry = create_registry_from_json(spec_json).unwrap();
2967
2968 let path_params = Map::new();
2969 let mut query = Map::new();
2970 query.insert("tags".into(), json!("alpha beta gamma"));
2971 query.insert("ids".into(), json!("1|2|3"));
2972 query.insert("filter[color]".into(), json!("red"));
2973
2974 assert!(registry
2975 .validate_request_with("/search", "GET", &path_params, &query, None)
2976 .is_ok());
2977 }
2978
2979 #[tokio::test]
2980 async fn test_oneof_anyof_allof_validation() {
2981 let spec_json = json!({
2982 "openapi": "3.0.0",
2983 "info": { "title": "Composite API", "version": "1.0.0" },
2984 "paths": {
2985 "/composite": {
2986 "post": {
2987 "requestBody": {
2988 "content": {
2989 "application/json": {
2990 "schema": {
2991 "allOf": [
2992 {"type": "object", "required": ["base"], "properties": {"base": {"type": "string"}}}
2993 ],
2994 "oneOf": [
2995 {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"], "not": {"required": ["b"]}},
2996 {"type": "object", "properties": {"b": {"type": "integer"}}, "required": ["b"], "not": {"required": ["a"]}}
2997 ],
2998 "anyOf": [
2999 {"type": "object", "properties": {"flag": {"type": "boolean"}}, "required": ["flag"]},
3000 {"type": "object", "properties": {"extra": {"type": "string"}}, "required": ["extra"]}
3001 ]
3002 }
3003 }
3004 }
3005 },
3006 "responses": {"200": {"description": "ok"}}
3007 }
3008 }
3009 }
3010 });
3011
3012 let registry = create_registry_from_json(spec_json).unwrap();
3013 let ok = json!({"base": "x", "a": 1, "flag": true});
3015 assert!(registry
3016 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&ok))
3017 .is_ok());
3018
3019 let bad_oneof = json!({"base": "x", "a": 1, "b": 2, "flag": false});
3021 assert!(registry
3022 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_oneof))
3023 .is_err());
3024
3025 let bad_anyof = json!({"base": "x", "a": 1});
3027 assert!(registry
3028 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_anyof))
3029 .is_err());
3030
3031 let bad_allof = json!({"a": 1, "flag": true});
3033 assert!(registry
3034 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_allof))
3035 .is_err());
3036 }
3037
3038 #[tokio::test]
3039 async fn test_overrides_warn_mode_allows_invalid() {
3040 let spec_json = json!({
3042 "openapi": "3.0.0",
3043 "info": { "title": "Overrides API", "version": "1.0.0" },
3044 "paths": {"/things": {"post": {
3045 "parameters": [{"name":"q","in":"query","required":true,"schema":{"type":"integer"}}],
3046 "responses": {"200": {"description":"ok"}}
3047 }}}
3048 });
3049
3050 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3051 let mut overrides = HashMap::new();
3052 overrides.insert("POST /things".to_string(), ValidationMode::Warn);
3053 let registry = OpenApiRouteRegistry::new_with_options(
3054 spec,
3055 ValidationOptions {
3056 request_mode: ValidationMode::Enforce,
3057 aggregate_errors: true,
3058 validate_responses: false,
3059 overrides,
3060 admin_skip_prefixes: vec![],
3061 response_template_expand: false,
3062 validation_status: None,
3063 },
3064 );
3065
3066 let ok = registry.validate_request_with("/things", "POST", &Map::new(), &Map::new(), None);
3068 assert!(ok.is_ok());
3069 }
3070
3071 #[tokio::test]
3072 async fn test_admin_skip_prefix_short_circuit() {
3073 let spec_json = json!({
3074 "openapi": "3.0.0",
3075 "info": { "title": "Skip API", "version": "1.0.0" },
3076 "paths": {}
3077 });
3078 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3079 let registry = OpenApiRouteRegistry::new_with_options(
3080 spec,
3081 ValidationOptions {
3082 request_mode: ValidationMode::Enforce,
3083 aggregate_errors: true,
3084 validate_responses: false,
3085 overrides: HashMap::new(),
3086 admin_skip_prefixes: vec!["/admin".into()],
3087 response_template_expand: false,
3088 validation_status: None,
3089 },
3090 );
3091
3092 let res = registry.validate_request_with_all(
3094 "/admin/__mockforge/health",
3095 "GET",
3096 &Map::new(),
3097 &Map::new(),
3098 &Map::new(),
3099 &Map::new(),
3100 None,
3101 );
3102 assert!(res.is_ok());
3103 }
3104
3105 #[test]
3106 fn test_path_conversion() {
3107 assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users"), "/users");
3108 assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users/{id}"), "/users/{id}");
3109 assert_eq!(
3110 OpenApiRouteRegistry::convert_path_to_axum("/users/{id}/posts/{postId}"),
3111 "/users/{id}/posts/{postId}"
3112 );
3113 }
3114
3115 #[test]
3116 fn test_validation_options_default() {
3117 let options = ValidationOptions::default();
3118 assert!(matches!(options.request_mode, ValidationMode::Enforce));
3119 assert!(options.aggregate_errors);
3120 assert!(!options.validate_responses);
3121 assert!(options.overrides.is_empty());
3122 assert!(options.admin_skip_prefixes.is_empty());
3123 assert!(!options.response_template_expand);
3124 assert!(options.validation_status.is_none());
3125 }
3126
3127 #[test]
3128 fn test_validation_mode_variants() {
3129 let disabled = ValidationMode::Disabled;
3131 let warn = ValidationMode::Warn;
3132 let enforce = ValidationMode::Enforce;
3133 let default = ValidationMode::default();
3134
3135 assert!(matches!(default, ValidationMode::Warn));
3137
3138 assert!(!matches!(disabled, ValidationMode::Warn));
3140 assert!(!matches!(warn, ValidationMode::Enforce));
3141 assert!(!matches!(enforce, ValidationMode::Disabled));
3142 }
3143
3144 #[test]
3145 fn test_registry_spec_accessor() {
3146 let spec_json = json!({
3147 "openapi": "3.0.0",
3148 "info": {
3149 "title": "Test API",
3150 "version": "1.0.0"
3151 },
3152 "paths": {}
3153 });
3154 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3155 let registry = OpenApiRouteRegistry::new(spec.clone());
3156
3157 let accessed_spec = registry.spec();
3159 assert_eq!(accessed_spec.title(), "Test API");
3160 }
3161
3162 #[test]
3163 fn test_clone_for_validation() {
3164 let spec_json = json!({
3165 "openapi": "3.0.0",
3166 "info": {
3167 "title": "Test API",
3168 "version": "1.0.0"
3169 },
3170 "paths": {
3171 "/users": {
3172 "get": {
3173 "responses": {
3174 "200": {
3175 "description": "Success"
3176 }
3177 }
3178 }
3179 }
3180 }
3181 });
3182 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3183 let registry = OpenApiRouteRegistry::new(spec);
3184
3185 let cloned = registry.clone_for_validation();
3187 assert_eq!(cloned.routes().len(), registry.routes().len());
3188 assert_eq!(cloned.spec().title(), registry.spec().title());
3189 }
3190
3191 #[test]
3192 fn test_with_custom_fixture_loader() {
3193 let temp_dir = TempDir::new().unwrap();
3194 let spec_json = json!({
3195 "openapi": "3.0.0",
3196 "info": {
3197 "title": "Test API",
3198 "version": "1.0.0"
3199 },
3200 "paths": {}
3201 });
3202 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3203 let registry = OpenApiRouteRegistry::new(spec);
3204 let original_routes_len = registry.routes().len();
3205
3206 let custom_loader = Arc::new(crate::custom_fixture::CustomFixtureLoader::new(
3208 temp_dir.path().to_path_buf(),
3209 true,
3210 ));
3211 let registry_with_loader = registry.with_custom_fixture_loader(custom_loader);
3212
3213 assert_eq!(registry_with_loader.routes().len(), original_routes_len);
3215 }
3216
3217 #[test]
3218 fn test_get_route() {
3219 let spec_json = json!({
3220 "openapi": "3.0.0",
3221 "info": {
3222 "title": "Test API",
3223 "version": "1.0.0"
3224 },
3225 "paths": {
3226 "/users": {
3227 "get": {
3228 "operationId": "getUsers",
3229 "responses": {
3230 "200": {
3231 "description": "Success"
3232 }
3233 }
3234 },
3235 "post": {
3236 "operationId": "createUser",
3237 "responses": {
3238 "201": {
3239 "description": "Created"
3240 }
3241 }
3242 }
3243 }
3244 }
3245 });
3246 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3247 let registry = OpenApiRouteRegistry::new(spec);
3248
3249 let route = registry.get_route("/users", "GET");
3251 assert!(route.is_some());
3252 assert_eq!(route.unwrap().method, "GET");
3253 assert_eq!(route.unwrap().path, "/users");
3254
3255 let route = registry.get_route("/nonexistent", "GET");
3257 assert!(route.is_none());
3258
3259 let route = registry.get_route("/users", "POST");
3261 assert!(route.is_some());
3262 assert_eq!(route.unwrap().method, "POST");
3263 }
3264
3265 #[test]
3266 fn test_get_routes_for_path() {
3267 let spec_json = json!({
3268 "openapi": "3.0.0",
3269 "info": {
3270 "title": "Test API",
3271 "version": "1.0.0"
3272 },
3273 "paths": {
3274 "/users": {
3275 "get": {
3276 "responses": {
3277 "200": {
3278 "description": "Success"
3279 }
3280 }
3281 },
3282 "post": {
3283 "responses": {
3284 "201": {
3285 "description": "Created"
3286 }
3287 }
3288 },
3289 "put": {
3290 "responses": {
3291 "200": {
3292 "description": "Success"
3293 }
3294 }
3295 }
3296 },
3297 "/posts": {
3298 "get": {
3299 "responses": {
3300 "200": {
3301 "description": "Success"
3302 }
3303 }
3304 }
3305 }
3306 }
3307 });
3308 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3309 let registry = OpenApiRouteRegistry::new(spec);
3310
3311 let routes = registry.get_routes_for_path("/users");
3313 assert_eq!(routes.len(), 3);
3314 let methods: Vec<&str> = routes.iter().map(|r| r.method.as_str()).collect();
3315 assert!(methods.contains(&"GET"));
3316 assert!(methods.contains(&"POST"));
3317 assert!(methods.contains(&"PUT"));
3318
3319 let routes = registry.get_routes_for_path("/posts");
3321 assert_eq!(routes.len(), 1);
3322 assert_eq!(routes[0].method, "GET");
3323
3324 let routes = registry.get_routes_for_path("/nonexistent");
3326 assert!(routes.is_empty());
3327 }
3328
3329 #[test]
3330 fn test_new_vs_new_with_options() {
3331 let spec_json = json!({
3332 "openapi": "3.0.0",
3333 "info": {
3334 "title": "Test API",
3335 "version": "1.0.0"
3336 },
3337 "paths": {}
3338 });
3339 let spec1 = OpenApiSpec::from_json(spec_json.clone()).unwrap();
3340 let spec2 = OpenApiSpec::from_json(spec_json).unwrap();
3341
3342 let registry1 = OpenApiRouteRegistry::new(spec1);
3344 assert_eq!(registry1.spec().title(), "Test API");
3345
3346 let options = ValidationOptions {
3348 request_mode: ValidationMode::Disabled,
3349 aggregate_errors: false,
3350 validate_responses: true,
3351 overrides: HashMap::new(),
3352 admin_skip_prefixes: vec!["/admin".to_string()],
3353 response_template_expand: true,
3354 validation_status: Some(422),
3355 };
3356 let registry2 = OpenApiRouteRegistry::new_with_options(spec2, options);
3357 assert_eq!(registry2.spec().title(), "Test API");
3358 }
3359
3360 #[test]
3361 fn test_new_with_env_vs_new() {
3362 let spec_json = json!({
3363 "openapi": "3.0.0",
3364 "info": {
3365 "title": "Test API",
3366 "version": "1.0.0"
3367 },
3368 "paths": {}
3369 });
3370 let spec1 = OpenApiSpec::from_json(spec_json.clone()).unwrap();
3371 let spec2 = OpenApiSpec::from_json(spec_json).unwrap();
3372
3373 let registry1 = OpenApiRouteRegistry::new(spec1);
3375
3376 let registry2 = OpenApiRouteRegistry::new_with_env(spec2);
3378
3379 assert_eq!(registry1.spec().title(), "Test API");
3381 assert_eq!(registry2.spec().title(), "Test API");
3382 }
3383
3384 #[test]
3385 fn test_validation_options_custom() {
3386 let options = ValidationOptions {
3387 request_mode: ValidationMode::Warn,
3388 aggregate_errors: false,
3389 validate_responses: true,
3390 overrides: {
3391 let mut map = HashMap::new();
3392 map.insert("getUsers".to_string(), ValidationMode::Disabled);
3393 map
3394 },
3395 admin_skip_prefixes: vec!["/admin".to_string(), "/internal".to_string()],
3396 response_template_expand: true,
3397 validation_status: Some(422),
3398 };
3399
3400 assert!(matches!(options.request_mode, ValidationMode::Warn));
3401 assert!(!options.aggregate_errors);
3402 assert!(options.validate_responses);
3403 assert_eq!(options.overrides.len(), 1);
3404 assert_eq!(options.admin_skip_prefixes.len(), 2);
3405 assert!(options.response_template_expand);
3406 assert_eq!(options.validation_status, Some(422));
3407 }
3408
3409 #[test]
3410 fn test_validation_mode_default_standalone() {
3411 let mode = ValidationMode::default();
3412 assert!(matches!(mode, ValidationMode::Warn));
3413 }
3414
3415 #[test]
3416 fn test_validation_mode_clone() {
3417 let mode1 = ValidationMode::Enforce;
3418 let mode2 = mode1.clone();
3419 assert!(matches!(mode1, ValidationMode::Enforce));
3420 assert!(matches!(mode2, ValidationMode::Enforce));
3421 }
3422
3423 #[test]
3424 fn test_validation_mode_debug() {
3425 let mode = ValidationMode::Disabled;
3426 let debug_str = format!("{:?}", mode);
3427 assert!(debug_str.contains("Disabled") || debug_str.contains("ValidationMode"));
3428 }
3429
3430 #[test]
3431 fn test_validation_options_clone() {
3432 let options1 = ValidationOptions {
3433 request_mode: ValidationMode::Warn,
3434 aggregate_errors: true,
3435 validate_responses: false,
3436 overrides: HashMap::new(),
3437 admin_skip_prefixes: vec![],
3438 response_template_expand: false,
3439 validation_status: None,
3440 };
3441 let options2 = options1.clone();
3442 assert!(matches!(options2.request_mode, ValidationMode::Warn));
3443 assert_eq!(options1.aggregate_errors, options2.aggregate_errors);
3444 }
3445
3446 #[test]
3447 fn test_validation_options_debug() {
3448 let options = ValidationOptions::default();
3449 let debug_str = format!("{:?}", options);
3450 assert!(debug_str.contains("ValidationOptions"));
3451 }
3452
3453 #[test]
3454 fn test_validation_options_with_all_fields() {
3455 let mut overrides = HashMap::new();
3456 overrides.insert("op1".to_string(), ValidationMode::Disabled);
3457 overrides.insert("op2".to_string(), ValidationMode::Warn);
3458
3459 let options = ValidationOptions {
3460 request_mode: ValidationMode::Enforce,
3461 aggregate_errors: false,
3462 validate_responses: true,
3463 overrides: overrides.clone(),
3464 admin_skip_prefixes: vec!["/admin".to_string(), "/internal".to_string()],
3465 response_template_expand: true,
3466 validation_status: Some(422),
3467 };
3468
3469 assert!(matches!(options.request_mode, ValidationMode::Enforce));
3470 assert!(!options.aggregate_errors);
3471 assert!(options.validate_responses);
3472 assert_eq!(options.overrides.len(), 2);
3473 assert_eq!(options.admin_skip_prefixes.len(), 2);
3474 assert!(options.response_template_expand);
3475 assert_eq!(options.validation_status, Some(422));
3476 }
3477
3478 #[test]
3479 fn test_openapi_route_registry_clone() {
3480 let spec_json = json!({
3481 "openapi": "3.0.0",
3482 "info": { "title": "Test API", "version": "1.0.0" },
3483 "paths": {}
3484 });
3485 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3486 let registry1 = OpenApiRouteRegistry::new(spec);
3487 let registry2 = registry1.clone();
3488 assert_eq!(registry1.spec().title(), registry2.spec().title());
3489 }
3490
3491 #[test]
3492 fn test_validation_mode_serialization() {
3493 let mode = ValidationMode::Enforce;
3494 let json = serde_json::to_string(&mode).unwrap();
3495 assert!(json.contains("Enforce") || json.contains("enforce"));
3496 }
3497
3498 #[test]
3499 fn test_validation_mode_deserialization() {
3500 let json = r#""Disabled""#;
3501 let mode: ValidationMode = serde_json::from_str(json).unwrap();
3502 assert!(matches!(mode, ValidationMode::Disabled));
3503 }
3504
3505 #[test]
3506 fn test_validation_options_default_values() {
3507 let options = ValidationOptions::default();
3508 assert!(matches!(options.request_mode, ValidationMode::Enforce));
3509 assert!(options.aggregate_errors);
3510 assert!(!options.validate_responses);
3511 assert!(options.overrides.is_empty());
3512 assert!(options.admin_skip_prefixes.is_empty());
3513 assert!(!options.response_template_expand);
3514 assert_eq!(options.validation_status, None);
3515 }
3516
3517 #[test]
3518 fn test_validation_mode_all_variants() {
3519 let disabled = ValidationMode::Disabled;
3520 let warn = ValidationMode::Warn;
3521 let enforce = ValidationMode::Enforce;
3522
3523 assert!(matches!(disabled, ValidationMode::Disabled));
3524 assert!(matches!(warn, ValidationMode::Warn));
3525 assert!(matches!(enforce, ValidationMode::Enforce));
3526 }
3527
3528 #[test]
3529 fn test_validation_options_with_overrides() {
3530 let mut overrides = HashMap::new();
3531 overrides.insert("operation1".to_string(), ValidationMode::Disabled);
3532 overrides.insert("operation2".to_string(), ValidationMode::Warn);
3533
3534 let options = ValidationOptions {
3535 request_mode: ValidationMode::Enforce,
3536 aggregate_errors: true,
3537 validate_responses: false,
3538 overrides,
3539 admin_skip_prefixes: vec![],
3540 response_template_expand: false,
3541 validation_status: None,
3542 };
3543
3544 assert_eq!(options.overrides.len(), 2);
3545 assert!(matches!(options.overrides.get("operation1"), Some(ValidationMode::Disabled)));
3546 assert!(matches!(options.overrides.get("operation2"), Some(ValidationMode::Warn)));
3547 }
3548
3549 #[test]
3550 fn test_validation_options_with_admin_skip_prefixes() {
3551 let options = ValidationOptions {
3552 request_mode: ValidationMode::Enforce,
3553 aggregate_errors: true,
3554 validate_responses: false,
3555 overrides: HashMap::new(),
3556 admin_skip_prefixes: vec![
3557 "/admin".to_string(),
3558 "/internal".to_string(),
3559 "/debug".to_string(),
3560 ],
3561 response_template_expand: false,
3562 validation_status: None,
3563 };
3564
3565 assert_eq!(options.admin_skip_prefixes.len(), 3);
3566 assert!(options.admin_skip_prefixes.contains(&"/admin".to_string()));
3567 assert!(options.admin_skip_prefixes.contains(&"/internal".to_string()));
3568 assert!(options.admin_skip_prefixes.contains(&"/debug".to_string()));
3569 }
3570
3571 #[test]
3572 fn test_validation_options_with_validation_status() {
3573 let options1 = ValidationOptions {
3574 request_mode: ValidationMode::Enforce,
3575 aggregate_errors: true,
3576 validate_responses: false,
3577 overrides: HashMap::new(),
3578 admin_skip_prefixes: vec![],
3579 response_template_expand: false,
3580 validation_status: Some(400),
3581 };
3582
3583 let options2 = ValidationOptions {
3584 request_mode: ValidationMode::Enforce,
3585 aggregate_errors: true,
3586 validate_responses: false,
3587 overrides: HashMap::new(),
3588 admin_skip_prefixes: vec![],
3589 response_template_expand: false,
3590 validation_status: Some(422),
3591 };
3592
3593 assert_eq!(options1.validation_status, Some(400));
3594 assert_eq!(options2.validation_status, Some(422));
3595 }
3596
3597 #[test]
3598 fn test_validate_request_with_disabled_mode() {
3599 let spec_json = json!({
3601 "openapi": "3.0.0",
3602 "info": {"title": "Test API", "version": "1.0.0"},
3603 "paths": {
3604 "/users": {
3605 "get": {
3606 "responses": {"200": {"description": "OK"}}
3607 }
3608 }
3609 }
3610 });
3611 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3612 let options = ValidationOptions {
3613 request_mode: ValidationMode::Disabled,
3614 ..Default::default()
3615 };
3616 let registry = OpenApiRouteRegistry::new_with_options(spec, options);
3617
3618 let result = registry.validate_request_with_all(
3620 "/users",
3621 "GET",
3622 &Map::new(),
3623 &Map::new(),
3624 &Map::new(),
3625 &Map::new(),
3626 None,
3627 );
3628 assert!(result.is_ok());
3629 }
3630
3631 #[test]
3632 fn test_validate_request_with_warn_mode() {
3633 let spec_json = json!({
3635 "openapi": "3.0.0",
3636 "info": {"title": "Test API", "version": "1.0.0"},
3637 "paths": {
3638 "/users": {
3639 "post": {
3640 "requestBody": {
3641 "required": true,
3642 "content": {
3643 "application/json": {
3644 "schema": {
3645 "type": "object",
3646 "required": ["name"],
3647 "properties": {
3648 "name": {"type": "string"}
3649 }
3650 }
3651 }
3652 }
3653 },
3654 "responses": {"200": {"description": "OK"}}
3655 }
3656 }
3657 }
3658 });
3659 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3660 let options = ValidationOptions {
3661 request_mode: ValidationMode::Warn,
3662 ..Default::default()
3663 };
3664 let registry = OpenApiRouteRegistry::new_with_options(spec, options);
3665
3666 let result = registry.validate_request_with_all(
3668 "/users",
3669 "POST",
3670 &Map::new(),
3671 &Map::new(),
3672 &Map::new(),
3673 &Map::new(),
3674 None, );
3676 assert!(result.is_ok()); }
3678
3679 #[test]
3680 fn test_validate_request_body_validation_error() {
3681 let spec_json = json!({
3683 "openapi": "3.0.0",
3684 "info": {"title": "Test API", "version": "1.0.0"},
3685 "paths": {
3686 "/users": {
3687 "post": {
3688 "requestBody": {
3689 "required": true,
3690 "content": {
3691 "application/json": {
3692 "schema": {
3693 "type": "object",
3694 "required": ["name"],
3695 "properties": {
3696 "name": {"type": "string"}
3697 }
3698 }
3699 }
3700 }
3701 },
3702 "responses": {"200": {"description": "OK"}}
3703 }
3704 }
3705 }
3706 });
3707 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3708 let registry = OpenApiRouteRegistry::new(spec);
3709
3710 let result = registry.validate_request_with_all(
3712 "/users",
3713 "POST",
3714 &Map::new(),
3715 &Map::new(),
3716 &Map::new(),
3717 &Map::new(),
3718 None, );
3720 assert!(result.is_err());
3721 }
3722
3723 #[test]
3724 fn test_validate_request_body_schema_validation_error() {
3725 let spec_json = json!({
3727 "openapi": "3.0.0",
3728 "info": {"title": "Test API", "version": "1.0.0"},
3729 "paths": {
3730 "/users": {
3731 "post": {
3732 "requestBody": {
3733 "required": true,
3734 "content": {
3735 "application/json": {
3736 "schema": {
3737 "type": "object",
3738 "required": ["name"],
3739 "properties": {
3740 "name": {"type": "string"}
3741 }
3742 }
3743 }
3744 }
3745 },
3746 "responses": {"200": {"description": "OK"}}
3747 }
3748 }
3749 }
3750 });
3751 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3752 let registry = OpenApiRouteRegistry::new(spec);
3753
3754 let invalid_body = json!({}); let result = registry.validate_request_with_all(
3757 "/users",
3758 "POST",
3759 &Map::new(),
3760 &Map::new(),
3761 &Map::new(),
3762 &Map::new(),
3763 Some(&invalid_body),
3764 );
3765 assert!(result.is_err());
3766 }
3767
3768 #[test]
3769 fn test_validate_request_body_referenced_schema_error() {
3770 let spec_json = json!({
3772 "openapi": "3.0.0",
3773 "info": {"title": "Test API", "version": "1.0.0"},
3774 "paths": {
3775 "/users": {
3776 "post": {
3777 "requestBody": {
3778 "required": true,
3779 "content": {
3780 "application/json": {
3781 "schema": {
3782 "$ref": "#/components/schemas/NonExistentSchema"
3783 }
3784 }
3785 }
3786 },
3787 "responses": {"200": {"description": "OK"}}
3788 }
3789 }
3790 },
3791 "components": {}
3792 });
3793 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3794 let registry = OpenApiRouteRegistry::new(spec);
3795
3796 let body = json!({"name": "test"});
3798 let result = registry.validate_request_with_all(
3799 "/users",
3800 "POST",
3801 &Map::new(),
3802 &Map::new(),
3803 &Map::new(),
3804 &Map::new(),
3805 Some(&body),
3806 );
3807 assert!(result.is_err());
3808 }
3809
3810 #[test]
3811 fn test_validate_request_body_referenced_request_body_error() {
3812 let spec_json = json!({
3814 "openapi": "3.0.0",
3815 "info": {"title": "Test API", "version": "1.0.0"},
3816 "paths": {
3817 "/users": {
3818 "post": {
3819 "requestBody": {
3820 "$ref": "#/components/requestBodies/NonExistentRequestBody"
3821 },
3822 "responses": {"200": {"description": "OK"}}
3823 }
3824 }
3825 },
3826 "components": {}
3827 });
3828 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3829 let registry = OpenApiRouteRegistry::new(spec);
3830
3831 let body = json!({"name": "test"});
3833 let result = registry.validate_request_with_all(
3834 "/users",
3835 "POST",
3836 &Map::new(),
3837 &Map::new(),
3838 &Map::new(),
3839 &Map::new(),
3840 Some(&body),
3841 );
3842 assert!(result.is_err());
3843 }
3844
3845 #[test]
3846 fn test_validate_request_body_provided_when_not_expected() {
3847 let spec_json = json!({
3849 "openapi": "3.0.0",
3850 "info": {"title": "Test API", "version": "1.0.0"},
3851 "paths": {
3852 "/users": {
3853 "get": {
3854 "responses": {"200": {"description": "OK"}}
3855 }
3856 }
3857 }
3858 });
3859 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3860 let registry = OpenApiRouteRegistry::new(spec);
3861
3862 let body = json!({"extra": "data"});
3864 let result = registry.validate_request_with_all(
3865 "/users",
3866 "GET",
3867 &Map::new(),
3868 &Map::new(),
3869 &Map::new(),
3870 &Map::new(),
3871 Some(&body),
3872 );
3873 assert!(result.is_ok());
3875 }
3876
3877 #[test]
3878 fn test_get_operation() {
3879 let spec_json = json!({
3881 "openapi": "3.0.0",
3882 "info": {"title": "Test API", "version": "1.0.0"},
3883 "paths": {
3884 "/users": {
3885 "get": {
3886 "operationId": "getUsers",
3887 "summary": "Get users",
3888 "responses": {"200": {"description": "OK"}}
3889 }
3890 }
3891 }
3892 });
3893 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3894 let registry = OpenApiRouteRegistry::new(spec);
3895
3896 let operation = registry.get_operation("/users", "GET");
3898 assert!(operation.is_some());
3899 assert_eq!(operation.unwrap().method, "GET");
3900
3901 assert!(registry.get_operation("/nonexistent", "GET").is_none());
3903 }
3904
3905 #[test]
3906 fn test_extract_path_parameters() {
3907 let spec_json = json!({
3909 "openapi": "3.0.0",
3910 "info": {"title": "Test API", "version": "1.0.0"},
3911 "paths": {
3912 "/users/{id}": {
3913 "get": {
3914 "parameters": [
3915 {
3916 "name": "id",
3917 "in": "path",
3918 "required": true,
3919 "schema": {"type": "string"}
3920 }
3921 ],
3922 "responses": {"200": {"description": "OK"}}
3923 }
3924 }
3925 }
3926 });
3927 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3928 let registry = OpenApiRouteRegistry::new(spec);
3929
3930 let params = registry.extract_path_parameters("/users/123", "GET");
3932 assert_eq!(params.get("id"), Some(&"123".to_string()));
3933
3934 let empty_params = registry.extract_path_parameters("/users", "GET");
3936 assert!(empty_params.is_empty());
3937 }
3938
3939 #[test]
3940 fn test_extract_path_parameters_multiple_params() {
3941 let spec_json = json!({
3943 "openapi": "3.0.0",
3944 "info": {"title": "Test API", "version": "1.0.0"},
3945 "paths": {
3946 "/users/{userId}/posts/{postId}": {
3947 "get": {
3948 "parameters": [
3949 {
3950 "name": "userId",
3951 "in": "path",
3952 "required": true,
3953 "schema": {"type": "string"}
3954 },
3955 {
3956 "name": "postId",
3957 "in": "path",
3958 "required": true,
3959 "schema": {"type": "string"}
3960 }
3961 ],
3962 "responses": {"200": {"description": "OK"}}
3963 }
3964 }
3965 }
3966 });
3967 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3968 let registry = OpenApiRouteRegistry::new(spec);
3969
3970 let params = registry.extract_path_parameters("/users/123/posts/456", "GET");
3972 assert_eq!(params.get("userId"), Some(&"123".to_string()));
3973 assert_eq!(params.get("postId"), Some(&"456".to_string()));
3974 }
3975
3976 #[test]
3977 fn test_validate_request_route_not_found() {
3978 let spec_json = json!({
3980 "openapi": "3.0.0",
3981 "info": {"title": "Test API", "version": "1.0.0"},
3982 "paths": {
3983 "/users": {
3984 "get": {
3985 "responses": {"200": {"description": "OK"}}
3986 }
3987 }
3988 }
3989 });
3990 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3991 let registry = OpenApiRouteRegistry::new(spec);
3992
3993 let result = registry.validate_request_with_all(
3995 "/nonexistent",
3996 "GET",
3997 &Map::new(),
3998 &Map::new(),
3999 &Map::new(),
4000 &Map::new(),
4001 None,
4002 );
4003 assert!(result.is_err());
4004 assert!(result.unwrap_err().to_string().contains("not found"));
4005 }
4006
4007 #[test]
4008 fn test_validate_request_with_path_parameters() {
4009 let spec_json = json!({
4011 "openapi": "3.0.0",
4012 "info": {"title": "Test API", "version": "1.0.0"},
4013 "paths": {
4014 "/users/{id}": {
4015 "get": {
4016 "parameters": [
4017 {
4018 "name": "id",
4019 "in": "path",
4020 "required": true,
4021 "schema": {"type": "string", "minLength": 1}
4022 }
4023 ],
4024 "responses": {"200": {"description": "OK"}}
4025 }
4026 }
4027 }
4028 });
4029 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4030 let registry = OpenApiRouteRegistry::new(spec);
4031
4032 let mut path_params = Map::new();
4034 path_params.insert("id".to_string(), json!("123"));
4035 let result = registry.validate_request_with_all(
4036 "/users/{id}",
4037 "GET",
4038 &path_params,
4039 &Map::new(),
4040 &Map::new(),
4041 &Map::new(),
4042 None,
4043 );
4044 assert!(result.is_ok());
4045 }
4046
4047 #[test]
4048 fn test_validate_request_with_query_parameters() {
4049 let spec_json = json!({
4051 "openapi": "3.0.0",
4052 "info": {"title": "Test API", "version": "1.0.0"},
4053 "paths": {
4054 "/users": {
4055 "get": {
4056 "parameters": [
4057 {
4058 "name": "page",
4059 "in": "query",
4060 "required": true,
4061 "schema": {"type": "integer", "minimum": 1}
4062 }
4063 ],
4064 "responses": {"200": {"description": "OK"}}
4065 }
4066 }
4067 }
4068 });
4069 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4070 let registry = OpenApiRouteRegistry::new(spec);
4071
4072 let mut query_params = Map::new();
4074 query_params.insert("page".to_string(), json!(1));
4075 let result = registry.validate_request_with_all(
4076 "/users",
4077 "GET",
4078 &Map::new(),
4079 &query_params,
4080 &Map::new(),
4081 &Map::new(),
4082 None,
4083 );
4084 assert!(result.is_ok());
4085 }
4086
4087 #[test]
4088 fn test_validate_request_with_header_parameters() {
4089 let spec_json = json!({
4091 "openapi": "3.0.0",
4092 "info": {"title": "Test API", "version": "1.0.0"},
4093 "paths": {
4094 "/users": {
4095 "get": {
4096 "parameters": [
4097 {
4098 "name": "X-API-Key",
4099 "in": "header",
4100 "required": true,
4101 "schema": {"type": "string"}
4102 }
4103 ],
4104 "responses": {"200": {"description": "OK"}}
4105 }
4106 }
4107 }
4108 });
4109 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4110 let registry = OpenApiRouteRegistry::new(spec);
4111
4112 let mut header_params = Map::new();
4114 header_params.insert("X-API-Key".to_string(), json!("secret-key"));
4115 let result = registry.validate_request_with_all(
4116 "/users",
4117 "GET",
4118 &Map::new(),
4119 &Map::new(),
4120 &header_params,
4121 &Map::new(),
4122 None,
4123 );
4124 assert!(result.is_ok());
4125 }
4126
4127 #[test]
4128 fn test_validate_request_with_cookie_parameters() {
4129 let spec_json = json!({
4131 "openapi": "3.0.0",
4132 "info": {"title": "Test API", "version": "1.0.0"},
4133 "paths": {
4134 "/users": {
4135 "get": {
4136 "parameters": [
4137 {
4138 "name": "sessionId",
4139 "in": "cookie",
4140 "required": true,
4141 "schema": {"type": "string"}
4142 }
4143 ],
4144 "responses": {"200": {"description": "OK"}}
4145 }
4146 }
4147 }
4148 });
4149 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4150 let registry = OpenApiRouteRegistry::new(spec);
4151
4152 let mut cookie_params = Map::new();
4154 cookie_params.insert("sessionId".to_string(), json!("abc123"));
4155 let result = registry.validate_request_with_all(
4156 "/users",
4157 "GET",
4158 &Map::new(),
4159 &Map::new(),
4160 &Map::new(),
4161 &cookie_params,
4162 None,
4163 );
4164 assert!(result.is_ok());
4165 }
4166
4167 #[test]
4168 fn test_validate_request_no_errors_early_return() {
4169 let spec_json = json!({
4171 "openapi": "3.0.0",
4172 "info": {"title": "Test API", "version": "1.0.0"},
4173 "paths": {
4174 "/users": {
4175 "get": {
4176 "responses": {"200": {"description": "OK"}}
4177 }
4178 }
4179 }
4180 });
4181 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4182 let registry = OpenApiRouteRegistry::new(spec);
4183
4184 let result = registry.validate_request_with_all(
4186 "/users",
4187 "GET",
4188 &Map::new(),
4189 &Map::new(),
4190 &Map::new(),
4191 &Map::new(),
4192 None,
4193 );
4194 assert!(result.is_ok());
4195 }
4196
4197 #[test]
4198 fn test_validate_request_query_parameter_different_styles() {
4199 let spec_json = json!({
4201 "openapi": "3.0.0",
4202 "info": {"title": "Test API", "version": "1.0.0"},
4203 "paths": {
4204 "/users": {
4205 "get": {
4206 "parameters": [
4207 {
4208 "name": "tags",
4209 "in": "query",
4210 "style": "pipeDelimited",
4211 "schema": {
4212 "type": "array",
4213 "items": {"type": "string"}
4214 }
4215 }
4216 ],
4217 "responses": {"200": {"description": "OK"}}
4218 }
4219 }
4220 }
4221 });
4222 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4223 let registry = OpenApiRouteRegistry::new(spec);
4224
4225 let mut query_params = Map::new();
4227 query_params.insert("tags".to_string(), json!(["tag1", "tag2"]));
4228 let result = registry.validate_request_with_all(
4229 "/users",
4230 "GET",
4231 &Map::new(),
4232 &query_params,
4233 &Map::new(),
4234 &Map::new(),
4235 None,
4236 );
4237 assert!(result.is_ok() || result.is_err()); }
4240}