Skip to main content

mockforge_core/
openapi_routes.rs

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