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 tracing::debug!(
1720 "MockAI generated response with status: {}",
1721 mockai_response.status_code
1722 );
1723 return (
1724 axum::http::StatusCode::from_u16(
1725 mockai_response.status_code,
1726 )
1727 .unwrap_or(axum::http::StatusCode::OK),
1728 Json(mockai_response.body),
1729 );
1730 }
1731 }
1732 Err(e) => {
1733 tracing::warn!(
1734 "MockAI processing failed for {} {}: {}, falling back to standard response",
1735 route.method,
1736 route.path,
1737 e
1738 );
1739 }
1741 }
1742 }
1743 } else {
1744 tracing::debug!(
1745 "Skipping MockAI for {} request {} - using OpenAPI response generation",
1746 method_upper,
1747 route.path
1748 );
1749 }
1750
1751 let (status, response) = route.mock_response_with_status();
1753 (
1754 axum::http::StatusCode::from_u16(status)
1755 .unwrap_or(axum::http::StatusCode::OK),
1756 Json(response),
1757 )
1758 }
1759 };
1760
1761 match route.method.as_str() {
1762 "GET" => {
1763 router = router.route(&route.path, get(handler));
1764 }
1765 "POST" => {
1766 router = router.route(&route.path, post(handler));
1767 }
1768 "PUT" => {
1769 router = router.route(&route.path, put(handler));
1770 }
1771 "DELETE" => {
1772 router = router.route(&route.path, delete(handler));
1773 }
1774 "PATCH" => {
1775 router = router.route(&route.path, patch(handler));
1776 }
1777 _ => tracing::warn!("Unsupported HTTP method for MockAI: {}", route.method),
1778 }
1779 }
1780
1781 router
1782 }
1783}
1784
1785async fn extract_multipart_from_bytes(
1790 body: &axum::body::Bytes,
1791 headers: &HeaderMap,
1792) -> Result<(HashMap<String, Value>, HashMap<String, String>)> {
1793 let boundary = headers
1795 .get(axum::http::header::CONTENT_TYPE)
1796 .and_then(|v| v.to_str().ok())
1797 .and_then(|ct| {
1798 ct.split(';').find_map(|part| {
1799 let part = part.trim();
1800 if part.starts_with("boundary=") {
1801 Some(part.strip_prefix("boundary=").unwrap_or("").trim_matches('"'))
1802 } else {
1803 None
1804 }
1805 })
1806 })
1807 .ok_or_else(|| Error::generic("Missing boundary in Content-Type header"))?;
1808
1809 let mut fields = HashMap::new();
1810 let mut files = HashMap::new();
1811
1812 let boundary_prefix = format!("--{}", boundary).into_bytes();
1815 let boundary_line = format!("\r\n--{}\r\n", boundary).into_bytes();
1816 let end_boundary = format!("\r\n--{}--\r\n", boundary).into_bytes();
1817
1818 let mut pos = 0;
1820 let mut parts = Vec::new();
1821
1822 if body.starts_with(&boundary_prefix) {
1824 if let Some(first_crlf) = body.iter().position(|&b| b == b'\r') {
1825 pos = first_crlf + 2; }
1827 }
1828
1829 while let Some(boundary_pos) = body[pos..]
1831 .windows(boundary_line.len())
1832 .position(|window| window == boundary_line.as_slice())
1833 {
1834 let actual_pos = pos + boundary_pos;
1835 if actual_pos > pos {
1836 parts.push((pos, actual_pos));
1837 }
1838 pos = actual_pos + boundary_line.len();
1839 }
1840
1841 if let Some(end_pos) = body[pos..]
1843 .windows(end_boundary.len())
1844 .position(|window| window == end_boundary.as_slice())
1845 {
1846 let actual_end = pos + end_pos;
1847 if actual_end > pos {
1848 parts.push((pos, actual_end));
1849 }
1850 } else if pos < body.len() {
1851 parts.push((pos, body.len()));
1853 }
1854
1855 for (start, end) in parts {
1857 let part_data = &body[start..end];
1858
1859 let separator = b"\r\n\r\n";
1861 if let Some(sep_pos) =
1862 part_data.windows(separator.len()).position(|window| window == separator)
1863 {
1864 let header_bytes = &part_data[..sep_pos];
1865 let body_start = sep_pos + separator.len();
1866 let body_data = &part_data[body_start..];
1867
1868 let header_str = String::from_utf8_lossy(header_bytes);
1870 let mut field_name = None;
1871 let mut filename = None;
1872
1873 for header_line in header_str.lines() {
1874 if header_line.starts_with("Content-Disposition:") {
1875 if let Some(name_start) = header_line.find("name=\"") {
1877 let name_start = name_start + 6;
1878 if let Some(name_end) = header_line[name_start..].find('"') {
1879 field_name =
1880 Some(header_line[name_start..name_start + name_end].to_string());
1881 }
1882 }
1883
1884 if let Some(file_start) = header_line.find("filename=\"") {
1886 let file_start = file_start + 10;
1887 if let Some(file_end) = header_line[file_start..].find('"') {
1888 filename =
1889 Some(header_line[file_start..file_start + file_end].to_string());
1890 }
1891 }
1892 }
1893 }
1894
1895 if let Some(name) = field_name {
1896 if let Some(file) = filename {
1897 let temp_dir = std::env::temp_dir().join("mockforge-uploads");
1899 std::fs::create_dir_all(&temp_dir).map_err(|e| {
1900 Error::generic(format!("Failed to create temp directory: {}", e))
1901 })?;
1902
1903 let file_path = temp_dir.join(format!("{}_{}", uuid::Uuid::new_v4(), file));
1904 std::fs::write(&file_path, body_data)
1905 .map_err(|e| Error::generic(format!("Failed to write file: {}", e)))?;
1906
1907 let file_path_str = file_path.to_string_lossy().to_string();
1908 files.insert(name.clone(), file_path_str.clone());
1909 fields.insert(name, Value::String(file_path_str));
1910 } else {
1911 let body_str = body_data
1914 .strip_suffix(b"\r\n")
1915 .or_else(|| body_data.strip_suffix(b"\n"))
1916 .unwrap_or(body_data);
1917
1918 if let Ok(field_value) = String::from_utf8(body_str.to_vec()) {
1919 fields.insert(name, Value::String(field_value.trim().to_string()));
1920 } else {
1921 use base64::{engine::general_purpose, Engine as _};
1923 fields.insert(
1924 name,
1925 Value::String(general_purpose::STANDARD.encode(body_str)),
1926 );
1927 }
1928 }
1929 }
1930 }
1931 }
1932
1933 Ok((fields, files))
1934}
1935
1936static LAST_ERRORS: Lazy<Mutex<VecDeque<Value>>> =
1937 Lazy::new(|| Mutex::new(VecDeque::with_capacity(20)));
1938
1939pub fn record_validation_error(v: &Value) {
1941 if let Ok(mut q) = LAST_ERRORS.lock() {
1942 if q.len() >= 20 {
1943 q.pop_front();
1944 }
1945 q.push_back(v.clone());
1946 }
1947 }
1949
1950pub fn get_last_validation_error() -> Option<Value> {
1952 LAST_ERRORS.lock().ok()?.back().cloned()
1953}
1954
1955pub fn get_validation_errors() -> Vec<Value> {
1957 LAST_ERRORS.lock().map(|q| q.iter().cloned().collect()).unwrap_or_default()
1958}
1959
1960fn coerce_value_for_schema(value: &Value, schema: &openapiv3::Schema) -> Value {
1965 match value {
1967 Value::String(s) => {
1968 if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
1970 &schema.schema_kind
1971 {
1972 if s.contains(',') {
1973 let parts: Vec<&str> = s.split(',').map(|s| s.trim()).collect();
1975 let mut array_values = Vec::new();
1976
1977 for part in parts {
1978 if let Some(items_schema) = &array_type.items {
1980 if let Some(items_schema_obj) = items_schema.as_item() {
1981 let part_value = Value::String(part.to_string());
1982 let coerced_part =
1983 coerce_value_for_schema(&part_value, items_schema_obj);
1984 array_values.push(coerced_part);
1985 } else {
1986 array_values.push(Value::String(part.to_string()));
1988 }
1989 } else {
1990 array_values.push(Value::String(part.to_string()));
1992 }
1993 }
1994 return Value::Array(array_values);
1995 }
1996 }
1997
1998 match &schema.schema_kind {
2000 openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
2001 value.clone()
2003 }
2004 openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => {
2005 if let Ok(n) = s.parse::<f64>() {
2007 if let Some(num) = serde_json::Number::from_f64(n) {
2008 return Value::Number(num);
2009 }
2010 }
2011 value.clone()
2012 }
2013 openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => {
2014 if let Ok(n) = s.parse::<i64>() {
2016 if let Some(num) = serde_json::Number::from_f64(n as f64) {
2017 return Value::Number(num);
2018 }
2019 }
2020 value.clone()
2021 }
2022 openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => {
2023 match s.to_lowercase().as_str() {
2025 "true" | "1" | "yes" | "on" => Value::Bool(true),
2026 "false" | "0" | "no" | "off" => Value::Bool(false),
2027 _ => value.clone(),
2028 }
2029 }
2030 _ => {
2031 value.clone()
2033 }
2034 }
2035 }
2036 _ => value.clone(),
2037 }
2038}
2039
2040fn coerce_by_style(value: &Value, schema: &openapiv3::Schema, style: Option<&str>) -> Value {
2042 match value {
2044 Value::String(s) => {
2045 if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) =
2047 &schema.schema_kind
2048 {
2049 let delimiter = match style {
2050 Some("spaceDelimited") => " ",
2051 Some("pipeDelimited") => "|",
2052 Some("form") | None => ",", _ => ",", };
2055
2056 if s.contains(delimiter) {
2057 let parts: Vec<&str> = s.split(delimiter).map(|s| s.trim()).collect();
2059 let mut array_values = Vec::new();
2060
2061 for part in parts {
2062 if let Some(items_schema) = &array_type.items {
2064 if let Some(items_schema_obj) = items_schema.as_item() {
2065 let part_value = Value::String(part.to_string());
2066 let coerced_part =
2067 coerce_by_style(&part_value, items_schema_obj, style);
2068 array_values.push(coerced_part);
2069 } else {
2070 array_values.push(Value::String(part.to_string()));
2072 }
2073 } else {
2074 array_values.push(Value::String(part.to_string()));
2076 }
2077 }
2078 return Value::Array(array_values);
2079 }
2080 }
2081
2082 if let Ok(n) = s.parse::<f64>() {
2084 if let Some(num) = serde_json::Number::from_f64(n) {
2085 return Value::Number(num);
2086 }
2087 }
2088 match s.to_lowercase().as_str() {
2090 "true" | "1" | "yes" | "on" => return Value::Bool(true),
2091 "false" | "0" | "no" | "off" => return Value::Bool(false),
2092 _ => {}
2093 }
2094 value.clone()
2096 }
2097 _ => value.clone(),
2098 }
2099}
2100
2101fn build_deep_object(name: &str, params: &Map<String, Value>) -> Option<Value> {
2103 let prefix = format!("{}[", name);
2104 let mut obj = Map::new();
2105 for (k, v) in params.iter() {
2106 if let Some(rest) = k.strip_prefix(&prefix) {
2107 if let Some(key) = rest.strip_suffix(']') {
2108 obj.insert(key.to_string(), v.clone());
2109 }
2110 }
2111 }
2112 if obj.is_empty() {
2113 None
2114 } else {
2115 Some(Value::Object(obj))
2116 }
2117}
2118
2119#[allow(clippy::too_many_arguments)]
2125fn generate_enhanced_422_response(
2126 validator: &OpenApiRouteRegistry,
2127 path_template: &str,
2128 method: &str,
2129 body: Option<&Value>,
2130 path_params: &Map<String, Value>,
2131 query_params: &Map<String, Value>,
2132 header_params: &Map<String, Value>,
2133 cookie_params: &Map<String, Value>,
2134) -> Value {
2135 let mut field_errors = Vec::new();
2136
2137 if let Some(route) = validator.get_route(path_template, method) {
2139 if let Some(schema) = &route.operation.request_body {
2141 if let Some(value) = body {
2142 if let Some(content) =
2143 schema.as_item().and_then(|rb| rb.content.get("application/json"))
2144 {
2145 if let Some(_schema_ref) = &content.schema {
2146 if serde_json::from_value::<Value>(value.clone()).is_err() {
2148 field_errors.push(json!({
2149 "path": "body",
2150 "message": "invalid JSON"
2151 }));
2152 }
2153 }
2154 }
2155 } else {
2156 field_errors.push(json!({
2157 "path": "body",
2158 "expected": "object",
2159 "found": "missing",
2160 "message": "Request body is required but not provided"
2161 }));
2162 }
2163 }
2164
2165 for param_ref in &route.operation.parameters {
2167 if let Some(param) = param_ref.as_item() {
2168 match param {
2169 openapiv3::Parameter::Path { parameter_data, .. } => {
2170 validate_parameter_detailed(
2171 parameter_data,
2172 path_params,
2173 "path",
2174 "path parameter",
2175 &mut field_errors,
2176 );
2177 }
2178 openapiv3::Parameter::Query { parameter_data, .. } => {
2179 let deep_value = if Some("form") == Some("deepObject") {
2180 build_deep_object(¶meter_data.name, query_params)
2181 } else {
2182 None
2183 };
2184 validate_parameter_detailed_with_deep(
2185 parameter_data,
2186 query_params,
2187 "query",
2188 "query parameter",
2189 deep_value,
2190 &mut field_errors,
2191 );
2192 }
2193 openapiv3::Parameter::Header { parameter_data, .. } => {
2194 validate_parameter_detailed(
2195 parameter_data,
2196 header_params,
2197 "header",
2198 "header parameter",
2199 &mut field_errors,
2200 );
2201 }
2202 openapiv3::Parameter::Cookie { parameter_data, .. } => {
2203 validate_parameter_detailed(
2204 parameter_data,
2205 cookie_params,
2206 "cookie",
2207 "cookie parameter",
2208 &mut field_errors,
2209 );
2210 }
2211 }
2212 }
2213 }
2214 }
2215
2216 json!({
2218 "error": "Schema validation failed",
2219 "details": field_errors,
2220 "method": method,
2221 "path": path_template,
2222 "timestamp": Utc::now().to_rfc3339(),
2223 "validation_type": "openapi_schema"
2224 })
2225}
2226
2227fn validate_parameter(
2229 parameter_data: &openapiv3::ParameterData,
2230 params_map: &Map<String, Value>,
2231 prefix: &str,
2232 aggregate: bool,
2233 errors: &mut Vec<String>,
2234 details: &mut Vec<Value>,
2235) {
2236 match params_map.get(¶meter_data.name) {
2237 Some(v) => {
2238 if let ParameterSchemaOrContent::Schema(s) = ¶meter_data.format {
2239 if let Some(schema) = s.as_item() {
2240 let coerced = coerce_value_for_schema(v, schema);
2241 if let Err(validation_error) =
2243 OpenApiSchema::new(schema.clone()).validate(&coerced)
2244 {
2245 let error_msg = validation_error.to_string();
2246 errors.push(format!(
2247 "{} parameter '{}' validation failed: {}",
2248 prefix, parameter_data.name, error_msg
2249 ));
2250 if aggregate {
2251 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
2252 }
2253 }
2254 }
2255 }
2256 }
2257 None => {
2258 if parameter_data.required {
2259 errors.push(format!(
2260 "missing required {} parameter '{}'",
2261 prefix, parameter_data.name
2262 ));
2263 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
2264 }
2265 }
2266 }
2267}
2268
2269#[allow(clippy::too_many_arguments)]
2271fn validate_parameter_with_deep_object(
2272 parameter_data: &openapiv3::ParameterData,
2273 params_map: &Map<String, Value>,
2274 prefix: &str,
2275 deep_value: Option<Value>,
2276 style: Option<&str>,
2277 aggregate: bool,
2278 errors: &mut Vec<String>,
2279 details: &mut Vec<Value>,
2280) {
2281 match deep_value.as_ref().or_else(|| params_map.get(¶meter_data.name)) {
2282 Some(v) => {
2283 if let ParameterSchemaOrContent::Schema(s) = ¶meter_data.format {
2284 if let Some(schema) = s.as_item() {
2285 let coerced = coerce_by_style(v, schema, style); if let Err(validation_error) =
2288 OpenApiSchema::new(schema.clone()).validate(&coerced)
2289 {
2290 let error_msg = validation_error.to_string();
2291 errors.push(format!(
2292 "{} parameter '{}' validation failed: {}",
2293 prefix, parameter_data.name, error_msg
2294 ));
2295 if aggregate {
2296 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"schema_validation","message":error_msg}));
2297 }
2298 }
2299 }
2300 }
2301 }
2302 None => {
2303 if parameter_data.required {
2304 errors.push(format!(
2305 "missing required {} parameter '{}'",
2306 prefix, parameter_data.name
2307 ));
2308 details.push(serde_json::json!({"path":format!("{}.{}", prefix, parameter_data.name),"code":"required","message":"Missing required parameter"}));
2309 }
2310 }
2311 }
2312}
2313
2314fn validate_parameter_detailed(
2316 parameter_data: &openapiv3::ParameterData,
2317 params_map: &Map<String, Value>,
2318 location: &str,
2319 value_type: &str,
2320 field_errors: &mut Vec<Value>,
2321) {
2322 match params_map.get(¶meter_data.name) {
2323 Some(value) => {
2324 if let ParameterSchemaOrContent::Schema(schema) = ¶meter_data.format {
2325 let details: Vec<Value> = Vec::new();
2327 let param_path = format!("{}.{}", location, parameter_data.name);
2328
2329 if let Some(schema_ref) = schema.as_item() {
2331 let coerced_value = coerce_value_for_schema(value, schema_ref);
2332 if let Err(validation_error) =
2334 OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
2335 {
2336 field_errors.push(json!({
2337 "path": param_path,
2338 "expected": "valid according to schema",
2339 "found": coerced_value,
2340 "message": validation_error.to_string()
2341 }));
2342 }
2343 }
2344
2345 for detail in details {
2346 field_errors.push(json!({
2347 "path": detail["path"],
2348 "expected": detail["expected_type"],
2349 "found": detail["value"],
2350 "message": detail["message"]
2351 }));
2352 }
2353 }
2354 }
2355 None => {
2356 if parameter_data.required {
2357 field_errors.push(json!({
2358 "path": format!("{}.{}", location, parameter_data.name),
2359 "expected": "value",
2360 "found": "missing",
2361 "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
2362 }));
2363 }
2364 }
2365 }
2366}
2367
2368fn validate_parameter_detailed_with_deep(
2370 parameter_data: &openapiv3::ParameterData,
2371 params_map: &Map<String, Value>,
2372 location: &str,
2373 value_type: &str,
2374 deep_value: Option<Value>,
2375 field_errors: &mut Vec<Value>,
2376) {
2377 match deep_value.as_ref().or_else(|| params_map.get(¶meter_data.name)) {
2378 Some(value) => {
2379 if let ParameterSchemaOrContent::Schema(schema) = ¶meter_data.format {
2380 let details: Vec<Value> = Vec::new();
2382 let param_path = format!("{}.{}", location, parameter_data.name);
2383
2384 if let Some(schema_ref) = schema.as_item() {
2386 let coerced_value = coerce_by_style(value, schema_ref, Some("form")); if let Err(validation_error) =
2389 OpenApiSchema::new(schema_ref.clone()).validate(&coerced_value)
2390 {
2391 field_errors.push(json!({
2392 "path": param_path,
2393 "expected": "valid according to schema",
2394 "found": coerced_value,
2395 "message": validation_error.to_string()
2396 }));
2397 }
2398 }
2399
2400 for detail in details {
2401 field_errors.push(json!({
2402 "path": detail["path"],
2403 "expected": detail["expected_type"],
2404 "found": detail["value"],
2405 "message": detail["message"]
2406 }));
2407 }
2408 }
2409 }
2410 None => {
2411 if parameter_data.required {
2412 field_errors.push(json!({
2413 "path": format!("{}.{}", location, parameter_data.name),
2414 "expected": "value",
2415 "found": "missing",
2416 "message": format!("Missing required {} '{}'", value_type, parameter_data.name)
2417 }));
2418 }
2419 }
2420 }
2421}
2422
2423pub async fn create_registry_from_file<P: AsRef<std::path::Path>>(
2425 path: P,
2426) -> Result<OpenApiRouteRegistry> {
2427 let spec = OpenApiSpec::from_file(path).await?;
2428 spec.validate()?;
2429 Ok(OpenApiRouteRegistry::new(spec))
2430}
2431
2432pub fn create_registry_from_json(json: Value) -> Result<OpenApiRouteRegistry> {
2434 let spec = OpenApiSpec::from_json(json)?;
2435 spec.validate()?;
2436 Ok(OpenApiRouteRegistry::new(spec))
2437}
2438
2439#[cfg(test)]
2440mod tests {
2441 use super::*;
2442 use serde_json::json;
2443 use tempfile::TempDir;
2444
2445 #[tokio::test]
2446 async fn test_registry_creation() {
2447 let spec_json = json!({
2448 "openapi": "3.0.0",
2449 "info": {
2450 "title": "Test API",
2451 "version": "1.0.0"
2452 },
2453 "paths": {
2454 "/users": {
2455 "get": {
2456 "summary": "Get users",
2457 "responses": {
2458 "200": {
2459 "description": "Success",
2460 "content": {
2461 "application/json": {
2462 "schema": {
2463 "type": "array",
2464 "items": {
2465 "type": "object",
2466 "properties": {
2467 "id": {"type": "integer"},
2468 "name": {"type": "string"}
2469 }
2470 }
2471 }
2472 }
2473 }
2474 }
2475 }
2476 },
2477 "post": {
2478 "summary": "Create user",
2479 "requestBody": {
2480 "content": {
2481 "application/json": {
2482 "schema": {
2483 "type": "object",
2484 "properties": {
2485 "name": {"type": "string"}
2486 },
2487 "required": ["name"]
2488 }
2489 }
2490 }
2491 },
2492 "responses": {
2493 "201": {
2494 "description": "Created",
2495 "content": {
2496 "application/json": {
2497 "schema": {
2498 "type": "object",
2499 "properties": {
2500 "id": {"type": "integer"},
2501 "name": {"type": "string"}
2502 }
2503 }
2504 }
2505 }
2506 }
2507 }
2508 }
2509 },
2510 "/users/{id}": {
2511 "get": {
2512 "summary": "Get user by ID",
2513 "parameters": [
2514 {
2515 "name": "id",
2516 "in": "path",
2517 "required": true,
2518 "schema": {"type": "integer"}
2519 }
2520 ],
2521 "responses": {
2522 "200": {
2523 "description": "Success",
2524 "content": {
2525 "application/json": {
2526 "schema": {
2527 "type": "object",
2528 "properties": {
2529 "id": {"type": "integer"},
2530 "name": {"type": "string"}
2531 }
2532 }
2533 }
2534 }
2535 }
2536 }
2537 }
2538 }
2539 }
2540 });
2541
2542 let registry = create_registry_from_json(spec_json).unwrap();
2543
2544 assert_eq!(registry.paths().len(), 2);
2546 assert!(registry.paths().contains(&"/users".to_string()));
2547 assert!(registry.paths().contains(&"/users/{id}".to_string()));
2548
2549 assert_eq!(registry.methods().len(), 2);
2550 assert!(registry.methods().contains(&"GET".to_string()));
2551 assert!(registry.methods().contains(&"POST".to_string()));
2552
2553 let get_users_route = registry.get_route("/users", "GET").unwrap();
2555 assert_eq!(get_users_route.method, "GET");
2556 assert_eq!(get_users_route.path, "/users");
2557
2558 let post_users_route = registry.get_route("/users", "POST").unwrap();
2559 assert_eq!(post_users_route.method, "POST");
2560 assert!(post_users_route.operation.request_body.is_some());
2561
2562 let user_by_id_route = registry.get_route("/users/{id}", "GET").unwrap();
2564 assert_eq!(user_by_id_route.axum_path(), "/users/{id}");
2565 }
2566
2567 #[tokio::test]
2568 async fn test_validate_request_with_params_and_formats() {
2569 let spec_json = json!({
2570 "openapi": "3.0.0",
2571 "info": { "title": "Test API", "version": "1.0.0" },
2572 "paths": {
2573 "/users/{id}": {
2574 "post": {
2575 "parameters": [
2576 { "name": "id", "in": "path", "required": true, "schema": {"type": "string"} },
2577 { "name": "q", "in": "query", "required": false, "schema": {"type": "integer"} }
2578 ],
2579 "requestBody": {
2580 "content": {
2581 "application/json": {
2582 "schema": {
2583 "type": "object",
2584 "required": ["email", "website"],
2585 "properties": {
2586 "email": {"type": "string", "format": "email"},
2587 "website": {"type": "string", "format": "uri"}
2588 }
2589 }
2590 }
2591 }
2592 },
2593 "responses": {"200": {"description": "ok"}}
2594 }
2595 }
2596 }
2597 });
2598
2599 let registry = create_registry_from_json(spec_json).unwrap();
2600 let mut path_params = Map::new();
2601 path_params.insert("id".to_string(), json!("abc"));
2602 let mut query_params = Map::new();
2603 query_params.insert("q".to_string(), json!(123));
2604
2605 let body = json!({"email":"a@b.co","website":"https://example.com"});
2607 assert!(registry
2608 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2609 .is_ok());
2610
2611 let bad_email = json!({"email":"not-an-email","website":"https://example.com"});
2613 assert!(registry
2614 .validate_request_with(
2615 "/users/{id}",
2616 "POST",
2617 &path_params,
2618 &query_params,
2619 Some(&bad_email)
2620 )
2621 .is_err());
2622
2623 let empty_path_params = Map::new();
2625 assert!(registry
2626 .validate_request_with(
2627 "/users/{id}",
2628 "POST",
2629 &empty_path_params,
2630 &query_params,
2631 Some(&body)
2632 )
2633 .is_err());
2634 }
2635
2636 #[tokio::test]
2637 async fn test_ref_resolution_for_params_and_body() {
2638 let spec_json = json!({
2639 "openapi": "3.0.0",
2640 "info": { "title": "Ref API", "version": "1.0.0" },
2641 "components": {
2642 "schemas": {
2643 "EmailWebsite": {
2644 "type": "object",
2645 "required": ["email", "website"],
2646 "properties": {
2647 "email": {"type": "string", "format": "email"},
2648 "website": {"type": "string", "format": "uri"}
2649 }
2650 }
2651 },
2652 "parameters": {
2653 "PathId": {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}},
2654 "QueryQ": {"name": "q", "in": "query", "required": false, "schema": {"type": "integer"}}
2655 },
2656 "requestBodies": {
2657 "CreateUser": {
2658 "content": {
2659 "application/json": {
2660 "schema": {"$ref": "#/components/schemas/EmailWebsite"}
2661 }
2662 }
2663 }
2664 }
2665 },
2666 "paths": {
2667 "/users/{id}": {
2668 "post": {
2669 "parameters": [
2670 {"$ref": "#/components/parameters/PathId"},
2671 {"$ref": "#/components/parameters/QueryQ"}
2672 ],
2673 "requestBody": {"$ref": "#/components/requestBodies/CreateUser"},
2674 "responses": {"200": {"description": "ok"}}
2675 }
2676 }
2677 }
2678 });
2679
2680 let registry = create_registry_from_json(spec_json).unwrap();
2681 let mut path_params = Map::new();
2682 path_params.insert("id".to_string(), json!("abc"));
2683 let mut query_params = Map::new();
2684 query_params.insert("q".to_string(), json!(7));
2685
2686 let body = json!({"email":"user@example.com","website":"https://example.com"});
2687 assert!(registry
2688 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&body))
2689 .is_ok());
2690
2691 let bad = json!({"email":"nope","website":"https://example.com"});
2692 assert!(registry
2693 .validate_request_with("/users/{id}", "POST", &path_params, &query_params, Some(&bad))
2694 .is_err());
2695 }
2696
2697 #[tokio::test]
2698 async fn test_header_cookie_and_query_coercion() {
2699 let spec_json = json!({
2700 "openapi": "3.0.0",
2701 "info": { "title": "Params API", "version": "1.0.0" },
2702 "paths": {
2703 "/items": {
2704 "get": {
2705 "parameters": [
2706 {"name": "X-Flag", "in": "header", "required": true, "schema": {"type": "boolean"}},
2707 {"name": "session", "in": "cookie", "required": true, "schema": {"type": "string"}},
2708 {"name": "ids", "in": "query", "required": false, "schema": {"type": "array", "items": {"type": "integer"}}}
2709 ],
2710 "responses": {"200": {"description": "ok"}}
2711 }
2712 }
2713 }
2714 });
2715
2716 let registry = create_registry_from_json(spec_json).unwrap();
2717
2718 let path_params = Map::new();
2719 let mut query_params = Map::new();
2720 query_params.insert("ids".to_string(), json!("1,2,3"));
2722 let mut header_params = Map::new();
2723 header_params.insert("X-Flag".to_string(), json!("true"));
2724 let mut cookie_params = Map::new();
2725 cookie_params.insert("session".to_string(), json!("abc123"));
2726
2727 assert!(registry
2728 .validate_request_with_all(
2729 "/items",
2730 "GET",
2731 &path_params,
2732 &query_params,
2733 &header_params,
2734 &cookie_params,
2735 None
2736 )
2737 .is_ok());
2738
2739 let empty_cookie = Map::new();
2741 assert!(registry
2742 .validate_request_with_all(
2743 "/items",
2744 "GET",
2745 &path_params,
2746 &query_params,
2747 &header_params,
2748 &empty_cookie,
2749 None
2750 )
2751 .is_err());
2752
2753 let mut bad_header = Map::new();
2755 bad_header.insert("X-Flag".to_string(), json!("notabool"));
2756 assert!(registry
2757 .validate_request_with_all(
2758 "/items",
2759 "GET",
2760 &path_params,
2761 &query_params,
2762 &bad_header,
2763 &cookie_params,
2764 None
2765 )
2766 .is_err());
2767 }
2768
2769 #[tokio::test]
2770 async fn test_query_styles_space_pipe_deepobject() {
2771 let spec_json = json!({
2772 "openapi": "3.0.0",
2773 "info": { "title": "Query Styles API", "version": "1.0.0" },
2774 "paths": {"/search": {"get": {
2775 "parameters": [
2776 {"name":"tags","in":"query","style":"spaceDelimited","schema":{"type":"array","items":{"type":"string"}}},
2777 {"name":"ids","in":"query","style":"pipeDelimited","schema":{"type":"array","items":{"type":"integer"}}},
2778 {"name":"filter","in":"query","style":"deepObject","schema":{"type":"object","properties":{"color":{"type":"string"}},"required":["color"]}}
2779 ],
2780 "responses": {"200": {"description":"ok"}}
2781 }} }
2782 });
2783
2784 let registry = create_registry_from_json(spec_json).unwrap();
2785
2786 let path_params = Map::new();
2787 let mut query = Map::new();
2788 query.insert("tags".into(), json!("alpha beta gamma"));
2789 query.insert("ids".into(), json!("1|2|3"));
2790 query.insert("filter[color]".into(), json!("red"));
2791
2792 assert!(registry
2793 .validate_request_with("/search", "GET", &path_params, &query, None)
2794 .is_ok());
2795 }
2796
2797 #[tokio::test]
2798 async fn test_oneof_anyof_allof_validation() {
2799 let spec_json = json!({
2800 "openapi": "3.0.0",
2801 "info": { "title": "Composite API", "version": "1.0.0" },
2802 "paths": {
2803 "/composite": {
2804 "post": {
2805 "requestBody": {
2806 "content": {
2807 "application/json": {
2808 "schema": {
2809 "allOf": [
2810 {"type": "object", "required": ["base"], "properties": {"base": {"type": "string"}}}
2811 ],
2812 "oneOf": [
2813 {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"], "not": {"required": ["b"]}},
2814 {"type": "object", "properties": {"b": {"type": "integer"}}, "required": ["b"], "not": {"required": ["a"]}}
2815 ],
2816 "anyOf": [
2817 {"type": "object", "properties": {"flag": {"type": "boolean"}}, "required": ["flag"]},
2818 {"type": "object", "properties": {"extra": {"type": "string"}}, "required": ["extra"]}
2819 ]
2820 }
2821 }
2822 }
2823 },
2824 "responses": {"200": {"description": "ok"}}
2825 }
2826 }
2827 }
2828 });
2829
2830 let registry = create_registry_from_json(spec_json).unwrap();
2831 let ok = json!({"base": "x", "a": 1, "flag": true});
2833 assert!(registry
2834 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&ok))
2835 .is_ok());
2836
2837 let bad_oneof = json!({"base": "x", "a": 1, "b": 2, "flag": false});
2839 assert!(registry
2840 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_oneof))
2841 .is_err());
2842
2843 let bad_anyof = json!({"base": "x", "a": 1});
2845 assert!(registry
2846 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_anyof))
2847 .is_err());
2848
2849 let bad_allof = json!({"a": 1, "flag": true});
2851 assert!(registry
2852 .validate_request_with("/composite", "POST", &Map::new(), &Map::new(), Some(&bad_allof))
2853 .is_err());
2854 }
2855
2856 #[tokio::test]
2857 async fn test_overrides_warn_mode_allows_invalid() {
2858 let spec_json = json!({
2860 "openapi": "3.0.0",
2861 "info": { "title": "Overrides API", "version": "1.0.0" },
2862 "paths": {"/things": {"post": {
2863 "parameters": [{"name":"q","in":"query","required":true,"schema":{"type":"integer"}}],
2864 "responses": {"200": {"description":"ok"}}
2865 }}}
2866 });
2867
2868 let spec = OpenApiSpec::from_json(spec_json).unwrap();
2869 let mut overrides = HashMap::new();
2870 overrides.insert("POST /things".to_string(), ValidationMode::Warn);
2871 let registry = OpenApiRouteRegistry::new_with_options(
2872 spec,
2873 ValidationOptions {
2874 request_mode: ValidationMode::Enforce,
2875 aggregate_errors: true,
2876 validate_responses: false,
2877 overrides,
2878 admin_skip_prefixes: vec![],
2879 response_template_expand: false,
2880 validation_status: None,
2881 },
2882 );
2883
2884 let ok = registry.validate_request_with("/things", "POST", &Map::new(), &Map::new(), None);
2886 assert!(ok.is_ok());
2887 }
2888
2889 #[tokio::test]
2890 async fn test_admin_skip_prefix_short_circuit() {
2891 let spec_json = json!({
2892 "openapi": "3.0.0",
2893 "info": { "title": "Skip API", "version": "1.0.0" },
2894 "paths": {}
2895 });
2896 let spec = OpenApiSpec::from_json(spec_json).unwrap();
2897 let registry = OpenApiRouteRegistry::new_with_options(
2898 spec,
2899 ValidationOptions {
2900 request_mode: ValidationMode::Enforce,
2901 aggregate_errors: true,
2902 validate_responses: false,
2903 overrides: HashMap::new(),
2904 admin_skip_prefixes: vec!["/admin".into()],
2905 response_template_expand: false,
2906 validation_status: None,
2907 },
2908 );
2909
2910 let res = registry.validate_request_with_all(
2912 "/admin/__mockforge/health",
2913 "GET",
2914 &Map::new(),
2915 &Map::new(),
2916 &Map::new(),
2917 &Map::new(),
2918 None,
2919 );
2920 assert!(res.is_ok());
2921 }
2922
2923 #[test]
2924 fn test_path_conversion() {
2925 assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users"), "/users");
2926 assert_eq!(OpenApiRouteRegistry::convert_path_to_axum("/users/{id}"), "/users/{id}");
2927 assert_eq!(
2928 OpenApiRouteRegistry::convert_path_to_axum("/users/{id}/posts/{postId}"),
2929 "/users/{id}/posts/{postId}"
2930 );
2931 }
2932
2933 #[test]
2934 fn test_validation_options_default() {
2935 let options = ValidationOptions::default();
2936 assert!(matches!(options.request_mode, ValidationMode::Enforce));
2937 assert!(options.aggregate_errors);
2938 assert!(!options.validate_responses);
2939 assert!(options.overrides.is_empty());
2940 assert!(options.admin_skip_prefixes.is_empty());
2941 assert!(!options.response_template_expand);
2942 assert!(options.validation_status.is_none());
2943 }
2944
2945 #[test]
2946 fn test_validation_mode_variants() {
2947 let disabled = ValidationMode::Disabled;
2949 let warn = ValidationMode::Warn;
2950 let enforce = ValidationMode::Enforce;
2951 let default = ValidationMode::default();
2952
2953 assert!(matches!(default, ValidationMode::Warn));
2955
2956 assert!(!matches!(disabled, ValidationMode::Warn));
2958 assert!(!matches!(warn, ValidationMode::Enforce));
2959 assert!(!matches!(enforce, ValidationMode::Disabled));
2960 }
2961
2962 #[test]
2963 fn test_registry_spec_accessor() {
2964 let spec_json = json!({
2965 "openapi": "3.0.0",
2966 "info": {
2967 "title": "Test API",
2968 "version": "1.0.0"
2969 },
2970 "paths": {}
2971 });
2972 let spec = OpenApiSpec::from_json(spec_json).unwrap();
2973 let registry = OpenApiRouteRegistry::new(spec.clone());
2974
2975 let accessed_spec = registry.spec();
2977 assert_eq!(accessed_spec.title(), "Test API");
2978 }
2979
2980 #[test]
2981 fn test_clone_for_validation() {
2982 let spec_json = json!({
2983 "openapi": "3.0.0",
2984 "info": {
2985 "title": "Test API",
2986 "version": "1.0.0"
2987 },
2988 "paths": {
2989 "/users": {
2990 "get": {
2991 "responses": {
2992 "200": {
2993 "description": "Success"
2994 }
2995 }
2996 }
2997 }
2998 }
2999 });
3000 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3001 let registry = OpenApiRouteRegistry::new(spec);
3002
3003 let cloned = registry.clone_for_validation();
3005 assert_eq!(cloned.routes().len(), registry.routes().len());
3006 assert_eq!(cloned.spec().title(), registry.spec().title());
3007 }
3008
3009 #[test]
3010 fn test_with_custom_fixture_loader() {
3011 let temp_dir = TempDir::new().unwrap();
3012 let spec_json = json!({
3013 "openapi": "3.0.0",
3014 "info": {
3015 "title": "Test API",
3016 "version": "1.0.0"
3017 },
3018 "paths": {}
3019 });
3020 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3021 let registry = OpenApiRouteRegistry::new(spec);
3022 let original_routes_len = registry.routes().len();
3023
3024 let custom_loader =
3026 Arc::new(crate::CustomFixtureLoader::new(temp_dir.path().to_path_buf(), true));
3027 let registry_with_loader = registry.with_custom_fixture_loader(custom_loader);
3028
3029 assert_eq!(registry_with_loader.routes().len(), original_routes_len);
3031 }
3032
3033 #[test]
3034 fn test_get_route() {
3035 let spec_json = json!({
3036 "openapi": "3.0.0",
3037 "info": {
3038 "title": "Test API",
3039 "version": "1.0.0"
3040 },
3041 "paths": {
3042 "/users": {
3043 "get": {
3044 "operationId": "getUsers",
3045 "responses": {
3046 "200": {
3047 "description": "Success"
3048 }
3049 }
3050 },
3051 "post": {
3052 "operationId": "createUser",
3053 "responses": {
3054 "201": {
3055 "description": "Created"
3056 }
3057 }
3058 }
3059 }
3060 }
3061 });
3062 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3063 let registry = OpenApiRouteRegistry::new(spec);
3064
3065 let route = registry.get_route("/users", "GET");
3067 assert!(route.is_some());
3068 assert_eq!(route.unwrap().method, "GET");
3069 assert_eq!(route.unwrap().path, "/users");
3070
3071 let route = registry.get_route("/nonexistent", "GET");
3073 assert!(route.is_none());
3074
3075 let route = registry.get_route("/users", "POST");
3077 assert!(route.is_some());
3078 assert_eq!(route.unwrap().method, "POST");
3079 }
3080
3081 #[test]
3082 fn test_get_routes_for_path() {
3083 let spec_json = json!({
3084 "openapi": "3.0.0",
3085 "info": {
3086 "title": "Test API",
3087 "version": "1.0.0"
3088 },
3089 "paths": {
3090 "/users": {
3091 "get": {
3092 "responses": {
3093 "200": {
3094 "description": "Success"
3095 }
3096 }
3097 },
3098 "post": {
3099 "responses": {
3100 "201": {
3101 "description": "Created"
3102 }
3103 }
3104 },
3105 "put": {
3106 "responses": {
3107 "200": {
3108 "description": "Success"
3109 }
3110 }
3111 }
3112 },
3113 "/posts": {
3114 "get": {
3115 "responses": {
3116 "200": {
3117 "description": "Success"
3118 }
3119 }
3120 }
3121 }
3122 }
3123 });
3124 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3125 let registry = OpenApiRouteRegistry::new(spec);
3126
3127 let routes = registry.get_routes_for_path("/users");
3129 assert_eq!(routes.len(), 3);
3130 let methods: Vec<&str> = routes.iter().map(|r| r.method.as_str()).collect();
3131 assert!(methods.contains(&"GET"));
3132 assert!(methods.contains(&"POST"));
3133 assert!(methods.contains(&"PUT"));
3134
3135 let routes = registry.get_routes_for_path("/posts");
3137 assert_eq!(routes.len(), 1);
3138 assert_eq!(routes[0].method, "GET");
3139
3140 let routes = registry.get_routes_for_path("/nonexistent");
3142 assert!(routes.is_empty());
3143 }
3144
3145 #[test]
3146 fn test_new_vs_new_with_options() {
3147 let spec_json = json!({
3148 "openapi": "3.0.0",
3149 "info": {
3150 "title": "Test API",
3151 "version": "1.0.0"
3152 },
3153 "paths": {}
3154 });
3155 let spec1 = OpenApiSpec::from_json(spec_json.clone()).unwrap();
3156 let spec2 = OpenApiSpec::from_json(spec_json).unwrap();
3157
3158 let registry1 = OpenApiRouteRegistry::new(spec1);
3160 assert_eq!(registry1.spec().title(), "Test API");
3161
3162 let options = ValidationOptions {
3164 request_mode: ValidationMode::Disabled,
3165 aggregate_errors: false,
3166 validate_responses: true,
3167 overrides: HashMap::new(),
3168 admin_skip_prefixes: vec!["/admin".to_string()],
3169 response_template_expand: true,
3170 validation_status: Some(422),
3171 };
3172 let registry2 = OpenApiRouteRegistry::new_with_options(spec2, options);
3173 assert_eq!(registry2.spec().title(), "Test API");
3174 }
3175
3176 #[test]
3177 fn test_new_with_env_vs_new() {
3178 let spec_json = json!({
3179 "openapi": "3.0.0",
3180 "info": {
3181 "title": "Test API",
3182 "version": "1.0.0"
3183 },
3184 "paths": {}
3185 });
3186 let spec1 = OpenApiSpec::from_json(spec_json.clone()).unwrap();
3187 let spec2 = OpenApiSpec::from_json(spec_json).unwrap();
3188
3189 let registry1 = OpenApiRouteRegistry::new(spec1);
3191
3192 let registry2 = OpenApiRouteRegistry::new_with_env(spec2);
3194
3195 assert_eq!(registry1.spec().title(), "Test API");
3197 assert_eq!(registry2.spec().title(), "Test API");
3198 }
3199
3200 #[test]
3201 fn test_validation_options_custom() {
3202 let options = ValidationOptions {
3203 request_mode: ValidationMode::Warn,
3204 aggregate_errors: false,
3205 validate_responses: true,
3206 overrides: {
3207 let mut map = HashMap::new();
3208 map.insert("getUsers".to_string(), ValidationMode::Disabled);
3209 map
3210 },
3211 admin_skip_prefixes: vec!["/admin".to_string(), "/internal".to_string()],
3212 response_template_expand: true,
3213 validation_status: Some(422),
3214 };
3215
3216 assert!(matches!(options.request_mode, ValidationMode::Warn));
3217 assert!(!options.aggregate_errors);
3218 assert!(options.validate_responses);
3219 assert_eq!(options.overrides.len(), 1);
3220 assert_eq!(options.admin_skip_prefixes.len(), 2);
3221 assert!(options.response_template_expand);
3222 assert_eq!(options.validation_status, Some(422));
3223 }
3224
3225 #[test]
3226 fn test_validation_mode_default_standalone() {
3227 let mode = ValidationMode::default();
3228 assert!(matches!(mode, ValidationMode::Warn));
3229 }
3230
3231 #[test]
3232 fn test_validation_mode_clone() {
3233 let mode1 = ValidationMode::Enforce;
3234 let mode2 = mode1.clone();
3235 assert!(matches!(mode1, ValidationMode::Enforce));
3236 assert!(matches!(mode2, ValidationMode::Enforce));
3237 }
3238
3239 #[test]
3240 fn test_validation_mode_debug() {
3241 let mode = ValidationMode::Disabled;
3242 let debug_str = format!("{:?}", mode);
3243 assert!(debug_str.contains("Disabled") || debug_str.contains("ValidationMode"));
3244 }
3245
3246 #[test]
3247 fn test_validation_options_clone() {
3248 let options1 = ValidationOptions {
3249 request_mode: ValidationMode::Warn,
3250 aggregate_errors: true,
3251 validate_responses: false,
3252 overrides: HashMap::new(),
3253 admin_skip_prefixes: vec![],
3254 response_template_expand: false,
3255 validation_status: None,
3256 };
3257 let options2 = options1.clone();
3258 assert!(matches!(options2.request_mode, ValidationMode::Warn));
3259 assert_eq!(options1.aggregate_errors, options2.aggregate_errors);
3260 }
3261
3262 #[test]
3263 fn test_validation_options_debug() {
3264 let options = ValidationOptions::default();
3265 let debug_str = format!("{:?}", options);
3266 assert!(debug_str.contains("ValidationOptions"));
3267 }
3268
3269 #[test]
3270 fn test_validation_options_with_all_fields() {
3271 let mut overrides = HashMap::new();
3272 overrides.insert("op1".to_string(), ValidationMode::Disabled);
3273 overrides.insert("op2".to_string(), ValidationMode::Warn);
3274
3275 let options = ValidationOptions {
3276 request_mode: ValidationMode::Enforce,
3277 aggregate_errors: false,
3278 validate_responses: true,
3279 overrides: overrides.clone(),
3280 admin_skip_prefixes: vec!["/admin".to_string(), "/internal".to_string()],
3281 response_template_expand: true,
3282 validation_status: Some(422),
3283 };
3284
3285 assert!(matches!(options.request_mode, ValidationMode::Enforce));
3286 assert!(!options.aggregate_errors);
3287 assert!(options.validate_responses);
3288 assert_eq!(options.overrides.len(), 2);
3289 assert_eq!(options.admin_skip_prefixes.len(), 2);
3290 assert!(options.response_template_expand);
3291 assert_eq!(options.validation_status, Some(422));
3292 }
3293
3294 #[test]
3295 fn test_openapi_route_registry_clone() {
3296 let spec_json = json!({
3297 "openapi": "3.0.0",
3298 "info": { "title": "Test API", "version": "1.0.0" },
3299 "paths": {}
3300 });
3301 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3302 let registry1 = OpenApiRouteRegistry::new(spec);
3303 let registry2 = registry1.clone();
3304 assert_eq!(registry1.spec().title(), registry2.spec().title());
3305 }
3306
3307 #[test]
3308 fn test_validation_mode_serialization() {
3309 let mode = ValidationMode::Enforce;
3310 let json = serde_json::to_string(&mode).unwrap();
3311 assert!(json.contains("Enforce") || json.contains("enforce"));
3312 }
3313
3314 #[test]
3315 fn test_validation_mode_deserialization() {
3316 let json = r#""Disabled""#;
3317 let mode: ValidationMode = serde_json::from_str(json).unwrap();
3318 assert!(matches!(mode, ValidationMode::Disabled));
3319 }
3320
3321 #[test]
3322 fn test_validation_options_default_values() {
3323 let options = ValidationOptions::default();
3324 assert!(matches!(options.request_mode, ValidationMode::Enforce));
3325 assert!(options.aggregate_errors);
3326 assert!(!options.validate_responses);
3327 assert!(options.overrides.is_empty());
3328 assert!(options.admin_skip_prefixes.is_empty());
3329 assert!(!options.response_template_expand);
3330 assert_eq!(options.validation_status, None);
3331 }
3332
3333 #[test]
3334 fn test_validation_mode_all_variants() {
3335 let disabled = ValidationMode::Disabled;
3336 let warn = ValidationMode::Warn;
3337 let enforce = ValidationMode::Enforce;
3338
3339 assert!(matches!(disabled, ValidationMode::Disabled));
3340 assert!(matches!(warn, ValidationMode::Warn));
3341 assert!(matches!(enforce, ValidationMode::Enforce));
3342 }
3343
3344 #[test]
3345 fn test_validation_options_with_overrides() {
3346 let mut overrides = HashMap::new();
3347 overrides.insert("operation1".to_string(), ValidationMode::Disabled);
3348 overrides.insert("operation2".to_string(), ValidationMode::Warn);
3349
3350 let options = ValidationOptions {
3351 request_mode: ValidationMode::Enforce,
3352 aggregate_errors: true,
3353 validate_responses: false,
3354 overrides,
3355 admin_skip_prefixes: vec![],
3356 response_template_expand: false,
3357 validation_status: None,
3358 };
3359
3360 assert_eq!(options.overrides.len(), 2);
3361 assert!(matches!(options.overrides.get("operation1"), Some(ValidationMode::Disabled)));
3362 assert!(matches!(options.overrides.get("operation2"), Some(ValidationMode::Warn)));
3363 }
3364
3365 #[test]
3366 fn test_validation_options_with_admin_skip_prefixes() {
3367 let options = ValidationOptions {
3368 request_mode: ValidationMode::Enforce,
3369 aggregate_errors: true,
3370 validate_responses: false,
3371 overrides: HashMap::new(),
3372 admin_skip_prefixes: vec![
3373 "/admin".to_string(),
3374 "/internal".to_string(),
3375 "/debug".to_string(),
3376 ],
3377 response_template_expand: false,
3378 validation_status: None,
3379 };
3380
3381 assert_eq!(options.admin_skip_prefixes.len(), 3);
3382 assert!(options.admin_skip_prefixes.contains(&"/admin".to_string()));
3383 assert!(options.admin_skip_prefixes.contains(&"/internal".to_string()));
3384 assert!(options.admin_skip_prefixes.contains(&"/debug".to_string()));
3385 }
3386
3387 #[test]
3388 fn test_validation_options_with_validation_status() {
3389 let options1 = ValidationOptions {
3390 request_mode: ValidationMode::Enforce,
3391 aggregate_errors: true,
3392 validate_responses: false,
3393 overrides: HashMap::new(),
3394 admin_skip_prefixes: vec![],
3395 response_template_expand: false,
3396 validation_status: Some(400),
3397 };
3398
3399 let options2 = ValidationOptions {
3400 request_mode: ValidationMode::Enforce,
3401 aggregate_errors: true,
3402 validate_responses: false,
3403 overrides: HashMap::new(),
3404 admin_skip_prefixes: vec![],
3405 response_template_expand: false,
3406 validation_status: Some(422),
3407 };
3408
3409 assert_eq!(options1.validation_status, Some(400));
3410 assert_eq!(options2.validation_status, Some(422));
3411 }
3412
3413 #[test]
3414 fn test_validate_request_with_disabled_mode() {
3415 let spec_json = json!({
3417 "openapi": "3.0.0",
3418 "info": {"title": "Test API", "version": "1.0.0"},
3419 "paths": {
3420 "/users": {
3421 "get": {
3422 "responses": {"200": {"description": "OK"}}
3423 }
3424 }
3425 }
3426 });
3427 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3428 let options = ValidationOptions {
3429 request_mode: ValidationMode::Disabled,
3430 ..Default::default()
3431 };
3432 let registry = OpenApiRouteRegistry::new_with_options(spec, options);
3433
3434 let result = registry.validate_request_with_all(
3436 "/users",
3437 "GET",
3438 &Map::new(),
3439 &Map::new(),
3440 &Map::new(),
3441 &Map::new(),
3442 None,
3443 );
3444 assert!(result.is_ok());
3445 }
3446
3447 #[test]
3448 fn test_validate_request_with_warn_mode() {
3449 let spec_json = json!({
3451 "openapi": "3.0.0",
3452 "info": {"title": "Test API", "version": "1.0.0"},
3453 "paths": {
3454 "/users": {
3455 "post": {
3456 "requestBody": {
3457 "required": true,
3458 "content": {
3459 "application/json": {
3460 "schema": {
3461 "type": "object",
3462 "required": ["name"],
3463 "properties": {
3464 "name": {"type": "string"}
3465 }
3466 }
3467 }
3468 }
3469 },
3470 "responses": {"200": {"description": "OK"}}
3471 }
3472 }
3473 }
3474 });
3475 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3476 let options = ValidationOptions {
3477 request_mode: ValidationMode::Warn,
3478 ..Default::default()
3479 };
3480 let registry = OpenApiRouteRegistry::new_with_options(spec, options);
3481
3482 let result = registry.validate_request_with_all(
3484 "/users",
3485 "POST",
3486 &Map::new(),
3487 &Map::new(),
3488 &Map::new(),
3489 &Map::new(),
3490 None, );
3492 assert!(result.is_ok()); }
3494
3495 #[test]
3496 fn test_validate_request_body_validation_error() {
3497 let spec_json = json!({
3499 "openapi": "3.0.0",
3500 "info": {"title": "Test API", "version": "1.0.0"},
3501 "paths": {
3502 "/users": {
3503 "post": {
3504 "requestBody": {
3505 "required": true,
3506 "content": {
3507 "application/json": {
3508 "schema": {
3509 "type": "object",
3510 "required": ["name"],
3511 "properties": {
3512 "name": {"type": "string"}
3513 }
3514 }
3515 }
3516 }
3517 },
3518 "responses": {"200": {"description": "OK"}}
3519 }
3520 }
3521 }
3522 });
3523 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3524 let registry = OpenApiRouteRegistry::new(spec);
3525
3526 let result = registry.validate_request_with_all(
3528 "/users",
3529 "POST",
3530 &Map::new(),
3531 &Map::new(),
3532 &Map::new(),
3533 &Map::new(),
3534 None, );
3536 assert!(result.is_err());
3537 }
3538
3539 #[test]
3540 fn test_validate_request_body_schema_validation_error() {
3541 let spec_json = json!({
3543 "openapi": "3.0.0",
3544 "info": {"title": "Test API", "version": "1.0.0"},
3545 "paths": {
3546 "/users": {
3547 "post": {
3548 "requestBody": {
3549 "required": true,
3550 "content": {
3551 "application/json": {
3552 "schema": {
3553 "type": "object",
3554 "required": ["name"],
3555 "properties": {
3556 "name": {"type": "string"}
3557 }
3558 }
3559 }
3560 }
3561 },
3562 "responses": {"200": {"description": "OK"}}
3563 }
3564 }
3565 }
3566 });
3567 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3568 let registry = OpenApiRouteRegistry::new(spec);
3569
3570 let invalid_body = json!({}); let result = registry.validate_request_with_all(
3573 "/users",
3574 "POST",
3575 &Map::new(),
3576 &Map::new(),
3577 &Map::new(),
3578 &Map::new(),
3579 Some(&invalid_body),
3580 );
3581 assert!(result.is_err());
3582 }
3583
3584 #[test]
3585 fn test_validate_request_body_referenced_schema_error() {
3586 let spec_json = json!({
3588 "openapi": "3.0.0",
3589 "info": {"title": "Test API", "version": "1.0.0"},
3590 "paths": {
3591 "/users": {
3592 "post": {
3593 "requestBody": {
3594 "required": true,
3595 "content": {
3596 "application/json": {
3597 "schema": {
3598 "$ref": "#/components/schemas/NonExistentSchema"
3599 }
3600 }
3601 }
3602 },
3603 "responses": {"200": {"description": "OK"}}
3604 }
3605 }
3606 },
3607 "components": {}
3608 });
3609 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3610 let registry = OpenApiRouteRegistry::new(spec);
3611
3612 let body = json!({"name": "test"});
3614 let result = registry.validate_request_with_all(
3615 "/users",
3616 "POST",
3617 &Map::new(),
3618 &Map::new(),
3619 &Map::new(),
3620 &Map::new(),
3621 Some(&body),
3622 );
3623 assert!(result.is_err());
3624 }
3625
3626 #[test]
3627 fn test_validate_request_body_referenced_request_body_error() {
3628 let spec_json = json!({
3630 "openapi": "3.0.0",
3631 "info": {"title": "Test API", "version": "1.0.0"},
3632 "paths": {
3633 "/users": {
3634 "post": {
3635 "requestBody": {
3636 "$ref": "#/components/requestBodies/NonExistentRequestBody"
3637 },
3638 "responses": {"200": {"description": "OK"}}
3639 }
3640 }
3641 },
3642 "components": {}
3643 });
3644 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3645 let registry = OpenApiRouteRegistry::new(spec);
3646
3647 let body = json!({"name": "test"});
3649 let result = registry.validate_request_with_all(
3650 "/users",
3651 "POST",
3652 &Map::new(),
3653 &Map::new(),
3654 &Map::new(),
3655 &Map::new(),
3656 Some(&body),
3657 );
3658 assert!(result.is_err());
3659 }
3660
3661 #[test]
3662 fn test_validate_request_body_provided_when_not_expected() {
3663 let spec_json = json!({
3665 "openapi": "3.0.0",
3666 "info": {"title": "Test API", "version": "1.0.0"},
3667 "paths": {
3668 "/users": {
3669 "get": {
3670 "responses": {"200": {"description": "OK"}}
3671 }
3672 }
3673 }
3674 });
3675 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3676 let registry = OpenApiRouteRegistry::new(spec);
3677
3678 let body = json!({"extra": "data"});
3680 let result = registry.validate_request_with_all(
3681 "/users",
3682 "GET",
3683 &Map::new(),
3684 &Map::new(),
3685 &Map::new(),
3686 &Map::new(),
3687 Some(&body),
3688 );
3689 assert!(result.is_ok());
3691 }
3692
3693 #[test]
3694 fn test_get_operation() {
3695 let spec_json = json!({
3697 "openapi": "3.0.0",
3698 "info": {"title": "Test API", "version": "1.0.0"},
3699 "paths": {
3700 "/users": {
3701 "get": {
3702 "operationId": "getUsers",
3703 "summary": "Get users",
3704 "responses": {"200": {"description": "OK"}}
3705 }
3706 }
3707 }
3708 });
3709 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3710 let registry = OpenApiRouteRegistry::new(spec);
3711
3712 let operation = registry.get_operation("/users", "GET");
3714 assert!(operation.is_some());
3715 assert_eq!(operation.unwrap().method, "GET");
3716
3717 assert!(registry.get_operation("/nonexistent", "GET").is_none());
3719 }
3720
3721 #[test]
3722 fn test_extract_path_parameters() {
3723 let spec_json = json!({
3725 "openapi": "3.0.0",
3726 "info": {"title": "Test API", "version": "1.0.0"},
3727 "paths": {
3728 "/users/{id}": {
3729 "get": {
3730 "parameters": [
3731 {
3732 "name": "id",
3733 "in": "path",
3734 "required": true,
3735 "schema": {"type": "string"}
3736 }
3737 ],
3738 "responses": {"200": {"description": "OK"}}
3739 }
3740 }
3741 }
3742 });
3743 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3744 let registry = OpenApiRouteRegistry::new(spec);
3745
3746 let params = registry.extract_path_parameters("/users/123", "GET");
3748 assert_eq!(params.get("id"), Some(&"123".to_string()));
3749
3750 let empty_params = registry.extract_path_parameters("/users", "GET");
3752 assert!(empty_params.is_empty());
3753 }
3754
3755 #[test]
3756 fn test_extract_path_parameters_multiple_params() {
3757 let spec_json = json!({
3759 "openapi": "3.0.0",
3760 "info": {"title": "Test API", "version": "1.0.0"},
3761 "paths": {
3762 "/users/{userId}/posts/{postId}": {
3763 "get": {
3764 "parameters": [
3765 {
3766 "name": "userId",
3767 "in": "path",
3768 "required": true,
3769 "schema": {"type": "string"}
3770 },
3771 {
3772 "name": "postId",
3773 "in": "path",
3774 "required": true,
3775 "schema": {"type": "string"}
3776 }
3777 ],
3778 "responses": {"200": {"description": "OK"}}
3779 }
3780 }
3781 }
3782 });
3783 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3784 let registry = OpenApiRouteRegistry::new(spec);
3785
3786 let params = registry.extract_path_parameters("/users/123/posts/456", "GET");
3788 assert_eq!(params.get("userId"), Some(&"123".to_string()));
3789 assert_eq!(params.get("postId"), Some(&"456".to_string()));
3790 }
3791
3792 #[test]
3793 fn test_validate_request_route_not_found() {
3794 let spec_json = json!({
3796 "openapi": "3.0.0",
3797 "info": {"title": "Test API", "version": "1.0.0"},
3798 "paths": {
3799 "/users": {
3800 "get": {
3801 "responses": {"200": {"description": "OK"}}
3802 }
3803 }
3804 }
3805 });
3806 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3807 let registry = OpenApiRouteRegistry::new(spec);
3808
3809 let result = registry.validate_request_with_all(
3811 "/nonexistent",
3812 "GET",
3813 &Map::new(),
3814 &Map::new(),
3815 &Map::new(),
3816 &Map::new(),
3817 None,
3818 );
3819 assert!(result.is_err());
3820 assert!(result.unwrap_err().to_string().contains("not found"));
3821 }
3822
3823 #[test]
3824 fn test_validate_request_with_path_parameters() {
3825 let spec_json = json!({
3827 "openapi": "3.0.0",
3828 "info": {"title": "Test API", "version": "1.0.0"},
3829 "paths": {
3830 "/users/{id}": {
3831 "get": {
3832 "parameters": [
3833 {
3834 "name": "id",
3835 "in": "path",
3836 "required": true,
3837 "schema": {"type": "string", "minLength": 1}
3838 }
3839 ],
3840 "responses": {"200": {"description": "OK"}}
3841 }
3842 }
3843 }
3844 });
3845 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3846 let registry = OpenApiRouteRegistry::new(spec);
3847
3848 let mut path_params = Map::new();
3850 path_params.insert("id".to_string(), json!("123"));
3851 let result = registry.validate_request_with_all(
3852 "/users/{id}",
3853 "GET",
3854 &path_params,
3855 &Map::new(),
3856 &Map::new(),
3857 &Map::new(),
3858 None,
3859 );
3860 assert!(result.is_ok());
3861 }
3862
3863 #[test]
3864 fn test_validate_request_with_query_parameters() {
3865 let spec_json = json!({
3867 "openapi": "3.0.0",
3868 "info": {"title": "Test API", "version": "1.0.0"},
3869 "paths": {
3870 "/users": {
3871 "get": {
3872 "parameters": [
3873 {
3874 "name": "page",
3875 "in": "query",
3876 "required": true,
3877 "schema": {"type": "integer", "minimum": 1}
3878 }
3879 ],
3880 "responses": {"200": {"description": "OK"}}
3881 }
3882 }
3883 }
3884 });
3885 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3886 let registry = OpenApiRouteRegistry::new(spec);
3887
3888 let mut query_params = Map::new();
3890 query_params.insert("page".to_string(), json!(1));
3891 let result = registry.validate_request_with_all(
3892 "/users",
3893 "GET",
3894 &Map::new(),
3895 &query_params,
3896 &Map::new(),
3897 &Map::new(),
3898 None,
3899 );
3900 assert!(result.is_ok());
3901 }
3902
3903 #[test]
3904 fn test_validate_request_with_header_parameters() {
3905 let spec_json = json!({
3907 "openapi": "3.0.0",
3908 "info": {"title": "Test API", "version": "1.0.0"},
3909 "paths": {
3910 "/users": {
3911 "get": {
3912 "parameters": [
3913 {
3914 "name": "X-API-Key",
3915 "in": "header",
3916 "required": true,
3917 "schema": {"type": "string"}
3918 }
3919 ],
3920 "responses": {"200": {"description": "OK"}}
3921 }
3922 }
3923 }
3924 });
3925 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3926 let registry = OpenApiRouteRegistry::new(spec);
3927
3928 let mut header_params = Map::new();
3930 header_params.insert("X-API-Key".to_string(), json!("secret-key"));
3931 let result = registry.validate_request_with_all(
3932 "/users",
3933 "GET",
3934 &Map::new(),
3935 &Map::new(),
3936 &header_params,
3937 &Map::new(),
3938 None,
3939 );
3940 assert!(result.is_ok());
3941 }
3942
3943 #[test]
3944 fn test_validate_request_with_cookie_parameters() {
3945 let spec_json = json!({
3947 "openapi": "3.0.0",
3948 "info": {"title": "Test API", "version": "1.0.0"},
3949 "paths": {
3950 "/users": {
3951 "get": {
3952 "parameters": [
3953 {
3954 "name": "sessionId",
3955 "in": "cookie",
3956 "required": true,
3957 "schema": {"type": "string"}
3958 }
3959 ],
3960 "responses": {"200": {"description": "OK"}}
3961 }
3962 }
3963 }
3964 });
3965 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3966 let registry = OpenApiRouteRegistry::new(spec);
3967
3968 let mut cookie_params = Map::new();
3970 cookie_params.insert("sessionId".to_string(), json!("abc123"));
3971 let result = registry.validate_request_with_all(
3972 "/users",
3973 "GET",
3974 &Map::new(),
3975 &Map::new(),
3976 &Map::new(),
3977 &cookie_params,
3978 None,
3979 );
3980 assert!(result.is_ok());
3981 }
3982
3983 #[test]
3984 fn test_validate_request_no_errors_early_return() {
3985 let spec_json = json!({
3987 "openapi": "3.0.0",
3988 "info": {"title": "Test API", "version": "1.0.0"},
3989 "paths": {
3990 "/users": {
3991 "get": {
3992 "responses": {"200": {"description": "OK"}}
3993 }
3994 }
3995 }
3996 });
3997 let spec = OpenApiSpec::from_json(spec_json).unwrap();
3998 let registry = OpenApiRouteRegistry::new(spec);
3999
4000 let result = registry.validate_request_with_all(
4002 "/users",
4003 "GET",
4004 &Map::new(),
4005 &Map::new(),
4006 &Map::new(),
4007 &Map::new(),
4008 None,
4009 );
4010 assert!(result.is_ok());
4011 }
4012
4013 #[test]
4014 fn test_validate_request_query_parameter_different_styles() {
4015 let spec_json = json!({
4017 "openapi": "3.0.0",
4018 "info": {"title": "Test API", "version": "1.0.0"},
4019 "paths": {
4020 "/users": {
4021 "get": {
4022 "parameters": [
4023 {
4024 "name": "tags",
4025 "in": "query",
4026 "style": "pipeDelimited",
4027 "schema": {
4028 "type": "array",
4029 "items": {"type": "string"}
4030 }
4031 }
4032 ],
4033 "responses": {"200": {"description": "OK"}}
4034 }
4035 }
4036 }
4037 });
4038 let spec = OpenApiSpec::from_json(spec_json).unwrap();
4039 let registry = OpenApiRouteRegistry::new(spec);
4040
4041 let mut query_params = Map::new();
4043 query_params.insert("tags".to_string(), json!(["tag1", "tag2"]));
4044 let result = registry.validate_request_with_all(
4045 "/users",
4046 "GET",
4047 &Map::new(),
4048 &query_params,
4049 &Map::new(),
4050 &Map::new(),
4051 None,
4052 );
4053 assert!(result.is_ok() || result.is_err()); }
4056}