From 734d255efaa26bcb18b29d655bf30f8affb3a852 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 26 Nov 2025 15:36:12 +0000 Subject: test: use fixtures in push tests --- grasp-audit/src/specs/grasp01/git_clone.rs | 2 +- grasp-audit/src/specs/grasp01/mod.rs | 2 +- .../src/specs/grasp01/push_authorization.rs | 1608 ++++++-------------- 3 files changed, 444 insertions(+), 1168 deletions(-) (limited to 'grasp-audit/src/specs') diff --git a/grasp-audit/src/specs/grasp01/git_clone.rs b/grasp-audit/src/specs/grasp01/git_clone.rs index f85f94a..da60f26 100644 --- a/grasp-audit/src/specs/grasp01/git_clone.rs +++ b/grasp-audit/src/specs/grasp01/git_clone.rs @@ -267,7 +267,7 @@ impl GitCloneTests { #[cfg(test)] mod tests { - use super::*; + #[test] fn test_module_exists() { diff --git a/grasp-audit/src/specs/grasp01/mod.rs b/grasp-audit/src/specs/grasp01/mod.rs index 0d0bd9c..5ce5eca 100644 --- a/grasp-audit/src/specs/grasp01/mod.rs +++ b/grasp-audit/src/specs/grasp01/mod.rs @@ -13,5 +13,5 @@ pub use event_acceptance_policy::EventAcceptancePolicyTests; pub use git_clone::GitCloneTests; pub use nip01_smoke::Nip01SmokeTests; pub use nip11_document::Nip11DocumentTests; -pub use push_authorization::PushAuthorizationTests; +pub use push_authorization::{CommitVariant, PushAuthorizationTests}; pub use repository_creation::RepositoryCreationTests; diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index 5545b1a..cba9e69 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs @@ -100,11 +100,51 @@ fn create_commit(clone_path: &Path, message: &str) -> Result { 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 -pub fn create_deterministic_commit(clone_path: &Path, message: &str) -> Result { +/// +/// 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"); - fs::write(&test_file, message).map_err(|e| format!("Failed to write file: {}", e))?; + 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"]) @@ -147,6 +187,14 @@ pub fn create_deterministic_commit(clone_path: &Path, message: &str) -> Result 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 { @@ -286,6 +334,282 @@ pub async fn setup_repo_with_deterministic_commit( }) } +/// 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") @@ -426,908 +750,133 @@ impl PushAuthorizationTests { } } - /// Test that latest state event is used for authorization + /// Test push authorized by maintainer state event only (no announcement) /// - /// GRASP-01 requires that the relay use the LATEST state event (by created_at - /// timestamp) when determining push authorization. This test verifies that - /// a newer state event takes precedence over an older one. + /// GRASP-01: "respecting the recursive maintainer set" + /// This tests that a maintainer can authorize pushes with ONLY a state event, + /// without publishing their own repo announcement. The maintainer is still + /// listed in the owner's announcement, so they're a valid maintainer. /// /// Scenario: - /// 1. Owner creates repo with maintainer - /// 2. Owner publishes state event for commit_a at t=100 (older) - /// 3. Maintainer publishes state event for commit_b at t=200 (newer) - /// 4. Push commit_b should be ACCEPTED (newer timestamp wins) - /// 5. Push commit_a should be REJECTED (older state event superseded) - pub async fn test_latest_state_event_used( + /// 1. Owner's repo announcement lists maintainer in maintainers tag + /// 2. Maintainer publishes ONLY a state event (no announcement) + /// 3. setup_repo_for_maintainer() clones, creates maintainer commit, verifies hash, pushes + /// 4. The push should be ACCEPTED because maintainer's state event authorizes it + pub async fn test_push_authorized_by_maintainer_state_only( client: &AuditClient, git_data_dir: &Path, relay_domain: &str, ) -> TestResult { - let test_name = "test_latest_state_event_used"; - let description = "Latest state event takes precedence"; + let test_name = "test_push_authorized_by_maintainer_state_only"; + + // Use setup_repo_for_maintainer which publishes ONLY the state event, no announcement + match setup_repo_for_maintainer(client, git_data_dir, relay_domain).await { + Ok(_setup) => { + // Push succeeded in setup - this means the relay accepted the push + // authorized by the maintainer's state event alone + TestResult::new( + test_name, + "GRASP-01", + "Push authorized by maintainer state event only (no announcement)", + ) + .pass() + } + Err(e) => { + // Check if this was specifically a push rejection + if e.contains("Failed to push") { + TestResult::new( + test_name, + "GRASP-01", + "Push authorized by maintainer state event only (no announcement)", + ) + .fail(&format!( + "Push was rejected but should have been accepted. \ + The maintainer published a state event with a commit hash, \ + and even without a separate announcement, the relay should \ + authorize pushes matching this state event since the maintainer \ + is listed in the owner's announcement. \ + Error: {}", + e + )) + } else { + // Some other error during setup + TestResult::new( + test_name, + "GRASP-01", + "Push authorized by maintainer state event only (no announcement)", + ) + .fail(&format!("Setup failed: {}", e)) + } + } + } + } - // 1. Generate maintainer keypair - let maintainer_keys = Keys::generate(); - let maintainer_pubkey = maintainer_keys.public_key().to_hex(); + /// Test push authorized by recursive maintainer state event + /// + /// GRASP-01: "respecting the recursive maintainer set" + /// This tests recursive maintainer chains: Owner -> MaintainerA -> MaintainerB + /// + /// Scenario: + /// 1. RecursiveMaintainerRepoAndState fixture creates: + /// - Repo announcement signed by recursive_maintainer keys + /// - Lists main pubkey and maintainer pubkey in maintainers tag + /// - State event with RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH (2s in past) + /// 2. setup_repo_for_recursive_maintainer() clones, creates recursive maintainer commit, verifies hash, pushes + /// 3. The push should be ACCEPTED because recursive maintainer's state event authorizes it + pub async fn test_push_authorized_by_recursive_maintainer_state( + client: &AuditClient, + git_data_dir: &Path, + relay_domain: &str, + ) -> TestResult { + let test_name = "test_push_authorized_by_recursive_maintainer_state"; - // 2. Owner creates repo with maintainer - let repo_event = match client - .create_repo_announcement_with_maintainers(test_name, &[maintainer_pubkey.clone()]) - .await - { - Ok(e) => e, - Err(e) => { - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to create repo with maintainers: {}", e)) + // Use setup_repo_for_recursive_maintainer which leverages RecursiveMaintainerRepoAndState fixture + // This does all the heavy lifting: + // 1. Creates repo announcement signed by recursive maintainer keys + // 2. Creates state event pointing to RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH + // 3. Clones the repo + // 4. Creates the recursive maintainer deterministic commit locally + // 5. Verifies commit hash matches expected + // 6. Creates main branch, checks it out, and pushes + match setup_repo_for_recursive_maintainer(client, git_data_dir, relay_domain).await { + Ok(_setup) => { + // Push succeeded in setup - this means the relay accepted the push + // authorized by the recursive maintainer's state event + TestResult::new( + test_name, + "GRASP-01", + "Push authorized by recursive maintainer state event", + ) + .pass() + } + Err(e) => { + // Check if this was specifically a push rejection + if e.contains("Failed to push") { + TestResult::new( + test_name, + "GRASP-01", + "Push authorized by recursive maintainer state event", + ) + .fail(&format!( + "Push was rejected but should have been accepted. \ + The recursive maintainer published a state event with a commit hash, \ + and the relay should authorize pushes matching this state event \ + through recursive maintainer traversal. \ + Error: {}", + e + )) + } else { + // Some other error during setup + TestResult::new( + test_name, + "GRASP-01", + "Push authorized by recursive maintainer state event", + ) + .fail(&format!("Setup failed: {}", e)) + } } - }; - - // Send the owner's repo event - if let Err(e) = client.send_event(repo_event.clone()).await { - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to send owner repo event: {}", e)); } - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - // Extract repo details - let repo_id = match repo_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", description) - .fail("Repository event missing d tag") - } - }; - - // Get relay URL for maintainer's repo announcement - let relay_url = match client.relay_url().await { - Ok(u) => u, - Err(e) => { - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to get relay URL: {}", e)) - } - }; - let http_url = relay_url - .replace("ws://", "http://") - .replace("wss://", "https://"); - let maintainer_npub = match maintainer_keys.public_key().to_bech32() { - Ok(n) => n, - Err(e) => { - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to convert maintainer pubkey to npub: {}", e)) - } - }; - - // 3. Maintainer creates their own repo announcement (same d-tag) - let maintainer_repo_event = match client - .event_builder( - Kind::GitRepoAnnouncement, - format!("Maintainer's view of {} repository", test_name), - ) - .tag(Tag::identifier(&repo_id)) - .tag(Tag::custom( - TagKind::custom("name"), - vec![format!("{} Test Repository (Maintainer)", test_name)], - )) - .tag(Tag::custom( - TagKind::custom("clone"), - vec![format!("{}/{}/{}.git", http_url, maintainer_npub, repo_id)], - )) - .tag(Tag::custom( - TagKind::custom("relays"), - vec![relay_url.clone()], - )) - .build(&maintainer_keys) - { - Ok(e) => e, - Err(e) => { - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to build maintainer repo event: {}", e)) - } - }; - - if let Err(e) = client.client().send_event(&maintainer_repo_event).await { - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to send maintainer repo event: {}", e)); - } - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - // Verify maintainer's repo was created - let maintainer_repo_path = git_data_dir - .join(&maintainer_npub) - .join(format!("{}.git", repo_id)); - if !maintainer_repo_path.exists() { - return TestResult::new(test_name, "GRASP-01", description).fail(&format!( - "Maintainer repo not created at: {}", - maintainer_repo_path.display() - )); - } - - // 4. Clone maintainer's repo - let clone_path = match clone_repo(relay_domain, &maintainer_npub, &repo_id) { - Ok(p) => p, - Err(e) => { - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to clone maintainer repo: {}", e)) - } - }; - - // 5. Create first commit (commit_a) - this will be the one with OLDER timestamp - let commit_a = match create_commit(&clone_path, "Commit A - older state") { - Ok(h) => h, - Err(e) => { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to create commit_a: {}", e)); - } - }; - - // 6. Create second commit (commit_b) - this will be the one with NEWER timestamp - let commit_b = match create_commit(&clone_path, "Commit B - newer state") { - Ok(h) => h, - Err(e) => { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to create commit_b: {}", e)); - } - }; - - // 7. Calculate timestamps: older_timestamp (100 seconds ago) and newer_timestamp (now) - let base_time = Timestamp::now().as_u64(); - let older_timestamp = Timestamp::from(base_time - 100); // 100 seconds ago - let newer_timestamp = Timestamp::from(base_time); // now - - // 8. Owner publishes state event for commit_a at OLDER timestamp - let owner_state_event = match client - .event_builder(Kind::Custom(30618), "") - .tag(Tag::identifier(&repo_id)) - .tag(Tag::custom( - TagKind::custom("refs/heads/main"), - vec![commit_a.clone()], - )) - .custom_time(older_timestamp) - .build(client.keys()) - { - Ok(e) => e, - Err(e) => { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to build owner state event: {}", e)); - } - }; - - if let Err(e) = client.client().send_event(&owner_state_event).await { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to send owner state event: {}", e)); - } - - // 9. Maintainer publishes state event for commit_b at NEWER timestamp - let maintainer_state_event = match client - .event_builder(Kind::Custom(30618), "") - .tag(Tag::identifier(&repo_id)) - .tag(Tag::custom( - TagKind::custom("refs/heads/main"), - vec![commit_b.clone()], - )) - .custom_time(newer_timestamp) - .build(&maintainer_keys) - { - Ok(e) => e, - Err(e) => { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to build maintainer state event: {}", e)); - } - }; - - if let Err(e) = client.client().send_event(&maintainer_state_event).await { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to send maintainer state event: {}", e)); - } - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - // 10. Create and checkout main branch pointing to commit_b (the newer state) - let branch_output = Command::new("git") - .args(["branch", "main"]) - .current_dir(&clone_path) - .output(); - - if let Ok(output) = branch_output { - if !output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01", description).fail(&format!( - "Failed to create main branch: {}", - String::from_utf8_lossy(&output.stderr) - )); - } - } - - let checkout_output = Command::new("git") - .args(["checkout", "main"]) - .current_dir(&clone_path) - .output(); - - if let Ok(output) = checkout_output { - if !output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01", description).fail(&format!( - "Failed to checkout main branch: {}", - String::from_utf8_lossy(&output.stderr) - )); - } - } - - // 11. Attempt push - should be ACCEPTED because maintainer's newer state event - // announces commit_b which is now HEAD of main - let push_result = try_push(&clone_path); - let _ = fs::remove_dir_all(&clone_path); - - match push_result { - Ok(true) => TestResult::new(test_name, "GRASP-01", description).pass(), - Ok(false) => TestResult::new(test_name, "GRASP-01", description).fail(&format!( - "Push was rejected but should have been accepted. \ - The maintainer published a state event at timestamp {} announcing commit_b ({}). \ - The owner published an older state event at timestamp {} announcing commit_a ({}). \ - The relay should use the NEWER state event (maintainer's) for authorization.", - newer_timestamp.as_u64(), - commit_b, - older_timestamp.as_u64(), - commit_a - )), - Err(e) => { - TestResult::new(test_name, "GRASP-01", description).fail(&format!("Push error: {}", e)) - } - } - } - - /// Test push authorized by direct maintainer state event - /// - /// GRASP-01: "respecting the recursive maintainer set" - /// This tests the first level: direct maintainers listed in the maintainers tag. - /// - /// Scenario: - /// 1. Owner creates repo with `["maintainers", ""]` tag - /// 2. Maintainer creates their own repo announcement (same d-tag) - /// 3. Maintainer publishes state event with a commit hash - /// 4. Push to that commit should be ACCEPTED - pub async fn test_push_authorized_by_direct_maintainer_state( - client: &AuditClient, - git_data_dir: &Path, - relay_domain: &str, - ) -> TestResult { - let test_name = "test_push_authorized_by_direct_maintainer_state"; - - // 1. Generate maintainer keypair - let maintainer_keys = Keys::generate(); - let maintainer_pubkey = maintainer_keys.public_key().to_hex(); - - // 2. Owner creates repo with maintainer listed - let repo_event = match client - .create_repo_announcement_with_maintainers(test_name, &[maintainer_pubkey.clone()]) - .await - { - Ok(e) => e, - Err(e) => { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by direct maintainer state event", - ) - .fail(&format!("Failed to create repo with maintainers: {}", e)) - } - }; - - // Send the owner's repo event - if let Err(e) = client.send_event(repo_event.clone()).await { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by direct maintainer state event", - ) - .fail(&format!("Failed to send owner repo event: {}", e)); - } - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - // Extract repo details - let repo_id = match repo_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 by direct maintainer state event", - ) - .fail("Repository event missing d tag") - } - }; - - // Get relay URL for maintainer's repo announcement - let relay_url = match client.relay_url().await { - Ok(u) => u, - Err(e) => { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by direct maintainer state event", - ) - .fail(&format!("Failed to get relay URL: {}", e)) - } - }; - let http_url = relay_url - .replace("ws://", "http://") - .replace("wss://", "https://"); - let maintainer_npub = match maintainer_keys.public_key().to_bech32() { - Ok(n) => n, - Err(e) => { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by direct maintainer state event", - ) - .fail(&format!("Failed to convert maintainer pubkey to npub: {}", e)) - } - }; - - // 3. Maintainer creates their own repo announcement (same d-tag) - // This creates a separate repo at maintainer-npub/repo-id.git - let maintainer_repo_event = match client - .event_builder( - Kind::GitRepoAnnouncement, - format!("Maintainer's view of {} repository", test_name), - ) - .tag(Tag::identifier(&repo_id)) - .tag(Tag::custom( - TagKind::custom("name"), - vec![format!("{} Test Repository (Maintainer)", test_name)], - )) - .tag(Tag::custom( - TagKind::custom("clone"), - vec![format!("{}/{}/{}.git", http_url, maintainer_npub, repo_id)], - )) - .tag(Tag::custom( - TagKind::custom("relays"), - vec![relay_url.clone()], - )) - .build(&maintainer_keys) - { - Ok(e) => e, - Err(e) => { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by direct maintainer state event", - ) - .fail(&format!("Failed to build maintainer repo event: {}", e)) - } - }; - - if let Err(e) = client.client().send_event(&maintainer_repo_event).await { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by direct maintainer state event", - ) - .fail(&format!("Failed to send maintainer repo event: {}", e)); - } - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - // Verify maintainer's repo was created - let maintainer_repo_path = git_data_dir - .join(&maintainer_npub) - .join(format!("{}.git", repo_id)); - if !maintainer_repo_path.exists() { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by direct maintainer state event", - ) - .fail(&format!( - "Maintainer repo not created at: {}", - maintainer_repo_path.display() - )); - } - - // 4. Clone maintainer's repo - let clone_path = match clone_repo(relay_domain, &maintainer_npub, &repo_id) { - Ok(p) => p, - Err(e) => { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by direct maintainer state event", - ) - .fail(&format!("Failed to clone maintainer repo: {}", e)) - } - }; - - // 5. Create deterministic commit - let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") { - Ok(h) => h, - Err(e) => { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by direct maintainer state event", - ) - .fail(&format!("Failed to create commit: {}", e)); - } - }; - - // 6. Maintainer publishes state event with commit hash - let state_event = match client - .event_builder(Kind::Custom(30618), "") - .tag(Tag::identifier(&repo_id)) - .tag(Tag::custom( - TagKind::custom("refs/heads/main"), - vec![commit_hash.clone()], - )) - .build(&maintainer_keys) - { - Ok(e) => e, - Err(e) => { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by direct maintainer state event", - ) - .fail(&format!("Failed to build state event: {}", e)); - } - }; - - if let Err(e) = client.client().send_event(&state_event).await { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by direct maintainer state event", - ) - .fail(&format!("Failed to send state event: {}", e)); - } - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - // 7. Create and checkout main branch - let branch_output = Command::new("git") - .args(["branch", "main"]) - .current_dir(&clone_path) - .output(); - - if let Ok(output) = branch_output { - if !output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by direct maintainer state event", - ) - .fail(&format!( - "Failed to create main branch: {}", - String::from_utf8_lossy(&output.stderr) - )); - } - } - - let checkout_output = Command::new("git") - .args(["checkout", "main"]) - .current_dir(&clone_path) - .output(); - - if let Ok(output) = checkout_output { - if !output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by direct maintainer state event", - ) - .fail(&format!( - "Failed to checkout main branch: {}", - String::from_utf8_lossy(&output.stderr) - )); - } - } - - // 8. Attempt push - should be ACCEPTED because maintainer's state event authorizes it - let push_result = try_push(&clone_path); - let _ = fs::remove_dir_all(&clone_path); - - match push_result { - Ok(true) => TestResult::new( - test_name, - "GRASP-01", - "Push authorized by direct maintainer state event", - ) - .pass(), - Ok(false) => TestResult::new( - test_name, - "GRASP-01", - "Push authorized by direct maintainer state event", - ) - .fail(&format!( - "Push was rejected but should have been accepted. \ - The maintainer (pubkey: {}) is listed in the owner's maintainers tag \ - and published a state event announcing commit {}. \ - The relay should authorize pushes matching this state event.", - maintainer_pubkey, commit_hash - )), - Err(e) => TestResult::new( - test_name, - "GRASP-01", - "Push authorized by direct maintainer state event", - ) - .fail(&format!("Push error: {}", e)), - } - } - - /// Test push authorized by recursive maintainer state event - /// - /// GRASP-01: "respecting the recursive maintainer set" - /// This tests recursive maintainer chains: Owner -> MaintainerA -> MaintainerB - /// - /// Scenario: - /// 1. Owner creates repo with `["maintainers", ""]` tag - /// 2. MaintainerA creates their own repo announcement (same d-tag) with MaintainerB - /// 3. MaintainerB creates their own repo announcement (same d-tag, no further maintainers) - /// 4. MaintainerB publishes state event with a commit hash - /// 5. Push to that commit should be ACCEPTED (recursive maintainer chain) - pub async fn test_push_authorized_by_recursive_maintainer_state( - client: &AuditClient, - git_data_dir: &Path, - relay_domain: &str, - ) -> TestResult { - let test_name = "test_push_authorized_by_recursive_maintainer_state"; - - // 1. Generate MaintainerA and MaintainerB keypairs - let maintainer_a_keys = Keys::generate(); - let maintainer_a_pubkey = maintainer_a_keys.public_key().to_hex(); - - let maintainer_b_keys = Keys::generate(); - let maintainer_b_pubkey = maintainer_b_keys.public_key().to_hex(); - - // 2. Owner creates repo with MaintainerA listed - let repo_event = match client - .create_repo_announcement_with_maintainers(test_name, &[maintainer_a_pubkey.clone()]) - .await - { - Ok(e) => e, - Err(e) => { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!("Failed to create repo with maintainers: {}", e)) - } - }; - - // Send the owner's repo event - if let Err(e) = client.send_event(repo_event.clone()).await { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!("Failed to send owner repo event: {}", e)); - } - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - // Extract repo details - let repo_id = match repo_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 by recursive maintainer state event", - ) - .fail("Repository event missing d tag") - } - }; - - // Get relay URL for maintainers' repo announcements - let relay_url = match client.relay_url().await { - Ok(u) => u, - Err(e) => { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!("Failed to get relay URL: {}", e)) - } - }; - let http_url = relay_url - .replace("ws://", "http://") - .replace("wss://", "https://"); - - let maintainer_a_npub = match maintainer_a_keys.public_key().to_bech32() { - Ok(n) => n, - Err(e) => { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!("Failed to convert maintainer A pubkey to npub: {}", e)) - } - }; - - let maintainer_b_npub = match maintainer_b_keys.public_key().to_bech32() { - Ok(n) => n, - Err(e) => { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!("Failed to convert maintainer B pubkey to npub: {}", e)) - } - }; - - // 3. MaintainerA creates their own repo announcement (same d-tag) with MaintainerB listed - let maintainer_a_repo_event = match client - .event_builder( - Kind::GitRepoAnnouncement, - format!("MaintainerA's view of {} repository", test_name), - ) - .tag(Tag::identifier(&repo_id)) - .tag(Tag::custom( - TagKind::custom("name"), - vec![format!("{} Test Repository (MaintainerA)", test_name)], - )) - .tag(Tag::custom( - TagKind::custom("clone"), - vec![format!("{}/{}/{}.git", http_url, maintainer_a_npub, repo_id)], - )) - .tag(Tag::custom( - TagKind::custom("relays"), - vec![relay_url.clone()], - )) - .tag(Tag::custom( - TagKind::custom("maintainers"), - vec![maintainer_b_pubkey.clone()], - )) - .build(&maintainer_a_keys) - { - Ok(e) => e, - Err(e) => { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!("Failed to build maintainer A repo event: {}", e)) - } - }; - - if let Err(e) = client.client().send_event(&maintainer_a_repo_event).await { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!("Failed to send maintainer A repo event: {}", e)); - } - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - // 4. MaintainerB creates their own repo announcement (same d-tag, no further maintainers) - let maintainer_b_repo_event = match client - .event_builder( - Kind::GitRepoAnnouncement, - format!("MaintainerB's view of {} repository", test_name), - ) - .tag(Tag::identifier(&repo_id)) - .tag(Tag::custom( - TagKind::custom("name"), - vec![format!("{} Test Repository (MaintainerB)", test_name)], - )) - .tag(Tag::custom( - TagKind::custom("clone"), - vec![format!("{}/{}/{}.git", http_url, maintainer_b_npub, repo_id)], - )) - .tag(Tag::custom( - TagKind::custom("relays"), - vec![relay_url.clone()], - )) - .build(&maintainer_b_keys) - { - Ok(e) => e, - Err(e) => { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!("Failed to build maintainer B repo event: {}", e)) - } - }; - - if let Err(e) = client.client().send_event(&maintainer_b_repo_event).await { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!("Failed to send maintainer B repo event: {}", e)); - } - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - // Verify maintainer B's repo was created - let maintainer_b_repo_path = git_data_dir - .join(&maintainer_b_npub) - .join(format!("{}.git", repo_id)); - if !maintainer_b_repo_path.exists() { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!( - "Maintainer B repo not created at: {}", - maintainer_b_repo_path.display() - )); - } - - // 5. Clone maintainer B's repo - let clone_path = match clone_repo(relay_domain, &maintainer_b_npub, &repo_id) { - Ok(p) => p, - Err(e) => { - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!("Failed to clone maintainer B repo: {}", e)) - } - }; - - // 6. Create deterministic commit - let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") { - Ok(h) => h, - Err(e) => { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!("Failed to create commit: {}", e)); - } - }; - - // 7. MaintainerB publishes state event with commit hash - let state_event = match client - .event_builder(Kind::Custom(30618), "") - .tag(Tag::identifier(&repo_id)) - .tag(Tag::custom( - TagKind::custom("refs/heads/main"), - vec![commit_hash.clone()], - )) - .build(&maintainer_b_keys) - { - Ok(e) => e, - Err(e) => { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!("Failed to build state event: {}", e)); - } - }; - - if let Err(e) = client.client().send_event(&state_event).await { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!("Failed to send state event: {}", e)); - } - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - // 8. Create and checkout main branch - let branch_output = Command::new("git") - .args(["branch", "main"]) - .current_dir(&clone_path) - .output(); - - if let Ok(output) = branch_output { - if !output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!( - "Failed to create main branch: {}", - String::from_utf8_lossy(&output.stderr) - )); - } - } - - let checkout_output = Command::new("git") - .args(["checkout", "main"]) - .current_dir(&clone_path) - .output(); - - if let Ok(output) = checkout_output { - if !output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!( - "Failed to checkout main branch: {}", - String::from_utf8_lossy(&output.stderr) - )); - } - } - - // 9. Attempt push - should be ACCEPTED because recursive maintainer chain authorizes it - // Owner -> MaintainerA -> MaintainerB, and MaintainerB has published the state event - let push_result = try_push(&clone_path); - let _ = fs::remove_dir_all(&clone_path); - - match push_result { - Ok(true) => TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .pass(), - Ok(false) => TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!( - "Push was rejected but should have been accepted. \ - The recursive maintainer chain is: Owner -> MaintainerA (pubkey: {}) -> MaintainerB (pubkey: {}). \ - MaintainerB published a state event announcing commit {}. \ - The relay should authorize pushes matching this state event through recursive maintainer traversal.", - maintainer_a_pubkey, maintainer_b_pubkey, commit_hash - )), - Err(e) => TestResult::new( - test_name, - "GRASP-01", - "Push authorized by recursive maintainer state event", - ) - .fail(&format!("Push error: {}", e)), - } - } + } /// Test that non-maintainer state event is ignored /// @@ -1413,279 +962,6 @@ impl PushAuthorizationTests { Err(e) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").fail(&e), } } - - /// Test that owner's newer state event beats maintainer's older state event - /// - /// GRASP-01 requires that the relay use the LATEST state event (by created_at - /// timestamp) when determining push authorization. This test is the MIRROR of - /// test_latest_state_event_used - confirming that timestamp is the deciding factor, - /// not who authored the state event. - /// - /// Scenario: - /// 1. Owner creates repo with maintainer - /// 2. Maintainer publishes state event for commit_a at t=100 (older) - /// 3. Owner publishes state event for commit_b at t=200 (newer) - /// 4. Push commit_b should be ACCEPTED (owner's newer state wins) - /// 5. Push commit_a should be REJECTED (maintainer's older state superseded) - /// - /// Key difference from test_latest_state_event_used: - /// - Task 8: Owner=older, Maintainer=newer → Maintainer wins - /// - Task 9: Maintainer=older, Owner=newer → Owner wins - /// - **This confirms symmetry**: timestamp is the deciding factor - pub async fn test_owner_newer_state_beats_maintainer( - client: &AuditClient, - git_data_dir: &Path, - relay_domain: &str, - ) -> TestResult { - let test_name = "test_owner_newer_state_beats_maintainer"; - let description = "Owner's newer state event beats maintainer's older state"; - - // 1. Generate maintainer keypair - let maintainer_keys = Keys::generate(); - let maintainer_pubkey = maintainer_keys.public_key().to_hex(); - - // 2. Owner creates repo with maintainer - let repo_event = match client - .create_repo_announcement_with_maintainers(test_name, &[maintainer_pubkey.clone()]) - .await - { - Ok(e) => e, - Err(e) => { - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to create repo with maintainers: {}", e)) - } - }; - - // Send the owner's repo event - if let Err(e) = client.send_event(repo_event.clone()).await { - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to send owner repo event: {}", e)); - } - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - // Extract repo details - let repo_id = match repo_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", description) - .fail("Repository event missing d tag") - } - }; - - // Get relay URL for maintainer's repo announcement - let relay_url = match client.relay_url().await { - Ok(u) => u, - Err(e) => { - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to get relay URL: {}", e)) - } - }; - let http_url = relay_url - .replace("ws://", "http://") - .replace("wss://", "https://"); - let maintainer_npub = match maintainer_keys.public_key().to_bech32() { - Ok(n) => n, - Err(e) => { - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to convert maintainer pubkey to npub: {}", e)) - } - }; - - // 3. Maintainer creates their own repo announcement (same d-tag) - let maintainer_repo_event = match client - .event_builder( - Kind::GitRepoAnnouncement, - format!("Maintainer's view of {} repository", test_name), - ) - .tag(Tag::identifier(&repo_id)) - .tag(Tag::custom( - TagKind::custom("name"), - vec![format!("{} Test Repository (Maintainer)", test_name)], - )) - .tag(Tag::custom( - TagKind::custom("clone"), - vec![format!("{}/{}/{}.git", http_url, maintainer_npub, repo_id)], - )) - .tag(Tag::custom( - TagKind::custom("relays"), - vec![relay_url.clone()], - )) - .build(&maintainer_keys) - { - Ok(e) => e, - Err(e) => { - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to build maintainer repo event: {}", e)) - } - }; - - if let Err(e) = client.client().send_event(&maintainer_repo_event).await { - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to send maintainer repo event: {}", e)); - } - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - // Verify maintainer's repo was created - let maintainer_repo_path = git_data_dir - .join(&maintainer_npub) - .join(format!("{}.git", repo_id)); - if !maintainer_repo_path.exists() { - return TestResult::new(test_name, "GRASP-01", description).fail(&format!( - "Maintainer repo not created at: {}", - maintainer_repo_path.display() - )); - } - - // 4. Clone maintainer's repo - let clone_path = match clone_repo(relay_domain, &maintainer_npub, &repo_id) { - Ok(p) => p, - Err(e) => { - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to clone maintainer repo: {}", e)) - } - }; - - // 5. Create first commit (commit_a) - MAINTAINER will announce this with OLDER timestamp - let commit_a = match create_commit(&clone_path, "Commit A - older state (maintainer)") { - Ok(h) => h, - Err(e) => { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to create commit_a: {}", e)); - } - }; - - // 6. Create second commit (commit_b) - OWNER will announce this with NEWER timestamp - let commit_b = match create_commit(&clone_path, "Commit B - newer state (owner)") { - Ok(h) => h, - Err(e) => { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to create commit_b: {}", e)); - } - }; - - // 7. Calculate timestamps: older_timestamp (100 seconds ago) and newer_timestamp (now) - let base_time = Timestamp::now().as_u64(); - let older_timestamp = Timestamp::from(base_time - 100); // 100 seconds ago - for MAINTAINER - let newer_timestamp = Timestamp::from(base_time); // now - for OWNER - - // 8. MAINTAINER publishes state event for commit_a at OLDER timestamp - // This is the KEY DIFFERENCE from test_latest_state_event_used: - // - In Task 8: Owner was older, Maintainer was newer - // - In Task 9 (this test): Maintainer is older, Owner is newer - let maintainer_state_event = match client - .event_builder(Kind::Custom(30618), "") - .tag(Tag::identifier(&repo_id)) - .tag(Tag::custom( - TagKind::custom("refs/heads/main"), - vec![commit_a.clone()], - )) - .custom_time(older_timestamp) - .build(&maintainer_keys) - { - Ok(e) => e, - Err(e) => { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to build maintainer state event: {}", e)); - } - }; - - if let Err(e) = client.client().send_event(&maintainer_state_event).await { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to send maintainer state event: {}", e)); - } - - // 9. OWNER publishes state event for commit_b at NEWER timestamp - let owner_state_event = match client - .event_builder(Kind::Custom(30618), "") - .tag(Tag::identifier(&repo_id)) - .tag(Tag::custom( - TagKind::custom("refs/heads/main"), - vec![commit_b.clone()], - )) - .custom_time(newer_timestamp) - .build(client.keys()) - { - Ok(e) => e, - Err(e) => { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to build owner state event: {}", e)); - } - }; - - if let Err(e) = client.client().send_event(&owner_state_event).await { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01", description) - .fail(&format!("Failed to send owner state event: {}", e)); - } - - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - - // 10. Create and checkout main branch pointing to commit_b (the newer state) - let branch_output = Command::new("git") - .args(["branch", "main"]) - .current_dir(&clone_path) - .output(); - - if let Ok(output) = branch_output { - if !output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01", description).fail(&format!( - "Failed to create main branch: {}", - String::from_utf8_lossy(&output.stderr) - )); - } - } - - let checkout_output = Command::new("git") - .args(["checkout", "main"]) - .current_dir(&clone_path) - .output(); - - if let Ok(output) = checkout_output { - if !output.status.success() { - let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01", description).fail(&format!( - "Failed to checkout main branch: {}", - String::from_utf8_lossy(&output.stderr) - )); - } - } - - // 11. Attempt push - should be ACCEPTED because owner's newer state event - // announces commit_b which is now HEAD of main - let push_result = try_push(&clone_path); - let _ = fs::remove_dir_all(&clone_path); - - match push_result { - Ok(true) => TestResult::new(test_name, "GRASP-01", description).pass(), - Ok(false) => TestResult::new(test_name, "GRASP-01", description).fail(&format!( - "Push was rejected but should have been accepted. \ - The OWNER published a state event at timestamp {} announcing commit_b ({}). \ - The MAINTAINER published an older state event at timestamp {} announcing commit_a ({}). \ - The relay should use the NEWER state event (owner's) for authorization. \ - This confirms symmetry with test_latest_state_event_used: timestamp is the deciding factor.", - newer_timestamp.as_u64(), - commit_b, - older_timestamp.as_u64(), - commit_a - )), - Err(e) => { - TestResult::new(test_name, "GRASP-01", description).fail(&format!("Push error: {}", e)) - } - } - } } #[cfg(test)] -- cgit v1.2.3