upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src
diff options
context:
space:
mode:
Diffstat (limited to 'grasp-audit/src')
-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