auth_framework/errors.rs
1//! Comprehensive error types for the AuthFramework.
2//!
3//! This module defines all error types used throughout the authentication framework,
4//! providing detailed error information for debugging, logging, and user feedback.
5//! All errors implement standard Rust error traits and provide contextual information
6//! to help diagnose issues.
7//!
8//! # Error Categories
9//!
10//! - **Authentication Errors**: Credential validation and method failures
11//! - **Authorization Errors**: Permission and access control failures
12//! - **Token Errors**: JWT creation, validation, and lifecycle issues
13//! - **Configuration Errors**: Setup and configuration problems
14//! - **Storage Errors**: Database and persistence layer issues
15//! - **Network Errors**: External service communication failures
16//! - **Cryptographic Errors**: Security operation failures
17//!
18//! # Error Handling Patterns
19//!
20//! The framework uses structured error handling with:
21//! - Contextual error messages with relevant details
22//! - Error chaining to preserve root cause information
23//! - Categorized errors for appropriate response handling
24//! - Security-safe error messages that don't leak sensitive data
25//!
26//! # Example Error Handling
27//!
28//! ```rust,no_run
29//! use auth_framework::{AuthFramework, AuthError};
30//! use auth_framework::authentication::credentials::Credential;
31//! use auth_framework::authentication::CredentialMetadata;
32//!
33//! # #[tokio::main]
34//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
35//! # let auth_framework: AuthFramework = unimplemented!();
36//! # let credential: Credential = unimplemented!();
37//! # fn handle_success<T>(_: T) {}
38//! # fn respond_with_auth_failure() {}
39//! # fn respond_with_rate_limit(_: Option<u64>) {}
40//! # fn respond_with_system_error() {}
41//! match auth_framework.authenticate("password", credential).await {
42//! Ok(result) => handle_success(result),
43//! Err(AuthError::InvalidCredential { credential_type, message, .. }) => {
44//! log::warn!("Invalid {} credential: {}", credential_type, message);
45//! respond_with_auth_failure()
46//! },
47//! Err(AuthError::RateLimit { message, .. }) => {
48//! respond_with_rate_limit(None)
49//! },
50//! Err(e) => {
51//! log::error!("Authentication system error: {}", e);
52//! respond_with_system_error()
53//! }
54//! }
55//! # Ok(())
56//! # }
57//! ```
58//!
59//! # Security Considerations
60//!
61//! Error messages are designed to:
62//! - Provide useful debugging information for developers
63//! - Avoid exposing sensitive information to potential attackers
64//! - Enable proper security monitoring and alerting
65//! - Support compliance requirements for audit logging
66
67use thiserror::Error;
68
69/// Type alias for Results in the authentication framework.
70///
71/// This alias simplifies error handling throughout the framework by defaulting
72/// to `AuthError` as the error type while allowing flexibility for other error
73/// types when needed.
74pub type Result<T, E = AuthError> = std::result::Result<T, E>;
75
76/// Comprehensive error type covering all authentication and authorization failures.
77///
78/// `AuthError` provides detailed error information for all aspects of the authentication
79/// framework, from configuration issues to runtime failures. Each error variant includes
80/// contextual information to aid in debugging and provide appropriate user feedback.
81///
82/// This enhanced error type provides:
83/// - **Actionable error messages** with specific suggestions for fixes
84/// - **Documentation links** to relevant guides and troubleshooting
85/// - **Contextual help** that guides users to solutions
86/// - **Security-aware messaging** that doesn't leak sensitive information
87///
88/// # Error Categories
89///
90/// ## Configuration Errors
91/// Errors that occur during framework setup and configuration validation.
92///
93/// ## Authentication Errors
94/// Errors related to credential validation and authentication method execution.
95///
96/// ## Authorization Errors
97/// Errors related to permission checking and access control.
98///
99/// ## Token Errors
100/// JWT token creation, validation, expiration, and lifecycle issues.
101///
102/// ## Storage Errors
103/// Database connectivity, query failures, and data persistence issues.
104///
105/// ## Network Errors
106/// External service communication, timeouts, and connectivity problems.
107///
108/// ## Cryptographic Errors
109/// Encryption, decryption, signing, and other security operation failures.
110///
111/// # Enhanced Error Handling
112///
113/// ```rust,no_run
114/// use auth_framework::AuthError;
115///
116/// # let auth_result: auth_framework::errors::Result<()> = Ok(());
117/// // Enhanced error handling with contextual help
118/// match auth_result {
119/// Err(AuthError::Configuration { message, help, docs_url, .. }) => {
120/// eprintln!("Configuration Error: {}", message);
121/// if let Some(help) = help {
122/// eprintln!("Help: {}", help);
123/// }
124/// if let Some(docs) = docs_url {
125/// eprintln!("See: {}", docs);
126/// }
127/// },
128/// Err(AuthError::InvalidCredential { credential_type, message, .. }) => {
129/// eprintln!("Invalid {}: {}", credential_type, message);
130/// },
131/// // ... handle other error types
132/// _ => {}
133/// }
134/// ```
135///
136/// # Security Notes
137///
138/// Error messages are carefully crafted to:
139/// - Provide sufficient detail for debugging and monitoring
140/// - Avoid exposing sensitive information that could aid attackers
141/// - Enable security teams to identify potential threats
142/// - Support compliance and audit requirements
143/// - Guide users to secure solutions and best practices
144#[derive(Error, Debug)]
145pub enum AuthError {
146 /// Configuration validation and setup errors.
147 ///
148 /// These errors occur when the authentication framework is misconfigured
149 /// or when configuration validation fails during startup.
150 #[error("Configuration error: {message}")]
151 Configuration {
152 message: String,
153 #[source]
154 source: Option<Box<dyn std::error::Error + Send + Sync>>,
155 /// Helpful guidance for fixing the issue
156 help: Option<String>,
157 /// Link to relevant documentation
158 docs_url: Option<String>,
159 /// Specific fix suggestion with commands or code
160 suggested_fix: Option<String>,
161 },
162
163 /// Authentication method execution errors.
164 ///
165 /// These errors occur when a specific authentication method fails to
166 /// execute properly, such as OAuth provider communication failures.
167 #[error("Authentication method '{method}' error: {message}")]
168 AuthMethod {
169 method: String,
170 message: String,
171 /// Helpful guidance for fixing the issue
172 help: Option<String>,
173 /// Link to relevant documentation
174 docs_url: Option<String>,
175 /// Specific fix suggestion
176 suggested_fix: Option<String>,
177 },
178
179 /// Token-related errors
180 #[error("Token error: {0}")]
181 Token(#[from] TokenError),
182
183 /// Permission-related errors
184 #[error("Permission error: {0}")]
185 Permission(#[from] PermissionError),
186
187 /// Storage-related errors
188 #[error("Storage error: {0}")]
189 Storage(#[from] StorageError),
190
191 /// Network/HTTP errors
192 #[error("Network error: {0}")]
193 Network(#[from] reqwest::Error),
194
195 /// JSON parsing errors
196 #[error("JSON error: {0}")]
197 Json(#[from] serde_json::Error),
198
199 /// JWT errors
200 #[error("JWT error: {0}")]
201 Jwt(#[from] jsonwebtoken::errors::Error),
202
203 /// YAML parsing errors
204 #[error("YAML error: {0}")]
205 Yaml(#[from] serde_yaml::Error),
206
207 /// TOML parsing errors
208 #[error("TOML error: {0}")]
209 Toml(#[from] toml::ser::Error),
210
211 /// Prometheus metrics errors
212 #[cfg(feature = "prometheus")]
213 #[error("Metrics error: {0}")]
214 Metrics(#[from] prometheus::Error),
215
216 /// IO errors
217 #[error("IO error: {0}")]
218 Io(#[from] std::io::Error),
219
220 /// CLI interaction errors
221 #[error("CLI error: {0}")]
222 Cli(String),
223
224 /// System time errors
225 #[error("System time error: {0}")]
226 SystemTime(#[from] std::time::SystemTimeError),
227
228 /// Rate limiting errors
229 #[error("Rate limit exceeded: {message}")]
230 RateLimit { message: String },
231
232 /// Session-related errors
233 #[error(
234 "Too many concurrent sessions for user (limit reached). \
235 Revoke an existing session before creating a new one."
236 )]
237 TooManyConcurrentSessions,
238
239 /// MFA-related errors
240 #[error("MFA error: {0}")]
241 Mfa(#[from] MfaError),
242
243 /// Device flow errors
244 #[error("Device flow error: {0}")]
245 DeviceFlow(#[from] DeviceFlowError),
246
247 /// OAuth provider errors
248 #[error("OAuth provider error: {0}")]
249 OAuthProvider(#[from] OAuthProviderError),
250
251 /// The stored password hash does not match the supplied credential.
252 ///
253 /// This typically means the user entered the wrong password. Do **not**
254 /// reveal which field (username vs. password) was incorrect to the caller.
255 #[error("Password verification failed: {0}")]
256 PasswordVerification(String),
257
258 /// The password hashing algorithm (argon2/bcrypt) encountered an error.
259 #[error("Password hashing failed: {0}")]
260 PasswordHashing(String),
261
262 /// No user record exists for the given identifier.
263 #[error("User not found. The requested user ID does not exist in the store.")]
264 UserNotFound,
265
266 /// A request parameter failed validation (e.g. empty username, invalid email format).
267 #[error("Invalid input: {0}")]
268 InvalidInput(String),
269
270 /// A hardware security key (FIDO2 / WebAuthn) operation failed.
271 #[error("Hardware token error: {0}")]
272 HardwareToken(String),
273
274 /// A one-time backup code was rejected (already used, incorrect, or expired).
275 #[error("Backup code verification failed: {0}")]
276 BackupCodeVerification(String),
277
278 /// Failed to hash a backup code for secure storage.
279 #[error("Backup code hashing failed: {0}")]
280 BackupCodeHashing(String),
281
282 /// The TOTP or MFA secret is in an invalid format (e.g. not valid Base32).
283 #[error("Invalid secret format")]
284 InvalidSecret,
285
286 /// An error occurred while reading or updating a user profile.
287 #[error("User profile error: {message}")]
288 UserProfile { message: String },
289
290 /// Credential validation errors
291 #[error("Invalid credential: {credential_type} - {message}")]
292 InvalidCredential {
293 credential_type: String,
294 message: String,
295 },
296
297 /// An authentication operation did not complete within the allowed window.
298 #[error("Authentication timeout after {timeout_seconds} seconds")]
299 Timeout { timeout_seconds: u64 },
300
301 /// Provider configuration missing
302 #[error(
303 "Provider '{provider}' is not configured or supported. \
304 Add it via AuthConfig::method_config() or check available providers."
305 )]
306 ProviderNotConfigured { provider: String },
307
308 /// Cryptography errors
309 #[error("Cryptography error: {message}")]
310 Crypto { message: String },
311
312 /// Validation errors
313 #[error("Validation error: {message}")]
314 Validation { message: String },
315
316 /// Generic internal errors
317 #[error("Internal error: {message}")]
318 Internal { message: String },
319
320 /// Invalid request error
321 #[error("Invalid request: {0}")]
322 InvalidRequest(String),
323
324 /// Step-up authentication required
325 #[error(
326 "Step-up authentication required: current level '{current_level}', required level '{required_level}'"
327 )]
328 StepUpRequired {
329 current_level: String,
330 required_level: String,
331 step_up_url: String,
332 },
333
334 /// A session-layer error (e.g. session not found, session expired, store failure).
335 #[error("Session error: {0}")]
336 SessionError(String),
337
338 /// The caller lacks valid credentials or the credentials have been revoked.
339 #[error("Unauthorized: {0}")]
340 Unauthorized(String),
341
342 /// Token creation failed at the signing or encoding stage.
343 ///
344 /// Consider using [`AuthError::token`] for richer [`TokenError`] variants.
345 #[error("Token generation error: {0}")]
346 TokenGeneration(String),
347
348 /// Invalid token error.
349 ///
350 /// **Prefer** [`AuthError::token`] which routes through the structured
351 /// [`TokenError`] hierarchy for consistent token error handling.
352 #[deprecated(
353 since = "0.5.0",
354 note = "Use `AuthError::token(msg)` instead — it routes through the structured TokenError hierarchy"
355 )]
356 #[error("Invalid token: {0}")]
357 InvalidToken(String),
358
359 /// The named authentication provider is not compiled in or not recognized.
360 #[error("Unsupported provider: {0}")]
361 UnsupportedProvider(String),
362
363 /// Network error with custom message.
364 ///
365 /// **Prefer** [`AuthError::Internal`] with a descriptive message, or let
366 /// [`reqwest::Error`] convert automatically via the [`From`] impl on
367 /// [`AuthError::Network`].
368 #[deprecated(
369 since = "0.5.0",
370 note = "Use `AuthError::internal(msg)` or let `reqwest::Error` convert via `AuthError::Network` instead"
371 )]
372 #[error("Network error: {0}")]
373 NetworkError(String),
374
375 /// Parse error with custom message.
376 ///
377 /// **Prefer** [`AuthError::Internal`] with a descriptive message, or let
378 /// [`serde_json::Error`] convert automatically via the [`From`] impl on
379 /// [`AuthError::Json`].
380 #[deprecated(
381 since = "0.5.0",
382 note = "Use `AuthError::internal(msg)` or let serde errors convert via `AuthError::Json` instead"
383 )]
384 #[error("Parse error: {0}")]
385 ParseError(String),
386
387 /// Configuration error with custom message.
388 ///
389 /// **Prefer** [`AuthError::config`] or [`AuthError::config_with_help`] which
390 /// carry richer context (help text, docs URL, suggested fix).
391 #[deprecated(
392 since = "0.5.0",
393 note = "Use `AuthError::config(msg)` instead — it provides richer context fields"
394 )]
395 #[error("Configuration error: {0}")]
396 ConfigurationError(String),
397}
398
399/// Token-specific errors.
400///
401/// Covers the lifecycle of authentication tokens: creation, validation,
402/// refresh, and revocation.
403#[derive(Error, Debug)]
404pub enum TokenError {
405 /// The token's `exp` claim is in the past.
406 #[error("Token has expired")]
407 Expired,
408
409 /// The token failed signature verification or structural validation.
410 #[error("Token is invalid: {message}")]
411 Invalid { message: String },
412
413 /// No token matching the given identifier exists in the store.
414 #[error("Token not found")]
415 NotFound,
416
417 /// The request did not include a required token (e.g. missing `Authorization` header).
418 #[error("Token is missing")]
419 Missing,
420
421 /// A new token could not be issued (e.g. signing key unavailable).
422 #[error("Token creation failed: {message}")]
423 CreationFailed { message: String },
424
425 /// An existing token could not be refreshed (e.g. refresh token revoked).
426 #[error("Token refresh failed: {message}")]
427 RefreshFailed { message: String },
428
429 /// Token revocation did not complete (e.g. storage write failure).
430 #[error("Token revocation failed: {message}")]
431 RevocationFailed { message: String },
432}
433
434/// Permission and access-control errors.
435///
436/// Returned when an authenticated principal lacks the required permissions
437/// or roles to perform an operation.
438#[derive(Error, Debug)]
439pub enum PermissionError {
440 /// The principal does not hold the required permission for the target resource.
441 #[error("Access denied: missing permission '{permission}' for resource '{resource}'")]
442 AccessDenied {
443 permission: String,
444 resource: String,
445 },
446
447 /// The specified role does not exist in the role store.
448 #[error("Role '{role}' not found")]
449 RoleNotFound { role: String },
450
451 /// The specified permission identifier does not exist.
452 #[error("Permission '{permission}' not found")]
453 PermissionNotFound { permission: String },
454
455 /// A permission string could not be parsed (e.g. missing `resource:action` separator).
456 #[error("Invalid permission format: {message}")]
457 InvalidFormat { message: String },
458
459 /// General permission denial with a descriptive message.
460 #[error("Permission denied: {message}")]
461 Denied {
462 action: String,
463 resource: String,
464 message: String,
465 },
466}
467
468/// Storage backend errors.
469///
470/// Covers connectivity, query execution, and serialization issues for
471/// all storage backends (Memory, PostgreSQL, MySQL, Redis, SQLite).
472#[derive(Error, Debug)]
473pub enum StorageError {
474 /// Could not establish a connection to the storage backend.
475 #[error("Connection failed: {message}")]
476 ConnectionFailed { message: String },
477
478 /// A read or write operation against the store failed.
479 #[error("Operation failed: {message}")]
480 OperationFailed { message: String },
481
482 /// Data could not be serialized to or deserialized from the storage format.
483 #[error("Serialization error: {message}")]
484 Serialization { message: String },
485
486 /// The configured storage backend is unreachable or not initialized.
487 #[error("Storage backend not available")]
488 BackendUnavailable,
489}
490
491/// Multi-factor authentication errors.
492#[derive(Error, Debug)]
493pub enum MfaError {
494 /// The MFA challenge window has elapsed; the user must request a new challenge.
495 #[error("MFA challenge expired")]
496 ChallengeExpired,
497
498 /// The TOTP or one-time code provided by the user is incorrect.
499 #[error("Invalid MFA code")]
500 InvalidCode,
501
502 /// The requested MFA method (e.g. SMS, hardware key) is not enabled.
503 #[error("MFA method not supported: {method}")]
504 MethodNotSupported { method: String },
505
506 /// MFA has not been configured for this account yet.
507 #[error("MFA setup required")]
508 SetupRequired,
509
510 /// MFA verification failed for a reason described in `message`.
511 #[error("MFA verification failed: {message}")]
512 VerificationFailed { message: String },
513}
514
515/// Device Authorization Grant (RFC 8628) errors.
516///
517/// These map 1-to-1 to the error codes defined in RFC 8628 §3.5.
518#[derive(Error, Debug)]
519pub enum DeviceFlowError {
520 /// The user has not yet completed authorization on the verification URI.
521 #[error("Authorization pending - user has not yet completed authorization")]
522 AuthorizationPending,
523
524 /// The client is polling faster than the allowed `interval`.
525 #[error("Slow down - polling too frequently")]
526 SlowDown,
527
528 /// The device code has exceeded its `expires_in` window.
529 #[error("Device code expired")]
530 ExpiredToken,
531
532 /// The end user denied the authorization request.
533 #[error("Access denied by user")]
534 AccessDenied,
535
536 /// The device code presented by the client is not recognized.
537 #[error("Invalid device code")]
538 InvalidDeviceCode,
539
540 /// The grant type is not supported by this server.
541 #[error("Unsupported grant type")]
542 UnsupportedGrantType,
543}
544
545/// OAuth 2.0 provider interaction errors.
546///
547/// Covers issues encountered when communicating with upstream OAuth providers
548/// or when validating OAuth protocol messages.
549#[derive(Error, Debug)]
550pub enum OAuthProviderError {
551 /// The authorization code is expired, already consumed, or invalid.
552 #[error("Invalid authorization code")]
553 InvalidAuthorizationCode,
554
555 /// The `redirect_uri` does not match the registered value for the client.
556 #[error("Invalid redirect URI")]
557 InvalidRedirectUri,
558
559 /// The client ID or client secret is incorrect.
560 #[error("Invalid client credentials")]
561 InvalidClientCredentials,
562
563 /// The granted scopes do not satisfy the required scopes for the operation.
564 #[error("Insufficient scope: required '{required}', granted '{granted}'")]
565 InsufficientScope { required: String, granted: String },
566
567 /// The provider does not implement the requested feature.
568 #[error("Provider '{provider}' does not support '{feature}'")]
569 UnsupportedFeature { provider: String, feature: String },
570
571 /// The upstream provider returned a rate-limit response.
572 #[error("Rate limited by provider: {message}")]
573 RateLimited { message: String },
574}
575
576impl AuthError {
577 /// Create a new configuration error.
578 ///
579 /// # Example
580 ///
581 /// ```rust
582 /// use auth_framework::AuthError;
583 ///
584 /// let err = AuthError::config("Missing database URL");
585 /// assert!(err.to_string().contains("Missing database URL"));
586 /// ```
587 pub fn config(message: impl Into<String>) -> Self {
588 Self::Configuration {
589 message: message.into(),
590 source: None,
591 help: None,
592 docs_url: None,
593 suggested_fix: None,
594 }
595 }
596
597 /// Create a configuration error with helpful context.
598 ///
599 /// Includes a human-readable `help` string and an optional `suggested_fix`
600 /// that can be displayed by CLI tools or IDE integrations.
601 ///
602 /// # Example
603 ///
604 /// ```rust
605 /// use auth_framework::AuthError;
606 ///
607 /// let err = AuthError::config_with_help(
608 /// "JWT secret not set",
609 /// "Set the JWT_SECRET environment variable",
610 /// Some("export JWT_SECRET=$(openssl rand -hex 32)".into()),
611 /// );
612 /// assert!(err.to_string().contains("JWT secret not set"));
613 /// ```
614 pub fn config_with_help(
615 message: impl Into<String>,
616 help: impl Into<String>,
617 suggested_fix: Option<String>,
618 ) -> Self {
619 Self::Configuration {
620 message: message.into(),
621 source: None,
622 help: Some(help.into()),
623 docs_url: Some(
624 "https://docs.rs/auth-framework/latest/auth_framework/config/".to_string(),
625 ),
626 suggested_fix,
627 }
628 }
629
630 /// Create a JWT secret validation error with helpful guidance.
631 ///
632 /// # Example
633 ///
634 /// ```rust
635 /// use auth_framework::AuthError;
636 ///
637 /// let err = AuthError::jwt_secret_too_short(16);
638 /// assert!(err.to_string().contains("16 characters"));
639 /// ```
640 pub fn jwt_secret_too_short(current_length: usize) -> Self {
641 Self::Configuration {
642 message: format!(
643 "JWT secret too short (got {} characters, need 32+ for security)",
644 current_length
645 ),
646 source: None,
647 help: Some("Use a cryptographically secure random string of at least 32 characters".to_string()),
648 docs_url: Some("https://docs.rs/auth-framework/latest/auth_framework/config/struct.SecurityConfig.html".to_string()),
649 suggested_fix: Some("Generate a secure secret: `openssl rand -hex 32`".to_string()),
650 }
651 }
652
653 /// Create a production environment error with guidance.
654 ///
655 /// # Example
656 ///
657 /// ```rust
658 /// use auth_framework::AuthError;
659 ///
660 /// let err = AuthError::production_memory_storage();
661 /// assert!(err.to_string().contains("Memory storage"));
662 /// ```
663 pub fn production_memory_storage() -> Self {
664 Self::Configuration {
665 message: "Memory storage is not suitable for production environments".to_string(),
666 source: None,
667 help: Some("Use a persistent storage backend like PostgreSQL or Redis".to_string()),
668 docs_url: Some("https://docs.rs/auth-framework/latest/auth_framework/storage/".to_string()),
669 suggested_fix: Some("Configure PostgreSQL: .with_postgres(\"postgresql://...\") or Redis: .with_redis(\"redis://...\")".to_string()),
670 }
671 }
672
673 /// Create a new auth method error.
674 ///
675 /// # Example
676 ///
677 /// ```rust
678 /// use auth_framework::AuthError;
679 ///
680 /// let err = AuthError::auth_method("oauth2", "token endpoint unreachable");
681 /// assert!(err.to_string().contains("oauth2"));
682 /// ```
683 pub fn auth_method(method: impl Into<String>, message: impl Into<String>) -> Self {
684 Self::AuthMethod {
685 method: method.into(),
686 message: message.into(),
687 help: None,
688 docs_url: None,
689 suggested_fix: None,
690 }
691 }
692
693 /// Create an auth method error with helpful context.
694 ///
695 /// # Example
696 ///
697 /// ```rust
698 /// use auth_framework::AuthError;
699 ///
700 /// let err = AuthError::auth_method_with_help(
701 /// "saml",
702 /// "certificate expired",
703 /// "Renew the SAML signing certificate",
704 /// Some("openssl x509 -req -in cert.csr -signkey key.pem -out cert.pem".into()),
705 /// );
706 /// assert!(err.to_string().contains("saml"));
707 /// ```
708 pub fn auth_method_with_help(
709 method: impl Into<String>,
710 message: impl Into<String>,
711 help: impl Into<String>,
712 suggested_fix: Option<String>,
713 ) -> Self {
714 Self::AuthMethod {
715 method: method.into(),
716 message: message.into(),
717 help: Some(help.into()),
718 docs_url: Some(
719 "https://docs.rs/auth-framework/latest/auth_framework/methods/".to_string(),
720 ),
721 suggested_fix,
722 }
723 }
724
725 /// Create a new rate limit error.
726 ///
727 /// # Example
728 ///
729 /// ```rust
730 /// use auth_framework::AuthError;
731 ///
732 /// let err = AuthError::rate_limit("too many login attempts");
733 /// assert!(err.to_string().contains("too many login attempts"));
734 /// ```
735 pub fn rate_limit(message: impl Into<String>) -> Self {
736 Self::RateLimit {
737 message: message.into(),
738 }
739 }
740
741 /// Create a new crypto error.
742 ///
743 /// # Example
744 ///
745 /// ```rust
746 /// use auth_framework::AuthError;
747 ///
748 /// let err = AuthError::crypto("HMAC key too short");
749 /// assert!(err.to_string().contains("HMAC key too short"));
750 /// ```
751 pub fn crypto(message: impl Into<String>) -> Self {
752 Self::Crypto {
753 message: message.into(),
754 }
755 }
756
757 /// Create a new validation error.
758 ///
759 /// # Example
760 ///
761 /// ```rust
762 /// use auth_framework::AuthError;
763 ///
764 /// let err = AuthError::validation("email format invalid");
765 /// assert!(err.to_string().contains("email format invalid"));
766 /// ```
767 pub fn validation(message: impl Into<String>) -> Self {
768 Self::Validation {
769 message: message.into(),
770 }
771 }
772
773 /// Create a new internal error.
774 ///
775 /// # Example
776 ///
777 /// ```rust
778 /// use auth_framework::AuthError;
779 ///
780 /// let err = AuthError::internal("unexpected state");
781 /// assert!(err.to_string().contains("unexpected state"));
782 /// ```
783 pub fn internal(message: impl Into<String>) -> Self {
784 Self::Internal {
785 message: message.into(),
786 }
787 }
788
789 /// Create an authorization error.
790 ///
791 /// # Example
792 ///
793 /// ```rust
794 /// use auth_framework::AuthError;
795 ///
796 /// let err = AuthError::authorization("insufficient privileges");
797 /// assert!(err.to_string().contains("insufficient privileges"));
798 /// ```
799 pub fn authorization(message: impl Into<String>) -> Self {
800 Self::Permission(PermissionError::Denied {
801 action: "authorize".to_string(),
802 resource: "resource".to_string(),
803 message: message.into(),
804 })
805 }
806
807 /// Create an access denied error.
808 ///
809 /// # Example
810 ///
811 /// ```rust
812 /// use auth_framework::AuthError;
813 ///
814 /// let err = AuthError::access_denied("admin role required");
815 /// assert!(err.to_string().contains("admin role required"));
816 /// ```
817 pub fn access_denied(message: impl Into<String>) -> Self {
818 Self::Permission(PermissionError::Denied {
819 action: "access".to_string(),
820 resource: "resource".to_string(),
821 message: message.into(),
822 })
823 }
824
825 /// Create a token error.
826 ///
827 /// # Example
828 ///
829 /// ```rust
830 /// use auth_framework::AuthError;
831 ///
832 /// let err = AuthError::token("signature mismatch");
833 /// assert!(err.to_string().contains("signature mismatch"));
834 /// ```
835 pub fn token(message: impl Into<String>) -> Self {
836 Self::Token(TokenError::Invalid {
837 message: message.into(),
838 })
839 }
840
841 /// Create a device flow error.
842 ///
843 /// # Example
844 ///
845 /// ```rust
846 /// use auth_framework::errors::{AuthError, DeviceFlowError};
847 ///
848 /// let err = AuthError::device_flow(DeviceFlowError::ExpiredToken);
849 /// assert!(err.to_string().contains("expired"));
850 /// ```
851 pub fn device_flow(error: DeviceFlowError) -> Self {
852 Self::DeviceFlow(error)
853 }
854
855 /// Create an OAuth provider error.
856 ///
857 /// # Example
858 ///
859 /// ```rust
860 /// use auth_framework::errors::{AuthError, OAuthProviderError};
861 ///
862 /// let err = AuthError::oauth_provider(OAuthProviderError::InvalidRedirectUri);
863 /// assert!(err.to_string().contains("redirect"));
864 /// ```
865 pub fn oauth_provider(error: OAuthProviderError) -> Self {
866 Self::OAuthProvider(error)
867 }
868
869 /// Create a user profile error.
870 ///
871 /// # Example
872 ///
873 /// ```rust
874 /// use auth_framework::AuthError;
875 ///
876 /// let err = AuthError::user_profile("email already in use");
877 /// assert!(err.to_string().contains("email already in use"));
878 /// ```
879 pub fn user_profile(message: impl Into<String>) -> Self {
880 Self::UserProfile {
881 message: message.into(),
882 }
883 }
884
885 /// Create an invalid credential error.
886 ///
887 /// # Example
888 ///
889 /// ```rust
890 /// use auth_framework::AuthError;
891 ///
892 /// let err = AuthError::invalid_credential("password", "too short");
893 /// assert!(err.to_string().contains("password"));
894 /// ```
895 pub fn invalid_credential(
896 credential_type: impl Into<String>,
897 message: impl Into<String>,
898 ) -> Self {
899 Self::InvalidCredential {
900 credential_type: credential_type.into(),
901 message: message.into(),
902 }
903 }
904
905 /// Create a timeout error.
906 ///
907 /// # Example
908 ///
909 /// ```rust
910 /// use auth_framework::AuthError;
911 ///
912 /// let err = AuthError::timeout(30);
913 /// assert!(err.to_string().contains("30"));
914 /// ```
915 pub fn timeout(timeout_seconds: u64) -> Self {
916 Self::Timeout { timeout_seconds }
917 }
918
919 /// Create a provider not configured error.
920 ///
921 /// # Example
922 ///
923 /// ```rust
924 /// use auth_framework::AuthError;
925 ///
926 /// let err = AuthError::provider_not_configured("github");
927 /// assert!(err.to_string().contains("github"));
928 /// ```
929 pub fn provider_not_configured(provider: impl Into<String>) -> Self {
930 Self::ProviderNotConfigured {
931 provider: provider.into(),
932 }
933 }
934
935 /// Create a rate limited error (alias for [`rate_limit`](Self::rate_limit)).
936 ///
937 /// # Example
938 ///
939 /// ```rust
940 /// use auth_framework::AuthError;
941 ///
942 /// let err = AuthError::rate_limited("5 requests per second exceeded");
943 /// assert!(err.to_string().contains("5 requests"));
944 /// ```
945 pub fn rate_limited(message: impl Into<String>) -> Self {
946 Self::RateLimit {
947 message: message.into(),
948 }
949 }
950
951 /// Create a configuration error (alias for [`config`](Self::config)).
952 ///
953 /// # Example
954 ///
955 /// ```rust
956 /// use auth_framework::AuthError;
957 ///
958 /// let err = AuthError::configuration("invalid issuer URL");
959 /// assert!(err.to_string().contains("invalid issuer URL"));
960 /// ```
961 pub fn configuration(message: impl Into<String>) -> Self {
962 Self::Configuration {
963 message: message.into(),
964 source: None,
965 help: None,
966 docs_url: None,
967 suggested_fix: None,
968 }
969 }
970
971 // -----------------------------------------------------------------------
972 // Classification helpers — framework-agnostic error introspection
973 // -----------------------------------------------------------------------
974
975 /// Return the HTTP status code that best represents this error.
976 ///
977 /// This is framework-agnostic and works without enabling any web framework
978 /// feature flag. Web framework integrations (actix-web, axum, …) use this
979 /// internally but you can also call it directly when building custom
980 /// response types.
981 ///
982 /// # Example
983 ///
984 /// ```rust
985 /// use auth_framework::AuthError;
986 ///
987 /// assert_eq!(AuthError::rate_limit("slow down").http_status_code(), 429);
988 /// assert_eq!(AuthError::internal("oops").http_status_code(), 500);
989 /// assert_eq!(AuthError::token("bad jwt").http_status_code(), 401);
990 /// ```
991 pub fn http_status_code(&self) -> u16 {
992 match self {
993 // 400 Bad Request
994 Self::InvalidInput(_)
995 | Self::Validation { .. }
996 | Self::InvalidCredential { .. }
997 | Self::HardwareToken(_)
998 | Self::InvalidRequest(_)
999 | Self::InvalidSecret => 400,
1000
1001 // 401 Unauthorized
1002 Self::Token(_)
1003 | Self::AuthMethod { .. }
1004 | Self::Jwt(_)
1005 | Self::Unauthorized(_)
1006 | Self::PasswordVerification(_) => 401,
1007
1008 // 403 Forbidden
1009 Self::Permission(_)
1010 | Self::StepUpRequired { .. } => 403,
1011
1012 // 404 Not Found
1013 Self::UserNotFound
1014 | Self::ProviderNotConfigured { .. } => 404,
1015
1016 // 408 Request Timeout
1017 Self::Timeout { .. } => 408,
1018
1019 // 429 Too Many Requests
1020 Self::RateLimit { .. }
1021 | Self::TooManyConcurrentSessions => 429,
1022
1023 // 502 Bad Gateway (upstream provider errors)
1024 Self::OAuthProvider(_)
1025 | Self::Network(_) => 502,
1026
1027 // 503 Service Unavailable (storage / infra)
1028 Self::Storage(_) => 503,
1029
1030 // 500 Internal Server Error (everything else)
1031 Self::Configuration { .. }
1032 | Self::Crypto { .. }
1033 | Self::Internal { .. }
1034 | Self::Json(_)
1035 | Self::Yaml(_)
1036 | Self::Toml(_)
1037 | Self::Io(_)
1038 | Self::Mfa(_)
1039 | Self::DeviceFlow(_)
1040 | Self::TokenGeneration(_)
1041 | Self::SessionError(_)
1042 | Self::UserProfile { .. }
1043 | Self::Cli(_)
1044 | Self::SystemTime(_)
1045 | Self::PasswordHashing(_)
1046 | Self::BackupCodeVerification(_)
1047 | Self::BackupCodeHashing(_)
1048 | Self::UnsupportedProvider(_) => 500,
1049
1050 #[cfg(feature = "prometheus")]
1051 Self::Metrics(_) => 500,
1052
1053 // Deprecated variants
1054 #[allow(deprecated)]
1055 Self::InvalidToken(_)
1056 | Self::NetworkError(_)
1057 | Self::ParseError(_)
1058 | Self::ConfigurationError(_) => 500,
1059 }
1060 }
1061
1062 /// Whether this error is transient and the operation may succeed on retry.
1063 ///
1064 /// Returns `true` for rate-limit, timeout, network, and storage
1065 /// connectivity errors. Returns `false` for authentication failures,
1066 /// validation errors, and other permanent conditions.
1067 ///
1068 /// # Example
1069 ///
1070 /// ```rust
1071 /// use auth_framework::AuthError;
1072 ///
1073 /// assert!(AuthError::rate_limit("too fast").is_retryable());
1074 /// assert!(AuthError::timeout(30).is_retryable());
1075 /// assert!(!AuthError::token("invalid").is_retryable());
1076 /// assert!(!AuthError::validation("bad input").is_retryable());
1077 /// ```
1078 pub fn is_retryable(&self) -> bool {
1079 matches!(
1080 self,
1081 Self::RateLimit { .. }
1082 | Self::Timeout { .. }
1083 | Self::TooManyConcurrentSessions
1084 | Self::Network(_)
1085 | Self::Storage(StorageError::ConnectionFailed { .. })
1086 )
1087 }
1088
1089 /// A short, stable, machine-readable code for this error category.
1090 ///
1091 /// Useful for API responses, metrics labels, and log filtering.
1092 /// These codes are stable across patch releases.
1093 ///
1094 /// # Example
1095 ///
1096 /// ```rust
1097 /// use auth_framework::AuthError;
1098 ///
1099 /// assert_eq!(AuthError::rate_limit("nope").error_code(), "rate_limit");
1100 /// assert_eq!(AuthError::token("bad").error_code(), "invalid_token");
1101 /// assert_eq!(AuthError::config("bad").error_code(), "configuration");
1102 /// ```
1103 pub fn error_code(&self) -> &'static str {
1104 match self {
1105 Self::Configuration { .. } => "configuration",
1106 Self::AuthMethod { .. } => "auth_method",
1107 Self::Token(_) => "invalid_token",
1108 Self::Permission(_) => "insufficient_permissions",
1109 Self::Storage(_) => "storage",
1110 Self::Network(_) => "network",
1111 Self::Json(_) | Self::Yaml(_) | Self::Toml(_) => "serialization",
1112 Self::Jwt(_) => "jwt",
1113 Self::Io(_) => "io",
1114 Self::RateLimit { .. } => "rate_limit",
1115 Self::TooManyConcurrentSessions => "concurrent_sessions",
1116 Self::Mfa(_) => "mfa",
1117 Self::DeviceFlow(_) => "device_flow",
1118 Self::OAuthProvider(_) => "oauth_provider",
1119 Self::PasswordVerification(_) => "password_verification",
1120 Self::UserNotFound => "user_not_found",
1121 Self::InvalidInput(_) => "invalid_input",
1122 Self::HardwareToken(_) => "hardware_token",
1123 Self::InvalidCredential { .. } => "invalid_credential",
1124 Self::Timeout { .. } => "timeout",
1125 Self::Crypto { .. } => "crypto",
1126 Self::Validation { .. } => "validation",
1127 Self::Internal { .. } => "internal",
1128 Self::StepUpRequired { .. } => "step_up_required",
1129 Self::SessionError(_) => "session",
1130 Self::Unauthorized(_) => "unauthorized",
1131 Self::TokenGeneration(_) => "token_generation",
1132 Self::UserProfile { .. } => "user_profile",
1133 Self::ProviderNotConfigured { .. } => "provider_not_configured",
1134 Self::Cli(_) => "cli",
1135 Self::SystemTime(_) => "internal",
1136 Self::PasswordHashing(_) => "password_hashing",
1137 Self::BackupCodeVerification(_) => "backup_code",
1138 Self::BackupCodeHashing(_) => "backup_code",
1139 Self::InvalidSecret => "invalid_secret",
1140 Self::InvalidRequest(_) => "invalid_request",
1141 Self::UnsupportedProvider(_) => "unsupported_provider",
1142 #[cfg(feature = "prometheus")]
1143 Self::Metrics(_) => "metrics",
1144 #[allow(deprecated)]
1145 Self::InvalidToken(_) => "invalid_token",
1146 #[allow(deprecated)]
1147 Self::NetworkError(_) => "network",
1148 #[allow(deprecated)]
1149 Self::ParseError(_) => "parse",
1150 #[allow(deprecated)]
1151 Self::ConfigurationError(_) => "configuration",
1152 }
1153 }
1154
1155 /// Whether this is a client-side error (4xx status code).
1156 ///
1157 /// # Example
1158 ///
1159 /// ```rust
1160 /// use auth_framework::AuthError;
1161 ///
1162 /// assert!(AuthError::validation("bad").is_client_error());
1163 /// assert!(!AuthError::internal("oops").is_client_error());
1164 /// ```
1165 pub fn is_client_error(&self) -> bool {
1166 (400..500).contains(&self.http_status_code())
1167 }
1168
1169 /// Whether this is a server-side error (5xx status code).
1170 ///
1171 /// # Example
1172 ///
1173 /// ```rust
1174 /// use auth_framework::AuthError;
1175 ///
1176 /// assert!(AuthError::internal("oops").is_server_error());
1177 /// assert!(!AuthError::validation("bad").is_server_error());
1178 /// ```
1179 pub fn is_server_error(&self) -> bool {
1180 self.http_status_code() >= 500
1181 }
1182}
1183
1184impl TokenError {
1185 /// Create a new token creation failed error.
1186 ///
1187 /// # Example
1188 ///
1189 /// ```rust
1190 /// use auth_framework::errors::TokenError;
1191 ///
1192 /// let err = TokenError::creation_failed("signing key unavailable");
1193 /// assert!(err.to_string().contains("signing key unavailable"));
1194 /// ```
1195 pub fn creation_failed(message: impl Into<String>) -> Self {
1196 Self::CreationFailed {
1197 message: message.into(),
1198 }
1199 }
1200
1201 /// Create a new token refresh failed error.
1202 ///
1203 /// # Example
1204 ///
1205 /// ```rust
1206 /// use auth_framework::errors::TokenError;
1207 ///
1208 /// let err = TokenError::refresh_failed("refresh token revoked");
1209 /// assert!(err.to_string().contains("refresh token revoked"));
1210 /// ```
1211 pub fn refresh_failed(message: impl Into<String>) -> Self {
1212 Self::RefreshFailed {
1213 message: message.into(),
1214 }
1215 }
1216
1217 /// Create a new token revocation failed error.
1218 ///
1219 /// # Example
1220 ///
1221 /// ```rust
1222 /// use auth_framework::errors::TokenError;
1223 ///
1224 /// let err = TokenError::revocation_failed("storage write failed");
1225 /// assert!(err.to_string().contains("storage write failed"));
1226 /// ```
1227 pub fn revocation_failed(message: impl Into<String>) -> Self {
1228 Self::RevocationFailed {
1229 message: message.into(),
1230 }
1231 }
1232}
1233
1234impl PermissionError {
1235 /// Create a new access denied error.
1236 ///
1237 /// # Example
1238 ///
1239 /// ```rust
1240 /// use auth_framework::errors::PermissionError;
1241 ///
1242 /// let err = PermissionError::access_denied("write", "documents");
1243 /// assert!(err.to_string().contains("write"));
1244 /// ```
1245 pub fn access_denied(permission: impl Into<String>, resource: impl Into<String>) -> Self {
1246 Self::AccessDenied {
1247 permission: permission.into(),
1248 resource: resource.into(),
1249 }
1250 }
1251
1252 /// Create a new role not found error.
1253 ///
1254 /// # Example
1255 ///
1256 /// ```rust
1257 /// use auth_framework::errors::PermissionError;
1258 ///
1259 /// let err = PermissionError::role_not_found("superadmin");
1260 /// assert!(err.to_string().contains("superadmin"));
1261 /// ```
1262 pub fn role_not_found(role: impl Into<String>) -> Self {
1263 Self::RoleNotFound { role: role.into() }
1264 }
1265
1266 /// Create a new permission not found error.
1267 ///
1268 /// # Example
1269 ///
1270 /// ```rust
1271 /// use auth_framework::errors::PermissionError;
1272 ///
1273 /// let err = PermissionError::permission_not_found("delete_user");
1274 /// assert!(err.to_string().contains("delete_user"));
1275 /// ```
1276 pub fn permission_not_found(permission: impl Into<String>) -> Self {
1277 Self::PermissionNotFound {
1278 permission: permission.into(),
1279 }
1280 }
1281
1282 /// Create a new invalid format error.
1283 ///
1284 /// # Example
1285 ///
1286 /// ```rust
1287 /// use auth_framework::errors::PermissionError;
1288 ///
1289 /// let err = PermissionError::invalid_format("missing resource:action separator");
1290 /// assert!(err.to_string().contains("separator"));
1291 /// ```
1292 pub fn invalid_format(message: impl Into<String>) -> Self {
1293 Self::InvalidFormat {
1294 message: message.into(),
1295 }
1296 }
1297}
1298
1299impl StorageError {
1300 /// Create a new connection failed error.
1301 ///
1302 /// # Example
1303 ///
1304 /// ```rust
1305 /// use auth_framework::errors::StorageError;
1306 ///
1307 /// let err = StorageError::connection_failed("connection refused");
1308 /// assert!(err.to_string().contains("connection refused"));
1309 /// ```
1310 pub fn connection_failed(message: impl Into<String>) -> Self {
1311 Self::ConnectionFailed {
1312 message: message.into(),
1313 }
1314 }
1315
1316 /// Create a new operation failed error.
1317 ///
1318 /// # Example
1319 ///
1320 /// ```rust
1321 /// use auth_framework::errors::StorageError;
1322 ///
1323 /// let err = StorageError::operation_failed("table not found");
1324 /// assert!(err.to_string().contains("table not found"));
1325 /// ```
1326 pub fn operation_failed(message: impl Into<String>) -> Self {
1327 Self::OperationFailed {
1328 message: message.into(),
1329 }
1330 }
1331
1332 /// Create a new serialization error.
1333 ///
1334 /// # Example
1335 ///
1336 /// ```rust
1337 /// use auth_framework::errors::StorageError;
1338 ///
1339 /// let err = StorageError::serialization("invalid JSON");
1340 /// assert!(err.to_string().contains("invalid JSON"));
1341 /// ```
1342 pub fn serialization(message: impl Into<String>) -> Self {
1343 Self::Serialization {
1344 message: message.into(),
1345 }
1346 }
1347}
1348
1349impl MfaError {
1350 /// Create a new method not supported error.
1351 ///
1352 /// # Example
1353 ///
1354 /// ```rust
1355 /// use auth_framework::errors::MfaError;
1356 ///
1357 /// let err = MfaError::method_not_supported("biometric");
1358 /// assert!(err.to_string().contains("biometric"));
1359 /// ```
1360 pub fn method_not_supported(method: impl Into<String>) -> Self {
1361 Self::MethodNotSupported {
1362 method: method.into(),
1363 }
1364 }
1365
1366 /// Create a new verification failed error.
1367 ///
1368 /// # Example
1369 ///
1370 /// ```rust
1371 /// use auth_framework::errors::MfaError;
1372 ///
1373 /// let err = MfaError::verification_failed("code expired");
1374 /// assert!(err.to_string().contains("code expired"));
1375 /// ```
1376 pub fn verification_failed(message: impl Into<String>) -> Self {
1377 Self::VerificationFailed {
1378 message: message.into(),
1379 }
1380 }
1381}
1382
1383// Actix-web ResponseError implementation
1384#[cfg(feature = "actix-web")]
1385impl actix_web::ResponseError for AuthError {
1386 fn error_response(&self) -> actix_web::HttpResponse {
1387 match self {
1388 AuthError::Token(_) => {
1389 actix_web::HttpResponse::Unauthorized().json(serde_json::json!({
1390 "error": "invalid_token",
1391 "error_description": self.to_string()
1392 }))
1393 }
1394 AuthError::Permission(_) => {
1395 actix_web::HttpResponse::Forbidden().json(serde_json::json!({
1396 "error": "insufficient_permissions",
1397 "error_description": self.to_string()
1398 }))
1399 }
1400 AuthError::RateLimit { .. } => {
1401 actix_web::HttpResponse::TooManyRequests().json(serde_json::json!({
1402 "error": "rate_limit_exceeded",
1403 "error_description": self.to_string()
1404 }))
1405 }
1406 AuthError::Configuration { .. }
1407 | AuthError::Storage(_)
1408 | AuthError::Internal { .. } => {
1409 actix_web::HttpResponse::InternalServerError().json(serde_json::json!({
1410 "error": "internal_error",
1411 "error_description": "An internal error occurred"
1412 }))
1413 }
1414 _ => actix_web::HttpResponse::BadRequest().json(serde_json::json!({
1415 "error": "bad_request",
1416 "error_description": self.to_string()
1417 })),
1418 }
1419 }
1420
1421 fn status_code(&self) -> actix_web::http::StatusCode {
1422 match self {
1423 AuthError::Token(_) => actix_web::http::StatusCode::UNAUTHORIZED,
1424 AuthError::Permission(_) => actix_web::http::StatusCode::FORBIDDEN,
1425 AuthError::RateLimit { .. } => actix_web::http::StatusCode::TOO_MANY_REQUESTS,
1426 AuthError::Internal { .. }
1427 | AuthError::Configuration { .. }
1428 | AuthError::Storage(_) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
1429 _ => actix_web::http::StatusCode::BAD_REQUEST,
1430 }
1431 }
1432}
1433
1434// Additional From implementations for admin tools
1435impl From<Box<dyn std::error::Error + Send + Sync>> for AuthError {
1436 fn from(error: Box<dyn std::error::Error + Send + Sync>) -> Self {
1437 AuthError::Cli(format!("Admin tool error: {}", error))
1438 }
1439}
1440
1441impl From<Box<dyn std::error::Error>> for AuthError {
1442 fn from(error: Box<dyn std::error::Error>) -> Self {
1443 AuthError::Cli(format!("Admin tool error: {}", error))
1444 }
1445}
1446
1447#[cfg(test)]
1448mod tests {
1449 use super::*;
1450 use std::error::Error;
1451
1452 #[test]
1453 fn test_auth_error_creation() {
1454 let token_error = AuthError::token("Invalid JWT signature");
1455 assert!(matches!(token_error, AuthError::Token(_)));
1456 assert!(token_error.to_string().contains("Invalid JWT signature"));
1457
1458 let permission_error = AuthError::access_denied("Access denied");
1459 assert!(matches!(permission_error, AuthError::Permission(_)));
1460 assert!(permission_error.to_string().contains("Access denied"));
1461
1462 let config_error = AuthError::config("Database connection failed");
1463 assert!(matches!(config_error, AuthError::Configuration { .. }));
1464 assert!(
1465 config_error
1466 .to_string()
1467 .contains("Database connection failed")
1468 );
1469 }
1470
1471 #[test]
1472 fn test_auth_error_categorization() {
1473 // Test various error types maintain their category
1474 let errors = vec![
1475 (AuthError::token("test"), "Token"),
1476 (AuthError::access_denied("test"), "Permission"),
1477 (AuthError::config("test"), "Configuration"),
1478 (AuthError::crypto("test"), "Crypto"),
1479 (AuthError::validation("test"), "Validation"),
1480 ];
1481
1482 for (error, expected_category) in errors {
1483 let error_string = format!("{:?}", error);
1484 assert!(
1485 error_string.contains(expected_category),
1486 "Error {:?} should contain category {}",
1487 error,
1488 expected_category
1489 );
1490 }
1491 }
1492
1493 #[test]
1494 fn test_rate_limit_error() {
1495 let rate_limit_error = AuthError::rate_limit("Too many requests");
1496
1497 match rate_limit_error {
1498 AuthError::RateLimit { message } => {
1499 assert_eq!(message, "Too many requests");
1500 }
1501 _ => panic!("Expected RateLimit error"),
1502 }
1503 }
1504
1505 #[test]
1506 fn test_validation_error() {
1507 let validation_error = AuthError::validation("username must not be empty");
1508
1509 match validation_error {
1510 AuthError::Validation { message } => {
1511 assert_eq!(message, "username must not be empty");
1512 }
1513 _ => panic!("Expected Validation error"),
1514 }
1515 }
1516
1517 #[test]
1518 fn test_configuration_error() {
1519 let config_error = AuthError::config("jwt_secret is required");
1520
1521 match config_error {
1522 AuthError::Configuration { message, .. } => {
1523 assert_eq!(message, "jwt_secret is required");
1524 }
1525 _ => panic!("Expected Configuration error"),
1526 }
1527 }
1528
1529 #[test]
1530 fn test_error_chain() {
1531 let root_cause = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
1532 let auth_error = AuthError::internal(format!("Config file error: {}", root_cause));
1533
1534 // Test that error information is preserved
1535 assert!(auth_error.to_string().contains("File not found"));
1536 assert!(auth_error.to_string().contains("Config file error"));
1537 }
1538
1539 #[test]
1540 fn test_error_source() {
1541 let token_error = AuthError::token("JWT parsing failed");
1542
1543 // AuthError implements Error trait
1544 // Token error wraps TokenError, so it should have a source
1545 assert!(token_error.source().is_some());
1546
1547 // Test error display
1548 let error_msg = format!("{}", token_error);
1549 assert!(error_msg.contains("JWT parsing failed"));
1550 }
1551
1552 #[test]
1553 fn test_from_conversions() {
1554 // Test conversion from std::io::Error
1555 let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied");
1556 let auth_error: AuthError = io_error.into();
1557 assert!(matches!(auth_error, AuthError::Io(_)));
1558
1559 // Test conversion from serde_json::Error
1560 let json_str = r#"{"invalid": json"#;
1561 let json_error: serde_json::Error =
1562 serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
1563 let auth_error: AuthError = json_error.into();
1564 assert!(matches!(auth_error, AuthError::Json(_)));
1565 }
1566
1567 #[test]
1568 fn test_error_equality() {
1569 let error1 = AuthError::token("Same message");
1570 let error2 = AuthError::token("Same message");
1571 let error3 = AuthError::token("Different message");
1572
1573 // Test Debug representation for consistency
1574 assert_eq!(format!("{:?}", error1), format!("{:?}", error2));
1575 assert_ne!(format!("{:?}", error1), format!("{:?}", error3));
1576 }
1577
1578 #[test]
1579 fn test_actix_web_integration() {
1580 #[cfg(feature = "actix-web")]
1581 {
1582 use actix_web::ResponseError;
1583
1584 // Test status codes
1585 assert_eq!(
1586 AuthError::token("test").status_code(),
1587 actix_web::http::StatusCode::UNAUTHORIZED
1588 );
1589 assert_eq!(
1590 AuthError::access_denied("test").status_code(),
1591 actix_web::http::StatusCode::FORBIDDEN
1592 );
1593 assert_eq!(
1594 AuthError::rate_limit("test").status_code(),
1595 actix_web::http::StatusCode::TOO_MANY_REQUESTS
1596 );
1597 assert_eq!(
1598 AuthError::internal("test").status_code(),
1599 actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
1600 );
1601 }
1602 }
1603
1604 #[test]
1605 fn test_error_message_safety() {
1606 // Ensure error messages don't leak sensitive information
1607 let sensitive_data = "password123";
1608 let safe_error = AuthError::token("Invalid credentials");
1609
1610 // Error message should not contain sensitive data
1611 assert!(!safe_error.to_string().contains(sensitive_data));
1612
1613 // Test that we can create errors without exposing internals
1614 let config_error = AuthError::config("connection failed");
1615 assert!(!config_error.to_string().contains("password"));
1616 assert!(!config_error.to_string().contains("secret"));
1617 }
1618
1619 #[test]
1620 fn test_cli_error_conversion() {
1621 let boxed_error: Box<dyn std::error::Error + Send + Sync> = "CLI operation failed".into();
1622 let auth_error: AuthError = boxed_error.into();
1623
1624 assert!(matches!(auth_error, AuthError::Cli(_)));
1625 assert!(auth_error.to_string().contains("CLI operation failed"));
1626 }
1627
1628 #[test]
1629 fn test_error_variants_coverage() {
1630 // Ensure all error variants can be created and have proper messages
1631 let test_errors = vec![
1632 AuthError::token("token error"),
1633 AuthError::access_denied("permission error"),
1634 AuthError::internal("internal error"),
1635 AuthError::crypto("crypto error"),
1636 AuthError::Cli("cli error".to_string()),
1637 AuthError::validation("validation error"),
1638 AuthError::config("config error"),
1639 AuthError::rate_limit("rate limit error"),
1640 ];
1641
1642 for error in test_errors {
1643 // All errors should have non-empty messages
1644 assert!(
1645 !error.to_string().is_empty(),
1646 "Error should have message: {:?}",
1647 error
1648 );
1649
1650 // All errors should implement Debug
1651 let debug_repr = format!("{:?}", error);
1652 assert!(
1653 !debug_repr.is_empty(),
1654 "Error should have debug representation: {:?}",
1655 error
1656 );
1657 }
1658 }
1659
1660 #[test]
1661 fn test_oauth_specific_errors() {
1662 // Test OAuth-specific error creation using auth_method
1663 let invalid_client = AuthError::auth_method("oauth", "Client authentication failed");
1664 assert!(
1665 invalid_client
1666 .to_string()
1667 .contains("Client authentication failed")
1668 );
1669
1670 let invalid_grant = AuthError::auth_method("oauth", "Authorization code expired");
1671 assert!(
1672 invalid_grant
1673 .to_string()
1674 .contains("Authorization code expired")
1675 );
1676 }
1677
1678 #[test]
1679 fn test_error_context_preservation() {
1680 // Test that errors maintain context through transformations
1681 let original_msg = "Original error message";
1682 let context_msg = "Additional context";
1683
1684 let base_error = AuthError::internal(original_msg);
1685 let contextual_error = AuthError::internal(format!("{}: {}", context_msg, base_error));
1686
1687 assert!(contextual_error.to_string().contains(original_msg));
1688 assert!(contextual_error.to_string().contains(context_msg));
1689 }
1690
1691 #[test]
1692 fn test_error_serialization() {
1693 // Test that errors can be converted to JSON for API responses
1694 let error = AuthError::validation("email invalid format");
1695
1696 // Should be able to include in structured responses
1697 let error_response = serde_json::json!({
1698 "error": "validation_failed",
1699 "message": error.to_string(),
1700 "field": "email"
1701 });
1702
1703 assert!(
1704 error_response["message"]
1705 .as_str()
1706 .unwrap()
1707 .contains("invalid format")
1708 );
1709 }
1710
1711 #[test]
1712 fn test_concurrent_error_creation() {
1713 use std::thread;
1714
1715 let handles: Vec<_> = (0..10)
1716 .map(|i| {
1717 thread::spawn(move || {
1718 let error = AuthError::token(format!("Concurrent error {}", i));
1719 assert!(
1720 error
1721 .to_string()
1722 .contains(&format!("Concurrent error {}", i))
1723 );
1724 error
1725 })
1726 })
1727 .collect();
1728
1729 // Wait for all threads and verify errors
1730 for handle in handles {
1731 let error = handle.join().unwrap();
1732 assert!(!error.to_string().is_empty());
1733 }
1734 }
1735}