1use thiserror::Error;
4
5#[derive(Debug, Error)]
7#[non_exhaustive]
8pub enum AuthError {
9 #[error("invalid credentials: {0}")]
11 InvalidCredentials(String),
12
13 #[error("authentication failed: {0}")]
15 AuthenticationFailed(String),
16
17 #[error("token expired or invalid")]
19 TokenExpired,
20
21 #[error("failed to acquire token: {0}")]
23 TokenAcquisition(String),
24
25 #[error("unsupported authentication method: {0}")]
27 UnsupportedMethod(String),
28
29 #[error("SSPI error: {0}")]
31 Sspi(String),
32
33 #[error("certificate error: {0}")]
35 Certificate(String),
36
37 #[error("network error: {0}")]
39 Network(String),
40
41 #[error("configuration error: {0}")]
43 Configuration(String),
44
45 #[error("Azure identity error: {0}")]
47 AzureIdentity(String),
48}
49
50impl AuthError {
51 #[must_use]
57 pub fn is_transient(&self) -> bool {
58 matches!(
59 self,
60 Self::Network(_) | Self::TokenAcquisition(_) | Self::AzureIdentity(_)
61 )
62 }
63
64 #[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 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 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 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}