diff options
Diffstat (limited to 'grasp-audit/src/fixtures.rs')
| -rw-r--r-- | grasp-audit/src/fixtures.rs | 181 |
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 | ||
| 227 | impl FixtureKind { | 252 | impl 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. |