diff options
Diffstat (limited to 'grasp-audit/src/specs/grasp01/purgatory.rs')
| -rw-r--r-- | grasp-audit/src/specs/grasp01/purgatory.rs | 329 |
1 files changed, 216 insertions, 113 deletions
diff --git a/grasp-audit/src/specs/grasp01/purgatory.rs b/grasp-audit/src/specs/grasp01/purgatory.rs index 9d97d3b..29eabad 100644 --- a/grasp-audit/src/specs/grasp01/purgatory.rs +++ b/grasp-audit/src/specs/grasp01/purgatory.rs | |||
| @@ -27,9 +27,11 @@ | |||
| 27 | //! - `test_pr_event_in_purgatory_git_push_accepted` - Git push to refs/nostr/<event-id> succeeds | 27 | //! - `test_pr_event_in_purgatory_git_push_accepted` - Git push to refs/nostr/<event-id> succeeds |
| 28 | //! - `test_pr_event_served_after_git_push` - Event becomes queryable after git data | 28 | //! - `test_pr_event_served_after_git_push` - Event becomes queryable after git data |
| 29 | 29 | ||
| 30 | use crate::fixtures::{clone_repo, create_commit, try_push}; | ||
| 30 | use crate::specs::grasp01::SpecRef; | 31 | use crate::specs::grasp01::SpecRef; |
| 31 | use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; | 32 | use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; |
| 32 | use nostr_sdk::prelude::*; | 33 | use nostr_sdk::prelude::*; |
| 34 | use std::fs; | ||
| 33 | use std::time::Duration; | 35 | use std::time::Duration; |
| 34 | 36 | ||
| 35 | /// Test suite for GRASP-01 purgatory behavior | 37 | /// Test suite for GRASP-01 purgatory behavior |
| @@ -47,9 +49,9 @@ impl PurgatoryTests { | |||
| 47 | results.add(Self::test_state_event_accepted_for_purgatory_announcement(client).await); | 49 | results.add(Self::test_state_event_accepted_for_purgatory_announcement(client).await); |
| 48 | 50 | ||
| 49 | // Deletion event tests (NIP-09) | 51 | // Deletion event tests (NIP-09) |
| 50 | results.add(Self::test_deletion_by_event_id_removes_purgatory_announcement(client).await); | 52 | results.add(Self::test_deletion_by_event_id_removes_purgatory_state_event(client).await); |
| 51 | results.add( | 53 | results.add( |
| 52 | Self::test_deletion_by_coordinate_removes_purgatory_announcement(client).await, | 54 | Self::test_deletion_by_coordinate_removes_purgatory_state_event(client).await, |
| 53 | ); | 55 | ); |
| 54 | 56 | ||
| 55 | // State event purgatory tests (already implemented) | 57 | // State event purgatory tests (already implemented) |
| @@ -656,192 +658,293 @@ impl PurgatoryTests { | |||
| 656 | // Deletion Event Tests (NIP-09) | 658 | // Deletion Event Tests (NIP-09) |
| 657 | // ============================================================ | 659 | // ============================================================ |
| 658 | 660 | ||
| 659 | /// Test: Kind 5 deletion event by event ID removes purgatory announcement | 661 | /// Test: Kind 5 deletion event by event ID removes a purgatory state event |
| 660 | /// | 662 | /// |
| 661 | /// Spec: NIP-09 | 663 | /// Spec: NIP-09 |
| 662 | /// "A special event with kind 5... having a list of one or more `e` or `a` tags, | 664 | /// "A special event with kind 5... having a list of one or more `e` or `a` tags, |
| 663 | /// each referencing an event the author is requesting to be deleted." | 665 | /// each referencing an event the author is requesting to be deleted." |
| 664 | /// | 666 | /// |
| 665 | /// This test verifies: | 667 | /// This test verifies: |
| 666 | /// 1. Send a valid repository announcement (enters purgatory) | 668 | /// 1. Get a promoted repo (OwnerStateDataPushed) so git pushes are possible |
| 667 | /// 2. Send a kind 5 deletion event referencing the announcement by event ID | 669 | /// 2. Clone the repo and create a unique commit (not yet pushed) |
| 668 | /// 3. The announcement is no longer in purgatory (git push would fail) | 670 | /// 3. Submit a state event pointing to that unique commit (enters purgatory) |
| 669 | /// 4. The deletion event itself is accepted by the relay | 671 | /// 4. Send a kind 5 deletion event referencing the state event by event ID |
| 670 | pub async fn test_deletion_by_event_id_removes_purgatory_announcement( | 672 | /// 5. Attempt to push the unique commit — MUST be rejected (no authorized state event) |
| 673 | pub async fn test_deletion_by_event_id_removes_purgatory_state_event( | ||
| 671 | client: &AuditClient, | 674 | client: &AuditClient, |
| 672 | ) -> TestResult { | 675 | ) -> TestResult { |
| 673 | TestResult::new( | 676 | TestResult::new( |
| 674 | "deletion_by_event_id_removes_purgatory_announcement", | 677 | "deletion_by_event_id_removes_purgatory_state_event", |
| 675 | SpecRef::PurgatoryAcceptUntilGitData, | 678 | SpecRef::PurgatoryAcceptUntilGitData, |
| 676 | "Kind 5 deletion by event ID SHOULD remove a purgatory announcement", | 679 | "Kind 5 deletion by event ID SHOULD remove a purgatory state event, causing push rejection", |
| 677 | ) | 680 | ) |
| 678 | .run(|| async { | 681 | .run(|| async { |
| 679 | let ctx = TestContext::new(client); | 682 | let ctx = TestContext::new(client); |
| 680 | 683 | ||
| 681 | // Send announcement to purgatory | 684 | // Stage 1: get a promoted repo with git data already on the relay |
| 682 | let repo = ctx | 685 | let existing_state = ctx |
| 683 | .get_fixture(FixtureKind::ValidRepoSent) | 686 | .get_fixture(FixtureKind::OwnerStateDataPushed) |
| 684 | .await | 687 | .await |
| 685 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; | 688 | .map_err(|e| format!("Failed to get promoted repo: {}", e))?; |
| 686 | 689 | ||
| 687 | let repo_id = repo | 690 | let repo_id = existing_state |
| 688 | .tags | 691 | .tags |
| 689 | .iter() | 692 | .iter() |
| 690 | .find(|t| t.kind() == TagKind::d()) | 693 | .find(|t| t.kind() == TagKind::d()) |
| 691 | .and_then(|t| t.content()) | 694 | .and_then(|t| t.content()) |
| 692 | .ok_or("Missing d tag in repo announcement")? | 695 | .ok_or("Missing d tag in state event")? |
| 693 | .to_string(); | 696 | .to_string(); |
| 694 | 697 | ||
| 695 | // Verify it's in purgatory (not served) | 698 | let relay_domain = client |
| 696 | tokio::time::sleep(Duration::from_millis(300)).await; | 699 | .relay_url() |
| 697 | if client.is_event_on_relay(repo.id).await.map_err(|e| e.to_string())? { | 700 | .await |
| 698 | return Err( | 701 | .map_err(|e| e.to_string())? |
| 699 | "Announcement was served immediately - purgatory not working".to_string(), | 702 | .trim_start_matches("ws://") |
| 700 | ); | 703 | .trim_start_matches("wss://") |
| 704 | .to_string(); | ||
| 705 | |||
| 706 | let npub = client | ||
| 707 | .public_key() | ||
| 708 | .to_bech32() | ||
| 709 | .map_err(|e| e.to_string())?; | ||
| 710 | |||
| 711 | // Stage 2: clone the repo and create a unique commit (not pushed yet) | ||
| 712 | let clone_path = clone_repo(&relay_domain, &npub, &repo_id) | ||
| 713 | .map_err(|e| format!("Failed to clone repo: {}", e))?; | ||
| 714 | |||
| 715 | let cleanup = || { let _ = fs::remove_dir_all(&clone_path); }; | ||
| 716 | |||
| 717 | let unique_commit = match create_commit(&clone_path, "deletion test unique commit") { | ||
| 718 | Ok(h) => h, | ||
| 719 | Err(e) => { cleanup(); return Err(format!("Failed to create commit: {}", e)); } | ||
| 720 | }; | ||
| 721 | |||
| 722 | // Stage 3: submit a state event pointing to the unique commit (enters purgatory) | ||
| 723 | let state_event = client | ||
| 724 | .event_builder(Kind::RepoState, "") | ||
| 725 | .tag(Tag::identifier(&repo_id)) | ||
| 726 | .tag(Tag::custom( | ||
| 727 | TagKind::custom("refs/heads/main"), | ||
| 728 | vec![unique_commit.clone()], | ||
| 729 | )) | ||
| 730 | .tag(Tag::custom( | ||
| 731 | TagKind::custom("HEAD"), | ||
| 732 | vec!["ref: refs/heads/main".to_string()], | ||
| 733 | )) | ||
| 734 | .build(client.keys()) | ||
| 735 | .map_err(|e| { cleanup(); format!("Failed to build state event: {}", e) })?; | ||
| 736 | |||
| 737 | let (_, in_purgatory) = client | ||
| 738 | .send_event_and_note_purgatory(state_event.clone()) | ||
| 739 | .await | ||
| 740 | .map_err(|e| { cleanup(); format!("Failed to send state event: {}", e) })?; | ||
| 741 | |||
| 742 | if !in_purgatory { | ||
| 743 | cleanup(); | ||
| 744 | return Err(format!( | ||
| 745 | "State event was served immediately (not in purgatory). \ | ||
| 746 | Commit {} may already exist on relay.", | ||
| 747 | unique_commit | ||
| 748 | )); | ||
| 701 | } | 749 | } |
| 702 | 750 | ||
| 703 | // Build and send kind 5 deletion event referencing the announcement by event ID | 751 | // Stage 4: send kind 5 deletion event referencing the state event by event ID |
| 704 | let deletion = client | 752 | let deletion = client |
| 705 | .event_builder(Kind::EventDeletion, "") | 753 | .event_builder(Kind::EventDeletion, "") |
| 706 | .tag(Tag::event(repo.id)) | 754 | .tag(Tag::event(state_event.id)) |
| 707 | .tag(Tag::custom( | 755 | .tag(Tag::custom(TagKind::custom("k"), vec!["30618"])) |
| 708 | TagKind::custom("k"), | ||
| 709 | vec!["30617"], | ||
| 710 | )) | ||
| 711 | .build(client.keys()) | 756 | .build(client.keys()) |
| 712 | .map_err(|e| format!("Failed to build deletion event: {}", e))?; | 757 | .map_err(|e| { cleanup(); format!("Failed to build deletion event: {}", e) })?; |
| 713 | 758 | ||
| 714 | client | 759 | client |
| 715 | .send_event(deletion) | 760 | .send_event(deletion) |
| 716 | .await | 761 | .await |
| 717 | .map_err(|e| format!("Relay rejected deletion event: {}", e))?; | 762 | .map_err(|e| { cleanup(); format!("Relay rejected deletion event: {}", e) })?; |
| 718 | 763 | ||
| 719 | tokio::time::sleep(Duration::from_millis(300)).await; | 764 | tokio::time::sleep(Duration::from_millis(300)).await; |
| 720 | 765 | ||
| 721 | // Verify the announcement can no longer be promoted by attempting a git push. | 766 | // Stage 5: attempt to push the unique commit — must be rejected |
| 722 | // We check this indirectly: if the purgatory entry was removed, a subsequent | 767 | let push_result = try_push(&clone_path); |
| 723 | // git push to the repo path should fail (no bare repo). | 768 | cleanup(); |
| 724 | // For the integration test we verify the announcement is still not served | 769 | |
| 725 | // (it was never promoted) and that the deletion event was accepted. | 770 | match push_result { |
| 726 | // The bare-repo deletion is verified by attempting a git clone. | 771 | Ok(false) => Ok(()), // push rejected as expected |
| 727 | let http_url = AuditClient::ws_to_http_url(&client.relay_url().await.map_err(|e| e.to_string())?) | 772 | Ok(true) => Err(format!( |
| 728 | .map_err(|e| e.to_string())?; | 773 | "Push was accepted but should have been rejected. \ |
| 729 | let clone_url = format!( | 774 | The state event (id={}) was deleted, so commit {} \ |
| 730 | "{}/{}/{}.git", | 775 | should not be authorized.", |
| 731 | http_url, | 776 | state_event.id, unique_commit |
| 732 | client.public_key().to_bech32().map_err(|e| e.to_string())?, | 777 | )), |
| 733 | repo_id | 778 | Err(e) => Err(format!("Git push error: {}", e)), |
| 734 | ); | ||
| 735 | |||
| 736 | // git ls-remote should fail (bare repo deleted) | ||
| 737 | let output = std::process::Command::new("git") | ||
| 738 | .args(["ls-remote", &clone_url]) | ||
| 739 | .output() | ||
| 740 | .map_err(|e| format!("Failed to run git ls-remote: {}", e))?; | ||
| 741 | |||
| 742 | if output.status.success() { | ||
| 743 | return Err(format!( | ||
| 744 | "Bare repo still exists after deletion event. \ | ||
| 745 | Expected git ls-remote to fail for {}", | ||
| 746 | clone_url | ||
| 747 | )); | ||
| 748 | } | 779 | } |
| 749 | |||
| 750 | Ok(()) | ||
| 751 | }) | 780 | }) |
| 752 | .await | 781 | .await |
| 753 | } | 782 | } |
| 754 | 783 | ||
| 755 | /// Test: Kind 5 deletion event by `a` tag coordinate removes purgatory announcement | 784 | /// Test: Kind 5 deletion event by `a` tag coordinate removes a purgatory state event |
| 756 | /// | 785 | /// |
| 757 | /// Spec: NIP-09 | 786 | /// Spec: NIP-09 |
| 758 | /// "When an `a` tag is used, relays SHOULD delete all versions of the replaceable | 787 | /// "When an `a` tag is used, relays SHOULD delete all versions of the replaceable |
| 759 | /// event up to the `created_at` timestamp of the deletion request event." | 788 | /// event up to the `created_at` timestamp of the deletion request event." |
| 760 | /// | 789 | /// |
| 761 | /// This test verifies: | 790 | /// This test verifies: |
| 762 | /// 1. Send a valid repository announcement (enters purgatory) | 791 | /// 1. Get a promoted repo (OwnerStateDataPushed) so git pushes are possible |
| 763 | /// 2. Send a kind 5 deletion event referencing the announcement by coordinate | 792 | /// 2. Generate a fresh keypair for a new maintainer |
| 764 | /// (`30617:<pubkey>:<identifier>`) | 793 | /// 3. Send a replacement owner announcement adding the new maintainer (goes to DB) |
| 765 | /// 3. The announcement is no longer in purgatory | 794 | /// 4. Send a state event signed by the new maintainer pointing to a unique commit |
| 766 | pub async fn test_deletion_by_coordinate_removes_purgatory_announcement( | 795 | /// (enters purgatory — maintainer is authorized but commit doesn't exist yet) |
| 796 | /// 5. Delete by coordinate `30618:<new_maintainer_pubkey>:<identifier>` | ||
| 797 | /// 6. Clone repo, create that unique commit, attempt to push — MUST be rejected | ||
| 798 | /// (the state event was deleted, so the commit is no longer authorized) | ||
| 799 | pub async fn test_deletion_by_coordinate_removes_purgatory_state_event( | ||
| 767 | client: &AuditClient, | 800 | client: &AuditClient, |
| 768 | ) -> TestResult { | 801 | ) -> TestResult { |
| 769 | TestResult::new( | 802 | TestResult::new( |
| 770 | "deletion_by_coordinate_removes_purgatory_announcement", | 803 | "deletion_by_coordinate_removes_purgatory_state_event", |
| 771 | SpecRef::PurgatoryAcceptUntilGitData, | 804 | SpecRef::PurgatoryAcceptUntilGitData, |
| 772 | "Kind 5 deletion by `a` coordinate SHOULD remove a purgatory announcement", | 805 | "Kind 5 deletion by `a` coordinate SHOULD remove a purgatory state event, causing push rejection", |
| 773 | ) | 806 | ) |
| 774 | .run(|| async { | 807 | .run(|| async { |
| 775 | let ctx = TestContext::new(client); | 808 | let ctx = TestContext::new(client); |
| 776 | 809 | ||
| 777 | // Send announcement to purgatory | 810 | // Stage 1: get a promoted repo with git data already on the relay |
| 778 | let repo = ctx | 811 | let existing_state = ctx |
| 779 | .get_fixture(FixtureKind::ValidRepoSent) | 812 | .get_fixture(FixtureKind::OwnerStateDataPushed) |
| 780 | .await | 813 | .await |
| 781 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; | 814 | .map_err(|e| format!("Failed to get promoted repo: {}", e))?; |
| 782 | 815 | ||
| 783 | let repo_id = repo | 816 | let repo_id = existing_state |
| 784 | .tags | 817 | .tags |
| 785 | .iter() | 818 | .iter() |
| 786 | .find(|t| t.kind() == TagKind::d()) | 819 | .find(|t| t.kind() == TagKind::d()) |
| 787 | .and_then(|t| t.content()) | 820 | .and_then(|t| t.content()) |
| 788 | .ok_or("Missing d tag in repo announcement")? | 821 | .ok_or("Missing d tag in state event")? |
| 789 | .to_string(); | 822 | .to_string(); |
| 790 | 823 | ||
| 791 | // Verify it's in purgatory (not served) | 824 | // Stage 2: generate a fresh keypair for a new maintainer |
| 792 | tokio::time::sleep(Duration::from_millis(300)).await; | 825 | let new_maintainer_keys = Keys::generate(); |
| 793 | if client.is_event_on_relay(repo.id).await.map_err(|e| e.to_string())? { | 826 | let new_maintainer_hex = new_maintainer_keys.public_key().to_hex(); |
| 794 | return Err( | ||
| 795 | "Announcement was served immediately - purgatory not working".to_string(), | ||
| 796 | ); | ||
| 797 | } | ||
| 798 | 827 | ||
| 799 | // Build coordinate: `30617:<pubkey_hex>:<identifier>` | 828 | // Stage 3: send a replacement owner announcement that adds the new maintainer. |
| 800 | let coord = format!( | 829 | // This is a replacement (same pubkey + identifier already in DB) so it goes |
| 801 | "30617:{}:{}", | 830 | // straight to the database without entering purgatory. |
| 802 | client.public_key().to_hex(), | 831 | let relay_url = client |
| 803 | repo_id | 832 | .relay_url() |
| 804 | ); | 833 | .await |
| 834 | .map_err(|e| e.to_string())?; | ||
| 835 | let http_url = relay_url | ||
| 836 | .replace("ws://", "http://") | ||
| 837 | .replace("wss://", "https://"); | ||
| 838 | let npub = client | ||
| 839 | .public_key() | ||
| 840 | .to_bech32() | ||
| 841 | .map_err(|e| e.to_string())?; | ||
| 805 | 842 | ||
| 806 | // Build and send kind 5 deletion event referencing by coordinate | 843 | let replacement_announcement = client |
| 807 | let deletion = client | 844 | .event_builder(Kind::GitRepoAnnouncement, "") |
| 808 | .event_builder(Kind::EventDeletion, "") | 845 | .tag(Tag::identifier(&repo_id)) |
| 809 | .tag(Tag::custom(TagKind::custom("a"), vec![coord])) | 846 | .tag(Tag::custom( |
| 810 | .tag(Tag::custom(TagKind::custom("k"), vec!["30617"])) | 847 | TagKind::custom("clone"), |
| 848 | vec![format!("{}/{}/{}.git", http_url, npub, repo_id)], | ||
| 849 | )) | ||
| 850 | .tag(Tag::custom( | ||
| 851 | TagKind::custom("relays"), | ||
| 852 | vec![relay_url.clone()], | ||
| 853 | )) | ||
| 854 | .tag(Tag::custom( | ||
| 855 | TagKind::custom("maintainers"), | ||
| 856 | vec![new_maintainer_hex.clone()], | ||
| 857 | )) | ||
| 811 | .build(client.keys()) | 858 | .build(client.keys()) |
| 812 | .map_err(|e| format!("Failed to build deletion event: {}", e))?; | 859 | .map_err(|e| format!("Failed to build replacement announcement: {}", e))?; |
| 813 | 860 | ||
| 814 | client | 861 | client |
| 815 | .send_event(deletion) | 862 | .send_event(replacement_announcement) |
| 816 | .await | 863 | .await |
| 817 | .map_err(|e| format!("Relay rejected deletion event: {}", e))?; | 864 | .map_err(|e| format!("Relay rejected replacement announcement: {}", e))?; |
| 818 | 865 | ||
| 819 | tokio::time::sleep(Duration::from_millis(300)).await; | 866 | tokio::time::sleep(Duration::from_millis(200)).await; |
| 820 | 867 | ||
| 821 | // Verify bare repo was deleted | 868 | // Stage 4: clone the repo and create a unique commit (not pushed yet) |
| 822 | let http_url = AuditClient::ws_to_http_url(&client.relay_url().await.map_err(|e| e.to_string())?) | 869 | let relay_domain = relay_url |
| 823 | .map_err(|e| e.to_string())?; | 870 | .trim_start_matches("ws://") |
| 824 | let clone_url = format!( | 871 | .trim_start_matches("wss://") |
| 825 | "{}/{}/{}.git", | 872 | .to_string(); |
| 826 | http_url, | 873 | |
| 827 | client.public_key().to_bech32().map_err(|e| e.to_string())?, | 874 | let clone_path = clone_repo(&relay_domain, &npub, &repo_id) |
| 828 | repo_id | 875 | .map_err(|e| format!("Failed to clone repo: {}", e))?; |
| 829 | ); | 876 | |
| 877 | let cleanup = || { let _ = fs::remove_dir_all(&clone_path); }; | ||
| 830 | 878 | ||
| 831 | let output = std::process::Command::new("git") | 879 | let unique_commit = match create_commit(&clone_path, "deletion coordinate test unique commit") { |
| 832 | .args(["ls-remote", &clone_url]) | 880 | Ok(h) => h, |
| 833 | .output() | 881 | Err(e) => { cleanup(); return Err(format!("Failed to create commit: {}", e)); } |
| 834 | .map_err(|e| format!("Failed to run git ls-remote: {}", e))?; | 882 | }; |
| 835 | 883 | ||
| 836 | if output.status.success() { | 884 | // Stage 5: submit a state event signed by the new maintainer pointing to the |
| 885 | // unique commit. The new maintainer is now authorized (listed in the replacement | ||
| 886 | // announcement), so the state event should enter purgatory (commit doesn't exist). | ||
| 887 | let state_event = client | ||
| 888 | .event_builder(Kind::RepoState, "") | ||
| 889 | .tag(Tag::identifier(&repo_id)) | ||
| 890 | .tag(Tag::custom( | ||
| 891 | TagKind::custom("refs/heads/main"), | ||
| 892 | vec![unique_commit.clone()], | ||
| 893 | )) | ||
| 894 | .tag(Tag::custom( | ||
| 895 | TagKind::custom("HEAD"), | ||
| 896 | vec!["ref: refs/heads/main".to_string()], | ||
| 897 | )) | ||
| 898 | .build(&new_maintainer_keys) | ||
| 899 | .map_err(|e| { cleanup(); format!("Failed to build state event: {}", e) })?; | ||
| 900 | |||
| 901 | let (_, in_purgatory) = client | ||
| 902 | .send_event_and_note_purgatory(state_event.clone()) | ||
| 903 | .await | ||
| 904 | .map_err(|e| { cleanup(); format!("Failed to send state event: {}", e) })?; | ||
| 905 | |||
| 906 | if !in_purgatory { | ||
| 907 | cleanup(); | ||
| 837 | return Err(format!( | 908 | return Err(format!( |
| 838 | "Bare repo still exists after deletion event. \ | 909 | "State event was served immediately (not in purgatory). \ |
| 839 | Expected git ls-remote to fail for {}", | 910 | Commit {} may already exist on relay.", |
| 840 | clone_url | 911 | unique_commit |
| 841 | )); | 912 | )); |
| 842 | } | 913 | } |
| 843 | 914 | ||
| 844 | Ok(()) | 915 | // Stage 6: send kind 5 deletion event signed by the new maintainer, |
| 916 | // referencing their state event by coordinate `30618:<pubkey>:<identifier>` | ||
| 917 | let coord = format!("30618:{}:{}", new_maintainer_hex, repo_id); | ||
| 918 | |||
| 919 | let deletion = client | ||
| 920 | .event_builder(Kind::EventDeletion, "") | ||
| 921 | .tag(Tag::custom(TagKind::custom("a"), vec![coord])) | ||
| 922 | .tag(Tag::custom(TagKind::custom("k"), vec!["30618"])) | ||
| 923 | .build(&new_maintainer_keys) | ||
| 924 | .map_err(|e| { cleanup(); format!("Failed to build deletion event: {}", e) })?; | ||
| 925 | |||
| 926 | client | ||
| 927 | .send_event(deletion) | ||
| 928 | .await | ||
| 929 | .map_err(|e| { cleanup(); format!("Relay rejected deletion event: {}", e) })?; | ||
| 930 | |||
| 931 | tokio::time::sleep(Duration::from_millis(300)).await; | ||
| 932 | |||
| 933 | // Stage 7: attempt to push the unique commit — must be rejected because | ||
| 934 | // the new maintainer's state event was deleted from purgatory | ||
| 935 | let push_result = try_push(&clone_path); | ||
| 936 | cleanup(); | ||
| 937 | |||
| 938 | match push_result { | ||
| 939 | Ok(false) => Ok(()), // push rejected as expected | ||
| 940 | Ok(true) => Err(format!( | ||
| 941 | "Push was accepted but should have been rejected. \ | ||
| 942 | The new maintainer's state event (id={}) was deleted by coordinate, \ | ||
| 943 | so commit {} should not be authorized.", | ||
| 944 | state_event.id, unique_commit | ||
| 945 | )), | ||
| 946 | Err(e) => Err(format!("Git push error: {}", e)), | ||
| 947 | } | ||
| 845 | }) | 948 | }) |
| 846 | .await | 949 | .await |
| 847 | } | 950 | } |