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