diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-28 03:38:50 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-28 03:38:50 +0000 |
| commit | f41550ea1898be2ec6c4be205e4cad0085400313 (patch) | |
| tree | 00cc474031bf81fe382c6276e52fd769b275cd3f /grasp-audit/src/specs/grasp01/repository_creation.rs | |
| parent | 3f74ababf338d65ac5e29e7eb5541ce416b7fe75 (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.rs | 208 |
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 | ||
| 19 | use crate::{AuditClient, FixtureKind, TestContext, TestResult}; | 18 | use crate::{AuditClient, FixtureKind, TestContext, TestResult}; |
| 20 | use nostr_sdk::prelude::*; | 19 | use nostr_sdk::prelude::*; |
| 21 | use std::path::Path; | ||
| 22 | 20 | ||
| 23 | /// Test suite for repository creation | 21 | /// Test suite for repository creation |
| 24 | pub struct RepositoryCreationTests; | 22 | pub 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 | 250 | async fn check_repo_accessible_via_http( |
| 309 | /// - objects/ directory | 251 | relay_domain: &str, |
| 310 | /// - refs/ directory | 252 | npub: &str, |
| 311 | pub 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)] |
| 326 | mod tests { | 293 | mod 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 | } |