Skip to main content

ironflow_store/memory/
user_store.rs

1//! [`UserStore`] trait implementation for [`InMemoryStore`].
2
3use 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        // Another user's session is untouched.
339        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        // First user but explicitly set to non-admin
457        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        // Second user but explicitly set to admin
469        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        // Create two users so the first gets auto-admin
677        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}