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