upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/specs/grasp01/repository_creation.rs
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/grasp01/repository_creation.rs
parent3f74ababf338d65ac5e29e7eb5541ce416b7fe75 (diff)
audit: stop checking git_data_directory
Diffstat (limited to 'grasp-audit/src/specs/grasp01/repository_creation.rs')
-rw-r--r--grasp-audit/src/specs/grasp01/repository_creation.rs208
1 files changed, 72 insertions, 136 deletions
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}