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