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