diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-18 21:53:15 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-18 21:56:12 +0000 |
| commit | 1f0298bcfe125bee5d996e163ad8f3e9c17e3a9e (patch) | |
| tree | 6ce75db88d1d3c3ee9b46f9a3f657e35b2446c3a /grasp-audit/src | |
| parent | cdd129b715753c7b4042a519a7c3fb92be94da04 (diff) | |
extract OwnerRepoState fixture to make dependency chain explicit
OwnerStateDataPushed was secretly building and sending the state event
internally, with no corresponding fixture in the chain. Add OwnerRepoState
as the explicit 'state event sent, sitting in purgatory' step so the
dependency chain reads: ValidRepoSent -> OwnerRepoState -> OwnerStateDataPushed -> ValidRepoServed.
OwnerStateDataPushed now reads the state event from the OwnerRepoState cache
rather than rebuilding it, and only owns the git push + purgatory release.
Diffstat (limited to 'grasp-audit/src')
| -rw-r--r-- | grasp-audit/src/fixtures.rs | 110 |
1 files changed, 67 insertions, 43 deletions
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index 9a00aef..fc6e8cb 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs | |||
| @@ -154,6 +154,22 @@ pub enum FixtureKind { | |||
| 154 | /// - Timestamp: 10 seconds in the past | 154 | /// - Timestamp: 10 seconds in the past |
| 155 | RepoState, | 155 | RepoState, |
| 156 | 156 | ||
| 157 | /// Owner's repository state announcement (kind 30618) sent to relay and accepted into purgatory | ||
| 158 | /// | ||
| 159 | /// This is the "sent" stage: the state event has been published to the relay and | ||
| 160 | /// accepted (OK response), but no git data has been pushed yet so it remains in | ||
| 161 | /// purgatory and is not served to clients. | ||
| 162 | /// | ||
| 163 | /// Use this when you need the state event to exist on the relay but do not need | ||
| 164 | /// the full push/serve cycle. For the complete cycle (git pushed + verified served), | ||
| 165 | /// use `OwnerStateDataPushed`. | ||
| 166 | /// | ||
| 167 | /// - Requires ValidRepoSent (uses same repo_id) | ||
| 168 | /// - Signed by owner keys (`client.keys()`) | ||
| 169 | /// - Points to DETERMINISTIC_COMMIT_HASH | ||
| 170 | /// - Timestamp: 10 seconds in the past | ||
| 171 | OwnerRepoStateSent, | ||
| 172 | |||
| 157 | /// PR (Pull Request) event for the SAME repo_id as ValidRepoServed | 173 | /// PR (Pull Request) event for the SAME repo_id as ValidRepoServed |
| 158 | /// - Requires ValidRepoServed (uses same repo_id, needs queryable repo) | 174 | /// - Requires ValidRepoServed (uses same repo_id, needs queryable repo) |
| 159 | /// - Signed by `client.pr_author_keys()` | 175 | /// - Signed by `client.pr_author_keys()` |
| @@ -343,6 +359,8 @@ impl FixtureKind { | |||
| 343 | // Fixtures that depend on ValidRepoServed (need queryable announcement) | 359 | // Fixtures that depend on ValidRepoServed (need queryable announcement) |
| 344 | Self::RepoWithIssue => vec![Self::ValidRepoServed], | 360 | Self::RepoWithIssue => vec![Self::ValidRepoServed], |
| 345 | Self::RepoState => vec![Self::ValidRepoSent], | 361 | Self::RepoState => vec![Self::ValidRepoSent], |
| 362 | // OwnerRepoStateSent depends on ValidRepoSent: state event sent, sitting in purgatory | ||
| 363 | Self::OwnerRepoStateSent => vec![Self::ValidRepoSent], | ||
| 346 | Self::PREvent => vec![Self::ValidRepoServed], | 364 | Self::PREvent => vec![Self::ValidRepoServed], |
| 347 | Self::PREventGenerated => vec![Self::ValidRepoServed], | 365 | Self::PREventGenerated => vec![Self::ValidRepoServed], |
| 348 | Self::PRWrongCommitPushedBeforeEvent => vec![Self::PREventGenerated], | 366 | Self::PRWrongCommitPushedBeforeEvent => vec![Self::PREventGenerated], |
| @@ -354,7 +372,8 @@ impl FixtureKind { | |||
| 354 | Self::PREvent2GitDataPushed => vec![Self::PREvent2Sent], | 372 | Self::PREvent2GitDataPushed => vec![Self::PREvent2Sent], |
| 355 | Self::PREvent2Served => vec![Self::PREvent2GitDataPushed], | 373 | Self::PREvent2Served => vec![Self::PREvent2GitDataPushed], |
| 356 | 374 | ||
| 357 | Self::OwnerStateDataPushed => vec![Self::ValidRepoSent], | 375 | // OwnerStateDataPushed depends on OwnerRepoStateSent (git push + purgatory release) |
| 376 | Self::OwnerStateDataPushed => vec![Self::OwnerRepoStateSent], | ||
| 358 | 377 | ||
| 359 | // Fixtures that depend on RepoWithIssue | 378 | // Fixtures that depend on RepoWithIssue |
| 360 | Self::RepoWithComment => vec![Self::RepoWithIssue], | 379 | Self::RepoWithComment => vec![Self::RepoWithIssue], |
| @@ -399,6 +418,8 @@ impl FixtureKind { | |||
| 399 | Self::HeadSetToDevelopBranch => true, | 418 | Self::HeadSetToDevelopBranch => true, |
| 400 | // ValidRepoServed doesn't send anything itself, just returns cached event | 419 | // ValidRepoServed doesn't send anything itself, just returns cached event |
| 401 | Self::ValidRepoServed => true, | 420 | Self::ValidRepoServed => true, |
| 421 | // OwnerRepoStateSent sends its state event and notes purgatory internally | ||
| 422 | Self::OwnerRepoStateSent => true, | ||
| 402 | // All other fixtures return a single event for the caller to send | 423 | // All other fixtures return a single event for the caller to send |
| 403 | _ => false, | 424 | _ => false, |
| 404 | } | 425 | } |
| @@ -774,6 +795,40 @@ impl<'a> TestContext<'a> { | |||
| 774 | .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e)) | 795 | .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e)) |
| 775 | } | 796 | } |
| 776 | 797 | ||
| 798 | FixtureKind::OwnerRepoStateSent => { | ||
| 799 | use nostr_sdk::prelude::*; | ||
| 800 | |||
| 801 | // ValidRepoSent is ensured by ensure_fixture before this is called | ||
| 802 | let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; | ||
| 803 | let repo_id = self.extract_repo_id(&repo)?; | ||
| 804 | |||
| 805 | let base_time = Timestamp::now().as_secs(); | ||
| 806 | let older_timestamp = Timestamp::from(base_time - 10); | ||
| 807 | |||
| 808 | let state_event = self | ||
| 809 | .client | ||
| 810 | .event_builder(Kind::RepoState, "") | ||
| 811 | .tag(Tag::identifier(&repo_id)) | ||
| 812 | .tag(Tag::custom( | ||
| 813 | TagKind::custom("refs/heads/main"), | ||
| 814 | vec![DETERMINISTIC_COMMIT_HASH.to_string()], | ||
| 815 | )) | ||
| 816 | .tag(Tag::custom( | ||
| 817 | TagKind::custom("HEAD"), | ||
| 818 | vec!["ref: refs/heads/main".to_string()], | ||
| 819 | )) | ||
| 820 | .custom_time(older_timestamp) | ||
| 821 | .build(self.client.keys()) | ||
| 822 | .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e))?; | ||
| 823 | |||
| 824 | // Send to relay - event will be accepted but held in purgatory (no git data yet) | ||
| 825 | self.client | ||
| 826 | .send_event_and_note_purgatory(state_event.clone()) | ||
| 827 | .await?; | ||
| 828 | |||
| 829 | Ok(state_event) | ||
| 830 | } | ||
| 831 | |||
| 777 | FixtureKind::PREvent => { | 832 | FixtureKind::PREvent => { |
| 778 | use nostr_sdk::prelude::*; | 833 | use nostr_sdk::prelude::*; |
| 779 | 834 | ||
| @@ -945,57 +1000,26 @@ impl<'a> TestContext<'a> { | |||
| 945 | .ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement")) | 1000 | .ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement")) |
| 946 | } | 1001 | } |
| 947 | 1002 | ||
| 948 | /// Build OwnerStateDataPushed fixture: full 4-stage fixture for push authorization | 1003 | /// Build OwnerStateDataPushed fixture: git push + purgatory release for owner's state event |
| 949 | /// | 1004 | /// |
| 950 | /// This handles all stages of the fixture: | 1005 | /// `OwnerRepoStateSent` is ensured as a dependency before this is called — the state event |
| 951 | /// 1. **Generated**: Creates RepoState (repo announcement + state event) | 1006 | /// is already on the relay in purgatory. This fixture completes the cycle: |
| 952 | /// 2. **Sent**: Sends events to relay (returns OK, accepted but 'purgatory:...' message) | 1007 | /// 1. **DataPushed**: Clones repo, creates deterministic commit, pushes to relay |
| 953 | /// 3. **Verify Not Served**: Confirms event is not served by relays | 1008 | /// 2. **Verified**: Confirms state event is released from purgatory and served |
| 954 | /// 4. **DataPushed**: Clones repo, creates deterministic commit, pushes to relay | ||
| 955 | /// 5. **Verified**: Confirms event is served by relay | ||
| 956 | /// | 1009 | /// |
| 957 | /// # Returns | 1010 | /// # Returns |
| 958 | /// The state event (kind 30618) after all stages complete successfully | 1011 | /// The state event (kind 30618) after git data is pushed and purgatory is released |
| 959 | async fn build_owner_state_data_pushed(&self) -> Result<Event> { | 1012 | async fn build_owner_state_data_pushed(&self) -> Result<Event> { |
| 960 | use nostr_sdk::prelude::*; | 1013 | use nostr_sdk::prelude::*; |
| 961 | 1014 | ||
| 962 | // ============================================================ | 1015 | // OwnerRepoStateSent is ensured by ensure_fixture before this is called. |
| 963 | // Stage 1: ValidRepoSent is ensured by ensure_fixture before this is called | 1016 | // The state event is already on the relay in purgatory - retrieve it from cache. |
| 964 | // ============================================================ | 1017 | let state_event = self.get_cached_dependency(FixtureKind::OwnerRepoStateSent)?; |
| 965 | let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; | 1018 | let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; |
| 966 | let repo_id = self.extract_repo_id(&repo)?; | 1019 | let repo_id = self.extract_repo_id(&repo)?; |
| 967 | 1020 | ||
| 968 | // Build state event | ||
| 969 | let base_time = Timestamp::now().as_secs(); | ||
| 970 | let older_timestamp = Timestamp::from(base_time - 10); // 10 seconds ago | ||
| 971 | |||
| 972 | let state_event = self | ||
| 973 | .client | ||
| 974 | .event_builder(Kind::RepoState, "") | ||
| 975 | .tag(Tag::identifier(&repo_id)) | ||
| 976 | .tag(Tag::custom( | ||
| 977 | TagKind::custom("refs/heads/main"), | ||
| 978 | vec![DETERMINISTIC_COMMIT_HASH.to_string()], | ||
| 979 | )) | ||
| 980 | .tag(Tag::custom( | ||
| 981 | TagKind::custom("HEAD"), | ||
| 982 | vec!["ref: refs/heads/main".to_string()], | ||
| 983 | )) | ||
| 984 | .custom_time(older_timestamp) | ||
| 985 | .build(self.client.keys()) | ||
| 986 | .map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e))?; | ||
| 987 | |||
| 988 | // ============================================================ | 1021 | // ============================================================ |
| 989 | // Stage 2 & 3: Send to Relay, get Accepted response and Verify its Not Served | 1022 | // Stage 1: DataPushed - Clone repo, create commit, push |
| 990 | // ============================================================ | ||
| 991 | let (_, _in_purgatory) = self | ||
| 992 | .client | ||
| 993 | .send_event_and_note_purgatory(state_event.clone()) | ||
| 994 | .await?; | ||
| 995 | // Note: We don't fail if purgatory wasn't observed - the fixture proceeds regardless | ||
| 996 | |||
| 997 | // ============================================================ | ||
| 998 | // Stage 4: DataPushed - Clone repo, create commit, push | ||
| 999 | // ============================================================ | 1023 | // ============================================================ |
| 1000 | 1024 | ||
| 1001 | // Get relay domain from connected relay | 1025 | // Get relay domain from connected relay |
| @@ -1097,7 +1121,7 @@ impl<'a> TestContext<'a> { | |||
| 1097 | } | 1121 | } |
| 1098 | 1122 | ||
| 1099 | // ============================================================ | 1123 | // ============================================================ |
| 1100 | // Stage 5: Verify state event is on relay | 1124 | // Stage 2: Verify state event is released from purgatory |
| 1101 | // ============================================================ | 1125 | // ============================================================ |
| 1102 | 1126 | ||
| 1103 | tokio::time::sleep(Duration::from_millis(200)).await; | 1127 | tokio::time::sleep(Duration::from_millis(200)).await; |