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:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 21:20:58 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 21:20:58 +0000
commit80053758daf365896cdfd2b9a40496adad229ce9 (patch)
tree8fe2c6c59ebe420dc5cdda2e24d332f16bc38b1f /grasp-audit/src/fixtures.rs
parentb95460cb136f3e022896148afb9c67755af5d832 (diff)
better fixtures: MaintainerStateDataPushed
Diffstat (limited to 'grasp-audit/src/fixtures.rs')
-rw-r--r--grasp-audit/src/fixtures.rs165
1 files changed, 165 insertions, 0 deletions
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs
index 062bb9b..fef5c5c 100644
--- a/grasp-audit/src/fixtures.rs
+++ b/grasp-audit/src/fixtures.rs
@@ -201,6 +201,27 @@ pub enum FixtureKind {
201 /// - Points to DETERMINISTIC_COMMIT_HASH 201 /// - Points to DETERMINISTIC_COMMIT_HASH
202 /// - Git push verified to succeed (state matches pushed commit) 202 /// - Git push verified to succeed (state matches pushed commit)
203 OwnerStateDataPushed, 203 OwnerStateDataPushed,
204
205 /// Maintainer's state event with git data successfully pushed (full 4-stage fixture)
206 ///
207 /// This fixture tests that a maintainer can authorize pushes with ONLY a state event,
208 /// without publishing their own repo announcement. The maintainer is still listed in
209 /// the owner's announcement, so they're a valid maintainer.
210 ///
211 /// GRASP-01: "respecting the recursive maintainer set"
212 ///
213 /// Stages:
214 /// 1. **Generated**: Creates ValidRepo (owner's announcement with maintainer in maintainers tag)
215 /// + MaintainerState (maintainer's state event ONLY - no announcement)
216 /// 2. **Sent**: Sends events to relay
217 /// 3. **Verified**: Confirms events accepted by relay
218 /// 4. **DataPushed**: Clones repo, creates maintainer deterministic commit, pushes to relay
219 ///
220 /// - Requires ValidRepo (owner's announcement lists maintainer)
221 /// - State event signed by maintainer keys (`client.maintainer_keys()`)
222 /// - Points to MAINTAINER_DETERMINISTIC_COMMIT_HASH
223 /// - Git push verified to succeed (maintainer's state event authorizes the commit)
224 MaintainerStateDataPushed,
204} 225}
205 226
206/// Context mode for fixture management 227/// Context mode for fixture management
@@ -781,6 +802,10 @@ impl<'a> TestContext<'a> {
781 FixtureKind::OwnerStateDataPushed => { 802 FixtureKind::OwnerStateDataPushed => {
782 self.build_owner_state_data_pushed().await 803 self.build_owner_state_data_pushed().await
783 } 804 }
805
806 FixtureKind::MaintainerStateDataPushed => {
807 self.build_maintainer_state_data_pushed().await
808 }
784 } 809 }
785 } 810 }
786 811
@@ -1099,6 +1124,146 @@ impl<'a> TestContext<'a> {
1099 } 1124 }
1100 } 1125 }
1101 1126
1127 /// Build MaintainerStateDataPushed fixture: full 4-stage fixture for maintainer push authorization
1128 ///
1129 /// This tests that a maintainer can authorize pushes with ONLY a state event,
1130 /// without publishing their own repo announcement.
1131 ///
1132 /// # Returns
1133 /// The maintainer's state event (kind 30618) after all stages complete successfully
1134 async fn build_maintainer_state_data_pushed(&self) -> Result<Event> {
1135 use nostr_sdk::prelude::*;
1136
1137 // ============================================================
1138 // Stage 1 & 2: Generate and Send ValidRepo + MaintainerState fixtures
1139 // ============================================================
1140
1141 // Get owner's repo (ValidRepo) - this includes maintainer in maintainers tag
1142 let repo = self.get_or_create_repo().await?;
1143
1144 // Extract repo_id from repo announcement
1145 let repo_id = repo
1146 .tags
1147 .iter()
1148 .find(|t| t.kind() == TagKind::d())
1149 .and_then(|t| t.content())
1150 .ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement"))?
1151 .to_string();
1152
1153 // Build maintainer's state event (state event ONLY - no announcement)
1154 let base_time = Timestamp::now().as_u64();
1155 let maintainer_timestamp = Timestamp::from(base_time - 5); // 5 seconds ago (more recent than owner's state)
1156
1157 let maintainer_state_event = self
1158 .client
1159 .event_builder(Kind::Custom(30618), "")
1160 .tag(Tag::identifier(&repo_id))
1161 .tag(Tag::custom(
1162 TagKind::custom("refs/heads/main"),
1163 vec![MAINTAINER_DETERMINISTIC_COMMIT_HASH.to_string()],
1164 ))
1165 .tag(Tag::custom(
1166 TagKind::custom("HEAD"),
1167 vec!["ref: refs/heads/main".to_string()],
1168 ))
1169 .custom_time(maintainer_timestamp)
1170 .build(self.client.maintainer_keys())
1171 .map_err(|e| anyhow::anyhow!("Failed to build maintainer state event: {}", e))?;
1172
1173 // Send maintainer state event to relay
1174 self.client.send_event(maintainer_state_event.clone()).await?;
1175
1176 // ============================================================
1177 // Stage 3: Verify state event was accepted
1178 // ============================================================
1179 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1180
1181 // ============================================================
1182 // Stage 4: DataPushed - Clone repo, create maintainer commit, push
1183 // ============================================================
1184
1185 // Get relay domain from connected relay
1186 let relay_domain = self.get_relay_domain().await?;
1187
1188 // Use owner's npub for cloning (repo belongs to owner)
1189 let npub = repo
1190 .pubkey
1191 .to_bech32()
1192 .map_err(|e| anyhow::anyhow!("Failed to convert pubkey to bech32: {}", e))?;
1193
1194 // Clone the repository
1195 let clone_path = clone_repo(&relay_domain, &npub, &repo_id)
1196 .map_err(|e| anyhow::anyhow!("Failed to clone repo: {}", e))?;
1197
1198 // Cleanup helper (always clean up on error or success)
1199 let cleanup = |path: &PathBuf| {
1200 let _ = fs::remove_dir_all(path);
1201 };
1202
1203 // Reset to orphan state and create deterministic root commit
1204 // Step 1: Create orphan branch (removes all history)
1205 let _ = Command::new("git")
1206 .args(["checkout", "--orphan", "main-new"])
1207 .current_dir(&clone_path)
1208 .output();
1209
1210 // Step 2: Clear staged files (orphan keeps files staged from previous branch)
1211 let _ = Command::new("git")
1212 .args(["rm", "-rf", "--cached", "."])
1213 .current_dir(&clone_path)
1214 .output();
1215
1216 // Step 3: Create deterministic commit using maintainer variant
1217 let commit_hash = match create_deterministic_commit_with_variant(
1218 &clone_path,
1219 CommitVariant::Maintainer,
1220 ) {
1221 Ok(h) => h,
1222 Err(e) => {
1223 cleanup(&clone_path);
1224 return Err(anyhow::anyhow!("Failed to create maintainer commit: {}", e));
1225 }
1226 };
1227
1228 // Step 4: Replace main branch with our new orphan branch
1229 let _ = Command::new("git")
1230 .args(["branch", "-D", "main"])
1231 .current_dir(&clone_path)
1232 .output();
1233
1234 let _ = Command::new("git")
1235 .args(["branch", "-m", "main"])
1236 .current_dir(&clone_path)
1237 .output();
1238
1239 // Verify commit hash matches expected
1240 if commit_hash != MAINTAINER_DETERMINISTIC_COMMIT_HASH {
1241 cleanup(&clone_path);
1242 return Err(anyhow::anyhow!(
1243 "Maintainer commit hash mismatch: got {}, expected {}",
1244 commit_hash,
1245 MAINTAINER_DETERMINISTIC_COMMIT_HASH
1246 ));
1247 }
1248
1249 // Push to relay
1250 let push_result = try_push(&clone_path);
1251 cleanup(&clone_path);
1252
1253 match push_result {
1254 Ok(true) => Ok(maintainer_state_event),
1255 Ok(false) => Err(anyhow::anyhow!(
1256 "Push was rejected but should have been accepted. \
1257 The maintainer published a state event with commit {}, \
1258 and even without a separate announcement, the relay should \
1259 authorize pushes matching this state event since the maintainer \
1260 is listed in the owner's announcement.",
1261 MAINTAINER_DETERMINISTIC_COMMIT_HASH
1262 )),
1263 Err(e) => Err(anyhow::anyhow!("Push error: {}", e)),
1264 }
1265 }
1266
1102 /// Get relay domain (host:port) from the connected relay 1267 /// Get relay domain (host:port) from the connected relay
1103 /// 1268 ///
1104 /// Extracts the domain from the relay URL for git HTTP operations. 1269 /// Extracts the domain from the relay URL for git HTTP operations.