diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-27 15:59:58 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-27 15:59:58 +0000 |
| commit | c4a35fe8421bc0a0e6608f1b153cb6043230e8b5 (patch) | |
| tree | 435e7b199f1e66f7d92fb2b2bf0c04b00e8af28a /grasp-audit | |
| parent | 33a8870b6015fb989430edbbf5810a2d7d1a5247 (diff) | |
Task 4: Refactor recursive maintainer push test to fixture-first pattern
- Deprecated setup_repo_for_recursive_maintainer helper in fixtures.rs
- test_push_authorized_by_recursive_maintainer_state now creates own TestContext
- Uses FixtureKind chain: RepoState, MaintainerAnnouncement, MaintainerState, RecursiveMaintainerRepoAndState
- Uses git helpers from fixtures.rs (clone_repo, create_deterministic_commit_with_variant, try_push)
- Updated imports to include RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH
- All unit tests pass: cargo test --lib
Diffstat (limited to 'grasp-audit')
| -rw-r--r-- | grasp-audit/src/fixtures.rs | 32 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/push_authorization.rs | 287 |
2 files changed, 277 insertions, 42 deletions
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index 571ab20..a7806ec 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs | |||
| @@ -1435,6 +1435,34 @@ pub async fn setup_repo_for_maintainer( | |||
| 1435 | 1435 | ||
| 1436 | /// Set up a recursive maintainer repository with deterministic commit | 1436 | /// Set up a recursive maintainer repository with deterministic commit |
| 1437 | /// | 1437 | /// |
| 1438 | /// # Deprecated | ||
| 1439 | /// | ||
| 1440 | /// This function is deprecated in favor of the fixture-first pattern. | ||
| 1441 | /// Tests should create their own TestContext and use the fixture chain directly, | ||
| 1442 | /// following the Generate → Send → Verify pattern. | ||
| 1443 | /// | ||
| 1444 | /// See `test_push_authorized_by_recursive_maintainer_state` in `push_authorization.rs` for | ||
| 1445 | /// an example of the fixture-first pattern with recursive maintainers. | ||
| 1446 | /// | ||
| 1447 | /// ## Migration Guide | ||
| 1448 | /// | ||
| 1449 | /// Instead of: | ||
| 1450 | /// ```ignore | ||
| 1451 | /// let setup = setup_repo_for_recursive_maintainer(client, git_data_dir, relay_domain).await?; | ||
| 1452 | /// ``` | ||
| 1453 | /// | ||
| 1454 | /// Use: | ||
| 1455 | /// ```ignore | ||
| 1456 | /// let ctx = TestContext::new(client); | ||
| 1457 | /// let state_event = ctx.get_fixture(FixtureKind::RepoState).await?; | ||
| 1458 | /// ctx.get_fixture(FixtureKind::MaintainerAnnouncement).await?; | ||
| 1459 | /// ctx.get_fixture(FixtureKind::MaintainerState).await?; | ||
| 1460 | /// ctx.get_fixture(FixtureKind::RecursiveMaintainerRepoAndState).await?; | ||
| 1461 | /// // Then clone, create deterministic commit with RecursiveMaintainer variant, and push inline | ||
| 1462 | /// ``` | ||
| 1463 | /// | ||
| 1464 | /// --- | ||
| 1465 | /// | ||
| 1438 | /// This performs all the common setup steps needed for recursive maintainer push authorization tests: | 1466 | /// This performs all the common setup steps needed for recursive maintainer push authorization tests: |
| 1439 | /// 1. Gets RepoState fixture (owner's repo announcement + state event with owner's deterministic commit) | 1467 | /// 1. Gets RepoState fixture (owner's repo announcement + state event with owner's deterministic commit) |
| 1440 | /// 2. Gets MaintainerAnnouncement fixture (maintainer's repo announcement with recursive maintainer in maintainers tag) | 1468 | /// 2. Gets MaintainerAnnouncement fixture (maintainer's repo announcement with recursive maintainer in maintainers tag) |
| @@ -1449,6 +1477,10 @@ pub async fn setup_repo_for_maintainer( | |||
| 1449 | /// 11. Pushes the commit so the grasp server has the state in the state event | 1477 | /// 11. Pushes the commit so the grasp server has the state in the state event |
| 1450 | /// | 1478 | /// |
| 1451 | /// Returns RepoSetup which auto-cleans up the clone_path on drop | 1479 | /// Returns RepoSetup which auto-cleans up the clone_path on drop |
| 1480 | #[deprecated( | ||
| 1481 | since = "0.1.0", | ||
| 1482 | note = "Use fixture-first pattern with TestContext and fixture chain instead. See test_push_authorized_by_recursive_maintainer_state for example." | ||
| 1483 | )] | ||
| 1452 | pub async fn setup_repo_for_recursive_maintainer( | 1484 | pub async fn setup_repo_for_recursive_maintainer( |
| 1453 | client: &crate::AuditClient, | 1485 | client: &crate::AuditClient, |
| 1454 | git_data_dir: &Path, | 1486 | git_data_dir: &Path, |
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index 1de3fe1..f1d6970 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs | |||
| @@ -18,9 +18,9 @@ | |||
| 18 | 18 | ||
| 19 | use crate::{ | 19 | use crate::{ |
| 20 | clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, | 20 | clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, |
| 21 | setup_repo_for_recursive_maintainer, setup_repo_with_deterministic_commit, try_push, | 21 | setup_repo_with_deterministic_commit, try_push, |
| 22 | AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, DETERMINISTIC_COMMIT_HASH, | 22 | AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, DETERMINISTIC_COMMIT_HASH, |
| 23 | MAINTAINER_DETERMINISTIC_COMMIT_HASH, | 23 | MAINTAINER_DETERMINISTIC_COMMIT_HASH, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, |
| 24 | }; | 24 | }; |
| 25 | use nostr_sdk::prelude::*; | 25 | use nostr_sdk::prelude::*; |
| 26 | use std::fs; | 26 | use std::fs; |
| @@ -559,65 +559,268 @@ impl PushAuthorizationTests { | |||
| 559 | /// GRASP-01: "respecting the recursive maintainer set" | 559 | /// GRASP-01: "respecting the recursive maintainer set" |
| 560 | /// This tests recursive maintainer chains: Owner -> MaintainerA -> MaintainerB | 560 | /// This tests recursive maintainer chains: Owner -> MaintainerA -> MaintainerB |
| 561 | /// | 561 | /// |
| 562 | /// Scenario: | 562 | /// ## Fixture-First Pattern |
| 563 | /// 1. RecursiveMaintainerRepoAndState fixture creates: | 563 | /// |
| 564 | /// - Repo announcement signed by recursive_maintainer keys | 564 | /// 1. **Generate**: Create TestContext and get fixture chain: |
| 565 | /// - Lists main pubkey and maintainer pubkey in maintainers tag | 565 | /// - RepoState (owner's repo announcement + state event) |
| 566 | /// - State event with RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH (2s in past) | 566 | /// - MaintainerAnnouncement (maintainer lists recursive-maintainer) |
| 567 | /// 2. setup_repo_for_recursive_maintainer() clones, creates recursive maintainer commit, verifies hash, pushes | 567 | /// - MaintainerState (maintainer's state event) |
| 568 | /// 3. The push should be ACCEPTED because recursive maintainer's state event authorizes it | 568 | /// - RecursiveMaintainerRepoAndState (recursive maintainer's announcement + state) |
| 569 | /// 2. **Send**: Clone repo, create recursive maintainer deterministic commit, push | ||
| 570 | /// 3. **Verify**: Push should succeed because recursive maintainer's state event authorizes it | ||
| 571 | /// | ||
| 572 | /// The fixture chain establishes: Owner -> Maintainer -> RecursiveMaintainer | ||
| 573 | /// Each level publishes announcements that authorize the next level. | ||
| 569 | pub async fn test_push_authorized_by_recursive_maintainer_state( | 574 | pub async fn test_push_authorized_by_recursive_maintainer_state( |
| 570 | client: &AuditClient, | 575 | client: &AuditClient, |
| 571 | git_data_dir: &Path, | 576 | git_data_dir: &Path, |
| 572 | relay_domain: &str, | 577 | relay_domain: &str, |
| 573 | ) -> TestResult { | 578 | ) -> TestResult { |
| 579 | use std::process::Command; | ||
| 580 | |||
| 574 | let test_name = "test_push_authorized_by_recursive_maintainer_state"; | 581 | let test_name = "test_push_authorized_by_recursive_maintainer_state"; |
| 575 | 582 | ||
| 576 | // Use setup_repo_for_recursive_maintainer which leverages RecursiveMaintainerRepoAndState fixture | 583 | // ============================================================ |
| 577 | // This does all the heavy lifting: | 584 | // Step 1: GENERATE - Create TestContext and get fixture chain |
| 578 | // 1. Creates repo announcement signed by recursive maintainer keys | 585 | // ============================================================ |
| 579 | // 2. Creates state event pointing to RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH | 586 | let ctx = TestContext::new(client); |
| 580 | // 3. Clones the repo | 587 | |
| 581 | // 4. Creates the recursive maintainer deterministic commit locally | 588 | // Get RepoState fixture (owner's repo announcement + state event) |
| 582 | // 5. Verifies commit hash matches expected | 589 | let state_event = match ctx.get_fixture(FixtureKind::RepoState).await { |
| 583 | // 6. Creates main branch, checks it out, and pushes | 590 | Ok(e) => e, |
| 584 | match setup_repo_for_recursive_maintainer(client, git_data_dir, relay_domain).await { | 591 | Err(e) => { |
| 585 | Ok(_setup) => { | 592 | return TestResult::new( |
| 586 | // Push succeeded in setup - this means the relay accepted the push | ||
| 587 | // authorized by the recursive maintainer's state event | ||
| 588 | TestResult::new( | ||
| 589 | test_name, | 593 | test_name, |
| 590 | "GRASP-01", | 594 | "GRASP-01", |
| 591 | "Push authorized by recursive maintainer state event", | 595 | "Push authorized by recursive maintainer state event", |
| 592 | ) | 596 | ) |
| 593 | .pass() | 597 | .fail(&format!("Failed to create RepoState fixture: {}", e)); |
| 594 | } | 598 | } |
| 599 | }; | ||
| 600 | |||
| 601 | // Get MaintainerAnnouncement fixture (maintainer's repo announcement listing recursive maintainer) | ||
| 602 | match ctx.get_fixture(FixtureKind::MaintainerAnnouncement).await { | ||
| 603 | Ok(_) => {} | ||
| 595 | Err(e) => { | 604 | Err(e) => { |
| 596 | // Check if this was specifically a push rejection | 605 | return TestResult::new( |
| 597 | if e.contains("Failed to push") { | 606 | test_name, |
| 598 | TestResult::new( | 607 | "GRASP-01", |
| 599 | test_name, | 608 | "Push authorized by recursive maintainer state event", |
| 600 | "GRASP-01", | 609 | ) |
| 601 | "Push authorized by recursive maintainer state event", | 610 | .fail(&format!("Failed to create MaintainerAnnouncement fixture: {}", e)); |
| 602 | ) | 611 | } |
| 603 | .fail(&format!( | 612 | }; |
| 604 | "Push was rejected but should have been accepted. \ | 613 | |
| 605 | The recursive maintainer published a state event with a commit hash, \ | 614 | // Get MaintainerState fixture (maintainer's state event) |
| 606 | and the relay should authorize pushes matching this state event \ | 615 | match ctx.get_fixture(FixtureKind::MaintainerState).await { |
| 607 | through recursive maintainer traversal. \ | 616 | Ok(_) => {} |
| 608 | Error: {}", | 617 | Err(e) => { |
| 609 | e | 618 | return TestResult::new( |
| 610 | )) | 619 | test_name, |
| 611 | } else { | 620 | "GRASP-01", |
| 612 | // Some other error during setup | 621 | "Push authorized by recursive maintainer state event", |
| 613 | TestResult::new( | 622 | ) |
| 623 | .fail(&format!("Failed to create MaintainerState fixture: {}", e)); | ||
| 624 | } | ||
| 625 | }; | ||
| 626 | |||
| 627 | // Get RecursiveMaintainerRepoAndState fixture (completes 3-level delegation chain) | ||
| 628 | match ctx.get_fixture(FixtureKind::RecursiveMaintainerRepoAndState).await { | ||
| 629 | Ok(_) => {} | ||
| 630 | Err(e) => { | ||
| 631 | return TestResult::new( | ||
| 632 | test_name, | ||
| 633 | "GRASP-01", | ||
| 634 | "Push authorized by recursive maintainer state event", | ||
| 635 | ) | ||
| 636 | .fail(&format!("Failed to create RecursiveMaintainerRepoAndState fixture: {}", e)); | ||
| 637 | } | ||
| 638 | }; | ||
| 639 | |||
| 640 | tokio::time::sleep(std::time::Duration::from_millis(200)).await; | ||
| 641 | |||
| 642 | // Extract repo_id and npub from owner's state event | ||
| 643 | let repo_id = match state_event | ||
| 644 | .tags | ||
| 645 | .iter() | ||
| 646 | .find(|t| t.kind() == TagKind::d()) | ||
| 647 | .and_then(|t| t.content()) | ||
| 648 | { | ||
| 649 | Some(id) => id.to_string(), | ||
| 650 | None => { | ||
| 651 | return TestResult::new( | ||
| 652 | test_name, | ||
| 653 | "GRASP-01", | ||
| 654 | "Push authorized by recursive maintainer state event", | ||
| 655 | ) | ||
| 656 | .fail("Missing repo_id in state event"); | ||
| 657 | } | ||
| 658 | }; | ||
| 659 | |||
| 660 | let npub = match state_event.pubkey.to_bech32() { | ||
| 661 | Ok(n) => n, | ||
| 662 | Err(e) => { | ||
| 663 | return TestResult::new( | ||
| 664 | test_name, | ||
| 665 | "GRASP-01", | ||
| 666 | "Push authorized by recursive maintainer state event", | ||
| 667 | ) | ||
| 668 | .fail(&format!("Failed to convert pubkey to bech32: {}", e)); | ||
| 669 | } | ||
| 670 | }; | ||
| 671 | |||
| 672 | // Verify repo exists on disk | ||
| 673 | let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id)); | ||
| 674 | if !repo_path.exists() { | ||
| 675 | return TestResult::new( | ||
| 676 | test_name, | ||
| 677 | "GRASP-01", | ||
| 678 | "Push authorized by recursive maintainer state event", | ||
| 679 | ) | ||
| 680 | .fail(&format!("Repo not found: {}", repo_path.display())); | ||
| 681 | } | ||
| 682 | |||
| 683 | // ============================================================ | ||
| 684 | // Step 2: SEND - Clone, create recursive maintainer commit, push | ||
| 685 | // ============================================================ | ||
| 686 | let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { | ||
| 687 | Ok(p) => p, | ||
| 688 | Err(e) => { | ||
| 689 | return TestResult::new( | ||
| 690 | test_name, | ||
| 691 | "GRASP-01", | ||
| 692 | "Push authorized by recursive maintainer state event", | ||
| 693 | ) | ||
| 694 | .fail(&e); | ||
| 695 | } | ||
| 696 | }; | ||
| 697 | let cleanup = || { | ||
| 698 | let _ = fs::remove_dir_all(&clone_path); | ||
| 699 | }; | ||
| 700 | |||
| 701 | // Create recursive maintainer deterministic commit | ||
| 702 | let commit_hash = | ||
| 703 | match create_deterministic_commit_with_variant(&clone_path, CommitVariant::RecursiveMaintainer) { | ||
| 704 | Ok(h) => h, | ||
| 705 | Err(e) => { | ||
| 706 | cleanup(); | ||
| 707 | return TestResult::new( | ||
| 614 | test_name, | 708 | test_name, |
| 615 | "GRASP-01", | 709 | "GRASP-01", |
| 616 | "Push authorized by recursive maintainer state event", | 710 | "Push authorized by recursive maintainer state event", |
| 617 | ) | 711 | ) |
| 618 | .fail(&format!("Setup failed: {}", e)) | 712 | .fail(&format!("Failed to create recursive maintainer commit: {}", e)); |
| 619 | } | 713 | } |
| 714 | }; | ||
| 715 | |||
| 716 | // Verify commit hash matches expected | ||
| 717 | if commit_hash != RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH { | ||
| 718 | cleanup(); | ||
| 719 | return TestResult::new( | ||
| 720 | test_name, | ||
| 721 | "GRASP-01", | ||
| 722 | "Push authorized by recursive maintainer state event", | ||
| 723 | ) | ||
| 724 | .fail(&format!( | ||
| 725 | "Recursive maintainer commit hash mismatch: got {}, expected {}", | ||
| 726 | commit_hash, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH | ||
| 727 | )); | ||
| 728 | } | ||
| 729 | |||
| 730 | // Create main branch | ||
| 731 | let branch_output = Command::new("git") | ||
| 732 | .args(["branch", "main"]) | ||
| 733 | .current_dir(&clone_path) | ||
| 734 | .output(); | ||
| 735 | |||
| 736 | match branch_output { | ||
| 737 | Err(e) => { | ||
| 738 | cleanup(); | ||
| 739 | return TestResult::new( | ||
| 740 | test_name, | ||
| 741 | "GRASP-01", | ||
| 742 | "Push authorized by recursive maintainer state event", | ||
| 743 | ) | ||
| 744 | .fail(&format!("Failed to create main branch: {}", e)); | ||
| 745 | } | ||
| 746 | Ok(output) if !output.status.success() => { | ||
| 747 | cleanup(); | ||
| 748 | return TestResult::new( | ||
| 749 | test_name, | ||
| 750 | "GRASP-01", | ||
| 751 | "Push authorized by recursive maintainer state event", | ||
| 752 | ) | ||
| 753 | .fail(&format!( | ||
| 754 | "Failed to create main branch: {}", | ||
| 755 | String::from_utf8_lossy(&output.stderr) | ||
| 756 | )); | ||
| 757 | } | ||
| 758 | _ => {} | ||
| 759 | } | ||
| 760 | |||
| 761 | // Checkout main branch | ||
| 762 | let checkout_output = Command::new("git") | ||
| 763 | .args(["checkout", "main"]) | ||
| 764 | .current_dir(&clone_path) | ||
| 765 | .output(); | ||
| 766 | |||
| 767 | match checkout_output { | ||
| 768 | Err(e) => { | ||
| 769 | cleanup(); | ||
| 770 | return TestResult::new( | ||
| 771 | test_name, | ||
| 772 | "GRASP-01", | ||
| 773 | "Push authorized by recursive maintainer state event", | ||
| 774 | ) | ||
| 775 | .fail(&format!("Failed to checkout main branch: {}", e)); | ||
| 776 | } | ||
| 777 | Ok(output) if !output.status.success() => { | ||
| 778 | cleanup(); | ||
| 779 | return TestResult::new( | ||
| 780 | test_name, | ||
| 781 | "GRASP-01", | ||
| 782 | "Push authorized by recursive maintainer state event", | ||
| 783 | ) | ||
| 784 | .fail(&format!( | ||
| 785 | "Failed to checkout main branch: {}", | ||
| 786 | String::from_utf8_lossy(&output.stderr) | ||
| 787 | )); | ||
| 620 | } | 788 | } |
| 789 | _ => {} | ||
| 790 | } | ||
| 791 | |||
| 792 | // ============================================================ | ||
| 793 | // Step 3: VERIFY - Push should succeed because recursive | ||
| 794 | // maintainer's state event authorizes this commit | ||
| 795 | // ============================================================ | ||
| 796 | let push_result = try_push(&clone_path); | ||
| 797 | cleanup(); | ||
| 798 | |||
| 799 | match push_result { | ||
| 800 | Ok(true) => TestResult::new( | ||
| 801 | test_name, | ||
| 802 | "GRASP-01", | ||
| 803 | "Push authorized by recursive maintainer state event", | ||
| 804 | ) | ||
| 805 | .pass(), | ||
| 806 | Ok(false) => TestResult::new( | ||
| 807 | test_name, | ||
| 808 | "GRASP-01", | ||
| 809 | "Push authorized by recursive maintainer state event", | ||
| 810 | ) | ||
| 811 | .fail(&format!( | ||
| 812 | "Push was rejected but should have been accepted. \ | ||
| 813 | The recursive maintainer published a state event with commit {}, \ | ||
| 814 | and the relay should authorize pushes matching this state event \ | ||
| 815 | through recursive maintainer traversal (Owner -> Maintainer -> RecursiveMaintainer).", | ||
| 816 | RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH | ||
| 817 | )), | ||
| 818 | Err(e) => TestResult::new( | ||
| 819 | test_name, | ||
| 820 | "GRASP-01", | ||
| 821 | "Push authorized by recursive maintainer state event", | ||
| 822 | ) | ||
| 823 | .fail(&format!("Push error: {}", e)), | ||
| 621 | } | 824 | } |
| 622 | } | 825 | } |
| 623 | 826 | ||