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:
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