upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/specs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-28 03:38:50 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-28 03:38:50 +0000
commitf41550ea1898be2ec6c4be205e4cad0085400313 (patch)
tree00cc474031bf81fe382c6276e52fd769b275cd3f /grasp-audit/src/specs
parent3f74ababf338d65ac5e29e7eb5541ce416b7fe75 (diff)
audit: stop checking git_data_directory
Diffstat (limited to 'grasp-audit/src/specs')
-rw-r--r--grasp-audit/src/specs/grasp01/cors.rs1
-rw-r--r--grasp-audit/src/specs/grasp01/git_clone.rs45
-rw-r--r--grasp-audit/src/specs/grasp01/mod.rs2
-rw-r--r--grasp-audit/src/specs/grasp01/push_authorization.rs64
-rw-r--r--grasp-audit/src/specs/grasp01/repository_creation.rs208
5 files changed, 82 insertions, 238 deletions
diff --git a/grasp-audit/src/specs/grasp01/cors.rs b/grasp-audit/src/specs/grasp01/cors.rs
index 4e0513e..08c5ab6 100644
--- a/grasp-audit/src/specs/grasp01/cors.rs
+++ b/grasp-audit/src/specs/grasp01/cors.rs
@@ -257,7 +257,6 @@ impl CorsTests {
257 /// For integration tests that want to test against real repositories 257 /// For integration tests that want to test against real repositories
258 pub async fn test_cors_on_real_repo( 258 pub async fn test_cors_on_real_repo(
259 client: &AuditClient, 259 client: &AuditClient,
260 _git_data_dir: &Path,
261 relay_domain: &str, 260 relay_domain: &str,
262 ) -> TestResult { 261 ) -> TestResult {
263 let test_name = "test_cors_on_real_repo"; 262 let test_name = "test_cors_on_real_repo";
diff --git a/grasp-audit/src/specs/grasp01/git_clone.rs b/grasp-audit/src/specs/grasp01/git_clone.rs
index 4666a40..9ee6ed7 100644
--- a/grasp-audit/src/specs/grasp01/git_clone.rs
+++ b/grasp-audit/src/specs/grasp01/git_clone.rs
@@ -18,7 +18,6 @@
18use crate::{AuditClient, FixtureKind, TestContext, TestResult}; 18use crate::{AuditClient, FixtureKind, TestContext, TestResult};
19use nostr_sdk::prelude::*; 19use nostr_sdk::prelude::*;
20use std::fs; 20use std::fs;
21use std::path::Path;
22use std::process::Command; 21use std::process::Command;
23 22
24/// Test suite for Git clone operations 23/// Test suite for Git clone operations
@@ -28,14 +27,13 @@ impl GitCloneTests {
28 /// Run all Git clone tests 27 /// Run all Git clone tests
29 pub async fn run_all( 28 pub async fn run_all(
30 client: &AuditClient, 29 client: &AuditClient,
31 git_data_dir: &Path,
32 relay_domain: &str, 30 relay_domain: &str,
33 ) -> crate::AuditResult { 31 ) -> crate::AuditResult {
34 let mut results = crate::AuditResult::new("GRASP-01 Git Clone Tests"); 32 let mut results = crate::AuditResult::new("GRASP-01 Git Clone Tests");
35 33
36 results.add(Self::test_basic_git_clone(client, git_data_dir, relay_domain).await); 34 results.add(Self::test_basic_git_clone(client, relay_domain).await);
37 results.add(Self::test_clone_url_format(client, git_data_dir, relay_domain).await); 35 results.add(Self::test_clone_url_format(client, relay_domain).await);
38 results.add(Self::test_sha1_capabilities_advertised(client, git_data_dir, relay_domain).await); 36 results.add(Self::test_sha1_capabilities_advertised(client, relay_domain).await);
39 37
40 results 38 results
41 } 39 }
@@ -49,7 +47,6 @@ impl GitCloneTests {
49 /// 4. Verifies the clone succeeded 47 /// 4. Verifies the clone succeeded
50 pub async fn test_basic_git_clone( 48 pub async fn test_basic_git_clone(
51 client: &AuditClient, 49 client: &AuditClient,
52 git_data_dir: &Path,
53 relay_domain: &str, 50 relay_domain: &str,
54 ) -> TestResult { 51 ) -> TestResult {
55 let test_name = "test_basic_git_clone"; 52 let test_name = "test_basic_git_clone";
@@ -101,20 +98,6 @@ impl GitCloneTests {
101 } 98 }
102 }; 99 };
103 100
104 // Verify repository exists
105 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
106 if !repo_path.exists() {
107 return TestResult::new(
108 test_name,
109 "GRASP-01",
110 "Repository must be cloneable via Git HTTP backend",
111 )
112 .fail(&format!(
113 "Repository not found at: {}",
114 repo_path.display()
115 ));
116 }
117
118 // Create a test clone directory using standard library 101 // Create a test clone directory using standard library
119 let temp_base = std::env::temp_dir(); 102 let temp_base = std::env::temp_dir();
120 let clone_dir_name = format!("grasp-test-clone-{}", uuid::Uuid::new_v4()); 103 let clone_dir_name = format!("grasp-test-clone-{}", uuid::Uuid::new_v4());
@@ -128,7 +111,7 @@ impl GitCloneTests {
128 111
129 // Attempt to clone the repository 112 // Attempt to clone the repository
130 let output = Command::new("git") 113 let output = Command::new("git")
131 .args(&["clone", &clone_url, clone_path.to_str().unwrap()]) 114 .args(["clone", &clone_url, clone_path.to_str().unwrap()])
132 .env("GIT_TERMINAL_PROMPT", "0") // Disable password prompts 115 .env("GIT_TERMINAL_PROMPT", "0") // Disable password prompts
133 .output(); 116 .output();
134 117
@@ -188,7 +171,6 @@ impl GitCloneTests {
188 /// 2. Invalid URLs are rejected properly 171 /// 2. Invalid URLs are rejected properly
189 pub async fn test_clone_url_format( 172 pub async fn test_clone_url_format(
190 client: &AuditClient, 173 client: &AuditClient,
191 _git_data_dir: &Path,
192 relay_domain: &str, 174 relay_domain: &str,
193 ) -> TestResult { 175 ) -> TestResult {
194 let test_name = "test_clone_url_format"; 176 let test_name = "test_clone_url_format";
@@ -254,7 +236,7 @@ impl GitCloneTests {
254 let invalid_url = format!("http://{}/invalid/path", relay_domain); 236 let invalid_url = format!("http://{}/invalid/path", relay_domain);
255 237
256 let output = Command::new("git") 238 let output = Command::new("git")
257 .args(&["clone", &invalid_url, clone_path.to_str().unwrap()]) 239 .args(["clone", &invalid_url, clone_path.to_str().unwrap()])
258 .env("GIT_TERMINAL_PROMPT", "0") 240 .env("GIT_TERMINAL_PROMPT", "0")
259 .output() 241 .output()
260 .unwrap(); 242 .unwrap();
@@ -291,7 +273,6 @@ impl GitCloneTests {
291 /// 2. Both allow-reachable-sha1-in-want and allow-tip-sha1-in-want are present 273 /// 2. Both allow-reachable-sha1-in-want and allow-tip-sha1-in-want are present
292 pub async fn test_sha1_capabilities_advertised( 274 pub async fn test_sha1_capabilities_advertised(
293 client: &AuditClient, 275 client: &AuditClient,
294 git_data_dir: &Path,
295 relay_domain: &str, 276 relay_domain: &str,
296 ) -> TestResult { 277 ) -> TestResult {
297 let test_name = "test_sha1_capabilities_advertised"; 278 let test_name = "test_sha1_capabilities_advertised";
@@ -343,20 +324,6 @@ impl GitCloneTests {
343 } 324 }
344 }; 325 };
345 326
346 // Verify repository exists
347 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
348 if !repo_path.exists() {
349 return TestResult::new(
350 test_name,
351 "GRASP-01",
352 "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement",
353 )
354 .fail(&format!(
355 "Repository not found at: {}",
356 repo_path.display()
357 ));
358 }
359
360 // Build info/refs URL for git-upload-pack service 327 // Build info/refs URL for git-upload-pack service
361 let info_refs_url = format!( 328 let info_refs_url = format!(
362 "http://{}/{}/{}.git/info/refs?service=git-upload-pack", 329 "http://{}/{}/{}.git/info/refs?service=git-upload-pack",
@@ -435,8 +402,6 @@ impl GitCloneTests {
435 402
436#[cfg(test)] 403#[cfg(test)]
437mod tests { 404mod tests {
438
439
440 #[test] 405 #[test]
441 fn test_module_exists() { 406 fn test_module_exists() {
442 // Simple compilation test 407 // Simple compilation test
diff --git a/grasp-audit/src/specs/grasp01/mod.rs b/grasp-audit/src/specs/grasp01/mod.rs
index b5471d1..6f58b96 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::{is_bare_repository, RepositoryCreationTests}; 29pub use repository_creation::{RepositoryCreationTests};
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs
index 4599ea5..69664d6 100644
--- a/grasp-audit/src/specs/grasp01/push_authorization.rs
+++ b/grasp-audit/src/specs/grasp01/push_authorization.rs
@@ -33,15 +33,14 @@ impl PushAuthorizationTests {
33 /// Run all push authorization tests 33 /// Run all push authorization tests
34 pub async fn run_all( 34 pub async fn run_all(
35 client: &AuditClient, 35 client: &AuditClient,
36 git_data_dir: &Path,
37 relay_domain: &str, 36 relay_domain: &str,
38 ) -> crate::AuditResult { 37 ) -> crate::AuditResult {
39 let mut results = crate::AuditResult::new("GRASP-01 Push Authorization Tests"); 38 let mut results = crate::AuditResult::new("GRASP-01 Push Authorization Tests");
40 39
41 results.add(Self::test_push_authorized_by_owner_state(client, git_data_dir, relay_domain).await); 40 results.add(Self::test_push_authorized_by_owner_state(client, relay_domain).await);
42 results.add(Self::test_push_rejected_without_state_event(client, git_data_dir, relay_domain).await); 41 results.add(Self::test_push_rejected_without_state_event(client, relay_domain).await);
43 results.add(Self::test_push_rejected_wrong_commit(client, git_data_dir, relay_domain).await); 42 results.add(Self::test_push_rejected_wrong_commit(client, relay_domain).await);
44 results.add(Self::test_push_authorized_by_maintainer_state_only(client, git_data_dir, relay_domain).await); 43 results.add(Self::test_push_authorized_by_maintainer_state_only(client, relay_domain).await);
45 44
46 results 45 results
47 } 46 }
@@ -59,7 +58,6 @@ impl PushAuthorizationTests {
59 /// 3. **Verify**: Push should succeed because state event authorizes this commit 58 /// 3. **Verify**: Push should succeed because state event authorizes this commit
60 pub async fn test_push_authorized_by_owner_state( 59 pub async fn test_push_authorized_by_owner_state(
61 client: &AuditClient, 60 client: &AuditClient,
62 git_data_dir: &Path,
63 relay_domain: &str, 61 relay_domain: &str,
64 ) -> TestResult { 62 ) -> TestResult {
65 use std::process::Command; 63 use std::process::Command;
@@ -103,13 +101,6 @@ impl PushAuthorizationTests {
103 } 101 }
104 }; 102 };
105 103
106 // Verify repo exists on disk
107 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
108 if !repo_path.exists() {
109 return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state")
110 .fail(&format!("Repo not found: {}", repo_path.display()));
111 }
112
113 // ============================================================ 104 // ============================================================
114 // Step 2: SEND - Clone repo, create deterministic commit, push 105 // Step 2: SEND - Clone repo, create deterministic commit, push
115 // ============================================================ 106 // ============================================================
@@ -222,7 +213,6 @@ impl PushAuthorizationTests {
222 /// Test that push is rejected when no state event exists 213 /// Test that push is rejected when no state event exists
223 pub async fn test_push_rejected_without_state_event( 214 pub async fn test_push_rejected_without_state_event(
224 client: &AuditClient, 215 client: &AuditClient,
225 git_data_dir: &Path,
226 relay_domain: &str, 216 relay_domain: &str,
227 ) -> TestResult { 217 ) -> TestResult {
228 let test_name = "test_push_rejected_without_state_event"; 218 let test_name = "test_push_rejected_without_state_event";
@@ -243,12 +233,6 @@ impl PushAuthorizationTests {
243 .and_then(|t| t.content()).unwrap().to_string(); 233 .and_then(|t| t.content()).unwrap().to_string();
244 let npub = repo.pubkey.to_bech32().unwrap(); 234 let npub = repo.pubkey.to_bech32().unwrap();
245 235
246 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
247 if !repo_path.exists() {
248 return TestResult::new(test_name, "GRASP-01", "Push rejected without state event")
249 .fail(&format!("Repo not found: {}", repo_path.display()));
250 }
251
252 // Clone and create commit 236 // Clone and create commit
253 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { 237 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
254 Ok(p) => p, 238 Ok(p) => p,
@@ -286,7 +270,6 @@ impl PushAuthorizationTests {
286 /// 4. **Verify**: Push should be rejected because new commit doesn't match state event 270 /// 4. **Verify**: Push should be rejected because new commit doesn't match state event
287 pub async fn test_push_rejected_wrong_commit( 271 pub async fn test_push_rejected_wrong_commit(
288 client: &AuditClient, 272 client: &AuditClient,
289 git_data_dir: &Path,
290 relay_domain: &str, 273 relay_domain: &str,
291 ) -> TestResult { 274 ) -> TestResult {
292 use std::process::Command; 275 use std::process::Command;
@@ -330,13 +313,6 @@ impl PushAuthorizationTests {
330 } 313 }
331 }; 314 };
332 315
333 // Verify repo exists on disk
334 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
335 if !repo_path.exists() {
336 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
337 .fail(&format!("Repo not found: {}", repo_path.display()));
338 }
339
340 // ============================================================ 316 // ============================================================
341 // Step 2: SEND - Clone repo, create deterministic commit, push 317 // Step 2: SEND - Clone repo, create deterministic commit, push
342 // (establishes the state on the relay) 318 // (establishes the state on the relay)
@@ -495,7 +471,6 @@ impl PushAuthorizationTests {
495 /// 4. The push should be ACCEPTED because maintainer's state event authorizes it 471 /// 4. The push should be ACCEPTED because maintainer's state event authorizes it
496 pub async fn test_push_authorized_by_maintainer_state_only( 472 pub async fn test_push_authorized_by_maintainer_state_only(
497 client: &AuditClient, 473 client: &AuditClient,
498 git_data_dir: &Path,
499 relay_domain: &str, 474 relay_domain: &str,
500 ) -> TestResult { 475 ) -> TestResult {
501 use std::process::Command; 476 use std::process::Command;
@@ -566,17 +541,6 @@ impl PushAuthorizationTests {
566 } 541 }
567 }; 542 };
568 543
569 // Verify repo exists on disk
570 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
571 if !repo_path.exists() {
572 return TestResult::new(
573 test_name,
574 "GRASP-01",
575 "Push authorized by maintainer state event only (no announcement)",
576 )
577 .fail(&format!("Repo not found: {}", repo_path.display()));
578 }
579
580 // ============================================================ 544 // ============================================================
581 // Step 2: SEND - Clone, create maintainer commit, push 545 // Step 2: SEND - Clone, create maintainer commit, push
582 // ============================================================ 546 // ============================================================
@@ -741,7 +705,6 @@ impl PushAuthorizationTests {
741 /// Each level publishes announcements that authorize the next level. 705 /// Each level publishes announcements that authorize the next level.
742 pub async fn test_push_authorized_by_recursive_maintainer_state( 706 pub async fn test_push_authorized_by_recursive_maintainer_state(
743 client: &AuditClient, 707 client: &AuditClient,
744 git_data_dir: &Path,
745 relay_domain: &str, 708 relay_domain: &str,
746 ) -> TestResult { 709 ) -> TestResult {
747 use std::process::Command; 710 use std::process::Command;
@@ -837,17 +800,6 @@ impl PushAuthorizationTests {
837 } 800 }
838 }; 801 };
839 802
840 // Verify repo exists on disk
841 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
842 if !repo_path.exists() {
843 return TestResult::new(
844 test_name,
845 "GRASP-01",
846 "Push authorized by recursive maintainer state event",
847 )
848 .fail(&format!("Repo not found: {}", repo_path.display()));
849 }
850
851 // ============================================================ 803 // ============================================================
852 // Step 2: SEND - Clone, create recursive maintainer commit, push 804 // Step 2: SEND - Clone, create recursive maintainer commit, push
853 // ============================================================ 805 // ============================================================
@@ -1007,7 +959,6 @@ impl PushAuthorizationTests {
1007 /// 5. **Verify**: Push should be rejected because rogue state event is ignored 959 /// 5. **Verify**: Push should be rejected because rogue state event is ignored
1008 pub async fn test_non_maintainer_state_rejected( 960 pub async fn test_non_maintainer_state_rejected(
1009 client: &AuditClient, 961 client: &AuditClient,
1010 git_data_dir: &Path,
1011 relay_domain: &str, 962 relay_domain: &str,
1012 ) -> TestResult { 963 ) -> TestResult {
1013 use std::process::Command; 964 use std::process::Command;
@@ -1051,13 +1002,6 @@ impl PushAuthorizationTests {
1051 } 1002 }
1052 }; 1003 };
1053 1004
1054 // Verify repo exists on disk
1055 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
1056 if !repo_path.exists() {
1057 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1058 .fail(&format!("Repo not found: {}", repo_path.display()));
1059 }
1060
1061 // ============================================================ 1005 // ============================================================
1062 // Step 2: SEND - Clone repo, create deterministic commit, push 1006 // Step 2: SEND - Clone repo, create deterministic commit, push
1063 // (establishes the state on the relay) 1007 // (establishes the state on the relay)
diff --git a/grasp-audit/src/specs/grasp01/repository_creation.rs b/grasp-audit/src/specs/grasp01/repository_creation.rs
index 2eaf32f..588187b 100644
--- a/grasp-audit/src/specs/grasp01/repository_creation.rs
+++ b/grasp-audit/src/specs/grasp01/repository_creation.rs
@@ -5,10 +5,9 @@
5//! 5//!
6//! ## Test Coverage 6//! ## Test Coverage
7//! 7//!
8//! - Repository creation on valid announcement 8//! - Repository creation on valid announcement (verified via HTTP)
9//! - Idempotent creation (no error if repo already exists) 9//! - Idempotent creation (no error if repo already exists)
10//! - Proper directory structure (<npub>/<identifier>.git) 10//! - Repository accessibility via Smart HTTP service
11//! - Bare repository validation (has HEAD, config, objects, refs)
12//! 11//!
13//! ## Running Tests 12//! ## Running Tests
14//! 13//!
@@ -18,7 +17,6 @@
18 17
19use crate::{AuditClient, FixtureKind, TestContext, TestResult}; 18use crate::{AuditClient, FixtureKind, TestContext, TestResult};
20use nostr_sdk::prelude::*; 19use nostr_sdk::prelude::*;
21use std::path::Path;
22 20
23/// Test suite for repository creation 21/// Test suite for repository creation
24pub struct RepositoryCreationTests; 22pub struct RepositoryCreationTests;
@@ -27,13 +25,13 @@ impl RepositoryCreationTests {
27 /// Run all repository creation tests 25 /// Run all repository creation tests
28 pub async fn run_all( 26 pub async fn run_all(
29 client: &AuditClient, 27 client: &AuditClient,
30 git_data_dir: &Path, 28 relay_domain: &str,
31 ) -> crate::AuditResult { 29 ) -> crate::AuditResult {
32 let mut results = crate::AuditResult::new("GRASP-01 Repository Creation Tests"); 30 let mut results = crate::AuditResult::new("GRASP-01 Repository Creation Tests");
33 31
34 results.add(Self::test_bare_repo_created_on_announcement(client, git_data_dir).await); 32 results.add(Self::test_bare_repo_created_on_announcement(client, relay_domain).await);
35 results.add(Self::test_repo_creation_idempotent(client, git_data_dir).await); 33 results.add(Self::test_repo_creation_idempotent(client, relay_domain).await);
36 results.add(Self::test_bare_repo_structure(client, git_data_dir).await); 34 results.add(Self::test_repo_accessible_via_http(client, relay_domain).await);
37 35
38 results 36 results
39 } 37 }
@@ -43,10 +41,10 @@ impl RepositoryCreationTests {
43 /// This test: 41 /// This test:
44 /// 1. Sends a valid repository announcement via TestContext 42 /// 1. Sends a valid repository announcement via TestContext
45 /// 2. Verifies the announcement was accepted 43 /// 2. Verifies the announcement was accepted
46 /// 3. Checks that a bare git repository was created at the expected path 44 /// 3. Verifies the repository is accessible via Smart HTTP service
47 pub async fn test_bare_repo_created_on_announcement( 45 pub async fn test_bare_repo_created_on_announcement(
48 client: &AuditClient, 46 client: &AuditClient,
49 git_data_dir: &Path, 47 relay_domain: &str,
50 ) -> TestResult { 48 ) -> TestResult {
51 let test_name = "test_bare_repo_created_on_announcement"; 49 let test_name = "test_bare_repo_created_on_announcement";
52 let ctx = TestContext::new(client); 50 let ctx = TestContext::new(client);
@@ -97,19 +95,14 @@ impl RepositoryCreationTests {
97 } 95 }
98 }; 96 };
99 97
100 // Check if repository was created 98 // Verify repository exists via HTTP (info/refs endpoint)
101 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id)); 99 if let Err(e) = check_repo_accessible_via_http(relay_domain, &npub, &repo_id).await {
102
103 if !is_bare_repository(&repo_path) {
104 return TestResult::new( 100 return TestResult::new(
105 test_name, 101 test_name,
106 "GRASP-01", 102 "GRASP-01",
107 "Bare repository must be created when announcement is accepted", 103 "Bare repository must be created when announcement is accepted",
108 ) 104 )
109 .fail(&format!( 105 .fail(&format!("Repository not accessible via HTTP: {}", e));
110 "Bare repository not found at: {}",
111 repo_path.display()
112 ));
113 } 106 }
114 107
115 TestResult::new( 108 TestResult::new(
@@ -128,7 +121,7 @@ impl RepositoryCreationTests {
128 /// 3. Verifies no error occurs and repo still exists 121 /// 3. Verifies no error occurs and repo still exists
129 pub async fn test_repo_creation_idempotent( 122 pub async fn test_repo_creation_idempotent(
130 client: &AuditClient, 123 client: &AuditClient,
131 git_data_dir: &Path, 124 relay_domain: &str,
132 ) -> TestResult { 125 ) -> TestResult {
133 let test_name = "test_repo_creation_idempotent"; 126 let test_name = "test_repo_creation_idempotent";
134 let ctx = TestContext::new(client); 127 let ctx = TestContext::new(client);
@@ -162,7 +155,7 @@ impl RepositoryCreationTests {
162 // Wait again 155 // Wait again
163 tokio::time::sleep(std::time::Duration::from_millis(200)).await; 156 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
164 157
165 // Verify repository still exists and is valid 158 // Verify repository still exists and is accessible via HTTP
166 let repo_id = repo 159 let repo_id = repo
167 .tags 160 .tags
168 .iter() 161 .iter()
@@ -173,15 +166,14 @@ impl RepositoryCreationTests {
173 .to_string(); 166 .to_string();
174 167
175 let npub = repo.pubkey.to_bech32().unwrap(); 168 let npub = repo.pubkey.to_bech32().unwrap();
176 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
177 169
178 if !is_bare_repository(&repo_path) { 170 if let Err(e) = check_repo_accessible_via_http(relay_domain, &npub, &repo_id).await {
179 return TestResult::new( 171 return TestResult::new(
180 test_name, 172 test_name,
181 "GRASP-01", 173 "GRASP-01",
182 "Repository creation must be idempotent", 174 "Repository creation must be idempotent",
183 ) 175 )
184 .fail("Repository not found after second send"); 176 .fail(&format!("Repository not accessible after second send: {}", e));
185 } 177 }
186 178
187 TestResult::new( 179 TestResult::new(
@@ -192,14 +184,16 @@ impl RepositoryCreationTests {
192 .pass() 184 .pass()
193 } 185 }
194 186
195 /// Test that the repository has the correct structure 187 /// Test that the repository is accessible via Smart HTTP service
196 /// 188 ///
197 /// This test verifies: 189 /// This test verifies:
198 /// 1. Repository is at <git_data_path>/<npub>/<identifier>.git 190 /// 1. Repository responds to git-upload-pack service discovery
199 /// 2. Repository is bare (no working directory) 191 /// 2. URL format follows http://domain/npub/identifier.git pattern
200 /// 3. Repository has required git structure (HEAD, config, objects/, refs/) 192 pub async fn test_repo_accessible_via_http(
201 pub async fn test_bare_repo_structure(client: &AuditClient, git_data_dir: &Path) -> TestResult { 193 client: &AuditClient,
202 let test_name = "test_bare_repo_structure"; 194 relay_domain: &str,
195 ) -> TestResult {
196 let test_name = "test_repo_accessible_via_http";
203 let ctx = TestContext::new(client); 197 let ctx = TestContext::new(client);
204 198
205 // Create and send repository announcement via TestContext 199 // Create and send repository announcement via TestContext
@@ -209,7 +203,7 @@ impl RepositoryCreationTests {
209 return TestResult::new( 203 return TestResult::new(
210 test_name, 204 test_name,
211 "GRASP-01", 205 "GRASP-01",
212 "Bare repository must have correct structure", 206 "Repository must be accessible via Smart HTTP service",
213 ) 207 )
214 .fail(&format!("Failed to create repo fixture: {}", e)) 208 .fail(&format!("Failed to create repo fixture: {}", e))
215 } 209 }
@@ -230,134 +224,76 @@ impl RepositoryCreationTests {
230 224
231 let npub = repo.pubkey.to_bech32().unwrap(); 225 let npub = repo.pubkey.to_bech32().unwrap();
232 226
233 // Verify correct path structure: <git_data_path>/<npub>/<identifier>.git 227 // Verify repository is accessible via HTTP
234 let expected_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id)); 228 if let Err(e) = check_repo_accessible_via_http(relay_domain, &npub, &repo_id).await {
235
236 if !expected_path.exists() {
237 return TestResult::new(
238 test_name,
239 "GRASP-01",
240 "Bare repository must have correct structure",
241 )
242 .fail(&format!(
243 "Repository not at expected path: {}",
244 expected_path.display()
245 ));
246 }
247
248 // Verify it's a bare repository with correct structure
249 if !expected_path.join("HEAD").is_file() {
250 return TestResult::new(
251 test_name,
252 "GRASP-01",
253 "Bare repository must have correct structure",
254 )
255 .fail("Missing HEAD file");
256 }
257
258 if !expected_path.join("config").is_file() {
259 return TestResult::new(
260 test_name,
261 "GRASP-01",
262 "Bare repository must have correct structure",
263 )
264 .fail("Missing config file");
265 }
266
267 if !expected_path.join("objects").is_dir() {
268 return TestResult::new( 229 return TestResult::new(
269 test_name, 230 test_name,
270 "GRASP-01", 231 "GRASP-01",
271 "Bare repository must have correct structure", 232 "Repository must be accessible via Smart HTTP service",
272 ) 233 )
273 .fail("Missing objects/ directory"); 234 .fail(&e);
274 }
275
276 if !expected_path.join("refs").is_dir() {
277 return TestResult::new(
278 test_name,
279 "GRASP-01",
280 "Bare repository must have correct structure",
281 )
282 .fail("Missing refs/ directory");
283 }
284
285 // Verify the helper function agrees
286 if !is_bare_repository(&expected_path) {
287 return TestResult::new(
288 test_name,
289 "GRASP-01",
290 "Bare repository must have correct structure",
291 )
292 .fail("Helper function does not recognize repository as bare");
293 } 235 }
294 236
295 TestResult::new( 237 TestResult::new(
296 test_name, 238 test_name,
297 "GRASP-01", 239 "GRASP-01",
298 "Bare repository must have correct structure", 240 "Repository must be accessible via Smart HTTP service",
299 ) 241 )
300 .pass() 242 .pass()
301 } 243 }
302} 244}
303 245
304/// Helper function to check if a path is a valid bare git repository 246/// Helper function to check if a repository is accessible via Smart HTTP service
305/// 247///
306/// A bare repository must have: 248/// Verifies that the repository responds correctly to git-upload-pack service discovery
307/// - HEAD file 249/// at the URL: http://domain/npub/identifier.git/info/refs?service=git-upload-pack
308/// - config file 250async fn check_repo_accessible_via_http(
309/// - objects/ directory 251 relay_domain: &str,
310/// - refs/ directory 252 npub: &str,
311pub fn is_bare_repository(path: &Path) -> bool { 253 repo_id: &str,
312 if !path.exists() { 254) -> Result<(), String> {
313 return false; 255 let info_refs_url = format!(
256 "http://{}/{}/{}.git/info/refs?service=git-upload-pack",
257 relay_domain, npub, repo_id
258 );
259
260 let http_client = reqwest::Client::new();
261 let response = http_client
262 .get(&info_refs_url)
263 .send()
264 .await
265 .map_err(|e| format!("HTTP request failed: {}", e))?;
266
267 if !response.status().is_success() {
268 return Err(format!(
269 "info/refs returned status {} for URL: {}",
270 response.status(),
271 info_refs_url
272 ));
314 } 273 }
315 274
316 // Check for required bare repository components 275 // Verify Content-Type indicates git-upload-pack service
317 let has_head = path.join("HEAD").is_file(); 276 let content_type = response
318 let has_config = path.join("config").is_file(); 277 .headers()
319 let has_objects = path.join("objects").is_dir(); 278 .get("content-type")
320 let has_refs = path.join("refs").is_dir(); 279 .and_then(|v| v.to_str().ok())
280 .unwrap_or("");
281
282 if !content_type.contains("application/x-git-upload-pack-advertisement") {
283 return Err(format!(
284 "Expected Content-Type: application/x-git-upload-pack-advertisement, got: {}",
285 content_type
286 ));
287 }
321 288
322 has_head && has_config && has_objects && has_refs 289 Ok(())
323} 290}
324 291
325#[cfg(test)] 292#[cfg(test)]
326mod tests { 293mod tests {
327 use super::*;
328 use std::process::Command;
329
330 #[test]
331 fn test_is_bare_repository_detects_valid_repo() {
332 // Create a temporary bare repository for testing
333 let temp_dir = tempfile::tempdir().unwrap();
334 let repo_path = temp_dir.path().join("test.git");
335
336 // Initialize a bare repository
337 Command::new("git")
338 .args(&["init", "--bare", repo_path.to_str().unwrap()])
339 .output()
340 .expect("Failed to create test repository");
341
342 // Verify our helper function detects it
343 assert!(
344 is_bare_repository(&repo_path),
345 "Should detect valid bare repository"
346 );
347 }
348
349 #[test]
350 fn test_is_bare_repository_rejects_non_repo() {
351 let temp_dir = tempfile::tempdir().unwrap();
352 assert!(
353 !is_bare_repository(temp_dir.path()),
354 "Should reject non-repository directory"
355 );
356 }
357
358 #[test] 294 #[test]
359 fn test_is_bare_repository_rejects_nonexistent() { 295 fn test_module_exists() {
360 let path = Path::new("/nonexistent/path/to/repo.git"); 296 // Simple compilation test
361 assert!(!is_bare_repository(path), "Should reject nonexistent path"); 297 assert!(true);
362 } 298 }
363} 299}