From bf93f737aeec7b0ba6d007e867a55a8528615c23 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 27 Nov 2025 15:49:27 +0000 Subject: Task 2: Refactor owner push authorization test to fixture-first pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactored test_push_authorized_by_owner_state to use fixture-first pattern - Test now creates its own TestContext and uses FixtureKind::RepoState - Uses git helper functions from fixtures.rs (clone_repo, create_deterministic_commit, try_push) - Follows the 3-step pattern: Generate fixtures → Send to relay → Verify behavior - Deprecated setup_repo_with_deterministic_commit with migration guide - Test passes: cargo test --test push_authorization test_push_authorized_by_owner_state - No API changes required for main project tests --- .../src/specs/grasp01/push_authorization.rs | 168 +++++++++++++++++++-- 1 file changed, 159 insertions(+), 9 deletions(-) (limited to 'grasp-audit/src/specs/grasp01/push_authorization.rs') diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index d58247d..7b7e9dc 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs @@ -17,9 +17,9 @@ //! ``` use crate::{ - clone_repo, create_commit, setup_repo_for_maintainer, setup_repo_for_recursive_maintainer, - setup_repo_with_deterministic_commit, try_push, AuditClient, FixtureKind, TestContext, - TestResult, + clone_repo, create_commit, create_deterministic_commit, setup_repo_for_maintainer, + setup_repo_for_recursive_maintainer, setup_repo_with_deterministic_commit, try_push, + AuditClient, FixtureKind, TestContext, TestResult, DETERMINISTIC_COMMIT_HASH, }; use nostr_sdk::prelude::*; use std::fs; @@ -33,23 +33,173 @@ impl PushAuthorizationTests { /// /// GRASP-01: "MUST accept pushes via this service that match the latest /// repo state announcement on the relay" + /// + /// ## 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 locally, push to relay + /// 3. **Verify**: Push should succeed because state event authorizes this commit pub async fn test_push_authorized_by_owner_state( client: &AuditClient, git_data_dir: &Path, relay_domain: &str, ) -> TestResult { + use std::process::Command; + let test_name = "test_push_authorized_by_owner_state"; - // this setup is exactly what we are testing - match setup_repo_with_deterministic_commit(client, git_data_dir, relay_domain).await { - Ok(_) => { - return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state").pass() - }, + // ============================================================ + // 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 authorized with matching state") + .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 authorized with matching state") + .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 authorized with matching state") + .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", "Push authorized with matching state") + .fail(&format!("Repo not found: {}", repo_path.display())); + } + + // ============================================================ + // Step 2: SEND - Clone repo, create deterministic commit, push + // ============================================================ + let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { + Ok(p) => p, + Err(e) => { + return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") + .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 authorized with matching state") - .fail(&format!("Failed: {}", e)) + .fail(&format!("Failed to create deterministic commit: {}", e)); } }; + + // Verify commit hash matches expected + if commit_hash != DETERMINISTIC_COMMIT_HASH { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") + .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 authorized with matching state") + .fail(&format!("Failed to create main branch: {}", e)); + } + Ok(output) if !output.status.success() => { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") + .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 authorized with matching state") + .fail(&format!("Failed to checkout main branch: {}", e)); + } + Ok(output) if !output.status.success() => { + cleanup(); + return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") + .fail(&format!( + "Failed to checkout main branch: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + _ => {} + } + + // ============================================================ + // Step 3: VERIFY - Push should succeed because state event + // authorizes this commit + // ============================================================ + let push_result = try_push(&clone_path); + cleanup(); + + match push_result { + Ok(true) => { + TestResult::new(test_name, "GRASP-01", "Push authorized with matching state").pass() + } + Ok(false) => { + TestResult::new(test_name, "GRASP-01", "Push authorized with matching state").fail( + &format!( + "Push was rejected but should have been accepted. \ + The state event points to commit {} which matches the pushed commit.", + DETERMINISTIC_COMMIT_HASH + ), + ) + } + Err(e) => { + TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") + .fail(&format!("Push error: {}", e)) + } + } } /// Test that push is rejected when no state event exists -- cgit v1.2.3