From b94262161df99966fbb8aa6861fb46603039111f Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 28 Nov 2025 12:40:31 +0000 Subject: allow push to ref/nostr/ --- grasp-audit/src/fixtures.rs | 38 ++++ grasp-audit/src/lib.rs | 2 +- .../src/specs/grasp01/push_authorization.rs | 242 ++++++++++++++++++++- 3 files changed, 280 insertions(+), 2 deletions(-) (limited to 'grasp-audit') diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index 8cee964..b6fbc79 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs @@ -1208,6 +1208,44 @@ pub fn try_push(clone_path: &Path) -> Result { Ok(output.status.success()) } +/// Attempt a git push to a specific ref and return success/failure +/// +/// This is used for testing refs/nostr/ push validation. +/// +/// # Arguments +/// * `clone_path` - Path to the git repository +/// * `ref_name` - The ref to push to (e.g., "refs/nostr/") +/// +/// # Returns +/// * `Ok(true)` - Push succeeded +/// * `Ok(false)` - Push was rejected +/// * `Err(String)` - Error executing git push +/// +/// # Example +/// ```no_run +/// # use grasp_audit::*; +/// # use std::path::Path; +/// # fn example() -> Result<(), String> { +/// let success = try_push_to_ref(Path::new("/tmp/my-repo"), "refs/nostr/abc123")?; +/// if success { +/// println!("Push to refs/nostr/abc123 succeeded"); +/// } else { +/// println!("Push was rejected"); +/// } +/// # Ok(()) +/// # } +/// ``` +pub fn try_push_to_ref(clone_path: &Path, ref_name: &str) -> Result { + let output = Command::new("git") + .args(["push", "origin", &format!("HEAD:{}", ref_name)]) + .current_dir(clone_path) + .env("GIT_TERMINAL_PROMPT", "0") + .output() + .map_err(|e| format!("Failed to execute git push: {}", e))?; + + Ok(output.status.success()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/grasp-audit/src/lib.rs b/grasp-audit/src/lib.rs index 3d11bd8..5ee93b3 100644 --- a/grasp-audit/src/lib.rs +++ b/grasp-audit/src/lib.rs @@ -40,7 +40,7 @@ pub use client::AuditClient; pub use fixtures::{ // Git operation helpers clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, - try_push, + try_push, try_push_to_ref, // Verification helpers send_and_verify_accepted, send_and_verify_rejected, // Types and constants diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index 0a5b1ec..0170f6e 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs @@ -18,7 +18,7 @@ use crate::{ clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, - try_push, AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, + try_push, try_push_to_ref, AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, }; @@ -41,6 +41,8 @@ impl PushAuthorizationTests { results.add(Self::test_push_rejected_wrong_commit(client, relay_domain).await); results.add(Self::test_push_authorized_by_maintainer_state_only(client, relay_domain).await); results.add(Self::test_push_authorized_by_recursive_maintainer_state(client, relay_domain).await); + results.add(Self::test_push_to_refs_nostr_valid_event_id(client, relay_domain).await); + results.add(Self::test_push_to_refs_nostr_invalid_event_id(client, relay_domain).await); results } @@ -1029,6 +1031,244 @@ impl PushAuthorizationTests { Err(e) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").fail(&e), } } + + /// Test that push to refs/nostr/ succeeds with valid EventId format + /// + /// GRASP-01: "MUST accept pushes via this service to `refs/nostr/`" + /// The event_id must parse as a valid rust-nostr EventId (64-char hex string). + /// This does NOT require the ref to be listed in any state event - it's purely format validation. + /// + /// ## Fixture-First Pattern + /// + /// 1. **Generate**: Create repo with ValidRepo fixture (no state event needed) + /// 2. **Send**: Clone repo, create commit, push to refs/nostr/ + /// 3. **Verify**: Push should succeed because event-id format is valid + pub async fn test_push_to_refs_nostr_valid_event_id( + client: &AuditClient, + relay_domain: &str, + ) -> TestResult { + let test_name = "test_push_to_refs_nostr_valid_event_id"; + + // ============================================================ + // Step 1: GENERATE - Create repo (no state event needed for refs/nostr/) + // ============================================================ + let ctx = TestContext::new(client); + + let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + Ok(r) => r, + Err(e) => { + return TestResult::new( + test_name, + "GRASP-01", + "Push to refs/nostr/ accepted", + ) + .fail(&format!("Failed to create repo: {}", e)); + } + }; + + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + + let repo_id = repo + .tags + .iter() + .find(|t| t.kind() == TagKind::d()) + .and_then(|t| t.content()) + .unwrap() + .to_string(); + let npub = repo.pubkey.to_bech32().unwrap(); + + // ============================================================ + // Step 2: SEND - Clone repo, create commit, push to refs/nostr/ + // ============================================================ + let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { + Ok(p) => p, + Err(e) => { + return TestResult::new( + test_name, + "GRASP-01", + "Push to refs/nostr/ accepted", + ) + .fail(&e); + } + }; + let cleanup = || { + let _ = fs::remove_dir_all(&clone_path); + }; + + // Create a unique commit + if let Err(e) = create_commit(&clone_path, "Test commit for refs/nostr push") { + cleanup(); + return TestResult::new( + test_name, + "GRASP-01", + "Push to refs/nostr/ accepted", + ) + .fail(&e); + } + + // Generate a random event to get a valid EventId + let keys = Keys::generate(); + let event = match EventBuilder::text_note("test") + .sign(&keys) + .await + { + Ok(e) => e, + Err(e) => { + cleanup(); + return TestResult::new( + test_name, + "GRASP-01", + "Push to refs/nostr/ accepted", + ) + .fail(&format!("Failed to create test event: {}", e)); + } + }; + + // Use the event id as the refs/nostr/ target + let ref_name = format!("refs/nostr/{}", event.id); + + // ============================================================ + // Step 3: VERIFY - Push should succeed with valid event-id format + // ============================================================ + let push_result = try_push_to_ref(&clone_path, &ref_name); + cleanup(); + + match push_result { + Ok(true) => TestResult::new( + test_name, + "GRASP-01", + "Push to refs/nostr/ accepted", + ) + .pass(), + Ok(false) => TestResult::new( + test_name, + "GRASP-01", + "Push to refs/nostr/ accepted", + ) + .fail(&format!( + "Push to {} was rejected but should be accepted. \ + The event-id '{}' is a valid 64-character hex string (EventId format).", + ref_name, event.id + )), + Err(e) => TestResult::new( + test_name, + "GRASP-01", + "Push to refs/nostr/ accepted", + ) + .fail(&format!("Push error: {}", e)), + } + } + + /// Test that push to refs/nostr/ is rejected with invalid EventId format + /// + /// GRASP-01: "MUST accept pushes via this service to `refs/nostr/`" + /// The event_id must parse as a valid rust-nostr EventId (64-char hex string). + /// Invalid formats (too short, non-hex, etc.) should be rejected. + /// + /// ## Fixture-First Pattern + /// + /// 1. **Generate**: Create repo with ValidRepo fixture (no state event needed) + /// 2. **Send**: Clone repo, create commit, try to push to refs/nostr/123 (invalid) + /// 3. **Verify**: Push should be rejected because event-id format is invalid + pub async fn test_push_to_refs_nostr_invalid_event_id( + client: &AuditClient, + relay_domain: &str, + ) -> TestResult { + let test_name = "test_push_to_refs_nostr_invalid_event_id"; + + // ============================================================ + // Step 1: GENERATE - Create repo (no state event needed for refs/nostr/) + // ============================================================ + let ctx = TestContext::new(client); + + let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + Ok(r) => r, + Err(e) => { + return TestResult::new( + test_name, + "GRASP-01", + "Push to refs/nostr/ rejected", + ) + .fail(&format!("Failed to create repo: {}", e)); + } + }; + + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + + let repo_id = repo + .tags + .iter() + .find(|t| t.kind() == TagKind::d()) + .and_then(|t| t.content()) + .unwrap() + .to_string(); + let npub = repo.pubkey.to_bech32().unwrap(); + + // ============================================================ + // Step 2: SEND - Clone repo, create commit, try push to invalid ref + // ============================================================ + let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { + Ok(p) => p, + Err(e) => { + return TestResult::new( + test_name, + "GRASP-01", + "Push to refs/nostr/ rejected", + ) + .fail(&e); + } + }; + let cleanup = || { + let _ = fs::remove_dir_all(&clone_path); + }; + + // Create a unique commit + if let Err(e) = create_commit(&clone_path, "Test commit for invalid refs/nostr push") { + cleanup(); + return TestResult::new( + test_name, + "GRASP-01", + "Push to refs/nostr/ rejected", + ) + .fail(&e); + } + + // Use an invalid event-id (too short, not a valid 64-char hex) + let invalid_event_id = "123"; + let ref_name = format!("refs/nostr/{}", invalid_event_id); + + // ============================================================ + // Step 3: VERIFY - Push should be rejected with invalid event-id format + // ============================================================ + let push_result = try_push_to_ref(&clone_path, &ref_name); + cleanup(); + + match push_result { + Ok(false) => TestResult::new( + test_name, + "GRASP-01", + "Push to refs/nostr/ rejected", + ) + .pass(), + Ok(true) => TestResult::new( + test_name, + "GRASP-01", + "Push to refs/nostr/ rejected", + ) + .fail(&format!( + "Push to {} was accepted but should be rejected. \ + The event-id '{}' is NOT a valid 64-character hex string (EventId format). \ + The relay should reject pushes to refs/nostr/ with invalid event-id format.", + ref_name, invalid_event_id + )), + Err(e) => TestResult::new( + test_name, + "GRASP-01", + "Push to refs/nostr/ rejected", + ) + .fail(&format!("Push error: {}", e)), + } + } } #[cfg(test)] -- cgit v1.2.3