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