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