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