upleb.uk

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

summaryrefslogtreecommitdiff
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
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.
-rw-r--r--grasp-audit/src/bin/grasp-audit.rs55
-rw-r--r--grasp-audit/src/fixtures.rs203
-rw-r--r--grasp-audit/src/specs/grasp01/purgatory.rs107
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