1use std::collections::BTreeSet;
4
5use chrono::Utc;
6use uuid::Uuid;
7
8use crate::entities::{NewUser, Page, User};
9use crate::error::StoreError;
10use crate::store::StoreFuture;
11use crate::user_store::UserStore;
12
13use super::InMemoryStore;
14
15impl UserStore for InMemoryStore {
16 fn create_user(&self, req: NewUser) -> StoreFuture<'_, User> {
17 Box::pin(async move {
18 let mut state = self.state.write().await;
19
20 let email_exists = state.users.values().any(|u| u.email == req.email);
21 if email_exists {
22 return Err(StoreError::DuplicateEmail(req.email));
23 }
24
25 let username_exists = state.users.values().any(|u| u.username == req.username);
26 if username_exists {
27 return Err(StoreError::DuplicateUsername(req.username));
28 }
29
30 let is_admin = req.is_admin.unwrap_or(state.users.is_empty());
31
32 let now = Utc::now();
33 let user = User {
34 id: Uuid::now_v7(),
35 email: req.email,
36 username: req.username,
37 password_hash: req.password_hash,
38 is_admin,
39 created_at: now,
40 updated_at: now,
41 };
42
43 state.users.insert(user.id, user.clone());
44 Ok(user)
45 })
46 }
47
48 fn find_user_by_email(&self, email: &str) -> StoreFuture<'_, Option<User>> {
49 let email = email.to_string();
50 Box::pin(async move {
51 let state = self.state.read().await;
52 Ok(state.users.values().find(|u| u.email == email).cloned())
53 })
54 }
55
56 fn find_user_by_username(&self, username: &str) -> StoreFuture<'_, Option<User>> {
57 let username = username.to_string();
58 Box::pin(async move {
59 let state = self.state.read().await;
60 Ok(state
61 .users
62 .values()
63 .find(|u| u.username == username)
64 .cloned())
65 })
66 }
67
68 fn find_user_by_id(&self, id: Uuid) -> StoreFuture<'_, Option<User>> {
69 Box::pin(async move {
70 let state = self.state.read().await;
71 Ok(state.users.get(&id).cloned())
72 })
73 }
74
75 fn count_users(&self) -> StoreFuture<'_, u64> {
76 Box::pin(async move {
77 let state = self.state.read().await;
78 Ok(state.users.len() as u64)
79 })
80 }
81
82 fn list_users(&self, page: u32, per_page: u32) -> StoreFuture<'_, Page<User>> {
83 Box::pin(async move {
84 let state = self.state.read().await;
85 let mut users: Vec<User> = state.users.values().cloned().collect();
86 users.sort_by_key(|u| std::cmp::Reverse(u.created_at));
87
88 let total = users.len() as u64;
89 let offset = ((page.saturating_sub(1)) as usize) * (per_page as usize);
90 let items: Vec<User> = users
91 .into_iter()
92 .skip(offset)
93 .take(per_page as usize)
94 .collect();
95
96 Ok(Page {
97 items,
98 total,
99 page,
100 per_page,
101 })
102 })
103 }
104
105 fn delete_user(&self, id: Uuid) -> StoreFuture<'_, ()> {
106 Box::pin(async move {
107 let mut state = self.state.write().await;
108 state
109 .users
110 .remove(&id)
111 .ok_or(StoreError::UserNotFound(id))?;
112 state.user_groups.remove(&id);
113 Ok(())
114 })
115 }
116
117 fn update_user_role(&self, id: Uuid, is_admin: bool) -> StoreFuture<'_, User> {
118 Box::pin(async move {
119 let mut state = self.state.write().await;
120 let user = state
121 .users
122 .get_mut(&id)
123 .ok_or(StoreError::UserNotFound(id))?;
124 user.is_admin = is_admin;
125 user.updated_at = Utc::now();
126 Ok(user.clone())
127 })
128 }
129
130 fn update_user_password(&self, id: Uuid, password_hash: String) -> StoreFuture<'_, ()> {
131 Box::pin(async move {
132 let mut state = self.state.write().await;
133 let user = state
134 .users
135 .get_mut(&id)
136 .ok_or(StoreError::UserNotFound(id))?;
137 user.password_hash = password_hash;
138 user.updated_at = Utc::now();
139 Ok(())
140 })
141 }
142
143 fn list_user_groups(&self, user_id: Uuid) -> StoreFuture<'_, Vec<String>> {
144 Box::pin(async move {
145 let state = self.state.read().await;
146 Ok(state
147 .user_groups
148 .get(&user_id)
149 .map(|groups| groups.iter().cloned().collect())
150 .unwrap_or_default())
151 })
152 }
153
154 fn set_user_groups(&self, user_id: Uuid, groups: Vec<String>) -> StoreFuture<'_, Vec<String>> {
155 Box::pin(async move {
156 let mut state = self.state.write().await;
157 if !state.users.contains_key(&user_id) {
158 return Err(StoreError::UserNotFound(user_id));
159 }
160 let groups: BTreeSet<String> = groups.into_iter().collect();
161 let sorted: Vec<String> = groups.iter().cloned().collect();
162 if groups.is_empty() {
163 state.user_groups.remove(&user_id);
164 } else {
165 state.user_groups.insert(user_id, groups);
166 }
167 Ok(sorted)
168 })
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 fn new_user(email: &str, username: &str) -> NewUser {
177 NewUser {
178 email: email.to_string(),
179 username: username.to_string(),
180 password_hash: "argon2hash".to_string(),
181 is_admin: None,
182 }
183 }
184
185 #[tokio::test]
186 async fn create_user_first_user_is_admin() {
187 let store = InMemoryStore::new();
188 let user = store
189 .create_user(new_user("alice@example.com", "alice"))
190 .await
191 .unwrap();
192
193 assert_eq!(user.email, "alice@example.com");
194 assert_eq!(user.username, "alice");
195 assert_eq!(user.password_hash, "argon2hash");
196 assert!(user.is_admin);
197 }
198
199 #[tokio::test]
200 async fn create_user_second_user_is_not_admin() {
201 let store = InMemoryStore::new();
202 store
203 .create_user(new_user("alice@example.com", "alice"))
204 .await
205 .unwrap();
206
207 let second = store
208 .create_user(new_user("bob@example.com", "bob"))
209 .await
210 .unwrap();
211
212 assert!(!second.is_admin);
213 }
214
215 #[tokio::test]
216 async fn create_user_explicit_admin_flag() {
217 let store = InMemoryStore::new();
218 let first = store
220 .create_user(NewUser {
221 email: "alice@example.com".to_string(),
222 username: "alice".to_string(),
223 password_hash: "argon2hash".to_string(),
224 is_admin: Some(false),
225 })
226 .await
227 .unwrap();
228 assert!(!first.is_admin);
229
230 let second = store
232 .create_user(NewUser {
233 email: "bob@example.com".to_string(),
234 username: "bob".to_string(),
235 password_hash: "argon2hash".to_string(),
236 is_admin: Some(true),
237 })
238 .await
239 .unwrap();
240 assert!(second.is_admin);
241 }
242
243 #[tokio::test]
244 async fn create_user_duplicate_email_returns_error() {
245 let store = InMemoryStore::new();
246 store
247 .create_user(new_user("alice@example.com", "alice"))
248 .await
249 .unwrap();
250
251 let err = store
252 .create_user(new_user("alice@example.com", "bob"))
253 .await
254 .unwrap_err();
255
256 assert!(
257 matches!(err, StoreError::DuplicateEmail(ref e) if e == "alice@example.com"),
258 "expected DuplicateEmail, got: {err}"
259 );
260 }
261
262 #[tokio::test]
263 async fn create_user_duplicate_username_returns_error() {
264 let store = InMemoryStore::new();
265 store
266 .create_user(new_user("alice@example.com", "alice"))
267 .await
268 .unwrap();
269
270 let err = store
271 .create_user(new_user("bob@example.com", "alice"))
272 .await
273 .unwrap_err();
274
275 assert!(
276 matches!(err, StoreError::DuplicateUsername(ref u) if u == "alice"),
277 "expected DuplicateUsername, got: {err}"
278 );
279 }
280
281 #[tokio::test]
282 async fn find_user_by_email_existing() {
283 let store = InMemoryStore::new();
284 let created = store
285 .create_user(new_user("alice@example.com", "alice"))
286 .await
287 .unwrap();
288
289 let found = store
290 .find_user_by_email("alice@example.com")
291 .await
292 .unwrap()
293 .expect("user should exist");
294
295 assert_eq!(found.id, created.id);
296 assert_eq!(found.email, "alice@example.com");
297 }
298
299 #[tokio::test]
300 async fn find_user_by_email_missing_returns_none() {
301 let store = InMemoryStore::new();
302 let found = store
303 .find_user_by_email("nobody@example.com")
304 .await
305 .unwrap();
306
307 assert!(found.is_none());
308 }
309
310 #[tokio::test]
311 async fn find_user_by_username_existing() {
312 let store = InMemoryStore::new();
313 let created = store
314 .create_user(new_user("alice@example.com", "alice"))
315 .await
316 .unwrap();
317
318 let found = store
319 .find_user_by_username("alice")
320 .await
321 .unwrap()
322 .expect("user should exist");
323
324 assert_eq!(found.id, created.id);
325 }
326
327 #[tokio::test]
328 async fn find_user_by_username_missing_returns_none() {
329 let store = InMemoryStore::new();
330 store
331 .create_user(new_user("alice@example.com", "alice"))
332 .await
333 .unwrap();
334
335 let found = store.find_user_by_username("Alice").await.unwrap();
336
337 assert!(found.is_none());
338 }
339
340 #[tokio::test]
341 async fn find_user_by_id_existing() {
342 let store = InMemoryStore::new();
343 let created = store
344 .create_user(new_user("alice@example.com", "alice"))
345 .await
346 .unwrap();
347
348 let found = store
349 .find_user_by_id(created.id)
350 .await
351 .unwrap()
352 .expect("user should exist");
353
354 assert_eq!(found.email, "alice@example.com");
355 assert_eq!(found.username, "alice");
356 }
357
358 #[tokio::test]
359 async fn find_user_by_id_missing_returns_none() {
360 let store = InMemoryStore::new();
361 let found = store.find_user_by_id(Uuid::now_v7()).await.unwrap();
362 assert!(found.is_none());
363 }
364
365 #[tokio::test]
366 async fn count_users_empty_store() {
367 let store = InMemoryStore::new();
368 assert_eq!(store.count_users().await.unwrap(), 0);
369 }
370
371 #[tokio::test]
372 async fn count_users_with_users() {
373 let store = InMemoryStore::new();
374 store
375 .create_user(new_user("alice@example.com", "alice"))
376 .await
377 .unwrap();
378 store
379 .create_user(new_user("bob@example.com", "bob"))
380 .await
381 .unwrap();
382 assert_eq!(store.count_users().await.unwrap(), 2);
383 }
384
385 #[tokio::test]
386 async fn list_users_paginated() {
387 let store = InMemoryStore::new();
388 for i in 0..5 {
389 store
390 .create_user(new_user(
391 &format!("user{i}@example.com"),
392 &format!("user{i}"),
393 ))
394 .await
395 .unwrap();
396 }
397
398 let page = store.list_users(1, 2).await.unwrap();
399 assert_eq!(page.items.len(), 2);
400 assert_eq!(page.total, 5);
401 assert_eq!(page.page, 1);
402 assert_eq!(page.per_page, 2);
403
404 let page2 = store.list_users(3, 2).await.unwrap();
405 assert_eq!(page2.items.len(), 1);
406 }
407
408 #[tokio::test]
409 async fn list_users_empty_store() {
410 let store = InMemoryStore::new();
411 let page = store.list_users(1, 20).await.unwrap();
412 assert!(page.items.is_empty());
413 assert_eq!(page.total, 0);
414 }
415
416 #[tokio::test]
417 async fn delete_user_existing() {
418 let store = InMemoryStore::new();
419 let user = store
420 .create_user(new_user("alice@example.com", "alice"))
421 .await
422 .unwrap();
423
424 store.delete_user(user.id).await.unwrap();
425 assert_eq!(store.count_users().await.unwrap(), 0);
426 }
427
428 #[tokio::test]
429 async fn delete_user_not_found() {
430 let store = InMemoryStore::new();
431 let err = store.delete_user(Uuid::now_v7()).await.unwrap_err();
432 assert!(matches!(err, StoreError::UserNotFound(_)));
433 }
434
435 #[tokio::test]
436 async fn update_user_role_promote() {
437 let store = InMemoryStore::new();
438 let _admin = store
440 .create_user(new_user("admin@example.com", "admin"))
441 .await
442 .unwrap();
443 let member = store
444 .create_user(new_user("member@example.com", "member"))
445 .await
446 .unwrap();
447 assert!(!member.is_admin);
448
449 let promoted = store.update_user_role(member.id, true).await.unwrap();
450 assert!(promoted.is_admin);
451 }
452
453 #[tokio::test]
454 async fn update_user_role_demote() {
455 let store = InMemoryStore::new();
456 let admin = store
457 .create_user(new_user("admin@example.com", "admin"))
458 .await
459 .unwrap();
460 assert!(admin.is_admin);
461
462 let demoted = store.update_user_role(admin.id, false).await.unwrap();
463 assert!(!demoted.is_admin);
464 }
465
466 #[tokio::test]
467 async fn update_user_role_not_found() {
468 let store = InMemoryStore::new();
469 let err = store
470 .update_user_role(Uuid::now_v7(), true)
471 .await
472 .unwrap_err();
473 assert!(matches!(err, StoreError::UserNotFound(_)));
474 }
475
476 #[tokio::test]
477 async fn set_user_groups_replaces_sorts_and_dedups() {
478 let store = InMemoryStore::new();
479 let user = store
480 .create_user(new_user("alice@example.com", "alice"))
481 .await
482 .unwrap();
483
484 let groups = store
485 .set_user_groups(
486 user.id,
487 vec!["sre".to_string(), "finance".to_string(), "sre".to_string()],
488 )
489 .await
490 .unwrap();
491 assert_eq!(groups, vec!["finance".to_string(), "sre".to_string()]);
492 assert_eq!(store.list_user_groups(user.id).await.unwrap(), groups);
493
494 let replaced = store
495 .set_user_groups(user.id, vec!["legal".to_string()])
496 .await
497 .unwrap();
498 assert_eq!(replaced, vec!["legal".to_string()]);
499 assert_eq!(
500 store.list_user_groups(user.id).await.unwrap(),
501 vec!["legal".to_string()]
502 );
503
504 let cleared = store.set_user_groups(user.id, Vec::new()).await.unwrap();
505 assert!(cleared.is_empty());
506 assert!(store.list_user_groups(user.id).await.unwrap().is_empty());
507 }
508
509 #[tokio::test]
510 async fn set_user_groups_unknown_user_is_not_found() {
511 let store = InMemoryStore::new();
512 let err = store
513 .set_user_groups(Uuid::now_v7(), vec!["sre".to_string()])
514 .await
515 .unwrap_err();
516 assert!(matches!(err, StoreError::UserNotFound(_)));
517 }
518
519 #[tokio::test]
520 async fn list_user_groups_unknown_user_is_empty() {
521 let store = InMemoryStore::new();
522 assert!(
523 store
524 .list_user_groups(Uuid::now_v7())
525 .await
526 .unwrap()
527 .is_empty()
528 );
529 }
530
531 #[tokio::test]
532 async fn delete_user_removes_group_membership() {
533 let store = InMemoryStore::new();
534 let user = store
535 .create_user(new_user("alice@example.com", "alice"))
536 .await
537 .unwrap();
538 store
539 .set_user_groups(user.id, vec!["sre".to_string()])
540 .await
541 .unwrap();
542
543 store.delete_user(user.id).await.unwrap();
544
545 assert!(store.list_user_groups(user.id).await.unwrap().is_empty());
546 }
547}