Skip to main content

mssql_auth/
error.rs

1//! Authentication error types.
2
3use thiserror::Error;
4
5/// Errors that can occur during authentication.
6#[derive(Debug, Error)]
7#[non_exhaustive]
8pub enum AuthError {
9    /// Invalid credentials provided.
10    #[error("invalid credentials: {0}")]
11    InvalidCredentials(String),
12
13    /// Authentication failed on server.
14    #[error("authentication failed: {0}")]
15    AuthenticationFailed(String),
16
17    /// Token expired or invalid.
18    #[error("token expired or invalid")]
19    TokenExpired,
20
21    /// Token acquisition failed.
22    #[error("failed to acquire token: {0}")]
23    TokenAcquisition(String),
24
25    /// Unsupported authentication method.
26    #[error("unsupported authentication method: {0}")]
27    UnsupportedMethod(String),
28
29    /// SSPI/GSSAPI error.
30    #[error("SSPI error: {0}")]
31    Sspi(String),
32
33    /// Certificate error.
34    #[error("certificate error: {0}")]
35    Certificate(String),
36
37    /// Network error during authentication.
38    #[error("network error: {0}")]
39    Network(String),
40
41    /// Configuration error.
42    #[error("configuration error: {0}")]
43    Configuration(String),
44
45    /// Azure identity error.
46    #[error("Azure identity error: {0}")]
47    AzureIdentity(String),
48}
49
50impl AuthError {
51    /// Check if this error is transient and may succeed on retry.
52    ///
53    /// Network errors, token acquisition failures (may be temporary service
54    /// issues), and Azure identity errors are potentially transient. Invalid
55    /// credentials and unsupported methods are terminal.
56    #[must_use]
57    pub fn is_transient(&self) -> bool {
58        matches!(
59            self,
60            Self::Network(_) | Self::TokenAcquisition(_) | Self::AzureIdentity(_)
61        )
62    }
63
64    /// Check if this error is terminal and will never succeed on retry.
65    ///
66    /// Invalid credentials, unsupported methods, certificate errors, and
67    /// configuration errors are permanent.
68    #[must_use]
69    pub fn is_terminal(&self) -> bool {
70        matches!(
71            self,
72            Self::InvalidCredentials(_)
73                | Self::UnsupportedMethod(_)
74                | Self::Certificate(_)
75                | Self::Configuration(_)
76        )
77    }
78}
79
80#[cfg(test)]
81#[allow(clippy::unwrap_used)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_transient_errors() {
87        assert!(AuthError::Network("connection reset".into()).is_transient());
88        assert!(AuthError::TokenAcquisition("timeout".into()).is_transient());
89        assert!(AuthError::AzureIdentity("service unavailable".into()).is_transient());
90    }
91
92    #[test]
93    fn test_terminal_errors() {
94        assert!(AuthError::InvalidCredentials("bad password".into()).is_terminal());
95        assert!(AuthError::UnsupportedMethod("NTLM".into()).is_terminal());
96        assert!(AuthError::Certificate("expired cert".into()).is_terminal());
97        assert!(AuthError::Configuration("missing field".into()).is_terminal());
98    }
99
100    #[test]
101    fn test_transient_terminal_mutual_exclusion() {
102        // Transient errors should not be terminal
103        assert!(!AuthError::Network("err".into()).is_terminal());
104        assert!(!AuthError::TokenAcquisition("err".into()).is_terminal());
105        assert!(!AuthError::AzureIdentity("err".into()).is_terminal());
106
107        // Terminal errors should not be transient
108        assert!(!AuthError::InvalidCredentials("err".into()).is_transient());
109        assert!(!AuthError::UnsupportedMethod("err".into()).is_transient());
110        assert!(!AuthError::Certificate("err".into()).is_transient());
111        assert!(!AuthError::Configuration("err".into()).is_transient());
112    }
113
114    #[test]
115    fn test_ambiguous_errors_classified() {
116        // Errors that are neither transient nor terminal
117        // (i.e., require case-by-case handling)
118        let sspi = AuthError::Sspi("negotiate failed".into());
119        assert!(!sspi.is_transient());
120        assert!(!sspi.is_terminal());
121
122        let expired = AuthError::TokenExpired;
123        assert!(!expired.is_transient());
124        assert!(!expired.is_terminal());
125
126        let auth_failed = AuthError::AuthenticationFailed("bad user".into());
127        assert!(!auth_failed.is_transient());
128        assert!(!auth_failed.is_terminal());
129    }
130
131    #[test]
132    fn test_error_display() {
133        assert_eq!(
134            AuthError::InvalidCredentials("no password".into()).to_string(),
135            "invalid credentials: no password"
136        );
137        assert_eq!(
138            AuthError::TokenExpired.to_string(),
139            "token expired or invalid"
140        );
141        assert_eq!(
142            AuthError::Sspi("ctx init".into()).to_string(),
143            "SSPI error: ctx init"
144        );
145        assert_eq!(
146            AuthError::Configuration("missing host".into()).to_string(),
147            "configuration error: missing host"
148        );
149    }
150
151    #[test]
152    fn test_error_is_send_sync() {
153        fn assert_send_sync<T: Send + Sync>() {}
154        assert_send_sync::<AuthError>();
155    }
156}