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