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            // Decided under the state write lock: concurrent sign-ups cannot both
31            // see an empty store.
32            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        // Another user's session is untouched.
341        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        // First user but explicitly set to non-admin
459        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        // Second user but explicitly set to admin
471        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        // Create two users so the first gets auto-admin
679        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}