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