From d07dc0e3b14b8464e47f5ab009552eacda568a36 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 12 Feb 2026 11:23:48 +0000 Subject: fix: use consistent git identity for PR test commit hash The PR_TEST_COMMIT_HASH constant was incorrect because the discovery test used a different git identity (pr-test@example.com) than the actual create_pr_test_commit function (test@grasp-audit.local from fixtures.rs). This caused the same commit content to produce different hashes due to different author/committer info being embedded in the commit object. Fixed by updating the discovery test to use the same git identity as clone_repo() in fixtures.rs, ensuring consistent commit hashes. --- grasp-audit/src/specs/grasp01/push_authorization.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 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 c1003b9..677af89 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs @@ -1701,16 +1701,16 @@ mod tests { String::from_utf8_lossy(&output.stderr) ); - // Configure git user - use PR Test Author identity + // Configure git user - use same identity as clone_repo in fixtures.rs let output = Command::new("git") - .args(["config", "user.email", "pr-test@example.com"]) + .args(["config", "user.email", "test@grasp-audit.local"]) .current_dir(path) .output() .expect("git config email failed"); assert!(output.status.success(), "git config email failed"); let output = Command::new("git") - .args(["config", "user.name", "PR Test Author"]) + .args(["config", "user.name", "GRASP Audit Test"]) .current_dir(path) .output() .expect("git config name failed"); -- cgit v1.2.3 From 3fd6ce4149d567c67009b0332ca76c0cd6f51055 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 12 Feb 2026 12:36:23 +0000 Subject: refactor(grasp-audit): introduce SpecRef enum for type-safe spec references Replace string-based spec references with typed SpecRef enum for compile-time validation and better IDE support. TestResult::new() now accepts SpecRef enum plus a requirement description string for test-specific context. --- grasp-audit/src/result.rs | 43 +++-- grasp-audit/src/specs/grasp01/cors.rs | 43 ++--- .../src/specs/grasp01/event_acceptance_policy.rs | 45 ++--- grasp-audit/src/specs/grasp01/git_clone.rs | 43 ++--- grasp-audit/src/specs/grasp01/git_filter.rs | 37 ++-- grasp-audit/src/specs/grasp01/mod.rs | 4 +- grasp-audit/src/specs/grasp01/nip01_smoke.rs | 25 +-- grasp-audit/src/specs/grasp01/nip11_document.rs | 17 +- .../src/specs/grasp01/push_authorization.rs | 196 +++++++++++---------- .../src/specs/grasp01/repository_creation.rs | 29 +-- grasp-audit/src/specs/grasp01/spec_requirements.rs | 150 +++++++++++++--- 11 files changed, 388 insertions(+), 244 deletions(-) (limited to 'grasp-audit/src/specs/grasp01/push_authorization.rs') diff --git a/grasp-audit/src/result.rs b/grasp-audit/src/result.rs index ae3ef26..0c3ec08 100644 --- a/grasp-audit/src/result.rs +++ b/grasp-audit/src/result.rs @@ -1,6 +1,6 @@ //! Test result types -use crate::specs::grasp01::{get_sections, GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID}; +use crate::specs::grasp01::{get_sections, SpecRef, GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID}; use std::collections::BTreeMap; use std::time::{Duration, Instant}; @@ -68,10 +68,16 @@ pub struct TestResult { impl TestResult { /// Create a new test result - pub fn new(name: &str, spec_ref: &str, requirement: &str) -> Self { + /// + /// # Arguments + /// * `name` - Test name identifier + /// * `spec_ref` - Reference to the spec requirement being tested + /// * `requirement` - Human-readable description of what this test validates + /// (can be more specific than the general spec text) + pub fn new(name: &str, spec_ref: SpecRef, requirement: &str) -> Self { TestResult { name: name.to_string(), - spec_ref: spec_ref.to_string(), + spec_ref: spec_ref.spec_ref_string().to_string(), requirement: requirement.to_string(), passed: false, error: None, @@ -293,9 +299,13 @@ mod tests { #[tokio::test] async fn test_result_pass() { - let result = TestResult::new("test", "SPEC:1", "Must work") - .run(|| async { Ok(()) }) - .await; + let result = TestResult::new( + "test", + SpecRef::NostrRelayNip01Compliant, + "Test requirement", + ) + .run(|| async { Ok(()) }) + .await; assert!(result.passed); assert!(result.error.is_none()); @@ -303,9 +313,13 @@ mod tests { #[tokio::test] async fn test_result_fail() { - let result = TestResult::new("test", "SPEC:1", "Must work") - .run(|| async { Err("Failed".to_string()) }) - .await; + let result = TestResult::new( + "test", + SpecRef::NostrRelayNip01Compliant, + "Test requirement", + ) + .run(|| async { Err("Failed".to_string()) }) + .await; assert!(!result.passed); assert_eq!(result.error, Some("Failed".to_string())); @@ -315,8 +329,15 @@ mod tests { fn test_audit_result() { let mut audit = AuditResult::new("Test Spec"); - audit.add(TestResult::new("test1", "SPEC:1", "Req1").pass()); - audit.add(TestResult::new("test2", "SPEC:2", "Req2").fail("Error")); + audit.add(TestResult::new("test1", SpecRef::NostrRelayNip01Compliant, "Test 1").pass()); + audit.add( + TestResult::new( + "test2", + SpecRef::NostrRelayRejectMissingCloneRelays, + "Test 2", + ) + .fail("Error"), + ); assert_eq!(audit.total_count(), 2); assert_eq!(audit.passed_count(), 1); diff --git a/grasp-audit/src/specs/grasp01/cors.rs b/grasp-audit/src/specs/grasp01/cors.rs index f8b5f3b..eba9e42 100644 --- a/grasp-audit/src/specs/grasp01/cors.rs +++ b/grasp-audit/src/specs/grasp01/cors.rs @@ -14,6 +14,7 @@ //! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test //! ``` +use crate::specs::grasp01::SpecRef; use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; use nostr_sdk::prelude::*; @@ -44,7 +45,7 @@ impl CorsTests { pub async fn test_cors_allow_origin(_client: &AuditClient, relay_domain: &str) -> TestResult { TestResult::new( "cors_allow_origin", - "GRASP-01:git-http:cors:50", + SpecRef::CorsAllowOrigin, "Access-Control-Allow-Origin: * on all responses", ) .run(|| { @@ -90,7 +91,7 @@ impl CorsTests { pub async fn test_cors_allow_methods(_client: &AuditClient, relay_domain: &str) -> TestResult { TestResult::new( "cors_allow_methods", - "GRASP-01:git-http:cors:51", + SpecRef::CorsAllowMethods, "Access-Control-Allow-Methods: GET, POST on all responses", ) .run(|| { @@ -134,7 +135,7 @@ impl CorsTests { pub async fn test_cors_allow_headers(_client: &AuditClient, relay_domain: &str) -> TestResult { TestResult::new( "cors_allow_headers", - "GRASP-01:git-http:cors:52", + SpecRef::CorsAllowHeaders, "Access-Control-Allow-Headers: Content-Type on all responses", ) .run(|| { @@ -181,8 +182,8 @@ impl CorsTests { ) -> TestResult { TestResult::new( "cors_options_preflight", - "GRASP-01:git-http:cors:53", - "OPTIONS requests return 204 No Content", + SpecRef::CorsOptionsResponse, + "OPTIONS requests return 204 No Content with CORS headers", ) .run(|| { let relay_domain = relay_domain.to_string(); @@ -250,8 +251,8 @@ impl CorsTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01", - "CORS headers on real repository endpoint", + SpecRef::CorsAllowOrigin, + "CORS headers on real repository endpoints", ) .fail(format!("Failed to create repo fixture: {}", e)) } @@ -271,8 +272,8 @@ impl CorsTests { None => { return TestResult::new( test_name, - "GRASP-01", - "CORS headers on real repository endpoint", + SpecRef::CorsAllowOrigin, + "CORS headers on real repository endpoints", ) .fail("Repository announcement missing d tag") } @@ -283,8 +284,8 @@ impl CorsTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01", - "CORS headers on real repository endpoint", + SpecRef::CorsAllowOrigin, + "CORS headers on real repository endpoints", ) .fail(format!("Failed to convert pubkey to npub: {}", e)) } @@ -302,8 +303,8 @@ impl CorsTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01", - "CORS headers on real repository endpoint", + SpecRef::CorsAllowOrigin, + "CORS headers on real repository endpoints", ) .fail(format!("Failed to GET info/refs: {}", e)) } @@ -313,8 +314,8 @@ impl CorsTests { if let Err(e) = check_cors_allow_origin(&response, "info/refs") { return TestResult::new( test_name, - "GRASP-01", - "CORS headers on real repository endpoint", + SpecRef::CorsAllowOrigin, + "CORS headers on real repository endpoints", ) .fail(&e); } @@ -322,8 +323,8 @@ impl CorsTests { if let Err(e) = check_cors_allow_methods(&response, "info/refs") { return TestResult::new( test_name, - "GRASP-01", - "CORS headers on real repository endpoint", + SpecRef::CorsAllowMethods, + "CORS headers on real repository endpoints", ) .fail(&e); } @@ -331,16 +332,16 @@ impl CorsTests { if let Err(e) = check_cors_allow_headers(&response, "info/refs") { return TestResult::new( test_name, - "GRASP-01", - "CORS headers on real repository endpoint", + SpecRef::CorsAllowHeaders, + "CORS headers on real repository endpoints", ) .fail(&e); } TestResult::new( test_name, - "GRASP-01", - "CORS headers on real repository endpoint", + SpecRef::CorsAllowOrigin, + "CORS headers on real repository endpoints", ) .pass() } diff --git a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs index 5b697d8..8259283 100644 --- a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs +++ b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs @@ -92,6 +92,7 @@ //! - Transitive tests verify multi-hop acceptance chains use crate::fixtures::{send_and_verify_accepted, send_and_verify_rejected}; +use crate::specs::grasp01::SpecRef; use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp, ToBech32}; use std::time::Duration; @@ -148,8 +149,8 @@ impl EventAcceptancePolicyTests { pub async fn test_accept_valid_repo_announcement(client: &AuditClient) -> TestResult { TestResult::new( "accept_valid_repo_announcement", - "GRASP-01:nostr-relay:7", - "Accept valid repository announcements with service in clone and relays tags", + SpecRef::NostrRelayNip01Compliant, + "MUST accept repo announcements listing service in clone & relays tags", ) .run(|| async { // Create TestContext for mode-aware fixture management @@ -253,8 +254,8 @@ impl EventAcceptancePolicyTests { ) -> TestResult { TestResult::new( "reject_repo_announcement_missing_clone_tag", - "GRASP-01:nostr-relay:9", - "Reject repository announcements without service in clone tag", + SpecRef::NostrRelayRejectMissingCloneRelays, + "MUST reject announcements not listing service in clone tag", ) .run(|| async { // Get relay URL from client @@ -329,8 +330,8 @@ impl EventAcceptancePolicyTests { ) -> TestResult { TestResult::new( "reject_repo_announcement_missing_relays_tag", - "GRASP-01:nostr-relay:9", - "Reject repository announcements without service in relays tag", + SpecRef::NostrRelayRejectMissingCloneRelays, + "MUST reject announcements not listing service in relays tag", ) .run(|| async { // Get relay URL from client @@ -425,8 +426,8 @@ impl EventAcceptancePolicyTests { ) -> TestResult { TestResult::new( "accept_recursive_maintainer_announcement_without_service", - "GRASP-01:nostr-relay:9", - "Accept recursive maintainer announcement for chain discovery (even without GRASP server in clone)", + SpecRef::NostrRelayRejectMissingCloneRelays, + "MUST accept recursive maintainer announcements for chain discovery", ) .run(|| async { // Create TestContext for mode-aware fixture management @@ -593,7 +594,7 @@ impl EventAcceptancePolicyTests { pub async fn test_accept_issue_via_a_tag(client: &AuditClient) -> TestResult { TestResult::new( "accept_issue_via_a_tag", - "GRASP-01:nostr-relay:13", + SpecRef::NostrRelayMustAcceptTaggedEvents, "Accept issue referencing repo via 'a' tag", ) .run(|| async { @@ -628,7 +629,7 @@ impl EventAcceptancePolicyTests { pub async fn test_accept_comment_via_capital_a_tag(client: &AuditClient) -> TestResult { TestResult::new( "accept_comment_via_A_tag", - "GRASP-01:nostr-relay:13", + SpecRef::NostrRelayMustAcceptTaggedEvents, "Accept NIP-22 comment with root 'A' tag referencing repo", ) .run(|| async { @@ -681,8 +682,8 @@ impl EventAcceptancePolicyTests { pub async fn test_accept_kind1_via_q_tag(client: &AuditClient) -> TestResult { TestResult::new( "accept_kind1_via_q_tag", - "GRASP-01:nostr-relay:13", - "Accept kind 1 note quoting repo via 'q' tag", + SpecRef::NostrRelayMustAcceptTaggedEvents, + "Accept kind 1 text note quoting repo via 'q' tag", ) .run(|| async { // Create TestContext @@ -731,8 +732,8 @@ impl EventAcceptancePolicyTests { pub async fn test_accept_issue_quoting_issue_via_q(client: &AuditClient) -> TestResult { TestResult::new( "accept_issue_quoting_issue_via_q", - "GRASP-01:nostr-relay:13", - "Accept issue quoting accepted issue (transitive)", + SpecRef::NostrRelayMustAcceptTaggedEvents, + "Accept issue quoting another accepted issue (transitive)", ) .run(|| async { // Create TestContext @@ -777,7 +778,7 @@ impl EventAcceptancePolicyTests { pub async fn test_accept_comment_via_capital_e_tag(client: &AuditClient) -> TestResult { TestResult::new( "accept_comment_via_E_tag", - "GRASP-01:nostr-relay:13", + SpecRef::NostrRelayMustAcceptTaggedEvents, "Accept NIP-22 comment with root 'E' tag to accepted issue", ) .run(|| async { @@ -816,7 +817,7 @@ impl EventAcceptancePolicyTests { pub async fn test_accept_kind1_via_e_tag(client: &AuditClient) -> TestResult { TestResult::new( "accept_kind1_via_e_tag", - "GRASP-01:nostr-relay:13", + SpecRef::NostrRelayMustAcceptTaggedEvents, "Accept kind 1 reply via 'e' tag to accepted kind 1", ) .run(|| async { @@ -872,7 +873,7 @@ impl EventAcceptancePolicyTests { pub async fn test_accept_kind1_referenced_in_issue(client: &AuditClient) -> TestResult { TestResult::new( "accept_kind1_referenced_in_issue", - "GRASP-01:nostr-relay:13", + SpecRef::NostrRelayMustAcceptTaggedEvents, "Accept kind 1 referenced in accepted issue (forward ref)", ) .run(|| async { @@ -964,7 +965,7 @@ impl EventAcceptancePolicyTests { pub async fn test_accept_comment_referenced_in_comment(client: &AuditClient) -> TestResult { TestResult::new( "accept_comment_referenced_in_comment", - "GRASP-01:nostr-relay:13", + SpecRef::NostrRelayMustAcceptTaggedEvents, "Accept comment referenced in another accepted comment (forward ref)", ) .run(|| async { @@ -1025,7 +1026,7 @@ impl EventAcceptancePolicyTests { pub async fn test_accept_kind1_referenced_in_kind1(client: &AuditClient) -> TestResult { TestResult::new( "accept_kind1_referenced_in_kind1", - "GRASP-01:nostr-relay:13", + SpecRef::NostrRelayMustAcceptTaggedEvents, "Accept kind 1 referenced in another accepted kind 1 (forward ref)", ) .run(|| async { @@ -1083,7 +1084,7 @@ impl EventAcceptancePolicyTests { pub async fn test_reject_orphan_issue(client: &AuditClient) -> TestResult { TestResult::new( "reject_orphan_issue", - "GRASP-01:nostr-relay:18", + SpecRef::NostrRelayMayRejectSpamCuration, "Reject issue referencing unaccepted repo", ) .run(|| async { @@ -1110,7 +1111,7 @@ impl EventAcceptancePolicyTests { pub async fn test_reject_orphan_kind1(client: &AuditClient) -> TestResult { TestResult::new( "reject_orphan_kind1", - "GRASP-01:nostr-relay:18", + SpecRef::NostrRelayMayRejectSpamCuration, "Reject kind 1 with no repo references", ) .run(|| async { @@ -1139,7 +1140,7 @@ impl EventAcceptancePolicyTests { pub async fn test_reject_comment_quoting_other_repo(client: &AuditClient) -> TestResult { TestResult::new( "reject_comment_quoting_other_repo", - "GRASP-01:nostr-relay:18", + SpecRef::NostrRelayMayRejectSpamCuration, "Reject comment quoting unaccepted repo", ) .run(|| async { diff --git a/grasp-audit/src/specs/grasp01/git_clone.rs b/grasp-audit/src/specs/grasp01/git_clone.rs index e162558..fda472b 100644 --- a/grasp-audit/src/specs/grasp01/git_clone.rs +++ b/grasp-audit/src/specs/grasp01/git_clone.rs @@ -15,6 +15,7 @@ //! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test //! ``` +use crate::specs::grasp01::SpecRef; use crate::{AuditClient, FixtureKind, TestContext, TestResult}; use nostr_sdk::prelude::*; use std::fs; @@ -53,7 +54,7 @@ impl GitCloneTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:34", + SpecRef::GitServeRepository, "Repository must be cloneable via Git HTTP backend", ) .fail(format!("Failed to create repo fixture: {}", e)) @@ -74,7 +75,7 @@ impl GitCloneTests { None => { return TestResult::new( test_name, - "GRASP-01", + SpecRef::GitServeRepository, "Repository must be cloneable via Git HTTP backend", ) .fail("Repository announcement missing d tag") @@ -86,7 +87,7 @@ impl GitCloneTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:34", + SpecRef::GitServeRepository, "Repository must be cloneable via Git HTTP backend", ) .fail(format!("Failed to convert pubkey to npub: {}", e)) @@ -121,7 +122,7 @@ impl GitCloneTests { cleanup(); return TestResult::new( test_name, - "GRASP-01:git-http:34", + SpecRef::GitServeRepository, "Repository must be cloneable via Git HTTP backend", ) .fail(format!("Failed to execute git clone: {}", e)); @@ -133,7 +134,7 @@ impl GitCloneTests { let stderr = String::from_utf8_lossy(&output.stderr); return TestResult::new( test_name, - "GRASP-01:git-http:34", + SpecRef::GitServeRepository, "Repository must be cloneable via Git HTTP backend", ) .fail(format!("Git clone failed: {}", stderr)); @@ -144,7 +145,7 @@ impl GitCloneTests { cleanup(); return TestResult::new( test_name, - "GRASP-01:git-http:34", + SpecRef::GitServeRepository, "Repository must be cloneable via Git HTTP backend", ) .fail("Cloned repository missing .git directory"); @@ -153,7 +154,7 @@ impl GitCloneTests { cleanup(); TestResult::new( test_name, - "GRASP-01:git-http:34", + SpecRef::GitServeRepository, "Repository must be cloneable via Git HTTP backend", ) .pass() @@ -175,7 +176,7 @@ impl GitCloneTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:34", + SpecRef::GitServeRepository, "Clone URL must follow correct format", ) .fail(format!("Failed to create repo fixture: {}", e)) @@ -203,7 +204,7 @@ impl GitCloneTests { if !valid_url.contains(&npub) { return TestResult::new( test_name, - "GRASP-01:git-http:34", + SpecRef::GitServeRepository, "Clone URL must follow correct format", ) .fail("URL missing npub"); @@ -212,7 +213,7 @@ impl GitCloneTests { if !valid_url.contains(&format!("{}.git", repo_id)) { return TestResult::new( test_name, - "GRASP-01:git-http:34", + SpecRef::GitServeRepository, "Clone URL must follow correct format", ) .fail("URL missing repository identifier"); @@ -241,7 +242,7 @@ impl GitCloneTests { if output.status.success() { return TestResult::new( test_name, - "GRASP-01:git-http:34", + SpecRef::GitServeRepository, "Clone URL must follow correct format", ) .fail("Invalid URL was accepted (should have been rejected)"); @@ -249,7 +250,7 @@ impl GitCloneTests { TestResult::new( test_name, - "GRASP-01:git-http:34", + SpecRef::GitServeRepository, "Clone URL must follow correct format", ) .pass() @@ -278,7 +279,7 @@ impl GitCloneTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", ) .fail(format!("Failed to create repo fixture: {}", e)) @@ -299,7 +300,7 @@ impl GitCloneTests { None => { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", ) .fail("Repository announcement missing d tag") @@ -311,7 +312,7 @@ impl GitCloneTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", ) .fail(format!("Failed to convert pubkey to npub: {}", e)) @@ -331,7 +332,7 @@ impl GitCloneTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", ) .fail(format!("HTTP request failed: {}", e)) @@ -341,7 +342,7 @@ impl GitCloneTests { if !response.status().is_success() { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", ) .fail(format!( @@ -356,7 +357,7 @@ impl GitCloneTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", ) .fail(format!("Failed to read response body: {}", e)) @@ -370,7 +371,7 @@ impl GitCloneTests { if !has_allow_reachable { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", ) .fail("Missing capability: allow-reachable-sha1-in-want"); @@ -379,7 +380,7 @@ impl GitCloneTests { if !has_allow_tip { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", ) .fail("Missing capability: allow-tip-sha1-in-want"); @@ -387,7 +388,7 @@ impl GitCloneTests { TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", ) .pass() diff --git a/grasp-audit/src/specs/grasp01/git_filter.rs b/grasp-audit/src/specs/grasp01/git_filter.rs index 21bab0a..7f203a2 100644 --- a/grasp-audit/src/specs/grasp01/git_filter.rs +++ b/grasp-audit/src/specs/grasp01/git_filter.rs @@ -22,6 +22,7 @@ //! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test //! ``` +use crate::specs::grasp01::SpecRef; use crate::{AuditClient, FixtureKind, TestContext, TestResult}; use nostr_sdk::prelude::*; use std::fs; @@ -66,7 +67,7 @@ impl GitFilterTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include uploadpack.allowFilter in advertisement", ) .fail(format!("Failed to create repo fixture: {}", e)) @@ -87,7 +88,7 @@ impl GitFilterTests { None => { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include uploadpack.allowFilter in advertisement", ) .fail("Repository announcement missing d tag") @@ -99,7 +100,7 @@ impl GitFilterTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include uploadpack.allowFilter in advertisement", ) .fail(format!("Failed to convert pubkey to npub: {}", e)) @@ -119,7 +120,7 @@ impl GitFilterTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include uploadpack.allowFilter in advertisement", ) .fail(format!("HTTP request failed: {}", e)) @@ -129,7 +130,7 @@ impl GitFilterTests { if !response.status().is_success() { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include uploadpack.allowFilter in advertisement", ) .fail(format!( @@ -144,7 +145,7 @@ impl GitFilterTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include uploadpack.allowFilter in advertisement", ) .fail(format!("Failed to read response body: {}", e)) @@ -155,7 +156,7 @@ impl GitFilterTests { if !body.contains("filter") { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include uploadpack.allowFilter in advertisement", ) .fail("Missing capability: filter"); @@ -163,7 +164,7 @@ impl GitFilterTests { TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST include uploadpack.allowFilter in advertisement", ) .pass() @@ -189,7 +190,7 @@ impl GitFilterTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST serve filtered clone requests", ) .fail(format!("Failed to create repo fixture: {}", e)) @@ -243,7 +244,7 @@ impl GitFilterTests { cleanup(); return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST serve filtered clone requests", ) .fail(format!("Failed to execute git clone: {}", e)); @@ -255,7 +256,7 @@ impl GitFilterTests { let stderr = String::from_utf8_lossy(&output.stderr); return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST serve filtered clone requests", ) .fail(format!("Filtered git clone failed: {}", stderr)); @@ -266,7 +267,7 @@ impl GitFilterTests { cleanup(); return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST serve filtered clone requests", ) .fail("Filtered clone missing .git directory"); @@ -275,7 +276,7 @@ impl GitFilterTests { cleanup(); TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST serve filtered clone requests", ) .pass() @@ -300,7 +301,7 @@ impl GitFilterTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST serve filtered fetch requests", ) .fail(format!("Failed to create repo fixture: {}", e)) @@ -352,7 +353,7 @@ impl GitFilterTests { cleanup(); return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST serve filtered fetch requests", ) .fail("Failed to create initial shallow clone for fetch test"); @@ -371,7 +372,7 @@ impl GitFilterTests { cleanup(); return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST serve filtered fetch requests", ) .fail(format!("Failed to execute git fetch: {}", e)); @@ -383,7 +384,7 @@ impl GitFilterTests { let stderr = String::from_utf8_lossy(&output.stderr); return TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST serve filtered fetch requests", ) .fail(format!("Filtered git fetch failed: {}", stderr)); @@ -392,7 +393,7 @@ impl GitFilterTests { cleanup(); TestResult::new( test_name, - "GRASP-01:git-http:42", + SpecRef::GitIncludeAllowSha1InWant, "MUST serve filtered fetch requests", ) .pass() diff --git a/grasp-audit/src/specs/grasp01/mod.rs b/grasp-audit/src/specs/grasp01/mod.rs index 0a819ee..125594c 100644 --- a/grasp-audit/src/specs/grasp01/mod.rs +++ b/grasp-audit/src/specs/grasp01/mod.rs @@ -32,6 +32,6 @@ pub use nip11_document::Nip11DocumentTests; pub use push_authorization::PushAuthorizationTests; pub use repository_creation::RepositoryCreationTests; pub use spec_requirements::{ - get_requirement, get_requirements_for_section, get_sections, RequirementLevel, SpecRequirement, - GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID, + get_requirement, get_requirement_by_ref, get_requirements_for_section, get_sections, + RequirementLevel, SpecRef, SpecRequirement, GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID, }; diff --git a/grasp-audit/src/specs/grasp01/nip01_smoke.rs b/grasp-audit/src/specs/grasp01/nip01_smoke.rs index 4d0b8a4..5976252 100644 --- a/grasp-audit/src/specs/grasp01/nip01_smoke.rs +++ b/grasp-audit/src/specs/grasp01/nip01_smoke.rs @@ -4,6 +4,7 @@ //! We don't comprehensively test NIP-01 because rust-nostr already has 1000+ tests. //! These are just smoke tests to ensure the relay is working at all. +use crate::specs::grasp01::SpecRef; use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; use nostr_sdk::prelude::*; @@ -32,8 +33,8 @@ impl Nip01SmokeTests { pub async fn test_websocket_connection(client: &AuditClient) -> TestResult { TestResult::new( "websocket_connection", - "GRASP-01:nostr-relay:7", - "Can establish WebSocket connection to /", + SpecRef::NostrRelayNip01Compliant, + "MUST serve a relay at / via WebSocket", ) .run(|| async { if !client.is_connected().await { @@ -61,8 +62,8 @@ impl Nip01SmokeTests { pub async fn test_send_receive_event(client: &AuditClient) -> TestResult { TestResult::new( "send_receive_event", - "GRASP-01:nostr-relay:7", - "Can send EVENT and receive OK response", + SpecRef::NostrRelayNip01Compliant, + "MUST accept valid EVENT messages", ) .run(|| async { // Step 1: GENERATE - Create TestContext and get ValidRepo fixture @@ -127,8 +128,8 @@ impl Nip01SmokeTests { pub async fn test_create_subscription(client: &AuditClient) -> TestResult { TestResult::new( "create_subscription", - "GRASP-01:nostr-relay:7", - "Can create subscription with REQ and receive EOSE", + SpecRef::NostrRelayNip01Compliant, + "MUST support REQ subscriptions", ) .run(|| async { // Step 1: GENERATE - Create TestContext and get ValidRepo fixture @@ -165,8 +166,8 @@ impl Nip01SmokeTests { pub async fn test_close_subscription(client: &AuditClient) -> TestResult { TestResult::new( "close_subscription", - "GRASP-01:nostr-relay:7", - "Can close subscriptions", + SpecRef::NostrRelayNip01Compliant, + "MUST support CLOSE to end subscriptions", ) .run(|| async { // For now, we just verify we can query events @@ -193,8 +194,8 @@ impl Nip01SmokeTests { pub async fn test_reject_invalid_signature(client: &AuditClient) -> TestResult { TestResult::new( "reject_invalid_signature", - "GRASP-01:nostr-relay:7", - "Rejects events with invalid signatures", + SpecRef::NostrRelayNip01Compliant, + "MUST reject events with invalid signatures", ) .run(|| async { // Create a valid event @@ -247,8 +248,8 @@ impl Nip01SmokeTests { pub async fn test_reject_invalid_event_id(client: &AuditClient) -> TestResult { TestResult::new( "reject_invalid_event_id", - "GRASP-01:nostr-relay:7", - "Rejects events with invalid event IDs", + SpecRef::NostrRelayNip01Compliant, + "MUST reject events where ID doesn't match hash", ) .run(|| async { // Create a valid event diff --git a/grasp-audit/src/specs/grasp01/nip11_document.rs b/grasp-audit/src/specs/grasp01/nip11_document.rs index 19ceace..5bf53bd 100644 --- a/grasp-audit/src/specs/grasp01/nip11_document.rs +++ b/grasp-audit/src/specs/grasp01/nip11_document.rs @@ -8,6 +8,7 @@ //! - Includes repo_acceptance_criteria field describing acceptance policy //! - Handles curation field correctly (present if curated, absent otherwise) +use crate::specs::grasp01::SpecRef; use crate::{AuditClient, AuditResult, TestResult}; pub struct Nip11DocumentTests; @@ -37,8 +38,8 @@ impl Nip11DocumentTests { pub async fn test_nip11_document_exists(client: &AuditClient) -> TestResult { TestResult::new( "nip11_document_exists", - "GRASP-01:nostr-relay:26", - "Serve NIP-11 relay information document", + SpecRef::Nip11ServeDocument, + "MUST serve NIP-11 document", ) .run(|| async { // 1. Extract HTTP(S) URL from client's WebSocket URL @@ -96,8 +97,8 @@ impl Nip11DocumentTests { pub async fn test_nip11_supported_grasps_field(client: &AuditClient) -> TestResult { TestResult::new( "nip11_supported_grasps_field", - "GRASP-01:nostr-relay:28", - "NIP-11 document includes supported_grasps field with GRASP-01", + SpecRef::Nip11ListSupportedGrasps, + "MUST list supported GRASPs as string array", ) .run(|| async { // 1. Fetch NIP-11 document @@ -172,8 +173,8 @@ impl Nip11DocumentTests { pub async fn test_nip11_repo_acceptance_criteria_field(client: &AuditClient) -> TestResult { TestResult::new( "nip11_repo_acceptance_criteria_field", - "GRASP-01:nostr-relay:29", - "NIP-11 document includes repo_acceptance_criteria field", + SpecRef::Nip11ListRepoAcceptanceCriteria, + "MUST list repository acceptance criteria", ) .run(|| async { // 1. Fetch NIP-11 document @@ -227,8 +228,8 @@ impl Nip11DocumentTests { pub async fn test_nip11_curation_field(client: &AuditClient) -> TestResult { TestResult::new( "nip11_curation_field", - "GRASP-01:nostr-relay:30", - "NIP-11 curation field present if curated, absent otherwise", + SpecRef::Nip11ListCurationPolicy, + "MUST include curation if curated, omit otherwise", ) .run(|| async { // 1. Fetch NIP-11 document diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index 677af89..be354a0 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs @@ -31,6 +31,7 @@ #[allow(dead_code)] const PR_TEST_COMMIT_HASH: &str = "5d40fb1555a0c28bf4d650515a73aaa54d4d9bfb"; +use crate::specs::grasp01::SpecRef; use crate::{ clone_repo, create_commit, create_deterministic_commit_with_variant, try_push, try_push_to_ref, AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, @@ -411,7 +412,7 @@ impl PushAuthorizationTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push rejected without state event", ) .fail(format!("Failed to create repo: {}", e)) @@ -435,7 +436,7 @@ impl PushAuthorizationTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push rejected without state event", ) .fail(&e) @@ -449,7 +450,7 @@ impl PushAuthorizationTests { cleanup(); return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push rejected without state event", ) .fail(&e); @@ -462,19 +463,19 @@ impl PushAuthorizationTests { match push_result { Ok(false) => TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push rejected without state event", ) .pass(), Ok(true) => TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push rejected without state event", ) .fail("Push accepted but should be rejected"), Err(e) => TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push rejected without state event", ) .fail(&e), @@ -507,13 +508,13 @@ impl PushAuthorizationTests { match ctx.get_fixture(FixtureKind::OwnerStateDataPushed).await { Ok(_state_event) => TestResult::new( test_name, - "GRASP-01:git-http:36", // TODO do we add purgatory line here? + SpecRef::GitAcceptPushesAlignState, "Push authorized with matching state", ) .pass(), Err(e) => TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push authorized with matching state", ) .fail(format!("{}", e)), @@ -555,7 +556,7 @@ impl PushAuthorizationTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push rejected when commit not in state event", ) .fail(format!("Failed to create RepoState fixture: {}", e)); @@ -575,7 +576,7 @@ impl PushAuthorizationTests { None => { return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push rejected when commit not in state event", ) .fail("Missing repo_id in state event"); @@ -587,7 +588,7 @@ impl PushAuthorizationTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push rejected when commit not in state event", ) .fail(format!("Failed to convert pubkey to bech32: {}", e)); @@ -603,7 +604,7 @@ impl PushAuthorizationTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push rejected when commit not in state event", ) .fail(format!("Failed to clone repo: {}", e)); @@ -626,7 +627,7 @@ impl PushAuthorizationTests { cleanup(); return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push rejected when commit not in state event", ) .fail(format!("Failed to create/checkout main branch: {}", e)); @@ -635,7 +636,7 @@ impl PushAuthorizationTests { cleanup(); return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push rejected when commit not in state event", ) .fail(format!( @@ -652,7 +653,7 @@ impl PushAuthorizationTests { cleanup(); return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push rejected when commit not in state event", ) .fail(format!("Failed to create wrong commit: {}", e)); @@ -666,10 +667,10 @@ impl PushAuthorizationTests { cleanup(); match push_result { - Ok(false) => TestResult::new(test_name, "GRASP-01:git-http:36", "Push rejected when commit not in state event").pass(), - Ok(true) => TestResult::new(test_name, "GRASP-01:git-http:36", "Push rejected when commit not in state event") + Ok(false) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Push rejected when commit not in state event").pass(), + Ok(true) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Push rejected when commit not in state event") .fail("Push accepted but should be rejected. The pushed commit is not in the state event."), - Err(e) => TestResult::new(test_name, "GRASP-01:git-http:36", "Push rejected when commit not in state event").fail(&e), + Err(e) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Push rejected when commit not in state event").fail(&e), } } @@ -704,13 +705,13 @@ impl PushAuthorizationTests { { Ok(_maintainer_state_event) => TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push authorized by maintainer state event only (no announcement)", ) .pass(), Err(e) => TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push authorized by maintainer state event only (no announcement)", ) .fail(format!("{}", e)), @@ -747,13 +748,13 @@ impl PushAuthorizationTests { { Ok(_recursive_maintainer_state_event) => TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push authorized by recursive maintainer state event", ) .pass(), Err(e) => TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Push authorized by recursive maintainer state event", ) .fail(format!("{}", e)), @@ -797,7 +798,7 @@ impl PushAuthorizationTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Non-maintainer state events ignored", ) .fail(format!("Failed to get OwnerStateDataPushed fixture: {}", e)); @@ -815,7 +816,7 @@ impl PushAuthorizationTests { None => { return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Non-maintainer state events ignored", ) .fail("Missing repo_id in state event"); @@ -827,7 +828,7 @@ impl PushAuthorizationTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Non-maintainer state events ignored", ) .fail(format!("Failed to convert pubkey to bech32: {}", e)); @@ -842,7 +843,7 @@ impl PushAuthorizationTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Non-maintainer state events ignored", ) .fail(format!("Failed to clone repo: {}", e)); @@ -864,7 +865,7 @@ impl PushAuthorizationTests { cleanup(); return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Non-maintainer state events ignored", ) .fail(format!("Failed to create commit: {}", e)); @@ -890,7 +891,7 @@ impl PushAuthorizationTests { cleanup(); return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Non-maintainer state events ignored", ) .fail(format!("Failed to build rogue state event: {}", e)); @@ -902,7 +903,7 @@ impl PushAuthorizationTests { cleanup(); return TestResult::new( test_name, - "GRASP-01:git-http:36", + SpecRef::GitAcceptPushesAlignState, "Non-maintainer state events ignored", ) .fail(format!("Failed to send rogue state event: {}", e)); @@ -919,8 +920,8 @@ impl PushAuthorizationTests { cleanup(); match push_result { - Ok(false) => TestResult::new(test_name, "GRASP-01:git-http:36", "Non-maintainer state events ignored").pass(), - Ok(true) => TestResult::new(test_name, "GRASP-01:git-http:36", "Non-maintainer state events ignored") + Ok(false) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Non-maintainer state events ignored").pass(), + Ok(true) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Non-maintainer state events ignored") .fail(format!( "Push accepted but should be rejected. A non-maintainer (pubkey: {}) published \ a state event announcing commit {}, but the push was accepted. The relay should \ @@ -929,7 +930,7 @@ impl PushAuthorizationTests { new_commit, client.public_key() )), - Err(e) => TestResult::new(test_name, "GRASP-01:git-http:36", "Non-maintainer state events ignored").fail(&e), + Err(e) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Non-maintainer state events ignored").fail(&e), } } @@ -960,7 +961,7 @@ impl PushAuthorizationTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:40", + SpecRef::GitAcceptRefsNostrEventId, "Push to refs/nostr/ rejected", ) .fail(format!("Failed to create repo: {}", e)); @@ -986,7 +987,7 @@ impl PushAuthorizationTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:40", + SpecRef::GitAcceptRefsNostrEventId, "Push to refs/nostr/ rejected", ) .fail(&e); @@ -1001,7 +1002,7 @@ impl PushAuthorizationTests { cleanup(); return TestResult::new( test_name, - "GRASP-01:git-http:40", + SpecRef::GitAcceptRefsNostrEventId, "Push to refs/nostr/ rejected", ) .fail(&e); @@ -1020,13 +1021,13 @@ impl PushAuthorizationTests { match push_result { Ok(false) => TestResult::new( test_name, - "GRASP-01:git-http:40", + SpecRef::GitAcceptRefsNostrEventId, "Push to refs/nostr/ rejected", ) .pass(), Ok(true) => TestResult::new( test_name, - "GRASP-01:git-http:40", + SpecRef::GitAcceptRefsNostrEventId, "Push to refs/nostr/ rejected", ) .fail(format!( @@ -1037,7 +1038,7 @@ impl PushAuthorizationTests { )), Err(e) => TestResult::new( test_name, - "GRASP-01:git-http:40", + SpecRef::GitAcceptRefsNostrEventId, "Push to refs/nostr/ rejected", ) .fail(format!("Push error: {}", e)), @@ -1071,10 +1072,11 @@ impl PushAuthorizationTests { .get_fixture(FixtureKind::PRWrongCommitPushedBeforeEvent) .await { - Ok(_pr_event) => TestResult::new(test_name, "GRASP-01:git-http:40", desc).pass(), - Err(e) => { - TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(format!("{}", e)) + Ok(_pr_event) => { + TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).pass() } + Err(e) => TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) + .fail(format!("{}", e)), } } @@ -1100,7 +1102,7 @@ impl PushAuthorizationTests { { Ok(e) => e, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc) + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) .fail(format!("{}", e)); } }; @@ -1111,7 +1113,7 @@ impl PushAuthorizationTests { let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { Ok(r) => r, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc) + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) .fail(format!("{}", e)); } }; @@ -1127,7 +1129,7 @@ impl PushAuthorizationTests { let owner_npub = match repo.pubkey.to_bech32() { Ok(n) => n, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc) + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) .fail(format!("Failed to get owner npub: {}", e)); } }; @@ -1136,7 +1138,8 @@ impl PushAuthorizationTests { let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { Ok(p) => p, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) + .fail(&e); } }; @@ -1146,7 +1149,8 @@ impl PushAuthorizationTests { Ok(exists) => exists, Err(e) => { let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) + .fail(&e); } }; @@ -1154,13 +1158,13 @@ impl PushAuthorizationTests { // Ref should be deleted since the pushed commit doesn't match the PR event's `c` tag if refs_exist { - TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(format!( + TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).fail(format!( "Expected refs/nostr/{} to be deleted when PR event published with non-matching commit, \ but the ref still exists. The relay should delete refs that don't match the event's `c` tag.", pr_event_id )) } else { - TestResult::new(test_name, "GRASP-01:git-http:40", desc).pass() + TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).pass() } } @@ -1186,7 +1190,7 @@ impl PushAuthorizationTests { { Ok(e) => e, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc) + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) .fail(format!("{}", e)); } }; @@ -1197,7 +1201,7 @@ impl PushAuthorizationTests { let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { Ok(r) => r, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc) + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) .fail(format!("{}", e)); } }; @@ -1213,7 +1217,7 @@ impl PushAuthorizationTests { let owner_npub = match repo.pubkey.to_bech32() { Ok(n) => n, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc) + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) .fail(format!("Failed to get owner npub: {}", e)); } }; @@ -1222,7 +1226,8 @@ impl PushAuthorizationTests { let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { Ok(p) => p, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) + .fail(&e); } }; @@ -1230,7 +1235,7 @@ impl PushAuthorizationTests { if let Err(e) = create_deterministic_commit_with_variant(&clone_path, CommitVariant::Owner) { let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).fail(&e); } // Try to push with wrong commit (should be rejected since PR event exists) @@ -1238,7 +1243,8 @@ impl PushAuthorizationTests { Ok(success) => success, Err(e) => { let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) + .fail(&e); } }; @@ -1246,11 +1252,11 @@ impl PushAuthorizationTests { // Should REJECT - PR event exists with different commit hash if push_succeeded { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc) + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) .fail("Push accepted (expected rejection due to commit hash mismatch)"); } - TestResult::new(test_name, "GRASP-01:git-http:40", desc).pass() + TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).pass() } /// Test 4: Push correct commit to refs/nostr/ AFTER PR event exists @@ -1275,7 +1281,7 @@ impl PushAuthorizationTests { { Ok(e) => e, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc) + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) .fail(format!("{}", e)); } }; @@ -1286,7 +1292,7 @@ impl PushAuthorizationTests { let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { Ok(r) => r, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc) + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) .fail(format!("{}", e)); } }; @@ -1302,7 +1308,7 @@ impl PushAuthorizationTests { let owner_npub = match repo.pubkey.to_bech32() { Ok(n) => n, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc) + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) .fail(format!("Failed to get owner npub: {}", e)); } }; @@ -1311,26 +1317,27 @@ impl PushAuthorizationTests { let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { Ok(p) => p, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) + .fail(&e); } }; // Create the CORRECT PR test commit (the one expected by PR event) if let Err(e) = reset_to_correct_pr_commit(&clone_path) { let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).fail(&e); } // Check event is not yet served by relay (still in purgatory) match client.is_event_on_relay(pr_event.id).await { Ok(on_relay) => { if on_relay { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc) + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) .fail("PR event not in purgatory before correct commit pushed to refs/nostr/ (the relay serve the PR event)"); } } Err(_) => { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc) + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) .fail("failed to query relay"); } } @@ -1340,7 +1347,8 @@ impl PushAuthorizationTests { Ok(success) => success, Err(e) => { let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) + .fail(&e); } }; @@ -1348,7 +1356,7 @@ impl PushAuthorizationTests { // Should ACCEPT - commit matches PR event's c tag if !push_succeeded { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc) + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) .fail("Push rejected (expected acceptance since commit matches PR event)"); } @@ -1361,17 +1369,17 @@ impl PushAuthorizationTests { match client.is_event_on_relay(pr_event.id).await { Ok(on_relay) => { if !on_relay { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc) + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) .fail("PR event not served after correct commit at refs/nostr/"); } } Err(_) => { - return TestResult::new(test_name, "GRASP-01:git-http:40", desc) + return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) .fail("failed to query relay"); } } - TestResult::new(test_name, "GRASP-01:git-http:40", desc).pass() + TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).pass() } /// Test that HEAD is set after a state event is published with an existing commit @@ -1408,10 +1416,9 @@ impl PushAuthorizationTests { { Ok(e) => e, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:38", desc).fail(format!( - "Failed to create HeadSetToDevelopBranch fixture: {}", - e - )); + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).fail( + format!("Failed to create HeadSetToDevelopBranch fixture: {}", e), + ); } }; @@ -1421,7 +1428,7 @@ impl PushAuthorizationTests { let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { Ok(e) => e, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:38", desc) + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) .fail(format!("Failed to get ValidRepo fixture: {}", e)); } }; @@ -1434,7 +1441,7 @@ impl PushAuthorizationTests { { Some(id) => id.to_string(), None => { - return TestResult::new(test_name, "GRASP-01:git-http:38", desc) + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) .fail("Missing repo_id in ValidRepo"); } }; @@ -1442,7 +1449,7 @@ impl PushAuthorizationTests { let npub = match valid_repo.pubkey.to_bech32() { Ok(n) => n, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:38", desc) + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) .fail(format!("Failed to convert pubkey to bech32: {}", e)); } }; @@ -1454,16 +1461,16 @@ impl PushAuthorizationTests { match get_default_branch_from_info_refs(relay_domain, &npub, &repo_id).await { Ok(branch) => branch, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:38", desc) + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) .fail(format!("Failed to get default branch: {}", e)); } }; // Verify HEAD points to refs/heads/develop if default_branch == "refs/heads/develop" { - TestResult::new(test_name, "GRASP-01:git-http:38", desc).pass() + TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).pass() } else { - TestResult::new(test_name, "GRASP-01:git-http:38", desc).fail(format!( + TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).fail(format!( "Expected HEAD to point to 'refs/heads/develop' but got '{}'. \ GRASP-01 requires: 'MUST set repository HEAD per repository state announcement \ as soon as the git data related to that branch has been received.'", @@ -1512,10 +1519,9 @@ impl PushAuthorizationTests { let _develop_state = match ctx.get_fixture(FixtureKind::HeadSetToDevelopBranch).await { Ok(e) => e, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:38", desc).fail(format!( - "Failed to create HeadSetToDevelopBranch fixture: {}", - e - )); + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).fail( + format!("Failed to create HeadSetToDevelopBranch fixture: {}", e), + ); } }; @@ -1525,7 +1531,7 @@ impl PushAuthorizationTests { let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { Ok(e) => e, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:38", desc) + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) .fail(format!("Failed to get ValidRepo fixture: {}", e)); } }; @@ -1538,7 +1544,7 @@ impl PushAuthorizationTests { { Some(id) => id.to_string(), None => { - return TestResult::new(test_name, "GRASP-01:git-http:38", desc) + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) .fail("Missing repo_id in ValidRepo"); } }; @@ -1546,7 +1552,7 @@ impl PushAuthorizationTests { let npub = match valid_repo.pubkey.to_bech32() { Ok(n) => n, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:38", desc) + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) .fail(format!("Failed to convert pubkey to bech32: {}", e)); } }; @@ -1557,7 +1563,7 @@ impl PushAuthorizationTests { let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { Ok(path) => path, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:38", desc) + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) .fail(format!("Failed to clone repo: {}", e)); } }; @@ -1572,7 +1578,7 @@ impl PushAuthorizationTests { if let Err(e) = output { let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01:git-http:38", desc) + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) .fail(format!("Failed to create develop1 branch: {}", e)); } @@ -1581,7 +1587,7 @@ impl PushAuthorizationTests { Ok(hash) => hash, Err(e) => { let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01:git-http:38", desc) + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) .fail(format!("Failed to create commit: {}", e)); } }; @@ -1610,7 +1616,7 @@ impl PushAuthorizationTests { Ok(e) => e, Err(e) => { let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01:git-http:38", desc) + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) .fail(format!("Failed to build state event: {}", e)); } }; @@ -1621,7 +1627,7 @@ impl PushAuthorizationTests { .await { let _ = fs::remove_dir_all(&clone_path); - return TestResult::new(test_name, "GRASP-01:git-http:38", desc) + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) .fail(format!("Failed to send state event: {}", e)); } @@ -1634,11 +1640,11 @@ impl PushAuthorizationTests { match push_result { Ok(true) => { /* Push succeeded, continue to verify */ } Ok(false) => { - return TestResult::new(test_name, "GRASP-01:git-http:38", desc) + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) .fail("Push to refs/heads/develop1 was rejected"); } Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:38", desc) + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) .fail(format!("Failed to push develop1 branch: {}", e)); } } @@ -1651,16 +1657,16 @@ impl PushAuthorizationTests { match get_default_branch_from_info_refs(relay_domain, &npub, &repo_id).await { Ok(branch) => branch, Err(e) => { - return TestResult::new(test_name, "GRASP-01:git-http:38", desc) + return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) .fail(format!("Failed to get default branch: {}", e)); } }; // Verify HEAD points to refs/heads/develop1 if default_branch == "refs/heads/develop1" { - TestResult::new(test_name, "GRASP-01:git-http:38", desc).pass() + TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).pass() } else { - TestResult::new(test_name, "GRASP-01:git-http:38", desc).fail(format!( + TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).fail(format!( "Expected HEAD to point to 'refs/heads/develop1' but got '{}'. \ GRASP-01 requires: 'MUST set repository HEAD per repository state announcement \ as soon as the git data related to that branch has been received.'", diff --git a/grasp-audit/src/specs/grasp01/repository_creation.rs b/grasp-audit/src/specs/grasp01/repository_creation.rs index 2eddb97..a702afe 100644 --- a/grasp-audit/src/specs/grasp01/repository_creation.rs +++ b/grasp-audit/src/specs/grasp01/repository_creation.rs @@ -15,6 +15,7 @@ //! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test //! ``` +use crate::specs::grasp01::SpecRef; use crate::{AuditClient, FixtureKind, TestContext, TestResult}; use nostr_sdk::prelude::*; @@ -55,7 +56,7 @@ impl RepositoryCreationTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:34", + SpecRef::GitServeRepository, "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", ) .fail(format!("Failed to create repo fixture: {}", e)) @@ -76,7 +77,7 @@ impl RepositoryCreationTests { None => { return TestResult::new( test_name, - "GRASP-01:git-http:34", + SpecRef::GitServeRepository, "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", ) .fail("Repository announcement missing d tag") @@ -88,7 +89,7 @@ impl RepositoryCreationTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:34", + SpecRef::GitServeRepository, "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", ) .fail(format!("Failed to convert pubkey to npub: {}", e)) @@ -99,7 +100,7 @@ impl RepositoryCreationTests { if let Err(e) = check_repo_accessible_via_http(relay_domain, &npub, &repo_id).await { return TestResult::new( test_name, - "GRASP-01:git-http:34", + SpecRef::GitServeRepository, "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", ) .fail(format!("Repository not accessible via HTTP: {}", e)); @@ -107,7 +108,7 @@ impl RepositoryCreationTests { TestResult::new( test_name, - "GRASP-01:git-http:34", + SpecRef::GitServeRepository, "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", ) .pass() @@ -135,7 +136,7 @@ impl RepositoryCreationTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:44", + SpecRef::GitServeWebpage, "Relay SHOULD serve a webpage for existing repositories", ) .fail(format!("Failed to create repo fixture: {}", e)) @@ -156,7 +157,7 @@ impl RepositoryCreationTests { None => { return TestResult::new( test_name, - "GRASP-01:git-http:44", + SpecRef::GitServeWebpage, "Relay SHOULD serve a webpage for existing repositories", ) .fail("Repository announcement missing d tag") @@ -168,7 +169,7 @@ impl RepositoryCreationTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:44", + SpecRef::GitServeWebpage, "Relay SHOULD serve a webpage for existing repositories", ) .fail(format!("Failed to convert pubkey to npub: {}", e)) @@ -179,7 +180,7 @@ impl RepositoryCreationTests { if let Err(e) = check_webpage_served(relay_domain, &npub, &repo_id).await { return TestResult::new( test_name, - "GRASP-01:git-http:44", + SpecRef::GitServeWebpage, "Relay SHOULD serve a webpage for existing repositories", ) .fail(format!("Webpage not served: {}", e)); @@ -187,7 +188,7 @@ impl RepositoryCreationTests { TestResult::new( test_name, - "GRASP-01:git-http:44", + SpecRef::GitServeWebpage, "Relay SHOULD serve a webpage for existing repositories", ) .pass() @@ -214,7 +215,7 @@ impl RepositoryCreationTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:44", + SpecRef::GitServeWebpage, "Relay SHOULD return 404 for repositories it doesn't host", ) .fail(format!("Failed to create repo fixture: {}", e)) @@ -226,7 +227,7 @@ impl RepositoryCreationTests { Err(e) => { return TestResult::new( test_name, - "GRASP-01:git-http:44", + SpecRef::GitServeWebpage, "Relay SHOULD return 404 for repositories it doesn't host", ) .fail(format!("Failed to convert pubkey to npub: {}", e)) @@ -239,7 +240,7 @@ impl RepositoryCreationTests { if let Err(e) = check_404_for_nonexistent_repo(relay_domain, &npub, fake_repo_id).await { return TestResult::new( test_name, - "GRASP-01:git-http:44", + SpecRef::GitServeWebpage, "Relay SHOULD return 404 for repositories it doesn't host", ) .fail(format!("Expected 404, got: {}", e)); @@ -247,7 +248,7 @@ impl RepositoryCreationTests { TestResult::new( test_name, - "GRASP-01:git-http:44", + SpecRef::GitServeWebpage, "Relay SHOULD return 404 for repositories it doesn't host", ) .pass() diff --git a/grasp-audit/src/specs/grasp01/spec_requirements.rs b/grasp-audit/src/specs/grasp01/spec_requirements.rs index 71b2d69..6bc961c 100644 --- a/grasp-audit/src/specs/grasp01/spec_requirements.rs +++ b/grasp-audit/src/specs/grasp01/spec_requirements.rs @@ -6,9 +6,36 @@ /// GRASP spec repository commit ID that this version is based on pub const GRASP_COMMIT_ID: &str = "1fdb8f7"; +/// Reference to a specific GRASP-01 specification requirement +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SpecRef { + NostrRelayNip01Compliant, + NostrRelayRejectMissingCloneRelays, + NostrRelayMayRejectOtherCriteria, + NostrRelayMustAcceptTaggedEvents, + NostrRelayMayRejectSpamCuration, + PurgatoryAcceptUntilGitData, + Nip11ServeDocument, + Nip11ListSupportedGrasps, + Nip11ListRepoAcceptanceCriteria, + Nip11ListCurationPolicy, + GitServeRepository, + GitAcceptPushesAlignState, + GitSetHeadOnReceive, + GitAcceptRefsNostrEventId, + GitIncludeAllowSha1InWant, + GitServeWebpage, + CorsAllowOrigin, + CorsAllowMethods, + CorsAllowHeaders, + CorsOptionsResponse, +} + /// A single specification requirement #[derive(Debug, Clone)] pub struct SpecRequirement { + /// Unique reference to this requirement + pub spec_ref: SpecRef, /// Line number in the spec document pub line: u32, /// Section name (e.g., "Nostr Relay", "Git Smart HTTP Service", "CORS Support") @@ -37,121 +64,175 @@ impl std::fmt::Display for RequirementLevel { } } +impl SpecRef { + /// Get the spec reference string in format "GRASP-01:section:line" + pub fn spec_ref_string(self) -> &'static str { + match self { + SpecRef::NostrRelayNip01Compliant => "GRASP-01:nostr-relay:7", + SpecRef::NostrRelayRejectMissingCloneRelays => "GRASP-01:nostr-relay:9", + SpecRef::NostrRelayMayRejectOtherCriteria => "GRASP-01:nostr-relay:11", + SpecRef::NostrRelayMustAcceptTaggedEvents => "GRASP-01:nostr-relay:13", + SpecRef::NostrRelayMayRejectSpamCuration => "GRASP-01:nostr-relay:18", + SpecRef::PurgatoryAcceptUntilGitData => "GRASP-01:purgatory:22", + SpecRef::Nip11ServeDocument => "GRASP-01:nip-11:26", + SpecRef::Nip11ListSupportedGrasps => "GRASP-01:nip-11:28", + SpecRef::Nip11ListRepoAcceptanceCriteria => "GRASP-01:nip-11:29", + SpecRef::Nip11ListCurationPolicy => "GRASP-01:nip-11:30", + SpecRef::GitServeRepository => "GRASP-01:git-http:34", + SpecRef::GitAcceptPushesAlignState => "GRASP-01:git-http:36", + SpecRef::GitSetHeadOnReceive => "GRASP-01:git-http:39", + SpecRef::GitAcceptRefsNostrEventId => "GRASP-01:git-http:45", + SpecRef::GitIncludeAllowSha1InWant => "GRASP-01:git-http:56", + SpecRef::GitServeWebpage => "GRASP-01:git-http:58", + SpecRef::CorsAllowOrigin => "GRASP-01:cors:64", + SpecRef::CorsAllowMethods => "GRASP-01:cors:65", + SpecRef::CorsAllowHeaders => "GRASP-01:cors:66", + SpecRef::CorsOptionsResponse => "GRASP-01:cors:67", + } + } +} + /// All GRASP-01 specification requirements pub const GRASP_01_REQUIREMENTS: &[SpecRequirement] = &[ // Nostr Relay section SpecRequirement { + spec_ref: SpecRef::NostrRelayNip01Compliant, line: 7, section: "Nostr Relay", text: "MUST serve a NIP-01 compliant nostr relay at `/` that accepts git repository announcements and their corresponding repo state announcements.", level: RequirementLevel::Must, }, SpecRequirement { + spec_ref: SpecRef::NostrRelayRejectMissingCloneRelays, line: 9, section: "Nostr Relay", text: "MUST reject git repository announcements that do not list the service in both `clone` and `relays` tags unless implementing `GRASP-05`.", level: RequirementLevel::Must, }, SpecRequirement { + spec_ref: SpecRef::NostrRelayMayRejectOtherCriteria, line: 11, section: "Nostr Relay", text: "MAY reject git repository announcements based on other criteria such as pre-payment, quotas, WoT, whitelist, SPAM prevention, etc.", level: RequirementLevel::May, }, SpecRequirement { + spec_ref: SpecRef::NostrRelayMustAcceptTaggedEvents, line: 13, section: "Nostr Relay", text: "MUST accept other events that tag, or are tagged by, either: 1. accepted git repository announcements; or 2. accepted issues or patches", level: RequirementLevel::Must, }, SpecRequirement { + spec_ref: SpecRef::NostrRelayMayRejectSpamCuration, line: 18, section: "Nostr Relay", text: "MAY reject or delete events for generic SPAM prevention reasons or curation eg. WoT, whitelist, user bans and banned topics.", level: RequirementLevel::May, }, SpecRequirement { + spec_ref: SpecRef::PurgatoryAcceptUntilGitData, + line: 22, + section: "Purgatory", + text: "New repository announcements, repo state announcements, PRs and PR Updates SHOULD be accepted with message \"purgatory: won't be served until git data arrives\" and kept in purgatory (not served) until the related git data arrives and otherwise discarded after 30 minutes.", + level: RequirementLevel::Should, + }, + SpecRequirement { + spec_ref: SpecRef::Nip11ServeDocument, line: 26, - section: "Nostr Relay", + section: "NIP-11", text: "MUST serve a NIP-11 document", level: RequirementLevel::Must, }, SpecRequirement { + spec_ref: SpecRef::Nip11ListSupportedGrasps, line: 28, - section: "Nostr Relay", + section: "NIP-11", text: "MUST list each supported GRASP under `supported_grasps` in format `GRASP-XX` eg `GRASP-01` as a string array", level: RequirementLevel::Must, }, SpecRequirement { + spec_ref: SpecRef::Nip11ListRepoAcceptanceCriteria, line: 29, - section: "Nostr Relay", + section: "NIP-11", text: "MUST list repository acceptance criteria under `repo_acceptance_criteria` as a human readable string", level: RequirementLevel::Must, }, SpecRequirement { + spec_ref: SpecRef::Nip11ListCurationPolicy, line: 30, - section: "Nostr Relay", + section: "NIP-11", text: "MUST list brief summary of curation policy under `curation` if events are curated beyond generic SPAM prevention; otherwise `curation` MUST be omitted", level: RequirementLevel::Must, }, // Git Smart HTTP Service section SpecRequirement { + spec_ref: SpecRef::GitServeRepository, line: 34, section: "Git Smart HTTP Service", - text: "MUST serve a git repository via an unauthenticated git smart http service at `//.git` for each accepted git repository announcement.", + text: "MUST serve a git repository via an unauthenticated git smart http service at `//.git` for each git repository announcement the relay serves or has in purgatory.", level: RequirementLevel::Must, }, SpecRequirement { + spec_ref: SpecRef::GitAcceptPushesAlignState, line: 36, section: "Git Smart HTTP Service", - text: "MUST accept pushes via this service that match the latest repo state announcement on the relay, respecting the recursive maintainer set.", + text: "MUST accept pushes via this service that fully align the git repository state with a repo state announcement in purgatory that is authorised for this repository, respecting the recursive maintainer set.", level: RequirementLevel::Must, }, SpecRequirement { - line: 38, + spec_ref: SpecRef::GitSetHeadOnReceive, + line: 39, section: "Git Smart HTTP Service", - text: "MUST set repository HEAD per repo state announcement as soon as the git data related to that branch has been received.", + text: "As soon as the `receive-pack` is successful, the server MUST: 1. Release the event (and related repository announcement) from purgatory. 2. Align the repository HEAD with the repo state announcement. 3. Synchronize git state with other git repositories on the server for which this state event is authoritative.", level: RequirementLevel::Must, }, SpecRequirement { - line: 40, + spec_ref: SpecRef::GitAcceptRefsNostrEventId, + line: 45, section: "Git Smart HTTP Service", - text: "MUST accept pushes via this service to `refs/nostr/` but SHOULD reject if event exists on relay listing a different tip and MAY reject based on criteria such as size, SPAM prevention, etc. SHOULD delete and MAY garbage collect these refs if no corresponding git PR event or git PR update event, with a `c` tag that matches the ref tip, is accepted by relay within 20 minutes.", + text: "MUST accept pushes via this service to `refs/nostr/` but SHOULD reject if the event exists in purgatory listing a different tip, and MAY reject based on criteria such as size, SPAM prevention, etc.", level: RequirementLevel::Must, }, SpecRequirement { - line: 42, + spec_ref: SpecRef::GitIncludeAllowSha1InWant, + line: 56, section: "Git Smart HTTP Service", text: "MUST include `allow-reachable-sha1-in-want` and `allow-tip-sha1-in-want` in advertisement and serve available oids.", level: RequirementLevel::Must, }, SpecRequirement { - line: 44, + spec_ref: SpecRef::GitServeWebpage, + line: 58, section: "Git Smart HTTP Service", text: "SHOULD serve a webpage at the same endpoint linking to git nostr client(s) to browse the repository and a 404 page for repositories it doesn't host.", level: RequirementLevel::Should, }, // CORS Support section SpecRequirement { - line: 50, + spec_ref: SpecRef::CorsAllowOrigin, + line: 64, section: "CORS Support", text: "Set `Access-Control-Allow-Origin: *` on ALL responses", level: RequirementLevel::Must, }, SpecRequirement { - line: 51, + spec_ref: SpecRef::CorsAllowMethods, + line: 65, section: "CORS Support", text: "Set `Access-Control-Allow-Methods: GET, POST` on ALL responses", level: RequirementLevel::Must, }, SpecRequirement { - line: 52, + spec_ref: SpecRef::CorsAllowHeaders, + line: 66, section: "CORS Support", text: "Set `Access-Control-Allow-Headers: Content-Type` on ALL responses", level: RequirementLevel::Must, }, SpecRequirement { - line: 53, + spec_ref: SpecRef::CorsOptionsResponse, + line: 67, section: "CORS Support", text: "Respond to OPTIONS requests with 204 No Content", level: RequirementLevel::Must, @@ -163,6 +244,13 @@ pub fn get_requirement(line: u32) -> Option<&'static SpecRequirement> { GRASP_01_REQUIREMENTS.iter().find(|r| r.line == line) } +/// Get a requirement by its SpecRef +pub fn get_requirement_by_ref(spec_ref: SpecRef) -> Option<&'static SpecRequirement> { + GRASP_01_REQUIREMENTS + .iter() + .find(|r| r.spec_ref == spec_ref) +} + /// Get all requirements for a section pub fn get_requirements_for_section(section: &str) -> Vec<&'static SpecRequirement> { GRASP_01_REQUIREMENTS @@ -193,17 +281,39 @@ mod tests { assert!(req.text.contains("NIP-01")); } + #[test] + fn test_get_requirement_by_ref() { + let req = get_requirement_by_ref(SpecRef::NostrRelayNip01Compliant) + .expect("SpecRef should exist"); + assert_eq!(req.line, 7); + assert_eq!(req.spec_ref, SpecRef::NostrRelayNip01Compliant); + } + #[test] fn test_get_sections() { let sections = get_sections(); - assert_eq!(sections.len(), 3); + assert_eq!(sections.len(), 5); assert_eq!(sections[0], "Nostr Relay"); - assert_eq!(sections[1], "Git Smart HTTP Service"); - assert_eq!(sections[2], "CORS Support"); + assert_eq!(sections[1], "Purgatory"); + assert_eq!(sections[2], "NIP-11"); + assert_eq!(sections[3], "Git Smart HTTP Service"); + assert_eq!(sections[4], "CORS Support"); } #[test] fn test_requirement_count() { - assert_eq!(GRASP_01_REQUIREMENTS.len(), 19); + assert_eq!(GRASP_01_REQUIREMENTS.len(), 20); + } + + #[test] + fn test_spec_ref_unique() { + let mut refs = std::collections::HashSet::new(); + for req in GRASP_01_REQUIREMENTS { + assert!( + refs.insert(req.spec_ref), + "Duplicate SpecRef found: {:?}", + req.spec_ref + ); + } } } -- cgit v1.2.3 From dcaaa0c44c46f963929ab0baa91f63759ec702dc Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 12 Feb 2026 12:57:44 +0000 Subject: refactor(grasp-audit): split ValidRepo into Sent/Served, add tolerant purgatory - Rename ValidRepo to ValidRepoSent (announcement sent, may be in purgatory) - Add ValidRepoServed (announcement queryable after git data pushed) - Add send_event_and_note_purgatory() for tolerant purgatory detection - Update fixtures to use tolerant method instead of strict assertion - Update event_acceptance_policy tests to use ValidRepoServed This enables tests to pass regardless of purgatory implementation status while still having explicit purgatory tests that verify the behavior. --- grasp-audit/README.md | 2 +- grasp-audit/src/client.rs | 30 +++++ grasp-audit/src/fixtures.rs | 139 ++++++++++++--------- grasp-audit/src/specs/grasp01/cors.rs | 2 +- .../src/specs/grasp01/event_acceptance_policy.rs | 120 +++++++++++------- grasp-audit/src/specs/grasp01/git_clone.rs | 6 +- grasp-audit/src/specs/grasp01/git_filter.rs | 6 +- grasp-audit/src/specs/grasp01/nip01_smoke.rs | 4 +- .../src/specs/grasp01/push_authorization.rs | 16 +-- .../src/specs/grasp01/repository_creation.rs | 6 +- 10 files changed, 204 insertions(+), 127 deletions(-) (limited to 'grasp-audit/src/specs/grasp01/push_authorization.rs') diff --git a/grasp-audit/README.md b/grasp-audit/README.md index 4d2401f..2cc9247 100644 --- a/grasp-audit/README.md +++ b/grasp-audit/README.md @@ -245,7 +245,7 @@ pub async fn test_something(client: &AuditClient) -> TestResult { let ctx = TestContext::new(client); // 2. Prerequisites (cached per-TestContext) - let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?; + let repo = ctx.get_fixture(FixtureKind::ValidRepoSent).await?; // 3. Test-specific event let my_event = client.create_issue(&repo, "Title", "Content", vec![])?; diff --git a/grasp-audit/src/client.rs b/grasp-audit/src/client.rs index 91a93dc..5c263ad 100644 --- a/grasp-audit/src/client.rs +++ b/grasp-audit/src/client.rs @@ -209,6 +209,36 @@ impl AuditClient { Ok(event_id) } + /// Send event and note whether it entered purgatory (not served) or was served immediately. + /// + /// This is a tolerant version of `send_event_expect_purgatory_not_served` that doesn't + /// fail if purgatory is not observed. It returns whether purgatory was observed so + /// fixtures can proceed regardless of relay implementation status. + /// + /// Returns (EventId, bool) where bool = true if event was NOT served (purgatory observed). + pub async fn send_event_and_note_purgatory(&self, event: Event) -> Result<(EventId, bool)> { + if self.config.read_only { + return Err(anyhow!("Client is in read-only mode")); + } + + let output = self.client.send_event(&event).await?; + let event_id = *output.id(); + + // Check if any relay rejected the event and return the error message + if !output.failed.is_empty() { + let (relay_url, error) = output.failed.iter().next().unwrap(); + return Err(anyhow!("Relay {} rejected event: {}", relay_url, error)); + } + + // Wait a bit for event to propagate + tokio::time::sleep(Duration::from_millis(300)).await; + + // Check if event is served (not in purgatory) or not served (in purgatory) + let in_purgatory = !self.is_event_on_relay(event.id).await?; + + Ok((event_id, in_purgatory)) + } + /// check if an event is on the relay pub async fn is_event_on_relay(&self, id: EventId) -> Result { Ok(!self diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index e1a5320..2c53bf0 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs @@ -47,7 +47,7 @@ //! let ctx = TestContext::new(&client); //! //! // Request a fixture - behavior depends on mode -//! let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?; +//! let repo = ctx.get_fixture(FixtureKind::ValidRepoSent).await?; //! # Ok(()) //! # } //! ``` @@ -109,11 +109,11 @@ pub const PR_TEST_COMMIT_HASH: &str = "5d40fb1555a0c28bf4d650515a73aaa54d4d9bfb" /// /// ## Fixture Dependencies /// -/// Several fixtures depend on `ValidRepo` - they all use the SAME repo_id +/// Several fixtures depend on `ValidRepoSent` - they all use the SAME repo_id /// within a single TestContext instance to ensure proper fixture relationships: -/// - `RepoState` → uses ValidRepo's repo_id -/// - `MaintainerAnnouncement` + `MaintainerState` → uses ValidRepo's repo_id -/// - `RecursiveMaintainerRepoAndState` → uses ValidRepo's repo_id +/// - `RepoState` → uses ValidRepoSent's repo_id +/// - `MaintainerAnnouncement` + `MaintainerState` → uses ValidRepoSent's repo_id +/// - `RecursiveMaintainerRepoAndState` → uses ValidRepoSent's repo_id /// /// This enables testing recursive maintainer authorization chains where multiple /// parties publish announcements and state events for the same repository. @@ -122,10 +122,16 @@ pub enum FixtureKind { /// Basic repository announcement (kind 30617) /// - Signed by owner keys (`client.keys()`) /// - Lists `client.maintainer_pubkey_hex()` in maintainers tag - ValidRepo, + ValidRepoSent, + + /// Repository announcement that is queryable from the relay (served, not in purgatory) + /// - Depends on OwnerStateDataPushed (git data pushed, announcement promoted) + /// - Returns the same event as ValidRepoSent (now queryable) + /// - Use this for tests that need to query the announcement back from the relay + ValidRepoServed, /// Repository with one issue (kind 1621) - /// - Requires ValidRepo (reuses same repo_id) + /// - Requires ValidRepoSent (reuses same repo_id) RepoWithIssue, /// Repository with issue and comment (kind 1111) @@ -133,14 +139,14 @@ pub enum FixtureKind { RepoWithComment, /// Repository state announcement (kind 30618) for owner - /// - Requires ValidRepo (uses same repo_id) + /// - Requires ValidRepoSent (uses same repo_id) /// - Signed by owner keys (`client.keys()`) /// - Points to DETERMINISTIC_COMMIT_HASH /// - Timestamp: 10 seconds in the past RepoState, - /// PR (Pull Request) event for the SAME repo_id as ValidRepo - /// - Requires ValidRepo (uses same repo_id) + /// PR (Pull Request) event for the SAME repo_id as ValidRepoSent + /// - Requires ValidRepoSent (uses same repo_id) /// - Signed by `client.pr_author_keys()` /// - Kind 1618 (NIP-34 PR) /// - Includes `a` tag referencing the repo @@ -153,7 +159,7 @@ pub enum FixtureKind { /// This is a "Generated" stage fixture - the event is created but not published. /// Useful for tests that need the PR event ID before the event exists on the relay. /// - /// - Requires ValidRepo (uses same repo_id) + /// - Requires ValidRepoSent (uses same repo_id) /// - Signed by `client.pr_author_keys()` /// - Kind 1618 (NIP-34 PR) /// - Includes `c` tag pointing to PR_TEST_COMMIT_HASH @@ -187,7 +193,7 @@ pub enum FixtureKind { /// (the "wrong" commit), but no PR event exists yet on the relay. /// /// Server state after this fixture: - /// - ValidRepo announcement on relay + /// - ValidRepoSent announcement on relay /// - refs/nostr/ exists on git server with wrong commit /// - PR event is NOT on relay (but returned for tests to publish later) /// @@ -203,7 +209,7 @@ pub enum FixtureKind { /// then the PR event was published (which may trigger cleanup). /// /// Server state after this fixture: - /// - ValidRepo announcement on relay + /// - ValidRepoSent announcement on relay /// - PR event is on relay /// - refs/nostr/ may have been cleaned up (that's what tests verify) /// @@ -221,7 +227,7 @@ pub enum FixtureKind { /// 4. **DataPushed**: Clones repo, creates deterministic commit, pushes to relay /// 5. **Verified**: Confirms event is served by relay /// - /// - Requires ValidRepo (uses same repo_id) + /// - Requires ValidRepoSent (uses same repo_id) /// - State event signed by owner keys (`client.keys()`) /// - Points to DETERMINISTIC_COMMIT_HASH /// - Git push verified to succeed (state matches pushed commit) @@ -252,7 +258,7 @@ pub enum FixtureKind { /// not the owner's announcement, so this tests the recursive maintainer traversal. /// /// This fixture represents the complete flow for testing recursive maintainer push authorization: - /// 1. **Generated**: (MaintainerStateDataPushed dependency includes ValidRepo + OwnerStateDataPushed) + /// 1. **Generated**: (MaintainerStateDataPushed dependency includes ValidRepoSent + OwnerStateDataPushed) /// Creates MaintainerAnnouncement + RecursiveMaintainerState /// 2. **Sent**: Sends events to relay (returns OK, accepted but 'purgatory:...' message) /// 3. **Verify Not Served**: Confirms event is not served by relays @@ -276,16 +282,19 @@ impl FixtureKind { pub fn dependencies(&self) -> Vec { match self { // Base fixtures - no dependencies - Self::ValidRepo => vec![], + Self::ValidRepoSent => vec![], + + // ValidRepoServed depends on OwnerStateDataPushed (announcement promoted after git push) + Self::ValidRepoServed => vec![Self::OwnerStateDataPushed], - // Fixtures that depend on ValidRepo - Self::RepoWithIssue => vec![Self::ValidRepo], - Self::RepoState => vec![Self::ValidRepo], - Self::PREvent => vec![Self::ValidRepo], - Self::PREventGenerated => vec![Self::ValidRepo], + // Fixtures that depend on ValidRepoServed (need queryable announcement) + Self::RepoWithIssue => vec![Self::ValidRepoServed], + Self::RepoState => vec![Self::ValidRepoSent], + Self::PREvent => vec![Self::ValidRepoSent], + Self::PREventGenerated => vec![Self::ValidRepoSent], Self::PRWrongCommitPushedBeforeEvent => vec![Self::PREventGenerated], Self::PREventSentAfterWrongPush => vec![Self::PRWrongCommitPushedBeforeEvent], - Self::OwnerStateDataPushed => vec![Self::ValidRepo], + Self::OwnerStateDataPushed => vec![Self::ValidRepoSent], // Fixtures that depend on RepoWithIssue Self::RepoWithComment => vec![Self::RepoWithIssue], @@ -323,6 +332,8 @@ impl FixtureKind { Self::PREventSentAfterWrongPush => true, // HeadSetToDevelopBranch sends its state event internally Self::HeadSetToDevelopBranch => true, + // ValidRepoServed doesn't send anything itself, just returns cached event + Self::ValidRepoServed => true, // All other fixtures return a single event for the caller to send _ => false, } @@ -373,7 +384,7 @@ impl From for ContextMode { /// let ctx = TestContext::new(&client); /// /// // Get a repository fixture - will be reused by subsequent TestContexts -/// let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?; +/// let repo = ctx.get_fixture(FixtureKind::ValidRepoSent).await?; /// /// // For cargo test (isolated fixtures) /// let config = AuditConfig::isolated(); @@ -381,7 +392,7 @@ impl From for ContextMode { /// let ctx = TestContext::new(&client); /// /// // Get a repository fixture - fresh for this TestContext only -/// let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?; +/// let repo = ctx.get_fixture(FixtureKind::ValidRepoSent).await?; /// # Ok(()) /// # } /// ``` @@ -436,7 +447,7 @@ impl<'a> TestContext<'a> { /// ```no_run /// # use grasp_audit::*; /// # async fn example(ctx: &TestContext<'_>) -> anyhow::Result<()> { - /// let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?; + /// let repo = ctx.get_fixture(FixtureKind::ValidRepoSent).await?; /// # Ok(()) /// # } /// ``` @@ -517,7 +528,7 @@ impl<'a> TestContext<'a> { /// ```no_run /// # use grasp_audit::*; /// # async fn example(ctx: &TestContext<'_>) -> anyhow::Result<()> { - /// // This ensures ValidRepo exists first, then creates RepoState + /// // This ensures ValidRepoSent exists first, then creates RepoState /// let state = ctx.ensure_fixture(FixtureKind::RepoState).await?; /// # Ok(()) /// # } @@ -625,10 +636,10 @@ impl<'a> TestContext<'a> { /// already-cached dependencies. async fn build_fixture_inner(&self, kind: FixtureKind) -> Result { match kind { - FixtureKind::ValidRepo => { - // ValidRepo has no dependencies - create a new repo announcement + FixtureKind::ValidRepoSent => { + // ValidRepoSent has no dependencies - create a new repo announcement let test_name = format!( - "fixture-ValidRepo-{}", + "fixture-ValidRepoSent-{}", &uuid::Uuid::new_v4().to_string()[..8] ); @@ -638,9 +649,15 @@ impl<'a> TestContext<'a> { .with_context(|| format!("create_repo_announcement failed for {}", test_name)) } + FixtureKind::ValidRepoServed => { + // OwnerStateDataPushed is already ensured as a dependency. + // The announcement is now promoted (served). Return the cached ValidRepoSent event. + self.get_cached_dependency(FixtureKind::ValidRepoSent) + } + FixtureKind::RepoWithIssue => { - // ValidRepo is ensured by ensure_fixture before this is called - let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; + // ValidRepoServed is ensured by ensure_fixture before this is called + let repo = self.get_cached_dependency(FixtureKind::ValidRepoServed)?; // Build issue referencing it - caller will send it self.client @@ -658,8 +675,8 @@ impl<'a> TestContext<'a> { FixtureKind::RepoState => { use nostr_sdk::prelude::*; - // ValidRepo is ensured by ensure_fixture before this is called - let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; + // ValidRepoSent is ensured by ensure_fixture before this is called + let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; // Extract repo_id from repo announcement let repo_id = repo @@ -695,15 +712,15 @@ impl<'a> TestContext<'a> { FixtureKind::PREvent => { use nostr_sdk::prelude::*; - // ValidRepo is ensured by ensure_fixture before this is called - let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; + // ValidRepoSent is ensured by ensure_fixture before this is called + let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; let repo_id = repo .tags .iter() .find(|t| t.kind() == TagKind::d()) .and_then(|t| t.content()) - .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepo fixture"))? + .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepoSent fixture"))? .to_string(); // Create PR event 1 second in the past @@ -738,15 +755,15 @@ impl<'a> TestContext<'a> { // This fixture is for "Generated" stage only use nostr_sdk::prelude::*; - // ValidRepo is ensured by ensure_fixture before this is called - let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; + // ValidRepoSent is ensured by ensure_fixture before this is called + let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; let repo_id = repo .tags .iter() .find(|t| t.kind() == TagKind::d()) .and_then(|t| t.content()) - .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepo fixture"))? + .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepoSent fixture"))? .to_string(); // Create PR event 1 second in the past @@ -873,9 +890,9 @@ impl<'a> TestContext<'a> { use nostr_sdk::prelude::*; // ============================================================ - // Stage 1: ValidRepo is ensured by ensure_fixture before this is called + // Stage 1: ValidRepoSent is ensured by ensure_fixture before this is called // ============================================================ - let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; + let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; let repo_id = self.extract_repo_id(&repo)?; // Build state event @@ -901,9 +918,11 @@ impl<'a> TestContext<'a> { // ============================================================ // Stage 2 & 3: Send to Relay, get Accepted response and Verify its Not Served // ============================================================ - self.client - .send_event_expect_purgatory_not_served(state_event.clone()) + let (_, _in_purgatory) = self + .client + .send_event_and_note_purgatory(state_event.clone()) .await?; + // Note: We don't fail if purgatory wasn't observed - the fixture proceeds regardless // ============================================================ // Stage 4: DataPushed - Clone repo, create commit, push @@ -1048,8 +1067,8 @@ impl<'a> TestContext<'a> { // Extract repo_id from owner's state event (same d-tag structure) let repo_id = self.extract_repo_id(&owner_state)?; - // Get the repo (ValidRepo, also cached) for the owner's npub - let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; + // Get the repo (ValidRepoSent, also cached) for the owner's npub + let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; // Build maintainer's state event (state event ONLY - no announcement) let base_time = Timestamp::now().as_secs(); @@ -1074,9 +1093,11 @@ impl<'a> TestContext<'a> { // ============================================================ // Stage 2 & 3: Send to Relay, get Accepted response and Verify its Not Served // ============================================================ - self.client - .send_event_expect_purgatory_not_served(maintainer_state_event.clone()) + let (_, _in_purgatory) = self + .client + .send_event_and_note_purgatory(maintainer_state_event.clone()) .await?; + // Note: We don't fail if purgatory wasn't observed - the fixture proceeds regardless // ============================================================ // Stage 4: DataPushed - Clone repo, create maintainer commit, push @@ -1194,7 +1215,7 @@ impl<'a> TestContext<'a> { /// recursive maintainer force-pushes their commit on top. /// /// This handles all stages of the fixture: - /// 1. **Generated**: (MaintainerStateDataPushed dependency includes ValidRepo + OwnerStateDataPushed) + /// 1. **Generated**: (MaintainerStateDataPushed dependency includes ValidRepoSent + OwnerStateDataPushed) /// Creates MaintainerAnnouncement + RecursiveMaintainerState /// 2. **Sent**: Sends events to relay (returns OK, accepted but 'purgatory:...' message) /// 3. **Verify Not Served**: Confirms event is not served by relays @@ -1215,8 +1236,8 @@ impl<'a> TestContext<'a> { // Extract repo_id from maintainer's state event (same d-tag structure) let repo_id = self.extract_repo_id(&maintainer_state)?; - // Get the repo (ValidRepo, also cached) for the owner's npub - let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; + // Get the repo (ValidRepoSent, also cached) for the owner's npub + let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; // ============================================================ // Stage 1 (continued): Generate MaintainerAnnouncement and RecursiveMaintainerState @@ -1249,9 +1270,11 @@ impl<'a> TestContext<'a> { // ============================================================ // Stage 2 & 3: Send to Relay, get Accepted response and Verify its Not Served // ============================================================ - self.client - .send_event_expect_purgatory_not_served(recursive_maintainer_state_event.clone()) + let (_, _in_purgatory) = self + .client + .send_event_and_note_purgatory(recursive_maintainer_state_event.clone()) .await?; + // Note: We don't fail if purgatory wasn't observed - the fixture proceeds regardless // ============================================================ // Stage 4: DataPushed - Clone repo, create recursive maintainer commit, push @@ -1428,7 +1451,7 @@ impl<'a> TestContext<'a> { /// 3. A wrong commit is pushed to refs/nostr/ /// /// Server state after: - /// - ValidRepo announcement on relay + /// - ValidRepoSent announcement on relay /// - refs/nostr/ on git server pointing to DETERMINISTIC_COMMIT_HASH (wrong) /// - NO PR event on relay /// @@ -1440,8 +1463,8 @@ impl<'a> TestContext<'a> { let pr_event = self.get_cached_dependency(FixtureKind::PREventGenerated)?; let pr_event_id = pr_event.id.to_hex(); - // Get the ValidRepo to extract repo info - let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?; + // Get the ValidRepoSent to extract repo info + let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; let repo_id = self.extract_repo_id(&repo)?; // Get relay domain for cloning @@ -1520,7 +1543,7 @@ impl<'a> TestContext<'a> { /// /// This fixture builds on PRWrongCommitPushedBeforeEvent by sending the PR event. /// After this fixture, the relay has: - /// - ValidRepo announcement + /// - ValidRepoSent announcement /// - PR event /// - refs/nostr/ may have been cleaned up (that's what tests verify) /// @@ -2040,10 +2063,10 @@ mod tests { use std::collections::HashSet; let mut set = HashSet::new(); - set.insert(FixtureKind::ValidRepo); + set.insert(FixtureKind::ValidRepoSent); set.insert(FixtureKind::RepoWithIssue); - assert!(set.contains(&FixtureKind::ValidRepo)); + assert!(set.contains(&FixtureKind::ValidRepoSent)); assert!(!set.contains(&FixtureKind::RepoWithComment)); } diff --git a/grasp-audit/src/specs/grasp01/cors.rs b/grasp-audit/src/specs/grasp01/cors.rs index eba9e42..e5d9a27 100644 --- a/grasp-audit/src/specs/grasp01/cors.rs +++ b/grasp-audit/src/specs/grasp01/cors.rs @@ -246,7 +246,7 @@ impl CorsTests { let ctx = TestContext::new(client); // Create repository announcement to get a real repo path - let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(r) => r, Err(e) => { return TestResult::new( diff --git a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs index 8259283..3375c4d 100644 --- a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs +++ b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs @@ -157,12 +157,15 @@ impl EventAcceptancePolicyTests { let ctx = TestContext::new(client); // Request repository fixture - behavior depends on mode - let event = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { - format!( - "Test setup failed: could not get valid repository fixture: {}", - e - ) - })?; + let event = ctx + .get_fixture(FixtureKind::ValidRepoServed) + .await + .map_err(|e| { + format!( + "Test setup failed: could not get valid repository fixture: {}", + e + ) + })?; // Get relay URL for validation let relay_url = client @@ -602,12 +605,15 @@ impl EventAcceptancePolicyTests { let ctx = TestContext::new(client); // NEW: Get repository fixture (mode-aware) - let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { - format!( - "Test setup failed: could not get valid repository fixture: {}", - e - ) - })?; + let repo = ctx + .get_fixture(FixtureKind::ValidRepoServed) + .await + .map_err(|e| { + format!( + "Test setup failed: could not get valid repository fixture: {}", + e + ) + })?; // 2. Create issue that references the repo let issue = Self::create_issue_for_repo(client, &repo, "Test Issue 1")?; @@ -637,12 +643,15 @@ impl EventAcceptancePolicyTests { let ctx = TestContext::new(client); // Get repository fixture (mode-aware) - let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { - format!( - "Test setup failed: could not get valid repository fixture: {}", - e - ) - })?; + let repo = ctx + .get_fixture(FixtureKind::ValidRepoServed) + .await + .map_err(|e| { + format!( + "Test setup failed: could not get valid repository fixture: {}", + e + ) + })?; // Extract repo_id and create `A` tag manually let repo_id = @@ -690,12 +699,15 @@ impl EventAcceptancePolicyTests { let ctx = TestContext::new(client); // Get repository fixture (mode-aware) - let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { - format!( - "Test setup failed: could not get valid repository fixture: {}", - e - ) - })?; + let repo = ctx + .get_fixture(FixtureKind::ValidRepoServed) + .await + .map_err(|e| { + format!( + "Test setup failed: could not get valid repository fixture: {}", + e + ) + })?; // Extract repo_id and create `q` tag let repo_id = @@ -825,12 +837,15 @@ impl EventAcceptancePolicyTests { let ctx = TestContext::new(client); // Get repository fixture (mode-aware) - let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { - format!( - "Test setup failed: could not get valid repository fixture: {}", - e - ) - })?; + let repo = ctx + .get_fixture(FixtureKind::ValidRepoServed) + .await + .map_err(|e| { + format!( + "Test setup failed: could not get valid repository fixture: {}", + e + ) + })?; // Create Kind 1 A that quotes the repo (makes it accepted) let repo_id = Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id")?; @@ -881,12 +896,15 @@ impl EventAcceptancePolicyTests { let ctx = TestContext::new(client); // Get repository fixture (mode-aware) - let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { - format!( - "Test setup failed: could not get valid repository fixture: {}", - e - ) - })?; + let repo = ctx + .get_fixture(FixtureKind::ValidRepoServed) + .await + .map_err(|e| { + format!( + "Test setup failed: could not get valid repository fixture: {}", + e + ) + })?; // Verify repo is queryable (ensures it's fully indexed before we reference it) let repo_id = Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id")?; @@ -1034,12 +1052,15 @@ impl EventAcceptancePolicyTests { let ctx = TestContext::new(client); // Get repository fixture (mode-aware) - let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { - format!( - "Test setup failed: could not get valid repository fixture: {}", - e - ) - })?; + let repo = ctx + .get_fixture(FixtureKind::ValidRepoServed) + .await + .map_err(|e| { + format!( + "Test setup failed: could not get valid repository fixture: {}", + e + ) + })?; // Create Kind 1 A locally but DON'T send it yet let kind1_a = client @@ -1148,12 +1169,15 @@ impl EventAcceptancePolicyTests { let ctx = TestContext::new(client); // Get accepted repo A fixture (mode-aware) - let _repo_a = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { - format!( - "Test setup failed: could not get valid repository fixture: {}", - e - ) - })?; + let _repo_a = ctx + .get_fixture(FixtureKind::ValidRepoServed) + .await + .map_err(|e| { + format!( + "Test setup failed: could not get valid repository fixture: {}", + e + ) + })?; // Create Repo B but DON'T send it (unaccepted) let repo_b = Self::create_test_repo(client, "unaccepted-repo-b").await?; diff --git a/grasp-audit/src/specs/grasp01/git_clone.rs b/grasp-audit/src/specs/grasp01/git_clone.rs index fda472b..0c223f4 100644 --- a/grasp-audit/src/specs/grasp01/git_clone.rs +++ b/grasp-audit/src/specs/grasp01/git_clone.rs @@ -49,7 +49,7 @@ impl GitCloneTests { let ctx = TestContext::new(client); // Create repository announcement - let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(r) => r, Err(e) => { return TestResult::new( @@ -171,7 +171,7 @@ impl GitCloneTests { let ctx = TestContext::new(client); // Create repository announcement - let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(r) => r, Err(e) => { return TestResult::new( @@ -274,7 +274,7 @@ impl GitCloneTests { let ctx = TestContext::new(client); // Create repository announcement - let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(r) => r, Err(e) => { return TestResult::new( diff --git a/grasp-audit/src/specs/grasp01/git_filter.rs b/grasp-audit/src/specs/grasp01/git_filter.rs index 7f203a2..31d86aa 100644 --- a/grasp-audit/src/specs/grasp01/git_filter.rs +++ b/grasp-audit/src/specs/grasp01/git_filter.rs @@ -62,7 +62,7 @@ impl GitFilterTests { let ctx = TestContext::new(client); // Create repository announcement - let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(r) => r, Err(e) => { return TestResult::new( @@ -185,7 +185,7 @@ impl GitFilterTests { let ctx = TestContext::new(client); // Create repository announcement - let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(r) => r, Err(e) => { return TestResult::new( @@ -296,7 +296,7 @@ impl GitFilterTests { let ctx = TestContext::new(client); // Create repository announcement - let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(r) => r, Err(e) => { return TestResult::new( diff --git a/grasp-audit/src/specs/grasp01/nip01_smoke.rs b/grasp-audit/src/specs/grasp01/nip01_smoke.rs index 5976252..8cb4166 100644 --- a/grasp-audit/src/specs/grasp01/nip01_smoke.rs +++ b/grasp-audit/src/specs/grasp01/nip01_smoke.rs @@ -69,7 +69,7 @@ impl Nip01SmokeTests { // Step 1: GENERATE - Create TestContext and get ValidRepo fixture let ctx = TestContext::new(client); let event = ctx - .get_fixture(FixtureKind::ValidRepo) + .get_fixture(FixtureKind::ValidRepoSent) .await .map_err(|e| format!("Failed to create ValidRepo fixture: {}", e))?; @@ -135,7 +135,7 @@ impl Nip01SmokeTests { // Step 1: GENERATE - Create TestContext and get ValidRepo fixture let ctx = TestContext::new(client); let _event = ctx - .get_fixture(FixtureKind::ValidRepo) + .get_fixture(FixtureKind::ValidRepoSent) .await .map_err(|e| format!("Failed to create ValidRepo fixture: {}", e))?; diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index be354a0..78ef471 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs @@ -208,7 +208,7 @@ async fn setup_pr_test_repo( ) -> Result<(PathBuf, String, String, String), String> { // Get fixtures let repo_event = ctx - .get_fixture(FixtureKind::ValidRepo) + .get_fixture(FixtureKind::ValidRepoSent) .await .map_err(|e| format!("Failed to get repo announcement: {}", e))?; @@ -407,7 +407,7 @@ impl PushAuthorizationTests { let ctx = TestContext::new(client); // Create repository (no state event) - let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(r) => r, Err(e) => { return TestResult::new( @@ -956,7 +956,7 @@ impl PushAuthorizationTests { // ============================================================ let ctx = TestContext::new(client); - let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(r) => r, Err(e) => { return TestResult::new( @@ -1110,7 +1110,7 @@ impl PushAuthorizationTests { let pr_event_id = pr_event.id.to_hex(); // Get repo info for cloning (fresh clone for verification) - let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(r) => r, Err(e) => { return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) @@ -1198,7 +1198,7 @@ impl PushAuthorizationTests { let pr_event_id = pr_event.id.to_hex(); // Get repo info for cloning (fresh clone for this test) - let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(r) => r, Err(e) => { return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) @@ -1289,7 +1289,7 @@ impl PushAuthorizationTests { let pr_event_id = pr_event.id.to_hex(); // Get repo info for cloning (fresh clone for this test) - let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(r) => r, Err(e) => { return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) @@ -1425,7 +1425,7 @@ impl PushAuthorizationTests { // ============================================================ // Step 2: Extract repo_id and owner npub from ValidRepo (cached by fixture) // ============================================================ - let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(e) => e, Err(e) => { return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) @@ -1528,7 +1528,7 @@ impl PushAuthorizationTests { // ============================================================ // Step 2: Extract repo_id and owner npub from ValidRepo (cached by fixture) // ============================================================ - let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(e) => e, Err(e) => { return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc) diff --git a/grasp-audit/src/specs/grasp01/repository_creation.rs b/grasp-audit/src/specs/grasp01/repository_creation.rs index a702afe..5730f1c 100644 --- a/grasp-audit/src/specs/grasp01/repository_creation.rs +++ b/grasp-audit/src/specs/grasp01/repository_creation.rs @@ -51,7 +51,7 @@ impl RepositoryCreationTests { let ctx = TestContext::new(client); // Use TestContext to create and send repository announcement - let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(r) => r, Err(e) => { return TestResult::new( @@ -131,7 +131,7 @@ impl RepositoryCreationTests { let ctx = TestContext::new(client); // Create a repository announcement - let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(r) => r, Err(e) => { return TestResult::new( @@ -210,7 +210,7 @@ impl RepositoryCreationTests { let ctx = TestContext::new(client); - let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { Ok(r) => r, Err(e) => { return TestResult::new( -- cgit v1.2.3 From faac6027deaf5f1e121c05df2d8a6336fd6eaf8d Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 13 Feb 2026 09:09:59 +0000 Subject: fix: add trailing newlines to deterministic commit content The CommitVariant::file_content() methods were returning strings without trailing newlines, but the expected hash constants were calculated with trailing newlines. This caused hash mismatches in tests. Updated all hash constants to match the actual commit hashes produced with trailing newlines in the file content. --- grasp-audit/README.md | 8 +++--- grasp-audit/src/fixtures.rs | 29 +++++++++++----------- .../src/specs/grasp01/push_authorization.rs | 8 +++--- 3 files changed, 22 insertions(+), 23 deletions(-) (limited to 'grasp-audit/src/specs/grasp01/push_authorization.rs') diff --git a/grasp-audit/README.md b/grasp-audit/README.md index 2cc9247..936f10f 100644 --- a/grasp-audit/README.md +++ b/grasp-audit/README.md @@ -298,10 +298,10 @@ Fixtures use deterministic commit hashes for reproducible testing: | Constant | Hash | Used By | | ------------------------------------------------ | ------------------------------------------ | ------------------------------------------------ | -| `DETERMINISTIC_COMMIT_HASH` | `64ea71d79a57a7acb334cd9651f8aec067c0ce5d` | Owner fixtures (RepoState, OwnerStateDataPushed) | -| `MAINTAINER_DETERMINISTIC_COMMIT_HASH` | `1c2d472c9b71ed51968a66500281a3c4a6840464` | MaintainerStateDataPushed | -| `RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH` | `05939b82de66fbdb9c077d0a64fc68522f3cb8e0` | RecursiveMaintainerStateDataPushed | -| `PR_TEST_COMMIT_HASH` | `5d40fb1555a0c28bf4d650515a73aaa54d4d9bfb` | PR fixtures (PREvent, PREventGenerated) | +| `DETERMINISTIC_COMMIT_HASH` | `d6e4b26ccf9c268d18d60e6d09804313cc850821` | Owner fixtures (RepoState, OwnerStateDataPushed) | +| `MAINTAINER_DETERMINISTIC_COMMIT_HASH` | `d26703c007eff6d17fee3bb70ce8be5d1427d0e7` | MaintainerStateDataPushed | +| `RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH` | `54a2b4b3cbc3373ad1438b8ffad1681d12bc6c4a` | RecursiveMaintainerStateDataPushed | +| `PR_TEST_COMMIT_HASH` | `5a51b30e4615b572dcd5b9e487861b58605a5c21` | PR fixtures (PREvent, PREventGenerated) | #### Fixture Dependencies diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index 2c53bf0..56d29ef 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs @@ -61,49 +61,47 @@ use std::sync::{Arc, Mutex}; /// Deterministic commit hash used in RepoState fixtures (Owner variant) /// This is the hash produced by creating a commit with: /// - Message: "Initial commit" -/// - File: test.txt containing "Initial commit" +/// - File: test.txt containing "Initial commit\n" (with trailing newline) /// - Author date: 2024-01-01T00:00:00Z /// - Committer date: 2024-01-01T00:00:00Z /// - GPG signing: disabled /// - User: "GRASP Audit Test " -/// - Parent: Initial empty commit (09cc37de80f3434fa98864a86730b8d7777bd6ae) -pub const DETERMINISTIC_COMMIT_HASH: &str = "64ea71d79a57a7acb334cd9651f8aec067c0ce5d"; +/// - Parent: none (root commit) +pub const DETERMINISTIC_COMMIT_HASH: &str = "d6e4b26ccf9c268d18d60e6d09804313cc850821"; /// Deterministic commit hash for maintainer fixtures (Maintainer variant) /// This is the hash produced by creating a commit with: /// - Message: "Maintainer initial commit" -/// - File: test.txt containing "Maintainer initial commit" +/// - File: test.txt containing "Maintainer initial commit\n" (with trailing newline) /// - Author date: 2024-01-01T00:00:00Z /// - Committer date: 2024-01-01T00:00:00Z /// - GPG signing: disabled /// - User: "GRASP Audit Test " /// - Parent: none (root commit) -/// NOTE: This value is different from DETERMINISTIC_COMMIT_HASH due to different content -pub const MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = "1c2d472c9b71ed51968a66500281a3c4a6840464"; +pub const MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = "d26703c007eff6d17fee3bb70ce8be5d1427d0e7"; /// Deterministic commit hash for recursive maintainer fixtures (RecursiveMaintainer variant) /// This is the hash produced by creating a commit with: /// - Message: "Recursive maintainer initial commit" -/// - File: test.txt containing "Recursive maintainer initial commit" +/// - File: test.txt containing "Recursive maintainer initial commit\n" (with trailing newline) /// - Author date: 2024-01-01T00:00:00Z /// - Committer date: 2024-01-01T00:00:00Z /// - GPG signing: disabled /// - User: "GRASP Audit Test " /// - Parent: none (root commit) -/// NOTE: This value is different from DETERMINISTIC_COMMIT_HASH due to different content pub const RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = - "05939b82de66fbdb9c077d0a64fc68522f3cb8e0"; + "54a2b4b3cbc3373ad1438b8ffad1681d12bc6c4a"; /// Deterministic commit hash for PR test fixtures (PRTestCommit variant) /// This is the hash produced by creating a commit with: /// - Message: "PR test deterministic commit" -/// - File: test.txt containing "PR test deterministic commit" +/// - File: test.txt containing "PR test deterministic commit\n" (with trailing newline) /// - Author date: 2024-01-01T00:00:00Z /// - Committer date: 2024-01-01T00:00:00Z /// - GPG signing: disabled /// - User: "GRASP Audit Test " /// - Parent: none (root commit) -pub const PR_TEST_COMMIT_HASH: &str = "5d40fb1555a0c28bf4d650515a73aaa54d4d9bfb"; +pub const PR_TEST_COMMIT_HASH: &str = "5a51b30e4615b572dcd5b9e487861b58605a5c21"; /// Types of test fixtures available /// @@ -294,6 +292,7 @@ impl FixtureKind { Self::PREventGenerated => vec![Self::ValidRepoSent], Self::PRWrongCommitPushedBeforeEvent => vec![Self::PREventGenerated], Self::PREventSentAfterWrongPush => vec![Self::PRWrongCommitPushedBeforeEvent], + Self::OwnerStateDataPushed => vec![Self::ValidRepoSent], // Fixtures that depend on RepoWithIssue @@ -1874,10 +1873,10 @@ 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", - CommitVariant::PRTestCommit => "PR test deterministic commit", + CommitVariant::Owner => "Initial commit\n", + CommitVariant::Maintainer => "Maintainer initial commit\n", + CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit\n", + CommitVariant::PRTestCommit => "PR test deterministic commit\n", } } diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index 78ef471..dc78b49 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs @@ -19,7 +19,7 @@ /// Expected hash for PR test deterministic commit /// /// This hash is produced by creating a commit with: -/// - File: test.txt containing "PR test deterministic commit" +/// - File: test.txt containing "PR test deterministic commit\n" (with trailing newline) /// - Message: "PR test deterministic commit" /// - Author: "GRASP Audit Test " /// - Author date: 2024-01-01T00:00:00Z @@ -29,7 +29,7 @@ /// /// Run `test_pr_test_commit_hash_discovery` to discover/verify this value. #[allow(dead_code)] -const PR_TEST_COMMIT_HASH: &str = "5d40fb1555a0c28bf4d650515a73aaa54d4d9bfb"; +const PR_TEST_COMMIT_HASH: &str = "5a51b30e4615b572dcd5b9e487861b58605a5c21"; use crate::specs::grasp01::SpecRef; use crate::{ @@ -1722,9 +1722,9 @@ mod tests { .expect("git config name failed"); assert!(output.status.success(), "git config name failed"); - // Create the deterministic file content + // Create the deterministic file content (must match CommitVariant::PRTestCommit exactly) let test_file = path.join("test.txt"); - fs::write(&test_file, "PR test deterministic commit").expect("Failed to write test file"); + fs::write(&test_file, "PR test deterministic commit\n").expect("Failed to write test file"); // Add the file let output = Command::new("git") -- cgit v1.2.3 From a2a99d5a4137b57e4141cf2840f2f51b38035cfa Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 13 Feb 2026 12:07:37 +0000 Subject: fix: use ValidRepoServed for events that tag repo events PR events, issues, and comments need a queryable repo announcement to reference. Changed PREvent and PREventGenerated fixtures and related tests to depend on ValidRepoServed instead of ValidRepoSent. This ensures tests will fail correctly when announcement purgatory is implemented - events tagging a repo should require that repo to be served (not in purgatory). --- grasp-audit/src/fixtures.rs | 34 +++++++++++----------- grasp-audit/src/specs/grasp01/nip01_smoke.rs | 14 ++++----- .../src/specs/grasp01/push_authorization.rs | 8 ++--- 3 files changed, 28 insertions(+), 28 deletions(-) (limited to 'grasp-audit/src/specs/grasp01/push_authorization.rs') diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index 8a51d77..9a00aef 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs @@ -140,7 +140,7 @@ pub enum FixtureKind { ValidRepoServed, /// Repository with one issue (kind 1621) - /// - Requires ValidRepoSent (reuses same repo_id) + /// - Requires ValidRepoServed (needs queryable repo for issue to reference) RepoWithIssue, /// Repository with issue and comment (kind 1111) @@ -154,8 +154,8 @@ pub enum FixtureKind { /// - Timestamp: 10 seconds in the past RepoState, - /// PR (Pull Request) event for the SAME repo_id as ValidRepoSent - /// - Requires ValidRepoSent (uses same repo_id) + /// PR (Pull Request) event for the SAME repo_id as ValidRepoServed + /// - Requires ValidRepoServed (uses same repo_id, needs queryable repo) /// - Signed by `client.pr_author_keys()` /// - Kind 1618 (NIP-34 PR) /// - Includes `a` tag referencing the repo @@ -168,7 +168,7 @@ pub enum FixtureKind { /// This is a "Generated" stage fixture - the event is created but not published. /// Useful for tests that need the PR event ID before the event exists on the relay. /// - /// - Requires ValidRepoSent (uses same repo_id) + /// - Requires ValidRepoServed (uses same repo_id, needs queryable repo) /// - Signed by `client.pr_author_keys()` /// - Kind 1618 (NIP-34 PR) /// - Includes `c` tag pointing to PR_TEST_COMMIT_HASH @@ -202,7 +202,7 @@ pub enum FixtureKind { /// (the "wrong" commit), but no PR event exists yet on the relay. /// /// Server state after this fixture: - /// - ValidRepoSent announcement on relay + /// - ValidRepoServed announcement on relay (repo is queryable) /// - refs/nostr/ exists on git server with wrong commit /// - PR event is NOT on relay (but returned for tests to publish later) /// @@ -218,7 +218,7 @@ pub enum FixtureKind { /// then the PR event was published (which may trigger cleanup). /// /// Server state after this fixture: - /// - ValidRepoSent announcement on relay + /// - ValidRepoServed announcement on relay /// - PR event is on relay /// - refs/nostr/ may have been cleaned up (that's what tests verify) /// @@ -343,8 +343,8 @@ impl FixtureKind { // Fixtures that depend on ValidRepoServed (need queryable announcement) Self::RepoWithIssue => vec![Self::ValidRepoServed], Self::RepoState => vec![Self::ValidRepoSent], - Self::PREvent => vec![Self::ValidRepoSent], - Self::PREventGenerated => vec![Self::ValidRepoSent], + Self::PREvent => vec![Self::ValidRepoServed], + Self::PREventGenerated => vec![Self::ValidRepoServed], Self::PRWrongCommitPushedBeforeEvent => vec![Self::PREventGenerated], Self::PREventSentAfterWrongPush => vec![Self::PRWrongCommitPushedBeforeEvent], @@ -777,15 +777,15 @@ impl<'a> TestContext<'a> { FixtureKind::PREvent => { use nostr_sdk::prelude::*; - // ValidRepoSent is ensured by ensure_fixture before this is called - let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; + // ValidRepoServed is ensured by ensure_fixture before this is called + let repo = self.get_cached_dependency(FixtureKind::ValidRepoServed)?; let repo_id = repo .tags .iter() .find(|t| t.kind() == TagKind::d()) .and_then(|t| t.content()) - .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepoSent fixture"))? + .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepoServed fixture"))? .to_string(); // Create PR event 1 second in the past @@ -820,15 +820,15 @@ impl<'a> TestContext<'a> { // This fixture is for "Generated" stage only use nostr_sdk::prelude::*; - // ValidRepoSent is ensured by ensure_fixture before this is called - let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; + // ValidRepoServed is ensured by ensure_fixture before this is called + let repo = self.get_cached_dependency(FixtureKind::ValidRepoServed)?; let repo_id = repo .tags .iter() .find(|t| t.kind() == TagKind::d()) .and_then(|t| t.content()) - .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepoSent fixture"))? + .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepoServed fixture"))? .to_string(); // Create PR event 1 second in the past @@ -1533,8 +1533,8 @@ impl<'a> TestContext<'a> { let pr_event = self.get_cached_dependency(FixtureKind::PREventGenerated)?; let pr_event_id = pr_event.id.to_hex(); - // Get the ValidRepoSent to extract repo info - let repo = self.get_cached_dependency(FixtureKind::ValidRepoSent)?; + // Get the ValidRepoServed to extract repo info + let repo = self.get_cached_dependency(FixtureKind::ValidRepoServed)?; let repo_id = self.extract_repo_id(&repo)?; // Get relay domain for cloning @@ -1613,7 +1613,7 @@ impl<'a> TestContext<'a> { /// /// This fixture builds on PRWrongCommitPushedBeforeEvent by sending the PR event. /// After this fixture, the relay has: - /// - ValidRepoSent announcement + /// - ValidRepoServed announcement /// - PR event /// - refs/nostr/ may have been cleaned up (that's what tests verify) /// diff --git a/grasp-audit/src/specs/grasp01/nip01_smoke.rs b/grasp-audit/src/specs/grasp01/nip01_smoke.rs index 8cb4166..e3206fc 100644 --- a/grasp-audit/src/specs/grasp01/nip01_smoke.rs +++ b/grasp-audit/src/specs/grasp01/nip01_smoke.rs @@ -66,12 +66,12 @@ impl Nip01SmokeTests { "MUST accept valid EVENT messages", ) .run(|| async { - // Step 1: GENERATE - Create TestContext and get ValidRepo fixture + // Step 1: GENERATE - Create TestContext and get ValidRepoServed fixture let ctx = TestContext::new(client); let event = ctx - .get_fixture(FixtureKind::ValidRepoSent) + .get_fixture(FixtureKind::ValidRepoServed) .await - .map_err(|e| format!("Failed to create ValidRepo fixture: {}", e))?; + .map_err(|e| format!("Failed to create ValidRepoServed fixture: {}", e))?; let event_id = event.id; @@ -122,7 +122,7 @@ impl Nip01SmokeTests { /// /// ## Fixture-First Pattern /// - /// 1. **Generate**: Create TestContext and get ValidRepo fixture + /// 1. **Generate**: Create TestContext and get ValidRepoServed fixture /// 2. **Send**: Fixture already sends the event to relay /// 3. **Verify**: Subscribe and verify we receive the event pub async fn test_create_subscription(client: &AuditClient) -> TestResult { @@ -132,12 +132,12 @@ impl Nip01SmokeTests { "MUST support REQ subscriptions", ) .run(|| async { - // Step 1: GENERATE - Create TestContext and get ValidRepo fixture + // Step 1: GENERATE - Create TestContext and get ValidRepoServed fixture let ctx = TestContext::new(client); let _event = ctx - .get_fixture(FixtureKind::ValidRepoSent) + .get_fixture(FixtureKind::ValidRepoServed) .await - .map_err(|e| format!("Failed to create ValidRepo fixture: {}", e))?; + .map_err(|e| format!("Failed to create ValidRepoServed fixture: {}", e))?; // Step 2: VERIFY - Subscribe to NIP-34 announcements from this author let filter = Filter::new() diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index dc78b49..768e8f9 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs @@ -208,7 +208,7 @@ async fn setup_pr_test_repo( ) -> Result<(PathBuf, String, String, String), String> { // Get fixtures let repo_event = ctx - .get_fixture(FixtureKind::ValidRepoSent) + .get_fixture(FixtureKind::ValidRepoServed) .await .map_err(|e| format!("Failed to get repo announcement: {}", e))?; @@ -1110,7 +1110,7 @@ impl PushAuthorizationTests { let pr_event_id = pr_event.id.to_hex(); // Get repo info for cloning (fresh clone for verification) - let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoServed).await { Ok(r) => r, Err(e) => { return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) @@ -1198,7 +1198,7 @@ impl PushAuthorizationTests { let pr_event_id = pr_event.id.to_hex(); // Get repo info for cloning (fresh clone for this test) - let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoServed).await { Ok(r) => r, Err(e) => { return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) @@ -1289,7 +1289,7 @@ impl PushAuthorizationTests { let pr_event_id = pr_event.id.to_hex(); // Get repo info for cloning (fresh clone for this test) - let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await { + let repo = match ctx.get_fixture(FixtureKind::ValidRepoServed).await { Ok(r) => r, Err(e) => { return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc) -- cgit v1.2.3 From fefb37e040eb3cf91093d597737e1431fed38c81 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 18 Feb 2026 22:12:22 +0000 Subject: fix: use unique commit instead of deterministic Owner variant for wrong-commit PR tests PRWrongCommitPushedBeforeEvent and test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected were calling create_deterministic_commit_with_variant(CommitVariant::Owner) on a clone that already had test.txt with 'Initial commit\n' content from OwnerStateDataPushed. Writing identical content staged nothing so git commit failed silently. Now that ValidRepoServed always depends on OwnerStateDataPushed (git data pushed), the clone is never empty - use create_commit (unique file) instead since the wrong commit only needs to differ from PR_TEST_COMMIT_HASH, not be deterministic. --- grasp-audit/src/fixtures.rs | 10 +++++++--- grasp-audit/src/specs/grasp01/push_authorization.rs | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) (limited to 'grasp-audit/src/specs/grasp01/push_authorization.rs') diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index fc6e8cb..45d3094 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs @@ -1579,10 +1579,14 @@ impl<'a> TestContext<'a> { let _ = fs::remove_dir_all(path); }; - // Create a WRONG commit (Owner variant, not PRTestCommit) - // This commit hash will NOT match what's in the PR event's `c` tag + // Create a WRONG commit using a unique file (not PRTestCommit) + // We use create_commit (non-deterministic) so it always succeeds even if the + // repo already has a commit (e.g. from OwnerStateDataPushed) with the same + // deterministic content. The only requirement is that the hash differs from + // PR_TEST_COMMIT_HASH, which is guaranteed since PR_TEST_COMMIT_HASH is a + // deterministic root-commit with specific content and dates. let wrong_commit_hash = - match create_deterministic_commit_with_variant(&clone_path, CommitVariant::Owner) { + match create_commit(&clone_path, "wrong commit - not the PR test commit") { Ok(h) => h, Err(e) => { cleanup(&clone_path); diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs index 768e8f9..73cbe1f 100644 --- a/grasp-audit/src/specs/grasp01/push_authorization.rs +++ b/grasp-audit/src/specs/grasp01/push_authorization.rs @@ -1231,9 +1231,9 @@ impl PushAuthorizationTests { } }; - // Create a wrong commit (Owner variant, not PRTestCommit) - if let Err(e) = create_deterministic_commit_with_variant(&clone_path, CommitVariant::Owner) - { + // Create a wrong commit (unique, not PRTestCommit) - use create_commit so it always + // succeeds even when the clone already has the Owner deterministic content on disk. + if let Err(e) = create_commit(&clone_path, "wrong commit - not the PR test commit") { let _ = fs::remove_dir_all(&clone_path); return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).fail(&e); } -- cgit v1.2.3