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_id(&self, id: Uuid) -> StoreFuture<'_, Option<User>> {
55        Box::pin(async move {
56            let state = self.state.read().await;
57            Ok(state.users.get(&id).cloned())
58        })
59    }
60
61    fn count_users(&self) -> StoreFuture<'_, u64> {
62        Box::pin(async move {
63            let state = self.state.read().await;
64            Ok(state.users.len() as u64)
65        })
66    }
67
68    fn list_users(&self, page: u32, per_page: u32) -> StoreFuture<'_, Page<User>> {
69        Box::pin(async move {
70            let state = self.state.read().await;
71            let mut users: Vec<User> = state.users.values().cloned().collect();
72            users.sort_by(|a, b| b.created_at.cmp(&a.created_at));
73
74            let total = users.len() as u64;
75            let offset = ((page.saturating_sub(1)) as usize) * (per_page as usize);
76            let items: Vec<User> = users
77                .into_iter()
78                .skip(offset)
79                .take(per_page as usize)
80                .collect();
81
82            Ok(Page {
83                items,
84                total,
85                page,
86                per_page,
87            })
88        })
89    }
90
91    fn delete_user(&self, id: Uuid) -> StoreFuture<'_, ()> {
92        Box::pin(async move {
93            let mut state = self.state.write().await;
94            state
95                .users
96                .remove(&id)
97                .ok_or(StoreError::UserNotFound(id))?;
98            Ok(())
99        })
100    }
101
102    fn update_user_role(&self, id: Uuid, is_admin: bool) -> StoreFuture<'_, User> {
103        Box::pin(async move {
104            let mut state = self.state.write().await;
105            let user = state
106                .users
107                .get_mut(&id)
108                .ok_or(StoreError::UserNotFound(id))?;
109            user.is_admin = is_admin;
110            user.updated_at = Utc::now();
111            Ok(user.clone())
112        })
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    fn new_user(email: &str, username: &str) -> NewUser {
121        NewUser {
122            email: email.to_string(),
123            username: username.to_string(),
124            password_hash: "argon2hash".to_string(),
125            is_admin: None,
126        }
127    }
128
129    #[tokio::test]
130    async fn create_user_first_user_is_admin() {
131        let store = InMemoryStore::new();
132        let user = store
133            .create_user(new_user("alice@example.com", "alice"))
134            .await
135            .unwrap();
136
137        assert_eq!(user.email, "alice@example.com");
138        assert_eq!(user.username, "alice");
139        assert_eq!(user.password_hash, "argon2hash");
140        assert!(user.is_admin);
141    }
142
143    #[tokio::test]
144    async fn create_user_second_user_is_not_admin() {
145        let store = InMemoryStore::new();
146        store
147            .create_user(new_user("alice@example.com", "alice"))
148            .await
149            .unwrap();
150
151        let second = store
152            .create_user(new_user("bob@example.com", "bob"))
153            .await
154            .unwrap();
155
156        assert!(!second.is_admin);
157    }
158
159    #[tokio::test]
160    async fn create_user_explicit_admin_flag() {
161        let store = InMemoryStore::new();
162        // First user but explicitly set to non-admin
163        let first = store
164            .create_user(NewUser {
165                email: "alice@example.com".to_string(),
166                username: "alice".to_string(),
167                password_hash: "argon2hash".to_string(),
168                is_admin: Some(false),
169            })
170            .await
171            .unwrap();
172        assert!(!first.is_admin);
173
174        // Second user but explicitly set to admin
175        let second = store
176            .create_user(NewUser {
177                email: "bob@example.com".to_string(),
178                username: "bob".to_string(),
179                password_hash: "argon2hash".to_string(),
180                is_admin: Some(true),
181            })
182            .await
183            .unwrap();
184        assert!(second.is_admin);
185    }
186
187    #[tokio::test]
188    async fn create_user_duplicate_email_returns_error() {
189        let store = InMemoryStore::new();
190        store
191            .create_user(new_user("alice@example.com", "alice"))
192            .await
193            .unwrap();
194
195        let err = store
196            .create_user(new_user("alice@example.com", "bob"))
197            .await
198            .unwrap_err();
199
200        assert!(
201            matches!(err, StoreError::DuplicateEmail(ref e) if e == "alice@example.com"),
202            "expected DuplicateEmail, got: {err}"
203        );
204    }
205
206    #[tokio::test]
207    async fn create_user_duplicate_username_returns_error() {
208        let store = InMemoryStore::new();
209        store
210            .create_user(new_user("alice@example.com", "alice"))
211            .await
212            .unwrap();
213
214        let err = store
215            .create_user(new_user("bob@example.com", "alice"))
216            .await
217            .unwrap_err();
218
219        assert!(
220            matches!(err, StoreError::DuplicateUsername(ref u) if u == "alice"),
221            "expected DuplicateUsername, got: {err}"
222        );
223    }
224
225    #[tokio::test]
226    async fn find_user_by_email_existing() {
227        let store = InMemoryStore::new();
228        let created = store
229            .create_user(new_user("alice@example.com", "alice"))
230            .await
231            .unwrap();
232
233        let found = store
234            .find_user_by_email("alice@example.com")
235            .await
236            .unwrap()
237            .expect("user should exist");
238
239        assert_eq!(found.id, created.id);
240        assert_eq!(found.email, "alice@example.com");
241    }
242
243    #[tokio::test]
244    async fn find_user_by_email_missing_returns_none() {
245        let store = InMemoryStore::new();
246        let found = store
247            .find_user_by_email("nobody@example.com")
248            .await
249            .unwrap();
250
251        assert!(found.is_none());
252    }
253
254    #[tokio::test]
255    async fn find_user_by_id_existing() {
256        let store = InMemoryStore::new();
257        let created = store
258            .create_user(new_user("alice@example.com", "alice"))
259            .await
260            .unwrap();
261
262        let found = store
263            .find_user_by_id(created.id)
264            .await
265            .unwrap()
266            .expect("user should exist");
267
268        assert_eq!(found.email, "alice@example.com");
269        assert_eq!(found.username, "alice");
270    }
271
272    #[tokio::test]
273    async fn find_user_by_id_missing_returns_none() {
274        let store = InMemoryStore::new();
275        let found = store.find_user_by_id(Uuid::now_v7()).await.unwrap();
276        assert!(found.is_none());
277    }
278
279    #[tokio::test]
280    async fn count_users_empty_store() {
281        let store = InMemoryStore::new();
282        assert_eq!(store.count_users().await.unwrap(), 0);
283    }
284
285    #[tokio::test]
286    async fn count_users_with_users() {
287        let store = InMemoryStore::new();
288        store
289            .create_user(new_user("alice@example.com", "alice"))
290            .await
291            .unwrap();
292        store
293            .create_user(new_user("bob@example.com", "bob"))
294            .await
295            .unwrap();
296        assert_eq!(store.count_users().await.unwrap(), 2);
297    }
298
299    #[tokio::test]
300    async fn list_users_paginated() {
301        let store = InMemoryStore::new();
302        for i in 0..5 {
303            store
304                .create_user(new_user(
305                    &format!("user{i}@example.com"),
306                    &format!("user{i}"),
307                ))
308                .await
309                .unwrap();
310        }
311
312        let page = store.list_users(1, 2).await.unwrap();
313        assert_eq!(page.items.len(), 2);
314        assert_eq!(page.total, 5);
315        assert_eq!(page.page, 1);
316        assert_eq!(page.per_page, 2);
317
318        let page2 = store.list_users(3, 2).await.unwrap();
319        assert_eq!(page2.items.len(), 1);
320    }
321
322    #[tokio::test]
323    async fn list_users_empty_store() {
324        let store = InMemoryStore::new();
325        let page = store.list_users(1, 20).await.unwrap();
326        assert!(page.items.is_empty());
327        assert_eq!(page.total, 0);
328    }
329
330    #[tokio::test]
331    async fn delete_user_existing() {
332        let store = InMemoryStore::new();
333        let user = store
334            .create_user(new_user("alice@example.com", "alice"))
335            .await
336            .unwrap();
337
338        store.delete_user(user.id).await.unwrap();
339        assert_eq!(store.count_users().await.unwrap(), 0);
340    }
341
342    #[tokio::test]
343    async fn delete_user_not_found() {
344        let store = InMemoryStore::new();
345        let err = store.delete_user(Uuid::now_v7()).await.unwrap_err();
346        assert!(matches!(err, StoreError::UserNotFound(_)));
347    }
348
349    #[tokio::test]
350    async fn update_user_role_promote() {
351        let store = InMemoryStore::new();
352        // Create two users so the first gets auto-admin
353        let _admin = store
354            .create_user(new_user("admin@example.com", "admin"))
355            .await
356            .unwrap();
357        let member = store
358            .create_user(new_user("member@example.com", "member"))
359            .await
360            .unwrap();
361        assert!(!member.is_admin);
362
363        let promoted = store.update_user_role(member.id, true).await.unwrap();
364        assert!(promoted.is_admin);
365    }
366
367    #[tokio::test]
368    async fn update_user_role_demote() {
369        let store = InMemoryStore::new();
370        let admin = store
371            .create_user(new_user("admin@example.com", "admin"))
372            .await
373            .unwrap();
374        assert!(admin.is_admin);
375
376        let demoted = store.update_user_role(admin.id, false).await.unwrap();
377        assert!(!demoted.is_admin);
378    }
379
380    #[tokio::test]
381    async fn update_user_role_not_found() {
382        let store = InMemoryStore::new();
383        let err = store
384            .update_user_role(Uuid::now_v7(), true)
385            .await
386            .unwrap_err();
387        assert!(matches!(err, StoreError::UserNotFound(_)));
388    }
389}