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