1use crate::application::dto::{
2 Claims, LoginRequest, LoginResponse, RefreshTokenRequest, RegisterRequest, UserResponse,
3 UserRoleSummary,
4};
5use crate::application::error::AppError;
6use crate::application::ports::{RefreshTokenRepository, UserRepository, UserRoleRepository};
7use crate::domain::entities::{RefreshToken, User, UserRole, UserRoleAssignment};
8use crate::infrastructure::audit::{log_audit_event, AuditEventType};
9use bcrypt::{hash, verify, DEFAULT_COST};
10use chrono::Utc;
11use jsonwebtoken::{encode, EncodingKey, Header};
12use std::sync::Arc;
13use uuid::Uuid;
14
15async fn hacher_hors_worker(mot_de_passe: String) -> Result<String, String> {
45 tokio::task::spawn_blocking(move || hash(&mot_de_passe, DEFAULT_COST))
46 .await
47 .map_err(|e| format!("Hachage interrompu : {e}"))?
48 .map_err(|e| format!("Failed to hash password: {e}"))
49}
50
51async fn verifier_hors_worker(mot_de_passe: String, empreinte: String) -> Result<bool, AppError> {
60 tokio::task::spawn_blocking(move || verify(&mot_de_passe, &empreinte))
61 .await
62 .map_err(|e| AppError::Internal(format!("Vérification du mot de passe interrompue : {e}")))?
63 .map_err(AppError::from)
64}
65
66pub struct AuthUseCases {
67 user_repo: Arc<dyn UserRepository>,
68 refresh_token_repo: Arc<dyn RefreshTokenRepository>,
69 user_role_repo: Arc<dyn UserRoleRepository>,
70 jwt_secret: String,
71}
72
73impl AuthUseCases {
74 pub fn new(
75 user_repo: Arc<dyn UserRepository>,
76 refresh_token_repo: Arc<dyn RefreshTokenRepository>,
77 user_role_repo: Arc<dyn UserRoleRepository>,
78 jwt_secret: String,
79 ) -> Self {
80 Self {
81 user_repo,
82 refresh_token_repo,
83 user_role_repo,
84 jwt_secret,
85 }
86 }
87
88 pub async fn login(&self, request: LoginRequest) -> Result<LoginResponse, AppError> {
89 let normalized_email = request.email.trim().to_lowercase();
94 let user = self
95 .user_repo
96 .find_by_email(&normalized_email)
97 .await?
98 .ok_or_else(|| {
99 tokio::spawn(async move {
101 log_audit_event(
102 AuditEventType::AuthenticationFailed,
103 None,
104 None,
105 Some(format!("Failed login attempt for email: {}", request.email)),
106 None,
107 )
108 .await;
109 });
110 AppError::InvalidCredentials
112 })?;
113
114 if !user.is_active {
115 let user_id = user.id;
117 tokio::spawn(async move {
118 log_audit_event(
119 AuditEventType::AuthenticationFailed,
120 Some(user_id),
121 None,
122 Some("Login attempt on deactivated account".to_string()),
123 None,
124 )
125 .await;
126 });
127 return Err(AppError::AccountDeactivated);
128 }
129
130 let is_valid =
132 verifier_hors_worker(request.password.clone(), user.password_hash.clone()).await?;
133
134 if !is_valid {
135 let user_id = user.id;
137 tokio::spawn(async move {
138 log_audit_event(
139 AuditEventType::AuthenticationFailed,
140 Some(user_id),
141 None,
142 Some("Invalid password".to_string()),
143 None,
144 )
145 .await;
146 });
147 return Err(AppError::InvalidCredentials);
148 }
149
150 let (roles, active_role) = self.ensure_role_assignments(&user).await?;
151 let user_for_token = self.apply_active_role_metadata(&user, &active_role).await?;
152 let token = self.generate_token(&user_for_token, &active_role)?;
153
154 let refresh_token_string = self.generate_refresh_token_string(&user_for_token);
155 let refresh_token = RefreshToken::new(user_for_token.id, refresh_token_string.clone());
156 self.refresh_token_repo.create(&refresh_token).await?;
157
158 let user_id = user_for_token.id;
160 let organization_id = active_role.organization_id;
161 tokio::spawn(async move {
162 log_audit_event(
163 AuditEventType::UserLogin,
164 Some(user_id),
165 organization_id,
166 Some("Successful login".to_string()),
167 None,
168 )
169 .await;
170 });
171
172 Ok(LoginResponse {
173 token,
174 refresh_token: refresh_token_string,
175 user: self.build_user_response(&user_for_token, &roles, &active_role),
176 })
177 }
178
179 pub async fn register(&self, request: RegisterRequest) -> Result<LoginResponse, String> {
180 let normalized_email = request.email.trim().to_lowercase();
185 if (self.user_repo.find_by_email(&normalized_email).await?).is_some() {
186 return Err("Email already exists".to_string());
187 }
188
189 let role: UserRole = request
190 .role
191 .parse()
192 .map_err(|e| format!("Invalid role: {}", e))?;
193
194 let password_hash = hacher_hors_worker(request.password.clone()).await?;
196
197 let user = User::new(
198 request.email,
199 password_hash,
200 request.first_name,
201 request.last_name,
202 role.clone(),
203 request.organization_id,
204 )?;
205
206 let created_user = self.user_repo.create(&user).await?;
207
208 let primary_assignment = self
210 .user_role_repo
211 .create(&UserRoleAssignment::new(
212 created_user.id,
213 role,
214 created_user.organization_id,
215 true,
216 ))
217 .await?;
218 let roles = vec![primary_assignment.clone()];
219 let user_for_token = self
220 .apply_active_role_metadata(&created_user, &primary_assignment)
221 .await?;
222
223 let token = self.generate_token(&user_for_token, &primary_assignment)?;
224 let refresh_token_string = self.generate_refresh_token_string(&user_for_token);
225 let refresh_token = RefreshToken::new(user_for_token.id, refresh_token_string.clone());
226 self.refresh_token_repo.create(&refresh_token).await?;
227
228 let user_id = created_user.id;
230 let organization_id = created_user.organization_id;
231 let email = created_user.email.clone();
232 tokio::spawn(async move {
233 log_audit_event(
234 AuditEventType::UserRegistration,
235 Some(user_id),
236 organization_id,
237 Some(format!("New user registered: {}", email)),
238 None,
239 )
240 .await;
241 });
242
243 Ok(LoginResponse {
244 token,
245 refresh_token: refresh_token_string,
246 user: self.build_user_response(&user_for_token, &roles, &primary_assignment),
247 })
248 }
249
250 pub async fn switch_active_role(
251 &self,
252 user_id: Uuid,
253 role_id: Uuid,
254 ) -> Result<LoginResponse, String> {
255 let user = self
256 .user_repo
257 .find_by_id(user_id)
258 .await?
259 .ok_or("User not found")?;
260
261 if !user.is_active {
262 return Err("User account is deactivated".to_string());
263 }
264
265 let target_role = self
266 .user_role_repo
267 .find_by_id(role_id)
268 .await?
269 .ok_or("Role assignment not found")?;
270
271 if target_role.user_id != user.id {
272 return Err("Role assignment does not belong to user".to_string());
273 }
274
275 let updated_primary = self
276 .user_role_repo
277 .set_primary_role(user.id, role_id)
278 .await?;
279
280 let roles = self.user_role_repo.list_for_user(user.id).await?;
281 let active_role = roles
282 .iter()
283 .find(|assignment| assignment.is_primary)
284 .cloned()
285 .unwrap_or(updated_primary.clone());
286
287 let updated_user = self.apply_active_role_metadata(&user, &active_role).await?;
288
289 let token = self.generate_token(&updated_user, &active_role)?;
290 let refresh_token_string = self.generate_refresh_token_string(&updated_user);
291 let refresh_token = RefreshToken::new(updated_user.id, refresh_token_string.clone());
292 self.refresh_token_repo.create(&refresh_token).await?;
293
294 Ok(LoginResponse {
295 token,
296 refresh_token: refresh_token_string,
297 user: self.build_user_response(&updated_user, &roles, &active_role),
298 })
299 }
300
301 pub async fn get_user_by_id(&self, user_id: uuid::Uuid) -> Result<UserResponse, AppError> {
302 let user = self
303 .user_repo
304 .find_by_id(user_id)
305 .await?
306 .ok_or_else(|| AppError::NotFound(format!("user {}", user_id)))?;
307
308 let (roles, active_role) = self.ensure_role_assignments(&user).await?;
309 Ok(self.build_user_response(&user, &roles, &active_role))
310 }
311
312 pub fn verify_token(&self, token: &str) -> Result<Claims, AppError> {
313 use jsonwebtoken::{decode, DecodingKey, Validation};
314
315 let token_data = decode::<Claims>(
316 token,
317 &DecodingKey::from_secret(self.jwt_secret.as_bytes()),
318 &Validation::default(),
319 )?;
320
321 Ok(token_data.claims)
322 }
323
324 pub async fn refresh_token(
325 &self,
326 request: RefreshTokenRequest,
327 ) -> Result<LoginResponse, AppError> {
328 let refresh_token = self
329 .refresh_token_repo
330 .find_by_token(&request.refresh_token)
331 .await?
332 .ok_or_else(|| {
333 tokio::spawn(async {
335 log_audit_event(
336 AuditEventType::InvalidToken,
337 None,
338 None,
339 Some("Invalid refresh token attempted".to_string()),
340 None,
341 )
342 .await;
343 });
344 AppError::TokenError("invalid refresh token".to_string())
345 })?;
346
347 if !refresh_token.is_valid() {
348 let user_id = refresh_token.user_id;
350 let reason = if refresh_token.is_expired() {
351 "Expired refresh token"
352 } else {
353 "Revoked refresh token"
354 };
355 tokio::spawn(async move {
356 log_audit_event(
357 AuditEventType::InvalidToken,
358 Some(user_id),
359 None,
360 Some(format!("{} attempted", reason)),
361 None,
362 )
363 .await;
364 });
365 return Err(AppError::TokenError(
366 "refresh token expired or revoked".to_string(),
367 ));
368 }
369
370 let user = self
371 .user_repo
372 .find_by_id(refresh_token.user_id)
373 .await?
374 .ok_or_else(|| AppError::NotFound("user for refresh token".to_string()))?;
375
376 if !user.is_active {
377 let user_id = user.id;
379 tokio::spawn(async move {
380 log_audit_event(
381 AuditEventType::AuthenticationFailed,
382 Some(user_id),
383 None,
384 Some("Refresh token attempt on deactivated account".to_string()),
385 None,
386 )
387 .await;
388 });
389 return Err(AppError::AccountDeactivated);
390 }
391
392 let (roles, active_role) = self.ensure_role_assignments(&user).await?;
393 let user_for_token = self.apply_active_role_metadata(&user, &active_role).await?;
394 let token = self.generate_token(&user_for_token, &active_role)?;
395
396 self.refresh_token_repo
398 .revoke(&request.refresh_token)
399 .await?;
400
401 let new_refresh_token_string = self.generate_refresh_token_string(&user_for_token);
402 let new_refresh_token =
403 RefreshToken::new(user_for_token.id, new_refresh_token_string.clone());
404 self.refresh_token_repo.create(&new_refresh_token).await?;
405
406 let user_id = user_for_token.id;
408 let organization_id = active_role.organization_id;
409 tokio::spawn(async move {
410 log_audit_event(
411 AuditEventType::TokenRefresh,
412 Some(user_id),
413 organization_id,
414 Some("Refresh token successfully exchanged".to_string()),
415 None,
416 )
417 .await;
418 });
419
420 Ok(LoginResponse {
421 token,
422 refresh_token: new_refresh_token_string,
423 user: self.build_user_response(&user_for_token, &roles, &active_role),
424 })
425 }
426
427 pub async fn revoke_all_refresh_tokens(&self, user_id: Uuid) -> Result<u64, String> {
428 self.refresh_token_repo.revoke_all_for_user(user_id).await
429 }
430
431 fn build_user_response(
432 &self,
433 user: &User,
434 roles: &[UserRoleAssignment],
435 active_role: &UserRoleAssignment,
436 ) -> UserResponse {
437 UserResponse {
438 id: user.id,
439 email: user.email.clone(),
440 first_name: user.first_name.clone(),
441 last_name: user.last_name.clone(),
442 role: active_role.role.to_string(),
443 organization_id: active_role.organization_id,
444 is_active: user.is_active,
445 created_at: user.created_at,
446 roles: roles.iter().map(Self::summarize_role).collect(),
447 active_role: Some(Self::summarize_role(active_role)),
448 }
449 }
450
451 async fn ensure_role_assignments(
452 &self,
453 user: &User,
454 ) -> Result<(Vec<UserRoleAssignment>, UserRoleAssignment), String> {
455 let mut assignments = self.user_role_repo.list_for_user(user.id).await?;
456
457 if assignments.is_empty() {
458 let assignment = self
459 .user_role_repo
460 .create(&UserRoleAssignment::new(
461 user.id,
462 user.role.clone(),
463 user.organization_id,
464 true,
465 ))
466 .await?;
467 assignments.push(assignment.clone());
468 }
469
470 if !assignments.iter().any(|assignment| assignment.is_primary) {
471 let first = assignments[0].id;
472 self.user_role_repo.set_primary_role(user.id, first).await?;
473 assignments = self.user_role_repo.list_for_user(user.id).await?;
474 }
475
476 let active = assignments
477 .iter()
478 .find(|assignment| assignment.is_primary)
479 .cloned()
480 .unwrap_or_else(|| assignments[0].clone());
481
482 Ok((assignments, active))
483 }
484
485 async fn apply_active_role_metadata(
486 &self,
487 user: &User,
488 active_role: &UserRoleAssignment,
489 ) -> Result<User, String> {
490 let mut updated_user = user.clone();
491 let mut requires_update = false;
492
493 if updated_user.role != active_role.role {
494 updated_user.role = active_role.role.clone();
495 requires_update = true;
496 }
497
498 if updated_user.organization_id != active_role.organization_id {
499 updated_user.organization_id = active_role.organization_id;
500 requires_update = true;
501 }
502
503 if requires_update {
504 updated_user.updated_at = Utc::now();
505 return self.user_repo.update(&updated_user).await;
506 }
507
508 Ok(updated_user)
509 }
510
511 fn summarize_role(assignment: &UserRoleAssignment) -> UserRoleSummary {
512 UserRoleSummary {
513 id: assignment.id,
514 role: assignment.role.to_string(),
515 organization_id: assignment.organization_id,
516 is_primary: assignment.is_primary,
517 }
518 }
519
520 fn generate_token(
521 &self,
522 user: &User,
523 active_role: &UserRoleAssignment,
524 ) -> Result<String, String> {
525 let now = Utc::now().timestamp();
526 let expiration = now + (15 * 60);
527
528 let claims = Claims {
529 sub: user.id.to_string(),
530 email: user.email.clone(),
531 role: active_role.role.to_string(),
532 organization_id: active_role.organization_id,
533 role_id: Some(active_role.id),
534 exp: expiration,
535 iat: now,
536 };
537
538 encode(
539 &Header::default(),
540 &claims,
541 &EncodingKey::from_secret(self.jwt_secret.as_bytes()),
542 )
543 .map_err(|e| format!("Failed to generate token: {}", e))
544 }
545
546 fn generate_refresh_token_string(&self, user: &User) -> String {
547 let now = Utc::now().timestamp();
548 format!("{}:{}:{}", user.id, now, uuid::Uuid::new_v4())
549 }
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555 use crate::application::ports::{RefreshTokenRepository, UserRoleRepository};
556 use crate::domain::entities::{RefreshToken, User, UserRole, UserRoleAssignment};
557 use async_trait::async_trait;
558 use mockall::mock;
559 use std::sync::Arc;
560
561 use crate::application::ports::user_repository::MockUserRepo;
563
564 mock! {
566 pub RefreshTokenRepo {}
567
568 #[async_trait]
569 impl RefreshTokenRepository for RefreshTokenRepo {
570 async fn create(&self, refresh_token: &RefreshToken) -> Result<RefreshToken, String>;
571 async fn find_by_token(&self, token: &str) -> Result<Option<RefreshToken>, String>;
572 async fn find_by_user_id(&self, user_id: Uuid) -> Result<Vec<RefreshToken>, String>;
573 async fn revoke(&self, token: &str) -> Result<bool, String>;
574 async fn revoke_all_for_user(&self, user_id: Uuid) -> Result<u64, String>;
575 async fn delete_expired(&self) -> Result<u64, String>;
576 }
577 }
578
579 mock! {
581 pub UserRoleRepo {}
582
583 #[async_trait]
584 impl UserRoleRepository for UserRoleRepo {
585 async fn create(&self, assignment: &UserRoleAssignment) -> Result<UserRoleAssignment, String>;
586 async fn list_for_user(&self, user_id: Uuid) -> Result<Vec<UserRoleAssignment>, String>;
587 async fn list_for_users(&self, user_ids: &[Uuid]) -> Result<std::collections::HashMap<Uuid, Vec<UserRoleAssignment>>, String>;
588 async fn replace_all(&self, user_id: Uuid, assignments: &[UserRoleAssignment]) -> Result<(), String>;
589 async fn find_by_id(&self, id: Uuid) -> Result<Option<UserRoleAssignment>, String>;
590 async fn set_primary_role(&self, user_id: Uuid, role_id: Uuid) -> Result<UserRoleAssignment, String>;
591 async fn delete_by_id(&self, id: Uuid) -> Result<bool, String>;
592 }
593 }
594
595 const TEST_JWT_SECRET: &str = "test-secret-key-that-is-long-enough-for-jwt";
596
597 fn make_user(email: &str, password: &str, org_id: Option<Uuid>) -> User {
599 let hash = bcrypt::hash(password, 4).expect("bcrypt hash");
600 User::new(
601 email.to_string(),
602 hash,
603 "Test".to_string(),
604 "User".to_string(),
605 UserRole::Syndic,
606 org_id,
607 )
608 .expect("valid user")
609 }
610
611 fn make_primary_role(user_id: Uuid, org_id: Option<Uuid>) -> UserRoleAssignment {
613 UserRoleAssignment::new(user_id, UserRole::Syndic, org_id, true)
614 }
615
616 fn build_use_cases(
618 user_repo: MockUserRepo,
619 refresh_repo: MockRefreshTokenRepo,
620 role_repo: MockUserRoleRepo,
621 ) -> AuthUseCases {
622 AuthUseCases::new(
623 Arc::new(user_repo),
624 Arc::new(refresh_repo),
625 Arc::new(role_repo),
626 TEST_JWT_SECRET.to_string(),
627 )
628 }
629
630 #[tokio::test]
633 async fn test_login_success() {
634 let org_id = Some(Uuid::new_v4());
635 let user = make_user("login@example.com", "password123", org_id);
636 let user_clone = user.clone();
637 let _user_for_update = user.clone();
638 let role = make_primary_role(user.id, org_id);
639 let role_clone = role.clone();
640
641 let mut user_repo = MockUserRepo::new();
642 user_repo
643 .expect_find_by_email()
644 .withf(|e| e == "login@example.com")
645 .returning(move |_| Ok(Some(user_clone.clone())));
646 user_repo.expect_update().returning(move |u| Ok(u.clone()));
647
648 let mut refresh_repo = MockRefreshTokenRepo::new();
649 refresh_repo.expect_create().returning(|rt| Ok(rt.clone()));
650
651 let mut role_repo = MockUserRoleRepo::new();
652 role_repo
653 .expect_list_for_user()
654 .returning(move |_| Ok(vec![role_clone.clone()]));
655
656 let uc = build_use_cases(user_repo, refresh_repo, role_repo);
657
658 let result = uc
659 .login(LoginRequest {
660 email: "login@example.com".to_string(),
661 password: "password123".to_string(),
662 })
663 .await;
664
665 assert!(result.is_ok(), "login should succeed: {:?}", result.err());
666 let response = result.unwrap();
667 assert!(!response.token.is_empty());
668 assert!(!response.refresh_token.is_empty());
669 assert_eq!(response.user.email, "login@example.com");
670 assert!(response.user.is_active);
671 }
672
673 #[tokio::test]
676 async fn test_login_invalid_email() {
677 let mut user_repo = MockUserRepo::new();
678 user_repo.expect_find_by_email().returning(|_| Ok(None));
679
680 let uc = build_use_cases(
681 user_repo,
682 MockRefreshTokenRepo::new(),
683 MockUserRoleRepo::new(),
684 );
685
686 let result = uc
687 .login(LoginRequest {
688 email: "nonexistent@example.com".to_string(),
689 password: "whatever".to_string(),
690 })
691 .await;
692
693 assert!(
696 matches!(result, Err(AppError::InvalidCredentials)),
697 "expected AppError::InvalidCredentials, got {:?}",
698 result
699 );
700 }
701
702 #[tokio::test]
705 async fn test_login_invalid_password() {
706 let user = make_user("user@example.com", "correct_password", None);
707 let user_clone = user.clone();
708
709 let mut user_repo = MockUserRepo::new();
710 user_repo
711 .expect_find_by_email()
712 .returning(move |_| Ok(Some(user_clone.clone())));
713
714 let uc = build_use_cases(
715 user_repo,
716 MockRefreshTokenRepo::new(),
717 MockUserRoleRepo::new(),
718 );
719
720 let result = uc
721 .login(LoginRequest {
722 email: "user@example.com".to_string(),
723 password: "wrong_password".to_string(),
724 })
725 .await;
726
727 assert!(
729 matches!(result, Err(AppError::InvalidCredentials)),
730 "expected AppError::InvalidCredentials, got {:?}",
731 result
732 );
733 }
734
735 #[tokio::test]
738 async fn test_login_deactivated_account() {
739 let mut user = make_user("deactivated@example.com", "password123", None);
740 user.deactivate();
741 let user_clone = user.clone();
742
743 let mut user_repo = MockUserRepo::new();
744 user_repo
745 .expect_find_by_email()
746 .returning(move |_| Ok(Some(user_clone.clone())));
747
748 let uc = build_use_cases(
749 user_repo,
750 MockRefreshTokenRepo::new(),
751 MockUserRoleRepo::new(),
752 );
753
754 let result = uc
755 .login(LoginRequest {
756 email: "deactivated@example.com".to_string(),
757 password: "password123".to_string(),
758 })
759 .await;
760
761 assert!(
765 matches!(result, Err(AppError::AccountDeactivated)),
766 "expected AppError::AccountDeactivated, got {:?}",
767 result
768 );
769 }
770
771 #[tokio::test]
774 async fn test_register_success() {
775 let mut user_repo = MockUserRepo::new();
776 user_repo.expect_find_by_email().returning(|_| Ok(None)); user_repo.expect_create().returning(|u| Ok(u.clone()));
778 user_repo.expect_update().returning(|u| Ok(u.clone()));
779
780 let mut refresh_repo = MockRefreshTokenRepo::new();
781 refresh_repo.expect_create().returning(|rt| Ok(rt.clone()));
782
783 let mut role_repo = MockUserRoleRepo::new();
784 role_repo.expect_create().returning(|a| Ok(a.clone()));
785
786 let uc = build_use_cases(user_repo, refresh_repo, role_repo);
787
788 let result = uc
789 .register(RegisterRequest {
790 email: "new@example.com".to_string(),
791 password: "password123".to_string(),
792 first_name: "Alice".to_string(),
793 last_name: "Dupont".to_string(),
794 role: "syndic".to_string(),
795 organization_id: None,
796 })
797 .await;
798
799 assert!(
800 result.is_ok(),
801 "register should succeed: {:?}",
802 result.err()
803 );
804 let response = result.unwrap();
805 assert!(!response.token.is_empty());
806 assert_eq!(response.user.email, "new@example.com");
807 assert_eq!(response.user.first_name, "Alice");
808 }
809
810 #[tokio::test]
813 async fn test_register_duplicate_email() {
814 let existing = make_user("taken@example.com", "pw123456", None);
815 let existing_clone = existing.clone();
816
817 let mut user_repo = MockUserRepo::new();
818 user_repo
819 .expect_find_by_email()
820 .returning(move |_| Ok(Some(existing_clone.clone())));
821
822 let uc = build_use_cases(
823 user_repo,
824 MockRefreshTokenRepo::new(),
825 MockUserRoleRepo::new(),
826 );
827
828 let result = uc
829 .register(RegisterRequest {
830 email: "taken@example.com".to_string(),
831 password: "password123".to_string(),
832 first_name: "Bob".to_string(),
833 last_name: "Martin".to_string(),
834 role: "owner".to_string(),
835 organization_id: None,
836 })
837 .await;
838
839 assert!(result.is_err());
840 assert_eq!(result.unwrap_err(), "Email already exists");
841 }
842
843 #[tokio::test]
846 async fn test_switch_role_success() {
847 let org_id = Some(Uuid::new_v4());
848 let user = make_user("multi@example.com", "password123", org_id);
849 let user_clone = user.clone();
850
851 let target_role = UserRoleAssignment::new(user.id, UserRole::Accountant, org_id, false);
853 let target_role_id = target_role.id;
854 let target_clone = target_role.clone();
855
856 let mut switched_role = target_role.clone();
858 switched_role.is_primary = true;
859 let switched_clone = switched_role.clone();
860 let switched_for_list = switched_role.clone();
861
862 let mut user_repo = MockUserRepo::new();
863 user_repo
864 .expect_find_by_id()
865 .returning(move |_| Ok(Some(user_clone.clone())));
866 user_repo.expect_update().returning(|u| Ok(u.clone()));
867
868 let mut refresh_repo = MockRefreshTokenRepo::new();
869 refresh_repo.expect_create().returning(|rt| Ok(rt.clone()));
870
871 let mut role_repo = MockUserRoleRepo::new();
872 role_repo
873 .expect_find_by_id()
874 .returning(move |_| Ok(Some(target_clone.clone())));
875 role_repo
876 .expect_set_primary_role()
877 .returning(move |_, _| Ok(switched_clone.clone()));
878 role_repo
879 .expect_list_for_user()
880 .returning(move |_| Ok(vec![switched_for_list.clone()]));
881
882 let uc = build_use_cases(user_repo, refresh_repo, role_repo);
883
884 let result = uc.switch_active_role(user.id, target_role_id).await;
885
886 assert!(
887 result.is_ok(),
888 "switch_role should succeed: {:?}",
889 result.err()
890 );
891 let response = result.unwrap();
892 assert!(!response.token.is_empty());
893 assert_eq!(response.user.role, "accountant");
894 }
895
896 #[tokio::test]
899 async fn test_switch_role_not_found() {
900 let user = make_user("user@example.com", "password123", None);
901 let user_clone = user.clone();
902
903 let mut user_repo = MockUserRepo::new();
904 user_repo
905 .expect_find_by_id()
906 .returning(move |_| Ok(Some(user_clone.clone())));
907
908 let mut role_repo = MockUserRoleRepo::new();
909 role_repo.expect_find_by_id().returning(|_| Ok(None)); let uc = build_use_cases(user_repo, MockRefreshTokenRepo::new(), role_repo);
912
913 let result = uc.switch_active_role(user.id, Uuid::new_v4()).await;
914
915 assert!(result.is_err());
916 assert_eq!(result.unwrap_err(), "Role assignment not found");
917 }
918
919 #[tokio::test]
922 async fn test_verify_token_valid() {
923 let org_id = Some(Uuid::new_v4());
924 let user = make_user("verify@example.com", "password123", org_id);
925 let role = make_primary_role(user.id, org_id);
926
927 let uc = build_use_cases(
928 MockUserRepo::new(),
929 MockRefreshTokenRepo::new(),
930 MockUserRoleRepo::new(),
931 );
932
933 let token = uc.generate_token(&user, &role).expect("token generation");
935
936 let claims = uc.verify_token(&token);
937 assert!(claims.is_ok(), "verify should succeed: {:?}", claims.err());
938 let claims = claims.unwrap();
939 assert_eq!(claims.sub, user.id.to_string());
940 assert_eq!(claims.email, "verify@example.com");
941 assert_eq!(claims.role, "syndic");
942 assert_eq!(claims.organization_id, org_id);
943 }
944
945 #[tokio::test]
948 async fn test_verify_token_invalid() {
949 let uc = build_use_cases(
950 MockUserRepo::new(),
951 MockRefreshTokenRepo::new(),
952 MockUserRoleRepo::new(),
953 );
954
955 let result = uc.verify_token("this.is.not.a.valid.jwt");
956 assert!(
958 matches!(result, Err(AppError::TokenError(_))),
959 "expected AppError::TokenError, got {:?}",
960 result
961 );
962 }
963
964 #[tokio::test]
967 async fn test_revoke_all_refresh_tokens() {
968 let user_id = Uuid::new_v4();
969
970 let mut refresh_repo = MockRefreshTokenRepo::new();
971 refresh_repo
972 .expect_revoke_all_for_user()
973 .withf(move |id| *id == user_id)
974 .returning(|_| Ok(3));
975
976 let uc = build_use_cases(MockUserRepo::new(), refresh_repo, MockUserRoleRepo::new());
977
978 let result = uc.revoke_all_refresh_tokens(user_id).await;
979 assert!(result.is_ok());
980 assert_eq!(result.unwrap(), 3);
981 }
982 #[tokio::test(flavor = "current_thread")]
1007 async fn le_hachage_ne_retient_pas_la_boucle_devenements() {
1008 use std::sync::atomic::{AtomicUsize, Ordering};
1009 use std::sync::Arc as StdArc;
1010
1011 let tours = StdArc::new(AtomicUsize::new(0));
1012 let compteur = tours.clone();
1013
1014 let temoin = tokio::spawn(async move {
1015 for _ in 0..100_000 {
1019 compteur.fetch_add(1, Ordering::Relaxed);
1020 tokio::task::yield_now().await;
1021 }
1022 });
1023
1024 let empreinte = hacher_hors_worker("mot-de-passe-temoin".to_string())
1025 .await
1026 .expect("le hachage doit réussir");
1027
1028 let avances = tours.load(Ordering::Relaxed);
1029 temoin.abort();
1030
1031 assert!(
1032 avances > 0,
1033 "La tâche témoin n'a pas avancé d'un seul tour pendant le hachage : \
1034 bcrypt tient le thread de travail (#718). Sur un runtime à un \
1035 thread, c'est exactement ce qui fait rendre 502 à Traefik quand \
1036 une deuxième requête arrive."
1037 );
1038
1039 let compteur2 = StdArc::new(AtomicUsize::new(0));
1041 let c2 = compteur2.clone();
1042 let temoin2 = tokio::spawn(async move {
1043 for _ in 0..100_000 {
1044 c2.fetch_add(1, Ordering::Relaxed);
1045 tokio::task::yield_now().await;
1046 }
1047 });
1048
1049 let valide = verifier_hors_worker("mot-de-passe-temoin".to_string(), empreinte)
1050 .await
1051 .expect("la vérification doit réussir");
1052 let avances2 = compteur2.load(Ordering::Relaxed);
1053 temoin2.abort();
1054
1055 assert!(valide, "l'empreinte fraîchement produite doit se vérifier");
1056 assert!(
1057 avances2 > 0,
1058 "La tâche témoin n'a pas avancé pendant la vérification : \
1059 `verify` tient le thread de travail (#718). Le login est le \
1060 chemin le plus chaud du produit."
1061 );
1062 }
1063}