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 match schema_ref {
1247 openapiv3::ReferenceOr::Item(schema) => {
1248 if let Err(validation_error) =
1250 OpenApiSchema::new(schema.clone()).validate(value)
1251 {
1252 let error_msg = validation_error.to_string();
1253 errors.push(format!(
1254 "body validation failed: {}",
1255 error_msg
1256 ));
1257 if aggregate {
1258 details.push(serde_json::json!({"path":"body","code":"schema_validation","message":error_msg}));
1259 }
1260 }
1261 }
1262 openapiv3::ReferenceOr::Reference { reference } => {
1263 if let Some(resolved_schema_ref) =
1265 self.spec.get_schema(reference)
1266 {
1267 if let Err(validation_error) = OpenApiSchema::new(
1268 resolved_schema_ref.schema.clone(),
1269 )
1270 .validate(value)
1271 {
1272 let error_msg = validation_error.to_string();
1273 errors.push(format!(
1274 "body validation failed: {}",
1275 error_msg
1276 ));
1277 if aggregate {
1278 details.push(serde_json::json!({"path":"body","code":"schema_validation","message":error_msg}));
1279 }
1280 }
1281 } else {
1282 errors.push(format!("body validation failed: could not resolve schema reference {}", reference));
1284 if aggregate {
1285 details.push(serde_json::json!({"path":"body","code":"reference_error","message":"Could not resolve schema reference"}));
1286 }
1287 }
1288 }
1289 }
1290 }
1291 }
1292 } else {
1293 errors.push("body validation failed: could not resolve request body or no application/json content".to_string());
1295 if aggregate {
1296 details.push(serde_json::json!({"path":"body","code":"reference_error","message":"Could not resolve request body reference"}));
1297 }
1298 }
1299 } else {
1300 errors.push("body: Request body is required but not provided".to_string());
1301 details.push(serde_json::json!({"path":"body","code":"required","message":"Request body is required"}));
1302 }
1303 } else if body.is_some() {
1304 tracing::debug!("Body provided for operation without requestBody; accepting");
1306 }
1307
1308 for p_ref in &route.operation.parameters {
1310 if let Some(p) = p_ref.as_item() {
1311 match p {
1312 openapiv3::Parameter::Path { parameter_data, .. } => {
1313 validate_parameter(
1314 parameter_data,
1315 path_params,
1316 "path",
1317 aggregate,
1318 &mut errors,
1319 &mut details,
1320 );
1321 }
1322 openapiv3::Parameter::Query {
1323 parameter_data,
1324 style,
1325 ..
1326 } => {
1327 let deep_value = if matches!(style, openapiv3::QueryStyle::DeepObject) {
1330 let prefix_bracket = format!("{}[", parameter_data.name);
1331 let mut obj = Map::new();
1332 for (key, val) in query_params.iter() {
1333 if let Some(rest) = key.strip_prefix(&prefix_bracket) {
1334 if let Some(prop) = rest.strip_suffix(']') {
1335 obj.insert(prop.to_string(), val.clone());
1336 }
1337 }
1338 }
1339 if obj.is_empty() {
1340 None
1341 } else {
1342 Some(Value::Object(obj))
1343 }
1344 } else {
1345 None
1346 };
1347 let style_str = match style {
1348 openapiv3::QueryStyle::Form => Some("form"),
1349 openapiv3::QueryStyle::SpaceDelimited => Some("spaceDelimited"),
1350 openapiv3::QueryStyle::PipeDelimited => Some("pipeDelimited"),
1351 openapiv3::QueryStyle::DeepObject => Some("deepObject"),
1352 };
1353 validate_parameter_with_deep_object(
1354 parameter_data,
1355 query_params,
1356 "query",
1357 deep_value,
1358 style_str,
1359 aggregate,
1360 &mut errors,
1361 &mut details,
1362 );
1363 }
1364 openapiv3::Parameter::Header { parameter_data, .. } => {
1365 validate_parameter(
1366 parameter_data,
1367 header_params,
1368 "header",
1369 aggregate,
1370 &mut errors,
1371 &mut details,
1372 );
1373 }
1374 openapiv3::Parameter::Cookie { parameter_data, .. } => {
1375 validate_parameter(
1376 parameter_data,
1377 cookie_params,
1378 "cookie",
1379 aggregate,
1380 &mut errors,
1381 &mut details,
1382 );
1383 }
1384 }
1385 }
1386 }
1387 if errors.is_empty() {
1388 return Ok(());
1389 }
1390 match effective_mode {
1391 ValidationMode::Disabled => Ok(()),
1392 ValidationMode::Warn => {
1393 tracing::warn!("Request validation warnings: {:?}", errors);
1394 Ok(())
1395 }
1396 ValidationMode::Enforce => Err(Error::validation(
1397 serde_json::json!({"errors": errors, "details": details}).to_string(),
1398 )),
1399 }
1400 } else {
1401 Err(Error::internal(format!("Route {} {} not found in OpenAPI spec", method, path)))
1402 }
1403 }
1404
1405 pub fn paths(&self) -> Vec<String> {
1409 let mut paths: Vec<String> = self.routes.iter().map(|route| route.path.clone()).collect();
1410 paths.sort();
1411 paths.dedup();
1412 paths
1413 }
1414
1415 pub fn methods(&self) -> Vec<String> {
1417 let mut methods: Vec<String> =
1418 self.routes.iter().map(|route| route.method.clone()).collect();
1419 methods.sort();
1420 methods.dedup();
1421 methods
1422 }
1423
1424 pub fn get_operation(&self, path: &str, method: &str) -> Option<OpenApiOperation> {
1426 self.get_route(path, method).map(|route| {
1427 OpenApiOperation::from_operation(
1428 &route.method,
1429 route.path.clone(),
1430 &route.operation,
1431 &self.spec,
1432 )
1433 })
1434 }
1435
1436 pub fn extract_path_parameters(&self, path: &str, method: &str) -> HashMap<String, String> {
1438 let mut best: Option<(usize, HashMap<String, String>)> = None;
1444 for route in &self.routes {
1445 if route.method != method {
1446 continue;
1447 }
1448
1449 if let Some(params) = self.match_path_to_route(path, &route.path) {
1450 let static_segments = route
1451 .path
1452 .trim_start_matches('/')
1453 .split('/')
1454 .filter(|s| !(s.starts_with('{') && s.ends_with('}')))
1455 .count();
1456 let is_more_specific = match &best {
1457 None => true,
1458 Some((score, _)) => static_segments > *score,
1459 };
1460 if is_more_specific {
1461 best = Some((static_segments, params));
1462 }
1463 }
1464 }
1465 best.map(|(_, params)| params).unwrap_or_default()
1466 }
1467
1468 fn match_path_to_route(
1470 &self,
1471 request_path: &str,
1472 route_pattern: &str,
1473 ) -> Option<HashMap<String, String>> {
1474 let mut params = HashMap::new();
1475
1476 let request_segments: Vec<&str> = request_path.trim_start_matches('/').split('/').collect();
1478 let pattern_segments: Vec<&str> =
1479 route_pattern.trim_start_matches('/').split('/').collect();
1480
1481 if request_segments.len() != pattern_segments.len() {
1482 return None;
1483 }
1484
1485 for (req_seg, pat_seg) in request_segments.iter().zip(pattern_segments.iter()) {
1486 if pat_seg.starts_with('{') && pat_seg.ends_with('}') {
1487 if req_seg.is_empty() {
1492 return None;
1493 }
1494 let param_name = &pat_seg[1..pat_seg.len() - 1];
1495 params.insert(param_name.to_string(), req_seg.to_string());
1496 } else if req_seg != pat_seg {
1497 return None;
1499 }
1500 }
1501
1502 Some(params)
1503 }
1504
1505 pub fn convert_path_to_axum(openapi_path: &str) -> String {
1508 openapi_path.to_string()
1510 }
1511
1512 pub fn build_router_with_ai(
1514 &self,
1515 ai_generator: Option<Arc<dyn AiGenerator + Send + Sync>>,
1516 ) -> Router {
1517 let mut router = Router::new();
1518 let deduped = self.deduplicated_routes();
1519 tracing::debug!("Building router with AI support from {} routes", self.routes.len());
1520
1521 let validator = Arc::new(self.clone_for_validation());
1525 for (axum_path, route) in &deduped {
1526 tracing::debug!("Adding AI-enabled route: {} {}", route.method, route.path);
1527
1528 let route_clone = (*route).clone();
1529 let ai_generator_clone = ai_generator.clone();
1530 let validator_clone = validator.clone();
1534
1535 let handler = move |AxumPath(path_params): AxumPath<HashMap<String, String>>,
1537 axum::extract::Query(query_params): axum::extract::Query<
1538 HashMap<String, String>,
1539 >,
1540 headers: HeaderMap,
1541 body: Option<Json<Value>>| {
1542 let route = route_clone.clone();
1543 let ai_generator = ai_generator_clone.clone();
1544 let validator = validator_clone.clone();
1545
1546 async move {
1547 let mut path_map = Map::new();
1552 for (k, v) in &path_params {
1553 path_map.insert(k.clone(), Value::String(v.clone()));
1554 }
1555 let mut query_map = Map::new();
1556 for (k, v) in &query_params {
1557 query_map.insert(k.clone(), Value::String(v.clone()));
1558 }
1559 let mut header_map = Map::new();
1560 for (k, v) in headers.iter() {
1561 if let Ok(s) = v.to_str() {
1562 header_map.insert(k.to_string(), Value::String(s.to_string()));
1563 }
1564 }
1565 let body_val: Option<&Value> = body.as_ref().map(|Json(b)| b);
1566 if let Err((status_code, payload)) = validator.run_validation_with_recording(
1567 &route.path,
1568 &route.method,
1569 &path_map,
1570 &query_map,
1571 &header_map,
1572 &Map::new(),
1573 body_val,
1574 ) {
1575 let status = axum::http::StatusCode::from_u16(status_code)
1576 .unwrap_or(axum::http::StatusCode::BAD_REQUEST);
1577 return (status, Json(payload));
1578 }
1579
1580 tracing::debug!(
1581 "Handling AI request for route: {} {}",
1582 route.method,
1583 route.path
1584 );
1585
1586 let mut context = RequestContext::new(route.method.clone(), route.path.clone());
1588
1589 context.headers = headers
1591 .iter()
1592 .map(|(k, v)| {
1593 (k.to_string(), Value::String(v.to_str().unwrap_or("").to_string()))
1594 })
1595 .collect();
1596
1597 context.body = body.map(|Json(b)| b);
1599
1600 let (status, response) = if let (Some(generator), Some(_ai_config)) =
1602 (ai_generator, &route.ai_config)
1603 {
1604 route
1605 .mock_response_with_status_async(&context, Some(generator.as_ref()))
1606 .await
1607 } else {
1608 route.mock_response_with_status()
1610 };
1611
1612 (
1613 axum::http::StatusCode::from_u16(status)
1614 .unwrap_or(axum::http::StatusCode::OK),
1615 Json(response),
1616 )
1617 }
1618 };
1619
1620 router = Self::route_for_method(router, axum_path, &route.method, handler);
1621 }
1622
1623 router.layer(DefaultBodyLimit::max(openapi_body_limit_bytes()))
1627 }
1628
1629 pub fn build_router_with_mockai(
1640 &self,
1641 mockai: Option<
1642 Arc<
1643 tokio::sync::RwLock<
1644 dyn mockforge_foundation::intelligent_behavior::MockAiBehavior + Send + Sync,
1645 >,
1646 >,
1647 >,
1648 ) -> Router {
1649 use mockforge_foundation::intelligent_behavior::Request as MockAIRequest;
1650
1651 let mut router = Router::new();
1652 let deduped = self.deduplicated_routes();
1653 tracing::debug!("Building router with MockAI support from {} routes", self.routes.len());
1654
1655 let custom_loader = self.custom_fixture_loader.clone();
1656 let validator = Arc::new(self.clone_for_validation());
1660 for (axum_path, route) in &deduped {
1661 tracing::debug!("Adding MockAI-enabled route: {} {}", route.method, route.path);
1662
1663 let route_clone = (*route).clone();
1664 let mockai_clone = mockai.clone();
1665 let custom_loader_clone = custom_loader.clone();
1666 let validator_clone = validator.clone();
1672
1673 let handler = move |AxumPath(path_params): AxumPath<HashMap<String, String>>,
1677 query: axum::extract::Query<HashMap<String, String>>,
1678 headers: HeaderMap,
1679 body: Option<Json<Value>>| {
1680 let route = route_clone.clone();
1681 let mockai = mockai_clone.clone();
1682 let validator = validator_clone.clone();
1683
1684 async move {
1685 let mut path_map = Map::new();
1690 for (k, v) in &path_params {
1691 path_map.insert(k.clone(), Value::String(v.clone()));
1692 }
1693 let mut query_map = Map::new();
1694 for (k, v) in &query.0 {
1695 query_map.insert(k.clone(), Value::String(v.clone()));
1696 }
1697 let mut header_map = Map::new();
1698 for (k, v) in headers.iter() {
1699 if let Ok(s) = v.to_str() {
1700 header_map.insert(k.to_string(), Value::String(s.to_string()));
1701 }
1702 }
1703 let body_val: Option<&Value> = body.as_ref().map(|Json(b)| b);
1704 if let Err((status_code, payload)) = validator.run_validation_with_recording(
1705 &route.path,
1706 &route.method,
1707 &path_map,
1708 &query_map,
1709 &header_map,
1710 &Map::new(),
1711 body_val,
1712 ) {
1713 let status = axum::http::StatusCode::from_u16(status_code)
1714 .unwrap_or(axum::http::StatusCode::BAD_REQUEST);
1715 return (status, Json(payload));
1716 }
1717
1718 tracing::info!(
1719 "[FIXTURE DEBUG] Starting fixture check for {} {} (custom_loader available: {})",
1720 route.method,
1721 route.path,
1722 custom_loader_clone.is_some()
1723 );
1724
1725 if let Some(ref loader) = custom_loader_clone {
1727 use crate::request_fingerprint::RequestFingerprint;
1728 use axum::http::{Method, Uri};
1729
1730 let query_string = if query.0.is_empty() {
1732 String::new()
1733 } else {
1734 query
1735 .0
1736 .iter()
1737 .map(|(k, v)| format!("{}={}", k, v))
1738 .collect::<Vec<_>>()
1739 .join("&")
1740 };
1741
1742 let normalized_request_path =
1744 crate::custom_fixture::CustomFixtureLoader::normalize_path(&route.path);
1745
1746 tracing::info!(
1747 "[FIXTURE DEBUG] Path normalization: original='{}', normalized='{}'",
1748 route.path,
1749 normalized_request_path
1750 );
1751
1752 let uri_str = if query_string.is_empty() {
1754 normalized_request_path.clone()
1755 } else {
1756 format!("{}?{}", normalized_request_path, query_string)
1757 };
1758
1759 tracing::info!(
1760 "[FIXTURE DEBUG] URI construction: uri_str='{}', query_string='{}'",
1761 uri_str,
1762 query_string
1763 );
1764
1765 if let Ok(uri) = uri_str.parse::<Uri>() {
1766 let http_method =
1767 Method::from_bytes(route.method.as_bytes()).unwrap_or(Method::GET);
1768
1769 let body_bytes =
1771 body.as_ref().and_then(|Json(b)| serde_json::to_vec(b).ok());
1772 let body_slice = body_bytes.as_deref();
1773
1774 let fingerprint =
1775 RequestFingerprint::new(http_method, &uri, &headers, body_slice);
1776
1777 tracing::info!(
1778 "[FIXTURE DEBUG] RequestFingerprint created: method='{}', path='{}', query='{}', body_hash={:?}",
1779 fingerprint.method,
1780 fingerprint.path,
1781 fingerprint.query,
1782 fingerprint.body_hash
1783 );
1784
1785 let available_fixtures = loader.has_fixture(&fingerprint);
1787 tracing::info!(
1788 "[FIXTURE DEBUG] Fixture check result: has_fixture={}",
1789 available_fixtures
1790 );
1791
1792 if let Some(custom_fixture) = loader.load_fixture(&fingerprint) {
1793 tracing::info!(
1794 "[FIXTURE DEBUG] ✅ FIXTURE MATCHED! Using custom fixture for {} {} (status: {}, path: '{}')",
1795 route.method,
1796 route.path,
1797 custom_fixture.status,
1798 custom_fixture.path
1799 );
1800
1801 if custom_fixture.delay_ms > 0 {
1803 tokio::time::sleep(tokio::time::Duration::from_millis(
1804 custom_fixture.delay_ms,
1805 ))
1806 .await;
1807 }
1808
1809 let response_body = if custom_fixture.response.is_string() {
1811 custom_fixture.response.as_str().unwrap().to_string()
1812 } else {
1813 serde_json::to_string(&custom_fixture.response)
1814 .unwrap_or_else(|_| "{}".to_string())
1815 };
1816
1817 let json_value: Value = serde_json::from_str(&response_body)
1819 .unwrap_or_else(|_| serde_json::json!({}));
1820
1821 let status =
1823 axum::http::StatusCode::from_u16(custom_fixture.status)
1824 .unwrap_or(axum::http::StatusCode::OK);
1825
1826 return (status, Json(json_value));
1828 } else {
1829 tracing::warn!(
1830 "[FIXTURE DEBUG] ❌ No fixture match found for {} {} (fingerprint.path='{}', normalized='{}')",
1831 route.method,
1832 route.path,
1833 fingerprint.path,
1834 normalized_request_path
1835 );
1836 }
1837 } else {
1838 tracing::warn!("[FIXTURE DEBUG] Failed to parse URI: '{}'", uri_str);
1839 }
1840 } else {
1841 tracing::warn!(
1842 "[FIXTURE DEBUG] Custom fixture loader not available for {} {}",
1843 route.method,
1844 route.path
1845 );
1846 }
1847
1848 tracing::debug!(
1849 "Handling MockAI request for route: {} {}",
1850 route.method,
1851 route.path
1852 );
1853
1854 let mockai_query = query.0;
1856
1857 let method_upper = route.method.to_uppercase();
1862 let should_use_mockai =
1863 matches!(method_upper.as_str(), "POST" | "PUT" | "PATCH" | "DELETE");
1864
1865 if should_use_mockai {
1866 if let Some(mockai_arc) = mockai {
1867 let mockai_guard = mockai_arc.read().await;
1868
1869 let mut mockai_headers = HashMap::new();
1871 for (k, v) in headers.iter() {
1872 mockai_headers
1873 .insert(k.to_string(), v.to_str().unwrap_or("").to_string());
1874 }
1875
1876 let mockai_request = MockAIRequest {
1877 method: route.method.clone(),
1878 path: route.path.clone(),
1879 body: body.as_ref().map(|Json(b)| b.clone()),
1880 query_params: mockai_query,
1881 headers: mockai_headers,
1882 };
1883
1884 match mockai_guard.process_request(&mockai_request).await {
1886 Ok(mockai_response) => {
1887 let is_empty = mockai_response.body.is_object()
1889 && mockai_response
1890 .body
1891 .as_object()
1892 .map(|obj| obj.is_empty())
1893 .unwrap_or(false);
1894
1895 if is_empty {
1896 tracing::debug!(
1897 "MockAI returned empty object for {} {}, falling back to OpenAPI response generation",
1898 route.method,
1899 route.path
1900 );
1901 } else {
1903 let spec_status = route.find_first_available_status_code();
1907 tracing::debug!(
1908 "MockAI generated response for {} {}, using spec status: {} (MockAI suggested: {})",
1909 route.method,
1910 route.path,
1911 spec_status,
1912 mockai_response.status_code
1913 );
1914 return (
1915 axum::http::StatusCode::from_u16(spec_status)
1916 .unwrap_or(axum::http::StatusCode::OK),
1917 Json(mockai_response.body),
1918 );
1919 }
1920 }
1921 Err(e) => {
1922 tracing::warn!(
1923 "MockAI processing failed for {} {}: {}, falling back to standard response",
1924 route.method,
1925 route.path,
1926 e
1927 );
1928 }
1930 }
1931 }
1932 } else {
1933 tracing::debug!(
1934 "Skipping MockAI for {} request {} - using OpenAPI response generation",
1935 method_upper,
1936 route.path
1937 );
1938 }
1939
1940 let status_override = headers
1942 .get("X-Mockforge-Response-Status")
1943 .and_then(|v| v.to_str().ok())
1944 .and_then(|s| s.parse::<u16>().ok());
1945
1946 let scenario = headers
1948 .get("X-Mockforge-Scenario")
1949 .and_then(|v| v.to_str().ok())
1950 .map(|s| s.to_string())
1951 .or_else(|| std::env::var("MOCKFORGE_HTTP_SCENARIO").ok());
1952
1953 let (status, response) = route
1955 .mock_response_with_status_and_scenario_and_override(
1956 scenario.as_deref(),
1957 status_override,
1958 );
1959 (
1960 axum::http::StatusCode::from_u16(status)
1961 .unwrap_or(axum::http::StatusCode::OK),
1962 Json(response),
1963 )
1964 }
1965 };
1966
1967 router = Self::route_for_method(router, axum_path, &route.method, handler);
1968 }
1969
1970 router.layer(DefaultBodyLimit::max(openapi_body_limit_bytes()))
1973 }
1974}
1975
1976async fn extract_multipart_from_bytes(
1981 body: &axum::body::Bytes,
1982 headers: &HeaderMap,
1983) -> Result<(HashMap<String, Value>, HashMap<String, String>)> {
1984 let boundary = headers
1986 .get(axum::http::header::CONTENT_TYPE)
1987 .and_then(|v| v.to_str().ok())
1988 .and_then(|ct| {
1989 ct.split(';').find_map(|part| {
1990 let part = part.trim();
1991 if part.starts_with("boundary=") {
1992 Some(part.strip_prefix("boundary=").unwrap_or("").trim_matches('"'))
1993 } else {
1994 None
1995 }
1996 })
1997 })
1998 .ok_or_else(|| Error::internal("Missing boundary in Content-Type header"))?;
1999
2000 let mut fields = HashMap::new();
2001 let mut files = HashMap::new();
2002
2003 let boundary_prefix = format!("--{}", boundary).into_bytes();
2006 let boundary_line = format!("\r\n--{}\r\n", boundary).into_bytes();
2007 let end_boundary = format!("\r\n--{}--\r\n", boundary).into_bytes();
2008
2009 let mut pos = 0;
2011 let mut parts = Vec::new();
2012
2013 if body.starts_with(&boundary_prefix) {
2015 if let Some(first_crlf) = body.iter().position(|&b| b == b'\r') {
2016 pos = first_crlf + 2; }
2018 }
2019
2020 while let Some(boundary_pos) = body[pos..]
2022 .windows(boundary_line.len())
2023 .position(|window| window == boundary_line.as_slice())
2024 {
2025 let actual_pos = pos + boundary_pos;
2026 if actual_pos > pos {
2027 parts.push((pos, actual_pos));
2028 }
2029 pos = actual_pos + boundary_line.len();
2030 }
2031
2032 if let Some(end_pos) = body[pos..]
2034 .windows(end_boundary.len())
2035 .position(|window| window == end_boundary.as_slice())
2036 {
2037 let actual_end = pos + end_pos;
2038 if actual_end > pos {
2039 parts.push((pos, actual_end));
2040 }
2041 } else if pos < body.len() {
2042 parts.push((pos, body.len()));
2044 }
2045
2046 for (start, end) in parts {
2048 let part_data = &body[start..end];
2049
2050 let separator = b"\r\n\r\n";
2052 if let Some(sep_pos) =
2053 part_data.windows(separator.len()).position(|window| window == separator)
2054 {
2055 let header_bytes = &part_data[..sep_pos];
2056 let body_start = sep_pos + separator.len();
2057 let body_data = &part_data[body_start..];
2058
2059 let header_str = String::from_utf8_lossy(header_bytes);
2061 let mut field_name = None;
2062 let mut filename = None;
2063
2064 for header_line in header_str.lines() {
2065 if header_line.starts_with("Content-Disposition:") {
2066 if let Some(name_start) = header_line.find("name=\"") {
2068 let name_start = name_start + 6;
2069 if let Some(name_end) = header_line[name_start..].find('"') {
2070 field_name =
2071 Some(header_line[name_start..name_start + name_end].to_string());
2072 }
2073 }
2074
2075 if let Some(file_start) = header_line.find("filename=\"") {
2077 let file_start = file_start + 10;
2078 if let Some(file_end) = header_line[file_start..].find('"') {
2079 filename =
2080 Some(header_line[file_start..file_start + file_end].to_string());
2081 }
2082 }
2083 }
2084 }
2085
2086 if let Some(name) = field_name {
2087 if let Some(file) = filename {
2088 let temp_dir = std::env::temp_dir().join("mockforge-uploads");
2090 std::fs::create_dir_all(&temp_dir)
2091 .map_err(|e| Error::io_with_context("temp directory", e.to_string()))?;
2092
2093 let file_path = temp_dir.join(format!("{}_{}", uuid::Uuid::new_v4(), file));
2094 std::fs::write(&file_path, body_data)
2095 .map_err(|e| Error::io_with_context("file", e.to_string()))?;
2096
2097 let file_path_str = file_path.to_string_lossy().to_string();
2098 files.insert(name.clone(), file_path_str.clone());
2099 fields.insert(name, Value::String(file_path_str));
2100 } else {
2101 let body_str = body_data
2104 .strip_suffix(b"\r\n")
2105 .or_else(|| body_data.strip_suffix(b"\n"))
2106 .unwrap_or(body_data);
2107
2108 if let Ok(field_value) = String::from_utf8(body_str.to_vec()) {
2109 fields.insert(name, Value::String(field_value.trim().to_string()));
2110 } else {
2111 use base64::{engine::general_purpose, Engine as _};
2113 fields.insert(
2114 name,
2115 Value::String(general_purpose::STANDARD.encode(body_str)),
2116 );
2117 }
2118 }
2119 }
2120 }
2121 }
2122
2123 Ok((fields, files))
2124}
2125
2126static LAST_ERRORS: Lazy<Mutex<VecDeque<Value>>> =
2127 Lazy::new(|| Mutex::new(VecDeque::with_capacity(20)));
2128
2129pub fn classify_validation_reason(reason: &str) -> String {
2137 let r = reason.to_ascii_lowercase();
2138 if r.contains("required")
2139 && (r.contains("param") || r.contains("query") || r.contains("header"))
2140 {
2141 return "parameters".into();
2142 }
2143 if r.contains("schema") || r.contains("body") || r.contains("json") {
2144 return "request-body".into();
2145 }
2146 if r.contains("content-type") || r.contains("content type") {
2147 return "content-types".into();
2148 }
2149 if r.contains("header") {
2150 return "headers".into();
2151 }
2152 if r.contains("cookie") {
2153 return "cookies".into();
2154 }
2155 if r.contains("method") {
2156 return "http-methods".into();
2157 }
2158 if r.contains("auth") || r.contains("security") {
2159 return "security".into();
2160 }
2161 if r.contains("enum") || r.contains("min") || r.contains("max") || r.contains("pattern") {
2162 return "constraints".into();
2163 }
2164 String::new()
2165}
2166
2167pub fn record_validation_error(v: &Value) {
2169 if let Ok(mut q) = LAST_ERRORS.lock() {
2170 if q.len() >= 20 {
2171 q.pop_front();
2172 }
2173 q.push_back(v.clone());
2174 }
2175 }
2177
2178pub fn get_last_validation_error() -> Option<Value> {
2180 LAST_ERRORS.lock().ok()?.back().cloned()
2181}
2182
2183pub fn get_validation_errors() -> Vec<Value> {
2185 LAST_ERRORS.lock().map(|q| q.iter().cloned().collect()).unwrap_or_default()
2186}
2187
2188fn coerce_value_for_schema(value: &Value, schema: &openapiv3::Schema) -> Value {
2193 match value {
2195 Value::String(s) => {
2196 if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
2198 &schema.schema_kind
2199 {
2200 if s.contains(',') {
2201 let parts: Vec<&str> = s.split(',').map(|s| s.trim()).collect();
2203 let mut array_values = Vec::new();
2204
2205 for part in parts {
2206 if let Some(items_schema) = &array_type.items {
2208 if let Some(items_schema_obj) = items_schema.as_item() {
2209 let part_value = Value::String(part.to_string());
2210 let coerced_part =
2211 coerce_value_for_schema(&part_value, items_schema_obj);
2212 array_values.push(coerced_part);
2213 } else {
2214 array_values.push(Value::String(part.to_string()));
2216 }
2217 } else {
2218 array_values.push(Value::String(part.to_string()));
2220 }
2221 }
2222 return Value::Array(array_values);
2223 }
2224 }
2225
2226 match &schema.schema_kind {
2228 openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
2229 value.clone()
2231 }
2232 openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => {
2233 if let Ok(n) = s.parse::<f64>() {
2235 if let Some(num) = serde_json::Number::from_f64(n) {
2236 return Value::Number(num);
2237 }
2238 }
2239 value.clone()
2240 }
2241 openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => {
2242 if let Ok(n) = s.parse::<i64>() {
2244 if let Some(num) = serde_json::Number::from_f64(n as f64) {
2245 return Value::Number(num);
2246 }
2247 }
2248 value.clone()
2249 }
2250 openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => {
2251 match s.to_lowercase().as_str() {
2253 "true" | "1" | "yes" | "on" => Value::Bool(true),
2254 "false" | "0" | "no" | "off" => Value::Bool(false),
2255 _ => value.clone(),
2256 }
2257 }
2258 _ => {
2259 value.clone()
2261 }
2262 }
2263 }
2264 _ => value.clone(),
2265 }
2266}
2267
2268fn coerce_by_style(value: &Value, schema: &openapiv3::Schema, style: Option<&str>) -> Value {
2270 match value {
2272 Value::String(s) => {
2273 if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
2275 &schema.schema_kind
2276 {
2277 let delimiter = match style {
2278 Some("spaceDelimited") => " ",
2279 Some("pipeDelimited") => "|",
2280 Some("form") | None => ",", _ => ",", };
2283
2284 if s.contains(delimiter) {
2285 let parts: Vec<&str> = s.split(delimiter).map(|s| s.trim()).collect();
2287 let mut array_values = Vec::new();
2288
2289 for part in parts {
2290 if let Some(items_schema) = &array_type.items {
2292 if let Some(items_schema_obj) = items_schema.as_item() {
2293 let part_value = Value::String(part.to_string());
2294 let coerced_part =
2295 coerce_by_style(&part_value, items_schema_obj, style);
2296 array_values.push(coerced_part);
2297 } else {
2298 array_values.push(Value::String(part.to_string()));
2300 }
2301 } else {
2302 array_values.push(Value::String(part.to_string()));
2304 }
2305 }
2306 return Value::Array(array_values);
2307 }
2308 }
2309
2310 if let Ok(n) = s.parse::<f64>() {
2312 if let Some(num) = serde_json::Number::from_f64(n) {
2313 return Value::Number(num);
2314 }
2315 }
2316 match s.to_lowercase().as_str() {
2318 "true" | "1" | "yes" | "on" => return Value::Bool(true),
2319 "false" | "0" | "no" | "off" => return Value::Bool(false),
2320 _ => {}
2321 }
2322 value.clone()
2324 }
2325 _ => value.clone(),
2326 }
2327}
2328
2329fn build_deep_object(name: &str, params: &Map<String, Value>) -> Option<Value> {
2331 let prefix = format!("{}[", name);
2332 let mut obj = Map::new();
2333 for (k, v) in params.iter() {
2334 if let Some(rest) = k.strip_prefix(&prefix) {
2335 if let Some(key) = rest.strip_suffix(']') {
2336 obj.insert(key.to_string(), v.clone());
2337 }
2338 }
2339 }
2340 if obj.is_empty() {
2341 None
2342 } else {
2343 Some(Value::Object(obj))
2344 }
2345}
2346
2347#[allow(clippy::too_many_arguments)]
2353fn generate_enhanced_422_response(
2354 validator: &OpenApiRouteRegistry,
2355 path_template: &str,
2356 method: &str,
2357 body: Option<&Value>,
2358 path_params: &Map<String, Value>,
2359 query_params: &Map<String, Value>,
2360 header_params: &Map<String, Value>,
2361 cookie_params: &Map<String, Value>,
2362) -> Value {
2363 let mut field_errors = Vec::new();
2364
2365 if let Some(route) = validator.get_route(path_template, method) {
2367 if let Some(schema) = &route.operation.request_body {
2369 if let Some(value) = body {
2370 if let Some(content) =
2371 schema.as_item().and_then(|rb| rb.content.get("application/json"))
2372 {
2373 if let Some(_schema_ref) = &content.schema {
2374 if serde_json::from_value::<Value>(value.clone()).is_err() {
2376 field_errors.push(json!({
2377 "path": "body",
2378 "message": "invalid JSON"
2379 }));
2380 }
2381 }
2382 }
2383 } else {
2384 field_errors.push(json!({
2385 "path": "body",
2386 "expected": "object",
2387 "found": "missing",
2388 "message": "Request body is required but not provided"
2389 }));
2390 }
2391 }
2392
2393 for param_ref in &route.operation.parameters {
2395 if let Some(param) = param_ref.as_item() {
2396 match param {
2397 openapiv3::Parameter::Path { parameter_data, .. } => {
2398 validate_parameter_detailed(
2399 parameter_data,
2400 path_params,
2401 "path",
2402 "path parameter",
2403 &mut field_errors,
2404 );
2405 }
2406 openapiv3::Parameter::Query { parameter_data, .. } => {
2407 let deep_value = if Some("form") == Some("deepObject") {
2408 build_deep_object(¶meter_data.name, query_params)
2409 } else {
2410 None
2411 };
2412 validate_parameter_detailed_with_deep(
2413 parameter_data,
2414 query_params,
2415 "query",
2416 "query parameter",
2417 deep_value,
2418 &mut field_errors,
2419 );
2420 }
2421 openapiv3::Parameter::Header { parameter_data, .. } => {
2422 validate_parameter_detailed(
2423 parameter_data,
2424 header_params,
2425 "header",
2426 "header parameter",
2427 &mut field_errors,
2428 );
2429 }
2430 openapiv3::Parameter::Cookie { parameter_data, .. } => {
2431 validate_parameter_detailed(
2432 parameter_data,
2433 cookie_params,
2434 "cookie",
2435 "cookie parameter",
2436 &mut field_errors,
2437 );
2438 }
2439 }
2440 }
2441 }
2442 }
2443
2444 json!({
2446 "error": "Schema validation failed",
2447 "details": field_errors,
2448 "method": method,
2449 "path": path_template,
2450 "timestamp": Utc::now().to_rfc3339(),
2451 "validation_type": "openapi_schema"
2452 })
2453}
2454
2455fn validate_parameter(
2457 parameter_data: &openapiv3::ParameterData,
2458 params_map: &Map<String, Value>,
2459 prefix: &str,
2460 aggregate: bool,
2461 errors: &mut Vec<String>,
2462 details: &mut Vec<Value>,
2463) {
2464 match params_map.get(¶meter_data.name) {
2465 Some(v) => {
2466 if let ParameterSchemaOrContent::Schema(s) = ¶meter_data.format {
2467 if let Some(schema) = s.as_item() {
2468 let coerced = coerce_value_for_schema(v, schema);
2469 if let Err(validation_error) =
2471 OpenApiSchema::new(schema.clone()).validate(&coerced)
2472 {
2473 let error_msg = validation_error.to_string();
2474 errors.push(format!(
2475 "{} parameter '{}' validation failed: {}",
2476 prefix, parameter_data.name, error_msg
2477 ));
2478 if aggregate {
2479 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
2480 }
2481 }
2482 }
2483 }
2484 }
2485 None => {
2486 if parameter_data.required {
2487 errors.push(format!(
2488 "missing required {} parameter '{}'",
2489 prefix, parameter_data.name
2490 ));
2491 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
2492 }
2493 }
2494 }
2495}
2496
2497#[allow(clippy::too_many_arguments)]
2499fn validate_parameter_with_deep_object(
2500 parameter_data: &openapiv3::ParameterData,
2501 params_map: &Map<String, Value>,
2502 prefix: &str,
2503 deep_value: Option<Value>,
2504 style: Option<&str>,
2505 aggregate: bool,
2506 errors: &mut Vec<String>,
2507 details: &mut Vec<Value>,
2508) {
2509 match deep_value.as_ref().or_else(|| params_map.get(¶meter_data.name)) {
2510 Some(v) => {
2511 if let ParameterSchemaOrContent::Schema(s) = ¶meter_data.format {
2512 if let Some(schema) = s.as_item() {
2513 let coerced = coerce_by_style(v, schema, style); if let Err(validation_error) =
2516 OpenApiSchema::new(schema.clone()).validate(&coerced)
2517 {
2518 let error_msg = validation_error.to_string();
2519 errors.push(format!(
2520 "{} parameter '{}' validation failed: {}",
2521 prefix, parameter_data.name, error_msg
2522 ));
2523 if aggregate {
2524 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
2525 }
2526 }
2527 }
2528 }
2529 }
2530 None => {
2531 if parameter_data.required {
2532 errors.push(format!(
2533 "missing required {} parameter '{}'",
2534 prefix, parameter_data.name
2535 ));
2536 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
2537 }
2538 }
2539 }
2540}
2541
2542fn validate_parameter_detailed(
2544 parameter_data: &openapiv3::ParameterData,
2545 params_map: &Map<String, Value>,
2546 location: &str,
2547 value_type: &str,
2548 field_errors: &mut Vec<Value>,
2549) {
2550 match params_map.get(¶meter_data.name) {
2551 Some(value) => {
2552 if let ParameterSchemaOrContent::Schema(schema) = ¶meter_data.format {
2553 let details: Vec<Value> = Vec::new();
2555 let param_path = format!("{}.{}", location, parameter_data.name);
2556
2557 if let Some(schema_ref) = schema.as_item() {
2559 let coerced_value = coerce_value_for_schema(value, schema_ref);
2560 if let Err(validation_error) =
2562 OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
2563 {
2564 field_errors.push(json!({
2565 "path": param_path,
2566 "expected": "valid according to schema",
2567 "found": coerced_value,
2568 "message": validation_error.to_string()
2569 }));
2570 }
2571 }
2572
2573 for detail in details {
2574 field_errors.push(json!({
2575 "path": detail["path"],
2576 "expected": detail["expected_type"],
2577 "found": detail["value"],
2578 "message": detail["message"]
2579 }));
2580 }
2581 }
2582 }
2583 None => {
2584 if parameter_data.required {
2585 field_errors.push(json!({
2586 "path": format!("{}.{}", location, parameter_data.name),
2587 "expected": "value",
2588 "found": "missing",
2589 "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
2590 }));
2591 }
2592 }
2593 }
2594}
2595
2596fn validate_parameter_detailed_with_deep(
2598 parameter_data: &openapiv3::ParameterData,
2599 params_map: &Map<String, Value>,
2600 location: &str,
2601 value_type: &str,
2602 deep_value: Option<Value>,
2603 field_errors: &mut Vec<Value>,
2604) {
2605 match deep_value.as_ref().or_else(|| params_map.get(¶meter_data.name)) {
2606 Some(value) => {
2607 if let ParameterSchemaOrContent::Schema(schema) = ¶meter_data.format {
2608 let details: Vec<Value> = Vec::new();
2610 let param_path = format!("{}.{}", location, parameter_data.name);
2611
2612 if let Some(schema_ref) = schema.as_item() {
2614 let coerced_value = coerce_by_style(value, schema_ref, Some("form")); if let Err(validation_error) =
2617 OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
2618 {
2619 field_errors.push(json!({
2620 "path": param_path,
2621 "expected": "valid according to schema",
2622 "found": coerced_value,
2623 "message": validation_error.to_string()
2624 }));
2625 }
2626 }
2627
2628 for detail in details {
2629 field_errors.push(json!({
2630 "path": detail["path"],
2631 "expected": detail["expected_type"],
2632 "found": detail["value"],
2633 "message": detail["message"]
2634 }));
2635 }
2636 }
2637 }
2638 None => {
2639 if parameter_data.required {
2640 field_errors.push(json!({
2641 "path": format!("{}.{}", location, parameter_data.name),
2642 "expected": "value",
2643 "found": "missing",
2644 "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
2645 }));
2646 }
2647 }
2648 }
2649}
2650
2651pub async fn create_registry_from_file<P: AsRef<std::path::Path>>(
2653 path: P,
2654) -> Result<OpenApiRouteRegistry> {
2655 let spec = OpenApiSpec::from_file(path).await?;
2656 spec.validate()?;
2657 Ok(OpenApiRouteRegistry::new(spec))
2658}
2659
2660pub fn create_registry_from_json(json: Value) -> Result<OpenApiRouteRegistry> {
2662 let spec = OpenApiSpec::from_json(json)?;
2663 spec.validate()?;
2664 Ok(OpenApiRouteRegistry::new(spec))
2665}
2666
2667#[cfg(test)]
2668mod tests {
2669 use super::*;
2670 use serde_json::json;
2671 use tempfile::TempDir;
2672
2673 #[tokio::test]
2674 async fn test_registry_creation() {
2675 let spec_json = json!({
2676 "openapi": "3.0.0",
2677 "info": {
2678 "title": "Test API",
2679 "version": "1.0.0"
2680 },
2681 "paths": {
2682 "/users": {
2683 "get": {
2684 "summary": "Get users",
2685 "responses": {
2686 "200": {
2687 "description": "Success",
2688 "content": {
2689 "application/json": {
2690 "schema": {
2691 "type": "array",
2692 "items": {
2693 "type": "object",
2694 "properties": {
2695 "id": {"type": "integer"},
2696 "name": {"type": "string"}
2697 }
2698 }
2699 }
2700 }
2701 }
2702 }
2703 }
2704 },
2705 "post": {
2706 "summary": "Create user",
2707 "requestBody": {
2708 "content": {
2709 "application/json": {
2710 "schema": {
2711 "type": "object",
2712 "properties": {
2713 "name": {"type": "string"}
2714 },
2715 "required": ["name"]
2716 }
2717 }
2718 }
2719 },
2720 "responses": {
2721 "201": {
2722 "description": "Created",
2723 "content": {
2724 "application/json": {
2725 "schema": {
2726 "type": "object",
2727 "properties": {
2728 "id": {"type": "integer"},
2729 "name": {"type": "string"}
2730 }
2731 }
2732 }
2733 }
2734 }
2735 }
2736 }
2737 },
2738 "/users/{id}": {
2739 "get": {
2740 "summary": "Get user by ID",
2741 "parameters": [
2742 {
2743 "name": "id",
2744 "in": "path",
2745 "required": true,
2746 "schema": {"type": "integer"}
2747 }
2748 ],
2749 "responses": {
2750 "200": {
2751 "description": "Success",
2752 "content": {
2753 "application/json": {
2754 "schema": {
2755 "type": "object",
2756 "properties": {
2757 "id": {"type": "integer"},
2758 "name": {"type": "string"}
2759 }
2760 }
2761 }
2762 }
2763 }
2764 }
2765 }
2766 }
2767 }
2768 });
2769
2770 let registry = create_registry_from_json(spec_json).unwrap();
2771
2772 assert_eq!(registry.paths().len(), 2);
2774 assert!(registry.paths().contains(&"/users".to_string()));
2775 assert!(registry.paths().contains(&"/users/{id}".to_string()));
2776
2777 assert_eq!(registry.methods().len(), 2);
2778 assert!(registry.methods().contains(&"GET".to_string()));
2779 assert!(registry.methods().contains(&"POST".to_string()));
2780
2781 let get_users_route = registry.get_route("/users", "GET").unwrap();
2783 assert_eq!(get_users_route.method, "GET");
2784 assert_eq!(get_users_route.path, "/users");
2785
2786 let post_users_route = registry.get_route("/users", "POST").unwrap();
2787 assert_eq!(post_users_route.method, "POST");
2788 assert!(post_users_route.operation.request_body.is_some());
2789
2790 let user_by_id_route = registry.get_route("/users/{id}", "GET").unwrap();
2792 assert_eq!(user_by_id_route.axum_path(), "/users/{id}");
2793 }
2794
2795 #[tokio::test]
2796 async fn test_validate_request_with_params_and_formats() {
2797 let spec_json = json!({
2798 "openapi": "3.0.0",
2799 "info": { "title": "Test API", "version": "1.0.0" },
2800 "paths": {
2801 "/users/{id}": {
2802 "post": {
2803 "parameters": [
2804 { "name": "id", "in": "path", "required": true, "schema": {"type": "string"} },
2805 { "name": "q", "in": "query", "required": false, "schema": {"type": "integer"} }
2806 ],
2807 "requestBody": {
2808 "content": {
2809 "application/json": {
2810 "schema": {
2811 "type": "object",
2812 "required": ["email", "website"],
2813 "properties": {
2814 "email": {"type": "string", "format": "email"},
2815 "website": {"type": "string", "format": "uri"}
2816 }
2817 }
2818 }
2819 }
2820 },
2821 "responses": {"200": {"description": "ok"}}
2822 }
2823 }
2824 }
2825 });
2826
2827 let registry = create_registry_from_json(spec_json).unwrap();
2828 let mut path_params = Map::new();
2829 path_params.insert("id".to_string(), json!("abc"));
2830 let mut query_params = Map::new();
2831 query_params.insert("q".to_string(), json!(123));
2832
2833 let body = json!({"email":"a@b.co","website":"https://example.com"});
2835 assert!(registry
2836 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2837 .is_ok());
2838
2839 let bad_email = json!({"email":"not-an-email","website":"https://example.com"});
2841 assert!(registry
2842 .validate_request_with(
2843 "/users/{id}",
2844 "POST",
2845 &path_params,
2846 &query_params,
2847 Some(&bad_email)
2848 )
2849 .is_err());
2850
2851 let empty_path_params = Map::new();
2853 assert!(registry
2854 .validate_request_with(
2855 "/users/{id}",
2856 "POST",
2857 &empty_path_params,
2858 &query_params,
2859 Some(&body)
2860 )
2861 .is_err());
2862 }
2863
2864 #[tokio::test]
2865 async fn test_ref_resolution_for_params_and_body() {
2866 let spec_json = json!({
2867 "openapi": "3.0.0",
2868 "info": { "title": "Ref API", "version": "1.0.0" },
2869 "components": {
2870 "schemas": {
2871 "EmailWebsite": {
2872 "type": "object",
2873 "required": ["email", "website"],
2874 "properties": {
2875 "email": {"type": "string", "format": "email"},
2876 "website": {"type": "string", "format": "uri"}
2877 }
2878 }
2879 },
2880 "parameters": {
2881 "PathId": {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}},
2882 "QueryQ": {"name": "q", "in": "query", "required": false, "schema": {"type": "integer"}}
2883 },
2884 "requestBodies": {
2885 "CreateUser": {
2886 "content": {
2887 "application/json": {
2888 "schema": {"$ref": "#/components/schemas/EmailWebsite"}
2889 }
2890 }
2891 }
2892 }
2893 },
2894 "paths": {
2895 "/users/{id}": {
2896 "post": {
2897 "parameters": [
2898 {"$ref": "#/components/parameters/PathId"},
2899 {"$ref": "#/components/parameters/QueryQ"}
2900 ],
2901 "requestBody": {"$ref": "#/components/requestBodies/CreateUser"},
2902 "responses": {"200": {"description": "ok"}}
2903 }
2904 }
2905 }
2906 });
2907
2908 let registry = create_registry_from_json(spec_json).unwrap();
2909 let mut path_params = Map::new();
2910 path_params.insert("id".to_string(), json!("abc"));
2911 let mut query_params = Map::new();
2912 query_params.insert("q".to_string(), json!(7));
2913
2914 let body = json!({"email":"user@example.com","website":"https://example.com"});
2915 assert!(registry
2916 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2917 .is_ok());
2918
2919 let bad = json!({"email":"nope","website":"https://example.com"});
2920 assert!(registry
2921 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&bad))
2922 .is_err());
2923 }
2924
2925 #[tokio::test]
2926 async fn test_header_cookie_and_query_coercion() {
2927 let spec_json = json!({
2928 "openapi": "3.0.0",
2929 "info": { "title": "Params API", "version": "1.0.0" },
2930 "paths": {
2931 "/items": {
2932 "get": {
2933 "parameters": [
2934 {"name": "X-Flag", "in": "header", "required": true, "schema": {"type": "boolean"}},
2935 {"name": "session", "in": "cookie", "required": true, "schema": {"type": "string"}},
2936 {"name": "ids", "in": "query", "required": false, "schema": {"type": "array", "items": {"type": "integer"}}}
2937 ],
2938 "responses": {"200": {"description": "ok"}}
2939 }
2940 }
2941 }
2942 });
2943
2944 let registry = create_registry_from_json(spec_json).unwrap();
2945
2946 let path_params = Map::new();
2947 let mut query_params = Map::new();
2948 query_params.insert("ids".to_string(), json!("1,2,3"));
2950 let mut header_params = Map::new();
2951 header_params.insert("X-Flag".to_string(), json!("true"));
2952 let mut cookie_params = Map::new();
2953 cookie_params.insert("session".to_string(), json!("abc123"));
2954
2955 assert!(registry
2956 .validate_request_with_all(
2957 "/items",
2958 "GET",
2959 &path_params,
2960 &query_params,
2961 &header_params,
2962 &cookie_params,
2963 None
2964 )
2965 .is_ok());
2966
2967 let empty_cookie = Map::new();
2969 assert!(registry
2970 .validate_request_with_all(
2971 "/items",
2972 "GET",
2973 &path_params,
2974 &query_params,
2975 &header_params,
2976 &empty_cookie,
2977 None
2978 )
2979 .is_err());
2980
2981 let mut bad_header = Map::new();
2983 bad_header.insert("X-Flag".to_string(), json!("notabool"));
2984 assert!(registry
2985 .validate_request_with_all(
2986 "/items",
2987 "GET",
2988 &path_params,
2989 &query_params,
2990 &bad_header,
2991 &cookie_params,
2992 None
2993 )
2994 .is_err());
2995 }
2996
2997 #[tokio::test]
2998 async fn test_query_styles_space_pipe_deepobject() {
2999 let spec_json = json!({
3000 "openapi": "3.0.0",
3001 "info": { "title": "Query Styles API", "version": "1.0.0" },
3002 "paths": {"/search": {"get": {
3003 "parameters": [
3004 {"name":"tags","in":"query","style":"spaceDelimited","schema":{"type":"array","items":{"type":"string"}}},
3005 {"name":"ids","in":"query","style":"pipeDelimited","schema":{"type":"array","items":{"type":"integer"}}},
3006 {"name":"filter","in":"query","style":"deepObject","schema":{"type":"object","properties":{"color":{"type":"string"}},"required":["color"]}}
3007 ],
3008 "responses": {"200": {"description":"ok"}}
3009 }} }
3010 });
3011
3012 let registry = create_registry_from_json(spec_json).unwrap();
3013
3014 let path_params = Map::new();
3015 let mut query = Map::new();
3016 query.insert("tags".into(), json!("alpha beta gamma"));
3017 query.insert("ids".into(), json!("1|2|3"));
3018 query.insert("filter[color]".into(), json!("red"));
3019
3020 assert!(registry
3021 .validate_request_with("/search", "GET", &path_params, &query, None)
3022 .is_ok());
3023 }
3024
3025 #[tokio::test]
3026 async fn test_oneof_anyof_allof_validation() {
3027 let spec_json = json!({
3028 "openapi": "3.0.0",
3029 "info": { "title": "Composite API", "version": "1.0.0" },
3030 "paths": {
3031 "/composite": {
3032 "post": {
3033 "requestBody": {
3034 "content": {
3035 "application/json": {
3036 "schema": {
3037 "allOf": [
3038 {"type": "object", "required": ["base"], "properties": {"base": {"type": "string"}}}
3039 ],
3040 "oneOf": [
3041 {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"], "not": {"required": ["b"]}},
3042 {"type": "object", "properties": {"b": {"type": "integer"}}, "required": ["b"], "not": {"required": ["a"]}}
3043 ],
3044 "anyOf": [
3045 {"type": "object", "properties": {"flag": {"type": "boolean"}}, "required": ["flag"]},
3046 {"type": "object", "properties": {"extra": {"type": "string"}}, "required": ["extra"]}
3047 ]
3048 }
3049 }
3050 }
3051 },
3052 "responses": {"200": {"description": "ok"}}
3053 }
3054 }
3055 }
3056 });
3057
3058 let registry = create_registry_from_json(spec_json).unwrap();
3059 let ok = json!({"base": "x", "a": 1, "flag": true});
3061 assert!(registry
3062 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&ok))
3063 .is_ok());
3064
3065 let bad_oneof = json!({"base": "x", "a": 1, "b": 2, "flag": false});
3067 assert!(registry
3068 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_oneof))
3069 .is_err());
3070
3071 let bad_anyof = json!({"base": "x", "a": 1});
3073 assert!(registry
3074 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_anyof))
3075 .is_err());
3076
3077 let bad_allof = json!({"a": 1, "flag": true});
3079 assert!(registry
3080 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_allof))
3081 .is_err());
3082 }
3083
3084 #[tokio::test]
3085 async fn test_overrides_warn_mode_allows_invalid() {
3086 let spec_json = json!({
3088 "openapi": "3.0.0",
3089 "info": { "title": "Overrides API", "version": "1.0.0" },
3090 "paths": {"/things": {"post": {
3091 "parameters": [{"name":"q","in":"query","required":true,"schema":{"type":"integer"}}],
3092 "responses": {"200": {"description":"ok"}}
3093 }}}
3094 });
3095
3096 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3097 let mut overrides = HashMap::new();
3098 overrides.insert("POST /things".to_string(), ValidationMode::Warn);
3099 let registry = OpenApiRouteRegistry::new_with_options(
3100 spec,
3101 ValidationOptions {
3102 request_mode: ValidationMode::Enforce,
3103 aggregate_errors: true,
3104 validate_responses: false,
3105 overrides,
3106 admin_skip_prefixes: vec![],
3107 response_template_expand: false,
3108 validation_status: None,
3109 },
3110 );
3111
3112 let ok = registry.validate_request_with("/things", "POST", &Map::new(), &Map::new(), None);
3114 assert!(ok.is_ok());
3115 }
3116
3117 #[tokio::test]
3118 async fn test_admin_skip_prefix_short_circuit() {
3119 let spec_json = json!({
3120 "openapi": "3.0.0",
3121 "info": { "title": "Skip API", "version": "1.0.0" },
3122 "paths": {}
3123 });
3124 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3125 let registry = OpenApiRouteRegistry::new_with_options(
3126 spec,
3127 ValidationOptions {
3128 request_mode: ValidationMode::Enforce,
3129 aggregate_errors: true,
3130 validate_responses: false,
3131 overrides: HashMap::new(),
3132 admin_skip_prefixes: vec!["/admin".into()],
3133 response_template_expand: false,
3134 validation_status: None,
3135 },
3136 );
3137
3138 let res = registry.validate_request_with_all(
3140 "/admin/__mockforge/health",
3141 "GET",
3142 &Map::new(),
3143 &Map::new(),
3144 &Map::new(),
3145 &Map::new(),
3146 None,
3147 );
3148 assert!(res.is_ok());
3149 }
3150
3151 #[test]
3152 fn test_path_conversion() {
3153 assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users"), "/users");
3154 assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users/{id}"), "/users/{id}");
3155 assert_eq!(
3156 OpenApiRouteRegistry::convert_path_to_axum("/users/{id}/posts/{postId}"),
3157 "/users/{id}/posts/{postId}"
3158 );
3159 }
3160
3161 #[test]
3162 fn test_validation_options_default() {
3163 let options = ValidationOptions::default();
3164 assert!(matches!(options.request_mode, ValidationMode::Enforce));
3165 assert!(options.aggregate_errors);
3166 assert!(!options.validate_responses);
3167 assert!(options.overrides.is_empty());
3168 assert!(options.admin_skip_prefixes.is_empty());
3169 assert!(!options.response_template_expand);
3170 assert!(options.validation_status.is_none());
3171 }
3172
3173 #[test]
3174 fn test_validation_mode_variants() {
3175 let disabled = ValidationMode::Disabled;
3177 let warn = ValidationMode::Warn;
3178 let enforce = ValidationMode::Enforce;
3179 let default = ValidationMode::default();
3180
3181 assert!(matches!(default, ValidationMode::Warn));
3183
3184 assert!(!matches!(disabled, ValidationMode::Warn));
3186 assert!(!matches!(warn, ValidationMode::Enforce));
3187 assert!(!matches!(enforce, ValidationMode::Disabled));
3188 }
3189
3190 #[test]
3191 fn test_registry_spec_accessor() {
3192 let spec_json = json!({
3193 "openapi": "3.0.0",
3194 "info": {
3195 "title": "Test API",
3196 "version": "1.0.0"
3197 },
3198 "paths": {}
3199 });
3200 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3201 let registry = OpenApiRouteRegistry::new(spec.clone());
3202
3203 let accessed_spec = registry.spec();
3205 assert_eq!(accessed_spec.title(), "Test API");
3206 }
3207
3208 #[test]
3209 fn test_clone_for_validation() {
3210 let spec_json = json!({
3211 "openapi": "3.0.0",
3212 "info": {
3213 "title": "Test API",
3214 "version": "1.0.0"
3215 },
3216 "paths": {
3217 "/users": {
3218 "get": {
3219 "responses": {
3220 "200": {
3221 "description": "Success"
3222 }
3223 }
3224 }
3225 }
3226 }
3227 });
3228 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3229 let registry = OpenApiRouteRegistry::new(spec);
3230
3231 let cloned = registry.clone_for_validation();
3233 assert_eq!(cloned.routes().len(), registry.routes().len());
3234 assert_eq!(cloned.spec().title(), registry.spec().title());
3235 }
3236
3237 #[test]
3238 fn test_with_custom_fixture_loader() {
3239 let temp_dir = TempDir::new().unwrap();
3240 let spec_json = json!({
3241 "openapi": "3.0.0",
3242 "info": {
3243 "title": "Test API",
3244 "version": "1.0.0"
3245 },
3246 "paths": {}
3247 });
3248 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3249 let registry = OpenApiRouteRegistry::new(spec);
3250 let original_routes_len = registry.routes().len();
3251
3252 let custom_loader = Arc::new(crate::custom_fixture::CustomFixtureLoader::new(
3254 temp_dir.path().to_path_buf(),
3255 true,
3256 ));
3257 let registry_with_loader = registry.with_custom_fixture_loader(custom_loader);
3258
3259 assert_eq!(registry_with_loader.routes().len(), original_routes_len);
3261 }
3262
3263 #[test]
3264 fn test_get_route() {
3265 let spec_json = json!({
3266 "openapi": "3.0.0",
3267 "info": {
3268 "title": "Test API",
3269 "version": "1.0.0"
3270 },
3271 "paths": {
3272 "/users": {
3273 "get": {
3274 "operationId": "getUsers",
3275 "responses": {
3276 "200": {
3277 "description": "Success"
3278 }
3279 }
3280 },
3281 "post": {
3282 "operationId": "createUser",
3283 "responses": {
3284 "201": {
3285 "description": "Created"
3286 }
3287 }
3288 }
3289 }
3290 }
3291 });
3292 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3293 let registry = OpenApiRouteRegistry::new(spec);
3294
3295 let route = registry.get_route("/users", "GET");
3297 assert!(route.is_some());
3298 assert_eq!(route.unwrap().method, "GET");
3299 assert_eq!(route.unwrap().path, "/users");
3300
3301 let route = registry.get_route("/nonexistent", "GET");
3303 assert!(route.is_none());
3304
3305 let route = registry.get_route("/users", "POST");
3307 assert!(route.is_some());
3308 assert_eq!(route.unwrap().method, "POST");
3309 }
3310
3311 #[test]
3312 fn test_get_routes_for_path() {
3313 let spec_json = json!({
3314 "openapi": "3.0.0",
3315 "info": {
3316 "title": "Test API",
3317 "version": "1.0.0"
3318 },
3319 "paths": {
3320 "/users": {
3321 "get": {
3322 "responses": {
3323 "200": {
3324 "description": "Success"
3325 }
3326 }
3327 },
3328 "post": {
3329 "responses": {
3330 "201": {
3331 "description": "Created"
3332 }
3333 }
3334 },
3335 "put": {
3336 "responses": {
3337 "200": {
3338 "description": "Success"
3339 }
3340 }
3341 }
3342 },
3343 "/posts": {
3344 "get": {
3345 "responses": {
3346 "200": {
3347 "description": "Success"
3348 }
3349 }
3350 }
3351 }
3352 }
3353 });
3354 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3355 let registry = OpenApiRouteRegistry::new(spec);
3356
3357 let routes = registry.get_routes_for_path("/users");
3359 assert_eq!(routes.len(), 3);
3360 let methods: Vec<&str> = routes.iter().map(|r| r.method.as_str()).collect();
3361 assert!(methods.contains(&"GET"));
3362 assert!(methods.contains(&"POST"));
3363 assert!(methods.contains(&"PUT"));
3364
3365 let routes = registry.get_routes_for_path("/posts");
3367 assert_eq!(routes.len(), 1);
3368 assert_eq!(routes[0].method, "GET");
3369
3370 let routes = registry.get_routes_for_path("/nonexistent");
3372 assert!(routes.is_empty());
3373 }
3374
3375 #[test]
3376 fn test_new_vs_new_with_options() {
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 });
3385 let spec1 = OpenApiSpec::from_json(spec_json.clone()).unwrap();
3386 let spec2 = OpenApiSpec::from_json(spec_json).unwrap();
3387
3388 let registry1 = OpenApiRouteRegistry::new(spec1);
3390 assert_eq!(registry1.spec().title(), "Test API");
3391
3392 let options = ValidationOptions {
3394 request_mode: ValidationMode::Disabled,
3395 aggregate_errors: false,
3396 validate_responses: true,
3397 overrides: HashMap::new(),
3398 admin_skip_prefixes: vec!["/admin".to_string()],
3399 response_template_expand: true,
3400 validation_status: Some(422),
3401 };
3402 let registry2 = OpenApiRouteRegistry::new_with_options(spec2, options);
3403 assert_eq!(registry2.spec().title(), "Test API");
3404 }
3405
3406 #[test]
3407 fn test_new_with_env_vs_new() {
3408 let spec_json = json!({
3409 "openapi": "3.0.0",
3410 "info": {
3411 "title": "Test API",
3412 "version": "1.0.0"
3413 },
3414 "paths": {}
3415 });
3416 let spec1 = OpenApiSpec::from_json(spec_json.clone()).unwrap();
3417 let spec2 = OpenApiSpec::from_json(spec_json).unwrap();
3418
3419 let registry1 = OpenApiRouteRegistry::new(spec1);
3421
3422 let registry2 = OpenApiRouteRegistry::new_with_env(spec2);
3424
3425 assert_eq!(registry1.spec().title(), "Test API");
3427 assert_eq!(registry2.spec().title(), "Test API");
3428 }
3429
3430 #[test]
3431 fn test_validation_options_custom() {
3432 let options = ValidationOptions {
3433 request_mode: ValidationMode::Warn,
3434 aggregate_errors: false,
3435 validate_responses: true,
3436 overrides: {
3437 let mut map = HashMap::new();
3438 map.insert("getUsers".to_string(), ValidationMode::Disabled);
3439 map
3440 },
3441 admin_skip_prefixes: vec!["/admin".to_string(), "/internal".to_string()],
3442 response_template_expand: true,
3443 validation_status: Some(422),
3444 };
3445
3446 assert!(matches!(options.request_mode, ValidationMode::Warn));
3447 assert!(!options.aggregate_errors);
3448 assert!(options.validate_responses);
3449 assert_eq!(options.overrides.len(), 1);
3450 assert_eq!(options.admin_skip_prefixes.len(), 2);
3451 assert!(options.response_template_expand);
3452 assert_eq!(options.validation_status, Some(422));
3453 }
3454
3455 #[test]
3456 fn test_validation_mode_default_standalone() {
3457 let mode = ValidationMode::default();
3458 assert!(matches!(mode, ValidationMode::Warn));
3459 }
3460
3461 #[test]
3462 fn test_validation_mode_clone() {
3463 let mode1 = ValidationMode::Enforce;
3464 let mode2 = mode1.clone();
3465 assert!(matches!(mode1, ValidationMode::Enforce));
3466 assert!(matches!(mode2, ValidationMode::Enforce));
3467 }
3468
3469 #[test]
3470 fn test_validation_mode_debug() {
3471 let mode = ValidationMode::Disabled;
3472 let debug_str = format!("{:?}", mode);
3473 assert!(debug_str.contains("Disabled") || debug_str.contains("ValidationMode"));
3474 }
3475
3476 #[test]
3477 fn test_validation_options_clone() {
3478 let options1 = ValidationOptions {
3479 request_mode: ValidationMode::Warn,
3480 aggregate_errors: true,
3481 validate_responses: false,
3482 overrides: HashMap::new(),
3483 admin_skip_prefixes: vec![],
3484 response_template_expand: false,
3485 validation_status: None,
3486 };
3487 let options2 = options1.clone();
3488 assert!(matches!(options2.request_mode, ValidationMode::Warn));
3489 assert_eq!(options1.aggregate_errors, options2.aggregate_errors);
3490 }
3491
3492 #[test]
3493 fn test_validation_options_debug() {
3494 let options = ValidationOptions::default();
3495 let debug_str = format!("{:?}", options);
3496 assert!(debug_str.contains("ValidationOptions"));
3497 }
3498
3499 #[test]
3500 fn test_validation_options_with_all_fields() {
3501 let mut overrides = HashMap::new();
3502 overrides.insert("op1".to_string(), ValidationMode::Disabled);
3503 overrides.insert("op2".to_string(), ValidationMode::Warn);
3504
3505 let options = ValidationOptions {
3506 request_mode: ValidationMode::Enforce,
3507 aggregate_errors: false,
3508 validate_responses: true,
3509 overrides: overrides.clone(),
3510 admin_skip_prefixes: vec!["/admin".to_string(), "/internal".to_string()],
3511 response_template_expand: true,
3512 validation_status: Some(422),
3513 };
3514
3515 assert!(matches!(options.request_mode, ValidationMode::Enforce));
3516 assert!(!options.aggregate_errors);
3517 assert!(options.validate_responses);
3518 assert_eq!(options.overrides.len(), 2);
3519 assert_eq!(options.admin_skip_prefixes.len(), 2);
3520 assert!(options.response_template_expand);
3521 assert_eq!(options.validation_status, Some(422));
3522 }
3523
3524 #[test]
3525 fn test_openapi_route_registry_clone() {
3526 let spec_json = json!({
3527 "openapi": "3.0.0",
3528 "info": { "title": "Test API", "version": "1.0.0" },
3529 "paths": {}
3530 });
3531 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3532 let registry1 = OpenApiRouteRegistry::new(spec);
3533 let registry2 = registry1.clone();
3534 assert_eq!(registry1.spec().title(), registry2.spec().title());
3535 }
3536
3537 #[test]
3538 fn test_validation_mode_serialization() {
3539 let mode = ValidationMode::Enforce;
3540 let json = serde_json::to_string(&mode).unwrap();
3541 assert!(json.contains("Enforce") || json.contains("enforce"));
3542 }
3543
3544 #[test]
3545 fn test_validation_mode_deserialization() {
3546 let json = r#""Disabled""#;
3547 let mode: ValidationMode = serde_json::from_str(json).unwrap();
3548 assert!(matches!(mode, ValidationMode::Disabled));
3549 }
3550
3551 #[test]
3552 fn test_validation_options_default_values() {
3553 let options = ValidationOptions::default();
3554 assert!(matches!(options.request_mode, ValidationMode::Enforce));
3555 assert!(options.aggregate_errors);
3556 assert!(!options.validate_responses);
3557 assert!(options.overrides.is_empty());
3558 assert!(options.admin_skip_prefixes.is_empty());
3559 assert!(!options.response_template_expand);
3560 assert_eq!(options.validation_status, None);
3561 }
3562
3563 #[test]
3564 fn test_validation_mode_all_variants() {
3565 let disabled = ValidationMode::Disabled;
3566 let warn = ValidationMode::Warn;
3567 let enforce = ValidationMode::Enforce;
3568
3569 assert!(matches!(disabled, ValidationMode::Disabled));
3570 assert!(matches!(warn, ValidationMode::Warn));
3571 assert!(matches!(enforce, ValidationMode::Enforce));
3572 }
3573
3574 #[test]
3575 fn test_validation_options_with_overrides() {
3576 let mut overrides = HashMap::new();
3577 overrides.insert("operation1".to_string(), ValidationMode::Disabled);
3578 overrides.insert("operation2".to_string(), ValidationMode::Warn);
3579
3580 let options = ValidationOptions {
3581 request_mode: ValidationMode::Enforce,
3582 aggregate_errors: true,
3583 validate_responses: false,
3584 overrides,
3585 admin_skip_prefixes: vec![],
3586 response_template_expand: false,
3587 validation_status: None,
3588 };
3589
3590 assert_eq!(options.overrides.len(), 2);
3591 assert!(matches!(options.overrides.get("operation1"), Some(ValidationMode::Disabled)));
3592 assert!(matches!(options.overrides.get("operation2"), Some(ValidationMode::Warn)));
3593 }
3594
3595 #[test]
3596 fn test_validation_options_with_admin_skip_prefixes() {
3597 let options = ValidationOptions {
3598 request_mode: ValidationMode::Enforce,
3599 aggregate_errors: true,
3600 validate_responses: false,
3601 overrides: HashMap::new(),
3602 admin_skip_prefixes: vec![
3603 "/admin".to_string(),
3604 "/internal".to_string(),
3605 "/debug".to_string(),
3606 ],
3607 response_template_expand: false,
3608 validation_status: None,
3609 };
3610
3611 assert_eq!(options.admin_skip_prefixes.len(), 3);
3612 assert!(options.admin_skip_prefixes.contains(&"/admin".to_string()));
3613 assert!(options.admin_skip_prefixes.contains(&"/internal".to_string()));
3614 assert!(options.admin_skip_prefixes.contains(&"/debug".to_string()));
3615 }
3616
3617 #[test]
3618 fn test_validation_options_with_validation_status() {
3619 let options1 = ValidationOptions {
3620 request_mode: ValidationMode::Enforce,
3621 aggregate_errors: true,
3622 validate_responses: false,
3623 overrides: HashMap::new(),
3624 admin_skip_prefixes: vec![],
3625 response_template_expand: false,
3626 validation_status: Some(400),
3627 };
3628
3629 let options2 = ValidationOptions {
3630 request_mode: ValidationMode::Enforce,
3631 aggregate_errors: true,
3632 validate_responses: false,
3633 overrides: HashMap::new(),
3634 admin_skip_prefixes: vec![],
3635 response_template_expand: false,
3636 validation_status: Some(422),
3637 };
3638
3639 assert_eq!(options1.validation_status, Some(400));
3640 assert_eq!(options2.validation_status, Some(422));
3641 }
3642
3643 #[test]
3644 fn test_validate_request_with_disabled_mode() {
3645 let spec_json = json!({
3647 "openapi": "3.0.0",
3648 "info": {"title": "Test API", "version": "1.0.0"},
3649 "paths": {
3650 "/users": {
3651 "get": {
3652 "responses": {"200": {"description": "OK"}}
3653 }
3654 }
3655 }
3656 });
3657 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3658 let options = ValidationOptions {
3659 request_mode: ValidationMode::Disabled,
3660 ..Default::default()
3661 };
3662 let registry = OpenApiRouteRegistry::new_with_options(spec, options);
3663
3664 let result = registry.validate_request_with_all(
3666 "/users",
3667 "GET",
3668 &Map::new(),
3669 &Map::new(),
3670 &Map::new(),
3671 &Map::new(),
3672 None,
3673 );
3674 assert!(result.is_ok());
3675 }
3676
3677 #[test]
3678 fn test_validate_request_with_warn_mode() {
3679 let spec_json = json!({
3681 "openapi": "3.0.0",
3682 "info": {"title": "Test API", "version": "1.0.0"},
3683 "paths": {
3684 "/users": {
3685 "post": {
3686 "requestBody": {
3687 "required": true,
3688 "content": {
3689 "application/json": {
3690 "schema": {
3691 "type": "object",
3692 "required": ["name"],
3693 "properties": {
3694 "name": {"type": "string"}
3695 }
3696 }
3697 }
3698 }
3699 },
3700 "responses": {"200": {"description": "OK"}}
3701 }
3702 }
3703 }
3704 });
3705 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3706 let options = ValidationOptions {
3707 request_mode: ValidationMode::Warn,
3708 ..Default::default()
3709 };
3710 let registry = OpenApiRouteRegistry::new_with_options(spec, options);
3711
3712 let result = registry.validate_request_with_all(
3714 "/users",
3715 "POST",
3716 &Map::new(),
3717 &Map::new(),
3718 &Map::new(),
3719 &Map::new(),
3720 None, );
3722 assert!(result.is_ok()); }
3724
3725 #[test]
3726 fn test_validate_request_body_validation_error() {
3727 let spec_json = json!({
3729 "openapi": "3.0.0",
3730 "info": {"title": "Test API", "version": "1.0.0"},
3731 "paths": {
3732 "/users": {
3733 "post": {
3734 "requestBody": {
3735 "required": true,
3736 "content": {
3737 "application/json": {
3738 "schema": {
3739 "type": "object",
3740 "required": ["name"],
3741 "properties": {
3742 "name": {"type": "string"}
3743 }
3744 }
3745 }
3746 }
3747 },
3748 "responses": {"200": {"description": "OK"}}
3749 }
3750 }
3751 }
3752 });
3753 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3754 let registry = OpenApiRouteRegistry::new(spec);
3755
3756 let result = registry.validate_request_with_all(
3758 "/users",
3759 "POST",
3760 &Map::new(),
3761 &Map::new(),
3762 &Map::new(),
3763 &Map::new(),
3764 None, );
3766 assert!(result.is_err());
3767 }
3768
3769 #[test]
3770 fn test_validate_request_body_schema_validation_error() {
3771 let spec_json = json!({
3773 "openapi": "3.0.0",
3774 "info": {"title": "Test API", "version": "1.0.0"},
3775 "paths": {
3776 "/users": {
3777 "post": {
3778 "requestBody": {
3779 "required": true,
3780 "content": {
3781 "application/json": {
3782 "schema": {
3783 "type": "object",
3784 "required": ["name"],
3785 "properties": {
3786 "name": {"type": "string"}
3787 }
3788 }
3789 }
3790 }
3791 },
3792 "responses": {"200": {"description": "OK"}}
3793 }
3794 }
3795 }
3796 });
3797 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3798 let registry = OpenApiRouteRegistry::new(spec);
3799
3800 let invalid_body = json!({}); let result = registry.validate_request_with_all(
3803 "/users",
3804 "POST",
3805 &Map::new(),
3806 &Map::new(),
3807 &Map::new(),
3808 &Map::new(),
3809 Some(&invalid_body),
3810 );
3811 assert!(result.is_err());
3812 }
3813
3814 #[test]
3815 fn test_validate_request_body_referenced_schema_error() {
3816 let spec_json = json!({
3818 "openapi": "3.0.0",
3819 "info": {"title": "Test API", "version": "1.0.0"},
3820 "paths": {
3821 "/users": {
3822 "post": {
3823 "requestBody": {
3824 "required": true,
3825 "content": {
3826 "application/json": {
3827 "schema": {
3828 "$ref": "#/components/schemas/NonExistentSchema"
3829 }
3830 }
3831 }
3832 },
3833 "responses": {"200": {"description": "OK"}}
3834 }
3835 }
3836 },
3837 "components": {}
3838 });
3839 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3840 let registry = OpenApiRouteRegistry::new(spec);
3841
3842 let body = json!({"name": "test"});
3844 let result = registry.validate_request_with_all(
3845 "/users",
3846 "POST",
3847 &Map::new(),
3848 &Map::new(),
3849 &Map::new(),
3850 &Map::new(),
3851 Some(&body),
3852 );
3853 assert!(result.is_err());
3854 }
3855
3856 #[test]
3857 fn test_validate_request_body_referenced_request_body_error() {
3858 let spec_json = json!({
3860 "openapi": "3.0.0",
3861 "info": {"title": "Test API", "version": "1.0.0"},
3862 "paths": {
3863 "/users": {
3864 "post": {
3865 "requestBody": {
3866 "$ref": "#/components/requestBodies/NonExistentRequestBody"
3867 },
3868 "responses": {"200": {"description": "OK"}}
3869 }
3870 }
3871 },
3872 "components": {}
3873 });
3874 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3875 let registry = OpenApiRouteRegistry::new(spec);
3876
3877 let body = json!({"name": "test"});
3879 let result = registry.validate_request_with_all(
3880 "/users",
3881 "POST",
3882 &Map::new(),
3883 &Map::new(),
3884 &Map::new(),
3885 &Map::new(),
3886 Some(&body),
3887 );
3888 assert!(result.is_err());
3889 }
3890
3891 #[test]
3892 fn test_validate_request_body_provided_when_not_expected() {
3893 let spec_json = json!({
3895 "openapi": "3.0.0",
3896 "info": {"title": "Test API", "version": "1.0.0"},
3897 "paths": {
3898 "/users": {
3899 "get": {
3900 "responses": {"200": {"description": "OK"}}
3901 }
3902 }
3903 }
3904 });
3905 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3906 let registry = OpenApiRouteRegistry::new(spec);
3907
3908 let body = json!({"extra": "data"});
3910 let result = registry.validate_request_with_all(
3911 "/users",
3912 "GET",
3913 &Map::new(),
3914 &Map::new(),
3915 &Map::new(),
3916 &Map::new(),
3917 Some(&body),
3918 );
3919 assert!(result.is_ok());
3921 }
3922
3923 #[test]
3924 fn test_get_operation() {
3925 let spec_json = json!({
3927 "openapi": "3.0.0",
3928 "info": {"title": "Test API", "version": "1.0.0"},
3929 "paths": {
3930 "/users": {
3931 "get": {
3932 "operationId": "getUsers",
3933 "summary": "Get users",
3934 "responses": {"200": {"description": "OK"}}
3935 }
3936 }
3937 }
3938 });
3939 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3940 let registry = OpenApiRouteRegistry::new(spec);
3941
3942 let operation = registry.get_operation("/users", "GET");
3944 assert!(operation.is_some());
3945 assert_eq!(operation.unwrap().method, "GET");
3946
3947 assert!(registry.get_operation("/nonexistent", "GET").is_none());
3949 }
3950
3951 #[test]
3952 fn test_extract_path_parameters() {
3953 let spec_json = json!({
3955 "openapi": "3.0.0",
3956 "info": {"title": "Test API", "version": "1.0.0"},
3957 "paths": {
3958 "/users/{id}": {
3959 "get": {
3960 "parameters": [
3961 {
3962 "name": "id",
3963 "in": "path",
3964 "required": true,
3965 "schema": {"type": "string"}
3966 }
3967 ],
3968 "responses": {"200": {"description": "OK"}}
3969 }
3970 }
3971 }
3972 });
3973 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3974 let registry = OpenApiRouteRegistry::new(spec);
3975
3976 let params = registry.extract_path_parameters("/users/123", "GET");
3978 assert_eq!(params.get("id"), Some(&"123".to_string()));
3979
3980 let empty_params = registry.extract_path_parameters("/users", "GET");
3982 assert!(empty_params.is_empty());
3983 }
3984
3985 #[test]
3986 fn extract_path_parameters_prefers_static_route_and_rejects_empty() {
3987 let spec_json = json!({
3990 "openapi": "3.0.0",
3991 "info": {"title": "Test API", "version": "1.0.0"},
3992 "paths": {
3993 "/users/{id}": { "get": { "responses": {"200": {"description": "OK"}} } },
3994 "/users/me": { "get": { "responses": {"200": {"description": "OK"}} } }
3995 }
3996 });
3997 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3998 let registry = OpenApiRouteRegistry::new(spec);
3999
4000 let me = registry.extract_path_parameters("/users/me", "GET");
4002 assert!(me.get("id").is_none(), "literal route should win, got {me:?}");
4003
4004 let by_id = registry.extract_path_parameters("/users/123", "GET");
4006 assert_eq!(by_id.get("id"), Some(&"123".to_string()));
4007
4008 let trailing = registry.extract_path_parameters("/users/", "GET");
4010 assert!(
4011 trailing.is_empty(),
4012 "empty trailing segment should not bind id, got {trailing:?}"
4013 );
4014 }
4015
4016 #[test]
4017 fn test_extract_path_parameters_multiple_params() {
4018 let spec_json = json!({
4020 "openapi": "3.0.0",
4021 "info": {"title": "Test API", "version": "1.0.0"},
4022 "paths": {
4023 "/users/{userId}/posts/{postId}": {
4024 "get": {
4025 "parameters": [
4026 {
4027 "name": "userId",
4028 "in": "path",
4029 "required": true,
4030 "schema": {"type": "string"}
4031 },
4032 {
4033 "name": "postId",
4034 "in": "path",
4035 "required": true,
4036 "schema": {"type": "string"}
4037 }
4038 ],
4039 "responses": {"200": {"description": "OK"}}
4040 }
4041 }
4042 }
4043 });
4044 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4045 let registry = OpenApiRouteRegistry::new(spec);
4046
4047 let params = registry.extract_path_parameters("/users/123/posts/456", "GET");
4049 assert_eq!(params.get("userId"), Some(&"123".to_string()));
4050 assert_eq!(params.get("postId"), Some(&"456".to_string()));
4051 }
4052
4053 #[test]
4054 fn test_validate_request_route_not_found() {
4055 let spec_json = json!({
4057 "openapi": "3.0.0",
4058 "info": {"title": "Test API", "version": "1.0.0"},
4059 "paths": {
4060 "/users": {
4061 "get": {
4062 "responses": {"200": {"description": "OK"}}
4063 }
4064 }
4065 }
4066 });
4067 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4068 let registry = OpenApiRouteRegistry::new(spec);
4069
4070 let result = registry.validate_request_with_all(
4072 "/nonexistent",
4073 "GET",
4074 &Map::new(),
4075 &Map::new(),
4076 &Map::new(),
4077 &Map::new(),
4078 None,
4079 );
4080 assert!(result.is_err());
4081 assert!(result.unwrap_err().to_string().contains("not found"));
4082 }
4083
4084 #[test]
4085 fn test_validate_request_with_path_parameters() {
4086 let spec_json = json!({
4088 "openapi": "3.0.0",
4089 "info": {"title": "Test API", "version": "1.0.0"},
4090 "paths": {
4091 "/users/{id}": {
4092 "get": {
4093 "parameters": [
4094 {
4095 "name": "id",
4096 "in": "path",
4097 "required": true,
4098 "schema": {"type": "string", "minLength": 1}
4099 }
4100 ],
4101 "responses": {"200": {"description": "OK"}}
4102 }
4103 }
4104 }
4105 });
4106 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4107 let registry = OpenApiRouteRegistry::new(spec);
4108
4109 let mut path_params = Map::new();
4111 path_params.insert("id".to_string(), json!("123"));
4112 let result = registry.validate_request_with_all(
4113 "/users/{id}",
4114 "GET",
4115 &path_params,
4116 &Map::new(),
4117 &Map::new(),
4118 &Map::new(),
4119 None,
4120 );
4121 assert!(result.is_ok());
4122 }
4123
4124 #[test]
4125 fn test_validate_request_with_query_parameters() {
4126 let spec_json = json!({
4128 "openapi": "3.0.0",
4129 "info": {"title": "Test API", "version": "1.0.0"},
4130 "paths": {
4131 "/users": {
4132 "get": {
4133 "parameters": [
4134 {
4135 "name": "page",
4136 "in": "query",
4137 "required": true,
4138 "schema": {"type": "integer", "minimum": 1}
4139 }
4140 ],
4141 "responses": {"200": {"description": "OK"}}
4142 }
4143 }
4144 }
4145 });
4146 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4147 let registry = OpenApiRouteRegistry::new(spec);
4148
4149 let mut query_params = Map::new();
4151 query_params.insert("page".to_string(), json!(1));
4152 let result = registry.validate_request_with_all(
4153 "/users",
4154 "GET",
4155 &Map::new(),
4156 &query_params,
4157 &Map::new(),
4158 &Map::new(),
4159 None,
4160 );
4161 assert!(result.is_ok());
4162 }
4163
4164 #[test]
4165 fn test_validate_request_with_header_parameters() {
4166 let spec_json = json!({
4168 "openapi": "3.0.0",
4169 "info": {"title": "Test API", "version": "1.0.0"},
4170 "paths": {
4171 "/users": {
4172 "get": {
4173 "parameters": [
4174 {
4175 "name": "X-API-Key",
4176 "in": "header",
4177 "required": true,
4178 "schema": {"type": "string"}
4179 }
4180 ],
4181 "responses": {"200": {"description": "OK"}}
4182 }
4183 }
4184 }
4185 });
4186 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4187 let registry = OpenApiRouteRegistry::new(spec);
4188
4189 let mut header_params = Map::new();
4191 header_params.insert("X-API-Key".to_string(), json!("secret-key"));
4192 let result = registry.validate_request_with_all(
4193 "/users",
4194 "GET",
4195 &Map::new(),
4196 &Map::new(),
4197 &header_params,
4198 &Map::new(),
4199 None,
4200 );
4201 assert!(result.is_ok());
4202 }
4203
4204 #[test]
4205 fn test_validate_request_with_cookie_parameters() {
4206 let spec_json = json!({
4208 "openapi": "3.0.0",
4209 "info": {"title": "Test API", "version": "1.0.0"},
4210 "paths": {
4211 "/users": {
4212 "get": {
4213 "parameters": [
4214 {
4215 "name": "sessionId",
4216 "in": "cookie",
4217 "required": true,
4218 "schema": {"type": "string"}
4219 }
4220 ],
4221 "responses": {"200": {"description": "OK"}}
4222 }
4223 }
4224 }
4225 });
4226 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4227 let registry = OpenApiRouteRegistry::new(spec);
4228
4229 let mut cookie_params = Map::new();
4231 cookie_params.insert("sessionId".to_string(), json!("abc123"));
4232 let result = registry.validate_request_with_all(
4233 "/users",
4234 "GET",
4235 &Map::new(),
4236 &Map::new(),
4237 &Map::new(),
4238 &cookie_params,
4239 None,
4240 );
4241 assert!(result.is_ok());
4242 }
4243
4244 #[test]
4245 fn test_validate_request_no_errors_early_return() {
4246 let spec_json = json!({
4248 "openapi": "3.0.0",
4249 "info": {"title": "Test API", "version": "1.0.0"},
4250 "paths": {
4251 "/users": {
4252 "get": {
4253 "responses": {"200": {"description": "OK"}}
4254 }
4255 }
4256 }
4257 });
4258 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4259 let registry = OpenApiRouteRegistry::new(spec);
4260
4261 let result = registry.validate_request_with_all(
4263 "/users",
4264 "GET",
4265 &Map::new(),
4266 &Map::new(),
4267 &Map::new(),
4268 &Map::new(),
4269 None,
4270 );
4271 assert!(result.is_ok());
4272 }
4273
4274 #[test]
4275 fn test_validate_request_query_parameter_different_styles() {
4276 let spec_json = json!({
4278 "openapi": "3.0.0",
4279 "info": {"title": "Test API", "version": "1.0.0"},
4280 "paths": {
4281 "/users": {
4282 "get": {
4283 "parameters": [
4284 {
4285 "name": "tags",
4286 "in": "query",
4287 "style": "pipeDelimited",
4288 "schema": {
4289 "type": "array",
4290 "items": {"type": "string"}
4291 }
4292 }
4293 ],
4294 "responses": {"200": {"description": "OK"}}
4295 }
4296 }
4297 }
4298 });
4299 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4300 let registry = OpenApiRouteRegistry::new(spec);
4301
4302 let mut query_params = Map::new();
4304 query_params.insert("tags".to_string(), json!(["tag1", "tag2"]));
4305 let result = registry.validate_request_with_all(
4306 "/users",
4307 "GET",
4308 &Map::new(),
4309 &query_params,
4310 &Map::new(),
4311 &Map::new(),
4312 None,
4313 );
4314 assert!(result.is_ok() || result.is_err()); }
4317}