Skip to main content

koprogo_api/application/use_cases/
auth_use_cases.rs

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
15pub struct AuthUseCases {
16    user_repo: Arc<dyn UserRepository>,
17    refresh_token_repo: Arc<dyn RefreshTokenRepository>,
18    user_role_repo: Arc<dyn UserRoleRepository>,
19    jwt_secret: String,
20}
21
22impl AuthUseCases {
23    pub fn new(
24        user_repo: Arc<dyn UserRepository>,
25        refresh_token_repo: Arc<dyn RefreshTokenRepository>,
26        user_role_repo: Arc<dyn UserRoleRepository>,
27        jwt_secret: String,
28    ) -> Self {
29        Self {
30            user_repo,
31            refresh_token_repo,
32            user_role_repo,
33            jwt_secret,
34        }
35    }
36
37    pub async fn login(&self, request: LoginRequest) -> Result<LoginResponse, AppError> {
38        let user = self
39            .user_repo
40            .find_by_email(&request.email)
41            .await?
42            .ok_or_else(|| {
43                // Audit failed login attempt
44                tokio::spawn(async move {
45                    log_audit_event(
46                        AuditEventType::AuthenticationFailed,
47                        None,
48                        None,
49                        Some(format!("Failed login attempt for email: {}", request.email)),
50                        None,
51                    )
52                    .await;
53                });
54                // Uniform InvalidCredentials prevents username enumeration.
55                AppError::InvalidCredentials
56            })?;
57
58        if !user.is_active {
59            // Audit login attempt on deactivated account
60            let user_id = user.id;
61            tokio::spawn(async move {
62                log_audit_event(
63                    AuditEventType::AuthenticationFailed,
64                    Some(user_id),
65                    None,
66                    Some("Login attempt on deactivated account".to_string()),
67                    None,
68                )
69                .await;
70            });
71            return Err(AppError::AccountDeactivated);
72        }
73
74        let is_valid = verify(&request.password, &user.password_hash)?;
75
76        if !is_valid {
77            // Audit failed password verification
78            let user_id = user.id;
79            tokio::spawn(async move {
80                log_audit_event(
81                    AuditEventType::AuthenticationFailed,
82                    Some(user_id),
83                    None,
84                    Some("Invalid password".to_string()),
85                    None,
86                )
87                .await;
88            });
89            return Err(AppError::InvalidCredentials);
90        }
91
92        let (roles, active_role) = self.ensure_role_assignments(&user).await?;
93        let user_for_token = self.apply_active_role_metadata(&user, &active_role).await?;
94        let token = self.generate_token(&user_for_token, &active_role)?;
95
96        let refresh_token_string = self.generate_refresh_token_string(&user_for_token);
97        let refresh_token = RefreshToken::new(user_for_token.id, refresh_token_string.clone());
98        self.refresh_token_repo.create(&refresh_token).await?;
99
100        // Audit successful login
101        let user_id = user_for_token.id;
102        let organization_id = active_role.organization_id;
103        tokio::spawn(async move {
104            log_audit_event(
105                AuditEventType::UserLogin,
106                Some(user_id),
107                organization_id,
108                Some("Successful login".to_string()),
109                None,
110            )
111            .await;
112        });
113
114        Ok(LoginResponse {
115            token,
116            refresh_token: refresh_token_string,
117            user: self.build_user_response(&user_for_token, &roles, &active_role),
118        })
119    }
120
121    pub async fn register(&self, request: RegisterRequest) -> Result<LoginResponse, String> {
122        if (self.user_repo.find_by_email(&request.email).await?).is_some() {
123            return Err("Email already exists".to_string());
124        }
125
126        let role: UserRole = request
127            .role
128            .parse()
129            .map_err(|e| format!("Invalid role: {}", e))?;
130
131        let password_hash = hash(&request.password, DEFAULT_COST)
132            .map_err(|e| format!("Failed to hash password: {}", e))?;
133
134        let user = User::new(
135            request.email,
136            password_hash,
137            request.first_name,
138            request.last_name,
139            role.clone(),
140            request.organization_id,
141        )?;
142
143        let created_user = self.user_repo.create(&user).await?;
144
145        // Create primary role assignment
146        let primary_assignment = self
147            .user_role_repo
148            .create(&UserRoleAssignment::new(
149                created_user.id,
150                role,
151                created_user.organization_id,
152                true,
153            ))
154            .await?;
155        let roles = vec![primary_assignment.clone()];
156        let user_for_token = self
157            .apply_active_role_metadata(&created_user, &primary_assignment)
158            .await?;
159
160        let token = self.generate_token(&user_for_token, &primary_assignment)?;
161        let refresh_token_string = self.generate_refresh_token_string(&user_for_token);
162        let refresh_token = RefreshToken::new(user_for_token.id, refresh_token_string.clone());
163        self.refresh_token_repo.create(&refresh_token).await?;
164
165        // Audit successful registration
166        let user_id = created_user.id;
167        let organization_id = created_user.organization_id;
168        let email = created_user.email.clone();
169        tokio::spawn(async move {
170            log_audit_event(
171                AuditEventType::UserRegistration,
172                Some(user_id),
173                organization_id,
174                Some(format!("New user registered: {}", email)),
175                None,
176            )
177            .await;
178        });
179
180        Ok(LoginResponse {
181            token,
182            refresh_token: refresh_token_string,
183            user: self.build_user_response(&user_for_token, &roles, &primary_assignment),
184        })
185    }
186
187    pub async fn switch_active_role(
188        &self,
189        user_id: Uuid,
190        role_id: Uuid,
191    ) -> Result<LoginResponse, String> {
192        let user = self
193            .user_repo
194            .find_by_id(user_id)
195            .await?
196            .ok_or("User not found")?;
197
198        if !user.is_active {
199            return Err("User account is deactivated".to_string());
200        }
201
202        let target_role = self
203            .user_role_repo
204            .find_by_id(role_id)
205            .await?
206            .ok_or("Role assignment not found")?;
207
208        if target_role.user_id != user.id {
209            return Err("Role assignment does not belong to user".to_string());
210        }
211
212        let updated_primary = self
213            .user_role_repo
214            .set_primary_role(user.id, role_id)
215            .await?;
216
217        let roles = self.user_role_repo.list_for_user(user.id).await?;
218        let active_role = roles
219            .iter()
220            .find(|assignment| assignment.is_primary)
221            .cloned()
222            .unwrap_or(updated_primary.clone());
223
224        let updated_user = self.apply_active_role_metadata(&user, &active_role).await?;
225
226        let token = self.generate_token(&updated_user, &active_role)?;
227        let refresh_token_string = self.generate_refresh_token_string(&updated_user);
228        let refresh_token = RefreshToken::new(updated_user.id, refresh_token_string.clone());
229        self.refresh_token_repo.create(&refresh_token).await?;
230
231        Ok(LoginResponse {
232            token,
233            refresh_token: refresh_token_string,
234            user: self.build_user_response(&updated_user, &roles, &active_role),
235        })
236    }
237
238    pub async fn get_user_by_id(&self, user_id: uuid::Uuid) -> Result<UserResponse, AppError> {
239        let user = self
240            .user_repo
241            .find_by_id(user_id)
242            .await?
243            .ok_or_else(|| AppError::NotFound(format!("user {}", user_id)))?;
244
245        let (roles, active_role) = self.ensure_role_assignments(&user).await?;
246        Ok(self.build_user_response(&user, &roles, &active_role))
247    }
248
249    pub fn verify_token(&self, token: &str) -> Result<Claims, AppError> {
250        use jsonwebtoken::{decode, DecodingKey, Validation};
251
252        let token_data = decode::<Claims>(
253            token,
254            &DecodingKey::from_secret(self.jwt_secret.as_bytes()),
255            &Validation::default(),
256        )?;
257
258        Ok(token_data.claims)
259    }
260
261    pub async fn refresh_token(
262        &self,
263        request: RefreshTokenRequest,
264    ) -> Result<LoginResponse, AppError> {
265        let refresh_token = self
266            .refresh_token_repo
267            .find_by_token(&request.refresh_token)
268            .await?
269            .ok_or_else(|| {
270                // Audit invalid refresh token attempt
271                tokio::spawn(async {
272                    log_audit_event(
273                        AuditEventType::InvalidToken,
274                        None,
275                        None,
276                        Some("Invalid refresh token attempted".to_string()),
277                        None,
278                    )
279                    .await;
280                });
281                AppError::TokenError("invalid refresh token".to_string())
282            })?;
283
284        if !refresh_token.is_valid() {
285            // Audit expired/revoked token attempt
286            let user_id = refresh_token.user_id;
287            let reason = if refresh_token.is_expired() {
288                "Expired refresh token"
289            } else {
290                "Revoked refresh token"
291            };
292            tokio::spawn(async move {
293                log_audit_event(
294                    AuditEventType::InvalidToken,
295                    Some(user_id),
296                    None,
297                    Some(format!("{} attempted", reason)),
298                    None,
299                )
300                .await;
301            });
302            return Err(AppError::TokenError(
303                "refresh token expired or revoked".to_string(),
304            ));
305        }
306
307        let user = self
308            .user_repo
309            .find_by_id(refresh_token.user_id)
310            .await?
311            .ok_or_else(|| AppError::NotFound("user for refresh token".to_string()))?;
312
313        if !user.is_active {
314            // Audit refresh attempt on deactivated account
315            let user_id = user.id;
316            tokio::spawn(async move {
317                log_audit_event(
318                    AuditEventType::AuthenticationFailed,
319                    Some(user_id),
320                    None,
321                    Some("Refresh token attempt on deactivated account".to_string()),
322                    None,
323                )
324                .await;
325            });
326            return Err(AppError::AccountDeactivated);
327        }
328
329        let (roles, active_role) = self.ensure_role_assignments(&user).await?;
330        let user_for_token = self.apply_active_role_metadata(&user, &active_role).await?;
331        let token = self.generate_token(&user_for_token, &active_role)?;
332
333        // Revoke old token (refresh token rotation)
334        self.refresh_token_repo
335            .revoke(&request.refresh_token)
336            .await?;
337
338        let new_refresh_token_string = self.generate_refresh_token_string(&user_for_token);
339        let new_refresh_token =
340            RefreshToken::new(user_for_token.id, new_refresh_token_string.clone());
341        self.refresh_token_repo.create(&new_refresh_token).await?;
342
343        // Audit successful token refresh
344        let user_id = user_for_token.id;
345        let organization_id = active_role.organization_id;
346        tokio::spawn(async move {
347            log_audit_event(
348                AuditEventType::TokenRefresh,
349                Some(user_id),
350                organization_id,
351                Some("Refresh token successfully exchanged".to_string()),
352                None,
353            )
354            .await;
355        });
356
357        Ok(LoginResponse {
358            token,
359            refresh_token: new_refresh_token_string,
360            user: self.build_user_response(&user_for_token, &roles, &active_role),
361        })
362    }
363
364    pub async fn revoke_all_refresh_tokens(&self, user_id: Uuid) -> Result<u64, String> {
365        self.refresh_token_repo.revoke_all_for_user(user_id).await
366    }
367
368    fn build_user_response(
369        &self,
370        user: &User,
371        roles: &[UserRoleAssignment],
372        active_role: &UserRoleAssignment,
373    ) -> UserResponse {
374        UserResponse {
375            id: user.id,
376            email: user.email.clone(),
377            first_name: user.first_name.clone(),
378            last_name: user.last_name.clone(),
379            role: active_role.role.to_string(),
380            organization_id: active_role.organization_id,
381            is_active: user.is_active,
382            roles: roles.iter().map(Self::summarize_role).collect(),
383            active_role: Some(Self::summarize_role(active_role)),
384        }
385    }
386
387    async fn ensure_role_assignments(
388        &self,
389        user: &User,
390    ) -> Result<(Vec<UserRoleAssignment>, UserRoleAssignment), String> {
391        let mut assignments = self.user_role_repo.list_for_user(user.id).await?;
392
393        if assignments.is_empty() {
394            let assignment = self
395                .user_role_repo
396                .create(&UserRoleAssignment::new(
397                    user.id,
398                    user.role.clone(),
399                    user.organization_id,
400                    true,
401                ))
402                .await?;
403            assignments.push(assignment.clone());
404        }
405
406        if !assignments.iter().any(|assignment| assignment.is_primary) {
407            let first = assignments[0].id;
408            self.user_role_repo.set_primary_role(user.id, first).await?;
409            assignments = self.user_role_repo.list_for_user(user.id).await?;
410        }
411
412        let active = assignments
413            .iter()
414            .find(|assignment| assignment.is_primary)
415            .cloned()
416            .unwrap_or_else(|| assignments[0].clone());
417
418        Ok((assignments, active))
419    }
420
421    async fn apply_active_role_metadata(
422        &self,
423        user: &User,
424        active_role: &UserRoleAssignment,
425    ) -> Result<User, String> {
426        let mut updated_user = user.clone();
427        let mut requires_update = false;
428
429        if updated_user.role != active_role.role {
430            updated_user.role = active_role.role.clone();
431            requires_update = true;
432        }
433
434        if updated_user.organization_id != active_role.organization_id {
435            updated_user.organization_id = active_role.organization_id;
436            requires_update = true;
437        }
438
439        if requires_update {
440            updated_user.updated_at = Utc::now();
441            return self.user_repo.update(&updated_user).await;
442        }
443
444        Ok(updated_user)
445    }
446
447    fn summarize_role(assignment: &UserRoleAssignment) -> UserRoleSummary {
448        UserRoleSummary {
449            id: assignment.id,
450            role: assignment.role.to_string(),
451            organization_id: assignment.organization_id,
452            is_primary: assignment.is_primary,
453        }
454    }
455
456    fn generate_token(
457        &self,
458        user: &User,
459        active_role: &UserRoleAssignment,
460    ) -> Result<String, String> {
461        let now = Utc::now().timestamp();
462        let expiration = now + (15 * 60);
463
464        let claims = Claims {
465            sub: user.id.to_string(),
466            email: user.email.clone(),
467            role: active_role.role.to_string(),
468            organization_id: active_role.organization_id,
469            role_id: Some(active_role.id),
470            exp: expiration,
471            iat: now,
472        };
473
474        encode(
475            &Header::default(),
476            &claims,
477            &EncodingKey::from_secret(self.jwt_secret.as_bytes()),
478        )
479        .map_err(|e| format!("Failed to generate token: {}", e))
480    }
481
482    fn generate_refresh_token_string(&self, user: &User) -> String {
483        let now = Utc::now().timestamp();
484        format!("{}:{}:{}", user.id, now, uuid::Uuid::new_v4())
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491    use crate::application::ports::{RefreshTokenRepository, UserRoleRepository};
492    use crate::domain::entities::{RefreshToken, User, UserRole, UserRoleAssignment};
493    use async_trait::async_trait;
494    use mockall::mock;
495    use std::sync::Arc;
496
497    // Re-use the existing MockUserRepo from the user_repository port
498    use crate::application::ports::user_repository::MockUserRepo;
499
500    // Create mock for RefreshTokenRepository (not defined elsewhere)
501    mock! {
502        pub RefreshTokenRepo {}
503
504        #[async_trait]
505        impl RefreshTokenRepository for RefreshTokenRepo {
506            async fn create(&self, refresh_token: &RefreshToken) -> Result<RefreshToken, String>;
507            async fn find_by_token(&self, token: &str) -> Result<Option<RefreshToken>, String>;
508            async fn find_by_user_id(&self, user_id: Uuid) -> Result<Vec<RefreshToken>, String>;
509            async fn revoke(&self, token: &str) -> Result<bool, String>;
510            async fn revoke_all_for_user(&self, user_id: Uuid) -> Result<u64, String>;
511            async fn delete_expired(&self) -> Result<u64, String>;
512        }
513    }
514
515    // Create mock for UserRoleRepository (not defined elsewhere)
516    mock! {
517        pub UserRoleRepo {}
518
519        #[async_trait]
520        impl UserRoleRepository for UserRoleRepo {
521            async fn create(&self, assignment: &UserRoleAssignment) -> Result<UserRoleAssignment, String>;
522            async fn list_for_user(&self, user_id: Uuid) -> Result<Vec<UserRoleAssignment>, String>;
523            async fn list_for_users(&self, user_ids: &[Uuid]) -> Result<std::collections::HashMap<Uuid, Vec<UserRoleAssignment>>, String>;
524            async fn replace_all(&self, user_id: Uuid, assignments: &[UserRoleAssignment]) -> Result<(), String>;
525            async fn find_by_id(&self, id: Uuid) -> Result<Option<UserRoleAssignment>, String>;
526            async fn set_primary_role(&self, user_id: Uuid, role_id: Uuid) -> Result<UserRoleAssignment, String>;
527        }
528    }
529
530    const TEST_JWT_SECRET: &str = "test-secret-key-that-is-long-enough-for-jwt";
531
532    /// Helper: create a valid active User with a bcrypt-hashed password.
533    fn make_user(email: &str, password: &str, org_id: Option<Uuid>) -> User {
534        let hash = bcrypt::hash(password, 4).expect("bcrypt hash");
535        User::new(
536            email.to_string(),
537            hash,
538            "Test".to_string(),
539            "User".to_string(),
540            UserRole::Syndic,
541            org_id,
542        )
543        .expect("valid user")
544    }
545
546    /// Helper: create a UserRoleAssignment marked as primary for a given user.
547    fn make_primary_role(user_id: Uuid, org_id: Option<Uuid>) -> UserRoleAssignment {
548        UserRoleAssignment::new(user_id, UserRole::Syndic, org_id, true)
549    }
550
551    /// Build an AuthUseCases from the three mocks.
552    fn build_use_cases(
553        user_repo: MockUserRepo,
554        refresh_repo: MockRefreshTokenRepo,
555        role_repo: MockUserRoleRepo,
556    ) -> AuthUseCases {
557        AuthUseCases::new(
558            Arc::new(user_repo),
559            Arc::new(refresh_repo),
560            Arc::new(role_repo),
561            TEST_JWT_SECRET.to_string(),
562        )
563    }
564
565    // ── 1. login success ────────────────────────────────────────────────
566
567    #[tokio::test]
568    async fn test_login_success() {
569        let org_id = Some(Uuid::new_v4());
570        let user = make_user("login@example.com", "password123", org_id);
571        let user_clone = user.clone();
572        let _user_for_update = user.clone();
573        let role = make_primary_role(user.id, org_id);
574        let role_clone = role.clone();
575
576        let mut user_repo = MockUserRepo::new();
577        user_repo
578            .expect_find_by_email()
579            .withf(|e| e == "login@example.com")
580            .returning(move |_| Ok(Some(user_clone.clone())));
581        user_repo.expect_update().returning(move |u| Ok(u.clone()));
582
583        let mut refresh_repo = MockRefreshTokenRepo::new();
584        refresh_repo.expect_create().returning(|rt| Ok(rt.clone()));
585
586        let mut role_repo = MockUserRoleRepo::new();
587        role_repo
588            .expect_list_for_user()
589            .returning(move |_| Ok(vec![role_clone.clone()]));
590
591        let uc = build_use_cases(user_repo, refresh_repo, role_repo);
592
593        let result = uc
594            .login(LoginRequest {
595                email: "login@example.com".to_string(),
596                password: "password123".to_string(),
597            })
598            .await;
599
600        assert!(result.is_ok(), "login should succeed: {:?}", result.err());
601        let response = result.unwrap();
602        assert!(!response.token.is_empty());
603        assert!(!response.refresh_token.is_empty());
604        assert_eq!(response.user.email, "login@example.com");
605        assert!(response.user.is_active);
606    }
607
608    // ── 2. login — user not found ───────────────────────────────────────
609
610    #[tokio::test]
611    async fn test_login_invalid_email() {
612        let mut user_repo = MockUserRepo::new();
613        user_repo.expect_find_by_email().returning(|_| Ok(None));
614
615        let uc = build_use_cases(
616            user_repo,
617            MockRefreshTokenRepo::new(),
618            MockUserRoleRepo::new(),
619        );
620
621        let result = uc
622            .login(LoginRequest {
623                email: "nonexistent@example.com".to_string(),
624                password: "whatever".to_string(),
625            })
626            .await;
627
628        // @security : doit être InvalidCredentials (uniforme avec wrong password
629        // pour ne pas leaker l'existence du compte).
630        assert!(
631            matches!(result, Err(AppError::InvalidCredentials)),
632            "expected AppError::InvalidCredentials, got {:?}",
633            result
634        );
635    }
636
637    // ── 3. login — wrong password ───────────────────────────────────────
638
639    #[tokio::test]
640    async fn test_login_invalid_password() {
641        let user = make_user("user@example.com", "correct_password", None);
642        let user_clone = user.clone();
643
644        let mut user_repo = MockUserRepo::new();
645        user_repo
646            .expect_find_by_email()
647            .returning(move |_| Ok(Some(user_clone.clone())));
648
649        let uc = build_use_cases(
650            user_repo,
651            MockRefreshTokenRepo::new(),
652            MockUserRoleRepo::new(),
653        );
654
655        let result = uc
656            .login(LoginRequest {
657                email: "user@example.com".to_string(),
658                password: "wrong_password".to_string(),
659            })
660            .await;
661
662        // @security : InvalidCredentials uniforme (anti-énumération).
663        assert!(
664            matches!(result, Err(AppError::InvalidCredentials)),
665            "expected AppError::InvalidCredentials, got {:?}",
666            result
667        );
668    }
669
670    // ── 4. login — deactivated account ──────────────────────────────────
671
672    #[tokio::test]
673    async fn test_login_deactivated_account() {
674        let mut user = make_user("deactivated@example.com", "password123", None);
675        user.deactivate();
676        let user_clone = user.clone();
677
678        let mut user_repo = MockUserRepo::new();
679        user_repo
680            .expect_find_by_email()
681            .returning(move |_| Ok(Some(user_clone.clone())));
682
683        let uc = build_use_cases(
684            user_repo,
685            MockRefreshTokenRepo::new(),
686            MockUserRoleRepo::new(),
687        );
688
689        let result = uc
690            .login(LoginRequest {
691                email: "deactivated@example.com".to_string(),
692                password: "password123".to_string(),
693            })
694            .await;
695
696        // @security : AccountDeactivated → 403 Forbidden (cf. AppError::status_code).
697        // Note: déballer cette info VS uniforme InvalidCredentials est un trade-off
698        // sécurité (énumération potentielle). À reconsidérer en RFC dédié.
699        assert!(
700            matches!(result, Err(AppError::AccountDeactivated)),
701            "expected AppError::AccountDeactivated, got {:?}",
702            result
703        );
704    }
705
706    // ── 5. register success ─────────────────────────────────────────────
707
708    #[tokio::test]
709    async fn test_register_success() {
710        let mut user_repo = MockUserRepo::new();
711        user_repo.expect_find_by_email().returning(|_| Ok(None)); // email not taken
712        user_repo.expect_create().returning(|u| Ok(u.clone()));
713        user_repo.expect_update().returning(|u| Ok(u.clone()));
714
715        let mut refresh_repo = MockRefreshTokenRepo::new();
716        refresh_repo.expect_create().returning(|rt| Ok(rt.clone()));
717
718        let mut role_repo = MockUserRoleRepo::new();
719        role_repo.expect_create().returning(|a| Ok(a.clone()));
720
721        let uc = build_use_cases(user_repo, refresh_repo, role_repo);
722
723        let result = uc
724            .register(RegisterRequest {
725                email: "new@example.com".to_string(),
726                password: "password123".to_string(),
727                first_name: "Alice".to_string(),
728                last_name: "Dupont".to_string(),
729                role: "syndic".to_string(),
730                organization_id: None,
731            })
732            .await;
733
734        assert!(
735            result.is_ok(),
736            "register should succeed: {:?}",
737            result.err()
738        );
739        let response = result.unwrap();
740        assert!(!response.token.is_empty());
741        assert_eq!(response.user.email, "new@example.com");
742        assert_eq!(response.user.first_name, "Alice");
743    }
744
745    // ── 6. register — duplicate email ───────────────────────────────────
746
747    #[tokio::test]
748    async fn test_register_duplicate_email() {
749        let existing = make_user("taken@example.com", "pw123456", None);
750        let existing_clone = existing.clone();
751
752        let mut user_repo = MockUserRepo::new();
753        user_repo
754            .expect_find_by_email()
755            .returning(move |_| Ok(Some(existing_clone.clone())));
756
757        let uc = build_use_cases(
758            user_repo,
759            MockRefreshTokenRepo::new(),
760            MockUserRoleRepo::new(),
761        );
762
763        let result = uc
764            .register(RegisterRequest {
765                email: "taken@example.com".to_string(),
766                password: "password123".to_string(),
767                first_name: "Bob".to_string(),
768                last_name: "Martin".to_string(),
769                role: "owner".to_string(),
770                organization_id: None,
771            })
772            .await;
773
774        assert!(result.is_err());
775        assert_eq!(result.unwrap_err(), "Email already exists");
776    }
777
778    // ── 7. switch_active_role success ───────────────────────────────────
779
780    #[tokio::test]
781    async fn test_switch_role_success() {
782        let org_id = Some(Uuid::new_v4());
783        let user = make_user("multi@example.com", "password123", org_id);
784        let user_clone = user.clone();
785
786        // The target role that we want to switch to
787        let target_role = UserRoleAssignment::new(user.id, UserRole::Accountant, org_id, false);
788        let target_role_id = target_role.id;
789        let target_clone = target_role.clone();
790
791        // After switching, the target role becomes primary
792        let mut switched_role = target_role.clone();
793        switched_role.is_primary = true;
794        let switched_clone = switched_role.clone();
795        let switched_for_list = switched_role.clone();
796
797        let mut user_repo = MockUserRepo::new();
798        user_repo
799            .expect_find_by_id()
800            .returning(move |_| Ok(Some(user_clone.clone())));
801        user_repo.expect_update().returning(|u| Ok(u.clone()));
802
803        let mut refresh_repo = MockRefreshTokenRepo::new();
804        refresh_repo.expect_create().returning(|rt| Ok(rt.clone()));
805
806        let mut role_repo = MockUserRoleRepo::new();
807        role_repo
808            .expect_find_by_id()
809            .returning(move |_| Ok(Some(target_clone.clone())));
810        role_repo
811            .expect_set_primary_role()
812            .returning(move |_, _| Ok(switched_clone.clone()));
813        role_repo
814            .expect_list_for_user()
815            .returning(move |_| Ok(vec![switched_for_list.clone()]));
816
817        let uc = build_use_cases(user_repo, refresh_repo, role_repo);
818
819        let result = uc.switch_active_role(user.id, target_role_id).await;
820
821        assert!(
822            result.is_ok(),
823            "switch_role should succeed: {:?}",
824            result.err()
825        );
826        let response = result.unwrap();
827        assert!(!response.token.is_empty());
828        assert_eq!(response.user.role, "accountant");
829    }
830
831    // ── 8. switch_active_role — role not found ──────────────────────────
832
833    #[tokio::test]
834    async fn test_switch_role_not_found() {
835        let user = make_user("user@example.com", "password123", None);
836        let user_clone = user.clone();
837
838        let mut user_repo = MockUserRepo::new();
839        user_repo
840            .expect_find_by_id()
841            .returning(move |_| Ok(Some(user_clone.clone())));
842
843        let mut role_repo = MockUserRoleRepo::new();
844        role_repo.expect_find_by_id().returning(|_| Ok(None)); // role not found
845
846        let uc = build_use_cases(user_repo, MockRefreshTokenRepo::new(), role_repo);
847
848        let result = uc.switch_active_role(user.id, Uuid::new_v4()).await;
849
850        assert!(result.is_err());
851        assert_eq!(result.unwrap_err(), "Role assignment not found");
852    }
853
854    // ── 9. verify_token — valid token ───────────────────────────────────
855
856    #[tokio::test]
857    async fn test_verify_token_valid() {
858        let org_id = Some(Uuid::new_v4());
859        let user = make_user("verify@example.com", "password123", org_id);
860        let role = make_primary_role(user.id, org_id);
861
862        let uc = build_use_cases(
863            MockUserRepo::new(),
864            MockRefreshTokenRepo::new(),
865            MockUserRoleRepo::new(),
866        );
867
868        // Generate a token using the same secret
869        let token = uc.generate_token(&user, &role).expect("token generation");
870
871        let claims = uc.verify_token(&token);
872        assert!(claims.is_ok(), "verify should succeed: {:?}", claims.err());
873        let claims = claims.unwrap();
874        assert_eq!(claims.sub, user.id.to_string());
875        assert_eq!(claims.email, "verify@example.com");
876        assert_eq!(claims.role, "syndic");
877        assert_eq!(claims.organization_id, org_id);
878    }
879
880    // ── 10. verify_token — invalid token ────────────────────────────────
881
882    #[tokio::test]
883    async fn test_verify_token_invalid() {
884        let uc = build_use_cases(
885            MockUserRepo::new(),
886            MockRefreshTokenRepo::new(),
887            MockUserRoleRepo::new(),
888        );
889
890        let result = uc.verify_token("this.is.not.a.valid.jwt");
891        // @negative : token invalide → AppError::TokenError (mappé 401 par ResponseError).
892        assert!(
893            matches!(result, Err(AppError::TokenError(_))),
894            "expected AppError::TokenError, got {:?}",
895            result
896        );
897    }
898
899    // ── 11. revoke_all_refresh_tokens ───────────────────────────────────
900
901    #[tokio::test]
902    async fn test_revoke_all_refresh_tokens() {
903        let user_id = Uuid::new_v4();
904
905        let mut refresh_repo = MockRefreshTokenRepo::new();
906        refresh_repo
907            .expect_revoke_all_for_user()
908            .withf(move |id| *id == user_id)
909            .returning(|_| Ok(3));
910
911        let uc = build_use_cases(MockUserRepo::new(), refresh_repo, MockUserRoleRepo::new());
912
913        let result = uc.revoke_all_refresh_tokens(user_id).await;
914        assert!(result.is_ok());
915        assert_eq!(result.unwrap(), 3);
916    }
917}