steid

@jamesgill /

1//! In-memory repository implementations.
2//!
3//! These exist so use cases can be tested without a database. Attempt #2 reached 60
4//! tests this way and they stayed fast enough to run on every change.
5
6use std::{
7 collections::HashMap,
8 sync::{Arc, Mutex},
9};
10
11use crate::domain::{
12 Email, Membership, OrgId, OrgName, Organization, User, UserId,
13 repository::{MembershipRepository, OrgRepository, RepositoryResult, UserRepository},
14};
15
16/// Shared, cloneable storage. Cloning shares the same underlying map, so a repository
17/// handed to two use cases sees one set of data.
18#[derive(Debug, Default, Clone)]
19pub struct InMemoryUserRepo {
20 users: Arc<Mutex<HashMap<String, User>>>,
21}
22
23impl InMemoryUserRepo {
24 pub fn new() -> Self {
25 Self::default()
26 }
27}
28
29impl UserRepository for InMemoryUserRepo {
30 async fn find_by_id(&self, id: &UserId) -> RepositoryResult<Option<User>> {
31 let users = self.users.lock().expect("lock poisoned");
32 Ok(users.get(id.as_str()).cloned())
33 }
34
35 async fn find_by_email(&self, email: &Email) -> RepositoryResult<Option<User>> {
36 let users = self.users.lock().expect("lock poisoned");
37 Ok(users.values().find(|user| &user.email == email).cloned())
38 }
39
40 async fn save(&self, user: &User) -> RepositoryResult<()> {
41 let mut users = self.users.lock().expect("lock poisoned");
42 users.insert(user.id.as_str().to_owned(), user.clone());
43 Ok(())
44 }
45
46 async fn any_exist(&self) -> RepositoryResult<bool> {
47 let users = self.users.lock().expect("lock poisoned");
48 Ok(!users.is_empty())
49 }
50}
51
52#[derive(Debug, Default, Clone)]
53pub struct InMemoryOrgRepo {
54 orgs: Arc<Mutex<HashMap<String, Organization>>>,
55}
56
57impl InMemoryOrgRepo {
58 pub fn new() -> Self {
59 Self::default()
60 }
61}
62
63impl OrgRepository for InMemoryOrgRepo {
64 async fn find_by_id(&self, id: &OrgId) -> RepositoryResult<Option<Organization>> {
65 let orgs = self.orgs.lock().expect("lock poisoned");
66 Ok(orgs.get(id.as_str()).cloned())
67 }
68
69 async fn find_by_name(&self, name: &OrgName) -> RepositoryResult<Option<Organization>> {
70 let orgs = self.orgs.lock().expect("lock poisoned");
71 Ok(orgs.values().find(|org| &org.name == name).cloned())
72 }
73
74 async fn save(&self, org: &Organization) -> RepositoryResult<()> {
75 let mut orgs = self.orgs.lock().expect("lock poisoned");
76 orgs.insert(org.id.as_str().to_owned(), org.clone());
77 Ok(())
78 }
79}
80
81#[derive(Debug, Default, Clone)]
82pub struct InMemoryMembershipRepo {
83 memberships: Arc<Mutex<HashMap<String, Membership>>>,
84}
85
86impl InMemoryMembershipRepo {
87 pub fn new() -> Self {
88 Self::default()
89 }
90}
91
92impl MembershipRepository for InMemoryMembershipRepo {
93 async fn find(&self, org_id: &OrgId, user_id: &UserId) -> RepositoryResult<Option<Membership>> {
94 let memberships = self.memberships.lock().expect("lock poisoned");
95 Ok(memberships
96 .values()
97 .find(|m| &m.org_id == org_id && &m.user_id == user_id)
98 .cloned())
99 }
100
101 async fn list_for_user(&self, user_id: &UserId) -> RepositoryResult<Vec<Membership>> {
102 let memberships = self.memberships.lock().expect("lock poisoned");
103 Ok(memberships
104 .values()
105 .filter(|m| &m.user_id == user_id)
106 .cloned()
107 .collect())
108 }
109
110 async fn save(&self, membership: &Membership) -> RepositoryResult<()> {
111 let mut memberships = self.memberships.lock().expect("lock poisoned");
112 memberships.insert(membership.id.as_str().to_owned(), membership.clone());
113 Ok(())
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::domain::{MembershipId, PasswordHash, Role};
121
122 fn user(email: &str, org_id: &OrgId) -> User {
123 User::new(
124 UserId::generate(),
125 Email::new(email).expect("valid email"),
126 PasswordHash::from_trusted("$argon2id$test"),
127 org_id.clone(),
128 )
129 }
130
131 #[tokio::test]
132 async fn users_are_found_by_id_and_email() {
133 let repo = InMemoryUserRepo::new();
134 let org_id = OrgId::generate();
135 let user = user("dev@example.com", &org_id);
136 repo.save(&user).await.expect("save");
137
138 let by_id = repo.find_by_id(&user.id).await.expect("lookup");
139 let by_email = repo.find_by_email(&user.email).await.expect("lookup");
140
141 assert_eq!(by_id.as_ref(), Some(&user));
142 assert_eq!(by_email.as_ref(), Some(&user));
143 }
144
145 #[tokio::test]
146 async fn missing_users_are_none_not_an_error() {
147 let repo = InMemoryUserRepo::new();
148
149 let found = repo.find_by_id(&UserId::generate()).await.expect("lookup");
150
151 assert_eq!(found, None);
152 }
153
154 #[tokio::test]
155 async fn saving_the_same_id_replaces_rather_than_duplicates() {
156 let repo = InMemoryUserRepo::new();
157 let org_id = OrgId::generate();
158 let mut user = user("dev@example.com", &org_id);
159 repo.save(&user).await.expect("save");
160
161 user.email = Email::new("changed@example.com").expect("valid email");
162 repo.save(&user).await.expect("save");
163
164 assert_eq!(
165 repo.find_by_email(&Email::new("dev@example.com").unwrap())
166 .await
167 .expect("lookup"),
168 None
169 );
170 assert!(repo.any_exist().await.expect("any_exist"));
171 }
172
173 #[tokio::test]
174 async fn any_exist_reports_emptiness() {
175 let repo = InMemoryUserRepo::new();
176 assert!(!repo.any_exist().await.expect("any_exist"));
177
178 repo.save(&user("dev@example.com", &OrgId::generate()))
179 .await
180 .expect("save");
181
182 assert!(repo.any_exist().await.expect("any_exist"));
183 }
184
185 #[tokio::test]
186 async fn clones_share_storage() {
187 let repo = InMemoryUserRepo::new();
188 let clone = repo.clone();
189
190 clone
191 .save(&user("dev@example.com", &OrgId::generate()))
192 .await
193 .expect("save");
194
195 assert!(repo.any_exist().await.expect("any_exist"));
196 }
197
198 #[tokio::test]
199 async fn orgs_are_found_by_id_and_name() {
200 let repo = InMemoryOrgRepo::new();
201 let org = Organization::new(OrgId::generate(), "steid", None).expect("valid org");
202 repo.save(&org).await.expect("save");
203
204 assert_eq!(
205 repo.find_by_id(&org.id).await.expect("lookup").as_ref(),
206 Some(&org)
207 );
208 assert_eq!(
209 repo.find_by_name(&org.name).await.expect("lookup").as_ref(),
210 Some(&org)
211 );
212 }
213
214 #[tokio::test]
215 async fn org_lookup_by_name_respects_normalisation() {
216 let repo = InMemoryOrgRepo::new();
217 let org = Organization::new(OrgId::generate(), "Steid", None).expect("valid org");
218 repo.save(&org).await.expect("save");
219
220 let found = repo
221 .find_by_name(&OrgName::new("STEID").expect("valid"))
222 .await
223 .expect("lookup");
224
225 assert_eq!(found.as_ref(), Some(&org));
226 }
227
228 #[tokio::test]
229 async fn memberships_are_found_by_org_and_user() {
230 let repo = InMemoryMembershipRepo::new();
231 let org_id = OrgId::generate();
232 let user_id = UserId::generate();
233 let membership = Membership::new(
234 MembershipId::generate(),
235 org_id.clone(),
236 user_id.clone(),
237 Role::Owner,
238 );
239 repo.save(&membership).await.expect("save");
240
241 let found = repo.find(&org_id, &user_id).await.expect("lookup");
242
243 assert_eq!(found.as_ref(), Some(&membership));
244 }
245
246 #[tokio::test]
247 async fn membership_lookup_does_not_match_a_different_user() {
248 let repo = InMemoryMembershipRepo::new();
249 let org_id = OrgId::generate();
250 repo.save(&Membership::new(
251 MembershipId::generate(),
252 org_id.clone(),
253 UserId::generate(),
254 Role::Owner,
255 ))
256 .await
257 .expect("save");
258
259 let found = repo
260 .find(&org_id, &UserId::generate())
261 .await
262 .expect("lookup");
263
264 assert_eq!(found, None);
265 }
266
267 #[tokio::test]
268 async fn list_for_user_returns_only_that_users_memberships() {
269 let repo = InMemoryMembershipRepo::new();
270 let user_id = UserId::generate();
271 for _ in 0..2 {
272 repo.save(&Membership::new(
273 MembershipId::generate(),
274 OrgId::generate(),
275 user_id.clone(),
276 Role::Member,
277 ))
278 .await
279 .expect("save");
280 }
281 repo.save(&Membership::new(
282 MembershipId::generate(),
283 OrgId::generate(),
284 UserId::generate(),
285 Role::Owner,
286 ))
287 .await
288 .expect("save");
289
290 let found = repo.list_for_user(&user_id).await.expect("lookup");
291
292 assert_eq!(found.len(), 2);
293 assert!(found.iter().all(|m| m.user_id == user_id));
294 }
295}