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 | |
| 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')
| -rw-r--r-- | grasp-audit/src/bin/grasp-audit.rs | 55 | ||||
| -rw-r--r-- | grasp-audit/src/fixtures.rs | 203 | ||||
| -rw-r--r-- | grasp-audit/src/specs/grasp01/purgatory.rs | 107 |
3 files changed, 285 insertions, 80 deletions
diff --git a/grasp-audit/src/bin/grasp-audit.rs b/grasp-audit/src/bin/grasp-audit.rs index b3fa0db..d192f04 100644 --- a/grasp-audit/src/bin/grasp-audit.rs +++ b/grasp-audit/src/bin/grasp-audit.rs | |||
| @@ -144,51 +144,56 @@ async fn main() -> Result<()> { | |||
| 144 | println!("Running all tests...\n"); | 144 | println!("Running all tests...\n"); |
| 145 | let mut all_results = AuditResult::new("All GRASP-01 Tests"); | 145 | let mut all_results = AuditResult::new("All GRASP-01 Tests"); |
| 146 | 146 | ||
| 147 | // Repository creation tests | 147 | // NIP-01 smoke tests (stateless - no shared fixture dependencies) |
| 148 | println!(" → NIP-01 smoke tests..."); | ||
| 149 | let nip01_results = specs::Nip01SmokeTests::run_all(&client).await; | ||
| 150 | all_results.merge(nip01_results); | ||
| 151 | |||
| 152 | // NIP-11 document tests (stateless) | ||
| 153 | println!(" → NIP-11 document tests..."); | ||
| 154 | let nip11_results = specs::Nip11DocumentTests::run_all(&client).await; | ||
| 155 | all_results.merge(nip11_results); | ||
| 156 | |||
| 157 | // CORS tests (stateless HTTP checks) | ||
| 158 | println!(" → CORS tests..."); | ||
| 159 | let cors_results = specs::CorsTests::run_all(&client, &relay_domain).await; | ||
| 160 | all_results.merge(cors_results); | ||
| 161 | |||
| 162 | // Repository creation tests (uses ValidRepoSent only - no state events) | ||
| 148 | println!(" → Repository creation tests..."); | 163 | println!(" → Repository creation tests..."); |
| 149 | let repo_results = specs::RepositoryCreationTests::run_all(&client, &relay_domain).await; | 164 | let repo_results = specs::RepositoryCreationTests::run_all(&client, &relay_domain).await; |
| 150 | all_results.merge(repo_results); | 165 | all_results.merge(repo_results); |
| 151 | 166 | ||
| 152 | // Git clone tests | 167 | // Git clone tests (uses ValidRepoSent only - no state events) |
| 153 | println!(" → Git clone tests..."); | 168 | println!(" → Git clone tests..."); |
| 154 | let clone_results = specs::GitCloneTests::run_all(&client, &relay_domain).await; | 169 | let clone_results = specs::GitCloneTests::run_all(&client, &relay_domain).await; |
| 155 | all_results.merge(clone_results); | 170 | all_results.merge(clone_results); |
| 156 | 171 | ||
| 157 | // Git filter capability tests | 172 | // Git filter capability tests (uses ValidRepoSent only - no state events) |
| 158 | println!(" → Git filter capability tests..."); | 173 | println!(" → Git filter capability tests..."); |
| 159 | let filter_results = specs::GitFilterTests::run_all(&client, &relay_domain).await; | 174 | let filter_results = specs::GitFilterTests::run_all(&client, &relay_domain).await; |
| 160 | all_results.merge(filter_results); | 175 | all_results.merge(filter_results); |
| 161 | 176 | ||
| 162 | // Push authorization tests | 177 | // Event acceptance policy tests (uses ValidRepoServed - no extra state events) |
| 163 | println!(" → Push authorization tests..."); | ||
| 164 | let push_results = specs::PushAuthorizationTests::run_all(&client, &relay_domain).await; | ||
| 165 | all_results.merge(push_results); | ||
| 166 | |||
| 167 | // Event acceptance policy tests | ||
| 168 | println!(" → Event acceptance policy tests..."); | 178 | println!(" → Event acceptance policy tests..."); |
| 169 | let event_results = specs::EventAcceptancePolicyTests::run_all(&client).await; | 179 | let event_results = specs::EventAcceptancePolicyTests::run_all(&client).await; |
| 170 | all_results.merge(event_results); | 180 | all_results.merge(event_results); |
| 171 | 181 | ||
| 172 | // NIP-01 smoke tests | 182 | // Purgatory tests MUST run before push-auth. |
| 173 | println!(" → NIP-01 smoke tests..."); | 183 | // Push-auth sends new replaceable state events (kind 30618) for the same |
| 174 | let nip01_results = specs::Nip01SmokeTests::run_all(&client).await; | 184 | // repo_id as OwnerStateDataPushed (e.g. test_head_set_after_git_push_with_required_oids |
| 175 | all_results.merge(nip01_results); | 185 | // sends a develop1 state event that displaces the original). If purgatory ran |
| 176 | 186 | // after push-auth, is_event_on_relay(original_id) would return false because | |
| 177 | // NIP-11 document tests | 187 | // the original state event has been replaced on the relay. |
| 178 | println!(" → NIP-11 document tests..."); | ||
| 179 | let nip11_results = specs::Nip11DocumentTests::run_all(&client).await; | ||
| 180 | all_results.merge(nip11_results); | ||
| 181 | |||
| 182 | // CORS tests | ||
| 183 | println!(" → CORS tests..."); | ||
| 184 | let cors_results = specs::CorsTests::run_all(&client, &relay_domain).await; | ||
| 185 | all_results.merge(cors_results); | ||
| 186 | |||
| 187 | // Purgatory tests | ||
| 188 | println!(" → Purgatory tests..."); | 188 | println!(" → Purgatory tests..."); |
| 189 | let purgatory_results = specs::PurgatoryTests::run_all(&client).await; | 189 | let purgatory_results = specs::PurgatoryTests::run_all(&client).await; |
| 190 | all_results.merge(purgatory_results); | 190 | all_results.merge(purgatory_results); |
| 191 | 191 | ||
| 192 | // Push authorization tests (mutates shared state - must run last among git specs) | ||
| 193 | println!(" → Push authorization tests..."); | ||
| 194 | let push_results = specs::PushAuthorizationTests::run_all(&client, &relay_domain).await; | ||
| 195 | all_results.merge(push_results); | ||
| 196 | |||
| 192 | println!(); | 197 | println!(); |
| 193 | all_results | 198 | all_results |
| 194 | } | 199 | } |
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 |
diff --git a/grasp-audit/src/specs/grasp01/purgatory.rs b/grasp-audit/src/specs/grasp01/purgatory.rs index 29eabad..0686da8 100644 --- a/grasp-audit/src/specs/grasp01/purgatory.rs +++ b/grasp-audit/src/specs/grasp01/purgatory.rs | |||
| @@ -46,7 +46,11 @@ impl PurgatoryTests { | |||
| 46 | results.add(Self::test_announcement_not_served_before_git_data(client).await); | 46 | results.add(Self::test_announcement_not_served_before_git_data(client).await); |
| 47 | results.add(Self::test_announcement_served_after_git_push(client).await); | 47 | results.add(Self::test_announcement_served_after_git_push(client).await); |
| 48 | results.add(Self::test_bare_repo_exists_for_purgatory_announcement(client).await); | 48 | results.add(Self::test_bare_repo_exists_for_purgatory_announcement(client).await); |
| 49 | |||
| 50 | // State event purgatory tests | ||
| 49 | results.add(Self::test_state_event_accepted_for_purgatory_announcement(client).await); | 51 | results.add(Self::test_state_event_accepted_for_purgatory_announcement(client).await); |
| 52 | results.add(Self::test_state_event_not_served_before_git_data(client).await); | ||
| 53 | results.add(Self::test_state_event_served_after_git_push(client).await); | ||
| 50 | 54 | ||
| 51 | // Deletion event tests (NIP-09) | 55 | // Deletion event tests (NIP-09) |
| 52 | results.add(Self::test_deletion_by_event_id_removes_purgatory_state_event(client).await); | 56 | results.add(Self::test_deletion_by_event_id_removes_purgatory_state_event(client).await); |
| @@ -54,10 +58,6 @@ impl PurgatoryTests { | |||
| 54 | Self::test_deletion_by_coordinate_removes_purgatory_state_event(client).await, | 58 | Self::test_deletion_by_coordinate_removes_purgatory_state_event(client).await, |
| 55 | ); | 59 | ); |
| 56 | 60 | ||
| 57 | // State event purgatory tests (already implemented) | ||
| 58 | results.add(Self::test_state_event_not_served_before_git_data(client).await); | ||
| 59 | results.add(Self::test_state_event_served_after_git_push(client).await); | ||
| 60 | |||
| 61 | // PR purgatory tests | 61 | // PR purgatory tests |
| 62 | results.add(Self::test_pr_event_accepted_into_purgatory_and_isnt_served(client).await); | 62 | results.add(Self::test_pr_event_accepted_into_purgatory_and_isnt_served(client).await); |
| 63 | results.add(Self::test_pr_event_in_purgatory_git_push_accepted(client).await); | 63 | results.add(Self::test_pr_event_in_purgatory_git_push_accepted(client).await); |
| @@ -92,9 +92,12 @@ impl PurgatoryTests { | |||
| 92 | .run(|| async { | 92 | .run(|| async { |
| 93 | let ctx = TestContext::new(client); | 93 | let ctx = TestContext::new(client); |
| 94 | 94 | ||
| 95 | // Create a fresh repo announcement (not the served variant) | 95 | // Use the purgatory-specific fixture which creates its own independent repo. |
| 96 | // The shared ValidRepoSent may already be promoted (served) by the time this | ||
| 97 | // test runs if earlier specs triggered OwnerStateDataPushed. PurgatoryValidRepoSent | ||
| 98 | // is never promoted by any other test so the announcement stays in purgatory. | ||
| 96 | let repo = ctx | 99 | let repo = ctx |
| 97 | .get_fixture(FixtureKind::ValidRepoSent) | 100 | .get_fixture(FixtureKind::PurgatoryValidRepoSent) |
| 98 | .await | 101 | .await |
| 99 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; | 102 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; |
| 100 | 103 | ||
| @@ -106,7 +109,7 @@ impl PurgatoryTests { | |||
| 106 | .ok_or("Missing d tag in repo announcement")? | 109 | .ok_or("Missing d tag in repo announcement")? |
| 107 | .to_string(); | 110 | .to_string(); |
| 108 | 111 | ||
| 109 | // Query for the announcement - should NOT be served | 112 | // Query for the announcement - should NOT be served (purgatory) |
| 110 | let filter = Filter::new() | 113 | let filter = Filter::new() |
| 111 | .kind(Kind::GitRepoAnnouncement) | 114 | .kind(Kind::GitRepoAnnouncement) |
| 112 | .author(client.public_key()) | 115 | .author(client.public_key()) |
| @@ -153,13 +156,13 @@ impl PurgatoryTests { | |||
| 153 | .run(|| async { | 156 | .run(|| async { |
| 154 | let ctx = TestContext::new(client); | 157 | let ctx = TestContext::new(client); |
| 155 | 158 | ||
| 156 | // OwnerStateDataPushed fixture handles the full lifecycle: | 159 | // PurgatoryOwnerStateDataPushed fixture handles the full lifecycle: |
| 157 | // 1. Creates repo announcement (purgatory) | 160 | // 1. Creates repo announcement (purgatory) |
| 158 | // 2. Creates state event (purgatory) | 161 | // 2. Creates state event (purgatory) |
| 159 | // 3. Pushes git data | 162 | // 3. Pushes git data |
| 160 | // 4. Verifies events are served | 163 | // 4. Verifies events are served |
| 161 | let state_event = ctx | 164 | let state_event = ctx |
| 162 | .get_fixture(FixtureKind::OwnerStateDataPushed) | 165 | .get_fixture(FixtureKind::PurgatoryOwnerStateDataPushed) |
| 163 | .await | 166 | .await |
| 164 | .map_err(|e| format!("Failed to complete full lifecycle: {}", e))?; | 167 | .map_err(|e| format!("Failed to complete full lifecycle: {}", e))?; |
| 165 | 168 | ||
| @@ -190,18 +193,16 @@ impl PurgatoryTests { | |||
| 190 | )); | 193 | )); |
| 191 | } | 194 | } |
| 192 | 195 | ||
| 193 | // Verify state event is served | 196 | // Verify state event is served by querying its specific event ID. |
| 194 | let state_filter = Filter::new() | 197 | // We intentionally query by ID rather than kind+author+identifier because |
| 195 | .kind(Kind::RepoState) | 198 | // other tests (e.g. push-auth) may have sent a newer replaceable state event |
| 196 | .author(client.public_key()) | 199 | // for the same repo_id, which would displace this one in an identifier query. |
| 197 | .identifier(&repo_id); | 200 | let served = client |
| 198 | 201 | .is_event_on_relay(state_event.id) | |
| 199 | let state_events = client | ||
| 200 | .query(state_filter) | ||
| 201 | .await | 202 | .await |
| 202 | .map_err(|e| format!("Failed to query state events: {}", e))?; | 203 | .map_err(|e| format!("Failed to query state event: {}", e))?; |
| 203 | 204 | ||
| 204 | if !state_events.iter().any(|e| e.id == state_event.id) { | 205 | if !served { |
| 205 | return Err(format!( | 206 | return Err(format!( |
| 206 | "State event not served after git push. Event ID: {}", | 207 | "State event not served after git push. Event ID: {}", |
| 207 | state_event.id | 208 | state_event.id |
| @@ -234,9 +235,9 @@ impl PurgatoryTests { | |||
| 234 | .run(|| async { | 235 | .run(|| async { |
| 235 | let ctx = TestContext::new(client); | 236 | let ctx = TestContext::new(client); |
| 236 | 237 | ||
| 237 | // Get a repo announcement (in purgatory, no git data yet) | 238 | // Get the purgatory-specific repo announcement (never promoted by other tests) |
| 238 | let repo = ctx | 239 | let repo = ctx |
| 239 | .get_fixture(FixtureKind::ValidRepoSent) | 240 | .get_fixture(FixtureKind::PurgatoryValidRepoSent) |
| 240 | .await | 241 | .await |
| 241 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; | 242 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; |
| 242 | 243 | ||
| @@ -314,9 +315,9 @@ impl PurgatoryTests { | |||
| 314 | .run(|| async { | 315 | .run(|| async { |
| 315 | let ctx = TestContext::new(client); | 316 | let ctx = TestContext::new(client); |
| 316 | 317 | ||
| 317 | // Get a repo announcement (in purgatory) | 318 | // Get the purgatory-specific repo announcement (never promoted by other tests) |
| 318 | let repo = ctx | 319 | let repo = ctx |
| 319 | .get_fixture(FixtureKind::ValidRepoSent) | 320 | .get_fixture(FixtureKind::PurgatoryValidRepoSent) |
| 320 | .await | 321 | .await |
| 321 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; | 322 | .map_err(|e| format!("Failed to create repo announcement: {}", e))?; |
| 322 | 323 | ||
| @@ -407,11 +408,13 @@ impl PurgatoryTests { | |||
| 407 | .run(|| async { | 408 | .run(|| async { |
| 408 | let ctx = TestContext::new(client); | 409 | let ctx = TestContext::new(client); |
| 409 | 410 | ||
| 410 | // Get a repo with git data already pushed | 411 | // Use the isolated purgatory repo so this test's new state event |
| 412 | // does not displace the shared OwnerStateDataPushed state event | ||
| 413 | // that push-authorization tests depend on. | ||
| 411 | let existing_state = ctx | 414 | let existing_state = ctx |
| 412 | .get_fixture(FixtureKind::OwnerStateDataPushed) | 415 | .get_fixture(FixtureKind::PurgatoryOwnerStateDataPushed) |
| 413 | .await | 416 | .await |
| 414 | .map_err(|e| format!("Failed to get existing repo: {}", e))?; | 417 | .map_err(|e| format!("Failed to get purgatory test repo: {}", e))?; |
| 415 | 418 | ||
| 416 | let repo_id = existing_state | 419 | let repo_id = existing_state |
| 417 | .tags | 420 | .tags |
| @@ -461,7 +464,7 @@ impl PurgatoryTests { | |||
| 461 | /// Spec: GRASP-01 Line 22 | 464 | /// Spec: GRASP-01 Line 22 |
| 462 | /// "...kept in purgatory (not served) until the related git data arrives" | 465 | /// "...kept in purgatory (not served) until the related git data arrives" |
| 463 | /// | 466 | /// |
| 464 | /// This test verifies the full lifecycle using OwnerStateDataPushed fixture: | 467 | /// This test verifies the full lifecycle using PurgatoryOwnerStateDataPushed fixture: |
| 465 | /// 1. State event is sent (enters purgatory) | 468 | /// 1. State event is sent (enters purgatory) |
| 466 | /// 2. Git data is pushed matching the state event | 469 | /// 2. Git data is pushed matching the state event |
| 467 | /// 3. State event is now served | 470 | /// 3. State event is now served |
| @@ -474,32 +477,22 @@ impl PurgatoryTests { | |||
| 474 | .run(|| async { | 477 | .run(|| async { |
| 475 | let ctx = TestContext::new(client); | 478 | let ctx = TestContext::new(client); |
| 476 | 479 | ||
| 477 | // OwnerStateDataPushed handles the full lifecycle | 480 | // PurgatoryOwnerStateDataPushed handles the full lifecycle |
| 478 | let state_event = ctx | 481 | let state_event = ctx |
| 479 | .get_fixture(FixtureKind::OwnerStateDataPushed) | 482 | .get_fixture(FixtureKind::PurgatoryOwnerStateDataPushed) |
| 480 | .await | 483 | .await |
| 481 | .map_err(|e| format!("Failed to complete full lifecycle: {}", e))?; | 484 | .map_err(|e| format!("Failed to complete full lifecycle: {}", e))?; |
| 482 | 485 | ||
| 483 | // Verify state event is now served | 486 | // Verify state event is served by querying its specific event ID. |
| 484 | let repo_id = state_event | 487 | // We intentionally query by ID rather than kind+author+identifier because |
| 485 | .tags | 488 | // other tests (e.g. push-auth) may have sent a newer replaceable state event |
| 486 | .iter() | 489 | // for the same repo_id, which would displace this one in an identifier query. |
| 487 | .find(|t| t.kind() == TagKind::d()) | 490 | let served = client |
| 488 | .and_then(|t| t.content()) | 491 | .is_event_on_relay(state_event.id) |
| 489 | .ok_or("Missing d tag in state event")? | ||
| 490 | .to_string(); | ||
| 491 | |||
| 492 | let filter = Filter::new() | ||
| 493 | .kind(Kind::RepoState) | ||
| 494 | .author(client.public_key()) | ||
| 495 | .identifier(&repo_id); | ||
| 496 | |||
| 497 | let events = client | ||
| 498 | .query(filter) | ||
| 499 | .await | 492 | .await |
| 500 | .map_err(|e| format!("Failed to query state events: {}", e))?; | 493 | .map_err(|e| format!("Failed to query state event: {}", e))?; |
| 501 | 494 | ||
| 502 | if !events.iter().any(|e| e.id == state_event.id) { | 495 | if !served { |
| 503 | return Err(format!( | 496 | return Err(format!( |
| 504 | "State event not served after git push. Event ID: {}", | 497 | "State event not served after git push. Event ID: {}", |
| 505 | state_event.id | 498 | state_event.id |
| @@ -665,7 +658,7 @@ impl PurgatoryTests { | |||
| 665 | /// each referencing an event the author is requesting to be deleted." | 658 | /// each referencing an event the author is requesting to be deleted." |
| 666 | /// | 659 | /// |
| 667 | /// This test verifies: | 660 | /// This test verifies: |
| 668 | /// 1. Get a promoted repo (OwnerStateDataPushed) so git pushes are possible | 661 | /// 1. Get a promoted repo (PurgatoryOwnerStateDataPushed) so git pushes are possible |
| 669 | /// 2. Clone the repo and create a unique commit (not yet pushed) | 662 | /// 2. Clone the repo and create a unique commit (not yet pushed) |
| 670 | /// 3. Submit a state event pointing to that unique commit (enters purgatory) | 663 | /// 3. Submit a state event pointing to that unique commit (enters purgatory) |
| 671 | /// 4. Send a kind 5 deletion event referencing the state event by event ID | 664 | /// 4. Send a kind 5 deletion event referencing the state event by event ID |
| @@ -681,11 +674,12 @@ impl PurgatoryTests { | |||
| 681 | .run(|| async { | 674 | .run(|| async { |
| 682 | let ctx = TestContext::new(client); | 675 | let ctx = TestContext::new(client); |
| 683 | 676 | ||
| 684 | // Stage 1: get a promoted repo with git data already on the relay | 677 | // Stage 1: get the isolated purgatory repo (independent from the shared |
| 678 | // OwnerStateDataPushed chain that push-authorization tests depend on) | ||
| 685 | let existing_state = ctx | 679 | let existing_state = ctx |
| 686 | .get_fixture(FixtureKind::OwnerStateDataPushed) | 680 | .get_fixture(FixtureKind::PurgatoryOwnerStateDataPushed) |
| 687 | .await | 681 | .await |
| 688 | .map_err(|e| format!("Failed to get promoted repo: {}", e))?; | 682 | .map_err(|e| format!("Failed to get purgatory test repo: {}", e))?; |
| 689 | 683 | ||
| 690 | let repo_id = existing_state | 684 | let repo_id = existing_state |
| 691 | .tags | 685 | .tags |
| @@ -788,7 +782,7 @@ impl PurgatoryTests { | |||
| 788 | /// event up to the `created_at` timestamp of the deletion request event." | 782 | /// event up to the `created_at` timestamp of the deletion request event." |
| 789 | /// | 783 | /// |
| 790 | /// This test verifies: | 784 | /// This test verifies: |
| 791 | /// 1. Get a promoted repo (OwnerStateDataPushed) so git pushes are possible | 785 | /// 1. Get a promoted repo (PurgatoryOwnerStateDataPushed) so git pushes are possible |
| 792 | /// 2. Generate a fresh keypair for a new maintainer | 786 | /// 2. Generate a fresh keypair for a new maintainer |
| 793 | /// 3. Send a replacement owner announcement adding the new maintainer (goes to DB) | 787 | /// 3. Send a replacement owner announcement adding the new maintainer (goes to DB) |
| 794 | /// 4. Send a state event signed by the new maintainer pointing to a unique commit | 788 | /// 4. Send a state event signed by the new maintainer pointing to a unique commit |
| @@ -807,11 +801,14 @@ impl PurgatoryTests { | |||
| 807 | .run(|| async { | 801 | .run(|| async { |
| 808 | let ctx = TestContext::new(client); | 802 | let ctx = TestContext::new(client); |
| 809 | 803 | ||
| 810 | // Stage 1: get a promoted repo with git data already on the relay | 804 | // Stage 1: get the isolated purgatory repo (independent from the shared |
| 805 | // OwnerStateDataPushed chain that push-authorization tests depend on). | ||
| 806 | // This test sends a replacement announcement (kind 30617) for the repo which | ||
| 807 | // would corrupt the shared repo's maintainer set if we used OwnerStateDataPushed. | ||
| 811 | let existing_state = ctx | 808 | let existing_state = ctx |
| 812 | .get_fixture(FixtureKind::OwnerStateDataPushed) | 809 | .get_fixture(FixtureKind::PurgatoryOwnerStateDataPushed) |
| 813 | .await | 810 | .await |
| 814 | .map_err(|e| format!("Failed to get promoted repo: {}", e))?; | 811 | .map_err(|e| format!("Failed to get purgatory test repo: {}", e))?; |
| 815 | 812 | ||
| 816 | let repo_id = existing_state | 813 | let repo_id = existing_state |
| 817 | .tags | 814 | .tags |