1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4use validator::Validate;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
20pub enum UserRole {
21 SuperAdmin,
22 Syndic,
23 Accountant,
25 AccountantEncodeur,
27 AccountantEmetteur,
29 BoardMember, Contractor, Owner,
32 CommunityModerator,
34 Lawyer,
36 Notary,
38 Amo,
40 Architect,
42 Bet,
44 Warden,
46}
47
48impl std::fmt::Display for UserRole {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 UserRole::SuperAdmin => write!(f, "superadmin"),
52 UserRole::Syndic => write!(f, "syndic"),
53 UserRole::Accountant => write!(f, "accountant"),
54 UserRole::AccountantEncodeur => write!(f, "accountant.encodeur"),
55 UserRole::AccountantEmetteur => write!(f, "accountant.emetteur"),
56 UserRole::BoardMember => write!(f, "board_member"),
57 UserRole::Contractor => write!(f, "contractor"),
58 UserRole::Owner => write!(f, "owner"),
59 UserRole::CommunityModerator => write!(f, "community.moderator"),
60 UserRole::Lawyer => write!(f, "lawyer"),
61 UserRole::Notary => write!(f, "notary"),
62 UserRole::Amo => write!(f, "amo"),
63 UserRole::Architect => write!(f, "architect"),
64 UserRole::Bet => write!(f, "bet"),
65 UserRole::Warden => write!(f, "warden"),
66 }
67 }
68}
69
70impl std::str::FromStr for UserRole {
71 type Err = String;
72
73 fn from_str(s: &str) -> Result<Self, Self::Err> {
74 let normalized = s.trim().to_lowercase();
76 if normalized.is_empty() {
77 return Err("Invalid user role: empty string".to_string());
78 }
79 if !normalized
81 .chars()
82 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '.')
83 {
84 return Err(format!("Invalid user role: invalid characters in {}", s));
85 }
86 match normalized.as_str() {
87 "superadmin" => Ok(UserRole::SuperAdmin),
88 "syndic" => Ok(UserRole::Syndic),
89 "accountant" => Ok(UserRole::Accountant),
90 "accountant.encodeur" => Ok(UserRole::AccountantEncodeur),
91 "accountant.emetteur" => Ok(UserRole::AccountantEmetteur),
92 "board_member" => Ok(UserRole::BoardMember),
93 "contractor" => Ok(UserRole::Contractor),
94 "owner" => Ok(UserRole::Owner),
95 "community.moderator" => Ok(UserRole::CommunityModerator),
96 "lawyer" => Ok(UserRole::Lawyer),
97 "notary" => Ok(UserRole::Notary),
98 "amo" => Ok(UserRole::Amo),
99 "architect" => Ok(UserRole::Architect),
100 "bet" => Ok(UserRole::Bet),
101 "warden" => Ok(UserRole::Warden),
102 _ => Err(format!("Invalid user role: {}", s)),
103 }
104 }
105}
106
107impl UserRole {
108 pub fn can_encode_invoices(&self) -> bool {
117 matches!(
118 self,
119 UserRole::SuperAdmin
120 | UserRole::Syndic
121 | UserRole::Accountant
122 | UserRole::AccountantEncodeur
123 )
124 }
125
126 pub fn can_emit_expenses(&self) -> bool {
132 matches!(
133 self,
134 UserRole::SuperAdmin
135 | UserRole::Syndic
136 | UserRole::Accountant
137 | UserRole::AccountantEmetteur
138 )
139 }
140
141 pub fn can_create_call_for_funds(&self) -> bool {
145 matches!(
146 self,
147 UserRole::SuperAdmin
148 | UserRole::Syndic
149 | UserRole::Accountant
150 | UserRole::AccountantEmetteur
151 )
152 }
153
154 pub fn can_moderate_community(&self) -> bool {
156 matches!(
157 self,
158 UserRole::SuperAdmin | UserRole::Syndic | UserRole::CommunityModerator
159 )
160 }
161
162 pub fn is_accountant(&self) -> bool {
169 matches!(
170 self,
171 UserRole::Accountant | UserRole::AccountantEncodeur | UserRole::AccountantEmetteur
172 )
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
177pub struct User {
178 pub id: Uuid,
179
180 #[validate(email(message = "Email must be valid"))]
181 pub email: String,
182
183 #[serde(skip_serializing)]
184 pub password_hash: String,
185
186 #[validate(length(min = 2, message = "First name must be at least 2 characters"))]
187 pub first_name: String,
188
189 #[validate(length(min = 2, message = "Last name must be at least 2 characters"))]
190 pub last_name: String,
191
192 pub role: UserRole,
193
194 pub organization_id: Option<Uuid>,
195
196 pub is_active: bool,
197
198 pub processing_restricted: bool,
200 pub processing_restricted_at: Option<DateTime<Utc>>,
201
202 pub marketing_opt_out: bool,
204 pub marketing_opt_out_at: Option<DateTime<Utc>>,
205
206 pub created_at: DateTime<Utc>,
207 pub updated_at: DateTime<Utc>,
208}
209
210impl User {
211 pub fn new(
212 email: String,
213 password_hash: String,
214 first_name: String,
215 last_name: String,
216 role: UserRole,
217 organization_id: Option<Uuid>,
218 ) -> Result<Self, String> {
219 let user = Self {
220 id: Uuid::new_v4(),
221 email: email.to_lowercase().trim().to_string(),
222 password_hash,
223 first_name: first_name.trim().to_string(),
224 last_name: last_name.trim().to_string(),
225 role,
226 organization_id,
227 is_active: true,
228 processing_restricted: false,
229 processing_restricted_at: None,
230 marketing_opt_out: false,
231 marketing_opt_out_at: None,
232 created_at: Utc::now(),
233 updated_at: Utc::now(),
234 };
235
236 user.validate()
237 .map_err(|e| format!("Validation error: {}", e))?;
238
239 Ok(user)
240 }
241
242 pub fn full_name(&self) -> String {
243 format!("{} {}", self.first_name, self.last_name)
244 }
245
246 pub fn update_profile(&mut self, first_name: String, last_name: String) -> Result<(), String> {
247 self.first_name = first_name.trim().to_string();
248 self.last_name = last_name.trim().to_string();
249 self.updated_at = Utc::now();
250
251 self.validate()
252 .map_err(|e| format!("Validation error: {}", e))?;
253
254 Ok(())
255 }
256
257 pub fn deactivate(&mut self) {
258 self.is_active = false;
259 self.updated_at = Utc::now();
260 }
261
262 pub fn activate(&mut self) {
263 self.is_active = true;
264 self.updated_at = Utc::now();
265 }
266
267 pub fn can_access_building(&self, building_org_id: Option<Uuid>) -> bool {
268 match self.role {
269 UserRole::SuperAdmin => true,
270 _ => self.organization_id == building_org_id,
271 }
272 }
273
274 pub fn rectify_data(
277 &mut self,
278 email: Option<String>,
279 first_name: Option<String>,
280 last_name: Option<String>,
281 ) -> Result<(), String> {
282 if email.is_none() && first_name.is_none() && last_name.is_none() {
284 return Err("No fields provided for rectification".to_string());
285 }
286
287 if let Some(ref new_email) = email {
289 let email_normalized = new_email.to_lowercase().trim().to_string();
290 if !email_normalized.contains('@') || email_normalized.len() < 3 {
291 return Err(format!("Invalid email format: {}", new_email));
292 }
293 }
294
295 if let Some(ref new_first_name) = first_name {
297 if new_first_name.trim().is_empty() {
298 return Err("First name cannot be empty".to_string());
299 }
300 }
301 if let Some(ref new_last_name) = last_name {
302 if new_last_name.trim().is_empty() {
303 return Err("Last name cannot be empty".to_string());
304 }
305 }
306
307 if let Some(new_email) = email {
309 self.email = new_email.to_lowercase().trim().to_string();
310 }
311 if let Some(new_first_name) = first_name {
312 self.first_name = new_first_name.trim().to_string();
313 }
314 if let Some(new_last_name) = last_name {
315 self.last_name = new_last_name.trim().to_string();
316 }
317
318 self.updated_at = Utc::now();
319
320 self.validate()
322 .map_err(|e| format!("Validation error: {}", e))?;
323
324 Ok(())
325 }
326
327 pub fn restrict_processing(&mut self) -> Result<(), String> {
330 if self.processing_restricted {
331 return Err("Processing is already restricted for this user".to_string());
332 }
333
334 self.processing_restricted = true;
335 self.processing_restricted_at = Some(Utc::now());
336 self.updated_at = Utc::now();
337
338 Ok(())
339 }
340
341 pub fn unrestrict_processing(&mut self) {
343 self.processing_restricted = false;
344 self.updated_at = Utc::now();
346 }
347
348 pub fn set_marketing_opt_out(&mut self, opt_out: bool) {
351 if opt_out && !self.marketing_opt_out {
352 self.marketing_opt_out = true;
354 self.marketing_opt_out_at = Some(Utc::now());
355 } else if !opt_out && self.marketing_opt_out {
356 self.marketing_opt_out = false;
358 }
360
361 self.updated_at = Utc::now();
362 }
363
364 pub fn can_process_data(&self) -> bool {
366 !self.processing_restricted
367 }
368
369 pub fn can_send_marketing(&self) -> bool {
371 !self.marketing_opt_out
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use std::str::FromStr;
379
380 #[test]
387 fn happy_parse_accountant_encodeur() {
388 assert_eq!(
389 UserRole::from_str("accountant.encodeur").unwrap(),
390 UserRole::AccountantEncodeur
391 );
392 }
393
394 #[test]
395 fn happy_parse_accountant_emetteur() {
396 assert_eq!(
397 UserRole::from_str("accountant.emetteur").unwrap(),
398 UserRole::AccountantEmetteur
399 );
400 }
401
402 #[test]
403 fn happy_parse_community_moderator() {
404 assert_eq!(
405 UserRole::from_str("community.moderator").unwrap(),
406 UserRole::CommunityModerator
407 );
408 }
409
410 #[test]
411 fn happy_parse_mandataires() {
412 assert_eq!(UserRole::from_str("lawyer").unwrap(), UserRole::Lawyer);
413 assert_eq!(UserRole::from_str("notary").unwrap(), UserRole::Notary);
414 assert_eq!(UserRole::from_str("amo").unwrap(), UserRole::Amo);
415 assert_eq!(
416 UserRole::from_str("architect").unwrap(),
417 UserRole::Architect
418 );
419 assert_eq!(UserRole::from_str("bet").unwrap(), UserRole::Bet);
420 assert_eq!(UserRole::from_str("warden").unwrap(), UserRole::Warden);
421 }
422
423 #[test]
424 fn happy_encodeur_can_encode_invoices() {
425 assert!(UserRole::AccountantEncodeur.can_encode_invoices());
426 }
427
428 #[test]
429 fn happy_emetteur_can_emit_expenses() {
430 assert!(UserRole::AccountantEmetteur.can_emit_expenses());
431 assert!(UserRole::AccountantEmetteur.can_create_call_for_funds());
432 }
433
434 #[test]
435 fn happy_community_moderator_can_moderate() {
436 assert!(UserRole::CommunityModerator.can_moderate_community());
437 }
438
439 #[test]
440 fn happy_accountant_subroles_are_accountant() {
441 assert!(UserRole::Accountant.is_accountant());
444 assert!(UserRole::AccountantEncodeur.is_accountant());
445 assert!(UserRole::AccountantEmetteur.is_accountant());
446 }
447
448 #[test]
449 fn happy_display_round_trip() {
450 for role in [
451 UserRole::SuperAdmin,
452 UserRole::Syndic,
453 UserRole::Accountant,
454 UserRole::AccountantEncodeur,
455 UserRole::AccountantEmetteur,
456 UserRole::BoardMember,
457 UserRole::Contractor,
458 UserRole::Owner,
459 UserRole::CommunityModerator,
460 UserRole::Lawyer,
461 UserRole::Notary,
462 UserRole::Amo,
463 UserRole::Architect,
464 UserRole::Bet,
465 UserRole::Warden,
466 ] {
467 let s = role.to_string();
468 let parsed = UserRole::from_str(&s)
469 .unwrap_or_else(|e| panic!("Round-trip failed for {:?} -> {} : {}", role, s, e));
470 assert_eq!(parsed, role);
471 }
472 }
473
474 #[test]
477 fn edge_parse_accountant_encodeur_trim_uppercase() {
478 assert_eq!(
479 UserRole::from_str(" ACCOUNTANT.ENCODEUR ").unwrap(),
480 UserRole::AccountantEncodeur
481 );
482 }
483
484 #[test]
485 fn edge_parse_mixed_case_community_moderator() {
486 assert_eq!(
487 UserRole::from_str("CoMmUnItY.MoDeRaToR").unwrap(),
488 UserRole::CommunityModerator
489 );
490 }
491
492 #[test]
493 fn edge_accountant_role_from_str_roundtrip_is_still_accountant() {
494 assert!(UserRole::from_str("accountant.encodeur")
498 .unwrap()
499 .is_accountant());
500 assert!(UserRole::from_str(" ACCOUNTANT.EMETTEUR ")
501 .unwrap()
502 .is_accountant());
503 }
504
505 #[test]
506 fn edge_syndic_keeps_all_finance_powers() {
507 assert!(UserRole::Syndic.can_encode_invoices());
509 assert!(UserRole::Syndic.can_emit_expenses());
510 assert!(UserRole::Syndic.can_create_call_for_funds());
511 assert!(UserRole::Syndic.can_moderate_community());
512 }
513
514 #[test]
515 fn edge_generic_accountant_keeps_both_powers() {
516 assert!(UserRole::Accountant.can_encode_invoices());
518 assert!(UserRole::Accountant.can_emit_expenses());
519 assert!(UserRole::Accountant.can_create_call_for_funds());
520 }
521
522 #[test]
523 fn edge_cumul_encodeur_et_emetteur_via_assignments() {
524 let encodeur = UserRole::AccountantEncodeur;
527 let emetteur = UserRole::AccountantEmetteur;
528 let can_encode = encodeur.can_encode_invoices() || emetteur.can_encode_invoices();
530 let can_emit = encodeur.can_emit_expenses() || emetteur.can_emit_expenses();
531 let can_call = encodeur.can_create_call_for_funds() || emetteur.can_create_call_for_funds();
532 assert!(
533 can_encode && can_emit && can_call,
534 "Encodeur+Emetteur cumul should grant all finance powers"
535 );
536 }
537
538 #[test]
541 fn security_encodeur_cannot_emit_expenses() {
542 assert!(!UserRole::AccountantEncodeur.can_emit_expenses());
544 }
545
546 #[test]
547 fn security_encodeur_cannot_create_call_for_funds() {
548 assert!(!UserRole::AccountantEncodeur.can_create_call_for_funds());
549 }
550
551 #[test]
552 fn security_emetteur_cannot_encode_invoices() {
553 assert!(!UserRole::AccountantEmetteur.can_encode_invoices());
555 }
556
557 #[test]
558 fn security_owner_has_no_finance_power() {
559 assert!(!UserRole::Owner.can_encode_invoices());
560 assert!(!UserRole::Owner.can_emit_expenses());
561 assert!(!UserRole::Owner.can_create_call_for_funds());
562 assert!(!UserRole::Owner.can_moderate_community());
563 }
564
565 #[test]
566 fn security_community_moderator_has_no_finance_power() {
567 assert!(!UserRole::CommunityModerator.can_emit_expenses());
568 assert!(!UserRole::CommunityModerator.can_encode_invoices());
569 assert!(!UserRole::CommunityModerator.can_create_call_for_funds());
570 }
571
572 #[test]
573 fn security_non_accountant_roles_are_not_flagged_as_accountant() {
574 for role in [
578 UserRole::SuperAdmin,
579 UserRole::Syndic,
580 UserRole::BoardMember,
581 UserRole::Contractor,
582 UserRole::Owner,
583 UserRole::CommunityModerator,
584 UserRole::Lawyer,
585 UserRole::Notary,
586 UserRole::Amo,
587 UserRole::Architect,
588 UserRole::Bet,
589 UserRole::Warden,
590 ] {
591 assert!(
592 !role.is_accountant(),
593 "{} should not be flagged as accountant",
594 role
595 );
596 }
597 }
598
599 #[test]
600 fn security_mandataires_have_no_finance_power() {
601 for role in [
602 UserRole::Lawyer,
603 UserRole::Notary,
604 UserRole::Amo,
605 UserRole::Architect,
606 UserRole::Bet,
607 UserRole::Warden,
608 UserRole::Contractor,
609 UserRole::BoardMember,
610 ] {
611 assert!(
612 !role.can_emit_expenses(),
613 "{} should not be able to emit expenses",
614 role
615 );
616 assert!(
617 !role.can_encode_invoices(),
618 "{} should not be able to encode invoices",
619 role
620 );
621 assert!(
622 !role.can_create_call_for_funds(),
623 "{} should not be able to create call for funds",
624 role
625 );
626 assert!(
627 !role.can_moderate_community(),
628 "{} should not be able to moderate community",
629 role
630 );
631 }
632 }
633
634 #[test]
637 fn negative_unknown_role_rejected() {
638 let err = UserRole::from_str("hackerman").unwrap_err();
639 assert!(
640 err.contains("Invalid user role"),
641 "Unknown role should fail typed: got {}",
642 err
643 );
644 }
645
646 #[test]
647 fn negative_empty_role_rejected() {
648 let err = UserRole::from_str("").unwrap_err();
649 assert!(
650 err.contains("empty") || err.contains("Invalid"),
651 "Empty role should fail: got {}",
652 err
653 );
654 }
655
656 #[test]
657 fn negative_whitespace_only_role_rejected() {
658 assert!(UserRole::from_str(" ").is_err());
659 }
660
661 #[test]
662 fn negative_role_with_special_chars_rejected() {
663 assert!(UserRole::from_str("accountant.<script>").is_err());
665 assert!(UserRole::from_str("accountant';drop").is_err());
666 assert!(UserRole::from_str("accountant/encodeur").is_err());
667 }
668
669 #[test]
670 fn negative_partial_subrole_rejected() {
671 assert!(UserRole::from_str("accountant.foo").is_err());
673 assert!(UserRole::from_str("community.spam").is_err());
674 }
675
676 #[test]
681 fn test_create_user_success() {
682 let user = User::new(
683 "test@example.com".to_string(),
684 "hashed_password".to_string(),
685 "John".to_string(),
686 "Doe".to_string(),
687 UserRole::Syndic,
688 Some(Uuid::new_v4()),
689 );
690
691 assert!(user.is_ok());
692 let user = user.unwrap();
693 assert_eq!(user.email, "test@example.com");
694 assert_eq!(user.full_name(), "John Doe");
695 assert!(user.is_active);
696 }
697
698 #[test]
699 fn test_create_user_invalid_email() {
700 let user = User::new(
701 "invalid-email".to_string(),
702 "hashed_password".to_string(),
703 "John".to_string(),
704 "Doe".to_string(),
705 UserRole::Syndic,
706 None,
707 );
708
709 assert!(user.is_err());
710 }
711
712 #[test]
713 fn test_update_profile() {
714 let mut user = User::new(
715 "test@example.com".to_string(),
716 "hashed_password".to_string(),
717 "John".to_string(),
718 "Doe".to_string(),
719 UserRole::Syndic,
720 None,
721 )
722 .unwrap();
723
724 let result = user.update_profile("Jane".to_string(), "Smith".to_string());
725 assert!(result.is_ok());
726 assert_eq!(user.full_name(), "Jane Smith");
727 }
728
729 #[test]
730 fn test_deactivate_user() {
731 let mut user = User::new(
732 "test@example.com".to_string(),
733 "hashed_password".to_string(),
734 "John".to_string(),
735 "Doe".to_string(),
736 UserRole::Syndic,
737 None,
738 )
739 .unwrap();
740
741 user.deactivate();
742 assert!(!user.is_active);
743 }
744
745 #[test]
746 fn test_superadmin_can_access_all_buildings() {
747 let user = User::new(
748 "admin@example.com".to_string(),
749 "hashed_password".to_string(),
750 "Admin".to_string(),
751 "User".to_string(),
752 UserRole::SuperAdmin,
753 None,
754 )
755 .unwrap();
756
757 assert!(user.can_access_building(Some(Uuid::new_v4())));
758 assert!(user.can_access_building(None));
759 }
760
761 #[test]
762 fn test_regular_user_access_control() {
763 let org_id = Uuid::new_v4();
764 let user = User::new(
765 "syndic@example.com".to_string(),
766 "hashed_password".to_string(),
767 "John".to_string(),
768 "Syndic".to_string(),
769 UserRole::Syndic,
770 Some(org_id),
771 )
772 .unwrap();
773
774 assert!(user.can_access_building(Some(org_id)));
775 assert!(!user.can_access_building(Some(Uuid::new_v4())));
776 }
777
778 #[test]
780 fn test_rectify_data_success() {
781 let mut user = User::new(
782 "old@example.com".to_string(),
783 "hashed_password".to_string(),
784 "OldFirst".to_string(),
785 "OldLast".to_string(),
786 UserRole::Owner,
787 None,
788 )
789 .unwrap();
790
791 let result = user.rectify_data(
792 Some("new@example.com".to_string()),
793 Some("NewFirst".to_string()),
794 Some("NewLast".to_string()),
795 );
796
797 assert!(result.is_ok());
798 assert_eq!(user.email, "new@example.com");
799 assert_eq!(user.first_name, "NewFirst");
800 assert_eq!(user.last_name, "NewLast");
801 }
802
803 #[test]
804 fn test_rectify_data_partial() {
805 let mut user = User::new(
806 "test@example.com".to_string(),
807 "hashed_password".to_string(),
808 "John".to_string(),
809 "Doe".to_string(),
810 UserRole::Owner,
811 None,
812 )
813 .unwrap();
814
815 let result = user.rectify_data(None, Some("Jane".to_string()), None);
816
817 assert!(result.is_ok());
818 assert_eq!(user.email, "test@example.com"); assert_eq!(user.first_name, "Jane"); assert_eq!(user.last_name, "Doe"); }
822
823 #[test]
824 fn test_rectify_data_invalid_email() {
825 let mut user = User::new(
826 "test@example.com".to_string(),
827 "hashed_password".to_string(),
828 "John".to_string(),
829 "Doe".to_string(),
830 UserRole::Owner,
831 None,
832 )
833 .unwrap();
834
835 let result = user.rectify_data(Some("invalid-email".to_string()), None, None);
836
837 assert!(result.is_err());
838 assert_eq!(user.email, "test@example.com"); }
840
841 #[test]
843 fn test_restrict_processing_success() {
844 let mut user = User::new(
845 "test@example.com".to_string(),
846 "hashed_password".to_string(),
847 "John".to_string(),
848 "Doe".to_string(),
849 UserRole::Owner,
850 None,
851 )
852 .unwrap();
853
854 assert!(!user.processing_restricted);
855 assert!(user.can_process_data());
856
857 let result = user.restrict_processing();
858
859 assert!(result.is_ok());
860 assert!(user.processing_restricted);
861 assert!(user.processing_restricted_at.is_some());
862 assert!(!user.can_process_data());
863 }
864
865 #[test]
866 fn test_restrict_processing_already_restricted() {
867 let mut user = User::new(
868 "test@example.com".to_string(),
869 "hashed_password".to_string(),
870 "John".to_string(),
871 "Doe".to_string(),
872 UserRole::Owner,
873 None,
874 )
875 .unwrap();
876
877 user.restrict_processing().unwrap();
878
879 let result = user.restrict_processing();
880
881 assert!(result.is_err());
882 assert!(result
883 .unwrap_err()
884 .contains("Processing is already restricted"));
885 }
886
887 #[test]
888 fn test_unrestrict_processing() {
889 let mut user = User::new(
890 "test@example.com".to_string(),
891 "hashed_password".to_string(),
892 "John".to_string(),
893 "Doe".to_string(),
894 UserRole::Owner,
895 None,
896 )
897 .unwrap();
898
899 user.restrict_processing().unwrap();
900 assert!(!user.can_process_data());
901
902 let restriction_timestamp = user.processing_restricted_at;
903
904 user.unrestrict_processing();
905
906 assert!(!user.processing_restricted);
907 assert!(user.can_process_data());
908 assert_eq!(user.processing_restricted_at, restriction_timestamp); }
910
911 #[test]
913 fn test_set_marketing_opt_out() {
914 let mut user = User::new(
915 "test@example.com".to_string(),
916 "hashed_password".to_string(),
917 "John".to_string(),
918 "Doe".to_string(),
919 UserRole::Owner,
920 None,
921 )
922 .unwrap();
923
924 assert!(!user.marketing_opt_out);
925 assert!(user.can_send_marketing());
926
927 user.set_marketing_opt_out(true);
928
929 assert!(user.marketing_opt_out);
930 assert!(user.marketing_opt_out_at.is_some());
931 assert!(!user.can_send_marketing());
932 }
933
934 #[test]
935 fn test_set_marketing_opt_in_after_opt_out() {
936 let mut user = User::new(
937 "test@example.com".to_string(),
938 "hashed_password".to_string(),
939 "John".to_string(),
940 "Doe".to_string(),
941 UserRole::Owner,
942 None,
943 )
944 .unwrap();
945
946 user.set_marketing_opt_out(true);
947 assert!(!user.can_send_marketing());
948
949 let opt_out_timestamp = user.marketing_opt_out_at;
950
951 user.set_marketing_opt_out(false);
952
953 assert!(!user.marketing_opt_out);
954 assert!(user.can_send_marketing());
955 assert_eq!(user.marketing_opt_out_at, opt_out_timestamp); }
957
958 #[test]
959 fn test_gdpr_defaults_on_new_user() {
960 let user = User::new(
961 "test@example.com".to_string(),
962 "hashed_password".to_string(),
963 "John".to_string(),
964 "Doe".to_string(),
965 UserRole::Owner,
966 None,
967 )
968 .unwrap();
969
970 assert!(!user.processing_restricted);
972 assert!(user.processing_restricted_at.is_none());
973 assert!(!user.marketing_opt_out);
974 assert!(user.marketing_opt_out_at.is_none());
975
976 assert!(user.can_process_data());
978 assert!(user.can_send_marketing());
979 }
980}