upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/fixtures.rs
diff options
context:
space:
mode:
Diffstat (limited to 'grasp-audit/src/fixtures.rs')
-rw-r--r--grasp-audit/src/fixtures.rs181
1 files changed, 181 insertions, 0 deletions
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs
index d894ed8..5e8c50a 100644
--- a/grasp-audit/src/fixtures.rs
+++ b/grasp-audit/src/fixtures.rs
@@ -222,6 +222,31 @@ pub enum FixtureKind {
222 /// - Points to MAINTAINER_DETERMINISTIC_COMMIT_HASH 222 /// - Points to MAINTAINER_DETERMINISTIC_COMMIT_HASH
223 /// - Git push verified to succeed (force push with maintainer's state event authorizes the commit) 223 /// - Git push verified to succeed (force push with maintainer's state event authorizes the commit)
224 MaintainerStateDataPushed, 224 MaintainerStateDataPushed,
225
226 /// Recursive maintainer's state event with git data successfully pushed (full 4-stage fixture)
227 ///
228 /// This fixture tests that a recursive maintainer (authorized via maintainer chain) can
229 /// authorize pushes. The recursive maintainer is listed in the maintainer's announcement,
230 /// not the owner's announcement, so this tests the recursive maintainer traversal.
231 ///
232 /// GRASP-01: "respecting the recursive maintainer set"
233 ///
234 /// Chain: Owner -> Maintainer -> RecursiveMaintainer
235 ///
236 /// Stages:
237 /// 1. **Generated**: Creates MaintainerStateDataPushed (includes ValidRepo + OwnerStateDataPushed)
238 /// + MaintainerAnnouncement (maintainer's announcement listing recursive maintainer)
239 /// + RecursiveMaintainerState (recursive maintainer's state event)
240 /// 2. **Sent**: Sends events to relay
241 /// 3. **Verified**: Confirms events accepted by relay
242 /// 4. **DataPushed**: Clones repo, creates recursive maintainer deterministic commit, pushes to relay
243 ///
244 /// - Requires MaintainerStateDataPushed (establishes Owner -> Maintainer chain with git data)
245 /// - Sends MaintainerAnnouncement (establishes Maintainer -> RecursiveMaintainer connection)
246 /// - State event signed by recursive maintainer keys (`client.recursive_maintainer_keys()`)
247 /// - Points to RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH
248 /// - Git push verified to succeed (recursive maintainer's state event authorizes the commit)
249 RecursiveMaintainerStateDataPushed,
225} 250}
226 251
227impl FixtureKind { 252impl FixtureKind {
@@ -251,6 +276,10 @@ impl FixtureKind {
251 // MaintainerStateDataPushed depends on OwnerStateDataPushed 276 // MaintainerStateDataPushed depends on OwnerStateDataPushed
252 // (maintainer force-pushes over owner's data) 277 // (maintainer force-pushes over owner's data)
253 Self::MaintainerStateDataPushed => vec![Self::OwnerStateDataPushed], 278 Self::MaintainerStateDataPushed => vec![Self::OwnerStateDataPushed],
279
280 // RecursiveMaintainerStateDataPushed depends on MaintainerStateDataPushed
281 // (recursive maintainer force-pushes over maintainer's data)
282 Self::RecursiveMaintainerStateDataPushed => vec![Self::MaintainerStateDataPushed],
254 } 283 }
255 } 284 }
256 285
@@ -264,6 +293,7 @@ impl FixtureKind {
264 // These fixtures send events and push git data internally 293 // These fixtures send events and push git data internally
265 Self::OwnerStateDataPushed => true, 294 Self::OwnerStateDataPushed => true,
266 Self::MaintainerStateDataPushed => true, 295 Self::MaintainerStateDataPushed => true,
296 Self::RecursiveMaintainerStateDataPushed => true,
267 // RecursiveMaintainerRepoAndState sends multiple events internally 297 // RecursiveMaintainerRepoAndState sends multiple events internally
268 Self::RecursiveMaintainerRepoAndState => true, 298 Self::RecursiveMaintainerRepoAndState => true,
269 // All other fixtures return a single event for the caller to send 299 // All other fixtures return a single event for the caller to send
@@ -730,6 +760,10 @@ impl<'a> TestContext<'a> {
730 FixtureKind::MaintainerStateDataPushed => { 760 FixtureKind::MaintainerStateDataPushed => {
731 self.build_maintainer_state_data_pushed().await 761 self.build_maintainer_state_data_pushed().await
732 } 762 }
763
764 FixtureKind::RecursiveMaintainerStateDataPushed => {
765 self.build_recursive_maintainer_state_data_pushed().await
766 }
733 } 767 }
734 } 768 }
735 769
@@ -1189,6 +1223,153 @@ impl<'a> TestContext<'a> {
1189 } 1223 }
1190 } 1224 }
1191 1225
1226 /// Build RecursiveMaintainerStateDataPushed fixture: full 4-stage fixture for recursive maintainer push authorization
1227 ///
1228 /// This tests that a recursive maintainer (authorized via maintainer chain) can authorize pushes.
1229 /// The recursive maintainer is listed in the maintainer's announcement, not the owner's announcement,
1230 /// so this tests the recursive maintainer traversal (Owner -> Maintainer -> RecursiveMaintainer).
1231 ///
1232 /// Depends on MaintainerStateDataPushed - the maintainer's data has already been pushed.
1233 /// We then send the MaintainerAnnouncement (which lists the recursive maintainer), and the
1234 /// recursive maintainer force-pushes their commit on top.
1235 ///
1236 /// # Returns
1237 /// The recursive maintainer's state event (kind 30618) after all stages complete successfully
1238 async fn build_recursive_maintainer_state_data_pushed(&self) -> Result<Event> {
1239 use nostr_sdk::prelude::*;
1240
1241 // ============================================================
1242 // Stage 1: MaintainerStateDataPushed is ensured by ensure_fixture before this is called
1243 // The owner's repo, owner's state event, and maintainer's state event are already on the relay,
1244 // and maintainer's git data is pushed
1245 // ============================================================
1246 let maintainer_state = self.get_cached_dependency(FixtureKind::MaintainerStateDataPushed)?;
1247
1248 // Extract repo_id from maintainer's state event (same d-tag structure)
1249 let repo_id = self.extract_repo_id(&maintainer_state)?;
1250
1251 // Get the repo (ValidRepo, also cached) for the owner's npub
1252 let repo = self.get_cached_dependency(FixtureKind::ValidRepo)?;
1253
1254 // ============================================================
1255 // Stage 2: Send MaintainerAnnouncement (establishes Maintainer -> RecursiveMaintainer chain)
1256 // ============================================================
1257 let maintainer_announcement = self.build_maintainer_announcement(&repo_id).await?;
1258 self.client.send_event(maintainer_announcement).await?;
1259
1260 // Build recursive maintainer's state event
1261 let base_time = Timestamp::now().as_u64();
1262 let recursive_maintainer_timestamp = Timestamp::from(base_time - 2); // 2 seconds ago (most recent)
1263
1264 let recursive_maintainer_state_event = self
1265 .client
1266 .event_builder(Kind::Custom(30618), "")
1267 .tag(Tag::identifier(&repo_id))
1268 .tag(Tag::custom(
1269 TagKind::custom("refs/heads/main"),
1270 vec![RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH.to_string()],
1271 ))
1272 .tag(Tag::custom(
1273 TagKind::custom("HEAD"),
1274 vec!["ref: refs/heads/main".to_string()],
1275 ))
1276 .custom_time(recursive_maintainer_timestamp)
1277 .build(self.client.recursive_maintainer_keys())
1278 .map_err(|e| anyhow::anyhow!("Failed to build recursive maintainer state event: {}", e))?;
1279
1280 // Send recursive maintainer state event to relay
1281 self.client.send_event(recursive_maintainer_state_event.clone()).await?;
1282
1283 // ============================================================
1284 // Stage 3: Verify state event was accepted
1285 // ============================================================
1286 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1287
1288 // ============================================================
1289 // Stage 4: DataPushed - Clone repo, create recursive maintainer commit, push
1290 // ============================================================
1291
1292 // Get relay domain from connected relay
1293 let relay_domain = self.get_relay_domain().await?;
1294
1295 // Use owner's npub for cloning (repo belongs to owner)
1296 let npub = repo
1297 .pubkey
1298 .to_bech32()
1299 .map_err(|e| anyhow::anyhow!("Failed to convert pubkey to bech32: {}", e))?;
1300
1301 // Clone the repository
1302 let clone_path = clone_repo(&relay_domain, &npub, &repo_id)
1303 .map_err(|e| anyhow::anyhow!("Failed to clone repo: {}", e))?;
1304
1305 // Cleanup helper (always clean up on error or success)
1306 let cleanup = |path: &PathBuf| {
1307 let _ = fs::remove_dir_all(path);
1308 };
1309
1310 // Reset to orphan state and create deterministic root commit
1311 // Step 1: Create orphan branch (removes all history)
1312 let _ = Command::new("git")
1313 .args(["checkout", "--orphan", "main-new"])
1314 .current_dir(&clone_path)
1315 .output();
1316
1317 // Step 2: Clear staged files (orphan keeps files staged from previous branch)
1318 let _ = Command::new("git")
1319 .args(["rm", "-rf", "--cached", "."])
1320 .current_dir(&clone_path)
1321 .output();
1322
1323 // Step 3: Create deterministic commit using recursive maintainer variant
1324 let commit_hash = match create_deterministic_commit_with_variant(
1325 &clone_path,
1326 CommitVariant::RecursiveMaintainer,
1327 ) {
1328 Ok(h) => h,
1329 Err(e) => {
1330 cleanup(&clone_path);
1331 return Err(anyhow::anyhow!("Failed to create recursive maintainer commit: {}", e));
1332 }
1333 };
1334
1335 // Step 4: Replace main branch with our new orphan branch
1336 let _ = Command::new("git")
1337 .args(["branch", "-D", "main"])
1338 .current_dir(&clone_path)
1339 .output();
1340
1341 let _ = Command::new("git")
1342 .args(["branch", "-m", "main"])
1343 .current_dir(&clone_path)
1344 .output();
1345
1346 // Verify commit hash matches expected
1347 if commit_hash != RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH {
1348 cleanup(&clone_path);
1349 return Err(anyhow::anyhow!(
1350 "Recursive maintainer commit hash mismatch: got {}, expected {}",
1351 commit_hash,
1352 RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH
1353 ));
1354 }
1355
1356 // Push to relay
1357 let push_result = try_push(&clone_path);
1358 cleanup(&clone_path);
1359
1360 match push_result {
1361 Ok(true) => Ok(recursive_maintainer_state_event),
1362 Ok(false) => Err(anyhow::anyhow!(
1363 "Push was rejected but should have been accepted. \
1364 The recursive maintainer published a state event with commit {}, \
1365 and the relay should authorize pushes matching this state event \
1366 through recursive maintainer traversal (Owner -> Maintainer -> RecursiveMaintainer).",
1367 RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH
1368 )),
1369 Err(e) => Err(anyhow::anyhow!("Push error: {}", e)),
1370 }
1371 }
1372
1192 /// Get relay domain (host:port) from the connected relay 1373 /// Get relay domain (host:port) from the connected relay
1193 /// 1374 ///
1194 /// Extracts the domain from the relay URL for git HTTP operations. 1375 /// Extracts the domain from the relay URL for git HTTP operations.