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