diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-24 14:15:04 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-24 14:15:04 +0000 |
| commit | 7f71a2e75a66bcacad9057f5e339e511e689b828 (patch) | |
| tree | b1bc6e9d2df28f9b740cde37f836c76bc1bb1c8e /grasp-audit/src/specs/grasp01/purgatory.rs | |
| parent | ef279a881fc1694fe2d868a32224874eb50cd358 (diff) | |
fix grasp-audit test isolation to prevent cross-spec relay state corruption
Add Purgatory-prefixed fixture variants (PurgatoryValidRepoSent,
PurgatoryOwnerStateDataPushed) that create independent repos never
shared with the main fixture chain. Purgatory tests that mutate relay
state (replacement announcements, new state events, deletions) now use
these isolated fixtures so they cannot corrupt the repo that
push-authorization tests depend on.
Run purgatory tests before push-auth in the full suite, since push-auth
sends new replaceable state events (kind 30618) for the shared repo_id
that would displace the original served state event.
Diffstat (limited to 'grasp-audit/src/specs/grasp01/purgatory.rs')
| -rw-r--r-- | grasp-audit/src/specs/grasp01/purgatory.rs | 107 |
1 files changed, 52 insertions, 55 deletions
diff --git a/grasp-audit/src/specs/grasp01/purgatory.rs b/grasp-audit/src/specs/grasp01/purgatory.rs index 29eabad..0686da8 100644 --- a/grasp-audit/src/specs/grasp01/purgatory.rs +++ b/grasp-audit/src/specs/grasp01/purgatory.rs | |||
| @@ -46,7 +46,11 @@ impl PurgatoryTests { | |||
| 46 | results.add(Self::test_announcement_not_served_before_git_data(client).await); | 46 | results.add(Self::test_announcement_not_served_before_git_data(client).await); |
| 47 | results.add(Self::test_announcement_served_after_git_push(client).await); | 47 | results.add(Self::test_announcement_served_after_git_push(client).await); |
| 48 | results.add(Self::test_bare_repo_exists_for_purgatory_announcement(client).await); | 48 | results.add(Self::test_bare_repo_exists_for_purgatory_announcement(client).await); |
| 49 | |||
| 50 | // State event purgatory tests | ||
| 49 | results.add(Self::test_state_event_accepted_for_purgatory_announcement(client).await); | 51 | results.add(Self::test_state_event_accepted_for_purgatory_announcement(client).await); |
| 52 | results.add(Self::test_state_event_not_served_before_git_data(client).await); | ||
| 53 | results.add(Self::test_state_event_served_after_git_push(client).await); | ||
| 50 | 54 | ||
| 51 | // Deletion event tests (NIP-09) | 55 | // Deletion event tests (NIP-09) |
| 52 | results.add(Self::test_deletion_by_event_id_removes_purgatory_state_event(client).await); | 56 | results.add(Self::test_deletion_by_event_id_removes_purgatory_state_event(client).await); |
| @@ -54,10 +58,6 @@ impl PurgatoryTests { | |||
| 54 | Self::test_deletion_by_coordinate_removes_purgatory_state_event(client).await, | 58 | Self::test_deletion_by_coordinate_removes_purgatory_state_event(client).await, |
| 55 | ); | 59 | ); |
| 56 | 60 | ||
| 57 | // State event purgatory tests (already implemented) | ||
| 58 | results.add(Self::test_state_event_not_served_before_git_data(client).await); | ||
| 59 | results.add(Self::test_state_event_served_after_git_push(client).await); | ||
| 60 | |||
| 61 | // PR purgatory tests | 61 | // PR purgatory tests |
| 62 | results.add(Self::test_pr_event_accepted_into_purgatory_and_isnt_served(client).await); | 62 | results.add(Self::test_pr_event_accepted_into_purgatory_and_isnt_served(client).await); |
| 63 | results.add(Self::test_pr_event_in_purgatory_git_push_accepted(client).await); | 63 | results.add(Self::test_pr_event_in_purgatory_git_push_accepted(client).await); |
| @@ -92,9 +92,12 @@ impl PurgatoryTests { | |||
| 92 | .run(|| async { | 92 | .run(|| async { |
| 93 | let ctx = TestContext::new(client); | 93 | let ctx = TestContext::new(client); |
| 94 | 94 | ||
| 95 | // Create a fresh repo announcement (not the served variant) | 95 | // Use the purgatory-specific fixture which creates its own independent repo. |
| 96 | // The shared ValidRepoSent may already be promoted (served) by the time this | ||
| 97 | // test runs if earlier specs triggered OwnerStateDataPushed. PurgatoryValidRepoSent | ||
| 98 | // is never promoted by any other test so the announcement stays in purgatory. | ||
| 96 | let repo = ctx | 99 | let repo = ctx |
| 97 | .get_fixture(FixtureKind::ValidRepoSent) | 100 | .get_fixture(FixtureKind::PurgatoryValidRepoSent) |
| 98 | .await | 101 | .await |
| 99 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; | 102 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; |
| 100 | 103 | ||
| @@ -106,7 +109,7 @@ impl PurgatoryTests { | |||
| 106 | .ok_or("Missing d tag in repo announcement")? | 109 | .ok_or("Missing d tag in repo announcement")? |
| 107 | .to_string(); | 110 | .to_string(); |
| 108 | 111 | ||
| 109 | // Query for the announcement - should NOT be served | 112 | // Query for the announcement - should NOT be served (purgatory) |
| 110 | let filter = Filter::new() | 113 | let filter = Filter::new() |
| 111 | .kind(Kind::GitRepoAnnouncement) | 114 | .kind(Kind::GitRepoAnnouncement) |
| 112 | .author(client.public_key()) | 115 | .author(client.public_key()) |
| @@ -153,13 +156,13 @@ impl PurgatoryTests { | |||
| 153 | .run(|| async { | 156 | .run(|| async { |
| 154 | let ctx = TestContext::new(client); | 157 | let ctx = TestContext::new(client); |
| 155 | 158 | ||
| 156 | // OwnerStateDataPushed fixture handles the full lifecycle: | 159 | // PurgatoryOwnerStateDataPushed fixture handles the full lifecycle: |
| 157 | // 1. Creates repo announcement (purgatory) | 160 | // 1. Creates repo announcement (purgatory) |
| 158 | // 2. Creates state event (purgatory) | 161 | // 2. Creates state event (purgatory) |
| 159 | // 3. Pushes git data | 162 | // 3. Pushes git data |
| 160 | // 4. Verifies events are served | 163 | // 4. Verifies events are served |
| 161 | let state_event = ctx | 164 | let state_event = ctx |
| 162 | .get_fixture(FixtureKind::OwnerStateDataPushed) | 165 | .get_fixture(FixtureKind::PurgatoryOwnerStateDataPushed) |
| 163 | .await | 166 | .await |
| 164 | .map_err(|e| format!("Failed to complete full lifecycle: {}", e))?; | 167 | .map_err(|e| format!("Failed to complete full lifecycle: {}", e))?; |
| 165 | 168 | ||
| @@ -190,18 +193,16 @@ impl PurgatoryTests { | |||
| 190 | )); | 193 | )); |
| 191 | } | 194 | } |
| 192 | 195 | ||
| 193 | // Verify state event is served | 196 | // Verify state event is served by querying its specific event ID. |
| 194 | let state_filter = Filter::new() | 197 | // We intentionally query by ID rather than kind+author+identifier because |
| 195 | .kind(Kind::RepoState) | 198 | // other tests (e.g. push-auth) may have sent a newer replaceable state event |
| 196 | .author(client.public_key()) | 199 | // for the same repo_id, which would displace this one in an identifier query. |
| 197 | .identifier(&repo_id); | 200 | let served = client |
| 198 | 201 | .is_event_on_relay(state_event.id) | |
| 199 | let state_events = client | ||
| 200 | .query(state_filter) | ||
| 201 | .await | 202 | .await |
| 202 | .map_err(|e| format!("Failed to query state events: {}", e))?; | 203 | .map_err(|e| format!("Failed to query state event: {}", e))?; |
| 203 | 204 | ||
| 204 | if !state_events.iter().any(|e| e.id == state_event.id) { | 205 | if !served { |
| 205 | return Err(format!( | 206 | return Err(format!( |
| 206 | "State event not served after git push. Event ID: {}", | 207 | "State event not served after git push. Event ID: {}", |
| 207 | state_event.id | 208 | state_event.id |
| @@ -234,9 +235,9 @@ impl PurgatoryTests { | |||
| 234 | .run(|| async { | 235 | .run(|| async { |
| 235 | let ctx = TestContext::new(client); | 236 | let ctx = TestContext::new(client); |
| 236 | 237 | ||
| 237 | // Get a repo announcement (in purgatory, no git data yet) | 238 | // Get the purgatory-specific repo announcement (never promoted by other tests) |
| 238 | let repo = ctx | 239 | let repo = ctx |
| 239 | .get_fixture(FixtureKind::ValidRepoSent) | 240 | .get_fixture(FixtureKind::PurgatoryValidRepoSent) |
| 240 | .await | 241 | .await |
| 241 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; | 242 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; |
| 242 | 243 | ||
| @@ -314,9 +315,9 @@ impl PurgatoryTests { | |||
| 314 | .run(|| async { | 315 | .run(|| async { |
| 315 | let ctx = TestContext::new(client); | 316 | let ctx = TestContext::new(client); |
| 316 | 317 | ||
| 317 | // Get a repo announcement (in purgatory) | 318 | // Get the purgatory-specific repo announcement (never promoted by other tests) |
| 318 | let repo = ctx | 319 | let repo = ctx |
| 319 | .get_fixture(FixtureKind::ValidRepoSent) | 320 | .get_fixture(FixtureKind::PurgatoryValidRepoSent) |
| 320 | .await | 321 | .await |
| 321 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; | 322 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; |
| 322 | 323 | ||
| @@ -407,11 +408,13 @@ impl PurgatoryTests { | |||
| 407 | .run(|| async { | 408 | .run(|| async { |
| 408 | let ctx = TestContext::new(client); | 409 | let ctx = TestContext::new(client); |
| 409 | 410 | ||
| 410 | // Get a repo with git data already pushed | 411 | // Use the isolated purgatory repo so this test's new state event |
| 412 | // does not displace the shared OwnerStateDataPushed state event | ||
| 413 | // that push-authorization tests depend on. | ||
| 411 | let existing_state = ctx | 414 | let existing_state = ctx |
| 412 | .get_fixture(FixtureKind::OwnerStateDataPushed) | 415 | .get_fixture(FixtureKind::PurgatoryOwnerStateDataPushed) |
| 413 | .await | 416 | .await |
| 414 | .map_err(|e| format!("Failed to get existing repo: {}", e))?; | 417 | .map_err(|e| format!("Failed to get purgatory test repo: {}", e))?; |
| 415 | 418 | ||
| 416 | let repo_id = existing_state | 419 | let repo_id = existing_state |
| 417 | .tags | 420 | .tags |
| @@ -461,7 +464,7 @@ impl PurgatoryTests { | |||
| 461 | /// Spec: GRASP-01 Line 22 | 464 | /// Spec: GRASP-01 Line 22 |
| 462 | /// "...kept in purgatory (not served) until the related git data arrives" | 465 | /// "...kept in purgatory (not served) until the related git data arrives" |
| 463 | /// | 466 | /// |
| 464 | /// This test verifies the full lifecycle using OwnerStateDataPushed fixture: | 467 | /// This test verifies the full lifecycle using PurgatoryOwnerStateDataPushed fixture: |
| 465 | /// 1. State event is sent (enters purgatory) | 468 | /// 1. State event is sent (enters purgatory) |
| 466 | /// 2. Git data is pushed matching the state event | 469 | /// 2. Git data is pushed matching the state event |
| 467 | /// 3. State event is now served | 470 | /// 3. State event is now served |
| @@ -474,32 +477,22 @@ impl PurgatoryTests { | |||
| 474 | .run(|| async { | 477 | .run(|| async { |
| 475 | let ctx = TestContext::new(client); | 478 | let ctx = TestContext::new(client); |
| 476 | 479 | ||
| 477 | // OwnerStateDataPushed handles the full lifecycle | 480 | // PurgatoryOwnerStateDataPushed handles the full lifecycle |
| 478 | let state_event = ctx | 481 | let state_event = ctx |
| 479 | .get_fixture(FixtureKind::OwnerStateDataPushed) | 482 | .get_fixture(FixtureKind::PurgatoryOwnerStateDataPushed) |
| 480 | .await | 483 | .await |
| 481 | .map_err(|e| format!("Failed to complete full lifecycle: {}", e))?; | 484 | .map_err(|e| format!("Failed to complete full lifecycle: {}", e))?; |
| 482 | 485 | ||
| 483 | // Verify state event is now served | 486 | // Verify state event is served by querying its specific event ID. |
| 484 | let repo_id = state_event | 487 | // We intentionally query by ID rather than kind+author+identifier because |
| 485 | .tags | 488 | // other tests (e.g. push-auth) may have sent a newer replaceable state event |
| 486 | .iter() | 489 | // for the same repo_id, which would displace this one in an identifier query. |
| 487 | .find(|t| t.kind() == TagKind::d()) | 490 | let served = client |
| 488 | .and_then(|t| t.content()) | 491 | .is_event_on_relay(state_event.id) |
| 489 | .ok_or("Missing d tag in state event")? | ||
| 490 | .to_string(); | ||
| 491 | |||
| 492 | let filter = Filter::new() | ||
| 493 | .kind(Kind::RepoState) | ||
| 494 | .author(client.public_key()) | ||
| 495 | .identifier(&repo_id); | ||
| 496 | |||
| 497 | let events = client | ||
| 498 | .query(filter) | ||
| 499 | .await | 492 | .await |
| 500 | .map_err(|e| format!("Failed to query state events: {}", e))?; | 493 | .map_err(|e| format!("Failed to query state event: {}", e))?; |
| 501 | 494 | ||
| 502 | if !events.iter().any(|e| e.id == state_event.id) { | 495 | if !served { |
| 503 | return Err(format!( | 496 | return Err(format!( |
| 504 | "State event not served after git push. Event ID: {}", | 497 | "State event not served after git push. Event ID: {}", |
| 505 | state_event.id | 498 | state_event.id |
| @@ -665,7 +658,7 @@ impl PurgatoryTests { | |||
| 665 | /// each referencing an event the author is requesting to be deleted." | 658 | /// each referencing an event the author is requesting to be deleted." |
| 666 | /// | 659 | /// |
| 667 | /// This test verifies: | 660 | /// This test verifies: |
| 668 | /// 1. Get a promoted repo (OwnerStateDataPushed) so git pushes are possible | 661 | /// 1. Get a promoted repo (PurgatoryOwnerStateDataPushed) so git pushes are possible |
| 669 | /// 2. Clone the repo and create a unique commit (not yet pushed) | 662 | /// 2. Clone the repo and create a unique commit (not yet pushed) |
| 670 | /// 3. Submit a state event pointing to that unique commit (enters purgatory) | 663 | /// 3. Submit a state event pointing to that unique commit (enters purgatory) |
| 671 | /// 4. Send a kind 5 deletion event referencing the state event by event ID | 664 | /// 4. Send a kind 5 deletion event referencing the state event by event ID |
| @@ -681,11 +674,12 @@ impl PurgatoryTests { | |||
| 681 | .run(|| async { | 674 | .run(|| async { |
| 682 | let ctx = TestContext::new(client); | 675 | let ctx = TestContext::new(client); |
| 683 | 676 | ||
| 684 | // Stage 1: get a promoted repo with git data already on the relay | 677 | // Stage 1: get the isolated purgatory repo (independent from the shared |
| 678 | // OwnerStateDataPushed chain that push-authorization tests depend on) | ||
| 685 | let existing_state = ctx | 679 | let existing_state = ctx |
| 686 | .get_fixture(FixtureKind::OwnerStateDataPushed) | 680 | .get_fixture(FixtureKind::PurgatoryOwnerStateDataPushed) |
| 687 | .await | 681 | .await |
| 688 | .map_err(|e| format!("Failed to get promoted repo: {}", e))?; | 682 | .map_err(|e| format!("Failed to get purgatory test repo: {}", e))?; |
| 689 | 683 | ||
| 690 | let repo_id = existing_state | 684 | let repo_id = existing_state |
| 691 | .tags | 685 | .tags |
| @@ -788,7 +782,7 @@ impl PurgatoryTests { | |||
| 788 | /// event up to the `created_at` timestamp of the deletion request event." | 782 | /// event up to the `created_at` timestamp of the deletion request event." |
| 789 | /// | 783 | /// |
| 790 | /// This test verifies: | 784 | /// This test verifies: |
| 791 | /// 1. Get a promoted repo (OwnerStateDataPushed) so git pushes are possible | 785 | /// 1. Get a promoted repo (PurgatoryOwnerStateDataPushed) so git pushes are possible |
| 792 | /// 2. Generate a fresh keypair for a new maintainer | 786 | /// 2. Generate a fresh keypair for a new maintainer |
| 793 | /// 3. Send a replacement owner announcement adding the new maintainer (goes to DB) | 787 | /// 3. Send a replacement owner announcement adding the new maintainer (goes to DB) |
| 794 | /// 4. Send a state event signed by the new maintainer pointing to a unique commit | 788 | /// 4. Send a state event signed by the new maintainer pointing to a unique commit |
| @@ -807,11 +801,14 @@ impl PurgatoryTests { | |||
| 807 | .run(|| async { | 801 | .run(|| async { |
| 808 | let ctx = TestContext::new(client); | 802 | let ctx = TestContext::new(client); |
| 809 | 803 | ||
| 810 | // Stage 1: get a promoted repo with git data already on the relay | 804 | // Stage 1: get the isolated purgatory repo (independent from the shared |
| 805 | // OwnerStateDataPushed chain that push-authorization tests depend on). | ||
| 806 | // This test sends a replacement announcement (kind 30617) for the repo which | ||
| 807 | // would corrupt the shared repo's maintainer set if we used OwnerStateDataPushed. | ||
| 811 | let existing_state = ctx | 808 | let existing_state = ctx |
| 812 | .get_fixture(FixtureKind::OwnerStateDataPushed) | 809 | .get_fixture(FixtureKind::PurgatoryOwnerStateDataPushed) |
| 813 | .await | 810 | .await |
| 814 | .map_err(|e| format!("Failed to get promoted repo: {}", e))?; | 811 | .map_err(|e| format!("Failed to get purgatory test repo: {}", e))?; |
| 815 | 812 | ||
| 816 | let repo_id = existing_state | 813 | let repo_id = existing_state |
| 817 | .tags | 814 | .tags |