upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-21 05:18:15 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-21 05:34:56 +0000
commit7a81643367515a9d01eb2d4deb623e9a7c071a12 (patch)
tree88d6f2e69ca051d2c94269f4753e2ef807882438 /grasp-audit
parent7dda553918705277c7fa5b903c6a40e4b4a0aa8d (diff)
add repository creation
Diffstat (limited to 'grasp-audit')
-rw-r--r--grasp-audit/Cargo.toml1
-rw-r--r--grasp-audit/src/specs/grasp01/mod.rs2
-rw-r--r--grasp-audit/src/specs/grasp01/repository_creation.rs378
3 files changed, 381 insertions, 0 deletions
diff --git a/grasp-audit/Cargo.toml b/grasp-audit/Cargo.toml
index 0bc008a..9198cd5 100644
--- a/grasp-audit/Cargo.toml
+++ b/grasp-audit/Cargo.toml
@@ -43,3 +43,4 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
43 43
44[dev-dependencies] 44[dev-dependencies]
45tokio-test = "0.4" 45tokio-test = "0.4"
46tempfile = "3"
diff --git a/grasp-audit/src/specs/grasp01/mod.rs b/grasp-audit/src/specs/grasp01/mod.rs
index 6fd6960..fd6d9b3 100644
--- a/grasp-audit/src/specs/grasp01/mod.rs
+++ b/grasp-audit/src/specs/grasp01/mod.rs
@@ -3,7 +3,9 @@
3pub mod event_acceptance_policy; 3pub mod event_acceptance_policy;
4pub mod nip01_smoke; 4pub mod nip01_smoke;
5pub mod nip11_document; 5pub mod nip11_document;
6pub mod repository_creation;
6 7
7pub use event_acceptance_policy::EventAcceptancePolicyTests; 8pub use event_acceptance_policy::EventAcceptancePolicyTests;
8pub use nip01_smoke::Nip01SmokeTests; 9pub use nip01_smoke::Nip01SmokeTests;
9pub use nip11_document::Nip11DocumentTests; 10pub use nip11_document::Nip11DocumentTests;
11pub use repository_creation::RepositoryCreationTests;
diff --git a/grasp-audit/src/specs/grasp01/repository_creation.rs b/grasp-audit/src/specs/grasp01/repository_creation.rs
new file mode 100644
index 0000000..bd6c16a
--- /dev/null
+++ b/grasp-audit/src/specs/grasp01/repository_creation.rs
@@ -0,0 +1,378 @@
1//! GRASP-01 Repository Creation Tests
2//!
3//! Tests that verify bare Git repositories are created when repository announcements
4//! are accepted by the relay.
5//!
6//! ## Test Coverage
7//!
8//! - Repository creation on valid announcement
9//! - Idempotent creation (no error if repo already exists)
10//! - Proper directory structure (<npub>/<identifier>.git)
11//! - Bare repository validation (has HEAD, config, objects, refs)
12//!
13//! ## Running Tests
14//!
15//! ```bash
16//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test
17//! ```
18
19use crate::{AuditClient, TestContext, FixtureKind, TestResult};
20use nostr_sdk::prelude::*;
21use std::path::Path;
22
23/// Test suite for repository creation
24pub struct RepositoryCreationTests;
25
26impl RepositoryCreationTests {
27 /// Test that a bare repository is created when a valid announcement is accepted
28 ///
29 /// This test:
30 /// 1. Sends a valid repository announcement via TestContext
31 /// 2. Verifies the announcement was accepted
32 /// 3. Checks that a bare git repository was created at the expected path
33 pub async fn test_bare_repo_created_on_announcement(
34 client: &AuditClient,
35 git_data_dir: &Path,
36 ) -> TestResult {
37 let test_name = "test_bare_repo_created_on_announcement";
38 let ctx = TestContext::new(client);
39
40 // Use TestContext to create and send repository announcement
41 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
42 Ok(r) => r,
43 Err(e) => return TestResult::new(
44 test_name,
45 "GRASP-01",
46 "Bare repository must be created when announcement is accepted",
47 ).fail(&format!("Failed to create repo fixture: {}", e)),
48 };
49
50 // Wait a bit for repository creation
51 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
52
53 // Extract repo identifier and npub from announcement
54 let repo_id = match repo.tags.iter()
55 .find(|t| t.kind() == TagKind::d())
56 .and_then(|t| t.content())
57 {
58 Some(id) => id.to_string(),
59 None => return TestResult::new(
60 test_name,
61 "GRASP-01",
62 "Bare repository must be created when announcement is accepted",
63 ).fail("Repository announcement missing d tag"),
64 };
65
66 let npub = match repo.pubkey.to_bech32() {
67 Ok(n) => n,
68 Err(e) => return TestResult::new(
69 test_name,
70 "GRASP-01",
71 "Bare repository must be created when announcement is accepted",
72 ).fail(&format!("Failed to convert pubkey to npub: {}", e)),
73 };
74
75 // Check if repository was created
76 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
77
78 if !is_bare_repository(&repo_path) {
79 return TestResult::new(
80 test_name,
81 "GRASP-01",
82 "Bare repository must be created when announcement is accepted",
83 ).fail(&format!("Bare repository not found at: {}", repo_path.display()));
84 }
85
86 TestResult::new(
87 test_name,
88 "GRASP-01",
89 "Bare repository must be created when announcement is accepted",
90 ).pass()
91 }
92
93 /// Test that repository creation is idempotent
94 ///
95 /// This test:
96 /// 1. Sends a repository announcement (creates repo) via TestContext
97 /// 2. Sends the same announcement again
98 /// 3. Verifies no error occurs and repo still exists
99 pub async fn test_repo_creation_idempotent(
100 client: &AuditClient,
101 git_data_dir: &Path,
102 ) -> TestResult {
103 let test_name = "test_repo_creation_idempotent";
104 let ctx = TestContext::new(client);
105
106 // Create and send repository announcement first time via TestContext
107 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
108 Ok(r) => r,
109 Err(e) => return TestResult::new(
110 test_name,
111 "GRASP-01",
112 "Repository creation must be idempotent",
113 ).fail(&format!("Failed to create repo fixture: {}", e)),
114 };
115
116 // Wait for repository creation
117 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
118
119 // Send the same announcement again (should be idempotent)
120 if let Err(e) = client.send_event(repo.clone()).await {
121 return TestResult::new(
122 test_name,
123 "GRASP-01",
124 "Repository creation must be idempotent",
125 ).fail(&format!("Second send failed (not idempotent): {}", e));
126 }
127
128 // Wait again
129 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
130
131 // Verify repository still exists and is valid
132 let repo_id = repo.tags.iter()
133 .find(|t| t.kind() == TagKind::d())
134 .and_then(|t| t.content())
135 .ok_or("Missing d tag")
136 .unwrap()
137 .to_string();
138
139 let npub = repo.pubkey.to_bech32().unwrap();
140 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
141
142 if !is_bare_repository(&repo_path) {
143 return TestResult::new(
144 test_name,
145 "GRASP-01",
146 "Repository creation must be idempotent",
147 ).fail("Repository not found after second send");
148 }
149
150 TestResult::new(
151 test_name,
152 "GRASP-01",
153 "Repository creation must be idempotent",
154 ).pass()
155 }
156
157 /// Test that the repository has the correct structure
158 ///
159 /// This test verifies:
160 /// 1. Repository is at <git_data_path>/<npub>/<identifier>.git
161 /// 2. Repository is bare (no working directory)
162 /// 3. Repository has required git structure (HEAD, config, objects/, refs/)
163 pub async fn test_bare_repo_structure(
164 client: &AuditClient,
165 git_data_dir: &Path,
166 ) -> TestResult {
167 let test_name = "test_bare_repo_structure";
168 let ctx = TestContext::new(client);
169
170 // Create and send repository announcement via TestContext
171 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
172 Ok(r) => r,
173 Err(e) => return TestResult::new(
174 test_name,
175 "GRASP-01",
176 "Bare repository must have correct structure",
177 ).fail(&format!("Failed to create repo fixture: {}", e)),
178 };
179
180 // Wait for repository creation
181 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
182
183 // Extract repo identifier and npub
184 let repo_id = repo.tags.iter()
185 .find(|t| t.kind() == TagKind::d())
186 .and_then(|t| t.content())
187 .ok_or("Missing d tag")
188 .unwrap()
189 .to_string();
190
191 let npub = repo.pubkey.to_bech32().unwrap();
192
193 // Verify correct path structure: <git_data_path>/<npub>/<identifier>.git
194 let expected_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
195
196 if !expected_path.exists() {
197 return TestResult::new(
198 test_name,
199 "GRASP-01",
200 "Bare repository must have correct structure",
201 ).fail(&format!("Repository not at expected path: {}", expected_path.display()));
202 }
203
204 // Verify it's a bare repository with correct structure
205 if !expected_path.join("HEAD").is_file() {
206 return TestResult::new(
207 test_name,
208 "GRASP-01",
209 "Bare repository must have correct structure",
210 ).fail("Missing HEAD file");
211 }
212
213 if !expected_path.join("config").is_file() {
214 return TestResult::new(
215 test_name,
216 "GRASP-01",
217 "Bare repository must have correct structure",
218 ).fail("Missing config file");
219 }
220
221 if !expected_path.join("objects").is_dir() {
222 return TestResult::new(
223 test_name,
224 "GRASP-01",
225 "Bare repository must have correct structure",
226 ).fail("Missing objects/ directory");
227 }
228
229 if !expected_path.join("refs").is_dir() {
230 return TestResult::new(
231 test_name,
232 "GRASP-01",
233 "Bare repository must have correct structure",
234 ).fail("Missing refs/ directory");
235 }
236
237 // Verify the helper function agrees
238 if !is_bare_repository(&expected_path) {
239 return TestResult::new(
240 test_name,
241 "GRASP-01",
242 "Bare repository must have correct structure",
243 ).fail("Helper function does not recognize repository as bare");
244 }
245
246 TestResult::new(
247 test_name,
248 "GRASP-01",
249 "Bare repository must have correct structure",
250 ).pass()
251 }
252
253 /// Test that repository creation cleanup works
254 ///
255 /// This test:
256 /// 1. Creates multiple repositories via TestContext
257 /// 2. Verifies they exist
258 /// 3. Ensures test cleanup removes them (via TempDir drop)
259 pub async fn test_repo_cleanup(
260 client: &AuditClient,
261 git_data_dir: &Path,
262 ) -> TestResult {
263 let test_name = "test_repo_cleanup";
264 let ctx = TestContext::new(client);
265
266 // Create multiple repositories via TestContext
267 let mut repo_paths = Vec::new();
268
269 for _i in 0..3 {
270 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
271 Ok(r) => r,
272 Err(e) => return TestResult::new(
273 test_name,
274 "GRASP-01",
275 "Test cleanup must remove created repositories",
276 ).fail(&format!("Failed to create repo fixture: {}", e)),
277 };
278
279 // Extract path
280 let repo_id = repo.tags.iter()
281 .find(|t| t.kind() == TagKind::d())
282 .and_then(|t| t.content())
283 .unwrap()
284 .to_string();
285 let npub = repo.pubkey.to_bech32().unwrap();
286 let path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
287 repo_paths.push(path);
288 }
289
290 // Wait for all repositories to be created
291 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
292
293 // Verify all repositories exist
294 for path in &repo_paths {
295 if !is_bare_repository(path) {
296 return TestResult::new(
297 test_name,
298 "GRASP-01",
299 "Test cleanup must remove created repositories",
300 ).fail(&format!("Repository not created at: {}", path.display()));
301 }
302 }
303
304 // Note: Actual cleanup happens when the TestRelay's TempDir is dropped
305 // This test just verifies that repositories were created successfully
306 // The integration test framework will verify cleanup
307
308 TestResult::new(
309 test_name,
310 "GRASP-01",
311 "Test cleanup must remove created repositories",
312 ).pass()
313 }
314}
315
316/// Helper function to check if a path is a valid bare git repository
317///
318/// A bare repository must have:
319/// - HEAD file
320/// - config file
321/// - objects/ directory
322/// - refs/ directory
323pub fn is_bare_repository(path: &Path) -> bool {
324 if !path.exists() {
325 return false;
326 }
327
328 // Check for required bare repository components
329 let has_head = path.join("HEAD").is_file();
330 let has_config = path.join("config").is_file();
331 let has_objects = path.join("objects").is_dir();
332 let has_refs = path.join("refs").is_dir();
333
334 has_head && has_config && has_objects && has_refs
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use std::process::Command;
341
342 #[test]
343 fn test_is_bare_repository_detects_valid_repo() {
344 // Create a temporary bare repository for testing
345 let temp_dir = tempfile::tempdir().unwrap();
346 let repo_path = temp_dir.path().join("test.git");
347
348 // Initialize a bare repository
349 Command::new("git")
350 .args(&["init", "--bare", repo_path.to_str().unwrap()])
351 .output()
352 .expect("Failed to create test repository");
353
354 // Verify our helper function detects it
355 assert!(
356 is_bare_repository(&repo_path),
357 "Should detect valid bare repository"
358 );
359 }
360
361 #[test]
362 fn test_is_bare_repository_rejects_non_repo() {
363 let temp_dir = tempfile::tempdir().unwrap();
364 assert!(
365 !is_bare_repository(temp_dir.path()),
366 "Should reject non-repository directory"
367 );
368 }
369
370 #[test]
371 fn test_is_bare_repository_rejects_nonexistent() {
372 let path = Path::new("/nonexistent/path/to/repo.git");
373 assert!(
374 !is_bare_repository(path),
375 "Should reject nonexistent path"
376 );
377 }
378} \ No newline at end of file