steid

@jamesgill /

steid/src/application/access_tests.rs
Rust12.0 KBCode·Blame·History·Raw
1use std::time::SystemTime;
2
3use super::*;
4use crate::{
5 domain::{
6 Email, Membership, MembershipId, OrgId, PasswordHash, RepoId, User, UserId, Visibility,
7 repository::{MembershipRepository, OrgRepository, RepoRepository, UserRepository},
8 },
9 infrastructure::repository::{
10 InMemoryMembershipRepo, InMemoryOrgRepo, InMemoryRepoRepo, InMemoryUserRepo,
11 },
12};
13
14fn at() -> SystemTime {
15 SystemTime::UNIX_EPOCH
16}
17
18/// One organisation with an owner, a plain member and a signed-in stranger, plus a
19/// public and a private repository. Every predicate here is a question about one of
20/// those three actors, so they are named rather than generated per test.
21struct Fixture {
22 users: InMemoryUserRepo,
23 orgs: InMemoryOrgRepo,
24 memberships: InMemoryMembershipRepo,
25 repos: InMemoryRepoRepo,
26 org: Organization,
27 owner: Actor,
28 member: Actor,
29 stranger: Actor,
30 handle: OrgName,
31}
32
33async fn fixture() -> Fixture {
34 let users = InMemoryUserRepo::new();
35 let orgs = InMemoryOrgRepo::new();
36 let memberships = InMemoryMembershipRepo::new();
37 let repos = InMemoryRepoRepo::new();
38
39 let org = Organization::new(OrgId::generate(), "acme", None).expect("valid org");
40 orgs.save(&org).await.expect("save org");
41
42 let owner = UserId::generate();
43 let member = UserId::generate();
44
45 for (user, role) in [(&owner, Role::Owner), (&member, Role::Member)] {
46 memberships
47 .save(&Membership::new(
48 MembershipId::generate(),
49 org.id.clone(),
50 user.clone(),
51 role,
52 ))
53 .await
54 .expect("save membership");
55 }
56
57 for (name, visibility) in [
58 ("public", Visibility::Public),
59 ("secret", Visibility::Private),
60 ] {
61 repos
62 .save(
63 &Repository::new(
64 RepoId::generate(),
65 org.id.clone(),
66 name,
67 None,
68 visibility,
69 at(),
70 )
71 .expect("valid repository"),
72 )
73 .await
74 .expect("save repository");
75 }
76
77 Fixture {
78 users,
79 orgs,
80 memberships,
81 repos,
82 handle: org.name.clone(),
83 org,
84 owner: Actor::User(owner),
85 member: Actor::User(member),
86 stranger: Actor::User(UserId::generate()),
87 }
88}
89
90impl Fixture {
91 /// Records `user` as the instance's sole user, which is what "instance owner"
92 /// means while Steid has exactly one.
93 async fn register(&self, user: &Actor) {
94 let id = user.user_id().expect("a signed-in actor").clone();
95
96 self.users
97 .save(&User::new(
98 id,
99 Email::new("dev@example.com").expect("valid email"),
100 PasswordHash::from_trusted("$argon2id$test"),
101 self.org.id.clone(),
102 ))
103 .await
104 .expect("save user");
105 }
106
107 async fn readable(&self, actor: &Actor, name: &str) -> Option<(Repository, bool)> {
108 readable_repo(
109 actor,
110 &self.handle,
111 &RepoName::new(name).expect("valid name"),
112 &Stores::new(&self.orgs, &self.memberships, &self.repos),
113 )
114 .await
115 .expect("should resolve")
116 }
117
118 async fn visible(&self, actor: &Actor, name: &str) -> Option<(Organization, Repository)> {
119 visible_repo(
120 actor,
121 &self.handle,
122 &RepoName::new(name).expect("valid name"),
123 &Stores::new(&self.orgs, &self.memberships, &self.repos),
124 )
125 .await
126 .expect("should resolve")
127 }
128
129 async fn writable(&self, actor: &Actor, name: &str) -> Result<Repository> {
130 writable_repo(
131 actor,
132 &self.handle,
133 &RepoName::new(name).expect("valid name"),
134 &Stores::new(&self.orgs, &self.memberships, &self.repos),
135 )
136 .await
137 }
138
139 async fn changeable(&self, actor: &Actor, name: &str) -> Result<Repository> {
140 changeable_repo(
141 actor,
142 &self.handle,
143 &RepoName::new(name).expect("valid name"),
144 &Stores::new(&self.orgs, &self.memberships, &self.repos),
145 )
146 .await
147 }
148}
149
150fn is_not_found<T>(outcome: &Result<T>) -> bool {
151 matches!(
152 outcome,
153 Err(Error::Domain(DomainError::NotFound {
154 entity: "repository"
155 }))
156 )
157}
158
159fn is_forbidden<T>(outcome: &Result<T>) -> bool {
160 matches!(outcome, Err(Error::Domain(DomainError::Forbidden)))
161}
162
163// --- The organisation predicates ------------------------------------------------------
164
165#[tokio::test]
166async fn owner_is_owner_and_member_is_only_a_member() {
167 let f = fixture().await;
168
169 assert!(
170 is_org_owner(&f.org, &f.owner, &f.memberships)
171 .await
172 .expect("should read")
173 );
174 assert!(
175 !is_org_owner(&f.org, &f.member, &f.memberships)
176 .await
177 .expect("should read"),
178 "membership is not ownership"
179 );
180 assert!(
181 is_org_member(&f.org, &f.member, &f.memberships)
182 .await
183 .expect("should read")
184 );
185 assert!(
186 is_org_member(&f.org, &f.owner, &f.memberships)
187 .await
188 .expect("should read"),
189 "an owner is also a member"
190 );
191}
192
193#[tokio::test]
194async fn an_anonymous_actor_is_neither_owner_nor_member() {
195 // The check that "signed in" has not quietly become "allowed", read from the
196 // other end: no membership row can exist for an actor with no user id.
197 let f = fixture().await;
198
199 assert!(
200 !is_org_owner(&f.org, &Actor::Anonymous, &f.memberships)
201 .await
202 .expect("should read")
203 );
204 assert!(
205 !is_org_member(&f.org, &Actor::Anonymous, &f.memberships)
206 .await
207 .expect("should read")
208 );
209 assert!(
210 !is_org_member(&f.org, &f.stranger, &f.memberships)
211 .await
212 .expect("should read")
213 );
214}
215
216#[tokio::test]
217async fn the_sole_user_owns_the_instance_and_nobody_else_does() {
218 let f = fixture().await;
219 f.register(&f.owner).await;
220
221 assert!(
222 instance_owner(&f.owner, &f.users)
223 .await
224 .expect("should read")
225 );
226 assert!(
227 !instance_owner(&f.stranger, &f.users)
228 .await
229 .expect("should read"),
230 "being a user is not being *the* user"
231 );
232 assert!(
233 !instance_owner(&Actor::Anonymous, &f.users)
234 .await
235 .expect("should read")
236 );
237}
238
239#[tokio::test]
240async fn nobody_owns_an_unclaimed_instance() {
241 let f = fixture().await;
242
243 assert!(
244 !instance_owner(&f.owner, &f.users)
245 .await
246 .expect("should read")
247 );
248}
249
250// --- Reading -------------------------------------------------------------------------
251
252#[tokio::test]
253async fn anyone_reads_a_public_repository_and_only_the_owner_is_flagged_as_one() {
254 let f = fixture().await;
255
256 for (actor, expected_owner) in [
257 (&f.owner, true),
258 (&f.member, false),
259 (&f.stranger, false),
260 (&Actor::Anonymous, false),
261 ] {
262 let (repo, is_owner) = f
263 .readable(actor, "public")
264 .await
265 .expect("a public repository is readable");
266
267 assert_eq!(repo.name.as_str(), "public");
268 assert_eq!(is_owner, expected_owner);
269 }
270}
271
272#[tokio::test]
273async fn a_private_repository_is_absent_rather_than_locked() {
274 // `Ok(None)`, not `Forbidden`: a refusal that distinguishes the two confirms the
275 // repository exists, which is the thing being protected.
276 let f = fixture().await;
277
278 assert!(f.readable(&f.owner, "secret").await.is_some());
279 assert!(
280 f.readable(&f.member, "secret").await.is_some(),
281 "membership is what gates seeing a private repository"
282 );
283 assert!(f.readable(&f.stranger, "secret").await.is_none());
284 assert!(f.readable(&Actor::Anonymous, "secret").await.is_none());
285}
286
287#[tokio::test]
288async fn an_unknown_organisation_or_repository_reads_as_absent() {
289 let f = fixture().await;
290
291 assert!(f.readable(&f.owner, "no-such-repo").await.is_none());
292 assert!(
293 readable_repo(
294 &f.owner,
295 &OrgName::new("nobody").expect("valid handle"),
296 &RepoName::new("public").expect("valid name"),
297 &Stores::new(&f.orgs, &f.memberships, &f.repos),
298 )
299 .await
300 .expect("should resolve")
301 .is_none()
302 );
303}
304
305// --- Writing and changing -------------------------------------------------------------
306
307#[tokio::test]
308async fn writing_and_changing_are_owner_only() {
309 let f = fixture().await;
310
311 assert!(f.writable(&f.owner, "public").await.is_ok());
312 assert!(f.changeable(&f.owner, "public").await.is_ok());
313
314 for actor in [&f.member, &f.stranger, &Actor::Anonymous] {
315 assert!(
316 is_forbidden(&f.writable(actor, "public").await),
317 "a public repository the actor can see refuses with Forbidden"
318 );
319 assert!(is_forbidden(&f.changeable(actor, "public").await));
320 }
321}
322
323#[tokio::test]
324async fn a_write_to_an_invisible_repository_is_not_found_not_forbidden() {
325 // The pair that matters: `Forbidden` on something they can see, `NotFound` on
326 // something they cannot, so a private repository never leaks through the refusal.
327 let f = fixture().await;
328
329 assert!(is_not_found(&f.writable(&f.stranger, "secret").await));
330 assert!(is_not_found(&f.changeable(&f.stranger, "secret").await));
331 assert!(is_not_found(&f.writable(&Actor::Anonymous, "secret").await));
332 assert!(is_not_found(
333 &f.changeable(&Actor::Anonymous, "secret").await
334 ));
335
336 assert!(
337 is_forbidden(&f.writable(&f.member, "secret").await),
338 "a member can see it, so the refusal explains itself"
339 );
340 assert!(is_forbidden(&f.changeable(&f.member, "secret").await));
341}
342
343#[tokio::test]
344async fn writable_and_changeable_agree_on_every_actor() {
345 // They enforce the same rule from opposite ends — one layered on `readable_repo`,
346 // one resolving directly. This is the test that notices if they drift apart.
347 let f = fixture().await;
348
349 for actor in [&f.owner, &f.member, &f.stranger, &Actor::Anonymous] {
350 for name in ["public", "secret", "no-such-repo"] {
351 let writable = f.writable(actor, name).await;
352 let changeable = f.changeable(actor, name).await;
353
354 assert_eq!(
355 writable.is_ok(),
356 changeable.is_ok(),
357 "disagreed on {name} for {actor:?}"
358 );
359 assert_eq!(
360 is_forbidden(&writable),
361 is_forbidden(&changeable),
362 "disagreed on the refusal for {name}"
363 );
364 }
365 }
366}
367
368#[tokio::test]
369async fn a_write_to_an_unknown_repository_is_not_found() {
370 let f = fixture().await;
371
372 assert!(is_not_found(&f.writable(&f.owner, "no-such-repo").await));
373 assert!(is_not_found(&f.changeable(&f.owner, "no-such-repo").await));
374}
375
376#[tokio::test]
377async fn every_resolver_agrees_with_the_one_visibility_rule() {
378 // `visible_repo` is the body the other three are built on, so the thing worth
379 // pinning is that none of them can be seen to disagree with it: whatever is
380 // invisible is unreadable, unwritable and unchangeable, and the refusal for an
381 // invisible repository is always `NotFound` rather than `Forbidden`.
382 let f = fixture().await;
383
384 for actor in [&f.owner, &f.member, &f.stranger, &Actor::Anonymous] {
385 for name in ["public", "secret", "no-such-repo"] {
386 let visible = f.visible(actor, name).await.is_some();
387
388 assert_eq!(
389 visible,
390 f.readable(actor, name).await.is_some(),
391 "readable_repo disagreed on {name} for {actor:?}"
392 );
393
394 if !visible {
395 assert!(
396 is_not_found(&f.writable(actor, name).await),
397 "an invisible {name} must be absent, not forbidden, for {actor:?}"
398 );
399 assert!(is_not_found(&f.changeable(actor, name).await));
400 }
401 }
402 }
403}