1use std::fmt;
59use thiserror::Error;
60
61pub use crate::accounts::errors::{AccountOperation, AccountsError};
63pub use crate::authn::errors::{AuthenticationError, AuthnError};
64pub use crate::authz::errors::AuthzError;
65pub use crate::codecs::errors::{CodecOperation, CodecsError, JwtError, JwtOperation};
66pub use crate::hashing::errors::{HashingError, HashingOperation};
67pub use crate::permissions::errors::PermissionsError;
68pub use crate::repositories::errors::{
69 DatabaseError, DatabaseOperation, RepositoriesError, RepositoryOperation, RepositoryType,
70};
71pub use crate::secrets::errors::SecretError;
72
73pub use crate::gate::oauth2::errors::OAuth2Error;
75pub trait UserFriendlyError: fmt::Display + fmt::Debug {
86 fn user_message(&self) -> String;
99
100 fn developer_message(&self) -> String;
108
109 fn support_code(&self) -> String;
117
118 fn severity(&self) -> ErrorSeverity;
120
121 fn suggested_actions(&self) -> Vec<String> {
123 Vec::new()
124 }
125
126 fn is_retryable(&self) -> bool {
128 false
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
134pub enum ErrorSeverity {
135 Critical,
137 Error,
139 Warning,
141 Info,
143}
144
145pub type Result<T> = std::result::Result<T, Error>;
166
167#[derive(Debug, Error)]
176pub enum Error {
177 #[error(transparent)]
179 Accounts(#[from] AccountsError),
180
181 #[error(transparent)]
183 Authn(#[from] AuthnError),
184
185 #[error(transparent)]
187 Authz(#[from] AuthzError),
188
189 #[error(transparent)]
191 Permissions(#[from] PermissionsError),
192
193 #[error(transparent)]
195 Codecs(#[from] CodecsError),
196
197 #[error(transparent)]
199 Jwt(#[from] JwtError),
200
201 #[error(transparent)]
203 Repositories(#[from] RepositoriesError),
204
205 #[error(transparent)]
207 Database(#[from] DatabaseError),
208
209 #[error(transparent)]
211 Hashing(#[from] HashingError),
212
213 #[error(transparent)]
215 Secrets(#[from] SecretError),
216
217 #[error(transparent)]
219 OAuth2(#[from] crate::gate::oauth2::errors::OAuth2Error),
220}
221
222impl UserFriendlyError for Error {
223 fn user_message(&self) -> String {
224 match self {
225 Error::Accounts(err) => err.user_message(),
226 Error::Authn(err) => err.user_message(),
227 Error::Authz(err) => err.user_message(),
228 Error::Permissions(err) => err.user_message(),
229 Error::Codecs(err) => err.user_message(),
230 Error::Jwt(err) => err.user_message(),
231 Error::Repositories(err) => err.user_message(),
232 Error::Database(err) => err.user_message(),
233 Error::Hashing(err) => err.user_message(),
234 Error::Secrets(err) => err.user_message(),
235 Error::OAuth2(err) => err.user_message(),
236 }
237 }
238
239 fn developer_message(&self) -> String {
240 match self {
241 Error::Accounts(err) => err.developer_message(),
242 Error::Authn(err) => err.developer_message(),
243 Error::Authz(err) => err.developer_message(),
244 Error::Permissions(err) => err.developer_message(),
245 Error::Codecs(err) => err.developer_message(),
246 Error::Jwt(err) => err.developer_message(),
247 Error::Repositories(err) => err.developer_message(),
248 Error::Database(err) => err.developer_message(),
249 Error::Hashing(err) => err.developer_message(),
250 Error::Secrets(err) => err.developer_message(),
251 Error::OAuth2(err) => err.developer_message(),
252 }
253 }
254
255 fn support_code(&self) -> String {
256 match self {
257 Error::Accounts(err) => err.support_code(),
258 Error::Authn(err) => err.support_code(),
259 Error::Authz(err) => err.support_code(),
260 Error::Permissions(err) => err.support_code(),
261 Error::Codecs(err) => err.support_code(),
262 Error::Jwt(err) => err.support_code(),
263 Error::Repositories(err) => err.support_code(),
264 Error::Database(err) => err.support_code(),
265 Error::Hashing(err) => err.support_code(),
266 Error::Secrets(err) => err.support_code(),
267 Error::OAuth2(err) => err.support_code(),
268 }
269 }
270
271 fn severity(&self) -> ErrorSeverity {
272 match self {
273 Error::Accounts(err) => err.severity(),
274 Error::Authn(err) => err.severity(),
275 Error::Authz(err) => err.severity(),
276 Error::Permissions(err) => err.severity(),
277 Error::Codecs(err) => err.severity(),
278 Error::Jwt(err) => err.severity(),
279 Error::Repositories(err) => err.severity(),
280 Error::Database(err) => err.severity(),
281 Error::Hashing(err) => err.severity(),
282 Error::Secrets(err) => err.severity(),
283 Error::OAuth2(err) => err.severity(),
284 }
285 }
286
287 fn suggested_actions(&self) -> Vec<String> {
288 match self {
289 Error::Accounts(err) => err.suggested_actions(),
290 Error::Authn(err) => err.suggested_actions(),
291 Error::Authz(err) => err.suggested_actions(),
292 Error::Permissions(err) => err.suggested_actions(),
293 Error::Codecs(err) => err.suggested_actions(),
294 Error::Jwt(err) => err.suggested_actions(),
295 Error::Repositories(err) => err.suggested_actions(),
296 Error::Database(err) => err.suggested_actions(),
297 Error::Hashing(err) => err.suggested_actions(),
298 Error::Secrets(err) => err.suggested_actions(),
299 Error::OAuth2(err) => err.suggested_actions(),
300 }
301 }
302
303 fn is_retryable(&self) -> bool {
304 match self {
305 Error::Accounts(err) => err.is_retryable(),
306 Error::Authn(err) => err.is_retryable(),
307 Error::Authz(err) => err.is_retryable(),
308 Error::Permissions(err) => err.is_retryable(),
309 Error::Codecs(err) => err.is_retryable(),
310 Error::Jwt(err) => err.is_retryable(),
311 Error::Repositories(err) => err.is_retryable(),
312 Error::Database(err) => err.is_retryable(),
313 Error::Hashing(err) => err.is_retryable(),
314 Error::Secrets(err) => err.is_retryable(),
315 Error::OAuth2(err) => err.is_retryable(),
316 }
317 }
318}
319
320#[cfg(feature = "storage-surrealdb")]
322impl From<surrealdb::Error> for Error {
323 fn from(err: surrealdb::Error) -> Self {
324 Error::Database(DatabaseError::with_context(
325 DatabaseOperation::Query,
326 format!("SurrealDB error: {}", err),
327 None,
328 None,
329 ))
330 }
331}
332
333impl From<argon2::Error> for Error {
335 fn from(err: argon2::Error) -> Self {
336 Error::Hashing(HashingError::with_context(
337 HashingOperation::Hash,
338 format!("Argon2 error: {}", err),
339 Some("Argon2id".to_string()),
340 None,
341 ))
342 }
343}
344
345impl From<crate::cookie_template::CookieTemplateBuilderError> for Error {
349 fn from(err: crate::cookie_template::CookieTemplateBuilderError) -> Self {
350 Error::Codecs(CodecsError::codec_with_format(
351 CodecOperation::Encode,
352 format!("Invalid cookie template configuration: {}", err),
353 Some("cookie::CookieBuilder".to_string()),
354 Some("Invalid cookie settings".to_string()),
355 ))
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use crate::errors::{
362 AccountOperation, AccountsError, AuthenticationError, AuthnError, AuthzError,
363 CodecOperation, DatabaseError, DatabaseOperation, Error, ErrorSeverity, HashingOperation,
364 JwtOperation, RepositoriesError, RepositoryOperation, RepositoryType, UserFriendlyError,
365 };
366
367 #[test]
368 fn authz_error_permission_collision() {
369 let permissions = vec!["read:file".to_string(), "write:file".to_string()];
370 let error = Error::Authz(AuthzError::collision(123u64, permissions.clone()));
371
372 match &error {
374 Error::Authz(AuthzError::PermissionCollision {
375 collision_count,
376 hash_id,
377 permissions: perms,
378 }) => {
379 assert_eq!(*collision_count, 2);
380 assert_eq!(*hash_id, 123u64);
381 assert_eq!(*perms, permissions);
382 }
383 _ => panic!("Expected PermissionCollision variant"),
384 }
385
386 assert!(error.user_message().contains("technical issue"));
388 assert!(error.developer_message().contains("Permission collision"));
389 assert!(error.support_code().starts_with("AUTHZ-PERM-COLLISION-"));
390 assert_eq!(error.severity(), ErrorSeverity::Critical);
391 assert!(!error.suggested_actions().is_empty());
392 }
393
394 #[test]
395 fn authn_error_authentication() {
396 let auth_error = AuthenticationError::InvalidCredentials;
397 let error = Error::Authn(AuthnError::from_authentication(
398 auth_error,
399 Some("test context".to_string()),
400 ));
401
402 match &error {
404 Error::Authn(AuthnError::Authentication { error, context }) => {
405 matches!(error, AuthenticationError::InvalidCredentials);
406 assert_eq!(*context, Some("test context".to_string()));
407 }
408 _ => panic!("Expected Authn::Authentication variant"),
409 }
410
411 assert!(error.user_message().contains("username or password"));
413 assert!(error.developer_message().contains("Invalid credentials"));
414 assert_eq!(error.severity(), ErrorSeverity::Warning);
415 assert!(
416 error
417 .suggested_actions()
418 .iter()
419 .any(|action| action.contains("username") || action.contains("password"))
420 );
421 }
422
423 #[test]
424 fn database_error_query() {
425 let error = Error::Database(DatabaseError::new(
426 DatabaseOperation::Query,
427 "Connection failed",
428 ));
429
430 match &error {
432 Error::Database(DatabaseError::Operation {
433 operation, message, ..
434 }) => {
435 matches!(operation, DatabaseOperation::Query);
436 assert_eq!(*message, "Connection failed");
437 }
438 _ => panic!("Expected Database::Operation variant"),
439 }
440
441 assert!(error.user_message().contains("technical difficulties"));
443 assert!(error.developer_message().contains("Database"));
444 assert_eq!(error.severity(), ErrorSeverity::Error);
445 assert!(error.is_retryable());
446 }
447
448 #[test]
449 fn repositories_error_operation_failed() {
450 let error = Error::Repositories(RepositoriesError::operation_failed(
451 RepositoryType::Account,
452 RepositoryOperation::Insert,
453 "Insert failed",
454 Some("user-123".into()),
455 Some("insert_account".into()),
456 ));
457
458 match &error {
460 Error::Repositories(RepositoriesError::OperationFailed {
461 repository,
462 operation,
463 message,
464 ..
465 }) => {
466 matches!(repository, RepositoryType::Account);
467 matches!(operation, RepositoryOperation::Insert);
468 assert_eq!(*message, "Insert failed");
469 }
470 _ => panic!("Expected Repositories::OperationFailed variant"),
471 }
472
473 assert!(error.user_message().contains("account information"));
475 assert!(
476 error
477 .developer_message()
478 .contains("Repository operation failed")
479 );
480 assert!(
481 error.severity() == ErrorSeverity::Error || error.severity() == ErrorSeverity::Critical
482 );
483 assert!(error.is_retryable());
484 }
485
486 #[test]
487 fn error_display() {
488 let error = Error::Accounts(AccountsError::operation(
489 AccountOperation::Create,
490 "create failed",
491 Some("acc-1".into()),
492 ));
493 let display = format!("{}", error);
494 assert!(display.contains("Account operation"));
495
496 assert!(!error.user_message().is_empty());
498 assert!(!error.developer_message().is_empty());
499 assert!(!error.support_code().is_empty());
500 assert!(!matches!(error.severity(), ErrorSeverity::Info));
501 }
502
503 #[test]
504 fn operation_display() {
505 assert_eq!(format!("{}", AccountOperation::Create), "create");
506 assert_eq!(format!("{}", DatabaseOperation::Query), "query");
507 assert_eq!(format!("{}", JwtOperation::Encode), "encode");
508 assert_eq!(format!("{}", CodecOperation::Decode), "decode");
509 assert_eq!(format!("{}", HashingOperation::Verify), "verify");
510 }
511
512 #[test]
513 fn error_severity_levels() {
514 let authz_error = Error::Authz(AuthzError::collision(123, vec!["test".to_string()]));
515 assert_eq!(authz_error.severity(), ErrorSeverity::Critical);
516
517 assert_ne!(ErrorSeverity::Critical, ErrorSeverity::Error);
519 assert_ne!(ErrorSeverity::Error, ErrorSeverity::Warning);
520 assert_ne!(ErrorSeverity::Warning, ErrorSeverity::Info);
521 }
522
523 #[test]
524 fn error_support_codes_are_unique() {
525 let authz_error = Error::Authz(AuthzError::collision(123, vec!["test".to_string()]));
526 let authn_error = Error::Authn(AuthnError::invalid_credentials(None));
527
528 assert_ne!(authz_error.support_code(), authn_error.support_code());
529 assert!(authz_error.support_code().starts_with("AUTHZ-"));
530 assert!(authn_error.support_code().starts_with("AUTHN-"));
531 }
532
533 #[test]
534 fn error_suggested_actions() {
535 let error = Error::Authn(AuthnError::invalid_credentials(None));
536 let actions = error.suggested_actions();
537 assert!(!actions.is_empty());
538 assert!(actions.iter().any(|action| action.contains("username")
539 || action.contains("password")
540 || action.contains("check")));
541 }
542}