1use std::collections::BTreeSet;
4
5use chrono::Utc;
6use uuid::Uuid;
7
8use crate::entities::{NewRefreshToken, NewUser, Page, User};
9use crate::error::StoreError;
10use crate::store::StoreFuture;
11use crate::user_store::UserStore;
12
13use super::{InMemoryStore, StoredRefreshToken};
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 token_version: 0,
40 created_at: now,
41 updated_at: now,
42 };
43
44 state.users.insert(user.id, user.clone());
45 Ok(user)
46 })
47 }
48
49 fn find_user_by_email(&self, email: &str) -> StoreFuture<'_, Option<User>> {
50 let email = email.to_string();
51 Box::pin(async move {
52 let state = self.state.read().await;
53 Ok(state.users.values().find(|u| u.email == email).cloned())
54 })
55 }
56
57 fn find_user_by_username(&self, username: &str) -> StoreFuture<'_, Option<User>> {
58 let username = username.to_string();
59 Box::pin(async move {
60 let state = self.state.read().await;
61 Ok(state
62 .users
63 .values()
64 .find(|u| u.username == username)
65 .cloned())
66 })
67 }
68
69 fn find_user_by_id(&self, id: Uuid) -> StoreFuture<'_, Option<User>> {
70 Box::pin(async move {
71 let state = self.state.read().await;
72 Ok(state.users.get(&id).cloned())
73 })
74 }
75
76 fn count_users(&self) -> StoreFuture<'_, u64> {
77 Box::pin(async move {
78 let state = self.state.read().await;
79 Ok(state.users.len() as u64)
80 })
81 }
82
83 fn list_users(&self, page: u32, per_page: u32) -> StoreFuture<'_, Page<User>> {
84 Box::pin(async move {
85 let state = self.state.read().await;
86 let mut users: Vec<User> = state.users.values().cloned().collect();
87 users.sort_by_key(|u| std::cmp::Reverse(u.created_at));
88
89 let total = users.len() as u64;
90 let offset = ((page.saturating_sub(1)) as usize) * (per_page as usize);
91 let items: Vec<User> = users
92 .into_iter()
93 .skip(offset)
94 .take(per_page as usize)
95 .collect();
96
97 Ok(Page {
98 items,
99 total,
100 page,
101 per_page,
102 })
103 })
104 }
105
106 fn delete_user(&self, id: Uuid) -> StoreFuture<'_, ()> {
107 Box::pin(async move {
108 let mut state = self.state.write().await;
109 state
110 .users
111 .remove(&id)
112 .ok_or(StoreError::UserNotFound(id))?;
113 state.user_groups.remove(&id);
114 state.refresh_tokens.retain(|_, t| t.user_id != id);
115 Ok(())
116 })
117 }
118
119 fn update_user_role(&self, id: Uuid, is_admin: bool) -> StoreFuture<'_, User> {
120 Box::pin(async move {
121 let mut state = self.state.write().await;
122 let user = state
123 .users
124 .get_mut(&id)
125 .ok_or(StoreError::UserNotFound(id))?;
126 user.is_admin = is_admin;
127 user.token_version += 1;
128 user.updated_at = Utc::now();
129 let user = user.clone();
130 state.refresh_tokens.retain(|_, t| t.user_id != id);
131 Ok(user)
132 })
133 }
134
135 fn update_user_password(&self, id: Uuid, password_hash: String) -> StoreFuture<'_, ()> {
136 Box::pin(async move {
137 let mut state = self.state.write().await;
138 let user = state
139 .users
140 .get_mut(&id)
141 .ok_or(StoreError::UserNotFound(id))?;
142 user.password_hash = password_hash;
143 user.token_version += 1;
144 user.updated_at = Utc::now();
145 state.refresh_tokens.retain(|_, t| t.user_id != id);
146 Ok(())
147 })
148 }
149
150 fn list_user_groups(&self, user_id: Uuid) -> StoreFuture<'_, Vec<String>> {
151 Box::pin(async move {
152 let state = self.state.read().await;
153 Ok(state
154 .user_groups
155 .get(&user_id)
156 .map(|groups| groups.iter().cloned().collect())
157 .unwrap_or_default())
158 })
159 }
160
161 fn set_user_groups(&self, user_id: Uuid, groups: Vec<String>) -> StoreFuture<'_, Vec<String>> {
162 Box::pin(async move {
163 let mut state = self.state.write().await;
164 if !state.users.contains_key(&user_id) {
165 return Err(StoreError::UserNotFound(user_id));
166 }
167 let groups: BTreeSet<String> = groups.into_iter().collect();
168 let sorted: Vec<String> = groups.iter().cloned().collect();
169 if groups.is_empty() {
170 state.user_groups.remove(&user_id);
171 } else {
172 state.user_groups.insert(user_id, groups);
173 }
174 Ok(sorted)
175 })
176 }
177
178 fn revoke_user_sessions(&self, id: Uuid) -> StoreFuture<'_, i64> {
179 Box::pin(async move {
180 let mut state = self.state.write().await;
181 let user = state
182 .users
183 .get_mut(&id)
184 .ok_or(StoreError::UserNotFound(id))?;
185 user.token_version += 1;
186 user.updated_at = Utc::now();
187 let version = user.token_version;
188 state.refresh_tokens.retain(|_, t| t.user_id != id);
189 Ok(version)
190 })
191 }
192
193 fn store_refresh_token(&self, token: NewRefreshToken) -> StoreFuture<'_, ()> {
194 Box::pin(async move {
195 let mut state = self.state.write().await;
196 if !state.users.contains_key(&token.user_id) {
197 return Err(StoreError::UserNotFound(token.user_id));
198 }
199 let now = Utc::now();
200 state
201 .refresh_tokens
202 .retain(|_, t| t.user_id != token.user_id || t.expires_at > now);
203 state.refresh_tokens.insert(
204 token.token_hash,
205 StoredRefreshToken {
206 user_id: token.user_id,
207 expires_at: token.expires_at,
208 },
209 );
210 Ok(())
211 })
212 }
213
214 fn consume_refresh_token(&self, token_hash: &str) -> StoreFuture<'_, Option<Uuid>> {
215 let token_hash = token_hash.to_string();
216 Box::pin(async move {
217 let mut state = self.state.write().await;
218 Ok(state
219 .refresh_tokens
220 .remove(&token_hash)
221 .filter(|t| t.expires_at > Utc::now())
222 .map(|t| t.user_id))
223 })
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use chrono::Duration;
230
231 use super::*;
232
233 fn refresh_token(hash: &str, user_id: Uuid, ttl: Duration) -> NewRefreshToken {
234 NewRefreshToken {
235 token_hash: hash.to_string(),
236 user_id,
237 expires_at: Utc::now() + ttl,
238 }
239 }
240
241 #[tokio::test]
242 async fn consume_refresh_token_is_single_use() {
243 let store = InMemoryStore::new();
244 let user = store
245 .create_user(new_user("alice@example.com", "alice"))
246 .await
247 .unwrap();
248 store
249 .store_refresh_token(refresh_token("h1", user.id, Duration::hours(1)))
250 .await
251 .unwrap();
252
253 assert_eq!(
254 store.consume_refresh_token("h1").await.unwrap(),
255 Some(user.id)
256 );
257 assert_eq!(store.consume_refresh_token("h1").await.unwrap(), None);
258 }
259
260 #[tokio::test]
261 async fn consume_unknown_refresh_token_returns_none() {
262 let store = InMemoryStore::new();
263 assert_eq!(store.consume_refresh_token("missing").await.unwrap(), None);
264 }
265
266 #[tokio::test]
267 async fn consume_expired_refresh_token_returns_none() {
268 let store = InMemoryStore::new();
269 let user = store
270 .create_user(new_user("alice@example.com", "alice"))
271 .await
272 .unwrap();
273 store
274 .store_refresh_token(refresh_token("old", user.id, Duration::seconds(-5)))
275 .await
276 .unwrap();
277
278 assert_eq!(store.consume_refresh_token("old").await.unwrap(), None);
279 }
280
281 #[tokio::test]
282 async fn store_refresh_token_drops_expired_rows_of_the_user() {
283 let store = InMemoryStore::new();
284 let user = store
285 .create_user(new_user("alice@example.com", "alice"))
286 .await
287 .unwrap();
288 store
289 .store_refresh_token(refresh_token("old", user.id, Duration::seconds(-5)))
290 .await
291 .unwrap();
292 store
293 .store_refresh_token(refresh_token("new", user.id, Duration::hours(1)))
294 .await
295 .unwrap();
296
297 let state = store.state.read().await;
298 assert!(!state.refresh_tokens.contains_key("old"));
299 assert!(state.refresh_tokens.contains_key("new"));
300 }
301
302 #[tokio::test]
303 async fn store_refresh_token_unknown_user_errors() {
304 let store = InMemoryStore::new();
305 let err = store
306 .store_refresh_token(refresh_token("h1", Uuid::now_v7(), Duration::hours(1)))
307 .await
308 .unwrap_err();
309 assert!(matches!(err, StoreError::UserNotFound(_)));
310 }
311
312 #[tokio::test]
313 async fn revoke_user_sessions_bumps_version_and_drops_refresh_tokens() {
314 let store = InMemoryStore::new();
315 let alice = store
316 .create_user(new_user("alice@example.com", "alice"))
317 .await
318 .unwrap();
319 let bob = store
320 .create_user(new_user("bob@example.com", "bob"))
321 .await
322 .unwrap();
323 store
324 .store_refresh_token(refresh_token("a1", alice.id, Duration::hours(1)))
325 .await
326 .unwrap();
327 store
328 .store_refresh_token(refresh_token("b1", bob.id, Duration::hours(1)))
329 .await
330 .unwrap();
331
332 assert_eq!(store.revoke_user_sessions(alice.id).await.unwrap(), 1);
333 assert_eq!(store.revoke_user_sessions(alice.id).await.unwrap(), 2);
334
335 let found = store.find_user_by_id(alice.id).await.unwrap().unwrap();
336 assert_eq!(found.token_version, 2);
337 assert_eq!(store.consume_refresh_token("a1").await.unwrap(), None);
338 assert_eq!(
340 store.consume_refresh_token("b1").await.unwrap(),
341 Some(bob.id)
342 );
343 }
344
345 #[tokio::test]
346 async fn revoke_user_sessions_unknown_user_errors() {
347 let store = InMemoryStore::new();
348 let err = store
349 .revoke_user_sessions(Uuid::now_v7())
350 .await
351 .unwrap_err();
352 assert!(matches!(err, StoreError::UserNotFound(_)));
353 }
354
355 #[tokio::test]
356 async fn update_user_role_bumps_token_version() {
357 let store = InMemoryStore::new();
358 let admin = store
359 .create_user(new_user("admin@example.com", "admin"))
360 .await
361 .unwrap();
362 assert_eq!(admin.token_version, 0);
363 store
364 .store_refresh_token(refresh_token("h1", admin.id, Duration::hours(1)))
365 .await
366 .unwrap();
367
368 let demoted = store.update_user_role(admin.id, false).await.unwrap();
369
370 assert_eq!(demoted.token_version, 1);
371 assert_eq!(store.consume_refresh_token("h1").await.unwrap(), None);
372 }
373
374 #[tokio::test]
375 async fn update_user_password_bumps_token_version() {
376 let store = InMemoryStore::new();
377 let user = store
378 .create_user(new_user("alice@example.com", "alice"))
379 .await
380 .unwrap();
381 store
382 .store_refresh_token(refresh_token("h1", user.id, Duration::hours(1)))
383 .await
384 .unwrap();
385
386 store
387 .update_user_password(user.id, "newhash".to_string())
388 .await
389 .unwrap();
390
391 let found = store.find_user_by_id(user.id).await.unwrap().unwrap();
392 assert_eq!(found.token_version, 1);
393 assert_eq!(found.password_hash, "newhash");
394 assert_eq!(store.consume_refresh_token("h1").await.unwrap(), None);
395 }
396
397 #[tokio::test]
398 async fn delete_user_drops_refresh_tokens() {
399 let store = InMemoryStore::new();
400 let user = store
401 .create_user(new_user("alice@example.com", "alice"))
402 .await
403 .unwrap();
404 store
405 .store_refresh_token(refresh_token("h1", user.id, Duration::hours(1)))
406 .await
407 .unwrap();
408
409 store.delete_user(user.id).await.unwrap();
410
411 assert_eq!(store.consume_refresh_token("h1").await.unwrap(), None);
412 }
413
414 fn new_user(email: &str, username: &str) -> NewUser {
415 NewUser {
416 email: email.to_string(),
417 username: username.to_string(),
418 password_hash: "argon2hash".to_string(),
419 is_admin: None,
420 }
421 }
422
423 #[tokio::test]
424 async fn create_user_first_user_is_admin() {
425 let store = InMemoryStore::new();
426 let user = store
427 .create_user(new_user("alice@example.com", "alice"))
428 .await
429 .unwrap();
430
431 assert_eq!(user.email, "alice@example.com");
432 assert_eq!(user.username, "alice");
433 assert_eq!(user.password_hash, "argon2hash");
434 assert!(user.is_admin);
435 }
436
437 #[tokio::test]
438 async fn create_user_second_user_is_not_admin() {
439 let store = InMemoryStore::new();
440 store
441 .create_user(new_user("alice@example.com", "alice"))
442 .await
443 .unwrap();
444
445 let second = store
446 .create_user(new_user("bob@example.com", "bob"))
447 .await
448 .unwrap();
449
450 assert!(!second.is_admin);
451 }
452
453 #[tokio::test]
454 async fn create_user_explicit_admin_flag() {
455 let store = InMemoryStore::new();
456 let first = store
458 .create_user(NewUser {
459 email: "alice@example.com".to_string(),
460 username: "alice".to_string(),
461 password_hash: "argon2hash".to_string(),
462 is_admin: Some(false),
463 })
464 .await
465 .unwrap();
466 assert!(!first.is_admin);
467
468 let second = store
470 .create_user(NewUser {
471 email: "bob@example.com".to_string(),
472 username: "bob".to_string(),
473 password_hash: "argon2hash".to_string(),
474 is_admin: Some(true),
475 })
476 .await
477 .unwrap();
478 assert!(second.is_admin);
479 }
480
481 #[tokio::test]
482 async fn create_user_duplicate_email_returns_error() {
483 let store = InMemoryStore::new();
484 store
485 .create_user(new_user("alice@example.com", "alice"))
486 .await
487 .unwrap();
488
489 let err = store
490 .create_user(new_user("alice@example.com", "bob"))
491 .await
492 .unwrap_err();
493
494 assert!(
495 matches!(err, StoreError::DuplicateEmail(ref e) if e == "alice@example.com"),
496 "expected DuplicateEmail, got: {err}"
497 );
498 }
499
500 #[tokio::test]
501 async fn create_user_duplicate_username_returns_error() {
502 let store = InMemoryStore::new();
503 store
504 .create_user(new_user("alice@example.com", "alice"))
505 .await
506 .unwrap();
507
508 let err = store
509 .create_user(new_user("bob@example.com", "alice"))
510 .await
511 .unwrap_err();
512
513 assert!(
514 matches!(err, StoreError::DuplicateUsername(ref u) if u == "alice"),
515 "expected DuplicateUsername, got: {err}"
516 );
517 }
518
519 #[tokio::test]
520 async fn find_user_by_email_existing() {
521 let store = InMemoryStore::new();
522 let created = store
523 .create_user(new_user("alice@example.com", "alice"))
524 .await
525 .unwrap();
526
527 let found = store
528 .find_user_by_email("alice@example.com")
529 .await
530 .unwrap()
531 .expect("user should exist");
532
533 assert_eq!(found.id, created.id);
534 assert_eq!(found.email, "alice@example.com");
535 }
536
537 #[tokio::test]
538 async fn find_user_by_email_missing_returns_none() {
539 let store = InMemoryStore::new();
540 let found = store
541 .find_user_by_email("nobody@example.com")
542 .await
543 .unwrap();
544
545 assert!(found.is_none());
546 }
547
548 #[tokio::test]
549 async fn find_user_by_username_existing() {
550 let store = InMemoryStore::new();
551 let created = store
552 .create_user(new_user("alice@example.com", "alice"))
553 .await
554 .unwrap();
555
556 let found = store
557 .find_user_by_username("alice")
558 .await
559 .unwrap()
560 .expect("user should exist");
561
562 assert_eq!(found.id, created.id);
563 }
564
565 #[tokio::test]
566 async fn find_user_by_username_missing_returns_none() {
567 let store = InMemoryStore::new();
568 store
569 .create_user(new_user("alice@example.com", "alice"))
570 .await
571 .unwrap();
572
573 let found = store.find_user_by_username("Alice").await.unwrap();
574
575 assert!(found.is_none());
576 }
577
578 #[tokio::test]
579 async fn find_user_by_id_existing() {
580 let store = InMemoryStore::new();
581 let created = store
582 .create_user(new_user("alice@example.com", "alice"))
583 .await
584 .unwrap();
585
586 let found = store
587 .find_user_by_id(created.id)
588 .await
589 .unwrap()
590 .expect("user should exist");
591
592 assert_eq!(found.email, "alice@example.com");
593 assert_eq!(found.username, "alice");
594 }
595
596 #[tokio::test]
597 async fn find_user_by_id_missing_returns_none() {
598 let store = InMemoryStore::new();
599 let found = store.find_user_by_id(Uuid::now_v7()).await.unwrap();
600 assert!(found.is_none());
601 }
602
603 #[tokio::test]
604 async fn count_users_empty_store() {
605 let store = InMemoryStore::new();
606 assert_eq!(store.count_users().await.unwrap(), 0);
607 }
608
609 #[tokio::test]
610 async fn count_users_with_users() {
611 let store = InMemoryStore::new();
612 store
613 .create_user(new_user("alice@example.com", "alice"))
614 .await
615 .unwrap();
616 store
617 .create_user(new_user("bob@example.com", "bob"))
618 .await
619 .unwrap();
620 assert_eq!(store.count_users().await.unwrap(), 2);
621 }
622
623 #[tokio::test]
624 async fn list_users_paginated() {
625 let store = InMemoryStore::new();
626 for i in 0..5 {
627 store
628 .create_user(new_user(
629 &format!("user{i}@example.com"),
630 &format!("user{i}"),
631 ))
632 .await
633 .unwrap();
634 }
635
636 let page = store.list_users(1, 2).await.unwrap();
637 assert_eq!(page.items.len(), 2);
638 assert_eq!(page.total, 5);
639 assert_eq!(page.page, 1);
640 assert_eq!(page.per_page, 2);
641
642 let page2 = store.list_users(3, 2).await.unwrap();
643 assert_eq!(page2.items.len(), 1);
644 }
645
646 #[tokio::test]
647 async fn list_users_empty_store() {
648 let store = InMemoryStore::new();
649 let page = store.list_users(1, 20).await.unwrap();
650 assert!(page.items.is_empty());
651 assert_eq!(page.total, 0);
652 }
653
654 #[tokio::test]
655 async fn delete_user_existing() {
656 let store = InMemoryStore::new();
657 let user = store
658 .create_user(new_user("alice@example.com", "alice"))
659 .await
660 .unwrap();
661
662 store.delete_user(user.id).await.unwrap();
663 assert_eq!(store.count_users().await.unwrap(), 0);
664 }
665
666 #[tokio::test]
667 async fn delete_user_not_found() {
668 let store = InMemoryStore::new();
669 let err = store.delete_user(Uuid::now_v7()).await.unwrap_err();
670 assert!(matches!(err, StoreError::UserNotFound(_)));
671 }
672
673 #[tokio::test]
674 async fn update_user_role_promote() {
675 let store = InMemoryStore::new();
676 let _admin = store
678 .create_user(new_user("admin@example.com", "admin"))
679 .await
680 .unwrap();
681 let member = store
682 .create_user(new_user("member@example.com", "member"))
683 .await
684 .unwrap();
685 assert!(!member.is_admin);
686
687 let promoted = store.update_user_role(member.id, true).await.unwrap();
688 assert!(promoted.is_admin);
689 }
690
691 #[tokio::test]
692 async fn update_user_role_demote() {
693 let store = InMemoryStore::new();
694 let admin = store
695 .create_user(new_user("admin@example.com", "admin"))
696 .await
697 .unwrap();
698 assert!(admin.is_admin);
699
700 let demoted = store.update_user_role(admin.id, false).await.unwrap();
701 assert!(!demoted.is_admin);
702 }
703
704 #[tokio::test]
705 async fn update_user_role_not_found() {
706 let store = InMemoryStore::new();
707 let err = store
708 .update_user_role(Uuid::now_v7(), true)
709 .await
710 .unwrap_err();
711 assert!(matches!(err, StoreError::UserNotFound(_)));
712 }
713
714 #[tokio::test]
715 async fn set_user_groups_replaces_sorts_and_dedups() {
716 let store = InMemoryStore::new();
717 let user = store
718 .create_user(new_user("alice@example.com", "alice"))
719 .await
720 .unwrap();
721
722 let groups = store
723 .set_user_groups(
724 user.id,
725 vec!["sre".to_string(), "finance".to_string(), "sre".to_string()],
726 )
727 .await
728 .unwrap();
729 assert_eq!(groups, vec!["finance".to_string(), "sre".to_string()]);
730 assert_eq!(store.list_user_groups(user.id).await.unwrap(), groups);
731
732 let replaced = store
733 .set_user_groups(user.id, vec!["legal".to_string()])
734 .await
735 .unwrap();
736 assert_eq!(replaced, vec!["legal".to_string()]);
737 assert_eq!(
738 store.list_user_groups(user.id).await.unwrap(),
739 vec!["legal".to_string()]
740 );
741
742 let cleared = store.set_user_groups(user.id, Vec::new()).await.unwrap();
743 assert!(cleared.is_empty());
744 assert!(store.list_user_groups(user.id).await.unwrap().is_empty());
745 }
746
747 #[tokio::test]
748 async fn set_user_groups_unknown_user_is_not_found() {
749 let store = InMemoryStore::new();
750 let err = store
751 .set_user_groups(Uuid::now_v7(), vec!["sre".to_string()])
752 .await
753 .unwrap_err();
754 assert!(matches!(err, StoreError::UserNotFound(_)));
755 }
756
757 #[tokio::test]
758 async fn list_user_groups_unknown_user_is_empty() {
759 let store = InMemoryStore::new();
760 assert!(
761 store
762 .list_user_groups(Uuid::now_v7())
763 .await
764 .unwrap()
765 .is_empty()
766 );
767 }
768
769 #[tokio::test]
770 async fn delete_user_removes_group_membership() {
771 let store = InMemoryStore::new();
772 let user = store
773 .create_user(new_user("alice@example.com", "alice"))
774 .await
775 .unwrap();
776 store
777 .set_user_groups(user.id, vec!["sre".to_string()])
778 .await
779 .unwrap();
780
781 store.delete_user(user.id).await.unwrap();
782
783 assert!(store.list_user_groups(user.id).await.unwrap().is_empty());
784 }
785}