upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-02 20:54:15 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-02 21:03:24 +0000
commit5c10ca008413744b09136618eaa85275c997704c (patch)
treeaf24387d8916bdec26315a31f67bd99c39544544 /grasp-audit
parentc07954f44f4c03cc17d4a83b144667cbcbb226cf (diff)
grasp-audit: show tests under GRASP-01 line
Diffstat (limited to 'grasp-audit')
-rw-r--r--grasp-audit/src/result.rs188
-rw-r--r--grasp-audit/src/specs/grasp01/event_acceptance_policy.rs57
-rw-r--r--grasp-audit/src/specs/grasp01/git_clone.rs43
-rw-r--r--grasp-audit/src/specs/grasp01/mod.rs5
-rw-r--r--grasp-audit/src/specs/grasp01/nip01_smoke.rs12
-rw-r--r--grasp-audit/src/specs/grasp01/nip11_document.rs16
-rw-r--r--grasp-audit/src/specs/grasp01/push_authorization.rs168
-rw-r--r--grasp-audit/src/specs/grasp01/repository_creation.rs35
-rw-r--r--grasp-audit/src/specs/grasp01/spec_requirements.rs209
9 files changed, 531 insertions, 202 deletions
diff --git a/grasp-audit/src/result.rs b/grasp-audit/src/result.rs
index bc0008a..0de16ae 100644
--- a/grasp-audit/src/result.rs
+++ b/grasp-audit/src/result.rs
@@ -1,35 +1,56 @@
1//! Test result types 1//! Test result types
2 2
3use crate::specs::grasp01::{get_sections, GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID};
3use std::collections::BTreeMap; 4use std::collections::BTreeMap;
4use std::time::{Duration, Instant}; 5use std::time::{Duration, Instant};
5 6
6// ANSI color codes 7// ANSI color codes
7const GREEN: &str = "\x1b[32m"; 8const GREEN: &str = "\x1b[32m";
8const RED: &str = "\x1b[31m"; 9const RED: &str = "\x1b[31m";
10const YELLOW: &str = "\x1b[33m";
11const BLUE: &str = "\x1b[34m";
12const CYAN: &str = "\x1b[36m";
9const RESET: &str = "\x1b[0m"; 13const RESET: &str = "\x1b[0m";
10const BOLD: &str = "\x1b[1m"; 14const BOLD: &str = "\x1b[1m";
11 15
12/// Extract spec category from a spec_ref by removing trailing test number 16/// Parse line number(s) from a spec_ref string
13/// e.g., "GRASP-01:event-acceptance:1.1" -> "GRASP-01:event-acceptance" 17/// Returns a vector of line numbers that this spec_ref covers
14/// e.g., "NIP-01:basic:2" -> "NIP-01:basic" 18///
15fn extract_spec_category(spec_ref: &str) -> String { 19/// Examples:
20/// - "GRASP-01:nostr-relay:7" -> [7]
21/// - "GRASP-01:nostr-relay:7-9" -> [7, 8, 9]
22/// - "NIP-01:basic:2" -> [] (not a GRASP-01 ref)
23fn parse_spec_lines(spec_ref: &str) -> Vec<u32> {
24 // Only parse GRASP-01 refs
25 if !spec_ref.starts_with("GRASP-01:") {
26 return vec![];
27 }
28
29 // Get the last part after the last colon
16 let parts: Vec<&str> = spec_ref.split(':').collect(); 30 let parts: Vec<&str> = spec_ref.split(':').collect();
17 if parts.len() >= 2 { 31 if parts.len() < 3 {
18 // Check if the last part looks like a test number (starts with digit) 32 return vec![];
19 if let Some(last) = parts.last() { 33 }
20 if last 34
21 .chars() 35 let line_part = parts.last().unwrap();
22 .next() 36
23 .map(|c| c.is_ascii_digit()) 37 // Handle range format like "7-9"
24 .unwrap_or(false) 38 if line_part.contains('-') {
25 { 39 let range_parts: Vec<&str> = line_part.split('-').collect();
26 // Remove the trailing number part 40 if range_parts.len() == 2 {
27 return parts[..parts.len() - 1].join(":"); 41 if let (Ok(start), Ok(end)) = (range_parts[0].parse::<u32>(), range_parts[1].parse::<u32>()) {
42 return (start..=end).collect();
28 } 43 }
29 } 44 }
45 return vec![];
30 } 46 }
31 // Return as-is if no trailing number found 47
32 spec_ref.to_string() 48 // Handle single line number
49 if let Ok(line) = line_part.parse::<u32>() {
50 return vec![line];
51 }
52
53 vec![]
33} 54}
34 55
35/// Result of a single test 56/// Result of a single test
@@ -138,53 +159,100 @@ impl AuditResult {
138 self.results.len() 159 self.results.len()
139 } 160 }
140 161
141 /// Print a detailed report with tests grouped by spec_ref 162 /// Print a detailed report aligned to GRASP-01 specification
142 pub fn print_report(&self) { 163 pub fn print_report(&self) {
143 println!("\n{}{}{}", BOLD, self.spec, RESET); 164 println!();
144 println!("{}", "═".repeat(60)); 165 println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET);
145 166 println!("{}GRASP-01 Compliance Report{}", BOLD, RESET);
146 let passed = self.passed_count(); 167 println!("Source: github.com/nostr-protocol/grasp (commit: {})", GRASP_COMMIT_ID);
147 let total = self.total_count(); 168 println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET);
148 169
149 // Group results by spec category (strip trailing test number like ":1.1") 170 // Build a map of spec line -> tests that cover it
150 let mut grouped: BTreeMap<String, Vec<&TestResult>> = BTreeMap::new(); 171 let mut tests_by_line: BTreeMap<u32, Vec<&TestResult>> = BTreeMap::new();
151 for result in &self.results { 172 for result in &self.results {
152 // Extract category from spec_ref (e.g., "GRASP-01:event-acceptance:1.1" -> "GRASP-01:event-acceptance") 173 let lines = parse_spec_lines(&result.spec_ref);
153 let category = extract_spec_category(&result.spec_ref); 174 for line in lines {
154 grouped.entry(category).or_default().push(result); 175 tests_by_line.entry(line).or_default().push(result);
176 }
155 } 177 }
156 178
157 // Print grouped results 179 // Track how many spec requirements have tests
158 for (category, results) in &grouped { 180 let mut tested_requirements = 0;
159 println!("\n{}[{}]{}", BOLD, category, RESET); 181 let total_requirements = GRASP_01_REQUIREMENTS.len();
160 182
161 for result in results { 183 // Print results organized by section and spec line
162 let (color, status) = if result.passed { 184 for section in get_sections() {
163 (GREEN, "✓") 185 println!();
186 println!("{}{}## {}{}", CYAN, BOLD, section, RESET);
187
188 for req in GRASP_01_REQUIREMENTS.iter().filter(|r| r.section == section) {
189 println!();
190 // Print spec requirement in blue
191 println!("{}📘 Line {}: {}{}", BLUE, req.line, req.text, RESET);
192
193 // Get tests for this line
194 if let Some(tests) = tests_by_line.get(&req.line) {
195 tested_requirements += 1;
196 for test in tests {
197 let (color, status) = if test.passed {
198 (GREEN, "✓")
199 } else {
200 (RED, "✗")
201 };
202 println!(" {}{} {}{}", color, status, test.name, RESET);
203
204 if let Some(error) = &test.error {
205 // Truncate long errors
206 let error_display = if error.len() > 100 {
207 format!("{}...", &error[..100])
208 } else {
209 error.clone()
210 };
211 println!(" {}Error: {}{}", RED, error_display, RESET);
212 }
213 }
164 } else { 214 } else {
165 (RED, "✗") 215 println!(" {}⚠️ No Tests Implemented{}", YELLOW, RESET);
166 };
167
168 println!(" {}{} {}{}", color, status, result.name, RESET);
169
170 if let Some(error) = &result.error {
171 println!(" {}Error: {}{}", RED, error, RESET);
172 } 216 }
173 } 217 }
174 } 218 }
175 219
176 println!(); 220 println!();
177 let pass_rate = if total > 0 { 221 println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET);
178 (passed as f64 / total as f64) * 100.0 222
223 // Summary statistics
224 let passed = self.passed_count();
225 let total_tests = self.total_count();
226
227 let spec_coverage = if total_requirements > 0 {
228 (tested_requirements as f64 / total_requirements as f64) * 100.0
229 } else {
230 0.0
231 };
232
233 let pass_rate = if total_tests > 0 {
234 (passed as f64 / total_tests as f64) * 100.0
179 } else { 235 } else {
180 0.0 236 0.0
181 }; 237 };
182 238
183 let summary_color = if passed == total { GREEN } else { RED }; 239 let summary_color = if passed == total_tests && tested_requirements == total_requirements {
240 GREEN
241 } else if passed == total_tests {
242 YELLOW
243 } else {
244 RED
245 };
246
184 println!( 247 println!(
185 "{}Results: {}/{} passed ({:.1}%){}", 248 "{}Spec coverage: {}/{} requirements tested ({:.1}%){}",
186 summary_color, passed, total, pass_rate, RESET 249 summary_color, tested_requirements, total_requirements, spec_coverage, RESET
187 ); 250 );
251 println!(
252 "{}Test results: {}/{} tests passed ({:.1}%){}",
253 summary_color, passed, total_tests, pass_rate, RESET
254 );
255 println!("{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", BOLD, RESET);
188 println!(); 256 println!();
189 } 257 }
190 258
@@ -235,4 +303,28 @@ mod tests {
235 assert_eq!(audit.failed_count(), 1); 303 assert_eq!(audit.failed_count(), 1);
236 assert!(!audit.all_passed()); 304 assert!(!audit.all_passed());
237 } 305 }
238} 306
307 #[test]
308 fn test_parse_spec_lines_single() {
309 assert_eq!(parse_spec_lines("GRASP-01:nostr-relay:7"), vec![7]);
310 assert_eq!(parse_spec_lines("GRASP-01:git-http:28"), vec![28]);
311 }
312
313 #[test]
314 fn test_parse_spec_lines_range() {
315 assert_eq!(parse_spec_lines("GRASP-01:nostr-relay:7-9"), vec![7, 8, 9]);
316 assert_eq!(parse_spec_lines("GRASP-01:cors:44-47"), vec![44, 45, 46, 47]);
317 }
318
319 #[test]
320 fn test_parse_spec_lines_non_grasp() {
321 assert_eq!(parse_spec_lines("NIP-01:basic:1"), Vec::<u32>::new());
322 assert_eq!(parse_spec_lines("OTHER:spec:5"), Vec::<u32>::new());
323 }
324
325 #[test]
326 fn test_parse_spec_lines_invalid() {
327 assert_eq!(parse_spec_lines("GRASP-01:invalid"), Vec::<u32>::new());
328 assert_eq!(parse_spec_lines("GRASP-01:test:abc"), Vec::<u32>::new());
329 }
330} \ No newline at end of file
diff --git a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs
index 1fc7f73..fee51db 100644
--- a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs
+++ b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs
@@ -138,7 +138,7 @@ impl EventAcceptancePolicyTests {
138 138
139 /// Test: Accept valid repository announcements 139 /// Test: Accept valid repository announcements
140 /// 140 ///
141 /// Spec: Lines 3-5 of ../grasp/01.md 141 /// Spec: Line 7 of ../grasp/01.md
142 /// Requirement: MUST accept repo announcements listing service in clone & relays tags 142 /// Requirement: MUST accept repo announcements listing service in clone & relays tags
143 /// 143 ///
144 /// **Using TestContext pattern:** 144 /// **Using TestContext pattern:**
@@ -147,7 +147,7 @@ impl EventAcceptancePolicyTests {
147 pub async fn test_accept_valid_repo_announcement(client: &AuditClient) -> TestResult { 147 pub async fn test_accept_valid_repo_announcement(client: &AuditClient) -> TestResult {
148 TestResult::new( 148 TestResult::new(
149 "accept_valid_repo_announcement", 149 "accept_valid_repo_announcement",
150 "GRASP-01:nostr-relay:3-5", 150 "GRASP-01:nostr-relay:7",
151 "Accept valid repository announcements with service in clone and relays tags", 151 "Accept valid repository announcements with service in clone and relays tags",
152 ) 152 )
153 .run(|| async { 153 .run(|| async {
@@ -245,14 +245,14 @@ impl EventAcceptancePolicyTests {
245 245
246 /// Test: Reject repo announcements not listing service in clone tag 246 /// Test: Reject repo announcements not listing service in clone tag
247 /// 247 ///
248 /// Spec: Line 5 of ../grasp/01.md 248 /// Spec: Line 9 of ../grasp/01.md
249 /// Requirement: MUST reject announcements not listing service (unless GRASP-05) 249 /// Requirement: MUST reject announcements not listing service (unless GRASP-05)
250 pub async fn test_reject_repo_announcement_missing_clone_tag( 250 pub async fn test_reject_repo_announcement_missing_clone_tag(
251 client: &AuditClient, 251 client: &AuditClient,
252 ) -> TestResult { 252 ) -> TestResult {
253 TestResult::new( 253 TestResult::new(
254 "reject_repo_announcement_missing_clone_tag", 254 "reject_repo_announcement_missing_clone_tag",
255 "GRASP-01:nostr-relay:5", 255 "GRASP-01:nostr-relay:9",
256 "Reject repository announcements without service in clone tag", 256 "Reject repository announcements without service in clone tag",
257 ) 257 )
258 .run(|| async { 258 .run(|| async {
@@ -321,14 +321,14 @@ impl EventAcceptancePolicyTests {
321 321
322 /// Test: Reject repo announcements not listing service in relays tag 322 /// Test: Reject repo announcements not listing service in relays tag
323 /// 323 ///
324 /// Spec: Line 5 of ../grasp/01.md 324 /// Spec: Line 9 of ../grasp/01.md
325 /// Requirement: MUST reject announcements not listing service in relays 325 /// Requirement: MUST reject announcements not listing service in relays
326 pub async fn test_reject_repo_announcement_missing_relays_tag( 326 pub async fn test_reject_repo_announcement_missing_relays_tag(
327 client: &AuditClient, 327 client: &AuditClient,
328 ) -> TestResult { 328 ) -> TestResult {
329 TestResult::new( 329 TestResult::new(
330 "reject_repo_announcement_missing_relays_tag", 330 "reject_repo_announcement_missing_relays_tag",
331 "GRASP-01:nostr-relay:5", 331 "GRASP-01:nostr-relay:9",
332 "Reject repository announcements without service in relays tag", 332 "Reject repository announcements without service in relays tag",
333 ) 333 )
334 .run(|| async { 334 .run(|| async {
@@ -410,7 +410,7 @@ impl EventAcceptancePolicyTests {
410 410
411 /// Test: Accept valid repository state announcements 411 /// Test: Accept valid repository state announcements
412 /// 412 ///
413 /// Spec: Lines 6-7 of ../grasp/01.md 413 /// Spec: Line 7 of ../grasp/01.md
414 /// Requirement: MUST accept repo state announcements with d, maintainers, and r tags 414 /// Requirement: MUST accept repo state announcements with d, maintainers, and r tags
415 /// 415 ///
416 /// **EXAMPLE: Using TestContext pattern for fixture management** 416 /// **EXAMPLE: Using TestContext pattern for fixture management**
@@ -420,7 +420,7 @@ impl EventAcceptancePolicyTests {
420 pub async fn test_accept_valid_repo_state_announcement(client: &AuditClient) -> TestResult { 420 pub async fn test_accept_valid_repo_state_announcement(client: &AuditClient) -> TestResult {
421 TestResult::new( 421 TestResult::new(
422 "accept_valid_repo_state_announcement", 422 "accept_valid_repo_state_announcement",
423 "GRASP-01:nostr-relay:6-7", 423 "GRASP-01:nostr-relay:7",
424 "Accept valid state announcements after repo announcement accepted", 424 "Accept valid state announcements after repo announcement accepted",
425 ) 425 )
426 .run(|| async { 426 .run(|| async {
@@ -526,12 +526,13 @@ impl EventAcceptancePolicyTests {
526 526
527 /// Test 1.1: Issue referencing repo via `a` tag should be accepted 527 /// Test 1.1: Issue referencing repo via `a` tag should be accepted
528 /// 528 ///
529 /// Spec: Line 13 of ../grasp/01.md
529 /// **EXAMPLE: Using TestContext for prerequisite events** 530 /// **EXAMPLE: Using TestContext for prerequisite events**
530 /// Demonstrates how TestContext simplifies test setup while supporting dual modes 531 /// Demonstrates how TestContext simplifies test setup while supporting dual modes
531 pub async fn test_accept_issue_via_a_tag(client: &AuditClient) -> TestResult { 532 pub async fn test_accept_issue_via_a_tag(client: &AuditClient) -> TestResult {
532 TestResult::new( 533 TestResult::new(
533 "accept_issue_via_a_tag", 534 "accept_issue_via_a_tag",
534 "GRASP-01:event-acceptance:1.1", 535 "GRASP-01:nostr-relay:13",
535 "Accept issue referencing repo via 'a' tag", 536 "Accept issue referencing repo via 'a' tag",
536 ) 537 )
537 .run(|| async { 538 .run(|| async {
@@ -559,13 +560,14 @@ impl EventAcceptancePolicyTests {
559 560
560 /// Test 1.2: NIP-22 comment with root `A` tag referencing repo should be accepted 561 /// Test 1.2: NIP-22 comment with root `A` tag referencing repo should be accepted
561 /// 562 ///
563 /// Spec: Line 13 of ../grasp/01.md
562 /// **Using TestContext pattern:** 564 /// **Using TestContext pattern:**
563 /// - In CI mode: Creates fresh repo for full isolation 565 /// - In CI mode: Creates fresh repo for full isolation
564 /// - In Production mode: Reuses cached repo to minimize events 566 /// - In Production mode: Reuses cached repo to minimize events
565 pub async fn test_accept_comment_via_capital_a_tag(client: &AuditClient) -> TestResult { 567 pub async fn test_accept_comment_via_capital_a_tag(client: &AuditClient) -> TestResult {
566 TestResult::new( 568 TestResult::new(
567 "accept_comment_via_A_tag", 569 "accept_comment_via_A_tag",
568 "GRASP-01:event-acceptance:1.2", 570 "GRASP-01:nostr-relay:13",
569 "Accept NIP-22 comment with root 'A' tag referencing repo", 571 "Accept NIP-22 comment with root 'A' tag referencing repo",
570 ) 572 )
571 .run(|| async { 573 .run(|| async {
@@ -611,13 +613,14 @@ impl EventAcceptancePolicyTests {
611 613
612 /// Test 1.3: Kind 1 text note quoting repo via `q` tag should be accepted 614 /// Test 1.3: Kind 1 text note quoting repo via `q` tag should be accepted
613 /// 615 ///
616 /// Spec: Line 13 of ../grasp/01.md
614 /// **Using TestContext pattern:** 617 /// **Using TestContext pattern:**
615 /// - In CI mode: Creates fresh repo for full isolation 618 /// - In CI mode: Creates fresh repo for full isolation
616 /// - In Production mode: Reuses cached repo to minimize events 619 /// - In Production mode: Reuses cached repo to minimize events
617 pub async fn test_accept_kind1_via_q_tag(client: &AuditClient) -> TestResult { 620 pub async fn test_accept_kind1_via_q_tag(client: &AuditClient) -> TestResult {
618 TestResult::new( 621 TestResult::new(
619 "accept_kind1_via_q_tag", 622 "accept_kind1_via_q_tag",
620 "GRASP-01:event-acceptance:1.3", 623 "GRASP-01:nostr-relay:13",
621 "Accept kind 1 note quoting repo via 'q' tag", 624 "Accept kind 1 note quoting repo via 'q' tag",
622 ) 625 )
623 .run(|| async { 626 .run(|| async {
@@ -660,13 +663,14 @@ impl EventAcceptancePolicyTests {
660 663
661 /// Test 2.1: Issue quoting another accepted issue should be accepted (transitive) 664 /// Test 2.1: Issue quoting another accepted issue should be accepted (transitive)
662 /// 665 ///
666 /// Spec: Line 13 of ../grasp/01.md
663 /// **Using TestContext pattern:** 667 /// **Using TestContext pattern:**
664 /// - In CI mode: Creates fresh repo+issue for full isolation 668 /// - In CI mode: Creates fresh repo+issue for full isolation
665 /// - In Production mode: Reuses cached repo+issue to minimize events 669 /// - In Production mode: Reuses cached repo+issue to minimize events
666 pub async fn test_accept_issue_quoting_issue_via_q(client: &AuditClient) -> TestResult { 670 pub async fn test_accept_issue_quoting_issue_via_q(client: &AuditClient) -> TestResult {
667 TestResult::new( 671 TestResult::new(
668 "accept_issue_quoting_issue_via_q", 672 "accept_issue_quoting_issue_via_q",
669 "GRASP-01:event-acceptance:2.1", 673 "GRASP-01:nostr-relay:13",
670 "Accept issue quoting accepted issue (transitive)", 674 "Accept issue quoting accepted issue (transitive)",
671 ) 675 )
672 .run(|| async { 676 .run(|| async {
@@ -705,13 +709,14 @@ impl EventAcceptancePolicyTests {
705 709
706 /// Test 2.2: NIP-22 comment with root 'E' tag to accepted issue should be accepted 710 /// Test 2.2: NIP-22 comment with root 'E' tag to accepted issue should be accepted
707 /// 711 ///
712 /// Spec: Line 13 of ../grasp/01.md
708 /// **Using TestContext pattern:** 713 /// **Using TestContext pattern:**
709 /// - In CI mode: Creates fresh repo+issue for full isolation 714 /// - In CI mode: Creates fresh repo+issue for full isolation
710 /// - In Production mode: Reuses cached repo+issue to minimize events 715 /// - In Production mode: Reuses cached repo+issue to minimize events
711 pub async fn test_accept_comment_via_capital_e_tag(client: &AuditClient) -> TestResult { 716 pub async fn test_accept_comment_via_capital_e_tag(client: &AuditClient) -> TestResult {
712 TestResult::new( 717 TestResult::new(
713 "accept_comment_via_E_tag", 718 "accept_comment_via_E_tag",
714 "GRASP-01:event-acceptance:2.2", 719 "GRASP-01:nostr-relay:13",
715 "Accept NIP-22 comment with root 'E' tag to accepted issue", 720 "Accept NIP-22 comment with root 'E' tag to accepted issue",
716 ) 721 )
717 .run(|| async { 722 .run(|| async {
@@ -743,13 +748,14 @@ impl EventAcceptancePolicyTests {
743 748
744 /// Test 2.3: Kind 1 note with 'e' tag reply to accepted kind 1 should be accepted 749 /// Test 2.3: Kind 1 note with 'e' tag reply to accepted kind 1 should be accepted
745 /// 750 ///
751 /// Spec: Line 13 of ../grasp/01.md
746 /// **Using TestContext pattern:** 752 /// **Using TestContext pattern:**
747 /// - In CI mode: Creates fresh repo for full isolation 753 /// - In CI mode: Creates fresh repo for full isolation
748 /// - In Production mode: Reuses cached repo to minimize events 754 /// - In Production mode: Reuses cached repo to minimize events
749 pub async fn test_accept_kind1_via_e_tag(client: &AuditClient) -> TestResult { 755 pub async fn test_accept_kind1_via_e_tag(client: &AuditClient) -> TestResult {
750 TestResult::new( 756 TestResult::new(
751 "accept_kind1_via_e_tag", 757 "accept_kind1_via_e_tag",
752 "GRASP-01:event-acceptance:2.3", 758 "GRASP-01:nostr-relay:13",
753 "Accept kind 1 reply via 'e' tag to accepted kind 1", 759 "Accept kind 1 reply via 'e' tag to accepted kind 1",
754 ) 760 )
755 .run(|| async { 761 .run(|| async {
@@ -798,13 +804,14 @@ impl EventAcceptancePolicyTests {
798 804
799 /// Test 3.1: Kind 1 note should be accepted when referenced by an accepted issue (forward ref) 805 /// Test 3.1: Kind 1 note should be accepted when referenced by an accepted issue (forward ref)
800 /// 806 ///
807 /// Spec: Line 13 of ../grasp/01.md
801 /// **Using TestContext pattern:** 808 /// **Using TestContext pattern:**
802 /// - In CI mode: Creates fresh repo for full isolation 809 /// - In CI mode: Creates fresh repo for full isolation
803 /// - In Production mode: Reuses cached repo to minimize events 810 /// - In Production mode: Reuses cached repo to minimize events
804 pub async fn test_accept_kind1_referenced_in_issue(client: &AuditClient) -> TestResult { 811 pub async fn test_accept_kind1_referenced_in_issue(client: &AuditClient) -> TestResult {
805 TestResult::new( 812 TestResult::new(
806 "accept_kind1_referenced_in_issue", 813 "accept_kind1_referenced_in_issue",
807 "GRASP-01:event-acceptance:3.1", 814 "GRASP-01:nostr-relay:13",
808 "Accept kind 1 referenced in accepted issue (forward ref)", 815 "Accept kind 1 referenced in accepted issue (forward ref)",
809 ) 816 )
810 .run(|| async { 817 .run(|| async {
@@ -889,13 +896,14 @@ impl EventAcceptancePolicyTests {
889 896
890 /// Test 3.2: Comment should be accepted when referenced by another accepted comment (forward ref) 897 /// Test 3.2: Comment should be accepted when referenced by another accepted comment (forward ref)
891 /// 898 ///
899 /// Spec: Line 13 of ../grasp/01.md
892 /// **Using TestContext pattern:** 900 /// **Using TestContext pattern:**
893 /// - In CI mode: Creates fresh repo+issue for full isolation 901 /// - In CI mode: Creates fresh repo+issue for full isolation
894 /// - In Production mode: Reuses cached repo+issue to minimize events 902 /// - In Production mode: Reuses cached repo+issue to minimize events
895 pub async fn test_accept_comment_referenced_in_comment(client: &AuditClient) -> TestResult { 903 pub async fn test_accept_comment_referenced_in_comment(client: &AuditClient) -> TestResult {
896 TestResult::new( 904 TestResult::new(
897 "accept_comment_referenced_in_comment", 905 "accept_comment_referenced_in_comment",
898 "GRASP-01:event-acceptance:3.2", 906 "GRASP-01:nostr-relay:13",
899 "Accept comment referenced in another accepted comment (forward ref)", 907 "Accept comment referenced in another accepted comment (forward ref)",
900 ) 908 )
901 .run(|| async { 909 .run(|| async {
@@ -949,13 +957,14 @@ impl EventAcceptancePolicyTests {
949 957
950 /// Test 3.3: Kind 1 note should be accepted when referenced by another accepted kind 1 (forward ref) 958 /// Test 3.3: Kind 1 note should be accepted when referenced by another accepted kind 1 (forward ref)
951 /// 959 ///
960 /// Spec: Line 13 of ../grasp/01.md
952 /// **Using TestContext pattern:** 961 /// **Using TestContext pattern:**
953 /// - In CI mode: Creates fresh repo for full isolation 962 /// - In CI mode: Creates fresh repo for full isolation
954 /// - In Production mode: Reuses cached repo to minimize events 963 /// - In Production mode: Reuses cached repo to minimize events
955 pub async fn test_accept_kind1_referenced_in_kind1(client: &AuditClient) -> TestResult { 964 pub async fn test_accept_kind1_referenced_in_kind1(client: &AuditClient) -> TestResult {
956 TestResult::new( 965 TestResult::new(
957 "accept_kind1_referenced_in_kind1", 966 "accept_kind1_referenced_in_kind1",
958 "GRASP-01:event-acceptance:3.3", 967 "GRASP-01:nostr-relay:13",
959 "Accept kind 1 referenced in another accepted kind 1 (forward ref)", 968 "Accept kind 1 referenced in another accepted kind 1 (forward ref)",
960 ) 969 )
961 .run(|| async { 970 .run(|| async {
@@ -1007,10 +1016,13 @@ impl EventAcceptancePolicyTests {
1007 // ============================================================ 1016 // ============================================================
1008 1017
1009 /// Test 4.1: Issue referencing unaccepted repo should be rejected 1018 /// Test 4.1: Issue referencing unaccepted repo should be rejected
1019 ///
1020 /// Spec: Line 18 of ../grasp/01.md
1021 /// (Rejecting non-git-related events falls under MAY reject for curation)
1010 pub async fn test_reject_orphan_issue(client: &AuditClient) -> TestResult { 1022 pub async fn test_reject_orphan_issue(client: &AuditClient) -> TestResult {
1011 TestResult::new( 1023 TestResult::new(
1012 "reject_orphan_issue", 1024 "reject_orphan_issue",
1013 "GRASP-01:event-acceptance:4.1", 1025 "GRASP-01:nostr-relay:18",
1014 "Reject issue referencing unaccepted repo", 1026 "Reject issue referencing unaccepted repo",
1015 ) 1027 )
1016 .run(|| async { 1028 .run(|| async {
@@ -1031,10 +1043,13 @@ impl EventAcceptancePolicyTests {
1031 } 1043 }
1032 1044
1033 /// Test 4.2: Generic kind 1 note with no repo references should be rejected 1045 /// Test 4.2: Generic kind 1 note with no repo references should be rejected
1046 ///
1047 /// Spec: Line 18 of ../grasp/01.md
1048 /// (Rejecting non-git-related events falls under MAY reject for curation)
1034 pub async fn test_reject_orphan_kind1(client: &AuditClient) -> TestResult { 1049 pub async fn test_reject_orphan_kind1(client: &AuditClient) -> TestResult {
1035 TestResult::new( 1050 TestResult::new(
1036 "reject_orphan_kind1", 1051 "reject_orphan_kind1",
1037 "GRASP-01:event-acceptance:4.2", 1052 "GRASP-01:nostr-relay:18",
1038 "Reject kind 1 with no repo references", 1053 "Reject kind 1 with no repo references",
1039 ) 1054 )
1040 .run(|| async { 1055 .run(|| async {
@@ -1054,6 +1069,8 @@ impl EventAcceptancePolicyTests {
1054 1069
1055 /// Test 4.3: Comment quoting unaccepted repo should be rejected 1070 /// Test 4.3: Comment quoting unaccepted repo should be rejected
1056 /// 1071 ///
1072 /// Spec: Line 18 of ../grasp/01.md
1073 /// (Rejecting non-git-related events falls under MAY reject for curation)
1057 /// **Using TestContext pattern:** 1074 /// **Using TestContext pattern:**
1058 /// - In CI mode: Creates fresh accepted repo for full isolation 1075 /// - In CI mode: Creates fresh accepted repo for full isolation
1059 /// - In Production mode: Reuses cached accepted repo to minimize events 1076 /// - In Production mode: Reuses cached accepted repo to minimize events
@@ -1061,7 +1078,7 @@ impl EventAcceptancePolicyTests {
1061 pub async fn test_reject_comment_quoting_other_repo(client: &AuditClient) -> TestResult { 1078 pub async fn test_reject_comment_quoting_other_repo(client: &AuditClient) -> TestResult {
1062 TestResult::new( 1079 TestResult::new(
1063 "reject_comment_quoting_other_repo", 1080 "reject_comment_quoting_other_repo",
1064 "GRASP-01:event-acceptance:4.3", 1081 "GRASP-01:nostr-relay:18",
1065 "Reject comment quoting unaccepted repo", 1082 "Reject comment quoting unaccepted repo",
1066 ) 1083 )
1067 .run(|| async { 1084 .run(|| async {
diff --git a/grasp-audit/src/specs/grasp01/git_clone.rs b/grasp-audit/src/specs/grasp01/git_clone.rs
index 95338e4..3d81964 100644
--- a/grasp-audit/src/specs/grasp01/git_clone.rs
+++ b/grasp-audit/src/specs/grasp01/git_clone.rs
@@ -37,6 +37,7 @@ impl GitCloneTests {
37 37
38 /// Test that a repository can be cloned via Git HTTP backend 38 /// Test that a repository can be cloned via Git HTTP backend
39 /// 39 ///
40 /// Spec: Line 28 of ../grasp/01.md
40 /// This test: 41 /// This test:
41 /// 1. Creates a repository announcement 42 /// 1. Creates a repository announcement
42 /// 2. Waits for repository creation 43 /// 2. Waits for repository creation
@@ -52,7 +53,7 @@ impl GitCloneTests {
52 Err(e) => { 53 Err(e) => {
53 return TestResult::new( 54 return TestResult::new(
54 test_name, 55 test_name,
55 "GRASP-01", 56 "GRASP-01:git-http:28",
56 "Repository must be cloneable via Git HTTP backend", 57 "Repository must be cloneable via Git HTTP backend",
57 ) 58 )
58 .fail(format!("Failed to create repo fixture: {}", e)) 59 .fail(format!("Failed to create repo fixture: {}", e))
@@ -85,7 +86,7 @@ impl GitCloneTests {
85 Err(e) => { 86 Err(e) => {
86 return TestResult::new( 87 return TestResult::new(
87 test_name, 88 test_name,
88 "GRASP-01", 89 "GRASP-01:git-http:28",
89 "Repository must be cloneable via Git HTTP backend", 90 "Repository must be cloneable via Git HTTP backend",
90 ) 91 )
91 .fail(format!("Failed to convert pubkey to npub: {}", e)) 92 .fail(format!("Failed to convert pubkey to npub: {}", e))
@@ -120,7 +121,7 @@ impl GitCloneTests {
120 cleanup(); 121 cleanup();
121 return TestResult::new( 122 return TestResult::new(
122 test_name, 123 test_name,
123 "GRASP-01", 124 "GRASP-01:git-http:28",
124 "Repository must be cloneable via Git HTTP backend", 125 "Repository must be cloneable via Git HTTP backend",
125 ) 126 )
126 .fail(format!("Failed to execute git clone: {}", e)); 127 .fail(format!("Failed to execute git clone: {}", e));
@@ -132,7 +133,7 @@ impl GitCloneTests {
132 let stderr = String::from_utf8_lossy(&output.stderr); 133 let stderr = String::from_utf8_lossy(&output.stderr);
133 return TestResult::new( 134 return TestResult::new(
134 test_name, 135 test_name,
135 "GRASP-01", 136 "GRASP-01:git-http:28",
136 "Repository must be cloneable via Git HTTP backend", 137 "Repository must be cloneable via Git HTTP backend",
137 ) 138 )
138 .fail(format!("Git clone failed: {}", stderr)); 139 .fail(format!("Git clone failed: {}", stderr));
@@ -143,7 +144,7 @@ impl GitCloneTests {
143 cleanup(); 144 cleanup();
144 return TestResult::new( 145 return TestResult::new(
145 test_name, 146 test_name,
146 "GRASP-01", 147 "GRASP-01:git-http:28",
147 "Repository must be cloneable via Git HTTP backend", 148 "Repository must be cloneable via Git HTTP backend",
148 ) 149 )
149 .fail("Cloned repository missing .git directory"); 150 .fail("Cloned repository missing .git directory");
@@ -152,7 +153,7 @@ impl GitCloneTests {
152 cleanup(); 153 cleanup();
153 TestResult::new( 154 TestResult::new(
154 test_name, 155 test_name,
155 "GRASP-01", 156 "GRASP-01:git-http:28",
156 "Repository must be cloneable via Git HTTP backend", 157 "Repository must be cloneable via Git HTTP backend",
157 ) 158 )
158 .pass() 159 .pass()
@@ -160,6 +161,7 @@ impl GitCloneTests {
160 161
161 /// Test clone URL format validation 162 /// Test clone URL format validation
162 /// 163 ///
164 /// Spec: Line 28 of ../grasp/01.md
163 /// This test verifies: 165 /// This test verifies:
164 /// 1. URLs follow the pattern http://domain/npub/identifier.git 166 /// 1. URLs follow the pattern http://domain/npub/identifier.git
165 /// 2. Invalid URLs are rejected properly 167 /// 2. Invalid URLs are rejected properly
@@ -173,7 +175,7 @@ impl GitCloneTests {
173 Err(e) => { 175 Err(e) => {
174 return TestResult::new( 176 return TestResult::new(
175 test_name, 177 test_name,
176 "GRASP-01", 178 "GRASP-01:git-http:28",
177 "Clone URL must follow correct format", 179 "Clone URL must follow correct format",
178 ) 180 )
179 .fail(format!("Failed to create repo fixture: {}", e)) 181 .fail(format!("Failed to create repo fixture: {}", e))
@@ -201,7 +203,7 @@ impl GitCloneTests {
201 if !valid_url.contains(&npub) { 203 if !valid_url.contains(&npub) {
202 return TestResult::new( 204 return TestResult::new(
203 test_name, 205 test_name,
204 "GRASP-01", 206 "GRASP-01:git-http:28",
205 "Clone URL must follow correct format", 207 "Clone URL must follow correct format",
206 ) 208 )
207 .fail("URL missing npub"); 209 .fail("URL missing npub");
@@ -210,7 +212,7 @@ impl GitCloneTests {
210 if !valid_url.contains(&format!("{}.git", repo_id)) { 212 if !valid_url.contains(&format!("{}.git", repo_id)) {
211 return TestResult::new( 213 return TestResult::new(
212 test_name, 214 test_name,
213 "GRASP-01", 215 "GRASP-01:git-http:28",
214 "Clone URL must follow correct format", 216 "Clone URL must follow correct format",
215 ) 217 )
216 .fail("URL missing repository identifier"); 218 .fail("URL missing repository identifier");
@@ -239,7 +241,7 @@ impl GitCloneTests {
239 if output.status.success() { 241 if output.status.success() {
240 return TestResult::new( 242 return TestResult::new(
241 test_name, 243 test_name,
242 "GRASP-01", 244 "GRASP-01:git-http:28",
243 "Clone URL must follow correct format", 245 "Clone URL must follow correct format",
244 ) 246 )
245 .fail("Invalid URL was accepted (should have been rejected)"); 247 .fail("Invalid URL was accepted (should have been rejected)");
@@ -247,7 +249,7 @@ impl GitCloneTests {
247 249
248 TestResult::new( 250 TestResult::new(
249 test_name, 251 test_name,
250 "GRASP-01", 252 "GRASP-01:git-http:28",
251 "Clone URL must follow correct format", 253 "Clone URL must follow correct format",
252 ) 254 )
253 .pass() 255 .pass()
@@ -255,6 +257,7 @@ impl GitCloneTests {
255 257
256 /// Test that SHA1 capabilities are advertised in git-upload-pack 258 /// Test that SHA1 capabilities are advertised in git-upload-pack
257 /// 259 ///
260 /// Spec: Line 36 of ../grasp/01.md
258 /// GRASP-01 requires: 261 /// GRASP-01 requires:
259 /// "MUST include `allow-reachable-sha1-in-want` and `allow-tip-sha1-in-want` 262 /// "MUST include `allow-reachable-sha1-in-want` and `allow-tip-sha1-in-want`
260 /// in advertisement and serve available oids." 263 /// in advertisement and serve available oids."
@@ -275,7 +278,7 @@ impl GitCloneTests {
275 Err(e) => { 278 Err(e) => {
276 return TestResult::new( 279 return TestResult::new(
277 test_name, 280 test_name,
278 "GRASP-01", 281 "GRASP-01:git-http:36",
279 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 282 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
280 ) 283 )
281 .fail(format!("Failed to create repo fixture: {}", e)) 284 .fail(format!("Failed to create repo fixture: {}", e))
@@ -296,7 +299,7 @@ impl GitCloneTests {
296 None => { 299 None => {
297 return TestResult::new( 300 return TestResult::new(
298 test_name, 301 test_name,
299 "GRASP-01", 302 "GRASP-01:git-http:36",
300 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 303 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
301 ) 304 )
302 .fail("Repository announcement missing d tag") 305 .fail("Repository announcement missing d tag")
@@ -308,7 +311,7 @@ impl GitCloneTests {
308 Err(e) => { 311 Err(e) => {
309 return TestResult::new( 312 return TestResult::new(
310 test_name, 313 test_name,
311 "GRASP-01", 314 "GRASP-01:git-http:36",
312 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 315 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
313 ) 316 )
314 .fail(format!("Failed to convert pubkey to npub: {}", e)) 317 .fail(format!("Failed to convert pubkey to npub: {}", e))
@@ -328,7 +331,7 @@ impl GitCloneTests {
328 Err(e) => { 331 Err(e) => {
329 return TestResult::new( 332 return TestResult::new(
330 test_name, 333 test_name,
331 "GRASP-01", 334 "GRASP-01:git-http:36",
332 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 335 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
333 ) 336 )
334 .fail(format!("HTTP request failed: {}", e)) 337 .fail(format!("HTTP request failed: {}", e))
@@ -338,7 +341,7 @@ impl GitCloneTests {
338 if !response.status().is_success() { 341 if !response.status().is_success() {
339 return TestResult::new( 342 return TestResult::new(
340 test_name, 343 test_name,
341 "GRASP-01", 344 "GRASP-01:git-http:36",
342 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 345 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
343 ) 346 )
344 .fail(format!( 347 .fail(format!(
@@ -353,7 +356,7 @@ impl GitCloneTests {
353 Err(e) => { 356 Err(e) => {
354 return TestResult::new( 357 return TestResult::new(
355 test_name, 358 test_name,
356 "GRASP-01", 359 "GRASP-01:git-http:36",
357 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 360 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
358 ) 361 )
359 .fail(format!("Failed to read response body: {}", e)) 362 .fail(format!("Failed to read response body: {}", e))
@@ -367,7 +370,7 @@ impl GitCloneTests {
367 if !has_allow_reachable { 370 if !has_allow_reachable {
368 return TestResult::new( 371 return TestResult::new(
369 test_name, 372 test_name,
370 "GRASP-01", 373 "GRASP-01:git-http:36",
371 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 374 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
372 ) 375 )
373 .fail("Missing capability: allow-reachable-sha1-in-want"); 376 .fail("Missing capability: allow-reachable-sha1-in-want");
@@ -376,7 +379,7 @@ impl GitCloneTests {
376 if !has_allow_tip { 379 if !has_allow_tip {
377 return TestResult::new( 380 return TestResult::new(
378 test_name, 381 test_name,
379 "GRASP-01", 382 "GRASP-01:git-http:36",
380 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 383 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
381 ) 384 )
382 .fail("Missing capability: allow-tip-sha1-in-want"); 385 .fail("Missing capability: allow-tip-sha1-in-want");
@@ -384,7 +387,7 @@ impl GitCloneTests {
384 387
385 TestResult::new( 388 TestResult::new(
386 test_name, 389 test_name,
387 "GRASP-01", 390 "GRASP-01:git-http:36",
388 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 391 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
389 ) 392 )
390 .pass() 393 .pass()
diff --git a/grasp-audit/src/specs/grasp01/mod.rs b/grasp-audit/src/specs/grasp01/mod.rs
index e16a351..ba27fef 100644
--- a/grasp-audit/src/specs/grasp01/mod.rs
+++ b/grasp-audit/src/specs/grasp01/mod.rs
@@ -19,6 +19,7 @@ pub mod nip01_smoke;
19pub mod nip11_document; 19pub mod nip11_document;
20pub mod push_authorization; 20pub mod push_authorization;
21pub mod repository_creation; 21pub mod repository_creation;
22pub mod spec_requirements;
22 23
23pub use cors::CorsTests; 24pub use cors::CorsTests;
24pub use event_acceptance_policy::EventAcceptancePolicyTests; 25pub use event_acceptance_policy::EventAcceptancePolicyTests;
@@ -27,3 +28,7 @@ pub use nip01_smoke::Nip01SmokeTests;
27pub use nip11_document::Nip11DocumentTests; 28pub use nip11_document::Nip11DocumentTests;
28pub use push_authorization::PushAuthorizationTests; 29pub use push_authorization::PushAuthorizationTests;
29pub use repository_creation::RepositoryCreationTests; 30pub use repository_creation::RepositoryCreationTests;
31pub use spec_requirements::{
32 get_requirement, get_requirements_for_section, get_sections, RequirementLevel,
33 SpecRequirement, GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID,
34};
diff --git a/grasp-audit/src/specs/grasp01/nip01_smoke.rs b/grasp-audit/src/specs/grasp01/nip01_smoke.rs
index 5161da8..9a7d7d1 100644
--- a/grasp-audit/src/specs/grasp01/nip01_smoke.rs
+++ b/grasp-audit/src/specs/grasp01/nip01_smoke.rs
@@ -32,7 +32,7 @@ impl Nip01SmokeTests {
32 pub async fn test_websocket_connection(client: &AuditClient) -> TestResult { 32 pub async fn test_websocket_connection(client: &AuditClient) -> TestResult {
33 TestResult::new( 33 TestResult::new(
34 "websocket_connection", 34 "websocket_connection",
35 "NIP-01", 35 "GRASP-01:nostr-relay:7",
36 "Can establish WebSocket connection to /", 36 "Can establish WebSocket connection to /",
37 ) 37 )
38 .run(|| async { 38 .run(|| async {
@@ -61,7 +61,7 @@ impl Nip01SmokeTests {
61 pub async fn test_send_receive_event(client: &AuditClient) -> TestResult { 61 pub async fn test_send_receive_event(client: &AuditClient) -> TestResult {
62 TestResult::new( 62 TestResult::new(
63 "send_receive_event", 63 "send_receive_event",
64 "NIP-01", 64 "GRASP-01:nostr-relay:7",
65 "Can send EVENT and receive OK response", 65 "Can send EVENT and receive OK response",
66 ) 66 )
67 .run(|| async { 67 .run(|| async {
@@ -127,7 +127,7 @@ impl Nip01SmokeTests {
127 pub async fn test_create_subscription(client: &AuditClient) -> TestResult { 127 pub async fn test_create_subscription(client: &AuditClient) -> TestResult {
128 TestResult::new( 128 TestResult::new(
129 "create_subscription", 129 "create_subscription",
130 "NIP-01", 130 "GRASP-01:nostr-relay:7",
131 "Can create subscription with REQ and receive EOSE", 131 "Can create subscription with REQ and receive EOSE",
132 ) 132 )
133 .run(|| async { 133 .run(|| async {
@@ -163,7 +163,7 @@ impl Nip01SmokeTests {
163 /// Spec: NIP-01 CLOSE message 163 /// Spec: NIP-01 CLOSE message
164 /// Requirement: Relay MUST support CLOSE to end subscriptions 164 /// Requirement: Relay MUST support CLOSE to end subscriptions
165 pub async fn test_close_subscription(client: &AuditClient) -> TestResult { 165 pub async fn test_close_subscription(client: &AuditClient) -> TestResult {
166 TestResult::new("close_subscription", "NIP-01", "Can close subscriptions") 166 TestResult::new("close_subscription", "GRASP-01:nostr-relay:7", "Can close subscriptions")
167 .run(|| async { 167 .run(|| async {
168 // For now, we just verify we can query events 168 // For now, we just verify we can query events
169 // Full subscription management with CLOSE would require 169 // Full subscription management with CLOSE would require
@@ -189,7 +189,7 @@ impl Nip01SmokeTests {
189 pub async fn test_reject_invalid_signature(client: &AuditClient) -> TestResult { 189 pub async fn test_reject_invalid_signature(client: &AuditClient) -> TestResult {
190 TestResult::new( 190 TestResult::new(
191 "reject_invalid_signature", 191 "reject_invalid_signature",
192 "NIP-01", 192 "GRASP-01:nostr-relay:7",
193 "Rejects events with invalid signatures", 193 "Rejects events with invalid signatures",
194 ) 194 )
195 .run(|| async { 195 .run(|| async {
@@ -243,7 +243,7 @@ impl Nip01SmokeTests {
243 pub async fn test_reject_invalid_event_id(client: &AuditClient) -> TestResult { 243 pub async fn test_reject_invalid_event_id(client: &AuditClient) -> TestResult {
244 TestResult::new( 244 TestResult::new(
245 "reject_invalid_event_id", 245 "reject_invalid_event_id",
246 "NIP-01", 246 "GRASP-01:nostr-relay:7",
247 "Rejects events with invalid event IDs", 247 "Rejects events with invalid event IDs",
248 ) 248 )
249 .run(|| async { 249 .run(|| async {
diff --git a/grasp-audit/src/specs/grasp01/nip11_document.rs b/grasp-audit/src/specs/grasp01/nip11_document.rs
index 51b147d..33599b1 100644
--- a/grasp-audit/src/specs/grasp01/nip11_document.rs
+++ b/grasp-audit/src/specs/grasp01/nip11_document.rs
@@ -32,12 +32,12 @@ impl Nip11DocumentTests {
32 32
33 /// Test: Serve NIP-11 document 33 /// Test: Serve NIP-11 document
34 /// 34 ///
35 /// Spec: Line 11 of ../grasp/01.md 35 /// Spec: Line 20 of ../grasp/01.md
36 /// Requirement: MUST serve NIP-11 document 36 /// Requirement: MUST serve NIP-11 document
37 pub async fn test_nip11_document_exists(client: &AuditClient) -> TestResult { 37 pub async fn test_nip11_document_exists(client: &AuditClient) -> TestResult {
38 TestResult::new( 38 TestResult::new(
39 "nip11_document_exists", 39 "nip11_document_exists",
40 "GRASP-01:nostr-relay:11", 40 "GRASP-01:nostr-relay:20",
41 "Serve NIP-11 relay information document", 41 "Serve NIP-11 relay information document",
42 ) 42 )
43 .run(|| async { 43 .run(|| async {
@@ -91,12 +91,12 @@ impl Nip11DocumentTests {
91 91
92 /// Test: NIP-11 includes supported_grasps field 92 /// Test: NIP-11 includes supported_grasps field
93 /// 93 ///
94 /// Spec: Line 12 of ../grasp/01.md 94 /// Spec: Line 22 of ../grasp/01.md
95 /// Requirement: MUST list supported GRASPs as string array 95 /// Requirement: MUST list supported GRASPs as string array
96 pub async fn test_nip11_supported_grasps_field(client: &AuditClient) -> TestResult { 96 pub async fn test_nip11_supported_grasps_field(client: &AuditClient) -> TestResult {
97 TestResult::new( 97 TestResult::new(
98 "nip11_supported_grasps_field", 98 "nip11_supported_grasps_field",
99 "GRASP-01:nostr-relay:12", 99 "GRASP-01:nostr-relay:22",
100 "NIP-11 document includes supported_grasps field with GRASP-01", 100 "NIP-11 document includes supported_grasps field with GRASP-01",
101 ) 101 )
102 .run(|| async { 102 .run(|| async {
@@ -167,12 +167,12 @@ impl Nip11DocumentTests {
167 167
168 /// Test: NIP-11 includes repo_acceptance_criteria field 168 /// Test: NIP-11 includes repo_acceptance_criteria field
169 /// 169 ///
170 /// Spec: Line 13 of ../grasp/01.md 170 /// Spec: Line 23 of ../grasp/01.md
171 /// Requirement: MUST list repository acceptance criteria 171 /// Requirement: MUST list repository acceptance criteria
172 pub async fn test_nip11_repo_acceptance_criteria_field(client: &AuditClient) -> TestResult { 172 pub async fn test_nip11_repo_acceptance_criteria_field(client: &AuditClient) -> TestResult {
173 TestResult::new( 173 TestResult::new(
174 "nip11_repo_acceptance_criteria_field", 174 "nip11_repo_acceptance_criteria_field",
175 "GRASP-01:nostr-relay:13", 175 "GRASP-01:nostr-relay:23",
176 "NIP-11 document includes repo_acceptance_criteria field", 176 "NIP-11 document includes repo_acceptance_criteria field",
177 ) 177 )
178 .run(|| async { 178 .run(|| async {
@@ -222,12 +222,12 @@ impl Nip11DocumentTests {
222 222
223 /// Test: NIP-11 curation field handling 223 /// Test: NIP-11 curation field handling
224 /// 224 ///
225 /// Spec: Line 14 of ../grasp/01.md 225 /// Spec: Line 24 of ../grasp/01.md
226 /// Requirement: MUST include curation if curated, omit otherwise 226 /// Requirement: MUST include curation if curated, omit otherwise
227 pub async fn test_nip11_curation_field(client: &AuditClient) -> TestResult { 227 pub async fn test_nip11_curation_field(client: &AuditClient) -> TestResult {
228 TestResult::new( 228 TestResult::new(
229 "nip11_curation_field", 229 "nip11_curation_field",
230 "GRASP-01:nostr-relay:14", 230 "GRASP-01:nostr-relay:24",
231 "NIP-11 curation field present if curated, absent otherwise", 231 "NIP-11 curation field present if curated, absent otherwise",
232 ) 232 )
233 .run(|| async { 233 .run(|| async {
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs
index af6247f..c06da0d 100644
--- a/grasp-audit/src/specs/grasp01/push_authorization.rs
+++ b/grasp-audit/src/specs/grasp01/push_authorization.rs
@@ -407,7 +407,7 @@ impl PushAuthorizationTests {
407 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 407 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
408 Ok(r) => r, 408 Ok(r) => r,
409 Err(e) => { 409 Err(e) => {
410 return TestResult::new(test_name, "GRASP-01", "Push rejected without state event") 410 return TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event")
411 .fail(format!("Failed to create repo: {}", e)) 411 .fail(format!("Failed to create repo: {}", e))
412 } 412 }
413 }; 413 };
@@ -427,7 +427,7 @@ impl PushAuthorizationTests {
427 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { 427 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
428 Ok(p) => p, 428 Ok(p) => p,
429 Err(e) => { 429 Err(e) => {
430 return TestResult::new(test_name, "GRASP-01", "Push rejected without state event") 430 return TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event")
431 .fail(&e) 431 .fail(&e)
432 } 432 }
433 }; 433 };
@@ -437,7 +437,7 @@ impl PushAuthorizationTests {
437 437
438 if let Err(e) = create_commit(&clone_path, "Unauthorized commit") { 438 if let Err(e) = create_commit(&clone_path, "Unauthorized commit") {
439 cleanup(); 439 cleanup();
440 return TestResult::new(test_name, "GRASP-01", "Push rejected without state event") 440 return TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event")
441 .fail(&e); 441 .fail(&e);
442 } 442 }
443 443
@@ -447,12 +447,12 @@ impl PushAuthorizationTests {
447 447
448 match push_result { 448 match push_result {
449 Ok(false) => { 449 Ok(false) => {
450 TestResult::new(test_name, "GRASP-01", "Push rejected without state event").pass() 450 TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event").pass()
451 } 451 }
452 Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event") 452 Ok(true) => TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event")
453 .fail("Push accepted but should be rejected"), 453 .fail("Push accepted but should be rejected"),
454 Err(e) => { 454 Err(e) => {
455 TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e) 455 TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected without state event").fail(&e)
456 } 456 }
457 } 457 }
458 } 458 }
@@ -481,9 +481,9 @@ impl PushAuthorizationTests {
481 // Generate → Send → Verify → DataPush 481 // Generate → Send → Verify → DataPush
482 match ctx.get_fixture(FixtureKind::OwnerStateDataPushed).await { 482 match ctx.get_fixture(FixtureKind::OwnerStateDataPushed).await {
483 Ok(_state_event) => { 483 Ok(_state_event) => {
484 TestResult::new(test_name, "GRASP-01", "Push authorized with matching state").pass() 484 TestResult::new(test_name, "GRASP-01:git-http:30", "Push authorized with matching state").pass()
485 } 485 }
486 Err(e) => TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") 486 Err(e) => TestResult::new(test_name, "GRASP-01:git-http:30", "Push authorized with matching state")
487 .fail(format!("{}", e)), 487 .fail(format!("{}", e)),
488 } 488 }
489 } 489 }
@@ -523,7 +523,7 @@ impl PushAuthorizationTests {
523 Err(e) => { 523 Err(e) => {
524 return TestResult::new( 524 return TestResult::new(
525 test_name, 525 test_name,
526 "GRASP-01", 526 "GRASP-01:git-http:30",
527 "Push rejected when commit not in state event", 527 "Push rejected when commit not in state event",
528 ) 528 )
529 .fail(format!("Failed to create RepoState fixture: {}", e)); 529 .fail(format!("Failed to create RepoState fixture: {}", e));
@@ -543,7 +543,7 @@ impl PushAuthorizationTests {
543 None => { 543 None => {
544 return TestResult::new( 544 return TestResult::new(
545 test_name, 545 test_name,
546 "GRASP-01", 546 "GRASP-01:git-http:30",
547 "Push rejected when commit not in state event", 547 "Push rejected when commit not in state event",
548 ) 548 )
549 .fail("Missing repo_id in state event"); 549 .fail("Missing repo_id in state event");
@@ -555,7 +555,7 @@ impl PushAuthorizationTests {
555 Err(e) => { 555 Err(e) => {
556 return TestResult::new( 556 return TestResult::new(
557 test_name, 557 test_name,
558 "GRASP-01", 558 "GRASP-01:git-http:30",
559 "Push rejected when commit not in state event", 559 "Push rejected when commit not in state event",
560 ) 560 )
561 .fail(format!("Failed to convert pubkey to bech32: {}", e)); 561 .fail(format!("Failed to convert pubkey to bech32: {}", e));
@@ -571,7 +571,7 @@ impl PushAuthorizationTests {
571 Err(e) => { 571 Err(e) => {
572 return TestResult::new( 572 return TestResult::new(
573 test_name, 573 test_name,
574 "GRASP-01", 574 "GRASP-01:git-http:30",
575 "Push rejected when commit not in state event", 575 "Push rejected when commit not in state event",
576 ) 576 )
577 .fail(format!("Failed to clone repo: {}", e)); 577 .fail(format!("Failed to clone repo: {}", e));
@@ -594,7 +594,7 @@ impl PushAuthorizationTests {
594 cleanup(); 594 cleanup();
595 return TestResult::new( 595 return TestResult::new(
596 test_name, 596 test_name,
597 "GRASP-01", 597 "GRASP-01:git-http:30",
598 "Push rejected when commit not in state event", 598 "Push rejected when commit not in state event",
599 ) 599 )
600 .fail(format!("Failed to create/checkout main branch: {}", e)); 600 .fail(format!("Failed to create/checkout main branch: {}", e));
@@ -603,7 +603,7 @@ impl PushAuthorizationTests {
603 cleanup(); 603 cleanup();
604 return TestResult::new( 604 return TestResult::new(
605 test_name, 605 test_name,
606 "GRASP-01", 606 "GRASP-01:git-http:30",
607 "Push rejected when commit not in state event", 607 "Push rejected when commit not in state event",
608 ) 608 )
609 .fail(format!( 609 .fail(format!(
@@ -620,7 +620,7 @@ impl PushAuthorizationTests {
620 cleanup(); 620 cleanup();
621 return TestResult::new( 621 return TestResult::new(
622 test_name, 622 test_name,
623 "GRASP-01", 623 "GRASP-01:git-http:30",
624 "Push rejected when commit not in state event", 624 "Push rejected when commit not in state event",
625 ) 625 )
626 .fail(format!("Failed to create wrong commit: {}", e)); 626 .fail(format!("Failed to create wrong commit: {}", e));
@@ -634,10 +634,10 @@ impl PushAuthorizationTests {
634 cleanup(); 634 cleanup();
635 635
636 match push_result { 636 match push_result {
637 Ok(false) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").pass(), 637 Ok(false) => TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected when commit not in state event").pass(),
638 Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 638 Ok(true) => TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected when commit not in state event")
639 .fail("Push accepted but should be rejected. The pushed commit is not in the state event."), 639 .fail("Push accepted but should be rejected. The pushed commit is not in the state event."),
640 Err(e) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").fail(&e), 640 Err(e) => TestResult::new(test_name, "GRASP-01:git-http:30", "Push rejected when commit not in state event").fail(&e),
641 } 641 }
642 } 642 }
643 643
@@ -672,13 +672,13 @@ impl PushAuthorizationTests {
672 { 672 {
673 Ok(_maintainer_state_event) => TestResult::new( 673 Ok(_maintainer_state_event) => TestResult::new(
674 test_name, 674 test_name,
675 "GRASP-01", 675 "GRASP-01:git-http:30",
676 "Push authorized by maintainer state event only (no announcement)", 676 "Push authorized by maintainer state event only (no announcement)",
677 ) 677 )
678 .pass(), 678 .pass(),
679 Err(e) => TestResult::new( 679 Err(e) => TestResult::new(
680 test_name, 680 test_name,
681 "GRASP-01", 681 "GRASP-01:git-http:30",
682 "Push authorized by maintainer state event only (no announcement)", 682 "Push authorized by maintainer state event only (no announcement)",
683 ) 683 )
684 .fail(format!("{}", e)), 684 .fail(format!("{}", e)),
@@ -715,13 +715,13 @@ impl PushAuthorizationTests {
715 { 715 {
716 Ok(_recursive_maintainer_state_event) => TestResult::new( 716 Ok(_recursive_maintainer_state_event) => TestResult::new(
717 test_name, 717 test_name,
718 "GRASP-01", 718 "GRASP-01:git-http:30",
719 "Push authorized by recursive maintainer state event", 719 "Push authorized by recursive maintainer state event",
720 ) 720 )
721 .pass(), 721 .pass(),
722 Err(e) => TestResult::new( 722 Err(e) => TestResult::new(
723 test_name, 723 test_name,
724 "GRASP-01", 724 "GRASP-01:git-http:30",
725 "Push authorized by recursive maintainer state event", 725 "Push authorized by recursive maintainer state event",
726 ) 726 )
727 .fail(format!("{}", e)), 727 .fail(format!("{}", e)),
@@ -765,7 +765,7 @@ impl PushAuthorizationTests {
765 Err(e) => { 765 Err(e) => {
766 return TestResult::new( 766 return TestResult::new(
767 test_name, 767 test_name,
768 "GRASP-01", 768 "GRASP-01:git-http:30",
769 "Non-maintainer state events ignored", 769 "Non-maintainer state events ignored",
770 ) 770 )
771 .fail(format!("Failed to get OwnerStateDataPushed fixture: {}", e)); 771 .fail(format!("Failed to get OwnerStateDataPushed fixture: {}", e));
@@ -783,7 +783,7 @@ impl PushAuthorizationTests {
783 None => { 783 None => {
784 return TestResult::new( 784 return TestResult::new(
785 test_name, 785 test_name,
786 "GRASP-01", 786 "GRASP-01:git-http:30",
787 "Non-maintainer state events ignored", 787 "Non-maintainer state events ignored",
788 ) 788 )
789 .fail("Missing repo_id in state event"); 789 .fail("Missing repo_id in state event");
@@ -795,7 +795,7 @@ impl PushAuthorizationTests {
795 Err(e) => { 795 Err(e) => {
796 return TestResult::new( 796 return TestResult::new(
797 test_name, 797 test_name,
798 "GRASP-01", 798 "GRASP-01:git-http:30",
799 "Non-maintainer state events ignored", 799 "Non-maintainer state events ignored",
800 ) 800 )
801 .fail(format!("Failed to convert pubkey to bech32: {}", e)); 801 .fail(format!("Failed to convert pubkey to bech32: {}", e));
@@ -810,7 +810,7 @@ impl PushAuthorizationTests {
810 Err(e) => { 810 Err(e) => {
811 return TestResult::new( 811 return TestResult::new(
812 test_name, 812 test_name,
813 "GRASP-01", 813 "GRASP-01:git-http:30",
814 "Non-maintainer state events ignored", 814 "Non-maintainer state events ignored",
815 ) 815 )
816 .fail(format!("Failed to clone repo: {}", e)); 816 .fail(format!("Failed to clone repo: {}", e));
@@ -832,7 +832,7 @@ impl PushAuthorizationTests {
832 cleanup(); 832 cleanup();
833 return TestResult::new( 833 return TestResult::new(
834 test_name, 834 test_name,
835 "GRASP-01", 835 "GRASP-01:git-http:30",
836 "Non-maintainer state events ignored", 836 "Non-maintainer state events ignored",
837 ) 837 )
838 .fail(format!("Failed to create commit: {}", e)); 838 .fail(format!("Failed to create commit: {}", e));
@@ -858,7 +858,7 @@ impl PushAuthorizationTests {
858 cleanup(); 858 cleanup();
859 return TestResult::new( 859 return TestResult::new(
860 test_name, 860 test_name,
861 "GRASP-01", 861 "GRASP-01:git-http:30",
862 "Non-maintainer state events ignored", 862 "Non-maintainer state events ignored",
863 ) 863 )
864 .fail(format!("Failed to build rogue state event: {}", e)); 864 .fail(format!("Failed to build rogue state event: {}", e));
@@ -868,7 +868,7 @@ impl PushAuthorizationTests {
868 // Send the rogue state event using the raw client to bypass AuditClient's key check 868 // Send the rogue state event using the raw client to bypass AuditClient's key check
869 if let Err(e) = client.client().send_event(&rogue_state).await { 869 if let Err(e) = client.client().send_event(&rogue_state).await {
870 cleanup(); 870 cleanup();
871 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 871 return TestResult::new(test_name, "GRASP-01:git-http:30", "Non-maintainer state events ignored")
872 .fail(format!("Failed to send rogue state event: {}", e)); 872 .fail(format!("Failed to send rogue state event: {}", e));
873 } 873 }
874 874
@@ -883,8 +883,8 @@ impl PushAuthorizationTests {
883 cleanup(); 883 cleanup();
884 884
885 match push_result { 885 match push_result {
886 Ok(false) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").pass(), 886 Ok(false) => TestResult::new(test_name, "GRASP-01:git-http:30", "Non-maintainer state events ignored").pass(),
887 Ok(true) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 887 Ok(true) => TestResult::new(test_name, "GRASP-01:git-http:30", "Non-maintainer state events ignored")
888 .fail(format!( 888 .fail(format!(
889 "Push accepted but should be rejected. A non-maintainer (pubkey: {}) published \ 889 "Push accepted but should be rejected. A non-maintainer (pubkey: {}) published \
890 a state event announcing commit {}, but the push was accepted. The relay should \ 890 a state event announcing commit {}, but the push was accepted. The relay should \
@@ -893,7 +893,7 @@ impl PushAuthorizationTests {
893 new_commit, 893 new_commit,
894 client.public_key() 894 client.public_key()
895 )), 895 )),
896 Err(e) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").fail(&e), 896 Err(e) => TestResult::new(test_name, "GRASP-01:git-http:30", "Non-maintainer state events ignored").fail(&e),
897 } 897 }
898 } 898 }
899 899
@@ -924,7 +924,7 @@ impl PushAuthorizationTests {
924 Err(e) => { 924 Err(e) => {
925 return TestResult::new( 925 return TestResult::new(
926 test_name, 926 test_name,
927 "GRASP-01", 927 "GRASP-01:git-http:34",
928 "Push to refs/nostr/<invalid-event-id> rejected", 928 "Push to refs/nostr/<invalid-event-id> rejected",
929 ) 929 )
930 .fail(format!("Failed to create repo: {}", e)); 930 .fail(format!("Failed to create repo: {}", e));
@@ -950,7 +950,7 @@ impl PushAuthorizationTests {
950 Err(e) => { 950 Err(e) => {
951 return TestResult::new( 951 return TestResult::new(
952 test_name, 952 test_name,
953 "GRASP-01", 953 "GRASP-01:git-http:34",
954 "Push to refs/nostr/<invalid-event-id> rejected", 954 "Push to refs/nostr/<invalid-event-id> rejected",
955 ) 955 )
956 .fail(&e); 956 .fail(&e);
@@ -965,7 +965,7 @@ impl PushAuthorizationTests {
965 cleanup(); 965 cleanup();
966 return TestResult::new( 966 return TestResult::new(
967 test_name, 967 test_name,
968 "GRASP-01", 968 "GRASP-01:git-http:34",
969 "Push to refs/nostr/<invalid-event-id> rejected", 969 "Push to refs/nostr/<invalid-event-id> rejected",
970 ) 970 )
971 .fail(&e); 971 .fail(&e);
@@ -984,13 +984,13 @@ impl PushAuthorizationTests {
984 match push_result { 984 match push_result {
985 Ok(false) => TestResult::new( 985 Ok(false) => TestResult::new(
986 test_name, 986 test_name,
987 "GRASP-01", 987 "GRASP-01:git-http:34",
988 "Push to refs/nostr/<invalid-event-id> rejected", 988 "Push to refs/nostr/<invalid-event-id> rejected",
989 ) 989 )
990 .pass(), 990 .pass(),
991 Ok(true) => TestResult::new( 991 Ok(true) => TestResult::new(
992 test_name, 992 test_name,
993 "GRASP-01", 993 "GRASP-01:git-http:34",
994 "Push to refs/nostr/<invalid-event-id> rejected", 994 "Push to refs/nostr/<invalid-event-id> rejected",
995 ) 995 )
996 .fail(format!( 996 .fail(format!(
@@ -1001,7 +1001,7 @@ impl PushAuthorizationTests {
1001 )), 1001 )),
1002 Err(e) => TestResult::new( 1002 Err(e) => TestResult::new(
1003 test_name, 1003 test_name,
1004 "GRASP-01", 1004 "GRASP-01:git-http:34",
1005 "Push to refs/nostr/<invalid-event-id> rejected", 1005 "Push to refs/nostr/<invalid-event-id> rejected",
1006 ) 1006 )
1007 .fail(format!("Push error: {}", e)), 1007 .fail(format!("Push error: {}", e)),
@@ -1035,8 +1035,8 @@ impl PushAuthorizationTests {
1035 .get_fixture(FixtureKind::PRWrongCommitPushedBeforeEvent) 1035 .get_fixture(FixtureKind::PRWrongCommitPushedBeforeEvent)
1036 .await 1036 .await
1037 { 1037 {
1038 Ok(_pr_event) => TestResult::new(test_name, "GRASP-01", desc).pass(), 1038 Ok(_pr_event) => TestResult::new(test_name, "GRASP-01:git-http:34", desc).pass(),
1039 Err(e) => TestResult::new(test_name, "GRASP-01", desc).fail(format!("{}", e)), 1039 Err(e) => TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e)),
1040 } 1040 }
1041 } 1041 }
1042 1042
@@ -1062,7 +1062,7 @@ impl PushAuthorizationTests {
1062 { 1062 {
1063 Ok(e) => e, 1063 Ok(e) => e,
1064 Err(e) => { 1064 Err(e) => {
1065 return TestResult::new(test_name, "GRASP-01", desc).fail(format!("{}", e)); 1065 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e));
1066 } 1066 }
1067 }; 1067 };
1068 1068
@@ -1072,7 +1072,7 @@ impl PushAuthorizationTests {
1072 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 1072 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
1073 Ok(r) => r, 1073 Ok(r) => r,
1074 Err(e) => { 1074 Err(e) => {
1075 return TestResult::new(test_name, "GRASP-01", desc).fail(format!("{}", e)); 1075 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e));
1076 } 1076 }
1077 }; 1077 };
1078 1078
@@ -1087,7 +1087,7 @@ impl PushAuthorizationTests {
1087 let owner_npub = match repo.pubkey.to_bech32() { 1087 let owner_npub = match repo.pubkey.to_bech32() {
1088 Ok(n) => n, 1088 Ok(n) => n,
1089 Err(e) => { 1089 Err(e) => {
1090 return TestResult::new(test_name, "GRASP-01", desc) 1090 return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
1091 .fail(format!("Failed to get owner npub: {}", e)); 1091 .fail(format!("Failed to get owner npub: {}", e));
1092 } 1092 }
1093 }; 1093 };
@@ -1096,7 +1096,7 @@ impl PushAuthorizationTests {
1096 let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { 1096 let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) {
1097 Ok(p) => p, 1097 Ok(p) => p,
1098 Err(e) => { 1098 Err(e) => {
1099 return TestResult::new(test_name, "GRASP-01", desc).fail(&e); 1099 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(&e);
1100 } 1100 }
1101 }; 1101 };
1102 1102
@@ -1106,7 +1106,7 @@ impl PushAuthorizationTests {
1106 Ok(exists) => exists, 1106 Ok(exists) => exists,
1107 Err(e) => { 1107 Err(e) => {
1108 let _ = fs::remove_dir_all(&clone_path); 1108 let _ = fs::remove_dir_all(&clone_path);
1109 return TestResult::new(test_name, "GRASP-01", desc).fail(&e); 1109 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(&e);
1110 } 1110 }
1111 }; 1111 };
1112 1112
@@ -1114,13 +1114,13 @@ impl PushAuthorizationTests {
1114 1114
1115 // Ref should be deleted since the pushed commit doesn't match the PR event's `c` tag 1115 // Ref should be deleted since the pushed commit doesn't match the PR event's `c` tag
1116 if refs_exist { 1116 if refs_exist {
1117 TestResult::new(test_name, "GRASP-01", desc).fail(format!( 1117 TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!(
1118 "Expected refs/nostr/{} to be deleted when PR event published with non-matching commit, \ 1118 "Expected refs/nostr/{} to be deleted when PR event published with non-matching commit, \
1119 but the ref still exists. The relay should delete refs that don't match the event's `c` tag.", 1119 but the ref still exists. The relay should delete refs that don't match the event's `c` tag.",
1120 pr_event_id 1120 pr_event_id
1121 )) 1121 ))
1122 } else { 1122 } else {
1123 TestResult::new(test_name, "GRASP-01", desc).pass() 1123 TestResult::new(test_name, "GRASP-01:git-http:34", desc).pass()
1124 } 1124 }
1125 } 1125 }
1126 1126
@@ -1146,7 +1146,7 @@ impl PushAuthorizationTests {
1146 { 1146 {
1147 Ok(e) => e, 1147 Ok(e) => e,
1148 Err(e) => { 1148 Err(e) => {
1149 return TestResult::new(test_name, "GRASP-01", desc).fail(format!("{}", e)); 1149 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e));
1150 } 1150 }
1151 }; 1151 };
1152 1152
@@ -1156,7 +1156,7 @@ impl PushAuthorizationTests {
1156 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 1156 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
1157 Ok(r) => r, 1157 Ok(r) => r,
1158 Err(e) => { 1158 Err(e) => {
1159 return TestResult::new(test_name, "GRASP-01", desc).fail(format!("{}", e)); 1159 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e));
1160 } 1160 }
1161 }; 1161 };
1162 1162
@@ -1171,7 +1171,7 @@ impl PushAuthorizationTests {
1171 let owner_npub = match repo.pubkey.to_bech32() { 1171 let owner_npub = match repo.pubkey.to_bech32() {
1172 Ok(n) => n, 1172 Ok(n) => n,
1173 Err(e) => { 1173 Err(e) => {
1174 return TestResult::new(test_name, "GRASP-01", desc) 1174 return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
1175 .fail(format!("Failed to get owner npub: {}", e)); 1175 .fail(format!("Failed to get owner npub: {}", e));
1176 } 1176 }
1177 }; 1177 };
@@ -1180,7 +1180,7 @@ impl PushAuthorizationTests {
1180 let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { 1180 let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) {
1181 Ok(p) => p, 1181 Ok(p) => p,
1182 Err(e) => { 1182 Err(e) => {
1183 return TestResult::new(test_name, "GRASP-01", desc).fail(&e); 1183 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(&e);
1184 } 1184 }
1185 }; 1185 };
1186 1186
@@ -1188,7 +1188,7 @@ impl PushAuthorizationTests {
1188 if let Err(e) = create_deterministic_commit_with_variant(&clone_path, CommitVariant::Owner) 1188 if let Err(e) = create_deterministic_commit_with_variant(&clone_path, CommitVariant::Owner)
1189 { 1189 {
1190 let _ = fs::remove_dir_all(&clone_path); 1190 let _ = fs::remove_dir_all(&clone_path);
1191 return TestResult::new(test_name, "GRASP-01", desc).fail(&e); 1191 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(&e);
1192 } 1192 }
1193 1193
1194 // Try to push with wrong commit (should be rejected since PR event exists) 1194 // Try to push with wrong commit (should be rejected since PR event exists)
@@ -1196,7 +1196,7 @@ impl PushAuthorizationTests {
1196 Ok(success) => success, 1196 Ok(success) => success,
1197 Err(e) => { 1197 Err(e) => {
1198 let _ = fs::remove_dir_all(&clone_path); 1198 let _ = fs::remove_dir_all(&clone_path);
1199 return TestResult::new(test_name, "GRASP-01", desc).fail(&e); 1199 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(&e);
1200 } 1200 }
1201 }; 1201 };
1202 1202
@@ -1204,11 +1204,11 @@ impl PushAuthorizationTests {
1204 1204
1205 // Should REJECT - PR event exists with different commit hash 1205 // Should REJECT - PR event exists with different commit hash
1206 if push_succeeded { 1206 if push_succeeded {
1207 return TestResult::new(test_name, "GRASP-01", desc) 1207 return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
1208 .fail("Push accepted (expected rejection due to commit hash mismatch)"); 1208 .fail("Push accepted (expected rejection due to commit hash mismatch)");
1209 } 1209 }
1210 1210
1211 TestResult::new(test_name, "GRASP-01", desc).pass() 1211 TestResult::new(test_name, "GRASP-01:git-http:34", desc).pass()
1212 } 1212 }
1213 1213
1214 /// Test 4: Push correct commit to refs/nostr/<pr-event-id> AFTER PR event exists 1214 /// Test 4: Push correct commit to refs/nostr/<pr-event-id> AFTER PR event exists
@@ -1233,7 +1233,7 @@ impl PushAuthorizationTests {
1233 { 1233 {
1234 Ok(e) => e, 1234 Ok(e) => e,
1235 Err(e) => { 1235 Err(e) => {
1236 return TestResult::new(test_name, "GRASP-01", desc).fail(format!("{}", e)); 1236 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e));
1237 } 1237 }
1238 }; 1238 };
1239 1239
@@ -1243,7 +1243,7 @@ impl PushAuthorizationTests {
1243 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 1243 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
1244 Ok(r) => r, 1244 Ok(r) => r,
1245 Err(e) => { 1245 Err(e) => {
1246 return TestResult::new(test_name, "GRASP-01", desc).fail(format!("{}", e)); 1246 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(format!("{}", e));
1247 } 1247 }
1248 }; 1248 };
1249 1249
@@ -1258,7 +1258,7 @@ impl PushAuthorizationTests {
1258 let owner_npub = match repo.pubkey.to_bech32() { 1258 let owner_npub = match repo.pubkey.to_bech32() {
1259 Ok(n) => n, 1259 Ok(n) => n,
1260 Err(e) => { 1260 Err(e) => {
1261 return TestResult::new(test_name, "GRASP-01", desc) 1261 return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
1262 .fail(format!("Failed to get owner npub: {}", e)); 1262 .fail(format!("Failed to get owner npub: {}", e));
1263 } 1263 }
1264 }; 1264 };
@@ -1267,14 +1267,14 @@ impl PushAuthorizationTests {
1267 let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) { 1267 let clone_path = match clone_repo(relay_domain, &owner_npub, &repo_id) {
1268 Ok(p) => p, 1268 Ok(p) => p,
1269 Err(e) => { 1269 Err(e) => {
1270 return TestResult::new(test_name, "GRASP-01", desc).fail(&e); 1270 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(&e);
1271 } 1271 }
1272 }; 1272 };
1273 1273
1274 // Create the CORRECT PR test commit (the one expected by PR event) 1274 // Create the CORRECT PR test commit (the one expected by PR event)
1275 if let Err(e) = reset_to_correct_pr_commit(&clone_path) { 1275 if let Err(e) = reset_to_correct_pr_commit(&clone_path) {
1276 let _ = fs::remove_dir_all(&clone_path); 1276 let _ = fs::remove_dir_all(&clone_path);
1277 return TestResult::new(test_name, "GRASP-01", desc).fail(&e); 1277 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(&e);
1278 } 1278 }
1279 1279
1280 // Push correct commit (should succeed) 1280 // Push correct commit (should succeed)
@@ -1282,7 +1282,7 @@ impl PushAuthorizationTests {
1282 Ok(success) => success, 1282 Ok(success) => success,
1283 Err(e) => { 1283 Err(e) => {
1284 let _ = fs::remove_dir_all(&clone_path); 1284 let _ = fs::remove_dir_all(&clone_path);
1285 return TestResult::new(test_name, "GRASP-01", desc).fail(&e); 1285 return TestResult::new(test_name, "GRASP-01:git-http:34", desc).fail(&e);
1286 } 1286 }
1287 }; 1287 };
1288 1288
@@ -1290,11 +1290,11 @@ impl PushAuthorizationTests {
1290 1290
1291 // Should ACCEPT - commit matches PR event's c tag 1291 // Should ACCEPT - commit matches PR event's c tag
1292 if !push_succeeded { 1292 if !push_succeeded {
1293 return TestResult::new(test_name, "GRASP-01", desc) 1293 return TestResult::new(test_name, "GRASP-01:git-http:34", desc)
1294 .fail("Push rejected (expected acceptance since commit matches PR event)"); 1294 .fail("Push rejected (expected acceptance since commit matches PR event)");
1295 } 1295 }
1296 1296
1297 TestResult::new(test_name, "GRASP-01", desc).pass() 1297 TestResult::new(test_name, "GRASP-01:git-http:34", desc).pass()
1298 } 1298 }
1299 1299
1300 /// Test that HEAD is set after a state event is published with an existing commit 1300 /// Test that HEAD is set after a state event is published with an existing commit
@@ -1331,7 +1331,7 @@ impl PushAuthorizationTests {
1331 { 1331 {
1332 Ok(e) => e, 1332 Ok(e) => e,
1333 Err(e) => { 1333 Err(e) => {
1334 return TestResult::new(test_name, "GRASP-01", desc).fail(format!( 1334 return TestResult::new(test_name, "GRASP-01:git-http:32", desc).fail(format!(
1335 "Failed to create HeadSetToDevelopBranch fixture: {}", 1335 "Failed to create HeadSetToDevelopBranch fixture: {}",
1336 e 1336 e
1337 )); 1337 ));
@@ -1344,7 +1344,7 @@ impl PushAuthorizationTests {
1344 let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 1344 let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
1345 Ok(e) => e, 1345 Ok(e) => e,
1346 Err(e) => { 1346 Err(e) => {
1347 return TestResult::new(test_name, "GRASP-01", desc) 1347 return TestResult::new(test_name, "GRASP-01:git-http:32", desc)
1348 .fail(format!("Failed to get ValidRepo fixture: {}", e)); 1348 .fail(format!("Failed to get ValidRepo fixture: {}", e));
1349 } 1349 }
1350 }; 1350 };
@@ -1357,7 +1357,7 @@ impl PushAuthorizationTests {
1357 { 1357 {
1358 Some(id) => id.to_string(), 1358 Some(id) => id.to_string(),
1359 None => { 1359 None => {
1360 return TestResult::new(test_name, "GRASP-01", desc) 1360 return TestResult::new(test_name, "GRASP-01:git-http:32", desc)
1361 .fail("Missing repo_id in ValidRepo"); 1361 .fail("Missing repo_id in ValidRepo");
1362 } 1362 }
1363 }; 1363 };
@@ -1365,7 +1365,7 @@ impl PushAuthorizationTests {
1365 let npub = match valid_repo.pubkey.to_bech32() { 1365 let npub = match valid_repo.pubkey.to_bech32() {
1366 Ok(n) => n, 1366 Ok(n) => n,
1367 Err(e) => { 1367 Err(e) => {
1368 return TestResult::new(test_name, "GRASP-01", desc) 1368 return TestResult::new(test_name, "GRASP-01:git-http:32", desc)
1369 .fail(format!("Failed to convert pubkey to bech32: {}", e)); 1369 .fail(format!("Failed to convert pubkey to bech32: {}", e));
1370 } 1370 }
1371 }; 1371 };
@@ -1377,16 +1377,16 @@ impl PushAuthorizationTests {
1377 match get_default_branch_from_info_refs(relay_domain, &npub, &repo_id).await { 1377 match get_default_branch_from_info_refs(relay_domain, &npub, &repo_id).await {
1378 Ok(branch) => branch, 1378 Ok(branch) => branch,
1379 Err(e) => { 1379 Err(e) => {
1380 return TestResult::new(test_name, "GRASP-01", desc) 1380 return TestResult::new(test_name, "GRASP-01:git-http:32", desc)
1381 .fail(format!("Failed to get default branch: {}", e)); 1381 .fail(format!("Failed to get default branch: {}", e));
1382 } 1382 }
1383 }; 1383 };
1384 1384
1385 // Verify HEAD points to refs/heads/develop 1385 // Verify HEAD points to refs/heads/develop
1386 if default_branch == "refs/heads/develop" { 1386 if default_branch == "refs/heads/develop" {
1387 TestResult::new(test_name, "GRASP-01", desc).pass() 1387 TestResult::new(test_name, "GRASP-01:git-http:32", desc).pass()
1388 } else { 1388 } else {
1389 TestResult::new(test_name, "GRASP-01", desc).fail(format!( 1389 TestResult::new(test_name, "GRASP-01:git-http:32", desc).fail(format!(
1390 "Expected HEAD to point to 'refs/heads/develop' but got '{}'. \ 1390 "Expected HEAD to point to 'refs/heads/develop' but got '{}'. \
1391 GRASP-01 requires: 'MUST set repository HEAD per repository state announcement \ 1391 GRASP-01 requires: 'MUST set repository HEAD per repository state announcement \
1392 as soon as the git data related to that branch has been received.'", 1392 as soon as the git data related to that branch has been received.'",
@@ -1435,7 +1435,7 @@ impl PushAuthorizationTests {
1435 let _develop_state = match ctx.get_fixture(FixtureKind::HeadSetToDevelopBranch).await { 1435 let _develop_state = match ctx.get_fixture(FixtureKind::HeadSetToDevelopBranch).await {
1436 Ok(e) => e, 1436 Ok(e) => e,
1437 Err(e) => { 1437 Err(e) => {
1438 return TestResult::new(test_name, "GRASP-01", desc).fail(format!( 1438 return TestResult::new(test_name, "GRASP-01:git-http:32", desc).fail(format!(
1439 "Failed to create HeadSetToDevelopBranch fixture: {}", 1439 "Failed to create HeadSetToDevelopBranch fixture: {}",
1440 e 1440 e
1441 )); 1441 ));
@@ -1448,7 +1448,7 @@ impl PushAuthorizationTests {
1448 let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 1448 let valid_repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
1449 Ok(e) => e, 1449 Ok(e) => e,
1450 Err(e) => { 1450 Err(e) => {
1451 return TestResult::new(test_name, "GRASP-01", desc) 1451 return TestResult::new(test_name, "GRASP-01:git-http:32", desc)
1452 .fail(format!("Failed to get ValidRepo fixture: {}", e)); 1452 .fail(format!("Failed to get ValidRepo fixture: {}", e));
1453 } 1453 }
1454 }; 1454 };
@@ -1461,7 +1461,7 @@ impl PushAuthorizationTests {
1461 { 1461 {
1462 Some(id) => id.to_string(), 1462 Some(id) => id.to_string(),
1463 None => { 1463 None => {
1464 return TestResult::new(test_name, "GRASP-01", desc) 1464 return TestResult::new(test_name, "GRASP-01:git-http:32", desc)
1465 .fail("Missing repo_id in ValidRepo"); 1465 .fail("Missing repo_id in ValidRepo");
1466 } 1466 }
1467 }; 1467 };
@@ -1469,7 +1469,7 @@ impl PushAuthorizationTests {
1469 let npub = match valid_repo.pubkey.to_bech32() { 1469 let npub = match valid_repo.pubkey.to_bech32() {
1470 Ok(n) => n, 1470 Ok(n) => n,
1471 Err(e) => { 1471 Err(e) => {
1472 return TestResult::new(test_name, "GRASP-01", desc) 1472 return TestResult::new(test_name, "GRASP-01:git-http:32", desc)
1473 .fail(format!("Failed to convert pubkey to bech32: {}", e)); 1473 .fail(format!("Failed to convert pubkey to bech32: {}", e));
1474 } 1474 }
1475 }; 1475 };
@@ -1480,7 +1480,7 @@ impl PushAuthorizationTests {
1480 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { 1480 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
1481 Ok(path) => path, 1481 Ok(path) => path,
1482 Err(e) => { 1482 Err(e) => {
1483 return TestResult::new(test_name, "GRASP-01", desc) 1483 return TestResult::new(test_name, "GRASP-01:git-http:32", desc)
1484 .fail(format!("Failed to clone repo: {}", e)); 1484 .fail(format!("Failed to clone repo: {}", e));
1485 } 1485 }
1486 }; 1486 };
@@ -1495,7 +1495,7 @@ impl PushAuthorizationTests {
1495 1495
1496 if let Err(e) = output { 1496 if let Err(e) = output {
1497 let _ = fs::remove_dir_all(&clone_path); 1497 let _ = fs::remove_dir_all(&clone_path);
1498 return TestResult::new(test_name, "GRASP-01", desc) 1498 return TestResult::new(test_name, "GRASP-01:git-http:32", desc)
1499 .fail(format!("Failed to create develop1 branch: {}", e)); 1499 .fail(format!("Failed to create develop1 branch: {}", e));
1500 } 1500 }
1501 1501
@@ -1504,7 +1504,7 @@ impl PushAuthorizationTests {
1504 Ok(hash) => hash, 1504 Ok(hash) => hash,
1505 Err(e) => { 1505 Err(e) => {
1506 let _ = fs::remove_dir_all(&clone_path); 1506 let _ = fs::remove_dir_all(&clone_path);
1507 return TestResult::new(test_name, "GRASP-01", desc) 1507 return TestResult::new(test_name, "GRASP-01:git-http:32", desc)
1508 .fail(format!("Failed to create commit: {}", e)); 1508 .fail(format!("Failed to create commit: {}", e));
1509 } 1509 }
1510 }; 1510 };
@@ -1529,7 +1529,7 @@ impl PushAuthorizationTests {
1529 Ok(e) => e, 1529 Ok(e) => e,
1530 Err(e) => { 1530 Err(e) => {
1531 let _ = fs::remove_dir_all(&clone_path); 1531 let _ = fs::remove_dir_all(&clone_path);
1532 return TestResult::new(test_name, "GRASP-01", desc) 1532 return TestResult::new(test_name, "GRASP-01:git-http:32", desc)
1533 .fail(format!("Failed to build state event: {}", e)); 1533 .fail(format!("Failed to build state event: {}", e));
1534 } 1534 }
1535 }; 1535 };
@@ -1537,7 +1537,7 @@ impl PushAuthorizationTests {
1537 // Send the state event (commit doesn't exist on relay yet) 1537 // Send the state event (commit doesn't exist on relay yet)
1538 if let Err(e) = client.send_event(state_event).await { 1538 if let Err(e) = client.send_event(state_event).await {
1539 let _ = fs::remove_dir_all(&clone_path); 1539 let _ = fs::remove_dir_all(&clone_path);
1540 return TestResult::new(test_name, "GRASP-01", desc) 1540 return TestResult::new(test_name, "GRASP-01:git-http:32", desc)
1541 .fail(format!("Failed to send state event: {}", e)); 1541 .fail(format!("Failed to send state event: {}", e));
1542 } 1542 }
1543 1543
@@ -1550,11 +1550,11 @@ impl PushAuthorizationTests {
1550 match push_result { 1550 match push_result {
1551 Ok(true) => { /* Push succeeded, continue to verify */ } 1551 Ok(true) => { /* Push succeeded, continue to verify */ }
1552 Ok(false) => { 1552 Ok(false) => {
1553 return TestResult::new(test_name, "GRASP-01", desc) 1553 return TestResult::new(test_name, "GRASP-01:git-http:32", desc)
1554 .fail("Push to refs/heads/develop1 was rejected"); 1554 .fail("Push to refs/heads/develop1 was rejected");
1555 } 1555 }
1556 Err(e) => { 1556 Err(e) => {
1557 return TestResult::new(test_name, "GRASP-01", desc) 1557 return TestResult::new(test_name, "GRASP-01:git-http:32", desc)
1558 .fail(format!("Failed to push develop1 branch: {}", e)); 1558 .fail(format!("Failed to push develop1 branch: {}", e));
1559 } 1559 }
1560 } 1560 }
@@ -1567,16 +1567,16 @@ impl PushAuthorizationTests {
1567 match get_default_branch_from_info_refs(relay_domain, &npub, &repo_id).await { 1567 match get_default_branch_from_info_refs(relay_domain, &npub, &repo_id).await {
1568 Ok(branch) => branch, 1568 Ok(branch) => branch,
1569 Err(e) => { 1569 Err(e) => {
1570 return TestResult::new(test_name, "GRASP-01", desc) 1570 return TestResult::new(test_name, "GRASP-01:git-http:32", desc)
1571 .fail(format!("Failed to get default branch: {}", e)); 1571 .fail(format!("Failed to get default branch: {}", e));
1572 } 1572 }
1573 }; 1573 };
1574 1574
1575 // Verify HEAD points to refs/heads/develop1 1575 // Verify HEAD points to refs/heads/develop1
1576 if default_branch == "refs/heads/develop1" { 1576 if default_branch == "refs/heads/develop1" {
1577 TestResult::new(test_name, "GRASP-01", desc).pass() 1577 TestResult::new(test_name, "GRASP-01:git-http:32", desc).pass()
1578 } else { 1578 } else {
1579 TestResult::new(test_name, "GRASP-01", desc).fail(format!( 1579 TestResult::new(test_name, "GRASP-01:git-http:32", desc).fail(format!(
1580 "Expected HEAD to point to 'refs/heads/develop1' but got '{}'. \ 1580 "Expected HEAD to point to 'refs/heads/develop1' but got '{}'. \
1581 GRASP-01 requires: 'MUST set repository HEAD per repository state announcement \ 1581 GRASP-01 requires: 'MUST set repository HEAD per repository state announcement \
1582 as soon as the git data related to that branch has been received.'", 1582 as soon as the git data related to that branch has been received.'",
diff --git a/grasp-audit/src/specs/grasp01/repository_creation.rs b/grasp-audit/src/specs/grasp01/repository_creation.rs
index 1014aa3..a99b0d2 100644
--- a/grasp-audit/src/specs/grasp01/repository_creation.rs
+++ b/grasp-audit/src/specs/grasp01/repository_creation.rs
@@ -36,6 +36,7 @@ impl RepositoryCreationTests {
36 /// Test that a bare repository is created when a valid announcement is accepted 36 /// Test that a bare repository is created when a valid announcement is accepted
37 /// and is accessible via Smart HTTP service 37 /// and is accessible via Smart HTTP service
38 /// 38 ///
39 /// Spec: Line 28 of ../grasp/01.md
39 /// This test verifies: 40 /// This test verifies:
40 /// 1. Sends a valid repository announcement via TestContext 41 /// 1. Sends a valid repository announcement via TestContext
41 /// 2. Verifies the announcement was accepted 42 /// 2. Verifies the announcement was accepted
@@ -54,7 +55,7 @@ impl RepositoryCreationTests {
54 Err(e) => { 55 Err(e) => {
55 return TestResult::new( 56 return TestResult::new(
56 test_name, 57 test_name,
57 "GRASP-01", 58 "GRASP-01:git-http:28",
58 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", 59 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
59 ) 60 )
60 .fail(format!("Failed to create repo fixture: {}", e)) 61 .fail(format!("Failed to create repo fixture: {}", e))
@@ -75,7 +76,7 @@ impl RepositoryCreationTests {
75 None => { 76 None => {
76 return TestResult::new( 77 return TestResult::new(
77 test_name, 78 test_name,
78 "GRASP-01", 79 "GRASP-01:git-http:28",
79 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", 80 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
80 ) 81 )
81 .fail("Repository announcement missing d tag") 82 .fail("Repository announcement missing d tag")
@@ -87,7 +88,7 @@ impl RepositoryCreationTests {
87 Err(e) => { 88 Err(e) => {
88 return TestResult::new( 89 return TestResult::new(
89 test_name, 90 test_name,
90 "GRASP-01", 91 "GRASP-01:git-http:28",
91 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", 92 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
92 ) 93 )
93 .fail(format!("Failed to convert pubkey to npub: {}", e)) 94 .fail(format!("Failed to convert pubkey to npub: {}", e))
@@ -98,7 +99,7 @@ impl RepositoryCreationTests {
98 if let Err(e) = check_repo_accessible_via_http(relay_domain, &npub, &repo_id).await { 99 if let Err(e) = check_repo_accessible_via_http(relay_domain, &npub, &repo_id).await {
99 return TestResult::new( 100 return TestResult::new(
100 test_name, 101 test_name,
101 "GRASP-01", 102 "GRASP-01:git-http:28",
102 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", 103 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
103 ) 104 )
104 .fail(format!("Repository not accessible via HTTP: {}", e)); 105 .fail(format!("Repository not accessible via HTTP: {}", e));
@@ -106,7 +107,7 @@ impl RepositoryCreationTests {
106 107
107 TestResult::new( 108 TestResult::new(
108 test_name, 109 test_name,
109 "GRASP-01", 110 "GRASP-01:git-http:28",
110 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", 111 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
111 ) 112 )
112 .pass() 113 .pass()
@@ -114,6 +115,7 @@ impl RepositoryCreationTests {
114 115
115 /// Test that a webpage is served for an existing repository 116 /// Test that a webpage is served for an existing repository
116 /// 117 ///
118 /// Spec: Line 38 of ../grasp/01.md
117 /// This test verifies: 119 /// This test verifies:
118 /// 1. Creates a valid repository announcement 120 /// 1. Creates a valid repository announcement
119 /// 2. Accesses the repository URL without git service parameters 121 /// 2. Accesses the repository URL without git service parameters
@@ -133,7 +135,7 @@ impl RepositoryCreationTests {
133 Err(e) => { 135 Err(e) => {
134 return TestResult::new( 136 return TestResult::new(
135 test_name, 137 test_name,
136 "GRASP-01", 138 "GRASP-01:git-http:38",
137 "Relay SHOULD serve a webpage for existing repositories", 139 "Relay SHOULD serve a webpage for existing repositories",
138 ) 140 )
139 .fail(format!("Failed to create repo fixture: {}", e)) 141 .fail(format!("Failed to create repo fixture: {}", e))
@@ -154,7 +156,7 @@ impl RepositoryCreationTests {
154 None => { 156 None => {
155 return TestResult::new( 157 return TestResult::new(
156 test_name, 158 test_name,
157 "GRASP-01", 159 "GRASP-01:git-http:38",
158 "Relay SHOULD serve a webpage for existing repositories", 160 "Relay SHOULD serve a webpage for existing repositories",
159 ) 161 )
160 .fail("Repository announcement missing d tag") 162 .fail("Repository announcement missing d tag")
@@ -166,7 +168,7 @@ impl RepositoryCreationTests {
166 Err(e) => { 168 Err(e) => {
167 return TestResult::new( 169 return TestResult::new(
168 test_name, 170 test_name,
169 "GRASP-01", 171 "GRASP-01:git-http:38",
170 "Relay SHOULD serve a webpage for existing repositories", 172 "Relay SHOULD serve a webpage for existing repositories",
171 ) 173 )
172 .fail(format!("Failed to convert pubkey to npub: {}", e)) 174 .fail(format!("Failed to convert pubkey to npub: {}", e))
@@ -177,7 +179,7 @@ impl RepositoryCreationTests {
177 if let Err(e) = check_webpage_served(relay_domain, &npub, &repo_id).await { 179 if let Err(e) = check_webpage_served(relay_domain, &npub, &repo_id).await {
178 return TestResult::new( 180 return TestResult::new(
179 test_name, 181 test_name,
180 "GRASP-01", 182 "GRASP-01:git-http:38",
181 "Relay SHOULD serve a webpage for existing repositories", 183 "Relay SHOULD serve a webpage for existing repositories",
182 ) 184 )
183 .fail(format!("Webpage not served: {}", e)); 185 .fail(format!("Webpage not served: {}", e));
@@ -185,7 +187,7 @@ impl RepositoryCreationTests {
185 187
186 TestResult::new( 188 TestResult::new(
187 test_name, 189 test_name,
188 "GRASP-01", 190 "GRASP-01:git-http:38",
189 "Relay SHOULD serve a webpage for existing repositories", 191 "Relay SHOULD serve a webpage for existing repositories",
190 ) 192 )
191 .pass() 193 .pass()
@@ -193,6 +195,7 @@ impl RepositoryCreationTests {
193 195
194 /// Test that 404 is returned for non-existent repositories 196 /// Test that 404 is returned for non-existent repositories
195 /// 197 ///
198 /// Spec: Line 38 of ../grasp/01.md
196 /// This test verifies: 199 /// This test verifies:
197 /// 1. Accesses a URL for a repository that doesn't exist 200 /// 1. Accesses a URL for a repository that doesn't exist
198 /// 2. Verifies a 404 status is returned 201 /// 2. Verifies a 404 status is returned
@@ -211,8 +214,8 @@ impl RepositoryCreationTests {
211 Err(e) => { 214 Err(e) => {
212 return TestResult::new( 215 return TestResult::new(
213 test_name, 216 test_name,
214 "GRASP-01", 217 "GRASP-01:git-http:38",
215 "Relay SHOULD serve a webpage for existing repositories", 218 "Relay SHOULD return 404 for repositories it doesn't host",
216 ) 219 )
217 .fail(format!("Failed to create repo fixture: {}", e)) 220 .fail(format!("Failed to create repo fixture: {}", e))
218 } 221 }
@@ -223,8 +226,8 @@ impl RepositoryCreationTests {
223 Err(e) => { 226 Err(e) => {
224 return TestResult::new( 227 return TestResult::new(
225 test_name, 228 test_name,
226 "GRASP-01", 229 "GRASP-01:git-http:38",
227 "Relay SHOULD serve a webpage for existing repositories", 230 "Relay SHOULD return 404 for repositories it doesn't host",
228 ) 231 )
229 .fail(format!("Failed to convert pubkey to npub: {}", e)) 232 .fail(format!("Failed to convert pubkey to npub: {}", e))
230 } 233 }
@@ -236,7 +239,7 @@ impl RepositoryCreationTests {
236 if let Err(e) = check_404_for_nonexistent_repo(relay_domain, &npub, fake_repo_id).await { 239 if let Err(e) = check_404_for_nonexistent_repo(relay_domain, &npub, fake_repo_id).await {
237 return TestResult::new( 240 return TestResult::new(
238 test_name, 241 test_name,
239 "GRASP-01", 242 "GRASP-01:git-http:38",
240 "Relay SHOULD return 404 for repositories it doesn't host", 243 "Relay SHOULD return 404 for repositories it doesn't host",
241 ) 244 )
242 .fail(format!("Expected 404, got: {}", e)); 245 .fail(format!("Expected 404, got: {}", e));
@@ -244,7 +247,7 @@ impl RepositoryCreationTests {
244 247
245 TestResult::new( 248 TestResult::new(
246 test_name, 249 test_name,
247 "GRASP-01", 250 "GRASP-01:git-http:38",
248 "Relay SHOULD return 404 for repositories it doesn't host", 251 "Relay SHOULD return 404 for repositories it doesn't host",
249 ) 252 )
250 .pass() 253 .pass()
diff --git a/grasp-audit/src/specs/grasp01/spec_requirements.rs b/grasp-audit/src/specs/grasp01/spec_requirements.rs
new file mode 100644
index 0000000..591fef1
--- /dev/null
+++ b/grasp-audit/src/specs/grasp01/spec_requirements.rs
@@ -0,0 +1,209 @@
1//! GRASP-01 Specification Requirements
2//!
3//! Embedded specification requirements from the GRASP-01 spec document.
4//! This is the single source of truth for spec text displayed in audit reports.
5
6/// GRASP spec repository commit ID that this version is based on
7pub const GRASP_COMMIT_ID: &str = "1fdb8f7";
8
9/// A single specification requirement
10#[derive(Debug, Clone)]
11pub struct SpecRequirement {
12 /// Line number in the spec document
13 pub line: u32,
14 /// Section name (e.g., "Nostr Relay", "Git Smart HTTP Service", "CORS Support")
15 pub section: &'static str,
16 /// The full requirement text
17 pub text: &'static str,
18 /// Requirement level: MUST, SHOULD, or MAY
19 pub level: RequirementLevel,
20}
21
22/// Requirement level per RFC 2119
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum RequirementLevel {
25 Must,
26 Should,
27 May,
28}
29
30impl std::fmt::Display for RequirementLevel {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 match self {
33 RequirementLevel::Must => write!(f, "MUST"),
34 RequirementLevel::Should => write!(f, "SHOULD"),
35 RequirementLevel::May => write!(f, "MAY"),
36 }
37 }
38}
39
40/// All GRASP-01 specification requirements
41pub const GRASP_01_REQUIREMENTS: &[SpecRequirement] = &[
42 // Nostr Relay section
43 SpecRequirement {
44 line: 7,
45 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.",
47 level: RequirementLevel::Must,
48 },
49 SpecRequirement {
50 line: 9,
51 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`.",
53 level: RequirementLevel::Must,
54 },
55 SpecRequirement {
56 line: 11,
57 section: "Nostr Relay",
58 text: "MAY reject git repository announcements based on other criteria such as pre-payment, quotas, WoT, whitelist, SPAM prevention, etc.",
59 level: RequirementLevel::May,
60 },
61 SpecRequirement {
62 line: 13,
63 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",
65 level: RequirementLevel::Must,
66 },
67 SpecRequirement {
68 line: 18,
69 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.",
71 level: RequirementLevel::May,
72 },
73 SpecRequirement {
74 line: 20,
75 section: "Nostr Relay",
76 text: "MUST serve a NIP-11 document",
77 level: RequirementLevel::Must,
78 },
79 SpecRequirement {
80 line: 22,
81 section: "Nostr Relay",
82 text: "MUST list each supported GRASP under `supported_grasps` in format `GRASP-XX` eg `GRASP-01` as a string array",
83 level: RequirementLevel::Must,
84 },
85 SpecRequirement {
86 line: 23,
87 section: "Nostr Relay",
88 text: "MUST list repository acceptance criteria under `repo_acceptance_criteria` as a human readable string",
89 level: RequirementLevel::Must,
90 },
91 SpecRequirement {
92 line: 24,
93 section: "Nostr Relay",
94 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,
96 },
97 // Git Smart HTTP Service section
98 SpecRequirement {
99 line: 28,
100 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.",
102 level: RequirementLevel::Must,
103 },
104 SpecRequirement {
105 line: 30,
106 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.",
108 level: RequirementLevel::Must,
109 },
110 SpecRequirement {
111 line: 32,
112 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.",
114 level: RequirementLevel::Must,
115 },
116 SpecRequirement {
117 line: 34,
118 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.",
120 level: RequirementLevel::Must,
121 },
122 SpecRequirement {
123 line: 36,
124 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.",
126 level: RequirementLevel::Must,
127 },
128 SpecRequirement {
129 line: 38,
130 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.",
132 level: RequirementLevel::Should,
133 },
134 // CORS Support section
135 SpecRequirement {
136 line: 44,
137 section: "CORS Support",
138 text: "Set `Access-Control-Allow-Origin: *` on ALL responses",
139 level: RequirementLevel::Must,
140 },
141 SpecRequirement {
142 line: 45,
143 section: "CORS Support",
144 text: "Set `Access-Control-Allow-Methods: GET, POST` on ALL responses",
145 level: RequirementLevel::Must,
146 },
147 SpecRequirement {
148 line: 46,
149 section: "CORS Support",
150 text: "Set `Access-Control-Allow-Headers: Content-Type` on ALL responses",
151 level: RequirementLevel::Must,
152 },
153 SpecRequirement {
154 line: 47,
155 section: "CORS Support",
156 text: "Respond to OPTIONS requests with 204 No Content",
157 level: RequirementLevel::Must,
158 },
159];
160
161/// Get a requirement by line number
162pub fn get_requirement(line: u32) -> Option<&'static SpecRequirement> {
163 GRASP_01_REQUIREMENTS.iter().find(|r| r.line == line)
164}
165
166/// Get all requirements for a section
167pub fn get_requirements_for_section(section: &str) -> Vec<&'static SpecRequirement> {
168 GRASP_01_REQUIREMENTS
169 .iter()
170 .filter(|r| r.section == section)
171 .collect()
172}
173
174/// Get all unique section names in order
175pub fn get_sections() -> Vec<&'static str> {
176 let mut sections = Vec::new();
177 for req in GRASP_01_REQUIREMENTS {
178 if !sections.contains(&req.section) {
179 sections.push(req.section);
180 }
181 }
182 sections
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_get_requirement() {
191 let req = get_requirement(7).expect("Line 7 should exist");
192 assert_eq!(req.section, "Nostr Relay");
193 assert!(req.text.contains("NIP-01"));
194 }
195
196 #[test]
197 fn test_get_sections() {
198 let sections = get_sections();
199 assert_eq!(sections.len(), 3);
200 assert_eq!(sections[0], "Nostr Relay");
201 assert_eq!(sections[1], "Git Smart HTTP Service");
202 assert_eq!(sections[2], "CORS Support");
203 }
204
205 #[test]
206 fn test_requirement_count() {
207 assert_eq!(GRASP_01_REQUIREMENTS.len(), 19);
208 }
209} \ No newline at end of file