diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-28 16:27:29 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-28 16:27:29 +0000 |
| commit | e6ceab90de1acad154624022a6036efac18abab6 (patch) | |
| tree | 315c7b7ffc22339ed6cd0a31002e2fb0df190684 /grasp-audit | |
| parent | b94262161df99966fbb8aa6861fb46603039111f (diff) | |
test: added checks that refs/nostr/<event-id> match commit in PR / update
Diffstat (limited to 'grasp-audit')
| -rw-r--r-- | grasp-audit/src/client.rs | 16 | ||||
| -rw-r--r-- | grasp-audit/src/fixtures.rs | 65 | ||||
| -rw-r--r-- | grasp-audit/src/lib.rs | 2 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/push_authorization.rs | 686 |
4 files changed, 641 insertions, 128 deletions
diff --git a/grasp-audit/src/client.rs b/grasp-audit/src/client.rs index 4f3401c..ed76a34 100644 --- a/grasp-audit/src/client.rs +++ b/grasp-audit/src/client.rs | |||
| @@ -25,6 +25,8 @@ pub struct AuditClient { | |||
| 25 | maintainer_keys: Keys, | 25 | maintainer_keys: Keys, |
| 26 | /// Recursive maintainer keys for testing recursive authorization scenarios | 26 | /// Recursive maintainer keys for testing recursive authorization scenarios |
| 27 | recursive_maintainer_keys: Keys, | 27 | recursive_maintainer_keys: Keys, |
| 28 | /// PR author keys for testing PR event scenarios | ||
| 29 | pr_author_keys: Keys, | ||
| 28 | /// Fixture cache for TestContext instances - shared across all contexts using this client | 30 | /// Fixture cache for TestContext instances - shared across all contexts using this client |
| 29 | fixture_cache: FixtureCache, | 31 | fixture_cache: FixtureCache, |
| 30 | } | 32 | } |
| @@ -36,6 +38,7 @@ impl AuditClient { | |||
| 36 | let keys = Keys::generate(); | 38 | let keys = Keys::generate(); |
| 37 | let maintainer_keys = Keys::generate(); | 39 | let maintainer_keys = Keys::generate(); |
| 38 | let recursive_maintainer_keys = Keys::generate(); | 40 | let recursive_maintainer_keys = Keys::generate(); |
| 41 | let pr_author_keys = Keys::generate(); | ||
| 39 | let client = Client::new(keys.clone()); | 42 | let client = Client::new(keys.clone()); |
| 40 | Self { | 43 | Self { |
| 41 | client, | 44 | client, |
| @@ -43,6 +46,7 @@ impl AuditClient { | |||
| 43 | keys, | 46 | keys, |
| 44 | maintainer_keys, | 47 | maintainer_keys, |
| 45 | recursive_maintainer_keys, | 48 | recursive_maintainer_keys, |
| 49 | pr_author_keys, | ||
| 46 | fixture_cache: Arc::new(Mutex::new(HashMap::new())), | 50 | fixture_cache: Arc::new(Mutex::new(HashMap::new())), |
| 47 | } | 51 | } |
| 48 | } | 52 | } |
| @@ -52,6 +56,7 @@ impl AuditClient { | |||
| 52 | let keys = Keys::generate(); | 56 | let keys = Keys::generate(); |
| 53 | let maintainer_keys = Keys::generate(); | 57 | let maintainer_keys = Keys::generate(); |
| 54 | let recursive_maintainer_keys = Keys::generate(); | 58 | let recursive_maintainer_keys = Keys::generate(); |
| 59 | let pr_author_keys = Keys::generate(); | ||
| 55 | let client = Client::new(keys.clone()); | 60 | let client = Client::new(keys.clone()); |
| 56 | 61 | ||
| 57 | // Add relay and connect | 62 | // Add relay and connect |
| @@ -102,6 +107,7 @@ impl AuditClient { | |||
| 102 | keys, | 107 | keys, |
| 103 | maintainer_keys, | 108 | maintainer_keys, |
| 104 | recursive_maintainer_keys, | 109 | recursive_maintainer_keys, |
| 110 | pr_author_keys, | ||
| 105 | fixture_cache: Arc::new(Mutex::new(HashMap::new())), | 111 | fixture_cache: Arc::new(Mutex::new(HashMap::new())), |
| 106 | }) | 112 | }) |
| 107 | } | 113 | } |
| @@ -278,6 +284,16 @@ impl AuditClient { | |||
| 278 | self.recursive_maintainer_keys.public_key().to_hex() | 284 | self.recursive_maintainer_keys.public_key().to_hex() |
| 279 | } | 285 | } |
| 280 | 286 | ||
| 287 | /// Get the PR author keys (for PR event testing) | ||
| 288 | pub fn pr_author_keys(&self) -> &Keys { | ||
| 289 | &self.pr_author_keys | ||
| 290 | } | ||
| 291 | |||
| 292 | /// Get the PR author public key as a hex string | ||
| 293 | pub fn pr_author_pubkey_hex(&self) -> String { | ||
| 294 | self.pr_author_keys.public_key().to_hex() | ||
| 295 | } | ||
| 296 | |||
| 281 | /// Create a NIP-34 repository announcement event with full customization | 297 | /// Create a NIP-34 repository announcement event with full customization |
| 282 | /// | 298 | /// |
| 283 | /// This is the core method for creating repository announcements. It allows | 299 | /// This is the core method for creating repository announcements. It allows |
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index b6fbc79..dc4e638 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs | |||
| @@ -74,6 +74,17 @@ pub const MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = "1c2d472c9b71ed51968a6650 | |||
| 74 | /// NOTE: This value is different from DETERMINISTIC_COMMIT_HASH due to different content | 74 | /// NOTE: This value is different from DETERMINISTIC_COMMIT_HASH due to different content |
| 75 | pub const RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = "05939b82de66fbdb9c077d0a64fc68522f3cb8e0"; | 75 | pub const RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = "05939b82de66fbdb9c077d0a64fc68522f3cb8e0"; |
| 76 | 76 | ||
| 77 | /// Deterministic commit hash for PR test fixtures (PRTestCommit variant) | ||
| 78 | /// This is the hash produced by creating a commit with: | ||
| 79 | /// - Message: "PR test deterministic commit" | ||
| 80 | /// - File: test.txt containing "PR test deterministic commit" | ||
| 81 | /// - Author date: 2024-01-01T00:00:00Z | ||
| 82 | /// - Committer date: 2024-01-01T00:00:00Z | ||
| 83 | /// - GPG signing: disabled | ||
| 84 | /// - User: "GRASP Audit Test <test@grasp-audit.local>" | ||
| 85 | /// - Parent: none (root commit) | ||
| 86 | pub const PR_TEST_COMMIT_HASH: &str = "8935183ff722bf04e861928c6a7e50868c6ca4a6"; | ||
| 87 | |||
| 77 | /// Types of test fixtures available | 88 | /// Types of test fixtures available |
| 78 | /// | 89 | /// |
| 79 | /// ## Fixture Dependencies | 90 | /// ## Fixture Dependencies |
| @@ -145,6 +156,15 @@ pub enum FixtureKind { | |||
| 145 | /// - State event points to RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH | 156 | /// - State event points to RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH |
| 146 | /// - Timestamp: 2 seconds in the past (most recent) | 157 | /// - Timestamp: 2 seconds in the past (most recent) |
| 147 | RecursiveMaintainerRepoAndState, | 158 | RecursiveMaintainerRepoAndState, |
| 159 | |||
| 160 | /// PR (Patch/Pull Request) event for the SAME repo_id as ValidRepo | ||
| 161 | /// - Requires ValidRepo (uses same repo_id) | ||
| 162 | /// - Signed by `client.pr_author_keys()` | ||
| 163 | /// - Kind 1617 (NIP-34 patch) | ||
| 164 | /// - Includes `a` tag referencing the repo | ||
| 165 | /// - Includes `c` tag pointing to PR_TEST_COMMIT_HASH | ||
| 166 | /// - Timestamp: 1 second in the past | ||
| 167 | PREvent, | ||
| 148 | } | 168 | } |
| 149 | 169 | ||
| 150 | /// Context mode for fixture management | 170 | /// Context mode for fixture management |
| @@ -633,6 +653,47 @@ impl<'a> TestContext<'a> { | |||
| 633 | // Return the state event (caller will send it) | 653 | // Return the state event (caller will send it) |
| 634 | self.build_recursive_maintainer_state(&repo_id) | 654 | self.build_recursive_maintainer_state(&repo_id) |
| 635 | } | 655 | } |
| 656 | |||
| 657 | FixtureKind::PREvent => { | ||
| 658 | use nostr_sdk::prelude::*; | ||
| 659 | |||
| 660 | // Reuse ValidRepo fixture to get repo_id and owner pubkey | ||
| 661 | let repo = self.get_or_create_repo().await?; | ||
| 662 | |||
| 663 | let repo_id = repo | ||
| 664 | .tags | ||
| 665 | .iter() | ||
| 666 | .find(|t| t.kind() == TagKind::d()) | ||
| 667 | .and_then(|t| t.content()) | ||
| 668 | .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepo fixture"))? | ||
| 669 | .to_string(); | ||
| 670 | |||
| 671 | // Create PR event 1 second in the past | ||
| 672 | let base_time = Timestamp::now().as_u64(); | ||
| 673 | let pr_timestamp = Timestamp::from(base_time - 1); | ||
| 674 | |||
| 675 | // Build NIP-34 patch event (kind 1617) | ||
| 676 | self.client | ||
| 677 | .event_builder( | ||
| 678 | Kind::Custom(1617), // NIP-34 patch/PR kind | ||
| 679 | "Test PR for GRASP validation", | ||
| 680 | ) | ||
| 681 | .tag(Tag::custom( | ||
| 682 | TagKind::custom("a"), | ||
| 683 | vec![format!( | ||
| 684 | "30617:{}:{}", | ||
| 685 | self.client.public_key().to_hex(), // Owner pubkey | ||
| 686 | repo_id | ||
| 687 | )], | ||
| 688 | )) | ||
| 689 | .tag(Tag::custom( | ||
| 690 | TagKind::custom("c"), | ||
| 691 | vec![PR_TEST_COMMIT_HASH.to_string()], | ||
| 692 | )) | ||
| 693 | .custom_time(pr_timestamp) | ||
| 694 | .build(self.client.pr_author_keys()) | ||
| 695 | .map_err(|e| anyhow::anyhow!("Failed to build PR event: {}", e)) | ||
| 696 | } | ||
| 636 | } | 697 | } |
| 637 | } | 698 | } |
| 638 | 699 | ||
| @@ -1071,6 +1132,8 @@ pub enum CommitVariant { | |||
| 1071 | Maintainer, | 1132 | Maintainer, |
| 1072 | /// Recursive maintainer pubkey variant - uses "Recursive maintainer initial commit" content | 1133 | /// Recursive maintainer pubkey variant - uses "Recursive maintainer initial commit" content |
| 1073 | RecursiveMaintainer, | 1134 | RecursiveMaintainer, |
| 1135 | /// PR test commit variant - for PR event tests | ||
| 1136 | PRTestCommit, | ||
| 1074 | } | 1137 | } |
| 1075 | 1138 | ||
| 1076 | impl CommitVariant { | 1139 | impl CommitVariant { |
| @@ -1080,6 +1143,7 @@ impl CommitVariant { | |||
| 1080 | CommitVariant::Owner => "Initial commit", | 1143 | CommitVariant::Owner => "Initial commit", |
| 1081 | CommitVariant::Maintainer => "Maintainer initial commit", | 1144 | CommitVariant::Maintainer => "Maintainer initial commit", |
| 1082 | CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit", | 1145 | CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit", |
| 1146 | CommitVariant::PRTestCommit => "PR test deterministic commit", | ||
| 1083 | } | 1147 | } |
| 1084 | } | 1148 | } |
| 1085 | 1149 | ||
| @@ -1089,6 +1153,7 @@ impl CommitVariant { | |||
| 1089 | CommitVariant::Owner => "Initial commit", | 1153 | CommitVariant::Owner => "Initial commit", |
| 1090 | CommitVariant::Maintainer => "Maintainer initial commit", | 1154 | CommitVariant::Maintainer => "Maintainer initial commit", |
| 1091 | CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit", | 1155 | CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit", |
| 1156 | CommitVariant::PRTestCommit => "PR test deterministic commit", | ||
| 1092 | } | 1157 | } |
| 1093 | } | 1158 | } |
| 1094 | } | 1159 | } |
diff --git a/grasp-audit/src/lib.rs b/grasp-audit/src/lib.rs index 5ee93b3..fb52ba7 100644 --- a/grasp-audit/src/lib.rs +++ b/grasp-audit/src/lib.rs | |||
| @@ -46,7 +46,7 @@ pub use fixtures::{ | |||
| 46 | // Types and constants | 46 | // Types and constants |
| 47 | CommitVariant, ContextMode, FixtureKind, TestContext, | 47 | CommitVariant, ContextMode, FixtureKind, TestContext, |
| 48 | DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH, | 48 | DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH, |
| 49 | RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, | 49 | PR_TEST_COMMIT_HASH, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, |
| 50 | }; | 50 | }; |
| 51 | pub use result::{AuditResult, TestResult}; | 51 | pub use result::{AuditResult, TestResult}; |
| 52 | 52 | ||
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index 0170f6e..97d068c 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs | |||
| @@ -16,6 +16,21 @@ | |||
| 16 | //! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test | 16 | //! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test |
| 17 | //! ``` | 17 | //! ``` |
| 18 | 18 | ||
| 19 | /// Expected hash for PR test deterministic commit | ||
| 20 | /// | ||
| 21 | /// This hash is produced by creating a commit with: | ||
| 22 | /// - File: test.txt containing "PR test deterministic commit" | ||
| 23 | /// - Message: "PR test deterministic commit" | ||
| 24 | /// - Author: "PR Test Author <pr-test@example.com>" | ||
| 25 | /// - Author date: 2024-01-01T00:00:00Z | ||
| 26 | /// - Committer date: 2024-01-01T00:00:00Z | ||
| 27 | /// - GPG signing: disabled | ||
| 28 | /// - Parent: none (root commit) | ||
| 29 | /// | ||
| 30 | /// Run `test_pr_test_commit_hash_discovery` to discover/verify this value. | ||
| 31 | #[allow(dead_code)] | ||
| 32 | const PR_TEST_COMMIT_HASH: &str = "8935183ff722bf04e861928c6a7e50868c6ca4a6"; | ||
| 33 | |||
| 19 | use crate::{ | 34 | use crate::{ |
| 20 | clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, | 35 | clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, |
| 21 | try_push, try_push_to_ref, AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, | 36 | try_push, try_push_to_ref, AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, |
| @@ -24,6 +39,268 @@ use crate::{ | |||
| 24 | }; | 39 | }; |
| 25 | use nostr_sdk::prelude::*; | 40 | use nostr_sdk::prelude::*; |
| 26 | use std::fs; | 41 | use std::fs; |
| 42 | use std::path::{Path, PathBuf}; | ||
| 43 | use std::process::Command; | ||
| 44 | |||
| 45 | // ============================================================ | ||
| 46 | // PR Event Test Helper Functions | ||
| 47 | // ============================================================ | ||
| 48 | |||
| 49 | /// Creates a deterministic PR test commit in the specified repository. | ||
| 50 | /// Returns the commit hash which should match PR_TEST_COMMIT_HASH. | ||
| 51 | /// | ||
| 52 | /// This function handles: | ||
| 53 | /// 1. Creating an orphan branch (removes all history) | ||
| 54 | /// 2. Clearing staged files | ||
| 55 | /// 3. Creating deterministic commit using PRTestCommit variant | ||
| 56 | /// 4. Replacing main branch with the orphan branch | ||
| 57 | /// 5. Verifying the commit hash matches expected value | ||
| 58 | /// | ||
| 59 | /// # Arguments | ||
| 60 | /// * `clone_path` - Path to the cloned repository | ||
| 61 | /// | ||
| 62 | /// # Returns | ||
| 63 | /// * `Ok(String)` - The commit hash (should match PR_TEST_COMMIT_HASH) | ||
| 64 | /// * `Err(String)` - Error message if commit creation failed | ||
| 65 | fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> { | ||
| 66 | // Step 1: Create orphan branch (removes all history) | ||
| 67 | let _ = Command::new("git") | ||
| 68 | .args(["checkout", "--orphan", "pr-test-branch"]) | ||
| 69 | .current_dir(clone_path) | ||
| 70 | .output(); | ||
| 71 | |||
| 72 | // Step 2: Clear staged files (orphan keeps files staged from previous branch) | ||
| 73 | let _ = Command::new("git") | ||
| 74 | .args(["rm", "-rf", "--cached", "."]) | ||
| 75 | .current_dir(clone_path) | ||
| 76 | .output(); | ||
| 77 | |||
| 78 | // Step 3: Create deterministic commit using existing function | ||
| 79 | let commit_hash = create_deterministic_commit_with_variant(clone_path, CommitVariant::PRTestCommit)?; | ||
| 80 | |||
| 81 | // Step 4: Replace main branch with our new orphan branch | ||
| 82 | let _ = Command::new("git") | ||
| 83 | .args(["branch", "-D", "main"]) | ||
| 84 | .current_dir(clone_path) | ||
| 85 | .output(); | ||
| 86 | |||
| 87 | let _ = Command::new("git") | ||
| 88 | .args(["branch", "-m", "main"]) | ||
| 89 | .current_dir(clone_path) | ||
| 90 | .output(); | ||
| 91 | |||
| 92 | // Verify commit hash matches expected | ||
| 93 | if commit_hash != PR_TEST_COMMIT_HASH { | ||
| 94 | return Err(format!( | ||
| 95 | "PR test commit hash mismatch: got {}, expected {}", | ||
| 96 | commit_hash, PR_TEST_COMMIT_HASH | ||
| 97 | )); | ||
| 98 | } | ||
| 99 | |||
| 100 | Ok(commit_hash) | ||
| 101 | } | ||
| 102 | |||
| 103 | /// Sets up a complete PR test repository with deterministic commit. | ||
| 104 | /// Returns: (clone_path, pr_event_id, repo_id, owner_npub) | ||
| 105 | /// | ||
| 106 | /// This function handles the complete setup for PR event tests: | ||
| 107 | /// 1. Gets RepoAnnouncement and PREvent fixtures | ||
| 108 | /// 2. Extracts repo details (repo_id, owner_npub, pr_event_id) | ||
| 109 | /// 3. Clones the repository | ||
| 110 | /// 4. Creates the deterministic PR test commit | ||
| 111 | /// | ||
| 112 | /// # Arguments | ||
| 113 | /// * `ctx` - The TestContext for fixture management | ||
| 114 | /// * `relay_url` - The relay URL for cloning (e.g., "localhost:7000") | ||
| 115 | /// | ||
| 116 | /// # Returns | ||
| 117 | /// * `Ok((PathBuf, String, String, String))` - (clone_path, pr_event_id, repo_id, owner_npub) | ||
| 118 | /// * `Err(String)` - Error message if setup failed | ||
| 119 | #[allow(dead_code)] | ||
| 120 | async fn setup_pr_test_repo( | ||
| 121 | ctx: &TestContext<'_>, | ||
| 122 | relay_url: &str, | ||
| 123 | ) -> Result<(PathBuf, String, String, String), String> { | ||
| 124 | // Get fixtures | ||
| 125 | let repo_event = ctx | ||
| 126 | .get_fixture(FixtureKind::ValidRepo) | ||
| 127 | .await | ||
| 128 | .map_err(|e| format!("Failed to get repo announcement: {}", e))?; | ||
| 129 | |||
| 130 | let pr_event = ctx | ||
| 131 | .get_fixture(FixtureKind::PREvent) | ||
| 132 | .await | ||
| 133 | .map_err(|e| format!("Failed to get PR event: {}", e))?; | ||
| 134 | |||
| 135 | // Extract repo details using nostr-sdk 0.43 API (field access) | ||
| 136 | let repo_id = repo_event | ||
| 137 | .tags | ||
| 138 | .iter() | ||
| 139 | .find(|t| t.kind() == TagKind::d()) | ||
| 140 | .and_then(|t| t.content()) | ||
| 141 | .ok_or("No repo identifier in announcement")? | ||
| 142 | .to_string(); | ||
| 143 | |||
| 144 | let owner_npub = repo_event.pubkey.to_bech32().map_err(|e| e.to_string())?; | ||
| 145 | let pr_event_id = pr_event.id.to_hex(); | ||
| 146 | |||
| 147 | // Clone the repository | ||
| 148 | let clone_path = clone_repo(relay_url, &owner_npub, &repo_id)?; | ||
| 149 | |||
| 150 | // Create the PR test commit | ||
| 151 | create_pr_test_commit(&clone_path)?; | ||
| 152 | |||
| 153 | Ok((clone_path, pr_event_id, repo_id, owner_npub)) | ||
| 154 | } | ||
| 155 | |||
| 156 | // ============================================================ | ||
| 157 | // PR Ref Push Test Setup Helpers - Minimize Test Duplication | ||
| 158 | // ============================================================ | ||
| 159 | |||
| 160 | /// Result of setting up a repo with a wrong commit pushed before PR event exists. | ||
| 161 | /// Used as shared setup for tests 3, 4, 5 which all depend on this scenario. | ||
| 162 | #[allow(dead_code)] | ||
| 163 | struct PrRefTestSetup { | ||
| 164 | clone_path: PathBuf, | ||
| 165 | pr_event_id: String, | ||
| 166 | repo_id: String, | ||
| 167 | owner_npub: String, | ||
| 168 | wrong_commit_hash: String, | ||
| 169 | } | ||
| 170 | |||
| 171 | impl PrRefTestSetup { | ||
| 172 | fn cleanup(&self) { | ||
| 173 | let _ = std::fs::remove_dir_all(&self.clone_path); | ||
| 174 | } | ||
| 175 | } | ||
| 176 | |||
| 177 | /// Sets up a repo and pushes a WRONG commit to refs/nostr/<pr-event-id> BEFORE PR event exists. | ||
| 178 | /// | ||
| 179 | /// This is the shared setup for PR ref lifecycle tests: | ||
| 180 | /// - Creates repo (gets PREvent fixture for event-id but doesn't publish yet) | ||
| 181 | /// - Clones repo | ||
| 182 | /// - Creates a commit that does NOT match PR_TEST_COMMIT_HASH | ||
| 183 | /// - Pushes to refs/nostr/<pr-event-id> (should succeed - no event to validate against) | ||
| 184 | /// | ||
| 185 | /// Tests using this setup: | ||
| 186 | /// - test_pr_push_to_nostr_ref_with_wrong_commit_accepted_before_event_received: verify initial push accepted | ||
| 187 | /// - test_pr_event_published_removes_nostr_ref_at_incorrect_commit: publish event, verify cleanup | ||
| 188 | /// - test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected: publish event, try push wrong commit | ||
| 189 | /// - test_push_to_nostr_ref_with_correct_commit_after_event_received_accepted: publish event, push correct commit | ||
| 190 | #[allow(dead_code)] | ||
| 191 | async fn setup_repo_with_wrong_commit_pushed( | ||
| 192 | ctx: &TestContext<'_>, | ||
| 193 | relay_domain: &str, | ||
| 194 | ) -> Result<PrRefTestSetup, String> { | ||
| 195 | // Get fixtures (PREvent fixture creates the event but doesn't publish until we call get_fixture) | ||
| 196 | let repo_event = ctx | ||
| 197 | .get_fixture(FixtureKind::ValidRepo) | ||
| 198 | .await | ||
| 199 | .map_err(|e| format!("Failed to get repo announcement: {}", e))?; | ||
| 200 | |||
| 201 | // Get PR event fixture (creates event object but doesn't publish to relay yet) | ||
| 202 | let pr_event = ctx | ||
| 203 | .get_fixture(FixtureKind::PREvent) | ||
| 204 | .await | ||
| 205 | .map_err(|e| format!("Failed to get PR event fixture: {}", e))?; | ||
| 206 | |||
| 207 | let repo_id = repo_event | ||
| 208 | .tags | ||
| 209 | .iter() | ||
| 210 | .find(|t| t.kind() == TagKind::d()) | ||
| 211 | .and_then(|t| t.content()) | ||
| 212 | .ok_or("No repo identifier in announcement")? | ||
| 213 | .to_string(); | ||
| 214 | |||
| 215 | let owner_npub = repo_event.pubkey.to_bech32().map_err(|e| e.to_string())?; | ||
| 216 | let pr_event_id = pr_event.id.to_hex(); | ||
| 217 | |||
| 218 | // Clone the repository | ||
| 219 | let clone_path = clone_repo(relay_domain, &owner_npub, &repo_id)?; | ||
| 220 | |||
| 221 | // Create a WRONG commit (not the one expected by PR event) | ||
| 222 | let wrong_commit_hash = create_deterministic_commit_with_variant(&clone_path, CommitVariant::Owner)?; | ||
| 223 | |||
| 224 | // Verify it's actually different from expected | ||
| 225 | if wrong_commit_hash == PR_TEST_COMMIT_HASH { | ||
| 226 | let _ = std::fs::remove_dir_all(&clone_path); | ||
| 227 | return Err("Test setup error: wrong_commit_hash equals PR_TEST_COMMIT_HASH".to_string()); | ||
| 228 | } | ||
| 229 | |||
| 230 | // Push to refs/nostr/<pr-event-id> (no event published yet, should succeed) | ||
| 231 | let push_output = Command::new("git") | ||
| 232 | .args(["push", "origin", &format!("main:refs/nostr/{}", pr_event_id)]) | ||
| 233 | .current_dir(&clone_path) | ||
| 234 | .output() | ||
| 235 | .map_err(|e| format!("Failed to execute git push: {}", e))?; | ||
| 236 | |||
| 237 | if !push_output.status.success() { | ||
| 238 | let stderr = String::from_utf8_lossy(&push_output.stderr); | ||
| 239 | let _ = std::fs::remove_dir_all(&clone_path); | ||
| 240 | return Err(format!( | ||
| 241 | "Initial push failed (expected success before PR event): {}", | ||
| 242 | stderr | ||
| 243 | )); | ||
| 244 | } | ||
| 245 | |||
| 246 | Ok(PrRefTestSetup { | ||
| 247 | clone_path, | ||
| 248 | pr_event_id, | ||
| 249 | repo_id, | ||
| 250 | owner_npub, | ||
| 251 | wrong_commit_hash, | ||
| 252 | }) | ||
| 253 | } | ||
| 254 | |||
| 255 | /// Publishes the PR event fixture and waits for relay to process it. | ||
| 256 | /// Call this after setup_repo_with_wrong_commit_pushed to test post-event behavior. | ||
| 257 | #[allow(dead_code)] | ||
| 258 | async fn publish_pr_event_and_wait(ctx: &TestContext<'_>) -> Result<Event, String> { | ||
| 259 | // Publishing the PR event - get_fixture publishes if not already published | ||
| 260 | let pr_event = ctx | ||
| 261 | .get_fixture(FixtureKind::PREvent) | ||
| 262 | .await | ||
| 263 | .map_err(|e| format!("Failed to publish PR event: {}", e))?; | ||
| 264 | |||
| 265 | // Wait for relay to process | ||
| 266 | tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; | ||
| 267 | |||
| 268 | Ok(pr_event) | ||
| 269 | } | ||
| 270 | |||
| 271 | /// Creates the correct PR test commit (matching PR_TEST_COMMIT_HASH) in an existing clone. | ||
| 272 | /// Used after wrong commit was pushed to test pushing the correct commit. | ||
| 273 | #[allow(dead_code)] | ||
| 274 | fn reset_to_correct_pr_commit(clone_path: &Path) -> Result<String, String> { | ||
| 275 | // Create the correct PR test commit (replaces current state) | ||
| 276 | create_pr_test_commit(clone_path) | ||
| 277 | } | ||
| 278 | |||
| 279 | /// Attempts to push current HEAD to refs/nostr/<pr-event-id>. | ||
| 280 | /// Returns Ok(true) if push succeeded, Ok(false) if rejected, Err on git error. | ||
| 281 | #[allow(dead_code)] | ||
| 282 | fn push_to_pr_ref(clone_path: &Path, pr_event_id: &str) -> Result<bool, String> { | ||
| 283 | let push_output = Command::new("git") | ||
| 284 | .args(["push", "--force", "origin", &format!("main:refs/nostr/{}", pr_event_id)]) | ||
| 285 | .current_dir(clone_path) | ||
| 286 | .output() | ||
| 287 | .map_err(|e| format!("Failed to execute git push: {}", e))?; | ||
| 288 | |||
| 289 | Ok(push_output.status.success()) | ||
| 290 | } | ||
| 291 | |||
| 292 | /// Checks if a ref exists on the remote. | ||
| 293 | #[allow(dead_code)] | ||
| 294 | fn ref_exists_on_remote(clone_path: &Path, ref_name: &str) -> Result<bool, String> { | ||
| 295 | let output = Command::new("git") | ||
| 296 | .args(["ls-remote", "origin", ref_name]) | ||
| 297 | .current_dir(clone_path) | ||
| 298 | .output() | ||
| 299 | .map_err(|e| format!("Failed to execute git ls-remote: {}", e))?; | ||
| 300 | |||
| 301 | let stdout = String::from_utf8_lossy(&output.stdout); | ||
| 302 | Ok(!stdout.trim().is_empty()) | ||
| 303 | } | ||
| 27 | 304 | ||
| 28 | /// Test suite for Push Authorization operations | 305 | /// Test suite for Push Authorization operations |
| 29 | pub struct PushAuthorizationTests; | 306 | pub struct PushAuthorizationTests; |
| @@ -41,8 +318,11 @@ impl PushAuthorizationTests { | |||
| 41 | results.add(Self::test_push_rejected_wrong_commit(client, relay_domain).await); | 318 | results.add(Self::test_push_rejected_wrong_commit(client, relay_domain).await); |
| 42 | results.add(Self::test_push_authorized_by_maintainer_state_only(client, relay_domain).await); | 319 | results.add(Self::test_push_authorized_by_maintainer_state_only(client, relay_domain).await); |
| 43 | results.add(Self::test_push_authorized_by_recursive_maintainer_state(client, relay_domain).await); | 320 | results.add(Self::test_push_authorized_by_recursive_maintainer_state(client, relay_domain).await); |
| 44 | results.add(Self::test_push_to_refs_nostr_valid_event_id(client, relay_domain).await); | 321 | results.add(Self::test_push_to_nostr_ref_with_invalid_event_id_rejected(client, relay_domain).await); |
| 45 | results.add(Self::test_push_to_refs_nostr_invalid_event_id(client, relay_domain).await); | 322 | results.add(Self::test_pr_push_to_nostr_ref_with_wrong_commit_accepted_before_event_received(client, relay_domain).await); |
| 323 | results.add(Self::test_pr_event_published_removes_nostr_ref_at_incorrect_commit(client, relay_domain).await); | ||
| 324 | results.add(Self::test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected(client, relay_domain).await); | ||
| 325 | results.add(Self::test_push_to_nostr_ref_with_correct_commit_after_event_received_accepted(client, relay_domain).await); | ||
| 46 | 326 | ||
| 47 | results | 327 | results |
| 48 | } | 328 | } |
| @@ -1032,22 +1312,22 @@ impl PushAuthorizationTests { | |||
| 1032 | } | 1312 | } |
| 1033 | } | 1313 | } |
| 1034 | 1314 | ||
| 1035 | /// Test that push to refs/nostr/<event-id> succeeds with valid EventId format | 1315 | /// Test that push to refs/nostr/<invalid> is rejected with invalid EventId format |
| 1036 | /// | 1316 | /// |
| 1037 | /// GRASP-01: "MUST accept pushes via this service to `refs/nostr/<event-id>`" | 1317 | /// GRASP-01: "MUST accept pushes via this service to `refs/nostr/<event-id>`" |
| 1038 | /// The event_id must parse as a valid rust-nostr EventId (64-char hex string). | 1318 | /// The event_id must parse as a valid rust-nostr EventId (64-char hex string). |
| 1039 | /// This does NOT require the ref to be listed in any state event - it's purely format validation. | 1319 | /// Invalid formats (too short, non-hex, etc.) should be rejected. |
| 1040 | /// | 1320 | /// |
| 1041 | /// ## Fixture-First Pattern | 1321 | /// ## Fixture-First Pattern |
| 1042 | /// | 1322 | /// |
| 1043 | /// 1. **Generate**: Create repo with ValidRepo fixture (no state event needed) | 1323 | /// 1. **Generate**: Create repo with ValidRepo fixture (no state event needed) |
| 1044 | /// 2. **Send**: Clone repo, create commit, push to refs/nostr/<valid-event-id> | 1324 | /// 2. **Send**: Clone repo, create commit, try to push to refs/nostr/123 (invalid) |
| 1045 | /// 3. **Verify**: Push should succeed because event-id format is valid | 1325 | /// 3. **Verify**: Push should be rejected because event-id format is invalid |
| 1046 | pub async fn test_push_to_refs_nostr_valid_event_id( | 1326 | pub async fn test_push_to_nostr_ref_with_invalid_event_id_rejected( |
| 1047 | client: &AuditClient, | 1327 | client: &AuditClient, |
| 1048 | relay_domain: &str, | 1328 | relay_domain: &str, |
| 1049 | ) -> TestResult { | 1329 | ) -> TestResult { |
| 1050 | let test_name = "test_push_to_refs_nostr_valid_event_id"; | 1330 | let test_name = "test_push_to_nostr_ref_with_invalid_event_id_rejected"; |
| 1051 | 1331 | ||
| 1052 | // ============================================================ | 1332 | // ============================================================ |
| 1053 | // Step 1: GENERATE - Create repo (no state event needed for refs/nostr/) | 1333 | // Step 1: GENERATE - Create repo (no state event needed for refs/nostr/) |
| @@ -1060,7 +1340,7 @@ impl PushAuthorizationTests { | |||
| 1060 | return TestResult::new( | 1340 | return TestResult::new( |
| 1061 | test_name, | 1341 | test_name, |
| 1062 | "GRASP-01", | 1342 | "GRASP-01", |
| 1063 | "Push to refs/nostr/<valid-event-id> accepted", | 1343 | "Push to refs/nostr/<invalid-event-id> rejected", |
| 1064 | ) | 1344 | ) |
| 1065 | .fail(&format!("Failed to create repo: {}", e)); | 1345 | .fail(&format!("Failed to create repo: {}", e)); |
| 1066 | } | 1346 | } |
| @@ -1078,7 +1358,7 @@ impl PushAuthorizationTests { | |||
| 1078 | let npub = repo.pubkey.to_bech32().unwrap(); | 1358 | let npub = repo.pubkey.to_bech32().unwrap(); |
| 1079 | 1359 | ||
| 1080 | // ============================================================ | 1360 | // ============================================================ |
| 1081 | // Step 2: SEND - Clone repo, create commit, push to refs/nostr/<event-id> | 1361 | // Step 2: SEND - Clone repo, create commit, try push to invalid ref |
| 1082 | // ============================================================ | 1362 | // ============================================================ |
| 1083 | let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { | 1363 | let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { |
| 1084 | Ok(p) => p, | 1364 | Ok(p) => p, |
| @@ -1086,7 +1366,7 @@ impl PushAuthorizationTests { | |||
| 1086 | return TestResult::new( | 1366 | return TestResult::new( |
| 1087 | test_name, | 1367 | test_name, |
| 1088 | "GRASP-01", | 1368 | "GRASP-01", |
| 1089 | "Push to refs/nostr/<valid-event-id> accepted", | 1369 | "Push to refs/nostr/<invalid-event-id> rejected", |
| 1090 | ) | 1370 | ) |
| 1091 | .fail(&e); | 1371 | .fail(&e); |
| 1092 | } | 1372 | } |
| @@ -1096,185 +1376,337 @@ impl PushAuthorizationTests { | |||
| 1096 | }; | 1376 | }; |
| 1097 | 1377 | ||
| 1098 | // Create a unique commit | 1378 | // Create a unique commit |
| 1099 | if let Err(e) = create_commit(&clone_path, "Test commit for refs/nostr push") { | 1379 | if let Err(e) = create_commit(&clone_path, "Test commit for invalid refs/nostr push") { |
| 1100 | cleanup(); | 1380 | cleanup(); |
| 1101 | return TestResult::new( | 1381 | return TestResult::new( |
| 1102 | test_name, | 1382 | test_name, |
| 1103 | "GRASP-01", | 1383 | "GRASP-01", |
| 1104 | "Push to refs/nostr/<valid-event-id> accepted", | 1384 | "Push to refs/nostr/<invalid-event-id> rejected", |
| 1105 | ) | 1385 | ) |
| 1106 | .fail(&e); | 1386 | .fail(&e); |
| 1107 | } | 1387 | } |
| 1108 | 1388 | ||
| 1109 | // Generate a random event to get a valid EventId | 1389 | // Use an invalid event-id (too short, not a valid 64-char hex) |
| 1110 | let keys = Keys::generate(); | 1390 | let invalid_event_id = "123"; |
| 1111 | let event = match EventBuilder::text_note("test") | 1391 | let ref_name = format!("refs/nostr/{}", invalid_event_id); |
| 1112 | .sign(&keys) | ||
| 1113 | .await | ||
| 1114 | { | ||
| 1115 | Ok(e) => e, | ||
| 1116 | Err(e) => { | ||
| 1117 | cleanup(); | ||
| 1118 | return TestResult::new( | ||
| 1119 | test_name, | ||
| 1120 | "GRASP-01", | ||
| 1121 | "Push to refs/nostr/<valid-event-id> accepted", | ||
| 1122 | ) | ||
| 1123 | .fail(&format!("Failed to create test event: {}", e)); | ||
| 1124 | } | ||
| 1125 | }; | ||
| 1126 | |||
| 1127 | // Use the event id as the refs/nostr/ target | ||
| 1128 | let ref_name = format!("refs/nostr/{}", event.id); | ||
| 1129 | 1392 | ||
| 1130 | // ============================================================ | 1393 | // ============================================================ |
| 1131 | // Step 3: VERIFY - Push should succeed with valid event-id format | 1394 | // Step 3: VERIFY - Push should be rejected with invalid event-id format |
| 1132 | // ============================================================ | 1395 | // ============================================================ |
| 1133 | let push_result = try_push_to_ref(&clone_path, &ref_name); | 1396 | let push_result = try_push_to_ref(&clone_path, &ref_name); |
| 1134 | cleanup(); | 1397 | cleanup(); |
| 1135 | 1398 | ||
| 1136 | match push_result { | 1399 | match push_result { |
| 1137 | Ok(true) => TestResult::new( | 1400 | Ok(false) => TestResult::new( |
| 1138 | test_name, | 1401 | test_name, |
| 1139 | "GRASP-01", | 1402 | "GRASP-01", |
| 1140 | "Push to refs/nostr/<valid-event-id> accepted", | 1403 | "Push to refs/nostr/<invalid-event-id> rejected", |
| 1141 | ) | 1404 | ) |
| 1142 | .pass(), | 1405 | .pass(), |
| 1143 | Ok(false) => TestResult::new( | 1406 | Ok(true) => TestResult::new( |
| 1144 | test_name, | 1407 | test_name, |
| 1145 | "GRASP-01", | 1408 | "GRASP-01", |
| 1146 | "Push to refs/nostr/<valid-event-id> accepted", | 1409 | "Push to refs/nostr/<invalid-event-id> rejected", |
| 1147 | ) | 1410 | ) |
| 1148 | .fail(&format!( | 1411 | .fail(&format!( |
| 1149 | "Push to {} was rejected but should be accepted. \ | 1412 | "Push to {} was accepted but should be rejected. \ |
| 1150 | The event-id '{}' is a valid 64-character hex string (EventId format).", | 1413 | The event-id '{}' is NOT a valid 64-character hex string (EventId format). \ |
| 1151 | ref_name, event.id | 1414 | The relay should reject pushes to refs/nostr/ with invalid event-id format.", |
| 1415 | ref_name, invalid_event_id | ||
| 1152 | )), | 1416 | )), |
| 1153 | Err(e) => TestResult::new( | 1417 | Err(e) => TestResult::new( |
| 1154 | test_name, | 1418 | test_name, |
| 1155 | "GRASP-01", | 1419 | "GRASP-01", |
| 1156 | "Push to refs/nostr/<valid-event-id> accepted", | 1420 | "Push to refs/nostr/<invalid-event-id> rejected", |
| 1157 | ) | 1421 | ) |
| 1158 | .fail(&format!("Push error: {}", e)), | 1422 | .fail(&format!("Push error: {}", e)), |
| 1159 | } | 1423 | } |
| 1160 | } | 1424 | } |
| 1161 | 1425 | ||
| 1162 | /// Test that push to refs/nostr/<invalid> is rejected with invalid EventId format | 1426 | /// Test 1: Push wrong commit to refs/nostr/<pr-event-id> BEFORE PR event is published |
| 1163 | /// | ||
| 1164 | /// GRASP-01: "MUST accept pushes via this service to `refs/nostr/<event-id>`" | ||
| 1165 | /// The event_id must parse as a valid rust-nostr EventId (64-char hex string). | ||
| 1166 | /// Invalid formats (too short, non-hex, etc.) should be rejected. | ||
| 1167 | /// | 1427 | /// |
| 1168 | /// ## Fixture-First Pattern | 1428 | /// This test verifies that the relay accepts pushes to refs/nostr/<event-id> |
| 1429 | /// when no corresponding event exists yet. This is expected behavior because | ||
| 1430 | /// there's no validation event to check against. | ||
| 1169 | /// | 1431 | /// |
| 1170 | /// 1. **Generate**: Create repo with ValidRepo fixture (no state event needed) | 1432 | /// Uses `setup_repo_with_wrong_commit_pushed` helper which handles all setup. |
| 1171 | /// 2. **Send**: Clone repo, create commit, try to push to refs/nostr/123 (invalid) | 1433 | pub async fn test_pr_push_to_nostr_ref_with_wrong_commit_accepted_before_event_received( |
| 1172 | /// 3. **Verify**: Push should be rejected because event-id format is invalid | ||
| 1173 | pub async fn test_push_to_refs_nostr_invalid_event_id( | ||
| 1174 | client: &AuditClient, | 1434 | client: &AuditClient, |
| 1175 | relay_domain: &str, | 1435 | relay_domain: &str, |
| 1176 | ) -> TestResult { | 1436 | ) -> TestResult { |
| 1177 | let test_name = "test_push_to_refs_nostr_invalid_event_id"; | 1437 | let test_name = "test_pr_push_to_nostr_ref_with_wrong_commit_accepted_before_event_received"; |
| 1438 | let desc = "Push wrong commit to refs/nostr/<pr-event-id> before PR event (should accept)"; | ||
| 1439 | let ctx = TestContext::new(client); | ||
| 1178 | 1440 | ||
| 1179 | // ============================================================ | 1441 | // Setup includes: create repo, clone, create wrong commit, push to refs/nostr/<event-id> |
| 1180 | // Step 1: GENERATE - Create repo (no state event needed for refs/nostr/) | 1442 | // The push happens BEFORE PR event is published, so should succeed |
| 1181 | // ============================================================ | 1443 | let setup = match setup_repo_with_wrong_commit_pushed(&ctx, relay_domain).await { |
| 1444 | Ok(s) => s, | ||
| 1445 | Err(e) => { | ||
| 1446 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | ||
| 1447 | } | ||
| 1448 | }; | ||
| 1449 | |||
| 1450 | // Setup already pushed and verified success - just cleanup and report pass | ||
| 1451 | setup.cleanup(); | ||
| 1452 | |||
| 1453 | TestResult::new(test_name, "GRASP-01", desc).pass() | ||
| 1454 | } | ||
| 1455 | |||
| 1456 | /// Test 2: After publishing PR event, verify that incorrect refs get cleaned up | ||
| 1457 | /// | ||
| 1458 | /// This test verifies the expected behavior: when a PR event is published, | ||
| 1459 | /// the relay should validate any existing refs/nostr/<event-id> refs and | ||
| 1460 | /// delete those that don't match the commit in the PR event's `c` tag. | ||
| 1461 | /// | ||
| 1462 | /// Currently NOT_IMPLEMENTED - the relay doesn't have this cleanup logic yet. | ||
| 1463 | /// | ||
| 1464 | /// Depends on: `setup_repo_with_wrong_commit_pushed` (wrong commit already pushed) | ||
| 1465 | pub async fn test_pr_event_published_removes_nostr_ref_at_incorrect_commit( | ||
| 1466 | client: &AuditClient, | ||
| 1467 | relay_domain: &str, | ||
| 1468 | ) -> TestResult { | ||
| 1469 | let test_name = "test_pr_event_published_removes_nostr_ref_at_incorrect_commit"; | ||
| 1470 | let desc = "Publishing PR event should trigger cleanup of incorrect refs (NOT_IMPLEMENTED)"; | ||
| 1182 | let ctx = TestContext::new(client); | 1471 | let ctx = TestContext::new(client); |
| 1183 | 1472 | ||
| 1184 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | 1473 | // Setup: wrong commit already pushed to refs/nostr/<pr-event-id> |
| 1185 | Ok(r) => r, | 1474 | let setup = match setup_repo_with_wrong_commit_pushed(&ctx, relay_domain).await { |
| 1475 | Ok(s) => s, | ||
| 1186 | Err(e) => { | 1476 | Err(e) => { |
| 1187 | return TestResult::new( | 1477 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); |
| 1188 | test_name, | ||
| 1189 | "GRASP-01", | ||
| 1190 | "Push to refs/nostr/<invalid-event-id> rejected", | ||
| 1191 | ) | ||
| 1192 | .fail(&format!("Failed to create repo: {}", e)); | ||
| 1193 | } | 1478 | } |
| 1194 | }; | 1479 | }; |
| 1195 | 1480 | ||
| 1196 | tokio::time::sleep(std::time::Duration::from_millis(200)).await; | 1481 | // NOW publish the PR event - this should trigger cleanup validation |
| 1482 | if let Err(e) = publish_pr_event_and_wait(&ctx).await { | ||
| 1483 | setup.cleanup(); | ||
| 1484 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | ||
| 1485 | } | ||
| 1197 | 1486 | ||
| 1198 | let repo_id = repo | 1487 | // Check if the incorrect ref was deleted |
| 1199 | .tags | 1488 | let ref_name = format!("refs/nostr/{}", setup.pr_event_id); |
| 1200 | .iter() | 1489 | let refs_exist = match ref_exists_on_remote(&setup.clone_path, &ref_name) { |
| 1201 | .find(|t| t.kind() == TagKind::d()) | 1490 | Ok(exists) => exists, |
| 1202 | .and_then(|t| t.content()) | 1491 | Err(e) => { |
| 1203 | .unwrap() | 1492 | setup.cleanup(); |
| 1204 | .to_string(); | 1493 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); |
| 1205 | let npub = repo.pubkey.to_bech32().unwrap(); | 1494 | } |
| 1495 | }; | ||
| 1206 | 1496 | ||
| 1207 | // ============================================================ | 1497 | setup.cleanup(); |
| 1208 | // Step 2: SEND - Clone repo, create commit, try push to invalid ref | 1498 | |
| 1209 | // ============================================================ | 1499 | // Document current behavior: relay doesn't implement automatic cleanup yet |
| 1210 | let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { | 1500 | TestResult::new(test_name, "GRASP-01", desc).fail(&format!( |
| 1211 | Ok(p) => p, | 1501 | "NOT_IMPLEMENTED: Relay should delete refs/nostr/<event-id> when PR event is published \ |
| 1502 | with non-matching commit. Currently ref still exists: {}. This requires relay-side validation logic.", | ||
| 1503 | refs_exist | ||
| 1504 | )) | ||
| 1505 | } | ||
| 1506 | |||
| 1507 | /// Test 3: Push wrong commit to refs/nostr/<pr-event-id> AFTER PR event exists | ||
| 1508 | /// | ||
| 1509 | /// This test verifies that the relay rejects pushes to refs/nostr/<event-id> | ||
| 1510 | /// when a corresponding event exists but the pushed commit doesn't match | ||
| 1511 | /// the commit in the PR event's `c` tag. | ||
| 1512 | /// | ||
| 1513 | /// Depends on: `setup_repo_with_wrong_commit_pushed` for repo/clone setup, then publishes PR event | ||
| 1514 | pub async fn test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected( | ||
| 1515 | client: &AuditClient, | ||
| 1516 | relay_domain: &str, | ||
| 1517 | ) -> TestResult { | ||
| 1518 | let test_name = "test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected"; | ||
| 1519 | let desc = "Push wrong commit to refs/nostr/<pr-event-id> after PR event (should reject)"; | ||
| 1520 | let ctx = TestContext::new(client); | ||
| 1521 | |||
| 1522 | // Setup: wrong commit already pushed (we'll use the same setup, but publish PR first) | ||
| 1523 | let setup = match setup_repo_with_wrong_commit_pushed(&ctx, relay_domain).await { | ||
| 1524 | Ok(s) => s, | ||
| 1212 | Err(e) => { | 1525 | Err(e) => { |
| 1213 | return TestResult::new( | 1526 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); |
| 1214 | test_name, | ||
| 1215 | "GRASP-01", | ||
| 1216 | "Push to refs/nostr/<invalid-event-id> rejected", | ||
| 1217 | ) | ||
| 1218 | .fail(&e); | ||
| 1219 | } | 1527 | } |
| 1220 | }; | 1528 | }; |
| 1221 | let cleanup = || { | 1529 | |
| 1222 | let _ = fs::remove_dir_all(&clone_path); | 1530 | // Publish PR event FIRST (before our test push) |
| 1531 | if let Err(e) = publish_pr_event_and_wait(&ctx).await { | ||
| 1532 | setup.cleanup(); | ||
| 1533 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | ||
| 1534 | } | ||
| 1535 | |||
| 1536 | // Try to push again with wrong commit (should be rejected now that PR event exists) | ||
| 1537 | let push_succeeded = match push_to_pr_ref(&setup.clone_path, &setup.pr_event_id) { | ||
| 1538 | Ok(success) => success, | ||
| 1539 | Err(e) => { | ||
| 1540 | setup.cleanup(); | ||
| 1541 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | ||
| 1542 | } | ||
| 1223 | }; | 1543 | }; |
| 1224 | 1544 | ||
| 1225 | // Create a unique commit | 1545 | setup.cleanup(); |
| 1226 | if let Err(e) = create_commit(&clone_path, "Test commit for invalid refs/nostr push") { | 1546 | |
| 1227 | cleanup(); | 1547 | // Should REJECT - PR event exists with different commit hash |
| 1228 | return TestResult::new( | 1548 | if push_succeeded { |
| 1229 | test_name, | 1549 | return TestResult::new(test_name, "GRASP-01", desc) |
| 1230 | "GRASP-01", | 1550 | .fail("Push accepted (expected rejection due to commit hash mismatch)"); |
| 1231 | "Push to refs/nostr/<invalid-event-id> rejected", | ||
| 1232 | ) | ||
| 1233 | .fail(&e); | ||
| 1234 | } | 1551 | } |
| 1235 | 1552 | ||
| 1236 | // Use an invalid event-id (too short, not a valid 64-char hex) | 1553 | TestResult::new(test_name, "GRASP-01", desc).pass() |
| 1237 | let invalid_event_id = "123"; | 1554 | } |
| 1238 | let ref_name = format!("refs/nostr/{}", invalid_event_id); | ||
| 1239 | 1555 | ||
| 1240 | // ============================================================ | 1556 | /// Test 4: Push correct commit to refs/nostr/<pr-event-id> AFTER PR event exists |
| 1241 | // Step 3: VERIFY - Push should be rejected with invalid event-id format | 1557 | /// |
| 1242 | // ============================================================ | 1558 | /// This test verifies that the relay accepts pushes to refs/nostr/<event-id> |
| 1243 | let push_result = try_push_to_ref(&clone_path, &ref_name); | 1559 | /// when a corresponding event exists AND the pushed commit matches |
| 1244 | cleanup(); | 1560 | /// the commit in the PR event's `c` tag. |
| 1561 | /// | ||
| 1562 | /// Depends on: `setup_repo_with_wrong_commit_pushed` for setup, then resets to correct commit | ||
| 1563 | pub async fn test_push_to_nostr_ref_with_correct_commit_after_event_received_accepted( | ||
| 1564 | client: &AuditClient, | ||
| 1565 | relay_domain: &str, | ||
| 1566 | ) -> TestResult { | ||
| 1567 | let test_name = "test_push_to_nostr_ref_with_correct_commit_after_event_received_accepted"; | ||
| 1568 | let desc = "Push correct commit to refs/nostr/<pr-event-id> after PR event (should accept)"; | ||
| 1569 | let ctx = TestContext::new(client); | ||
| 1245 | 1570 | ||
| 1246 | match push_result { | 1571 | // Setup: wrong commit already pushed |
| 1247 | Ok(false) => TestResult::new( | 1572 | let setup = match setup_repo_with_wrong_commit_pushed(&ctx, relay_domain).await { |
| 1248 | test_name, | 1573 | Ok(s) => s, |
| 1249 | "GRASP-01", | 1574 | Err(e) => { |
| 1250 | "Push to refs/nostr/<invalid-event-id> rejected", | 1575 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); |
| 1251 | ) | 1576 | } |
| 1252 | .pass(), | 1577 | }; |
| 1253 | Ok(true) => TestResult::new( | 1578 | |
| 1254 | test_name, | 1579 | // Publish PR event FIRST |
| 1255 | "GRASP-01", | 1580 | if let Err(e) = publish_pr_event_and_wait(&ctx).await { |
| 1256 | "Push to refs/nostr/<invalid-event-id> rejected", | 1581 | setup.cleanup(); |
| 1257 | ) | 1582 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); |
| 1258 | .fail(&format!( | 1583 | } |
| 1259 | "Push to {} was accepted but should be rejected. \ | 1584 | |
| 1260 | The event-id '{}' is NOT a valid 64-character hex string (EventId format). \ | 1585 | // Reset to CORRECT commit (the one expected by PR event) |
| 1261 | The relay should reject pushes to refs/nostr/ with invalid event-id format.", | 1586 | if let Err(e) = reset_to_correct_pr_commit(&setup.clone_path) { |
| 1262 | ref_name, invalid_event_id | 1587 | setup.cleanup(); |
| 1263 | )), | 1588 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); |
| 1264 | Err(e) => TestResult::new( | 1589 | } |
| 1265 | test_name, | 1590 | |
| 1266 | "GRASP-01", | 1591 | // Push correct commit (should succeed) |
| 1267 | "Push to refs/nostr/<invalid-event-id> rejected", | 1592 | let push_succeeded = match push_to_pr_ref(&setup.clone_path, &setup.pr_event_id) { |
| 1268 | ) | 1593 | Ok(success) => success, |
| 1269 | .fail(&format!("Push error: {}", e)), | 1594 | Err(e) => { |
| 1595 | setup.cleanup(); | ||
| 1596 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | ||
| 1597 | } | ||
| 1598 | }; | ||
| 1599 | |||
| 1600 | setup.cleanup(); | ||
| 1601 | |||
| 1602 | // Should ACCEPT - commit matches PR event's c tag | ||
| 1603 | if !push_succeeded { | ||
| 1604 | return TestResult::new(test_name, "GRASP-01", desc) | ||
| 1605 | .fail("Push rejected (expected acceptance since commit matches PR event)"); | ||
| 1270 | } | 1606 | } |
| 1607 | |||
| 1608 | TestResult::new(test_name, "GRASP-01", desc).pass() | ||
| 1271 | } | 1609 | } |
| 1272 | } | 1610 | } |
| 1273 | 1611 | ||
| 1274 | #[cfg(test)] | 1612 | #[cfg(test)] |
| 1275 | mod tests { | 1613 | mod tests { |
| 1614 | use super::*; | ||
| 1615 | |||
| 1276 | #[test] | 1616 | #[test] |
| 1277 | fn test_module_exists() { | 1617 | fn test_module_exists() { |
| 1278 | assert!(true); | 1618 | assert!(true); |
| 1279 | } | 1619 | } |
| 1620 | |||
| 1621 | /// Test to discover the PR test commit hash | ||
| 1622 | /// | ||
| 1623 | /// This test creates a deterministic commit with PR-specific parameters | ||
| 1624 | /// and prints out the hash value. Once discovered, update PR_TEST_COMMIT_HASH. | ||
| 1625 | /// | ||
| 1626 | /// Run with: cd grasp-audit && nix develop -c cargo test --lib test_pr_test_commit_hash_discovery -- --nocapture | ||
| 1627 | #[test] | ||
| 1628 | fn test_pr_test_commit_hash_discovery() { | ||
| 1629 | use std::process::Command; | ||
| 1630 | use tempfile::TempDir; | ||
| 1631 | use std::fs; | ||
| 1632 | |||
| 1633 | let temp_dir = TempDir::new().expect("Failed to create temp dir"); | ||
| 1634 | let path = temp_dir.path(); | ||
| 1635 | |||
| 1636 | // Initialize git repo | ||
| 1637 | let output = Command::new("git") | ||
| 1638 | .args(["init"]) | ||
| 1639 | .current_dir(path) | ||
| 1640 | .output() | ||
| 1641 | .expect("Failed to init git"); | ||
| 1642 | assert!(output.status.success(), "git init failed: {:?}", String::from_utf8_lossy(&output.stderr)); | ||
| 1643 | |||
| 1644 | // Configure git user - use PR Test Author identity | ||
| 1645 | let output = Command::new("git") | ||
| 1646 | .args(["config", "user.email", "pr-test@example.com"]) | ||
| 1647 | .current_dir(path) | ||
| 1648 | .output() | ||
| 1649 | .expect("git config email failed"); | ||
| 1650 | assert!(output.status.success(), "git config email failed"); | ||
| 1651 | |||
| 1652 | let output = Command::new("git") | ||
| 1653 | .args(["config", "user.name", "PR Test Author"]) | ||
| 1654 | .current_dir(path) | ||
| 1655 | .output() | ||
| 1656 | .expect("git config name failed"); | ||
| 1657 | assert!(output.status.success(), "git config name failed"); | ||
| 1658 | |||
| 1659 | // Create the deterministic file content | ||
| 1660 | let test_file = path.join("test.txt"); | ||
| 1661 | fs::write(&test_file, "PR test deterministic commit").expect("Failed to write test file"); | ||
| 1662 | |||
| 1663 | // Add the file | ||
| 1664 | let output = Command::new("git") | ||
| 1665 | .args(["add", "test.txt"]) | ||
| 1666 | .current_dir(path) | ||
| 1667 | .output() | ||
| 1668 | .expect("git add failed"); | ||
| 1669 | assert!(output.status.success(), "git add failed: {:?}", String::from_utf8_lossy(&output.stderr)); | ||
| 1670 | |||
| 1671 | // Create deterministic commit with fixed dates and GPG disabled | ||
| 1672 | let output = Command::new("git") | ||
| 1673 | .args([ | ||
| 1674 | "-c", "commit.gpgsign=false", | ||
| 1675 | "commit", | ||
| 1676 | "-m", "PR test deterministic commit", | ||
| 1677 | ]) | ||
| 1678 | .env("GIT_AUTHOR_DATE", "2024-01-01T00:00:00Z") | ||
| 1679 | .env("GIT_COMMITTER_DATE", "2024-01-01T00:00:00Z") | ||
| 1680 | .current_dir(path) | ||
| 1681 | .output() | ||
| 1682 | .expect("git commit failed"); | ||
| 1683 | assert!(output.status.success(), "git commit failed: {:?}", String::from_utf8_lossy(&output.stderr)); | ||
| 1684 | |||
| 1685 | // Get the commit hash | ||
| 1686 | let output = Command::new("git") | ||
| 1687 | .args(["rev-parse", "HEAD"]) | ||
| 1688 | .current_dir(path) | ||
| 1689 | .output() | ||
| 1690 | .expect("git rev-parse failed"); | ||
| 1691 | assert!(output.status.success(), "git rev-parse failed: {:?}", String::from_utf8_lossy(&output.stderr)); | ||
| 1692 | |||
| 1693 | let hash = String::from_utf8_lossy(&output.stdout).trim().to_string(); | ||
| 1694 | |||
| 1695 | println!("\n========================================"); | ||
| 1696 | println!("PR_TEST_COMMIT_HASH should be: {}", hash); | ||
| 1697 | println!("========================================\n"); | ||
| 1698 | |||
| 1699 | // Verify we got a valid 40-character hex hash | ||
| 1700 | assert_eq!(hash.len(), 40, "Hash should be 40 hex chars, got: {}", hash); | ||
| 1701 | assert!(hash.chars().all(|c| c.is_ascii_hexdigit()), "Hash should be hex chars only"); | ||
| 1702 | |||
| 1703 | // If the constant is not PLACEHOLDER, verify it matches | ||
| 1704 | if PR_TEST_COMMIT_HASH != "PLACEHOLDER" { | ||
| 1705 | assert_eq!( | ||
| 1706 | hash, PR_TEST_COMMIT_HASH, | ||
| 1707 | "Commit hash mismatch! Expected {}, got {}. Update PR_TEST_COMMIT_HASH if commit parameters changed.", | ||
| 1708 | PR_TEST_COMMIT_HASH, hash | ||
| 1709 | ); | ||
| 1710 | } | ||
| 1711 | } | ||
| 1280 | } \ No newline at end of file | 1712 | } \ No newline at end of file |