upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 21:38:10 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 21:38:10 +0000
commit3c398b5e528f79231fa55f91225f9e79be1d43f5 (patch)
tree188bfffbc6a6fae59b5f0d401047f0c77fb2244b /grasp-audit
parentbfbdd1c2fe2a556af099d79ea25d1b9bd1d3fd2c (diff)
better fixtures: MaintainerStateDataPushed
Diffstat (limited to 'grasp-audit')
-rw-r--r--grasp-audit/src/fixtures.rs181
-rw-r--r--grasp-audit/src/specs/grasp01/push_authorization.rs228
2 files changed, 200 insertions, 209 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.
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs
index f6a2314..2e14953 100644
--- a/grasp-audit/src/specs/grasp01/push_authorization.rs
+++ b/grasp-audit/src/specs/grasp01/push_authorization.rs
@@ -35,7 +35,7 @@ use crate::{
35 clone_repo, create_commit, create_deterministic_commit, 35 clone_repo, create_commit, create_deterministic_commit,
36 create_deterministic_commit_with_variant, try_push, try_push_to_ref, AuditClient, 36 create_deterministic_commit_with_variant, try_push, try_push_to_ref, AuditClient,
37 CommitVariant, FixtureKind, TestContext, TestResult, DETERMINISTIC_COMMIT_HASH, 37 CommitVariant, FixtureKind, TestContext, TestResult, DETERMINISTIC_COMMIT_HASH,
38 MAINTAINER_DETERMINISTIC_COMMIT_HASH, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, 38 MAINTAINER_DETERMINISTIC_COMMIT_HASH,
39}; 39};
40use nostr_sdk::prelude::*; 40use nostr_sdk::prelude::*;
41use std::fs; 41use std::fs;
@@ -814,234 +814,44 @@ impl PushAuthorizationTests {
814 /// Test push authorized by recursive maintainer state event 814 /// Test push authorized by recursive maintainer state event
815 /// 815 ///
816 /// GRASP-01: "respecting the recursive maintainer set" 816 /// GRASP-01: "respecting the recursive maintainer set"
817 /// This tests recursive maintainer chains: Owner -> MaintainerA -> MaintainerB 817 /// This tests recursive maintainer chains: Owner -> Maintainer -> RecursiveMaintainer
818 /// 818 ///
819 /// ## Fixture-First Pattern 819 /// This test uses the RecursiveMaintainerStateDataPushed fixture which handles all 4 stages:
820 /// 820 /// 1. **Generated**: Creates MaintainerStateDataPushed (owner's + maintainer's data pushed)
821 /// 1. **Generate**: Create TestContext and get fixture chain: 821 /// + MaintainerAnnouncement (maintainer lists recursive maintainer)
822 /// - RepoState (owner's repo announcement + state event) 822 /// + RecursiveMaintainerState (recursive maintainer's state event)
823 /// - MaintainerAnnouncement (maintainer lists recursive-maintainer) 823 /// 2. **Sent**: Sends events to relay
824 /// - MaintainerState (maintainer's state event) 824 /// 3. **Verified**: Confirms events accepted by relay
825 /// - RecursiveMaintainerRepoAndState (recursive maintainer's announcement + state) 825 /// 4. **DataPushed**: Clones repo, creates recursive maintainer deterministic commit, pushes to relay
826 /// 2. **Send**: Clone repo, create recursive maintainer deterministic commit, push
827 /// 3. **Verify**: Push should succeed because recursive maintainer's state event authorizes it
828 /// 826 ///
829 /// The fixture chain establishes: Owner -> Maintainer -> RecursiveMaintainer 827 /// The test wraps the fixture result in pass/fail using the error message.
830 /// Each level publishes announcements that authorize the next level. 828 #[allow(unused_variables)] // relay_domain is now handled by fixture
831 pub async fn test_push_authorized_by_recursive_maintainer_state( 829 pub async fn test_push_authorized_by_recursive_maintainer_state(
832 client: &AuditClient, 830 client: &AuditClient,
833 relay_domain: &str, 831 relay_domain: &str,
834 ) -> TestResult { 832 ) -> TestResult {
835 use std::process::Command;
836
837 let test_name = "test_push_authorized_by_recursive_maintainer_state"; 833 let test_name = "test_push_authorized_by_recursive_maintainer_state";
838
839 // ============================================================
840 // Step 1: GENERATE - Create TestContext and get fixture chain
841 // ============================================================
842 let ctx = TestContext::new(client); 834 let ctx = TestContext::new(client);
843 835
844 // Get RepoState fixture (owner's repo announcement + state event) 836 // The RecursiveMaintainerStateDataPushed fixture handles all stages:
845 let state_event = match ctx.get_fixture(FixtureKind::RepoState).await { 837 // Generate → Send → Verify → DataPush
846 Ok(e) => e, 838 match ctx.get_fixture(FixtureKind::RecursiveMaintainerStateDataPushed).await {
847 Err(e) => { 839 Ok(_recursive_maintainer_state_event) => {
848 return TestResult::new( 840 TestResult::new(
849 test_name,
850 "GRASP-01",
851 "Push authorized by recursive maintainer state event",
852 )
853 .fail(format!("Failed to create RepoState fixture: {}", e));
854 }
855 };
856
857 // Get MaintainerAnnouncement fixture (maintainer's repo announcement listing recursive maintainer)
858 match ctx.get_fixture(FixtureKind::MaintainerAnnouncement).await {
859 Ok(_) => {}
860 Err(e) => {
861 return TestResult::new(
862 test_name,
863 "GRASP-01",
864 "Push authorized by recursive maintainer state event",
865 )
866 .fail(format!(
867 "Failed to create MaintainerAnnouncement fixture: {}",
868 e
869 ));
870 }
871 };
872
873 // Get MaintainerState fixture (maintainer's state event)
874 match ctx.get_fixture(FixtureKind::MaintainerState).await {
875 Ok(_) => {}
876 Err(e) => {
877 return TestResult::new(
878 test_name,
879 "GRASP-01",
880 "Push authorized by recursive maintainer state event",
881 )
882 .fail(format!("Failed to create MaintainerState fixture: {}", e));
883 }
884 };
885
886 // Get RecursiveMaintainerRepoAndState fixture (completes 3-level delegation chain)
887 match ctx
888 .get_fixture(FixtureKind::RecursiveMaintainerRepoAndState)
889 .await
890 {
891 Ok(_) => {}
892 Err(e) => {
893 return TestResult::new(
894 test_name,
895 "GRASP-01",
896 "Push authorized by recursive maintainer state event",
897 )
898 .fail(format!(
899 "Failed to create RecursiveMaintainerRepoAndState fixture: {}",
900 e
901 ));
902 }
903 };
904
905 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
906
907 // Extract repo_id and npub from owner's state event
908 let repo_id = match state_event
909 .tags
910 .iter()
911 .find(|t| t.kind() == TagKind::d())
912 .and_then(|t| t.content())
913 {
914 Some(id) => id.to_string(),
915 None => {
916 return TestResult::new(
917 test_name,
918 "GRASP-01",
919 "Push authorized by recursive maintainer state event",
920 )
921 .fail("Missing repo_id in state event");
922 }
923 };
924
925 let npub = match state_event.pubkey.to_bech32() {
926 Ok(n) => n,
927 Err(e) => {
928 return TestResult::new(
929 test_name,
930 "GRASP-01",
931 "Push authorized by recursive maintainer state event",
932 )
933 .fail(format!("Failed to convert pubkey to bech32: {}", e));
934 }
935 };
936
937 // ============================================================
938 // Step 2: SEND - Clone, create recursive maintainer commit, push
939 // ============================================================
940 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
941 Ok(p) => p,
942 Err(e) => {
943 return TestResult::new(
944 test_name, 841 test_name,
945 "GRASP-01", 842 "GRASP-01",
946 "Push authorized by recursive maintainer state event", 843 "Push authorized by recursive maintainer state event",
947 ) 844 )
948 .fail(&e); 845 .pass()
949 } 846 }
950 };
951 let cleanup = || {
952 let _ = fs::remove_dir_all(&clone_path);
953 };
954
955 // Reset to orphan state and create deterministic root commit
956 // Step 1: Create orphan branch (removes all history)
957 let _ = Command::new("git")
958 .args(["checkout", "--orphan", "main-new"])
959 .current_dir(&clone_path)
960 .output();
961
962 // Step 2: Clear staged files (orphan keeps files staged from previous branch)
963 let _ = Command::new("git")
964 .args(["rm", "-rf", "--cached", "."])
965 .current_dir(&clone_path)
966 .output();
967
968 // Step 3: Create recursive maintainer deterministic commit
969 let commit_hash = match create_deterministic_commit_with_variant(
970 &clone_path,
971 CommitVariant::RecursiveMaintainer,
972 ) {
973 Ok(h) => h,
974 Err(e) => { 847 Err(e) => {
975 cleanup(); 848 TestResult::new(
976 return TestResult::new(
977 test_name, 849 test_name,
978 "GRASP-01", 850 "GRASP-01",
979 "Push authorized by recursive maintainer state event", 851 "Push authorized by recursive maintainer state event",
980 ) 852 )
981 .fail(format!( 853 .fail(format!("{}", e))
982 "Failed to create recursive maintainer commit: {}",
983 e
984 ));
985 } 854 }
986 };
987
988 // Step 4: Replace main branch with our new orphan branch
989 let _ = Command::new("git")
990 .args(["branch", "-D", "main"])
991 .current_dir(&clone_path)
992 .output();
993
994 let _ = Command::new("git")
995 .args(["branch", "-m", "main"])
996 .current_dir(&clone_path)
997 .output();
998
999 // Verify commit hash matches expected
1000 if commit_hash != RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH {
1001 cleanup();
1002 return TestResult::new(
1003 test_name,
1004 "GRASP-01",
1005 "Push authorized by recursive maintainer state event",
1006 )
1007 .fail(format!(
1008 "Recursive maintainer commit hash mismatch: got {}, expected {}",
1009 commit_hash, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH
1010 ));
1011 }
1012
1013 // ============================================================
1014 // Step 3: VERIFY - Push should succeed because recursive
1015 // maintainer's state event authorizes this commit
1016 // ============================================================
1017 let push_result = try_push(&clone_path);
1018 cleanup();
1019
1020 match push_result {
1021 Ok(true) => TestResult::new(
1022 test_name,
1023 "GRASP-01",
1024 "Push authorized by recursive maintainer state event",
1025 )
1026 .pass(),
1027 Ok(false) => TestResult::new(
1028 test_name,
1029 "GRASP-01",
1030 "Push authorized by recursive maintainer state event",
1031 )
1032 .fail(format!(
1033 "Push was rejected but should have been accepted. \
1034 The recursive maintainer published a state event with commit {}, \
1035 and the relay should authorize pushes matching this state event \
1036 through recursive maintainer traversal (Owner -> Maintainer -> RecursiveMaintainer).",
1037 RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH
1038 )),
1039 Err(e) => TestResult::new(
1040 test_name,
1041 "GRASP-01",
1042 "Push authorized by recursive maintainer state event",
1043 )
1044 .fail(format!("Push error: {}", e)),
1045 } 855 }
1046 } 856 }
1047 857