kindly_guard_server/error/
mod.rs

1// Copyright 2025 Kindly Software Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! Error handling and recovery mechanisms for production resilience
15//!
16//! This module provides comprehensive error handling with security-aware design.
17//! All error types are designed to fail securely and prevent information leakage.
18//!
19//! # Security Principles
20//!
21//! 1. **Fail Closed**: Security errors always fail closed (deny by default)
22//! 2. **Information Hiding**: Never expose internal details in external errors
23//! 3. **Audit Trail**: All security errors generate audit events
24//! 4. **Recovery Strategies**: Each error type has defined recovery behavior
25//! 5. **Rate Limiting**: Authentication errors trigger progressive penalties
26
27use anyhow::Result;
28use std::io;
29use thiserror::Error;
30
31/// Type alias for Result with `KindlyError`
32pub type KindlyResult<T> = Result<T, KindlyError>;
33
34/// Extension trait for Result types
35pub trait ResultExt<T> {
36    /// Convert to `KindlyResult`
37    fn kindly(self) -> KindlyResult<T>;
38}
39
40impl<T, E> ResultExt<T> for Result<T, E>
41where
42    E: Into<anyhow::Error>,
43{
44    fn kindly(self) -> KindlyResult<T> {
45        self.map_err(|e| KindlyError::ConfigError(e.into().to_string()))
46    }
47}
48
49/// `KindlyGuard` error types with actionable recovery strategies
50#[derive(Error, Debug)]
51pub enum KindlyError {
52    // Display and UI errors
53    #[error("Display rendering failed: {0}")]
54    DisplayError(String),
55
56    #[error("Terminal not available")]
57    TerminalError,
58
59    // Validation errors
60    /// Command validation failed. This occurs when user input doesn't meet
61    /// expected format or contains forbidden patterns.
62    ///
63    /// **Security Impact**: Medium - Could indicate probing for vulnerabilities
64    /// **Recovery**: Fail fast, log attempt, increment failure counter
65    /// **Safe Handling**: Never echo back the invalid input verbatim
66    #[error("Command validation failed: {0}")]
67    ValidationError(String),
68
69    /// Invalid input detected during parameter validation.
70    ///
71    /// **Security Impact**: High - Often precedes injection attacks
72    /// **Recovery**: Reject immediately, audit log with sanitized details
73    /// **Safe Handling**: Return generic "Invalid input" without specifics
74    /// **Example**: SQL injection attempts, path traversal patterns
75    #[error("Invalid input: {reason}")]
76    InvalidInput { reason: String },
77
78    #[error("Invalid configuration: {field}: {reason}")]
79    InvalidConfig { field: String, reason: String },
80
81    // IO errors
82    #[error("File operation failed: {0}")]
83    FileError(#[from] io::Error),
84
85    #[error("Path not found: {path}")]
86    PathNotFound { path: String },
87
88    // Serialization errors
89    #[error("JSON serialization failed: {0}")]
90    SerializationError(#[from] serde_json::Error),
91
92    #[error("Format error: expected {expected}, got {actual}")]
93    FormatError { expected: String, actual: String },
94
95    // Scanner errors
96    #[error("Scanner initialization failed: {0}")]
97    ScannerError(String),
98
99    /// **CRITICAL SECURITY ERROR**: Active threat detected in input/output.
100    ///
101    /// **When It Occurs**:
102    /// - Unicode attacks (invisible characters, RTL override)
103    /// - Injection attempts (SQL, command, path traversal)
104    /// - Known malicious patterns
105    ///
106    /// **Security Impact**: CRITICAL - Active attack in progress
107    /// **Recovery**: ALWAYS FAIL CLOSED
108    /// - Block the request immediately
109    /// - Generate high-priority audit event
110    /// - Increment threat counter for client
111    /// - Consider temporary IP ban after repeated attempts
112    ///
113    /// **Safe Handling**:
114    /// - NEVER include threat details in user-facing messages
115    /// - Log full details to secure audit log only
116    /// - Return generic "Security policy violation" to client
117    /// - Preserve evidence for forensic analysis
118    ///
119    /// **Example Response**:
120    /// ```json
121    /// {
122    ///   "error": {
123    ///     "code": -32004,
124    ///     "message": "Request blocked by security policy"
125    ///   }
126    /// }
127    /// ```
128    #[error("Threat detected: {threat_type} at {location}")]
129    ThreatDetected {
130        threat_type: String,
131        location: String,
132    },
133
134    // Resource errors
135    /// Resource exhaustion detected - possible DoS attempt.
136    ///
137    /// **Security Impact**: High - Resource exhaustion attacks
138    /// **Recovery**: Rate limit, circuit break, graceful degradation
139    /// **Safe Handling**: Generic message, preserve service availability
140    ///
141    /// **Common Scenarios**:
142    /// - Memory limit exceeded (large file uploads)
143    /// - Connection pool exhausted (connection flood)
144    /// - CPU quota exceeded (computational DoS)
145    ///
146    /// **Response Strategy**:
147    /// - Apply exponential backoff to client
148    /// - Shed load if necessary
149    /// - Return 503 Service Unavailable with Retry-After header
150    #[error("Resource limit exceeded: {resource}: {limit}")]
151    ResourceError { resource: String, limit: String },
152
153    /// Operation timeout - prevents indefinite resource holding.
154    ///
155    /// **Security Impact**: Medium - Possible slowloris attack
156    /// **Recovery**: Clean up resources, fail fast
157    /// **Safe Handling**: No internal timing information in response
158    #[error("Operation timed out after {0} seconds")]
159    TimeoutError(u64),
160
161    // Network errors
162    #[error("Network error: {0}")]
163    NetworkError(String),
164
165    #[error("Connection failed to {endpoint}: {reason}")]
166    ConnectionError { endpoint: String, reason: String },
167
168    // Auth errors
169    /// **CRITICAL**: Authentication failure - possible credential attack.
170    ///
171    /// **Security Impact**: CRITICAL - Unauthorized access attempt
172    /// **Recovery**: ALWAYS FAIL CLOSED
173    ///
174    /// **Required Actions**:
175    /// 1. Log to security audit with timestamp, IP, attempt details
176    /// 2. Increment auth failure counter for IP/client
177    /// 3. Apply progressive delay (2^n seconds after n failures)
178    /// 4. Trigger account lockout after threshold (e.g., 5 attempts)
179    /// 5. Alert on patterns (distributed attempts, timing attacks)
180    ///
181    /// **Safe Handling**:
182    /// - NEVER reveal why authentication failed
183    /// - Use constant-time comparison for credentials
184    /// - Return identical error for "user not found" vs "wrong password"
185    /// - Generic message: "Authentication failed"
186    ///
187    /// **Logging Requirements**:
188    /// ```rust
189    /// audit_log.critical(AuditEvent::AuthFailure {
190    ///     client_ip: ip,
191    ///     user_id: sanitize(user_id), // Hash if sensitive
192    ///     timestamp: SystemTime::now(),
193    ///     failure_count: count,
194    /// });
195    /// ```
196    #[error("Authentication failed: {reason}")]
197    AuthError { reason: String },
198
199    /// **CRITICAL**: Authorization failure - privilege escalation attempt.
200    ///
201    /// **Security Impact**: CRITICAL - Possible privilege escalation
202    /// **Recovery**: DENY and audit
203    ///
204    /// **Required Actions**:
205    /// 1. Deny the action immediately
206    /// 2. Log full context to security audit
207    /// 3. Check for authorization probe patterns
208    /// 4. Consider session termination for repeated attempts
209    ///
210    /// **Safe Handling**:
211    /// - Return minimal information: "Unauthorized"
212    /// - Don't reveal what permissions are needed
213    /// - Don't indicate if resource exists
214    #[error("Unauthorized: {action}")]
215    Unauthorized { action: String },
216
217    // MCP Protocol errors
218    #[error("Protocol error: {code}: {message}")]
219    ProtocolError { code: i32, message: String },
220
221    #[error("Method not found: {method}")]
222    MethodNotFound { method: String },
223
224    // Configuration errors
225    #[error("Configuration error: {0}")]
226    ConfigError(String),
227
228    // Internal errors
229    #[error("Internal error: {0}")]
230    Internal(String),
231}
232
233impl KindlyError {
234    /// Get the severity level of the error
235    pub const fn severity(&self) -> ErrorSeverity {
236        match self {
237            // Critical errors that require immediate attention
238            Self::ThreatDetected { .. } => ErrorSeverity::Critical,
239            Self::AuthError { .. } => ErrorSeverity::Critical,
240            Self::Unauthorized { .. } => ErrorSeverity::Critical,
241
242            // High severity errors that impact functionality
243            Self::ScannerError(_) => ErrorSeverity::High,
244            Self::ResourceError { .. } => ErrorSeverity::High,
245            Self::TimeoutError(_) => ErrorSeverity::High,
246            Self::Internal(_) => ErrorSeverity::High,
247
248            // Medium severity errors
249            Self::NetworkError(_) => ErrorSeverity::Medium,
250            Self::ConnectionError { .. } => ErrorSeverity::Medium,
251            Self::ProtocolError { .. } => ErrorSeverity::Medium,
252            Self::ConfigError(_) => ErrorSeverity::Medium,
253
254            // Low severity errors
255            Self::DisplayError(_) => ErrorSeverity::Low,
256            Self::TerminalError => ErrorSeverity::Low,
257            Self::ValidationError(_) => ErrorSeverity::Low,
258            Self::InvalidInput { .. } => ErrorSeverity::Low,
259            Self::InvalidConfig { .. } => ErrorSeverity::Low,
260            Self::FileError(_) => ErrorSeverity::Low,
261            Self::PathNotFound { .. } => ErrorSeverity::Low,
262            Self::SerializationError(_) => ErrorSeverity::Low,
263            Self::FormatError { .. } => ErrorSeverity::Low,
264            Self::MethodNotFound { .. } => ErrorSeverity::Low,
265        }
266    }
267
268    /// Check if the error is retryable
269    pub const fn is_retryable(&self) -> bool {
270        matches!(
271            self,
272            Self::NetworkError(_)
273                | Self::ConnectionError { .. }
274                | Self::TimeoutError(_)
275                | Self::ResourceError { .. }
276        )
277    }
278
279    /// Get a user-friendly error message
280    pub fn user_message(&self) -> String {
281        match self {
282            Self::ThreatDetected { .. } => {
283                // NEVER expose threat details to avoid information leakage
284                "Security threat detected: policy violation".to_string()
285            },
286            Self::AuthError { .. } => {
287                // Generic message - don't reveal why auth failed
288                "Authentication failed. Please check your credentials.".to_string()
289            },
290            Self::Unauthorized { .. } => {
291                // Don't reveal what action was attempted
292                "Unauthorized access".to_string()
293            },
294            Self::TimeoutError(_) => {
295                // Don't reveal exact timeout to prevent timing attacks
296                "Operation timed out".to_string()
297            },
298            Self::ResourceError { .. } => {
299                // Don't reveal specific resource or limits
300                "Resource limit exceeded".to_string()
301            },
302            _ => self.to_string(),
303        }
304    }
305
306    /// Convert to MCP protocol error code
307    pub const fn to_protocol_code(&self) -> i32 {
308        match self {
309            Self::ProtocolError { code, .. } => *code,
310            Self::MethodNotFound { .. } => -32601,
311            Self::InvalidInput { .. } => -32602,
312            Self::AuthError { .. } | Self::Unauthorized { .. } => -32001,
313            Self::TimeoutError(_) => -32002,
314            Self::ResourceError { .. } => -32003,
315            Self::ThreatDetected { .. } => -32004,
316            _ => -32603, // Internal error
317        }
318    }
319}
320
321/// Error severity levels
322#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
323pub enum ErrorSeverity {
324    Low,
325    Medium,
326    High,
327    Critical,
328}
329
330/// Error recovery strategies
331#[derive(Debug, Clone)]
332pub enum RecoveryStrategy {
333    /// Retry the operation with exponential backoff
334    RetryWithBackoff {
335        max_attempts: u32,
336        base_delay_ms: u64,
337    },
338
339    /// Fall back to a simpler operation
340    Fallback,
341
342    /// Log and continue
343    LogAndContinue,
344
345    /// Fail fast with user-friendly message
346    FailFast,
347}
348
349/// Error context with recovery hints
350pub struct ErrorContext {
351    pub error: KindlyError,
352    pub strategy: RecoveryStrategy,
353    pub user_hint: String,
354}
355
356impl ErrorContext {
357    /// Create a new error context
358    pub fn new(error: KindlyError, strategy: RecoveryStrategy, hint: &str) -> Self {
359        Self {
360            error,
361            strategy,
362            user_hint: hint.to_string(),
363        }
364    }
365
366    /// Convert to user-friendly message
367    pub fn user_message(&self) -> String {
368        format!("{}\n\nHint: {}", self.error, self.user_hint)
369    }
370}
371
372/// Recovery helper functions
373pub mod recovery {
374    use super::{KindlyError, Result};
375    use std::time::Duration;
376    use tokio::time::sleep;
377
378    /// Retry an operation with exponential backoff
379    pub async fn retry_with_backoff<F, T, E>(
380        mut operation: F,
381        max_attempts: u32,
382        base_delay_ms: u64,
383    ) -> Result<T>
384    where
385        F: FnMut() -> Result<T, E>,
386        E: std::error::Error + Send + Sync + 'static,
387    {
388        let mut attempt = 0;
389        let mut delay = base_delay_ms;
390
391        loop {
392            attempt += 1;
393
394            match operation() {
395                Ok(result) => return Ok(result),
396                Err(e) if attempt >= max_attempts => {
397                    return Err(anyhow::anyhow!(
398                        "Operation failed after {} attempts: {}",
399                        max_attempts,
400                        e
401                    ));
402                },
403                Err(_) => {
404                    sleep(Duration::from_millis(delay)).await;
405                    delay = (delay * 2).min(30_000); // Cap at 30 seconds
406                },
407            }
408        }
409    }
410
411    /// Execute with timeout
412    pub async fn with_timeout<F, T>(operation: F, timeout_secs: u64) -> anyhow::Result<T>
413    where
414        F: std::future::Future<Output = anyhow::Result<T>>,
415    {
416        match tokio::time::timeout(Duration::from_secs(timeout_secs), operation).await {
417            Ok(result) => result,
418            Err(_) => Err(KindlyError::TimeoutError(timeout_secs).into()),
419        }
420    }
421}
422
423/// Error handlers for specific components
424pub mod handlers {
425    use super::{io, ErrorContext, KindlyError, RecoveryStrategy};
426
427    /// Handle display rendering errors with fallback
428    pub fn handle_display_error(error: anyhow::Error) -> String {
429        eprintln!("Display error: {error}");
430
431        // Fallback to minimal text output
432        format!(
433            "KindlyGuard | Status: Error | Message: Display unavailable\n\
434             Error: {error}\n\
435             Try running with --format minimal or --no-color"
436        )
437    }
438
439    /// Handle file operation errors
440    pub fn handle_file_error(path: &str, error: io::Error) -> ErrorContext {
441        let hint = match error.kind() {
442            io::ErrorKind::NotFound => {
443                format!("File '{path}' not found. Check the path and try again.")
444            },
445            io::ErrorKind::PermissionDenied => {
446                format!("Permission denied for '{path}'. Check file permissions.")
447            },
448            io::ErrorKind::InvalidData => {
449                "File contains invalid data. It may be corrupted.".to_string()
450            },
451            _ => {
452                format!("Failed to access '{path}': {error}")
453            },
454        };
455
456        ErrorContext::new(
457            KindlyError::FileError(error),
458            RecoveryStrategy::FailFast,
459            &hint,
460        )
461    }
462
463    /// Handle validation errors with helpful messages
464    pub fn handle_validation_error(field: &str, value: &str, reason: &str) -> ErrorContext {
465        let hint = match field {
466            "path" => "Use absolute paths without '..' and ensure the file exists.".to_string(),
467            "port" => "Use a port number between 1024 and 65535.".to_string(),
468            "feature" => "Valid features: unicode, injection, path, advanced.".to_string(),
469            _ => {
470                format!("Check the {field} value and try again.")
471            },
472        };
473
474        ErrorContext::new(
475            KindlyError::ValidationError(format!("Invalid {field}: '{value}' - {reason}")),
476            RecoveryStrategy::FailFast,
477            &hint,
478        )
479    }
480}
481
482/// Graceful degradation for display operations
483pub mod degradation {
484
485    use crate::shield::Shield;
486    use std::sync::Arc;
487
488    /// Try multiple display formats until one works
489    pub fn degrade_display_format(
490        shield: Arc<Shield>,
491        mut formats: Vec<crate::shield::universal_display::DisplayFormat>,
492    ) -> String {
493        use crate::shield::{UniversalDisplay, UniversalDisplayConfig};
494
495        // Try each format in order
496        while let Some(format) = formats.pop() {
497            let config = UniversalDisplayConfig {
498                color: false, // Disable color for safety
499                detailed: false,
500                format,
501                status_file: None, // Skip file writing
502            };
503
504            let display = UniversalDisplay::new(shield.clone(), config);
505
506            match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| display.render())) {
507                Ok(output) if !output.is_empty() => return output,
508                _ => continue,
509            }
510        }
511
512        // Ultimate fallback
513        "KindlyGuard | Status: Unknown | Error: Display system failure".to_string()
514    }
515}
516
517/// Security-aware error handling patterns
518pub mod security_patterns {
519    use super::*;
520    use tracing::{error, warn};
521
522    /// Example: Handle authentication errors securely
523    ///
524    /// ```rust
525    /// use kindly_guard_server::error::{KindlyError, security_patterns::handle_auth_error};
526    ///
527    /// async fn authenticate(credentials: &Credentials) -> Result<User> {
528    ///     match verify_credentials(credentials).await {
529    ///         Ok(user) => Ok(user),
530    ///         Err(e) => handle_auth_error(e, credentials.username())
531    ///     }
532    /// }
533    /// ```
534    pub fn handle_auth_error(error: anyhow::Error, username: &str) -> Result<(), KindlyError> {
535        // Log detailed error internally (never expose to client)
536        error!(
537            target: "security",
538            username = %username,
539            error = %error,
540            "Authentication failed"
541        );
542
543        // Audit event
544        // audit_log.record(AuditEvent::AuthFailure { ... });
545
546        // Return generic error to client
547        Err(KindlyError::AuthError {
548            reason: "Authentication failed".to_string(), // Generic message
549        })
550    }
551
552    /// Example: Handle threat detection without information leakage
553    ///
554    /// ```rust
555    /// use kindly_guard_server::error::{KindlyError, security_patterns::handle_threat};
556    ///
557    /// fn scan_input(input: &str) -> Result<()> {
558    ///     let threats = scanner.scan_text(input)?;
559    ///     if !threats.is_empty() {
560    ///         return handle_threat(&threats[0], input);
561    ///     }
562    ///     Ok(())
563    /// }
564    /// ```
565    pub fn handle_threat(threat: &crate::scanner::Threat, input: &str) -> Result<(), KindlyError> {
566        // Log full details for security team
567        error!(
568            target: "security.threats",
569            threat_type = ?threat.threat_type,
570            severity = ?threat.severity,
571            input_hash = %sha256_hash(input), // Hash sensitive data
572            "Threat detected"
573        );
574
575        // Generic error for client
576        Err(KindlyError::ThreatDetected {
577            threat_type: "policy violation".to_string(), // Don't reveal attack type
578            location: "request".to_string(),             // Don't reveal specific location
579        })
580    }
581
582    /// Example: Handle resource exhaustion with rate limiting
583    ///
584    /// ```rust
585    /// use kindly_guard_server::error::{KindlyError, security_patterns::handle_resource_limit};
586    ///
587    /// async fn process_request(req: Request) -> Result<Response> {
588    ///     if !rate_limiter.check_limit(&req.client_id).await? {
589    ///         return handle_resource_limit("rate_limit", &req.client_id);
590    ///     }
591    ///     // Process request...
592    /// }
593    /// ```
594    pub fn handle_resource_limit(resource: &str, client_id: &str) -> Result<(), KindlyError> {
595        warn!(
596            target: "security.resources",
597            resource = %resource,
598            client_id = %client_id,
599            "Resource limit exceeded"
600        );
601
602        // Apply progressive penalties
603        // rate_limiter.apply_penalty(client_id);
604
605        Err(KindlyError::ResourceError {
606            resource: "request".to_string(),     // Generic resource name
607            limit: "quota exceeded".to_string(), // Don't reveal specific limits
608        })
609    }
610
611    /// Example: Timeout handling that prevents timing attacks
612    ///
613    /// ```rust
614    /// use kindly_guard_server::error::{KindlyError, security_patterns::handle_timeout};
615    /// use std::time::Duration;
616    ///
617    /// async fn timed_operation() -> Result<String> {
618    ///     match timeout(Duration::from_secs(30), operation()).await {
619    ///         Ok(result) => result,
620    ///         Err(_) => handle_timeout(30)
621    ///     }
622    /// }
623    /// ```
624    pub fn handle_timeout(timeout_secs: u64) -> Result<(), KindlyError> {
625        warn!(
626            target: "security.timeout",
627            timeout_secs = timeout_secs,
628            "Operation timed out"
629        );
630
631        // Add random jitter to prevent timing analysis
632        use rand::Rng;
633        let jitter = rand::thread_rng().gen_range(0..5);
634
635        Err(KindlyError::TimeoutError(timeout_secs + jitter))
636    }
637
638    /// Hash sensitive data for logging
639    fn sha256_hash(data: &str) -> String {
640        use sha2::{Digest, Sha256};
641        let mut hasher = Sha256::new();
642        hasher.update(data.as_bytes());
643        format!("{:x}", hasher.finalize())
644    }
645
646    /// Example: Constant-time string comparison for security
647    ///
648    /// ```rust
649    /// use kindly_guard_server::error::security_patterns::constant_time_compare;
650    ///
651    /// fn verify_token(provided: &str, expected: &str) -> bool {
652    ///     constant_time_compare(provided.as_bytes(), expected.as_bytes())
653    /// }
654    /// ```
655    pub fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
656        if a.len() != b.len() {
657            return false;
658        }
659
660        let mut result = 0u8;
661        for (x, y) in a.iter().zip(b.iter()) {
662            result |= x ^ y;
663        }
664
665        result == 0
666    }
667}
668
669#[cfg(test)]
670mod tests {
671    use super::*;
672
673    #[test]
674    fn test_error_context_formatting() {
675        let error = KindlyError::ValidationError("Invalid path".to_string());
676        let context =
677            ErrorContext::new(error, RecoveryStrategy::FailFast, "Use absolute paths only");
678
679        let msg = context.user_message();
680        assert!(msg.contains("Invalid path"));
681        assert!(msg.contains("Use absolute paths only"));
682    }
683
684    #[tokio::test]
685    async fn test_retry_with_backoff() {
686        let mut attempts = 0;
687        let result = recovery::retry_with_backoff(
688            || {
689                attempts += 1;
690                if attempts < 3 {
691                    Err(io::Error::other("temp error"))
692                } else {
693                    Ok("success")
694                }
695            },
696            5,
697            10,
698        )
699        .await;
700
701        assert!(result.is_ok());
702        assert_eq!(attempts, 3);
703    }
704
705    #[tokio::test]
706    async fn test_timeout() {
707        use std::time::Duration;
708
709        let result = recovery::with_timeout(
710            async {
711                tokio::time::sleep(Duration::from_secs(5)).await;
712                Ok("should timeout")
713            },
714            1,
715        )
716        .await;
717
718        assert!(result.is_err());
719        assert!(matches!(
720            result.unwrap_err().downcast::<KindlyError>().unwrap(),
721            KindlyError::TimeoutError(_) // Don't check exact value due to jitter
722        ));
723    }
724
725    #[test]
726    fn test_security_error_severity() {
727        // Critical errors
728        assert_eq!(
729            KindlyError::ThreatDetected {
730                threat_type: "sql_injection".to_string(),
731                location: "input".to_string()
732            }
733            .severity(),
734            ErrorSeverity::Critical
735        );
736
737        assert_eq!(
738            KindlyError::AuthError {
739                reason: "invalid_token".to_string()
740            }
741            .severity(),
742            ErrorSeverity::Critical
743        );
744
745        assert_eq!(
746            KindlyError::Unauthorized {
747                action: "read_secrets".to_string()
748            }
749            .severity(),
750            ErrorSeverity::Critical
751        );
752
753        // High severity
754        assert_eq!(
755            KindlyError::ResourceError {
756                resource: "memory".to_string(),
757                limit: "1GB".to_string()
758            }
759            .severity(),
760            ErrorSeverity::High
761        );
762
763        assert_eq!(
764            KindlyError::TimeoutError(30).severity(),
765            ErrorSeverity::High
766        );
767    }
768
769    #[test]
770    fn test_constant_time_compare() {
771        use security_patterns::constant_time_compare;
772
773        // Equal strings
774        assert!(constant_time_compare(b"secret123", b"secret123"));
775
776        // Different strings (same length)
777        assert!(!constant_time_compare(b"secret123", b"secret124"));
778
779        // Different lengths
780        assert!(!constant_time_compare(b"short", b"longer_string"));
781
782        // Empty strings
783        assert!(constant_time_compare(b"", b""));
784    }
785
786    #[test]
787    fn test_error_to_protocol_code() {
788        // Security-specific codes
789        assert_eq!(
790            KindlyError::AuthError {
791                reason: "test".to_string()
792            }
793            .to_protocol_code(),
794            -32001
795        );
796
797        assert_eq!(
798            KindlyError::Unauthorized {
799                action: "test".to_string()
800            }
801            .to_protocol_code(),
802            -32001
803        );
804
805        assert_eq!(
806            KindlyError::ThreatDetected {
807                threat_type: "test".to_string(),
808                location: "test".to_string()
809            }
810            .to_protocol_code(),
811            -32004
812        );
813
814        assert_eq!(KindlyError::TimeoutError(30).to_protocol_code(), -32002);
815
816        assert_eq!(
817            KindlyError::ResourceError {
818                resource: "test".to_string(),
819                limit: "test".to_string()
820            }
821            .to_protocol_code(),
822            -32003
823        );
824    }
825
826    #[test]
827    fn test_security_error_messages() {
828        // Auth errors should not reveal details
829        let auth_err = KindlyError::AuthError {
830            reason: "user_not_found_in_database".to_string(),
831        };
832        let user_msg = auth_err.user_message();
833        assert!(!user_msg.contains("database"));
834        assert!(!user_msg.contains("not_found"));
835        assert_eq!(
836            user_msg,
837            "Authentication failed. Please check your credentials."
838        );
839
840        // Threat errors should be generic
841        let threat_err = KindlyError::ThreatDetected {
842            threat_type: "sql_injection_union_select".to_string(),
843            location: "parameter_user_id".to_string(),
844        };
845        let user_msg = threat_err.user_message();
846        assert!(!user_msg.contains("sql"));
847        assert!(!user_msg.contains("injection"));
848        assert!(!user_msg.contains("parameter"));
849        assert!(!user_msg.contains("union"));
850        assert!(!user_msg.contains("user_id"));
851        assert_eq!(user_msg, "Security threat detected: policy violation");
852
853        // Unauthorized errors should hide the action
854        let unauth_err = KindlyError::Unauthorized {
855            action: "delete_all_users".to_string(),
856        };
857        let user_msg = unauth_err.user_message();
858        assert!(!user_msg.contains("delete"));
859        assert!(!user_msg.contains("users"));
860        assert_eq!(user_msg, "Unauthorized access");
861
862        // Resource errors should hide limits
863        let resource_err = KindlyError::ResourceError {
864            resource: "memory_heap".to_string(),
865            limit: "2GB".to_string(),
866        };
867        let user_msg = resource_err.user_message();
868        assert!(!user_msg.contains("memory"));
869        assert!(!user_msg.contains("heap"));
870        assert!(!user_msg.contains("2GB"));
871        assert_eq!(user_msg, "Resource limit exceeded");
872
873        // Timeout errors should hide duration
874        let timeout_err = KindlyError::TimeoutError(30);
875        let user_msg = timeout_err.user_message();
876        assert!(!user_msg.contains("30"));
877        assert_eq!(user_msg, "Operation timed out");
878    }
879}