use thiserror::Error;
pub type Result<T, E = AuthError> = std::result::Result<T, E>;
#[derive(Error, Debug)]
pub enum AuthError {
#[error("Configuration error: {message}")]
Configuration {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
help: Option<String>,
docs_url: Option<String>,
suggested_fix: Option<String>,
},
#[error("Authentication method '{method}' error: {message}")]
AuthMethod {
method: String,
message: String,
help: Option<String>,
docs_url: Option<String>,
suggested_fix: Option<String>,
},
#[error("Token error: {0}")]
Token(#[from] TokenError),
#[error("Permission error: {0}")]
Permission(#[from] PermissionError),
#[error("Storage error: {0}")]
Storage(#[from] StorageError),
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("JWT error: {0}")]
Jwt(#[from] jsonwebtoken::errors::Error),
#[error("YAML error: {0}")]
Yaml(#[from] serde_yaml::Error),
#[error("TOML error: {0}")]
Toml(#[from] toml::ser::Error),
#[cfg(feature = "prometheus")]
#[error("Metrics error: {0}")]
Metrics(#[from] prometheus::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("CLI error: {0}")]
Cli(String),
#[error("System time error: {0}")]
SystemTime(#[from] std::time::SystemTimeError),
#[error("Rate limit exceeded: {message}")]
RateLimit { message: String },
#[error(
"Too many concurrent sessions for user (limit reached). \
Revoke an existing session before creating a new one."
)]
TooManyConcurrentSessions,
#[error("MFA error: {0}")]
Mfa(#[from] MfaError),
#[error("Device flow error: {0}")]
DeviceFlow(#[from] DeviceFlowError),
#[error("OAuth provider error: {0}")]
OAuthProvider(#[from] OAuthProviderError),
#[error("Password verification failed: {0}")]
PasswordVerification(String),
#[error("Password hashing failed: {0}")]
PasswordHashing(String),
#[error("User not found. The requested user ID does not exist in the store.")]
UserNotFound,
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Hardware token error: {0}")]
HardwareToken(String),
#[error("Backup code verification failed: {0}")]
BackupCodeVerification(String),
#[error("Backup code hashing failed: {0}")]
BackupCodeHashing(String),
#[error("Invalid secret format")]
InvalidSecret,
#[error("User profile error: {message}")]
UserProfile { message: String },
#[error("Invalid credential: {credential_type} - {message}")]
InvalidCredential {
credential_type: String,
message: String,
},
#[error("Authentication timeout after {timeout_seconds} seconds")]
Timeout { timeout_seconds: u64 },
#[error(
"Provider '{provider}' is not configured or supported. \
Add it via AuthConfig::method_config() or check available providers."
)]
ProviderNotConfigured { provider: String },
#[error("Cryptography error: {message}")]
Crypto { message: String },
#[error("Validation error: {message}")]
Validation { message: String },
#[error("Internal error: {message}")]
Internal { message: String },
#[error("Invalid request: {0}")]
InvalidRequest(String),
#[error(
"Step-up authentication required: current level '{current_level}', required level '{required_level}'"
)]
StepUpRequired {
current_level: String,
required_level: String,
step_up_url: String,
},
#[error("Session error: {0}")]
SessionError(String),
#[error("Unauthorized: {0}")]
Unauthorized(String),
#[error("Token generation error: {0}")]
TokenGeneration(String),
#[deprecated(
since = "0.5.0",
note = "Use `AuthError::token(msg)` instead — it routes through the structured TokenError hierarchy"
)]
#[error("Invalid token: {0}")]
InvalidToken(String),
#[error("Unsupported provider: {0}")]
UnsupportedProvider(String),
#[deprecated(
since = "0.5.0",
note = "Use `AuthError::internal(msg)` or let `reqwest::Error` convert via `AuthError::Network` instead"
)]
#[error("Network error: {0}")]
NetworkError(String),
#[deprecated(
since = "0.5.0",
note = "Use `AuthError::internal(msg)` or let serde errors convert via `AuthError::Json` instead"
)]
#[error("Parse error: {0}")]
ParseError(String),
#[deprecated(
since = "0.5.0",
note = "Use `AuthError::config(msg)` instead — it provides richer context fields"
)]
#[error("Configuration error: {0}")]
ConfigurationError(String),
}
#[derive(Error, Debug)]
pub enum TokenError {
#[error("Token has expired")]
Expired,
#[error("Token is invalid: {message}")]
Invalid { message: String },
#[error("Token not found")]
NotFound,
#[error("Token is missing")]
Missing,
#[error("Token creation failed: {message}")]
CreationFailed { message: String },
#[error("Token refresh failed: {message}")]
RefreshFailed { message: String },
#[error("Token revocation failed: {message}")]
RevocationFailed { message: String },
}
#[derive(Error, Debug)]
pub enum PermissionError {
#[error("Access denied: missing permission '{permission}' for resource '{resource}'")]
AccessDenied {
permission: String,
resource: String,
},
#[error("Role '{role}' not found")]
RoleNotFound { role: String },
#[error("Permission '{permission}' not found")]
PermissionNotFound { permission: String },
#[error("Invalid permission format: {message}")]
InvalidFormat { message: String },
#[error("Permission denied: {message}")]
Denied {
action: String,
resource: String,
message: String,
},
}
#[derive(Error, Debug)]
pub enum StorageError {
#[error("Connection failed: {message}")]
ConnectionFailed { message: String },
#[error("Operation failed: {message}")]
OperationFailed { message: String },
#[error("Serialization error: {message}")]
Serialization { message: String },
#[error("Storage backend not available")]
BackendUnavailable,
}
#[derive(Error, Debug)]
pub enum MfaError {
#[error("MFA challenge expired")]
ChallengeExpired,
#[error("Invalid MFA code")]
InvalidCode,
#[error("MFA method not supported: {method}")]
MethodNotSupported { method: String },
#[error("MFA setup required")]
SetupRequired,
#[error("MFA verification failed: {message}")]
VerificationFailed { message: String },
}
#[derive(Error, Debug)]
pub enum DeviceFlowError {
#[error("Authorization pending - user has not yet completed authorization")]
AuthorizationPending,
#[error("Slow down - polling too frequently")]
SlowDown,
#[error("Device code expired")]
ExpiredToken,
#[error("Access denied by user")]
AccessDenied,
#[error("Invalid device code")]
InvalidDeviceCode,
#[error("Unsupported grant type")]
UnsupportedGrantType,
}
#[derive(Error, Debug)]
pub enum OAuthProviderError {
#[error("Invalid authorization code")]
InvalidAuthorizationCode,
#[error("Invalid redirect URI")]
InvalidRedirectUri,
#[error("Invalid client credentials")]
InvalidClientCredentials,
#[error("Insufficient scope: required '{required}', granted '{granted}'")]
InsufficientScope { required: String, granted: String },
#[error("Provider '{provider}' does not support '{feature}'")]
UnsupportedFeature { provider: String, feature: String },
#[error("Rate limited by provider: {message}")]
RateLimited { message: String },
}
impl AuthError {
pub fn config(message: impl Into<String>) -> Self {
Self::Configuration {
message: message.into(),
source: None,
help: None,
docs_url: None,
suggested_fix: None,
}
}
pub fn config_with_help(
message: impl Into<String>,
help: impl Into<String>,
suggested_fix: Option<String>,
) -> Self {
Self::Configuration {
message: message.into(),
source: None,
help: Some(help.into()),
docs_url: Some(
"https://docs.rs/auth-framework/latest/auth_framework/config/".to_string(),
),
suggested_fix,
}
}
pub fn jwt_secret_too_short(current_length: usize) -> Self {
Self::Configuration {
message: format!(
"JWT secret too short (got {} characters, need 32+ for security)",
current_length
),
source: None,
help: Some("Use a cryptographically secure random string of at least 32 characters".to_string()),
docs_url: Some("https://docs.rs/auth-framework/latest/auth_framework/config/struct.SecurityConfig.html".to_string()),
suggested_fix: Some("Generate a secure secret: `openssl rand -hex 32`".to_string()),
}
}
pub fn production_memory_storage() -> Self {
Self::Configuration {
message: "Memory storage is not suitable for production environments".to_string(),
source: None,
help: Some("Use a persistent storage backend like PostgreSQL or Redis".to_string()),
docs_url: Some("https://docs.rs/auth-framework/latest/auth_framework/storage/".to_string()),
suggested_fix: Some("Configure PostgreSQL: .with_postgres(\"postgresql://...\") or Redis: .with_redis(\"redis://...\")".to_string()),
}
}
pub fn auth_method(method: impl Into<String>, message: impl Into<String>) -> Self {
Self::AuthMethod {
method: method.into(),
message: message.into(),
help: None,
docs_url: None,
suggested_fix: None,
}
}
pub fn auth_method_with_help(
method: impl Into<String>,
message: impl Into<String>,
help: impl Into<String>,
suggested_fix: Option<String>,
) -> Self {
Self::AuthMethod {
method: method.into(),
message: message.into(),
help: Some(help.into()),
docs_url: Some(
"https://docs.rs/auth-framework/latest/auth_framework/methods/".to_string(),
),
suggested_fix,
}
}
pub fn rate_limit(message: impl Into<String>) -> Self {
Self::RateLimit {
message: message.into(),
}
}
pub fn crypto(message: impl Into<String>) -> Self {
Self::Crypto {
message: message.into(),
}
}
pub fn validation(message: impl Into<String>) -> Self {
Self::Validation {
message: message.into(),
}
}
pub fn internal(message: impl Into<String>) -> Self {
Self::Internal {
message: message.into(),
}
}
pub fn authorization(message: impl Into<String>) -> Self {
Self::Permission(PermissionError::Denied {
action: "authorize".to_string(),
resource: "resource".to_string(),
message: message.into(),
})
}
pub fn access_denied(message: impl Into<String>) -> Self {
Self::Permission(PermissionError::Denied {
action: "access".to_string(),
resource: "resource".to_string(),
message: message.into(),
})
}
pub fn token(message: impl Into<String>) -> Self {
Self::Token(TokenError::Invalid {
message: message.into(),
})
}
pub fn device_flow(error: DeviceFlowError) -> Self {
Self::DeviceFlow(error)
}
pub fn oauth_provider(error: OAuthProviderError) -> Self {
Self::OAuthProvider(error)
}
pub fn user_profile(message: impl Into<String>) -> Self {
Self::UserProfile {
message: message.into(),
}
}
pub fn invalid_credential(
credential_type: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self::InvalidCredential {
credential_type: credential_type.into(),
message: message.into(),
}
}
pub fn timeout(timeout_seconds: u64) -> Self {
Self::Timeout { timeout_seconds }
}
pub fn provider_not_configured(provider: impl Into<String>) -> Self {
Self::ProviderNotConfigured {
provider: provider.into(),
}
}
pub fn rate_limited(message: impl Into<String>) -> Self {
Self::RateLimit {
message: message.into(),
}
}
pub fn configuration(message: impl Into<String>) -> Self {
Self::Configuration {
message: message.into(),
source: None,
help: None,
docs_url: None,
suggested_fix: None,
}
}
pub fn http_status_code(&self) -> u16 {
match self {
Self::InvalidInput(_)
| Self::Validation { .. }
| Self::InvalidCredential { .. }
| Self::HardwareToken(_)
| Self::InvalidRequest(_)
| Self::InvalidSecret => 400,
Self::Token(_)
| Self::AuthMethod { .. }
| Self::Jwt(_)
| Self::Unauthorized(_)
| Self::PasswordVerification(_) => 401,
Self::Permission(_)
| Self::StepUpRequired { .. } => 403,
Self::UserNotFound
| Self::ProviderNotConfigured { .. } => 404,
Self::Timeout { .. } => 408,
Self::RateLimit { .. }
| Self::TooManyConcurrentSessions => 429,
Self::OAuthProvider(_)
| Self::Network(_) => 502,
Self::Storage(_) => 503,
Self::Configuration { .. }
| Self::Crypto { .. }
| Self::Internal { .. }
| Self::Json(_)
| Self::Yaml(_)
| Self::Toml(_)
| Self::Io(_)
| Self::Mfa(_)
| Self::DeviceFlow(_)
| Self::TokenGeneration(_)
| Self::SessionError(_)
| Self::UserProfile { .. }
| Self::Cli(_)
| Self::SystemTime(_)
| Self::PasswordHashing(_)
| Self::BackupCodeVerification(_)
| Self::BackupCodeHashing(_)
| Self::UnsupportedProvider(_) => 500,
#[cfg(feature = "prometheus")]
Self::Metrics(_) => 500,
#[allow(deprecated)]
Self::InvalidToken(_)
| Self::NetworkError(_)
| Self::ParseError(_)
| Self::ConfigurationError(_) => 500,
}
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
Self::RateLimit { .. }
| Self::Timeout { .. }
| Self::TooManyConcurrentSessions
| Self::Network(_)
| Self::Storage(StorageError::ConnectionFailed { .. })
)
}
pub fn error_code(&self) -> &'static str {
match self {
Self::Configuration { .. } => "configuration",
Self::AuthMethod { .. } => "auth_method",
Self::Token(_) => "invalid_token",
Self::Permission(_) => "insufficient_permissions",
Self::Storage(_) => "storage",
Self::Network(_) => "network",
Self::Json(_) | Self::Yaml(_) | Self::Toml(_) => "serialization",
Self::Jwt(_) => "jwt",
Self::Io(_) => "io",
Self::RateLimit { .. } => "rate_limit",
Self::TooManyConcurrentSessions => "concurrent_sessions",
Self::Mfa(_) => "mfa",
Self::DeviceFlow(_) => "device_flow",
Self::OAuthProvider(_) => "oauth_provider",
Self::PasswordVerification(_) => "password_verification",
Self::UserNotFound => "user_not_found",
Self::InvalidInput(_) => "invalid_input",
Self::HardwareToken(_) => "hardware_token",
Self::InvalidCredential { .. } => "invalid_credential",
Self::Timeout { .. } => "timeout",
Self::Crypto { .. } => "crypto",
Self::Validation { .. } => "validation",
Self::Internal { .. } => "internal",
Self::StepUpRequired { .. } => "step_up_required",
Self::SessionError(_) => "session",
Self::Unauthorized(_) => "unauthorized",
Self::TokenGeneration(_) => "token_generation",
Self::UserProfile { .. } => "user_profile",
Self::ProviderNotConfigured { .. } => "provider_not_configured",
Self::Cli(_) => "cli",
Self::SystemTime(_) => "internal",
Self::PasswordHashing(_) => "password_hashing",
Self::BackupCodeVerification(_) => "backup_code",
Self::BackupCodeHashing(_) => "backup_code",
Self::InvalidSecret => "invalid_secret",
Self::InvalidRequest(_) => "invalid_request",
Self::UnsupportedProvider(_) => "unsupported_provider",
#[cfg(feature = "prometheus")]
Self::Metrics(_) => "metrics",
#[allow(deprecated)]
Self::InvalidToken(_) => "invalid_token",
#[allow(deprecated)]
Self::NetworkError(_) => "network",
#[allow(deprecated)]
Self::ParseError(_) => "parse",
#[allow(deprecated)]
Self::ConfigurationError(_) => "configuration",
}
}
pub fn is_client_error(&self) -> bool {
(400..500).contains(&self.http_status_code())
}
pub fn is_server_error(&self) -> bool {
self.http_status_code() >= 500
}
}
impl TokenError {
pub fn creation_failed(message: impl Into<String>) -> Self {
Self::CreationFailed {
message: message.into(),
}
}
pub fn refresh_failed(message: impl Into<String>) -> Self {
Self::RefreshFailed {
message: message.into(),
}
}
pub fn revocation_failed(message: impl Into<String>) -> Self {
Self::RevocationFailed {
message: message.into(),
}
}
}
impl PermissionError {
pub fn access_denied(permission: impl Into<String>, resource: impl Into<String>) -> Self {
Self::AccessDenied {
permission: permission.into(),
resource: resource.into(),
}
}
pub fn role_not_found(role: impl Into<String>) -> Self {
Self::RoleNotFound { role: role.into() }
}
pub fn permission_not_found(permission: impl Into<String>) -> Self {
Self::PermissionNotFound {
permission: permission.into(),
}
}
pub fn invalid_format(message: impl Into<String>) -> Self {
Self::InvalidFormat {
message: message.into(),
}
}
}
impl StorageError {
pub fn connection_failed(message: impl Into<String>) -> Self {
Self::ConnectionFailed {
message: message.into(),
}
}
pub fn operation_failed(message: impl Into<String>) -> Self {
Self::OperationFailed {
message: message.into(),
}
}
pub fn serialization(message: impl Into<String>) -> Self {
Self::Serialization {
message: message.into(),
}
}
}
impl MfaError {
pub fn method_not_supported(method: impl Into<String>) -> Self {
Self::MethodNotSupported {
method: method.into(),
}
}
pub fn verification_failed(message: impl Into<String>) -> Self {
Self::VerificationFailed {
message: message.into(),
}
}
}
#[cfg(feature = "actix-web")]
impl actix_web::ResponseError for AuthError {
fn error_response(&self) -> actix_web::HttpResponse {
match self {
AuthError::Token(_) => {
actix_web::HttpResponse::Unauthorized().json(serde_json::json!({
"error": "invalid_token",
"error_description": self.to_string()
}))
}
AuthError::Permission(_) => {
actix_web::HttpResponse::Forbidden().json(serde_json::json!({
"error": "insufficient_permissions",
"error_description": self.to_string()
}))
}
AuthError::RateLimit { .. } => {
actix_web::HttpResponse::TooManyRequests().json(serde_json::json!({
"error": "rate_limit_exceeded",
"error_description": self.to_string()
}))
}
AuthError::Configuration { .. }
| AuthError::Storage(_)
| AuthError::Internal { .. } => {
actix_web::HttpResponse::InternalServerError().json(serde_json::json!({
"error": "internal_error",
"error_description": "An internal error occurred"
}))
}
_ => actix_web::HttpResponse::BadRequest().json(serde_json::json!({
"error": "bad_request",
"error_description": self.to_string()
})),
}
}
fn status_code(&self) -> actix_web::http::StatusCode {
match self {
AuthError::Token(_) => actix_web::http::StatusCode::UNAUTHORIZED,
AuthError::Permission(_) => actix_web::http::StatusCode::FORBIDDEN,
AuthError::RateLimit { .. } => actix_web::http::StatusCode::TOO_MANY_REQUESTS,
AuthError::Internal { .. }
| AuthError::Configuration { .. }
| AuthError::Storage(_) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
_ => actix_web::http::StatusCode::BAD_REQUEST,
}
}
}
impl From<Box<dyn std::error::Error + Send + Sync>> for AuthError {
fn from(error: Box<dyn std::error::Error + Send + Sync>) -> Self {
AuthError::Cli(format!("Admin tool error: {}", error))
}
}
impl From<Box<dyn std::error::Error>> for AuthError {
fn from(error: Box<dyn std::error::Error>) -> Self {
AuthError::Cli(format!("Admin tool error: {}", error))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
#[test]
fn test_auth_error_creation() {
let token_error = AuthError::token("Invalid JWT signature");
assert!(matches!(token_error, AuthError::Token(_)));
assert!(token_error.to_string().contains("Invalid JWT signature"));
let permission_error = AuthError::access_denied("Access denied");
assert!(matches!(permission_error, AuthError::Permission(_)));
assert!(permission_error.to_string().contains("Access denied"));
let config_error = AuthError::config("Database connection failed");
assert!(matches!(config_error, AuthError::Configuration { .. }));
assert!(
config_error
.to_string()
.contains("Database connection failed")
);
}
#[test]
fn test_auth_error_categorization() {
let errors = vec![
(AuthError::token("test"), "Token"),
(AuthError::access_denied("test"), "Permission"),
(AuthError::config("test"), "Configuration"),
(AuthError::crypto("test"), "Crypto"),
(AuthError::validation("test"), "Validation"),
];
for (error, expected_category) in errors {
let error_string = format!("{:?}", error);
assert!(
error_string.contains(expected_category),
"Error {:?} should contain category {}",
error,
expected_category
);
}
}
#[test]
fn test_rate_limit_error() {
let rate_limit_error = AuthError::rate_limit("Too many requests");
match rate_limit_error {
AuthError::RateLimit { message } => {
assert_eq!(message, "Too many requests");
}
_ => panic!("Expected RateLimit error"),
}
}
#[test]
fn test_validation_error() {
let validation_error = AuthError::validation("username must not be empty");
match validation_error {
AuthError::Validation { message } => {
assert_eq!(message, "username must not be empty");
}
_ => panic!("Expected Validation error"),
}
}
#[test]
fn test_configuration_error() {
let config_error = AuthError::config("jwt_secret is required");
match config_error {
AuthError::Configuration { message, .. } => {
assert_eq!(message, "jwt_secret is required");
}
_ => panic!("Expected Configuration error"),
}
}
#[test]
fn test_error_chain() {
let root_cause = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
let auth_error = AuthError::internal(format!("Config file error: {}", root_cause));
assert!(auth_error.to_string().contains("File not found"));
assert!(auth_error.to_string().contains("Config file error"));
}
#[test]
fn test_error_source() {
let token_error = AuthError::token("JWT parsing failed");
assert!(token_error.source().is_some());
let error_msg = format!("{}", token_error);
assert!(error_msg.contains("JWT parsing failed"));
}
#[test]
fn test_from_conversions() {
let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied");
let auth_error: AuthError = io_error.into();
assert!(matches!(auth_error, AuthError::Io(_)));
let json_str = r#"{"invalid": json"#;
let json_error: serde_json::Error =
serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
let auth_error: AuthError = json_error.into();
assert!(matches!(auth_error, AuthError::Json(_)));
}
#[test]
fn test_error_equality() {
let error1 = AuthError::token("Same message");
let error2 = AuthError::token("Same message");
let error3 = AuthError::token("Different message");
assert_eq!(format!("{:?}", error1), format!("{:?}", error2));
assert_ne!(format!("{:?}", error1), format!("{:?}", error3));
}
#[test]
fn test_actix_web_integration() {
#[cfg(feature = "actix-web")]
{
use actix_web::ResponseError;
assert_eq!(
AuthError::token("test").status_code(),
actix_web::http::StatusCode::UNAUTHORIZED
);
assert_eq!(
AuthError::access_denied("test").status_code(),
actix_web::http::StatusCode::FORBIDDEN
);
assert_eq!(
AuthError::rate_limit("test").status_code(),
actix_web::http::StatusCode::TOO_MANY_REQUESTS
);
assert_eq!(
AuthError::internal("test").status_code(),
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
);
}
}
#[test]
fn test_error_message_safety() {
let sensitive_data = "password123";
let safe_error = AuthError::token("Invalid credentials");
assert!(!safe_error.to_string().contains(sensitive_data));
let config_error = AuthError::config("connection failed");
assert!(!config_error.to_string().contains("password"));
assert!(!config_error.to_string().contains("secret"));
}
#[test]
fn test_cli_error_conversion() {
let boxed_error: Box<dyn std::error::Error + Send + Sync> = "CLI operation failed".into();
let auth_error: AuthError = boxed_error.into();
assert!(matches!(auth_error, AuthError::Cli(_)));
assert!(auth_error.to_string().contains("CLI operation failed"));
}
#[test]
fn test_error_variants_coverage() {
let test_errors = vec![
AuthError::token("token error"),
AuthError::access_denied("permission error"),
AuthError::internal("internal error"),
AuthError::crypto("crypto error"),
AuthError::Cli("cli error".to_string()),
AuthError::validation("validation error"),
AuthError::config("config error"),
AuthError::rate_limit("rate limit error"),
];
for error in test_errors {
assert!(
!error.to_string().is_empty(),
"Error should have message: {:?}",
error
);
let debug_repr = format!("{:?}", error);
assert!(
!debug_repr.is_empty(),
"Error should have debug representation: {:?}",
error
);
}
}
#[test]
fn test_oauth_specific_errors() {
let invalid_client = AuthError::auth_method("oauth", "Client authentication failed");
assert!(
invalid_client
.to_string()
.contains("Client authentication failed")
);
let invalid_grant = AuthError::auth_method("oauth", "Authorization code expired");
assert!(
invalid_grant
.to_string()
.contains("Authorization code expired")
);
}
#[test]
fn test_error_context_preservation() {
let original_msg = "Original error message";
let context_msg = "Additional context";
let base_error = AuthError::internal(original_msg);
let contextual_error = AuthError::internal(format!("{}: {}", context_msg, base_error));
assert!(contextual_error.to_string().contains(original_msg));
assert!(contextual_error.to_string().contains(context_msg));
}
#[test]
fn test_error_serialization() {
let error = AuthError::validation("email invalid format");
let error_response = serde_json::json!({
"error": "validation_failed",
"message": error.to_string(),
"field": "email"
});
assert!(
error_response["message"]
.as_str()
.unwrap()
.contains("invalid format")
);
}
#[test]
fn test_concurrent_error_creation() {
use std::thread;
let handles: Vec<_> = (0..10)
.map(|i| {
thread::spawn(move || {
let error = AuthError::token(format!("Concurrent error {}", i));
assert!(
error
.to_string()
.contains(&format!("Concurrent error {}", i))
);
error
})
})
.collect();
for handle in handles {
let error = handle.join().unwrap();
assert!(!error.to_string().is_empty());
}
}
}