diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-01 21:20:58 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-01 21:20:58 +0000 |
| commit | 80053758daf365896cdfd2b9a40496adad229ce9 (patch) | |
| tree | 8fe2c6c59ebe420dc5cdda2e24d332f16bc38b1f /grasp-audit/src/fixtures.rs | |
| parent | b95460cb136f3e022896148afb9c67755af5d832 (diff) | |
better fixtures: MaintainerStateDataPushed
Diffstat (limited to 'grasp-audit/src/fixtures.rs')
| -rw-r--r-- | grasp-audit/src/fixtures.rs | 165 |
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. |