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