diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-01 22:13:37 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-01 22:13:37 +0000 |
| commit | 11870a0f810accf0431d82a74b6fd3adec9d23df (patch) | |
| tree | 82a61995b1a18b4a618544b159e886d5c2e95015 /grasp-audit/src | |
| parent | 883774c7785c57bded21ba3be63d0fdac8a51dcf (diff) | |
better fixtures: refs/nostr tests
Diffstat (limited to 'grasp-audit/src')
| -rw-r--r-- | grasp-audit/src/fixtures.rs | 225 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/push_authorization.rs | 331 |
2 files changed, 373 insertions, 183 deletions
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index 5e8c50a..d054e36 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs | |||
| @@ -188,6 +188,50 @@ pub enum FixtureKind { | |||
| 188 | /// - Timestamp: 1 second in the past | 188 | /// - Timestamp: 1 second in the past |
| 189 | PREvent, | 189 | PREvent, |
| 190 | 190 | ||
| 191 | /// PR event generated (built) but NOT sent to relay | ||
| 192 | /// | ||
| 193 | /// This is a "Generated" stage fixture - the event is created but not published. | ||
| 194 | /// Useful for tests that need the PR event ID before the event exists on the relay. | ||
| 195 | /// | ||
| 196 | /// - Requires ValidRepo (uses same repo_id) | ||
| 197 | /// - Signed by `client.pr_author_keys()` | ||
| 198 | /// - Kind 1618 (NIP-34 PR) | ||
| 199 | /// - Includes `c` tag pointing to PR_TEST_COMMIT_HASH | ||
| 200 | /// - NOT sent to relay (use `client.send_event()` to publish when ready) | ||
| 201 | PREventGenerated, | ||
| 202 | |||
| 203 | /// Wrong commit pushed to refs/nostr/<pr-event-id> BEFORE PR event is sent | ||
| 204 | /// | ||
| 205 | /// This is a "DataPushed" stage fixture for testing pre-event ref behavior. | ||
| 206 | /// The server has refs/nostr/<pr-event-id> pointing to DETERMINISTIC_COMMIT_HASH | ||
| 207 | /// (the "wrong" commit), but no PR event exists yet on the relay. | ||
| 208 | /// | ||
| 209 | /// Server state after this fixture: | ||
| 210 | /// - ValidRepo announcement on relay | ||
| 211 | /// - refs/nostr/<pr-event-id> exists on git server with wrong commit | ||
| 212 | /// - PR event is NOT on relay (but returned for tests to publish later) | ||
| 213 | /// | ||
| 214 | /// - Requires PREventGenerated (for the event ID) | ||
| 215 | /// - Clones repo, creates wrong commit, pushes to refs/nostr/<event-id> | ||
| 216 | /// - Returns: the unsent PR event (tests can publish it later) | ||
| 217 | PRWrongCommitPushedBeforeEvent, | ||
| 218 | |||
| 219 | /// PR event sent to relay AFTER wrong commit was pushed to refs/nostr/<pr-event-id> | ||
| 220 | /// | ||
| 221 | /// This is a compound fixture testing post-event behavior. | ||
| 222 | /// The server had refs/nostr/<pr-event-id> pointing to wrong commit, | ||
| 223 | /// then the PR event was published (which may trigger cleanup). | ||
| 224 | /// | ||
| 225 | /// Server state after this fixture: | ||
| 226 | /// - ValidRepo announcement on relay | ||
| 227 | /// - PR event is on relay | ||
| 228 | /// - refs/nostr/<pr-event-id> may have been cleaned up (that's what tests verify) | ||
| 229 | /// | ||
| 230 | /// - Requires PRWrongCommitPushedBeforeEvent | ||
| 231 | /// - Sends the PR event to relay | ||
| 232 | /// - Returns: the sent PR event | ||
| 233 | PREventSentAfterWrongPush, | ||
| 234 | |||
| 191 | /// Owner's state event with git data successfully pushed (full 4-stage fixture) | 235 | /// Owner's state event with git data successfully pushed (full 4-stage fixture) |
| 192 | /// | 236 | /// |
| 193 | /// This fixture represents the complete flow for testing push authorization: | 237 | /// This fixture represents the complete flow for testing push authorization: |
| @@ -268,6 +312,9 @@ impl FixtureKind { | |||
| 268 | Self::RecursiveMaintainerState => vec![Self::ValidRepo], | 312 | Self::RecursiveMaintainerState => vec![Self::ValidRepo], |
| 269 | Self::RecursiveMaintainerRepoAndState => vec![Self::ValidRepo], | 313 | Self::RecursiveMaintainerRepoAndState => vec![Self::ValidRepo], |
| 270 | Self::PREvent => vec![Self::ValidRepo], | 314 | Self::PREvent => vec![Self::ValidRepo], |
| 315 | Self::PREventGenerated => vec![Self::ValidRepo], | ||
| 316 | Self::PRWrongCommitPushedBeforeEvent => vec![Self::PREventGenerated], | ||
| 317 | Self::PREventSentAfterWrongPush => vec![Self::PRWrongCommitPushedBeforeEvent], | ||
| 271 | Self::OwnerStateDataPushed => vec![Self::ValidRepo], | 318 | Self::OwnerStateDataPushed => vec![Self::ValidRepo], |
| 272 | 319 | ||
| 273 | // Fixtures that depend on RepoWithIssue | 320 | // Fixtures that depend on RepoWithIssue |
| @@ -296,6 +343,12 @@ impl FixtureKind { | |||
| 296 | Self::RecursiveMaintainerStateDataPushed => true, | 343 | Self::RecursiveMaintainerStateDataPushed => true, |
| 297 | // RecursiveMaintainerRepoAndState sends multiple events internally | 344 | // RecursiveMaintainerRepoAndState sends multiple events internally |
| 298 | Self::RecursiveMaintainerRepoAndState => true, | 345 | Self::RecursiveMaintainerRepoAndState => true, |
| 346 | // PREventGenerated builds but does NOT send the PR event (that's the point) | ||
| 347 | Self::PREventGenerated => true, | ||
| 348 | // PRWrongCommitPushedBeforeEvent pushes git data but doesn't send event | ||
| 349 | Self::PRWrongCommitPushedBeforeEvent => true, | ||
| 350 | // PREventSentAfterWrongPush sends the PR event internally | ||
| 351 | Self::PREventSentAfterWrongPush => true, | ||
| 299 | // All other fixtures return a single event for the caller to send | 352 | // All other fixtures return a single event for the caller to send |
| 300 | _ => false, | 353 | _ => false, |
| 301 | } | 354 | } |
| @@ -753,6 +806,57 @@ impl<'a> TestContext<'a> { | |||
| 753 | .map_err(|e| anyhow::anyhow!("Failed to build PR event: {}", e)) | 806 | .map_err(|e| anyhow::anyhow!("Failed to build PR event: {}", e)) |
| 754 | } | 807 | } |
| 755 | 808 | ||
| 809 | FixtureKind::PREventGenerated => { | ||
| 810 | // Same as PREvent but will NOT be sent to relay (caller may send it later) | ||
| 811 | // This fixture is for "Generated" stage only | ||
| 812 | use nostr_sdk::prelude::*; | ||
| 813 | |||
| 814 | // ValidRepo is ensured by ensure_fixture before this is called | ||
| 815 | let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; | ||
| 816 | |||
| 817 | let repo_id = repo | ||
| 818 | .tags | ||
| 819 | .iter() | ||
| 820 | .find(|t| t.kind() == TagKind::d()) | ||
| 821 | .and_then(|t| t.content()) | ||
| 822 | .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepo fixture"))? | ||
| 823 | .to_string(); | ||
| 824 | |||
| 825 | // Create PR event 1 second in the past | ||
| 826 | let base_time = Timestamp::now().as_u64(); | ||
| 827 | let pr_timestamp = Timestamp::from(base_time - 1); | ||
| 828 | |||
| 829 | // Build NIP-34 PR event (kind 1618) | ||
| 830 | self.client | ||
| 831 | .event_builder( | ||
| 832 | Kind::Custom(1618), // NIP-34 PR kind (has 'c' tag for commit) | ||
| 833 | "Test PR for GRASP validation", | ||
| 834 | ) | ||
| 835 | .tag(Tag::custom( | ||
| 836 | TagKind::custom("a"), | ||
| 837 | vec![format!( | ||
| 838 | "30617:{}:{}", | ||
| 839 | self.client.public_key().to_hex(), // Owner pubkey | ||
| 840 | repo_id | ||
| 841 | )], | ||
| 842 | )) | ||
| 843 | .tag(Tag::custom( | ||
| 844 | TagKind::custom("c"), | ||
| 845 | vec![PR_TEST_COMMIT_HASH.to_string()], | ||
| 846 | )) | ||
| 847 | .custom_time(pr_timestamp) | ||
| 848 | .build(self.client.pr_author_keys()) | ||
| 849 | .map_err(|e| anyhow::anyhow!("Failed to build PR event: {}", e)) | ||
| 850 | } | ||
| 851 | |||
| 852 | FixtureKind::PRWrongCommitPushedBeforeEvent => { | ||
| 853 | self.build_pr_wrong_commit_pushed_before_event().await | ||
| 854 | } | ||
| 855 | |||
| 856 | FixtureKind::PREventSentAfterWrongPush => { | ||
| 857 | self.build_pr_event_sent_after_wrong_push().await | ||
| 858 | } | ||
| 859 | |||
| 756 | FixtureKind::OwnerStateDataPushed => { | 860 | FixtureKind::OwnerStateDataPushed => { |
| 757 | self.build_owner_state_data_pushed().await | 861 | self.build_owner_state_data_pushed().await |
| 758 | } | 862 | } |
| @@ -1370,6 +1474,127 @@ impl<'a> TestContext<'a> { | |||
| 1370 | } | 1474 | } |
| 1371 | } | 1475 | } |
| 1372 | 1476 | ||
| 1477 | /// Build PRWrongCommitPushedBeforeEvent fixture | ||
| 1478 | /// | ||
| 1479 | /// This fixture sets up a scenario where: | ||
| 1480 | /// 1. A repo exists on the relay | ||
| 1481 | /// 2. A PR event is generated (but NOT sent to relay) | ||
| 1482 | /// 3. A wrong commit is pushed to refs/nostr/<pr-event-id> | ||
| 1483 | /// | ||
| 1484 | /// Server state after: | ||
| 1485 | /// - ValidRepo announcement on relay | ||
| 1486 | /// - refs/nostr/<pr-event-id> on git server pointing to DETERMINISTIC_COMMIT_HASH (wrong) | ||
| 1487 | /// - NO PR event on relay | ||
| 1488 | /// | ||
| 1489 | /// Returns: the unsent PR event (tests can publish it later) | ||
| 1490 | async fn build_pr_wrong_commit_pushed_before_event(&self) -> Result<Event> { | ||
| 1491 | use nostr_sdk::prelude::*; | ||
| 1492 | |||
| 1493 | // Get the cached PREventGenerated (the unsent PR event) | ||
| 1494 | let pr_event = self.get_cached_dependency(FixtureKind::PREventGenerated)?; | ||
| 1495 | let pr_event_id = pr_event.id.to_hex(); | ||
| 1496 | |||
| 1497 | // Get the ValidRepo to extract repo info | ||
| 1498 | let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; | ||
| 1499 | let repo_id = self.extract_repo_id(&repo)?; | ||
| 1500 | |||
| 1501 | // Get relay domain for cloning | ||
| 1502 | let relay_domain = self.get_relay_domain().await?; | ||
| 1503 | |||
| 1504 | // Owner npub for clone URL | ||
| 1505 | let npub = repo | ||
| 1506 | .pubkey | ||
| 1507 | .to_bech32() | ||
| 1508 | .map_err(|e| anyhow::anyhow!("Failed to convert pubkey to bech32: {}", e))?; | ||
| 1509 | |||
| 1510 | // Clone the repository (fresh clone - local repos are never cached) | ||
| 1511 | let clone_path = clone_repo(&relay_domain, &npub, &repo_id) | ||
| 1512 | .map_err(|e| anyhow::anyhow!("Failed to clone repo: {}", e))?; | ||
| 1513 | |||
| 1514 | // Cleanup helper | ||
| 1515 | let cleanup = |path: &PathBuf| { | ||
| 1516 | let _ = fs::remove_dir_all(path); | ||
| 1517 | }; | ||
| 1518 | |||
| 1519 | // Create a WRONG commit (Owner variant, not PRTestCommit) | ||
| 1520 | // This commit hash will NOT match what's in the PR event's `c` tag | ||
| 1521 | let wrong_commit_hash = match create_deterministic_commit_with_variant( | ||
| 1522 | &clone_path, | ||
| 1523 | CommitVariant::Owner, | ||
| 1524 | ) { | ||
| 1525 | Ok(h) => h, | ||
| 1526 | Err(e) => { | ||
| 1527 | cleanup(&clone_path); | ||
| 1528 | return Err(anyhow::anyhow!("Failed to create wrong commit: {}", e)); | ||
| 1529 | } | ||
| 1530 | }; | ||
| 1531 | |||
| 1532 | // Verify it's actually different from expected PR commit | ||
| 1533 | if wrong_commit_hash == PR_TEST_COMMIT_HASH { | ||
| 1534 | cleanup(&clone_path); | ||
| 1535 | return Err(anyhow::anyhow!( | ||
| 1536 | "Test setup error: wrong_commit_hash {} equals PR_TEST_COMMIT_HASH", | ||
| 1537 | wrong_commit_hash | ||
| 1538 | )); | ||
| 1539 | } | ||
| 1540 | |||
| 1541 | // Create master branch if needed and push to refs/nostr/<pr-event-id> | ||
| 1542 | let _ = Command::new("git") | ||
| 1543 | .args(["branch", "-M", "master"]) | ||
| 1544 | .current_dir(&clone_path) | ||
| 1545 | .output(); | ||
| 1546 | |||
| 1547 | let push_output = Command::new("git") | ||
| 1548 | .args([ | ||
| 1549 | "push", | ||
| 1550 | "origin", | ||
| 1551 | &format!("master:refs/nostr/{}", pr_event_id), | ||
| 1552 | ]) | ||
| 1553 | .current_dir(&clone_path) | ||
| 1554 | .output() | ||
| 1555 | .map_err(|e| { | ||
| 1556 | cleanup(&clone_path); | ||
| 1557 | anyhow::anyhow!("Failed to execute git push: {}", e) | ||
| 1558 | })?; | ||
| 1559 | |||
| 1560 | cleanup(&clone_path); | ||
| 1561 | |||
| 1562 | if !push_output.status.success() { | ||
| 1563 | let stderr = String::from_utf8_lossy(&push_output.stderr); | ||
| 1564 | return Err(anyhow::anyhow!( | ||
| 1565 | "Initial push to refs/nostr/{} failed (expected success before PR event exists): {}", | ||
| 1566 | pr_event_id, | ||
| 1567 | stderr | ||
| 1568 | )); | ||
| 1569 | } | ||
| 1570 | |||
| 1571 | // Return the unsent PR event (tests can publish it later) | ||
| 1572 | Ok(pr_event) | ||
| 1573 | } | ||
| 1574 | |||
| 1575 | /// Build PREventSentAfterWrongPush fixture | ||
| 1576 | /// | ||
| 1577 | /// This fixture builds on PRWrongCommitPushedBeforeEvent by sending the PR event. | ||
| 1578 | /// After this fixture, the relay has: | ||
| 1579 | /// - ValidRepo announcement | ||
| 1580 | /// - PR event | ||
| 1581 | /// - refs/nostr/<pr-event-id> may have been cleaned up (that's what tests verify) | ||
| 1582 | /// | ||
| 1583 | /// Returns: the sent PR event | ||
| 1584 | async fn build_pr_event_sent_after_wrong_push(&self) -> Result<Event> { | ||
| 1585 | // Get the PR event that was cached by PRWrongCommitPushedBeforeEvent | ||
| 1586 | let pr_event = self.get_cached_dependency(FixtureKind::PRWrongCommitPushedBeforeEvent)?; | ||
| 1587 | |||
| 1588 | // Send the PR event to relay | ||
| 1589 | self.client.send_event(pr_event.clone()).await?; | ||
| 1590 | |||
| 1591 | // Wait for relay to process | ||
| 1592 | tokio::time::sleep(std::time::Duration::from_millis(500)).await; | ||
| 1593 | |||
| 1594 | // Return the now-sent PR event | ||
| 1595 | Ok(pr_event) | ||
| 1596 | } | ||
| 1597 | |||
| 1373 | /// Get relay domain (host:port) from the connected relay | 1598 | /// Get relay domain (host:port) from the connected relay |
| 1374 | /// | 1599 | /// |
| 1375 | /// Extracts the domain from the relay URL for git HTTP operations. | 1600 | /// Extracts the domain from the relay URL for git HTTP operations. |
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index 36abd30..0119ab3 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs | |||
| @@ -32,9 +32,8 @@ | |||
| 32 | const PR_TEST_COMMIT_HASH: &str = "5d40fb1555a0c28bf4d650515a73aaa54d4d9bfb"; | 32 | const PR_TEST_COMMIT_HASH: &str = "5d40fb1555a0c28bf4d650515a73aaa54d4d9bfb"; |
| 33 | 33 | ||
| 34 | use crate::{ | 34 | use crate::{ |
| 35 | clone_repo, create_commit, create_deterministic_commit, | 35 | clone_repo, create_commit, create_deterministic_commit_with_variant, try_push, try_push_to_ref, |
| 36 | create_deterministic_commit_with_variant, try_push, try_push_to_ref, AuditClient, | 36 | AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, |
| 37 | CommitVariant, FixtureKind, TestContext, TestResult, DETERMINISTIC_COMMIT_HASH, | ||
| 38 | MAINTAINER_DETERMINISTIC_COMMIT_HASH, | 37 | MAINTAINER_DETERMINISTIC_COMMIT_HASH, |
| 39 | }; | 38 | }; |
| 40 | use nostr_sdk::prelude::*; | 39 | use nostr_sdk::prelude::*; |
| @@ -238,132 +237,9 @@ async fn setup_pr_test_repo( | |||
| 238 | } | 237 | } |
| 239 | 238 | ||
| 240 | // ============================================================ | 239 | // ============================================================ |
| 241 | // PR Ref Push Test Setup Helpers - Minimize Test Duplication | 240 | // PR Ref Push Test Helpers |
| 242 | // ============================================================ | 241 | // ============================================================ |
| 243 | 242 | ||
| 244 | /// Result of setting up a repo with a wrong commit pushed before PR event exists. | ||
| 245 | /// Used as shared setup for tests 3, 4, 5 which all depend on this scenario. | ||
| 246 | #[allow(dead_code)] | ||
| 247 | struct PrRefTestSetup { | ||
| 248 | clone_path: PathBuf, | ||
| 249 | pr_event_id: String, | ||
| 250 | repo_id: String, | ||
| 251 | owner_npub: String, | ||
| 252 | wrong_commit_hash: String, | ||
| 253 | /// The unpublished PR event - store it so we can publish the SAME event later | ||
| 254 | pr_event: Event, | ||
| 255 | } | ||
| 256 | |||
| 257 | impl PrRefTestSetup { | ||
| 258 | fn cleanup(&self) { | ||
| 259 | let _ = std::fs::remove_dir_all(&self.clone_path); | ||
| 260 | } | ||
| 261 | } | ||
| 262 | |||
| 263 | /// Sets up a repo and pushes a WRONG commit to refs/nostr/<pr-event-id> BEFORE PR event exists. | ||
| 264 | /// | ||
| 265 | /// This is the shared setup for PR ref lifecycle tests: | ||
| 266 | /// - Creates repo (gets PREvent fixture for event-id but doesn't publish yet) | ||
| 267 | /// - Clones repo | ||
| 268 | /// - Creates a commit that does NOT match PR_TEST_COMMIT_HASH | ||
| 269 | /// - Pushes to refs/nostr/<pr-event-id> (should succeed - no event to validate against) | ||
| 270 | /// | ||
| 271 | /// Tests using this setup: | ||
| 272 | /// - test_pr_push_to_nostr_ref_with_wrong_commit_accepted_before_event_received: verify initial push accepted | ||
| 273 | /// - test_pr_event_published_removes_nostr_ref_at_incorrect_commit: publish event, verify cleanup | ||
| 274 | /// - test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected: publish event, try push wrong commit | ||
| 275 | /// - test_push_to_nostr_ref_with_correct_commit_after_event_received_accepted: publish event, push correct commit | ||
| 276 | #[allow(dead_code)] | ||
| 277 | async fn setup_repo_with_wrong_commit_pushed( | ||
| 278 | ctx: &TestContext<'_>, | ||
| 279 | relay_domain: &str, | ||
| 280 | ) -> Result<PrRefTestSetup, String> { | ||
| 281 | // Get ValidRepo fixture (publishes repo announcement to relay) | ||
| 282 | let repo_event = ctx | ||
| 283 | .get_fixture(FixtureKind::ValidRepo) | ||
| 284 | .await | ||
| 285 | .map_err(|e| format!("Failed to get repo announcement: {}", e))?; | ||
| 286 | |||
| 287 | // Build PR event WITHOUT publishing - we need its ID before the event exists on relay | ||
| 288 | // This allows testing refs/nostr/<event-id> push behavior before the event is received | ||
| 289 | let pr_event = ctx | ||
| 290 | .build_fixture_only(FixtureKind::PREvent) | ||
| 291 | .await | ||
| 292 | .map_err(|e| format!("Failed to build PR event fixture: {}", e))?; | ||
| 293 | |||
| 294 | let repo_id = repo_event | ||
| 295 | .tags | ||
| 296 | .iter() | ||
| 297 | .find(|t| t.kind() == TagKind::d()) | ||
| 298 | .and_then(|t| t.content()) | ||
| 299 | .ok_or("No repo identifier in announcement")? | ||
| 300 | .to_string(); | ||
| 301 | |||
| 302 | let owner_npub = repo_event.pubkey.to_bech32().map_err(|e| e.to_string())?; | ||
| 303 | let pr_event_id = pr_event.id.to_hex(); | ||
| 304 | |||
| 305 | // Clone the repository | ||
| 306 | let clone_path = clone_repo(relay_domain, &owner_npub, &repo_id)?; | ||
| 307 | |||
| 308 | // Create a WRONG commit (not the one expected by PR event) | ||
| 309 | let wrong_commit_hash = | ||
| 310 | create_deterministic_commit_with_variant(&clone_path, CommitVariant::Owner)?; | ||
| 311 | |||
| 312 | // Verify it's actually different from expected | ||
| 313 | if wrong_commit_hash == PR_TEST_COMMIT_HASH { | ||
| 314 | let _ = std::fs::remove_dir_all(&clone_path); | ||
| 315 | return Err("Test setup error: wrong_commit_hash equals PR_TEST_COMMIT_HASH".to_string()); | ||
| 316 | } | ||
| 317 | |||
| 318 | // Push to refs/nostr/<pr-event-id> (no event published yet, should succeed) | ||
| 319 | let push_output = Command::new("git") | ||
| 320 | .args([ | ||
| 321 | "push", | ||
| 322 | "origin", | ||
| 323 | &format!("master:refs/nostr/{}", pr_event_id), | ||
| 324 | ]) | ||
| 325 | .current_dir(&clone_path) | ||
| 326 | .output() | ||
| 327 | .map_err(|e| format!("Failed to execute git push: {}", e))?; | ||
| 328 | |||
| 329 | if !push_output.status.success() { | ||
| 330 | let stderr = String::from_utf8_lossy(&push_output.stderr); | ||
| 331 | let _ = std::fs::remove_dir_all(&clone_path); | ||
| 332 | return Err(format!( | ||
| 333 | "Initial push failed (expected success before PR event): {}", | ||
| 334 | stderr | ||
| 335 | )); | ||
| 336 | } | ||
| 337 | |||
| 338 | Ok(PrRefTestSetup { | ||
| 339 | clone_path, | ||
| 340 | pr_event_id, | ||
| 341 | repo_id, | ||
| 342 | owner_npub, | ||
| 343 | wrong_commit_hash, | ||
| 344 | pr_event, | ||
| 345 | }) | ||
| 346 | } | ||
| 347 | |||
| 348 | /// Publishes the SAME PR event that was built during setup. | ||
| 349 | /// Call this after setup_repo_with_wrong_commit_pushed to test post-event behavior. | ||
| 350 | /// | ||
| 351 | /// IMPORTANT: We must publish the EXACT same event that was used during setup, | ||
| 352 | /// otherwise the event ID won't match the refs/nostr/<event-id> ref that was pushed. | ||
| 353 | #[allow(dead_code)] | ||
| 354 | async fn publish_pr_event_and_wait(ctx: &TestContext<'_>, pr_event: &Event) -> Result<(), String> { | ||
| 355 | // Publish the exact same PR event that was created during setup | ||
| 356 | ctx.client() | ||
| 357 | .send_event(pr_event.clone()) | ||
| 358 | .await | ||
| 359 | .map_err(|e| format!("Failed to publish PR event: {}", e))?; | ||
| 360 | |||
| 361 | // Wait for relay to process | ||
| 362 | tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; | ||
| 363 | |||
| 364 | Ok(()) | ||
| 365 | } | ||
| 366 | |||
| 367 | /// Creates the correct PR test commit (matching PR_TEST_COMMIT_HASH) in an existing clone. | 243 | /// Creates the correct PR test commit (matching PR_TEST_COMMIT_HASH) in an existing clone. |
| 368 | /// Used after wrong commit was pushed to test pushing the correct commit. | 244 | /// Used after wrong commit was pushed to test pushing the correct commit. |
| 369 | #[allow(dead_code)] | 245 | #[allow(dead_code)] |
| @@ -1139,7 +1015,9 @@ impl PushAuthorizationTests { | |||
| 1139 | /// when no corresponding event exists yet. This is expected behavior because | 1015 | /// when no corresponding event exists yet. This is expected behavior because |
| 1140 | /// there's no validation event to check against. | 1016 | /// there's no validation event to check against. |
| 1141 | /// | 1017 | /// |
| 1142 | /// Uses `setup_repo_with_wrong_commit_pushed` helper which handles all setup. | 1018 | /// Uses `PRWrongCommitPushedBeforeEvent` fixture which handles all setup |
| 1019 | /// and verifies the push succeeded. | ||
| 1020 | #[allow(unused_variables)] // relay_domain is now handled by fixture | ||
| 1143 | pub async fn test_pr_push_to_nostr_ref_with_wrong_commit_accepted_before_event_received( | 1021 | pub async fn test_pr_push_to_nostr_ref_with_wrong_commit_accepted_before_event_received( |
| 1144 | client: &AuditClient, | 1022 | client: &AuditClient, |
| 1145 | relay_domain: &str, | 1023 | relay_domain: &str, |
| @@ -1149,19 +1027,15 @@ impl PushAuthorizationTests { | |||
| 1149 | let desc = "Push wrong commit to refs/nostr/<pr-event-id> before PR event (should accept)"; | 1027 | let desc = "Push wrong commit to refs/nostr/<pr-event-id> before PR event (should accept)"; |
| 1150 | let ctx = TestContext::new(client); | 1028 | let ctx = TestContext::new(client); |
| 1151 | 1029 | ||
| 1152 | // Setup includes: create repo, clone, create wrong commit, push to refs/nostr/<event-id> | 1030 | // The PRWrongCommitPushedBeforeEvent fixture handles: |
| 1153 | // The push happens BEFORE PR event is published, so should succeed | 1031 | // 1. Create repo announcement |
| 1154 | let setup = match setup_repo_with_wrong_commit_pushed(&ctx, relay_domain).await { | 1032 | // 2. Build PR event (but don't send it) |
| 1155 | Ok(s) => s, | 1033 | // 3. Clone repo, create wrong commit, push to refs/nostr/<event-id> |
| 1156 | Err(e) => { | 1034 | // If the push fails, the fixture will return an error |
| 1157 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | 1035 | match ctx.get_fixture(FixtureKind::PRWrongCommitPushedBeforeEvent).await { |
| 1158 | } | 1036 | Ok(_pr_event) => TestResult::new(test_name, "GRASP-01", desc).pass(), |
| 1159 | }; | 1037 | Err(e) => TestResult::new(test_name, "GRASP-01", desc).fail(format!("{}", e)), |
| 1160 | 1038 | } | |
| 1161 | // Setup already pushed and verified success - just cleanup and report pass | ||
| 1162 | setup.cleanup(); | ||
| 1163 | |||
| 1164 | TestResult::new(test_name, "GRASP-01", desc).pass() | ||
| 1165 | } | 1039 | } |
| 1166 | 1040 | ||
| 1167 | /// Test 2: After publishing PR event, verify that incorrect refs get cleaned up | 1041 | /// Test 2: After publishing PR event, verify that incorrect refs get cleaned up |
| @@ -1170,7 +1044,7 @@ impl PushAuthorizationTests { | |||
| 1170 | /// the relay should validate any existing refs/nostr/<event-id> refs and | 1044 | /// the relay should validate any existing refs/nostr/<event-id> refs and |
| 1171 | /// delete those that don't match the commit in the PR event's `c` tag. | 1045 | /// delete those that don't match the commit in the PR event's `c` tag. |
| 1172 | /// | 1046 | /// |
| 1173 | /// Depends on: `setup_repo_with_wrong_commit_pushed` (wrong commit already pushed) | 1047 | /// Uses `PREventSentAfterWrongPush` fixture which builds on the wrong push fixture. |
| 1174 | pub async fn test_pr_event_published_removes_nostr_ref_at_incorrect_commit( | 1048 | pub async fn test_pr_event_published_removes_nostr_ref_at_incorrect_commit( |
| 1175 | client: &AuditClient, | 1049 | client: &AuditClient, |
| 1176 | relay_domain: &str, | 1050 | relay_domain: &str, |
| @@ -1179,38 +1053,66 @@ impl PushAuthorizationTests { | |||
| 1179 | let desc = "Publishing PR event should trigger cleanup of incorrect refs"; | 1053 | let desc = "Publishing PR event should trigger cleanup of incorrect refs"; |
| 1180 | let ctx = TestContext::new(client); | 1054 | let ctx = TestContext::new(client); |
| 1181 | 1055 | ||
| 1182 | // Setup: wrong commit already pushed to refs/nostr/<pr-event-id> | 1056 | // Get fixture: wrong commit was pushed, then PR event was sent |
| 1183 | let setup = match setup_repo_with_wrong_commit_pushed(&ctx, relay_domain).await { | 1057 | let pr_event = match ctx.get_fixture(FixtureKind::PREventSentAfterWrongPush).await { |
| 1184 | Ok(s) => s, | 1058 | Ok(e) => e, |
| 1185 | Err(e) => { | 1059 | Err(e) => { |
| 1186 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | 1060 | return TestResult::new(test_name, "GRASP-01", desc).fail(format!("{}", e)); |
| 1187 | } | 1061 | } |
| 1188 | }; | 1062 | }; |
| 1189 | 1063 | ||
| 1190 | // NOW publish the PR event - this should trigger cleanup validation | 1064 | let pr_event_id = pr_event.id.to_hex(); |
| 1191 | if let Err(e) = publish_pr_event_and_wait(&ctx, &setup.pr_event).await { | 1065 | |
| 1192 | setup.cleanup(); | 1066 | // Get repo info for cloning (fresh clone for verification) |
| 1193 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | 1067 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { |
| 1194 | } | 1068 | Ok(r) => r, |
| 1069 | Err(e) => { | ||
| 1070 | return TestResult::new(test_name, "GRASP-01", desc).fail(format!("{}", e)); | ||
| 1071 | } | ||
| 1072 | }; | ||
| 1073 | |||
| 1074 | let repo_id = repo | ||
| 1075 | .tags | ||
| 1076 | .iter() | ||
| 1077 | .find(|t| t.kind() == TagKind::d()) | ||
| 1078 | .and_then(|t| t.content()) | ||
| 1079 | .unwrap_or("unknown") | ||
| 1080 | .to_string(); | ||
| 1081 | |||
| 1082 | let owner_npub = match repo.pubkey.to_bech32() { | ||
| 1083 | Ok(n) => n, | ||
| 1084 | Err(e) => { | ||
| 1085 | return TestResult::new(test_name, "GRASP-01", desc) | ||
| 1086 | .fail(format!("Failed to get owner npub: {}", e)); | ||
| 1087 | } | ||
| 1088 | }; | ||
| 1089 | |||
| 1090 | // Clone fresh for verification | ||
| 1091 | let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { | ||
| 1092 | Ok(p) => p, | ||
| 1093 | Err(e) => { | ||
| 1094 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | ||
| 1095 | } | ||
| 1096 | }; | ||
| 1195 | 1097 | ||
| 1196 | // Check if the incorrect ref was deleted | 1098 | // Check if the incorrect ref was deleted |
| 1197 | let ref_name = format!("refs/nostr/{}", setup.pr_event_id); | 1099 | let ref_name = format!("refs/nostr/{}", pr_event_id); |
| 1198 | let refs_exist = match ref_exists_on_remote(&setup.clone_path, &ref_name) { | 1100 | let refs_exist = match ref_exists_on_remote(&clone_path, &ref_name) { |
| 1199 | Ok(exists) => exists, | 1101 | Ok(exists) => exists, |
| 1200 | Err(e) => { | 1102 | Err(e) => { |
| 1201 | setup.cleanup(); | 1103 | let _ = fs::remove_dir_all(&clone_path); |
| 1202 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | 1104 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); |
| 1203 | } | 1105 | } |
| 1204 | }; | 1106 | }; |
| 1205 | 1107 | ||
| 1206 | setup.cleanup(); | 1108 | let _ = fs::remove_dir_all(&clone_path); |
| 1207 | 1109 | ||
| 1208 | // Ref should be deleted since the pushed commit doesn't match the PR event's `c` tag | 1110 | // Ref should be deleted since the pushed commit doesn't match the PR event's `c` tag |
| 1209 | if refs_exist { | 1111 | if refs_exist { |
| 1210 | TestResult::new(test_name, "GRASP-01", desc).fail(format!( | 1112 | TestResult::new(test_name, "GRASP-01", desc).fail(format!( |
| 1211 | "Expected refs/nostr/{} to be deleted when PR event published with non-matching commit, \ | 1113 | "Expected refs/nostr/{} to be deleted when PR event published with non-matching commit, \ |
| 1212 | but the ref still exists. The relay should delete refs that don't match the event's `c` tag.", | 1114 | but the ref still exists. The relay should delete refs that don't match the event's `c` tag.", |
| 1213 | setup.pr_event_id | 1115 | pr_event_id |
| 1214 | )) | 1116 | )) |
| 1215 | } else { | 1117 | } else { |
| 1216 | TestResult::new(test_name, "GRASP-01", desc).pass() | 1118 | TestResult::new(test_name, "GRASP-01", desc).pass() |
| @@ -1223,7 +1125,7 @@ impl PushAuthorizationTests { | |||
| 1223 | /// when a corresponding event exists but the pushed commit doesn't match | 1125 | /// when a corresponding event exists but the pushed commit doesn't match |
| 1224 | /// the commit in the PR event's `c` tag. | 1126 | /// the commit in the PR event's `c` tag. |
| 1225 | /// | 1127 | /// |
| 1226 | /// Depends on: `setup_repo_with_wrong_commit_pushed` for repo/clone setup, then publishes PR event | 1128 | /// Uses `PREventSentAfterWrongPush` fixture, then attempts to push wrong commit again. |
| 1227 | pub async fn test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected( | 1129 | pub async fn test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected( |
| 1228 | client: &AuditClient, | 1130 | client: &AuditClient, |
| 1229 | relay_domain: &str, | 1131 | relay_domain: &str, |
| @@ -1232,30 +1134,65 @@ impl PushAuthorizationTests { | |||
| 1232 | let desc = "Push wrong commit to refs/nostr/<pr-event-id> after PR event (should reject)"; | 1134 | let desc = "Push wrong commit to refs/nostr/<pr-event-id> after PR event (should reject)"; |
| 1233 | let ctx = TestContext::new(client); | 1135 | let ctx = TestContext::new(client); |
| 1234 | 1136 | ||
| 1235 | // Setup: wrong commit already pushed (we'll use the same setup, but publish PR first) | 1137 | // Get fixture: PR event exists on relay (wrong commit was previously pushed but may have been cleaned up) |
| 1236 | let setup = match setup_repo_with_wrong_commit_pushed(&ctx, relay_domain).await { | 1138 | let pr_event = match ctx.get_fixture(FixtureKind::PREventSentAfterWrongPush).await { |
| 1237 | Ok(s) => s, | 1139 | Ok(e) => e, |
| 1140 | Err(e) => { | ||
| 1141 | return TestResult::new(test_name, "GRASP-01", desc).fail(format!("{}", e)); | ||
| 1142 | } | ||
| 1143 | }; | ||
| 1144 | |||
| 1145 | let pr_event_id = pr_event.id.to_hex(); | ||
| 1146 | |||
| 1147 | // Get repo info for cloning (fresh clone for this test) | ||
| 1148 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { | ||
| 1149 | Ok(r) => r, | ||
| 1150 | Err(e) => { | ||
| 1151 | return TestResult::new(test_name, "GRASP-01", desc).fail(format!("{}", e)); | ||
| 1152 | } | ||
| 1153 | }; | ||
| 1154 | |||
| 1155 | let repo_id = repo | ||
| 1156 | .tags | ||
| 1157 | .iter() | ||
| 1158 | .find(|t| t.kind() == TagKind::d()) | ||
| 1159 | .and_then(|t| t.content()) | ||
| 1160 | .unwrap_or("unknown") | ||
| 1161 | .to_string(); | ||
| 1162 | |||
| 1163 | let owner_npub = match repo.pubkey.to_bech32() { | ||
| 1164 | Ok(n) => n, | ||
| 1165 | Err(e) => { | ||
| 1166 | return TestResult::new(test_name, "GRASP-01", desc) | ||
| 1167 | .fail(format!("Failed to get owner npub: {}", e)); | ||
| 1168 | } | ||
| 1169 | }; | ||
| 1170 | |||
| 1171 | // Clone fresh for this test | ||
| 1172 | let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { | ||
| 1173 | Ok(p) => p, | ||
| 1238 | Err(e) => { | 1174 | Err(e) => { |
| 1239 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | 1175 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); |
| 1240 | } | 1176 | } |
| 1241 | }; | 1177 | }; |
| 1242 | 1178 | ||
| 1243 | // Publish PR event FIRST (before our test push) | 1179 | // Create a wrong commit (Owner variant, not PRTestCommit) |
| 1244 | if let Err(e) = publish_pr_event_and_wait(&ctx, &setup.pr_event).await { | 1180 | if let Err(e) = create_deterministic_commit_with_variant(&clone_path, CommitVariant::Owner) |
| 1245 | setup.cleanup(); | 1181 | { |
| 1182 | let _ = fs::remove_dir_all(&clone_path); | ||
| 1246 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | 1183 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); |
| 1247 | } | 1184 | } |
| 1248 | 1185 | ||
| 1249 | // Try to push again with wrong commit (should be rejected now that PR event exists) | 1186 | // Try to push with wrong commit (should be rejected since PR event exists) |
| 1250 | let push_succeeded = match push_to_pr_ref(&setup.clone_path, &setup.pr_event_id) { | 1187 | let push_succeeded = match push_to_pr_ref(&clone_path, &pr_event_id) { |
| 1251 | Ok(success) => success, | 1188 | Ok(success) => success, |
| 1252 | Err(e) => { | 1189 | Err(e) => { |
| 1253 | setup.cleanup(); | 1190 | let _ = fs::remove_dir_all(&clone_path); |
| 1254 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | 1191 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); |
| 1255 | } | 1192 | } |
| 1256 | }; | 1193 | }; |
| 1257 | 1194 | ||
| 1258 | setup.cleanup(); | 1195 | let _ = fs::remove_dir_all(&clone_path); |
| 1259 | 1196 | ||
| 1260 | // Should REJECT - PR event exists with different commit hash | 1197 | // Should REJECT - PR event exists with different commit hash |
| 1261 | if push_succeeded { | 1198 | if push_succeeded { |
| @@ -1272,7 +1209,7 @@ impl PushAuthorizationTests { | |||
| 1272 | /// when a corresponding event exists AND the pushed commit matches | 1209 | /// when a corresponding event exists AND the pushed commit matches |
| 1273 | /// the commit in the PR event's `c` tag. | 1210 | /// the commit in the PR event's `c` tag. |
| 1274 | /// | 1211 | /// |
| 1275 | /// Depends on: `setup_repo_with_wrong_commit_pushed` for setup, then resets to correct commit | 1212 | /// Uses `PREventSentAfterWrongPush` fixture, then creates correct commit and pushes. |
| 1276 | pub async fn test_push_to_nostr_ref_with_correct_commit_after_event_received_accepted( | 1213 | pub async fn test_push_to_nostr_ref_with_correct_commit_after_event_received_accepted( |
| 1277 | client: &AuditClient, | 1214 | client: &AuditClient, |
| 1278 | relay_domain: &str, | 1215 | relay_domain: &str, |
| @@ -1281,36 +1218,64 @@ impl PushAuthorizationTests { | |||
| 1281 | let desc = "Push correct commit to refs/nostr/<pr-event-id> after PR event (should accept)"; | 1218 | let desc = "Push correct commit to refs/nostr/<pr-event-id> after PR event (should accept)"; |
| 1282 | let ctx = TestContext::new(client); | 1219 | let ctx = TestContext::new(client); |
| 1283 | 1220 | ||
| 1284 | // Setup: wrong commit already pushed | 1221 | // Get fixture: PR event exists on relay |
| 1285 | let setup = match setup_repo_with_wrong_commit_pushed(&ctx, relay_domain).await { | 1222 | let pr_event = match ctx.get_fixture(FixtureKind::PREventSentAfterWrongPush).await { |
| 1286 | Ok(s) => s, | 1223 | Ok(e) => e, |
| 1287 | Err(e) => { | 1224 | Err(e) => { |
| 1288 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | 1225 | return TestResult::new(test_name, "GRASP-01", desc).fail(format!("{}", e)); |
| 1289 | } | 1226 | } |
| 1290 | }; | 1227 | }; |
| 1291 | 1228 | ||
| 1292 | // Publish PR event FIRST | 1229 | let pr_event_id = pr_event.id.to_hex(); |
| 1293 | if let Err(e) = publish_pr_event_and_wait(&ctx, &setup.pr_event).await { | 1230 | |
| 1294 | setup.cleanup(); | 1231 | // Get repo info for cloning (fresh clone for this test) |
| 1295 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | 1232 | let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { |
| 1296 | } | 1233 | Ok(r) => r, |
| 1234 | Err(e) => { | ||
| 1235 | return TestResult::new(test_name, "GRASP-01", desc).fail(format!("{}", e)); | ||
| 1236 | } | ||
| 1237 | }; | ||
| 1238 | |||
| 1239 | let repo_id = repo | ||
| 1240 | .tags | ||
| 1241 | .iter() | ||
| 1242 | .find(|t| t.kind() == TagKind::d()) | ||
| 1243 | .and_then(|t| t.content()) | ||
| 1244 | .unwrap_or("unknown") | ||
| 1245 | .to_string(); | ||
| 1297 | 1246 | ||
| 1298 | // Reset to CORRECT commit (the one expected by PR event) | 1247 | let owner_npub = match repo.pubkey.to_bech32() { |
| 1299 | if let Err(e) = reset_to_correct_pr_commit(&setup.clone_path) { | 1248 | Ok(n) => n, |
| 1300 | setup.cleanup(); | 1249 | Err(e) => { |
| 1250 | return TestResult::new(test_name, "GRASP-01", desc) | ||
| 1251 | .fail(format!("Failed to get owner npub: {}", e)); | ||
| 1252 | } | ||
| 1253 | }; | ||
| 1254 | |||
| 1255 | // Clone fresh for this test | ||
| 1256 | let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { | ||
| 1257 | Ok(p) => p, | ||
| 1258 | Err(e) => { | ||
| 1259 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | ||
| 1260 | } | ||
| 1261 | }; | ||
| 1262 | |||
| 1263 | // Create the CORRECT PR test commit (the one expected by PR event) | ||
| 1264 | if let Err(e) = reset_to_correct_pr_commit(&clone_path) { | ||
| 1265 | let _ = fs::remove_dir_all(&clone_path); | ||
| 1301 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | 1266 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); |
| 1302 | } | 1267 | } |
| 1303 | 1268 | ||
| 1304 | // Push correct commit (should succeed) | 1269 | // Push correct commit (should succeed) |
| 1305 | let push_succeeded = match push_to_pr_ref(&setup.clone_path, &setup.pr_event_id) { | 1270 | let push_succeeded = match push_to_pr_ref(&clone_path, &pr_event_id) { |
| 1306 | Ok(success) => success, | 1271 | Ok(success) => success, |
| 1307 | Err(e) => { | 1272 | Err(e) => { |
| 1308 | setup.cleanup(); | 1273 | let _ = fs::remove_dir_all(&clone_path); |
| 1309 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); | 1274 | return TestResult::new(test_name, "GRASP-01", desc).fail(&e); |
| 1310 | } | 1275 | } |
| 1311 | }; | 1276 | }; |
| 1312 | 1277 | ||
| 1313 | setup.cleanup(); | 1278 | let _ = fs::remove_dir_all(&clone_path); |
| 1314 | 1279 | ||
| 1315 | // Should ACCEPT - commit matches PR event's c tag | 1280 | // Should ACCEPT - commit matches PR event's c tag |
| 1316 | if !push_succeeded { | 1281 | if !push_succeeded { |