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 11:22:40 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-28 11:26:57 +0000
commite5b8854d5065cda8601546fc888e2ef1e00cc166 (patch)
tree839354b19ee8d13a88bfcffa099ded5dce63696e /grasp-audit/src/specs
parent744094c61d6e65892bcdb5a29b90b845ce87559f (diff)
audit: fix rejected push wrong commit test
Diffstat (limited to 'grasp-audit/src/specs')
-rw-r--r--grasp-audit/src/specs/grasp01/push_authorization.rs213
1 files changed, 71 insertions, 142 deletions
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs
index 69664d6..0e30238 100644
--- a/grasp-audit/src/specs/grasp01/push_authorization.rs
+++ b/grasp-audit/src/specs/grasp01/push_authorization.rs
@@ -24,7 +24,6 @@ use crate::{
24}; 24};
25use nostr_sdk::prelude::*; 25use nostr_sdk::prelude::*;
26use std::fs; 26use std::fs;
27use std::path::Path;
28 27
29/// Test suite for Push Authorization operations 28/// Test suite for Push Authorization operations
30pub struct PushAuthorizationTests; 29pub struct PushAuthorizationTests;
@@ -37,14 +36,60 @@ impl PushAuthorizationTests {
37 ) -> crate::AuditResult { 36 ) -> crate::AuditResult {
38 let mut results = crate::AuditResult::new("GRASP-01 Push Authorization Tests"); 37 let mut results = crate::AuditResult::new("GRASP-01 Push Authorization Tests");
39 38
40 results.add(Self::test_push_authorized_by_owner_state(client, relay_domain).await);
41 results.add(Self::test_push_rejected_without_state_event(client, relay_domain).await); 39 results.add(Self::test_push_rejected_without_state_event(client, relay_domain).await);
40 results.add(Self::test_push_authorized_by_owner_state(client, relay_domain).await);
42 results.add(Self::test_push_rejected_wrong_commit(client, relay_domain).await); 41 results.add(Self::test_push_rejected_wrong_commit(client, relay_domain).await);
43 results.add(Self::test_push_authorized_by_maintainer_state_only(client, relay_domain).await); 42 results.add(Self::test_push_authorized_by_maintainer_state_only(client, relay_domain).await);
44 43
45 results 44 results
46 } 45 }
47 46
47 /// Test that push is rejected when no state event exists
48 pub async fn test_push_rejected_without_state_event(
49 client: &AuditClient,
50 relay_domain: &str,
51 ) -> TestResult {
52 let test_name = "test_push_rejected_without_state_event";
53 let ctx = TestContext::new(client);
54
55 // Create repository (no state event)
56 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
57 Ok(r) => r,
58 Err(e) => {
59 return TestResult::new(test_name, "GRASP-01", "Push rejected without state event")
60 .fail(&format!("Failed to create repo: {}", e))
61 }
62 };
63
64 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
65
66 let repo_id = repo.tags.iter().find(|t| t.kind() == TagKind::d())
67 .and_then(|t| t.content()).unwrap().to_string();
68 let npub = repo.pubkey.to_bech32().unwrap();
69
70 // Clone and create commit
71 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
72 Ok(p) => p,
73 Err(e) => return TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e),
74 };
75 let cleanup = || { let _ = fs::remove_dir_all(&clone_path); };
76
77 if let Err(e) = create_commit(&clone_path, "Unauthorized commit") {
78 cleanup();
79 return TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e);
80 }
81
82 // Do NOT publish state event - push should be rejected
83 let push_result = try_push(&clone_path);
84 cleanup();
85
86 match push_result {
87 Ok(false) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event").pass(),
88 Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail("Push accepted but should be rejected"),
89 Err(e) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e),
90 }
91 }
92
48 /// Test that push is authorized when state event matches the commit 93 /// Test that push is authorized when state event matches the commit
49 /// 94 ///
50 /// GRASP-01: "MUST accept pushes via this service that match the latest 95 /// GRASP-01: "MUST accept pushes via this service that match the latest
@@ -210,52 +255,6 @@ impl PushAuthorizationTests {
210 } 255 }
211 } 256 }
212 257
213 /// Test that push is rejected when no state event exists
214 pub async fn test_push_rejected_without_state_event(
215 client: &AuditClient,
216 relay_domain: &str,
217 ) -> TestResult {
218 let test_name = "test_push_rejected_without_state_event";
219 let ctx = TestContext::new(client);
220
221 // Create repository (no state event)
222 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
223 Ok(r) => r,
224 Err(e) => {
225 return TestResult::new(test_name, "GRASP-01", "Push rejected without state event")
226 .fail(&format!("Failed to create repo: {}", e))
227 }
228 };
229
230 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
231
232 let repo_id = repo.tags.iter().find(|t| t.kind() == TagKind::d())
233 .and_then(|t| t.content()).unwrap().to_string();
234 let npub = repo.pubkey.to_bech32().unwrap();
235
236 // Clone and create commit
237 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
238 Ok(p) => p,
239 Err(e) => return TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e),
240 };
241 let cleanup = || { let _ = fs::remove_dir_all(&clone_path); };
242
243 if let Err(e) = create_commit(&clone_path, "Unauthorized commit") {
244 cleanup();
245 return TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e);
246 }
247
248 // Do NOT publish state event - push should be rejected
249 let push_result = try_push(&clone_path);
250 cleanup();
251
252 match push_result {
253 Ok(false) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event").pass(),
254 Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail("Push accepted but should be rejected"),
255 Err(e) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e),
256 }
257 }
258
259 /// Test that push is rejected when commit doesn't match state event 258 /// Test that push is rejected when commit doesn't match state event
260 /// 259 ///
261 /// GRASP-01: "MUST accept pushes via this service that match the latest repo state announcement" 260 /// GRASP-01: "MUST accept pushes via this service that match the latest repo state announcement"
@@ -264,10 +263,14 @@ impl PushAuthorizationTests {
264 /// ## Fixture-First Pattern 263 /// ## Fixture-First Pattern
265 /// 264 ///
266 /// 1. **Generate**: Create TestContext and get RepoState fixture 265 /// 1. **Generate**: Create TestContext and get RepoState fixture
267 /// (repo announcement + state event pointing to deterministic commit) 266 /// (repo announcement + state event pointing to DETERMINISTIC_COMMIT_HASH)
268 /// 2. **Send**: Clone repo, create deterministic commit, push (establishes state on relay) 267 /// 2. **Send**: Clone repo, create WRONG deterministic commit (Maintainer variant),
269 /// 3. **Test**: Create a NEW commit locally, try to push 268 /// try to push
270 /// 4. **Verify**: Push should be rejected because new commit doesn't match state event 269 /// 3. **Verify**: Push should be rejected because the commit doesn't match state event
270 ///
271 /// Note: This test directly pushes the wrong commit instead of first establishing
272 /// state on the relay. The state event already authorizes DETERMINISTIC_COMMIT_HASH,
273 /// but we try to push MAINTAINER_DETERMINISTIC_COMMIT_HASH which should be rejected.
271 pub async fn test_push_rejected_wrong_commit( 274 pub async fn test_push_rejected_wrong_commit(
272 client: &AuditClient, 275 client: &AuditClient,
273 relay_domain: &str, 276 relay_domain: &str,
@@ -278,6 +281,7 @@ impl PushAuthorizationTests {
278 281
279 // ============================================================ 282 // ============================================================
280 // Step 1: GENERATE - Create TestContext and get RepoState fixture 283 // Step 1: GENERATE - Create TestContext and get RepoState fixture
284 // The state event points to DETERMINISTIC_COMMIT_HASH
281 // ============================================================ 285 // ============================================================
282 let ctx = TestContext::new(client); 286 let ctx = TestContext::new(client);
283 287
@@ -314,8 +318,8 @@ impl PushAuthorizationTests {
314 }; 318 };
315 319
316 // ============================================================ 320 // ============================================================
317 // Step 2: SEND - Clone repo, create deterministic commit, push 321 // Step 2: SEND - Clone repo and create an unauthorized commit
318 // (establishes the state on the relay) 322 // Any commit with a hash different from what's in the state event will work
319 // ============================================================ 323 // ============================================================
320 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { 324 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
321 Ok(p) => p, 325 Ok(p) => p,
@@ -330,29 +334,9 @@ impl PushAuthorizationTests {
330 let _ = fs::remove_dir_all(&clone_path); 334 let _ = fs::remove_dir_all(&clone_path);
331 }; 335 };
332 336
333 // Create deterministic commit locally 337 // Create/checkout main branch
334 let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") {
335 Ok(h) => h,
336 Err(e) => {
337 cleanup();
338 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
339 .fail(&format!("Failed to create deterministic commit: {}", e));
340 }
341 };
342
343 // Verify commit hash matches expected
344 if commit_hash != DETERMINISTIC_COMMIT_HASH {
345 cleanup();
346 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
347 .fail(&format!(
348 "Commit hash mismatch: got {}, expected {}",
349 commit_hash, DETERMINISTIC_COMMIT_HASH
350 ));
351 }
352
353 // Create main branch pointing to our deterministic commit
354 let branch_output = Command::new("git") 338 let branch_output = Command::new("git")
355 .args(["branch", "main"]) 339 .args(["checkout", "-B", "main"])
356 .current_dir(&clone_path) 340 .current_dir(&clone_path)
357 .output(); 341 .output();
358 342
@@ -360,82 +344,30 @@ impl PushAuthorizationTests {
360 Err(e) => { 344 Err(e) => {
361 cleanup(); 345 cleanup();
362 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 346 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
363 .fail(&format!("Failed to create main branch: {}", e)); 347 .fail(&format!("Failed to create/checkout main branch: {}", e));
364 } 348 }
365 Ok(output) if !output.status.success() => { 349 Ok(output) if !output.status.success() => {
366 cleanup(); 350 cleanup();
367 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 351 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
368 .fail(&format!( 352 .fail(&format!(
369 "Failed to create main branch: {}", 353 "Failed to create/checkout main branch: {}",
370 String::from_utf8_lossy(&output.stderr) 354 String::from_utf8_lossy(&output.stderr)
371 )); 355 ));
372 } 356 }
373 _ => {} 357 _ => {}
374 } 358 }
375 359
376 // Checkout main branch 360 // Create a commit that is NOT in the state event
377 let checkout_output = Command::new("git") 361 // Any commit hash different from what's authorized in the state event will work
378 .args(["checkout", "main"]) 362 if let Err(e) = create_commit(&clone_path, "Unauthorized commit - should be rejected") {
379 .current_dir(&clone_path) 363 cleanup();
380 .output(); 364 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
381 365 .fail(&format!("Failed to create wrong commit: {}", e));
382 match checkout_output {
383 Err(e) => {
384 cleanup();
385 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
386 .fail(&format!("Failed to checkout main branch: {}", e));
387 }
388 Ok(output) if !output.status.success() => {
389 cleanup();
390 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
391 .fail(&format!(
392 "Failed to checkout main branch: {}",
393 String::from_utf8_lossy(&output.stderr)
394 ));
395 }
396 _ => {}
397 }
398
399 // Push the deterministic commit to establish state on relay
400 let push_output = Command::new("git")
401 .args(["push", "origin", "main"])
402 .current_dir(&clone_path)
403 .env("GIT_TERMINAL_PROMPT", "0")
404 .output();
405
406 match push_output {
407 Err(e) => {
408 cleanup();
409 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
410 .fail(&format!("Failed to push initial commit: {}", e));
411 }
412 Ok(output) if !output.status.success() => {
413 cleanup();
414 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
415 .fail(&format!(
416 "Failed to push initial commit: {}",
417 String::from_utf8_lossy(&output.stderr)
418 ));
419 }
420 _ => {}
421 } 366 }
422 367
423 // ============================================================ 368 // ============================================================
424 // Step 3: TEST - Create a NEW commit that is NOT announced 369 // Step 3: VERIFY - Push should be rejected because the commit
425 // in any state event 370 // doesn't match the state event
426 // ============================================================
427 let new_commit = match create_commit(&clone_path, "Unauthorized commit") {
428 Ok(h) => h,
429 Err(e) => {
430 cleanup();
431 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
432 .fail(&format!("Failed to create commit: {}", e));
433 }
434 };
435
436 // ============================================================
437 // Step 4: VERIFY - Push should be rejected because new commit
438 // doesn't match state event
439 // ============================================================ 371 // ============================================================
440 let push_result = try_push(&clone_path); 372 let push_result = try_push(&clone_path);
441 cleanup(); 373 cleanup();
@@ -443,10 +375,7 @@ impl PushAuthorizationTests {
443 match push_result { 375 match push_result {
444 Ok(false) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").pass(), 376 Ok(false) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").pass(),
445 Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 377 Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
446 .fail(&format!( 378 .fail("Push accepted but should be rejected. The pushed commit is not in the state event."),
447 "Push accepted but should be rejected. State event points to {}, but pushed {}",
448 DETERMINISTIC_COMMIT_HASH, new_commit
449 )),
450 Err(e) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").fail(&e), 379 Err(e) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").fail(&e),
451 } 380 }
452 } 381 }