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