Skip to main content

fastapi_core/
error.rs

1//! Error types.
2//!
3//! This module provides error types for HTTP responses with support for
4//! debug mode that can include additional diagnostic information.
5//!
6//! # Debug Mode
7//!
8//! Debug mode can be enabled to include additional diagnostic information
9//! in error responses:
10//!
11//! - Source location (file, line, function)
12//! - Full validation error context
13//! - Handler name and route pattern
14//!
15//! Debug mode is controlled per-error via the `with_debug_info` method,
16//! and should only be enabled when the application is in debug mode AND
17//! the request includes a valid debug token (if configured).
18//!
19//! # Example
20//!
21//! ```
22//! use fastapi_core::error::{HttpError, DebugInfo};
23//!
24//! // Production mode - no debug info
25//! let error = HttpError::not_found().with_detail("User not found");
26//!
27//! // Debug mode - with source location
28//! let error = HttpError::not_found()
29//!     .with_detail("User not found")
30//!     .with_debug_info(DebugInfo::new()
31//!         .with_source_location(file!(), line!(), "get_user")
32//!         .with_route_pattern("/users/{id}"));
33//! ```
34
35use crate::response::{IntoResponse, Response, ResponseBody, StatusCode};
36use serde::{Serialize, Serializer};
37use std::collections::HashMap;
38use std::sync::atomic::{AtomicBool, Ordering};
39
40// ============================================================================
41// Debug Mode Configuration
42// ============================================================================
43
44/// Global flag for thread-local debug mode state.
45/// This is used when debug info needs to be propagated through error creation.
46static DEBUG_MODE_ENABLED: AtomicBool = AtomicBool::new(false);
47
48/// Enable debug mode globally.
49///
50/// When debug mode is enabled, errors can include additional diagnostic
51/// information in their responses. This should be controlled by the
52/// application configuration and request-level debug token validation.
53pub fn enable_debug_mode() {
54    DEBUG_MODE_ENABLED.store(true, Ordering::SeqCst);
55}
56
57/// Disable debug mode globally.
58pub fn disable_debug_mode() {
59    DEBUG_MODE_ENABLED.store(false, Ordering::SeqCst);
60}
61
62/// Check if debug mode is enabled globally.
63#[must_use]
64pub fn is_debug_mode_enabled() -> bool {
65    DEBUG_MODE_ENABLED.load(Ordering::SeqCst)
66}
67
68/// Debug configuration for secure debug mode access.
69///
70/// This allows configuring a debug header that must be present with a
71/// secret token for debug information to be included in responses.
72///
73/// # Example
74///
75/// ```
76/// use fastapi_core::error::DebugConfig;
77///
78/// // Require X-Debug-Token header with a secret
79/// let config = DebugConfig::new()
80///     .with_debug_header("X-Debug-Token", "my-secret-token");
81///
82/// // Or allow debug mode without authentication (dangerous!)
83/// let config = DebugConfig::new().allow_unauthenticated();
84/// ```
85#[derive(Debug, Clone)]
86pub struct DebugConfig {
87    /// Enable debug mode for the application.
88    pub enabled: bool,
89    /// Header name for debug token authentication.
90    pub debug_header: Option<String>,
91    /// Expected token value for debug access.
92    pub debug_token: Option<String>,
93    /// Allow debug mode without authentication (dangerous in production).
94    pub allow_unauthenticated: bool,
95}
96
97impl Default for DebugConfig {
98    fn default() -> Self {
99        Self {
100            enabled: false,
101            debug_header: None,
102            debug_token: None,
103            allow_unauthenticated: false,
104        }
105    }
106}
107
108impl DebugConfig {
109    /// Create a new debug configuration with debug mode disabled.
110    #[must_use]
111    pub fn new() -> Self {
112        Self::default()
113    }
114
115    /// Enable debug mode.
116    #[must_use]
117    pub fn enable(mut self) -> Self {
118        self.enabled = true;
119        self
120    }
121
122    /// Configure the debug header and token for authenticated debug access.
123    ///
124    /// When configured, debug information will only be included in responses
125    /// if the request includes this header with the expected token value.
126    #[must_use]
127    pub fn with_debug_header(
128        mut self,
129        header_name: impl Into<String>,
130        token: impl Into<String>,
131    ) -> Self {
132        self.debug_header = Some(header_name.into());
133        self.debug_token = Some(token.into());
134        self
135    }
136
137    /// Allow debug mode without authentication.
138    ///
139    /// # Warning
140    ///
141    /// This is dangerous in production as it exposes internal details
142    /// to anyone. Only use for development/testing.
143    #[must_use]
144    pub fn allow_unauthenticated(mut self) -> Self {
145        self.allow_unauthenticated = true;
146        self
147    }
148
149    /// Check if a request is authorized for debug mode.
150    ///
151    /// Returns true if:
152    /// - Debug mode is disabled (debug info won't be shown anyway)
153    /// - `allow_unauthenticated` is true
154    /// - The request includes the correct debug header/token
155    ///
156    /// # Security
157    ///
158    /// Token comparison uses constant-time comparison to prevent timing attacks.
159    pub fn is_authorized(&self, request_headers: &[(String, Vec<u8>)]) -> bool {
160        if !self.enabled {
161            return false;
162        }
163
164        if self.allow_unauthenticated {
165            return true;
166        }
167
168        // Check for debug header
169        if let (Some(header_name), Some(expected_token)) = (&self.debug_header, &self.debug_token) {
170            for (name, value) in request_headers {
171                if name.eq_ignore_ascii_case(header_name) {
172                    if let Ok(token) = std::str::from_utf8(value) {
173                        // Use constant-time comparison to prevent timing attacks
174                        return constant_time_str_eq(token, expected_token);
175                    }
176                }
177            }
178        }
179
180        false
181    }
182}
183
184/// Constant-time string comparison to prevent timing attacks.
185///
186/// Delegates to `crate::password::constant_time_eq` to avoid duplication.
187fn constant_time_str_eq(a: &str, b: &str) -> bool {
188    crate::password::constant_time_eq(a.as_bytes(), b.as_bytes())
189}
190
191// ============================================================================
192// Debug Information
193// ============================================================================
194
195/// Debug information to include in error responses when debug mode is enabled.
196///
197/// This struct holds diagnostic information that helps developers understand
198/// where and why an error occurred. This information should NEVER be included
199/// in production responses as it can leak sensitive implementation details.
200///
201/// # Fields
202///
203/// - `source_file`: The file where the error originated
204/// - `source_line`: The line number in the source file
205/// - `function_name`: The function or handler that generated the error
206/// - `route_pattern`: The matched route pattern (e.g., "/users/{id}")
207/// - `handler_name`: The name of the handler function
208/// - `extra`: Additional key-value debug information
209///
210/// # Example
211///
212/// ```
213/// use fastapi_core::error::DebugInfo;
214///
215/// let debug = DebugInfo::new()
216///     .with_source_location("src/handlers/user.rs", 42, "get_user")
217///     .with_route_pattern("/users/{id}")
218///     .with_extra("user_id_received", "abc123");
219/// ```
220#[derive(Debug, Clone, Default, Serialize)]
221pub struct DebugInfo {
222    /// Source file path.
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub source_file: Option<String>,
225    /// Source line number.
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub source_line: Option<u32>,
228    /// Function or handler name.
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub function_name: Option<String>,
231    /// Matched route pattern.
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub route_pattern: Option<String>,
234    /// Handler name (may differ from function_name for wrapped handlers).
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub handler_name: Option<String>,
237    /// Additional debug context.
238    #[serde(skip_serializing_if = "HashMap::is_empty")]
239    pub extra: HashMap<String, String>,
240}
241
242impl DebugInfo {
243    /// Create empty debug info.
244    #[must_use]
245    pub fn new() -> Self {
246        Self::default()
247    }
248
249    /// Set the source location.
250    #[must_use]
251    pub fn with_source_location(
252        mut self,
253        file: impl Into<String>,
254        line: u32,
255        function: impl Into<String>,
256    ) -> Self {
257        self.source_file = Some(file.into());
258        self.source_line = Some(line);
259        self.function_name = Some(function.into());
260        self
261    }
262
263    /// Set the route pattern.
264    #[must_use]
265    pub fn with_route_pattern(mut self, pattern: impl Into<String>) -> Self {
266        self.route_pattern = Some(pattern.into());
267        self
268    }
269
270    /// Set the handler name.
271    #[must_use]
272    pub fn with_handler_name(mut self, name: impl Into<String>) -> Self {
273        self.handler_name = Some(name.into());
274        self
275    }
276
277    /// Add extra debug information.
278    #[must_use]
279    pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
280        self.extra.insert(key.into(), value.into());
281        self
282    }
283
284    /// Check if this debug info is empty (has no data).
285    #[must_use]
286    pub fn is_empty(&self) -> bool {
287        self.source_file.is_none()
288            && self.source_line.is_none()
289            && self.function_name.is_none()
290            && self.route_pattern.is_none()
291            && self.handler_name.is_none()
292            && self.extra.is_empty()
293    }
294}
295
296/// Macro to capture the current source location for debug info.
297///
298/// # Example
299///
300/// ```
301/// use fastapi_core::{debug_location, error::DebugInfo};
302///
303/// fn my_handler() -> DebugInfo {
304///     debug_location!()
305/// }
306/// ```
307#[macro_export]
308macro_rules! debug_location {
309    () => {
310        $crate::error::DebugInfo::new().with_source_location(
311            file!(),
312            line!(),
313            // Get a reasonable function name approximation
314            module_path!(),
315        )
316    };
317    ($func_name:expr) => {
318        $crate::error::DebugInfo::new().with_source_location(file!(), line!(), $func_name)
319    };
320}
321
322// ============================================================================
323// Location Types for Validation Errors (FastAPI-compatible)
324// ============================================================================
325
326/// Location item for validation error paths.
327///
328/// FastAPI uses tuples where items can be either strings (field names)
329/// or integers (array indices). This enum models that structure.
330///
331/// # Examples
332///
333/// ```
334/// use fastapi_core::error::LocItem;
335///
336/// // Field name
337/// let field = LocItem::field("email");
338/// assert_eq!(field.as_str(), Some("email"));
339///
340/// // Array index
341/// let idx = LocItem::index(0);
342/// assert_eq!(idx.as_index(), Some(0));
343/// ```
344#[derive(Debug, Clone, PartialEq, Eq)]
345pub enum LocItem {
346    /// Field name (string).
347    Field(String),
348    /// Array index (integer).
349    Index(usize),
350}
351
352impl LocItem {
353    /// Create a field location item.
354    #[must_use]
355    pub fn field(name: impl Into<String>) -> Self {
356        Self::Field(name.into())
357    }
358
359    /// Create an index location item.
360    #[must_use]
361    pub fn index(idx: usize) -> Self {
362        Self::Index(idx)
363    }
364
365    /// Get the field name if this is a Field variant.
366    #[must_use]
367    pub fn as_str(&self) -> Option<&str> {
368        match self {
369            Self::Field(s) => Some(s),
370            Self::Index(_) => None,
371        }
372    }
373
374    /// Get the index if this is an Index variant.
375    #[must_use]
376    pub fn as_index(&self) -> Option<usize> {
377        match self {
378            Self::Field(_) => None,
379            Self::Index(i) => Some(*i),
380        }
381    }
382}
383
384impl From<&str> for LocItem {
385    fn from(s: &str) -> Self {
386        Self::Field(s.to_owned())
387    }
388}
389
390impl From<String> for LocItem {
391    fn from(s: String) -> Self {
392        Self::Field(s)
393    }
394}
395
396impl From<usize> for LocItem {
397    fn from(i: usize) -> Self {
398        Self::Index(i)
399    }
400}
401
402impl Serialize for LocItem {
403    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
404    where
405        S: Serializer,
406    {
407        match self {
408            Self::Field(s) => serializer.serialize_str(s),
409            Self::Index(i) => serializer.serialize_u64(*i as u64),
410        }
411    }
412}
413
414// ============================================================================
415// Location Prefixes (FastAPI conventions)
416// ============================================================================
417
418/// Location prefixes for different extraction sources.
419pub mod loc {
420    use super::LocItem;
421
422    /// Path parameter location: `["path", "param_name"]`
423    #[must_use]
424    pub fn path(param: &str) -> Vec<LocItem> {
425        vec![LocItem::field("path"), LocItem::field(param)]
426    }
427
428    /// Query parameter location: `["query", "param_name"]`
429    #[must_use]
430    pub fn query(param: &str) -> Vec<LocItem> {
431        vec![LocItem::field("query"), LocItem::field(param)]
432    }
433
434    /// Header location: `["header", "header_name"]`
435    #[must_use]
436    pub fn header(name: &str) -> Vec<LocItem> {
437        vec![LocItem::field("header"), LocItem::field(name)]
438    }
439
440    /// Cookie location: `["cookie", "cookie_name"]`
441    #[must_use]
442    pub fn cookie(name: &str) -> Vec<LocItem> {
443        vec![LocItem::field("cookie"), LocItem::field(name)]
444    }
445
446    /// Body root location: `["body"]`
447    #[must_use]
448    pub fn body() -> Vec<LocItem> {
449        vec![LocItem::field("body")]
450    }
451
452    /// Body field location: `["body", "field"]`
453    #[must_use]
454    pub fn body_field(field: &str) -> Vec<LocItem> {
455        vec![LocItem::field("body"), LocItem::field(field)]
456    }
457
458    /// Body nested path: `["body", "field", "nested", ...]`
459    #[must_use]
460    pub fn body_path(fields: &[&str]) -> Vec<LocItem> {
461        let mut loc = vec![LocItem::field("body")];
462        for field in fields {
463            loc.push(LocItem::field(*field));
464        }
465        loc
466    }
467
468    /// Body with array index: `["body", "items", 0, "name"]`
469    #[must_use]
470    pub fn body_indexed(field: &str, idx: usize) -> Vec<LocItem> {
471        vec![
472            LocItem::field("body"),
473            LocItem::field(field),
474            LocItem::index(idx),
475        ]
476    }
477
478    /// Response root location: `["response"]`
479    #[must_use]
480    pub fn response() -> Vec<LocItem> {
481        vec![LocItem::field("response")]
482    }
483
484    /// Response field location: `["response", "field"]`
485    #[must_use]
486    pub fn response_field(field: &str) -> Vec<LocItem> {
487        vec![LocItem::field("response"), LocItem::field(field)]
488    }
489
490    /// Response nested path: `["response", "field", "nested", ...]`
491    #[must_use]
492    pub fn response_path(fields: &[&str]) -> Vec<LocItem> {
493        let mut loc = vec![LocItem::field("response")];
494        for field in fields {
495            loc.push(LocItem::field(*field));
496        }
497        loc
498    }
499}
500
501// ============================================================================
502// Common Validation Error Types (FastAPI/Pydantic conventions)
503// ============================================================================
504
505/// Common validation error type strings (matching Pydantic v2).
506pub mod error_types {
507    /// Required field is missing.
508    pub const MISSING: &str = "missing";
509    /// String is too short.
510    pub const STRING_TOO_SHORT: &str = "string_too_short";
511    /// String is too long.
512    pub const STRING_TOO_LONG: &str = "string_too_long";
513    /// Value is not a valid string.
514    pub const STRING_TYPE: &str = "string_type";
515    /// Value is not a valid integer.
516    pub const INT_TYPE: &str = "int_type";
517    /// Value is not a valid float.
518    pub const FLOAT_TYPE: &str = "float_type";
519    /// Value is not a valid boolean.
520    pub const BOOL_TYPE: &str = "bool_type";
521    /// Value is less than minimum.
522    pub const GREATER_THAN_EQUAL: &str = "greater_than_equal";
523    /// Value is greater than maximum.
524    pub const LESS_THAN_EQUAL: &str = "less_than_equal";
525    /// Value does not match pattern.
526    pub const STRING_PATTERN_MISMATCH: &str = "string_pattern_mismatch";
527    /// Value is not a valid email.
528    pub const VALUE_ERROR: &str = "value_error";
529    /// Value is not a valid URL.
530    pub const URL_TYPE: &str = "url_type";
531    /// Value is not a valid UUID.
532    pub const UUID_TYPE: &str = "uuid_type";
533    /// JSON parsing failed.
534    pub const JSON_INVALID: &str = "json_invalid";
535    /// JSON type mismatch.
536    pub const JSON_TYPE: &str = "json_type";
537    /// Array has too few items.
538    pub const TOO_SHORT: &str = "too_short";
539    /// Array has too many items.
540    pub const TOO_LONG: &str = "too_long";
541    /// Value is not in enum.
542    pub const ENUM: &str = "enum";
543    /// Extra field not allowed.
544    pub const EXTRA_FORBIDDEN: &str = "extra_forbidden";
545
546    // Response validation error types
547    /// Response failed to serialize (e.g., JSON serialization error).
548    pub const SERIALIZATION_ERROR: &str = "serialization_error";
549    /// Response doesn't match the declared response model.
550    pub const MODEL_VALIDATION_ERROR: &str = "model_validation_error";
551}
552
553// ============================================================================
554// HTTP Error
555// ============================================================================
556
557/// HTTP error that produces a response.
558///
559/// When debug mode is enabled, errors can include additional diagnostic
560/// information via the `debug_info` field. This information is conditionally
561/// serialized into the response when `is_debug_mode_enabled()` returns true.
562///
563/// # Production Mode (default)
564///
565/// ```json
566/// {"detail": "Not Found"}
567/// ```
568///
569/// # Debug Mode (when enabled)
570///
571/// ```json
572/// {
573///   "detail": "Not Found",
574///   "debug": {
575///     "source_file": "src/handlers/user.rs",
576///     "source_line": 42,
577///     "function_name": "get_user",
578///     "route_pattern": "/users/{id}"
579///   }
580/// }
581/// ```
582#[derive(Debug)]
583pub struct HttpError {
584    /// Status code.
585    pub status: StatusCode,
586    /// Error detail message.
587    pub detail: Option<String>,
588    /// Additional headers.
589    pub headers: Vec<(String, Vec<u8>)>,
590    /// Debug information (only included in response when debug mode is enabled).
591    pub debug_info: Option<DebugInfo>,
592}
593
594impl HttpError {
595    /// Create a new HTTP error.
596    #[must_use]
597    pub fn new(status: StatusCode) -> Self {
598        Self {
599            status,
600            detail: None,
601            headers: Vec::new(),
602            debug_info: None,
603        }
604    }
605
606    /// Add a detail message.
607    #[must_use]
608    pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
609        self.detail = Some(detail.into());
610        self
611    }
612
613    /// Add a header.
614    #[must_use]
615    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
616        self.headers.push((name.into(), value.into()));
617        self
618    }
619
620    /// Add debug information.
621    ///
622    /// This information will only be included in the response when debug mode
623    /// is enabled (via `enable_debug_mode()`).
624    ///
625    /// # Example
626    ///
627    /// ```
628    /// use fastapi_core::error::{HttpError, DebugInfo};
629    ///
630    /// let error = HttpError::not_found()
631    ///     .with_detail("User not found")
632    ///     .with_debug_info(DebugInfo::new()
633    ///         .with_source_location("src/handlers/user.rs", 42, "get_user")
634    ///         .with_route_pattern("/users/{id}"));
635    /// ```
636    #[must_use]
637    pub fn with_debug_info(mut self, debug_info: DebugInfo) -> Self {
638        self.debug_info = Some(debug_info);
639        self
640    }
641
642    /// Add debug information at the current source location.
643    ///
644    /// This is a convenience method that captures the current file and line.
645    /// Note: This captures the location where this method is called, not
646    /// where the error originated. For accurate source tracking, use
647    /// `with_debug_info` with a `DebugInfo` created via the `debug_location!` macro.
648    #[must_use]
649    pub fn with_debug_location(self, function_name: impl Into<String>) -> Self {
650        self.with_debug_info(DebugInfo::new().with_source_location(
651            std::any::type_name::<Self>(),
652            0, // Line unknown when called this way
653            function_name,
654        ))
655    }
656
657    /// Create a 400 Bad Request error.
658    #[must_use]
659    pub fn bad_request() -> Self {
660        Self::new(StatusCode::BAD_REQUEST)
661    }
662
663    /// Create a 401 Unauthorized error.
664    #[must_use]
665    pub fn unauthorized() -> Self {
666        Self::new(StatusCode::UNAUTHORIZED)
667    }
668
669    /// Create a 403 Forbidden error.
670    #[must_use]
671    pub fn forbidden() -> Self {
672        Self::new(StatusCode::FORBIDDEN)
673    }
674
675    /// Create a 404 Not Found error.
676    #[must_use]
677    pub fn not_found() -> Self {
678        Self::new(StatusCode::NOT_FOUND)
679    }
680
681    /// Create a 500 Internal Server Error.
682    #[must_use]
683    pub fn internal() -> Self {
684        Self::new(StatusCode::INTERNAL_SERVER_ERROR)
685    }
686
687    /// Create a 413 Payload Too Large error.
688    #[must_use]
689    pub fn payload_too_large() -> Self {
690        Self::new(StatusCode::PAYLOAD_TOO_LARGE)
691    }
692
693    /// Create a 415 Unsupported Media Type error.
694    #[must_use]
695    pub fn unsupported_media_type() -> Self {
696        Self::new(StatusCode::UNSUPPORTED_MEDIA_TYPE)
697    }
698}
699
700impl IntoResponse for HttpError {
701    fn into_response(self) -> Response {
702        let detail = self
703            .detail
704            .as_deref()
705            .unwrap_or_else(|| self.status.canonical_reason());
706
707        // Conditionally include debug info based on global debug mode flag
708        let body = if is_debug_mode_enabled() {
709            if let Some(ref debug_info) = self.debug_info {
710                #[derive(Serialize)]
711                struct ErrorBodyWithDebug<'a> {
712                    detail: &'a str,
713                    debug: &'a DebugInfo,
714                }
715                serde_json::to_vec(&ErrorBodyWithDebug {
716                    detail,
717                    debug: debug_info,
718                })
719                .unwrap_or_default()
720            } else {
721                #[derive(Serialize)]
722                struct ErrorBody<'a> {
723                    detail: &'a str,
724                }
725                serde_json::to_vec(&ErrorBody { detail }).unwrap_or_default()
726            }
727        } else {
728            #[derive(Serialize)]
729            struct ErrorBody<'a> {
730                detail: &'a str,
731            }
732            serde_json::to_vec(&ErrorBody { detail }).unwrap_or_default()
733        };
734
735        let mut response = Response::with_status(self.status)
736            .header("content-type", b"application/json".to_vec())
737            .body(ResponseBody::Bytes(body));
738
739        for (name, value) in self.headers {
740            response = response.header(name, value);
741        }
742
743        response
744    }
745}
746
747impl std::fmt::Display for HttpError {
748    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
749        write!(f, "{}", self.status.canonical_reason())?;
750        if let Some(ref detail) = self.detail {
751            write!(f, ": {detail}")?;
752        }
753        Ok(())
754    }
755}
756
757impl std::error::Error for HttpError {}
758
759// ============================================================================
760// Validation Error (FastAPI-compatible)
761// ============================================================================
762
763/// A single validation error item.
764///
765/// This structure matches FastAPI/Pydantic v2's validation error format exactly,
766/// allowing for seamless compatibility with clients expecting FastAPI responses.
767///
768/// # Examples
769///
770/// ```
771/// use fastapi_core::error::{ValidationError, loc, error_types};
772/// use serde_json::json;
773///
774/// // Missing required field
775/// let error = ValidationError::missing(loc::query("q"));
776/// assert_eq!(error.error_type, "missing");
777///
778/// // String too short with context
779/// let error = ValidationError::new(error_types::STRING_TOO_SHORT, loc::body_field("name"))
780///     .with_msg("String should have at least 3 characters")
781///     .with_input(json!("ab"))
782///     .with_ctx_value("min_length", json!(3));
783/// ```
784#[derive(Debug, Clone, Serialize)]
785pub struct ValidationError {
786    /// Error type identifier (e.g., "missing", "string_too_short").
787    #[serde(rename = "type")]
788    pub error_type: &'static str,
789
790    /// Location path as a tuple of strings and integers.
791    ///
792    /// Examples:
793    /// - `["query", "q"]` for query parameter
794    /// - `["body", "user", "email"]` for nested body field
795    /// - `["body", "items", 0, "name"]` for array item
796    pub loc: Vec<LocItem>,
797
798    /// Human-readable error message.
799    pub msg: String,
800
801    /// The input value that failed validation.
802    ///
803    /// This is the actual value the user provided that caused the error.
804    #[serde(skip_serializing_if = "Option::is_none")]
805    pub input: Option<serde_json::Value>,
806
807    /// Additional context about the validation constraint.
808    ///
809    /// Examples:
810    /// - `{"min_length": 3}` for string_too_short
811    /// - `{"ge": 0}` for greater_than_equal
812    /// - `{"pattern": "^\\d+$"}` for pattern mismatch
813    #[serde(skip_serializing_if = "Option::is_none")]
814    pub ctx: Option<HashMap<String, serde_json::Value>>,
815}
816
817impl ValidationError {
818    /// Create a new validation error.
819    #[must_use]
820    pub fn new(error_type: &'static str, loc: Vec<LocItem>) -> Self {
821        Self {
822            error_type,
823            loc,
824            msg: Self::default_message(error_type),
825            input: None,
826            ctx: None,
827        }
828    }
829
830    /// Create a "missing" error for a required field.
831    #[must_use]
832    pub fn missing(loc: Vec<LocItem>) -> Self {
833        Self::new(error_types::MISSING, loc)
834    }
835
836    /// Create a "string_too_short" error.
837    #[must_use]
838    pub fn string_too_short(loc: Vec<LocItem>, min_length: usize) -> Self {
839        Self::new(error_types::STRING_TOO_SHORT, loc)
840            .with_msg(format!(
841                "String should have at least {min_length} character{}",
842                if min_length == 1 { "" } else { "s" }
843            ))
844            .with_ctx_value("min_length", serde_json::json!(min_length))
845    }
846
847    /// Create a "string_too_long" error.
848    #[must_use]
849    pub fn string_too_long(loc: Vec<LocItem>, max_length: usize) -> Self {
850        Self::new(error_types::STRING_TOO_LONG, loc)
851            .with_msg(format!(
852                "String should have at most {max_length} character{}",
853                if max_length == 1 { "" } else { "s" }
854            ))
855            .with_ctx_value("max_length", serde_json::json!(max_length))
856    }
857
858    /// Create a type error (e.g., expected int, got string).
859    #[must_use]
860    pub fn type_error(loc: Vec<LocItem>, expected_type: &'static str) -> Self {
861        let error_type = match expected_type {
862            "string" => error_types::STRING_TYPE,
863            "int" | "integer" => error_types::INT_TYPE,
864            "float" | "number" => error_types::FLOAT_TYPE,
865            "bool" | "boolean" => error_types::BOOL_TYPE,
866            _ => error_types::VALUE_ERROR,
867        };
868        Self::new(error_type, loc).with_msg(format!("Input should be a valid {expected_type}"))
869    }
870
871    /// Create a JSON parsing error.
872    #[must_use]
873    pub fn json_invalid(loc: Vec<LocItem>, message: impl Into<String>) -> Self {
874        Self::new(error_types::JSON_INVALID, loc).with_msg(message)
875    }
876
877    /// Set the human-readable message.
878    #[must_use]
879    pub fn with_msg(mut self, msg: impl Into<String>) -> Self {
880        self.msg = msg.into();
881        self
882    }
883
884    /// Set the input value.
885    #[must_use]
886    pub fn with_input(mut self, input: serde_json::Value) -> Self {
887        self.input = Some(input);
888        self
889    }
890
891    /// Add a context key-value pair.
892    #[must_use]
893    pub fn with_ctx_value(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
894        self.ctx
895            .get_or_insert_with(HashMap::new)
896            .insert(key.into(), value);
897        self
898    }
899
900    /// Set the full context map.
901    #[must_use]
902    pub fn with_ctx(mut self, ctx: HashMap<String, serde_json::Value>) -> Self {
903        self.ctx = Some(ctx);
904        self
905    }
906
907    /// Add location items to the path.
908    #[must_use]
909    pub fn with_loc_prefix(mut self, prefix: Vec<LocItem>) -> Self {
910        let mut new_loc = prefix;
911        new_loc.extend(self.loc);
912        self.loc = new_loc;
913        self
914    }
915
916    /// Append a location item to the path.
917    #[must_use]
918    pub fn with_loc_suffix(mut self, item: impl Into<LocItem>) -> Self {
919        self.loc.push(item.into());
920        self
921    }
922
923    /// Create a "greater_than_equal" error for minimum value constraint.
924    #[must_use]
925    pub fn greater_than_equal<T: std::fmt::Display>(loc: Vec<LocItem>, min: T) -> Self {
926        let min_str = min.to_string();
927        Self::new(error_types::GREATER_THAN_EQUAL, loc)
928            .with_msg(format!(
929                "Input should be greater than or equal to {min_str}"
930            ))
931            .with_ctx_value("ge", serde_json::json!(min_str))
932    }
933
934    /// Create a "less_than_equal" error for maximum value constraint.
935    #[must_use]
936    pub fn less_than_equal<T: std::fmt::Display>(loc: Vec<LocItem>, max: T) -> Self {
937        let max_str = max.to_string();
938        Self::new(error_types::LESS_THAN_EQUAL, loc)
939            .with_msg(format!("Input should be less than or equal to {max_str}"))
940            .with_ctx_value("le", serde_json::json!(max_str))
941    }
942
943    /// Create a "string_pattern_mismatch" error for regex pattern constraint.
944    #[must_use]
945    pub fn pattern_mismatch(loc: Vec<LocItem>, pattern: &str) -> Self {
946        Self::new(error_types::STRING_PATTERN_MISMATCH, loc)
947            .with_msg(format!("String should match pattern '{pattern}'"))
948            .with_ctx_value("pattern", serde_json::json!(pattern))
949    }
950
951    /// Create a "value_error" for invalid email format.
952    #[must_use]
953    pub fn invalid_email(loc: Vec<LocItem>) -> Self {
954        Self::new(error_types::VALUE_ERROR, loc).with_msg("Value is not a valid email address")
955    }
956
957    /// Create a "value_error" for invalid URL format.
958    #[must_use]
959    pub fn invalid_url(loc: Vec<LocItem>) -> Self {
960        Self::new(error_types::URL_TYPE, loc).with_msg("Input should be a valid URL")
961    }
962
963    /// Create a "value_error" for invalid UUID format.
964    #[must_use]
965    pub fn invalid_uuid(loc: Vec<LocItem>) -> Self {
966        Self::new(error_types::UUID_TYPE, loc).with_msg("Input should be a valid UUID")
967    }
968
969    /// Create a generic "value_error" with custom message.
970    #[must_use]
971    pub fn value_error(loc: Vec<LocItem>, msg: impl Into<String>) -> Self {
972        Self::new(error_types::VALUE_ERROR, loc).with_msg(msg)
973    }
974
975    /// Get the default message for an error type.
976    fn default_message(error_type: &str) -> String {
977        match error_type {
978            error_types::MISSING => "Field required".to_owned(),
979            error_types::STRING_TOO_SHORT => "String too short".to_owned(),
980            error_types::STRING_TOO_LONG => "String too long".to_owned(),
981            error_types::STRING_TYPE => "Input should be a valid string".to_owned(),
982            error_types::INT_TYPE => "Input should be a valid integer".to_owned(),
983            error_types::FLOAT_TYPE => "Input should be a valid number".to_owned(),
984            error_types::BOOL_TYPE => "Input should be a valid boolean".to_owned(),
985            error_types::JSON_INVALID => "Invalid JSON".to_owned(),
986            error_types::VALUE_ERROR => "Value error".to_owned(),
987            _ => "Validation error".to_owned(),
988        }
989    }
990}
991
992// ============================================================================
993// Validation Errors Collection
994// ============================================================================
995
996/// Collection of validation errors (422 Unprocessable Entity response).
997///
998/// This collects multiple validation errors from extractors and validators,
999/// producing a FastAPI-compatible error response format.
1000///
1001/// # Examples
1002///
1003/// ```
1004/// use fastapi_core::error::{ValidationErrors, ValidationError, loc, error_types};
1005/// use serde_json::json;
1006///
1007/// let mut errors = ValidationErrors::new();
1008///
1009/// // Add multiple errors
1010/// errors.push(ValidationError::missing(loc::query("q")));
1011/// errors.push(ValidationError::string_too_short(loc::body_field("name"), 3)
1012///     .with_input(json!("ab")));
1013///
1014/// // Check and convert
1015/// if !errors.is_empty() {
1016///     let json = errors.to_json();
1017///     assert!(json.contains("missing"));
1018/// }
1019/// ```
1020#[derive(Debug, Clone, Default)]
1021pub struct ValidationErrors {
1022    /// The collected errors.
1023    pub errors: Vec<ValidationError>,
1024    /// The original input body (if available).
1025    pub body: Option<serde_json::Value>,
1026    /// Debug information (only included in response when debug mode is enabled).
1027    pub debug_info: Option<DebugInfo>,
1028}
1029
1030impl ValidationErrors {
1031    /// Create empty validation errors.
1032    #[must_use]
1033    pub fn new() -> Self {
1034        Self {
1035            errors: Vec::new(),
1036            body: None,
1037            debug_info: None,
1038        }
1039    }
1040
1041    /// Create from a single error.
1042    #[must_use]
1043    pub fn single(error: ValidationError) -> Self {
1044        Self {
1045            errors: vec![error],
1046            body: None,
1047            debug_info: None,
1048        }
1049    }
1050
1051    /// Create from multiple errors.
1052    #[must_use]
1053    pub fn from_errors(errors: Vec<ValidationError>) -> Self {
1054        Self {
1055            errors,
1056            body: None,
1057            debug_info: None,
1058        }
1059    }
1060
1061    /// Add an error.
1062    pub fn push(&mut self, error: ValidationError) {
1063        self.errors.push(error);
1064    }
1065
1066    /// Add multiple errors.
1067    pub fn extend(&mut self, errors: impl IntoIterator<Item = ValidationError>) {
1068        self.errors.extend(errors);
1069    }
1070
1071    /// Set the original body that failed validation.
1072    #[must_use]
1073    pub fn with_body(mut self, body: serde_json::Value) -> Self {
1074        self.body = Some(body);
1075        self
1076    }
1077
1078    /// Add debug information.
1079    ///
1080    /// This information will only be included in the response when debug mode
1081    /// is enabled (via `enable_debug_mode()`).
1082    #[must_use]
1083    pub fn with_debug_info(mut self, debug_info: DebugInfo) -> Self {
1084        self.debug_info = Some(debug_info);
1085        self
1086    }
1087
1088    /// Check if there are any errors.
1089    #[must_use]
1090    pub fn is_empty(&self) -> bool {
1091        self.errors.is_empty()
1092    }
1093
1094    /// Get the number of errors.
1095    #[must_use]
1096    pub fn len(&self) -> usize {
1097        self.errors.len()
1098    }
1099
1100    /// Get an iterator over the errors.
1101    pub fn iter(&self) -> impl Iterator<Item = &ValidationError> {
1102        self.errors.iter()
1103    }
1104
1105    /// Convert to FastAPI-compatible JSON string.
1106    #[must_use]
1107    pub fn to_json(&self) -> String {
1108        #[derive(Serialize)]
1109        struct Body<'a> {
1110            detail: &'a [ValidationError],
1111        }
1112
1113        serde_json::to_string(&Body {
1114            detail: &self.errors,
1115        })
1116        .unwrap_or_else(|_| r#"{"detail":[]}"#.to_owned())
1117    }
1118
1119    /// Convert to JSON bytes.
1120    #[must_use]
1121    pub fn to_json_bytes(&self) -> Vec<u8> {
1122        #[derive(Serialize)]
1123        struct Body<'a> {
1124            detail: &'a [ValidationError],
1125        }
1126
1127        serde_json::to_vec(&Body {
1128            detail: &self.errors,
1129        })
1130        .unwrap_or_else(|_| b"{\"detail\":[]}".to_vec())
1131    }
1132
1133    /// Merge another ValidationErrors into this one.
1134    pub fn merge(&mut self, other: ValidationErrors) {
1135        self.errors.extend(other.errors);
1136        if self.body.is_none() {
1137            self.body = other.body;
1138        }
1139        if self.debug_info.is_none() {
1140            self.debug_info = other.debug_info;
1141        }
1142    }
1143
1144    /// Add a prefix to all error locations.
1145    #[must_use]
1146    pub fn with_loc_prefix(mut self, prefix: Vec<LocItem>) -> Self {
1147        for error in &mut self.errors {
1148            let mut new_loc = prefix.clone();
1149            new_loc.extend(std::mem::take(&mut error.loc));
1150            error.loc = new_loc;
1151        }
1152        self
1153    }
1154}
1155
1156impl IntoIterator for ValidationErrors {
1157    type Item = ValidationError;
1158    type IntoIter = std::vec::IntoIter<ValidationError>;
1159
1160    fn into_iter(self) -> Self::IntoIter {
1161        self.errors.into_iter()
1162    }
1163}
1164
1165impl<'a> IntoIterator for &'a ValidationErrors {
1166    type Item = &'a ValidationError;
1167    type IntoIter = std::slice::Iter<'a, ValidationError>;
1168
1169    fn into_iter(self) -> Self::IntoIter {
1170        self.errors.iter()
1171    }
1172}
1173
1174impl Extend<ValidationError> for ValidationErrors {
1175    fn extend<T: IntoIterator<Item = ValidationError>>(&mut self, iter: T) {
1176        self.errors.extend(iter);
1177    }
1178}
1179
1180impl FromIterator<ValidationError> for ValidationErrors {
1181    fn from_iter<T: IntoIterator<Item = ValidationError>>(iter: T) -> Self {
1182        Self::from_errors(iter.into_iter().collect())
1183    }
1184}
1185
1186impl std::fmt::Display for ValidationErrors {
1187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1188        write!(f, "{} validation error", self.errors.len())?;
1189        if self.errors.len() != 1 {
1190            write!(f, "s")?;
1191        }
1192        Ok(())
1193    }
1194}
1195
1196impl std::error::Error for ValidationErrors {}
1197
1198impl IntoResponse for ValidationErrors {
1199    fn into_response(self) -> Response {
1200        // Conditionally include debug info based on global debug mode flag
1201        let body = if is_debug_mode_enabled() {
1202            if let Some(ref debug_info) = self.debug_info {
1203                #[derive(Serialize)]
1204                struct BodyWithDebug<'a> {
1205                    detail: &'a [ValidationError],
1206                    debug: &'a DebugInfo,
1207                }
1208                serde_json::to_vec(&BodyWithDebug {
1209                    detail: &self.errors,
1210                    debug: debug_info,
1211                })
1212                .unwrap_or_else(|_| b"{\"detail\":[]}".to_vec())
1213            } else {
1214                self.to_json_bytes()
1215            }
1216        } else {
1217            self.to_json_bytes()
1218        };
1219
1220        Response::with_status(StatusCode::UNPROCESSABLE_ENTITY)
1221            .header("content-type", b"application/json".to_vec())
1222            .body(ResponseBody::Bytes(body))
1223    }
1224}
1225
1226// ============================================================================
1227// Response Validation Error (500 Internal Server Error)
1228// ============================================================================
1229
1230/// Response validation error for internal failures (500 Internal Server Error).
1231///
1232/// This error type is used when a response fails to serialize or validate
1233/// against the expected response model. Unlike [`ValidationErrors`] which
1234/// indicates client errors (422), this represents a bug in the server code
1235/// and returns a 500 status.
1236///
1237/// # When This Is Used
1238///
1239/// - Response fails to serialize to JSON
1240/// - Response doesn't match the declared response_model
1241/// - Internal data transformation fails
1242///
1243/// # Security Note
1244///
1245/// Error details and the original response content are logged server-side but
1246/// are NOT exposed to clients (unless debug mode is explicitly enabled).
1247/// This prevents leaking internal implementation details.
1248///
1249/// # Examples
1250///
1251/// ```
1252/// use fastapi_core::error::{ResponseValidationError, ValidationError, loc};
1253///
1254/// // Serialization failure
1255/// let error = ResponseValidationError::serialization_failed(
1256///     "failed to serialize field 'created_at': invalid date format"
1257/// );
1258///
1259/// // Response model validation failure
1260/// let error = ResponseValidationError::new()
1261///     .with_error(ValidationError::missing(loc::response_field("user_id")))
1262///     .with_response_content(serde_json::json!({"name": "Alice"}));
1263/// ```
1264#[derive(Debug, Clone, Default)]
1265pub struct ResponseValidationError {
1266    /// The validation errors that occurred.
1267    pub errors: Vec<ValidationError>,
1268    /// The response content that failed validation (for logging only).
1269    /// This is NOT included in the response to clients.
1270    pub response_content: Option<serde_json::Value>,
1271    /// A summary message for logging.
1272    pub summary: Option<String>,
1273    /// Debug information (only included in response when debug mode is enabled).
1274    pub debug_info: Option<DebugInfo>,
1275}
1276
1277impl ResponseValidationError {
1278    /// Create a new empty response validation error.
1279    #[must_use]
1280    pub fn new() -> Self {
1281        Self::default()
1282    }
1283
1284    /// Create a serialization failure error.
1285    ///
1286    /// Use this when response serialization to JSON fails.
1287    #[must_use]
1288    pub fn serialization_failed(message: impl Into<String>) -> Self {
1289        let msg = message.into();
1290        Self {
1291            errors: vec![
1292                ValidationError::new(
1293                    error_types::SERIALIZATION_ERROR,
1294                    vec![LocItem::field("response")],
1295                )
1296                .with_msg(&msg),
1297            ],
1298            response_content: None,
1299            summary: Some(msg),
1300            debug_info: None,
1301        }
1302    }
1303
1304    /// Create a response model validation failure error.
1305    ///
1306    /// Use this when the response doesn't match the declared response_model.
1307    #[must_use]
1308    pub fn model_validation_failed(message: impl Into<String>) -> Self {
1309        let msg = message.into();
1310        Self {
1311            errors: vec![
1312                ValidationError::new(
1313                    error_types::MODEL_VALIDATION_ERROR,
1314                    vec![LocItem::field("response")],
1315                )
1316                .with_msg(&msg),
1317            ],
1318            response_content: None,
1319            summary: Some(msg),
1320            debug_info: None,
1321        }
1322    }
1323
1324    /// Add a validation error.
1325    #[must_use]
1326    pub fn with_error(mut self, error: ValidationError) -> Self {
1327        self.errors.push(error);
1328        self
1329    }
1330
1331    /// Add multiple validation errors.
1332    #[must_use]
1333    pub fn with_errors(mut self, errors: impl IntoIterator<Item = ValidationError>) -> Self {
1334        self.errors.extend(errors);
1335        self
1336    }
1337
1338    /// Set the response content that failed validation.
1339    ///
1340    /// This is logged server-side but NOT exposed to clients.
1341    #[must_use]
1342    pub fn with_response_content(mut self, content: serde_json::Value) -> Self {
1343        self.response_content = Some(content);
1344        self
1345    }
1346
1347    /// Set a summary message for logging.
1348    #[must_use]
1349    pub fn with_summary(mut self, summary: impl Into<String>) -> Self {
1350        self.summary = Some(summary.into());
1351        self
1352    }
1353
1354    /// Add debug information.
1355    ///
1356    /// This information will only be included in the response when debug mode
1357    /// is enabled (via `enable_debug_mode()`).
1358    #[must_use]
1359    pub fn with_debug_info(mut self, debug_info: DebugInfo) -> Self {
1360        self.debug_info = Some(debug_info);
1361        self
1362    }
1363
1364    /// Check if there are no errors.
1365    #[must_use]
1366    pub fn is_empty(&self) -> bool {
1367        self.errors.is_empty()
1368    }
1369
1370    /// Get the number of errors.
1371    #[must_use]
1372    pub fn len(&self) -> usize {
1373        self.errors.len()
1374    }
1375
1376    /// Get an iterator over the errors.
1377    pub fn iter(&self) -> impl Iterator<Item = &ValidationError> {
1378        self.errors.iter()
1379    }
1380
1381    /// Log the error details (for server-side logging).
1382    ///
1383    /// This returns a formatted string suitable for logging that includes
1384    /// the error details and response content (if any).
1385    #[must_use]
1386    pub fn to_log_string(&self) -> String {
1387        let mut parts = Vec::new();
1388
1389        if let Some(ref summary) = self.summary {
1390            parts.push(format!("Summary: {}", summary));
1391        }
1392
1393        parts.push(format!("Errors ({}): ", self.errors.len()));
1394        for (i, error) in self.errors.iter().enumerate() {
1395            let loc_str: Vec<String> = error
1396                .loc
1397                .iter()
1398                .map(|item| match item {
1399                    LocItem::Field(s) => s.clone(),
1400                    LocItem::Index(i) => i.to_string(),
1401                })
1402                .collect();
1403            parts.push(format!(
1404                "  [{}] {} at [{}]: {}",
1405                i + 1,
1406                error.error_type,
1407                loc_str.join("."),
1408                error.msg
1409            ));
1410        }
1411
1412        if let Some(ref content) = self.response_content {
1413            // Truncate large content for logging (UTF-8 safe)
1414            let content_str = serde_json::to_string(content).unwrap_or_default();
1415            let truncated = if content_str.len() > 500 {
1416                // Find a safe UTF-8 boundary near 500 chars
1417                let mut end = 500;
1418                while end > 0 && !content_str.is_char_boundary(end) {
1419                    end -= 1;
1420                }
1421                format!("{}...(truncated)", &content_str[..end])
1422            } else {
1423                content_str
1424            };
1425            parts.push(format!("Response content: {}", truncated));
1426        }
1427
1428        parts.join("\n")
1429    }
1430}
1431
1432impl std::fmt::Display for ResponseValidationError {
1433    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1434        write!(f, "Response validation failed")?;
1435        if let Some(ref summary) = self.summary {
1436            write!(f, ": {}", summary)?;
1437        }
1438        Ok(())
1439    }
1440}
1441
1442impl std::error::Error for ResponseValidationError {}
1443
1444impl IntoResponse for ResponseValidationError {
1445    fn into_response(self) -> Response {
1446        // Internal errors should be logged server-side.
1447        //
1448        // This framework intentionally does not pick a logging backend. If you want
1449        // structured logs, log `self.to_log_string()` from your middleware/handler.
1450        //
1451        // Debug mode controls what details are included in the client response.
1452
1453        // Build the response body
1454        let body = if is_debug_mode_enabled() {
1455            // In debug mode, include error details
1456            #[derive(Serialize)]
1457            struct DebugBody<'a> {
1458                error: &'static str,
1459                detail: &'static str,
1460                #[serde(skip_serializing_if = "Option::is_none")]
1461                debug: Option<DebugResponseInfo<'a>>,
1462            }
1463
1464            #[derive(Serialize)]
1465            struct DebugResponseInfo<'a> {
1466                #[serde(skip_serializing_if = "Option::is_none")]
1467                summary: Option<&'a str>,
1468                errors: &'a [ValidationError],
1469                #[serde(skip_serializing_if = "Option::is_none")]
1470                response_content: &'a Option<serde_json::Value>,
1471                #[serde(skip_serializing_if = "Option::is_none")]
1472                source: Option<&'a DebugInfo>,
1473            }
1474
1475            let debug_info = DebugResponseInfo {
1476                summary: self.summary.as_deref(),
1477                errors: &self.errors,
1478                response_content: &self.response_content,
1479                source: self.debug_info.as_ref(),
1480            };
1481
1482            serde_json::to_vec(&DebugBody {
1483                error: "internal_server_error",
1484                detail: "Response validation failed",
1485                debug: Some(debug_info),
1486            })
1487            .unwrap_or_else(|_| {
1488                b"{\"error\":\"internal_server_error\",\"detail\":\"Internal Server Error\"}"
1489                    .to_vec()
1490            })
1491        } else {
1492            // In production, return a generic error message
1493            b"{\"error\":\"internal_server_error\",\"detail\":\"Internal Server Error\"}".to_vec()
1494        };
1495
1496        Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
1497            .header("content-type", b"application/json".to_vec())
1498            .body(ResponseBody::Bytes(body))
1499    }
1500}
1501
1502// ============================================================================
1503// Tests
1504// ============================================================================
1505
1506#[cfg(test)]
1507mod tests {
1508    use super::*;
1509    use serde_json::json;
1510    use serial_test::serial;
1511
1512    // ========================================================================
1513    // LocItem Tests
1514    // ========================================================================
1515
1516    #[test]
1517    fn loc_item_field_creation() {
1518        let item = LocItem::field("email");
1519        assert_eq!(item.as_str(), Some("email"));
1520        assert_eq!(item.as_index(), None);
1521    }
1522
1523    #[test]
1524    fn loc_item_index_creation() {
1525        let item = LocItem::index(42);
1526        assert_eq!(item.as_str(), None);
1527        assert_eq!(item.as_index(), Some(42));
1528    }
1529
1530    #[test]
1531    fn loc_item_from_str() {
1532        let item: LocItem = "name".into();
1533        assert_eq!(item, LocItem::Field("name".to_owned()));
1534    }
1535
1536    #[test]
1537    fn loc_item_from_string() {
1538        let item: LocItem = String::from("age").into();
1539        assert_eq!(item, LocItem::Field("age".to_owned()));
1540    }
1541
1542    #[test]
1543    fn loc_item_from_usize() {
1544        let item: LocItem = 5usize.into();
1545        assert_eq!(item, LocItem::Index(5));
1546    }
1547
1548    #[test]
1549    fn loc_item_serialize_field() {
1550        let item = LocItem::field("email");
1551        let json = serde_json::to_string(&item).unwrap();
1552        assert_eq!(json, "\"email\"");
1553    }
1554
1555    #[test]
1556    fn loc_item_serialize_index() {
1557        let item = LocItem::index(3);
1558        let json = serde_json::to_string(&item).unwrap();
1559        assert_eq!(json, "3");
1560    }
1561
1562    // ========================================================================
1563    // Location Helper Tests
1564    // ========================================================================
1565
1566    #[test]
1567    fn loc_path_creates_correct_location() {
1568        let loc = loc::path("user_id");
1569        assert_eq!(loc.len(), 2);
1570        assert_eq!(loc[0].as_str(), Some("path"));
1571        assert_eq!(loc[1].as_str(), Some("user_id"));
1572    }
1573
1574    #[test]
1575    fn loc_query_creates_correct_location() {
1576        let loc = loc::query("q");
1577        assert_eq!(loc.len(), 2);
1578        assert_eq!(loc[0].as_str(), Some("query"));
1579        assert_eq!(loc[1].as_str(), Some("q"));
1580    }
1581
1582    #[test]
1583    fn loc_header_creates_correct_location() {
1584        let loc = loc::header("Authorization");
1585        assert_eq!(loc.len(), 2);
1586        assert_eq!(loc[0].as_str(), Some("header"));
1587        assert_eq!(loc[1].as_str(), Some("Authorization"));
1588    }
1589
1590    #[test]
1591    fn loc_cookie_creates_correct_location() {
1592        let loc = loc::cookie("session_id");
1593        assert_eq!(loc.len(), 2);
1594        assert_eq!(loc[0].as_str(), Some("cookie"));
1595        assert_eq!(loc[1].as_str(), Some("session_id"));
1596    }
1597
1598    #[test]
1599    fn loc_body_creates_root_location() {
1600        let loc = loc::body();
1601        assert_eq!(loc.len(), 1);
1602        assert_eq!(loc[0].as_str(), Some("body"));
1603    }
1604
1605    #[test]
1606    fn loc_body_field_creates_correct_location() {
1607        let loc = loc::body_field("email");
1608        assert_eq!(loc.len(), 2);
1609        assert_eq!(loc[0].as_str(), Some("body"));
1610        assert_eq!(loc[1].as_str(), Some("email"));
1611    }
1612
1613    #[test]
1614    fn loc_body_path_creates_nested_location() {
1615        let loc = loc::body_path(&["user", "profile", "name"]);
1616        assert_eq!(loc.len(), 4);
1617        assert_eq!(loc[0].as_str(), Some("body"));
1618        assert_eq!(loc[1].as_str(), Some("user"));
1619        assert_eq!(loc[2].as_str(), Some("profile"));
1620        assert_eq!(loc[3].as_str(), Some("name"));
1621    }
1622
1623    #[test]
1624    fn loc_body_indexed_creates_array_location() {
1625        let loc = loc::body_indexed("items", 0);
1626        assert_eq!(loc.len(), 3);
1627        assert_eq!(loc[0].as_str(), Some("body"));
1628        assert_eq!(loc[1].as_str(), Some("items"));
1629        assert_eq!(loc[2].as_index(), Some(0));
1630    }
1631
1632    // ========================================================================
1633    // ValidationError Tests
1634    // ========================================================================
1635
1636    #[test]
1637    fn validation_error_new_with_default_message() {
1638        let error = ValidationError::new(error_types::MISSING, loc::query("q"));
1639        assert_eq!(error.error_type, "missing");
1640        assert_eq!(error.msg, "Field required");
1641        assert!(error.input.is_none());
1642        assert!(error.ctx.is_none());
1643    }
1644
1645    #[test]
1646    fn validation_error_missing() {
1647        let error = ValidationError::missing(loc::query("page"));
1648        assert_eq!(error.error_type, "missing");
1649        assert_eq!(error.msg, "Field required");
1650    }
1651
1652    #[test]
1653    fn validation_error_string_too_short() {
1654        let error = ValidationError::string_too_short(loc::body_field("name"), 3);
1655        assert_eq!(error.error_type, "string_too_short");
1656        assert!(error.msg.contains("3"));
1657        assert!(error.ctx.is_some());
1658        let ctx = error.ctx.unwrap();
1659        assert_eq!(ctx.get("min_length"), Some(&json!(3)));
1660    }
1661
1662    #[test]
1663    fn validation_error_string_too_long() {
1664        let error = ValidationError::string_too_long(loc::body_field("bio"), 500);
1665        assert_eq!(error.error_type, "string_too_long");
1666        assert!(error.msg.contains("500"));
1667        assert!(error.ctx.is_some());
1668        let ctx = error.ctx.unwrap();
1669        assert_eq!(ctx.get("max_length"), Some(&json!(500)));
1670    }
1671
1672    #[test]
1673    fn validation_error_type_error_int() {
1674        let error = ValidationError::type_error(loc::query("count"), "integer");
1675        assert_eq!(error.error_type, "int_type");
1676        assert!(error.msg.contains("integer"));
1677    }
1678
1679    #[test]
1680    fn validation_error_type_error_string() {
1681        let error = ValidationError::type_error(loc::body_field("name"), "string");
1682        assert_eq!(error.error_type, "string_type");
1683        assert!(error.msg.contains("string"));
1684    }
1685
1686    #[test]
1687    fn validation_error_json_invalid() {
1688        let error = ValidationError::json_invalid(loc::body(), "unexpected end of input");
1689        assert_eq!(error.error_type, "json_invalid");
1690        assert_eq!(error.msg, "unexpected end of input");
1691    }
1692
1693    #[test]
1694    fn validation_error_with_input() {
1695        let error = ValidationError::missing(loc::query("q")).with_input(json!(null));
1696        assert_eq!(error.input, Some(json!(null)));
1697    }
1698
1699    #[test]
1700    fn validation_error_with_ctx_value() {
1701        let error = ValidationError::new(error_types::GREATER_THAN_EQUAL, loc::body_field("age"))
1702            .with_ctx_value("ge", json!(0));
1703        assert!(error.ctx.is_some());
1704        assert_eq!(error.ctx.unwrap().get("ge"), Some(&json!(0)));
1705    }
1706
1707    #[test]
1708    fn validation_error_with_multiple_ctx_values() {
1709        let error = ValidationError::new(
1710            error_types::STRING_PATTERN_MISMATCH,
1711            loc::body_field("email"),
1712        )
1713        .with_ctx_value("pattern", json!("^.+@.+$"))
1714        .with_ctx_value("expected", json!("email format"));
1715        let ctx = error.ctx.unwrap();
1716        assert_eq!(ctx.len(), 2);
1717        assert_eq!(ctx.get("pattern"), Some(&json!("^.+@.+$")));
1718        assert_eq!(ctx.get("expected"), Some(&json!("email format")));
1719    }
1720
1721    #[test]
1722    fn validation_error_with_loc_prefix() {
1723        let error = ValidationError::missing(vec![LocItem::field("email")])
1724            .with_loc_prefix(vec![LocItem::field("body"), LocItem::field("user")]);
1725        assert_eq!(error.loc.len(), 3);
1726        assert_eq!(error.loc[0].as_str(), Some("body"));
1727        assert_eq!(error.loc[1].as_str(), Some("user"));
1728        assert_eq!(error.loc[2].as_str(), Some("email"));
1729    }
1730
1731    #[test]
1732    fn validation_error_with_loc_suffix() {
1733        let error = ValidationError::missing(loc::body())
1734            .with_loc_suffix("items")
1735            .with_loc_suffix(0usize)
1736            .with_loc_suffix("name");
1737        assert_eq!(error.loc.len(), 4);
1738        assert_eq!(error.loc[0].as_str(), Some("body"));
1739        assert_eq!(error.loc[1].as_str(), Some("items"));
1740        assert_eq!(error.loc[2].as_index(), Some(0));
1741        assert_eq!(error.loc[3].as_str(), Some("name"));
1742    }
1743
1744    // ========================================================================
1745    // ValidationError Serialization Tests
1746    // ========================================================================
1747
1748    #[test]
1749    fn validation_error_serializes_to_fastapi_format() {
1750        let error = ValidationError::missing(loc::query("q"));
1751        let json = serde_json::to_value(&error).unwrap();
1752
1753        assert_eq!(json["type"], "missing");
1754        assert_eq!(json["loc"], json!(["query", "q"]));
1755        assert_eq!(json["msg"], "Field required");
1756        assert!(json.get("input").is_none()); // skip_serializing_if = None
1757        assert!(json.get("ctx").is_none());
1758    }
1759
1760    #[test]
1761    fn validation_error_serializes_with_array_index() {
1762        let error = ValidationError::missing(vec![
1763            LocItem::field("body"),
1764            LocItem::field("items"),
1765            LocItem::index(2),
1766            LocItem::field("name"),
1767        ]);
1768        let json = serde_json::to_value(&error).unwrap();
1769
1770        assert_eq!(json["loc"], json!(["body", "items", 2, "name"]));
1771    }
1772
1773    #[test]
1774    fn validation_error_serializes_with_input_and_ctx() {
1775        let error =
1776            ValidationError::string_too_short(loc::body_field("name"), 3).with_input(json!("ab"));
1777        let json = serde_json::to_value(&error).unwrap();
1778
1779        assert_eq!(json["input"], "ab");
1780        assert_eq!(json["ctx"]["min_length"], 3);
1781    }
1782
1783    // ========================================================================
1784    // ValidationErrors Collection Tests
1785    // ========================================================================
1786
1787    #[test]
1788    fn validation_errors_new_is_empty() {
1789        let errors = ValidationErrors::new();
1790        assert!(errors.is_empty());
1791        assert_eq!(errors.len(), 0);
1792    }
1793
1794    #[test]
1795    fn validation_errors_single() {
1796        let errors = ValidationErrors::single(ValidationError::missing(loc::query("q")));
1797        assert!(!errors.is_empty());
1798        assert_eq!(errors.len(), 1);
1799    }
1800
1801    #[test]
1802    fn validation_errors_push() {
1803        let mut errors = ValidationErrors::new();
1804        errors.push(ValidationError::missing(loc::query("q")));
1805        errors.push(ValidationError::missing(loc::query("page")));
1806        assert_eq!(errors.len(), 2);
1807    }
1808
1809    #[test]
1810    fn validation_errors_extend() {
1811        let mut errors = ValidationErrors::new();
1812        errors.extend(vec![
1813            ValidationError::missing(loc::query("q")),
1814            ValidationError::missing(loc::query("page")),
1815        ]);
1816        assert_eq!(errors.len(), 2);
1817    }
1818
1819    #[test]
1820    fn validation_errors_from_errors() {
1821        let errors = ValidationErrors::from_errors(vec![
1822            ValidationError::missing(loc::query("q")),
1823            ValidationError::string_too_short(loc::body_field("name"), 1),
1824        ]);
1825        assert_eq!(errors.len(), 2);
1826    }
1827
1828    #[test]
1829    fn validation_errors_with_body() {
1830        let body = json!({"name": ""});
1831        let errors = ValidationErrors::single(ValidationError::string_too_short(
1832            loc::body_field("name"),
1833            1,
1834        ))
1835        .with_body(body.clone());
1836
1837        assert_eq!(errors.body, Some(body));
1838    }
1839
1840    #[test]
1841    fn validation_errors_merge() {
1842        let mut errors1 = ValidationErrors::single(ValidationError::missing(loc::query("q")));
1843        let errors2 = ValidationErrors::single(ValidationError::missing(loc::query("page")));
1844
1845        errors1.merge(errors2);
1846        assert_eq!(errors1.len(), 2);
1847    }
1848
1849    #[test]
1850    fn validation_errors_with_loc_prefix() {
1851        let errors = ValidationErrors::from_errors(vec![
1852            ValidationError::missing(vec![LocItem::field("name")]),
1853            ValidationError::missing(vec![LocItem::field("email")]),
1854        ])
1855        .with_loc_prefix(vec![LocItem::field("body"), LocItem::field("user")]);
1856
1857        for error in &errors {
1858            assert_eq!(error.loc[0].as_str(), Some("body"));
1859            assert_eq!(error.loc[1].as_str(), Some("user"));
1860        }
1861    }
1862
1863    #[test]
1864    fn validation_errors_iter() {
1865        let errors = ValidationErrors::from_errors(vec![
1866            ValidationError::missing(loc::query("q")),
1867            ValidationError::missing(loc::query("page")),
1868        ]);
1869
1870        let count = errors.iter().count();
1871        assert_eq!(count, 2);
1872    }
1873
1874    #[test]
1875    fn validation_errors_into_iter() {
1876        let errors = ValidationErrors::from_errors(vec![
1877            ValidationError::missing(loc::query("q")),
1878            ValidationError::missing(loc::query("page")),
1879        ]);
1880
1881        let collected: Vec<_> = errors.into_iter().collect();
1882        assert_eq!(collected.len(), 2);
1883    }
1884
1885    #[test]
1886    fn validation_errors_from_iterator() {
1887        let errors: ValidationErrors = vec![
1888            ValidationError::missing(loc::query("q")),
1889            ValidationError::missing(loc::query("page")),
1890        ]
1891        .into_iter()
1892        .collect();
1893
1894        assert_eq!(errors.len(), 2);
1895    }
1896
1897    // ========================================================================
1898    // ValidationErrors JSON Output Tests
1899    // ========================================================================
1900
1901    #[test]
1902    fn validation_errors_to_json() {
1903        let errors = ValidationErrors::single(
1904            ValidationError::missing(loc::query("q")).with_input(json!(null)),
1905        );
1906        let json = errors.to_json();
1907
1908        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1909        assert!(parsed["detail"].is_array());
1910        assert_eq!(parsed["detail"][0]["type"], "missing");
1911        assert_eq!(parsed["detail"][0]["loc"], json!(["query", "q"]));
1912    }
1913
1914    #[test]
1915    fn validation_errors_to_json_bytes() {
1916        let errors = ValidationErrors::single(ValidationError::missing(loc::query("q")));
1917        let bytes = errors.to_json_bytes();
1918        let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
1919
1920        assert!(json["detail"].is_array());
1921    }
1922
1923    #[test]
1924    fn validation_errors_fastapi_format_match() {
1925        // This tests the exact format FastAPI/Pydantic v2 produces
1926        let errors = ValidationErrors::from_errors(vec![
1927            ValidationError::missing(loc::query("q")),
1928            ValidationError::string_too_short(loc::body_field("name"), 3).with_input(json!("ab")),
1929        ]);
1930
1931        let json = errors.to_json();
1932        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1933
1934        // First error: missing query param
1935        assert_eq!(parsed["detail"][0]["type"], "missing");
1936        assert_eq!(parsed["detail"][0]["loc"], json!(["query", "q"]));
1937        assert_eq!(parsed["detail"][0]["msg"], "Field required");
1938
1939        // Second error: string too short
1940        assert_eq!(parsed["detail"][1]["type"], "string_too_short");
1941        assert_eq!(parsed["detail"][1]["loc"], json!(["body", "name"]));
1942        assert_eq!(parsed["detail"][1]["input"], "ab");
1943        assert_eq!(parsed["detail"][1]["ctx"]["min_length"], 3);
1944    }
1945
1946    #[test]
1947    fn validation_errors_nested_array_location() {
1948        // Test the complex nested case: body > items[0] > name
1949        let error = ValidationError::missing(vec![
1950            LocItem::field("body"),
1951            LocItem::field("items"),
1952            LocItem::index(0),
1953            LocItem::field("name"),
1954        ]);
1955        let errors = ValidationErrors::single(error);
1956        let json = errors.to_json();
1957        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1958
1959        assert_eq!(
1960            parsed["detail"][0]["loc"],
1961            json!(["body", "items", 0, "name"])
1962        );
1963    }
1964
1965    // ========================================================================
1966    // IntoResponse Tests
1967    // ========================================================================
1968
1969    #[test]
1970    fn validation_errors_into_response() {
1971        let errors = ValidationErrors::single(ValidationError::missing(loc::query("q")));
1972        let response = errors.into_response();
1973
1974        assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
1975
1976        // Check content-type header
1977        let content_type = response
1978            .headers()
1979            .iter()
1980            .find(|(name, _): &&(String, Vec<u8>)| name.eq_ignore_ascii_case("content-type"))
1981            .map(|(_, v)| v.as_slice());
1982        assert_eq!(content_type, Some(b"application/json".as_slice()));
1983    }
1984
1985    // ========================================================================
1986    // Error Types Constants Tests
1987    // ========================================================================
1988
1989    #[test]
1990    fn error_types_match_pydantic() {
1991        // Verify our constants match Pydantic v2 error types
1992        assert_eq!(error_types::MISSING, "missing");
1993        assert_eq!(error_types::STRING_TOO_SHORT, "string_too_short");
1994        assert_eq!(error_types::STRING_TOO_LONG, "string_too_long");
1995        assert_eq!(error_types::STRING_TYPE, "string_type");
1996        assert_eq!(error_types::INT_TYPE, "int_type");
1997        assert_eq!(error_types::FLOAT_TYPE, "float_type");
1998        assert_eq!(error_types::BOOL_TYPE, "bool_type");
1999        assert_eq!(error_types::JSON_INVALID, "json_invalid");
2000        assert_eq!(error_types::VALUE_ERROR, "value_error");
2001    }
2002
2003    // ========================================================================
2004    // HttpError Tests
2005    // ========================================================================
2006
2007    #[test]
2008    fn http_error_new_with_status() {
2009        let error = HttpError::new(StatusCode::NOT_FOUND);
2010        assert_eq!(error.status, StatusCode::NOT_FOUND);
2011        assert!(error.detail.is_none());
2012        assert!(error.headers.is_empty());
2013    }
2014
2015    #[test]
2016    fn http_error_bad_request() {
2017        let error = HttpError::bad_request();
2018        assert_eq!(error.status, StatusCode::BAD_REQUEST);
2019        assert_eq!(error.status.as_u16(), 400);
2020    }
2021
2022    #[test]
2023    fn http_error_unauthorized() {
2024        let error = HttpError::unauthorized();
2025        assert_eq!(error.status, StatusCode::UNAUTHORIZED);
2026        assert_eq!(error.status.as_u16(), 401);
2027    }
2028
2029    #[test]
2030    fn http_error_forbidden() {
2031        let error = HttpError::forbidden();
2032        assert_eq!(error.status, StatusCode::FORBIDDEN);
2033        assert_eq!(error.status.as_u16(), 403);
2034    }
2035
2036    #[test]
2037    fn http_error_not_found() {
2038        let error = HttpError::not_found();
2039        assert_eq!(error.status, StatusCode::NOT_FOUND);
2040        assert_eq!(error.status.as_u16(), 404);
2041    }
2042
2043    #[test]
2044    fn http_error_internal() {
2045        let error = HttpError::internal();
2046        assert_eq!(error.status, StatusCode::INTERNAL_SERVER_ERROR);
2047        assert_eq!(error.status.as_u16(), 500);
2048    }
2049
2050    #[test]
2051    fn http_error_payload_too_large() {
2052        let error = HttpError::payload_too_large();
2053        assert_eq!(error.status, StatusCode::PAYLOAD_TOO_LARGE);
2054        assert_eq!(error.status.as_u16(), 413);
2055    }
2056
2057    #[test]
2058    fn http_error_unsupported_media_type() {
2059        let error = HttpError::unsupported_media_type();
2060        assert_eq!(error.status, StatusCode::UNSUPPORTED_MEDIA_TYPE);
2061        assert_eq!(error.status.as_u16(), 415);
2062    }
2063
2064    #[test]
2065    fn http_error_with_detail() {
2066        let error = HttpError::not_found().with_detail("User not found");
2067        assert_eq!(error.detail, Some("User not found".to_owned()));
2068    }
2069
2070    #[test]
2071    fn http_error_with_detail_owned_string() {
2072        let detail = String::from("Resource missing");
2073        let error = HttpError::not_found().with_detail(detail);
2074        assert_eq!(error.detail, Some("Resource missing".to_owned()));
2075    }
2076
2077    #[test]
2078    fn http_error_with_header() {
2079        let error = HttpError::unauthorized()
2080            .with_header("WWW-Authenticate", b"Bearer realm=\"api\"".to_vec());
2081        assert_eq!(error.headers.len(), 1);
2082        assert_eq!(error.headers[0].0, "WWW-Authenticate");
2083        assert_eq!(error.headers[0].1, b"Bearer realm=\"api\"".to_vec());
2084    }
2085
2086    #[test]
2087    fn http_error_with_multiple_headers() {
2088        let error = HttpError::bad_request()
2089            .with_header("X-Error-Code", b"E001".to_vec())
2090            .with_header("X-Error-Context", b"validation".to_vec())
2091            .with_header("Retry-After", b"60".to_vec());
2092        assert_eq!(error.headers.len(), 3);
2093    }
2094
2095    #[test]
2096    fn http_error_with_detail_and_headers() {
2097        let error = HttpError::unauthorized()
2098            .with_detail("Invalid or expired token")
2099            .with_header("WWW-Authenticate", b"Bearer".to_vec())
2100            .with_header("X-Token-Expired", b"true".to_vec());
2101
2102        assert_eq!(error.detail, Some("Invalid or expired token".to_owned()));
2103        assert_eq!(error.headers.len(), 2);
2104    }
2105
2106    #[test]
2107    fn http_error_display_without_detail() {
2108        let error = HttpError::not_found();
2109        let display = format!("{}", error);
2110        assert_eq!(display, "Not Found");
2111    }
2112
2113    #[test]
2114    fn http_error_display_with_detail() {
2115        let error = HttpError::not_found().with_detail("User 123 not found");
2116        let display = format!("{}", error);
2117        assert_eq!(display, "Not Found: User 123 not found");
2118    }
2119
2120    #[test]
2121    fn http_error_is_error_trait() {
2122        let error: Box<dyn std::error::Error> = Box::new(HttpError::internal());
2123        // Just verify it compiles and we can use it as a trait object
2124        assert!(error.to_string().contains("Internal Server Error"));
2125    }
2126
2127    #[test]
2128    fn http_error_into_response_status() {
2129        let error = HttpError::forbidden();
2130        let response = error.into_response();
2131        assert_eq!(response.status(), StatusCode::FORBIDDEN);
2132    }
2133
2134    #[test]
2135    fn http_error_into_response_json_content_type() {
2136        let error = HttpError::bad_request();
2137        let response = error.into_response();
2138
2139        let content_type = response
2140            .headers()
2141            .iter()
2142            .find(|(name, _)| name.eq_ignore_ascii_case("content-type"))
2143            .map(|(_, v)| v.as_slice());
2144        assert_eq!(content_type, Some(b"application/json".as_slice()));
2145    }
2146
2147    #[test]
2148    fn http_error_into_response_json_body_format() {
2149        let error = HttpError::not_found().with_detail("Resource not found");
2150        let response = error.into_response();
2151
2152        // Extract body
2153        let body = match response.body_ref() {
2154            ResponseBody::Bytes(b) => b.clone(),
2155            _ => panic!("Expected bytes body"),
2156        };
2157
2158        let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap();
2159        assert_eq!(parsed["detail"], "Resource not found");
2160    }
2161
2162    #[test]
2163    fn http_error_into_response_default_detail() {
2164        // When no detail is provided, should use canonical reason
2165        let error = HttpError::not_found();
2166        let response = error.into_response();
2167
2168        let body = match response.body_ref() {
2169            ResponseBody::Bytes(b) => b.clone(),
2170            _ => panic!("Expected bytes body"),
2171        };
2172
2173        let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap();
2174        assert_eq!(parsed["detail"], "Not Found");
2175    }
2176
2177    #[test]
2178    fn http_error_into_response_with_custom_headers() {
2179        let error = HttpError::unauthorized()
2180            .with_detail("Token expired")
2181            .with_header("WWW-Authenticate", b"Bearer realm=\"api\"".to_vec());
2182
2183        let response = error.into_response();
2184
2185        // Check custom header is present
2186        let www_auth = response
2187            .headers()
2188            .iter()
2189            .find(|(name, _)| name.eq_ignore_ascii_case("www-authenticate"))
2190            .map(|(_, v)| v.as_slice());
2191        assert_eq!(www_auth, Some(b"Bearer realm=\"api\"".as_slice()));
2192    }
2193
2194    #[test]
2195    fn http_error_into_response_multiple_custom_headers() {
2196        let error = HttpError::bad_request()
2197            .with_header("X-Error-Code", b"VALIDATION_FAILED".to_vec())
2198            .with_header("X-Request-Id", b"abc-123".to_vec());
2199
2200        let response = error.into_response();
2201
2202        let headers: Vec<_> = response.headers().iter().collect();
2203
2204        // Should have content-type plus our two custom headers
2205        assert!(headers.len() >= 3);
2206
2207        let error_code = headers
2208            .iter()
2209            .find(|(name, _)| name.eq_ignore_ascii_case("x-error-code"))
2210            .map(|(_, v)| v.as_slice());
2211        assert_eq!(error_code, Some(b"VALIDATION_FAILED".as_slice()));
2212
2213        let request_id = headers
2214            .iter()
2215            .find(|(name, _)| name.eq_ignore_ascii_case("x-request-id"))
2216            .map(|(_, v)| v.as_slice());
2217        assert_eq!(request_id, Some(b"abc-123".as_slice()));
2218    }
2219
2220    #[test]
2221    fn http_error_response_body_is_valid_json() {
2222        // Test all status code variants produce valid JSON
2223        let errors = vec![
2224            HttpError::bad_request(),
2225            HttpError::unauthorized(),
2226            HttpError::forbidden(),
2227            HttpError::not_found(),
2228            HttpError::internal(),
2229            HttpError::payload_too_large(),
2230            HttpError::unsupported_media_type(),
2231        ];
2232
2233        for error in errors {
2234            let status = error.status;
2235            let response = error.into_response();
2236            let body = match response.body_ref() {
2237                ResponseBody::Bytes(b) => b.clone(),
2238                _ => panic!("Expected bytes body"),
2239            };
2240
2241            // Should parse as valid JSON
2242            let parsed: Result<serde_json::Value, _> = serde_json::from_slice(&body);
2243            assert!(
2244                parsed.is_ok(),
2245                "Failed to parse JSON for status {}: {:?}",
2246                status.as_u16(),
2247                String::from_utf8_lossy(&body)
2248            );
2249
2250            // Should have detail field
2251            let json = parsed.unwrap();
2252            assert!(
2253                json.get("detail").is_some(),
2254                "Missing detail field for status {}",
2255                status.as_u16()
2256            );
2257        }
2258    }
2259
2260    #[test]
2261    fn http_error_fastapi_compatible_format() {
2262        // Verify our error format matches FastAPI's HTTPException
2263        // FastAPI returns: {"detail": "message"}
2264        let error = HttpError::forbidden().with_detail("Insufficient permissions");
2265        let response = error.into_response();
2266
2267        let body = match response.body_ref() {
2268            ResponseBody::Bytes(b) => b.clone(),
2269            _ => panic!("Expected bytes body"),
2270        };
2271
2272        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
2273
2274        // Only "detail" key, no extra fields
2275        let obj = json.as_object().unwrap();
2276        assert_eq!(obj.len(), 1, "Expected only 'detail' field");
2277        assert_eq!(json["detail"], "Insufficient permissions");
2278    }
2279
2280    #[test]
2281    fn http_error_chained_builder_pattern() {
2282        // Verify builder pattern works correctly with method chaining
2283        let error = HttpError::new(StatusCode::TOO_MANY_REQUESTS)
2284            .with_detail("Rate limit exceeded")
2285            .with_header("Retry-After", b"60".to_vec())
2286            .with_header("X-RateLimit-Remaining", b"0".to_vec());
2287
2288        assert_eq!(error.status, StatusCode::TOO_MANY_REQUESTS);
2289        assert_eq!(error.detail, Some("Rate limit exceeded".to_owned()));
2290        assert_eq!(error.headers.len(), 2);
2291    }
2292
2293    // ========================================================================
2294    // Additional Error Types Constants Tests
2295    // ========================================================================
2296
2297    #[test]
2298    fn error_types_all_constants_defined() {
2299        // Verify all error type constants are non-empty
2300        assert!(!error_types::MISSING.is_empty());
2301        assert!(!error_types::STRING_TOO_SHORT.is_empty());
2302        assert!(!error_types::STRING_TOO_LONG.is_empty());
2303        assert!(!error_types::STRING_TYPE.is_empty());
2304        assert!(!error_types::INT_TYPE.is_empty());
2305        assert!(!error_types::FLOAT_TYPE.is_empty());
2306        assert!(!error_types::BOOL_TYPE.is_empty());
2307        assert!(!error_types::GREATER_THAN_EQUAL.is_empty());
2308        assert!(!error_types::LESS_THAN_EQUAL.is_empty());
2309        assert!(!error_types::STRING_PATTERN_MISMATCH.is_empty());
2310        assert!(!error_types::VALUE_ERROR.is_empty());
2311        assert!(!error_types::URL_TYPE.is_empty());
2312        assert!(!error_types::UUID_TYPE.is_empty());
2313        assert!(!error_types::JSON_INVALID.is_empty());
2314        assert!(!error_types::JSON_TYPE.is_empty());
2315        assert!(!error_types::TOO_SHORT.is_empty());
2316        assert!(!error_types::TOO_LONG.is_empty());
2317        assert!(!error_types::ENUM.is_empty());
2318        assert!(!error_types::EXTRA_FORBIDDEN.is_empty());
2319    }
2320
2321    #[test]
2322    fn error_types_numeric_range_constants() {
2323        // Verify numeric range error types
2324        assert_eq!(error_types::GREATER_THAN_EQUAL, "greater_than_equal");
2325        assert_eq!(error_types::LESS_THAN_EQUAL, "less_than_equal");
2326    }
2327
2328    #[test]
2329    fn error_types_collection_constants() {
2330        // Verify collection-related error types
2331        assert_eq!(error_types::TOO_SHORT, "too_short");
2332        assert_eq!(error_types::TOO_LONG, "too_long");
2333    }
2334
2335    // ========================================================================
2336    // Edge Cases and Error Conditions
2337    // ========================================================================
2338
2339    #[test]
2340    fn validation_error_empty_location() {
2341        let error = ValidationError::new(error_types::VALUE_ERROR, vec![]);
2342        assert!(error.loc.is_empty());
2343
2344        let json = serde_json::to_value(&error).unwrap();
2345        assert_eq!(json["loc"], json!([]));
2346    }
2347
2348    #[test]
2349    fn validation_error_deeply_nested_location() {
2350        // Test very deeply nested location path
2351        let error = ValidationError::missing(vec![
2352            LocItem::field("body"),
2353            LocItem::field("data"),
2354            LocItem::field("users"),
2355            LocItem::index(0),
2356            LocItem::field("profile"),
2357            LocItem::field("settings"),
2358            LocItem::index(5),
2359            LocItem::field("value"),
2360        ]);
2361
2362        let json = serde_json::to_value(&error).unwrap();
2363        assert_eq!(
2364            json["loc"],
2365            json!([
2366                "body", "data", "users", 0, "profile", "settings", 5, "value"
2367            ])
2368        );
2369    }
2370
2371    #[test]
2372    fn validation_errors_empty_to_json() {
2373        let errors = ValidationErrors::new();
2374        let json = errors.to_json();
2375        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2376
2377        assert_eq!(parsed["detail"], json!([]));
2378    }
2379
2380    #[test]
2381    fn validation_errors_many_errors() {
2382        // Test with many errors to ensure performance
2383        let mut errors = ValidationErrors::new();
2384        for i in 0..100 {
2385            errors.push(ValidationError::missing(loc::query(&format!("param{}", i))));
2386        }
2387
2388        assert_eq!(errors.len(), 100);
2389
2390        let json = errors.to_json();
2391        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2392        assert_eq!(parsed["detail"].as_array().unwrap().len(), 100);
2393    }
2394
2395    #[test]
2396    fn validation_error_special_characters_in_field_name() {
2397        // Test field names with special characters
2398        let error = ValidationError::missing(vec![
2399            LocItem::field("body"),
2400            LocItem::field("user-name"),
2401            LocItem::field("email@domain"),
2402        ]);
2403
2404        let json = serde_json::to_value(&error).unwrap();
2405        assert_eq!(json["loc"], json!(["body", "user-name", "email@domain"]));
2406    }
2407
2408    #[test]
2409    fn validation_error_unicode_in_message() {
2410        let error = ValidationError::new(error_types::VALUE_ERROR, loc::body_field("name"))
2411            .with_msg("名前が無効です");
2412
2413        let json = serde_json::to_value(&error).unwrap();
2414        assert_eq!(json["msg"], "名前が無効です");
2415    }
2416
2417    #[test]
2418    fn validation_error_large_input_value() {
2419        // Test with large input value
2420        let large_string = "x".repeat(10000);
2421        let error = ValidationError::string_too_long(loc::body_field("bio"), 500)
2422            .with_input(json!(large_string));
2423
2424        let json = serde_json::to_value(&error).unwrap();
2425        assert_eq!(json["input"].as_str().unwrap().len(), 10000);
2426    }
2427
2428    #[test]
2429    fn http_error_empty_detail() {
2430        // Explicitly setting empty string as detail
2431        let error = HttpError::bad_request().with_detail("");
2432        assert_eq!(error.detail, Some(String::new()));
2433
2434        let response = error.into_response();
2435        let body = match response.body_ref() {
2436            ResponseBody::Bytes(b) => b.clone(),
2437            _ => panic!("Expected bytes body"),
2438        };
2439
2440        let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap();
2441        // Empty detail should be used as-is
2442        assert_eq!(parsed["detail"], "");
2443    }
2444
2445    #[test]
2446    fn http_error_binary_header_value() {
2447        // Test header with non-UTF8 bytes
2448        let error = HttpError::bad_request().with_header("X-Binary", vec![0x00, 0xFF, 0x80]);
2449
2450        assert_eq!(error.headers[0].1, vec![0x00, 0xFF, 0x80]);
2451    }
2452
2453    // ========================================================================
2454    // Debug Mode Tests
2455    // ========================================================================
2456
2457    #[test]
2458    #[serial]
2459    fn debug_mode_default_disabled() {
2460        // Ensure debug mode is disabled by default
2461        disable_debug_mode();
2462        assert!(!is_debug_mode_enabled());
2463    }
2464
2465    #[test]
2466    #[serial]
2467    fn debug_mode_can_be_enabled_and_disabled() {
2468        // Start disabled
2469        disable_debug_mode();
2470        assert!(!is_debug_mode_enabled());
2471
2472        // Enable
2473        enable_debug_mode();
2474        assert!(is_debug_mode_enabled());
2475
2476        // Disable again
2477        disable_debug_mode();
2478        assert!(!is_debug_mode_enabled());
2479    }
2480
2481    #[test]
2482    fn debug_config_default() {
2483        let config = DebugConfig::default();
2484        assert!(!config.enabled);
2485        assert!(config.debug_header.is_none());
2486        assert!(config.debug_token.is_none());
2487        assert!(!config.allow_unauthenticated);
2488    }
2489
2490    #[test]
2491    fn debug_config_builder() {
2492        let config = DebugConfig::new()
2493            .enable()
2494            .with_debug_header("X-Debug-Token", "secret123");
2495
2496        assert!(config.enabled);
2497        assert_eq!(config.debug_header, Some("X-Debug-Token".to_owned()));
2498        assert_eq!(config.debug_token, Some("secret123".to_owned()));
2499        assert!(!config.allow_unauthenticated);
2500    }
2501
2502    #[test]
2503    fn debug_config_allow_unauthenticated() {
2504        let config = DebugConfig::new().enable().allow_unauthenticated();
2505
2506        assert!(config.enabled);
2507        assert!(config.allow_unauthenticated);
2508    }
2509
2510    #[test]
2511    fn debug_config_is_authorized_when_disabled() {
2512        let config = DebugConfig::new();
2513        let headers: Vec<(String, Vec<u8>)> = vec![];
2514
2515        // Not authorized when debug mode is disabled
2516        assert!(!config.is_authorized(&headers));
2517    }
2518
2519    #[test]
2520    fn debug_config_is_authorized_unauthenticated() {
2521        let config = DebugConfig::new().enable().allow_unauthenticated();
2522        let headers: Vec<(String, Vec<u8>)> = vec![];
2523
2524        // Authorized when allow_unauthenticated is true
2525        assert!(config.is_authorized(&headers));
2526    }
2527
2528    #[test]
2529    fn debug_config_is_authorized_with_valid_token() {
2530        let config = DebugConfig::new()
2531            .enable()
2532            .with_debug_header("X-Debug-Token", "my-secret");
2533
2534        let headers = vec![("X-Debug-Token".to_owned(), b"my-secret".to_vec())];
2535
2536        assert!(config.is_authorized(&headers));
2537    }
2538
2539    #[test]
2540    fn debug_config_is_authorized_with_invalid_token() {
2541        let config = DebugConfig::new()
2542            .enable()
2543            .with_debug_header("X-Debug-Token", "my-secret");
2544
2545        let headers = vec![("X-Debug-Token".to_owned(), b"wrong-secret".to_vec())];
2546
2547        assert!(!config.is_authorized(&headers));
2548    }
2549
2550    #[test]
2551    fn debug_config_is_authorized_missing_header() {
2552        let config = DebugConfig::new()
2553            .enable()
2554            .with_debug_header("X-Debug-Token", "my-secret");
2555
2556        let headers: Vec<(String, Vec<u8>)> = vec![];
2557
2558        assert!(!config.is_authorized(&headers));
2559    }
2560
2561    #[test]
2562    fn debug_config_header_case_insensitive() {
2563        let config = DebugConfig::new()
2564            .enable()
2565            .with_debug_header("X-Debug-Token", "my-secret");
2566
2567        let headers = vec![("x-debug-token".to_owned(), b"my-secret".to_vec())];
2568
2569        assert!(config.is_authorized(&headers));
2570    }
2571
2572    #[test]
2573    fn debug_info_new() {
2574        let info = DebugInfo::new();
2575        assert!(info.is_empty());
2576        assert!(info.source_file.is_none());
2577        assert!(info.source_line.is_none());
2578        assert!(info.function_name.is_none());
2579        assert!(info.route_pattern.is_none());
2580        assert!(info.handler_name.is_none());
2581        assert!(info.extra.is_empty());
2582    }
2583
2584    #[test]
2585    fn debug_info_with_source_location() {
2586        let info = DebugInfo::new().with_source_location("src/handlers/user.rs", 42, "get_user");
2587
2588        assert!(!info.is_empty());
2589        assert_eq!(info.source_file, Some("src/handlers/user.rs".to_owned()));
2590        assert_eq!(info.source_line, Some(42));
2591        assert_eq!(info.function_name, Some("get_user".to_owned()));
2592    }
2593
2594    #[test]
2595    fn debug_info_with_route_pattern() {
2596        let info = DebugInfo::new().with_route_pattern("/users/{id}");
2597
2598        assert!(!info.is_empty());
2599        assert_eq!(info.route_pattern, Some("/users/{id}".to_owned()));
2600    }
2601
2602    #[test]
2603    fn debug_info_with_handler_name() {
2604        let info = DebugInfo::new().with_handler_name("UserController::get");
2605
2606        assert!(!info.is_empty());
2607        assert_eq!(info.handler_name, Some("UserController::get".to_owned()));
2608    }
2609
2610    #[test]
2611    fn debug_info_with_extra() {
2612        let info = DebugInfo::new()
2613            .with_extra("user_id", "abc123")
2614            .with_extra("request_id", "req-456");
2615
2616        assert!(!info.is_empty());
2617        assert_eq!(info.extra.get("user_id"), Some(&"abc123".to_owned()));
2618        assert_eq!(info.extra.get("request_id"), Some(&"req-456".to_owned()));
2619    }
2620
2621    #[test]
2622    fn debug_info_full_builder() {
2623        let info = DebugInfo::new()
2624            .with_source_location("src/api/users.rs", 100, "create_user")
2625            .with_route_pattern("/api/users")
2626            .with_handler_name("UsersHandler::create")
2627            .with_extra("method", "POST");
2628
2629        assert!(!info.is_empty());
2630        assert_eq!(info.source_file, Some("src/api/users.rs".to_owned()));
2631        assert_eq!(info.source_line, Some(100));
2632        assert_eq!(info.function_name, Some("create_user".to_owned()));
2633        assert_eq!(info.route_pattern, Some("/api/users".to_owned()));
2634        assert_eq!(info.handler_name, Some("UsersHandler::create".to_owned()));
2635        assert_eq!(info.extra.get("method"), Some(&"POST".to_owned()));
2636    }
2637
2638    #[test]
2639    fn debug_info_serialization() {
2640        let info = DebugInfo::new()
2641            .with_source_location("src/test.rs", 42, "test_fn")
2642            .with_route_pattern("/test");
2643
2644        let json = serde_json::to_value(&info).unwrap();
2645
2646        assert_eq!(json["source_file"], "src/test.rs");
2647        assert_eq!(json["source_line"], 42);
2648        assert_eq!(json["function_name"], "test_fn");
2649        assert_eq!(json["route_pattern"], "/test");
2650        // handler_name and extra should be omitted when empty/None
2651        assert!(json.get("handler_name").is_none());
2652        assert!(json.get("extra").is_none());
2653    }
2654
2655    #[test]
2656    fn debug_info_serialization_skip_none() {
2657        let info = DebugInfo::new().with_route_pattern("/test");
2658
2659        let json = serde_json::to_value(&info).unwrap();
2660
2661        // Only route_pattern should be present
2662        assert_eq!(json["route_pattern"], "/test");
2663        assert!(json.get("source_file").is_none());
2664        assert!(json.get("source_line").is_none());
2665        assert!(json.get("function_name").is_none());
2666    }
2667
2668    #[test]
2669    fn http_error_with_debug_info() {
2670        let debug = DebugInfo::new()
2671            .with_source_location("src/handlers.rs", 50, "handle_request")
2672            .with_route_pattern("/api/test");
2673
2674        let error = HttpError::not_found()
2675            .with_detail("Resource not found")
2676            .with_debug_info(debug);
2677
2678        assert!(error.debug_info.is_some());
2679        let info = error.debug_info.unwrap();
2680        assert_eq!(info.source_file, Some("src/handlers.rs".to_owned()));
2681        assert_eq!(info.source_line, Some(50));
2682    }
2683
2684    #[test]
2685    #[serial]
2686    fn http_error_response_without_debug_mode() {
2687        disable_debug_mode();
2688
2689        let error = HttpError::not_found()
2690            .with_detail("User not found")
2691            .with_debug_info(
2692                DebugInfo::new()
2693                    .with_source_location("src/test.rs", 42, "test")
2694                    .with_route_pattern("/users/{id}"),
2695            );
2696
2697        let response = error.into_response();
2698        let body = match response.body_ref() {
2699            ResponseBody::Bytes(b) => b.clone(),
2700            _ => panic!("Expected bytes body"),
2701        };
2702
2703        let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap();
2704
2705        // Should only have detail, no debug info
2706        assert_eq!(parsed["detail"], "User not found");
2707        assert!(parsed.get("debug").is_none());
2708    }
2709
2710    #[test]
2711    #[serial]
2712    fn http_error_response_with_debug_mode() {
2713        // Enable debug mode for this test
2714        enable_debug_mode();
2715
2716        let error = HttpError::not_found()
2717            .with_detail("User not found")
2718            .with_debug_info(
2719                DebugInfo::new()
2720                    .with_source_location("src/test.rs", 42, "test")
2721                    .with_route_pattern("/users/{id}"),
2722            );
2723
2724        let response = error.into_response();
2725        let body = match response.body_ref() {
2726            ResponseBody::Bytes(b) => b.clone(),
2727            _ => panic!("Expected bytes body"),
2728        };
2729
2730        let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap();
2731
2732        // Should have both detail and debug info
2733        assert_eq!(parsed["detail"], "User not found");
2734        assert!(parsed.get("debug").is_some());
2735        assert_eq!(parsed["debug"]["source_file"], "src/test.rs");
2736        assert_eq!(parsed["debug"]["source_line"], 42);
2737        assert_eq!(parsed["debug"]["function_name"], "test");
2738        assert_eq!(parsed["debug"]["route_pattern"], "/users/{id}");
2739
2740        // Clean up
2741        disable_debug_mode();
2742    }
2743
2744    #[test]
2745    #[serial]
2746    fn http_error_response_with_debug_mode_no_debug_info() {
2747        // Enable debug mode but don't add debug info
2748        enable_debug_mode();
2749
2750        let error = HttpError::not_found().with_detail("User not found");
2751
2752        let response = error.into_response();
2753        let body = match response.body_ref() {
2754            ResponseBody::Bytes(b) => b.clone(),
2755            _ => panic!("Expected bytes body"),
2756        };
2757
2758        let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap();
2759
2760        // Should only have detail, no debug field
2761        assert_eq!(parsed["detail"], "User not found");
2762        assert!(parsed.get("debug").is_none());
2763
2764        // Clean up
2765        disable_debug_mode();
2766    }
2767
2768    #[test]
2769    fn validation_errors_with_debug_info() {
2770        let errors = ValidationErrors::single(ValidationError::missing(loc::query("q")))
2771            .with_debug_info(
2772                DebugInfo::new()
2773                    .with_source_location("src/extractors.rs", 100, "extract_query")
2774                    .with_handler_name("SearchHandler::search"),
2775            );
2776
2777        assert!(errors.debug_info.is_some());
2778    }
2779
2780    #[test]
2781    #[serial]
2782    fn validation_errors_response_without_debug_mode() {
2783        disable_debug_mode();
2784
2785        let errors = ValidationErrors::single(ValidationError::missing(loc::query("q")))
2786            .with_debug_info(DebugInfo::new().with_source_location("src/test.rs", 42, "test"));
2787
2788        let response = errors.into_response();
2789        let body = match response.body_ref() {
2790            ResponseBody::Bytes(b) => b.clone(),
2791            _ => panic!("Expected bytes body"),
2792        };
2793
2794        let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap();
2795
2796        // Should only have detail array, no debug info
2797        assert!(parsed["detail"].is_array());
2798        assert!(parsed.get("debug").is_none());
2799    }
2800
2801    #[test]
2802    #[serial]
2803    fn validation_errors_response_with_debug_mode() {
2804        enable_debug_mode();
2805
2806        let errors = ValidationErrors::single(ValidationError::missing(loc::query("q")))
2807            .with_debug_info(
2808                DebugInfo::new()
2809                    .with_source_location("src/test.rs", 42, "test")
2810                    .with_route_pattern("/search"),
2811            );
2812
2813        let response = errors.into_response();
2814        let body = match response.body_ref() {
2815            ResponseBody::Bytes(b) => b.clone(),
2816            _ => panic!("Expected bytes body"),
2817        };
2818
2819        let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap();
2820
2821        // Should have both detail and debug info
2822        assert!(parsed["detail"].is_array());
2823        assert!(parsed.get("debug").is_some());
2824        assert_eq!(parsed["debug"]["source_file"], "src/test.rs");
2825        assert_eq!(parsed["debug"]["route_pattern"], "/search");
2826
2827        // Clean up
2828        disable_debug_mode();
2829    }
2830
2831    #[test]
2832    fn validation_errors_merge_preserves_debug_info() {
2833        let mut errors1 = ValidationErrors::single(ValidationError::missing(loc::query("q")))
2834            .with_debug_info(DebugInfo::new().with_source_location("src/a.rs", 1, "a"));
2835
2836        let errors2 = ValidationErrors::single(ValidationError::missing(loc::query("page")))
2837            .with_debug_info(DebugInfo::new().with_source_location("src/b.rs", 2, "b"));
2838
2839        errors1.merge(errors2);
2840
2841        // Should keep the first debug_info
2842        assert!(errors1.debug_info.is_some());
2843        assert_eq!(
2844            errors1.debug_info.as_ref().unwrap().source_file,
2845            Some("src/a.rs".to_owned())
2846        );
2847    }
2848
2849    #[test]
2850    fn validation_errors_merge_takes_other_debug_info_if_none() {
2851        let mut errors1 = ValidationErrors::single(ValidationError::missing(loc::query("q")));
2852
2853        let errors2 = ValidationErrors::single(ValidationError::missing(loc::query("page")))
2854            .with_debug_info(DebugInfo::new().with_source_location("src/b.rs", 2, "b"));
2855
2856        errors1.merge(errors2);
2857
2858        // Should take debug_info from errors2
2859        assert!(errors1.debug_info.is_some());
2860        assert_eq!(
2861            errors1.debug_info.as_ref().unwrap().source_file,
2862            Some("src/b.rs".to_owned())
2863        );
2864    }
2865
2866    // ========================================================================
2867    // ResponseValidationError Tests
2868    // ========================================================================
2869
2870    #[test]
2871    fn response_validation_error_new_is_empty() {
2872        let error = ResponseValidationError::new();
2873        assert!(error.is_empty());
2874        assert_eq!(error.len(), 0);
2875        assert!(error.response_content.is_none());
2876        assert!(error.summary.is_none());
2877    }
2878
2879    #[test]
2880    fn response_validation_error_serialization_failed() {
2881        let error = ResponseValidationError::serialization_failed("failed to serialize DateTime");
2882        assert_eq!(error.len(), 1);
2883        assert!(error.summary.is_some());
2884        assert_eq!(
2885            error.summary.as_deref(),
2886            Some("failed to serialize DateTime")
2887        );
2888        assert_eq!(error.errors[0].error_type, error_types::SERIALIZATION_ERROR);
2889    }
2890
2891    #[test]
2892    fn response_validation_error_model_validation_failed() {
2893        let error = ResponseValidationError::model_validation_failed("missing required field 'id'");
2894        assert_eq!(error.len(), 1);
2895        assert!(error.summary.is_some());
2896        assert_eq!(
2897            error.errors[0].error_type,
2898            error_types::MODEL_VALIDATION_ERROR
2899        );
2900    }
2901
2902    #[test]
2903    fn response_validation_error_with_error() {
2904        let error = ResponseValidationError::new()
2905            .with_error(ValidationError::missing(loc::response_field("user_id")));
2906        assert_eq!(error.len(), 1);
2907        assert_eq!(error.errors[0].loc.len(), 2);
2908    }
2909
2910    #[test]
2911    fn response_validation_error_with_errors() {
2912        let error = ResponseValidationError::new().with_errors(vec![
2913            ValidationError::missing(loc::response_field("id")),
2914            ValidationError::missing(loc::response_field("name")),
2915        ]);
2916        assert_eq!(error.len(), 2);
2917    }
2918
2919    #[test]
2920    fn response_validation_error_with_response_content() {
2921        let content = json!({"name": "Alice", "age": 30});
2922        let error = ResponseValidationError::serialization_failed("test")
2923            .with_response_content(content.clone());
2924        assert!(error.response_content.is_some());
2925        assert_eq!(error.response_content.as_ref().unwrap()["name"], "Alice");
2926    }
2927
2928    #[test]
2929    fn response_validation_error_with_summary() {
2930        let error = ResponseValidationError::new().with_summary("Custom summary");
2931        assert_eq!(error.summary.as_deref(), Some("Custom summary"));
2932    }
2933
2934    #[test]
2935    fn response_validation_error_with_debug_info() {
2936        let error = ResponseValidationError::serialization_failed("test")
2937            .with_debug_info(DebugInfo::new().with_source_location("handler.rs", 42, "get_user"));
2938        assert!(error.debug_info.is_some());
2939    }
2940
2941    #[test]
2942    fn response_validation_error_display() {
2943        let error = ResponseValidationError::new();
2944        assert_eq!(format!("{}", error), "Response validation failed");
2945
2946        let error = ResponseValidationError::new().with_summary("missing field");
2947        assert_eq!(
2948            format!("{}", error),
2949            "Response validation failed: missing field"
2950        );
2951    }
2952
2953    #[test]
2954    #[serial]
2955    fn response_validation_error_into_response_production_mode() {
2956        // Ensure debug mode is off
2957        disable_debug_mode();
2958
2959        let error = ResponseValidationError::serialization_failed("some internal error")
2960            .with_response_content(json!({"secret": "data"}));
2961
2962        let response = error.into_response();
2963        assert_eq!(response.status().as_u16(), 500);
2964
2965        // Check content-type header
2966        let content_type = response
2967            .headers()
2968            .iter()
2969            .find(|(name, _)| name == "content-type")
2970            .map(|(_, value)| String::from_utf8_lossy(value).to_string());
2971        assert_eq!(content_type, Some("application/json".to_string()));
2972
2973        // Check body - should NOT include internal details
2974        if let crate::response::ResponseBody::Bytes(bytes) = response.body_ref() {
2975            let body: serde_json::Value = serde_json::from_slice(bytes).unwrap();
2976            assert_eq!(body["error"], "internal_server_error");
2977            assert_eq!(body["detail"], "Internal Server Error");
2978            // Should NOT include debug info or response content
2979            assert!(body.get("debug").is_none());
2980        } else {
2981            panic!("Expected Bytes body");
2982        }
2983    }
2984
2985    #[test]
2986    #[serial]
2987    fn response_validation_error_into_response_debug_mode() {
2988        // Enable debug mode
2989        enable_debug_mode();
2990
2991        let error = ResponseValidationError::serialization_failed("DateTime serialize failed")
2992            .with_response_content(json!({"created_at": "invalid-date"}))
2993            .with_debug_info(DebugInfo::new().with_source_location("handler.rs", 100, "get_user"));
2994
2995        let response = error.into_response();
2996        assert_eq!(response.status().as_u16(), 500);
2997
2998        // Check body - should include debug info
2999        if let crate::response::ResponseBody::Bytes(bytes) = response.body_ref() {
3000            let body: serde_json::Value = serde_json::from_slice(bytes).unwrap();
3001            assert_eq!(body["error"], "internal_server_error");
3002            // Should include debug info
3003            assert!(body.get("debug").is_some());
3004            let debug = &body["debug"];
3005            assert_eq!(debug["summary"], "DateTime serialize failed");
3006            assert!(debug.get("errors").is_some());
3007            assert!(debug.get("response_content").is_some());
3008        } else {
3009            panic!("Expected Bytes body");
3010        }
3011
3012        // Restore default state
3013        disable_debug_mode();
3014    }
3015
3016    #[test]
3017    fn response_validation_error_to_log_string() {
3018        let error = ResponseValidationError::serialization_failed("test error")
3019            .with_error(ValidationError::missing(loc::response_field("id")))
3020            .with_response_content(json!({"name": "Alice"}));
3021
3022        let log = error.to_log_string();
3023        assert!(log.contains("Summary: test error"));
3024        assert!(log.contains("serialization_error"));
3025        assert!(log.contains("Response content:"));
3026        assert!(log.contains("Alice"));
3027    }
3028
3029    #[test]
3030    fn response_validation_error_to_log_string_truncates_large_content() {
3031        // Create a large response content
3032        let large_string = "x".repeat(1000);
3033        let error = ResponseValidationError::serialization_failed("test")
3034            .with_response_content(json!({"data": large_string}));
3035
3036        let log = error.to_log_string();
3037        assert!(log.contains("(truncated)"));
3038    }
3039
3040    #[test]
3041    fn response_validation_error_iter() {
3042        let error = ResponseValidationError::new()
3043            .with_error(ValidationError::missing(loc::response_field("a")))
3044            .with_error(ValidationError::missing(loc::response_field("b")));
3045
3046        let locs: Vec<_> = error.iter().map(|e| e.loc.clone()).collect();
3047        assert_eq!(locs.len(), 2);
3048    }
3049
3050    #[test]
3051    fn loc_response_helper() {
3052        let loc = loc::response();
3053        assert_eq!(loc.len(), 1);
3054        assert!(matches!(&loc[0], LocItem::Field(s) if s == "response"));
3055    }
3056
3057    #[test]
3058    fn loc_response_field_helper() {
3059        let loc = loc::response_field("user_id");
3060        assert_eq!(loc.len(), 2);
3061        assert!(matches!(&loc[0], LocItem::Field(s) if s == "response"));
3062        assert!(matches!(&loc[1], LocItem::Field(s) if s == "user_id"));
3063    }
3064
3065    #[test]
3066    fn loc_response_path_helper() {
3067        let loc = loc::response_path(&["user", "profile", "name"]);
3068        assert_eq!(loc.len(), 4);
3069        assert!(matches!(&loc[0], LocItem::Field(s) if s == "response"));
3070        assert!(matches!(&loc[1], LocItem::Field(s) if s == "user"));
3071        assert!(matches!(&loc[2], LocItem::Field(s) if s == "profile"));
3072        assert!(matches!(&loc[3], LocItem::Field(s) if s == "name"));
3073    }
3074}