From d3e703d7f522c30ac6634716654c24cb7415fabd Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 27 Nov 2025 17:05:51 +0000 Subject: remove depricated code --- grasp-audit/src/fixtures.rs | 520 --------------------- grasp-audit/src/lib.rs | 4 +- .../src/specs/grasp01/push_authorization.rs | 395 ++++++++++++++-- 3 files changed, 352 insertions(+), 567 deletions(-) (limited to 'grasp-audit/src') diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index a7806ec..89531c6 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs @@ -856,7 +856,6 @@ pub async fn send_and_verify_rejected( // Git Operation Helpers // ============================================================ -use nostr_sdk::ToBech32; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; @@ -1087,525 +1086,6 @@ pub fn create_deterministic_commit(clone_path: &Path, _message: &str) -> Result< create_deterministic_commit_with_variant(clone_path, CommitVariant::Owner) } -/// Repository setup with deterministic commit -/// This struct holds all the data needed for push authorization tests -pub struct RepoSetup { - /// Path to the cloned repository (auto-cleaned on drop) - pub clone_path: PathBuf, - /// Repository identifier (d-tag value) - pub repo_id: String, - /// Owner's bech32 public key - pub npub: String, - /// The deterministic commit hash - pub commit_hash: String, -} - -impl Drop for RepoSetup { - fn drop(&mut self) { - let _ = fs::remove_dir_all(&self.clone_path); - } -} - -/// Set up a repository with deterministic commit for testing -/// -/// # Deprecated -/// -/// This function is deprecated in favor of the fixture-first pattern. -/// Tests should create their own TestContext and use `FixtureKind::RepoState` -/// directly, following the Generate → Send → Verify pattern. -/// -/// See `test_push_authorized_by_owner_state` in `push_authorization.rs` for -/// an example of the fixture-first pattern. -/// -/// ## Migration Guide -/// -/// Instead of: -/// ```ignore -/// let setup = setup_repo_with_deterministic_commit(client, git_data_dir, relay_domain).await?; -/// ``` -/// -/// Use: -/// ```ignore -/// let ctx = TestContext::new(client); -/// let state_event = ctx.get_fixture(FixtureKind::RepoState).await?; -/// // Then clone, create deterministic commit, and push inline -/// ``` -/// -/// --- -/// -/// This performs all the common setup steps needed for push authorization tests: -/// 1. Gets RepoState fixture (repo announcement + state event with deterministic commit) -/// 2. Extracts repo_id and npub -/// 3. Verifies repo exists on disk -/// 4. Clones the repository -/// 5. Creates deterministic commit locally -/// 6. Verifies commit hash matches expected -/// 7. Creates and checks out main branch -/// 8. Pushes the commit so the grasp server has the state in the state event -/// -/// Returns RepoSetup which auto-cleans up the clone_path on drop -/// -/// # Arguments -/// * `client` - The AuditClient to use for fixtures -/// * `git_data_dir` - Path to the git data directory -/// * `relay_domain` - The domain of the relay (e.g., "localhost:7000") -/// -/// # Returns -/// * `Ok(RepoSetup)` - The setup data -/// * `Err(String)` - Error message if setup failed -#[deprecated( - since = "0.1.0", - note = "Use fixture-first pattern with TestContext and FixtureKind::RepoState instead. See test_push_authorized_by_owner_state for example." -)] -pub async fn setup_repo_with_deterministic_commit( - client: &crate::AuditClient, - git_data_dir: &Path, - relay_domain: &str, -) -> Result { - use nostr_sdk::prelude::TagKind; - - let ctx = TestContext::new(client); - - // Get RepoState fixture (includes repo announcement and state event with deterministic commit) - let state_event = ctx.get_fixture(FixtureKind::RepoState).await - .map_err(|e| format!("Failed to create repo state fixture: {}", e))?; - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - // Extract repo_id from state event - let repo_id = state_event.tags.iter().find(|t| t.kind() == TagKind::d()) - .and_then(|t| t.content()) - .ok_or("Missing repo_id")? - .to_string(); - let npub = state_event.pubkey.to_bech32() - .map_err(|e| format!("Failed to convert pubkey to bech32: {}", e))?; - - // Verify repo exists - let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id)); - if !repo_path.exists() { - return Err(format!("Repo not found: {}", repo_path.display())); - } - - // Clone repo - let clone_path = clone_repo(relay_domain, &npub, &repo_id)?; - - // Create deterministic commit locally (this will be the root commit with no parent) - let commit_hash = create_deterministic_commit(&clone_path, "Initial commit") - .map_err(|e| { - let _ = fs::remove_dir_all(&clone_path); - e - })?; - - // Verify commit hash matches expected deterministic hash - if commit_hash != DETERMINISTIC_COMMIT_HASH { - let _ = fs::remove_dir_all(&clone_path); - return Err(format!( - "Commit hash mismatch: got {}, expected {}", - commit_hash, DETERMINISTIC_COMMIT_HASH - )); - } - - // Create main branch pointing to our deterministic commit - let branch_output = Command::new("git") - .args(["branch", "main"]) - .current_dir(&clone_path) - .output() - .map_err(|e| { - let _ = fs::remove_dir_all(&clone_path); - format!("Failed to create main branch: {}", e) - })?; - - if !branch_output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return Err(format!( - "Failed to create main branch: {}", - String::from_utf8_lossy(&branch_output.stderr) - )); - } - - // Checkout main branch - let checkout_output = Command::new("git") - .args(["checkout", "main"]) - .current_dir(&clone_path) - .output() - .map_err(|e| { - let _ = fs::remove_dir_all(&clone_path); - format!("Failed to checkout main branch: {}", e) - })?; - - if !checkout_output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return Err(format!( - "Failed to checkout main branch: {}", - String::from_utf8_lossy(&checkout_output.stderr) - )); - } - - // Push the commit to the server so the bare repo matches the state event - let push_output = Command::new("git") - .args(["push", "origin", "main"]) - .current_dir(&clone_path) - .env("GIT_TERMINAL_PROMPT", "0") - .output() - .map_err(|e| { - let _ = fs::remove_dir_all(&clone_path); - format!("Failed to push to server: {}", e) - })?; - - if !push_output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return Err(format!( - "Failed to push to server: {}", - String::from_utf8_lossy(&push_output.stderr) - )); - } - - Ok(RepoSetup { - clone_path, - repo_id, - npub, - commit_hash, - }) -} - -/// Set up a maintainer repository with deterministic commit (state only) -/// -/// # Deprecated -/// -/// This function is deprecated in favor of the fixture-first pattern. -/// Tests should create their own TestContext and use `FixtureKind::MaintainerState` -/// directly, following the Generate → Send → Verify pattern. -/// -/// See `test_push_authorized_by_maintainer_state_only` in `push_authorization.rs` for -/// an example of the fixture-first pattern. -/// -/// ## Migration Guide -/// -/// Instead of: -/// ```ignore -/// let setup = setup_repo_for_maintainer(client, git_data_dir, relay_domain).await?; -/// ``` -/// -/// Use: -/// ```ignore -/// let ctx = TestContext::new(client); -/// let _state_event = ctx.get_fixture(FixtureKind::RepoState).await?; -/// let _maintainer_state = ctx.get_fixture(FixtureKind::MaintainerState).await?; -/// // Then clone, create maintainer deterministic commit, and push inline -/// ``` -/// -/// --- -/// -/// This performs all the common setup steps needed for maintainer push authorization tests: -/// 1. Gets RepoState fixture (owner's repo announcement + state event with owner's deterministic commit) -/// 2. Gets MaintainerState fixture (maintainer's state event ONLY - no announcement) -/// 3. Extracts repo_id and owner npub -/// 4. Verifies repo exists on disk -/// 5. Clones the repository using owner's npub -/// 6. Creates maintainer deterministic commit locally -/// 7. Verifies commit hash matches expected -/// 8. Creates and checks out main branch -/// 9. Pushes the commit so the grasp server has the state in the state event -/// -/// Note: This does NOT publish a maintainer announcement. For tests that need the -/// maintainer announcement (like recursive maintainer tests), use setup_repo_for_recursive_maintainer -/// which publishes MaintainerAnnouncement separately. -/// -/// Returns RepoSetup which auto-cleans up the clone_path on drop -#[deprecated( - since = "0.1.0", - note = "Use fixture-first pattern with TestContext and FixtureKind::MaintainerState instead. See test_push_authorized_by_maintainer_state_only for example." -)] -pub async fn setup_repo_for_maintainer( - client: &crate::AuditClient, - git_data_dir: &Path, - relay_domain: &str, -) -> Result { - use nostr_sdk::prelude::TagKind; - - let ctx = TestContext::new(client); - - // Get RepoState fixture (includes owner's repo announcement and state event with owner's deterministic commit) - let state_event = ctx.get_fixture(FixtureKind::RepoState).await - .map_err(|e| format!("Failed to create repo state fixture: {}", e))?; - - // Get MaintainerState fixture ONLY (no announcement - tests state-only authorization) - let _maintainer_state = ctx.get_fixture(FixtureKind::MaintainerState).await - .map_err(|e| format!("Failed to create maintainer state fixture: {}", e))?; - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - // Extract repo_id from state event - let repo_id = state_event.tags.iter().find(|t| t.kind() == TagKind::d()) - .and_then(|t| t.content()) - .ok_or("Missing repo_id")? - .to_string(); - - // The npub is from the owner keys (the signer of the state event) - let npub = state_event.pubkey.to_bech32() - .map_err(|e| format!("Failed to convert owner pubkey to bech32: {}", e))?; - - // Verify repo exists - let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id)); - if !repo_path.exists() { - return Err(format!("Owner repo not found: {}", repo_path.display())); - } - - // Clone repo using owner's npub - let clone_path = clone_repo(relay_domain, &npub, &repo_id)?; - - // Create maintainer deterministic commit locally (this will be the root commit with no parent) - let commit_hash = create_deterministic_commit_with_variant(&clone_path, CommitVariant::Maintainer) - .map_err(|e| { - let _ = fs::remove_dir_all(&clone_path); - e - })?; - - // Verify commit hash matches expected maintainer deterministic hash - if commit_hash != MAINTAINER_DETERMINISTIC_COMMIT_HASH { - let _ = fs::remove_dir_all(&clone_path); - return Err(format!( - "Maintainer commit hash mismatch: got {}, expected {}", - commit_hash, MAINTAINER_DETERMINISTIC_COMMIT_HASH - )); - } - - // Create main branch pointing to our deterministic commit - let branch_output = Command::new("git") - .args(["branch", "main"]) - .current_dir(&clone_path) - .output() - .map_err(|e| { - let _ = fs::remove_dir_all(&clone_path); - format!("Failed to create main branch: {}", e) - })?; - - if !branch_output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return Err(format!( - "Failed to create main branch: {}", - String::from_utf8_lossy(&branch_output.stderr) - )); - } - - // Checkout main branch - let checkout_output = Command::new("git") - .args(["checkout", "main"]) - .current_dir(&clone_path) - .output() - .map_err(|e| { - let _ = fs::remove_dir_all(&clone_path); - format!("Failed to checkout main branch: {}", e) - })?; - - if !checkout_output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return Err(format!( - "Failed to checkout main branch: {}", - String::from_utf8_lossy(&checkout_output.stderr) - )); - } - - // Push the commit to the server so the bare repo matches the state event - let push_output = Command::new("git") - .args(["push", "origin", "main"]) - .current_dir(&clone_path) - .env("GIT_TERMINAL_PROMPT", "0") - .output() - .map_err(|e| { - let _ = fs::remove_dir_all(&clone_path); - format!("Failed to push to server: {}", e) - })?; - - if !push_output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return Err(format!( - "Failed to push to server: {}", - String::from_utf8_lossy(&push_output.stderr) - )); - } - - Ok(RepoSetup { - clone_path, - repo_id, - npub, - commit_hash, - }) -} - -/// Set up a recursive maintainer repository with deterministic commit -/// -/// # Deprecated -/// -/// This function is deprecated in favor of the fixture-first pattern. -/// Tests should create their own TestContext and use the fixture chain directly, -/// following the Generate → Send → Verify pattern. -/// -/// See `test_push_authorized_by_recursive_maintainer_state` in `push_authorization.rs` for -/// an example of the fixture-first pattern with recursive maintainers. -/// -/// ## Migration Guide -/// -/// Instead of: -/// ```ignore -/// let setup = setup_repo_for_recursive_maintainer(client, git_data_dir, relay_domain).await?; -/// ``` -/// -/// Use: -/// ```ignore -/// let ctx = TestContext::new(client); -/// let state_event = ctx.get_fixture(FixtureKind::RepoState).await?; -/// ctx.get_fixture(FixtureKind::MaintainerAnnouncement).await?; -/// ctx.get_fixture(FixtureKind::MaintainerState).await?; -/// ctx.get_fixture(FixtureKind::RecursiveMaintainerRepoAndState).await?; -/// // Then clone, create deterministic commit with RecursiveMaintainer variant, and push inline -/// ``` -/// -/// --- -/// -/// This performs all the common setup steps needed for recursive maintainer push authorization tests: -/// 1. Gets RepoState fixture (owner's repo announcement + state event with owner's deterministic commit) -/// 2. Gets MaintainerAnnouncement fixture (maintainer's repo announcement with recursive maintainer in maintainers tag) -/// 3. Gets MaintainerState fixture (maintainer's state event) -/// 4. Gets RecursiveMaintainerRepoAndState fixture (recursive maintainer's repo - completes 3-level chain) -/// 5. Extracts repo_id and owner npub -/// 6. Verifies repo exists on disk -/// 7. Clones the repository using owner's npub -/// 8. Creates recursive maintainer deterministic commit locally -/// 9. Verifies commit hash matches expected -/// 10. Creates and checks out main branch -/// 11. Pushes the commit so the grasp server has the state in the state event -/// -/// Returns RepoSetup which auto-cleans up the clone_path on drop -#[deprecated( - since = "0.1.0", - note = "Use fixture-first pattern with TestContext and fixture chain instead. See test_push_authorized_by_recursive_maintainer_state for example." -)] -pub async fn setup_repo_for_recursive_maintainer( - client: &crate::AuditClient, - git_data_dir: &Path, - relay_domain: &str, -) -> Result { - use nostr_sdk::prelude::TagKind; - - let ctx = TestContext::new(client); - - // Get RepoState fixture (includes owner's repo announcement and state event) - let state_event = ctx.get_fixture(FixtureKind::RepoState).await - .map_err(|e| format!("Failed to create repo state fixture: {}", e))?; - - // Get MaintainerAnnouncement fixture (maintainer's repo announcement with recursive maintainer in maintainers tag) - let _maintainer_announcement = ctx.get_fixture(FixtureKind::MaintainerAnnouncement).await - .map_err(|e| format!("Failed to create maintainer announcement fixture: {}", e))?; - - // Get MaintainerState fixture (maintainer's state event) - let _maintainer_state = ctx.get_fixture(FixtureKind::MaintainerState).await - .map_err(|e| format!("Failed to create maintainer state fixture: {}", e))?; - - // Get RecursiveMaintainerRepoAndState fixture (completes 3-level delegation chain) - let _recursive_maintainer_state = ctx.get_fixture(FixtureKind::RecursiveMaintainerRepoAndState).await - .map_err(|e| format!("Failed to create recursive maintainer repo state fixture: {}", e))?; - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - // Extract repo_id from owner's state event - let repo_id = state_event.tags.iter().find(|t| t.kind() == TagKind::d()) - .and_then(|t| t.content()) - .ok_or("Missing repo_id")? - .to_string(); - - // The npub is from the owner keys (the signer of the state event) - let npub = state_event.pubkey.to_bech32() - .map_err(|e| format!("Failed to convert owner pubkey to bech32: {}", e))?; - - // Verify repo exists - let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id)); - if !repo_path.exists() { - return Err(format!("Owner repo not found: {}", repo_path.display())); - } - - // Clone repo using owner's npub - let clone_path = clone_repo(relay_domain, &npub, &repo_id)?; - - // Create recursive maintainer deterministic commit locally (this will be the root commit with no parent) - let commit_hash = create_deterministic_commit_with_variant(&clone_path, CommitVariant::RecursiveMaintainer) - .map_err(|e| { - let _ = fs::remove_dir_all(&clone_path); - e - })?; - - // Verify commit hash matches expected recursive maintainer deterministic hash - if commit_hash != RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH { - let _ = fs::remove_dir_all(&clone_path); - return Err(format!( - "Recursive maintainer commit hash mismatch: got {}, expected {}", - commit_hash, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH - )); - } - - // Create main branch pointing to our deterministic commit - let branch_output = Command::new("git") - .args(["branch", "main"]) - .current_dir(&clone_path) - .output() - .map_err(|e| { - let _ = fs::remove_dir_all(&clone_path); - format!("Failed to create main branch: {}", e) - })?; - - if !branch_output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return Err(format!( - "Failed to create main branch: {}", - String::from_utf8_lossy(&branch_output.stderr) - )); - } - - // Checkout main branch - let checkout_output = Command::new("git") - .args(["checkout", "main"]) - .current_dir(&clone_path) - .output() - .map_err(|e| { - let _ = fs::remove_dir_all(&clone_path); - format!("Failed to checkout main branch: {}", e) - })?; - - if !checkout_output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return Err(format!( - "Failed to checkout main branch: {}", - String::from_utf8_lossy(&checkout_output.stderr) - )); - } - - // Push the commit to the server so the bare repo matches the state event - let push_output = Command::new("git") - .args(["push", "origin", "main"]) - .current_dir(&clone_path) - .env("GIT_TERMINAL_PROMPT", "0") - .output() - .map_err(|e| { - let _ = fs::remove_dir_all(&clone_path); - format!("Failed to push to server: {}", e) - })?; - - if !push_output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return Err(format!( - "Failed to push to server: {}", - String::from_utf8_lossy(&push_output.stderr) - )); - } - - Ok(RepoSetup { - clone_path, - repo_id, - npub, - commit_hash, - }) -} - /// Attempt a git push and return success/failure /// /// # Arguments diff --git a/grasp-audit/src/lib.rs b/grasp-audit/src/lib.rs index 2d5531b..5eb9b71 100644 --- a/grasp-audit/src/lib.rs +++ b/grasp-audit/src/lib.rs @@ -43,10 +43,8 @@ pub use fixtures::{ try_push, // Verification helpers send_and_verify_accepted, send_and_verify_rejected, - // Repo setup helpers - setup_repo_for_maintainer, setup_repo_for_recursive_maintainer, setup_repo_with_deterministic_commit, // Types and constants - CommitVariant, ContextMode, FixtureKind, RepoSetup, TestContext, + CommitVariant, ContextMode, FixtureKind, TestContext, DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, }; diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index f1d6970..fad77fb 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs @@ -18,9 +18,9 @@ use crate::{ clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, - setup_repo_with_deterministic_commit, try_push, - AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, DETERMINISTIC_COMMIT_HASH, - MAINTAINER_DETERMINISTIC_COMMIT_HASH, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, + try_push, AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, + DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH, + RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, }; use nostr_sdk::prelude::*; use std::fs; @@ -258,50 +258,202 @@ impl PushAuthorizationTests { /// Test that push is rejected when commit doesn't match state event /// - /// This test verifies that the relay enforces state event authorization. - /// The state event (from fixture) points to the deterministic commit which is - /// already on the server. We create a new commit locally and try to push it. - /// The push should be rejected because the new commit doesn't match what the - /// state event announces. + /// GRASP-01: "MUST accept pushes via this service that match the latest repo state announcement" + /// (Conversely, MUST reject pushes that don't match) + /// + /// ## Fixture-First Pattern + /// + /// 1. **Generate**: Create TestContext and get RepoState fixture + /// (repo announcement + state event pointing to deterministic commit) + /// 2. **Send**: Clone repo, create deterministic commit, push (establishes state on relay) + /// 3. **Test**: Create a NEW commit locally, try to push + /// 4. **Verify**: Push should be rejected because new commit doesn't match state event pub async fn test_push_rejected_wrong_commit( client: &AuditClient, git_data_dir: &Path, relay_domain: &str, ) -> TestResult { + use std::process::Command; + let test_name = "test_push_rejected_wrong_commit"; - // Set up repository with deterministic commit - // This creates a state event pointing to DETERMINISTIC_COMMIT_HASH and pushes that commit - let setup = match setup_repo_with_deterministic_commit(client, git_data_dir, relay_domain).await { - Ok(s) => s, + // ============================================================ + // Step 1: GENERATE - Create TestContext and get RepoState fixture + // ============================================================ + let ctx = TestContext::new(client); + + let state_event = match ctx.get_fixture(FixtureKind::RepoState).await { + Ok(e) => e, Err(e) => { return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") - .fail(&format!("Setup failed: {}", e)) + .fail(&format!("Failed to create RepoState fixture: {}", e)); + } + }; + + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + + // Extract repo_id and npub from state event + let repo_id = match state_event + .tags + .iter() + .find(|t| t.kind() == TagKind::d()) + .and_then(|t| t.content()) + { + Some(id) => id.to_string(), + None => { + return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") + .fail("Missing repo_id in state event"); + } + }; + + let npub = match state_event.pubkey.to_bech32() { + Ok(n) => n, + Err(e) => { + return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") + .fail(&format!("Failed to convert pubkey to bech32: {}", e)); } }; - // Create a new commit locally - this is NOT announced in any state event - let new_commit = match create_commit(&setup.clone_path, "Unauthorized commit") { + // Verify repo exists on disk + let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id)); + if !repo_path.exists() { + return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") + .fail(&format!("Repo not found: {}", repo_path.display())); + } + + // ============================================================ + // Step 2: SEND - Clone repo, create deterministic commit, push + // (establishes the state on the relay) + // ============================================================ + let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { + Ok(p) => p, + Err(e) => { + return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") + .fail(&format!("Failed to clone repo: {}", e)); + } + }; + + // Cleanup helper + let cleanup = || { + let _ = fs::remove_dir_all(&clone_path); + }; + + // Create deterministic commit locally + let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") { Ok(h) => h, Err(e) => { + cleanup(); return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") - .fail(&format!("Failed to create commit: {}", e)) + .fail(&format!("Failed to create deterministic commit: {}", e)); } }; - // Try to push the new commit - // This should be REJECTED because: - // - The state event still points to the deterministic commit (setup.commit_hash) - // - We're trying to push new_commit which is different - // - The relay MUST reject pushes that don't match the announced state - let push_result = try_push(&setup.clone_path); + // Verify commit hash matches expected + if commit_hash != DETERMINISTIC_COMMIT_HASH { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") + .fail(&format!( + "Commit hash mismatch: got {}, expected {}", + commit_hash, DETERMINISTIC_COMMIT_HASH + )); + } + + // Create main branch pointing to our deterministic commit + let branch_output = Command::new("git") + .args(["branch", "main"]) + .current_dir(&clone_path) + .output(); + + match branch_output { + Err(e) => { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") + .fail(&format!("Failed to create main branch: {}", e)); + } + Ok(output) if !output.status.success() => { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") + .fail(&format!( + "Failed to create main branch: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + _ => {} + } + + // Checkout main branch + let checkout_output = Command::new("git") + .args(["checkout", "main"]) + .current_dir(&clone_path) + .output(); + + match checkout_output { + Err(e) => { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") + .fail(&format!("Failed to checkout main branch: {}", e)); + } + Ok(output) if !output.status.success() => { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") + .fail(&format!( + "Failed to checkout main branch: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + _ => {} + } + + // Push the deterministic commit to establish state on relay + let push_output = Command::new("git") + .args(["push", "origin", "main"]) + .current_dir(&clone_path) + .env("GIT_TERMINAL_PROMPT", "0") + .output(); + + match push_output { + Err(e) => { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") + .fail(&format!("Failed to push initial commit: {}", e)); + } + Ok(output) if !output.status.success() => { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") + .fail(&format!( + "Failed to push initial commit: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + _ => {} + } + + // ============================================================ + // Step 3: TEST - Create a NEW commit that is NOT announced + // in any state event + // ============================================================ + let new_commit = match create_commit(&clone_path, "Unauthorized commit") { + Ok(h) => h, + Err(e) => { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") + .fail(&format!("Failed to create commit: {}", e)); + } + }; + + // ============================================================ + // Step 4: VERIFY - Push should be rejected because new commit + // doesn't match state event + // ============================================================ + let push_result = try_push(&clone_path); + cleanup(); match push_result { Ok(false) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").pass(), Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") .fail(&format!( "Push accepted but should be rejected. State event points to {}, but pushed {}", - setup.commit_hash, new_commit + DETERMINISTIC_COMMIT_HASH, new_commit )), Err(e) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").fail(&e), } @@ -826,32 +978,187 @@ impl PushAuthorizationTests { /// Test that non-maintainer state event is ignored /// - /// This test verifies that the relay ignores state events from non-maintainers. - /// We set up a valid repo, then create a rogue state event signed by a different - /// keypair (not the repo maintainer) that announces a different commit. The push - /// should be rejected because the rogue state event is not authorized. + /// GRASP-01: "respecting the recursive maintainer set" + /// (Conversely, state events from non-maintainers MUST be ignored) + /// + /// ## Fixture-First Pattern + /// + /// 1. **Generate**: Create TestContext and get RepoState fixture + /// (repo announcement + state event pointing to deterministic commit) + /// 2. **Send**: Clone repo, create deterministic commit, push (establishes state on relay) + /// 3. **Attack**: Create a rogue state event signed by a non-maintainer + /// 4. **Test**: Create a new commit and try to push + /// 5. **Verify**: Push should be rejected because rogue state event is ignored pub async fn test_non_maintainer_state_rejected( client: &AuditClient, git_data_dir: &Path, relay_domain: &str, ) -> TestResult { + use std::process::Command; + let test_name = "test_non_maintainer_state_rejected"; - // Set up repository with deterministic commit (signed by maintainer) - let setup = match setup_repo_with_deterministic_commit(client, git_data_dir, relay_domain).await { - Ok(s) => s, + // ============================================================ + // Step 1: GENERATE - Create TestContext and get RepoState fixture + // ============================================================ + let ctx = TestContext::new(client); + + let state_event = match ctx.get_fixture(FixtureKind::RepoState).await { + Ok(e) => e, + Err(e) => { + return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") + .fail(&format!("Failed to create RepoState fixture: {}", e)); + } + }; + + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + + // Extract repo_id and npub from state event + let repo_id = match state_event + .tags + .iter() + .find(|t| t.kind() == TagKind::d()) + .and_then(|t| t.content()) + { + Some(id) => id.to_string(), + None => { + return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") + .fail("Missing repo_id in state event"); + } + }; + + let npub = match state_event.pubkey.to_bech32() { + Ok(n) => n, + Err(e) => { + return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") + .fail(&format!("Failed to convert pubkey to bech32: {}", e)); + } + }; + + // Verify repo exists on disk + let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id)); + if !repo_path.exists() { + return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") + .fail(&format!("Repo not found: {}", repo_path.display())); + } + + // ============================================================ + // Step 2: SEND - Clone repo, create deterministic commit, push + // (establishes the state on the relay) + // ============================================================ + let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { + Ok(p) => p, + Err(e) => { + return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") + .fail(&format!("Failed to clone repo: {}", e)); + } + }; + + // Cleanup helper + let cleanup = || { + let _ = fs::remove_dir_all(&clone_path); + }; + + // Create deterministic commit locally + let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") { + Ok(h) => h, Err(e) => { + cleanup(); return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") - .fail(&format!("Setup failed: {}", e)) + .fail(&format!("Failed to create deterministic commit: {}", e)); } }; - // Create a new commit locally that we want to push - let new_commit = match create_commit(&setup.clone_path, "New commit to push") { + // Verify commit hash matches expected + if commit_hash != DETERMINISTIC_COMMIT_HASH { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") + .fail(&format!( + "Commit hash mismatch: got {}, expected {}", + commit_hash, DETERMINISTIC_COMMIT_HASH + )); + } + + // Create main branch pointing to our deterministic commit + let branch_output = Command::new("git") + .args(["branch", "main"]) + .current_dir(&clone_path) + .output(); + + match branch_output { + Err(e) => { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") + .fail(&format!("Failed to create main branch: {}", e)); + } + Ok(output) if !output.status.success() => { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") + .fail(&format!( + "Failed to create main branch: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + _ => {} + } + + // Checkout main branch + let checkout_output = Command::new("git") + .args(["checkout", "main"]) + .current_dir(&clone_path) + .output(); + + match checkout_output { + Err(e) => { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") + .fail(&format!("Failed to checkout main branch: {}", e)); + } + Ok(output) if !output.status.success() => { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") + .fail(&format!( + "Failed to checkout main branch: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + _ => {} + } + + // Push the deterministic commit to establish state on relay + let push_output = Command::new("git") + .args(["push", "origin", "main"]) + .current_dir(&clone_path) + .env("GIT_TERMINAL_PROMPT", "0") + .output(); + + match push_output { + Err(e) => { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") + .fail(&format!("Failed to push initial commit: {}", e)); + } + Ok(output) if !output.status.success() => { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") + .fail(&format!( + "Failed to push initial commit: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + _ => {} + } + + // ============================================================ + // Step 3: ATTACK - Create a new commit and a rogue state event + // from a non-maintainer + // ============================================================ + let new_commit = match create_commit(&clone_path, "New commit to push") { Ok(h) => h, Err(e) => { + cleanup(); return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") - .fail(&format!("Failed to create commit: {}", e)) + .fail(&format!("Failed to create commit: {}", e)); } }; @@ -862,7 +1169,7 @@ impl PushAuthorizationTests { // This event has the correct repo_id but is signed by a non-maintainer let rogue_state = match client .event_builder(Kind::Custom(30618), "") - .tag(Tag::identifier(&setup.repo_id)) + .tag(Tag::identifier(&repo_id)) .tag(Tag::custom( TagKind::custom("refs/heads/main"), vec![new_commit.clone()], @@ -871,13 +1178,15 @@ impl PushAuthorizationTests { { Ok(e) => e, Err(e) => { + cleanup(); return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") - .fail(&format!("Failed to build rogue state event: {}", e)) + .fail(&format!("Failed to build rogue state event: {}", e)); } }; // Send the rogue state event using the raw client to bypass AuditClient's key check if let Err(e) = client.client().send_event(&rogue_state).await { + cleanup(); return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") .fail(&format!("Failed to send rogue state event: {}", e)); } @@ -885,14 +1194,12 @@ impl PushAuthorizationTests { // Wait for event to propagate tokio::time::sleep(std::time::Duration::from_millis(200)).await; - // Try to push the new commit - // This should be REJECTED because: - // - The rogue state event announces new_commit - // - But the rogue state event is NOT signed by the maintainer - // - The relay should ignore the rogue state event - // - The valid state event (from setup) still points to the deterministic commit - // - Therefore pushing new_commit should fail - let push_result = try_push(&setup.clone_path); + // ============================================================ + // Step 4 & 5: VERIFY - Push should be rejected because rogue + // state event is ignored + // ============================================================ + let push_result = try_push(&clone_path); + cleanup(); match push_result { Ok(false) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").pass(), -- cgit v1.2.3