crates/cli/git-ents/tests/members.rs
members.rshistorycomment on this file
| 1 | //! Integration coverage for `git ents members` against a real local |
| 2 | //! composition root (`roots.local`) — the bootstrap enrollment, then the |
| 3 | //! full add/revoke/unrevoke/check lifecycle atop it. |
| 4 | //! |
| 5 | //! rstest table-driven: the spec enumerates member-state transitions |
| 6 | //! (`model.member-revocation`) as a small closed set of cases, exactly the |
| 7 | //! shape the engineering conventions call out for table tests rather than |
| 8 | //! property tests. |
| 9 | #![allow( |
| 10 | clippy::expect_used, |
| 11 | clippy::indexing_slicing, |
| 12 | reason = "integration test" |
| 13 | )] |
| 14 | |
| 15 | mod common; |
| 16 | |
| 17 | use ents_model::MemberState; |
| 18 | use git_ents::commands::members; |
| 19 | use git_ents::root::LocalRoot; |
| 20 | use rstest::rstest; |
| 21 | |
| 22 | /// The bootstrap window (`gate.bootstrap`) admits the very first member |
| 23 | /// with no prior enrollment — [`git_ents::lib`]'s own doctest exercises |
| 24 | /// this too; this test additionally confirms `git ents members list` then |
| 25 | /// reads it back through the real composition root. |
| 26 | // @relation(roots.local, model.member-identity, scope=function, role=Verifies) |
| 27 | #[test] |
| 28 | fn bootstrap_enrolls_the_first_member() { |
| 29 | let fixture = common::Fixture::new(1); |
| 30 | let root = LocalRoot::open(fixture.path()).expect("opens"); |
| 31 | |
| 32 | members::add(&root, "jdc", None, Some(fixture.key_path.clone())).expect("bootstrap admits it"); |
| 33 | |
| 34 | let listed = members::list(&root.refs, &root.objects).expect("lists"); |
| 35 | assert_eq!(listed.len(), 1); |
| 36 | assert_eq!(listed[0].0, "jdc"); |
| 37 | assert_eq!(listed[0].1.state, MemberState::Active); |
| 38 | } |
| 39 | |
| 40 | /// Enroll an admin, then use that same key to add a second member — the |
| 41 | /// ordinary (non-bootstrap) admin-registered path. |
| 42 | // @relation(roots.local, model.member-identity, scope=function, role=Verifies) |
| 43 | #[test] |
| 44 | fn admin_enrolls_a_second_member() { |
| 45 | let fixture = common::Fixture::new(2); |
| 46 | let root = LocalRoot::open(fixture.path()).expect("opens"); |
| 47 | members::add(&root, "admin", None, Some(fixture.key_path.clone())).expect("bootstrap"); |
| 48 | |
| 49 | members::add( |
| 50 | &root, |
| 51 | "bob", |
| 52 | Some("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBogus bob".to_owned()), |
| 53 | Some(fixture.key_path.clone()), |
| 54 | ) |
| 55 | .expect("admin-registered add"); |
| 56 | |
| 57 | let listed = members::list(&root.refs, &root.objects).expect("lists"); |
| 58 | assert_eq!(listed.len(), 2); |
| 59 | assert!(listed.iter().any(|(name, _)| name == "bob")); |
| 60 | } |
| 61 | |
| 62 | #[rstest] |
| 63 | #[case::revoke_then_check(true)] |
| 64 | #[case::unrevoke_then_check(false)] |
| 65 | // @relation(model.member-revocation, scope=function, role=Verifies) |
| 66 | fn revoke_and_unrevoke_round_trip(#[case] end_revoked: bool) { |
| 67 | let fixture = common::Fixture::new(3); |
| 68 | let root = LocalRoot::open(fixture.path()).expect("opens"); |
| 69 | members::add(&root, "jdc", None, Some(fixture.key_path.clone())).expect("bootstrap"); |
| 70 | |
| 71 | members::set_revoked(&root, "jdc", true, Some(fixture.key_path.clone())).expect("revoke"); |
| 72 | if !end_revoked { |
| 73 | members::set_revoked(&root, "jdc", false, Some(fixture.key_path.clone())) |
| 74 | .expect("unrevoke"); |
| 75 | } |
| 76 | |
| 77 | let (_, state) = members::check(&root, Some(fixture.key_path.clone())) |
| 78 | .expect("reads") |
| 79 | .expect("still a member record (revocation records state, never deletes)"); |
| 80 | let expected = if end_revoked { |
| 81 | MemberState::Revoked |
| 82 | } else { |
| 83 | MemberState::Active |
| 84 | }; |
| 85 | assert_eq!(state, expected); |
| 86 | } |
| 87 | |
| 88 | /// `git ents members remove` deletes the ref entirely, distinct from |
| 89 | /// revocation (`model.member-revocation`'s "never deletes" contrast). |
| 90 | // @relation(model.member-revocation, scope=function, role=Verifies) |
| 91 | #[test] |
| 92 | fn remove_deletes_the_member_ref_entirely() { |
| 93 | let fixture = common::Fixture::new(4); |
| 94 | let root = LocalRoot::open(fixture.path()).expect("opens"); |
| 95 | members::add(&root, "jdc", None, Some(fixture.key_path.clone())).expect("bootstrap"); |
| 96 | |
| 97 | members::remove(&root, "jdc", Some(fixture.key_path.clone())).expect("removes"); |
| 98 | |
| 99 | let listed = members::list(&root.refs, &root.objects).expect("lists"); |
| 100 | assert!(listed.is_empty()); |
| 101 | } |