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