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