upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 14:31:32 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 15:22:38 +0000
commitd2ac69816567f092fe0d4661723bc43778cb481b (patch)
treee8b51b61a6a7b0ab1a214adebe4e237143b01f0b
parent7a78815e29b01c83f3d0ec195ba717a2eba8cd37 (diff)
fix cargo clippy and fmt warnings
-rw-r--r--grasp-audit/src/bin/grasp-audit.rs16
-rw-r--r--grasp-audit/src/lib.rs22
-rw-r--r--grasp-audit/src/result.rs12
-rw-r--r--grasp-audit/src/specs/grasp01/cors.rs51
-rw-r--r--grasp-audit/src/specs/grasp01/event_acceptance_policy.rs62
-rw-r--r--grasp-audit/src/specs/grasp01/git_clone.rs54
-rw-r--r--grasp-audit/src/specs/grasp01/mod.rs2
-rw-r--r--grasp-audit/src/specs/grasp01/nip01_smoke.rs32
-rw-r--r--grasp-audit/src/specs/grasp01/nip11_document.rs61
-rw-r--r--grasp-audit/src/specs/grasp01/push_authorization.rs10
-rw-r--r--grasp-audit/src/specs/grasp01/repository_creation.rs20
-rw-r--r--src/config.rs8
-rw-r--r--src/git/handlers.rs1
-rw-r--r--src/git/mod.rs13
-rw-r--r--src/git/protocol.rs18
-rw-r--r--src/git/subprocess.rs34
-rw-r--r--src/http/mod.rs125
-rw-r--r--src/http/nip11.rs34
-rw-r--r--src/nostr/builder.rs14
-rw-r--r--src/nostr/events.rs18
-rw-r--r--tests/common/relay.rs45
-rw-r--r--tests/cors.rs31
-rw-r--r--tests/git_clone.rs8
-rw-r--r--tests/nip11_document.rs2
-rw-r--r--tests/nip34_announcements.rs2
-rw-r--r--tests/push_authorization.rs7
26 files changed, 300 insertions, 402 deletions
diff --git a/grasp-audit/src/bin/grasp-audit.rs b/grasp-audit/src/bin/grasp-audit.rs
index 2aabefe..48c1580 100644
--- a/grasp-audit/src/bin/grasp-audit.rs
+++ b/grasp-audit/src/bin/grasp-audit.rs
@@ -47,14 +47,18 @@ async fn main() -> Result<()> {
47 let cli = Cli::parse(); 47 let cli = Cli::parse();
48 48
49 match cli.command { 49 match cli.command {
50 Commands::Audit { relay, mode, spec, git_data_dir } => { 50 Commands::Audit {
51 51 relay,
52 mode,
53 spec,
54 git_data_dir,
55 } => {
52 let mut config = match mode.as_str() { 56 let mut config = match mode.as_str() {
53 "ci" => AuditConfig::ci(), 57 "ci" => AuditConfig::ci(),
54 "production" => AuditConfig::production(), 58 "production" => AuditConfig::production(),
55 _ => return Err(anyhow!("Invalid mode: {}. Use 'ci' or 'production'", mode)), 59 _ => return Err(anyhow!("Invalid mode: {}. Use 'ci' or 'production'", mode)),
56 }; 60 };
57 61
58 // Audit needs to create events to test the relay, so disable read-only mode 62 // Audit needs to create events to test the relay, so disable read-only mode
59 config.read_only = false; 63 config.read_only = false;
60 64
@@ -145,17 +149,17 @@ async fn main() -> Result<()> {
145 println!(" → NIP-01 smoke tests..."); 149 println!(" → NIP-01 smoke tests...");
146 let nip01_results = specs::Nip01SmokeTests::run_all(&client).await; 150 let nip01_results = specs::Nip01SmokeTests::run_all(&client).await;
147 all_results.merge(nip01_results); 151 all_results.merge(nip01_results);
148 152
149 // NIP-11 document tests 153 // NIP-11 document tests
150 println!(" → NIP-11 document tests..."); 154 println!(" → NIP-11 document tests...");
151 let nip11_results = specs::Nip11DocumentTests::run_all(&client).await; 155 let nip11_results = specs::Nip11DocumentTests::run_all(&client).await;
152 all_results.merge(nip11_results); 156 all_results.merge(nip11_results);
153 157
154 // CORS tests 158 // CORS tests
155 println!(" → CORS tests..."); 159 println!(" → CORS tests...");
156 let cors_results = specs::CorsTests::run_all(&client, &relay_domain).await; 160 let cors_results = specs::CorsTests::run_all(&client, &relay_domain).await;
157 all_results.merge(cors_results); 161 all_results.merge(cors_results);
158 162
159 println!(); 163 println!();
160 all_results 164 all_results
161 } 165 }
diff --git a/grasp-audit/src/lib.rs b/grasp-audit/src/lib.rs
index fb52ba7..6df240f 100644
--- a/grasp-audit/src/lib.rs
+++ b/grasp-audit/src/lib.rs
@@ -39,14 +39,24 @@ pub use audit::{AuditConfig, AuditEventBuilder, AuditMode};
39pub use client::AuditClient; 39pub use client::AuditClient;
40pub use fixtures::{ 40pub use fixtures::{
41 // Git operation helpers 41 // Git operation helpers
42 clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, 42 clone_repo,
43 try_push, try_push_to_ref, 43 create_commit,
44 create_deterministic_commit,
45 create_deterministic_commit_with_variant,
44 // Verification helpers 46 // Verification helpers
45 send_and_verify_accepted, send_and_verify_rejected, 47 send_and_verify_accepted,
48 send_and_verify_rejected,
49 try_push,
50 try_push_to_ref,
46 // Types and constants 51 // Types and constants
47 CommitVariant, ContextMode, FixtureKind, TestContext, 52 CommitVariant,
48 DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH, 53 ContextMode,
49 PR_TEST_COMMIT_HASH, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, 54 FixtureKind,
55 TestContext,
56 DETERMINISTIC_COMMIT_HASH,
57 MAINTAINER_DETERMINISTIC_COMMIT_HASH,
58 PR_TEST_COMMIT_HASH,
59 RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH,
50}; 60};
51pub use result::{AuditResult, TestResult}; 61pub use result::{AuditResult, TestResult};
52 62
diff --git a/grasp-audit/src/result.rs b/grasp-audit/src/result.rs
index 2bec5c8..bc0008a 100644
--- a/grasp-audit/src/result.rs
+++ b/grasp-audit/src/result.rs
@@ -17,7 +17,12 @@ fn extract_spec_category(spec_ref: &str) -> String {
17 if parts.len() >= 2 { 17 if parts.len() >= 2 {
18 // Check if the last part looks like a test number (starts with digit) 18 // Check if the last part looks like a test number (starts with digit)
19 if let Some(last) = parts.last() { 19 if let Some(last) = parts.last() {
20 if last.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) { 20 if last
21 .chars()
22 .next()
23 .map(|c| c.is_ascii_digit())
24 .unwrap_or(false)
25 {
21 // Remove the trailing number part 26 // Remove the trailing number part
22 return parts[..parts.len() - 1].join(":"); 27 return parts[..parts.len() - 1].join(":");
23 } 28 }
@@ -146,10 +151,7 @@ impl AuditResult {
146 for result in &self.results { 151 for result in &self.results {
147 // Extract category from spec_ref (e.g., "GRASP-01:event-acceptance:1.1" -> "GRASP-01:event-acceptance") 152 // Extract category from spec_ref (e.g., "GRASP-01:event-acceptance:1.1" -> "GRASP-01:event-acceptance")
148 let category = extract_spec_category(&result.spec_ref); 153 let category = extract_spec_category(&result.spec_ref);
149 grouped 154 grouped.entry(category).or_default().push(result);
150 .entry(category)
151 .or_default()
152 .push(result);
153 } 155 }
154 156
155 // Print grouped results 157 // Print grouped results
diff --git a/grasp-audit/src/specs/grasp01/cors.rs b/grasp-audit/src/specs/grasp01/cors.rs
index 08c5ab6..c877c04 100644
--- a/grasp-audit/src/specs/grasp01/cors.rs
+++ b/grasp-audit/src/specs/grasp01/cors.rs
@@ -16,7 +16,6 @@
16 16
17use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult}; 17use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult};
18use nostr_sdk::prelude::*; 18use nostr_sdk::prelude::*;
19use std::path::Path;
20 19
21pub struct CorsTests; 20pub struct CorsTests;
22 21
@@ -42,10 +41,7 @@ impl CorsTests {
42 /// 41 ///
43 /// Spec: Line 44 of ../grasp/01.md 42 /// Spec: Line 44 of ../grasp/01.md
44 /// Requirement: Set `Access-Control-Allow-Origin: *` on ALL responses 43 /// Requirement: Set `Access-Control-Allow-Origin: *` on ALL responses
45 pub async fn test_cors_allow_origin( 44 pub async fn test_cors_allow_origin(_client: &AuditClient, relay_domain: &str) -> TestResult {
46 _client: &AuditClient,
47 relay_domain: &str,
48 ) -> TestResult {
49 TestResult::new( 45 TestResult::new(
50 "cors_allow_origin", 46 "cors_allow_origin",
51 "GRASP-01:git-http:cors:44", 47 "GRASP-01:git-http:cors:44",
@@ -91,10 +87,7 @@ impl CorsTests {
91 /// 87 ///
92 /// Spec: Line 45 of ../grasp/01.md 88 /// Spec: Line 45 of ../grasp/01.md
93 /// Requirement: Set `Access-Control-Allow-Methods: GET, POST` on ALL responses 89 /// Requirement: Set `Access-Control-Allow-Methods: GET, POST` on ALL responses
94 pub async fn test_cors_allow_methods( 90 pub async fn test_cors_allow_methods(_client: &AuditClient, relay_domain: &str) -> TestResult {
95 _client: &AuditClient,
96 relay_domain: &str,
97 ) -> TestResult {
98 TestResult::new( 91 TestResult::new(
99 "cors_allow_methods", 92 "cors_allow_methods",
100 "GRASP-01:git-http:cors:45", 93 "GRASP-01:git-http:cors:45",
@@ -138,10 +131,7 @@ impl CorsTests {
138 /// 131 ///
139 /// Spec: Line 46 of ../grasp/01.md 132 /// Spec: Line 46 of ../grasp/01.md
140 /// Requirement: Set `Access-Control-Allow-Headers: Content-Type` on ALL responses 133 /// Requirement: Set `Access-Control-Allow-Headers: Content-Type` on ALL responses
141 pub async fn test_cors_allow_headers( 134 pub async fn test_cors_allow_headers(_client: &AuditClient, relay_domain: &str) -> TestResult {
142 _client: &AuditClient,
143 relay_domain: &str,
144 ) -> TestResult {
145 TestResult::new( 135 TestResult::new(
146 "cors_allow_headers", 136 "cors_allow_headers",
147 "GRASP-01:git-http:cors:46", 137 "GRASP-01:git-http:cors:46",
@@ -212,10 +202,8 @@ impl CorsTests {
212 check_options_response(&response, "root endpoint")?; 202 check_options_response(&response, "root endpoint")?;
213 203
214 // 2. Test OPTIONS on git-upload-pack endpoint 204 // 2. Test OPTIONS on git-upload-pack endpoint
215 let repo_url = format!( 205 let repo_url =
216 "http://{}/npub1test/test.git/git-upload-pack", 206 format!("http://{}/npub1test/test.git/git-upload-pack", relay_domain);
217 relay_domain
218 );
219 let response = http_client 207 let response = http_client
220 .request(reqwest::Method::OPTIONS, &repo_url) 208 .request(reqwest::Method::OPTIONS, &repo_url)
221 .header("Origin", "https://example.com") 209 .header("Origin", "https://example.com")
@@ -227,10 +215,7 @@ impl CorsTests {
227 check_options_response(&response, "git-upload-pack endpoint")?; 215 check_options_response(&response, "git-upload-pack endpoint")?;
228 216
229 // 3. Test OPTIONS on info/refs endpoint 217 // 3. Test OPTIONS on info/refs endpoint
230 let refs_url = format!( 218 let refs_url = format!("http://{}/npub1test/test.git/info/refs", relay_domain);
231 "http://{}/npub1test/test.git/info/refs",
232 relay_domain
233 );
234 let response = http_client 219 let response = http_client
235 .request(reqwest::Method::OPTIONS, &refs_url) 220 .request(reqwest::Method::OPTIONS, &refs_url)
236 .header("Origin", "https://example.com") 221 .header("Origin", "https://example.com")
@@ -255,10 +240,7 @@ impl CorsTests {
255 /// Integration test: CORS Allow-Origin header with repository creation 240 /// Integration test: CORS Allow-Origin header with repository creation
256 /// 241 ///
257 /// For integration tests that want to test against real repositories 242 /// For integration tests that want to test against real repositories
258 pub async fn test_cors_on_real_repo( 243 pub async fn test_cors_on_real_repo(client: &AuditClient, relay_domain: &str) -> TestResult {
259 client: &AuditClient,
260 relay_domain: &str,
261 ) -> TestResult {
262 let test_name = "test_cors_on_real_repo"; 244 let test_name = "test_cors_on_real_repo";
263 let ctx = TestContext::new(client); 245 let ctx = TestContext::new(client);
264 246
@@ -271,7 +253,7 @@ impl CorsTests {
271 "GRASP-01", 253 "GRASP-01",
272 "CORS headers on real repository endpoint", 254 "CORS headers on real repository endpoint",
273 ) 255 )
274 .fail(&format!("Failed to create repo fixture: {}", e)) 256 .fail(format!("Failed to create repo fixture: {}", e))
275 } 257 }
276 }; 258 };
277 259
@@ -304,7 +286,7 @@ impl CorsTests {
304 "GRASP-01", 286 "GRASP-01",
305 "CORS headers on real repository endpoint", 287 "CORS headers on real repository endpoint",
306 ) 288 )
307 .fail(&format!("Failed to convert pubkey to npub: {}", e)) 289 .fail(format!("Failed to convert pubkey to npub: {}", e))
308 } 290 }
309 }; 291 };
310 292
@@ -323,7 +305,7 @@ impl CorsTests {
323 "GRASP-01", 305 "GRASP-01",
324 "CORS headers on real repository endpoint", 306 "CORS headers on real repository endpoint",
325 ) 307 )
326 .fail(&format!("Failed to GET info/refs: {}", e)) 308 .fail(format!("Failed to GET info/refs: {}", e))
327 } 309 }
328 }; 310 };
329 311
@@ -492,15 +474,6 @@ mod tests {
492 results.print_report(); 474 results.print_report();
493 475
494 // Assert all tests passed 476 // Assert all tests passed
495 assert!( 477 assert!(results.all_passed(), "Some GRASP-01 CORS tests failed");
496 results.all_passed(),
497 "Some GRASP-01 CORS tests failed"
498 );
499 } 478 }
500 479}
501 #[test]
502 fn test_module_exists() {
503 // Simple compilation test
504 assert!(true);
505 }
506} \ 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 3a8f18d..1fc7f73 100644
--- a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs
+++ b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs
@@ -247,7 +247,9 @@ impl EventAcceptancePolicyTests {
247 /// 247 ///
248 /// Spec: Line 5 of ../grasp/01.md 248 /// Spec: Line 5 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(client: &AuditClient) -> TestResult { 250 pub async fn test_reject_repo_announcement_missing_clone_tag(
251 client: &AuditClient,
252 ) -> TestResult {
251 TestResult::new( 253 TestResult::new(
252 "reject_repo_announcement_missing_clone_tag", 254 "reject_repo_announcement_missing_clone_tag",
253 "GRASP-01:nostr-relay:5", 255 "GRASP-01:nostr-relay:5",
@@ -321,7 +323,9 @@ impl EventAcceptancePolicyTests {
321 /// 323 ///
322 /// Spec: Line 5 of ../grasp/01.md 324 /// Spec: Line 5 of ../grasp/01.md
323 /// Requirement: MUST reject announcements not listing service in relays 325 /// Requirement: MUST reject announcements not listing service in relays
324 pub async fn test_reject_repo_announcement_missing_relays_tag(client: &AuditClient) -> TestResult { 326 pub async fn test_reject_repo_announcement_missing_relays_tag(
327 client: &AuditClient,
328 ) -> TestResult {
325 TestResult::new( 329 TestResult::new(
326 "reject_repo_announcement_missing_relays_tag", 330 "reject_repo_announcement_missing_relays_tag",
327 "GRASP-01:nostr-relay:5", 331 "GRASP-01:nostr-relay:5",
@@ -546,8 +550,7 @@ impl EventAcceptancePolicyTests {
546 let issue = Self::create_issue_for_repo(client, &repo, "Test Issue 1")?; 550 let issue = Self::create_issue_for_repo(client, &repo, "Test Issue 1")?;
547 551
548 // 3. Send issue and verify it's accepted 552 // 3. Send issue and verify it's accepted
549 send_and_verify_accepted(client, issue, "issue referencing repo via 'a' tag") 553 send_and_verify_accepted(client, issue, "issue referencing repo via 'a' tag").await?;
550 .await?;
551 554
552 Ok(()) 555 Ok(())
553 }) 556 })
@@ -693,8 +696,7 @@ impl EventAcceptancePolicyTests {
693 .map_err(|e| format!("Failed to build issue B: {}", e))?; 696 .map_err(|e| format!("Failed to build issue B: {}", e))?;
694 697
695 // Send Issue B and verify it's ACCEPTED (via transitive quote to Issue A) 698 // Send Issue B and verify it's ACCEPTED (via transitive quote to Issue A)
696 send_and_verify_accepted(client, issue_b, "issue B quoting accepted issue A") 699 send_and_verify_accepted(client, issue_b, "issue B quoting accepted issue A").await?;
697 .await?;
698 700
699 Ok(()) 701 Ok(())
700 }) 702 })
@@ -772,8 +774,7 @@ impl EventAcceptancePolicyTests {
772 .build(client.keys()) 774 .build(client.keys())
773 .map_err(|e| format!("Failed to build kind1 A: {}", e))?; 775 .map_err(|e| format!("Failed to build kind1 A: {}", e))?;
774 776
775 send_and_verify_accepted(client, kind1_a.clone(), "kind 1 A quoting repo") 777 send_and_verify_accepted(client, kind1_a.clone(), "kind 1 A quoting repo").await?;
776 .await?;
777 778
778 // Create Kind 1 B that replies to Kind 1 A via 'e' tag 779 // Create Kind 1 B that replies to Kind 1 A via 'e' tag
779 let kind1_b = client 780 let kind1_b = client
@@ -783,12 +784,8 @@ impl EventAcceptancePolicyTests {
783 .map_err(|e| format!("Failed to build kind1 B: {}", e))?; 784 .map_err(|e| format!("Failed to build kind1 B: {}", e))?;
784 785
785 // Send Kind 1 B and verify it's accepted (via 'e' tag to accepted kind 1 A) 786 // Send Kind 1 B and verify it's accepted (via 'e' tag to accepted kind 1 A)
786 send_and_verify_accepted( 787 send_and_verify_accepted(client, kind1_b, "kind 1 B replying to accepted kind 1 A")
787 client, 788 .await?;
788 kind1_b,
789 "kind 1 B replying to accepted kind 1 A",
790 )
791 .await?;
792 789
793 Ok(()) 790 Ok(())
794 }) 791 })
@@ -828,17 +825,19 @@ impl EventAcceptancePolicyTests {
828 .kind(Kind::GitRepoAnnouncement) 825 .kind(Kind::GitRepoAnnouncement)
829 .author(repo.pubkey) 826 .author(repo.pubkey)
830 .identifier(repo_id); 827 .identifier(repo_id);
831 828
832 // Poll until repo is available (with timeout) 829 // Poll until repo is available (with timeout)
833 for _ in 0..10 { 830 for _ in 0..10 {
834 let events = client.query(verify_filter.clone()).await 831 let events = client
832 .query(verify_filter.clone())
833 .await
835 .map_err(|e| format!("Failed to verify repo: {}", e))?; 834 .map_err(|e| format!("Failed to verify repo: {}", e))?;
836 if !events.is_empty() { 835 if !events.is_empty() {
837 break; 836 break;
838 } 837 }
839 tokio::time::sleep(Duration::from_millis(50)).await; 838 tokio::time::sleep(Duration::from_millis(50)).await;
840 } 839 }
841 840
842 // Extra delay to ensure relay's internal database is fully synchronized 841 // Extra delay to ensure relay's internal database is fully synchronized
843 tokio::time::sleep(Duration::from_millis(200)).await; 842 tokio::time::sleep(Duration::from_millis(200)).await;
844 843
@@ -907,12 +906,7 @@ impl EventAcceptancePolicyTests {
907 let issue = ctx 906 let issue = ctx
908 .get_fixture(FixtureKind::RepoWithIssue) 907 .get_fixture(FixtureKind::RepoWithIssue)
909 .await 908 .await
910 .map_err(|e| { 909 .map_err(|e| format!("Test setup failed: could not get issue fixture: {}", e))?;
911 format!(
912 "Test setup failed: could not get issue fixture: {}",
913 e
914 )
915 })?;
916 910
917 // Create Comment A locally but DON'T send it yet 911 // Create Comment A locally but DON'T send it yet
918 let comment_a = Self::create_comment_for_event(client, &issue, "Comment A")?; 912 let comment_a = Self::create_comment_for_event(client, &issue, "Comment A")?;
@@ -997,16 +991,11 @@ impl EventAcceptancePolicyTests {
997 .build(client.keys()) 991 .build(client.keys())
998 .map_err(|e| format!("Failed to build kind1 B: {}", e))?; 992 .map_err(|e| format!("Failed to build kind1 B: {}", e))?;
999 993
1000 send_and_verify_accepted(client, kind1_b, "kind1 B mentioning unsent kind1 A") 994 send_and_verify_accepted(client, kind1_b, "kind1 B mentioning unsent kind1 A").await?;
1001 .await?;
1002 995
1003 // NOW send Kind 1 A - should be accepted because accepted Kind 1 B mentions it 996 // NOW send Kind 1 A - should be accepted because accepted Kind 1 B mentions it
1004 send_and_verify_accepted( 997 send_and_verify_accepted(client, kind1_a, "kind1 A referenced by accepted kind1 B")
1005 client, 998 .await?;
1006 kind1_a,
1007 "kind1 A referenced by accepted kind1 B",
1008 )
1009 .await?;
1010 999
1011 Ok(()) 1000 Ok(())
1012 }) 1001 })
@@ -1033,12 +1022,8 @@ impl EventAcceptancePolicyTests {
1033 Self::create_issue_for_repo(client, &unaccepted_repo, "Orphan Issue")?; 1022 Self::create_issue_for_repo(client, &unaccepted_repo, "Orphan Issue")?;
1034 1023
1035 // 3. Send issue and verify it's REJECTED 1024 // 3. Send issue and verify it's REJECTED
1036 send_and_verify_rejected( 1025 send_and_verify_rejected(client, orphan_issue, "issue referencing unaccepted repo")
1037 client, 1026 .await?;
1038 orphan_issue,
1039 "issue referencing unaccepted repo",
1040 )
1041 .await?;
1042 1027
1043 Ok(()) 1028 Ok(())
1044 }) 1029 })
@@ -1060,8 +1045,7 @@ impl EventAcceptancePolicyTests {
1060 .map_err(|e| format!("Failed to build note: {}", e))?; 1045 .map_err(|e| format!("Failed to build note: {}", e))?;
1061 1046
1062 // 2. Send note and verify it's REJECTED 1047 // 2. Send note and verify it's REJECTED
1063 send_and_verify_rejected(client, orphan_note, "kind 1 with no repo references") 1048 send_and_verify_rejected(client, orphan_note, "kind 1 with no repo references").await?;
1064 .await?;
1065 1049
1066 Ok(()) 1050 Ok(())
1067 }) 1051 })
diff --git a/grasp-audit/src/specs/grasp01/git_clone.rs b/grasp-audit/src/specs/grasp01/git_clone.rs
index 9ee6ed7..95338e4 100644
--- a/grasp-audit/src/specs/grasp01/git_clone.rs
+++ b/grasp-audit/src/specs/grasp01/git_clone.rs
@@ -25,10 +25,7 @@ pub struct GitCloneTests;
25 25
26impl GitCloneTests { 26impl GitCloneTests {
27 /// Run all Git clone tests 27 /// Run all Git clone tests
28 pub async fn run_all( 28 pub async fn run_all(client: &AuditClient, relay_domain: &str) -> crate::AuditResult {
29 client: &AuditClient,
30 relay_domain: &str,
31 ) -> crate::AuditResult {
32 let mut results = crate::AuditResult::new("GRASP-01 Git Clone Tests"); 29 let mut results = crate::AuditResult::new("GRASP-01 Git Clone Tests");
33 30
34 results.add(Self::test_basic_git_clone(client, relay_domain).await); 31 results.add(Self::test_basic_git_clone(client, relay_domain).await);
@@ -45,10 +42,7 @@ impl GitCloneTests {
45 /// 2. Waits for repository creation 42 /// 2. Waits for repository creation
46 /// 3. Attempts to clone the repository using git clone 43 /// 3. Attempts to clone the repository using git clone
47 /// 4. Verifies the clone succeeded 44 /// 4. Verifies the clone succeeded
48 pub async fn test_basic_git_clone( 45 pub async fn test_basic_git_clone(client: &AuditClient, relay_domain: &str) -> TestResult {
49 client: &AuditClient,
50 relay_domain: &str,
51 ) -> TestResult {
52 let test_name = "test_basic_git_clone"; 46 let test_name = "test_basic_git_clone";
53 let ctx = TestContext::new(client); 47 let ctx = TestContext::new(client);
54 48
@@ -61,7 +55,7 @@ impl GitCloneTests {
61 "GRASP-01", 55 "GRASP-01",
62 "Repository must be cloneable via Git HTTP backend", 56 "Repository must be cloneable via Git HTTP backend",
63 ) 57 )
64 .fail(&format!("Failed to create repo fixture: {}", e)) 58 .fail(format!("Failed to create repo fixture: {}", e))
65 } 59 }
66 }; 60 };
67 61
@@ -94,7 +88,7 @@ impl GitCloneTests {
94 "GRASP-01", 88 "GRASP-01",
95 "Repository must be cloneable via Git HTTP backend", 89 "Repository must be cloneable via Git HTTP backend",
96 ) 90 )
97 .fail(&format!("Failed to convert pubkey to npub: {}", e)) 91 .fail(format!("Failed to convert pubkey to npub: {}", e))
98 } 92 }
99 }; 93 };
100 94
@@ -102,7 +96,7 @@ impl GitCloneTests {
102 let temp_base = std::env::temp_dir(); 96 let temp_base = std::env::temp_dir();
103 let clone_dir_name = format!("grasp-test-clone-{}", uuid::Uuid::new_v4()); 97 let clone_dir_name = format!("grasp-test-clone-{}", uuid::Uuid::new_v4());
104 let clone_path = temp_base.join(&clone_dir_name); 98 let clone_path = temp_base.join(&clone_dir_name);
105 99
106 // Ensure clean state 100 // Ensure clean state
107 let _ = fs::remove_dir_all(&clone_path); 101 let _ = fs::remove_dir_all(&clone_path);
108 102
@@ -114,7 +108,7 @@ impl GitCloneTests {
114 .args(["clone", &clone_url, clone_path.to_str().unwrap()]) 108 .args(["clone", &clone_url, clone_path.to_str().unwrap()])
115 .env("GIT_TERMINAL_PROMPT", "0") // Disable password prompts 109 .env("GIT_TERMINAL_PROMPT", "0") // Disable password prompts
116 .output(); 110 .output();
117 111
118 // Clean up on success or failure 112 // Clean up on success or failure
119 let cleanup = || { 113 let cleanup = || {
120 let _ = fs::remove_dir_all(&clone_path); 114 let _ = fs::remove_dir_all(&clone_path);
@@ -129,7 +123,7 @@ impl GitCloneTests {
129 "GRASP-01", 123 "GRASP-01",
130 "Repository must be cloneable via Git HTTP backend", 124 "Repository must be cloneable via Git HTTP backend",
131 ) 125 )
132 .fail(&format!("Failed to execute git clone: {}", e)) 126 .fail(format!("Failed to execute git clone: {}", e));
133 } 127 }
134 }; 128 };
135 129
@@ -141,7 +135,7 @@ impl GitCloneTests {
141 "GRASP-01", 135 "GRASP-01",
142 "Repository must be cloneable via Git HTTP backend", 136 "Repository must be cloneable via Git HTTP backend",
143 ) 137 )
144 .fail(&format!("Git clone failed: {}", stderr)); 138 .fail(format!("Git clone failed: {}", stderr));
145 } 139 }
146 140
147 // Verify clone succeeded by checking for .git directory 141 // Verify clone succeeded by checking for .git directory
@@ -169,10 +163,7 @@ impl GitCloneTests {
169 /// This test verifies: 163 /// This test verifies:
170 /// 1. URLs follow the pattern http://domain/npub/identifier.git 164 /// 1. URLs follow the pattern http://domain/npub/identifier.git
171 /// 2. Invalid URLs are rejected properly 165 /// 2. Invalid URLs are rejected properly
172 pub async fn test_clone_url_format( 166 pub async fn test_clone_url_format(client: &AuditClient, relay_domain: &str) -> TestResult {
173 client: &AuditClient,
174 relay_domain: &str,
175 ) -> TestResult {
176 let test_name = "test_clone_url_format"; 167 let test_name = "test_clone_url_format";
177 let ctx = TestContext::new(client); 168 let ctx = TestContext::new(client);
178 169
@@ -185,7 +176,7 @@ impl GitCloneTests {
185 "GRASP-01", 176 "GRASP-01",
186 "Clone URL must follow correct format", 177 "Clone URL must follow correct format",
187 ) 178 )
188 .fail(&format!("Failed to create repo fixture: {}", e)) 179 .fail(format!("Failed to create repo fixture: {}", e))
189 } 180 }
190 }; 181 };
191 182
@@ -205,7 +196,7 @@ impl GitCloneTests {
205 196
206 // Test valid URL format 197 // Test valid URL format
207 let valid_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id); 198 let valid_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
208 199
209 // Verify URL contains expected components 200 // Verify URL contains expected components
210 if !valid_url.contains(&npub) { 201 if !valid_url.contains(&npub) {
211 return TestResult::new( 202 return TestResult::new(
@@ -229,10 +220,10 @@ impl GitCloneTests {
229 let temp_base = std::env::temp_dir(); 220 let temp_base = std::env::temp_dir();
230 let clone_dir_name = format!("grasp-test-invalid-{}", uuid::Uuid::new_v4()); 221 let clone_dir_name = format!("grasp-test-invalid-{}", uuid::Uuid::new_v4());
231 let clone_path = temp_base.join(&clone_dir_name); 222 let clone_path = temp_base.join(&clone_dir_name);
232 223
233 // Ensure clean state 224 // Ensure clean state
234 let _ = fs::remove_dir_all(&clone_path); 225 let _ = fs::remove_dir_all(&clone_path);
235 226
236 let invalid_url = format!("http://{}/invalid/path", relay_domain); 227 let invalid_url = format!("http://{}/invalid/path", relay_domain);
237 228
238 let output = Command::new("git") 229 let output = Command::new("git")
@@ -287,7 +278,7 @@ impl GitCloneTests {
287 "GRASP-01", 278 "GRASP-01",
288 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 279 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
289 ) 280 )
290 .fail(&format!("Failed to create repo fixture: {}", e)) 281 .fail(format!("Failed to create repo fixture: {}", e))
291 } 282 }
292 }; 283 };
293 284
@@ -320,7 +311,7 @@ impl GitCloneTests {
320 "GRASP-01", 311 "GRASP-01",
321 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 312 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
322 ) 313 )
323 .fail(&format!("Failed to convert pubkey to npub: {}", e)) 314 .fail(format!("Failed to convert pubkey to npub: {}", e))
324 } 315 }
325 }; 316 };
326 317
@@ -340,7 +331,7 @@ impl GitCloneTests {
340 "GRASP-01", 331 "GRASP-01",
341 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 332 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
342 ) 333 )
343 .fail(&format!("HTTP request failed: {}", e)) 334 .fail(format!("HTTP request failed: {}", e))
344 } 335 }
345 }; 336 };
346 337
@@ -350,7 +341,7 @@ impl GitCloneTests {
350 "GRASP-01", 341 "GRASP-01",
351 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 342 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
352 ) 343 )
353 .fail(&format!( 344 .fail(format!(
354 "info/refs request failed with status: {}", 345 "info/refs request failed with status: {}",
355 response.status() 346 response.status()
356 )); 347 ));
@@ -365,7 +356,7 @@ impl GitCloneTests {
365 "GRASP-01", 356 "GRASP-01",
366 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", 357 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
367 ) 358 )
368 .fail(&format!("Failed to read response body: {}", e)) 359 .fail(format!("Failed to read response body: {}", e))
369 } 360 }
370 }; 361 };
371 362
@@ -399,12 +390,3 @@ impl GitCloneTests {
399 .pass() 390 .pass()
400 } 391 }
401} 392}
402
403#[cfg(test)]
404mod tests {
405 #[test]
406 fn test_module_exists() {
407 // Simple compilation test
408 assert!(true);
409 }
410} \ No newline at end of file
diff --git a/grasp-audit/src/specs/grasp01/mod.rs b/grasp-audit/src/specs/grasp01/mod.rs
index 6f58b96..e16a351 100644
--- a/grasp-audit/src/specs/grasp01/mod.rs
+++ b/grasp-audit/src/specs/grasp01/mod.rs
@@ -26,4 +26,4 @@ pub use git_clone::GitCloneTests;
26pub use nip01_smoke::Nip01SmokeTests; 26pub use nip01_smoke::Nip01SmokeTests;
27pub use nip11_document::Nip11DocumentTests; 27pub use nip11_document::Nip11DocumentTests;
28pub use push_authorization::PushAuthorizationTests; 28pub use push_authorization::PushAuthorizationTests;
29pub use repository_creation::{RepositoryCreationTests}; 29pub use repository_creation::RepositoryCreationTests;
diff --git a/grasp-audit/src/specs/grasp01/nip01_smoke.rs b/grasp-audit/src/specs/grasp01/nip01_smoke.rs
index b16d61a..5161da8 100644
--- a/grasp-audit/src/specs/grasp01/nip01_smoke.rs
+++ b/grasp-audit/src/specs/grasp01/nip01_smoke.rs
@@ -163,27 +163,23 @@ 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( 166 TestResult::new("close_subscription", "NIP-01", "Can close subscriptions")
167 "close_subscription", 167 .run(|| async {
168 "NIP-01", 168 // For now, we just verify we can query events
169 "Can close subscriptions", 169 // Full subscription management with CLOSE would require
170 ) 170 // lower-level WebSocket access
171 .run(|| async {
172 // For now, we just verify we can query events
173 // Full subscription management with CLOSE would require
174 // lower-level WebSocket access
175 171
176 let filter = Filter::new().kind(Kind::TextNote).limit(1); 172 let filter = Filter::new().kind(Kind::TextNote).limit(1);
177 173
178 let _events = client 174 let _events = client
179 .subscribe(vec![filter], Some(std::time::Duration::from_secs(2))) 175 .subscribe(vec![filter], Some(std::time::Duration::from_secs(2)))
180 .await 176 .await
181 .map_err(|e| format!("Failed to subscribe: {}", e))?; 177 .map_err(|e| format!("Failed to subscribe: {}", e))?;
182 178
183 // If we got here, subscription worked 179 // If we got here, subscription worked
184 Ok(()) 180 Ok(())
185 }) 181 })
186 .await 182 .await
187 } 183 }
188 184
189 /// Test 5: Rejects events with invalid signatures 185 /// Test 5: Rejects events with invalid signatures
diff --git a/grasp-audit/src/specs/grasp01/nip11_document.rs b/grasp-audit/src/specs/grasp01/nip11_document.rs
index bb864f2..51b147d 100644
--- a/grasp-audit/src/specs/grasp01/nip11_document.rs
+++ b/grasp-audit/src/specs/grasp01/nip11_document.rs
@@ -42,7 +42,9 @@ impl Nip11DocumentTests {
42 ) 42 )
43 .run(|| async { 43 .run(|| async {
44 // 1. Extract HTTP(S) URL from client's WebSocket URL 44 // 1. Extract HTTP(S) URL from client's WebSocket URL
45 let ws_url = client.relay_url().await 45 let ws_url = client
46 .relay_url()
47 .await
46 .map_err(|e| format!("Failed to get relay URL: {}", e))?; 48 .map_err(|e| format!("Failed to get relay URL: {}", e))?;
47 let http_url = AuditClient::ws_to_http_url(&ws_url) 49 let http_url = AuditClient::ws_to_http_url(&ws_url)
48 .map_err(|e| format!("Failed to convert WebSocket URL to HTTP: {}", e))?; 50 .map_err(|e| format!("Failed to convert WebSocket URL to HTTP: {}", e))?;
@@ -66,16 +68,18 @@ impl Nip11DocumentTests {
66 } 68 }
67 69
68 // 4. Verify response is valid JSON 70 // 4. Verify response is valid JSON
69 let json_text = response.text().await 71 let json_text = response
72 .text()
73 .await
70 .map_err(|e| format!("Failed to read response body: {}", e))?; 74 .map_err(|e| format!("Failed to read response body: {}", e))?;
71 75
72 let doc: serde_json::Value = serde_json::from_str(&json_text) 76 let doc: serde_json::Value = serde_json::from_str(&json_text)
73 .map_err(|e| format!("Response is not valid JSON: {}", e))?; 77 .map_err(|e| format!("Response is not valid JSON: {}", e))?;
74 78
75 // 5. Verify has required NIP-11 fields 79 // 5. Verify has required NIP-11 fields
76 let required_fields = ["name", "description", "software", "version"]; 80 let required_fields = ["name", "description", "software", "version"];
77 for field in &required_fields { 81 for field in &required_fields {
78 if !doc.get(field).is_some() { 82 if doc.get(field).is_none() {
79 return Err(format!("Missing required NIP-11 field: {}", field)); 83 return Err(format!("Missing required NIP-11 field: {}", field));
80 } 84 }
81 } 85 }
@@ -97,7 +101,9 @@ impl Nip11DocumentTests {
97 ) 101 )
98 .run(|| async { 102 .run(|| async {
99 // 1. Fetch NIP-11 document 103 // 1. Fetch NIP-11 document
100 let ws_url = client.relay_url().await 104 let ws_url = client
105 .relay_url()
106 .await
101 .map_err(|e| format!("Failed to get relay URL: {}", e))?; 107 .map_err(|e| format!("Failed to get relay URL: {}", e))?;
102 let http_url = AuditClient::ws_to_http_url(&ws_url) 108 let http_url = AuditClient::ws_to_http_url(&ws_url)
103 .map_err(|e| format!("Failed to convert WebSocket URL to HTTP: {}", e))?; 109 .map_err(|e| format!("Failed to convert WebSocket URL to HTTP: {}", e))?;
@@ -110,18 +116,22 @@ impl Nip11DocumentTests {
110 .await 116 .await
111 .map_err(|e| format!("Failed to fetch NIP-11 document: {}", e))?; 117 .map_err(|e| format!("Failed to fetch NIP-11 document: {}", e))?;
112 118
113 let json_text = response.text().await 119 let json_text = response
120 .text()
121 .await
114 .map_err(|e| format!("Failed to read response body: {}", e))?; 122 .map_err(|e| format!("Failed to read response body: {}", e))?;
115 123
116 let doc: serde_json::Value = serde_json::from_str(&json_text) 124 let doc: serde_json::Value = serde_json::from_str(&json_text)
117 .map_err(|e| format!("Response is not valid JSON: {}", e))?; 125 .map_err(|e| format!("Response is not valid JSON: {}", e))?;
118 126
119 // 2. Verify `supported_grasps` field exists 127 // 2. Verify `supported_grasps` field exists
120 let supported_grasps = doc.get("supported_grasps") 128 let supported_grasps = doc
129 .get("supported_grasps")
121 .ok_or_else(|| "Missing required field: supported_grasps".to_string())?; 130 .ok_or_else(|| "Missing required field: supported_grasps".to_string())?;
122 131
123 // 3. Verify it's a JSON array 132 // 3. Verify it's a JSON array
124 let grasps_array = supported_grasps.as_array() 133 let grasps_array = supported_grasps
134 .as_array()
125 .ok_or_else(|| "supported_grasps must be an array".to_string())?; 135 .ok_or_else(|| "supported_grasps must be an array".to_string())?;
126 136
127 // 4. Verify array includes "GRASP-01" 137 // 4. Verify array includes "GRASP-01"
@@ -140,7 +150,7 @@ impl Nip11DocumentTests {
140 // 5. Verify format: each entry should match pattern "GRASP-\d{2}" 150 // 5. Verify format: each entry should match pattern "GRASP-\d{2}"
141 let grasp_pattern = regex::Regex::new(r"^GRASP-\d{2}$") 151 let grasp_pattern = regex::Regex::new(r"^GRASP-\d{2}$")
142 .map_err(|e| format!("Failed to compile regex: {}", e))?; 152 .map_err(|e| format!("Failed to compile regex: {}", e))?;
143 153
144 for grasp in &grasp_strings { 154 for grasp in &grasp_strings {
145 if !grasp_pattern.is_match(grasp) { 155 if !grasp_pattern.is_match(grasp) {
146 return Err(format!( 156 return Err(format!(
@@ -167,7 +177,9 @@ impl Nip11DocumentTests {
167 ) 177 )
168 .run(|| async { 178 .run(|| async {
169 // 1. Fetch NIP-11 document 179 // 1. Fetch NIP-11 document
170 let ws_url = client.relay_url().await 180 let ws_url = client
181 .relay_url()
182 .await
171 .map_err(|e| format!("Failed to get relay URL: {}", e))?; 183 .map_err(|e| format!("Failed to get relay URL: {}", e))?;
172 let http_url = AuditClient::ws_to_http_url(&ws_url) 184 let http_url = AuditClient::ws_to_http_url(&ws_url)
173 .map_err(|e| format!("Failed to convert WebSocket URL to HTTP: {}", e))?; 185 .map_err(|e| format!("Failed to convert WebSocket URL to HTTP: {}", e))?;
@@ -180,18 +192,22 @@ impl Nip11DocumentTests {
180 .await 192 .await
181 .map_err(|e| format!("Failed to fetch NIP-11 document: {}", e))?; 193 .map_err(|e| format!("Failed to fetch NIP-11 document: {}", e))?;
182 194
183 let json_text = response.text().await 195 let json_text = response
196 .text()
197 .await
184 .map_err(|e| format!("Failed to read response body: {}", e))?; 198 .map_err(|e| format!("Failed to read response body: {}", e))?;
185 199
186 let doc: serde_json::Value = serde_json::from_str(&json_text) 200 let doc: serde_json::Value = serde_json::from_str(&json_text)
187 .map_err(|e| format!("Response is not valid JSON: {}", e))?; 201 .map_err(|e| format!("Response is not valid JSON: {}", e))?;
188 202
189 // 2. Verify `repo_acceptance_criteria` field exists 203 // 2. Verify `repo_acceptance_criteria` field exists
190 let criteria = doc.get("repo_acceptance_criteria") 204 let criteria = doc
205 .get("repo_acceptance_criteria")
191 .ok_or_else(|| "Missing required field: repo_acceptance_criteria".to_string())?; 206 .ok_or_else(|| "Missing required field: repo_acceptance_criteria".to_string())?;
192 207
193 // 3. Verify it's a string 208 // 3. Verify it's a string
194 let criteria_str = criteria.as_str() 209 let criteria_str = criteria
210 .as_str()
195 .ok_or_else(|| "repo_acceptance_criteria must be a string".to_string())?; 211 .ok_or_else(|| "repo_acceptance_criteria must be a string".to_string())?;
196 212
197 // 4. Verify non-empty 213 // 4. Verify non-empty
@@ -216,7 +232,9 @@ impl Nip11DocumentTests {
216 ) 232 )
217 .run(|| async { 233 .run(|| async {
218 // 1. Fetch NIP-11 document 234 // 1. Fetch NIP-11 document
219 let ws_url = client.relay_url().await 235 let ws_url = client
236 .relay_url()
237 .await
220 .map_err(|e| format!("Failed to get relay URL: {}", e))?; 238 .map_err(|e| format!("Failed to get relay URL: {}", e))?;
221 let http_url = AuditClient::ws_to_http_url(&ws_url) 239 let http_url = AuditClient::ws_to_http_url(&ws_url)
222 .map_err(|e| format!("Failed to convert WebSocket URL to HTTP: {}", e))?; 240 .map_err(|e| format!("Failed to convert WebSocket URL to HTTP: {}", e))?;
@@ -229,16 +247,19 @@ impl Nip11DocumentTests {
229 .await 247 .await
230 .map_err(|e| format!("Failed to fetch NIP-11 document: {}", e))?; 248 .map_err(|e| format!("Failed to fetch NIP-11 document: {}", e))?;
231 249
232 let json_text = response.text().await 250 let json_text = response
251 .text()
252 .await
233 .map_err(|e| format!("Failed to read response body: {}", e))?; 253 .map_err(|e| format!("Failed to read response body: {}", e))?;
234 254
235 let doc: serde_json::Value = serde_json::from_str(&json_text) 255 let doc: serde_json::Value = serde_json::from_str(&json_text)
236 .map_err(|e| format!("Response is not valid JSON: {}", e))?; 256 .map_err(|e| format!("Response is not valid JSON: {}", e))?;
237 257
238 // 2. Check if `curation` field exists 258 // 2. Check if `curation` field exists
239 if let Some(curation) = doc.get("curation") { 259 if let Some(curation) = doc.get("curation") {
240 // 3. If present: verify it's a non-empty string 260 // 3. If present: verify it's a non-empty string
241 let curation_str = curation.as_str() 261 let curation_str = curation
262 .as_str()
242 .ok_or_else(|| "curation field must be a string when present".to_string())?; 263 .ok_or_else(|| "curation field must be a string when present".to_string())?;
243 264
244 if curation_str.trim().is_empty() { 265 if curation_str.trim().is_empty() {
@@ -284,4 +305,4 @@ mod tests {
284 // Don't assert all passed yet - tests not implemented 305 // Don't assert all passed yet - tests not implemented
285 // assert!(results.all_passed(), "Some GRASP-01 NIP-11 document tests failed"); 306 // assert!(results.all_passed(), "Some GRASP-01 NIP-11 document tests failed");
286 } 307 }
287} \ No newline at end of file 308}
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs
index d8652ae..24eae1d 100644
--- a/grasp-audit/src/specs/grasp01/push_authorization.rs
+++ b/grasp-audit/src/specs/grasp01/push_authorization.rs
@@ -351,10 +351,7 @@ async fn setup_repo_with_wrong_commit_pushed(
351/// IMPORTANT: We must publish the EXACT same event that was used during setup, 351/// IMPORTANT: We must publish the EXACT same event that was used during setup,
352/// otherwise the event ID won't match the refs/nostr/<event-id> ref that was pushed. 352/// otherwise the event ID won't match the refs/nostr/<event-id> ref that was pushed.
353#[allow(dead_code)] 353#[allow(dead_code)]
354async fn publish_pr_event_and_wait( 354async fn publish_pr_event_and_wait(ctx: &TestContext<'_>, pr_event: &Event) -> Result<(), String> {
355 ctx: &TestContext<'_>,
356 pr_event: &Event,
357) -> Result<(), String> {
358 // Publish the exact same PR event that was created during setup 355 // Publish the exact same PR event that was created during setup
359 ctx.client() 356 ctx.client()
360 .send_event(pr_event.clone()) 357 .send_event(pr_event.clone())
@@ -1892,11 +1889,6 @@ impl PushAuthorizationTests {
1892mod tests { 1889mod tests {
1893 use super::*; 1890 use super::*;
1894 1891
1895 #[test]
1896 fn test_module_exists() {
1897 assert!(true);
1898 }
1899
1900 /// Test to discover the PR test commit hash 1892 /// Test to discover the PR test commit hash
1901 /// 1893 ///
1902 /// This test creates a deterministic commit with PR-specific parameters 1894 /// This test creates a deterministic commit with PR-specific parameters
diff --git a/grasp-audit/src/specs/grasp01/repository_creation.rs b/grasp-audit/src/specs/grasp01/repository_creation.rs
index 63b3dee..0b3eed5 100644
--- a/grasp-audit/src/specs/grasp01/repository_creation.rs
+++ b/grasp-audit/src/specs/grasp01/repository_creation.rs
@@ -23,10 +23,7 @@ pub struct RepositoryCreationTests;
23 23
24impl RepositoryCreationTests { 24impl RepositoryCreationTests {
25 /// Run all repository creation tests 25 /// Run all repository creation tests
26 pub async fn run_all( 26 pub async fn run_all(client: &AuditClient, relay_domain: &str) -> crate::AuditResult {
27 client: &AuditClient,
28 relay_domain: &str,
29 ) -> crate::AuditResult {
30 let mut results = crate::AuditResult::new("GRASP-01 Repository Creation Tests"); 27 let mut results = crate::AuditResult::new("GRASP-01 Repository Creation Tests");
31 28
32 results.add(Self::test_bare_repo_created_on_announcement(client, relay_domain).await); 29 results.add(Self::test_bare_repo_created_on_announcement(client, relay_domain).await);
@@ -58,7 +55,7 @@ impl RepositoryCreationTests {
58 "GRASP-01", 55 "GRASP-01",
59 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", 56 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
60 ) 57 )
61 .fail(&format!("Failed to create repo fixture: {}", e)) 58 .fail(format!("Failed to create repo fixture: {}", e))
62 } 59 }
63 }; 60 };
64 61
@@ -91,7 +88,7 @@ impl RepositoryCreationTests {
91 "GRASP-01", 88 "GRASP-01",
92 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", 89 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
93 ) 90 )
94 .fail(&format!("Failed to convert pubkey to npub: {}", e)) 91 .fail(format!("Failed to convert pubkey to npub: {}", e))
95 } 92 }
96 }; 93 };
97 94
@@ -102,7 +99,7 @@ impl RepositoryCreationTests {
102 "GRASP-01", 99 "GRASP-01",
103 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted", 100 "Bare repository must be created and accessible via Smart HTTP when announcement is accepted",
104 ) 101 )
105 .fail(&format!("Repository not accessible via HTTP: {}", e)); 102 .fail(format!("Repository not accessible via HTTP: {}", e));
106 } 103 }
107 104
108 TestResult::new( 105 TestResult::new(
@@ -159,12 +156,3 @@ async fn check_repo_accessible_via_http(
159 156
160 Ok(()) 157 Ok(())
161} 158}
162
163#[cfg(test)]
164mod tests {
165 #[test]
166 fn test_module_exists() {
167 // Simple compilation test
168 assert!(true);
169 }
170}
diff --git a/src/config.rs b/src/config.rs
index f04b7d8..9b0d0b8 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -5,8 +5,10 @@ use std::env;
5/// Database backend type for the relay 5/// Database backend type for the relay
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "lowercase")] 7#[serde(rename_all = "lowercase")]
8#[derive(Default)]
8pub enum DatabaseBackend { 9pub enum DatabaseBackend {
9 /// In-memory database (default, fastest, no persistence) 10 /// In-memory database (default, fastest, no persistence)
11 #[default]
10 Memory, 12 Memory,
11 /// NostrDB backend (persistent, optimized for Nostr) 13 /// NostrDB backend (persistent, optimized for Nostr)
12 NostrDb, 14 NostrDb,
@@ -14,12 +16,6 @@ pub enum DatabaseBackend {
14 Lmdb, 16 Lmdb,
15} 17}
16 18
17impl Default for DatabaseBackend {
18 fn default() -> Self {
19 Self::Memory
20 }
21}
22
23impl std::str::FromStr for DatabaseBackend { 19impl std::str::FromStr for DatabaseBackend {
24 type Err = anyhow::Error; 20 type Err = anyhow::Error;
25 21
diff --git a/src/git/handlers.rs b/src/git/handlers.rs
index 00f2449..e84cabb 100644
--- a/src/git/handlers.rs
+++ b/src/git/handlers.rs
@@ -5,7 +5,6 @@
5use http_body_util::Full; 5use http_body_util::Full;
6use hyper::{body::Bytes, Response, StatusCode}; 6use hyper::{body::Bytes, Response, StatusCode};
7use nostr_relay_builder::prelude::MemoryDatabase; 7use nostr_relay_builder::prelude::MemoryDatabase;
8use nostr_sdk::EventId;
9use std::path::PathBuf; 8use std::path::PathBuf;
10use std::sync::Arc; 9use std::sync::Arc;
11use tokio::io::{AsyncReadExt, AsyncWriteExt}; 10use tokio::io::{AsyncReadExt, AsyncWriteExt};
diff --git a/src/git/mod.rs b/src/git/mod.rs
index 494f8b9..a783782 100644
--- a/src/git/mod.rs
+++ b/src/git/mod.rs
@@ -306,11 +306,7 @@ pub fn parse_git_url(path: &str) -> Option<(&str, &str, &str)> {
306 let subpath = parts[2]; 306 let subpath = parts[2];
307 307
308 // Extract identifier (remove .git suffix if present for the middle part) 308 // Extract identifier (remove .git suffix if present for the middle part)
309 let identifier = if repo_part.ends_with(".git") { 309 let identifier = repo_part.strip_suffix(".git").unwrap_or(repo_part);
310 &repo_part[..repo_part.len() - 4]
311 } else {
312 repo_part
313 };
314 310
315 Some((npub, identifier, subpath)) 311 Some((npub, identifier, subpath))
316} 312}
@@ -343,7 +339,12 @@ mod tests {
343 339
344 // Initialize bare repository 340 // Initialize bare repository
345 Command::new("git") 341 Command::new("git")
346 .args(["init", "--bare", "--initial-branch=main", bare_repo.to_str().unwrap()]) 342 .args([
343 "init",
344 "--bare",
345 "--initial-branch=main",
346 bare_repo.to_str().unwrap(),
347 ])
347 .output() 348 .output()
348 .unwrap(); 349 .unwrap();
349 350
diff --git a/src/git/protocol.rs b/src/git/protocol.rs
index 93177de..8592c27 100644
--- a/src/git/protocol.rs
+++ b/src/git/protocol.rs
@@ -55,11 +55,11 @@ impl PktLine {
55 return Err(ProtocolError::InsufficientData); 55 return Err(ProtocolError::InsufficientData);
56 } 56 }
57 57
58 let len_str = std::str::from_utf8(&input[0..4]) 58 let len_str =
59 .map_err(|_| ProtocolError::InvalidLength)?; 59 std::str::from_utf8(&input[0..4]).map_err(|_| ProtocolError::InvalidLength)?;
60 60
61 let len = u16::from_str_radix(len_str, 16) 61 let len =
62 .map_err(|_| ProtocolError::InvalidLength)? as usize; 62 u16::from_str_radix(len_str, 16).map_err(|_| ProtocolError::InvalidLength)? as usize;
63 63
64 if len == 0 { 64 if len == 0 {
65 // Flush packet 65 // Flush packet
@@ -81,19 +81,19 @@ impl PktLine {
81 /// Parse all pkt-lines from bytes 81 /// Parse all pkt-lines from bytes
82 pub fn parse_all(mut input: &[u8]) -> Result<Vec<Self>, ProtocolError> { 82 pub fn parse_all(mut input: &[u8]) -> Result<Vec<Self>, ProtocolError> {
83 let mut packets = Vec::new(); 83 let mut packets = Vec::new();
84 84
85 while !input.is_empty() { 85 while !input.is_empty() {
86 let (packet, remaining) = Self::parse(input)?; 86 let (packet, remaining) = Self::parse(input)?;
87 let is_flush = matches!(packet, PktLine::Flush); 87 let is_flush = matches!(packet, PktLine::Flush);
88 packets.push(packet); 88 packets.push(packet);
89 input = remaining; 89 input = remaining;
90 90
91 // Stop at flush packet 91 // Stop at flush packet
92 if is_flush { 92 if is_flush {
93 break; 93 break;
94 } 94 }
95 } 95 }
96 96
97 Ok(packets) 97 Ok(packets)
98 } 98 }
99} 99}
@@ -259,4 +259,4 @@ mod tests {
259 "application/x-git-upload-pack-result" 259 "application/x-git-upload-pack-result"
260 ); 260 );
261 } 261 }
262} \ No newline at end of file 262}
diff --git a/src/git/subprocess.rs b/src/git/subprocess.rs
index c95bce5..2d9a981 100644
--- a/src/git/subprocess.rs
+++ b/src/git/subprocess.rs
@@ -28,9 +28,9 @@ impl GitSubprocess {
28 advertise: bool, 28 advertise: bool,
29 ) -> std::io::Result<Self> { 29 ) -> std::io::Result<Self> {
30 let repo_path = repo_path.as_ref(); 30 let repo_path = repo_path.as_ref();
31 31
32 let mut cmd = Command::new("git"); 32 let mut cmd = Command::new("git");
33 33
34 // GRASP-01 requirement: MUST include `allow-reachable-sha1-in-want` and 34 // GRASP-01 requirement: MUST include `allow-reachable-sha1-in-want` and
35 // `allow-tip-sha1-in-want` in advertisement and serve available oids. 35 // `allow-tip-sha1-in-want` in advertisement and serve available oids.
36 // These config options must be passed before the command name. 36 // These config options must be passed before the command name.
@@ -38,22 +38,22 @@ impl GitSubprocess {
38 cmd.arg("uploadpack.allowReachableSHA1InWant=true"); 38 cmd.arg("uploadpack.allowReachableSHA1InWant=true");
39 cmd.arg("-c"); 39 cmd.arg("-c");
40 cmd.arg("uploadpack.allowTipSHA1InWant=true"); 40 cmd.arg("uploadpack.allowTipSHA1InWant=true");
41 41
42 cmd.arg(service.command_name()); 42 cmd.arg(service.command_name());
43 43
44 if advertise { 44 if advertise {
45 cmd.arg("--advertise-refs"); 45 cmd.arg("--advertise-refs");
46 } 46 }
47 47
48 cmd.arg("--stateless-rpc"); 48 cmd.arg("--stateless-rpc");
49 cmd.arg(repo_path); 49 cmd.arg(repo_path);
50 50
51 cmd.stdin(Stdio::piped()); 51 cmd.stdin(Stdio::piped());
52 cmd.stdout(Stdio::piped()); 52 cmd.stdout(Stdio::piped());
53 cmd.stderr(Stdio::piped()); 53 cmd.stderr(Stdio::piped());
54 54
55 let child = cmd.spawn()?; 55 let child = cmd.spawn()?;
56 56
57 Ok(Self { child }) 57 Ok(Self { child })
58 } 58 }
59 59
@@ -101,8 +101,8 @@ impl GitSubprocess {
101#[cfg(test)] 101#[cfg(test)]
102mod tests { 102mod tests {
103 use super::*; 103 use super::*;
104 use tempfile::TempDir;
105 use std::process::Command as StdCommand; 104 use std::process::Command as StdCommand;
105 use tempfile::TempDir;
106 106
107 fn create_bare_repo() -> TempDir { 107 fn create_bare_repo() -> TempDir {
108 let dir = TempDir::new().unwrap(); 108 let dir = TempDir::new().unwrap();
@@ -118,11 +118,8 @@ mod tests {
118 #[tokio::test] 118 #[tokio::test]
119 async fn test_spawn_upload_pack_advertise() { 119 async fn test_spawn_upload_pack_advertise() {
120 let repo = create_bare_repo(); 120 let repo = create_bare_repo();
121 let mut proc = GitSubprocess::spawn( 121 let mut proc = GitSubprocess::spawn(GitService::UploadPack, repo.path(), true)
122 GitService::UploadPack, 122 .expect("Failed to spawn git");
123 repo.path(),
124 true,
125 ).expect("Failed to spawn git");
126 123
127 // Should have spawned successfully 124 // Should have spawned successfully
128 assert!(proc.stdout().is_some()); 125 assert!(proc.stdout().is_some());
@@ -135,15 +132,12 @@ mod tests {
135 #[tokio::test] 132 #[tokio::test]
136 async fn test_spawn_receive_pack() { 133 async fn test_spawn_receive_pack() {
137 let repo = create_bare_repo(); 134 let repo = create_bare_repo();
138 let mut proc = GitSubprocess::spawn( 135 let mut proc = GitSubprocess::spawn(GitService::ReceivePack, repo.path(), false)
139 GitService::ReceivePack, 136 .expect("Failed to spawn git");
140 repo.path(),
141 false,
142 ).expect("Failed to spawn git");
143 137
144 assert!(proc.stdout().is_some()); 138 assert!(proc.stdout().is_some());
145 assert!(proc.stdin().is_some()); 139 assert!(proc.stdin().is_some());
146 140
147 let _ = proc.kill().await; 141 let _ = proc.kill().await;
148 } 142 }
149} \ No newline at end of file 143}
diff --git a/src/http/mod.rs b/src/http/mod.rs
index 07b47ee..f43cf86 100644
--- a/src/http/mod.rs
+++ b/src/http/mod.rs
@@ -9,20 +9,20 @@ use std::net::SocketAddr;
9use std::pin::Pin; 9use std::pin::Pin;
10use std::sync::Arc; 10use std::sync::Arc;
11 11
12use base64::Engine;
13use http_body_util::{BodyExt, Full};
12use hyper::body::{Bytes, Incoming}; 14use hyper::body::{Bytes, Incoming};
13use hyper::header::{CONNECTION, SEC_WEBSOCKET_ACCEPT, UPGRADE}; 15use hyper::header::{CONNECTION, SEC_WEBSOCKET_ACCEPT, UPGRADE};
14use hyper::server::conn::http1; 16use hyper::server::conn::http1;
15use hyper::service::Service; 17use hyper::service::Service;
16use hyper::{Method, Request, Response}; 18use hyper::{Method, Request, Response};
17use hyper_util::rt::TokioIo; 19use hyper_util::rt::TokioIo;
18use http_body_util::{BodyExt, Full}; 20use nostr_relay_builder::prelude::MemoryDatabase;
21use nostr_relay_builder::LocalRelay;
19use nostr_sdk::hashes::sha1::Hash as Sha1Hash; 22use nostr_sdk::hashes::sha1::Hash as Sha1Hash;
20use nostr_sdk::hashes::{Hash, HashEngine}; 23use nostr_sdk::hashes::{Hash, HashEngine};
21use nostr_sdk::PublicKey; 24use nostr_sdk::PublicKey;
22use nostr_relay_builder::prelude::MemoryDatabase;
23use nostr_relay_builder::LocalRelay;
24use tokio::net::TcpListener; 25use tokio::net::TcpListener;
25use base64::Engine;
26 26
27use crate::config::Config; 27use crate::config::Config;
28use crate::git; 28use crate::git;
@@ -50,7 +50,12 @@ struct HttpService {
50} 50}
51 51
52impl HttpService { 52impl HttpService {
53 fn new(relay: LocalRelay, config: Config, remote: SocketAddr, database: Arc<MemoryDatabase>) -> Self { 53 fn new(
54 relay: LocalRelay,
55 config: Config,
56 remote: SocketAddr,
57 database: Arc<MemoryDatabase>,
58 ) -> Self {
54 Self { 59 Self {
55 relay, 60 relay,
56 config, 61 config,
@@ -77,10 +82,12 @@ impl Service<Request<Incoming>> for HttpService {
77 // GRASP-01 spec line 47: Respond to OPTIONS with 204 No Content 82 // GRASP-01 spec line 47: Respond to OPTIONS with 204 No Content
78 if method == Method::OPTIONS { 83 if method == Method::OPTIONS {
79 return Box::pin(async move { 84 return Box::pin(async move {
80 Ok(add_cors_headers(Response::builder().header("server", "ngit-grasp")) 85 Ok(
81 .status(204) 86 add_cors_headers(Response::builder().header("server", "ngit-grasp"))
82 .body(Full::new(Bytes::new())) 87 .status(204)
83 .unwrap()) 88 .body(Full::new(Bytes::new()))
89 .unwrap(),
90 )
84 }); 91 });
85 } 92 }
86 93
@@ -89,41 +96,47 @@ impl Service<Request<Incoming>> for HttpService {
89 let npub = npub.to_string(); 96 let npub = npub.to_string();
90 let identifier = identifier.to_string(); 97 let identifier = identifier.to_string();
91 let subpath = subpath.to_string(); 98 let subpath = subpath.to_string();
92 99
93 tracing::debug!("Git request: {} {} (npub={}, id={}, subpath={})", 100 tracing::debug!(
94 method, path, npub, identifier, subpath); 101 "Git request: {} {} (npub={}, id={}, subpath={})",
102 method,
103 path,
104 npub,
105 identifier,
106 subpath
107 );
95 108
96 let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier); 109 let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier);
97 110
98 return Box::pin(async move { 111 return Box::pin(async move {
99 // Collect request body once before the match statement 112 // Collect request body once before the match statement
100 let body_bytes = req.collect().await 113 let body_bytes = req
114 .collect()
115 .await
101 .map(|collected| collected.to_bytes()) 116 .map(|collected| collected.to_bytes())
102 .unwrap_or_else(|_| Bytes::new()); 117 .unwrap_or_else(|_| Bytes::new());
103 118
104 let result = match (method.as_ref(), subpath.as_str()) { 119 let result = match (method.as_ref(), subpath.as_str()) {
105 // GET /info/refs?service=git-upload-pack or git-receive-pack 120 // GET /info/refs?service=git-upload-pack or git-receive-pack
106 (m, sp) if m == Method::GET && sp.starts_with("info/refs") => { 121 (m, sp) if m == Method::GET && sp.starts_with("info/refs") => {
107 // Parse query string for service parameter 122 // Parse query string for service parameter
108 let service = query.as_deref().unwrap_or("") 123 let service = query
124 .as_deref()
125 .unwrap_or("")
109 .strip_prefix("service=") 126 .strip_prefix("service=")
110 .and_then(git::protocol::GitService::from_query_param); 127 .and_then(git::protocol::GitService::from_query_param);
111 128
112 match service { 129 match service {
113 Some(svc) => { 130 Some(svc) => git::handlers::handle_info_refs(repo_path, svc).await,
114 git::handlers::handle_info_refs(repo_path, svc).await 131 None => Err(git::handlers::GitError::RepositoryNotFound),
115 }
116 None => {
117 Err(git::handlers::GitError::RepositoryNotFound)
118 }
119 } 132 }
120 } 133 }
121 134
122 // POST /git-upload-pack (clone/fetch) 135 // POST /git-upload-pack (clone/fetch)
123 (m, "git-upload-pack") if m == Method::POST => { 136 (m, "git-upload-pack") if m == Method::POST => {
124 git::handlers::handle_upload_pack(repo_path, body_bytes).await 137 git::handlers::handle_upload_pack(repo_path, body_bytes).await
125 } 138 }
126 139
127 // POST /git-receive-pack (push) - with GRASP authorization via database 140 // POST /git-receive-pack (push) - with GRASP authorization via database
128 (m, "git-receive-pack") if m == Method::POST => { 141 (m, "git-receive-pack") if m == Method::POST => {
129 // Convert npub (bech32) to hex pubkey for authorization 142 // Convert npub (bech32) to hex pubkey for authorization
@@ -137,33 +150,41 @@ impl Service<Request<Incoming>> for HttpService {
137 .unwrap()); 150 .unwrap());
138 } 151 }
139 }; 152 };
140 153
141 git::handlers::handle_receive_pack( 154 git::handlers::handle_receive_pack(
142 repo_path, 155 repo_path,
143 body_bytes.clone(), 156 body_bytes.clone(),
144 Some(database.clone()), 157 Some(database.clone()),
145 &identifier, 158 &identifier,
146 &owner_pubkey_hex, 159 &owner_pubkey_hex,
147 ).await 160 )
148 } 161 .await
149
150 _ => {
151 Err(git::handlers::GitError::RepositoryNotFound)
152 } 162 }
163
164 _ => Err(git::handlers::GitError::RepositoryNotFound),
153 }; 165 };
154 166
155 match result { 167 match result {
156 Ok(response) => { 168 Ok(response) => {
157 // Add CORS headers to successful Git responses 169 // Add CORS headers to successful Git responses
158 let (parts, body) = response.into_parts(); 170 let (parts, body) = response.into_parts();
159 Ok(add_cors_headers(Response::builder() 171 Ok(add_cors_headers(Response::builder().status(parts.status))
160 .status(parts.status)) 172 .header(
161 .header("content-type", parts.headers.get("content-type") 173 "content-type",
162 .and_then(|v| v.to_str().ok()) 174 parts
163 .unwrap_or("application/octet-stream")) 175 .headers
164 .header("cache-control", parts.headers.get("cache-control") 176 .get("content-type")
165 .and_then(|v| v.to_str().ok()) 177 .and_then(|v| v.to_str().ok())
166 .unwrap_or("no-cache")) 178 .unwrap_or("application/octet-stream"),
179 )
180 .header(
181 "cache-control",
182 parts
183 .headers
184 .get("cache-control")
185 .and_then(|v| v.to_str().ok())
186 .unwrap_or("no-cache"),
187 )
167 .body(body) 188 .body(body)
168 .unwrap()) 189 .unwrap())
169 } 190 }
@@ -191,15 +212,20 @@ impl Service<Request<Incoming>> for HttpService {
191 tracing::error!("Failed to serialize NIP-11 document: {}", e); 212 tracing::error!("Failed to serialize NIP-11 document: {}", e);
192 "{}".to_string() 213 "{}".to_string()
193 }); 214 });
194 215
195 tracing::debug!("Serving NIP-11 relay information document to {}", self.remote); 216 tracing::debug!(
196 217 "Serving NIP-11 relay information document to {}",
218 self.remote
219 );
220
197 return Box::pin(async move { 221 return Box::pin(async move {
198 Ok(add_cors_headers(Response::builder().header("server", "ngit-grasp")) 222 Ok(
199 .status(200) 223 add_cors_headers(Response::builder().header("server", "ngit-grasp"))
200 .header("content-type", "application/nostr+json") 224 .status(200)
201 .body(Full::new(Bytes::from(json))) 225 .header("content-type", "application/nostr+json")
202 .unwrap()) 226 .body(Full::new(Bytes::from(json)))
227 .unwrap(),
228 )
203 }); 229 });
204 } 230 }
205 } 231 }
@@ -221,12 +247,13 @@ impl Service<Request<Incoming>> for HttpService {
221 247
222 let addr = self.remote; 248 let addr = self.remote;
223 let relay = self.relay.clone(); 249 let relay = self.relay.clone();
224 250
225 tokio::spawn(async move { 251 tokio::spawn(async move {
226 match hyper::upgrade::on(req).await { 252 match hyper::upgrade::on(req).await {
227 Ok(upgraded) => { 253 Ok(upgraded) => {
228 tracing::info!("WebSocket connection established from {}", addr); 254 tracing::info!("WebSocket connection established from {}", addr);
229 if let Err(e) = relay.take_connection(TokioIo::new(upgraded), addr).await 255 if let Err(e) =
256 relay.take_connection(TokioIo::new(upgraded), addr).await
230 { 257 {
231 tracing::error!("Relay error for {}: {}", addr, e); 258 tracing::error!("Relay error for {}: {}", addr, e);
232 } 259 }
@@ -288,12 +315,12 @@ pub async fn run_server(
288 tracing::info!("Domain: {}", config.domain); 315 tracing::info!("Domain: {}", config.domain);
289 316
290 let listener = TcpListener::bind(&bind_addr).await?; 317 let listener = TcpListener::bind(&bind_addr).await?;
291 318
292 loop { 319 loop {
293 let (socket, addr) = listener.accept().await?; 320 let (socket, addr) = listener.accept().await?;
294 let io = TokioIo::new(socket); 321 let io = TokioIo::new(socket);
295 let service = HttpService::new(relay.clone(), config.clone(), addr, database.clone()); 322 let service = HttpService::new(relay.clone(), config.clone(), addr, database.clone());
296 323
297 tokio::spawn(async move { 324 tokio::spawn(async move {
298 if let Err(e) = http1::Builder::new() 325 if let Err(e) = http1::Builder::new()
299 .serve_connection(io, service) 326 .serve_connection(io, service)
diff --git a/src/http/nip11.rs b/src/http/nip11.rs
index a93ee5f..593ef9a 100644
--- a/src/http/nip11.rs
+++ b/src/http/nip11.rs
@@ -1,10 +1,9 @@
1use crate::config::Config;
1/// NIP-11 Relay Information Document 2/// NIP-11 Relay Information Document
2/// 3///
3/// Implements NIP-11 relay information endpoint with GRASP-01 extensions. 4/// Implements NIP-11 relay information endpoint with GRASP-01 extensions.
4/// See: https://github.com/nostr-protocol/nips/blob/master/11.md 5/// See: https://github.com/nostr-protocol/nips/blob/master/11.md
5
6use serde::{Deserialize, Serialize}; 6use serde::{Deserialize, Serialize};
7use crate::config::Config;
8 7
9/// NIP-11 Relay Information Document 8/// NIP-11 Relay Information Document
10/// 9///
@@ -14,37 +13,36 @@ use crate::config::Config;
14pub struct RelayInformationDocument { 13pub struct RelayInformationDocument {
15 /// Relay name 14 /// Relay name
16 pub name: String, 15 pub name: String,
17 16
18 /// Relay description 17 /// Relay description
19 pub description: String, 18 pub description: String,
20 19
21 /// Relay owner's public key (hex format) 20 /// Relay owner's public key (hex format)
22 #[serde(skip_serializing_if = "Option::is_none")] 21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub pubkey: Option<String>, 22 pub pubkey: Option<String>,
24 23
25 /// Contact information for relay admin 24 /// Contact information for relay admin
26 #[serde(skip_serializing_if = "Option::is_none")] 25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub contact: Option<String>, 26 pub contact: Option<String>,
28 27
29 /// List of NIPs supported by this relay 28 /// List of NIPs supported by this relay
30 pub supported_nips: Vec<u16>, 29 pub supported_nips: Vec<u16>,
31 30
32 /// Relay software identifier 31 /// Relay software identifier
33 pub software: String, 32 pub software: String,
34 33
35 /// Software version 34 /// Software version
36 pub version: String, 35 pub version: String,
37 36
38 // GRASP-01 Extensions (lines 11-14 of GRASP-01 spec) 37 // GRASP-01 Extensions (lines 11-14 of GRASP-01 spec)
39
40 /// List of supported GRASPs (e.g., ["GRASP-01"]) 38 /// List of supported GRASPs (e.g., ["GRASP-01"])
41 /// Required by GRASP-01 specification line 12 39 /// Required by GRASP-01 specification line 12
42 pub supported_grasps: Vec<String>, 40 pub supported_grasps: Vec<String>,
43 41
44 /// Repository acceptance criteria description 42 /// Repository acceptance criteria description
45 /// Required by GRASP-01 specification line 13 43 /// Required by GRASP-01 specification line 13
46 pub repo_acceptance_criteria: String, 44 pub repo_acceptance_criteria: String,
47 45
48 /// Curation policy (present if curated, absent otherwise) 46 /// Curation policy (present if curated, absent otherwise)
49 /// Optional per GRASP-01 specification line 14 47 /// Optional per GRASP-01 specification line 14
50 #[serde(skip_serializing_if = "Option::is_none")] 48 #[serde(skip_serializing_if = "Option::is_none")]
@@ -66,7 +64,7 @@ impl RelayInformationDocument {
66 ], 64 ],
67 software: env!("CARGO_PKG_NAME").to_string(), 65 software: env!("CARGO_PKG_NAME").to_string(),
68 version: env!("CARGO_PKG_VERSION").to_string(), 66 version: env!("CARGO_PKG_VERSION").to_string(),
69 67
70 // GRASP-01 Extensions 68 // GRASP-01 Extensions
71 supported_grasps: vec!["GRASP-01".to_string()], 69 supported_grasps: vec!["GRASP-01".to_string()],
72 repo_acceptance_criteria: format!( 70 repo_acceptance_criteria: format!(
@@ -77,7 +75,7 @@ impl RelayInformationDocument {
77 curation: None, // Not a curated relay - only SPAM prevention via GRASP-01 policy 75 curation: None, // Not a curated relay - only SPAM prevention via GRASP-01 policy
78 } 76 }
79 } 77 }
80 78
81 /// Serialize to JSON string 79 /// Serialize to JSON string
82 pub fn to_json(&self) -> Result<String, serde_json::Error> { 80 pub fn to_json(&self) -> Result<String, serde_json::Error> {
83 serde_json::to_string_pretty(self) 81 serde_json::to_string_pretty(self)
@@ -102,7 +100,7 @@ mod tests {
102 }; 100 };
103 101
104 let doc = RelayInformationDocument::from_config(&config); 102 let doc = RelayInformationDocument::from_config(&config);
105 103
106 assert_eq!(doc.name, "Test Relay"); 104 assert_eq!(doc.name, "Test Relay");
107 assert_eq!(doc.description, "A test relay"); 105 assert_eq!(doc.description, "A test relay");
108 assert_eq!(doc.pubkey, Some("npub1test".to_string())); 106 assert_eq!(doc.pubkey, Some("npub1test".to_string()));
@@ -129,7 +127,7 @@ mod tests {
129 127
130 let doc = RelayInformationDocument::from_config(&config); 128 let doc = RelayInformationDocument::from_config(&config);
131 let json = doc.to_json().expect("Failed to serialize to JSON"); 129 let json = doc.to_json().expect("Failed to serialize to JSON");
132 130
133 // Verify JSON contains expected fields 131 // Verify JSON contains expected fields
134 assert!(json.contains("\"name\"")); 132 assert!(json.contains("\"name\""));
135 assert!(json.contains("\"description\"")); 133 assert!(json.contains("\"description\""));
@@ -137,10 +135,10 @@ mod tests {
137 assert!(json.contains("\"supported_grasps\"")); 135 assert!(json.contains("\"supported_grasps\""));
138 assert!(json.contains("\"repo_acceptance_criteria\"")); 136 assert!(json.contains("\"repo_acceptance_criteria\""));
139 assert!(json.contains("GRASP-01")); 137 assert!(json.contains("GRASP-01"));
140 138
141 // Verify it's valid JSON by parsing 139 // Verify it's valid JSON by parsing
142 let parsed: serde_json::Value = serde_json::from_str(&json).expect("Invalid JSON"); 140 let parsed: serde_json::Value = serde_json::from_str(&json).expect("Invalid JSON");
143 assert_eq!(parsed["name"], "Test Relay"); 141 assert_eq!(parsed["name"], "Test Relay");
144 assert_eq!(parsed["supported_grasps"][0], "GRASP-01"); 142 assert_eq!(parsed["supported_grasps"][0], "GRASP-01");
145 } 143 }
146} \ No newline at end of file 144}
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs
index 8e9926a..97fd17e 100644
--- a/src/nostr/builder.rs
+++ b/src/nostr/builder.rs
@@ -51,7 +51,7 @@ impl Nip34WritePolicy {
51 /// Create a bare git repository if it doesn't exist 51 /// Create a bare git repository if it doesn't exist
52 /// Path format: <git_data_path>/<npub>/<identifier>.git 52 /// Path format: <git_data_path>/<npub>/<identifier>.git
53 fn ensure_bare_repository(&self, announcement: &RepositoryAnnouncement) -> Result<(), String> { 53 fn ensure_bare_repository(&self, announcement: &RepositoryAnnouncement) -> Result<(), String> {
54 let repo_path = self.git_data_path.join(&announcement.repo_path()); 54 let repo_path = self.git_data_path.join(announcement.repo_path());
55 55
56 // Check if repository already exists 56 // Check if repository already exists
57 if repo_path.exists() { 57 if repo_path.exists() {
@@ -69,7 +69,7 @@ impl Nip34WritePolicy {
69 69
70 // Initialize bare repository using git command 70 // Initialize bare repository using git command
71 let output = std::process::Command::new("git") 71 let output = std::process::Command::new("git")
72 .args(&["init", "--bare", repo_path.to_str().unwrap()]) 72 .args(["init", "--bare", repo_path.to_str().unwrap()])
73 .output() 73 .output()
74 .map_err(|e| format!("Failed to execute git init: {}", e))?; 74 .map_err(|e| format!("Failed to execute git init: {}", e))?;
75 75
@@ -482,7 +482,7 @@ impl Nip34WritePolicy {
482 }; 482 };
483 483
484 // Build repository path 484 // Build repository path
485 let repo_path = self.git_data_path.join(&announcement.repo_path()); 485 let repo_path = self.git_data_path.join(announcement.repo_path());
486 486
487 // Validate the ref 487 // Validate the ref
488 match git::validate_nostr_ref(&repo_path, &event_id, &expected_commit) { 488 match git::validate_nostr_ref(&repo_path, &event_id, &expected_commit) {
@@ -631,8 +631,8 @@ impl Nip34WritePolicy {
631 let kind_u16 = event.kind.as_u16(); 631 let kind_u16 = event.kind.as_u16();
632 632
633 // Check if this is any kind of replaceable event 633 // Check if this is any kind of replaceable event
634 let is_regular_replaceable = kind_u16 >= 10000 && kind_u16 < 20000; 634 let is_regular_replaceable = (10000..20000).contains(&kind_u16);
635 let is_parameterized_replaceable = kind_u16 >= 30000 && kind_u16 < 40000; 635 let is_parameterized_replaceable = (30000..40000).contains(&kind_u16);
636 636
637 if is_regular_replaceable || is_parameterized_replaceable { 637 if is_regular_replaceable || is_parameterized_replaceable {
638 // Build the appropriate address format based on event type 638 // Build the appropriate address format based on event type
@@ -669,7 +669,7 @@ impl Nip34WritePolicy {
669 ]; 669 ];
670 670
671 for tag_type in &addressable_tags { 671 for tag_type in &addressable_tags {
672 let filter = Filter::new().custom_tag(tag_type.clone(), address.clone()); 672 let filter = Filter::new().custom_tag(*tag_type, address.clone());
673 673
674 match database.query(filter).await { 674 match database.query(filter).await {
675 Ok(events) => { 675 Ok(events) => {
@@ -691,7 +691,7 @@ impl Nip34WritePolicy {
691 ]; 691 ];
692 692
693 for tag_type in &event_id_tags { 693 for tag_type in &event_id_tags {
694 let filter = Filter::new().custom_tag(tag_type.clone(), event_id_hex.clone()); 694 let filter = Filter::new().custom_tag(*tag_type, event_id_hex.clone());
695 695
696 match database.query(filter).await { 696 match database.query(filter).await {
697 Ok(events) => { 697 Ok(events) => {
diff --git a/src/nostr/events.rs b/src/nostr/events.rs
index 6a62ccd..050bfdd 100644
--- a/src/nostr/events.rs
+++ b/src/nostr/events.rs
@@ -322,9 +322,9 @@ impl RepositoryState {
322 322
323 /// Get the HEAD branch name (without refs/heads/ prefix) 323 /// Get the HEAD branch name (without refs/heads/ prefix)
324 pub fn get_head_branch(&self) -> Option<&str> { 324 pub fn get_head_branch(&self) -> Option<&str> {
325 self.head.as_ref().and_then(|h| { 325 self.head
326 h.strip_prefix("refs/heads/") 326 .as_ref()
327 }) 327 .and_then(|h| h.strip_prefix("refs/heads/"))
328 } 328 }
329 329
330 /// Check if the HEAD commit is available in the git repository 330 /// Check if the HEAD commit is available in the git repository
@@ -397,7 +397,7 @@ pub fn validate_state(event: &Event) -> Result<()> {
397#[cfg(test)] 397#[cfg(test)]
398mod tests { 398mod tests {
399 use super::*; 399 use super::*;
400 use nostr_sdk::{EventBuilder, Keys, Tag}; 400 use nostr_sdk::{EventBuilder, Keys};
401 401
402 fn create_test_keys() -> Keys { 402 fn create_test_keys() -> Keys {
403 Keys::generate() 403 Keys::generate()
@@ -618,7 +618,10 @@ mod tests {
618 618
619 let announcement = RepositoryAnnouncement::from_event(event).unwrap(); 619 let announcement = RepositoryAnnouncement::from_event(event).unwrap();
620 assert_eq!(announcement.maintainers.len(), 1); 620 assert_eq!(announcement.maintainers.len(), 1);
621 assert_eq!(announcement.maintainers[0], maintainer_keys.public_key().to_hex()); 621 assert_eq!(
622 announcement.maintainers[0],
623 maintainer_keys.public_key().to_hex()
624 );
622 } 625 }
623 626
624 #[test] 627 #[test]
@@ -727,10 +730,7 @@ mod tests {
727 730
728 let keys = create_test_keys(); 731 let keys = create_test_keys();
729 let tags = vec![ 732 let tags = vec![
730 Tag::custom( 733 Tag::custom(nostr_sdk::TagKind::d(), vec!["test-repo".to_string()]),
731 nostr_sdk::TagKind::d(),
732 vec!["test-repo".to_string()],
733 ),
734 Tag::custom( 734 Tag::custom(
735 nostr_sdk::TagKind::Custom("refs/heads/main".into()), 735 nostr_sdk::TagKind::Custom("refs/heads/main".into()),
736 vec!["a1b2c3d4".to_string()], 736 vec!["a1b2c3d4".to_string()],
diff --git a/tests/common/relay.rs b/tests/common/relay.rs
index 6b512cd..449b4cb 100644
--- a/tests/common/relay.rs
+++ b/tests/common/relay.rs
@@ -3,7 +3,6 @@
3//! Provides automatic relay lifecycle management for integration tests. 3//! Provides automatic relay lifecycle management for integration tests.
4 4
5use nostr_sdk::ToBech32; 5use nostr_sdk::ToBech32;
6use std::path::PathBuf;
7use std::process::{Child, Command, Stdio}; 6use std::process::{Child, Command, Stdio};
8use std::time::Duration; 7use std::time::Duration;
9use tokio::time::sleep; 8use tokio::time::sleep;
@@ -16,7 +15,6 @@ pub struct TestRelay {
16 process: Child, 15 process: Child,
17 url: String, 16 url: String,
18 port: u16, 17 port: u16,
19 git_data_dir: tempfile::TempDir,
20} 18}
21 19
22impl TestRelay { 20impl TestRelay {
@@ -44,8 +42,8 @@ impl TestRelay {
44 let url = format!("ws://127.0.0.1:{}", port); 42 let url = format!("ws://127.0.0.1:{}", port);
45 43
46 // Create temporary directory for git repositories 44 // Create temporary directory for git repositories
47 let git_data_dir = tempfile::tempdir() 45 let git_data_dir =
48 .expect("Failed to create temporary git data directory"); 46 tempfile::tempdir().expect("Failed to create temporary git data directory");
49 47
50 // Use the built binary directly (faster than cargo run) 48 // Use the built binary directly (faster than cargo run)
51 let binary_path = std::env::current_exe() 49 let binary_path = std::env::current_exe()
@@ -58,7 +56,9 @@ impl TestRelay {
58 56
59 // Generate a test owner npub (using a random keypair) 57 // Generate a test owner npub (using a random keypair)
60 let test_keys = nostr_sdk::Keys::generate(); 58 let test_keys = nostr_sdk::Keys::generate();
61 let test_npub = test_keys.public_key().to_bech32() 59 let test_npub = test_keys
60 .public_key()
61 .to_bech32()
62 .expect("Failed to generate test npub"); 62 .expect("Failed to generate test npub");
63 63
64 // Start the relay process 64 // Start the relay process
@@ -73,12 +73,7 @@ impl TestRelay {
73 .spawn() 73 .spawn()
74 .expect("Failed to start relay process"); 74 .expect("Failed to start relay process");
75 75
76 let relay = Self { 76 let relay = Self { process, url, port };
77 process,
78 url,
79 port,
80 git_data_dir,
81 };
82 77
83 // Wait for relay to be ready 78 // Wait for relay to be ready
84 relay.wait_for_ready().await; 79 relay.wait_for_ready().await;
@@ -91,30 +86,11 @@ impl TestRelay {
91 &self.url 86 &self.url
92 } 87 }
93 88
94 /// Get the relay port
95 pub fn port(&self) -> u16 {
96 self.port
97 }
98
99 /// Get the relay domain (host:port) 89 /// Get the relay domain (host:port)
100 pub fn domain(&self) -> String { 90 pub fn domain(&self) -> String {
101 format!("127.0.0.1:{}", self.port) 91 format!("127.0.0.1:{}", self.port)
102 } 92 }
103 93
104 /// Get the git data directory path
105 pub fn git_data_dir(&self) -> &std::path::Path {
106 self.git_data_dir.path()
107 }
108
109 /// Get the expected repository path for a given npub and repo identifier
110 ///
111 /// Repositories are stored at: <git_data_dir>/<npub>/<identifier>.git
112 pub fn repo_path(&self, npub: &str, identifier: &str) -> PathBuf {
113 self.git_data_dir.path()
114 .join(npub)
115 .join(format!("{}.git", identifier))
116 }
117
118 /// Wait for the relay to be ready to accept connections 94 /// Wait for the relay to be ready to accept connections
119 async fn wait_for_ready(&self) { 95 async fn wait_for_ready(&self) {
120 let max_attempts = 50; // 5 seconds total 96 let max_attempts = 50; // 5 seconds total
@@ -183,15 +159,6 @@ impl Drop for TestRelay {
183mod tests { 159mod tests {
184 use super::*; 160 use super::*;
185 161
186 #[tokio::test]
187 #[ignore] // Requires relay binary to be built
188 async fn test_relay_lifecycle() {
189 let relay = TestRelay::start().await;
190 assert!(relay.url().starts_with("ws://127.0.0.1:"));
191 assert!(relay.port() > 0);
192 relay.stop().await;
193 }
194
195 #[test] 162 #[test]
196 fn test_find_free_port() { 163 fn test_find_free_port() {
197 let port = TestRelay::find_free_port(); 164 let port = TestRelay::find_free_port();
diff --git a/tests/cors.rs b/tests/cors.rs
index a27c145..b5a0a87 100644
--- a/tests/cors.rs
+++ b/tests/cors.rs
@@ -55,39 +55,10 @@ macro_rules! isolated_cors_test {
55 }; 55 };
56} 56}
57 57
58/// Macro for CORS tests that need git_data_dir (the full integration test)
59macro_rules! isolated_cors_test_with_repo {
60 ($test_name:ident) => {
61 #[tokio::test]
62 async fn $test_name() {
63 let relay = TestRelay::start().await;
64 let config = AuditConfig::ci();
65 let client = AuditClient::new(relay.url(), config)
66 .await
67 .expect("Failed to create audit client");
68
69 let result = CorsTests::$test_name(
70 &client,
71 &relay.domain(),
72 )
73 .await;
74
75 relay.stop().await;
76
77 assert!(
78 result.passed,
79 "{} failed: {}",
80 stringify!($test_name),
81 result.error.as_deref().unwrap_or("unknown error")
82 );
83 }
84 };
85}
86
87// Generate isolated tests for all CORS tests 58// Generate isolated tests for all CORS tests
88isolated_cors_test!(test_cors_allow_origin); 59isolated_cors_test!(test_cors_allow_origin);
89isolated_cors_test!(test_cors_allow_methods); 60isolated_cors_test!(test_cors_allow_methods);
90isolated_cors_test!(test_cors_allow_headers); 61isolated_cors_test!(test_cors_allow_headers);
91isolated_cors_test!(test_cors_options_preflight); 62isolated_cors_test!(test_cors_options_preflight);
92 63
93isolated_cors_test!(test_cors_on_real_repo); \ No newline at end of file 64isolated_cors_test!(test_cors_on_real_repo);
diff --git a/tests/git_clone.rs b/tests/git_clone.rs
index ffb04a3..c8a91a2 100644
--- a/tests/git_clone.rs
+++ b/tests/git_clone.rs
@@ -42,11 +42,7 @@ macro_rules! isolated_test {
42 .await 42 .await
43 .expect("Failed to create audit client"); 43 .expect("Failed to create audit client");
44 44
45 let result = GitCloneTests::$test_name( 45 let result = GitCloneTests::$test_name(&client, &relay.domain()).await;
46 &client,
47 &relay.domain(),
48 )
49 .await;
50 46
51 relay.stop().await; 47 relay.stop().await;
52 48
@@ -63,4 +59,4 @@ macro_rules! isolated_test {
63// Generate isolated tests for all git clone tests 59// Generate isolated tests for all git clone tests
64isolated_test!(test_basic_git_clone); 60isolated_test!(test_basic_git_clone);
65isolated_test!(test_clone_url_format); 61isolated_test!(test_clone_url_format);
66isolated_test!(test_sha1_capabilities_advertised); \ No newline at end of file 62isolated_test!(test_sha1_capabilities_advertised);
diff --git a/tests/nip11_document.rs b/tests/nip11_document.rs
index da8e9ce..2104ad0 100644
--- a/tests/nip11_document.rs
+++ b/tests/nip11_document.rs
@@ -59,4 +59,4 @@ macro_rules! isolated_test {
59isolated_test!(test_nip11_document_exists); 59isolated_test!(test_nip11_document_exists);
60isolated_test!(test_nip11_supported_grasps_field); 60isolated_test!(test_nip11_supported_grasps_field);
61isolated_test!(test_nip11_repo_acceptance_criteria_field); 61isolated_test!(test_nip11_repo_acceptance_criteria_field);
62isolated_test!(test_nip11_curation_field); \ No newline at end of file 62isolated_test!(test_nip11_curation_field);
diff --git a/tests/nip34_announcements.rs b/tests/nip34_announcements.rs
index 09d9c8f..2a83886 100644
--- a/tests/nip34_announcements.rs
+++ b/tests/nip34_announcements.rs
@@ -71,4 +71,4 @@ isolated_test!(test_accept_comment_referenced_in_comment);
71isolated_test!(test_accept_kind1_referenced_in_kind1); 71isolated_test!(test_accept_kind1_referenced_in_kind1);
72isolated_test!(test_reject_orphan_issue); 72isolated_test!(test_reject_orphan_issue);
73isolated_test!(test_reject_orphan_kind1); 73isolated_test!(test_reject_orphan_kind1);
74isolated_test!(test_reject_comment_quoting_other_repo); \ No newline at end of file 74isolated_test!(test_reject_comment_quoting_other_repo);
diff --git a/tests/push_authorization.rs b/tests/push_authorization.rs
index 1f8e0ca..357fefb 100644
--- a/tests/push_authorization.rs
+++ b/tests/push_authorization.rs
@@ -43,10 +43,7 @@ macro_rules! isolated_push_test {
43 .await 43 .await
44 .expect("Failed to create audit client"); 44 .expect("Failed to create audit client");
45 45
46 let result = PushAuthorizationTests::$test_name( 46 let result = PushAuthorizationTests::$test_name(&client, &relay.domain()).await;
47 &client,
48 &relay.domain()
49 ).await;
50 47
51 relay.stop().await; 48 relay.stop().await;
52 49
@@ -70,4 +67,4 @@ isolated_push_test!(test_push_to_nostr_ref_with_invalid_event_id_rejected);
70isolated_push_test!(test_pr_push_to_nostr_ref_with_wrong_commit_accepted_before_event_received); 67isolated_push_test!(test_pr_push_to_nostr_ref_with_wrong_commit_accepted_before_event_received);
71isolated_push_test!(test_pr_event_published_removes_nostr_ref_at_incorrect_commit); 68isolated_push_test!(test_pr_event_published_removes_nostr_ref_at_incorrect_commit);
72isolated_push_test!(test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected); 69isolated_push_test!(test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected);
73isolated_push_test!(test_push_to_nostr_ref_with_correct_commit_after_event_received_accepted); \ No newline at end of file 70isolated_push_test!(test_push_to_nostr_ref_with_correct_commit_after_event_received_accepted);