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