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-28 16:27:29 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-28 16:27:29 +0000
commite6ceab90de1acad154624022a6036efac18abab6 (patch)
tree315c7b7ffc22339ed6cd0a31002e2fb0df190684 /grasp-audit
parentb94262161df99966fbb8aa6861fb46603039111f (diff)
test: added checks that refs/nostr/<event-id> match commit in PR / update
Diffstat (limited to 'grasp-audit')
-rw-r--r--grasp-audit/src/client.rs16
-rw-r--r--grasp-audit/src/fixtures.rs65
-rw-r--r--grasp-audit/src/lib.rs2
-rw-r--r--grasp-audit/src/specs/grasp01/push_authorization.rs686
4 files changed, 641 insertions, 128 deletions
diff --git a/grasp-audit/src/client.rs b/grasp-audit/src/client.rs
index 4f3401c..ed76a34 100644
--- a/grasp-audit/src/client.rs
+++ b/grasp-audit/src/client.rs
@@ -25,6 +25,8 @@ pub struct AuditClient {
25 maintainer_keys: Keys, 25 maintainer_keys: Keys,
26 /// Recursive maintainer keys for testing recursive authorization scenarios 26 /// Recursive maintainer keys for testing recursive authorization scenarios
27 recursive_maintainer_keys: Keys, 27 recursive_maintainer_keys: Keys,
28 /// PR author keys for testing PR event scenarios
29 pr_author_keys: Keys,
28 /// Fixture cache for TestContext instances - shared across all contexts using this client 30 /// Fixture cache for TestContext instances - shared across all contexts using this client
29 fixture_cache: FixtureCache, 31 fixture_cache: FixtureCache,
30} 32}
@@ -36,6 +38,7 @@ impl AuditClient {
36 let keys = Keys::generate(); 38 let keys = Keys::generate();
37 let maintainer_keys = Keys::generate(); 39 let maintainer_keys = Keys::generate();
38 let recursive_maintainer_keys = Keys::generate(); 40 let recursive_maintainer_keys = Keys::generate();
41 let pr_author_keys = Keys::generate();
39 let client = Client::new(keys.clone()); 42 let client = Client::new(keys.clone());
40 Self { 43 Self {
41 client, 44 client,
@@ -43,6 +46,7 @@ impl AuditClient {
43 keys, 46 keys,
44 maintainer_keys, 47 maintainer_keys,
45 recursive_maintainer_keys, 48 recursive_maintainer_keys,
49 pr_author_keys,
46 fixture_cache: Arc::new(Mutex::new(HashMap::new())), 50 fixture_cache: Arc::new(Mutex::new(HashMap::new())),
47 } 51 }
48 } 52 }
@@ -52,6 +56,7 @@ impl AuditClient {
52 let keys = Keys::generate(); 56 let keys = Keys::generate();
53 let maintainer_keys = Keys::generate(); 57 let maintainer_keys = Keys::generate();
54 let recursive_maintainer_keys = Keys::generate(); 58 let recursive_maintainer_keys = Keys::generate();
59 let pr_author_keys = Keys::generate();
55 let client = Client::new(keys.clone()); 60 let client = Client::new(keys.clone());
56 61
57 // Add relay and connect 62 // Add relay and connect
@@ -102,6 +107,7 @@ impl AuditClient {
102 keys, 107 keys,
103 maintainer_keys, 108 maintainer_keys,
104 recursive_maintainer_keys, 109 recursive_maintainer_keys,
110 pr_author_keys,
105 fixture_cache: Arc::new(Mutex::new(HashMap::new())), 111 fixture_cache: Arc::new(Mutex::new(HashMap::new())),
106 }) 112 })
107 } 113 }
@@ -278,6 +284,16 @@ impl AuditClient {
278 self.recursive_maintainer_keys.public_key().to_hex() 284 self.recursive_maintainer_keys.public_key().to_hex()
279 } 285 }
280 286
287 /// Get the PR author keys (for PR event testing)
288 pub fn pr_author_keys(&self) -> &Keys {
289 &self.pr_author_keys
290 }
291
292 /// Get the PR author public key as a hex string
293 pub fn pr_author_pubkey_hex(&self) -> String {
294 self.pr_author_keys.public_key().to_hex()
295 }
296
281 /// Create a NIP-34 repository announcement event with full customization 297 /// Create a NIP-34 repository announcement event with full customization
282 /// 298 ///
283 /// This is the core method for creating repository announcements. It allows 299 /// This is the core method for creating repository announcements. It allows
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs
index b6fbc79..dc4e638 100644
--- a/grasp-audit/src/fixtures.rs
+++ b/grasp-audit/src/fixtures.rs
@@ -74,6 +74,17 @@ pub const MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = "1c2d472c9b71ed51968a6650
74/// NOTE: This value is different from DETERMINISTIC_COMMIT_HASH due to different content 74/// NOTE: This value is different from DETERMINISTIC_COMMIT_HASH due to different content
75pub const RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = "05939b82de66fbdb9c077d0a64fc68522f3cb8e0"; 75pub const RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH: &str = "05939b82de66fbdb9c077d0a64fc68522f3cb8e0";
76 76
77/// Deterministic commit hash for PR test fixtures (PRTestCommit variant)
78/// This is the hash produced by creating a commit with:
79/// - Message: "PR test deterministic commit"
80/// - File: test.txt containing "PR test deterministic commit"
81/// - Author date: 2024-01-01T00:00:00Z
82/// - Committer date: 2024-01-01T00:00:00Z
83/// - GPG signing: disabled
84/// - User: "GRASP Audit Test <test@grasp-audit.local>"
85/// - Parent: none (root commit)
86pub const PR_TEST_COMMIT_HASH: &str = "8935183ff722bf04e861928c6a7e50868c6ca4a6";
87
77/// Types of test fixtures available 88/// Types of test fixtures available
78/// 89///
79/// ## Fixture Dependencies 90/// ## Fixture Dependencies
@@ -145,6 +156,15 @@ pub enum FixtureKind {
145 /// - State event points to RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH 156 /// - State event points to RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH
146 /// - Timestamp: 2 seconds in the past (most recent) 157 /// - Timestamp: 2 seconds in the past (most recent)
147 RecursiveMaintainerRepoAndState, 158 RecursiveMaintainerRepoAndState,
159
160 /// PR (Patch/Pull Request) event for the SAME repo_id as ValidRepo
161 /// - Requires ValidRepo (uses same repo_id)
162 /// - Signed by `client.pr_author_keys()`
163 /// - Kind 1617 (NIP-34 patch)
164 /// - Includes `a` tag referencing the repo
165 /// - Includes `c` tag pointing to PR_TEST_COMMIT_HASH
166 /// - Timestamp: 1 second in the past
167 PREvent,
148} 168}
149 169
150/// Context mode for fixture management 170/// Context mode for fixture management
@@ -633,6 +653,47 @@ impl<'a> TestContext<'a> {
633 // Return the state event (caller will send it) 653 // Return the state event (caller will send it)
634 self.build_recursive_maintainer_state(&repo_id) 654 self.build_recursive_maintainer_state(&repo_id)
635 } 655 }
656
657 FixtureKind::PREvent => {
658 use nostr_sdk::prelude::*;
659
660 // Reuse ValidRepo fixture to get repo_id and owner pubkey
661 let repo = self.get_or_create_repo().await?;
662
663 let repo_id = repo
664 .tags
665 .iter()
666 .find(|t| t.kind() == TagKind::d())
667 .and_then(|t| t.content())
668 .ok_or_else(|| anyhow::anyhow!("Missing repo_id in ValidRepo fixture"))?
669 .to_string();
670
671 // Create PR event 1 second in the past
672 let base_time = Timestamp::now().as_u64();
673 let pr_timestamp = Timestamp::from(base_time - 1);
674
675 // Build NIP-34 patch event (kind 1617)
676 self.client
677 .event_builder(
678 Kind::Custom(1617), // NIP-34 patch/PR kind
679 "Test PR for GRASP validation",
680 )
681 .tag(Tag::custom(
682 TagKind::custom("a"),
683 vec![format!(
684 "30617:{}:{}",
685 self.client.public_key().to_hex(), // Owner pubkey
686 repo_id
687 )],
688 ))
689 .tag(Tag::custom(
690 TagKind::custom("c"),
691 vec![PR_TEST_COMMIT_HASH.to_string()],
692 ))
693 .custom_time(pr_timestamp)
694 .build(self.client.pr_author_keys())
695 .map_err(|e| anyhow::anyhow!("Failed to build PR event: {}", e))
696 }
636 } 697 }
637 } 698 }
638 699
@@ -1071,6 +1132,8 @@ pub enum CommitVariant {
1071 Maintainer, 1132 Maintainer,
1072 /// Recursive maintainer pubkey variant - uses "Recursive maintainer initial commit" content 1133 /// Recursive maintainer pubkey variant - uses "Recursive maintainer initial commit" content
1073 RecursiveMaintainer, 1134 RecursiveMaintainer,
1135 /// PR test commit variant - for PR event tests
1136 PRTestCommit,
1074} 1137}
1075 1138
1076impl CommitVariant { 1139impl CommitVariant {
@@ -1080,6 +1143,7 @@ impl CommitVariant {
1080 CommitVariant::Owner => "Initial commit", 1143 CommitVariant::Owner => "Initial commit",
1081 CommitVariant::Maintainer => "Maintainer initial commit", 1144 CommitVariant::Maintainer => "Maintainer initial commit",
1082 CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit", 1145 CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit",
1146 CommitVariant::PRTestCommit => "PR test deterministic commit",
1083 } 1147 }
1084 } 1148 }
1085 1149
@@ -1089,6 +1153,7 @@ impl CommitVariant {
1089 CommitVariant::Owner => "Initial commit", 1153 CommitVariant::Owner => "Initial commit",
1090 CommitVariant::Maintainer => "Maintainer initial commit", 1154 CommitVariant::Maintainer => "Maintainer initial commit",
1091 CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit", 1155 CommitVariant::RecursiveMaintainer => "Recursive maintainer initial commit",
1156 CommitVariant::PRTestCommit => "PR test deterministic commit",
1092 } 1157 }
1093 } 1158 }
1094} 1159}
diff --git a/grasp-audit/src/lib.rs b/grasp-audit/src/lib.rs
index 5ee93b3..fb52ba7 100644
--- a/grasp-audit/src/lib.rs
+++ b/grasp-audit/src/lib.rs
@@ -46,7 +46,7 @@ pub use fixtures::{
46 // Types and constants 46 // Types and constants
47 CommitVariant, ContextMode, FixtureKind, TestContext, 47 CommitVariant, ContextMode, FixtureKind, TestContext,
48 DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH, 48 DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH,
49 RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, 49 PR_TEST_COMMIT_HASH, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH,
50}; 50};
51pub use result::{AuditResult, TestResult}; 51pub use result::{AuditResult, TestResult};
52 52
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs
index 0170f6e..97d068c 100644
--- a/grasp-audit/src/specs/grasp01/push_authorization.rs
+++ b/grasp-audit/src/specs/grasp01/push_authorization.rs
@@ -16,6 +16,21 @@
16//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test 16//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test
17//! ``` 17//! ```
18 18
19/// Expected hash for PR test deterministic commit
20///
21/// This hash is produced by creating a commit with:
22/// - File: test.txt containing "PR test deterministic commit"
23/// - Message: "PR test deterministic commit"
24/// - Author: "PR Test Author <pr-test@example.com>"
25/// - Author date: 2024-01-01T00:00:00Z
26/// - Committer date: 2024-01-01T00:00:00Z
27/// - GPG signing: disabled
28/// - Parent: none (root commit)
29///
30/// Run `test_pr_test_commit_hash_discovery` to discover/verify this value.
31#[allow(dead_code)]
32const PR_TEST_COMMIT_HASH: &str = "8935183ff722bf04e861928c6a7e50868c6ca4a6";
33
19use crate::{ 34use crate::{
20 clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, 35 clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant,
21 try_push, try_push_to_ref, AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, 36 try_push, try_push_to_ref, AuditClient, CommitVariant, FixtureKind, TestContext, TestResult,
@@ -24,6 +39,268 @@ use crate::{
24}; 39};
25use nostr_sdk::prelude::*; 40use nostr_sdk::prelude::*;
26use std::fs; 41use std::fs;
42use std::path::{Path, PathBuf};
43use std::process::Command;
44
45// ============================================================
46// PR Event Test Helper Functions
47// ============================================================
48
49/// Creates a deterministic PR test commit in the specified repository.
50/// Returns the commit hash which should match PR_TEST_COMMIT_HASH.
51///
52/// This function handles:
53/// 1. Creating an orphan branch (removes all history)
54/// 2. Clearing staged files
55/// 3. Creating deterministic commit using PRTestCommit variant
56/// 4. Replacing main branch with the orphan branch
57/// 5. Verifying the commit hash matches expected value
58///
59/// # Arguments
60/// * `clone_path` - Path to the cloned repository
61///
62/// # Returns
63/// * `Ok(String)` - The commit hash (should match PR_TEST_COMMIT_HASH)
64/// * `Err(String)` - Error message if commit creation failed
65fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> {
66 // Step 1: Create orphan branch (removes all history)
67 let _ = Command::new("git")
68 .args(["checkout", "--orphan", "pr-test-branch"])
69 .current_dir(clone_path)
70 .output();
71
72 // Step 2: Clear staged files (orphan keeps files staged from previous branch)
73 let _ = Command::new("git")
74 .args(["rm", "-rf", "--cached", "."])
75 .current_dir(clone_path)
76 .output();
77
78 // Step 3: Create deterministic commit using existing function
79 let commit_hash = create_deterministic_commit_with_variant(clone_path, CommitVariant::PRTestCommit)?;
80
81 // Step 4: Replace main branch with our new orphan branch
82 let _ = Command::new("git")
83 .args(["branch", "-D", "main"])
84 .current_dir(clone_path)
85 .output();
86
87 let _ = Command::new("git")
88 .args(["branch", "-m", "main"])
89 .current_dir(clone_path)
90 .output();
91
92 // Verify commit hash matches expected
93 if commit_hash != PR_TEST_COMMIT_HASH {
94 return Err(format!(
95 "PR test commit hash mismatch: got {}, expected {}",
96 commit_hash, PR_TEST_COMMIT_HASH
97 ));
98 }
99
100 Ok(commit_hash)
101}
102
103/// Sets up a complete PR test repository with deterministic commit.
104/// Returns: (clone_path, pr_event_id, repo_id, owner_npub)
105///
106/// This function handles the complete setup for PR event tests:
107/// 1. Gets RepoAnnouncement and PREvent fixtures
108/// 2. Extracts repo details (repo_id, owner_npub, pr_event_id)
109/// 3. Clones the repository
110/// 4. Creates the deterministic PR test commit
111///
112/// # Arguments
113/// * `ctx` - The TestContext for fixture management
114/// * `relay_url` - The relay URL for cloning (e.g., "localhost:7000")
115///
116/// # Returns
117/// * `Ok((PathBuf, String, String, String))` - (clone_path, pr_event_id, repo_id, owner_npub)
118/// * `Err(String)` - Error message if setup failed
119#[allow(dead_code)]
120async fn setup_pr_test_repo(
121 ctx: &TestContext<'_>,
122 relay_url: &str,
123) -> Result<(PathBuf, String, String, String), String> {
124 // Get fixtures
125 let repo_event = ctx
126 .get_fixture(FixtureKind::ValidRepo)
127 .await
128 .map_err(|e| format!("Failed to get repo announcement: {}", e))?;
129
130 let pr_event = ctx
131 .get_fixture(FixtureKind::PREvent)
132 .await
133 .map_err(|e| format!("Failed to get PR event: {}", e))?;
134
135 // Extract repo details using nostr-sdk 0.43 API (field access)
136 let repo_id = repo_event
137 .tags
138 .iter()
139 .find(|t| t.kind() == TagKind::d())
140 .and_then(|t| t.content())
141 .ok_or("No repo identifier in announcement")?
142 .to_string();
143
144 let owner_npub = repo_event.pubkey.to_bech32().map_err(|e| e.to_string())?;
145 let pr_event_id = pr_event.id.to_hex();
146
147 // Clone the repository
148 let clone_path = clone_repo(relay_url, &owner_npub, &repo_id)?;
149
150 // Create the PR test commit
151 create_pr_test_commit(&clone_path)?;
152
153 Ok((clone_path, pr_event_id, repo_id, owner_npub))
154}
155
156// ============================================================
157// PR Ref Push Test Setup Helpers - Minimize Test Duplication
158// ============================================================
159
160/// Result of setting up a repo with a wrong commit pushed before PR event exists.
161/// Used as shared setup for tests 3, 4, 5 which all depend on this scenario.
162#[allow(dead_code)]
163struct PrRefTestSetup {
164 clone_path: PathBuf,
165 pr_event_id: String,
166 repo_id: String,
167 owner_npub: String,
168 wrong_commit_hash: String,
169}
170
171impl PrRefTestSetup {
172 fn cleanup(&self) {
173 let _ = std::fs::remove_dir_all(&self.clone_path);
174 }
175}
176
177/// Sets up a repo and pushes a WRONG commit to refs/nostr/<pr-event-id> BEFORE PR event exists.
178///
179/// This is the shared setup for PR ref lifecycle tests:
180/// - Creates repo (gets PREvent fixture for event-id but doesn't publish yet)
181/// - Clones repo
182/// - Creates a commit that does NOT match PR_TEST_COMMIT_HASH
183/// - Pushes to refs/nostr/<pr-event-id> (should succeed - no event to validate against)
184///
185/// Tests using this setup:
186/// - test_pr_push_to_nostr_ref_with_wrong_commit_accepted_before_event_received: verify initial push accepted
187/// - test_pr_event_published_removes_nostr_ref_at_incorrect_commit: publish event, verify cleanup
188/// - test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected: publish event, try push wrong commit
189/// - test_push_to_nostr_ref_with_correct_commit_after_event_received_accepted: publish event, push correct commit
190#[allow(dead_code)]
191async fn setup_repo_with_wrong_commit_pushed(
192 ctx: &TestContext<'_>,
193 relay_domain: &str,
194) -> Result<PrRefTestSetup, String> {
195 // Get fixtures (PREvent fixture creates the event but doesn't publish until we call get_fixture)
196 let repo_event = ctx
197 .get_fixture(FixtureKind::ValidRepo)
198 .await
199 .map_err(|e| format!("Failed to get repo announcement: {}", e))?;
200
201 // Get PR event fixture (creates event object but doesn't publish to relay yet)
202 let pr_event = ctx
203 .get_fixture(FixtureKind::PREvent)
204 .await
205 .map_err(|e| format!("Failed to get PR event fixture: {}", e))?;
206
207 let repo_id = repo_event
208 .tags
209 .iter()
210 .find(|t| t.kind() == TagKind::d())
211 .and_then(|t| t.content())
212 .ok_or("No repo identifier in announcement")?
213 .to_string();
214
215 let owner_npub = repo_event.pubkey.to_bech32().map_err(|e| e.to_string())?;
216 let pr_event_id = pr_event.id.to_hex();
217
218 // Clone the repository
219 let clone_path = clone_repo(relay_domain, &owner_npub, &repo_id)?;
220
221 // 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)?;
223
224 // Verify it's actually different from expected
225 if wrong_commit_hash == PR_TEST_COMMIT_HASH {
226 let _ = std::fs::remove_dir_all(&clone_path);
227 return Err("Test setup error: wrong_commit_hash equals PR_TEST_COMMIT_HASH".to_string());
228 }
229
230 // Push to refs/nostr/<pr-event-id> (no event published yet, should succeed)
231 let push_output = Command::new("git")
232 .args(["push", "origin", &format!("main:refs/nostr/{}", pr_event_id)])
233 .current_dir(&clone_path)
234 .output()
235 .map_err(|e| format!("Failed to execute git push: {}", e))?;
236
237 if !push_output.status.success() {
238 let stderr = String::from_utf8_lossy(&push_output.stderr);
239 let _ = std::fs::remove_dir_all(&clone_path);
240 return Err(format!(
241 "Initial push failed (expected success before PR event): {}",
242 stderr
243 ));
244 }
245
246 Ok(PrRefTestSetup {
247 clone_path,
248 pr_event_id,
249 repo_id,
250 owner_npub,
251 wrong_commit_hash,
252 })
253}
254
255/// Publishes the PR event fixture and waits for relay to process it.
256/// Call this after setup_repo_with_wrong_commit_pushed to test post-event behavior.
257#[allow(dead_code)]
258async fn publish_pr_event_and_wait(ctx: &TestContext<'_>) -> Result<Event, String> {
259 // Publishing the PR event - get_fixture publishes if not already published
260 let pr_event = ctx
261 .get_fixture(FixtureKind::PREvent)
262 .await
263 .map_err(|e| format!("Failed to publish PR event: {}", e))?;
264
265 // Wait for relay to process
266 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
267
268 Ok(pr_event)
269}
270
271/// Creates the correct PR test commit (matching PR_TEST_COMMIT_HASH) in an existing clone.
272/// Used after wrong commit was pushed to test pushing the correct commit.
273#[allow(dead_code)]
274fn reset_to_correct_pr_commit(clone_path: &Path) -> Result<String, String> {
275 // Create the correct PR test commit (replaces current state)
276 create_pr_test_commit(clone_path)
277}
278
279/// Attempts to push current HEAD to refs/nostr/<pr-event-id>.
280/// Returns Ok(true) if push succeeded, Ok(false) if rejected, Err on git error.
281#[allow(dead_code)]
282fn push_to_pr_ref(clone_path: &Path, pr_event_id: &str) -> Result<bool, String> {
283 let push_output = Command::new("git")
284 .args(["push", "--force", "origin", &format!("main:refs/nostr/{}", pr_event_id)])
285 .current_dir(clone_path)
286 .output()
287 .map_err(|e| format!("Failed to execute git push: {}", e))?;
288
289 Ok(push_output.status.success())
290}
291
292/// Checks if a ref exists on the remote.
293#[allow(dead_code)]
294fn ref_exists_on_remote(clone_path: &Path, ref_name: &str) -> Result<bool, String> {
295 let output = Command::new("git")
296 .args(["ls-remote", "origin", ref_name])
297 .current_dir(clone_path)
298 .output()
299 .map_err(|e| format!("Failed to execute git ls-remote: {}", e))?;
300
301 let stdout = String::from_utf8_lossy(&output.stdout);
302 Ok(!stdout.trim().is_empty())
303}
27 304
28/// Test suite for Push Authorization operations 305/// Test suite for Push Authorization operations
29pub struct PushAuthorizationTests; 306pub struct PushAuthorizationTests;
@@ -41,8 +318,11 @@ impl PushAuthorizationTests {
41 results.add(Self::test_push_rejected_wrong_commit(client, relay_domain).await); 318 results.add(Self::test_push_rejected_wrong_commit(client, relay_domain).await);
42 results.add(Self::test_push_authorized_by_maintainer_state_only(client, relay_domain).await); 319 results.add(Self::test_push_authorized_by_maintainer_state_only(client, relay_domain).await);
43 results.add(Self::test_push_authorized_by_recursive_maintainer_state(client, relay_domain).await); 320 results.add(Self::test_push_authorized_by_recursive_maintainer_state(client, relay_domain).await);
44 results.add(Self::test_push_to_refs_nostr_valid_event_id(client, relay_domain).await); 321 results.add(Self::test_push_to_nostr_ref_with_invalid_event_id_rejected(client, relay_domain).await);
45 results.add(Self::test_push_to_refs_nostr_invalid_event_id(client, relay_domain).await); 322 results.add(Self::test_pr_push_to_nostr_ref_with_wrong_commit_accepted_before_event_received(client, relay_domain).await);
323 results.add(Self::test_pr_event_published_removes_nostr_ref_at_incorrect_commit(client, relay_domain).await);
324 results.add(Self::test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected(client, relay_domain).await);
325 results.add(Self::test_push_to_nostr_ref_with_correct_commit_after_event_received_accepted(client, relay_domain).await);
46 326
47 results 327 results
48 } 328 }
@@ -1032,22 +1312,22 @@ impl PushAuthorizationTests {
1032 } 1312 }
1033 } 1313 }
1034 1314
1035 /// Test that push to refs/nostr/<event-id> succeeds with valid EventId format 1315 /// Test that push to refs/nostr/<invalid> is rejected with invalid EventId format
1036 /// 1316 ///
1037 /// GRASP-01: "MUST accept pushes via this service to `refs/nostr/<event-id>`" 1317 /// GRASP-01: "MUST accept pushes via this service to `refs/nostr/<event-id>`"
1038 /// The event_id must parse as a valid rust-nostr EventId (64-char hex string). 1318 /// The event_id must parse as a valid rust-nostr EventId (64-char hex string).
1039 /// This does NOT require the ref to be listed in any state event - it's purely format validation. 1319 /// Invalid formats (too short, non-hex, etc.) should be rejected.
1040 /// 1320 ///
1041 /// ## Fixture-First Pattern 1321 /// ## Fixture-First Pattern
1042 /// 1322 ///
1043 /// 1. **Generate**: Create repo with ValidRepo fixture (no state event needed) 1323 /// 1. **Generate**: Create repo with ValidRepo fixture (no state event needed)
1044 /// 2. **Send**: Clone repo, create commit, push to refs/nostr/<valid-event-id> 1324 /// 2. **Send**: Clone repo, create commit, try to push to refs/nostr/123 (invalid)
1045 /// 3. **Verify**: Push should succeed because event-id format is valid 1325 /// 3. **Verify**: Push should be rejected because event-id format is invalid
1046 pub async fn test_push_to_refs_nostr_valid_event_id( 1326 pub async fn test_push_to_nostr_ref_with_invalid_event_id_rejected(
1047 client: &AuditClient, 1327 client: &AuditClient,
1048 relay_domain: &str, 1328 relay_domain: &str,
1049 ) -> TestResult { 1329 ) -> TestResult {
1050 let test_name = "test_push_to_refs_nostr_valid_event_id"; 1330 let test_name = "test_push_to_nostr_ref_with_invalid_event_id_rejected";
1051 1331
1052 // ============================================================ 1332 // ============================================================
1053 // Step 1: GENERATE - Create repo (no state event needed for refs/nostr/) 1333 // Step 1: GENERATE - Create repo (no state event needed for refs/nostr/)
@@ -1060,7 +1340,7 @@ impl PushAuthorizationTests {
1060 return TestResult::new( 1340 return TestResult::new(
1061 test_name, 1341 test_name,
1062 "GRASP-01", 1342 "GRASP-01",
1063 "Push to refs/nostr/<valid-event-id> accepted", 1343 "Push to refs/nostr/<invalid-event-id> rejected",
1064 ) 1344 )
1065 .fail(&format!("Failed to create repo: {}", e)); 1345 .fail(&format!("Failed to create repo: {}", e));
1066 } 1346 }
@@ -1078,7 +1358,7 @@ impl PushAuthorizationTests {
1078 let npub = repo.pubkey.to_bech32().unwrap(); 1358 let npub = repo.pubkey.to_bech32().unwrap();
1079 1359
1080 // ============================================================ 1360 // ============================================================
1081 // Step 2: SEND - Clone repo, create commit, push to refs/nostr/<event-id> 1361 // Step 2: SEND - Clone repo, create commit, try push to invalid ref
1082 // ============================================================ 1362 // ============================================================
1083 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { 1363 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
1084 Ok(p) => p, 1364 Ok(p) => p,
@@ -1086,7 +1366,7 @@ impl PushAuthorizationTests {
1086 return TestResult::new( 1366 return TestResult::new(
1087 test_name, 1367 test_name,
1088 "GRASP-01", 1368 "GRASP-01",
1089 "Push to refs/nostr/<valid-event-id> accepted", 1369 "Push to refs/nostr/<invalid-event-id> rejected",
1090 ) 1370 )
1091 .fail(&e); 1371 .fail(&e);
1092 } 1372 }
@@ -1096,185 +1376,337 @@ impl PushAuthorizationTests {
1096 }; 1376 };
1097 1377
1098 // Create a unique commit 1378 // Create a unique commit
1099 if let Err(e) = create_commit(&clone_path, "Test commit for refs/nostr push") { 1379 if let Err(e) = create_commit(&clone_path, "Test commit for invalid refs/nostr push") {
1100 cleanup(); 1380 cleanup();
1101 return TestResult::new( 1381 return TestResult::new(
1102 test_name, 1382 test_name,
1103 "GRASP-01", 1383 "GRASP-01",
1104 "Push to refs/nostr/<valid-event-id> accepted", 1384 "Push to refs/nostr/<invalid-event-id> rejected",
1105 ) 1385 )
1106 .fail(&e); 1386 .fail(&e);
1107 } 1387 }
1108 1388
1109 // Generate a random event to get a valid EventId 1389 // Use an invalid event-id (too short, not a valid 64-char hex)
1110 let keys = Keys::generate(); 1390 let invalid_event_id = "123";
1111 let event = match EventBuilder::text_note("test") 1391 let ref_name = format!("refs/nostr/{}", invalid_event_id);
1112 .sign(&keys)
1113 .await
1114 {
1115 Ok(e) => e,
1116 Err(e) => {
1117 cleanup();
1118 return TestResult::new(
1119 test_name,
1120 "GRASP-01",
1121 "Push to refs/nostr/<valid-event-id> accepted",
1122 )
1123 .fail(&format!("Failed to create test event: {}", e));
1124 }
1125 };
1126
1127 // Use the event id as the refs/nostr/ target
1128 let ref_name = format!("refs/nostr/{}", event.id);
1129 1392
1130 // ============================================================ 1393 // ============================================================
1131 // Step 3: VERIFY - Push should succeed with valid event-id format 1394 // Step 3: VERIFY - Push should be rejected with invalid event-id format
1132 // ============================================================ 1395 // ============================================================
1133 let push_result = try_push_to_ref(&clone_path, &ref_name); 1396 let push_result = try_push_to_ref(&clone_path, &ref_name);
1134 cleanup(); 1397 cleanup();
1135 1398
1136 match push_result { 1399 match push_result {
1137 Ok(true) => TestResult::new( 1400 Ok(false) => TestResult::new(
1138 test_name, 1401 test_name,
1139 "GRASP-01", 1402 "GRASP-01",
1140 "Push to refs/nostr/<valid-event-id> accepted", 1403 "Push to refs/nostr/<invalid-event-id> rejected",
1141 ) 1404 )
1142 .pass(), 1405 .pass(),
1143 Ok(false) => TestResult::new( 1406 Ok(true) => TestResult::new(
1144 test_name, 1407 test_name,
1145 "GRASP-01", 1408 "GRASP-01",
1146 "Push to refs/nostr/<valid-event-id> accepted", 1409 "Push to refs/nostr/<invalid-event-id> rejected",
1147 ) 1410 )
1148 .fail(&format!( 1411 .fail(&format!(
1149 "Push to {} was rejected but should be accepted. \ 1412 "Push to {} was accepted but should be rejected. \
1150 The event-id '{}' is a valid 64-character hex string (EventId format).", 1413 The event-id '{}' is NOT a valid 64-character hex string (EventId format). \
1151 ref_name, event.id 1414 The relay should reject pushes to refs/nostr/ with invalid event-id format.",
1415 ref_name, invalid_event_id
1152 )), 1416 )),
1153 Err(e) => TestResult::new( 1417 Err(e) => TestResult::new(
1154 test_name, 1418 test_name,
1155 "GRASP-01", 1419 "GRASP-01",
1156 "Push to refs/nostr/<valid-event-id> accepted", 1420 "Push to refs/nostr/<invalid-event-id> rejected",
1157 ) 1421 )
1158 .fail(&format!("Push error: {}", e)), 1422 .fail(&format!("Push error: {}", e)),
1159 } 1423 }
1160 } 1424 }
1161 1425
1162 /// Test that push to refs/nostr/<invalid> is rejected with invalid EventId format 1426 /// Test 1: Push wrong commit to refs/nostr/<pr-event-id> BEFORE PR event is published
1163 ///
1164 /// GRASP-01: "MUST accept pushes via this service to `refs/nostr/<event-id>`"
1165 /// The event_id must parse as a valid rust-nostr EventId (64-char hex string).
1166 /// Invalid formats (too short, non-hex, etc.) should be rejected.
1167 /// 1427 ///
1168 /// ## Fixture-First Pattern 1428 /// This test verifies that the relay accepts pushes to refs/nostr/<event-id>
1429 /// when no corresponding event exists yet. This is expected behavior because
1430 /// there's no validation event to check against.
1169 /// 1431 ///
1170 /// 1. **Generate**: Create repo with ValidRepo fixture (no state event needed) 1432 /// Uses `setup_repo_with_wrong_commit_pushed` helper which handles all setup.
1171 /// 2. **Send**: Clone repo, create commit, try to push to refs/nostr/123 (invalid) 1433 pub async fn test_pr_push_to_nostr_ref_with_wrong_commit_accepted_before_event_received(
1172 /// 3. **Verify**: Push should be rejected because event-id format is invalid
1173 pub async fn test_push_to_refs_nostr_invalid_event_id(
1174 client: &AuditClient, 1434 client: &AuditClient,
1175 relay_domain: &str, 1435 relay_domain: &str,
1176 ) -> TestResult { 1436 ) -> TestResult {
1177 let test_name = "test_push_to_refs_nostr_invalid_event_id"; 1437 let test_name = "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)";
1439 let ctx = TestContext::new(client);
1178 1440
1179 // ============================================================ 1441 // Setup includes: create repo, clone, create wrong commit, push to refs/nostr/<event-id>
1180 // Step 1: GENERATE - Create repo (no state event needed for refs/nostr/) 1442 // The push happens BEFORE PR event is published, so should succeed
1181 // ============================================================ 1443 let setup = match setup_repo_with_wrong_commit_pushed(&ctx, relay_domain).await {
1444 Ok(s) => s,
1445 Err(e) => {
1446 return TestResult::new(test_name, "GRASP-01", desc).fail(&e);
1447 }
1448 };
1449
1450 // Setup already pushed and verified success - just cleanup and report pass
1451 setup.cleanup();
1452
1453 TestResult::new(test_name, "GRASP-01", desc).pass()
1454 }
1455
1456 /// Test 2: After publishing PR event, verify that incorrect refs get cleaned up
1457 ///
1458 /// This test verifies the expected behavior: when a PR event is published,
1459 /// 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.
1461 ///
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)
1465 pub async fn test_pr_event_published_removes_nostr_ref_at_incorrect_commit(
1466 client: &AuditClient,
1467 relay_domain: &str,
1468 ) -> TestResult {
1469 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)";
1182 let ctx = TestContext::new(client); 1471 let ctx = TestContext::new(client);
1183 1472
1184 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { 1473 // Setup: wrong commit already pushed to refs/nostr/<pr-event-id>
1185 Ok(r) => r, 1474 let setup = match setup_repo_with_wrong_commit_pushed(&ctx, relay_domain).await {
1475 Ok(s) => s,
1186 Err(e) => { 1476 Err(e) => {
1187 return TestResult::new( 1477 return TestResult::new(test_name, "GRASP-01", desc).fail(&e);
1188 test_name,
1189 "GRASP-01",
1190 "Push to refs/nostr/<invalid-event-id> rejected",
1191 )
1192 .fail(&format!("Failed to create repo: {}", e));
1193 } 1478 }
1194 }; 1479 };
1195 1480
1196 tokio::time::sleep(std::time::Duration::from_millis(200)).await; 1481 // NOW publish the PR event - this should trigger cleanup validation
1482 if let Err(e) = publish_pr_event_and_wait(&ctx).await {
1483 setup.cleanup();
1484 return TestResult::new(test_name, "GRASP-01", desc).fail(&e);
1485 }
1197 1486
1198 let repo_id = repo 1487 // Check if the incorrect ref was deleted
1199 .tags 1488 let ref_name = format!("refs/nostr/{}", setup.pr_event_id);
1200 .iter() 1489 let refs_exist = match ref_exists_on_remote(&setup.clone_path, &ref_name) {
1201 .find(|t| t.kind() == TagKind::d()) 1490 Ok(exists) => exists,
1202 .and_then(|t| t.content()) 1491 Err(e) => {
1203 .unwrap() 1492 setup.cleanup();
1204 .to_string(); 1493 return TestResult::new(test_name, "GRASP-01", desc).fail(&e);
1205 let npub = repo.pubkey.to_bech32().unwrap(); 1494 }
1495 };
1206 1496
1207 // ============================================================ 1497 setup.cleanup();
1208 // Step 2: SEND - Clone repo, create commit, try push to invalid ref 1498
1209 // ============================================================ 1499 // Document current behavior: relay doesn't implement automatic cleanup yet
1210 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) { 1500 TestResult::new(test_name, "GRASP-01", desc).fail(&format!(
1211 Ok(p) => p, 1501 "NOT_IMPLEMENTED: Relay should delete refs/nostr/<event-id> when PR event is published \
1502 with non-matching commit. Currently ref still exists: {}. This requires relay-side validation logic.",
1503 refs_exist
1504 ))
1505 }
1506
1507 /// Test 3: Push wrong commit to refs/nostr/<pr-event-id> AFTER PR event exists
1508 ///
1509 /// This test verifies that the relay rejects pushes to refs/nostr/<event-id>
1510 /// when a corresponding event exists but the pushed commit doesn't match
1511 /// the commit in the PR event's `c` tag.
1512 ///
1513 /// Depends on: `setup_repo_with_wrong_commit_pushed` for repo/clone setup, then publishes PR event
1514 pub async fn test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected(
1515 client: &AuditClient,
1516 relay_domain: &str,
1517 ) -> TestResult {
1518 let test_name = "test_push_to_nostr_ref_with_wrong_commit_after_event_received_rejected";
1519 let desc = "Push wrong commit to refs/nostr/<pr-event-id> after PR event (should reject)";
1520 let ctx = TestContext::new(client);
1521
1522 // Setup: wrong commit already pushed (we'll use the same setup, but publish PR first)
1523 let setup = match setup_repo_with_wrong_commit_pushed(&ctx, relay_domain).await {
1524 Ok(s) => s,
1212 Err(e) => { 1525 Err(e) => {
1213 return TestResult::new( 1526 return TestResult::new(test_name, "GRASP-01", desc).fail(&e);
1214 test_name,
1215 "GRASP-01",
1216 "Push to refs/nostr/<invalid-event-id> rejected",
1217 )
1218 .fail(&e);
1219 } 1527 }
1220 }; 1528 };
1221 let cleanup = || { 1529
1222 let _ = fs::remove_dir_all(&clone_path); 1530 // Publish PR event FIRST (before our test push)
1531 if let Err(e) = publish_pr_event_and_wait(&ctx).await {
1532 setup.cleanup();
1533 return TestResult::new(test_name, "GRASP-01", desc).fail(&e);
1534 }
1535
1536 // Try to push again with wrong commit (should be rejected now that PR event exists)
1537 let push_succeeded = match push_to_pr_ref(&setup.clone_path, &setup.pr_event_id) {
1538 Ok(success) => success,
1539 Err(e) => {
1540 setup.cleanup();
1541 return TestResult::new(test_name, "GRASP-01", desc).fail(&e);
1542 }
1223 }; 1543 };
1224 1544
1225 // Create a unique commit 1545 setup.cleanup();
1226 if let Err(e) = create_commit(&clone_path, "Test commit for invalid refs/nostr push") { 1546
1227 cleanup(); 1547 // Should REJECT - PR event exists with different commit hash
1228 return TestResult::new( 1548 if push_succeeded {
1229 test_name, 1549 return TestResult::new(test_name, "GRASP-01", desc)
1230 "GRASP-01", 1550 .fail("Push accepted (expected rejection due to commit hash mismatch)");
1231 "Push to refs/nostr/<invalid-event-id> rejected",
1232 )
1233 .fail(&e);
1234 } 1551 }
1235 1552
1236 // Use an invalid event-id (too short, not a valid 64-char hex) 1553 TestResult::new(test_name, "GRASP-01", desc).pass()
1237 let invalid_event_id = "123"; 1554 }
1238 let ref_name = format!("refs/nostr/{}", invalid_event_id);
1239 1555
1240 // ============================================================ 1556 /// Test 4: Push correct commit to refs/nostr/<pr-event-id> AFTER PR event exists
1241 // Step 3: VERIFY - Push should be rejected with invalid event-id format 1557 ///
1242 // ============================================================ 1558 /// This test verifies that the relay accepts pushes to refs/nostr/<event-id>
1243 let push_result = try_push_to_ref(&clone_path, &ref_name); 1559 /// when a corresponding event exists AND the pushed commit matches
1244 cleanup(); 1560 /// the commit in the PR event's `c` tag.
1561 ///
1562 /// Depends on: `setup_repo_with_wrong_commit_pushed` for setup, then resets to correct commit
1563 pub async fn test_push_to_nostr_ref_with_correct_commit_after_event_received_accepted(
1564 client: &AuditClient,
1565 relay_domain: &str,
1566 ) -> TestResult {
1567 let test_name = "test_push_to_nostr_ref_with_correct_commit_after_event_received_accepted";
1568 let desc = "Push correct commit to refs/nostr/<pr-event-id> after PR event (should accept)";
1569 let ctx = TestContext::new(client);
1245 1570
1246 match push_result { 1571 // Setup: wrong commit already pushed
1247 Ok(false) => TestResult::new( 1572 let setup = match setup_repo_with_wrong_commit_pushed(&ctx, relay_domain).await {
1248 test_name, 1573 Ok(s) => s,
1249 "GRASP-01", 1574 Err(e) => {
1250 "Push to refs/nostr/<invalid-event-id> rejected", 1575 return TestResult::new(test_name, "GRASP-01", desc).fail(&e);
1251 ) 1576 }
1252 .pass(), 1577 };
1253 Ok(true) => TestResult::new( 1578
1254 test_name, 1579 // Publish PR event FIRST
1255 "GRASP-01", 1580 if let Err(e) = publish_pr_event_and_wait(&ctx).await {
1256 "Push to refs/nostr/<invalid-event-id> rejected", 1581 setup.cleanup();
1257 ) 1582 return TestResult::new(test_name, "GRASP-01", desc).fail(&e);
1258 .fail(&format!( 1583 }
1259 "Push to {} was accepted but should be rejected. \ 1584
1260 The event-id '{}' is NOT a valid 64-character hex string (EventId format). \ 1585 // Reset to CORRECT commit (the one expected by PR event)
1261 The relay should reject pushes to refs/nostr/ with invalid event-id format.", 1586 if let Err(e) = reset_to_correct_pr_commit(&setup.clone_path) {
1262 ref_name, invalid_event_id 1587 setup.cleanup();
1263 )), 1588 return TestResult::new(test_name, "GRASP-01", desc).fail(&e);
1264 Err(e) => TestResult::new( 1589 }
1265 test_name, 1590
1266 "GRASP-01", 1591 // Push correct commit (should succeed)
1267 "Push to refs/nostr/<invalid-event-id> rejected", 1592 let push_succeeded = match push_to_pr_ref(&setup.clone_path, &setup.pr_event_id) {
1268 ) 1593 Ok(success) => success,
1269 .fail(&format!("Push error: {}", e)), 1594 Err(e) => {
1595 setup.cleanup();
1596 return TestResult::new(test_name, "GRASP-01", desc).fail(&e);
1597 }
1598 };
1599
1600 setup.cleanup();
1601
1602 // Should ACCEPT - commit matches PR event's c tag
1603 if !push_succeeded {
1604 return TestResult::new(test_name, "GRASP-01", desc)
1605 .fail("Push rejected (expected acceptance since commit matches PR event)");
1270 } 1606 }
1607
1608 TestResult::new(test_name, "GRASP-01", desc).pass()
1271 } 1609 }
1272} 1610}
1273 1611
1274#[cfg(test)] 1612#[cfg(test)]
1275mod tests { 1613mod tests {
1614 use super::*;
1615
1276 #[test] 1616 #[test]
1277 fn test_module_exists() { 1617 fn test_module_exists() {
1278 assert!(true); 1618 assert!(true);
1279 } 1619 }
1620
1621 /// Test to discover the PR test commit hash
1622 ///
1623 /// This test creates a deterministic commit with PR-specific parameters
1624 /// and prints out the hash value. Once discovered, update PR_TEST_COMMIT_HASH.
1625 ///
1626 /// Run with: cd grasp-audit && nix develop -c cargo test --lib test_pr_test_commit_hash_discovery -- --nocapture
1627 #[test]
1628 fn test_pr_test_commit_hash_discovery() {
1629 use std::process::Command;
1630 use tempfile::TempDir;
1631 use std::fs;
1632
1633 let temp_dir = TempDir::new().expect("Failed to create temp dir");
1634 let path = temp_dir.path();
1635
1636 // Initialize git repo
1637 let output = Command::new("git")
1638 .args(["init"])
1639 .current_dir(path)
1640 .output()
1641 .expect("Failed to init git");
1642 assert!(output.status.success(), "git init failed: {:?}", String::from_utf8_lossy(&output.stderr));
1643
1644 // Configure git user - use PR Test Author identity
1645 let output = Command::new("git")
1646 .args(["config", "user.email", "pr-test@example.com"])
1647 .current_dir(path)
1648 .output()
1649 .expect("git config email failed");
1650 assert!(output.status.success(), "git config email failed");
1651
1652 let output = Command::new("git")
1653 .args(["config", "user.name", "PR Test Author"])
1654 .current_dir(path)
1655 .output()
1656 .expect("git config name failed");
1657 assert!(output.status.success(), "git config name failed");
1658
1659 // Create the deterministic file content
1660 let test_file = path.join("test.txt");
1661 fs::write(&test_file, "PR test deterministic commit").expect("Failed to write test file");
1662
1663 // Add the file
1664 let output = Command::new("git")
1665 .args(["add", "test.txt"])
1666 .current_dir(path)
1667 .output()
1668 .expect("git add failed");
1669 assert!(output.status.success(), "git add failed: {:?}", String::from_utf8_lossy(&output.stderr));
1670
1671 // Create deterministic commit with fixed dates and GPG disabled
1672 let output = Command::new("git")
1673 .args([
1674 "-c", "commit.gpgsign=false",
1675 "commit",
1676 "-m", "PR test deterministic commit",
1677 ])
1678 .env("GIT_AUTHOR_DATE", "2024-01-01T00:00:00Z")
1679 .env("GIT_COMMITTER_DATE", "2024-01-01T00:00:00Z")
1680 .current_dir(path)
1681 .output()
1682 .expect("git commit failed");
1683 assert!(output.status.success(), "git commit failed: {:?}", String::from_utf8_lossy(&output.stderr));
1684
1685 // Get the commit hash
1686 let output = Command::new("git")
1687 .args(["rev-parse", "HEAD"])
1688 .current_dir(path)
1689 .output()
1690 .expect("git rev-parse failed");
1691 assert!(output.status.success(), "git rev-parse failed: {:?}", String::from_utf8_lossy(&output.stderr));
1692
1693 let hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
1694
1695 println!("\n========================================");
1696 println!("PR_TEST_COMMIT_HASH should be: {}", hash);
1697 println!("========================================\n");
1698
1699 // Verify we got a valid 40-character hex hash
1700 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");
1702
1703 // If the constant is not PLACEHOLDER, verify it matches
1704 if PR_TEST_COMMIT_HASH != "PLACEHOLDER" {
1705 assert_eq!(
1706 hash, PR_TEST_COMMIT_HASH,
1707 "Commit hash mismatch! Expected {}, got {}. Update PR_TEST_COMMIT_HASH if commit parameters changed.",
1708 PR_TEST_COMMIT_HASH, hash
1709 );
1710 }
1711 }
1280} \ No newline at end of file 1712} \ No newline at end of file