diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-24 14:15:04 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-24 14:15:04 +0000 |
| commit | 7f71a2e75a66bcacad9057f5e339e511e689b828 (patch) | |
| tree | b1bc6e9d2df28f9b740cde37f836c76bc1bb1c8e /grasp-audit/src/fixtures.rs | |
| parent | ef279a881fc1694fe2d868a32224874eb50cd358 (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.rs | 203 |
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 |