upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/specs
diff options
context:
space:
mode:
Diffstat (limited to 'grasp-audit/src/specs')
-rw-r--r--grasp-audit/src/specs/grasp01/cors.rs45
-rw-r--r--grasp-audit/src/specs/grasp01/event_acceptance_policy.rs165
-rw-r--r--grasp-audit/src/specs/grasp01/git_clone.rs49
-rw-r--r--grasp-audit/src/specs/grasp01/git_filter.rs43
-rw-r--r--grasp-audit/src/specs/grasp01/mod.rs6
-rw-r--r--grasp-audit/src/specs/grasp01/nip01_smoke.rs39
-rw-r--r--grasp-audit/src/specs/grasp01/nip11_document.rs17
-rw-r--r--grasp-audit/src/specs/grasp01/purgatory.rs983
-rw-r--r--grasp-audit/src/specs/grasp01/push_authorization.rs232
-rw-r--r--grasp-audit/src/specs/grasp01/repository_creation.rs35
-rw-r--r--grasp-audit/src/specs/grasp01/spec_requirements.rs150
-rw-r--r--grasp-audit/src/specs/mod.rs2
12 files changed, 1449 insertions, 317 deletions
diff --git a/grasp-audit/src/specs/grasp01/cors.rs b/grasp-audit/src/specs/grasp01/cors.rs
index f8b5f3b..e5d9a27 100644
--- a/grasp-audit/src/specs/grasp01/cors.rs
+++ b/grasp-audit/src/specs/grasp01/cors.rs
@@ -14,6 +14,7 @@
14//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test 14//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test
15//! ``` 15//! ```
16 16
17use crate::specs::grasp01::SpecRef;
17use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; 18use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult};
18use nostr_sdk::prelude::*; 19use nostr_sdk::prelude::*;
19 20
@@ -44,7 +45,7 @@ impl CorsTests {
44 pub async fn test_cors_allow_origin(_client: &AuditClient, relay_domain: &str) -> TestResult { 45 pub async fn test_cors_allow_origin(_client: &AuditClient, relay_domain: &str) -> TestResult {
45 TestResult::new( 46 TestResult::new(
46 "cors_allow_origin", 47 "cors_allow_origin",
47 "GRASP-01:git-http:cors:50", 48 SpecRef::CorsAllowOrigin,
48 "Access-Control-Allow-Origin: * on all responses", 49 "Access-Control-Allow-Origin: * on all responses",
49 ) 50 )
50 .run(|| { 51 .run(|| {
@@ -90,7 +91,7 @@ impl CorsTests {
90 pub async fn test_cors_allow_methods(_client: &AuditClient, relay_domain: &str) -> TestResult { 91 pub async fn test_cors_allow_methods(_client: &AuditClient, relay_domain: &str) -> TestResult {
91 TestResult::new( 92 TestResult::new(
92 "cors_allow_methods", 93 "cors_allow_methods",
93 "GRASP-01:git-http:cors:51", 94 SpecRef::CorsAllowMethods,
94 "Access-Control-Allow-Methods: GET, POST on all responses", 95 "Access-Control-Allow-Methods: GET, POST on all responses",
95 ) 96 )
96 .run(|| { 97 .run(|| {
@@ -134,7 +135,7 @@ impl CorsTests {
134 pub async fn test_cors_allow_headers(_client: &AuditClient, relay_domain: &str) -> TestResult { 135 pub async fn test_cors_allow_headers(_client: &AuditClient, relay_domain: &str) -> TestResult {
135 TestResult::new( 136 TestResult::new(
136 "cors_allow_headers", 137 "cors_allow_headers",
137 "GRASP-01:git-http:cors:52", 138 SpecRef::CorsAllowHeaders,
138 "Access-Control-Allow-Headers: Content-Type on all responses", 139 "Access-Control-Allow-Headers: Content-Type on all responses",
139 ) 140 )
140 .run(|| { 141 .run(|| {
@@ -181,8 +182,8 @@ impl CorsTests {
181 ) -> TestResult { 182 ) -> TestResult {
182 TestResult::new( 183 TestResult::new(
183 "cors_options_preflight", 184 "cors_options_preflight",
184 "GRASP-01:git-http:cors:53", 185 SpecRef::CorsOptionsResponse,
185 "OPTIONS requests return 204 No Content", 186 "OPTIONS requests return 204 No Content with CORS headers",
186 ) 187 )
187 .run(|| { 188 .run(|| {
188 let relay_domain = relay_domain.to_string(); 189 let relay_domain = relay_domain.to_string();
@@ -245,13 +246,13 @@ impl CorsTests {
245 let ctx = TestContext::new(client); 246 let ctx = TestContext::new(client);
246 247
247 // Create repository announcement to get a real repo path 248 // Create repository announcement to get a real repo path
248 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 249 let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await {
249 Ok(r) => r, 250 Ok(r) => r,
250 Err(e) => { 251 Err(e) => {
251 return TestResult::new( 252 return TestResult::new(
252 test_name, 253 test_name,
253 "GRASP-01", 254 SpecRef::CorsAllowOrigin,
254 "CORS headers on real repository endpoint", 255 "CORS headers on real repository endpoints",
255 ) 256 )
256 .fail(format!("Failed to create repo fixture: {}", e)) 257 .fail(format!("Failed to create repo fixture: {}", e))
257 } 258 }
@@ -271,8 +272,8 @@ impl CorsTests {
271 None => { 272 None => {
272 return TestResult::new( 273 return TestResult::new(
273 test_name, 274 test_name,
274 "GRASP-01", 275 SpecRef::CorsAllowOrigin,
275 "CORS headers on real repository endpoint", 276 "CORS headers on real repository endpoints",
276 ) 277 )
277 .fail("Repository announcement missing d tag") 278 .fail("Repository announcement missing d tag")
278 } 279 }
@@ -283,8 +284,8 @@ impl CorsTests {
283 Err(e) => { 284 Err(e) => {
284 return TestResult::new( 285 return TestResult::new(
285 test_name, 286 test_name,
286 "GRASP-01", 287 SpecRef::CorsAllowOrigin,
287 "CORS headers on real repository endpoint", 288 "CORS headers on real repository endpoints",
288 ) 289 )
289 .fail(format!("Failed to convert pubkey to npub: {}", e)) 290 .fail(format!("Failed to convert pubkey to npub: {}", e))
290 } 291 }
@@ -302,8 +303,8 @@ impl CorsTests {
302 Err(e) => { 303 Err(e) => {
303 return TestResult::new( 304 return TestResult::new(
304 test_name, 305 test_name,
305 "GRASP-01", 306 SpecRef::CorsAllowOrigin,
306 "CORS headers on real repository endpoint", 307 "CORS headers on real repository endpoints",
307 ) 308 )
308 .fail(format!("Failed to GET info/refs: {}", e)) 309 .fail(format!("Failed to GET info/refs: {}", e))
309 } 310 }
@@ -313,8 +314,8 @@ impl CorsTests {
313 if let Err(e) = check_cors_allow_origin(&response, "info/refs") { 314 if let Err(e) = check_cors_allow_origin(&response, "info/refs") {
314 return TestResult::new( 315 return TestResult::new(
315 test_name, 316 test_name,
316 "GRASP-01", 317 SpecRef::CorsAllowOrigin,
317 "CORS headers on real repository endpoint", 318 "CORS headers on real repository endpoints",
318 ) 319 )
319 .fail(&e); 320 .fail(&e);
320 } 321 }
@@ -322,8 +323,8 @@ impl CorsTests {
322 if let Err(e) = check_cors_allow_methods(&response, "info/refs") { 323 if let Err(e) = check_cors_allow_methods(&response, "info/refs") {
323 return TestResult::new( 324 return TestResult::new(
324 test_name, 325 test_name,
325 "GRASP-01", 326 SpecRef::CorsAllowMethods,
326 "CORS headers on real repository endpoint", 327 "CORS headers on real repository endpoints",
327 ) 328 )
328 .fail(&e); 329 .fail(&e);
329 } 330 }
@@ -331,16 +332,16 @@ impl CorsTests {
331 if let Err(e) = check_cors_allow_headers(&response, "info/refs") { 332 if let Err(e) = check_cors_allow_headers(&response, "info/refs") {
332 return TestResult::new( 333 return TestResult::new(
333 test_name, 334 test_name,
334 "GRASP-01", 335 SpecRef::CorsAllowHeaders,
335 "CORS headers on real repository endpoint", 336 "CORS headers on real repository endpoints",
336 ) 337 )
337 .fail(&e); 338 .fail(&e);
338 } 339 }
339 340
340 TestResult::new( 341 TestResult::new(
341 test_name, 342 test_name,
342 "GRASP-01", 343 SpecRef::CorsAllowOrigin,
343 "CORS headers on real repository endpoint", 344 "CORS headers on real repository endpoints",
344 ) 345 )
345 .pass() 346 .pass()
346 } 347 }
diff --git a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs
index 5b697d8..3375c4d 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 @@
92//! - Transitive tests verify multi-hop acceptance chains 92//! - Transitive tests verify multi-hop acceptance chains
93 93
94use crate::fixtures::{send_and_verify_accepted, send_and_verify_rejected}; 94use crate::fixtures::{send_and_verify_accepted, send_and_verify_rejected};
95use crate::specs::grasp01::SpecRef;
95use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; 96use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult};
96use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp, ToBech32}; 97use nostr_sdk::{Event, Filter, Kind, Tag, TagKind, Timestamp, ToBech32};
97use std::time::Duration; 98use std::time::Duration;
@@ -148,20 +149,23 @@ impl EventAcceptancePolicyTests {
148 pub async fn test_accept_valid_repo_announcement(client: &AuditClient) -> TestResult { 149 pub async fn test_accept_valid_repo_announcement(client: &AuditClient) -> TestResult {
149 TestResult::new( 150 TestResult::new(
150 "accept_valid_repo_announcement", 151 "accept_valid_repo_announcement",
151 "GRASP-01:nostr-relay:7", 152 SpecRef::NostrRelayNip01Compliant,
152 "Accept valid repository announcements with service in clone and relays tags", 153 "MUST accept repo announcements listing service in clone & relays tags",
153 ) 154 )
154 .run(|| async { 155 .run(|| async {
155 // Create TestContext for mode-aware fixture management 156 // Create TestContext for mode-aware fixture management
156 let ctx = TestContext::new(client); 157 let ctx = TestContext::new(client);
157 158
158 // Request repository fixture - behavior depends on mode 159 // Request repository fixture - behavior depends on mode
159 let event = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { 160 let event = ctx
160 format!( 161 .get_fixture(FixtureKind::ValidRepoServed)
161 "Test setup failed: could not get valid repository fixture: {}", 162 .await
162 e 163 .map_err(|e| {
163 ) 164 format!(
164 })?; 165 "Test setup failed: could not get valid repository fixture: {}",
166 e
167 )
168 })?;
165 169
166 // Get relay URL for validation 170 // Get relay URL for validation
167 let relay_url = client 171 let relay_url = client
@@ -253,8 +257,8 @@ impl EventAcceptancePolicyTests {
253 ) -> TestResult { 257 ) -> TestResult {
254 TestResult::new( 258 TestResult::new(
255 "reject_repo_announcement_missing_clone_tag", 259 "reject_repo_announcement_missing_clone_tag",
256 "GRASP-01:nostr-relay:9", 260 SpecRef::NostrRelayRejectMissingCloneRelays,
257 "Reject repository announcements without service in clone tag", 261 "MUST reject announcements not listing service in clone tag",
258 ) 262 )
259 .run(|| async { 263 .run(|| async {
260 // Get relay URL from client 264 // Get relay URL from client
@@ -329,8 +333,8 @@ impl EventAcceptancePolicyTests {
329 ) -> TestResult { 333 ) -> TestResult {
330 TestResult::new( 334 TestResult::new(
331 "reject_repo_announcement_missing_relays_tag", 335 "reject_repo_announcement_missing_relays_tag",
332 "GRASP-01:nostr-relay:9", 336 SpecRef::NostrRelayRejectMissingCloneRelays,
333 "Reject repository announcements without service in relays tag", 337 "MUST reject announcements not listing service in relays tag",
334 ) 338 )
335 .run(|| async { 339 .run(|| async {
336 // Get relay URL from client 340 // Get relay URL from client
@@ -425,8 +429,8 @@ impl EventAcceptancePolicyTests {
425 ) -> TestResult { 429 ) -> TestResult {
426 TestResult::new( 430 TestResult::new(
427 "accept_recursive_maintainer_announcement_without_service", 431 "accept_recursive_maintainer_announcement_without_service",
428 "GRASP-01:nostr-relay:9", 432 SpecRef::NostrRelayRejectMissingCloneRelays,
429 "Accept recursive maintainer announcement for chain discovery (even without GRASP server in clone)", 433 "MUST accept recursive maintainer announcements for chain discovery",
430 ) 434 )
431 .run(|| async { 435 .run(|| async {
432 // Create TestContext for mode-aware fixture management 436 // Create TestContext for mode-aware fixture management
@@ -593,7 +597,7 @@ impl EventAcceptancePolicyTests {
593 pub async fn test_accept_issue_via_a_tag(client: &AuditClient) -> TestResult { 597 pub async fn test_accept_issue_via_a_tag(client: &AuditClient) -> TestResult {
594 TestResult::new( 598 TestResult::new(
595 "accept_issue_via_a_tag", 599 "accept_issue_via_a_tag",
596 "GRASP-01:nostr-relay:13", 600 SpecRef::NostrRelayMustAcceptTaggedEvents,
597 "Accept issue referencing repo via 'a' tag", 601 "Accept issue referencing repo via 'a' tag",
598 ) 602 )
599 .run(|| async { 603 .run(|| async {
@@ -601,12 +605,15 @@ impl EventAcceptancePolicyTests {
601 let ctx = TestContext::new(client); 605 let ctx = TestContext::new(client);
602 606
603 // NEW: Get repository fixture (mode-aware) 607 // NEW: Get repository fixture (mode-aware)
604 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { 608 let repo = ctx
605 format!( 609 .get_fixture(FixtureKind::ValidRepoServed)
606 "Test setup failed: could not get valid repository fixture: {}", 610 .await
607 e 611 .map_err(|e| {
608 ) 612 format!(
609 })?; 613 "Test setup failed: could not get valid repository fixture: {}",
614 e
615 )
616 })?;
610 617
611 // 2. Create issue that references the repo 618 // 2. Create issue that references the repo
612 let issue = Self::create_issue_for_repo(client, &repo, "Test Issue 1")?; 619 let issue = Self::create_issue_for_repo(client, &repo, "Test Issue 1")?;
@@ -628,7 +635,7 @@ impl EventAcceptancePolicyTests {
628 pub async fn test_accept_comment_via_capital_a_tag(client: &AuditClient) -> TestResult { 635 pub async fn test_accept_comment_via_capital_a_tag(client: &AuditClient) -> TestResult {
629 TestResult::new( 636 TestResult::new(
630 "accept_comment_via_A_tag", 637 "accept_comment_via_A_tag",
631 "GRASP-01:nostr-relay:13", 638 SpecRef::NostrRelayMustAcceptTaggedEvents,
632 "Accept NIP-22 comment with root 'A' tag referencing repo", 639 "Accept NIP-22 comment with root 'A' tag referencing repo",
633 ) 640 )
634 .run(|| async { 641 .run(|| async {
@@ -636,12 +643,15 @@ impl EventAcceptancePolicyTests {
636 let ctx = TestContext::new(client); 643 let ctx = TestContext::new(client);
637 644
638 // Get repository fixture (mode-aware) 645 // Get repository fixture (mode-aware)
639 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { 646 let repo = ctx
640 format!( 647 .get_fixture(FixtureKind::ValidRepoServed)
641 "Test setup failed: could not get valid repository fixture: {}", 648 .await
642 e 649 .map_err(|e| {
643 ) 650 format!(
644 })?; 651 "Test setup failed: could not get valid repository fixture: {}",
652 e
653 )
654 })?;
645 655
646 // Extract repo_id and create `A` tag manually 656 // Extract repo_id and create `A` tag manually
647 let repo_id = 657 let repo_id =
@@ -681,20 +691,23 @@ impl EventAcceptancePolicyTests {
681 pub async fn test_accept_kind1_via_q_tag(client: &AuditClient) -> TestResult { 691 pub async fn test_accept_kind1_via_q_tag(client: &AuditClient) -> TestResult {
682 TestResult::new( 692 TestResult::new(
683 "accept_kind1_via_q_tag", 693 "accept_kind1_via_q_tag",
684 "GRASP-01:nostr-relay:13", 694 SpecRef::NostrRelayMustAcceptTaggedEvents,
685 "Accept kind 1 note quoting repo via 'q' tag", 695 "Accept kind 1 text note quoting repo via 'q' tag",
686 ) 696 )
687 .run(|| async { 697 .run(|| async {
688 // Create TestContext 698 // Create TestContext
689 let ctx = TestContext::new(client); 699 let ctx = TestContext::new(client);
690 700
691 // Get repository fixture (mode-aware) 701 // Get repository fixture (mode-aware)
692 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { 702 let repo = ctx
693 format!( 703 .get_fixture(FixtureKind::ValidRepoServed)
694 "Test setup failed: could not get valid repository fixture: {}", 704 .await
695 e 705 .map_err(|e| {
696 ) 706 format!(
697 })?; 707 "Test setup failed: could not get valid repository fixture: {}",
708 e
709 )
710 })?;
698 711
699 // Extract repo_id and create `q` tag 712 // Extract repo_id and create `q` tag
700 let repo_id = 713 let repo_id =
@@ -731,8 +744,8 @@ impl EventAcceptancePolicyTests {
731 pub async fn test_accept_issue_quoting_issue_via_q(client: &AuditClient) -> TestResult { 744 pub async fn test_accept_issue_quoting_issue_via_q(client: &AuditClient) -> TestResult {
732 TestResult::new( 745 TestResult::new(
733 "accept_issue_quoting_issue_via_q", 746 "accept_issue_quoting_issue_via_q",
734 "GRASP-01:nostr-relay:13", 747 SpecRef::NostrRelayMustAcceptTaggedEvents,
735 "Accept issue quoting accepted issue (transitive)", 748 "Accept issue quoting another accepted issue (transitive)",
736 ) 749 )
737 .run(|| async { 750 .run(|| async {
738 // Create TestContext 751 // Create TestContext
@@ -777,7 +790,7 @@ impl EventAcceptancePolicyTests {
777 pub async fn test_accept_comment_via_capital_e_tag(client: &AuditClient) -> TestResult { 790 pub async fn test_accept_comment_via_capital_e_tag(client: &AuditClient) -> TestResult {
778 TestResult::new( 791 TestResult::new(
779 "accept_comment_via_E_tag", 792 "accept_comment_via_E_tag",
780 "GRASP-01:nostr-relay:13", 793 SpecRef::NostrRelayMustAcceptTaggedEvents,
781 "Accept NIP-22 comment with root 'E' tag to accepted issue", 794 "Accept NIP-22 comment with root 'E' tag to accepted issue",
782 ) 795 )
783 .run(|| async { 796 .run(|| async {
@@ -816,7 +829,7 @@ impl EventAcceptancePolicyTests {
816 pub async fn test_accept_kind1_via_e_tag(client: &AuditClient) -> TestResult { 829 pub async fn test_accept_kind1_via_e_tag(client: &AuditClient) -> TestResult {
817 TestResult::new( 830 TestResult::new(
818 "accept_kind1_via_e_tag", 831 "accept_kind1_via_e_tag",
819 "GRASP-01:nostr-relay:13", 832 SpecRef::NostrRelayMustAcceptTaggedEvents,
820 "Accept kind 1 reply via 'e' tag to accepted kind 1", 833 "Accept kind 1 reply via 'e' tag to accepted kind 1",
821 ) 834 )
822 .run(|| async { 835 .run(|| async {
@@ -824,12 +837,15 @@ impl EventAcceptancePolicyTests {
824 let ctx = TestContext::new(client); 837 let ctx = TestContext::new(client);
825 838
826 // Get repository fixture (mode-aware) 839 // Get repository fixture (mode-aware)
827 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { 840 let repo = ctx
828 format!( 841 .get_fixture(FixtureKind::ValidRepoServed)
829 "Test setup failed: could not get valid repository fixture: {}", 842 .await
830 e 843 .map_err(|e| {
831 ) 844 format!(
832 })?; 845 "Test setup failed: could not get valid repository fixture: {}",
846 e
847 )
848 })?;
833 849
834 // Create Kind 1 A that quotes the repo (makes it accepted) 850 // Create Kind 1 A that quotes the repo (makes it accepted)
835 let repo_id = Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id")?; 851 let repo_id = Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id")?;
@@ -872,7 +888,7 @@ impl EventAcceptancePolicyTests {
872 pub async fn test_accept_kind1_referenced_in_issue(client: &AuditClient) -> TestResult { 888 pub async fn test_accept_kind1_referenced_in_issue(client: &AuditClient) -> TestResult {
873 TestResult::new( 889 TestResult::new(
874 "accept_kind1_referenced_in_issue", 890 "accept_kind1_referenced_in_issue",
875 "GRASP-01:nostr-relay:13", 891 SpecRef::NostrRelayMustAcceptTaggedEvents,
876 "Accept kind 1 referenced in accepted issue (forward ref)", 892 "Accept kind 1 referenced in accepted issue (forward ref)",
877 ) 893 )
878 .run(|| async { 894 .run(|| async {
@@ -880,12 +896,15 @@ impl EventAcceptancePolicyTests {
880 let ctx = TestContext::new(client); 896 let ctx = TestContext::new(client);
881 897
882 // Get repository fixture (mode-aware) 898 // Get repository fixture (mode-aware)
883 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { 899 let repo = ctx
884 format!( 900 .get_fixture(FixtureKind::ValidRepoServed)
885 "Test setup failed: could not get valid repository fixture: {}", 901 .await
886 e 902 .map_err(|e| {
887 ) 903 format!(
888 })?; 904 "Test setup failed: could not get valid repository fixture: {}",
905 e
906 )
907 })?;
889 908
890 // Verify repo is queryable (ensures it's fully indexed before we reference it) 909 // Verify repo is queryable (ensures it's fully indexed before we reference it)
891 let repo_id = Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id")?; 910 let repo_id = Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id")?;
@@ -964,7 +983,7 @@ impl EventAcceptancePolicyTests {
964 pub async fn test_accept_comment_referenced_in_comment(client: &AuditClient) -> TestResult { 983 pub async fn test_accept_comment_referenced_in_comment(client: &AuditClient) -> TestResult {
965 TestResult::new( 984 TestResult::new(
966 "accept_comment_referenced_in_comment", 985 "accept_comment_referenced_in_comment",
967 "GRASP-01:nostr-relay:13", 986 SpecRef::NostrRelayMustAcceptTaggedEvents,
968 "Accept comment referenced in another accepted comment (forward ref)", 987 "Accept comment referenced in another accepted comment (forward ref)",
969 ) 988 )
970 .run(|| async { 989 .run(|| async {
@@ -1025,7 +1044,7 @@ impl EventAcceptancePolicyTests {
1025 pub async fn test_accept_kind1_referenced_in_kind1(client: &AuditClient) -> TestResult { 1044 pub async fn test_accept_kind1_referenced_in_kind1(client: &AuditClient) -> TestResult {
1026 TestResult::new( 1045 TestResult::new(
1027 "accept_kind1_referenced_in_kind1", 1046 "accept_kind1_referenced_in_kind1",
1028 "GRASP-01:nostr-relay:13", 1047 SpecRef::NostrRelayMustAcceptTaggedEvents,
1029 "Accept kind 1 referenced in another accepted kind 1 (forward ref)", 1048 "Accept kind 1 referenced in another accepted kind 1 (forward ref)",
1030 ) 1049 )
1031 .run(|| async { 1050 .run(|| async {
@@ -1033,12 +1052,15 @@ impl EventAcceptancePolicyTests {
1033 let ctx = TestContext::new(client); 1052 let ctx = TestContext::new(client);
1034 1053
1035 // Get repository fixture (mode-aware) 1054 // Get repository fixture (mode-aware)
1036 let repo = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { 1055 let repo = ctx
1037 format!( 1056 .get_fixture(FixtureKind::ValidRepoServed)
1038 "Test setup failed: could not get valid repository fixture: {}", 1057 .await
1039 e 1058 .map_err(|e| {
1040 ) 1059 format!(
1041 })?; 1060 "Test setup failed: could not get valid repository fixture: {}",
1061 e
1062 )
1063 })?;
1042 1064
1043 // Create Kind 1 A locally but DON'T send it yet 1065 // Create Kind 1 A locally but DON'T send it yet
1044 let kind1_a = client 1066 let kind1_a = client
@@ -1083,7 +1105,7 @@ impl EventAcceptancePolicyTests {
1083 pub async fn test_reject_orphan_issue(client: &AuditClient) -> TestResult { 1105 pub async fn test_reject_orphan_issue(client: &AuditClient) -> TestResult {
1084 TestResult::new( 1106 TestResult::new(
1085 "reject_orphan_issue", 1107 "reject_orphan_issue",
1086 "GRASP-01:nostr-relay:18", 1108 SpecRef::NostrRelayMayRejectSpamCuration,
1087 "Reject issue referencing unaccepted repo", 1109 "Reject issue referencing unaccepted repo",
1088 ) 1110 )
1089 .run(|| async { 1111 .run(|| async {
@@ -1110,7 +1132,7 @@ impl EventAcceptancePolicyTests {
1110 pub async fn test_reject_orphan_kind1(client: &AuditClient) -> TestResult { 1132 pub async fn test_reject_orphan_kind1(client: &AuditClient) -> TestResult {
1111 TestResult::new( 1133 TestResult::new(
1112 "reject_orphan_kind1", 1134 "reject_orphan_kind1",
1113 "GRASP-01:nostr-relay:18", 1135 SpecRef::NostrRelayMayRejectSpamCuration,
1114 "Reject kind 1 with no repo references", 1136 "Reject kind 1 with no repo references",
1115 ) 1137 )
1116 .run(|| async { 1138 .run(|| async {
@@ -1139,7 +1161,7 @@ impl EventAcceptancePolicyTests {
1139 pub async fn test_reject_comment_quoting_other_repo(client: &AuditClient) -> TestResult { 1161 pub async fn test_reject_comment_quoting_other_repo(client: &AuditClient) -> TestResult {
1140 TestResult::new( 1162 TestResult::new(
1141 "reject_comment_quoting_other_repo", 1163 "reject_comment_quoting_other_repo",
1142 "GRASP-01:nostr-relay:18", 1164 SpecRef::NostrRelayMayRejectSpamCuration,
1143 "Reject comment quoting unaccepted repo", 1165 "Reject comment quoting unaccepted repo",
1144 ) 1166 )
1145 .run(|| async { 1167 .run(|| async {
@@ -1147,12 +1169,15 @@ impl EventAcceptancePolicyTests {
1147 let ctx = TestContext::new(client); 1169 let ctx = TestContext::new(client);
1148 1170
1149 // Get accepted repo A fixture (mode-aware) 1171 // Get accepted repo A fixture (mode-aware)
1150 let _repo_a = ctx.get_fixture(FixtureKind::ValidRepo).await.map_err(|e| { 1172 let _repo_a = ctx
1151 format!( 1173 .get_fixture(FixtureKind::ValidRepoServed)
1152 "Test setup failed: could not get valid repository fixture: {}", 1174 .await
1153 e 1175 .map_err(|e| {
1154 ) 1176 format!(
1155 })?; 1177 "Test setup failed: could not get valid repository fixture: {}",
1178 e
1179 )
1180 })?;
1156 1181
1157 // Create Repo B but DON'T send it (unaccepted) 1182 // Create Repo B but DON'T send it (unaccepted)
1158 let repo_b = Self::create_test_repo(client, "unaccepted-repo-b").await?; 1183 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 e162558..0c223f4 100644
--- a/grasp-audit/src/specs/grasp01/git_clone.rs
+++ b/grasp-audit/src/specs/grasp01/git_clone.rs
@@ -15,6 +15,7 @@
15//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test 15//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test
16//! ``` 16//! ```
17 17
18use crate::specs::grasp01::SpecRef;
18use crate::{AuditClient, FixtureKind, TestContext, TestResult}; 19use crate::{AuditClient, FixtureKind, TestContext, TestResult};
19use nostr_sdk::prelude::*; 20use nostr_sdk::prelude::*;
20use std::fs; 21use std::fs;
@@ -48,12 +49,12 @@ impl GitCloneTests {
48 let ctx = TestContext::new(client); 49 let ctx = TestContext::new(client);
49 50
50 // Create repository announcement 51 // Create repository announcement
51 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 52 let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await {
52 Ok(r) => r, 53 Ok(r) => r,
53 Err(e) => { 54 Err(e) => {
54 return TestResult::new( 55 return TestResult::new(
55 test_name, 56 test_name,
56 "GRASP-01:git-http:34", 57 SpecRef::GitServeRepository,
57 "Repository must be cloneable via Git HTTP backend", 58 "Repository must be cloneable via Git HTTP backend",
58 ) 59 )
59 .fail(format!("Failed to create repo fixture: {}", e)) 60 .fail(format!("Failed to create repo fixture: {}", e))
@@ -74,7 +75,7 @@ impl GitCloneTests {
74 None => { 75 None => {
75 return TestResult::new( 76 return TestResult::new(
76 test_name, 77 test_name,
77 "GRASP-01", 78 SpecRef::GitServeRepository,
78 "Repository must be cloneable via Git HTTP backend", 79 "Repository must be cloneable via Git HTTP backend",
79 ) 80 )
80 .fail("Repository announcement missing d tag") 81 .fail("Repository announcement missing d tag")
@@ -86,7 +87,7 @@ impl GitCloneTests {
86 Err(e) => { 87 Err(e) => {
87 return TestResult::new( 88 return TestResult::new(
88 test_name, 89 test_name,
89 "GRASP-01:git-http:34", 90 SpecRef::GitServeRepository,
90 "Repository must be cloneable via Git HTTP backend", 91 "Repository must be cloneable via Git HTTP backend",
91 ) 92 )
92 .fail(format!("Failed to convert pubkey to npub: {}", e)) 93 .fail(format!("Failed to convert pubkey to npub: {}", e))
@@ -121,7 +122,7 @@ impl GitCloneTests {
121 cleanup(); 122 cleanup();
122 return TestResult::new( 123 return TestResult::new(
123 test_name, 124 test_name,
124 "GRASP-01:git-http:34", 125 SpecRef::GitServeRepository,
125 "Repository must be cloneable via Git HTTP backend", 126 "Repository must be cloneable via Git HTTP backend",
126 ) 127 )
127 .fail(format!("Failed to execute git clone: {}", e)); 128 .fail(format!("Failed to execute git clone: {}", e));
@@ -133,7 +134,7 @@ impl GitCloneTests {
133 let stderr = String::from_utf8_lossy(&output.stderr); 134 let stderr = String::from_utf8_lossy(&output.stderr);
134 return TestResult::new( 135 return TestResult::new(
135 test_name, 136 test_name,
136 "GRASP-01:git-http:34", 137 SpecRef::GitServeRepository,
137 "Repository must be cloneable via Git HTTP backend", 138 "Repository must be cloneable via Git HTTP backend",
138 ) 139 )
139 .fail(format!("Git clone failed: {}", stderr)); 140 .fail(format!("Git clone failed: {}", stderr));
@@ -144,7 +145,7 @@ impl GitCloneTests {
144 cleanup(); 145 cleanup();
145 return TestResult::new( 146 return TestResult::new(
146 test_name, 147 test_name,
147 "GRASP-01:git-http:34", 148 SpecRef::GitServeRepository,
148 "Repository must be cloneable via Git HTTP backend", 149 "Repository must be cloneable via Git HTTP backend",
149 ) 150 )
150 .fail("Cloned repository missing .git directory"); 151 .fail("Cloned repository missing .git directory");
@@ -153,7 +154,7 @@ impl GitCloneTests {
153 cleanup(); 154 cleanup();
154 TestResult::new( 155 TestResult::new(
155 test_name, 156 test_name,
156 "GRASP-01:git-http:34", 157 SpecRef::GitServeRepository,
157 "Repository must be cloneable via Git HTTP backend", 158 "Repository must be cloneable via Git HTTP backend",
158 ) 159 )
159 .pass() 160 .pass()
@@ -170,12 +171,12 @@ impl GitCloneTests {
170 let ctx = TestContext::new(client); 171 let ctx = TestContext::new(client);
171 172
172 // Create repository announcement 173 // Create repository announcement
173 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 174 let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await {
174 Ok(r) => r, 175 Ok(r) => r,
175 Err(e) => { 176 Err(e) => {
176 return TestResult::new( 177 return TestResult::new(
177 test_name, 178 test_name,
178 "GRASP-01:git-http:34", 179 SpecRef::GitServeRepository,
179 "Clone URL must follow correct format", 180 "Clone URL must follow correct format",
180 ) 181 )
181 .fail(format!("Failed to create repo fixture: {}", e)) 182 .fail(format!("Failed to create repo fixture: {}", e))
@@ -203,7 +204,7 @@ impl GitCloneTests {
203 if !valid_url.contains(&npub) { 204 if !valid_url.contains(&npub) {
204 return TestResult::new( 205 return TestResult::new(
205 test_name, 206 test_name,
206 "GRASP-01:git-http:34", 207 SpecRef::GitServeRepository,
207 "Clone URL must follow correct format", 208 "Clone URL must follow correct format",
208 ) 209 )
209 .fail("URL missing npub"); 210 .fail("URL missing npub");
@@ -212,7 +213,7 @@ impl GitCloneTests {
212 if !valid_url.contains(&format!("{}.git", repo_id)) { 213 if !valid_url.contains(&format!("{}.git", repo_id)) {
213 return TestResult::new( 214 return TestResult::new(
214 test_name, 215 test_name,
215 "GRASP-01:git-http:34", 216 SpecRef::GitServeRepository,
216 "Clone URL must follow correct format", 217 "Clone URL must follow correct format",
217 ) 218 )
218 .fail("URL missing repository identifier"); 219 .fail("URL missing repository identifier");
@@ -241,7 +242,7 @@ impl GitCloneTests {
241 if output.status.success() { 242 if output.status.success() {
242 return TestResult::new( 243 return TestResult::new(
243 test_name, 244 test_name,
244 "GRASP-01:git-http:34", 245 SpecRef::GitServeRepository,
245 "Clone URL must follow correct format", 246 "Clone URL must follow correct format",
246 ) 247 )
247 .fail("Invalid URL was accepted (should have been rejected)"); 248 .fail("Invalid URL was accepted (should have been rejected)");
@@ -249,7 +250,7 @@ impl GitCloneTests {
249 250
250 TestResult::new( 251 TestResult::new(
251 test_name, 252 test_name,
252 "GRASP-01:git-http:34", 253 SpecRef::GitServeRepository,
253 "Clone URL must follow correct format", 254 "Clone URL must follow correct format",
254 ) 255 )
255 .pass() 256 .pass()
@@ -273,12 +274,12 @@ impl GitCloneTests {
273 let ctx = TestContext::new(client); 274 let ctx = TestContext::new(client);
274 275
275 // Create repository announcement 276 // Create repository announcement
276 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 277 let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await {
277 Ok(r) => r, 278 Ok(r) => r,
278 Err(e) => { 279 Err(e) => {
279 return TestResult::new( 280 return TestResult::new(
280 test_name, 281 test_name,
281 "GRASP-01:git-http:42", 282 SpecRef::GitIncludeAllowSha1InWant,
282 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 283 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
283 ) 284 )
284 .fail(format!("Failed to create repo fixture: {}", e)) 285 .fail(format!("Failed to create repo fixture: {}", e))
@@ -299,7 +300,7 @@ impl GitCloneTests {
299 None => { 300 None => {
300 return TestResult::new( 301 return TestResult::new(
301 test_name, 302 test_name,
302 "GRASP-01:git-http:42", 303 SpecRef::GitIncludeAllowSha1InWant,
303 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 304 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
304 ) 305 )
305 .fail("Repository announcement missing d tag") 306 .fail("Repository announcement missing d tag")
@@ -311,7 +312,7 @@ impl GitCloneTests {
311 Err(e) => { 312 Err(e) => {
312 return TestResult::new( 313 return TestResult::new(
313 test_name, 314 test_name,
314 "GRASP-01:git-http:42", 315 SpecRef::GitIncludeAllowSha1InWant,
315 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 316 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
316 ) 317 )
317 .fail(format!("Failed to convert pubkey to npub: {}", e)) 318 .fail(format!("Failed to convert pubkey to npub: {}", e))
@@ -331,7 +332,7 @@ impl GitCloneTests {
331 Err(e) => { 332 Err(e) => {
332 return TestResult::new( 333 return TestResult::new(
333 test_name, 334 test_name,
334 "GRASP-01:git-http:42", 335 SpecRef::GitIncludeAllowSha1InWant,
335 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 336 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
336 ) 337 )
337 .fail(format!("HTTP request failed: {}", e)) 338 .fail(format!("HTTP request failed: {}", e))
@@ -341,7 +342,7 @@ impl GitCloneTests {
341 if !response.status().is_success() { 342 if !response.status().is_success() {
342 return TestResult::new( 343 return TestResult::new(
343 test_name, 344 test_name,
344 "GRASP-01:git-http:42", 345 SpecRef::GitIncludeAllowSha1InWant,
345 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 346 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
346 ) 347 )
347 .fail(format!( 348 .fail(format!(
@@ -356,7 +357,7 @@ impl GitCloneTests {
356 Err(e) => { 357 Err(e) => {
357 return TestResult::new( 358 return TestResult::new(
358 test_name, 359 test_name,
359 "GRASP-01:git-http:42", 360 SpecRef::GitIncludeAllowSha1InWant,
360 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 361 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
361 ) 362 )
362 .fail(format!("Failed to read response body: {}", e)) 363 .fail(format!("Failed to read response body: {}", e))
@@ -370,7 +371,7 @@ impl GitCloneTests {
370 if !has_allow_reachable { 371 if !has_allow_reachable {
371 return TestResult::new( 372 return TestResult::new(
372 test_name, 373 test_name,
373 "GRASP-01:git-http:42", 374 SpecRef::GitIncludeAllowSha1InWant,
374 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 375 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
375 ) 376 )
376 .fail("Missing capability: allow-reachable-sha1-in-want"); 377 .fail("Missing capability: allow-reachable-sha1-in-want");
@@ -379,7 +380,7 @@ impl GitCloneTests {
379 if !has_allow_tip { 380 if !has_allow_tip {
380 return TestResult::new( 381 return TestResult::new(
381 test_name, 382 test_name,
382 "GRASP-01:git-http:42", 383 SpecRef::GitIncludeAllowSha1InWant,
383 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 384 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
384 ) 385 )
385 .fail("Missing capability: allow-tip-sha1-in-want"); 386 .fail("Missing capability: allow-tip-sha1-in-want");
@@ -387,7 +388,7 @@ impl GitCloneTests {
387 388
388 TestResult::new( 389 TestResult::new(
389 test_name, 390 test_name,
390 "GRASP-01:git-http:42", 391 SpecRef::GitIncludeAllowSha1InWant,
391 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 392 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
392 ) 393 )
393 .pass() 394 .pass()
diff --git a/grasp-audit/src/specs/grasp01/git_filter.rs b/grasp-audit/src/specs/grasp01/git_filter.rs
index 21bab0a..31d86aa 100644
--- a/grasp-audit/src/specs/grasp01/git_filter.rs
+++ b/grasp-audit/src/specs/grasp01/git_filter.rs
@@ -22,6 +22,7 @@
22//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test 22//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test
23//! ``` 23//! ```
24 24
25use crate::specs::grasp01::SpecRef;
25use crate::{AuditClient, FixtureKind, TestContext, TestResult}; 26use crate::{AuditClient, FixtureKind, TestContext, TestResult};
26use nostr_sdk::prelude::*; 27use nostr_sdk::prelude::*;
27use std::fs; 28use std::fs;
@@ -61,12 +62,12 @@ impl GitFilterTests {
61 let ctx = TestContext::new(client); 62 let ctx = TestContext::new(client);
62 63
63 // Create repository announcement 64 // Create repository announcement
64 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 65 let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await {
65 Ok(r) => r, 66 Ok(r) => r,
66 Err(e) => { 67 Err(e) => {
67 return TestResult::new( 68 return TestResult::new(
68 test_name, 69 test_name,
69 "GRASP-01:git-http:42", 70 SpecRef::GitIncludeAllowSha1InWant,
70 "MUST include uploadpack.allowFilter in advertisement", 71 "MUST include uploadpack.allowFilter in advertisement",
71 ) 72 )
72 .fail(format!("Failed to create repo fixture: {}", e)) 73 .fail(format!("Failed to create repo fixture: {}", e))
@@ -87,7 +88,7 @@ impl GitFilterTests {
87 None => { 88 None => {
88 return TestResult::new( 89 return TestResult::new(
89 test_name, 90 test_name,
90 "GRASP-01:git-http:42", 91 SpecRef::GitIncludeAllowSha1InWant,
91 "MUST include uploadpack.allowFilter in advertisement", 92 "MUST include uploadpack.allowFilter in advertisement",
92 ) 93 )
93 .fail("Repository announcement missing d tag") 94 .fail("Repository announcement missing d tag")
@@ -99,7 +100,7 @@ impl GitFilterTests {
99 Err(e) => { 100 Err(e) => {
100 return TestResult::new( 101 return TestResult::new(
101 test_name, 102 test_name,
102 "GRASP-01:git-http:42", 103 SpecRef::GitIncludeAllowSha1InWant,
103 "MUST include uploadpack.allowFilter in advertisement", 104 "MUST include uploadpack.allowFilter in advertisement",
104 ) 105 )
105 .fail(format!("Failed to convert pubkey to npub: {}", e)) 106 .fail(format!("Failed to convert pubkey to npub: {}", e))
@@ -119,7 +120,7 @@ impl GitFilterTests {
119 Err(e) => { 120 Err(e) => {
120 return TestResult::new( 121 return TestResult::new(
121 test_name, 122 test_name,
122 "GRASP-01:git-http:42", 123 SpecRef::GitIncludeAllowSha1InWant,
123 "MUST include uploadpack.allowFilter in advertisement", 124 "MUST include uploadpack.allowFilter in advertisement",
124 ) 125 )
125 .fail(format!("HTTP request failed: {}", e)) 126 .fail(format!("HTTP request failed: {}", e))
@@ -129,7 +130,7 @@ impl GitFilterTests {
129 if !response.status().is_success() { 130 if !response.status().is_success() {
130 return TestResult::new( 131 return TestResult::new(
131 test_name, 132 test_name,
132 "GRASP-01:git-http:42", 133 SpecRef::GitIncludeAllowSha1InWant,
133 "MUST include uploadpack.allowFilter in advertisement", 134 "MUST include uploadpack.allowFilter in advertisement",
134 ) 135 )
135 .fail(format!( 136 .fail(format!(
@@ -144,7 +145,7 @@ impl GitFilterTests {
144 Err(e) => { 145 Err(e) => {
145 return TestResult::new( 146 return TestResult::new(
146 test_name, 147 test_name,
147 "GRASP-01:git-http:42", 148 SpecRef::GitIncludeAllowSha1InWant,
148 "MUST include uploadpack.allowFilter in advertisement", 149 "MUST include uploadpack.allowFilter in advertisement",
149 ) 150 )
150 .fail(format!("Failed to read response body: {}", e)) 151 .fail(format!("Failed to read response body: {}", e))
@@ -155,7 +156,7 @@ impl GitFilterTests {
155 if !body.contains("filter") { 156 if !body.contains("filter") {
156 return TestResult::new( 157 return TestResult::new(
157 test_name, 158 test_name,
158 "GRASP-01:git-http:42", 159 SpecRef::GitIncludeAllowSha1InWant,
159 "MUST include uploadpack.allowFilter in advertisement", 160 "MUST include uploadpack.allowFilter in advertisement",
160 ) 161 )
161 .fail("Missing capability: filter"); 162 .fail("Missing capability: filter");
@@ -163,7 +164,7 @@ impl GitFilterTests {
163 164
164 TestResult::new( 165 TestResult::new(
165 test_name, 166 test_name,
166 "GRASP-01:git-http:42", 167 SpecRef::GitIncludeAllowSha1InWant,
167 "MUST include uploadpack.allowFilter in advertisement", 168 "MUST include uploadpack.allowFilter in advertisement",
168 ) 169 )
169 .pass() 170 .pass()
@@ -184,12 +185,12 @@ impl GitFilterTests {
184 let ctx = TestContext::new(client); 185 let ctx = TestContext::new(client);
185 186
186 // Create repository announcement 187 // Create repository announcement
187 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 188 let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await {
188 Ok(r) => r, 189 Ok(r) => r,
189 Err(e) => { 190 Err(e) => {
190 return TestResult::new( 191 return TestResult::new(
191 test_name, 192 test_name,
192 "GRASP-01:git-http:42", 193 SpecRef::GitIncludeAllowSha1InWant,
193 "MUST serve filtered clone requests", 194 "MUST serve filtered clone requests",
194 ) 195 )
195 .fail(format!("Failed to create repo fixture: {}", e)) 196 .fail(format!("Failed to create repo fixture: {}", e))
@@ -243,7 +244,7 @@ impl GitFilterTests {
243 cleanup(); 244 cleanup();
244 return TestResult::new( 245 return TestResult::new(
245 test_name, 246 test_name,
246 "GRASP-01:git-http:42", 247 SpecRef::GitIncludeAllowSha1InWant,
247 "MUST serve filtered clone requests", 248 "MUST serve filtered clone requests",
248 ) 249 )
249 .fail(format!("Failed to execute git clone: {}", e)); 250 .fail(format!("Failed to execute git clone: {}", e));
@@ -255,7 +256,7 @@ impl GitFilterTests {
255 let stderr = String::from_utf8_lossy(&output.stderr); 256 let stderr = String::from_utf8_lossy(&output.stderr);
256 return TestResult::new( 257 return TestResult::new(
257 test_name, 258 test_name,
258 "GRASP-01:git-http:42", 259 SpecRef::GitIncludeAllowSha1InWant,
259 "MUST serve filtered clone requests", 260 "MUST serve filtered clone requests",
260 ) 261 )
261 .fail(format!("Filtered git clone failed: {}", stderr)); 262 .fail(format!("Filtered git clone failed: {}", stderr));
@@ -266,7 +267,7 @@ impl GitFilterTests {
266 cleanup(); 267 cleanup();
267 return TestResult::new( 268 return TestResult::new(
268 test_name, 269 test_name,
269 "GRASP-01:git-http:42", 270 SpecRef::GitIncludeAllowSha1InWant,
270 "MUST serve filtered clone requests", 271 "MUST serve filtered clone requests",
271 ) 272 )
272 .fail("Filtered clone missing .git directory"); 273 .fail("Filtered clone missing .git directory");
@@ -275,7 +276,7 @@ impl GitFilterTests {
275 cleanup(); 276 cleanup();
276 TestResult::new( 277 TestResult::new(
277 test_name, 278 test_name,
278 "GRASP-01:git-http:42", 279 SpecRef::GitIncludeAllowSha1InWant,
279 "MUST serve filtered clone requests", 280 "MUST serve filtered clone requests",
280 ) 281 )
281 .pass() 282 .pass()
@@ -295,12 +296,12 @@ impl GitFilterTests {
295 let ctx = TestContext::new(client); 296 let ctx = TestContext::new(client);
296 297
297 // Create repository announcement 298 // Create repository announcement
298 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 299 let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await {
299 Ok(r) => r, 300 Ok(r) => r,
300 Err(e) => { 301 Err(e) => {
301 return TestResult::new( 302 return TestResult::new(
302 test_name, 303 test_name,
303 "GRASP-01:git-http:42", 304 SpecRef::GitIncludeAllowSha1InWant,
304 "MUST serve filtered fetch requests", 305 "MUST serve filtered fetch requests",
305 ) 306 )
306 .fail(format!("Failed to create repo fixture: {}", e)) 307 .fail(format!("Failed to create repo fixture: {}", e))
@@ -352,7 +353,7 @@ impl GitFilterTests {
352 cleanup(); 353 cleanup();
353 return TestResult::new( 354 return TestResult::new(
354 test_name, 355 test_name,
355 "GRASP-01:git-http:42", 356 SpecRef::GitIncludeAllowSha1InWant,
356 "MUST serve filtered fetch requests", 357 "MUST serve filtered fetch requests",
357 ) 358 )
358 .fail("Failed to create initial shallow clone for fetch test"); 359 .fail("Failed to create initial shallow clone for fetch test");
@@ -371,7 +372,7 @@ impl GitFilterTests {
371 cleanup(); 372 cleanup();
372 return TestResult::new( 373 return TestResult::new(
373 test_name, 374 test_name,
374 "GRASP-01:git-http:42", 375 SpecRef::GitIncludeAllowSha1InWant,
375 "MUST serve filtered fetch requests", 376 "MUST serve filtered fetch requests",
376 ) 377 )
377 .fail(format!("Failed to execute git fetch: {}", e)); 378 .fail(format!("Failed to execute git fetch: {}", e));
@@ -383,7 +384,7 @@ impl GitFilterTests {
383 let stderr = String::from_utf8_lossy(&output.stderr); 384 let stderr = String::from_utf8_lossy(&output.stderr);
384 return TestResult::new( 385 return TestResult::new(
385 test_name, 386 test_name,
386 "GRASP-01:git-http:42", 387 SpecRef::GitIncludeAllowSha1InWant,
387 "MUST serve filtered fetch requests", 388 "MUST serve filtered fetch requests",
388 ) 389 )
389 .fail(format!("Filtered git fetch failed: {}", stderr)); 390 .fail(format!("Filtered git fetch failed: {}", stderr));
@@ -392,7 +393,7 @@ impl GitFilterTests {
392 cleanup(); 393 cleanup();
393 TestResult::new( 394 TestResult::new(
394 test_name, 395 test_name,
395 "GRASP-01:git-http:42", 396 SpecRef::GitIncludeAllowSha1InWant,
396 "MUST serve filtered fetch requests", 397 "MUST serve filtered fetch requests",
397 ) 398 )
398 .pass() 399 .pass()
diff --git a/grasp-audit/src/specs/grasp01/mod.rs b/grasp-audit/src/specs/grasp01/mod.rs
index 0a819ee..1694f58 100644
--- a/grasp-audit/src/specs/grasp01/mod.rs
+++ b/grasp-audit/src/specs/grasp01/mod.rs
@@ -19,6 +19,7 @@ pub mod git_clone;
19pub mod git_filter; 19pub mod git_filter;
20pub mod nip01_smoke; 20pub mod nip01_smoke;
21pub mod nip11_document; 21pub mod nip11_document;
22pub mod purgatory;
22pub mod push_authorization; 23pub mod push_authorization;
23pub mod repository_creation; 24pub mod repository_creation;
24pub mod spec_requirements; 25pub mod spec_requirements;
@@ -29,9 +30,10 @@ pub use git_clone::GitCloneTests;
29pub use git_filter::GitFilterTests; 30pub use git_filter::GitFilterTests;
30pub use nip01_smoke::Nip01SmokeTests; 31pub use nip01_smoke::Nip01SmokeTests;
31pub use nip11_document::Nip11DocumentTests; 32pub use nip11_document::Nip11DocumentTests;
33pub use purgatory::PurgatoryTests;
32pub use push_authorization::PushAuthorizationTests; 34pub use push_authorization::PushAuthorizationTests;
33pub use repository_creation::RepositoryCreationTests; 35pub use repository_creation::RepositoryCreationTests;
34pub use spec_requirements::{ 36pub use spec_requirements::{
35 get_requirement, get_requirements_for_section, get_sections, RequirementLevel, SpecRequirement, 37 get_requirement, get_requirement_by_ref, get_requirements_for_section, get_sections,
36 GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID, 38 RequirementLevel, SpecRef, SpecRequirement, GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID,
37}; 39};
diff --git a/grasp-audit/src/specs/grasp01/nip01_smoke.rs b/grasp-audit/src/specs/grasp01/nip01_smoke.rs
index 4d0b8a4..e3206fc 100644
--- a/grasp-audit/src/specs/grasp01/nip01_smoke.rs
+++ b/grasp-audit/src/specs/grasp01/nip01_smoke.rs
@@ -4,6 +4,7 @@
4//! We don't comprehensively test NIP-01 because rust-nostr already has 1000+ tests. 4//! We don't comprehensively test NIP-01 because rust-nostr already has 1000+ tests.
5//! These are just smoke tests to ensure the relay is working at all. 5//! These are just smoke tests to ensure the relay is working at all.
6 6
7use crate::specs::grasp01::SpecRef;
7use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; 8use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult};
8use nostr_sdk::prelude::*; 9use nostr_sdk::prelude::*;
9 10
@@ -32,8 +33,8 @@ impl Nip01SmokeTests {
32 pub async fn test_websocket_connection(client: &AuditClient) -> TestResult { 33 pub async fn test_websocket_connection(client: &AuditClient) -> TestResult {
33 TestResult::new( 34 TestResult::new(
34 "websocket_connection", 35 "websocket_connection",
35 "GRASP-01:nostr-relay:7", 36 SpecRef::NostrRelayNip01Compliant,
36 "Can establish WebSocket connection to /", 37 "MUST serve a relay at / via WebSocket",
37 ) 38 )
38 .run(|| async { 39 .run(|| async {
39 if !client.is_connected().await { 40 if !client.is_connected().await {
@@ -61,16 +62,16 @@ impl Nip01SmokeTests {
61 pub async fn test_send_receive_event(client: &AuditClient) -> TestResult { 62 pub async fn test_send_receive_event(client: &AuditClient) -> TestResult {
62 TestResult::new( 63 TestResult::new(
63 "send_receive_event", 64 "send_receive_event",
64 "GRASP-01:nostr-relay:7", 65 SpecRef::NostrRelayNip01Compliant,
65 "Can send EVENT and receive OK response", 66 "MUST accept valid EVENT messages",
66 ) 67 )
67 .run(|| async { 68 .run(|| async {
68 // Step 1: GENERATE - Create TestContext and get ValidRepo fixture 69 // Step 1: GENERATE - Create TestContext and get ValidRepoServed fixture
69 let ctx = TestContext::new(client); 70 let ctx = TestContext::new(client);
70 let event = ctx 71 let event = ctx
71 .get_fixture(FixtureKind::ValidRepo) 72 .get_fixture(FixtureKind::ValidRepoServed)
72 .await 73 .await
73 .map_err(|e| format!("Failed to create ValidRepo fixture: {}", e))?; 74 .map_err(|e| format!("Failed to create ValidRepoServed fixture: {}", e))?;
74 75
75 let event_id = event.id; 76 let event_id = event.id;
76 77
@@ -121,22 +122,22 @@ impl Nip01SmokeTests {
121 /// 122 ///
122 /// ## Fixture-First Pattern 123 /// ## Fixture-First Pattern
123 /// 124 ///
124 /// 1. **Generate**: Create TestContext and get ValidRepo fixture 125 /// 1. **Generate**: Create TestContext and get ValidRepoServed fixture
125 /// 2. **Send**: Fixture already sends the event to relay 126 /// 2. **Send**: Fixture already sends the event to relay
126 /// 3. **Verify**: Subscribe and verify we receive the event 127 /// 3. **Verify**: Subscribe and verify we receive the event
127 pub async fn test_create_subscription(client: &AuditClient) -> TestResult { 128 pub async fn test_create_subscription(client: &AuditClient) -> TestResult {
128 TestResult::new( 129 TestResult::new(
129 "create_subscription", 130 "create_subscription",
130 "GRASP-01:nostr-relay:7", 131 SpecRef::NostrRelayNip01Compliant,
131 "Can create subscription with REQ and receive EOSE", 132 "MUST support REQ subscriptions",
132 ) 133 )
133 .run(|| async { 134 .run(|| async {
134 // Step 1: GENERATE - Create TestContext and get ValidRepo fixture 135 // Step 1: GENERATE - Create TestContext and get ValidRepoServed fixture
135 let ctx = TestContext::new(client); 136 let ctx = TestContext::new(client);
136 let _event = ctx 137 let _event = ctx
137 .get_fixture(FixtureKind::ValidRepo) 138 .get_fixture(FixtureKind::ValidRepoServed)
138 .await 139 .await
139 .map_err(|e| format!("Failed to create ValidRepo fixture: {}", e))?; 140 .map_err(|e| format!("Failed to create ValidRepoServed fixture: {}", e))?;
140 141
141 // Step 2: VERIFY - Subscribe to NIP-34 announcements from this author 142 // Step 2: VERIFY - Subscribe to NIP-34 announcements from this author
142 let filter = Filter::new() 143 let filter = Filter::new()
@@ -165,8 +166,8 @@ impl Nip01SmokeTests {
165 pub async fn test_close_subscription(client: &AuditClient) -> TestResult { 166 pub async fn test_close_subscription(client: &AuditClient) -> TestResult {
166 TestResult::new( 167 TestResult::new(
167 "close_subscription", 168 "close_subscription",
168 "GRASP-01:nostr-relay:7", 169 SpecRef::NostrRelayNip01Compliant,
169 "Can close subscriptions", 170 "MUST support CLOSE to end subscriptions",
170 ) 171 )
171 .run(|| async { 172 .run(|| async {
172 // For now, we just verify we can query events 173 // For now, we just verify we can query events
@@ -193,8 +194,8 @@ impl Nip01SmokeTests {
193 pub async fn test_reject_invalid_signature(client: &AuditClient) -> TestResult { 194 pub async fn test_reject_invalid_signature(client: &AuditClient) -> TestResult {
194 TestResult::new( 195 TestResult::new(
195 "reject_invalid_signature", 196 "reject_invalid_signature",
196 "GRASP-01:nostr-relay:7", 197 SpecRef::NostrRelayNip01Compliant,
197 "Rejects events with invalid signatures", 198 "MUST reject events with invalid signatures",
198 ) 199 )
199 .run(|| async { 200 .run(|| async {
200 // Create a valid event 201 // Create a valid event
@@ -247,8 +248,8 @@ impl Nip01SmokeTests {
247 pub async fn test_reject_invalid_event_id(client: &AuditClient) -> TestResult { 248 pub async fn test_reject_invalid_event_id(client: &AuditClient) -> TestResult {
248 TestResult::new( 249 TestResult::new(
249 "reject_invalid_event_id", 250 "reject_invalid_event_id",
250 "GRASP-01:nostr-relay:7", 251 SpecRef::NostrRelayNip01Compliant,
251 "Rejects events with invalid event IDs", 252 "MUST reject events where ID doesn't match hash",
252 ) 253 )
253 .run(|| async { 254 .run(|| async {
254 // Create a valid event 255 // 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 @@
8//! - Includes repo_acceptance_criteria field describing acceptance policy 8//! - Includes repo_acceptance_criteria field describing acceptance policy
9//! - Handles curation field correctly (present if curated, absent otherwise) 9//! - Handles curation field correctly (present if curated, absent otherwise)
10 10
11use crate::specs::grasp01::SpecRef;
11use crate::{AuditClient, AuditResult, TestResult}; 12use crate::{AuditClient, AuditResult, TestResult};
12 13
13pub struct Nip11DocumentTests; 14pub struct Nip11DocumentTests;
@@ -37,8 +38,8 @@ impl Nip11DocumentTests {
37 pub async fn test_nip11_document_exists(client: &AuditClient) -> TestResult { 38 pub async fn test_nip11_document_exists(client: &AuditClient) -> TestResult {
38 TestResult::new( 39 TestResult::new(
39 "nip11_document_exists", 40 "nip11_document_exists",
40 "GRASP-01:nostr-relay:26", 41 SpecRef::Nip11ServeDocument,
41 "Serve NIP-11 relay information document", 42 "MUST serve NIP-11 document",
42 ) 43 )
43 .run(|| async { 44 .run(|| async {
44 // 1. Extract HTTP(S) URL from client's WebSocket URL 45 // 1. Extract HTTP(S) URL from client's WebSocket URL
@@ -96,8 +97,8 @@ impl Nip11DocumentTests {
96 pub async fn test_nip11_supported_grasps_field(client: &AuditClient) -> TestResult { 97 pub async fn test_nip11_supported_grasps_field(client: &AuditClient) -> TestResult {
97 TestResult::new( 98 TestResult::new(
98 "nip11_supported_grasps_field", 99 "nip11_supported_grasps_field",
99 "GRASP-01:nostr-relay:28", 100 SpecRef::Nip11ListSupportedGrasps,
100 "NIP-11 document includes supported_grasps field with GRASP-01", 101 "MUST list supported GRASPs as string array",
101 ) 102 )
102 .run(|| async { 103 .run(|| async {
103 // 1. Fetch NIP-11 document 104 // 1. Fetch NIP-11 document
@@ -172,8 +173,8 @@ impl Nip11DocumentTests {
172 pub async fn test_nip11_repo_acceptance_criteria_field(client: &AuditClient) -> TestResult { 173 pub async fn test_nip11_repo_acceptance_criteria_field(client: &AuditClient) -> TestResult {
173 TestResult::new( 174 TestResult::new(
174 "nip11_repo_acceptance_criteria_field", 175 "nip11_repo_acceptance_criteria_field",
175 "GRASP-01:nostr-relay:29", 176 SpecRef::Nip11ListRepoAcceptanceCriteria,
176 "NIP-11 document includes repo_acceptance_criteria field", 177 "MUST list repository acceptance criteria",
177 ) 178 )
178 .run(|| async { 179 .run(|| async {
179 // 1. Fetch NIP-11 document 180 // 1. Fetch NIP-11 document
@@ -227,8 +228,8 @@ impl Nip11DocumentTests {
227 pub async fn test_nip11_curation_field(client: &AuditClient) -> TestResult { 228 pub async fn test_nip11_curation_field(client: &AuditClient) -> TestResult {
228 TestResult::new( 229 TestResult::new(
229 "nip11_curation_field", 230 "nip11_curation_field",
230 "GRASP-01:nostr-relay:30", 231 SpecRef::Nip11ListCurationPolicy,
231 "NIP-11 curation field present if curated, absent otherwise", 232 "MUST include curation if curated, omit otherwise",
232 ) 233 )
233 .run(|| async { 234 .run(|| async {
234 // 1. Fetch NIP-11 document 235 // 1. Fetch NIP-11 document
diff --git a/grasp-audit/src/specs/grasp01/purgatory.rs b/grasp-audit/src/specs/grasp01/purgatory.rs
new file mode 100644
index 0000000..29eabad
--- /dev/null
+++ b/grasp-audit/src/specs/grasp01/purgatory.rs
@@ -0,0 +1,983 @@
1//! GRASP-01 Purgatory Tests
2//!
3//! Tests for the GRASP-01 purgatory mechanism where events are accepted but not
4//! served until corresponding git data arrives.
5//!
6//! ## Purgatory Behavior (GRASP-01 Line 22)
7//!
8//! "New repository announcements, repo state announcements, PRs and PR Updates
9//! SHOULD be accepted with message 'purgatory: won't be served until git data arrives'
10//! and kept in purgatory (not served) until the related git data arrives and otherwise
11//! discarded after 30 minutes."
12//!
13//! ## Test Categories
14//!
15//! ### Announcement Purgatory (feature not yet implemented)
16//! - `test_announcement_not_served_before_git_data`
17//! - `test_announcement_served_after_git_push`
18//! - `test_bare_repo_exists_for_purgatory_announcement`
19//! - `test_state_event_accepted_for_purgatory_announcement`
20//!
21//! ### State Event Purgatory (already implemented)
22//! - `test_state_event_not_served_before_git_data`
23//! - `test_state_event_served_after_git_push`
24//!
25//! ### PR Purgatory (already implemented)
26//! - `test_pr_event_accepted_into_purgatory` - Event accepted, not queryable
27//! - `test_pr_event_in_purgatory_git_push_accepted` - Git push to refs/nostr/<event-id> succeeds
28//! - `test_pr_event_served_after_git_push` - Event becomes queryable after git data
29
30use crate::fixtures::{clone_repo, create_commit, try_push};
31use crate::specs::grasp01::SpecRef;
32use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult};
33use nostr_sdk::prelude::*;
34use std::fs;
35use std::time::Duration;
36
37/// Test suite for GRASP-01 purgatory behavior
38pub struct PurgatoryTests;
39
40impl PurgatoryTests {
41 /// Run all purgatory tests
42 pub async fn run_all(client: &AuditClient) -> AuditResult {
43 let mut results = AuditResult::new("GRASP-01 Purgatory Tests");
44
45 // Announcement purgatory tests (feature not yet implemented)
46 results.add(Self::test_announcement_not_served_before_git_data(client).await);
47 results.add(Self::test_announcement_served_after_git_push(client).await);
48 results.add(Self::test_bare_repo_exists_for_purgatory_announcement(client).await);
49 results.add(Self::test_state_event_accepted_for_purgatory_announcement(client).await);
50
51 // Deletion event tests (NIP-09)
52 results.add(Self::test_deletion_by_event_id_removes_purgatory_state_event(client).await);
53 results.add(
54 Self::test_deletion_by_coordinate_removes_purgatory_state_event(client).await,
55 );
56
57 // State event purgatory tests (already implemented)
58 results.add(Self::test_state_event_not_served_before_git_data(client).await);
59 results.add(Self::test_state_event_served_after_git_push(client).await);
60
61 // PR purgatory tests
62 results.add(Self::test_pr_event_accepted_into_purgatory_and_isnt_served(client).await);
63 results.add(Self::test_pr_event_in_purgatory_git_push_accepted(client).await);
64 results.add(Self::test_pr_event_served_after_git_push(client).await);
65
66 results
67 }
68
69 // ============================================================
70 // Announcement Purgatory Tests (#[ignore] - feature not yet implemented)
71 // ============================================================
72
73 /// Test: Repository announcement not served before git data arrives
74 ///
75 /// Spec: GRASP-01 Line 22
76 /// "New repository announcements... SHOULD be accepted with message
77 /// 'purgatory: won't be served until git data arrives' and kept in purgatory
78 /// (not served) until the related git data arrives"
79 ///
80 /// This test verifies:
81 /// 1. Send a valid repository announcement
82 /// 2. Event is accepted (OK response)
83 /// 3. Event is NOT queryable from the relay (in purgatory)
84 ///
85 /// NOTE: Announcement purgatory feature not yet implemented - test may fail
86 pub async fn test_announcement_not_served_before_git_data(client: &AuditClient) -> TestResult {
87 TestResult::new(
88 "announcement_not_served_before_git_data",
89 SpecRef::PurgatoryAcceptUntilGitData,
90 "Repository announcements SHOULD be accepted but not served until git data arrives",
91 )
92 .run(|| async {
93 let ctx = TestContext::new(client);
94
95 // Create a fresh repo announcement (not the served variant)
96 let repo = ctx
97 .get_fixture(FixtureKind::ValidRepoSent)
98 .await
99 .map_err(|e| format!("Failed to create repo announcement: {}", e))?;
100
101 let repo_id = repo
102 .tags
103 .iter()
104 .find(|t| t.kind() == TagKind::d())
105 .and_then(|t| t.content())
106 .ok_or("Missing d tag in repo announcement")?
107 .to_string();
108
109 // Query for the announcement - should NOT be served
110 let filter = Filter::new()
111 .kind(Kind::GitRepoAnnouncement)
112 .author(client.public_key())
113 .identifier(&repo_id);
114
115 tokio::time::sleep(Duration::from_millis(300)).await;
116
117 let events = client
118 .query(filter)
119 .await
120 .map_err(|e| format!("Failed to query relay: {}", e))?;
121
122 if events.iter().any(|e| e.id == repo.id) {
123 return Err(format!(
124 "Announcement was served immediately - purgatory not implemented. \
125 Event ID: {} should NOT be queryable until git data arrives",
126 repo.id
127 ));
128 }
129
130 Ok(())
131 })
132 .await
133 }
134
135 /// Test: Repository announcement served after git push
136 ///
137 /// Spec: GRASP-01 Line 22
138 /// "...kept in purgatory (not served) until the related git data arrives"
139 ///
140 /// This test verifies the full lifecycle:
141 /// 1. Send repository announcement (enters purgatory)
142 /// 2. Send state event (enters purgatory)
143 /// 3. Push git data matching state event
144 /// 4. Both announcement and state event are now served
145 ///
146 /// NOTE: Announcement purgatory feature not yet implemented - test may fail
147 pub async fn test_announcement_served_after_git_push(client: &AuditClient) -> TestResult {
148 TestResult::new(
149 "announcement_served_after_git_push",
150 SpecRef::PurgatoryAcceptUntilGitData,
151 "Repository announcements SHOULD be served after git data arrives",
152 )
153 .run(|| async {
154 let ctx = TestContext::new(client);
155
156 // OwnerStateDataPushed fixture handles the full lifecycle:
157 // 1. Creates repo announcement (purgatory)
158 // 2. Creates state event (purgatory)
159 // 3. Pushes git data
160 // 4. Verifies events are served
161 let state_event = ctx
162 .get_fixture(FixtureKind::OwnerStateDataPushed)
163 .await
164 .map_err(|e| format!("Failed to complete full lifecycle: {}", e))?;
165
166 // Extract repo_id from state event
167 let repo_id = state_event
168 .tags
169 .iter()
170 .find(|t| t.kind() == TagKind::d())
171 .and_then(|t| t.content())
172 .ok_or("Missing d tag in state event")?
173 .to_string();
174
175 // Verify announcement is now served
176 let announcement_filter = Filter::new()
177 .kind(Kind::GitRepoAnnouncement)
178 .author(client.public_key())
179 .identifier(&repo_id);
180
181 let announcements = client
182 .query(announcement_filter)
183 .await
184 .map_err(|e| format!("Failed to query announcements: {}", e))?;
185
186 if announcements.is_empty() {
187 return Err(format!(
188 "Announcement not served after git push. Repo ID: {}",
189 repo_id
190 ));
191 }
192
193 // Verify state event is served
194 let state_filter = Filter::new()
195 .kind(Kind::RepoState)
196 .author(client.public_key())
197 .identifier(&repo_id);
198
199 let state_events = client
200 .query(state_filter)
201 .await
202 .map_err(|e| format!("Failed to query state events: {}", e))?;
203
204 if !state_events.iter().any(|e| e.id == state_event.id) {
205 return Err(format!(
206 "State event not served after git push. Event ID: {}",
207 state_event.id
208 ));
209 }
210
211 Ok(())
212 })
213 .await
214 }
215
216 /// Test: Bare repository exists for purgatory announcement
217 ///
218 /// Spec: GRASP-01 Line 34
219 /// "MUST serve a git repository via an unauthenticated git smart http service
220 /// at `/<npub>/<identifier>.git` for each git repository announcement the relay
221 /// serves or has in purgatory."
222 ///
223 /// This test verifies that git HTTP service works even for repos in purgatory.
224 ///
225 /// NOTE: Announcement purgatory feature not yet implemented - test may fail
226 pub async fn test_bare_repo_exists_for_purgatory_announcement(
227 client: &AuditClient,
228 ) -> TestResult {
229 TestResult::new(
230 "bare_repo_exists_for_purgatory_announcement",
231 SpecRef::GitServeRepository,
232 "Git HTTP service MUST work for repos in purgatory",
233 )
234 .run(|| async {
235 let ctx = TestContext::new(client);
236
237 // Get a repo announcement (in purgatory, no git data yet)
238 let repo = ctx
239 .get_fixture(FixtureKind::ValidRepoSent)
240 .await
241 .map_err(|e| format!("Failed to create repo announcement: {}", e))?;
242
243 let repo_id = repo
244 .tags
245 .iter()
246 .find(|t| t.kind() == TagKind::d())
247 .and_then(|t| t.content())
248 .ok_or("Missing d tag in repo announcement")?
249 .to_string();
250
251 let npub = client
252 .public_key()
253 .to_bech32()
254 .map_err(|e| format!("Failed to convert pubkey: {}", e))?;
255
256 // Get relay domain
257 let relay_url = client
258 .client()
259 .relays()
260 .await
261 .keys()
262 .next()
263 .ok_or("No relay connected")?
264 .to_string();
265 let relay_domain = relay_url
266 .replace("ws://", "")
267 .replace("wss://", "")
268 .replace(":8080", "");
269
270 // Check git HTTP service is available
271 let info_refs_url = format!(
272 "http://{}/{}/{}.git/info/refs?service=git-upload-pack",
273 relay_domain, npub, repo_id
274 );
275
276 let http_client = reqwest::Client::new();
277 let response = http_client
278 .get(&info_refs_url)
279 .send()
280 .await
281 .map_err(|e| format!("HTTP request failed: {}", e))?;
282
283 if !response.status().is_success() {
284 return Err(format!(
285 "Git HTTP service not available for purgatory repo. \
286 URL: {}, Status: {}",
287 info_refs_url,
288 response.status()
289 ));
290 }
291
292 Ok(())
293 })
294 .await
295 }
296
297 /// Test: State event accepted for purgatory announcement
298 ///
299 /// Spec: GRASP-01 Line 22
300 /// "New repository announcements, repo state announcements... SHOULD be accepted"
301 ///
302 /// This test verifies that state events are accepted even when the repo
303 /// announcement is in purgatory (no git data yet).
304 ///
305 /// NOTE: Announcement purgatory feature not yet implemented - test may fail
306 pub async fn test_state_event_accepted_for_purgatory_announcement(
307 client: &AuditClient,
308 ) -> TestResult {
309 TestResult::new(
310 "state_event_accepted_for_purgatory_announcement",
311 SpecRef::PurgatoryAcceptUntilGitData,
312 "State events SHOULD be accepted for repos in purgatory",
313 )
314 .run(|| async {
315 let ctx = TestContext::new(client);
316
317 // Get a repo announcement (in purgatory)
318 let repo = ctx
319 .get_fixture(FixtureKind::ValidRepoSent)
320 .await
321 .map_err(|e| format!("Failed to create repo announcement: {}", e))?;
322
323 // Build a state event for this repo
324 let repo_id = repo
325 .tags
326 .iter()
327 .find(|t| t.kind() == TagKind::d())
328 .and_then(|t| t.content())
329 .ok_or("Missing d tag in repo announcement")?
330 .to_string();
331
332 let state_event = client
333 .event_builder(Kind::RepoState, "")
334 .tag(Tag::identifier(&repo_id))
335 .tag(Tag::custom(
336 TagKind::custom("refs/heads/main"),
337 vec!["abc123".to_string()],
338 ))
339 .tag(Tag::custom(
340 TagKind::custom("HEAD"),
341 vec!["ref: refs/heads/main".to_string()],
342 ))
343 .build(client.keys())
344 .map_err(|e| format!("Failed to build state event: {}", e))?;
345
346 // Send state event - should be accepted (even though repo is in purgatory)
347 let (_, in_purgatory) = client
348 .send_event_and_note_purgatory(state_event.clone())
349 .await
350 .map_err(|e| format!("Failed to send state event: {}", e))?;
351
352 // Event should be accepted (either in purgatory or served)
353 // We just verify it wasn't rejected
354 if !in_purgatory {
355 // Check if it's actually on the relay (might be served immediately)
356 let filter = Filter::new()
357 .kind(Kind::RepoState)
358 .author(client.public_key())
359 .identifier(&repo_id);
360
361 let events = client
362 .query(filter)
363 .await
364 .map_err(|e| format!("Failed to query: {}", e))?;
365
366 if events.iter().any(|e| e.id == state_event.id) {
367 return Err(format!(
368 "State event was served immediately - repo announcement purgatory not implemented. \
369 Event ID: {} should NOT be queryable until git data arrives",
370 state_event.id
371 ));
372 }
373
374 return Err(format!(
375 "State event was neither in purgatory nor served. \
376 Event ID: {}",
377 state_event.id
378 ));
379 }
380
381 // Feature IS implemented - state event in purgatory as expected
382 Ok(())
383 })
384 .await
385 }
386
387 // ============================================================
388 // State Event Purgatory Tests (non-ignored - already implemented)
389 // ============================================================
390
391 /// Test: State event not served before git data arrives
392 ///
393 /// Spec: GRASP-01 Line 22
394 /// "repo state announcements... SHOULD be accepted with message
395 /// 'purgatory: won't be served until git data arrives'"
396 ///
397 /// This test verifies:
398 /// 1. Send state event for a repo with git data
399 /// 2. State event points to a different commit than what's pushed
400 /// 3. State event is NOT queryable (in purgatory)
401 pub async fn test_state_event_not_served_before_git_data(client: &AuditClient) -> TestResult {
402 TestResult::new(
403 "state_event_not_served_before_git_data",
404 SpecRef::PurgatoryAcceptUntilGitData,
405 "State events SHOULD be accepted but not served until git data arrives",
406 )
407 .run(|| async {
408 let ctx = TestContext::new(client);
409
410 // Get a repo with git data already pushed
411 let existing_state = ctx
412 .get_fixture(FixtureKind::OwnerStateDataPushed)
413 .await
414 .map_err(|e| format!("Failed to get existing repo: {}", e))?;
415
416 let repo_id = existing_state
417 .tags
418 .iter()
419 .find(|t| t.kind() == TagKind::d())
420 .and_then(|t| t.content())
421 .ok_or("Missing d tag in state event")?
422 .to_string();
423
424 // Create a NEW state event pointing to a DIFFERENT commit
425 // This should enter purgatory since the commit doesn't exist
426 let new_state = client
427 .event_builder(Kind::RepoState, "")
428 .tag(Tag::identifier(&repo_id))
429 .tag(Tag::custom(
430 TagKind::custom("refs/heads/main"),
431 vec!["deadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_string()],
432 ))
433 .tag(Tag::custom(
434 TagKind::custom("HEAD"),
435 vec!["ref: refs/heads/main".to_string()],
436 ))
437 .build(client.keys())
438 .map_err(|e| format!("Failed to build state event: {}", e))?;
439
440 // Send the state event
441 let (_, in_purgatory) = client
442 .send_event_and_note_purgatory(new_state.clone())
443 .await
444 .map_err(|e| format!("Failed to send state event: {}", e))?;
445
446 if !in_purgatory {
447 return Err(format!(
448 "State event was served immediately despite pointing to \
449 non-existent commit. Event ID: {}",
450 new_state.id
451 ));
452 }
453
454 Ok(())
455 })
456 .await
457 }
458
459 /// Test: State event served after git push
460 ///
461 /// Spec: GRASP-01 Line 22
462 /// "...kept in purgatory (not served) until the related git data arrives"
463 ///
464 /// This test verifies the full lifecycle using OwnerStateDataPushed fixture:
465 /// 1. State event is sent (enters purgatory)
466 /// 2. Git data is pushed matching the state event
467 /// 3. State event is now served
468 pub async fn test_state_event_served_after_git_push(client: &AuditClient) -> TestResult {
469 TestResult::new(
470 "state_event_served_after_git_push",
471 SpecRef::PurgatoryAcceptUntilGitData,
472 "State events SHOULD be served after matching git data arrives",
473 )
474 .run(|| async {
475 let ctx = TestContext::new(client);
476
477 // OwnerStateDataPushed handles the full lifecycle
478 let state_event = ctx
479 .get_fixture(FixtureKind::OwnerStateDataPushed)
480 .await
481 .map_err(|e| format!("Failed to complete full lifecycle: {}", e))?;
482
483 // Verify state event is now served
484 let repo_id = state_event
485 .tags
486 .iter()
487 .find(|t| t.kind() == TagKind::d())
488 .and_then(|t| t.content())
489 .ok_or("Missing d tag in state event")?
490 .to_string();
491
492 let filter = Filter::new()
493 .kind(Kind::RepoState)
494 .author(client.public_key())
495 .identifier(&repo_id);
496
497 let events = client
498 .query(filter)
499 .await
500 .map_err(|e| format!("Failed to query state events: {}", e))?;
501
502 if !events.iter().any(|e| e.id == state_event.id) {
503 return Err(format!(
504 "State event not served after git push. Event ID: {}",
505 state_event.id
506 ));
507 }
508
509 Ok(())
510 })
511 .await
512 }
513
514 // ============================================================
515 // PR Purgatory Tests
516 // ============================================================
517
518 /// Test: PR event accepted into purgatory (not served before git data)
519 ///
520 /// Spec: GRASP-01 Line 22
521 /// "PRs and PR Updates SHOULD be accepted with message
522 /// 'purgatory: won't be served until git data arrives'"
523 ///
524 /// This test verifies:
525 /// 1. PR event is sent and relay responds OK (accepted)
526 /// 2. PR event is NOT queryable (in purgatory, not served)
527 ///
528 /// PASS means: Relay accepted the event and is holding it in purgatory
529 /// FAIL means: Either event was rejected, or served immediately (purgatory not implemented)
530 ///
531 /// Note: This test cannot distinguish between "event in purgatory" and
532 /// "event accepted but never stored" - both result in event not being queryable.
533 /// The fixture verifies the relay responded OK, which is the best we can do
534 /// with black-box testing.
535 pub async fn test_pr_event_accepted_into_purgatory_and_isnt_served(
536 client: &AuditClient,
537 ) -> TestResult {
538 TestResult::new(
539 "pr_event_accepted_into_purgatory",
540 SpecRef::PurgatoryAcceptUntilGitData,
541 "PR event SHOULD be accepted but not served until git data arrives",
542 )
543 .run(|| async {
544 let ctx = TestContext::new(client);
545
546 // PREvent2Sent fixture:
547 // 1. Sends PR event
548 // 2. Verifies relay responded OK (not rejected)
549 // 3. Verifies event is NOT queryable (in purgatory)
550 let pr_event = ctx
551 .get_fixture(FixtureKind::PREvent2Sent)
552 .await
553 .map_err(|e| format!("Failed to send PR event: {}", e))?;
554
555 // Double-check: event should not be queryable
556 let filter = Filter::new()
557 .kind(Kind::GitPullRequest)
558 .author(client.pr_author_keys().public_key())
559 .id(pr_event.id);
560
561 tokio::time::sleep(Duration::from_millis(300)).await;
562
563 let events = client
564 .query(filter)
565 .await
566 .map_err(|e| format!("Failed to query PR events: {}", e))?;
567
568 if !events.is_empty() {
569 return Err(format!(
570 "PR event was served immediately - purgatory not implemented. Event ID: {}",
571 pr_event.id
572 ));
573 }
574
575 Ok(())
576 })
577 .await
578 }
579
580 /// Test: Git push to refs/nostr/<pr-event-id> is accepted
581 ///
582 /// This test verifies that pushing git data for a PR event in purgatory
583 /// is accepted by the relay.
584 ///
585 /// PASS means: Git push succeeded, relay accepted the git data
586 /// FAIL means: Git push was rejected (wrong ref, permissions, etc.)
587 pub async fn test_pr_event_in_purgatory_git_push_accepted(client: &AuditClient) -> TestResult {
588 TestResult::new(
589 "pr_event_in_purgatory_git_push_accepted",
590 SpecRef::PurgatoryAcceptUntilGitData,
591 "Git push for PR event SHOULD be accepted",
592 )
593 .run(|| async {
594 let ctx = TestContext::new(client);
595
596 // PREvent2GitDataPushed fixture:
597 // 1. Gets PR event in purgatory (PREvent2Sent)
598 // 2. Pushes commit to refs/nostr/<pr-event-id>
599 // 3. Verifies push succeeded
600 let _pr_event = ctx
601 .get_fixture(FixtureKind::PREvent2GitDataPushed)
602 .await
603 .map_err(|e| format!("Failed to push git data for PR event: {}", e))?;
604
605 Ok(())
606 })
607 .await
608 }
609
610 /// Test: PR event served after git data arrives
611 ///
612 /// This test verifies the full purgatory release mechanism:
613 /// after git data is pushed to refs/nostr/<pr-event-id>, the event
614 /// becomes queryable.
615 ///
616 /// PASS means: Event was released from purgatory and is now served
617 /// FAIL means: Event still not queryable after git push (purgatory release broken)
618 pub async fn test_pr_event_served_after_git_push(client: &AuditClient) -> TestResult {
619 TestResult::new(
620 "pr_event_served_after_git_push",
621 SpecRef::PurgatoryAcceptUntilGitData,
622 "PR event SHOULD be served after matching git data arrives",
623 )
624 .run(|| async {
625 let ctx = TestContext::new(client);
626
627 // PREvent2Served fixture:
628 // 1. Gets PR event with git data pushed (PREvent2GitDataPushed)
629 // 2. Verifies event is now queryable
630 let pr_event = ctx
631 .get_fixture(FixtureKind::PREvent2Served)
632 .await
633 .map_err(|e| format!("Failed to complete purgatory release: {}", e))?;
634
635 // Double-check: event should be queryable now
636 let filter = Filter::new()
637 .kind(Kind::GitPullRequest)
638 .author(client.pr_author_keys().public_key())
639 .id(pr_event.id);
640
641 let events = client
642 .query(filter)
643 .await
644 .map_err(|e| format!("Failed to query PR events: {}", e))?;
645
646 if events.is_empty() {
647 return Err(format!(
648 "PR event not served after git push. Event ID: {} should be queryable",
649 pr_event.id
650 ));
651 }
652
653 Ok(())
654 })
655 .await
656 }
657 // ============================================================
658 // Deletion Event Tests (NIP-09)
659 // ============================================================
660
661 /// Test: Kind 5 deletion event by event ID removes a purgatory state event
662 ///
663 /// Spec: NIP-09
664 /// "A special event with kind 5... having a list of one or more `e` or `a` tags,
665 /// each referencing an event the author is requesting to be deleted."
666 ///
667 /// This test verifies:
668 /// 1. Get a promoted repo (OwnerStateDataPushed) so git pushes are possible
669 /// 2. Clone the repo and create a unique commit (not yet pushed)
670 /// 3. Submit a state event pointing to that unique commit (enters purgatory)
671 /// 4. Send a kind 5 deletion event referencing the state event by event ID
672 /// 5. Attempt to push the unique commit — MUST be rejected (no authorized state event)
673 pub async fn test_deletion_by_event_id_removes_purgatory_state_event(
674 client: &AuditClient,
675 ) -> TestResult {
676 TestResult::new(
677 "deletion_by_event_id_removes_purgatory_state_event",
678 SpecRef::PurgatoryAcceptUntilGitData,
679 "Kind 5 deletion by event ID SHOULD remove a purgatory state event, causing push rejection",
680 )
681 .run(|| async {
682 let ctx = TestContext::new(client);
683
684 // Stage 1: get a promoted repo with git data already on the relay
685 let existing_state = ctx
686 .get_fixture(FixtureKind::OwnerStateDataPushed)
687 .await
688 .map_err(|e| format!("Failed to get promoted repo: {}", e))?;
689
690 let repo_id = existing_state
691 .tags
692 .iter()
693 .find(|t| t.kind() == TagKind::d())
694 .and_then(|t| t.content())
695 .ok_or("Missing d tag in state event")?
696 .to_string();
697
698 let relay_domain = client
699 .relay_url()
700 .await
701 .map_err(|e| e.to_string())?
702 .trim_start_matches("ws://")
703 .trim_start_matches("wss://")
704 .to_string();
705
706 let npub = client
707 .public_key()
708 .to_bech32()
709 .map_err(|e| e.to_string())?;
710
711 // Stage 2: clone the repo and create a unique commit (not pushed yet)
712 let clone_path = clone_repo(&relay_domain, &npub, &repo_id)
713 .map_err(|e| format!("Failed to clone repo: {}", e))?;
714
715 let cleanup = || { let _ = fs::remove_dir_all(&clone_path); };
716
717 let unique_commit = match create_commit(&clone_path, "deletion test unique commit") {
718 Ok(h) => h,
719 Err(e) => { cleanup(); return Err(format!("Failed to create commit: {}", e)); }
720 };
721
722 // Stage 3: submit a state event pointing to the unique commit (enters purgatory)
723 let state_event = client
724 .event_builder(Kind::RepoState, "")
725 .tag(Tag::identifier(&repo_id))
726 .tag(Tag::custom(
727 TagKind::custom("refs/heads/main"),
728 vec![unique_commit.clone()],
729 ))
730 .tag(Tag::custom(
731 TagKind::custom("HEAD"),
732 vec!["ref: refs/heads/main".to_string()],
733 ))
734 .build(client.keys())
735 .map_err(|e| { cleanup(); format!("Failed to build state event: {}", e) })?;
736
737 let (_, in_purgatory) = client
738 .send_event_and_note_purgatory(state_event.clone())
739 .await
740 .map_err(|e| { cleanup(); format!("Failed to send state event: {}", e) })?;
741
742 if !in_purgatory {
743 cleanup();
744 return Err(format!(
745 "State event was served immediately (not in purgatory). \
746 Commit {} may already exist on relay.",
747 unique_commit
748 ));
749 }
750
751 // Stage 4: send kind 5 deletion event referencing the state event by event ID
752 let deletion = client
753 .event_builder(Kind::EventDeletion, "")
754 .tag(Tag::event(state_event.id))
755 .tag(Tag::custom(TagKind::custom("k"), vec!["30618"]))
756 .build(client.keys())
757 .map_err(|e| { cleanup(); format!("Failed to build deletion event: {}", e) })?;
758
759 client
760 .send_event(deletion)
761 .await
762 .map_err(|e| { cleanup(); format!("Relay rejected deletion event: {}", e) })?;
763
764 tokio::time::sleep(Duration::from_millis(300)).await;
765
766 // Stage 5: attempt to push the unique commit — must be rejected
767 let push_result = try_push(&clone_path);
768 cleanup();
769
770 match push_result {
771 Ok(false) => Ok(()), // push rejected as expected
772 Ok(true) => Err(format!(
773 "Push was accepted but should have been rejected. \
774 The state event (id={}) was deleted, so commit {} \
775 should not be authorized.",
776 state_event.id, unique_commit
777 )),
778 Err(e) => Err(format!("Git push error: {}", e)),
779 }
780 })
781 .await
782 }
783
784 /// Test: Kind 5 deletion event by `a` tag coordinate removes a purgatory state event
785 ///
786 /// Spec: NIP-09
787 /// "When an `a` tag is used, relays SHOULD delete all versions of the replaceable
788 /// event up to the `created_at` timestamp of the deletion request event."
789 ///
790 /// This test verifies:
791 /// 1. Get a promoted repo (OwnerStateDataPushed) so git pushes are possible
792 /// 2. Generate a fresh keypair for a new maintainer
793 /// 3. Send a replacement owner announcement adding the new maintainer (goes to DB)
794 /// 4. Send a state event signed by the new maintainer pointing to a unique commit
795 /// (enters purgatory — maintainer is authorized but commit doesn't exist yet)
796 /// 5. Delete by coordinate `30618:<new_maintainer_pubkey>:<identifier>`
797 /// 6. Clone repo, create that unique commit, attempt to push — MUST be rejected
798 /// (the state event was deleted, so the commit is no longer authorized)
799 pub async fn test_deletion_by_coordinate_removes_purgatory_state_event(
800 client: &AuditClient,
801 ) -> TestResult {
802 TestResult::new(
803 "deletion_by_coordinate_removes_purgatory_state_event",
804 SpecRef::PurgatoryAcceptUntilGitData,
805 "Kind 5 deletion by `a` coordinate SHOULD remove a purgatory state event, causing push rejection",
806 )
807 .run(|| async {
808 let ctx = TestContext::new(client);
809
810 // Stage 1: get a promoted repo with git data already on the relay
811 let existing_state = ctx
812 .get_fixture(FixtureKind::OwnerStateDataPushed)
813 .await
814 .map_err(|e| format!("Failed to get promoted repo: {}", e))?;
815
816 let repo_id = existing_state
817 .tags
818 .iter()
819 .find(|t| t.kind() == TagKind::d())
820 .and_then(|t| t.content())
821 .ok_or("Missing d tag in state event")?
822 .to_string();
823
824 // Stage 2: generate a fresh keypair for a new maintainer
825 let new_maintainer_keys = Keys::generate();
826 let new_maintainer_hex = new_maintainer_keys.public_key().to_hex();
827
828 // Stage 3: send a replacement owner announcement that adds the new maintainer.
829 // This is a replacement (same pubkey + identifier already in DB) so it goes
830 // straight to the database without entering purgatory.
831 let relay_url = client
832 .relay_url()
833 .await
834 .map_err(|e| e.to_string())?;
835 let http_url = relay_url
836 .replace("ws://", "http://")
837 .replace("wss://", "https://");
838 let npub = client
839 .public_key()
840 .to_bech32()
841 .map_err(|e| e.to_string())?;
842
843 let replacement_announcement = client
844 .event_builder(Kind::GitRepoAnnouncement, "")
845 .tag(Tag::identifier(&repo_id))
846 .tag(Tag::custom(
847 TagKind::custom("clone"),
848 vec![format!("{}/{}/{}.git", http_url, npub, repo_id)],
849 ))
850 .tag(Tag::custom(
851 TagKind::custom("relays"),
852 vec![relay_url.clone()],
853 ))
854 .tag(Tag::custom(
855 TagKind::custom("maintainers"),
856 vec![new_maintainer_hex.clone()],
857 ))
858 .build(client.keys())
859 .map_err(|e| format!("Failed to build replacement announcement: {}", e))?;
860
861 client
862 .send_event(replacement_announcement)
863 .await
864 .map_err(|e| format!("Relay rejected replacement announcement: {}", e))?;
865
866 tokio::time::sleep(Duration::from_millis(200)).await;
867
868 // Stage 4: clone the repo and create a unique commit (not pushed yet)
869 let relay_domain = relay_url
870 .trim_start_matches("ws://")
871 .trim_start_matches("wss://")
872 .to_string();
873
874 let clone_path = clone_repo(&relay_domain, &npub, &repo_id)
875 .map_err(|e| format!("Failed to clone repo: {}", e))?;
876
877 let cleanup = || { let _ = fs::remove_dir_all(&clone_path); };
878
879 let unique_commit = match create_commit(&clone_path, "deletion coordinate test unique commit") {
880 Ok(h) => h,
881 Err(e) => { cleanup(); return Err(format!("Failed to create commit: {}", e)); }
882 };
883
884 // Stage 5: submit a state event signed by the new maintainer pointing to the
885 // unique commit. The new maintainer is now authorized (listed in the replacement
886 // announcement), so the state event should enter purgatory (commit doesn't exist).
887 let state_event = client
888 .event_builder(Kind::RepoState, "")
889 .tag(Tag::identifier(&repo_id))
890 .tag(Tag::custom(
891 TagKind::custom("refs/heads/main"),
892 vec![unique_commit.clone()],
893 ))
894 .tag(Tag::custom(
895 TagKind::custom("HEAD"),
896 vec!["ref: refs/heads/main".to_string()],
897 ))
898 .build(&new_maintainer_keys)
899 .map_err(|e| { cleanup(); format!("Failed to build state event: {}", e) })?;
900
901 let (_, in_purgatory) = client
902 .send_event_and_note_purgatory(state_event.clone())
903 .await
904 .map_err(|e| { cleanup(); format!("Failed to send state event: {}", e) })?;
905
906 if !in_purgatory {
907 cleanup();
908 return Err(format!(
909 "State event was served immediately (not in purgatory). \
910 Commit {} may already exist on relay.",
911 unique_commit
912 ));
913 }
914
915 // Stage 6: send kind 5 deletion event signed by the new maintainer,
916 // referencing their state event by coordinate `30618:<pubkey>:<identifier>`
917 let coord = format!("30618:{}:{}", new_maintainer_hex, repo_id);
918
919 let deletion = client
920 .event_builder(Kind::EventDeletion, "")
921 .tag(Tag::custom(TagKind::custom("a"), vec![coord]))
922 .tag(Tag::custom(TagKind::custom("k"), vec!["30618"]))
923 .build(&new_maintainer_keys)
924 .map_err(|e| { cleanup(); format!("Failed to build deletion event: {}", e) })?;
925
926 client
927 .send_event(deletion)
928 .await
929 .map_err(|e| { cleanup(); format!("Relay rejected deletion event: {}", e) })?;
930
931 tokio::time::sleep(Duration::from_millis(300)).await;
932
933 // Stage 7: attempt to push the unique commit — must be rejected because
934 // the new maintainer's state event was deleted from purgatory
935 let push_result = try_push(&clone_path);
936 cleanup();
937
938 match push_result {
939 Ok(false) => Ok(()), // push rejected as expected
940 Ok(true) => Err(format!(
941 "Push was accepted but should have been rejected. \
942 The new maintainer's state event (id={}) was deleted by coordinate, \
943 so commit {} should not be authorized.",
944 state_event.id, unique_commit
945 )),
946 Err(e) => Err(format!("Git push error: {}", e)),
947 }
948 })
949 .await
950 }
951}
952
953#[cfg(test)]
954mod tests {
955 use super::*;
956 use crate::AuditConfig;
957
958 #[tokio::test]
959 #[ignore] // Requires running relay
960 async fn test_grasp01_purgatory_against_relay() {
961 let relay_url = std::env::var("RELAY_URL").expect(
962 "RELAY_URL environment variable must be set. Example: RELAY_URL=ws://localhost:18081",
963 );
964
965 let config = AuditConfig::isolated();
966 let client = AuditClient::new(&relay_url, config)
967 .await
968 .unwrap_or_else(|_| {
969 panic!(
970 "Failed to connect to relay at {}. Ensure relay is running and accessible.",
971 relay_url
972 )
973 });
974
975 let results = PurgatoryTests::run_all(&client).await;
976 results.print_report();
977
978 assert!(
979 results.all_passed(),
980 "Some purgatory tests failed. See report above."
981 );
982 }
983}
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs
index c1003b9..73cbe1f 100644
--- a/grasp-audit/src/specs/grasp01/push_authorization.rs
+++ b/grasp-audit/src/specs/grasp01/push_authorization.rs
@@ -19,7 +19,7 @@
19/// Expected hash for PR test deterministic commit 19/// Expected hash for PR test deterministic commit
20/// 20///
21/// This hash is produced by creating a commit with: 21/// This hash is produced by creating a commit with:
22/// - File: test.txt containing "PR test deterministic commit" 22/// - File: test.txt containing "PR test deterministic commit\n" (with trailing newline)
23/// - Message: "PR test deterministic commit" 23/// - Message: "PR test deterministic commit"
24/// - Author: "GRASP Audit Test <test@grasp-audit.local>" 24/// - Author: "GRASP Audit Test <test@grasp-audit.local>"
25/// - Author date: 2024-01-01T00:00:00Z 25/// - Author date: 2024-01-01T00:00:00Z
@@ -29,8 +29,9 @@
29/// 29///
30/// Run `test_pr_test_commit_hash_discovery` to discover/verify this value. 30/// Run `test_pr_test_commit_hash_discovery` to discover/verify this value.
31#[allow(dead_code)] 31#[allow(dead_code)]
32const PR_TEST_COMMIT_HASH: &str = "5d40fb1555a0c28bf4d650515a73aaa54d4d9bfb"; 32const PR_TEST_COMMIT_HASH: &str = "5a51b30e4615b572dcd5b9e487861b58605a5c21";
33 33
34use crate::specs::grasp01::SpecRef;
34use crate::{ 35use crate::{
35 clone_repo, create_commit, create_deterministic_commit_with_variant, try_push, try_push_to_ref, 36 clone_repo, create_commit, create_deterministic_commit_with_variant, try_push, try_push_to_ref,
36 AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, 37 AuditClient, CommitVariant, FixtureKind, TestContext, TestResult,
@@ -207,7 +208,7 @@ async fn setup_pr_test_repo(
207) -> Result<(PathBuf, String, String, String), String> { 208) -> Result<(PathBuf, String, String, String), String> {
208 // Get fixtures 209 // Get fixtures
209 let repo_event = ctx 210 let repo_event = ctx
210 .get_fixture(FixtureKind::ValidRepo) 211 .get_fixture(FixtureKind::ValidRepoServed)
211 .await 212 .await
212 .map_err(|e| format!("Failed to get repo announcement: {}", e))?; 213 .map_err(|e| format!("Failed to get repo announcement: {}", e))?;
213 214
@@ -406,12 +407,12 @@ impl PushAuthorizationTests {
406 let ctx = TestContext::new(client); 407 let ctx = TestContext::new(client);
407 408
408 // Create repository (no state event) 409 // Create repository (no state event)
409 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 410 let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await {
410 Ok(r) => r, 411 Ok(r) => r,
411 Err(e) => { 412 Err(e) => {
412 return TestResult::new( 413 return TestResult::new(
413 test_name, 414 test_name,
414 "GRASP-01:git-http:36", 415 SpecRef::GitAcceptPushesAlignState,
415 "Push rejected without state event", 416 "Push rejected without state event",
416 ) 417 )
417 .fail(format!("Failed to create repo: {}", e)) 418 .fail(format!("Failed to create repo: {}", e))
@@ -435,7 +436,7 @@ impl PushAuthorizationTests {
435 Err(e) => { 436 Err(e) => {
436 return TestResult::new( 437 return TestResult::new(
437 test_name, 438 test_name,
438 "GRASP-01:git-http:36", 439 SpecRef::GitAcceptPushesAlignState,
439 "Push rejected without state event", 440 "Push rejected without state event",
440 ) 441 )
441 .fail(&e) 442 .fail(&e)
@@ -449,7 +450,7 @@ impl PushAuthorizationTests {
449 cleanup(); 450 cleanup();
450 return TestResult::new( 451 return TestResult::new(
451 test_name, 452 test_name,
452 "GRASP-01:git-http:36", 453 SpecRef::GitAcceptPushesAlignState,
453 "Push rejected without state event", 454 "Push rejected without state event",
454 ) 455 )
455 .fail(&e); 456 .fail(&e);
@@ -462,19 +463,19 @@ impl PushAuthorizationTests {
462 match push_result { 463 match push_result {
463 Ok(false) => TestResult::new( 464 Ok(false) => TestResult::new(
464 test_name, 465 test_name,
465 "GRASP-01:git-http:36", 466 SpecRef::GitAcceptPushesAlignState,
466 "Push rejected without state event", 467 "Push rejected without state event",
467 ) 468 )
468 .pass(), 469 .pass(),
469 Ok(true) => TestResult::new( 470 Ok(true) => TestResult::new(
470 test_name, 471 test_name,
471 "GRASP-01:git-http:36", 472 SpecRef::GitAcceptPushesAlignState,
472 "Push rejected without state event", 473 "Push rejected without state event",
473 ) 474 )
474 .fail("Push accepted but should be rejected"), 475 .fail("Push accepted but should be rejected"),
475 Err(e) => TestResult::new( 476 Err(e) => TestResult::new(
476 test_name, 477 test_name,
477 "GRASP-01:git-http:36", 478 SpecRef::GitAcceptPushesAlignState,
478 "Push rejected without state event", 479 "Push rejected without state event",
479 ) 480 )
480 .fail(&e), 481 .fail(&e),
@@ -507,13 +508,13 @@ impl PushAuthorizationTests {
507 match ctx.get_fixture(FixtureKind::OwnerStateDataPushed).await { 508 match ctx.get_fixture(FixtureKind::OwnerStateDataPushed).await {
508 Ok(_state_event) => TestResult::new( 509 Ok(_state_event) => TestResult::new(
509 test_name, 510 test_name,
510 "GRASP-01:git-http:36", // TODO do we add purgatory line here? 511 SpecRef::GitAcceptPushesAlignState,
511 "Push authorized with matching state", 512 "Push authorized with matching state",
512 ) 513 )
513 .pass(), 514 .pass(),
514 Err(e) => TestResult::new( 515 Err(e) => TestResult::new(
515 test_name, 516 test_name,
516 "GRASP-01:git-http:36", 517 SpecRef::GitAcceptPushesAlignState,
517 "Push authorized with matching state", 518 "Push authorized with matching state",
518 ) 519 )
519 .fail(format!("{}", e)), 520 .fail(format!("{}", e)),
@@ -555,7 +556,7 @@ impl PushAuthorizationTests {
555 Err(e) => { 556 Err(e) => {
556 return TestResult::new( 557 return TestResult::new(
557 test_name, 558 test_name,
558 "GRASP-01:git-http:36", 559 SpecRef::GitAcceptPushesAlignState,
559 "Push rejected when commit not in state event", 560 "Push rejected when commit not in state event",
560 ) 561 )
561 .fail(format!("Failed to create RepoState fixture: {}", e)); 562 .fail(format!("Failed to create RepoState fixture: {}", e));
@@ -575,7 +576,7 @@ impl PushAuthorizationTests {
575 None => { 576 None => {
576 return TestResult::new( 577 return TestResult::new(
577 test_name, 578 test_name,
578 "GRASP-01:git-http:36", 579 SpecRef::GitAcceptPushesAlignState,
579 "Push rejected when commit not in state event", 580 "Push rejected when commit not in state event",
580 ) 581 )
581 .fail("Missing repo_id in state event"); 582 .fail("Missing repo_id in state event");
@@ -587,7 +588,7 @@ impl PushAuthorizationTests {
587 Err(e) => { 588 Err(e) => {
588 return TestResult::new( 589 return TestResult::new(
589 test_name, 590 test_name,
590 "GRASP-01:git-http:36", 591 SpecRef::GitAcceptPushesAlignState,
591 "Push rejected when commit not in state event", 592 "Push rejected when commit not in state event",
592 ) 593 )
593 .fail(format!("Failed to convert pubkey to bech32: {}", e)); 594 .fail(format!("Failed to convert pubkey to bech32: {}", e));
@@ -603,7 +604,7 @@ impl PushAuthorizationTests {
603 Err(e) => { 604 Err(e) => {
604 return TestResult::new( 605 return TestResult::new(
605 test_name, 606 test_name,
606 "GRASP-01:git-http:36", 607 SpecRef::GitAcceptPushesAlignState,
607 "Push rejected when commit not in state event", 608 "Push rejected when commit not in state event",
608 ) 609 )
609 .fail(format!("Failed to clone repo: {}", e)); 610 .fail(format!("Failed to clone repo: {}", e));
@@ -626,7 +627,7 @@ impl PushAuthorizationTests {
626 cleanup(); 627 cleanup();
627 return TestResult::new( 628 return TestResult::new(
628 test_name, 629 test_name,
629 "GRASP-01:git-http:36", 630 SpecRef::GitAcceptPushesAlignState,
630 "Push rejected when commit not in state event", 631 "Push rejected when commit not in state event",
631 ) 632 )
632 .fail(format!("Failed to create/checkout main branch: {}", e)); 633 .fail(format!("Failed to create/checkout main branch: {}", e));
@@ -635,7 +636,7 @@ impl PushAuthorizationTests {
635 cleanup(); 636 cleanup();
636 return TestResult::new( 637 return TestResult::new(
637 test_name, 638 test_name,
638 "GRASP-01:git-http:36", 639 SpecRef::GitAcceptPushesAlignState,
639 "Push rejected when commit not in state event", 640 "Push rejected when commit not in state event",
640 ) 641 )
641 .fail(format!( 642 .fail(format!(
@@ -652,7 +653,7 @@ impl PushAuthorizationTests {
652 cleanup(); 653 cleanup();
653 return TestResult::new( 654 return TestResult::new(
654 test_name, 655 test_name,
655 "GRASP-01:git-http:36", 656 SpecRef::GitAcceptPushesAlignState,
656 "Push rejected when commit not in state event", 657 "Push rejected when commit not in state event",
657 ) 658 )
658 .fail(format!("Failed to create wrong commit: {}", e)); 659 .fail(format!("Failed to create wrong commit: {}", e));
@@ -666,10 +667,10 @@ impl PushAuthorizationTests {
666 cleanup(); 667 cleanup();
667 668
668 match push_result { 669 match push_result {
669 Ok(false) => TestResult::new(test_name, "GRASP-01:git-http:36", "Push rejected when commit not in state event").pass(), 670 Ok(false) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Push rejected when commit not in state event").pass(),
670 Ok(true) => TestResult::new(test_name, "GRASP-01:git-http:36", "Push rejected when commit not in state event") 671 Ok(true) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Push rejected when commit not in state event")
671 .fail("Push accepted but should be rejected. The pushed commit is not in the state event."), 672 .fail("Push accepted but should be rejected. The pushed commit is not in the state event."),
672 Err(e) => TestResult::new(test_name, "GRASP-01:git-http:36", "Push rejected when commit not in state event").fail(&e), 673 Err(e) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Push rejected when commit not in state event").fail(&e),
673 } 674 }
674 } 675 }
675 676
@@ -704,13 +705,13 @@ impl PushAuthorizationTests {
704 { 705 {
705 Ok(_maintainer_state_event) => TestResult::new( 706 Ok(_maintainer_state_event) => TestResult::new(
706 test_name, 707 test_name,
707 "GRASP-01:git-http:36", 708 SpecRef::GitAcceptPushesAlignState,
708 "Push authorized by maintainer state event only (no announcement)", 709 "Push authorized by maintainer state event only (no announcement)",
709 ) 710 )
710 .pass(), 711 .pass(),
711 Err(e) => TestResult::new( 712 Err(e) => TestResult::new(
712 test_name, 713 test_name,
713 "GRASP-01:git-http:36", 714 SpecRef::GitAcceptPushesAlignState,
714 "Push authorized by maintainer state event only (no announcement)", 715 "Push authorized by maintainer state event only (no announcement)",
715 ) 716 )
716 .fail(format!("{}", e)), 717 .fail(format!("{}", e)),
@@ -747,13 +748,13 @@ impl PushAuthorizationTests {
747 { 748 {
748 Ok(_recursive_maintainer_state_event) => TestResult::new( 749 Ok(_recursive_maintainer_state_event) => TestResult::new(
749 test_name, 750 test_name,
750 "GRASP-01:git-http:36", 751 SpecRef::GitAcceptPushesAlignState,
751 "Push authorized by recursive maintainer state event", 752 "Push authorized by recursive maintainer state event",
752 ) 753 )
753 .pass(), 754 .pass(),
754 Err(e) => TestResult::new( 755 Err(e) => TestResult::new(
755 test_name, 756 test_name,
756 "GRASP-01:git-http:36", 757 SpecRef::GitAcceptPushesAlignState,
757 "Push authorized by recursive maintainer state event", 758 "Push authorized by recursive maintainer state event",
758 ) 759 )
759 .fail(format!("{}", e)), 760 .fail(format!("{}", e)),
@@ -797,7 +798,7 @@ impl PushAuthorizationTests {
797 Err(e) => { 798 Err(e) => {
798 return TestResult::new( 799 return TestResult::new(
799 test_name, 800 test_name,
800 "GRASP-01:git-http:36", 801 SpecRef::GitAcceptPushesAlignState,
801 "Non-maintainer state events ignored", 802 "Non-maintainer state events ignored",
802 ) 803 )
803 .fail(format!("Failed to get OwnerStateDataPushed fixture: {}", e)); 804 .fail(format!("Failed to get OwnerStateDataPushed fixture: {}", e));
@@ -815,7 +816,7 @@ impl PushAuthorizationTests {
815 None => { 816 None => {
816 return TestResult::new( 817 return TestResult::new(
817 test_name, 818 test_name,
818 "GRASP-01:git-http:36", 819 SpecRef::GitAcceptPushesAlignState,
819 "Non-maintainer state events ignored", 820 "Non-maintainer state events ignored",
820 ) 821 )
821 .fail("Missing repo_id in state event"); 822 .fail("Missing repo_id in state event");
@@ -827,7 +828,7 @@ impl PushAuthorizationTests {
827 Err(e) => { 828 Err(e) => {
828 return TestResult::new( 829 return TestResult::new(
829 test_name, 830 test_name,
830 "GRASP-01:git-http:36", 831 SpecRef::GitAcceptPushesAlignState,
831 "Non-maintainer state events ignored", 832 "Non-maintainer state events ignored",
832 ) 833 )
833 .fail(format!("Failed to convert pubkey to bech32: {}", e)); 834 .fail(format!("Failed to convert pubkey to bech32: {}", e));
@@ -842,7 +843,7 @@ impl PushAuthorizationTests {
842 Err(e) => { 843 Err(e) => {
843 return TestResult::new( 844 return TestResult::new(
844 test_name, 845 test_name,
845 "GRASP-01:git-http:36", 846 SpecRef::GitAcceptPushesAlignState,
846 "Non-maintainer state events ignored", 847 "Non-maintainer state events ignored",
847 ) 848 )
848 .fail(format!("Failed to clone repo: {}", e)); 849 .fail(format!("Failed to clone repo: {}", e));
@@ -864,7 +865,7 @@ impl PushAuthorizationTests {
864 cleanup(); 865 cleanup();
865 return TestResult::new( 866 return TestResult::new(
866 test_name, 867 test_name,
867 "GRASP-01:git-http:36", 868 SpecRef::GitAcceptPushesAlignState,
868 "Non-maintainer state events ignored", 869 "Non-maintainer state events ignored",
869 ) 870 )
870 .fail(format!("Failed to create commit: {}", e)); 871 .fail(format!("Failed to create commit: {}", e));
@@ -890,7 +891,7 @@ impl PushAuthorizationTests {
890 cleanup(); 891 cleanup();
891 return TestResult::new( 892 return TestResult::new(
892 test_name, 893 test_name,
893 "GRASP-01:git-http:36", 894 SpecRef::GitAcceptPushesAlignState,
894 "Non-maintainer state events ignored", 895 "Non-maintainer state events ignored",
895 ) 896 )
896 .fail(format!("Failed to build rogue state event: {}", e)); 897 .fail(format!("Failed to build rogue state event: {}", e));
@@ -902,7 +903,7 @@ impl PushAuthorizationTests {
902 cleanup(); 903 cleanup();
903 return TestResult::new( 904 return TestResult::new(
904 test_name, 905 test_name,
905 "GRASP-01:git-http:36", 906 SpecRef::GitAcceptPushesAlignState,
906 "Non-maintainer state events ignored", 907 "Non-maintainer state events ignored",
907 ) 908 )
908 .fail(format!("Failed to send rogue state event: {}", e)); 909 .fail(format!("Failed to send rogue state event: {}", e));
@@ -919,8 +920,8 @@ impl PushAuthorizationTests {
919 cleanup(); 920 cleanup();
920 921
921 match push_result { 922 match push_result {
922 Ok(false) => TestResult::new(test_name, "GRASP-01:git-http:36", "Non-maintainer state events ignored").pass(), 923 Ok(false) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Non-maintainer state events ignored").pass(),
923 Ok(true) => TestResult::new(test_name, "GRASP-01:git-http:36", "Non-maintainer state events ignored") 924 Ok(true) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Non-maintainer state events ignored")
924 .fail(format!( 925 .fail(format!(
925 "Push accepted but should be rejected. A non-maintainer (pubkey: {}) published \ 926 "Push accepted but should be rejected. A non-maintainer (pubkey: {}) published \
926 a state event announcing commit {}, but the push was accepted. The relay should \ 927 a state event announcing commit {}, but the push was accepted. The relay should \
@@ -929,7 +930,7 @@ impl PushAuthorizationTests {
929 new_commit, 930 new_commit,
930 client.public_key() 931 client.public_key()
931 )), 932 )),
932 Err(e) => TestResult::new(test_name, "GRASP-01:git-http:36", "Non-maintainer state events ignored").fail(&e), 933 Err(e) => TestResult::new(test_name, SpecRef::GitAcceptPushesAlignState, "Non-maintainer state events ignored").fail(&e),
933 } 934 }
934 } 935 }
935 936
@@ -955,12 +956,12 @@ impl PushAuthorizationTests {
955 // ============================================================ 956 // ============================================================
956 let ctx = TestContext::new(client); 957 let ctx = TestContext::new(client);
957 958
958 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 959 let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await {
959 Ok(r) => r, 960 Ok(r) => r,
960 Err(e) => { 961 Err(e) => {
961 return TestResult::new( 962 return TestResult::new(
962 test_name, 963 test_name,
963 "GRASP-01:git-http:40", 964 SpecRef::GitAcceptRefsNostrEventId,
964 "Push to refs/nostr/<invalid-event-id> rejected", 965 "Push to refs/nostr/<invalid-event-id> rejected",
965 ) 966 )
966 .fail(format!("Failed to create repo: {}", e)); 967 .fail(format!("Failed to create repo: {}", e));
@@ -986,7 +987,7 @@ impl PushAuthorizationTests {
986 Err(e) => { 987 Err(e) => {
987 return TestResult::new( 988 return TestResult::new(
988 test_name, 989 test_name,
989 "GRASP-01:git-http:40", 990 SpecRef::GitAcceptRefsNostrEventId,
990 "Push to refs/nostr/<invalid-event-id> rejected", 991 "Push to refs/nostr/<invalid-event-id> rejected",
991 ) 992 )
992 .fail(&e); 993 .fail(&e);
@@ -1001,7 +1002,7 @@ impl PushAuthorizationTests {
1001 cleanup(); 1002 cleanup();
1002 return TestResult::new( 1003 return TestResult::new(
1003 test_name, 1004 test_name,
1004 "GRASP-01:git-http:40", 1005 SpecRef::GitAcceptRefsNostrEventId,
1005 "Push to refs/nostr/<invalid-event-id> rejected", 1006 "Push to refs/nostr/<invalid-event-id> rejected",
1006 ) 1007 )
1007 .fail(&e); 1008 .fail(&e);
@@ -1020,13 +1021,13 @@ impl PushAuthorizationTests {
1020 match push_result { 1021 match push_result {
1021 Ok(false) => TestResult::new( 1022 Ok(false) => TestResult::new(
1022 test_name, 1023 test_name,
1023 "GRASP-01:git-http:40", 1024 SpecRef::GitAcceptRefsNostrEventId,
1024 "Push to refs/nostr/<invalid-event-id> rejected", 1025 "Push to refs/nostr/<invalid-event-id> rejected",
1025 ) 1026 )
1026 .pass(), 1027 .pass(),
1027 Ok(true) => TestResult::new( 1028 Ok(true) => TestResult::new(
1028 test_name, 1029 test_name,
1029 "GRASP-01:git-http:40", 1030 SpecRef::GitAcceptRefsNostrEventId,
1030 "Push to refs/nostr/<invalid-event-id> rejected", 1031 "Push to refs/nostr/<invalid-event-id> rejected",
1031 ) 1032 )
1032 .fail(format!( 1033 .fail(format!(
@@ -1037,7 +1038,7 @@ impl PushAuthorizationTests {
1037 )), 1038 )),
1038 Err(e) => TestResult::new( 1039 Err(e) => TestResult::new(
1039 test_name, 1040 test_name,
1040 "GRASP-01:git-http:40", 1041 SpecRef::GitAcceptRefsNostrEventId,
1041 "Push to refs/nostr/<invalid-event-id> rejected", 1042 "Push to refs/nostr/<invalid-event-id> rejected",
1042 ) 1043 )
1043 .fail(format!("Push error: {}", e)), 1044 .fail(format!("Push error: {}", e)),
@@ -1071,10 +1072,11 @@ impl PushAuthorizationTests {
1071 .get_fixture(FixtureKind::PRWrongCommitPushedBeforeEvent) 1072 .get_fixture(FixtureKind::PRWrongCommitPushedBeforeEvent)
1072 .await 1073 .await
1073 { 1074 {
1074 Ok(_pr_event) => TestResult::new(test_name, "GRASP-01:git-http:40", desc).pass(), 1075 Ok(_pr_event) => {
1075 Err(e) => { 1076 TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).pass()
1076 TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(format!("{}", e))
1077 } 1077 }
1078 Err(e) => TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1079 .fail(format!("{}", e)),
1078 } 1080 }
1079 } 1081 }
1080 1082
@@ -1100,7 +1102,7 @@ impl PushAuthorizationTests {
1100 { 1102 {
1101 Ok(e) => e, 1103 Ok(e) => e,
1102 Err(e) => { 1104 Err(e) => {
1103 return TestResult::new(test_name, "GRASP-01:git-http:40", desc) 1105 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1104 .fail(format!("{}", e)); 1106 .fail(format!("{}", e));
1105 } 1107 }
1106 }; 1108 };
@@ -1108,10 +1110,10 @@ impl PushAuthorizationTests {
1108 let pr_event_id = pr_event.id.to_hex(); 1110 let pr_event_id = pr_event.id.to_hex();
1109 1111
1110 // Get repo info for cloning (fresh clone for verification) 1112 // Get repo info for cloning (fresh clone for verification)
1111 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 1113 let repo = match ctx.get_fixture(FixtureKind::ValidRepoServed).await {
1112 Ok(r) => r, 1114 Ok(r) => r,
1113 Err(e) => { 1115 Err(e) => {
1114 return TestResult::new(test_name, "GRASP-01:git-http:40", desc) 1116 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1115 .fail(format!("{}", e)); 1117 .fail(format!("{}", e));
1116 } 1118 }
1117 }; 1119 };
@@ -1127,7 +1129,7 @@ impl PushAuthorizationTests {
1127 let owner_npub = match repo.pubkey.to_bech32() { 1129 let owner_npub = match repo.pubkey.to_bech32() {
1128 Ok(n) => n, 1130 Ok(n) => n,
1129 Err(e) => { 1131 Err(e) => {
1130 return TestResult::new(test_name, "GRASP-01:git-http:40", desc) 1132 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1131 .fail(format!("Failed to get owner npub: {}", e)); 1133 .fail(format!("Failed to get owner npub: {}", e));
1132 } 1134 }
1133 }; 1135 };
@@ -1136,7 +1138,8 @@ impl PushAuthorizationTests {
1136 let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { 1138 let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) {
1137 Ok(p) => p, 1139 Ok(p) => p,
1138 Err(e) => { 1140 Err(e) => {
1139 return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); 1141 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1142 .fail(&e);
1140 } 1143 }
1141 }; 1144 };
1142 1145
@@ -1146,7 +1149,8 @@ impl PushAuthorizationTests {
1146 Ok(exists) => exists, 1149 Ok(exists) => exists,
1147 Err(e) => { 1150 Err(e) => {
1148 let _ = fs::remove_dir_all(&clone_path); 1151 let _ = fs::remove_dir_all(&clone_path);
1149 return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); 1152 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1153 .fail(&e);
1150 } 1154 }
1151 }; 1155 };
1152 1156
@@ -1154,13 +1158,13 @@ impl PushAuthorizationTests {
1154 1158
1155 // Ref should be deleted since the pushed commit doesn't match the PR event's `c` tag 1159 // Ref should be deleted since the pushed commit doesn't match the PR event's `c` tag
1156 if refs_exist { 1160 if refs_exist {
1157 TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(format!( 1161 TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).fail(format!(
1158 "Expected refs/nostr/{} to be deleted when PR event published with non-matching commit, \ 1162 "Expected refs/nostr/{} to be deleted when PR event published with non-matching commit, \
1159 but the ref still exists. The relay should delete refs that don't match the event's `c` tag.", 1163 but the ref still exists. The relay should delete refs that don't match the event's `c` tag.",
1160 pr_event_id 1164 pr_event_id
1161 )) 1165 ))
1162 } else { 1166 } else {
1163 TestResult::new(test_name, "GRASP-01:git-http:40", desc).pass() 1167 TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).pass()
1164 } 1168 }
1165 } 1169 }
1166 1170
@@ -1186,7 +1190,7 @@ impl PushAuthorizationTests {
1186 { 1190 {
1187 Ok(e) => e, 1191 Ok(e) => e,
1188 Err(e) => { 1192 Err(e) => {
1189 return TestResult::new(test_name, "GRASP-01:git-http:40", desc) 1193 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1190 .fail(format!("{}", e)); 1194 .fail(format!("{}", e));
1191 } 1195 }
1192 }; 1196 };
@@ -1194,10 +1198,10 @@ impl PushAuthorizationTests {
1194 let pr_event_id = pr_event.id.to_hex(); 1198 let pr_event_id = pr_event.id.to_hex();
1195 1199
1196 // Get repo info for cloning (fresh clone for this test) 1200 // Get repo info for cloning (fresh clone for this test)
1197 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 1201 let repo = match ctx.get_fixture(FixtureKind::ValidRepoServed).await {
1198 Ok(r) => r, 1202 Ok(r) => r,
1199 Err(e) => { 1203 Err(e) => {
1200 return TestResult::new(test_name, "GRASP-01:git-http:40", desc) 1204 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1201 .fail(format!("{}", e)); 1205 .fail(format!("{}", e));
1202 } 1206 }
1203 }; 1207 };
@@ -1213,7 +1217,7 @@ impl PushAuthorizationTests {
1213 let owner_npub = match repo.pubkey.to_bech32() { 1217 let owner_npub = match repo.pubkey.to_bech32() {
1214 Ok(n) => n, 1218 Ok(n) => n,
1215 Err(e) => { 1219 Err(e) => {
1216 return TestResult::new(test_name, "GRASP-01:git-http:40", desc) 1220 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1217 .fail(format!("Failed to get owner npub: {}", e)); 1221 .fail(format!("Failed to get owner npub: {}", e));
1218 } 1222 }
1219 }; 1223 };
@@ -1222,15 +1226,16 @@ impl PushAuthorizationTests {
1222 let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { 1226 let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) {
1223 Ok(p) => p, 1227 Ok(p) => p,
1224 Err(e) => { 1228 Err(e) => {
1225 return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); 1229 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1230 .fail(&e);
1226 } 1231 }
1227 }; 1232 };
1228 1233
1229 // Create a wrong commit (Owner variant, not PRTestCommit) 1234 // Create a wrong commit (unique, not PRTestCommit) - use create_commit so it always
1230 if let Err(e) = create_deterministic_commit_with_variant(&clone_path, CommitVariant::Owner) 1235 // succeeds even when the clone already has the Owner deterministic content on disk.
1231 { 1236 if let Err(e) = create_commit(&clone_path, "wrong commit - not the PR test commit") {
1232 let _ = fs::remove_dir_all(&clone_path); 1237 let _ = fs::remove_dir_all(&clone_path);
1233 return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); 1238 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).fail(&e);
1234 } 1239 }
1235 1240
1236 // Try to push with wrong commit (should be rejected since PR event exists) 1241 // Try to push with wrong commit (should be rejected since PR event exists)
@@ -1238,7 +1243,8 @@ impl PushAuthorizationTests {
1238 Ok(success) => success, 1243 Ok(success) => success,
1239 Err(e) => { 1244 Err(e) => {
1240 let _ = fs::remove_dir_all(&clone_path); 1245 let _ = fs::remove_dir_all(&clone_path);
1241 return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); 1246 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1247 .fail(&e);
1242 } 1248 }
1243 }; 1249 };
1244 1250
@@ -1246,11 +1252,11 @@ impl PushAuthorizationTests {
1246 1252
1247 // Should REJECT - PR event exists with different commit hash 1253 // Should REJECT - PR event exists with different commit hash
1248 if push_succeeded { 1254 if push_succeeded {
1249 return TestResult::new(test_name, "GRASP-01:git-http:40", desc) 1255 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1250 .fail("Push accepted (expected rejection due to commit hash mismatch)"); 1256 .fail("Push accepted (expected rejection due to commit hash mismatch)");
1251 } 1257 }
1252 1258
1253 TestResult::new(test_name, "GRASP-01:git-http:40", desc).pass() 1259 TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).pass()
1254 } 1260 }
1255 1261
1256 /// Test 4: Push correct commit to refs/nostr/<pr-event-id> AFTER PR event exists 1262 /// Test 4: Push correct commit to refs/nostr/<pr-event-id> AFTER PR event exists
@@ -1275,7 +1281,7 @@ impl PushAuthorizationTests {
1275 { 1281 {
1276 Ok(e) => e, 1282 Ok(e) => e,
1277 Err(e) => { 1283 Err(e) => {
1278 return TestResult::new(test_name, "GRASP-01:git-http:40", desc) 1284 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1279 .fail(format!("{}", e)); 1285 .fail(format!("{}", e));
1280 } 1286 }
1281 }; 1287 };
@@ -1283,10 +1289,10 @@ impl PushAuthorizationTests {
1283 let pr_event_id = pr_event.id.to_hex(); 1289 let pr_event_id = pr_event.id.to_hex();
1284 1290
1285 // Get repo info for cloning (fresh clone for this test) 1291 // Get repo info for cloning (fresh clone for this test)
1286 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 1292 let repo = match ctx.get_fixture(FixtureKind::ValidRepoServed).await {
1287 Ok(r) => r, 1293 Ok(r) => r,
1288 Err(e) => { 1294 Err(e) => {
1289 return TestResult::new(test_name, "GRASP-01:git-http:40", desc) 1295 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1290 .fail(format!("{}", e)); 1296 .fail(format!("{}", e));
1291 } 1297 }
1292 }; 1298 };
@@ -1302,7 +1308,7 @@ impl PushAuthorizationTests {
1302 let owner_npub = match repo.pubkey.to_bech32() { 1308 let owner_npub = match repo.pubkey.to_bech32() {
1303 Ok(n) => n, 1309 Ok(n) => n,
1304 Err(e) => { 1310 Err(e) => {
1305 return TestResult::new(test_name, "GRASP-01:git-http:40", desc) 1311 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1306 .fail(format!("Failed to get owner npub: {}", e)); 1312 .fail(format!("Failed to get owner npub: {}", e));
1307 } 1313 }
1308 }; 1314 };
@@ -1311,26 +1317,27 @@ impl PushAuthorizationTests {
1311 let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { 1317 let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) {
1312 Ok(p) => p, 1318 Ok(p) => p,
1313 Err(e) => { 1319 Err(e) => {
1314 return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); 1320 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1321 .fail(&e);
1315 } 1322 }
1316 }; 1323 };
1317 1324
1318 // Create the CORRECT PR test commit (the one expected by PR event) 1325 // Create the CORRECT PR test commit (the one expected by PR event)
1319 if let Err(e) = reset_to_correct_pr_commit(&clone_path) { 1326 if let Err(e) = reset_to_correct_pr_commit(&clone_path) {
1320 let _ = fs::remove_dir_all(&clone_path); 1327 let _ = fs::remove_dir_all(&clone_path);
1321 return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); 1328 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).fail(&e);
1322 } 1329 }
1323 1330
1324 // Check event is not yet served by relay (still in purgatory) 1331 // Check event is not yet served by relay (still in purgatory)
1325 match client.is_event_on_relay(pr_event.id).await { 1332 match client.is_event_on_relay(pr_event.id).await {
1326 Ok(on_relay) => { 1333 Ok(on_relay) => {
1327 if on_relay { 1334 if on_relay {
1328 return TestResult::new(test_name, "GRASP-01:git-http:40", desc) 1335 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1329 .fail("PR event not in purgatory before correct commit pushed to refs/nostr/<event-id> (the relay serve the PR event)"); 1336 .fail("PR event not in purgatory before correct commit pushed to refs/nostr/<event-id> (the relay serve the PR event)");
1330 } 1337 }
1331 } 1338 }
1332 Err(_) => { 1339 Err(_) => {
1333 return TestResult::new(test_name, "GRASP-01:git-http:40", desc) 1340 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1334 .fail("failed to query relay"); 1341 .fail("failed to query relay");
1335 } 1342 }
1336 } 1343 }
@@ -1340,7 +1347,8 @@ impl PushAuthorizationTests {
1340 Ok(success) => success, 1347 Ok(success) => success,
1341 Err(e) => { 1348 Err(e) => {
1342 let _ = fs::remove_dir_all(&clone_path); 1349 let _ = fs::remove_dir_all(&clone_path);
1343 return TestResult::new(test_name, "GRASP-01:git-http:40", desc).fail(&e); 1350 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1351 .fail(&e);
1344 } 1352 }
1345 }; 1353 };
1346 1354
@@ -1348,7 +1356,7 @@ impl PushAuthorizationTests {
1348 1356
1349 // Should ACCEPT - commit matches PR event's c tag 1357 // Should ACCEPT - commit matches PR event's c tag
1350 if !push_succeeded { 1358 if !push_succeeded {
1351 return TestResult::new(test_name, "GRASP-01:git-http:40", desc) 1359 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1352 .fail("Push rejected (expected acceptance since commit matches PR event)"); 1360 .fail("Push rejected (expected acceptance since commit matches PR event)");
1353 } 1361 }
1354 1362
@@ -1361,17 +1369,17 @@ impl PushAuthorizationTests {
1361 match client.is_event_on_relay(pr_event.id).await { 1369 match client.is_event_on_relay(pr_event.id).await {
1362 Ok(on_relay) => { 1370 Ok(on_relay) => {
1363 if !on_relay { 1371 if !on_relay {
1364 return TestResult::new(test_name, "GRASP-01:git-http:40", desc) 1372 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1365 .fail("PR event not served after correct commit at refs/nostr/<event-id>"); 1373 .fail("PR event not served after correct commit at refs/nostr/<event-id>");
1366 } 1374 }
1367 } 1375 }
1368 Err(_) => { 1376 Err(_) => {
1369 return TestResult::new(test_name, "GRASP-01:git-http:40", desc) 1377 return TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc)
1370 .fail("failed to query relay"); 1378 .fail("failed to query relay");
1371 } 1379 }
1372 } 1380 }
1373 1381
1374 TestResult::new(test_name, "GRASP-01:git-http:40", desc).pass() 1382 TestResult::new(test_name, SpecRef::GitAcceptRefsNostrEventId, desc).pass()
1375 } 1383 }
1376 1384
1377 /// Test that HEAD is set after a state event is published with an existing commit 1385 /// Test that HEAD is set after a state event is published with an existing commit
@@ -1408,20 +1416,19 @@ impl PushAuthorizationTests {
1408 { 1416 {
1409 Ok(e) => e, 1417 Ok(e) => e,
1410 Err(e) => { 1418 Err(e) => {
1411 return TestResult::new(test_name, "GRASP-01:git-http:38", desc).fail(format!( 1419 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).fail(
1412 "Failed to create HeadSetToDevelopBranch fixture: {}", 1420 format!("Failed to create HeadSetToDevelopBranch fixture: {}", e),
1413 e 1421 );
1414 ));
1415 } 1422 }
1416 }; 1423 };
1417 1424
1418 // ============================================================ 1425 // ============================================================
1419 // Step 2: Extract repo_id and owner npub from ValidRepo (cached by fixture) 1426 // Step 2: Extract repo_id and owner npub from ValidRepo (cached by fixture)
1420 // ============================================================ 1427 // ============================================================
1421 let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 1428 let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await {
1422 Ok(e) => e, 1429 Ok(e) => e,
1423 Err(e) => { 1430 Err(e) => {
1424 return TestResult::new(test_name, "GRASP-01:git-http:38", desc) 1431 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc)
1425 .fail(format!("Failed to get ValidRepo fixture: {}", e)); 1432 .fail(format!("Failed to get ValidRepo fixture: {}", e));
1426 } 1433 }
1427 }; 1434 };
@@ -1434,7 +1441,7 @@ impl PushAuthorizationTests {
1434 { 1441 {
1435 Some(id) => id.to_string(), 1442 Some(id) => id.to_string(),
1436 None => { 1443 None => {
1437 return TestResult::new(test_name, "GRASP-01:git-http:38", desc) 1444 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc)
1438 .fail("Missing repo_id in ValidRepo"); 1445 .fail("Missing repo_id in ValidRepo");
1439 } 1446 }
1440 }; 1447 };
@@ -1442,7 +1449,7 @@ impl PushAuthorizationTests {
1442 let npub = match valid_repo.pubkey.to_bech32() { 1449 let npub = match valid_repo.pubkey.to_bech32() {
1443 Ok(n) => n, 1450 Ok(n) => n,
1444 Err(e) => { 1451 Err(e) => {
1445 return TestResult::new(test_name, "GRASP-01:git-http:38", desc) 1452 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc)
1446 .fail(format!("Failed to convert pubkey to bech32: {}", e)); 1453 .fail(format!("Failed to convert pubkey to bech32: {}", e));
1447 } 1454 }
1448 }; 1455 };
@@ -1454,16 +1461,16 @@ impl PushAuthorizationTests {
1454 match get_default_branch_from_info_refs(relay_domain, &npub, &repo_id).await { 1461 match get_default_branch_from_info_refs(relay_domain, &npub, &repo_id).await {
1455 Ok(branch) => branch, 1462 Ok(branch) => branch,
1456 Err(e) => { 1463 Err(e) => {
1457 return TestResult::new(test_name, "GRASP-01:git-http:38", desc) 1464 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc)
1458 .fail(format!("Failed to get default branch: {}", e)); 1465 .fail(format!("Failed to get default branch: {}", e));
1459 } 1466 }
1460 }; 1467 };
1461 1468
1462 // Verify HEAD points to refs/heads/develop 1469 // Verify HEAD points to refs/heads/develop
1463 if default_branch == "refs/heads/develop" { 1470 if default_branch == "refs/heads/develop" {
1464 TestResult::new(test_name, "GRASP-01:git-http:38", desc).pass() 1471 TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).pass()
1465 } else { 1472 } else {
1466 TestResult::new(test_name, "GRASP-01:git-http:38", desc).fail(format!( 1473 TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).fail(format!(
1467 "Expected HEAD to point to 'refs/heads/develop' but got '{}'. \ 1474 "Expected HEAD to point to 'refs/heads/develop' but got '{}'. \
1468 GRASP-01 requires: 'MUST set repository HEAD per repository state announcement \ 1475 GRASP-01 requires: 'MUST set repository HEAD per repository state announcement \
1469 as soon as the git data related to that branch has been received.'", 1476 as soon as the git data related to that branch has been received.'",
@@ -1512,20 +1519,19 @@ impl PushAuthorizationTests {
1512 let _develop_state = match ctx.get_fixture(FixtureKind::HeadSetToDevelopBranch).await { 1519 let _develop_state = match ctx.get_fixture(FixtureKind::HeadSetToDevelopBranch).await {
1513 Ok(e) => e, 1520 Ok(e) => e,
1514 Err(e) => { 1521 Err(e) => {
1515 return TestResult::new(test_name, "GRASP-01:git-http:38", desc).fail(format!( 1522 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).fail(
1516 "Failed to create HeadSetToDevelopBranch fixture: {}", 1523 format!("Failed to create HeadSetToDevelopBranch fixture: {}", e),
1517 e 1524 );
1518 ));
1519 } 1525 }
1520 }; 1526 };
1521 1527
1522 // ============================================================ 1528 // ============================================================
1523 // Step 2: Extract repo_id and owner npub from ValidRepo (cached by fixture) 1529 // Step 2: Extract repo_id and owner npub from ValidRepo (cached by fixture)
1524 // ============================================================ 1530 // ============================================================
1525 let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 1531 let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await {
1526 Ok(e) => e, 1532 Ok(e) => e,
1527 Err(e) => { 1533 Err(e) => {
1528 return TestResult::new(test_name, "GRASP-01:git-http:38", desc) 1534 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc)
1529 .fail(format!("Failed to get ValidRepo fixture: {}", e)); 1535 .fail(format!("Failed to get ValidRepo fixture: {}", e));
1530 } 1536 }
1531 }; 1537 };
@@ -1538,7 +1544,7 @@ impl PushAuthorizationTests {
1538 { 1544 {
1539 Some(id) => id.to_string(), 1545 Some(id) => id.to_string(),
1540 None => { 1546 None => {
1541 return TestResult::new(test_name, "GRASP-01:git-http:38", desc) 1547 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc)
1542 .fail("Missing repo_id in ValidRepo"); 1548 .fail("Missing repo_id in ValidRepo");
1543 } 1549 }
1544 }; 1550 };
@@ -1546,7 +1552,7 @@ impl PushAuthorizationTests {
1546 let npub = match valid_repo.pubkey.to_bech32() { 1552 let npub = match valid_repo.pubkey.to_bech32() {
1547 Ok(n) => n, 1553 Ok(n) => n,
1548 Err(e) => { 1554 Err(e) => {
1549 return TestResult::new(test_name, "GRASP-01:git-http:38", desc) 1555 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc)
1550 .fail(format!("Failed to convert pubkey to bech32: {}", e)); 1556 .fail(format!("Failed to convert pubkey to bech32: {}", e));
1551 } 1557 }
1552 }; 1558 };
@@ -1557,7 +1563,7 @@ impl PushAuthorizationTests {
1557 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { 1563 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
1558 Ok(path) => path, 1564 Ok(path) => path,
1559 Err(e) => { 1565 Err(e) => {
1560 return TestResult::new(test_name, "GRASP-01:git-http:38", desc) 1566 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc)
1561 .fail(format!("Failed to clone repo: {}", e)); 1567 .fail(format!("Failed to clone repo: {}", e));
1562 } 1568 }
1563 }; 1569 };
@@ -1572,7 +1578,7 @@ impl PushAuthorizationTests {
1572 1578
1573 if let Err(e) = output { 1579 if let Err(e) = output {
1574 let _ = fs::remove_dir_all(&clone_path); 1580 let _ = fs::remove_dir_all(&clone_path);
1575 return TestResult::new(test_name, "GRASP-01:git-http:38", desc) 1581 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc)
1576 .fail(format!("Failed to create develop1 branch: {}", e)); 1582 .fail(format!("Failed to create develop1 branch: {}", e));
1577 } 1583 }
1578 1584
@@ -1581,7 +1587,7 @@ impl PushAuthorizationTests {
1581 Ok(hash) => hash, 1587 Ok(hash) => hash,
1582 Err(e) => { 1588 Err(e) => {
1583 let _ = fs::remove_dir_all(&clone_path); 1589 let _ = fs::remove_dir_all(&clone_path);
1584 return TestResult::new(test_name, "GRASP-01:git-http:38", desc) 1590 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc)
1585 .fail(format!("Failed to create commit: {}", e)); 1591 .fail(format!("Failed to create commit: {}", e));
1586 } 1592 }
1587 }; 1593 };
@@ -1610,7 +1616,7 @@ impl PushAuthorizationTests {
1610 Ok(e) => e, 1616 Ok(e) => e,
1611 Err(e) => { 1617 Err(e) => {
1612 let _ = fs::remove_dir_all(&clone_path); 1618 let _ = fs::remove_dir_all(&clone_path);
1613 return TestResult::new(test_name, "GRASP-01:git-http:38", desc) 1619 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc)
1614 .fail(format!("Failed to build state event: {}", e)); 1620 .fail(format!("Failed to build state event: {}", e));
1615 } 1621 }
1616 }; 1622 };
@@ -1621,7 +1627,7 @@ impl PushAuthorizationTests {
1621 .await 1627 .await
1622 { 1628 {
1623 let _ = fs::remove_dir_all(&clone_path); 1629 let _ = fs::remove_dir_all(&clone_path);
1624 return TestResult::new(test_name, "GRASP-01:git-http:38", desc) 1630 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc)
1625 .fail(format!("Failed to send state event: {}", e)); 1631 .fail(format!("Failed to send state event: {}", e));
1626 } 1632 }
1627 1633
@@ -1634,11 +1640,11 @@ impl PushAuthorizationTests {
1634 match push_result { 1640 match push_result {
1635 Ok(true) => { /* Push succeeded, continue to verify */ } 1641 Ok(true) => { /* Push succeeded, continue to verify */ }
1636 Ok(false) => { 1642 Ok(false) => {
1637 return TestResult::new(test_name, "GRASP-01:git-http:38", desc) 1643 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc)
1638 .fail("Push to refs/heads/develop1 was rejected"); 1644 .fail("Push to refs/heads/develop1 was rejected");
1639 } 1645 }
1640 Err(e) => { 1646 Err(e) => {
1641 return TestResult::new(test_name, "GRASP-01:git-http:38", desc) 1647 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc)
1642 .fail(format!("Failed to push develop1 branch: {}", e)); 1648 .fail(format!("Failed to push develop1 branch: {}", e));
1643 } 1649 }
1644 } 1650 }
@@ -1651,16 +1657,16 @@ impl PushAuthorizationTests {
1651 match get_default_branch_from_info_refs(relay_domain, &npub, &repo_id).await { 1657 match get_default_branch_from_info_refs(relay_domain, &npub, &repo_id).await {
1652 Ok(branch) => branch, 1658 Ok(branch) => branch,
1653 Err(e) => { 1659 Err(e) => {
1654 return TestResult::new(test_name, "GRASP-01:git-http:38", desc) 1660 return TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc)
1655 .fail(format!("Failed to get default branch: {}", e)); 1661 .fail(format!("Failed to get default branch: {}", e));
1656 } 1662 }
1657 }; 1663 };
1658 1664
1659 // Verify HEAD points to refs/heads/develop1 1665 // Verify HEAD points to refs/heads/develop1
1660 if default_branch == "refs/heads/develop1" { 1666 if default_branch == "refs/heads/develop1" {
1661 TestResult::new(test_name, "GRASP-01:git-http:38", desc).pass() 1667 TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).pass()
1662 } else { 1668 } else {
1663 TestResult::new(test_name, "GRASP-01:git-http:38", desc).fail(format!( 1669 TestResult::new(test_name, SpecRef::GitSetHeadOnReceive, desc).fail(format!(
1664 "Expected HEAD to point to 'refs/heads/develop1' but got '{}'. \ 1670 "Expected HEAD to point to 'refs/heads/develop1' but got '{}'. \
1665 GRASP-01 requires: 'MUST set repository HEAD per repository state announcement \ 1671 GRASP-01 requires: 'MUST set repository HEAD per repository state announcement \
1666 as soon as the git data related to that branch has been received.'", 1672 as soon as the git data related to that branch has been received.'",
@@ -1701,24 +1707,24 @@ mod tests {
1701 String::from_utf8_lossy(&output.stderr) 1707 String::from_utf8_lossy(&output.stderr)
1702 ); 1708 );
1703 1709
1704 // Configure git user - use PR Test Author identity 1710 // Configure git user - use same identity as clone_repo in fixtures.rs
1705 let output = Command::new("git") 1711 let output = Command::new("git")
1706 .args(["config", "user.email", "pr-test@example.com"]) 1712 .args(["config", "user.email", "test@grasp-audit.local"])
1707 .current_dir(path) 1713 .current_dir(path)
1708 .output() 1714 .output()
1709 .expect("git config email failed"); 1715 .expect("git config email failed");
1710 assert!(output.status.success(), "git config email failed"); 1716 assert!(output.status.success(), "git config email failed");
1711 1717
1712 let output = Command::new("git") 1718 let output = Command::new("git")
1713 .args(["config", "user.name", "PR Test Author"]) 1719 .args(["config", "user.name", "GRASP Audit Test"])
1714 .current_dir(path) 1720 .current_dir(path)
1715 .output() 1721 .output()
1716 .expect("git config name failed"); 1722 .expect("git config name failed");
1717 assert!(output.status.success(), "git config name failed"); 1723 assert!(output.status.success(), "git config name failed");
1718 1724
1719 // Create the deterministic file content 1725 // Create the deterministic file content (must match CommitVariant::PRTestCommit exactly)
1720 let test_file = path.join("test.txt"); 1726 let test_file = path.join("test.txt");
1721 fs::write(&test_file, "PR test deterministic commit").expect("Failed to write test file"); 1727 fs::write(&test_file, "PR test deterministic commit\n").expect("Failed to write test file");
1722 1728
1723 // Add the file 1729 // Add the file
1724 let output = Command::new("git") 1730 let output = Command::new("git")
diff --git a/grasp-audit/src/specs/grasp01/repository_creation.rs b/grasp-audit/src/specs/grasp01/repository_creation.rs
index 2eddb97..5730f1c 100644
--- a/grasp-audit/src/specs/grasp01/repository_creation.rs
+++ b/grasp-audit/src/specs/grasp01/repository_creation.rs
@@ -15,6 +15,7 @@
15//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test 15//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test
16//! ``` 16//! ```
17 17
18use crate::specs::grasp01::SpecRef;
18use crate::{AuditClient, FixtureKind, TestContext, TestResult}; 19use crate::{AuditClient, FixtureKind, TestContext, TestResult};
19use nostr_sdk::prelude::*; 20use nostr_sdk::prelude::*;
20 21
@@ -50,12 +51,12 @@ impl RepositoryCreationTests {
50 let ctx = TestContext::new(client); 51 let ctx = TestContext::new(client);
51 52
52 // Use TestContext to create and send repository announcement 53 // Use TestContext to create and send repository announcement
53 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 54 let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await {
54 Ok(r) => r, 55 Ok(r) => r,
55 Err(e) => { 56 Err(e) => {
56 return TestResult::new( 57 return TestResult::new(
57 test_name, 58 test_name,
58 "GRASP-01:git-http:34", 59 SpecRef::GitServeRepository,
59 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", 60 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
60 ) 61 )
61 .fail(format!("Failed to create repo fixture: {}", e)) 62 .fail(format!("Failed to create repo fixture: {}", e))
@@ -76,7 +77,7 @@ impl RepositoryCreationTests {
76 None => { 77 None => {
77 return TestResult::new( 78 return TestResult::new(
78 test_name, 79 test_name,
79 "GRASP-01:git-http:34", 80 SpecRef::GitServeRepository,
80 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", 81 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
81 ) 82 )
82 .fail("Repository announcement missing d tag") 83 .fail("Repository announcement missing d tag")
@@ -88,7 +89,7 @@ impl RepositoryCreationTests {
88 Err(e) => { 89 Err(e) => {
89 return TestResult::new( 90 return TestResult::new(
90 test_name, 91 test_name,
91 "GRASP-01:git-http:34", 92 SpecRef::GitServeRepository,
92 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", 93 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
93 ) 94 )
94 .fail(format!("Failed to convert pubkey to npub: {}", e)) 95 .fail(format!("Failed to convert pubkey to npub: {}", e))
@@ -99,7 +100,7 @@ impl RepositoryCreationTests {
99 if let Err(e) = check_repo_accessible_via_http(relay_domain, &npub, &repo_id).await { 100 if let Err(e) = check_repo_accessible_via_http(relay_domain, &npub, &repo_id).await {
100 return TestResult::new( 101 return TestResult::new(
101 test_name, 102 test_name,
102 "GRASP-01:git-http:34", 103 SpecRef::GitServeRepository,
103 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", 104 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
104 ) 105 )
105 .fail(format!("Repository not accessible via HTTP: {}", e)); 106 .fail(format!("Repository not accessible via HTTP: {}", e));
@@ -107,7 +108,7 @@ impl RepositoryCreationTests {
107 108
108 TestResult::new( 109 TestResult::new(
109 test_name, 110 test_name,
110 "GRASP-01:git-http:34", 111 SpecRef::GitServeRepository,
111 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", 112 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
112 ) 113 )
113 .pass() 114 .pass()
@@ -130,12 +131,12 @@ impl RepositoryCreationTests {
130 let ctx = TestContext::new(client); 131 let ctx = TestContext::new(client);
131 132
132 // Create a repository announcement 133 // Create a repository announcement
133 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 134 let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await {
134 Ok(r) => r, 135 Ok(r) => r,
135 Err(e) => { 136 Err(e) => {
136 return TestResult::new( 137 return TestResult::new(
137 test_name, 138 test_name,
138 "GRASP-01:git-http:44", 139 SpecRef::GitServeWebpage,
139 "Relay SHOULD serve a webpage for existing repositories", 140 "Relay SHOULD serve a webpage for existing repositories",
140 ) 141 )
141 .fail(format!("Failed to create repo fixture: {}", e)) 142 .fail(format!("Failed to create repo fixture: {}", e))
@@ -156,7 +157,7 @@ impl RepositoryCreationTests {
156 None => { 157 None => {
157 return TestResult::new( 158 return TestResult::new(
158 test_name, 159 test_name,
159 "GRASP-01:git-http:44", 160 SpecRef::GitServeWebpage,
160 "Relay SHOULD serve a webpage for existing repositories", 161 "Relay SHOULD serve a webpage for existing repositories",
161 ) 162 )
162 .fail("Repository announcement missing d tag") 163 .fail("Repository announcement missing d tag")
@@ -168,7 +169,7 @@ impl RepositoryCreationTests {
168 Err(e) => { 169 Err(e) => {
169 return TestResult::new( 170 return TestResult::new(
170 test_name, 171 test_name,
171 "GRASP-01:git-http:44", 172 SpecRef::GitServeWebpage,
172 "Relay SHOULD serve a webpage for existing repositories", 173 "Relay SHOULD serve a webpage for existing repositories",
173 ) 174 )
174 .fail(format!("Failed to convert pubkey to npub: {}", e)) 175 .fail(format!("Failed to convert pubkey to npub: {}", e))
@@ -179,7 +180,7 @@ impl RepositoryCreationTests {
179 if let Err(e) = check_webpage_served(relay_domain, &npub, &repo_id).await { 180 if let Err(e) = check_webpage_served(relay_domain, &npub, &repo_id).await {
180 return TestResult::new( 181 return TestResult::new(
181 test_name, 182 test_name,
182 "GRASP-01:git-http:44", 183 SpecRef::GitServeWebpage,
183 "Relay SHOULD serve a webpage for existing repositories", 184 "Relay SHOULD serve a webpage for existing repositories",
184 ) 185 )
185 .fail(format!("Webpage not served: {}", e)); 186 .fail(format!("Webpage not served: {}", e));
@@ -187,7 +188,7 @@ impl RepositoryCreationTests {
187 188
188 TestResult::new( 189 TestResult::new(
189 test_name, 190 test_name,
190 "GRASP-01:git-http:44", 191 SpecRef::GitServeWebpage,
191 "Relay SHOULD serve a webpage for existing repositories", 192 "Relay SHOULD serve a webpage for existing repositories",
192 ) 193 )
193 .pass() 194 .pass()
@@ -209,12 +210,12 @@ impl RepositoryCreationTests {
209 210
210 let ctx = TestContext::new(client); 211 let ctx = TestContext::new(client);
211 212
212 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 213 let repo = match ctx.get_fixture(FixtureKind::ValidRepoSent).await {
213 Ok(r) => r, 214 Ok(r) => r,
214 Err(e) => { 215 Err(e) => {
215 return TestResult::new( 216 return TestResult::new(
216 test_name, 217 test_name,
217 "GRASP-01:git-http:44", 218 SpecRef::GitServeWebpage,
218 "Relay SHOULD return 404 for repositories it doesn't host", 219 "Relay SHOULD return 404 for repositories it doesn't host",
219 ) 220 )
220 .fail(format!("Failed to create repo fixture: {}", e)) 221 .fail(format!("Failed to create repo fixture: {}", e))
@@ -226,7 +227,7 @@ impl RepositoryCreationTests {
226 Err(e) => { 227 Err(e) => {
227 return TestResult::new( 228 return TestResult::new(
228 test_name, 229 test_name,
229 "GRASP-01:git-http:44", 230 SpecRef::GitServeWebpage,
230 "Relay SHOULD return 404 for repositories it doesn't host", 231 "Relay SHOULD return 404 for repositories it doesn't host",
231 ) 232 )
232 .fail(format!("Failed to convert pubkey to npub: {}", e)) 233 .fail(format!("Failed to convert pubkey to npub: {}", e))
@@ -239,7 +240,7 @@ impl RepositoryCreationTests {
239 if let Err(e) = check_404_for_nonexistent_repo(relay_domain, &npub, fake_repo_id).await { 240 if let Err(e) = check_404_for_nonexistent_repo(relay_domain, &npub, fake_repo_id).await {
240 return TestResult::new( 241 return TestResult::new(
241 test_name, 242 test_name,
242 "GRASP-01:git-http:44", 243 SpecRef::GitServeWebpage,
243 "Relay SHOULD return 404 for repositories it doesn't host", 244 "Relay SHOULD return 404 for repositories it doesn't host",
244 ) 245 )
245 .fail(format!("Expected 404, got: {}", e)); 246 .fail(format!("Expected 404, got: {}", e));
@@ -247,7 +248,7 @@ impl RepositoryCreationTests {
247 248
248 TestResult::new( 249 TestResult::new(
249 test_name, 250 test_name,
250 "GRASP-01:git-http:44", 251 SpecRef::GitServeWebpage,
251 "Relay SHOULD return 404 for repositories it doesn't host", 252 "Relay SHOULD return 404 for repositories it doesn't host",
252 ) 253 )
253 .pass() 254 .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 @@
6/// GRASP spec repository commit ID that this version is based on 6/// GRASP spec repository commit ID that this version is based on
7pub const GRASP_COMMIT_ID: &str = "1fdb8f7"; 7pub const GRASP_COMMIT_ID: &str = "1fdb8f7";
8 8
9/// Reference to a specific GRASP-01 specification requirement
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum SpecRef {
12 NostrRelayNip01Compliant,
13 NostrRelayRejectMissingCloneRelays,
14 NostrRelayMayRejectOtherCriteria,
15 NostrRelayMustAcceptTaggedEvents,
16 NostrRelayMayRejectSpamCuration,
17 PurgatoryAcceptUntilGitData,
18 Nip11ServeDocument,
19 Nip11ListSupportedGrasps,
20 Nip11ListRepoAcceptanceCriteria,
21 Nip11ListCurationPolicy,
22 GitServeRepository,
23 GitAcceptPushesAlignState,
24 GitSetHeadOnReceive,
25 GitAcceptRefsNostrEventId,
26 GitIncludeAllowSha1InWant,
27 GitServeWebpage,
28 CorsAllowOrigin,
29 CorsAllowMethods,
30 CorsAllowHeaders,
31 CorsOptionsResponse,
32}
33
9/// A single specification requirement 34/// A single specification requirement
10#[derive(Debug, Clone)] 35#[derive(Debug, Clone)]
11pub struct SpecRequirement { 36pub struct SpecRequirement {
37 /// Unique reference to this requirement
38 pub spec_ref: SpecRef,
12 /// Line number in the spec document 39 /// Line number in the spec document
13 pub line: u32, 40 pub line: u32,
14 /// Section name (e.g., "Nostr Relay", "Git Smart HTTP Service", "CORS Support") 41 /// Section name (e.g., "Nostr Relay", "Git Smart HTTP Service", "CORS Support")
@@ -37,121 +64,175 @@ impl std::fmt::Display for RequirementLevel {
37 } 64 }
38} 65}
39 66
67impl SpecRef {
68 /// Get the spec reference string in format "GRASP-01:section:line"
69 pub fn spec_ref_string(self) -> &'static str {
70 match self {
71 SpecRef::NostrRelayNip01Compliant => "GRASP-01:nostr-relay:7",
72 SpecRef::NostrRelayRejectMissingCloneRelays => "GRASP-01:nostr-relay:9",
73 SpecRef::NostrRelayMayRejectOtherCriteria => "GRASP-01:nostr-relay:11",
74 SpecRef::NostrRelayMustAcceptTaggedEvents => "GRASP-01:nostr-relay:13",
75 SpecRef::NostrRelayMayRejectSpamCuration => "GRASP-01:nostr-relay:18",
76 SpecRef::PurgatoryAcceptUntilGitData => "GRASP-01:purgatory:22",
77 SpecRef::Nip11ServeDocument => "GRASP-01:nip-11:26",
78 SpecRef::Nip11ListSupportedGrasps => "GRASP-01:nip-11:28",
79 SpecRef::Nip11ListRepoAcceptanceCriteria => "GRASP-01:nip-11:29",
80 SpecRef::Nip11ListCurationPolicy => "GRASP-01:nip-11:30",
81 SpecRef::GitServeRepository => "GRASP-01:git-http:34",
82 SpecRef::GitAcceptPushesAlignState => "GRASP-01:git-http:36",
83 SpecRef::GitSetHeadOnReceive => "GRASP-01:git-http:39",
84 SpecRef::GitAcceptRefsNostrEventId => "GRASP-01:git-http:45",
85 SpecRef::GitIncludeAllowSha1InWant => "GRASP-01:git-http:56",
86 SpecRef::GitServeWebpage => "GRASP-01:git-http:58",
87 SpecRef::CorsAllowOrigin => "GRASP-01:cors:64",
88 SpecRef::CorsAllowMethods => "GRASP-01:cors:65",
89 SpecRef::CorsAllowHeaders => "GRASP-01:cors:66",
90 SpecRef::CorsOptionsResponse => "GRASP-01:cors:67",
91 }
92 }
93}
94
40/// All GRASP-01 specification requirements 95/// All GRASP-01 specification requirements
41pub const GRASP_01_REQUIREMENTS: &[SpecRequirement] = &[ 96pub const GRASP_01_REQUIREMENTS: &[SpecRequirement] = &[
42 // Nostr Relay section 97 // Nostr Relay section
43 SpecRequirement { 98 SpecRequirement {
99 spec_ref: SpecRef::NostrRelayNip01Compliant,
44 line: 7, 100 line: 7,
45 section: "Nostr Relay", 101 section: "Nostr Relay",
46 text: "MUST serve a NIP-01 compliant nostr relay at `/` that accepts git repository announcements and their corresponding repo state announcements.", 102 text: "MUST serve a NIP-01 compliant nostr relay at `/` that accepts git repository announcements and their corresponding repo state announcements.",
47 level: RequirementLevel::Must, 103 level: RequirementLevel::Must,
48 }, 104 },
49 SpecRequirement { 105 SpecRequirement {
106 spec_ref: SpecRef::NostrRelayRejectMissingCloneRelays,
50 line: 9, 107 line: 9,
51 section: "Nostr Relay", 108 section: "Nostr Relay",
52 text: "MUST reject git repository announcements that do not list the service in both `clone` and `relays` tags unless implementing `GRASP-05`.", 109 text: "MUST reject git repository announcements that do not list the service in both `clone` and `relays` tags unless implementing `GRASP-05`.",
53 level: RequirementLevel::Must, 110 level: RequirementLevel::Must,
54 }, 111 },
55 SpecRequirement { 112 SpecRequirement {
113 spec_ref: SpecRef::NostrRelayMayRejectOtherCriteria,
56 line: 11, 114 line: 11,
57 section: "Nostr Relay", 115 section: "Nostr Relay",
58 text: "MAY reject git repository announcements based on other criteria such as pre-payment, quotas, WoT, whitelist, SPAM prevention, etc.", 116 text: "MAY reject git repository announcements based on other criteria such as pre-payment, quotas, WoT, whitelist, SPAM prevention, etc.",
59 level: RequirementLevel::May, 117 level: RequirementLevel::May,
60 }, 118 },
61 SpecRequirement { 119 SpecRequirement {
120 spec_ref: SpecRef::NostrRelayMustAcceptTaggedEvents,
62 line: 13, 121 line: 13,
63 section: "Nostr Relay", 122 section: "Nostr Relay",
64 text: "MUST accept other events that tag, or are tagged by, either: 1. accepted git repository announcements; or 2. accepted issues or patches", 123 text: "MUST accept other events that tag, or are tagged by, either: 1. accepted git repository announcements; or 2. accepted issues or patches",
65 level: RequirementLevel::Must, 124 level: RequirementLevel::Must,
66 }, 125 },
67 SpecRequirement { 126 SpecRequirement {
127 spec_ref: SpecRef::NostrRelayMayRejectSpamCuration,
68 line: 18, 128 line: 18,
69 section: "Nostr Relay", 129 section: "Nostr Relay",
70 text: "MAY reject or delete events for generic SPAM prevention reasons or curation eg. WoT, whitelist, user bans and banned topics.", 130 text: "MAY reject or delete events for generic SPAM prevention reasons or curation eg. WoT, whitelist, user bans and banned topics.",
71 level: RequirementLevel::May, 131 level: RequirementLevel::May,
72 }, 132 },
73 SpecRequirement { 133 SpecRequirement {
134 spec_ref: SpecRef::PurgatoryAcceptUntilGitData,
135 line: 22,
136 section: "Purgatory",
137 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.",
138 level: RequirementLevel::Should,
139 },
140 SpecRequirement {
141 spec_ref: SpecRef::Nip11ServeDocument,
74 line: 26, 142 line: 26,
75 section: "Nostr Relay", 143 section: "NIP-11",
76 text: "MUST serve a NIP-11 document", 144 text: "MUST serve a NIP-11 document",
77 level: RequirementLevel::Must, 145 level: RequirementLevel::Must,
78 }, 146 },
79 SpecRequirement { 147 SpecRequirement {
148 spec_ref: SpecRef::Nip11ListSupportedGrasps,
80 line: 28, 149 line: 28,
81 section: "Nostr Relay", 150 section: "NIP-11",
82 text: "MUST list each supported GRASP under `supported_grasps` in format `GRASP-XX` eg `GRASP-01` as a string array", 151 text: "MUST list each supported GRASP under `supported_grasps` in format `GRASP-XX` eg `GRASP-01` as a string array",
83 level: RequirementLevel::Must, 152 level: RequirementLevel::Must,
84 }, 153 },
85 SpecRequirement { 154 SpecRequirement {
155 spec_ref: SpecRef::Nip11ListRepoAcceptanceCriteria,
86 line: 29, 156 line: 29,
87 section: "Nostr Relay", 157 section: "NIP-11",
88 text: "MUST list repository acceptance criteria under `repo_acceptance_criteria` as a human readable string", 158 text: "MUST list repository acceptance criteria under `repo_acceptance_criteria` as a human readable string",
89 level: RequirementLevel::Must, 159 level: RequirementLevel::Must,
90 }, 160 },
91 SpecRequirement { 161 SpecRequirement {
162 spec_ref: SpecRef::Nip11ListCurationPolicy,
92 line: 30, 163 line: 30,
93 section: "Nostr Relay", 164 section: "NIP-11",
94 text: "MUST list brief summary of curation policy under `curation` if events are curated beyond generic SPAM prevention; otherwise `curation` MUST be omitted", 165 text: "MUST list brief summary of curation policy under `curation` if events are curated beyond generic SPAM prevention; otherwise `curation` MUST be omitted",
95 level: RequirementLevel::Must, 166 level: RequirementLevel::Must,
96 }, 167 },
97 // Git Smart HTTP Service section 168 // Git Smart HTTP Service section
98 SpecRequirement { 169 SpecRequirement {
170 spec_ref: SpecRef::GitServeRepository,
99 line: 34, 171 line: 34,
100 section: "Git Smart HTTP Service", 172 section: "Git Smart HTTP Service",
101 text: "MUST serve a git repository via an unauthenticated git smart http service at `/<npub>/<identifier>.git` for each accepted git repository announcement.", 173 text: "MUST serve a git repository via an unauthenticated git smart http service at `/<npub>/<identifier>.git` for each git repository announcement the relay serves or has in purgatory.",
102 level: RequirementLevel::Must, 174 level: RequirementLevel::Must,
103 }, 175 },
104 SpecRequirement { 176 SpecRequirement {
177 spec_ref: SpecRef::GitAcceptPushesAlignState,
105 line: 36, 178 line: 36,
106 section: "Git Smart HTTP Service", 179 section: "Git Smart HTTP Service",
107 text: "MUST accept pushes via this service that match the latest repo state announcement on the relay, respecting the recursive maintainer set.", 180 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.",
108 level: RequirementLevel::Must, 181 level: RequirementLevel::Must,
109 }, 182 },
110 SpecRequirement { 183 SpecRequirement {
111 line: 38, 184 spec_ref: SpecRef::GitSetHeadOnReceive,
185 line: 39,
112 section: "Git Smart HTTP Service", 186 section: "Git Smart HTTP Service",
113 text: "MUST set repository HEAD per repo state announcement as soon as the git data related to that branch has been received.", 187 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.",
114 level: RequirementLevel::Must, 188 level: RequirementLevel::Must,
115 }, 189 },
116 SpecRequirement { 190 SpecRequirement {
117 line: 40, 191 spec_ref: SpecRef::GitAcceptRefsNostrEventId,
192 line: 45,
118 section: "Git Smart HTTP Service", 193 section: "Git Smart HTTP Service",
119 text: "MUST accept pushes via this service to `refs/nostr/<event-id>` 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.", 194 text: "MUST accept pushes via this service to `refs/nostr/<event-id>` 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.",
120 level: RequirementLevel::Must, 195 level: RequirementLevel::Must,
121 }, 196 },
122 SpecRequirement { 197 SpecRequirement {
123 line: 42, 198 spec_ref: SpecRef::GitIncludeAllowSha1InWant,
199 line: 56,
124 section: "Git Smart HTTP Service", 200 section: "Git Smart HTTP Service",
125 text: "MUST include `allow-reachable-sha1-in-want` and `allow-tip-sha1-in-want` in advertisement and serve available oids.", 201 text: "MUST include `allow-reachable-sha1-in-want` and `allow-tip-sha1-in-want` in advertisement and serve available oids.",
126 level: RequirementLevel::Must, 202 level: RequirementLevel::Must,
127 }, 203 },
128 SpecRequirement { 204 SpecRequirement {
129 line: 44, 205 spec_ref: SpecRef::GitServeWebpage,
206 line: 58,
130 section: "Git Smart HTTP Service", 207 section: "Git Smart HTTP Service",
131 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.", 208 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.",
132 level: RequirementLevel::Should, 209 level: RequirementLevel::Should,
133 }, 210 },
134 // CORS Support section 211 // CORS Support section
135 SpecRequirement { 212 SpecRequirement {
136 line: 50, 213 spec_ref: SpecRef::CorsAllowOrigin,
214 line: 64,
137 section: "CORS Support", 215 section: "CORS Support",
138 text: "Set `Access-Control-Allow-Origin: *` on ALL responses", 216 text: "Set `Access-Control-Allow-Origin: *` on ALL responses",
139 level: RequirementLevel::Must, 217 level: RequirementLevel::Must,
140 }, 218 },
141 SpecRequirement { 219 SpecRequirement {
142 line: 51, 220 spec_ref: SpecRef::CorsAllowMethods,
221 line: 65,
143 section: "CORS Support", 222 section: "CORS Support",
144 text: "Set `Access-Control-Allow-Methods: GET, POST` on ALL responses", 223 text: "Set `Access-Control-Allow-Methods: GET, POST` on ALL responses",
145 level: RequirementLevel::Must, 224 level: RequirementLevel::Must,
146 }, 225 },
147 SpecRequirement { 226 SpecRequirement {
148 line: 52, 227 spec_ref: SpecRef::CorsAllowHeaders,
228 line: 66,
149 section: "CORS Support", 229 section: "CORS Support",
150 text: "Set `Access-Control-Allow-Headers: Content-Type` on ALL responses", 230 text: "Set `Access-Control-Allow-Headers: Content-Type` on ALL responses",
151 level: RequirementLevel::Must, 231 level: RequirementLevel::Must,
152 }, 232 },
153 SpecRequirement { 233 SpecRequirement {
154 line: 53, 234 spec_ref: SpecRef::CorsOptionsResponse,
235 line: 67,
155 section: "CORS Support", 236 section: "CORS Support",
156 text: "Respond to OPTIONS requests with 204 No Content", 237 text: "Respond to OPTIONS requests with 204 No Content",
157 level: RequirementLevel::Must, 238 level: RequirementLevel::Must,
@@ -163,6 +244,13 @@ pub fn get_requirement(line: u32) -> Option<&'static SpecRequirement> {
163 GRASP_01_REQUIREMENTS.iter().find(|r| r.line == line) 244 GRASP_01_REQUIREMENTS.iter().find(|r| r.line == line)
164} 245}
165 246
247/// Get a requirement by its SpecRef
248pub fn get_requirement_by_ref(spec_ref: SpecRef) -> Option<&'static SpecRequirement> {
249 GRASP_01_REQUIREMENTS
250 .iter()
251 .find(|r| r.spec_ref == spec_ref)
252}
253
166/// Get all requirements for a section 254/// Get all requirements for a section
167pub fn get_requirements_for_section(section: &str) -> Vec<&'static SpecRequirement> { 255pub fn get_requirements_for_section(section: &str) -> Vec<&'static SpecRequirement> {
168 GRASP_01_REQUIREMENTS 256 GRASP_01_REQUIREMENTS
@@ -194,16 +282,38 @@ mod tests {
194 } 282 }
195 283
196 #[test] 284 #[test]
285 fn test_get_requirement_by_ref() {
286 let req = get_requirement_by_ref(SpecRef::NostrRelayNip01Compliant)
287 .expect("SpecRef should exist");
288 assert_eq!(req.line, 7);
289 assert_eq!(req.spec_ref, SpecRef::NostrRelayNip01Compliant);
290 }
291
292 #[test]
197 fn test_get_sections() { 293 fn test_get_sections() {
198 let sections = get_sections(); 294 let sections = get_sections();
199 assert_eq!(sections.len(), 3); 295 assert_eq!(sections.len(), 5);
200 assert_eq!(sections[0], "Nostr Relay"); 296 assert_eq!(sections[0], "Nostr Relay");
201 assert_eq!(sections[1], "Git Smart HTTP Service"); 297 assert_eq!(sections[1], "Purgatory");
202 assert_eq!(sections[2], "CORS Support"); 298 assert_eq!(sections[2], "NIP-11");
299 assert_eq!(sections[3], "Git Smart HTTP Service");
300 assert_eq!(sections[4], "CORS Support");
203 } 301 }
204 302
205 #[test] 303 #[test]
206 fn test_requirement_count() { 304 fn test_requirement_count() {
207 assert_eq!(GRASP_01_REQUIREMENTS.len(), 19); 305 assert_eq!(GRASP_01_REQUIREMENTS.len(), 20);
306 }
307
308 #[test]
309 fn test_spec_ref_unique() {
310 let mut refs = std::collections::HashSet::new();
311 for req in GRASP_01_REQUIREMENTS {
312 assert!(
313 refs.insert(req.spec_ref),
314 "Duplicate SpecRef found: {:?}",
315 req.spec_ref
316 );
317 }
208 } 318 }
209} 319}
diff --git a/grasp-audit/src/specs/mod.rs b/grasp-audit/src/specs/mod.rs
index bf711fa..ceae684 100644
--- a/grasp-audit/src/specs/mod.rs
+++ b/grasp-audit/src/specs/mod.rs
@@ -7,5 +7,5 @@ pub mod grasp01;
7// Re-export all test structs from grasp01 module 7// Re-export all test structs from grasp01 module
8pub use grasp01::{ 8pub use grasp01::{
9 CorsTests, EventAcceptancePolicyTests, GitCloneTests, GitFilterTests, Nip01SmokeTests, 9 CorsTests, EventAcceptancePolicyTests, GitCloneTests, GitFilterTests, Nip01SmokeTests,
10 Nip11DocumentTests, PushAuthorizationTests, RepositoryCreationTests, 10 Nip11DocumentTests, PurgatoryTests, PushAuthorizationTests, RepositoryCreationTests,
11}; 11};