upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/fixtures.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-24 14:15:04 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-24 14:15:04 +0000
commit7f71a2e75a66bcacad9057f5e339e511e689b828 (patch)
treeb1bc6e9d2df28f9b740cde37f836c76bc1bb1c8e /grasp-audit/src/fixtures.rs
parentef279a881fc1694fe2d868a32224874eb50cd358 (diff)
fix grasp-audit test isolation to prevent cross-spec relay state corruption
Add Purgatory-prefixed fixture variants (PurgatoryValidRepoSent, PurgatoryOwnerStateDataPushed) that create independent repos never shared with the main fixture chain. Purgatory tests that mutate relay state (replacement announcements, new state events, deletions) now use these isolated fixtures so they cannot corrupt the repo that push-authorization tests depend on. Run purgatory tests before push-auth in the full suite, since push-auth sends new replaceable state events (kind 30618) for the shared repo_id that would displace the original served state event.
Diffstat (limited to 'grasp-audit/src/fixtures.rs')
-rw-r--r--grasp-audit/src/fixtures.rs203
1 files changed, 203 insertions, 0 deletions
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs
index 45d3094..0a9bf65 100644
--- a/grasp-audit/src/fixtures.rs
+++ b/grasp-audit/src/fixtures.rs
@@ -287,6 +287,36 @@ pub enum FixtureKind {
287 /// - Returns: the served PR event 287 /// - Returns: the served PR event
288 PREvent2Served, 288 PREvent2Served,
289 289
290 /// Independent repo announcement, used exclusively by purgatory tests.
291 ///
292 /// Creates its own fresh repo announcement (unique repo_id) that is NOT shared with
293 /// the main ValidRepoSent chain. The shared ValidRepoSent may already be promoted
294 /// (served) by the time purgatory tests run if earlier specs triggered OwnerStateDataPushed.
295 /// This fixture is never promoted by any other test, so the announcement stays in purgatory.
296 ///
297 /// - No dependencies
298 /// - Sends its own announcement to the relay
299 /// - Returns the repo announcement event (kind 30617)
300 PurgatoryValidRepoSent,
301
302 /// Independent owner state data pushed, used exclusively by purgatory tests.
303 ///
304 /// This fixture creates its own completely independent repo (fresh UUID, own announcement,
305 /// own state event, own git push) that is NOT shared with the main OwnerStateDataPushed
306 /// chain. It exists so that purgatory tests which mutate relay state (sending replacement
307 /// announcements, new state events pointing to non-existent commits, etc.) do not corrupt
308 /// the shared repo that push-authorization tests depend on.
309 ///
310 /// Stages (self-contained, no external dependencies):
311 /// 1. Creates a fresh repo announcement with a unique repo_id
312 /// 2. Creates and sends an owner state event (purgatory)
313 /// 3. Pushes git data (DETERMINISTIC_COMMIT_HASH) to release from purgatory
314 /// 4. Verifies state event is served
315 ///
316 /// - No dependencies (creates its own ValidRepoSent + OwnerStateDataPushed internally)
317 /// - Returns the owner state event (kind 30618) after git data is pushed
318 PurgatoryOwnerStateDataPushed,
319
290 /// Owner's state event with git data successfully pushed (full 4-stage fixture) 320 /// Owner's state event with git data successfully pushed (full 4-stage fixture)
291 /// 321 ///
292 /// This fixture represents the complete flow for testing state push authorization: 322 /// This fixture represents the complete flow for testing state push authorization:
@@ -372,6 +402,12 @@ impl FixtureKind {
372 Self::PREvent2GitDataPushed => vec![Self::PREvent2Sent], 402 Self::PREvent2GitDataPushed => vec![Self::PREvent2Sent],
373 Self::PREvent2Served => vec![Self::PREvent2GitDataPushed], 403 Self::PREvent2Served => vec![Self::PREvent2GitDataPushed],
374 404
405 // PurgatoryValidRepoSent has no dependencies — creates its own fresh repo
406 Self::PurgatoryValidRepoSent => vec![],
407
408 // PurgatoryOwnerStateDataPushed depends on PurgatoryValidRepoSent
409 Self::PurgatoryOwnerStateDataPushed => vec![Self::PurgatoryValidRepoSent],
410
375 // OwnerStateDataPushed depends on OwnerRepoStateSent (git push + purgatory release) 411 // OwnerStateDataPushed depends on OwnerRepoStateSent (git push + purgatory release)
376 Self::OwnerStateDataPushed => vec![Self::OwnerRepoStateSent], 412 Self::OwnerStateDataPushed => vec![Self::OwnerRepoStateSent],
377 413
@@ -416,6 +452,10 @@ impl FixtureKind {
416 Self::PREvent2Served => true, 452 Self::PREvent2Served => true,
417 // HeadSetToDevelopBranch sends its state event internally 453 // HeadSetToDevelopBranch sends its state event internally
418 Self::HeadSetToDevelopBranch => true, 454 Self::HeadSetToDevelopBranch => true,
455 // PurgatoryValidRepoSent sends its own announcement internally
456 Self::PurgatoryValidRepoSent => true,
457 // PurgatoryOwnerStateDataPushed sends its own state event and git push internally
458 Self::PurgatoryOwnerStateDataPushed => true,
419 // ValidRepoServed doesn't send anything itself, just returns cached event 459 // ValidRepoServed doesn't send anything itself, just returns cached event
420 Self::ValidRepoServed => true, 460 Self::ValidRepoServed => true,
421 // OwnerRepoStateSent sends its state event and notes purgatory internally 461 // OwnerRepoStateSent sends its state event and notes purgatory internally
@@ -926,6 +966,9 @@ impl<'a> TestContext<'a> {
926 FixtureKind::PREvent2GitDataPushed => self.build_pr_event_2_git_data_pushed().await, 966 FixtureKind::PREvent2GitDataPushed => self.build_pr_event_2_git_data_pushed().await,
927 FixtureKind::PREvent2Served => self.build_pr_event_2_served().await, 967 FixtureKind::PREvent2Served => self.build_pr_event_2_served().await,
928 968
969 FixtureKind::PurgatoryValidRepoSent => self.build_purgatory_valid_repo_sent().await,
970 FixtureKind::PurgatoryOwnerStateDataPushed => self.build_purgatory_owner_state_data_pushed().await,
971
929 FixtureKind::OwnerStateDataPushed => self.build_owner_state_data_pushed().await, 972 FixtureKind::OwnerStateDataPushed => self.build_owner_state_data_pushed().await,
930 973
931 FixtureKind::MaintainerStateDataPushed => { 974 FixtureKind::MaintainerStateDataPushed => {
@@ -1000,6 +1043,166 @@ impl<'a> TestContext<'a> {
1000 .ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement")) 1043 .ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement"))
1001 } 1044 }
1002 1045
1046 /// Build PurgatoryValidRepoSent fixture: independent repo announcement for purgatory tests.
1047 ///
1048 /// Creates a fresh repo announcement with a unique repo_id, sends it to the relay,
1049 /// and returns it. Never promoted by any other test so the announcement stays in purgatory.
1050 async fn build_purgatory_valid_repo_sent(&self) -> Result<Event> {
1051 use nostr_sdk::prelude::*;
1052
1053 let repo_id = format!(
1054 "fixture-PurgatoryValidRepoSent-{}",
1055 &uuid::Uuid::new_v4().to_string()[..8]
1056 );
1057
1058 let relay_domain = self.get_relay_domain().await?;
1059 let relay_url = format!("ws://{}", relay_domain);
1060 let http_url = format!("http://{}", relay_domain);
1061
1062 let npub = self
1063 .client
1064 .public_key()
1065 .to_bech32()
1066 .map_err(|e| anyhow::anyhow!("Failed to convert pubkey to bech32: {}", e))?;
1067
1068 let announcement = self
1069 .client
1070 .event_builder(Kind::GitRepoAnnouncement, "")
1071 .tag(Tag::identifier(&repo_id))
1072 .tag(Tag::custom(TagKind::custom("name"), vec![repo_id.clone()]))
1073 .tag(Tag::custom(
1074 TagKind::custom("clone"),
1075 vec![format!("{}/{}/{}.git", http_url, npub, repo_id)],
1076 ))
1077 .tag(Tag::custom(TagKind::custom("relays"), vec![relay_url]))
1078 .build(self.client.keys())
1079 .map_err(|e| anyhow::anyhow!("Failed to build repo announcement: {}", e))?;
1080
1081 self.client.send_event(announcement.clone()).await?;
1082
1083 Ok(announcement)
1084 }
1085
1086 /// Build PurgatoryOwnerStateDataPushed fixture: a self-contained independent repo for purgatory tests.
1087 ///
1088 /// Creates its own fresh repo announcement (unique repo_id), state event, and git push
1089 /// without touching the shared OwnerStateDataPushed chain. This ensures that purgatory
1090 /// tests which mutate relay state (replacement announcements, new state events, deletions)
1091 /// do not corrupt the repo that push-authorization tests depend on.
1092 async fn build_purgatory_owner_state_data_pushed(&self) -> Result<Event> {
1093 use nostr_sdk::prelude::*;
1094
1095 // ============================================================
1096 // Step 1: Get the cached PurgatoryValidRepoSent announcement
1097 // (ensured as a dependency before this is called)
1098 // ============================================================
1099 let announcement = self.get_cached_dependency(FixtureKind::PurgatoryValidRepoSent)?;
1100 let repo_id = self.extract_repo_id(&announcement)?;
1101
1102 let relay_domain = self.get_relay_domain().await?;
1103
1104 let npub = self
1105 .client
1106 .public_key()
1107 .to_bech32()
1108 .map_err(|e| anyhow::anyhow!("Failed to convert pubkey to bech32: {}", e))?;
1109
1110 // ============================================================
1111 // Step 2: Create and send owner state event (enters purgatory)
1112 // ============================================================
1113 let base_time = Timestamp::now().as_secs();
1114 let older_timestamp = Timestamp::from(base_time - 10);
1115
1116 let state_event = self
1117 .client
1118 .event_builder(Kind::RepoState, "")
1119 .tag(Tag::identifier(&repo_id))
1120 .tag(Tag::custom(
1121 TagKind::custom("refs/heads/main"),
1122 vec![DETERMINISTIC_COMMIT_HASH.to_string()],
1123 ))
1124 .tag(Tag::custom(
1125 TagKind::custom("HEAD"),
1126 vec!["ref: refs/heads/main".to_string()],
1127 ))
1128 .custom_time(older_timestamp)
1129 .build(self.client.keys())
1130 .map_err(|e| anyhow::anyhow!("Failed to build state event: {}", e))?;
1131
1132 self.client
1133 .send_event_and_note_purgatory(state_event.clone())
1134 .await?;
1135
1136 // ============================================================
1137 // Step 3: Clone repo, create deterministic commit, push
1138 // ============================================================
1139 let clone_path = clone_repo(&relay_domain, &npub, &repo_id)
1140 .map_err(|e| anyhow::anyhow!("Failed to clone repo: {}", e))?;
1141
1142 let cleanup = |path: &PathBuf| {
1143 let _ = fs::remove_dir_all(path);
1144 };
1145
1146 let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") {
1147 Ok(h) => h,
1148 Err(e) => {
1149 cleanup(&clone_path);
1150 return Err(anyhow::anyhow!("Failed to create deterministic commit: {}", e));
1151 }
1152 };
1153
1154 if commit_hash != DETERMINISTIC_COMMIT_HASH {
1155 cleanup(&clone_path);
1156 return Err(anyhow::anyhow!(
1157 "Commit hash mismatch: got {}, expected {}",
1158 commit_hash,
1159 DETERMINISTIC_COMMIT_HASH
1160 ));
1161 }
1162
1163 let branch_out = Command::new("git")
1164 .args(["branch", "main"])
1165 .current_dir(&clone_path)
1166 .output();
1167 if let Ok(o) = &branch_out {
1168 if !o.status.success() {
1169 // branch may already exist (detached HEAD clone) — ignore
1170 }
1171 }
1172
1173 let _ = Command::new("git")
1174 .args(["checkout", "main"])
1175 .current_dir(&clone_path)
1176 .output();
1177
1178 let push_result = try_push(&clone_path);
1179 cleanup(&clone_path);
1180
1181 match push_result {
1182 Ok(true) => {}
1183 Ok(false) => {
1184 return Err(anyhow::anyhow!(
1185 "PurgatoryOwnerStateDataPushed git push rejected (state event points to {})",
1186 DETERMINISTIC_COMMIT_HASH
1187 ));
1188 }
1189 Err(e) => return Err(anyhow::anyhow!("PurgatoryOwnerStateDataPushed push error: {}", e)),
1190 }
1191
1192 // ============================================================
1193 // Step 4: Verify state event released from purgatory
1194 // ============================================================
1195 tokio::time::sleep(Duration::from_millis(200)).await;
1196
1197 if !self.client.is_event_on_relay(state_event.id).await? {
1198 return Err(anyhow::anyhow!(
1199 "PurgatoryOwnerStateDataPushed state event not released from purgatory"
1200 ));
1201 }
1202
1203 Ok(state_event)
1204 }
1205
1003 /// Build OwnerStateDataPushed fixture: git push + purgatory release for owner's state event 1206 /// Build OwnerStateDataPushed fixture: git push + purgatory release for owner's state event
1004 /// 1207 ///
1005 /// `OwnerRepoStateSent` is ensured as a dependency before this is called — the state event 1208 /// `OwnerRepoStateSent` is ensured as a dependency before this is called — the state event