koprogo_api/domain/plateforme/
user_role_assignment.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5use super::user::UserRole;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21pub struct UserRoleAssignment {
22 pub id: Uuid,
23 pub user_id: Uuid,
24 pub role: UserRole,
25 pub organization_id: Option<Uuid>,
26 pub is_primary: bool,
27 #[serde(default)]
29 pub valid_until: Option<DateTime<Utc>>,
30 #[serde(default)]
33 pub delegated_from_user_id: Option<Uuid>,
34 pub created_at: DateTime<Utc>,
35 pub updated_at: DateTime<Utc>,
36}
37
38impl UserRoleAssignment {
39 pub fn new(
44 user_id: Uuid,
45 role: UserRole,
46 organization_id: Option<Uuid>,
47 is_primary: bool,
48 ) -> Self {
49 let now = Utc::now();
50 Self {
51 id: Uuid::new_v4(),
52 user_id,
53 role,
54 organization_id,
55 is_primary,
56 valid_until: None,
57 delegated_from_user_id: None,
58 created_at: now,
59 updated_at: now,
60 }
61 }
62
63 pub fn new_delegated(
68 user_id: Uuid,
69 role: UserRole,
70 organization_id: Option<Uuid>,
71 valid_until: DateTime<Utc>,
72 delegated_from_user_id: Uuid,
73 ) -> Self {
74 let now = Utc::now();
75 Self {
76 id: Uuid::new_v4(),
77 user_id,
78 role,
79 organization_id,
80 is_primary: false,
81 valid_until: Some(valid_until),
82 delegated_from_user_id: Some(delegated_from_user_id),
83 created_at: now,
84 updated_at: now,
85 }
86 }
87
88 pub fn set_primary(&mut self, primary: bool) {
89 self.is_primary = primary;
90 self.updated_at = Utc::now();
91 }
92
93 pub fn is_delegated(&self) -> bool {
97 self.delegated_from_user_id.is_some()
98 }
99
100 pub fn is_expired(&self) -> bool {
104 self.is_expired_at(Utc::now())
105 }
106
107 pub fn is_expired_at(&self, t: DateTime<Utc>) -> bool {
109 match self.valid_until {
110 None => false,
111 Some(until) => t >= until,
112 }
113 }
114
115 pub fn is_currently_active(&self) -> bool {
120 !self.is_expired()
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use chrono::Duration;
128
129 #[test]
134 fn test_new_assignment_defaults() {
135 let user_id = Uuid::new_v4();
136 let assignment = UserRoleAssignment::new(user_id, UserRole::Syndic, None, true);
137
138 assert_eq!(assignment.user_id, user_id);
139 assert_eq!(assignment.role, UserRole::Syndic);
140 assert!(assignment.is_primary);
141 assert!(assignment.organization_id.is_none());
142 assert!(!assignment.is_delegated());
144 assert!(assignment.valid_until.is_none());
145 assert!(assignment.is_currently_active());
146 }
147
148 #[test]
149 fn test_set_primary_updates_timestamp() {
150 let mut assignment =
151 UserRoleAssignment::new(Uuid::new_v4(), UserRole::Accountant, None, false);
152 let original_updated_at = assignment.updated_at;
153
154 assignment.set_primary(true);
155
156 assert!(assignment.is_primary);
157 assert!(
158 assignment.updated_at > original_updated_at,
159 "Updated_at should change when toggling primary flag"
160 );
161 }
162
163 #[test]
170 fn happy_new_delegated_assignment_is_currently_active() {
171 let user_id = Uuid::new_v4();
172 let delegator = Uuid::new_v4();
173 let valid_until = Utc::now() + Duration::days(7);
174 let a = UserRoleAssignment::new_delegated(
175 user_id,
176 UserRole::Syndic,
177 None,
178 valid_until,
179 delegator,
180 );
181
182 assert!(a.is_currently_active());
183 assert!(!a.is_expired());
184 assert!(a.is_delegated());
185 assert_eq!(a.delegated_from_user_id, Some(delegator));
186 assert_eq!(a.valid_until, Some(valid_until));
187 assert!(
188 !a.is_primary,
189 "delegated assignment must never be the user's primary role"
190 );
191 }
192
193 #[test]
194 fn happy_native_assignment_is_not_delegated() {
195 let a = UserRoleAssignment::new(Uuid::new_v4(), UserRole::Owner, None, true);
196 assert!(!a.is_delegated());
197 assert!(a.is_currently_active());
198 assert!(!a.is_expired());
199 }
200
201 #[test]
204 fn edge_just_before_valid_until_is_active() {
205 let now = Utc::now();
206 let valid_until = now + Duration::milliseconds(500);
207 let a = UserRoleAssignment::new_delegated(
208 Uuid::new_v4(),
209 UserRole::Syndic,
210 None,
211 valid_until,
212 Uuid::new_v4(),
213 );
214 assert!(!a.is_expired_at(now + Duration::milliseconds(100)));
216 assert!(a.is_currently_active());
217 }
218
219 #[test]
220 fn edge_at_or_after_valid_until_is_expired() {
221 let valid_until = Utc::now() + Duration::milliseconds(10);
222 let a = UserRoleAssignment::new_delegated(
223 Uuid::new_v4(),
224 UserRole::Syndic,
225 None,
226 valid_until,
227 Uuid::new_v4(),
228 );
229 assert!(a.is_expired_at(valid_until));
231 assert!(a.is_expired_at(valid_until + Duration::milliseconds(1)));
233 }
234
235 #[test]
238 fn security_delegated_flag_is_preserved_through_serde_roundtrip() {
239 let delegator = Uuid::new_v4();
240 let a = UserRoleAssignment::new_delegated(
241 Uuid::new_v4(),
242 UserRole::Syndic,
243 None,
244 Utc::now() + Duration::days(3),
245 delegator,
246 );
247 let json = serde_json::to_string(&a).expect("serialize ok");
248 let back: UserRoleAssignment = serde_json::from_str(&json).expect("deserialize ok");
249 assert_eq!(back.delegated_from_user_id, Some(delegator));
252 assert_eq!(back.valid_until, a.valid_until);
253 assert!(back.is_delegated());
254 }
255
256 #[test]
257 fn security_native_role_serde_keeps_none_delegation() {
258 let a = UserRoleAssignment::new(Uuid::new_v4(), UserRole::Owner, None, true);
259 let json = serde_json::to_string(&a).expect("serialize ok");
260 let back: UserRoleAssignment = serde_json::from_str(&json).expect("deserialize ok");
261 assert!(back.delegated_from_user_id.is_none());
264 assert!(back.valid_until.is_none());
265 assert!(!back.is_delegated());
266 }
267
268 #[test]
271 fn negative_expired_delegation_is_not_currently_active() {
272 let now = Utc::now();
275 let a = UserRoleAssignment {
276 id: Uuid::new_v4(),
277 user_id: Uuid::new_v4(),
278 role: UserRole::Syndic,
279 organization_id: None,
280 is_primary: false,
281 valid_until: Some(now - Duration::seconds(1)),
282 delegated_from_user_id: Some(Uuid::new_v4()),
283 created_at: now - Duration::days(1),
284 updated_at: now - Duration::days(1),
285 };
286 assert!(a.is_expired());
287 assert!(!a.is_currently_active());
288 }
289}