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