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-12-01 11:56:49 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 11:58:34 +0000
commit7a78815e29b01c83f3d0ec195ba717a2eba8cd37 (patch)
tree4c5ccd9b812f1d1d75ed218501192ddc5459fd12 /grasp-audit/src/specs
parente6ceab90de1acad154624022a6036efac18abab6 (diff)
reject push when refs/nostr/<event-id> doesnt match known event and delete incorrect ref on event receive
Diffstat (limited to 'grasp-audit/src/specs')
-rw-r--r--grasp-audit/src/specs/grasp01/push_authorization.rs708
1 files changed, 504 insertions, 204 deletions
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs
index 97d068c..d8652ae 100644
--- a/grasp-audit/src/specs/grasp01/push_authorization.rs
+++ b/grasp-audit/src/specs/grasp01/push_authorization.rs
@@ -21,7 +21,7 @@
21/// This hash is produced by creating a commit with: 21/// This hash is produced by creating a commit with:
22/// - File: test.txt containing "PR test deterministic commit" 22/// - File: test.txt containing "PR test deterministic commit"
23/// - Message: "PR test deterministic commit" 23/// - Message: "PR test deterministic commit"
24/// - Author: "PR Test Author <pr-test@example.com>" 24/// - Author: "GRASP Audit Test <test@grasp-audit.local>"
25/// - Author date: 2024-01-01T00:00:00Z 25/// - Author date: 2024-01-01T00:00:00Z
26/// - Committer date: 2024-01-01T00:00:00Z 26/// - Committer date: 2024-01-01T00:00:00Z
27/// - GPG signing: disabled 27/// - GPG signing: disabled
@@ -29,13 +29,13 @@
29/// 29///
30/// Run `test_pr_test_commit_hash_discovery` to discover/verify this value. 30/// Run `test_pr_test_commit_hash_discovery` to discover/verify this value.
31#[allow(dead_code)] 31#[allow(dead_code)]
32const PR_TEST_COMMIT_HASH: &str = "8935183ff722bf04e861928c6a7e50868c6ca4a6"; 32const PR_TEST_COMMIT_HASH: &str = "5d40fb1555a0c28bf4d650515a73aaa54d4d9bfb";
33 33
34use crate::{ 34use crate::{
35 clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, 35 clone_repo, create_commit, create_deterministic_commit,
36 try_push, try_push_to_ref, AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, 36 create_deterministic_commit_with_variant, try_push, try_push_to_ref, AuditClient,
37 DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH, 37 CommitVariant, FixtureKind, TestContext, TestResult, DETERMINISTIC_COMMIT_HASH,
38 RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, 38 MAINTAINER_DETERMINISTIC_COMMIT_HASH, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH,
39}; 39};
40use nostr_sdk::prelude::*; 40use nostr_sdk::prelude::*;
41use std::fs; 41use std::fs;
@@ -63,37 +63,121 @@ use std::process::Command;
63/// * `Ok(String)` - The commit hash (should match PR_TEST_COMMIT_HASH) 63/// * `Ok(String)` - The commit hash (should match PR_TEST_COMMIT_HASH)
64/// * `Err(String)` - Error message if commit creation failed 64/// * `Err(String)` - Error message if commit creation failed
65fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> { 65fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> {
66 // Step 1: Create orphan branch (removes all history) 66 // Step 1: Clean up any tracked files in the working directory
67 // This ensures we start with a clean slate
67 let _ = Command::new("git") 68 let _ = Command::new("git")
68 .args(["checkout", "--orphan", "pr-test-branch"]) 69 .args(["clean", "-fd"])
69 .current_dir(clone_path) 70 .current_dir(clone_path)
70 .output(); 71 .output();
71 72
72 // Step 2: Clear staged files (orphan keeps files staged from previous branch) 73 // Step 2: Create orphan branch (removes all history)
73 let _ = Command::new("git") 74 let output = Command::new("git")
75 .args(["checkout", "--orphan", "pr-test-branch"])
76 .current_dir(clone_path)
77 .output()
78 .map_err(|e| format!("Failed to execute git checkout --orphan: {}", e))?;
79
80 if !output.status.success() {
81 return Err(format!(
82 "git checkout --orphan failed: {}",
83 String::from_utf8_lossy(&output.stderr)
84 ));
85 }
86
87 // Step 3: Remove ALL files from the index (staging area)
88 let output = Command::new("git")
74 .args(["rm", "-rf", "--cached", "."]) 89 .args(["rm", "-rf", "--cached", "."])
75 .current_dir(clone_path) 90 .current_dir(clone_path)
76 .output(); 91 .output()
92 .map_err(|e| format!("Failed to execute git rm: {}", e))?;
93
94 // Note: git rm may return error if there are no files to remove, that's OK
95 if !output.status.success() {
96 let stderr = String::from_utf8_lossy(&output.stderr);
97 // Ignore "did not match any files" errors
98 if !stderr.contains("did not match any files") {
99 return Err(format!("git rm -rf --cached . failed: {}", stderr));
100 }
101 }
102
103 // Step 4: Remove ALL files from working directory (except .git)
104 // This ensures only test.txt will be in the commit
105 for entry in fs::read_dir(clone_path).map_err(|e| format!("Failed to read dir: {}", e))? {
106 let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
107 let path = entry.path();
108 let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
109 if file_name != ".git" {
110 if path.is_dir() {
111 fs::remove_dir_all(&path)
112 .map_err(|e| format!("Failed to remove dir {}: {}", path.display(), e))?;
113 } else {
114 fs::remove_file(&path)
115 .map_err(|e| format!("Failed to remove file {}: {}", path.display(), e))?;
116 }
117 }
118 }
119
120 // Step 5: Create deterministic commit using existing function
121 let commit_hash =
122 create_deterministic_commit_with_variant(clone_path, CommitVariant::PRTestCommit)?;
123
124 // Step 6: Verify this is actually a root commit (no parent)
125 let output = Command::new("git")
126 .args(["rev-list", "--max-parents=0", "HEAD"])
127 .current_dir(clone_path)
128 .output()
129 .map_err(|e| format!("Failed to check root commit: {}", e))?;
77 130
78 // Step 3: Create deterministic commit using existing function 131 let root_commits = String::from_utf8_lossy(&output.stdout);
79 let commit_hash = create_deterministic_commit_with_variant(clone_path, CommitVariant::PRTestCommit)?; 132 if !root_commits.trim().contains(&commit_hash) {
133 return Err(format!(
134 "Commit {} is not a root commit (has parent). Root commits: {}",
135 commit_hash,
136 root_commits.trim()
137 ));
138 }
80 139
81 // Step 4: Replace main branch with our new orphan branch 140 // Step 7: Replace main branch with our new orphan branch
82 let _ = Command::new("git") 141 let _ = Command::new("git")
83 .args(["branch", "-D", "main"]) 142 .args(["branch", "-D", "main"])
84 .current_dir(clone_path) 143 .current_dir(clone_path)
85 .output(); 144 .output();
86 145
87 let _ = Command::new("git") 146 let output = Command::new("git")
88 .args(["branch", "-m", "main"]) 147 .args(["branch", "-m", "main"])
89 .current_dir(clone_path) 148 .current_dir(clone_path)
90 .output(); 149 .output()
150 .map_err(|e| format!("Failed to rename branch: {}", e))?;
151
152 if !output.status.success() {
153 return Err(format!(
154 "Failed to rename branch to main: {}",
155 String::from_utf8_lossy(&output.stderr)
156 ));
157 }
91 158
92 // Verify commit hash matches expected 159 // Step 8: Verify commit hash matches expected
93 if commit_hash != PR_TEST_COMMIT_HASH { 160 if commit_hash != PR_TEST_COMMIT_HASH {
161 // Debug: Show what's in the commit
162 let tree_output = Command::new("git")
163 .args(["ls-tree", "-r", "HEAD"])
164 .current_dir(clone_path)
165 .output();
166 let tree_info = tree_output
167 .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
168 .unwrap_or_else(|_| "Failed to get tree".to_string());
169
170 let cat_output = Command::new("git")
171 .args(["cat-file", "-p", "HEAD"])
172 .current_dir(clone_path)
173 .output();
174 let commit_info = cat_output
175 .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
176 .unwrap_or_else(|_| "Failed to get commit".to_string());
177
94 return Err(format!( 178 return Err(format!(
95 "PR test commit hash mismatch: got {}, expected {}", 179 "PR test commit hash mismatch: got {}, expected {}\nTree contents:\n{}\nCommit info:\n{}",
96 commit_hash, PR_TEST_COMMIT_HASH 180 commit_hash, PR_TEST_COMMIT_HASH, tree_info, commit_info
97 )); 181 ));
98 } 182 }
99 183
@@ -166,6 +250,8 @@ struct PrRefTestSetup {
166 repo_id: String, 250 repo_id: String,
167 owner_npub: String, 251 owner_npub: String,
168 wrong_commit_hash: String, 252 wrong_commit_hash: String,
253 /// The unpublished PR event - store it so we can publish the SAME event later
254 pr_event: Event,
169} 255}
170 256
171impl PrRefTestSetup { 257impl PrRefTestSetup {
@@ -192,17 +278,18 @@ async fn setup_repo_with_wrong_commit_pushed(
192 ctx: &TestContext<'_>, 278 ctx: &TestContext<'_>,
193 relay_domain: &str, 279 relay_domain: &str,
194) -> Result<PrRefTestSetup, String> { 280) -> Result<PrRefTestSetup, String> {
195 // Get fixtures (PREvent fixture creates the event but doesn't publish until we call get_fixture) 281 // Get ValidRepo fixture (publishes repo announcement to relay)
196 let repo_event = ctx 282 let repo_event = ctx
197 .get_fixture(FixtureKind::ValidRepo) 283 .get_fixture(FixtureKind::ValidRepo)
198 .await 284 .await
199 .map_err(|e| format!("Failed to get repo announcement: {}", e))?; 285 .map_err(|e| format!("Failed to get repo announcement: {}", e))?;
200 286
201 // Get PR event fixture (creates event object but doesn't publish to relay yet) 287 // Build PR event WITHOUT publishing - we need its ID before the event exists on relay
288 // This allows testing refs/nostr/<event-id> push behavior before the event is received
202 let pr_event = ctx 289 let pr_event = ctx
203 .get_fixture(FixtureKind::PREvent) 290 .build_fixture_only(FixtureKind::PREvent)
204 .await 291 .await
205 .map_err(|e| format!("Failed to get PR event fixture: {}", e))?; 292 .map_err(|e| format!("Failed to build PR event fixture: {}", e))?;
206 293
207 let repo_id = repo_event 294 let repo_id = repo_event
208 .tags 295 .tags
@@ -219,7 +306,8 @@ async fn setup_repo_with_wrong_commit_pushed(
219 let clone_path = clone_repo(relay_domain, &owner_npub, &repo_id)?; 306 let clone_path = clone_repo(relay_domain, &owner_npub, &repo_id)?;
220 307
221 // Create a WRONG commit (not the one expected by PR event) 308 // Create a WRONG commit (not the one expected by PR event)
222 let wrong_commit_hash = create_deterministic_commit_with_variant(&clone_path, CommitVariant::Owner)?; 309 let wrong_commit_hash =
310 create_deterministic_commit_with_variant(&clone_path, CommitVariant::Owner)?;
223 311
224 // Verify it's actually different from expected 312 // Verify it's actually different from expected
225 if wrong_commit_hash == PR_TEST_COMMIT_HASH { 313 if wrong_commit_hash == PR_TEST_COMMIT_HASH {
@@ -229,7 +317,11 @@ async fn setup_repo_with_wrong_commit_pushed(
229 317
230 // Push to refs/nostr/<pr-event-id> (no event published yet, should succeed) 318 // Push to refs/nostr/<pr-event-id> (no event published yet, should succeed)
231 let push_output = Command::new("git") 319 let push_output = Command::new("git")
232 .args(["push", "origin", &format!("main:refs/nostr/{}", pr_event_id)]) 320 .args([
321 "push",
322 "origin",
323 &format!("master:refs/nostr/{}", pr_event_id),
324 ])
233 .current_dir(&clone_path) 325 .current_dir(&clone_path)
234 .output() 326 .output()
235 .map_err(|e| format!("Failed to execute git push: {}", e))?; 327 .map_err(|e| format!("Failed to execute git push: {}", e))?;
@@ -249,23 +341,30 @@ async fn setup_repo_with_wrong_commit_pushed(
249 repo_id, 341 repo_id,
250 owner_npub, 342 owner_npub,
251 wrong_commit_hash, 343 wrong_commit_hash,
344 pr_event,
252 }) 345 })
253} 346}
254 347
255/// Publishes the PR event fixture and waits for relay to process it. 348/// Publishes the SAME PR event that was built during setup.
256/// Call this after setup_repo_with_wrong_commit_pushed to test post-event behavior. 349/// Call this after setup_repo_with_wrong_commit_pushed to test post-event behavior.
350///
351/// IMPORTANT: We must publish the EXACT same event that was used during setup,
352/// otherwise the event ID won't match the refs/nostr/<event-id> ref that was pushed.
257#[allow(dead_code)] 353#[allow(dead_code)]
258async fn publish_pr_event_and_wait(ctx: &TestContext<'_>) -> Result<Event, String> { 354async fn publish_pr_event_and_wait(
259 // Publishing the PR event - get_fixture publishes if not already published 355 ctx: &TestContext<'_>,
260 let pr_event = ctx 356 pr_event: &Event,
261 .get_fixture(FixtureKind::PREvent) 357) -> Result<(), String> {
358 // Publish the exact same PR event that was created during setup
359 ctx.client()
360 .send_event(pr_event.clone())
262 .await 361 .await
263 .map_err(|e| format!("Failed to publish PR event: {}", e))?; 362 .map_err(|e| format!("Failed to publish PR event: {}", e))?;
264 363
265 // Wait for relay to process 364 // Wait for relay to process
266 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; 365 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
267 366
268 Ok(pr_event) 367 Ok(())
269} 368}
270 369
271/// Creates the correct PR test commit (matching PR_TEST_COMMIT_HASH) in an existing clone. 370/// Creates the correct PR test commit (matching PR_TEST_COMMIT_HASH) in an existing clone.
@@ -281,7 +380,12 @@ fn reset_to_correct_pr_commit(clone_path: &Path) -> Result<String, String> {
281#[allow(dead_code)] 380#[allow(dead_code)]
282fn push_to_pr_ref(clone_path: &Path, pr_event_id: &str) -> Result<bool, String> { 381fn push_to_pr_ref(clone_path: &Path, pr_event_id: &str) -> Result<bool, String> {
283 let push_output = Command::new("git") 382 let push_output = Command::new("git")
284 .args(["push", "--force", "origin", &format!("main:refs/nostr/{}", pr_event_id)]) 383 .args([
384 "push",
385 "--force",
386 "origin",
387 &format!("HEAD:refs/nostr/{}", pr_event_id),
388 ])
285 .current_dir(clone_path) 389 .current_dir(clone_path)
286 .output() 390 .output()
287 .map_err(|e| format!("Failed to execute git push: {}", e))?; 391 .map_err(|e| format!("Failed to execute git push: {}", e))?;
@@ -307,22 +411,48 @@ pub struct PushAuthorizationTests;
307 411
308impl PushAuthorizationTests { 412impl PushAuthorizationTests {
309 /// Run all push authorization tests 413 /// Run all push authorization tests
310 pub async fn run_all( 414 pub async fn run_all(client: &AuditClient, relay_domain: &str) -> crate::AuditResult {
311 client: &AuditClient,
312 relay_domain: &str,
313 ) -> crate::AuditResult {
314 let mut results = crate::AuditResult::new("GRASP-01 Push Authorization Tests"); 415 let mut results = crate::AuditResult::new("GRASP-01 Push Authorization Tests");
315 416
316 results.add(Self::test_push_rejected_without_state_event(client, relay_domain).await); 417 results.add(Self::test_push_rejected_without_state_event(client, relay_domain).await);
317 results.add(Self::test_push_authorized_by_owner_state(client, relay_domain).await); 418 results.add(Self::test_push_authorized_by_owner_state(client, relay_domain).await);
318 results.add(Self::test_push_rejected_wrong_commit(client, relay_domain).await); 419 results.add(Self::test_push_rejected_wrong_commit(client, relay_domain).await);
319 results.add(Self::test_push_authorized_by_maintainer_state_only(client, relay_domain).await); 420 results
320 results.add(Self::test_push_authorized_by_recursive_maintainer_state(client, relay_domain).await); 421 .add(Self::test_push_authorized_by_maintainer_state_only(client, relay_domain).await);
321 results.add(Self::test_push_to_nostr_ref_with_invalid_event_id_rejected(client, relay_domain).await); 422 results.add(
322 results.add(Self::test_pr_push_to_nostr_ref_with_wrong_commit_accepted_before_event_received(client, relay_domain).await); 423 Self::test_push_authorized_by_recursive_maintainer_state(client, relay_domain).await,
323 results.add(Self::test_pr_event_published_removes_nostr_ref_at_incorrect_commit(client, relay_domain).await); 424 );
324 results.add(Self::test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected(client, relay_domain).await); 425 results.add(
325 results.add(Self::test_push_to_nostr_ref_with_correct_commit_after_event_received_accepted(client, relay_domain).await); 426 Self::test_push_to_nostr_ref_with_invalid_event_id_rejected(client, relay_domain).await,
427 );
428 results.add(
429 Self::test_pr_push_to_nostr_ref_with_wrong_commit_accepted_before_event_received(
430 client,
431 relay_domain,
432 )
433 .await,
434 );
435 results.add(
436 Self::test_pr_event_published_removes_nostr_ref_at_incorrect_commit(
437 client,
438 relay_domain,
439 )
440 .await,
441 );
442 results.add(
443 Self::test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected(
444 client,
445 relay_domain,
446 )
447 .await,
448 );
449 results.add(
450 Self::test_push_to_nostr_ref_with_correct_commit_after_event_received_accepted(
451 client,
452 relay_domain,
453 )
454 .await,
455 );
326 456
327 results 457 results
328 } 458 }
@@ -340,26 +470,37 @@ impl PushAuthorizationTests {
340 Ok(r) => r, 470 Ok(r) => r,
341 Err(e) => { 471 Err(e) => {
342 return TestResult::new(test_name, "GRASP-01", "Push rejected without state event") 472 return TestResult::new(test_name, "GRASP-01", "Push rejected without state event")
343 .fail(&format!("Failed to create repo: {}", e)) 473 .fail(format!("Failed to create repo: {}", e))
344 } 474 }
345 }; 475 };
346 476
347 tokio::time::sleep(std::time::Duration::from_millis(200)).await; 477 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
348 478
349 let repo_id = repo.tags.iter().find(|t| t.kind() == TagKind::d()) 479 let repo_id = repo
350 .and_then(|t| t.content()).unwrap().to_string(); 480 .tags
481 .iter()
482 .find(|t| t.kind() == TagKind::d())
483 .and_then(|t| t.content())
484 .unwrap()
485 .to_string();
351 let npub = repo.pubkey.to_bech32().unwrap(); 486 let npub = repo.pubkey.to_bech32().unwrap();
352 487
353 // Clone and create commit 488 // Clone and create commit
354 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { 489 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
355 Ok(p) => p, 490 Ok(p) => p,
356 Err(e) => return TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e), 491 Err(e) => {
492 return TestResult::new(test_name, "GRASP-01", "Push rejected without state event")
493 .fail(&e)
494 }
495 };
496 let cleanup = || {
497 let _ = fs::remove_dir_all(&clone_path);
357 }; 498 };
358 let cleanup = || { let _ = fs::remove_dir_all(&clone_path); };
359 499
360 if let Err(e) = create_commit(&clone_path, "Unauthorized commit") { 500 if let Err(e) = create_commit(&clone_path, "Unauthorized commit") {
361 cleanup(); 501 cleanup();
362 return TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e); 502 return TestResult::new(test_name, "GRASP-01", "Push rejected without state event")
503 .fail(&e);
363 } 504 }
364 505
365 // Do NOT publish state event - push should be rejected 506 // Do NOT publish state event - push should be rejected
@@ -367,9 +508,14 @@ impl PushAuthorizationTests {
367 cleanup(); 508 cleanup();
368 509
369 match push_result { 510 match push_result {
370 Ok(false) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event").pass(), 511 Ok(false) => {
371 Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail("Push accepted but should be rejected"), 512 TestResult::new(test_name, "GRASP-01", "Push rejected without state event").pass()
372 Err(e) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e), 513 }
514 Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected without state event")
515 .fail("Push accepted but should be rejected"),
516 Err(e) => {
517 TestResult::new(test_name, "GRASP-01", "Push rejected without state event").fail(&e)
518 }
373 } 519 }
374 } 520 }
375 521
@@ -400,8 +546,12 @@ impl PushAuthorizationTests {
400 let state_event = match ctx.get_fixture(FixtureKind::RepoState).await { 546 let state_event = match ctx.get_fixture(FixtureKind::RepoState).await {
401 Ok(e) => e, 547 Ok(e) => e,
402 Err(e) => { 548 Err(e) => {
403 return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") 549 return TestResult::new(
404 .fail(&format!("Failed to create RepoState fixture: {}", e)); 550 test_name,
551 "GRASP-01",
552 "Push authorized with matching state",
553 )
554 .fail(format!("Failed to create RepoState fixture: {}", e));
405 } 555 }
406 }; 556 };
407 557
@@ -416,16 +566,24 @@ impl PushAuthorizationTests {
416 { 566 {
417 Some(id) => id.to_string(), 567 Some(id) => id.to_string(),
418 None => { 568 None => {
419 return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") 569 return TestResult::new(
420 .fail("Missing repo_id in state event"); 570 test_name,
571 "GRASP-01",
572 "Push authorized with matching state",
573 )
574 .fail("Missing repo_id in state event");
421 } 575 }
422 }; 576 };
423 577
424 let npub = match state_event.pubkey.to_bech32() { 578 let npub = match state_event.pubkey.to_bech32() {
425 Ok(n) => n, 579 Ok(n) => n,
426 Err(e) => { 580 Err(e) => {
427 return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") 581 return TestResult::new(
428 .fail(&format!("Failed to convert pubkey to bech32: {}", e)); 582 test_name,
583 "GRASP-01",
584 "Push authorized with matching state",
585 )
586 .fail(format!("Failed to convert pubkey to bech32: {}", e));
429 } 587 }
430 }; 588 };
431 589
@@ -435,8 +593,12 @@ impl PushAuthorizationTests {
435 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { 593 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
436 Ok(p) => p, 594 Ok(p) => p,
437 Err(e) => { 595 Err(e) => {
438 return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") 596 return TestResult::new(
439 .fail(&format!("Failed to clone repo: {}", e)); 597 test_name,
598 "GRASP-01",
599 "Push authorized with matching state",
600 )
601 .fail(format!("Failed to clone repo: {}", e));
440 } 602 }
441 }; 603 };
442 604
@@ -450,8 +612,12 @@ impl PushAuthorizationTests {
450 Ok(h) => h, 612 Ok(h) => h,
451 Err(e) => { 613 Err(e) => {
452 cleanup(); 614 cleanup();
453 return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") 615 return TestResult::new(
454 .fail(&format!("Failed to create deterministic commit: {}", e)); 616 test_name,
617 "GRASP-01",
618 "Push authorized with matching state",
619 )
620 .fail(format!("Failed to create deterministic commit: {}", e));
455 } 621 }
456 }; 622 };
457 623
@@ -459,7 +625,7 @@ impl PushAuthorizationTests {
459 if commit_hash != DETERMINISTIC_COMMIT_HASH { 625 if commit_hash != DETERMINISTIC_COMMIT_HASH {
460 cleanup(); 626 cleanup();
461 return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") 627 return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state")
462 .fail(&format!( 628 .fail(format!(
463 "Commit hash mismatch: got {}, expected {}", 629 "Commit hash mismatch: got {}, expected {}",
464 commit_hash, DETERMINISTIC_COMMIT_HASH 630 commit_hash, DETERMINISTIC_COMMIT_HASH
465 )); 631 ));
@@ -474,16 +640,24 @@ impl PushAuthorizationTests {
474 match branch_output { 640 match branch_output {
475 Err(e) => { 641 Err(e) => {
476 cleanup(); 642 cleanup();
477 return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") 643 return TestResult::new(
478 .fail(&format!("Failed to create main branch: {}", e)); 644 test_name,
645 "GRASP-01",
646 "Push authorized with matching state",
647 )
648 .fail(format!("Failed to create main branch: {}", e));
479 } 649 }
480 Ok(output) if !output.status.success() => { 650 Ok(output) if !output.status.success() => {
481 cleanup(); 651 cleanup();
482 return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") 652 return TestResult::new(
483 .fail(&format!( 653 test_name,
484 "Failed to create main branch: {}", 654 "GRASP-01",
485 String::from_utf8_lossy(&output.stderr) 655 "Push authorized with matching state",
486 )); 656 )
657 .fail(format!(
658 "Failed to create main branch: {}",
659 String::from_utf8_lossy(&output.stderr)
660 ));
487 } 661 }
488 _ => {} 662 _ => {}
489 } 663 }
@@ -497,16 +671,24 @@ impl PushAuthorizationTests {
497 match checkout_output { 671 match checkout_output {
498 Err(e) => { 672 Err(e) => {
499 cleanup(); 673 cleanup();
500 return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") 674 return TestResult::new(
501 .fail(&format!("Failed to checkout main branch: {}", e)); 675 test_name,
676 "GRASP-01",
677 "Push authorized with matching state",
678 )
679 .fail(format!("Failed to checkout main branch: {}", e));
502 } 680 }
503 Ok(output) if !output.status.success() => { 681 Ok(output) if !output.status.success() => {
504 cleanup(); 682 cleanup();
505 return TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") 683 return TestResult::new(
506 .fail(&format!( 684 test_name,
507 "Failed to checkout main branch: {}", 685 "GRASP-01",
508 String::from_utf8_lossy(&output.stderr) 686 "Push authorized with matching state",
509 )); 687 )
688 .fail(format!(
689 "Failed to checkout main branch: {}",
690 String::from_utf8_lossy(&output.stderr)
691 ));
510 } 692 }
511 _ => {} 693 _ => {}
512 } 694 }
@@ -524,17 +706,15 @@ impl PushAuthorizationTests {
524 } 706 }
525 Ok(false) => { 707 Ok(false) => {
526 TestResult::new(test_name, "GRASP-01", "Push authorized with matching state").fail( 708 TestResult::new(test_name, "GRASP-01", "Push authorized with matching state").fail(
527 &format!( 709 format!(
528 "Push was rejected but should have been accepted. \ 710 "Push was rejected but should have been accepted. \
529 The state event points to commit {} which matches the pushed commit.", 711 The state event points to commit {} which matches the pushed commit.",
530 DETERMINISTIC_COMMIT_HASH 712 DETERMINISTIC_COMMIT_HASH
531 ), 713 ),
532 ) 714 )
533 } 715 }
534 Err(e) => { 716 Err(e) => TestResult::new(test_name, "GRASP-01", "Push authorized with matching state")
535 TestResult::new(test_name, "GRASP-01", "Push authorized with matching state") 717 .fail(format!("Push error: {}", e)),
536 .fail(&format!("Push error: {}", e))
537 }
538 } 718 }
539 } 719 }
540 720
@@ -571,8 +751,12 @@ impl PushAuthorizationTests {
571 let state_event = match ctx.get_fixture(FixtureKind::RepoState).await { 751 let state_event = match ctx.get_fixture(FixtureKind::RepoState).await {
572 Ok(e) => e, 752 Ok(e) => e,
573 Err(e) => { 753 Err(e) => {
574 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 754 return TestResult::new(
575 .fail(&format!("Failed to create RepoState fixture: {}", e)); 755 test_name,
756 "GRASP-01",
757 "Push rejected when commit not in state event",
758 )
759 .fail(format!("Failed to create RepoState fixture: {}", e));
576 } 760 }
577 }; 761 };
578 762
@@ -587,16 +771,24 @@ impl PushAuthorizationTests {
587 { 771 {
588 Some(id) => id.to_string(), 772 Some(id) => id.to_string(),
589 None => { 773 None => {
590 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 774 return TestResult::new(
591 .fail("Missing repo_id in state event"); 775 test_name,
776 "GRASP-01",
777 "Push rejected when commit not in state event",
778 )
779 .fail("Missing repo_id in state event");
592 } 780 }
593 }; 781 };
594 782
595 let npub = match state_event.pubkey.to_bech32() { 783 let npub = match state_event.pubkey.to_bech32() {
596 Ok(n) => n, 784 Ok(n) => n,
597 Err(e) => { 785 Err(e) => {
598 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 786 return TestResult::new(
599 .fail(&format!("Failed to convert pubkey to bech32: {}", e)); 787 test_name,
788 "GRASP-01",
789 "Push rejected when commit not in state event",
790 )
791 .fail(format!("Failed to convert pubkey to bech32: {}", e));
600 } 792 }
601 }; 793 };
602 794
@@ -607,8 +799,12 @@ impl PushAuthorizationTests {
607 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { 799 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
608 Ok(p) => p, 800 Ok(p) => p,
609 Err(e) => { 801 Err(e) => {
610 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 802 return TestResult::new(
611 .fail(&format!("Failed to clone repo: {}", e)); 803 test_name,
804 "GRASP-01",
805 "Push rejected when commit not in state event",
806 )
807 .fail(format!("Failed to clone repo: {}", e));
612 } 808 }
613 }; 809 };
614 810
@@ -626,16 +822,24 @@ impl PushAuthorizationTests {
626 match branch_output { 822 match branch_output {
627 Err(e) => { 823 Err(e) => {
628 cleanup(); 824 cleanup();
629 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 825 return TestResult::new(
630 .fail(&format!("Failed to create/checkout main branch: {}", e)); 826 test_name,
827 "GRASP-01",
828 "Push rejected when commit not in state event",
829 )
830 .fail(format!("Failed to create/checkout main branch: {}", e));
631 } 831 }
632 Ok(output) if !output.status.success() => { 832 Ok(output) if !output.status.success() => {
633 cleanup(); 833 cleanup();
634 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 834 return TestResult::new(
635 .fail(&format!( 835 test_name,
636 "Failed to create/checkout main branch: {}", 836 "GRASP-01",
637 String::from_utf8_lossy(&output.stderr) 837 "Push rejected when commit not in state event",
638 )); 838 )
839 .fail(format!(
840 "Failed to create/checkout main branch: {}",
841 String::from_utf8_lossy(&output.stderr)
842 ));
639 } 843 }
640 _ => {} 844 _ => {}
641 } 845 }
@@ -644,8 +848,12 @@ impl PushAuthorizationTests {
644 // Any commit hash different from what's authorized in the state event will work 848 // Any commit hash different from what's authorized in the state event will work
645 if let Err(e) = create_commit(&clone_path, "Unauthorized commit - should be rejected") { 849 if let Err(e) = create_commit(&clone_path, "Unauthorized commit - should be rejected") {
646 cleanup(); 850 cleanup();
647 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 851 return TestResult::new(
648 .fail(&format!("Failed to create wrong commit: {}", e)); 852 test_name,
853 "GRASP-01",
854 "Push rejected when commit not in state event",
855 )
856 .fail(format!("Failed to create wrong commit: {}", e));
649 } 857 }
650 858
651 // ============================================================ 859 // ============================================================
@@ -703,7 +911,7 @@ impl PushAuthorizationTests {
703 "GRASP-01", 911 "GRASP-01",
704 "Push authorized by maintainer state event only (no announcement)", 912 "Push authorized by maintainer state event only (no announcement)",
705 ) 913 )
706 .fail(&format!("Failed to create RepoState fixture: {}", e)); 914 .fail(format!("Failed to create RepoState fixture: {}", e));
707 } 915 }
708 }; 916 };
709 917
@@ -717,7 +925,7 @@ impl PushAuthorizationTests {
717 "GRASP-01", 925 "GRASP-01",
718 "Push authorized by maintainer state event only (no announcement)", 926 "Push authorized by maintainer state event only (no announcement)",
719 ) 927 )
720 .fail(&format!("Failed to create MaintainerState fixture: {}", e)); 928 .fail(format!("Failed to create MaintainerState fixture: {}", e));
721 } 929 }
722 }; 930 };
723 931
@@ -749,7 +957,7 @@ impl PushAuthorizationTests {
749 "GRASP-01", 957 "GRASP-01",
750 "Push authorized by maintainer state event only (no announcement)", 958 "Push authorized by maintainer state event only (no announcement)",
751 ) 959 )
752 .fail(&format!("Failed to convert pubkey to bech32: {}", e)); 960 .fail(format!("Failed to convert pubkey to bech32: {}", e));
753 } 961 }
754 }; 962 };
755 963
@@ -785,19 +993,21 @@ impl PushAuthorizationTests {
785 .output(); 993 .output();
786 994
787 // Step 3: Create deterministic commit using existing function 995 // Step 3: Create deterministic commit using existing function
788 let commit_hash = 996 let commit_hash = match create_deterministic_commit_with_variant(
789 match create_deterministic_commit_with_variant(&clone_path, CommitVariant::Maintainer) { 997 &clone_path,
790 Ok(h) => h, 998 CommitVariant::Maintainer,
791 Err(e) => { 999 ) {
792 cleanup(); 1000 Ok(h) => h,
793 return TestResult::new( 1001 Err(e) => {
794 test_name, 1002 cleanup();
795 "GRASP-01", 1003 return TestResult::new(
796 "Push authorized by maintainer state event only (no announcement)", 1004 test_name,
797 ) 1005 "GRASP-01",
798 .fail(&format!("Failed to create maintainer commit: {}", e)); 1006 "Push authorized by maintainer state event only (no announcement)",
799 } 1007 )
800 }; 1008 .fail(format!("Failed to create maintainer commit: {}", e));
1009 }
1010 };
801 1011
802 // Step 4: Replace main branch with our new orphan branch 1012 // Step 4: Replace main branch with our new orphan branch
803 let _ = Command::new("git") 1013 let _ = Command::new("git")
@@ -818,7 +1028,7 @@ impl PushAuthorizationTests {
818 "GRASP-01", 1028 "GRASP-01",
819 "Push authorized by maintainer state event only (no announcement)", 1029 "Push authorized by maintainer state event only (no announcement)",
820 ) 1030 )
821 .fail(&format!( 1031 .fail(format!(
822 "Maintainer commit hash mismatch: got {}, expected {}", 1032 "Maintainer commit hash mismatch: got {}, expected {}",
823 commit_hash, MAINTAINER_DETERMINISTIC_COMMIT_HASH 1033 commit_hash, MAINTAINER_DETERMINISTIC_COMMIT_HASH
824 )); 1034 ));
@@ -843,7 +1053,7 @@ impl PushAuthorizationTests {
843 "GRASP-01", 1053 "GRASP-01",
844 "Push authorized by maintainer state event only (no announcement)", 1054 "Push authorized by maintainer state event only (no announcement)",
845 ) 1055 )
846 .fail(&format!( 1056 .fail(format!(
847 "Push was rejected but should have been accepted. \ 1057 "Push was rejected but should have been accepted. \
848 The maintainer published a state event with commit {}, \ 1058 The maintainer published a state event with commit {}, \
849 and even without a separate announcement, the relay should \ 1059 and even without a separate announcement, the relay should \
@@ -856,7 +1066,7 @@ impl PushAuthorizationTests {
856 "GRASP-01", 1066 "GRASP-01",
857 "Push authorized by maintainer state event only (no announcement)", 1067 "Push authorized by maintainer state event only (no announcement)",
858 ) 1068 )
859 .fail(&format!("Push error: {}", e)), 1069 .fail(format!("Push error: {}", e)),
860 } 1070 }
861 } 1071 }
862 1072
@@ -899,7 +1109,7 @@ impl PushAuthorizationTests {
899 "GRASP-01", 1109 "GRASP-01",
900 "Push authorized by recursive maintainer state event", 1110 "Push authorized by recursive maintainer state event",
901 ) 1111 )
902 .fail(&format!("Failed to create RepoState fixture: {}", e)); 1112 .fail(format!("Failed to create RepoState fixture: {}", e));
903 } 1113 }
904 }; 1114 };
905 1115
@@ -912,7 +1122,10 @@ impl PushAuthorizationTests {
912 "GRASP-01", 1122 "GRASP-01",
913 "Push authorized by recursive maintainer state event", 1123 "Push authorized by recursive maintainer state event",
914 ) 1124 )
915 .fail(&format!("Failed to create MaintainerAnnouncement fixture: {}", e)); 1125 .fail(format!(
1126 "Failed to create MaintainerAnnouncement fixture: {}",
1127 e
1128 ));
916 } 1129 }
917 }; 1130 };
918 1131
@@ -925,12 +1138,15 @@ impl PushAuthorizationTests {
925 "GRASP-01", 1138 "GRASP-01",
926 "Push authorized by recursive maintainer state event", 1139 "Push authorized by recursive maintainer state event",
927 ) 1140 )
928 .fail(&format!("Failed to create MaintainerState fixture: {}", e)); 1141 .fail(format!("Failed to create MaintainerState fixture: {}", e));
929 } 1142 }
930 }; 1143 };
931 1144
932 // Get RecursiveMaintainerRepoAndState fixture (completes 3-level delegation chain) 1145 // Get RecursiveMaintainerRepoAndState fixture (completes 3-level delegation chain)
933 match ctx.get_fixture(FixtureKind::RecursiveMaintainerRepoAndState).await { 1146 match ctx
1147 .get_fixture(FixtureKind::RecursiveMaintainerRepoAndState)
1148 .await
1149 {
934 Ok(_) => {} 1150 Ok(_) => {}
935 Err(e) => { 1151 Err(e) => {
936 return TestResult::new( 1152 return TestResult::new(
@@ -938,7 +1154,10 @@ impl PushAuthorizationTests {
938 "GRASP-01", 1154 "GRASP-01",
939 "Push authorized by recursive maintainer state event", 1155 "Push authorized by recursive maintainer state event",
940 ) 1156 )
941 .fail(&format!("Failed to create RecursiveMaintainerRepoAndState fixture: {}", e)); 1157 .fail(format!(
1158 "Failed to create RecursiveMaintainerRepoAndState fixture: {}",
1159 e
1160 ));
942 } 1161 }
943 }; 1162 };
944 1163
@@ -970,7 +1189,7 @@ impl PushAuthorizationTests {
970 "GRASP-01", 1189 "GRASP-01",
971 "Push authorized by recursive maintainer state event", 1190 "Push authorized by recursive maintainer state event",
972 ) 1191 )
973 .fail(&format!("Failed to convert pubkey to bech32: {}", e)); 1192 .fail(format!("Failed to convert pubkey to bech32: {}", e));
974 } 1193 }
975 }; 1194 };
976 1195
@@ -1006,19 +1225,24 @@ impl PushAuthorizationTests {
1006 .output(); 1225 .output();
1007 1226
1008 // Step 3: Create recursive maintainer deterministic commit 1227 // Step 3: Create recursive maintainer deterministic commit
1009 let commit_hash = 1228 let commit_hash = match create_deterministic_commit_with_variant(
1010 match create_deterministic_commit_with_variant(&clone_path, CommitVariant::RecursiveMaintainer) { 1229 &clone_path,
1011 Ok(h) => h, 1230 CommitVariant::RecursiveMaintainer,
1012 Err(e) => { 1231 ) {
1013 cleanup(); 1232 Ok(h) => h,
1014 return TestResult::new( 1233 Err(e) => {
1015 test_name, 1234 cleanup();
1016 "GRASP-01", 1235 return TestResult::new(
1017 "Push authorized by recursive maintainer state event", 1236 test_name,
1018 ) 1237 "GRASP-01",
1019 .fail(&format!("Failed to create recursive maintainer commit: {}", e)); 1238 "Push authorized by recursive maintainer state event",
1020 } 1239 )
1021 }; 1240 .fail(format!(
1241 "Failed to create recursive maintainer commit: {}",
1242 e
1243 ));
1244 }
1245 };
1022 1246
1023 // Step 4: Replace main branch with our new orphan branch 1247 // Step 4: Replace main branch with our new orphan branch
1024 let _ = Command::new("git") 1248 let _ = Command::new("git")
@@ -1039,7 +1263,7 @@ impl PushAuthorizationTests {
1039 "GRASP-01", 1263 "GRASP-01",
1040 "Push authorized by recursive maintainer state event", 1264 "Push authorized by recursive maintainer state event",
1041 ) 1265 )
1042 .fail(&format!( 1266 .fail(format!(
1043 "Recursive maintainer commit hash mismatch: got {}, expected {}", 1267 "Recursive maintainer commit hash mismatch: got {}, expected {}",
1044 commit_hash, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH 1268 commit_hash, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH
1045 )); 1269 ));
@@ -1064,7 +1288,7 @@ impl PushAuthorizationTests {
1064 "GRASP-01", 1288 "GRASP-01",
1065 "Push authorized by recursive maintainer state event", 1289 "Push authorized by recursive maintainer state event",
1066 ) 1290 )
1067 .fail(&format!( 1291 .fail(format!(
1068 "Push was rejected but should have been accepted. \ 1292 "Push was rejected but should have been accepted. \
1069 The recursive maintainer published a state event with commit {}, \ 1293 The recursive maintainer published a state event with commit {}, \
1070 and the relay should authorize pushes matching this state event \ 1294 and the relay should authorize pushes matching this state event \
@@ -1076,7 +1300,7 @@ impl PushAuthorizationTests {
1076 "GRASP-01", 1300 "GRASP-01",
1077 "Push authorized by recursive maintainer state event", 1301 "Push authorized by recursive maintainer state event",
1078 ) 1302 )
1079 .fail(&format!("Push error: {}", e)), 1303 .fail(format!("Push error: {}", e)),
1080 } 1304 }
1081 } 1305 }
1082 1306
@@ -1109,8 +1333,12 @@ impl PushAuthorizationTests {
1109 let state_event = match ctx.get_fixture(FixtureKind::RepoState).await { 1333 let state_event = match ctx.get_fixture(FixtureKind::RepoState).await {
1110 Ok(e) => e, 1334 Ok(e) => e,
1111 Err(e) => { 1335 Err(e) => {
1112 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1336 return TestResult::new(
1113 .fail(&format!("Failed to create RepoState fixture: {}", e)); 1337 test_name,
1338 "GRASP-01",
1339 "Non-maintainer state events ignored",
1340 )
1341 .fail(format!("Failed to create RepoState fixture: {}", e));
1114 } 1342 }
1115 }; 1343 };
1116 1344
@@ -1125,16 +1353,24 @@ impl PushAuthorizationTests {
1125 { 1353 {
1126 Some(id) => id.to_string(), 1354 Some(id) => id.to_string(),
1127 None => { 1355 None => {
1128 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1356 return TestResult::new(
1129 .fail("Missing repo_id in state event"); 1357 test_name,
1358 "GRASP-01",
1359 "Non-maintainer state events ignored",
1360 )
1361 .fail("Missing repo_id in state event");
1130 } 1362 }
1131 }; 1363 };
1132 1364
1133 let npub = match state_event.pubkey.to_bech32() { 1365 let npub = match state_event.pubkey.to_bech32() {
1134 Ok(n) => n, 1366 Ok(n) => n,
1135 Err(e) => { 1367 Err(e) => {
1136 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1368 return TestResult::new(
1137 .fail(&format!("Failed to convert pubkey to bech32: {}", e)); 1369 test_name,
1370 "GRASP-01",
1371 "Non-maintainer state events ignored",
1372 )
1373 .fail(format!("Failed to convert pubkey to bech32: {}", e));
1138 } 1374 }
1139 }; 1375 };
1140 1376
@@ -1145,8 +1381,12 @@ impl PushAuthorizationTests {
1145 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { 1381 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
1146 Ok(p) => p, 1382 Ok(p) => p,
1147 Err(e) => { 1383 Err(e) => {
1148 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1384 return TestResult::new(
1149 .fail(&format!("Failed to clone repo: {}", e)); 1385 test_name,
1386 "GRASP-01",
1387 "Non-maintainer state events ignored",
1388 )
1389 .fail(format!("Failed to clone repo: {}", e));
1150 } 1390 }
1151 }; 1391 };
1152 1392
@@ -1160,8 +1400,12 @@ impl PushAuthorizationTests {
1160 Ok(h) => h, 1400 Ok(h) => h,
1161 Err(e) => { 1401 Err(e) => {
1162 cleanup(); 1402 cleanup();
1163 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1403 return TestResult::new(
1164 .fail(&format!("Failed to create deterministic commit: {}", e)); 1404 test_name,
1405 "GRASP-01",
1406 "Non-maintainer state events ignored",
1407 )
1408 .fail(format!("Failed to create deterministic commit: {}", e));
1165 } 1409 }
1166 }; 1410 };
1167 1411
@@ -1169,7 +1413,7 @@ impl PushAuthorizationTests {
1169 if commit_hash != DETERMINISTIC_COMMIT_HASH { 1413 if commit_hash != DETERMINISTIC_COMMIT_HASH {
1170 cleanup(); 1414 cleanup();
1171 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1415 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1172 .fail(&format!( 1416 .fail(format!(
1173 "Commit hash mismatch: got {}, expected {}", 1417 "Commit hash mismatch: got {}, expected {}",
1174 commit_hash, DETERMINISTIC_COMMIT_HASH 1418 commit_hash, DETERMINISTIC_COMMIT_HASH
1175 )); 1419 ));
@@ -1184,16 +1428,24 @@ impl PushAuthorizationTests {
1184 match branch_output { 1428 match branch_output {
1185 Err(e) => { 1429 Err(e) => {
1186 cleanup(); 1430 cleanup();
1187 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1431 return TestResult::new(
1188 .fail(&format!("Failed to create main branch: {}", e)); 1432 test_name,
1433 "GRASP-01",
1434 "Non-maintainer state events ignored",
1435 )
1436 .fail(format!("Failed to create main branch: {}", e));
1189 } 1437 }
1190 Ok(output) if !output.status.success() => { 1438 Ok(output) if !output.status.success() => {
1191 cleanup(); 1439 cleanup();
1192 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1440 return TestResult::new(
1193 .fail(&format!( 1441 test_name,
1194 "Failed to create main branch: {}", 1442 "GRASP-01",
1195 String::from_utf8_lossy(&output.stderr) 1443 "Non-maintainer state events ignored",
1196 )); 1444 )
1445 .fail(format!(
1446 "Failed to create main branch: {}",
1447 String::from_utf8_lossy(&output.stderr)
1448 ));
1197 } 1449 }
1198 _ => {} 1450 _ => {}
1199 } 1451 }
@@ -1207,16 +1459,24 @@ impl PushAuthorizationTests {
1207 match checkout_output { 1459 match checkout_output {
1208 Err(e) => { 1460 Err(e) => {
1209 cleanup(); 1461 cleanup();
1210 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1462 return TestResult::new(
1211 .fail(&format!("Failed to checkout main branch: {}", e)); 1463 test_name,
1464 "GRASP-01",
1465 "Non-maintainer state events ignored",
1466 )
1467 .fail(format!("Failed to checkout main branch: {}", e));
1212 } 1468 }
1213 Ok(output) if !output.status.success() => { 1469 Ok(output) if !output.status.success() => {
1214 cleanup(); 1470 cleanup();
1215 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1471 return TestResult::new(
1216 .fail(&format!( 1472 test_name,
1217 "Failed to checkout main branch: {}", 1473 "GRASP-01",
1218 String::from_utf8_lossy(&output.stderr) 1474 "Non-maintainer state events ignored",
1219 )); 1475 )
1476 .fail(format!(
1477 "Failed to checkout main branch: {}",
1478 String::from_utf8_lossy(&output.stderr)
1479 ));
1220 } 1480 }
1221 _ => {} 1481 _ => {}
1222 } 1482 }
@@ -1231,16 +1491,24 @@ impl PushAuthorizationTests {
1231 match push_output { 1491 match push_output {
1232 Err(e) => { 1492 Err(e) => {
1233 cleanup(); 1493 cleanup();
1234 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1494 return TestResult::new(
1235 .fail(&format!("Failed to push initial commit: {}", e)); 1495 test_name,
1496 "GRASP-01",
1497 "Non-maintainer state events ignored",
1498 )
1499 .fail(format!("Failed to push initial commit: {}", e));
1236 } 1500 }
1237 Ok(output) if !output.status.success() => { 1501 Ok(output) if !output.status.success() => {
1238 cleanup(); 1502 cleanup();
1239 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1503 return TestResult::new(
1240 .fail(&format!( 1504 test_name,
1241 "Failed to push initial commit: {}", 1505 "GRASP-01",
1242 String::from_utf8_lossy(&output.stderr) 1506 "Non-maintainer state events ignored",
1243 )); 1507 )
1508 .fail(format!(
1509 "Failed to push initial commit: {}",
1510 String::from_utf8_lossy(&output.stderr)
1511 ));
1244 } 1512 }
1245 _ => {} 1513 _ => {}
1246 } 1514 }
@@ -1253,14 +1521,18 @@ impl PushAuthorizationTests {
1253 Ok(h) => h, 1521 Ok(h) => h,
1254 Err(e) => { 1522 Err(e) => {
1255 cleanup(); 1523 cleanup();
1256 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1524 return TestResult::new(
1257 .fail(&format!("Failed to create commit: {}", e)); 1525 test_name,
1526 "GRASP-01",
1527 "Non-maintainer state events ignored",
1528 )
1529 .fail(format!("Failed to create commit: {}", e));
1258 } 1530 }
1259 }; 1531 };
1260 1532
1261 // Create a rogue keypair (NOT the maintainer) 1533 // Create a rogue keypair (NOT the maintainer)
1262 let rogue_keys = Keys::generate(); 1534 let rogue_keys = Keys::generate();
1263 1535
1264 // Create a rogue state event announcing the new commit 1536 // Create a rogue state event announcing the new commit
1265 // This event has the correct repo_id but is signed by a non-maintainer 1537 // This event has the correct repo_id but is signed by a non-maintainer
1266 let rogue_state = match client 1538 let rogue_state = match client
@@ -1275,8 +1547,12 @@ impl PushAuthorizationTests {
1275 Ok(e) => e, 1547 Ok(e) => e,
1276 Err(e) => { 1548 Err(e) => {
1277 cleanup(); 1549 cleanup();
1278 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1550 return TestResult::new(
1279 .fail(&format!("Failed to build rogue state event: {}", e)); 1551 test_name,
1552 "GRASP-01",
1553 "Non-maintainer state events ignored",
1554 )
1555 .fail(format!("Failed to build rogue state event: {}", e));
1280 } 1556 }
1281 }; 1557 };
1282 1558
@@ -1284,7 +1560,7 @@ impl PushAuthorizationTests {
1284 if let Err(e) = client.client().send_event(&rogue_state).await { 1560 if let Err(e) = client.client().send_event(&rogue_state).await {
1285 cleanup(); 1561 cleanup();
1286 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1562 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1287 .fail(&format!("Failed to send rogue state event: {}", e)); 1563 .fail(format!("Failed to send rogue state event: {}", e));
1288 } 1564 }
1289 1565
1290 // Wait for event to propagate 1566 // Wait for event to propagate
@@ -1300,7 +1576,7 @@ impl PushAuthorizationTests {
1300 match push_result { 1576 match push_result {
1301 Ok(false) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").pass(), 1577 Ok(false) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").pass(),
1302 Ok(true) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1578 Ok(true) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1303 .fail(&format!( 1579 .fail(format!(
1304 "Push accepted but should be rejected. A non-maintainer (pubkey: {}) published \ 1580 "Push accepted but should be rejected. A non-maintainer (pubkey: {}) published \
1305 a state event announcing commit {}, but the push was accepted. The relay should \ 1581 a state event announcing commit {}, but the push was accepted. The relay should \
1306 only accept state events from maintainers (pubkey: {}).", 1582 only accept state events from maintainers (pubkey: {}).",
@@ -1342,7 +1618,7 @@ impl PushAuthorizationTests {
1342 "GRASP-01", 1618 "GRASP-01",
1343 "Push to refs/nostr/<invalid-event-id> rejected", 1619 "Push to refs/nostr/<invalid-event-id> rejected",
1344 ) 1620 )
1345 .fail(&format!("Failed to create repo: {}", e)); 1621 .fail(format!("Failed to create repo: {}", e));
1346 } 1622 }
1347 }; 1623 };
1348 1624
@@ -1408,7 +1684,7 @@ impl PushAuthorizationTests {
1408 "GRASP-01", 1684 "GRASP-01",
1409 "Push to refs/nostr/<invalid-event-id> rejected", 1685 "Push to refs/nostr/<invalid-event-id> rejected",
1410 ) 1686 )
1411 .fail(&format!( 1687 .fail(format!(
1412 "Push to {} was accepted but should be rejected. \ 1688 "Push to {} was accepted but should be rejected. \
1413 The event-id '{}' is NOT a valid 64-character hex string (EventId format). \ 1689 The event-id '{}' is NOT a valid 64-character hex string (EventId format). \
1414 The relay should reject pushes to refs/nostr/ with invalid event-id format.", 1690 The relay should reject pushes to refs/nostr/ with invalid event-id format.",
@@ -1419,7 +1695,7 @@ impl PushAuthorizationTests {
1419 "GRASP-01", 1695 "GRASP-01",
1420 "Push to refs/nostr/<invalid-event-id> rejected", 1696 "Push to refs/nostr/<invalid-event-id> rejected",
1421 ) 1697 )
1422 .fail(&format!("Push error: {}", e)), 1698 .fail(format!("Push error: {}", e)),
1423 } 1699 }
1424 } 1700 }
1425 1701
@@ -1434,7 +1710,8 @@ impl PushAuthorizationTests {
1434 client: &AuditClient, 1710 client: &AuditClient,
1435 relay_domain: &str, 1711 relay_domain: &str,
1436 ) -> TestResult { 1712 ) -> TestResult {
1437 let test_name = "test_pr_push_to_nostr_ref_with_wrong_commit_accepted_before_event_received"; 1713 let test_name =
1714 "test_pr_push_to_nostr_ref_with_wrong_commit_accepted_before_event_received";
1438 let desc = "Push wrong commit to refs/nostr/<pr-event-id> before PR event (should accept)"; 1715 let desc = "Push wrong commit to refs/nostr/<pr-event-id> before PR event (should accept)";
1439 let ctx = TestContext::new(client); 1716 let ctx = TestContext::new(client);
1440 1717
@@ -1459,15 +1736,13 @@ impl PushAuthorizationTests {
1459 /// the relay should validate any existing refs/nostr/<event-id> refs and 1736 /// the relay should validate any existing refs/nostr/<event-id> refs and
1460 /// delete those that don't match the commit in the PR event's `c` tag. 1737 /// delete those that don't match the commit in the PR event's `c` tag.
1461 /// 1738 ///
1462 /// Currently NOT_IMPLEMENTED - the relay doesn't have this cleanup logic yet.
1463 ///
1464 /// Depends on: `setup_repo_with_wrong_commit_pushed` (wrong commit already pushed) 1739 /// Depends on: `setup_repo_with_wrong_commit_pushed` (wrong commit already pushed)
1465 pub async fn test_pr_event_published_removes_nostr_ref_at_incorrect_commit( 1740 pub async fn test_pr_event_published_removes_nostr_ref_at_incorrect_commit(
1466 client: &AuditClient, 1741 client: &AuditClient,
1467 relay_domain: &str, 1742 relay_domain: &str,
1468 ) -> TestResult { 1743 ) -> TestResult {
1469 let test_name = "test_pr_event_published_removes_nostr_ref_at_incorrect_commit"; 1744 let test_name = "test_pr_event_published_removes_nostr_ref_at_incorrect_commit";
1470 let desc = "Publishing PR event should trigger cleanup of incorrect refs (NOT_IMPLEMENTED)"; 1745 let desc = "Publishing PR event should trigger cleanup of incorrect refs";
1471 let ctx = TestContext::new(client); 1746 let ctx = TestContext::new(client);
1472 1747
1473 // Setup: wrong commit already pushed to refs/nostr/<pr-event-id> 1748 // Setup: wrong commit already pushed to refs/nostr/<pr-event-id>
@@ -1479,7 +1754,7 @@ impl PushAuthorizationTests {
1479 }; 1754 };
1480 1755
1481 // NOW publish the PR event - this should trigger cleanup validation 1756 // NOW publish the PR event - this should trigger cleanup validation
1482 if let Err(e) = publish_pr_event_and_wait(&ctx).await { 1757 if let Err(e) = publish_pr_event_and_wait(&ctx, &setup.pr_event).await {
1483 setup.cleanup(); 1758 setup.cleanup();
1484 return TestResult::new(test_name, "GRASP-01", desc).fail(&e); 1759 return TestResult::new(test_name, "GRASP-01", desc).fail(&e);
1485 } 1760 }
@@ -1496,12 +1771,16 @@ impl PushAuthorizationTests {
1496 1771
1497 setup.cleanup(); 1772 setup.cleanup();
1498 1773
1499 // Document current behavior: relay doesn't implement automatic cleanup yet 1774 // Ref should be deleted since the pushed commit doesn't match the PR event's `c` tag
1500 TestResult::new(test_name, "GRASP-01", desc).fail(&format!( 1775 if refs_exist {
1501 "NOT_IMPLEMENTED: Relay should delete refs/nostr/<event-id> when PR event is published \ 1776 TestResult::new(test_name, "GRASP-01", desc).fail(format!(
1502 with non-matching commit. Currently ref still exists: {}. This requires relay-side validation logic.", 1777 "Expected refs/nostr/{} to be deleted when PR event published with non-matching commit, \
1503 refs_exist 1778 but the ref still exists. The relay should delete refs that don't match the event's `c` tag.",
1504 )) 1779 setup.pr_event_id
1780 ))
1781 } else {
1782 TestResult::new(test_name, "GRASP-01", desc).pass()
1783 }
1505 } 1784 }
1506 1785
1507 /// Test 3: Push wrong commit to refs/nostr/<pr-event-id> AFTER PR event exists 1786 /// Test 3: Push wrong commit to refs/nostr/<pr-event-id> AFTER PR event exists
@@ -1528,7 +1807,7 @@ impl PushAuthorizationTests {
1528 }; 1807 };
1529 1808
1530 // Publish PR event FIRST (before our test push) 1809 // Publish PR event FIRST (before our test push)
1531 if let Err(e) = publish_pr_event_and_wait(&ctx).await { 1810 if let Err(e) = publish_pr_event_and_wait(&ctx, &setup.pr_event).await {
1532 setup.cleanup(); 1811 setup.cleanup();
1533 return TestResult::new(test_name, "GRASP-01", desc).fail(&e); 1812 return TestResult::new(test_name, "GRASP-01", desc).fail(&e);
1534 } 1813 }
@@ -1577,7 +1856,7 @@ impl PushAuthorizationTests {
1577 }; 1856 };
1578 1857
1579 // Publish PR event FIRST 1858 // Publish PR event FIRST
1580 if let Err(e) = publish_pr_event_and_wait(&ctx).await { 1859 if let Err(e) = publish_pr_event_and_wait(&ctx, &setup.pr_event).await {
1581 setup.cleanup(); 1860 setup.cleanup();
1582 return TestResult::new(test_name, "GRASP-01", desc).fail(&e); 1861 return TestResult::new(test_name, "GRASP-01", desc).fail(&e);
1583 } 1862 }
@@ -1626,9 +1905,9 @@ mod tests {
1626 /// Run with: cd grasp-audit && nix develop -c cargo test --lib test_pr_test_commit_hash_discovery -- --nocapture 1905 /// Run with: cd grasp-audit && nix develop -c cargo test --lib test_pr_test_commit_hash_discovery -- --nocapture
1627 #[test] 1906 #[test]
1628 fn test_pr_test_commit_hash_discovery() { 1907 fn test_pr_test_commit_hash_discovery() {
1908 use std::fs;
1629 use std::process::Command; 1909 use std::process::Command;
1630 use tempfile::TempDir; 1910 use tempfile::TempDir;
1631 use std::fs;
1632 1911
1633 let temp_dir = TempDir::new().expect("Failed to create temp dir"); 1912 let temp_dir = TempDir::new().expect("Failed to create temp dir");
1634 let path = temp_dir.path(); 1913 let path = temp_dir.path();
@@ -1639,7 +1918,11 @@ mod tests {
1639 .current_dir(path) 1918 .current_dir(path)
1640 .output() 1919 .output()
1641 .expect("Failed to init git"); 1920 .expect("Failed to init git");
1642 assert!(output.status.success(), "git init failed: {:?}", String::from_utf8_lossy(&output.stderr)); 1921 assert!(
1922 output.status.success(),
1923 "git init failed: {:?}",
1924 String::from_utf8_lossy(&output.stderr)
1925 );
1643 1926
1644 // Configure git user - use PR Test Author identity 1927 // Configure git user - use PR Test Author identity
1645 let output = Command::new("git") 1928 let output = Command::new("git")
@@ -1666,21 +1949,31 @@ mod tests {
1666 .current_dir(path) 1949 .current_dir(path)
1667 .output() 1950 .output()
1668 .expect("git add failed"); 1951 .expect("git add failed");
1669 assert!(output.status.success(), "git add failed: {:?}", String::from_utf8_lossy(&output.stderr)); 1952 assert!(
1953 output.status.success(),
1954 "git add failed: {:?}",
1955 String::from_utf8_lossy(&output.stderr)
1956 );
1670 1957
1671 // Create deterministic commit with fixed dates and GPG disabled 1958 // Create deterministic commit with fixed dates and GPG disabled
1672 let output = Command::new("git") 1959 let output = Command::new("git")
1673 .args([ 1960 .args([
1674 "-c", "commit.gpgsign=false", 1961 "-c",
1962 "commit.gpgsign=false",
1675 "commit", 1963 "commit",
1676 "-m", "PR test deterministic commit", 1964 "-m",
1965 "PR test deterministic commit",
1677 ]) 1966 ])
1678 .env("GIT_AUTHOR_DATE", "2024-01-01T00:00:00Z") 1967 .env("GIT_AUTHOR_DATE", "2024-01-01T00:00:00Z")
1679 .env("GIT_COMMITTER_DATE", "2024-01-01T00:00:00Z") 1968 .env("GIT_COMMITTER_DATE", "2024-01-01T00:00:00Z")
1680 .current_dir(path) 1969 .current_dir(path)
1681 .output() 1970 .output()
1682 .expect("git commit failed"); 1971 .expect("git commit failed");
1683 assert!(output.status.success(), "git commit failed: {:?}", String::from_utf8_lossy(&output.stderr)); 1972 assert!(
1973 output.status.success(),
1974 "git commit failed: {:?}",
1975 String::from_utf8_lossy(&output.stderr)
1976 );
1684 1977
1685 // Get the commit hash 1978 // Get the commit hash
1686 let output = Command::new("git") 1979 let output = Command::new("git")
@@ -1688,7 +1981,11 @@ mod tests {
1688 .current_dir(path) 1981 .current_dir(path)
1689 .output() 1982 .output()
1690 .expect("git rev-parse failed"); 1983 .expect("git rev-parse failed");
1691 assert!(output.status.success(), "git rev-parse failed: {:?}", String::from_utf8_lossy(&output.stderr)); 1984 assert!(
1985 output.status.success(),
1986 "git rev-parse failed: {:?}",
1987 String::from_utf8_lossy(&output.stderr)
1988 );
1692 1989
1693 let hash = String::from_utf8_lossy(&output.stdout).trim().to_string(); 1990 let hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
1694 1991
@@ -1698,7 +1995,10 @@ mod tests {
1698 1995
1699 // Verify we got a valid 40-character hex hash 1996 // Verify we got a valid 40-character hex hash
1700 assert_eq!(hash.len(), 40, "Hash should be 40 hex chars, got: {}", hash); 1997 assert_eq!(hash.len(), 40, "Hash should be 40 hex chars, got: {}", hash);
1701 assert!(hash.chars().all(|c| c.is_ascii_hexdigit()), "Hash should be hex chars only"); 1998 assert!(
1999 hash.chars().all(|c| c.is_ascii_hexdigit()),
2000 "Hash should be hex chars only"
2001 );
1702 2002
1703 // If the constant is not PLACEHOLDER, verify it matches 2003 // If the constant is not PLACEHOLDER, verify it matches
1704 if PR_TEST_COMMIT_HASH != "PLACEHOLDER" { 2004 if PR_TEST_COMMIT_HASH != "PLACEHOLDER" {
@@ -1709,4 +2009,4 @@ mod tests {
1709 ); 2009 );
1710 } 2010 }
1711 } 2011 }
1712} \ No newline at end of file 2012}