Skip to main content

ironflow_store/memory/
user_store.rs

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