719 lines
26 KiB
Rust
719 lines
26 KiB
Rust
|
|
//! # Gestion d'erreurs unifiée pour Veza Chat Server
|
||
|
|
//!
|
||
|
|
//! Ce module fournit un système d'erreurs cohérent et complet avec:
|
||
|
|
//! - Catégorisation des erreurs par domaine
|
||
|
|
//! - Codes d'erreur standardisés
|
||
|
|
//! - Logging automatique selon la gravité
|
||
|
|
//! - Sérialisation pour l'API
|
||
|
|
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
use std::fmt;
|
||
|
|
use thiserror::Error;
|
||
|
|
|
||
|
|
/// Type alias pour Result avec notre erreur personnalisée
|
||
|
|
pub type Result<T> = std::result::Result<T, ChatError>;
|
||
|
|
|
||
|
|
/// Erreurs principales du système de chat
|
||
|
|
#[derive(Error, Debug)]
|
||
|
|
pub enum ChatError {
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
// ERREURS D'AUTHENTIFICATION ET AUTORISATION
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
/// Token JWT invalide ou expiré
|
||
|
|
#[error("Token d'authentification invalide: {reason}")]
|
||
|
|
InvalidToken { reason: String },
|
||
|
|
|
||
|
|
/// Utilisateur non autorisé pour cette action
|
||
|
|
#[error("Accès refusé: {action}")]
|
||
|
|
Unauthorized { action: String },
|
||
|
|
|
||
|
|
/// Utilisateur banni ou suspendu
|
||
|
|
#[error("Compte suspendu: {reason}")]
|
||
|
|
AccountSuspended { reason: String },
|
||
|
|
|
||
|
|
/// Tentative de connexion avec des identifiants invalides
|
||
|
|
#[error("Identifiants invalides")]
|
||
|
|
InvalidCredentials,
|
||
|
|
|
||
|
|
/// Authentification à deux facteurs requise
|
||
|
|
#[error("Authentification 2FA requise")]
|
||
|
|
TwoFactorRequired,
|
||
|
|
|
||
|
|
/// Code 2FA invalide
|
||
|
|
#[error("Code d'authentification 2FA invalide")]
|
||
|
|
InvalidTwoFactorCode,
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
// ERREURS DE VALIDATION ET CONTENU
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
/// Contenu de message trop long
|
||
|
|
#[error("Message trop long: {actual} caractères (max: {max})")]
|
||
|
|
MessageTooLong { actual: usize, max: usize },
|
||
|
|
|
||
|
|
/// Contenu inapproprié détecté
|
||
|
|
#[error("Contenu inapproprié détecté: {reason}")]
|
||
|
|
InappropriateContent { reason: String },
|
||
|
|
|
||
|
|
/// Spam détecté par les filtres
|
||
|
|
#[error("Contenu identifié comme spam")]
|
||
|
|
SpamDetected,
|
||
|
|
|
||
|
|
/// Format de données invalide
|
||
|
|
#[error("Format invalide pour {field}: {reason}")]
|
||
|
|
InvalidFormat { field: String, reason: String },
|
||
|
|
|
||
|
|
/// Paramètre requis manquant
|
||
|
|
#[error("Paramètre requis manquant: {param}")]
|
||
|
|
MissingParameter { param: String },
|
||
|
|
|
||
|
|
/// Valeur hors limites acceptables
|
||
|
|
#[error("{field} hors limites: {value} (min: {min}, max: {max})")]
|
||
|
|
OutOfRange {
|
||
|
|
field: String,
|
||
|
|
value: i64,
|
||
|
|
min: i64,
|
||
|
|
max: i64,
|
||
|
|
},
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
// ERREURS DE RATE LIMITING ET QUOTA
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
/// Limite de taux dépassée
|
||
|
|
#[error("Limite de taux dépassée pour {action}: {current}/{limit} dans {window}s")]
|
||
|
|
RateLimitExceeded {
|
||
|
|
action: String,
|
||
|
|
current: u32,
|
||
|
|
limit: u32,
|
||
|
|
window: u64,
|
||
|
|
},
|
||
|
|
|
||
|
|
/// Quota utilisateur dépassé
|
||
|
|
#[error("Quota {quota_type} dépassé: {used}/{limit}")]
|
||
|
|
QuotaExceeded {
|
||
|
|
quota_type: String,
|
||
|
|
used: u64,
|
||
|
|
limit: u64,
|
||
|
|
},
|
||
|
|
|
||
|
|
/// Trop de connexions simultanées
|
||
|
|
#[error("Trop de connexions simultanées: {current}/{max}")]
|
||
|
|
TooManyConnections { current: u32, max: u32 },
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
// ERREURS RÉSEAU ET WEBSOCKET
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
/// Erreur de connexion WebSocket
|
||
|
|
#[error("Erreur WebSocket: {source}")]
|
||
|
|
WebSocket {
|
||
|
|
#[source]
|
||
|
|
source: tokio_tungstenite::tungstenite::Error,
|
||
|
|
},
|
||
|
|
|
||
|
|
/// Connexion fermée de manière inattendue
|
||
|
|
#[error("Connexion fermée: {reason}")]
|
||
|
|
ConnectionClosed { reason: String },
|
||
|
|
|
||
|
|
/// Timeout de connexion
|
||
|
|
#[error("Timeout de connexion après {seconds}s")]
|
||
|
|
ConnectionTimeout { seconds: u64 },
|
||
|
|
|
||
|
|
/// Erreur réseau générale
|
||
|
|
#[error("Erreur réseau: {message}")]
|
||
|
|
NetworkError { message: String },
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
// ERREURS DE BASE DE DONNÉES
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
/// Erreur de base de données
|
||
|
|
#[error("Erreur base de données: {operation}")]
|
||
|
|
Database {
|
||
|
|
operation: String,
|
||
|
|
#[source]
|
||
|
|
source: sqlx::Error,
|
||
|
|
},
|
||
|
|
|
||
|
|
/// Ressource non trouvée
|
||
|
|
#[error("{resource} non trouvé(e): {id}")]
|
||
|
|
NotFound { resource: String, id: String },
|
||
|
|
|
||
|
|
/// Conflit de données (ex: violation de contrainte unique)
|
||
|
|
#[error("Conflit de données: {reason}")]
|
||
|
|
Conflict { reason: String },
|
||
|
|
|
||
|
|
/// Transaction échouée
|
||
|
|
#[error("Transaction échouée: {reason}")]
|
||
|
|
TransactionFailed { reason: String },
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
// ERREURS DE CONVERSATIONS ET MESSAGES
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
/// Conversation inexistante
|
||
|
|
#[error("Conversation {id} inexistante")]
|
||
|
|
ConversationNotFound { id: String },
|
||
|
|
|
||
|
|
/// Utilisateur pas membre de la conversation
|
||
|
|
#[error("Utilisateur non membre de la conversation {conversation_id}")]
|
||
|
|
NotMember { conversation_id: String },
|
||
|
|
|
||
|
|
/// Permissions insuffisantes dans la conversation
|
||
|
|
#[error("Permissions insuffisantes pour {action} dans {conversation_id}")]
|
||
|
|
InsufficientPermissions {
|
||
|
|
action: String,
|
||
|
|
conversation_id: String,
|
||
|
|
},
|
||
|
|
|
||
|
|
/// Conversation archivée
|
||
|
|
#[error("Conversation {id} archivée")]
|
||
|
|
ConversationArchived { id: String },
|
||
|
|
|
||
|
|
/// Message non trouvé
|
||
|
|
#[error("Message {id} non trouvé")]
|
||
|
|
MessageNotFound { id: String },
|
||
|
|
|
||
|
|
/// Impossible d'éditer le message
|
||
|
|
#[error("Edition impossible: {reason}")]
|
||
|
|
EditForbidden { reason: String },
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
// ERREURS DE FICHIERS ET UPLOAD
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
/// Fichier trop volumineux
|
||
|
|
#[error("Fichier trop volumineux: {size} bytes (max: {max_size})")]
|
||
|
|
FileTooLarge { size: u64, max_size: u64 },
|
||
|
|
|
||
|
|
/// Type de fichier non autorisé
|
||
|
|
#[error("Type de fichier non autorisé: {mime_type}")]
|
||
|
|
UnsupportedFileType { mime_type: String },
|
||
|
|
|
||
|
|
/// Fichier infecté détecté
|
||
|
|
#[error("Fichier potentiellement dangereux détecté")]
|
||
|
|
MaliciousFile,
|
||
|
|
|
||
|
|
/// Erreur d'upload
|
||
|
|
#[error("Erreur upload: {reason}")]
|
||
|
|
UploadError { reason: String },
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
// ERREURS SYSTÈME ET CONFIGURATION
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
/// Erreur de configuration
|
||
|
|
#[error("Erreur configuration: {message}")]
|
||
|
|
Configuration { message: String },
|
||
|
|
|
||
|
|
/// Service indisponible
|
||
|
|
#[error("Service {service} indisponible: {reason}")]
|
||
|
|
ServiceUnavailable { service: String, reason: String },
|
||
|
|
|
||
|
|
/// Erreur de cache
|
||
|
|
#[error("Erreur cache: {operation}")]
|
||
|
|
Cache { operation: String },
|
||
|
|
|
||
|
|
/// Timeout d'arrêt du serveur
|
||
|
|
#[error("Timeout lors de l'arrêt du serveur")]
|
||
|
|
ShutdownTimeout,
|
||
|
|
|
||
|
|
/// Erreur interne non spécifiée
|
||
|
|
#[error("Erreur interne: {message}")]
|
||
|
|
Internal { message: String },
|
||
|
|
|
||
|
|
/// EventBus RabbitMQ indisponible
|
||
|
|
#[error("EventBus indisponible: {source}")]
|
||
|
|
EventBusUnavailable {
|
||
|
|
#[from]
|
||
|
|
source: crate::event_bus::EventBusUnavailableError,
|
||
|
|
},
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
// ERREURS DE PERMISSIONS ET RÉACTIONS
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
/// Permission refusée
|
||
|
|
#[error("Permission refusée: {message}")]
|
||
|
|
PermissionDenied { message: String },
|
||
|
|
|
||
|
|
/// Réaction déjà existante
|
||
|
|
#[error("Réaction déjà existante pour ce message")]
|
||
|
|
ReactionAlreadyExists,
|
||
|
|
|
||
|
|
/// Réaction non trouvée
|
||
|
|
#[error("Réaction non trouvée")]
|
||
|
|
ReactionNotFound,
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
// ERREURS DE SÉCURITÉ
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════
|
||
|
|
/// Activité suspecte détectée
|
||
|
|
#[error("Activité suspecte détectée: {reason}")]
|
||
|
|
SuspiciousActivity { reason: String },
|
||
|
|
|
||
|
|
/// IP bloquée
|
||
|
|
#[error("Adresse IP {ip} bloquée: {reason}")]
|
||
|
|
IpBlocked { ip: String, reason: String },
|
||
|
|
|
||
|
|
/// Tentative d'injection détectée
|
||
|
|
#[error("Tentative d'injection détectée")]
|
||
|
|
InjectionAttempt,
|
||
|
|
|
||
|
|
/// Validation de sécurité échouée
|
||
|
|
#[error("Validation sécurité échouée: {check}")]
|
||
|
|
SecurityValidationFailed { check: String },
|
||
|
|
|
||
|
|
/// Erreur de sérialisation JSON
|
||
|
|
#[error("Erreur JSON: {source}")]
|
||
|
|
Json {
|
||
|
|
#[source]
|
||
|
|
source: serde_json::Error,
|
||
|
|
},
|
||
|
|
|
||
|
|
/// Erreur de sérialisation générale
|
||
|
|
#[error("Erreur de sérialisation {operation}: {message}")]
|
||
|
|
Serialization { operation: String, message: String },
|
||
|
|
|
||
|
|
/// Fonctionnalité non disponible
|
||
|
|
#[error("Fonctionnalité {feature} non disponible: {reason}")]
|
||
|
|
FeatureNotAvailable { feature: String, reason: String },
|
||
|
|
|
||
|
|
/// Erreur de validation
|
||
|
|
#[error("Erreur de validation pour {field}: {reason}")]
|
||
|
|
ValidationError { field: String, reason: String },
|
||
|
|
|
||
|
|
/// Erreur de parsing
|
||
|
|
#[error("Erreur de parsing: {reason}")]
|
||
|
|
ParseError { reason: String },
|
||
|
|
|
||
|
|
/// Limite de connexions atteinte
|
||
|
|
#[error("Limite de connexions simultanées atteinte")]
|
||
|
|
ConnectionLimitReached,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ChatError {
|
||
|
|
/// Retourne le code d'erreur HTTP approprié
|
||
|
|
pub fn http_status(&self) -> u16 {
|
||
|
|
match self {
|
||
|
|
// 400 Bad Request
|
||
|
|
Self::InvalidFormat { .. }
|
||
|
|
| Self::MissingParameter { .. }
|
||
|
|
| Self::OutOfRange { .. }
|
||
|
|
| Self::MessageTooLong { .. }
|
||
|
|
| Self::FileTooLarge { .. }
|
||
|
|
| Self::UnsupportedFileType { .. } => 400,
|
||
|
|
|
||
|
|
// 401 Unauthorized
|
||
|
|
Self::InvalidToken { .. }
|
||
|
|
| Self::InvalidCredentials
|
||
|
|
| Self::TwoFactorRequired
|
||
|
|
| Self::InvalidTwoFactorCode => 401,
|
||
|
|
|
||
|
|
// 403 Forbidden
|
||
|
|
Self::Unauthorized { .. }
|
||
|
|
| Self::AccountSuspended { .. }
|
||
|
|
| Self::InsufficientPermissions { .. }
|
||
|
|
| Self::EditForbidden { .. }
|
||
|
|
| Self::IpBlocked { .. } => 403,
|
||
|
|
|
||
|
|
// 404 Not Found
|
||
|
|
Self::NotFound { .. }
|
||
|
|
| Self::ConversationNotFound { .. }
|
||
|
|
| Self::MessageNotFound { .. } => 404,
|
||
|
|
|
||
|
|
// 409 Conflict
|
||
|
|
Self::Conflict { .. } => 409,
|
||
|
|
|
||
|
|
// 413 Payload Too Large
|
||
|
|
|
||
|
|
// 422 Unprocessable Entity
|
||
|
|
Self::InappropriateContent { .. } | Self::SpamDetected | Self::MaliciousFile => 422,
|
||
|
|
|
||
|
|
// 429 Too Many Requests
|
||
|
|
Self::RateLimitExceeded { .. }
|
||
|
|
| Self::QuotaExceeded { .. }
|
||
|
|
| Self::TooManyConnections { .. } => 429,
|
||
|
|
|
||
|
|
// 500 Internal Server Error
|
||
|
|
Self::Database { .. }
|
||
|
|
| Self::Internal { .. }
|
||
|
|
| Self::Configuration { .. }
|
||
|
|
| Self::TransactionFailed { .. }
|
||
|
|
| Self::UploadError { .. }
|
||
|
|
| Self::Cache { .. } => 500,
|
||
|
|
|
||
|
|
// 503 Service Unavailable
|
||
|
|
Self::ServiceUnavailable { .. }
|
||
|
|
| Self::ShutdownTimeout
|
||
|
|
| Self::EventBusUnavailable { .. } => 503, // Added EventBusUnavailable
|
||
|
|
|
||
|
|
// 418 I'm a teapot (pour les tentatives d'injection)
|
||
|
|
Self::InjectionAttempt => 418,
|
||
|
|
|
||
|
|
// Autres erreurs -> 500
|
||
|
|
Self::Json { .. }
|
||
|
|
| Self::Serialization { .. }
|
||
|
|
| Self::FeatureNotAvailable { .. }
|
||
|
|
| Self::ConnectionLimitReached
|
||
|
|
| Self::SecurityValidationFailed { .. }
|
||
|
|
| Self::SuspiciousActivity { .. }
|
||
|
|
| Self::ConversationArchived { .. }
|
||
|
|
| Self::NetworkError { .. }
|
||
|
|
| Self::ConnectionClosed { .. }
|
||
|
|
| Self::ConnectionTimeout { .. }
|
||
|
|
| Self::WebSocket { .. }
|
||
|
|
| Self::NotMember { .. } => 500,
|
||
|
|
|
||
|
|
// Nouvelles erreurs
|
||
|
|
Self::PermissionDenied { .. } => 403,
|
||
|
|
Self::ReactionAlreadyExists => 409,
|
||
|
|
Self::ReactionNotFound => 404,
|
||
|
|
Self::ValidationError { .. } => 400,
|
||
|
|
Self::ParseError { .. } => 400,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne la sévérité de l'erreur pour les logs
|
||
|
|
pub fn severity(&self) -> ErrorSeverity {
|
||
|
|
match self {
|
||
|
|
// CRITICAL - Erreur système critique
|
||
|
|
Self::Database { .. }
|
||
|
|
| Self::ServiceUnavailable { .. }
|
||
|
|
| Self::ShutdownTimeout
|
||
|
|
| Self::SuspiciousActivity { .. }
|
||
|
|
| Self::InjectionAttempt
|
||
|
|
| Self::IpBlocked { .. }
|
||
|
|
| Self::EventBusUnavailable { .. } => ErrorSeverity::High, // Added EventBusUnavailable
|
||
|
|
// HIGH - Problème sérieux à traiter rapidement
|
||
|
|
Self::InvalidToken { .. }
|
||
|
|
| Self::AccountSuspended { .. }
|
||
|
|
| Self::InvalidFormat { .. }
|
||
|
|
| Self::MissingParameter { .. }
|
||
|
|
| Self::OutOfRange { .. }
|
||
|
|
| Self::MessageTooLong { .. }
|
||
|
|
| Self::FileTooLarge { .. }
|
||
|
|
| Self::UnsupportedFileType { .. }
|
||
|
|
| Self::TransactionFailed { .. }
|
||
|
|
| Self::UploadError { .. }
|
||
|
|
| Self::InvalidCredentials
|
||
|
|
| Self::InvalidTwoFactorCode
|
||
|
|
| Self::InappropriateContent { .. }
|
||
|
|
| Self::SpamDetected
|
||
|
|
| Self::MaliciousFile
|
||
|
|
| Self::ConversationNotFound { .. }
|
||
|
|
| Self::InsufficientPermissions { .. }
|
||
|
|
| Self::MessageNotFound { .. }
|
||
|
|
| Self::EditForbidden { .. }
|
||
|
|
| Self::Conflict { .. }
|
||
|
|
| Self::ConnectionLimitReached
|
||
|
|
| Self::SecurityValidationFailed { .. } => ErrorSeverity::Medium,
|
||
|
|
|
||
|
|
// Gravité moyenne - Erreurs qui affectent l'utilisateur
|
||
|
|
Self::RateLimitExceeded { .. }
|
||
|
|
| Self::QuotaExceeded { .. }
|
||
|
|
| Self::TooManyConnections { .. }
|
||
|
|
| Self::Unauthorized { .. }
|
||
|
|
| Self::NotFound { .. } => ErrorSeverity::Low,
|
||
|
|
|
||
|
|
// INFO - Information
|
||
|
|
Self::ConnectionClosed { .. }
|
||
|
|
| Self::TwoFactorRequired
|
||
|
|
| Self::NotMember { .. }
|
||
|
|
| Self::Json { .. }
|
||
|
|
| Self::Serialization { .. }
|
||
|
|
| Self::FeatureNotAvailable { .. }
|
||
|
|
| Self::ConversationArchived { .. }
|
||
|
|
| Self::WebSocket { .. }
|
||
|
|
| Self::NetworkError { .. }
|
||
|
|
| Self::ConnectionTimeout { .. }
|
||
|
|
| Self::Cache { .. }
|
||
|
|
| Self::Internal { .. }
|
||
|
|
| Self::Configuration { .. } => ErrorSeverity::Info,
|
||
|
|
|
||
|
|
// Nouvelles erreurs
|
||
|
|
Self::PermissionDenied { .. } => ErrorSeverity::Warning,
|
||
|
|
Self::ReactionAlreadyExists => ErrorSeverity::Info,
|
||
|
|
Self::ReactionNotFound => ErrorSeverity::Info,
|
||
|
|
Self::ValidationError { .. } => ErrorSeverity::Low,
|
||
|
|
Self::ParseError { .. } => ErrorSeverity::Low,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne un message d'erreur sécurisé pour le client
|
||
|
|
pub fn public_message(&self) -> String {
|
||
|
|
match self {
|
||
|
|
// Messages détaillés OK pour le client
|
||
|
|
Self::InvalidFormat { field, .. } => format!("Format invalide pour {}", field),
|
||
|
|
Self::MissingParameter { param } => format!("Paramètre manquant: {}", param),
|
||
|
|
Self::MessageTooLong { max, .. } => {
|
||
|
|
format!("Message trop long (max: {} caractères)", max)
|
||
|
|
}
|
||
|
|
Self::RateLimitExceeded { action, window, .. } => {
|
||
|
|
format!(
|
||
|
|
"Trop de requêtes pour {}, veuillez patienter {}s",
|
||
|
|
action, window
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Messages génériques pour éviter la divulgation d'informations
|
||
|
|
Self::Database { .. } => "Erreur temporaire, veuillez réessayer".to_string(),
|
||
|
|
Self::Internal { .. } => "Erreur interne du serveur".to_string(),
|
||
|
|
Self::Configuration { .. } => "Service temporairement indisponible".to_string(),
|
||
|
|
Self::InjectionAttempt => "Requête rejetée".to_string(),
|
||
|
|
Self::SuspiciousActivity { .. } => "Activité inhabituelle détectée".to_string(),
|
||
|
|
|
||
|
|
// Message par défaut
|
||
|
|
_ => self.to_string(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Crée une erreur de base de données avec contexte
|
||
|
|
pub fn database_error(operation: &str, source: sqlx::Error) -> Self {
|
||
|
|
Self::Database {
|
||
|
|
operation: operation.to_string(),
|
||
|
|
source,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Crée une erreur d'autorisation avec contexte
|
||
|
|
pub fn unauthorized(action: &str) -> Self {
|
||
|
|
Self::Unauthorized {
|
||
|
|
action: action.to_string(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Crée une erreur de ressource non trouvée
|
||
|
|
pub fn not_found(resource: &str, id: &str) -> Self {
|
||
|
|
Self::NotFound {
|
||
|
|
resource: resource.to_string(),
|
||
|
|
id: id.to_string(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper pour les erreurs de configuration
|
||
|
|
pub fn configuration_error(message: &str) -> Self {
|
||
|
|
Self::Configuration {
|
||
|
|
message: message.to_string(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper pour les erreurs de message trop long
|
||
|
|
pub fn message_too_long(actual: usize, max: usize) -> Self {
|
||
|
|
Self::MessageTooLong { actual, max }
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper pour les erreurs de sérialisation
|
||
|
|
pub fn serialization_error(type_name: &str, _data: &str, source: serde_json::Error) -> Self {
|
||
|
|
Self::Serialization {
|
||
|
|
operation: format!("serialize {}", type_name),
|
||
|
|
message: source.to_string(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper pour les erreurs WebSocket
|
||
|
|
pub fn websocket_error(
|
||
|
|
_operation: &str,
|
||
|
|
source: tokio_tungstenite::tungstenite::Error,
|
||
|
|
) -> Self {
|
||
|
|
Self::WebSocket { source }
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper pour les fonctionnalités non disponibles
|
||
|
|
pub fn feature_not_available(feature: &str, reason: &str) -> Self {
|
||
|
|
Self::FeatureNotAvailable {
|
||
|
|
feature: feature.to_string(),
|
||
|
|
reason: reason.to_string(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper pour convertir sqlx::Error avec une meilleure gestion
|
||
|
|
pub fn from_sqlx_error(operation: &str, error: sqlx::Error) -> Self {
|
||
|
|
Self::Database {
|
||
|
|
operation: operation.to_string(),
|
||
|
|
source: error,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper pour les erreurs JSON
|
||
|
|
pub fn from_json_error(error: serde_json::Error) -> Self {
|
||
|
|
Self::Json { source: error }
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper pour les erreurs de rate limiting avec des valeurs par défaut
|
||
|
|
pub fn rate_limit_exceeded_simple(action: &str) -> Self {
|
||
|
|
Self::RateLimitExceeded {
|
||
|
|
action: action.to_string(),
|
||
|
|
current: 0,
|
||
|
|
limit: 0,
|
||
|
|
window: 60,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper pour les erreurs d'autorisation
|
||
|
|
pub fn unauthorized_simple(action: &str) -> Self {
|
||
|
|
Self::Unauthorized {
|
||
|
|
action: action.to_string(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper pour les erreurs de contenu inapproprié
|
||
|
|
pub fn inappropriate_content_simple(reason: &str) -> Self {
|
||
|
|
Self::InappropriateContent {
|
||
|
|
reason: reason.to_string(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper pour les erreurs de validation
|
||
|
|
pub fn validation_error(reason: &str) -> Self {
|
||
|
|
Self::ValidationError {
|
||
|
|
field: "general".to_string(),
|
||
|
|
reason: reason.to_string(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper pour les erreurs de permission
|
||
|
|
pub fn permission_denied(message: &str) -> Self {
|
||
|
|
Self::PermissionDenied {
|
||
|
|
message: message.to_string(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper pour les erreurs internes
|
||
|
|
pub fn internal_error(message: String) -> Self {
|
||
|
|
// Changed to String
|
||
|
|
Self::Internal {
|
||
|
|
message, // Direct assignment
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper pour les erreurs not found avec un seul paramètre
|
||
|
|
pub fn not_found_simple(message: &str) -> Self {
|
||
|
|
Self::NotFound {
|
||
|
|
resource: "resource".to_string(),
|
||
|
|
id: message.to_string(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Niveaux de sévérité des erreurs
|
||
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||
|
|
pub enum ErrorSeverity {
|
||
|
|
Info,
|
||
|
|
Low,
|
||
|
|
Medium,
|
||
|
|
High,
|
||
|
|
Critical,
|
||
|
|
Warning,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl fmt::Display for ErrorSeverity {
|
||
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
|
match self {
|
||
|
|
Self::Info => write!(f, "INFO"),
|
||
|
|
Self::Low => write!(f, "LOW"),
|
||
|
|
Self::Medium => write!(f, "MEDIUM"),
|
||
|
|
Self::High => write!(f, "HIGH"),
|
||
|
|
Self::Critical => write!(f, "CRITICAL"),
|
||
|
|
Self::Warning => write!(f, "WARNING"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Implémentations de conversion depuis des erreurs externes
|
||
|
|
impl From<sqlx::Error> for ChatError {
|
||
|
|
fn from(err: sqlx::Error) -> Self {
|
||
|
|
Self::database_error("query", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<tokio_tungstenite::tungstenite::Error> for ChatError {
|
||
|
|
fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
|
||
|
|
Self::WebSocket { source: err }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<serde_json::Error> for ChatError {
|
||
|
|
fn from(err: serde_json::Error) -> Self {
|
||
|
|
Self::InvalidFormat {
|
||
|
|
field: "json".to_string(),
|
||
|
|
reason: err.to_string(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<std::env::VarError> for ChatError {
|
||
|
|
fn from(err: std::env::VarError) -> Self {
|
||
|
|
Self::Configuration {
|
||
|
|
message: format!("Variable d'environnement manquante: {}", err),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Macro pour simplifier la création d'erreurs
|
||
|
|
#[macro_export]
|
||
|
|
macro_rules! chat_error {
|
||
|
|
($variant:ident, $($field:ident = $value:expr),*) => {
|
||
|
|
$crate::error::ChatError::$variant {
|
||
|
|
$($field: $value.into()),*
|
||
|
|
}
|
||
|
|
};
|
||
|
|
($variant:ident) => {
|
||
|
|
$crate::error::ChatError::$variant
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_error_http_status() {
|
||
|
|
assert_eq!(ChatError::InvalidCredentials.http_status(), 401);
|
||
|
|
assert_eq!(ChatError::not_found("user", "123").http_status(), 404);
|
||
|
|
assert_eq!(ChatError::unauthorized("send_message").http_status(), 403);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_error_severity() {
|
||
|
|
assert_eq!(ChatError::InjectionAttempt.severity(), ErrorSeverity::High);
|
||
|
|
assert_eq!(
|
||
|
|
ChatError::InvalidCredentials.severity(),
|
||
|
|
ErrorSeverity::Medium
|
||
|
|
);
|
||
|
|
assert_eq!(ChatError::SpamDetected.severity(), ErrorSeverity::Medium);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_public_message() {
|
||
|
|
let error = ChatError::InvalidFormat {
|
||
|
|
field: "email".to_string(),
|
||
|
|
reason: "invalid format".to_string(),
|
||
|
|
};
|
||
|
|
assert_eq!(error.public_message(), "Format invalide pour email");
|
||
|
|
|
||
|
|
let db_error = ChatError::database_error("insert", sqlx::Error::RowNotFound);
|
||
|
|
assert_eq!(
|
||
|
|
db_error.public_message(),
|
||
|
|
"Erreur temporaire, veuillez réessayer"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_error_creation_helpers() {
|
||
|
|
let error = ChatError::not_found("conversation", "room_123");
|
||
|
|
match error {
|
||
|
|
ChatError::NotFound { resource, id } => {
|
||
|
|
assert_eq!(resource, "conversation");
|
||
|
|
assert_eq!(id, "room_123");
|
||
|
|
}
|
||
|
|
_ => panic!("Wrong error type"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_macro() {
|
||
|
|
let error = chat_error!(MessageTooLong, actual = 5000_usize, max = 4000_usize);
|
||
|
|
match error {
|
||
|
|
ChatError::MessageTooLong { actual, max } => {
|
||
|
|
assert_eq!(actual, 5000);
|
||
|
|
assert_eq!(max, 4000);
|
||
|
|
}
|
||
|
|
_ => panic!("Wrong error type"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|