From 233feae6af4b291e4860a1ddf9df2ccf82e57c2f Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 27 Nov 2025 15:16:58 +0000 Subject: fix(tests): update main project tests for grasp-audit API changes --- .../src/specs/grasp01/push_authorization.rs | 606 +-------------------- 1 file changed, 6 insertions(+), 600 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 cba9e69..d58247d 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs @@ -16,612 +16,18 @@ //! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test //! ``` -use crate::{AuditClient, FixtureKind, TestContext, TestResult}; +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, +}; use nostr_sdk::prelude::*; use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; +use std::path::Path; /// Test suite for Push Authorization operations pub struct PushAuthorizationTests; -/// Helper to clone a repository and return the path -fn clone_repo( - relay_domain: &str, - npub: &str, - repo_id: &str, -) -> Result { - let temp_base = std::env::temp_dir(); - let clone_dir_name = format!("grasp-push-test-{}", uuid::Uuid::new_v4()); - let clone_path = temp_base.join(&clone_dir_name); - let _ = fs::remove_dir_all(&clone_path); - - let clone_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id); - let output = Command::new("git") - .args(["clone", &clone_url, clone_path.to_str().unwrap()]) - .env("GIT_TERMINAL_PROMPT", "0") - .output() - .map_err(|e| format!("Failed to execute git clone: {}", e))?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(format!("Git clone failed: {}", stderr)); - } - - // Configure git user - let _ = Command::new("git") - .args(["config", "user.email", "test@grasp-audit.local"]) - .current_dir(&clone_path) - .output(); - let _ = Command::new("git") - .args(["config", "user.name", "GRASP Audit Test"]) - .current_dir(&clone_path) - .output(); - - Ok(clone_path) -} - -/// Helper to create a commit and return the hash -fn create_commit(clone_path: &Path, message: &str) -> Result { - let test_file = clone_path.join(format!("test-{}.txt", uuid::Uuid::new_v4())); - fs::write(&test_file, message).map_err(|e| format!("Failed to write file: {}", e))?; - - let filename = test_file.file_name().unwrap().to_str().unwrap(); - let output = Command::new("git") - .args(["add", filename]) - .current_dir(clone_path) - .output() - .map_err(|e| format!("Git add failed: {}", e))?; - - if !output.status.success() { - return Err("Git add failed".to_string()); - } - - let output = Command::new("git") - .args(["commit", "-m", message]) - .current_dir(clone_path) - .output() - .map_err(|e| format!("Git commit failed: {}", e))?; - - if !output.status.success() { - return Err("Git commit failed".to_string()); - } - - let output = Command::new("git") - .args(["rev-parse", "HEAD"]) - .current_dir(clone_path) - .output() - .map_err(|e| format!("Git rev-parse failed: {}", e))?; - - if !output.status.success() { - return Err("Failed to get commit hash".to_string()); - } - - Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) -} - -/// Variant of deterministic commit for different pubkey types -/// Each variant produces a different but reproducible commit hash -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CommitVariant { - /// Main pubkey variant - uses "Initial commit" content - Owner, - /// Maintainer pubkey variant - uses "Maintainer initial commit" content - Maintainer, - /// Recursive maintainer pubkey variant - uses "Recursive maintainer initial commit" content - RecursiveMaintainer, -} - -impl CommitVariant { - /// Get the file content for this variant - pub fn file_content(&self) -> &'static str { - match self { - CommitVariant::Owner => "Initial commit", - CommitVariant::Maintainer => "Maintainer initial commit", - CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit", - } - } - - /// Get the commit message for this variant - pub fn commit_message(&self) -> &'static str { - match self { - CommitVariant::Owner => "Initial commit", - CommitVariant::Maintainer => "Maintainer initial commit", - CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit", - } - } -} - -/// Helper to create a deterministic commit (for fixtures) -/// Uses fixed author/committer dates and disables GPG signing to ensure consistent hash -/// -/// The variant parameter allows different commit hashes for different pubkey types: -/// - Owner: uses the original DETERMINISTIC_COMMIT_HASH -/// - Maintainer: uses MAINTAINER_DETERMINISTIC_COMMIT_HASH -/// - RecursiveMaintainer: uses RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH -pub fn create_deterministic_commit_with_variant(clone_path: &Path, variant: CommitVariant) -> Result { - let test_file = clone_path.join("test.txt"); - let content = variant.file_content(); - let message = variant.commit_message(); - - fs::write(&test_file, content).map_err(|e| format!("Failed to write file: {}", e))?; - - let output = Command::new("git") - .args(["add", "test.txt"]) - .current_dir(clone_path) - .output() - .map_err(|e| format!("Git add failed: {}", e))?; - - if !output.status.success() { - return Err("Git add failed".to_string()); - } - - // Create deterministic commit with fixed dates and GPG disabled - let output = Command::new("git") - .args([ - "-c", "commit.gpgsign=false", - "commit", - "-m", message, - ]) - .env("GIT_AUTHOR_DATE", "2024-01-01T00:00:00Z") - .env("GIT_COMMITTER_DATE", "2024-01-01T00:00:00Z") - .current_dir(clone_path) - .output() - .map_err(|e| format!("Git commit failed: {}", e))?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(format!("Git commit failed: {}", stderr)); - } - - let output = Command::new("git") - .args(["rev-parse", "HEAD"]) - .current_dir(clone_path) - .output() - .map_err(|e| format!("Git rev-parse failed: {}", e))?; - - if !output.status.success() { - return Err("Failed to get commit hash".to_string()); - } - - Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) -} - -/// Helper to create a deterministic commit (for fixtures) - uses Owner variant -/// Uses fixed author/committer dates and disables GPG signing to ensure consistent hash -pub fn create_deterministic_commit(clone_path: &Path, _message: &str) -> Result { - // Note: message parameter is ignored for backwards compatibility - // The Owner variant always uses "Initial commit" - 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 { - pub clone_path: PathBuf, - pub repo_id: String, - pub npub: String, - pub commit_hash: String, -} - -impl Drop for RepoSetup { - fn drop(&mut self) { - let _ = fs::remove_dir_all(&self.clone_path); - } -} - -/// Helper function to set up a repository with deterministic commit -/// -/// 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 -pub async fn setup_repo_with_deterministic_commit( - client: &AuditClient, - git_data_dir: &Path, - relay_domain: &str, -) -> Result { - use crate::DETERMINISTIC_COMMIT_HASH; - - 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, - }) -} - -/// Helper function to set up a maintainer repository with deterministic commit (state only) -/// -/// 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 -pub async fn setup_repo_for_maintainer( - client: &AuditClient, - git_data_dir: &Path, - relay_domain: &str, -) -> Result { - use crate::MAINTAINER_DETERMINISTIC_COMMIT_HASH; - - 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, - }) -} - -/// Helper function to set up a recursive maintainer repository with deterministic commit -/// -/// 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 -pub async fn setup_repo_for_recursive_maintainer( - client: &AuditClient, - git_data_dir: &Path, - relay_domain: &str, -) -> Result { - use crate::RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH; - - 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, - }) -} - -/// Helper to attempt a push and return success/failure -fn try_push(clone_path: &Path) -> Result { - let output = Command::new("git") - .args(["push", "origin", "main"]) - .current_dir(clone_path) - .env("GIT_TERMINAL_PROMPT", "0") - .output() - .map_err(|e| format!("Failed to execute git push: {}", e))?; - - Ok(output.status.success()) -} - impl PushAuthorizationTests { /// Test that push is authorized when state event matches the commit /// -- cgit v1.2.3