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