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