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