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::{ApiKeyScope, 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            // Mirrors the `ON DELETE CASCADE` of `iam.api_keys.user_id` in Postgres.
118            state.api_keys.retain(|_, k| k.user_id != id);
119            Ok(())
120        })
121    }
122
123    fn update_user_role(&self, id: Uuid, is_admin: bool) -> StoreFuture<'_, User> {
124        Box::pin(async move {
125            let mut state = self.state.write().await;
126            let user = state
127                .users
128                .get_mut(&id)
129                .ok_or(StoreError::UserNotFound(id))?;
130            user.is_admin = is_admin;
131            user.token_version += 1;
132            user.updated_at = Utc::now();
133            let user = user.clone();
134            state.refresh_tokens.retain(|_, t| t.user_id != id);
135            if !is_admin {
136                let now = Utc::now();
137                let allowed = ApiKeyScope::member_allowed();
138                for key in state.api_keys.values_mut().filter(|k| k.user_id == id) {
139                    let before = key.scopes.len();
140                    key.scopes.retain(|s| allowed.contains(s));
141                    if key.scopes.len() != before {
142                        if key.scopes.is_empty() {
143                            key.is_active = false;
144                        }
145                        key.updated_at = now;
146                    }
147                }
148            }
149            Ok(user)
150        })
151    }
152
153    fn update_user_password(&self, id: Uuid, password_hash: String) -> StoreFuture<'_, ()> {
154        Box::pin(async move {
155            let mut state = self.state.write().await;
156            let user = state
157                .users
158                .get_mut(&id)
159                .ok_or(StoreError::UserNotFound(id))?;
160            user.password_hash = password_hash;
161            user.token_version += 1;
162            user.updated_at = Utc::now();
163            state.refresh_tokens.retain(|_, t| t.user_id != id);
164            Ok(())
165        })
166    }
167
168    fn list_user_groups(&self, user_id: Uuid) -> StoreFuture<'_, Vec<String>> {
169        Box::pin(async move {
170            let state = self.state.read().await;
171            Ok(state
172                .user_groups
173                .get(&user_id)
174                .map(|groups| groups.iter().cloned().collect())
175                .unwrap_or_default())
176        })
177    }
178
179    fn set_user_groups(&self, user_id: Uuid, groups: Vec<String>) -> StoreFuture<'_, Vec<String>> {
180        Box::pin(async move {
181            let mut state = self.state.write().await;
182            if !state.users.contains_key(&user_id) {
183                return Err(StoreError::UserNotFound(user_id));
184            }
185            let groups: BTreeSet<String> = groups.into_iter().collect();
186            let sorted: Vec<String> = groups.iter().cloned().collect();
187            if groups.is_empty() {
188                state.user_groups.remove(&user_id);
189            } else {
190                state.user_groups.insert(user_id, groups);
191            }
192            Ok(sorted)
193        })
194    }
195
196    fn revoke_user_sessions(&self, id: Uuid) -> StoreFuture<'_, i64> {
197        Box::pin(async move {
198            let mut state = self.state.write().await;
199            let user = state
200                .users
201                .get_mut(&id)
202                .ok_or(StoreError::UserNotFound(id))?;
203            user.token_version += 1;
204            user.updated_at = Utc::now();
205            let version = user.token_version;
206            state.refresh_tokens.retain(|_, t| t.user_id != id);
207            Ok(version)
208        })
209    }
210
211    fn store_refresh_token(&self, token: NewRefreshToken) -> StoreFuture<'_, ()> {
212        Box::pin(async move {
213            let mut state = self.state.write().await;
214            if !state.users.contains_key(&token.user_id) {
215                return Err(StoreError::UserNotFound(token.user_id));
216            }
217            let now = Utc::now();
218            state
219                .refresh_tokens
220                .retain(|_, t| t.user_id != token.user_id || t.expires_at > now);
221            state.refresh_tokens.insert(
222                token.token_hash,
223                StoredRefreshToken {
224                    user_id: token.user_id,
225                    expires_at: token.expires_at,
226                },
227            );
228            Ok(())
229        })
230    }
231
232    fn consume_refresh_token(&self, token_hash: &str) -> StoreFuture<'_, Option<Uuid>> {
233        let token_hash = token_hash.to_string();
234        Box::pin(async move {
235            let mut state = self.state.write().await;
236            Ok(state
237                .refresh_tokens
238                .remove(&token_hash)
239                .filter(|t| t.expires_at > Utc::now())
240                .map(|t| t.user_id))
241        })
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use chrono::Duration;
248
249    use crate::api_key_store::ApiKeyStore;
250    use crate::entities::NewApiKey;
251
252    use super::*;
253
254    fn refresh_token(hash: &str, user_id: Uuid, ttl: Duration) -> NewRefreshToken {
255        NewRefreshToken {
256            token_hash: hash.to_string(),
257            user_id,
258            expires_at: Utc::now() + ttl,
259        }
260    }
261
262    #[tokio::test]
263    async fn consume_refresh_token_is_single_use() {
264        let store = InMemoryStore::new();
265        let user = store
266            .create_user(new_user("alice@example.com", "alice"))
267            .await
268            .unwrap();
269        store
270            .store_refresh_token(refresh_token("h1", user.id, Duration::hours(1)))
271            .await
272            .unwrap();
273
274        assert_eq!(
275            store.consume_refresh_token("h1").await.unwrap(),
276            Some(user.id)
277        );
278        assert_eq!(store.consume_refresh_token("h1").await.unwrap(), None);
279    }
280
281    #[tokio::test]
282    async fn consume_unknown_refresh_token_returns_none() {
283        let store = InMemoryStore::new();
284        assert_eq!(store.consume_refresh_token("missing").await.unwrap(), None);
285    }
286
287    #[tokio::test]
288    async fn consume_expired_refresh_token_returns_none() {
289        let store = InMemoryStore::new();
290        let user = store
291            .create_user(new_user("alice@example.com", "alice"))
292            .await
293            .unwrap();
294        store
295            .store_refresh_token(refresh_token("old", user.id, Duration::seconds(-5)))
296            .await
297            .unwrap();
298
299        assert_eq!(store.consume_refresh_token("old").await.unwrap(), None);
300    }
301
302    #[tokio::test]
303    async fn store_refresh_token_drops_expired_rows_of_the_user() {
304        let store = InMemoryStore::new();
305        let user = store
306            .create_user(new_user("alice@example.com", "alice"))
307            .await
308            .unwrap();
309        store
310            .store_refresh_token(refresh_token("old", user.id, Duration::seconds(-5)))
311            .await
312            .unwrap();
313        store
314            .store_refresh_token(refresh_token("new", user.id, Duration::hours(1)))
315            .await
316            .unwrap();
317
318        let state = store.state.read().await;
319        assert!(!state.refresh_tokens.contains_key("old"));
320        assert!(state.refresh_tokens.contains_key("new"));
321    }
322
323    #[tokio::test]
324    async fn store_refresh_token_unknown_user_errors() {
325        let store = InMemoryStore::new();
326        let err = store
327            .store_refresh_token(refresh_token("h1", Uuid::now_v7(), Duration::hours(1)))
328            .await
329            .unwrap_err();
330        assert!(matches!(err, StoreError::UserNotFound(_)));
331    }
332
333    #[tokio::test]
334    async fn revoke_user_sessions_bumps_version_and_drops_refresh_tokens() {
335        let store = InMemoryStore::new();
336        let alice = store
337            .create_user(new_user("alice@example.com", "alice"))
338            .await
339            .unwrap();
340        let bob = store
341            .create_user(new_user("bob@example.com", "bob"))
342            .await
343            .unwrap();
344        store
345            .store_refresh_token(refresh_token("a1", alice.id, Duration::hours(1)))
346            .await
347            .unwrap();
348        store
349            .store_refresh_token(refresh_token("b1", bob.id, Duration::hours(1)))
350            .await
351            .unwrap();
352
353        assert_eq!(store.revoke_user_sessions(alice.id).await.unwrap(), 1);
354        assert_eq!(store.revoke_user_sessions(alice.id).await.unwrap(), 2);
355
356        let found = store.find_user_by_id(alice.id).await.unwrap().unwrap();
357        assert_eq!(found.token_version, 2);
358        assert_eq!(store.consume_refresh_token("a1").await.unwrap(), None);
359        // Another user's session is untouched.
360        assert_eq!(
361            store.consume_refresh_token("b1").await.unwrap(),
362            Some(bob.id)
363        );
364    }
365
366    #[tokio::test]
367    async fn revoke_user_sessions_unknown_user_errors() {
368        let store = InMemoryStore::new();
369        let err = store
370            .revoke_user_sessions(Uuid::now_v7())
371            .await
372            .unwrap_err();
373        assert!(matches!(err, StoreError::UserNotFound(_)));
374    }
375
376    #[tokio::test]
377    async fn update_user_role_bumps_token_version() {
378        let store = InMemoryStore::new();
379        let admin = store
380            .create_user(new_user("admin@example.com", "admin"))
381            .await
382            .unwrap();
383        assert_eq!(admin.token_version, 0);
384        store
385            .store_refresh_token(refresh_token("h1", admin.id, Duration::hours(1)))
386            .await
387            .unwrap();
388
389        let demoted = store.update_user_role(admin.id, false).await.unwrap();
390
391        assert_eq!(demoted.token_version, 1);
392        assert_eq!(store.consume_refresh_token("h1").await.unwrap(), None);
393    }
394
395    #[tokio::test]
396    async fn update_user_password_bumps_token_version() {
397        let store = InMemoryStore::new();
398        let user = store
399            .create_user(new_user("alice@example.com", "alice"))
400            .await
401            .unwrap();
402        store
403            .store_refresh_token(refresh_token("h1", user.id, Duration::hours(1)))
404            .await
405            .unwrap();
406
407        store
408            .update_user_password(user.id, "newhash".to_string())
409            .await
410            .unwrap();
411
412        let found = store.find_user_by_id(user.id).await.unwrap().unwrap();
413        assert_eq!(found.token_version, 1);
414        assert_eq!(found.password_hash, "newhash");
415        assert_eq!(store.consume_refresh_token("h1").await.unwrap(), None);
416    }
417
418    #[tokio::test]
419    async fn delete_user_drops_refresh_tokens() {
420        let store = InMemoryStore::new();
421        let user = store
422            .create_user(new_user("alice@example.com", "alice"))
423            .await
424            .unwrap();
425        store
426            .store_refresh_token(refresh_token("h1", user.id, Duration::hours(1)))
427            .await
428            .unwrap();
429
430        store.delete_user(user.id).await.unwrap();
431
432        assert_eq!(store.consume_refresh_token("h1").await.unwrap(), None);
433    }
434
435    fn new_user(email: &str, username: &str) -> NewUser {
436        NewUser {
437            email: email.to_string(),
438            username: username.to_string(),
439            password_hash: "argon2hash".to_string(),
440            is_admin: None,
441        }
442    }
443
444    #[tokio::test]
445    async fn create_user_first_user_is_admin() {
446        let store = InMemoryStore::new();
447        let user = store
448            .create_user(new_user("alice@example.com", "alice"))
449            .await
450            .unwrap();
451
452        assert_eq!(user.email, "alice@example.com");
453        assert_eq!(user.username, "alice");
454        assert_eq!(user.password_hash, "argon2hash");
455        assert!(user.is_admin);
456    }
457
458    #[tokio::test]
459    async fn create_user_second_user_is_not_admin() {
460        let store = InMemoryStore::new();
461        store
462            .create_user(new_user("alice@example.com", "alice"))
463            .await
464            .unwrap();
465
466        let second = store
467            .create_user(new_user("bob@example.com", "bob"))
468            .await
469            .unwrap();
470
471        assert!(!second.is_admin);
472    }
473
474    #[tokio::test]
475    async fn create_user_explicit_admin_flag() {
476        let store = InMemoryStore::new();
477        // First user but explicitly set to non-admin
478        let first = store
479            .create_user(NewUser {
480                email: "alice@example.com".to_string(),
481                username: "alice".to_string(),
482                password_hash: "argon2hash".to_string(),
483                is_admin: Some(false),
484            })
485            .await
486            .unwrap();
487        assert!(!first.is_admin);
488
489        // Second user but explicitly set to admin
490        let second = store
491            .create_user(NewUser {
492                email: "bob@example.com".to_string(),
493                username: "bob".to_string(),
494                password_hash: "argon2hash".to_string(),
495                is_admin: Some(true),
496            })
497            .await
498            .unwrap();
499        assert!(second.is_admin);
500    }
501
502    #[tokio::test]
503    async fn create_user_duplicate_email_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("alice@example.com", "bob"))
512            .await
513            .unwrap_err();
514
515        assert!(
516            matches!(err, StoreError::DuplicateEmail(ref e) if e == "alice@example.com"),
517            "expected DuplicateEmail, got: {err}"
518        );
519    }
520
521    #[tokio::test]
522    async fn create_user_duplicate_username_returns_error() {
523        let store = InMemoryStore::new();
524        store
525            .create_user(new_user("alice@example.com", "alice"))
526            .await
527            .unwrap();
528
529        let err = store
530            .create_user(new_user("bob@example.com", "alice"))
531            .await
532            .unwrap_err();
533
534        assert!(
535            matches!(err, StoreError::DuplicateUsername(ref u) if u == "alice"),
536            "expected DuplicateUsername, got: {err}"
537        );
538    }
539
540    #[tokio::test]
541    async fn find_user_by_email_existing() {
542        let store = InMemoryStore::new();
543        let created = store
544            .create_user(new_user("alice@example.com", "alice"))
545            .await
546            .unwrap();
547
548        let found = store
549            .find_user_by_email("alice@example.com")
550            .await
551            .unwrap()
552            .expect("user should exist");
553
554        assert_eq!(found.id, created.id);
555        assert_eq!(found.email, "alice@example.com");
556    }
557
558    #[tokio::test]
559    async fn find_user_by_email_missing_returns_none() {
560        let store = InMemoryStore::new();
561        let found = store
562            .find_user_by_email("nobody@example.com")
563            .await
564            .unwrap();
565
566        assert!(found.is_none());
567    }
568
569    #[tokio::test]
570    async fn find_user_by_username_existing() {
571        let store = InMemoryStore::new();
572        let created = store
573            .create_user(new_user("alice@example.com", "alice"))
574            .await
575            .unwrap();
576
577        let found = store
578            .find_user_by_username("alice")
579            .await
580            .unwrap()
581            .expect("user should exist");
582
583        assert_eq!(found.id, created.id);
584    }
585
586    #[tokio::test]
587    async fn find_user_by_username_missing_returns_none() {
588        let store = InMemoryStore::new();
589        store
590            .create_user(new_user("alice@example.com", "alice"))
591            .await
592            .unwrap();
593
594        let found = store.find_user_by_username("Alice").await.unwrap();
595
596        assert!(found.is_none());
597    }
598
599    #[tokio::test]
600    async fn find_user_by_id_existing() {
601        let store = InMemoryStore::new();
602        let created = store
603            .create_user(new_user("alice@example.com", "alice"))
604            .await
605            .unwrap();
606
607        let found = store
608            .find_user_by_id(created.id)
609            .await
610            .unwrap()
611            .expect("user should exist");
612
613        assert_eq!(found.email, "alice@example.com");
614        assert_eq!(found.username, "alice");
615    }
616
617    #[tokio::test]
618    async fn find_user_by_id_missing_returns_none() {
619        let store = InMemoryStore::new();
620        let found = store.find_user_by_id(Uuid::now_v7()).await.unwrap();
621        assert!(found.is_none());
622    }
623
624    #[tokio::test]
625    async fn count_users_empty_store() {
626        let store = InMemoryStore::new();
627        assert_eq!(store.count_users().await.unwrap(), 0);
628    }
629
630    #[tokio::test]
631    async fn count_users_with_users() {
632        let store = InMemoryStore::new();
633        store
634            .create_user(new_user("alice@example.com", "alice"))
635            .await
636            .unwrap();
637        store
638            .create_user(new_user("bob@example.com", "bob"))
639            .await
640            .unwrap();
641        assert_eq!(store.count_users().await.unwrap(), 2);
642    }
643
644    #[tokio::test]
645    async fn list_users_paginated() {
646        let store = InMemoryStore::new();
647        for i in 0..5 {
648            store
649                .create_user(new_user(
650                    &format!("user{i}@example.com"),
651                    &format!("user{i}"),
652                ))
653                .await
654                .unwrap();
655        }
656
657        let page = store.list_users(1, 2).await.unwrap();
658        assert_eq!(page.items.len(), 2);
659        assert_eq!(page.total, 5);
660        assert_eq!(page.page, 1);
661        assert_eq!(page.per_page, 2);
662
663        let page2 = store.list_users(3, 2).await.unwrap();
664        assert_eq!(page2.items.len(), 1);
665    }
666
667    #[tokio::test]
668    async fn list_users_empty_store() {
669        let store = InMemoryStore::new();
670        let page = store.list_users(1, 20).await.unwrap();
671        assert!(page.items.is_empty());
672        assert_eq!(page.total, 0);
673    }
674
675    #[tokio::test]
676    async fn delete_user_existing() {
677        let store = InMemoryStore::new();
678        let user = store
679            .create_user(new_user("alice@example.com", "alice"))
680            .await
681            .unwrap();
682
683        store.delete_user(user.id).await.unwrap();
684        assert_eq!(store.count_users().await.unwrap(), 0);
685    }
686
687    #[tokio::test]
688    async fn delete_user_not_found() {
689        let store = InMemoryStore::new();
690        let err = store.delete_user(Uuid::now_v7()).await.unwrap_err();
691        assert!(matches!(err, StoreError::UserNotFound(_)));
692    }
693
694    #[tokio::test]
695    async fn update_user_role_promote() {
696        let store = InMemoryStore::new();
697        // Create two users so the first gets auto-admin
698        let _admin = store
699            .create_user(new_user("admin@example.com", "admin"))
700            .await
701            .unwrap();
702        let member = store
703            .create_user(new_user("member@example.com", "member"))
704            .await
705            .unwrap();
706        assert!(!member.is_admin);
707
708        let promoted = store.update_user_role(member.id, true).await.unwrap();
709        assert!(promoted.is_admin);
710    }
711
712    #[tokio::test]
713    async fn update_user_role_demote() {
714        let store = InMemoryStore::new();
715        let admin = store
716            .create_user(new_user("admin@example.com", "admin"))
717            .await
718            .unwrap();
719        assert!(admin.is_admin);
720
721        let demoted = store.update_user_role(admin.id, false).await.unwrap();
722        assert!(!demoted.is_admin);
723    }
724
725    #[tokio::test]
726    async fn update_user_role_not_found() {
727        let store = InMemoryStore::new();
728        let err = store
729            .update_user_role(Uuid::now_v7(), true)
730            .await
731            .unwrap_err();
732        assert!(matches!(err, StoreError::UserNotFound(_)));
733    }
734
735    #[tokio::test]
736    async fn set_user_groups_replaces_sorts_and_dedups() {
737        let store = InMemoryStore::new();
738        let user = store
739            .create_user(new_user("alice@example.com", "alice"))
740            .await
741            .unwrap();
742
743        let groups = store
744            .set_user_groups(
745                user.id,
746                vec!["sre".to_string(), "finance".to_string(), "sre".to_string()],
747            )
748            .await
749            .unwrap();
750        assert_eq!(groups, vec!["finance".to_string(), "sre".to_string()]);
751        assert_eq!(store.list_user_groups(user.id).await.unwrap(), groups);
752
753        let replaced = store
754            .set_user_groups(user.id, vec!["legal".to_string()])
755            .await
756            .unwrap();
757        assert_eq!(replaced, vec!["legal".to_string()]);
758        assert_eq!(
759            store.list_user_groups(user.id).await.unwrap(),
760            vec!["legal".to_string()]
761        );
762
763        let cleared = store.set_user_groups(user.id, Vec::new()).await.unwrap();
764        assert!(cleared.is_empty());
765        assert!(store.list_user_groups(user.id).await.unwrap().is_empty());
766    }
767
768    #[tokio::test]
769    async fn set_user_groups_unknown_user_is_not_found() {
770        let store = InMemoryStore::new();
771        let err = store
772            .set_user_groups(Uuid::now_v7(), vec!["sre".to_string()])
773            .await
774            .unwrap_err();
775        assert!(matches!(err, StoreError::UserNotFound(_)));
776    }
777
778    #[tokio::test]
779    async fn list_user_groups_unknown_user_is_empty() {
780        let store = InMemoryStore::new();
781        assert!(
782            store
783                .list_user_groups(Uuid::now_v7())
784                .await
785                .unwrap()
786                .is_empty()
787        );
788    }
789
790    #[tokio::test]
791    async fn delete_user_removes_group_membership() {
792        let store = InMemoryStore::new();
793        let user = store
794            .create_user(new_user("alice@example.com", "alice"))
795            .await
796            .unwrap();
797        store
798            .set_user_groups(user.id, vec!["sre".to_string()])
799            .await
800            .unwrap();
801
802        store.delete_user(user.id).await.unwrap();
803
804        assert!(store.list_user_groups(user.id).await.unwrap().is_empty());
805    }
806
807    fn new_key(user_id: Uuid, prefix: &str, scopes: Vec<ApiKeyScope>) -> NewApiKey {
808        NewApiKey {
809            user_id,
810            name: format!("key-{prefix}"),
811            key_hash: "argon2hash".to_string(),
812            key_prefix: prefix.to_string(),
813            scopes,
814            expires_at: None,
815            rate_limit_override: None,
816        }
817    }
818
819    #[tokio::test]
820    async fn update_user_role_demote_strips_non_member_scopes_from_api_keys() {
821        let store = InMemoryStore::new();
822        let admin = store
823            .create_user(new_user("admin@example.com", "admin"))
824            .await
825            .unwrap();
826        let key = store
827            .create_api_key(new_key(
828                admin.id,
829                "irfl_aaaa",
830                vec![
831                    ApiKeyScope::RunsRead,
832                    ApiKeyScope::SignalsSend,
833                    ApiKeyScope::Admin,
834                ],
835            ))
836            .await
837            .unwrap();
838
839        store.update_user_role(admin.id, false).await.unwrap();
840
841        let found = store.find_api_key_by_id(key.id).await.unwrap().unwrap();
842        assert_eq!(found.scopes, vec![ApiKeyScope::RunsRead]);
843        assert!(found.is_active);
844        assert!(ApiKeyScope::all_allowed_for_member(&found.scopes));
845    }
846
847    #[tokio::test]
848    async fn update_user_role_demote_deactivates_api_keys_left_without_scope() {
849        let store = InMemoryStore::new();
850        let admin = store
851            .create_user(new_user("admin@example.com", "admin"))
852            .await
853            .unwrap();
854        let key = store
855            .create_api_key(new_key(admin.id, "irfl_bbbb", vec![ApiKeyScope::Admin]))
856            .await
857            .unwrap();
858
859        store.update_user_role(admin.id, false).await.unwrap();
860
861        let found = store.find_api_key_by_id(key.id).await.unwrap().unwrap();
862        assert!(found.scopes.is_empty());
863        assert!(!found.is_active);
864        assert!(
865            store
866                .find_api_key_by_prefix("irfl_bbbb")
867                .await
868                .unwrap()
869                .is_none()
870        );
871    }
872
873    #[tokio::test]
874    async fn update_user_role_demote_leaves_other_users_keys_untouched() {
875        let store = InMemoryStore::new();
876        let admin = store
877            .create_user(new_user("admin@example.com", "admin"))
878            .await
879            .unwrap();
880        let other = store
881            .create_user(NewUser {
882                is_admin: Some(true),
883                ..new_user("other@example.com", "other")
884            })
885            .await
886            .unwrap();
887        let other_key = store
888            .create_api_key(new_key(other.id, "irfl_cccc", vec![ApiKeyScope::Admin]))
889            .await
890            .unwrap();
891
892        store.update_user_role(admin.id, false).await.unwrap();
893
894        let found = store
895            .find_api_key_by_id(other_key.id)
896            .await
897            .unwrap()
898            .unwrap();
899        assert_eq!(found.scopes, vec![ApiKeyScope::Admin]);
900        assert!(found.is_active);
901        assert_eq!(found.updated_at, other_key.updated_at);
902    }
903
904    #[tokio::test]
905    async fn update_user_role_promote_keeps_api_keys_unchanged() {
906        let store = InMemoryStore::new();
907        let _admin = store
908            .create_user(new_user("admin@example.com", "admin"))
909            .await
910            .unwrap();
911        let member = store
912            .create_user(new_user("member@example.com", "member"))
913            .await
914            .unwrap();
915        let key = store
916            .create_api_key(new_key(member.id, "irfl_dddd", vec![ApiKeyScope::RunsRead]))
917            .await
918            .unwrap();
919
920        store.update_user_role(member.id, true).await.unwrap();
921
922        let found = store.find_api_key_by_id(key.id).await.unwrap().unwrap();
923        assert_eq!(found.scopes, vec![ApiKeyScope::RunsRead]);
924        assert!(found.is_active);
925        assert_eq!(found.updated_at, key.updated_at);
926    }
927
928    #[tokio::test]
929    async fn delete_user_drops_api_keys() {
930        let store = InMemoryStore::new();
931        let user = store
932            .create_user(new_user("alice@example.com", "alice"))
933            .await
934            .unwrap();
935        let key = store
936            .create_api_key(new_key(user.id, "irfl_eeee", vec![ApiKeyScope::RunsRead]))
937            .await
938            .unwrap();
939
940        store.delete_user(user.id).await.unwrap();
941
942        assert!(store.find_api_key_by_id(key.id).await.unwrap().is_none());
943        assert!(
944            store
945                .list_api_keys_by_user(user.id)
946                .await
947                .unwrap()
948                .is_empty()
949        );
950    }
951}