1use thiserror::Error;
56
57pub type Result<T, E = AuthError> = std::result::Result<T, E>;
63
64#[derive(Error, Debug)]
134pub enum AuthError {
135 #[error("Configuration error: {message}")]
140 Configuration {
141 message: String,
142 #[source]
143 source: Option<Box<dyn std::error::Error + Send + Sync>>,
144 help: Option<String>,
146 docs_url: Option<String>,
148 suggested_fix: Option<String>,
150 },
151
152 #[error("Authentication method '{method}' error: {message}")]
157 AuthMethod {
158 method: String,
159 message: String,
160 help: Option<String>,
162 docs_url: Option<String>,
164 suggested_fix: Option<String>,
166 },
167
168 #[error("Token error: {0}")]
170 Token(#[from] TokenError),
171
172 #[error("Permission error: {0}")]
174 Permission(#[from] PermissionError),
175
176 #[error("Storage error: {0}")]
178 Storage(#[from] StorageError),
179
180 #[error("Network error: {0}")]
182 Network(#[from] reqwest::Error),
183
184 #[error("JSON error: {0}")]
186 Json(#[from] serde_json::Error),
187
188 #[error("JWT error: {0}")]
190 Jwt(#[from] jsonwebtoken::errors::Error),
191
192 #[error("YAML error: {0}")]
194 Yaml(#[from] serde_yaml::Error),
195
196 #[error("TOML error: {0}")]
198 Toml(#[from] toml::ser::Error),
199
200 #[cfg(feature = "prometheus")]
202 #[error("Metrics error: {0}")]
203 Metrics(#[from] prometheus::Error),
204
205 #[error("IO error: {0}")]
207 Io(#[from] std::io::Error),
208
209 #[error("CLI error: {0}")]
211 Cli(String),
212
213 #[error("System time error: {0}")]
215 SystemTime(#[from] std::time::SystemTimeError),
216
217 #[error("Rate limit exceeded: {message}")]
219 RateLimit { message: String },
220
221 #[error("Too many concurrent sessions for user")]
223 TooManyConcurrentSessions,
224
225 #[error("MFA error: {0}")]
227 Mfa(#[from] MfaError),
228
229 #[error("Device flow error: {0}")]
231 DeviceFlow(#[from] DeviceFlowError),
232
233 #[error("OAuth provider error: {0}")]
235 OAuthProvider(#[from] OAuthProviderError),
236
237 #[error("Password verification failed: {0}")]
239 PasswordVerification(String),
240
241 #[error("Password hashing failed: {0}")]
243 PasswordHashing(String),
244
245 #[error("User not found")]
247 UserNotFound,
248
249 #[error("Invalid input: {0}")]
251 InvalidInput(String),
252
253 #[error("Hardware token error: {0}")]
255 HardwareToken(String),
256
257 #[error("Backup code verification failed: {0}")]
259 BackupCodeVerification(String),
260
261 #[error("Backup code hashing failed: {0}")]
263 BackupCodeHashing(String),
264
265 #[error("Invalid secret format")]
267 InvalidSecret,
268
269 #[error("User profile error: {message}")]
271 UserProfile { message: String },
272
273 #[error("Invalid credential: {credential_type} - {message}")]
275 InvalidCredential {
276 credential_type: String,
277 message: String,
278 },
279
280 #[error("Authentication timeout after {timeout_seconds} seconds")]
282 Timeout { timeout_seconds: u64 },
283
284 #[error("Provider '{provider}' is not configured or supported")]
286 ProviderNotConfigured { provider: String },
287
288 #[error("Cryptography error: {message}")]
290 Crypto { message: String },
291
292 #[error("Validation error: {message}")]
294 Validation { message: String },
295
296 #[error("Internal error: {message}")]
298 Internal { message: String },
299
300 #[error("Invalid request: {0}")]
302 InvalidRequest(String),
303
304 #[error(
306 "Step-up authentication required: current level '{current_level}', required level '{required_level}'"
307 )]
308 StepUpRequired {
309 current_level: String,
310 required_level: String,
311 step_up_url: String,
312 },
313
314 #[error("Session error: {0}")]
316 SessionError(String),
317
318 #[error("Unauthorized: {0}")]
320 Unauthorized(String),
321
322 #[error("Token generation error: {0}")]
324 TokenGeneration(String),
325
326 #[error("Invalid token: {0}")]
328 InvalidToken(String),
329
330 #[error("Unsupported provider: {0}")]
332 UnsupportedProvider(String),
333
334 #[error("Network error: {0}")]
336 NetworkError(String),
337
338 #[error("Parse error: {0}")]
340 ParseError(String),
341
342 #[error("Configuration error: {0}")]
344 ConfigurationError(String),
345}
346
347#[derive(Error, Debug)]
349pub enum TokenError {
350 #[error("Token has expired")]
351 Expired,
352
353 #[error("Token is invalid: {message}")]
354 Invalid { message: String },
355
356 #[error("Token not found")]
357 NotFound,
358
359 #[error("Token is missing")]
360 Missing,
361
362 #[error("Token creation failed: {message}")]
363 CreationFailed { message: String },
364
365 #[error("Token refresh failed: {message}")]
366 RefreshFailed { message: String },
367
368 #[error("Token revocation failed: {message}")]
369 RevocationFailed { message: String },
370}
371
372#[derive(Error, Debug)]
374pub enum PermissionError {
375 #[error("Access denied: missing permission '{permission}' for resource '{resource}'")]
376 AccessDenied {
377 permission: String,
378 resource: String,
379 },
380
381 #[error("Role '{role}' not found")]
382 RoleNotFound { role: String },
383
384 #[error("Permission '{permission}' not found")]
385 PermissionNotFound { permission: String },
386
387 #[error("Invalid permission format: {message}")]
388 InvalidFormat { message: String },
389
390 #[error("Permission denied: {message}")]
391 Denied {
392 action: String,
393 resource: String,
394 message: String,
395 },
396}
397
398#[derive(Error, Debug)]
400pub enum StorageError {
401 #[error("Connection failed: {message}")]
402 ConnectionFailed { message: String },
403
404 #[error("Operation failed: {message}")]
405 OperationFailed { message: String },
406
407 #[error("Serialization error: {message}")]
408 Serialization { message: String },
409
410 #[error("Storage backend not available")]
411 BackendUnavailable,
412}
413
414#[derive(Error, Debug)]
416pub enum MfaError {
417 #[error("MFA challenge expired")]
418 ChallengeExpired,
419
420 #[error("Invalid MFA code")]
421 InvalidCode,
422
423 #[error("MFA method not supported: {method}")]
424 MethodNotSupported { method: String },
425
426 #[error("MFA setup required")]
427 SetupRequired,
428
429 #[error("MFA verification failed: {message}")]
430 VerificationFailed { message: String },
431}
432
433#[derive(Error, Debug)]
435pub enum DeviceFlowError {
436 #[error("Authorization pending - user has not yet completed authorization")]
437 AuthorizationPending,
438
439 #[error("Slow down - polling too frequently")]
440 SlowDown,
441
442 #[error("Device code expired")]
443 ExpiredToken,
444
445 #[error("Access denied by user")]
446 AccessDenied,
447
448 #[error("Invalid device code")]
449 InvalidDeviceCode,
450
451 #[error("Unsupported grant type")]
452 UnsupportedGrantType,
453}
454
455#[derive(Error, Debug)]
457pub enum OAuthProviderError {
458 #[error("Invalid authorization code")]
459 InvalidAuthorizationCode,
460
461 #[error("Invalid redirect URI")]
462 InvalidRedirectUri,
463
464 #[error("Invalid client credentials")]
465 InvalidClientCredentials,
466
467 #[error("Insufficient scope: required '{required}', granted '{granted}'")]
468 InsufficientScope { required: String, granted: String },
469
470 #[error("Provider '{provider}' does not support '{feature}'")]
471 UnsupportedFeature { provider: String, feature: String },
472
473 #[error("Rate limited by provider: {message}")]
474 RateLimited { message: String },
475}
476
477impl AuthError {
478 pub fn config(message: impl Into<String>) -> Self {
480 Self::Configuration {
481 message: message.into(),
482 source: None,
483 help: None,
484 docs_url: None,
485 suggested_fix: None,
486 }
487 }
488
489 pub fn config_with_help(
491 message: impl Into<String>,
492 help: impl Into<String>,
493 suggested_fix: Option<String>,
494 ) -> Self {
495 Self::Configuration {
496 message: message.into(),
497 source: None,
498 help: Some(help.into()),
499 docs_url: Some(
500 "https://docs.rs/auth-framework/latest/auth_framework/config/".to_string(),
501 ),
502 suggested_fix,
503 }
504 }
505
506 pub fn jwt_secret_too_short(current_length: usize) -> Self {
508 Self::Configuration {
509 message: format!(
510 "JWT secret too short (got {} characters, need 32+ for security)",
511 current_length
512 ),
513 source: None,
514 help: Some("Use a cryptographically secure random string of at least 32 characters".to_string()),
515 docs_url: Some("https://docs.rs/auth-framework/latest/auth_framework/config/struct.SecurityConfig.html".to_string()),
516 suggested_fix: Some("Generate a secure secret: `openssl rand -hex 32`".to_string()),
517 }
518 }
519
520 pub fn production_memory_storage() -> Self {
522 Self::Configuration {
523 message: "Memory storage is not suitable for production environments".to_string(),
524 source: None,
525 help: Some("Use a persistent storage backend like PostgreSQL or Redis".to_string()),
526 docs_url: Some("https://docs.rs/auth-framework/latest/auth_framework/storage/".to_string()),
527 suggested_fix: Some("Configure PostgreSQL: .with_postgres(\"postgresql://...\") or Redis: .with_redis(\"redis://...\")".to_string()),
528 }
529 }
530
531 pub fn auth_method(method: impl Into<String>, message: impl Into<String>) -> Self {
533 Self::AuthMethod {
534 method: method.into(),
535 message: message.into(),
536 help: None,
537 docs_url: None,
538 suggested_fix: None,
539 }
540 }
541
542 pub fn auth_method_with_help(
544 method: impl Into<String>,
545 message: impl Into<String>,
546 help: impl Into<String>,
547 suggested_fix: Option<String>,
548 ) -> Self {
549 Self::AuthMethod {
550 method: method.into(),
551 message: message.into(),
552 help: Some(help.into()),
553 docs_url: Some(
554 "https://docs.rs/auth-framework/latest/auth_framework/methods/".to_string(),
555 ),
556 suggested_fix,
557 }
558 }
559
560 pub fn rate_limit(message: impl Into<String>) -> Self {
562 Self::RateLimit {
563 message: message.into(),
564 }
565 }
566
567 pub fn crypto(message: impl Into<String>) -> Self {
569 Self::Crypto {
570 message: message.into(),
571 }
572 }
573
574 pub fn validation(message: impl Into<String>) -> Self {
576 Self::Validation {
577 message: message.into(),
578 }
579 }
580
581 pub fn internal(message: impl Into<String>) -> Self {
583 Self::Internal {
584 message: message.into(),
585 }
586 }
587
588 pub fn authorization(message: impl Into<String>) -> Self {
590 Self::Permission(PermissionError::Denied {
591 action: "authorize".to_string(),
592 resource: "resource".to_string(),
593 message: message.into(),
594 })
595 }
596
597 pub fn access_denied(message: impl Into<String>) -> Self {
599 Self::Permission(PermissionError::Denied {
600 action: "access".to_string(),
601 resource: "resource".to_string(),
602 message: message.into(),
603 })
604 }
605
606 pub fn token(message: impl Into<String>) -> Self {
608 Self::Token(TokenError::Invalid {
609 message: message.into(),
610 })
611 }
612
613 pub fn device_flow(error: DeviceFlowError) -> Self {
615 Self::DeviceFlow(error)
616 }
617
618 pub fn oauth_provider(error: OAuthProviderError) -> Self {
620 Self::OAuthProvider(error)
621 }
622
623 pub fn user_profile(message: impl Into<String>) -> Self {
625 Self::UserProfile {
626 message: message.into(),
627 }
628 }
629
630 pub fn invalid_credential(
632 credential_type: impl Into<String>,
633 message: impl Into<String>,
634 ) -> Self {
635 Self::InvalidCredential {
636 credential_type: credential_type.into(),
637 message: message.into(),
638 }
639 }
640
641 pub fn timeout(timeout_seconds: u64) -> Self {
643 Self::Timeout { timeout_seconds }
644 }
645
646 pub fn provider_not_configured(provider: impl Into<String>) -> Self {
648 Self::ProviderNotConfigured {
649 provider: provider.into(),
650 }
651 }
652
653 pub fn rate_limited(message: impl Into<String>) -> Self {
655 Self::RateLimit {
656 message: message.into(),
657 }
658 }
659
660 pub fn configuration(message: impl Into<String>) -> Self {
662 Self::Configuration {
663 message: message.into(),
664 source: None,
665 help: None,
666 docs_url: None,
667 suggested_fix: None,
668 }
669 }
670}
671
672impl TokenError {
673 pub fn creation_failed(message: impl Into<String>) -> Self {
675 Self::CreationFailed {
676 message: message.into(),
677 }
678 }
679
680 pub fn refresh_failed(message: impl Into<String>) -> Self {
682 Self::RefreshFailed {
683 message: message.into(),
684 }
685 }
686
687 pub fn revocation_failed(message: impl Into<String>) -> Self {
689 Self::RevocationFailed {
690 message: message.into(),
691 }
692 }
693}
694
695impl PermissionError {
696 pub fn access_denied(permission: impl Into<String>, resource: impl Into<String>) -> Self {
698 Self::AccessDenied {
699 permission: permission.into(),
700 resource: resource.into(),
701 }
702 }
703
704 pub fn role_not_found(role: impl Into<String>) -> Self {
706 Self::RoleNotFound { role: role.into() }
707 }
708
709 pub fn permission_not_found(permission: impl Into<String>) -> Self {
711 Self::PermissionNotFound {
712 permission: permission.into(),
713 }
714 }
715
716 pub fn invalid_format(message: impl Into<String>) -> Self {
718 Self::InvalidFormat {
719 message: message.into(),
720 }
721 }
722}
723
724impl StorageError {
725 pub fn connection_failed(message: impl Into<String>) -> Self {
727 Self::ConnectionFailed {
728 message: message.into(),
729 }
730 }
731
732 pub fn operation_failed(message: impl Into<String>) -> Self {
734 Self::OperationFailed {
735 message: message.into(),
736 }
737 }
738
739 pub fn serialization(message: impl Into<String>) -> Self {
741 Self::Serialization {
742 message: message.into(),
743 }
744 }
745}
746
747impl MfaError {
748 pub fn method_not_supported(method: impl Into<String>) -> Self {
750 Self::MethodNotSupported {
751 method: method.into(),
752 }
753 }
754
755 pub fn verification_failed(message: impl Into<String>) -> Self {
757 Self::VerificationFailed {
758 message: message.into(),
759 }
760 }
761}
762
763#[cfg(feature = "actix-integration")]
765impl actix_web::ResponseError for AuthError {
766 fn error_response(&self) -> actix_web::HttpResponse {
767 match self {
768 AuthError::Token(_) => {
769 actix_web::HttpResponse::Unauthorized().json(serde_json::json!({
770 "error": "invalid_token",
771 "error_description": self.to_string()
772 }))
773 }
774 AuthError::Permission(_) => {
775 actix_web::HttpResponse::Forbidden().json(serde_json::json!({
776 "error": "insufficient_permissions",
777 "error_description": self.to_string()
778 }))
779 }
780 AuthError::RateLimit { .. } => {
781 actix_web::HttpResponse::TooManyRequests().json(serde_json::json!({
782 "error": "rate_limit_exceeded",
783 "error_description": self.to_string()
784 }))
785 }
786 AuthError::Configuration { .. }
787 | AuthError::Storage(_)
788 | AuthError::Internal { .. } => {
789 actix_web::HttpResponse::InternalServerError().json(serde_json::json!({
790 "error": "internal_error",
791 "error_description": "An internal error occurred"
792 }))
793 }
794 _ => actix_web::HttpResponse::BadRequest().json(serde_json::json!({
795 "error": "bad_request",
796 "error_description": self.to_string()
797 })),
798 }
799 }
800
801 fn status_code(&self) -> actix_web::http::StatusCode {
802 match self {
803 AuthError::Token(_) => actix_web::http::StatusCode::UNAUTHORIZED,
804 AuthError::Permission(_) => actix_web::http::StatusCode::FORBIDDEN,
805 AuthError::RateLimit { .. } => actix_web::http::StatusCode::TOO_MANY_REQUESTS,
806 AuthError::Configuration { .. } | AuthError::Storage(_) => {
807 actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
808 }
809 _ => actix_web::http::StatusCode::BAD_REQUEST,
810 }
811 }
812}
813
814impl From<Box<dyn std::error::Error + Send + Sync>> for AuthError {
816 fn from(error: Box<dyn std::error::Error + Send + Sync>) -> Self {
817 AuthError::Cli(format!("Admin tool error: {}", error))
818 }
819}
820
821impl From<Box<dyn std::error::Error>> for AuthError {
822 fn from(error: Box<dyn std::error::Error>) -> Self {
823 AuthError::Cli(format!("Admin tool error: {}", error))
824 }
825}
826
827#[cfg(test)]
828mod tests {
829 use super::*;
830 use std::error::Error;
831
832 #[test]
833 fn test_auth_error_creation() {
834 let token_error = AuthError::token("Invalid JWT signature");
835 assert!(matches!(token_error, AuthError::Token(_)));
836 assert!(token_error.to_string().contains("Invalid JWT signature"));
837
838 let permission_error = AuthError::access_denied("Access denied");
839 assert!(matches!(permission_error, AuthError::Permission(_)));
840 assert!(permission_error.to_string().contains("Access denied"));
841
842 let config_error = AuthError::config("Database connection failed");
843 assert!(matches!(config_error, AuthError::Configuration { .. }));
844 assert!(
845 config_error
846 .to_string()
847 .contains("Database connection failed")
848 );
849 }
850
851 #[test]
852 fn test_auth_error_categorization() {
853 let errors = vec![
855 (AuthError::token("test"), "Token"),
856 (AuthError::access_denied("test"), "Permission"),
857 (AuthError::config("test"), "Configuration"),
858 (AuthError::crypto("test"), "Crypto"),
859 (AuthError::validation("test"), "Validation"),
860 ];
861
862 for (error, expected_category) in errors {
863 let error_string = format!("{:?}", error);
864 assert!(
865 error_string.contains(expected_category),
866 "Error {:?} should contain category {}",
867 error,
868 expected_category
869 );
870 }
871 }
872
873 #[test]
874 fn test_rate_limit_error() {
875 let rate_limit_error = AuthError::rate_limit("Too many requests");
876
877 match rate_limit_error {
878 AuthError::RateLimit { message } => {
879 assert_eq!(message, "Too many requests");
880 }
881 _ => panic!("Expected RateLimit error"),
882 }
883 }
884
885 #[test]
886 fn test_validation_error() {
887 let validation_error = AuthError::validation("username must not be empty");
888
889 match validation_error {
890 AuthError::Validation { message } => {
891 assert_eq!(message, "username must not be empty");
892 }
893 _ => panic!("Expected Validation error"),
894 }
895 }
896
897 #[test]
898 fn test_configuration_error() {
899 let config_error = AuthError::config("jwt_secret is required");
900
901 match config_error {
902 AuthError::Configuration { message, .. } => {
903 assert_eq!(message, "jwt_secret is required");
904 }
905 _ => panic!("Expected Configuration error"),
906 }
907 }
908
909 #[test]
910 fn test_error_chain() {
911 let root_cause = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
912 let auth_error = AuthError::internal(format!("Config file error: {}", root_cause));
913
914 assert!(auth_error.to_string().contains("File not found"));
916 assert!(auth_error.to_string().contains("Config file error"));
917 }
918
919 #[test]
920 fn test_error_source() {
921 let token_error = AuthError::token("JWT parsing failed");
922
923 assert!(token_error.source().is_some());
926
927 let error_msg = format!("{}", token_error);
929 assert!(error_msg.contains("JWT parsing failed"));
930 }
931
932 #[test]
933 fn test_from_conversions() {
934 let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied");
936 let auth_error: AuthError = io_error.into();
937 assert!(matches!(auth_error, AuthError::Io(_)));
938
939 let json_str = r#"{"invalid": json"#;
941 let json_error: serde_json::Error =
942 serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
943 let auth_error: AuthError = json_error.into();
944 assert!(matches!(auth_error, AuthError::Json(_)));
945 }
946
947 #[test]
948 fn test_error_equality() {
949 let error1 = AuthError::token("Same message");
950 let error2 = AuthError::token("Same message");
951 let error3 = AuthError::token("Different message");
952
953 assert_eq!(format!("{:?}", error1), format!("{:?}", error2));
955 assert_ne!(format!("{:?}", error1), format!("{:?}", error3));
956 }
957
958 #[test]
959 fn test_actix_web_integration() {
960 #[cfg(feature = "actix-web")]
961 {
962 use actix_web::ResponseError;
963
964 assert_eq!(
966 AuthError::token("test").status_code(),
967 actix_web::http::StatusCode::UNAUTHORIZED
968 );
969 assert_eq!(
970 AuthError::access_denied("test").status_code(),
971 actix_web::http::StatusCode::FORBIDDEN
972 );
973 assert_eq!(
974 AuthError::rate_limit("test").status_code(),
975 actix_web::http::StatusCode::TOO_MANY_REQUESTS
976 );
977 assert_eq!(
978 AuthError::internal("test").status_code(),
979 actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
980 );
981 }
982 }
983
984 #[test]
985 fn test_error_message_safety() {
986 let sensitive_data = "password123";
988 let safe_error = AuthError::token("Invalid credentials");
989
990 assert!(!safe_error.to_string().contains(sensitive_data));
992
993 let config_error = AuthError::config("connection failed");
995 assert!(!config_error.to_string().contains("password"));
996 assert!(!config_error.to_string().contains("secret"));
997 }
998
999 #[test]
1000 fn test_cli_error_conversion() {
1001 let boxed_error: Box<dyn std::error::Error + Send + Sync> = "CLI operation failed".into();
1002 let auth_error: AuthError = boxed_error.into();
1003
1004 assert!(matches!(auth_error, AuthError::Cli(_)));
1005 assert!(auth_error.to_string().contains("CLI operation failed"));
1006 }
1007
1008 #[test]
1009 fn test_error_variants_coverage() {
1010 let test_errors = vec![
1012 AuthError::token("token error"),
1013 AuthError::access_denied("permission error"),
1014 AuthError::internal("internal error"),
1015 AuthError::crypto("crypto error"),
1016 AuthError::Cli("cli error".to_string()),
1017 AuthError::validation("validation error"),
1018 AuthError::config("config error"),
1019 AuthError::rate_limit("rate limit error"),
1020 ];
1021
1022 for error in test_errors {
1023 assert!(
1025 !error.to_string().is_empty(),
1026 "Error should have message: {:?}",
1027 error
1028 );
1029
1030 let debug_repr = format!("{:?}", error);
1032 assert!(
1033 !debug_repr.is_empty(),
1034 "Error should have debug representation: {:?}",
1035 error
1036 );
1037 }
1038 }
1039
1040 #[test]
1041 fn test_oauth_specific_errors() {
1042 let invalid_client = AuthError::auth_method("oauth", "Client authentication failed");
1044 assert!(
1045 invalid_client
1046 .to_string()
1047 .contains("Client authentication failed")
1048 );
1049
1050 let invalid_grant = AuthError::auth_method("oauth", "Authorization code expired");
1051 assert!(
1052 invalid_grant
1053 .to_string()
1054 .contains("Authorization code expired")
1055 );
1056 }
1057
1058 #[test]
1059 fn test_error_context_preservation() {
1060 let original_msg = "Original error message";
1062 let context_msg = "Additional context";
1063
1064 let base_error = AuthError::internal(original_msg);
1065 let contextual_error = AuthError::internal(format!("{}: {}", context_msg, base_error));
1066
1067 assert!(contextual_error.to_string().contains(original_msg));
1068 assert!(contextual_error.to_string().contains(context_msg));
1069 }
1070
1071 #[test]
1072 fn test_error_serialization() {
1073 let error = AuthError::validation("email invalid format");
1075
1076 let error_response = serde_json::json!({
1078 "error": "validation_failed",
1079 "message": error.to_string(),
1080 "field": "email"
1081 });
1082
1083 assert!(
1084 error_response["message"]
1085 .as_str()
1086 .unwrap()
1087 .contains("invalid format")
1088 );
1089 }
1090
1091 #[test]
1092 fn test_concurrent_error_creation() {
1093 use std::thread;
1094
1095 let handles: Vec<_> = (0..10)
1096 .map(|i| {
1097 thread::spawn(move || {
1098 let error = AuthError::token(format!("Concurrent error {}", i));
1099 assert!(
1100 error
1101 .to_string()
1102 .contains(&format!("Concurrent error {}", i))
1103 );
1104 error
1105 })
1106 })
1107 .collect();
1108
1109 for handle in handles {
1111 let error = handle.join().unwrap();
1112 assert!(!error.to_string().is_empty());
1113 }
1114 }
1115}