1use crate::application::error::AppError;
2use crate::application::ports::{UserRepository, UserRoleRepository};
3use crate::domain::entities::{User, UserRole, UserRoleAssignment};
4use chrono::{DateTime, Utc};
5use serde::Serialize;
6use std::collections::HashMap;
7use std::sync::Arc;
8use uuid::Uuid;
9
10#[derive(Serialize, Clone)]
11pub struct RoleResponse {
12 pub id: String,
13 pub role: String,
14 pub organization_id: Option<String>,
15 pub is_primary: bool,
16}
17
18#[derive(Serialize, Clone)]
19pub struct UserResponse {
20 pub id: String,
21 pub email: String,
22 pub first_name: String,
23 pub last_name: String,
24 pub role: String,
25 pub organization_id: Option<String>,
26 pub is_active: bool,
27 pub created_at: DateTime<Utc>,
28 pub roles: Vec<RoleResponse>,
29 pub active_role: Option<RoleResponse>,
30}
31
32pub struct UserUseCases {
33 user_repo: Arc<dyn UserRepository>,
34 role_repo: Arc<dyn UserRoleRepository>,
35}
36
37impl UserUseCases {
38 pub fn new(user_repo: Arc<dyn UserRepository>, role_repo: Arc<dyn UserRoleRepository>) -> Self {
39 Self {
40 user_repo,
41 role_repo,
42 }
43 }
44
45 fn to_role_response(a: &UserRoleAssignment) -> RoleResponse {
46 RoleResponse {
47 id: a.id.to_string(),
48 role: a.role.to_string(),
49 organization_id: a.organization_id.map(|id| id.to_string()),
50 is_primary: a.is_primary,
51 }
52 }
53
54 fn fallback_role(role: &str, organization_id: Option<Uuid>) -> RoleResponse {
55 RoleResponse {
56 id: Uuid::new_v4().to_string(),
57 role: role.to_string(),
58 organization_id: organization_id.map(|id| id.to_string()),
59 is_primary: true,
60 }
61 }
62
63 fn ensure_primary(roles: &mut [RoleResponse]) {
64 if roles.is_empty() {
65 return;
66 }
67 if roles.iter().filter(|r| r.is_primary).count() == 0 {
68 roles[0].is_primary = true;
69 }
70 roles.sort_by_key(|r| std::cmp::Reverse(r.is_primary));
71 }
72
73 fn build_response(user: User, assignments: Vec<UserRoleAssignment>) -> UserResponse {
74 let mut roles: Vec<RoleResponse> = if assignments.is_empty() {
75 vec![Self::fallback_role(
76 &user.role.to_string(),
77 user.organization_id,
78 )]
79 } else {
80 assignments.iter().map(Self::to_role_response).collect()
81 };
82
83 Self::ensure_primary(&mut roles);
84 let active_role = roles
85 .iter()
86 .find(|r| r.is_primary)
87 .cloned()
88 .or_else(|| roles.first().cloned());
89
90 UserResponse {
91 id: user.id.to_string(),
92 email: user.email.clone(),
93 first_name: user.first_name.clone(),
94 last_name: user.last_name.clone(),
95 role: active_role
96 .as_ref()
97 .map(|r| r.role.clone())
98 .unwrap_or_else(|| user.role.to_string()),
99 organization_id: active_role
100 .as_ref()
101 .and_then(|r| r.organization_id.clone())
102 .or_else(|| user.organization_id.map(|id| id.to_string())),
103 is_active: user.is_active,
104 created_at: user.created_at,
105 roles,
106 active_role,
107 }
108 }
109
110 pub async fn find_display_name(&self, id: Uuid) -> Result<Option<String>, String> {
113 Ok(self.user_repo.find_by_id(id).await?.map(|u| {
114 format!("{} {}", u.first_name, u.last_name)
115 .trim()
116 .to_string()
117 }))
118 }
119
120 pub async fn list_all(&self) -> Result<Vec<UserResponse>, String> {
122 let users = self.user_repo.find_all().await?;
123 let user_ids: Vec<Uuid> = users.iter().map(|u| u.id).collect();
124 let mut roles_map: HashMap<Uuid, Vec<UserRoleAssignment>> =
125 self.role_repo.list_for_users(&user_ids).await?;
126
127 Ok(users
128 .into_iter()
129 .map(|user| {
130 let assignments = roles_map.remove(&user.id).unwrap_or_default();
131 Self::build_response(user, assignments)
132 })
133 .collect())
134 }
135
136 pub async fn list_page(
146 &self,
147 recherche: Option<String>,
148 role: Option<String>,
149 limit: i64,
150 offset: i64,
151 ) -> Result<(Vec<UserResponse>, i64), String> {
152 let users = self
153 .user_repo
154 .find_page(recherche.clone(), role.clone(), limit, offset)
155 .await?;
156 let total = self.user_repo.count_matching(recherche, role).await?;
157
158 let user_ids: Vec<Uuid> = users.iter().map(|u| u.id).collect();
161 let mut roles_map: HashMap<Uuid, Vec<UserRoleAssignment>> =
162 self.role_repo.list_for_users(&user_ids).await?;
163
164 let page = users
165 .into_iter()
166 .map(|user| {
167 let assignments = roles_map.remove(&user.id).unwrap_or_default();
168 Self::build_response(user, assignments)
169 })
170 .collect();
171
172 Ok((page, total))
173 }
174
175 pub async fn list_by_organization(
180 &self,
181 organization_id: Uuid,
182 ) -> Result<Vec<UserResponse>, String> {
183 let users = self.user_repo.find_by_organization(organization_id).await?;
184 let user_ids: Vec<Uuid> = users.iter().map(|u| u.id).collect();
185 let mut roles_map: HashMap<Uuid, Vec<UserRoleAssignment>> =
186 self.role_repo.list_for_users(&user_ids).await?;
187
188 Ok(users
189 .into_iter()
190 .map(|user| {
191 let assignments = roles_map.remove(&user.id).unwrap_or_default();
192 Self::build_response(user, assignments)
193 })
194 .collect())
195 }
196
197 pub async fn create(
200 &self,
201 email: String,
202 password_hash: String,
203 first_name: String,
204 last_name: String,
205 primary_role: UserRole,
206 primary_org: Option<Uuid>,
207 role_assignments: Vec<UserRoleAssignment>,
208 ) -> Result<UserResponse, String> {
209 let user = User::new(
210 email,
211 password_hash,
212 first_name,
213 last_name,
214 primary_role,
215 primary_org,
216 )?;
217 let created = self.user_repo.create(&user).await?;
218
219 let assignments: Vec<UserRoleAssignment> = role_assignments
221 .into_iter()
222 .map(|mut a| {
223 a.user_id = created.id;
224 a
225 })
226 .collect();
227 self.role_repo.replace_all(created.id, &assignments).await?;
228
229 let final_roles = self.role_repo.list_for_user(created.id).await?;
230 Ok(Self::build_response(created, final_roles))
231 }
232
233 pub async fn update(
236 &self,
237 id: Uuid,
238 email: String,
239 first_name: String,
240 last_name: String,
241 primary_role: UserRole,
242 primary_org: Option<Uuid>,
243 password_hash: Option<String>,
244 role_assignments: Vec<UserRoleAssignment>,
245 ) -> Result<Option<UserResponse>, String> {
246 let mut user = match self.user_repo.find_by_id(id).await? {
247 Some(u) => u,
248 None => return Ok(None),
249 };
250
251 user.email = email.trim().to_lowercase();
252 user.first_name = first_name.trim().to_string();
253 user.last_name = last_name.trim().to_string();
254 user.role = primary_role;
255 user.organization_id = primary_org;
256 user.updated_at = Utc::now();
257
258 if let Some(pw) = password_hash {
259 self.user_repo.update_password(id, &pw).await?;
260 }
261
262 self.user_repo.update(&user).await?;
263
264 let assignments: Vec<UserRoleAssignment> = role_assignments
265 .into_iter()
266 .map(|mut a| {
267 a.user_id = id;
268 a
269 })
270 .collect();
271 self.role_repo.replace_all(id, &assignments).await?;
272
273 let final_roles = self.role_repo.list_for_user(id).await?;
274 Ok(Some(Self::build_response(user, final_roles)))
275 }
276
277 pub async fn activate(&self, id: Uuid) -> Result<Option<UserResponse>, String> {
279 let user = match self.user_repo.activate(id).await? {
280 Some(u) => u,
281 None => return Ok(None),
282 };
283 let roles = self.role_repo.list_for_user(id).await?;
284 Ok(Some(Self::build_response(user, roles)))
285 }
286
287 pub async fn deactivate(&self, id: Uuid) -> Result<Option<UserResponse>, String> {
289 let user = match self.user_repo.deactivate(id).await? {
290 Some(u) => u,
291 None => return Ok(None),
292 };
293 let roles = self.role_repo.list_for_user(id).await?;
294 Ok(Some(Self::build_response(user, roles)))
295 }
296
297 pub async fn delete(&self, id: Uuid) -> Result<bool, String> {
299 self.user_repo.delete(id).await
300 }
301
302 pub async fn validate_user_has_role(&self, user_id: Uuid, role: &str) -> Result<(), String> {
305 self.user_repo
306 .find_by_id(user_id)
307 .await?
308 .ok_or_else(|| "User not found".to_string())?;
309
310 let roles = self.role_repo.list_for_user(user_id).await?;
311 if !roles.iter().any(|r| r.role.to_string() == role) {
312 return Err(format!(
313 "User must have role '{}' to be linked to an owner entity",
314 role
315 ));
316 }
317 Ok(())
318 }
319
320 pub async fn assign_role(
351 &self,
352 target_user_id: Uuid,
353 role: UserRole,
354 organization_id: Option<Uuid>,
355 valid_until: Option<DateTime<Utc>>,
356 granted_by_user_id: Uuid,
357 ) -> Result<UserRoleAssignment, AppError> {
358 if let Some(t) = valid_until {
360 if t <= Utc::now() {
361 return Err(AppError::Validation(
362 "valid_until must be strictly in the future".to_string(),
363 ));
364 }
365 }
366
367 let high_privilege = matches!(role, UserRole::SuperAdmin | UserRole::Syndic);
371 if high_privilege && granted_by_user_id == target_user_id {
372 return Err(AppError::Validation(
373 "A user cannot self-grant a high-privilege role".to_string(),
374 ));
375 }
376
377 let target = self
379 .user_repo
380 .find_by_id(target_user_id)
381 .await
382 .map_err(AppError::from)?
383 .ok_or_else(|| AppError::NotFound(format!("User {} not found", target_user_id)))?;
384
385 let existing = self
387 .role_repo
388 .list_for_user(target_user_id)
389 .await
390 .map_err(AppError::from)?;
391 let already = existing.iter().any(|a| {
392 a.role == role && a.organization_id == organization_id && a.is_currently_active()
393 });
394 if already {
395 return Err(AppError::RoleAlreadyAssigned {
396 user_id: target_user_id,
397 role: role.to_string(),
398 });
399 }
400
401 let is_first = existing.is_empty();
403 let assignment = match valid_until {
404 Some(t) => UserRoleAssignment::new_delegated(
405 target_user_id,
406 role,
407 organization_id,
408 t,
409 granted_by_user_id,
410 ),
411 None => UserRoleAssignment::new(target_user_id, role, organization_id, is_first),
412 };
413
414 let saved = self
415 .role_repo
416 .create(&assignment)
417 .await
418 .map_err(AppError::from)?;
419 let _ = target.id;
421 Ok(saved)
422 }
423
424 pub async fn list_assignments_for_user(
430 &self,
431 user_id: Uuid,
432 ) -> Result<Vec<UserRoleAssignment>, AppError> {
433 self.role_repo
434 .list_for_user(user_id)
435 .await
436 .map_err(AppError::from)
437 }
438
439 pub async fn revoke_assignment(&self, assignment_id: Uuid) -> Result<(), AppError> {
444 let existing = self
445 .role_repo
446 .find_by_id(assignment_id)
447 .await
448 .map_err(AppError::from)?
449 .ok_or_else(|| {
450 AppError::NotFound(format!("Role assignment {} not found", assignment_id))
451 })?;
452 let _ = existing.id;
453 let deleted = self
454 .role_repo
455 .delete_by_id(assignment_id)
456 .await
457 .map_err(AppError::from)?;
458 if !deleted {
459 return Err(AppError::NotFound(format!(
461 "Role assignment {} not found",
462 assignment_id
463 )));
464 }
465 Ok(())
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use crate::application::ports::user_repository::MockUserRepo;
473 use crate::application::ports::user_role_repository::MockUserRoleRepo;
474 use chrono::Utc;
475
476 fn make_user(id: Uuid) -> User {
477 User {
478 id,
479 email: "alice@example.com".to_string(),
480 password_hash: "hash".to_string(),
481 first_name: "Alice".to_string(),
482 last_name: "Smith".to_string(),
483 role: UserRole::Syndic,
484 organization_id: Some(Uuid::new_v4()),
485 is_active: true,
486 processing_restricted: false,
487 processing_restricted_at: None,
488 marketing_opt_out: false,
489 marketing_opt_out_at: None,
490 created_at: Utc::now(),
491 updated_at: Utc::now(),
492 }
493 }
494
495 #[tokio::test]
496 async fn test_list_all_returns_users_with_roles() {
497 let user_id = Uuid::new_v4();
498 let user = make_user(user_id);
499
500 let mut mock_user = MockUserRepo::new();
501 mock_user
502 .expect_find_all()
503 .returning(move || Ok(vec![user.clone()]));
504
505 let mut mock_role = MockUserRoleRepo::new();
506 mock_role
507 .expect_list_for_users()
508 .returning(|_| Ok(std::collections::HashMap::new()));
509
510 let uc = UserUseCases::new(Arc::new(mock_user), Arc::new(mock_role));
511 let result = uc.list_all().await.unwrap();
512 assert_eq!(result.len(), 1);
513 assert_eq!(result[0].email, "alice@example.com");
514 }
515
516 #[tokio::test]
524 async fn happy_list_page_transmet_recherche_role_et_bornes() {
525 let user = make_user(Uuid::new_v4());
526
527 let mut mock_user = MockUserRepo::new();
528 mock_user
529 .expect_find_page()
530 .withf(|recherche, role, limit, offset| {
531 recherche.as_deref() == Some("ali")
532 && role.as_deref() == Some("syndic")
533 && *limit == 50
534 && *offset == 100
535 })
536 .returning(move |_, _, _, _| Ok(vec![user.clone()]));
537 mock_user
538 .expect_count_matching()
539 .withf(|recherche, role| {
540 recherche.as_deref() == Some("ali") && role.as_deref() == Some("syndic")
541 })
542 .returning(|_, _| Ok(4120));
543
544 let mut mock_role = MockUserRoleRepo::new();
545 mock_role
546 .expect_list_for_users()
547 .returning(|_| Ok(std::collections::HashMap::new()));
548
549 let uc = UserUseCases::new(Arc::new(mock_user), Arc::new(mock_role));
550 let (page, total) = uc
551 .list_page(Some("ali".to_string()), Some("syndic".to_string()), 50, 100)
552 .await
553 .unwrap();
554
555 assert_eq!(page.len(), 1);
556 assert_eq!(total, 4120, "le total vient du dépôt, pas de la page");
557 }
558
559 #[tokio::test]
560 async fn negative_list_page_remonte_lerreur_du_comptage() {
561 let mut mock_user = MockUserRepo::new();
565 mock_user
566 .expect_find_page()
567 .returning(|_, _, _, _| Ok(Vec::new()));
568 mock_user
569 .expect_count_matching()
570 .returning(|_, _| Err("base injoignable".to_string()));
571
572 let mock_role = MockUserRoleRepo::new();
573 let uc = UserUseCases::new(Arc::new(mock_user), Arc::new(mock_role));
574 let resultat = uc.list_page(None, None, 20, 0).await;
575
576 assert!(resultat.is_err(), "l'erreur de comptage doit remonter");
577 }
578
579 #[tokio::test]
580 async fn edge_list_page_sans_filtre_passe_none_au_depot() {
581 let mut mock_user = MockUserRepo::new();
584 mock_user
585 .expect_find_page()
586 .withf(|recherche, role, _, _| recherche.is_none() && role.is_none())
587 .returning(|_, _, _, _| Ok(Vec::new()));
588 mock_user
589 .expect_count_matching()
590 .withf(|recherche, role| recherche.is_none() && role.is_none())
591 .returning(|_, _| Ok(0));
592
593 let mut mock_role = MockUserRoleRepo::new();
594 mock_role
595 .expect_list_for_users()
596 .returning(|_| Ok(std::collections::HashMap::new()));
597
598 let uc = UserUseCases::new(Arc::new(mock_user), Arc::new(mock_role));
599 let (page, total) = uc.list_page(None, None, 20, 0).await.unwrap();
600
601 assert!(page.is_empty());
602 assert_eq!(total, 0);
603 }
604
605 #[tokio::test]
606 async fn security_list_page_ne_charge_les_roles_que_de_la_page() {
607 let un = make_user(Uuid::new_v4());
611 let deux = make_user(Uuid::new_v4());
612 let attendus = [un.id, deux.id];
613
614 let mut mock_user = MockUserRepo::new();
615 mock_user
616 .expect_find_page()
617 .returning(move |_, _, _, _| Ok(vec![un.clone(), deux.clone()]));
618 mock_user.expect_count_matching().returning(|_, _| Ok(4120));
619
620 let mut mock_role = MockUserRoleRepo::new();
621 mock_role
622 .expect_list_for_users()
623 .withf(move |ids: &[Uuid]| ids.len() == 2 && ids.iter().all(|i| attendus.contains(i)))
624 .returning(|_| Ok(std::collections::HashMap::new()));
625
626 let uc = UserUseCases::new(Arc::new(mock_user), Arc::new(mock_role));
627 let (page, _) = uc.list_page(None, None, 50, 0).await.unwrap();
628
629 assert_eq!(page.len(), 2);
630 }
631
632 #[tokio::test]
633 async fn test_activate_not_found() {
634 let mut mock_user = MockUserRepo::new();
635 mock_user.expect_activate().returning(|_| Ok(None));
636
637 let mock_role = MockUserRoleRepo::new();
638 let uc = UserUseCases::new(Arc::new(mock_user), Arc::new(mock_role));
639 let result = uc.activate(Uuid::new_v4()).await.unwrap();
640 assert!(result.is_none());
641 }
642
643 #[tokio::test]
644 async fn test_delete_delegates_to_repo() {
645 let mut mock_user = MockUserRepo::new();
646 mock_user.expect_delete().returning(|_| Ok(true));
647
648 let mock_role = MockUserRoleRepo::new();
649 let uc = UserUseCases::new(Arc::new(mock_user), Arc::new(mock_role));
650 let result = uc.delete(Uuid::new_v4()).await.unwrap();
651 assert!(result);
652 }
653
654 fn make_uc_with_mocks(mock_user: MockUserRepo, mock_role: MockUserRoleRepo) -> UserUseCases {
660 UserUseCases::new(Arc::new(mock_user), Arc::new(mock_role))
661 }
662
663 #[tokio::test]
666 async fn happy_assign_role_creates_native_assignment() {
667 let target = Uuid::new_v4();
668 let granted_by = Uuid::new_v4();
669 let user = make_user(target);
670
671 let mut mock_user = MockUserRepo::new();
672 let u = user.clone();
673 mock_user
674 .expect_find_by_id()
675 .returning(move |_| Ok(Some(u.clone())));
676
677 let mut mock_role = MockUserRoleRepo::new();
678 mock_role.expect_list_for_user().returning(|_| Ok(vec![]));
679 mock_role
680 .expect_create()
681 .returning(|a: &UserRoleAssignment| Ok(a.clone()));
682
683 let uc = make_uc_with_mocks(mock_user, mock_role);
684 let saved = uc
685 .assign_role(target, UserRole::AccountantEncodeur, None, None, granted_by)
686 .await
687 .unwrap();
688 assert_eq!(saved.user_id, target);
689 assert_eq!(saved.role, UserRole::AccountantEncodeur);
690 assert!(saved.valid_until.is_none(), "native assignment");
691 }
692
693 #[tokio::test]
694 async fn happy_list_assignments_returns_repo_rows() {
695 let user_id = Uuid::new_v4();
696 let a = UserRoleAssignment::new(user_id, UserRole::Owner, None, true);
697
698 let mock_user = MockUserRepo::new();
699 let mut mock_role = MockUserRoleRepo::new();
700 let row = a.clone();
701 mock_role
702 .expect_list_for_user()
703 .returning(move |_| Ok(vec![row.clone()]));
704
705 let uc = make_uc_with_mocks(mock_user, mock_role);
706 let rows = uc.list_assignments_for_user(user_id).await.unwrap();
707 assert_eq!(rows.len(), 1);
708 assert_eq!(rows[0].id, a.id);
709 }
710
711 #[tokio::test]
712 async fn happy_revoke_assignment_deletes_existing() {
713 let user_id = Uuid::new_v4();
714 let a = UserRoleAssignment::new(user_id, UserRole::Owner, None, true);
715 let a_id = a.id;
716
717 let mock_user = MockUserRepo::new();
718 let mut mock_role = MockUserRoleRepo::new();
719 let row = a.clone();
720 mock_role
721 .expect_find_by_id()
722 .returning(move |_| Ok(Some(row.clone())));
723 mock_role.expect_delete_by_id().returning(|_| Ok(true));
724
725 let uc = make_uc_with_mocks(mock_user, mock_role);
726 uc.revoke_assignment(a_id).await.unwrap();
727 }
728
729 #[tokio::test]
732 async fn edge_assign_role_with_future_valid_until_creates_delegated() {
733 let target = Uuid::new_v4();
734 let granted_by = Uuid::new_v4();
735 let user = make_user(target);
736
737 let mut mock_user = MockUserRepo::new();
738 let u = user.clone();
739 mock_user
740 .expect_find_by_id()
741 .returning(move |_| Ok(Some(u.clone())));
742
743 let mut mock_role = MockUserRoleRepo::new();
744 mock_role.expect_list_for_user().returning(|_| Ok(vec![]));
745 mock_role
746 .expect_create()
747 .returning(|a: &UserRoleAssignment| Ok(a.clone()));
748
749 let uc = make_uc_with_mocks(mock_user, mock_role);
750 let valid_until = Utc::now() + chrono::Duration::seconds(60);
751 let saved = uc
752 .assign_role(
753 target,
754 UserRole::Lawyer,
755 None,
756 Some(valid_until),
757 granted_by,
758 )
759 .await
760 .unwrap();
761 assert!(saved.is_delegated());
762 assert_eq!(saved.valid_until, Some(valid_until));
763 }
764
765 #[tokio::test]
766 async fn edge_assign_role_with_past_valid_until_is_rejected() {
767 let target = Uuid::new_v4();
768 let granted_by = Uuid::new_v4();
769
770 let mock_user = MockUserRepo::new();
771 let mock_role = MockUserRoleRepo::new();
772
773 let uc = make_uc_with_mocks(mock_user, mock_role);
774 let valid_until = Utc::now() - chrono::Duration::seconds(1);
775 let err = uc
776 .assign_role(target, UserRole::Owner, None, Some(valid_until), granted_by)
777 .await
778 .unwrap_err();
779 assert!(matches!(err, AppError::Validation(_)));
780 }
781
782 #[tokio::test]
785 async fn security_assign_high_privilege_role_to_self_is_rejected() {
786 let same = Uuid::new_v4();
788
789 let mock_user = MockUserRepo::new();
790 let mock_role = MockUserRoleRepo::new();
791
792 let uc = make_uc_with_mocks(mock_user, mock_role);
793 let err = uc
794 .assign_role(same, UserRole::Syndic, None, None, same)
795 .await
796 .unwrap_err();
797 assert!(matches!(err, AppError::Validation(_)));
798 }
799
800 #[tokio::test]
801 async fn security_assign_non_privileged_self_role_is_allowed() {
802 let same = Uuid::new_v4();
806 let user = make_user(same);
807
808 let mut mock_user = MockUserRepo::new();
809 let u = user.clone();
810 mock_user
811 .expect_find_by_id()
812 .returning(move |_| Ok(Some(u.clone())));
813
814 let mut mock_role = MockUserRoleRepo::new();
815 mock_role.expect_list_for_user().returning(|_| Ok(vec![]));
816 mock_role
817 .expect_create()
818 .returning(|a: &UserRoleAssignment| Ok(a.clone()));
819
820 let uc = make_uc_with_mocks(mock_user, mock_role);
821 let saved = uc
822 .assign_role(same, UserRole::CommunityModerator, None, None, same)
823 .await
824 .unwrap();
825 assert_eq!(saved.role, UserRole::CommunityModerator);
826 }
827
828 #[tokio::test]
831 async fn negative_assign_role_to_unknown_user_returns_not_found() {
832 let target = Uuid::new_v4();
833 let granted_by = Uuid::new_v4();
834
835 let mut mock_user = MockUserRepo::new();
836 mock_user.expect_find_by_id().returning(|_| Ok(None));
837
838 let mock_role = MockUserRoleRepo::new();
839
840 let uc = make_uc_with_mocks(mock_user, mock_role);
841 let err = uc
842 .assign_role(target, UserRole::Owner, None, None, granted_by)
843 .await
844 .unwrap_err();
845 assert!(matches!(err, AppError::NotFound(_)));
846 }
847
848 #[tokio::test]
849 async fn negative_assign_role_double_returns_conflict() {
850 let target = Uuid::new_v4();
851 let granted_by = Uuid::new_v4();
852 let user = make_user(target);
853 let existing = UserRoleAssignment::new(target, UserRole::Owner, None, true);
855
856 let mut mock_user = MockUserRepo::new();
857 let u = user.clone();
858 mock_user
859 .expect_find_by_id()
860 .returning(move |_| Ok(Some(u.clone())));
861
862 let mut mock_role = MockUserRoleRepo::new();
863 let row = existing.clone();
864 mock_role
865 .expect_list_for_user()
866 .returning(move |_| Ok(vec![row.clone()]));
867
868 let uc = make_uc_with_mocks(mock_user, mock_role);
869 let err = uc
870 .assign_role(target, UserRole::Owner, None, None, granted_by)
871 .await
872 .unwrap_err();
873 assert!(
874 matches!(err, AppError::RoleAlreadyAssigned { .. }),
875 "expected RoleAlreadyAssigned, got {:?}",
876 err
877 );
878 }
879
880 #[tokio::test]
881 async fn negative_revoke_unknown_assignment_returns_not_found() {
882 let mock_user = MockUserRepo::new();
883 let mut mock_role = MockUserRoleRepo::new();
884 mock_role.expect_find_by_id().returning(|_| Ok(None));
885
886 let uc = make_uc_with_mocks(mock_user, mock_role);
887 let err = uc.revoke_assignment(Uuid::new_v4()).await.unwrap_err();
888 assert!(matches!(err, AppError::NotFound(_)));
889 }
890}