upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-27 17:05:51 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-27 17:05:51 +0000
commitd3e703d7f522c30ac6634716654c24cb7415fabd (patch)
tree0d7b64eb04c6c76a65ec06508e004f9357ca9374 /grasp-audit/src
parentadf22539e3c1b4a96fa4b8fe04095c216b4d5541 (diff)
remove depricated code
Diffstat (limited to 'grasp-audit/src')
-rw-r--r--grasp-audit/src/fixtures.rs520
-rw-r--r--grasp-audit/src/lib.rs4
-rw-r--r--grasp-audit/src/specs/grasp01/push_authorization.rs395
3 files changed, 352 insertions, 567 deletions
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs
index a7806ec..89531c6 100644
--- a/grasp-audit/src/fixtures.rs
+++ b/grasp-audit/src/fixtures.rs
@@ -856,7 +856,6 @@ pub async fn send_and_verify_rejected(
856// Git Operation Helpers 856// Git Operation Helpers
857// ============================================================ 857// ============================================================
858 858
859use nostr_sdk::ToBech32;
860use std::fs; 859use std::fs;
861use std::path::{Path, PathBuf}; 860use std::path::{Path, PathBuf};
862use std::process::Command; 861use std::process::Command;
@@ -1087,525 +1086,6 @@ pub fn create_deterministic_commit(clone_path: &Path, _message: &str) -> Result<
1087 create_deterministic_commit_with_variant(clone_path, CommitVariant::Owner) 1086 create_deterministic_commit_with_variant(clone_path, CommitVariant::Owner)
1088} 1087}
1089 1088
1090/// Repository setup with deterministic commit
1091/// This struct holds all the data needed for push authorization tests
1092pub struct RepoSetup {
1093 /// Path to the cloned repository (auto-cleaned on drop)
1094 pub clone_path: PathBuf,
1095 /// Repository identifier (d-tag value)
1096 pub repo_id: String,
1097 /// Owner's bech32 public key
1098 pub npub: String,
1099 /// The deterministic commit hash
1100 pub commit_hash: String,
1101}
1102
1103impl Drop for RepoSetup {
1104 fn drop(&mut self) {
1105 let _ = fs::remove_dir_all(&self.clone_path);
1106 }
1107}
1108
1109/// Set up a repository with deterministic commit for testing
1110///
1111/// # Deprecated
1112///
1113/// This function is deprecated in favor of the fixture-first pattern.
1114/// Tests should create their own TestContext and use `FixtureKind::RepoState`
1115/// directly, following the Generate → Send → Verify pattern.
1116///
1117/// See `test_push_authorized_by_owner_state` in `push_authorization.rs` for
1118/// an example of the fixture-first pattern.
1119///
1120/// ## Migration Guide
1121///
1122/// Instead of:
1123/// ```ignore
1124/// let setup = setup_repo_with_deterministic_commit(client, git_data_dir, relay_domain).await?;
1125/// ```
1126///
1127/// Use:
1128/// ```ignore
1129/// let ctx = TestContext::new(client);
1130/// let state_event = ctx.get_fixture(FixtureKind::RepoState).await?;
1131/// // Then clone, create deterministic commit, and push inline
1132/// ```
1133///
1134/// ---
1135///
1136/// This performs all the common setup steps needed for push authorization tests:
1137/// 1. Gets RepoState fixture (repo announcement + state event with deterministic commit)
1138/// 2. Extracts repo_id and npub
1139/// 3. Verifies repo exists on disk
1140/// 4. Clones the repository
1141/// 5. Creates deterministic commit locally
1142/// 6. Verifies commit hash matches expected
1143/// 7. Creates and checks out main branch
1144/// 8. Pushes the commit so the grasp server has the state in the state event
1145///
1146/// Returns RepoSetup which auto-cleans up the clone_path on drop
1147///
1148/// # Arguments
1149/// * `client` - The AuditClient to use for fixtures
1150/// * `git_data_dir` - Path to the git data directory
1151/// * `relay_domain` - The domain of the relay (e.g., "localhost:7000")
1152///
1153/// # Returns
1154/// * `Ok(RepoSetup)` - The setup data
1155/// * `Err(String)` - Error message if setup failed
1156#[deprecated(
1157 since = "0.1.0",
1158 note = "Use fixture-first pattern with TestContext and FixtureKind::RepoState instead. See test_push_authorized_by_owner_state for example."
1159)]
1160pub async fn setup_repo_with_deterministic_commit(
1161 client: &crate::AuditClient,
1162 git_data_dir: &Path,
1163 relay_domain: &str,
1164) -> Result<RepoSetup, String> {
1165 use nostr_sdk::prelude::TagKind;
1166
1167 let ctx = TestContext::new(client);
1168
1169 // Get RepoState fixture (includes repo announcement and state event with deterministic commit)
1170 let state_event = ctx.get_fixture(FixtureKind::RepoState).await
1171 .map_err(|e| format!("Failed to create repo state fixture: {}", e))?;
1172
1173 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1174
1175 // Extract repo_id from state event
1176 let repo_id = state_event.tags.iter().find(|t| t.kind() == TagKind::d())
1177 .and_then(|t| t.content())
1178 .ok_or("Missing repo_id")?
1179 .to_string();
1180 let npub = state_event.pubkey.to_bech32()
1181 .map_err(|e| format!("Failed to convert pubkey to bech32: {}", e))?;
1182
1183 // Verify repo exists
1184 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
1185 if !repo_path.exists() {
1186 return Err(format!("Repo not found: {}", repo_path.display()));
1187 }
1188
1189 // Clone repo
1190 let clone_path = clone_repo(relay_domain, &npub, &repo_id)?;
1191
1192 // Create deterministic commit locally (this will be the root commit with no parent)
1193 let commit_hash = create_deterministic_commit(&clone_path, "Initial commit")
1194 .map_err(|e| {
1195 let _ = fs::remove_dir_all(&clone_path);
1196 e
1197 })?;
1198
1199 // Verify commit hash matches expected deterministic hash
1200 if commit_hash != DETERMINISTIC_COMMIT_HASH {
1201 let _ = fs::remove_dir_all(&clone_path);
1202 return Err(format!(
1203 "Commit hash mismatch: got {}, expected {}",
1204 commit_hash, DETERMINISTIC_COMMIT_HASH
1205 ));
1206 }
1207
1208 // Create main branch pointing to our deterministic commit
1209 let branch_output = Command::new("git")
1210 .args(["branch", "main"])
1211 .current_dir(&clone_path)
1212 .output()
1213 .map_err(|e| {
1214 let _ = fs::remove_dir_all(&clone_path);
1215 format!("Failed to create main branch: {}", e)
1216 })?;
1217
1218 if !branch_output.status.success() {
1219 let _ = fs::remove_dir_all(&clone_path);
1220 return Err(format!(
1221 "Failed to create main branch: {}",
1222 String::from_utf8_lossy(&branch_output.stderr)
1223 ));
1224 }
1225
1226 // Checkout main branch
1227 let checkout_output = Command::new("git")
1228 .args(["checkout", "main"])
1229 .current_dir(&clone_path)
1230 .output()
1231 .map_err(|e| {
1232 let _ = fs::remove_dir_all(&clone_path);
1233 format!("Failed to checkout main branch: {}", e)
1234 })?;
1235
1236 if !checkout_output.status.success() {
1237 let _ = fs::remove_dir_all(&clone_path);
1238 return Err(format!(
1239 "Failed to checkout main branch: {}",
1240 String::from_utf8_lossy(&checkout_output.stderr)
1241 ));
1242 }
1243
1244 // Push the commit to the server so the bare repo matches the state event
1245 let push_output = Command::new("git")
1246 .args(["push", "origin", "main"])
1247 .current_dir(&clone_path)
1248 .env("GIT_TERMINAL_PROMPT", "0")
1249 .output()
1250 .map_err(|e| {
1251 let _ = fs::remove_dir_all(&clone_path);
1252 format!("Failed to push to server: {}", e)
1253 })?;
1254
1255 if !push_output.status.success() {
1256 let _ = fs::remove_dir_all(&clone_path);
1257 return Err(format!(
1258 "Failed to push to server: {}",
1259 String::from_utf8_lossy(&push_output.stderr)
1260 ));
1261 }
1262
1263 Ok(RepoSetup {
1264 clone_path,
1265 repo_id,
1266 npub,
1267 commit_hash,
1268 })
1269}
1270
1271/// Set up a maintainer repository with deterministic commit (state only)
1272///
1273/// # Deprecated
1274///
1275/// This function is deprecated in favor of the fixture-first pattern.
1276/// Tests should create their own TestContext and use `FixtureKind::MaintainerState`
1277/// directly, following the Generate → Send → Verify pattern.
1278///
1279/// See `test_push_authorized_by_maintainer_state_only` in `push_authorization.rs` for
1280/// an example of the fixture-first pattern.
1281///
1282/// ## Migration Guide
1283///
1284/// Instead of:
1285/// ```ignore
1286/// let setup = setup_repo_for_maintainer(client, git_data_dir, relay_domain).await?;
1287/// ```
1288///
1289/// Use:
1290/// ```ignore
1291/// let ctx = TestContext::new(client);
1292/// let _state_event = ctx.get_fixture(FixtureKind::RepoState).await?;
1293/// let _maintainer_state = ctx.get_fixture(FixtureKind::MaintainerState).await?;
1294/// // Then clone, create maintainer deterministic commit, and push inline
1295/// ```
1296///
1297/// ---
1298///
1299/// This performs all the common setup steps needed for maintainer push authorization tests:
1300/// 1. Gets RepoState fixture (owner's repo announcement + state event with owner's deterministic commit)
1301/// 2. Gets MaintainerState fixture (maintainer's state event ONLY - no announcement)
1302/// 3. Extracts repo_id and owner npub
1303/// 4. Verifies repo exists on disk
1304/// 5. Clones the repository using owner's npub
1305/// 6. Creates maintainer deterministic commit locally
1306/// 7. Verifies commit hash matches expected
1307/// 8. Creates and checks out main branch
1308/// 9. Pushes the commit so the grasp server has the state in the state event
1309///
1310/// Note: This does NOT publish a maintainer announcement. For tests that need the
1311/// maintainer announcement (like recursive maintainer tests), use setup_repo_for_recursive_maintainer
1312/// which publishes MaintainerAnnouncement separately.
1313///
1314/// Returns RepoSetup which auto-cleans up the clone_path on drop
1315#[deprecated(
1316 since = "0.1.0",
1317 note = "Use fixture-first pattern with TestContext and FixtureKind::MaintainerState instead. See test_push_authorized_by_maintainer_state_only for example."
1318)]
1319pub async fn setup_repo_for_maintainer(
1320 client: &crate::AuditClient,
1321 git_data_dir: &Path,
1322 relay_domain: &str,
1323) -> Result<RepoSetup, String> {
1324 use nostr_sdk::prelude::TagKind;
1325
1326 let ctx = TestContext::new(client);
1327
1328 // Get RepoState fixture (includes owner's repo announcement and state event with owner's deterministic commit)
1329 let state_event = ctx.get_fixture(FixtureKind::RepoState).await
1330 .map_err(|e| format!("Failed to create repo state fixture: {}", e))?;
1331
1332 // Get MaintainerState fixture ONLY (no announcement - tests state-only authorization)
1333 let _maintainer_state = ctx.get_fixture(FixtureKind::MaintainerState).await
1334 .map_err(|e| format!("Failed to create maintainer state fixture: {}", e))?;
1335
1336 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1337
1338 // Extract repo_id from state event
1339 let repo_id = state_event.tags.iter().find(|t| t.kind() == TagKind::d())
1340 .and_then(|t| t.content())
1341 .ok_or("Missing repo_id")?
1342 .to_string();
1343
1344 // The npub is from the owner keys (the signer of the state event)
1345 let npub = state_event.pubkey.to_bech32()
1346 .map_err(|e| format!("Failed to convert owner pubkey to bech32: {}", e))?;
1347
1348 // Verify repo exists
1349 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
1350 if !repo_path.exists() {
1351 return Err(format!("Owner repo not found: {}", repo_path.display()));
1352 }
1353
1354 // Clone repo using owner's npub
1355 let clone_path = clone_repo(relay_domain, &npub, &repo_id)?;
1356
1357 // Create maintainer deterministic commit locally (this will be the root commit with no parent)
1358 let commit_hash = create_deterministic_commit_with_variant(&clone_path, CommitVariant::Maintainer)
1359 .map_err(|e| {
1360 let _ = fs::remove_dir_all(&clone_path);
1361 e
1362 })?;
1363
1364 // Verify commit hash matches expected maintainer deterministic hash
1365 if commit_hash != MAINTAINER_DETERMINISTIC_COMMIT_HASH {
1366 let _ = fs::remove_dir_all(&clone_path);
1367 return Err(format!(
1368 "Maintainer commit hash mismatch: got {}, expected {}",
1369 commit_hash, MAINTAINER_DETERMINISTIC_COMMIT_HASH
1370 ));
1371 }
1372
1373 // Create main branch pointing to our deterministic commit
1374 let branch_output = Command::new("git")
1375 .args(["branch", "main"])
1376 .current_dir(&clone_path)
1377 .output()
1378 .map_err(|e| {
1379 let _ = fs::remove_dir_all(&clone_path);
1380 format!("Failed to create main branch: {}", e)
1381 })?;
1382
1383 if !branch_output.status.success() {
1384 let _ = fs::remove_dir_all(&clone_path);
1385 return Err(format!(
1386 "Failed to create main branch: {}",
1387 String::from_utf8_lossy(&branch_output.stderr)
1388 ));
1389 }
1390
1391 // Checkout main branch
1392 let checkout_output = Command::new("git")
1393 .args(["checkout", "main"])
1394 .current_dir(&clone_path)
1395 .output()
1396 .map_err(|e| {
1397 let _ = fs::remove_dir_all(&clone_path);
1398 format!("Failed to checkout main branch: {}", e)
1399 })?;
1400
1401 if !checkout_output.status.success() {
1402 let _ = fs::remove_dir_all(&clone_path);
1403 return Err(format!(
1404 "Failed to checkout main branch: {}",
1405 String::from_utf8_lossy(&checkout_output.stderr)
1406 ));
1407 }
1408
1409 // Push the commit to the server so the bare repo matches the state event
1410 let push_output = Command::new("git")
1411 .args(["push", "origin", "main"])
1412 .current_dir(&clone_path)
1413 .env("GIT_TERMINAL_PROMPT", "0")
1414 .output()
1415 .map_err(|e| {
1416 let _ = fs::remove_dir_all(&clone_path);
1417 format!("Failed to push to server: {}", e)
1418 })?;
1419
1420 if !push_output.status.success() {
1421 let _ = fs::remove_dir_all(&clone_path);
1422 return Err(format!(
1423 "Failed to push to server: {}",
1424 String::from_utf8_lossy(&push_output.stderr)
1425 ));
1426 }
1427
1428 Ok(RepoSetup {
1429 clone_path,
1430 repo_id,
1431 npub,
1432 commit_hash,
1433 })
1434}
1435
1436/// Set up a recursive maintainer repository with deterministic commit
1437///
1438/// # Deprecated
1439///
1440/// This function is deprecated in favor of the fixture-first pattern.
1441/// Tests should create their own TestContext and use the fixture chain directly,
1442/// following the Generate → Send → Verify pattern.
1443///
1444/// See `test_push_authorized_by_recursive_maintainer_state` in `push_authorization.rs` for
1445/// an example of the fixture-first pattern with recursive maintainers.
1446///
1447/// ## Migration Guide
1448///
1449/// Instead of:
1450/// ```ignore
1451/// let setup = setup_repo_for_recursive_maintainer(client, git_data_dir, relay_domain).await?;
1452/// ```
1453///
1454/// Use:
1455/// ```ignore
1456/// let ctx = TestContext::new(client);
1457/// let state_event = ctx.get_fixture(FixtureKind::RepoState).await?;
1458/// ctx.get_fixture(FixtureKind::MaintainerAnnouncement).await?;
1459/// ctx.get_fixture(FixtureKind::MaintainerState).await?;
1460/// ctx.get_fixture(FixtureKind::RecursiveMaintainerRepoAndState).await?;
1461/// // Then clone, create deterministic commit with RecursiveMaintainer variant, and push inline
1462/// ```
1463///
1464/// ---
1465///
1466/// This performs all the common setup steps needed for recursive maintainer push authorization tests:
1467/// 1. Gets RepoState fixture (owner's repo announcement + state event with owner's deterministic commit)
1468/// 2. Gets MaintainerAnnouncement fixture (maintainer's repo announcement with recursive maintainer in maintainers tag)
1469/// 3. Gets MaintainerState fixture (maintainer's state event)
1470/// 4. Gets RecursiveMaintainerRepoAndState fixture (recursive maintainer's repo - completes 3-level chain)
1471/// 5. Extracts repo_id and owner npub
1472/// 6. Verifies repo exists on disk
1473/// 7. Clones the repository using owner's npub
1474/// 8. Creates recursive maintainer deterministic commit locally
1475/// 9. Verifies commit hash matches expected
1476/// 10. Creates and checks out main branch
1477/// 11. Pushes the commit so the grasp server has the state in the state event
1478///
1479/// Returns RepoSetup which auto-cleans up the clone_path on drop
1480#[deprecated(
1481 since = "0.1.0",
1482 note = "Use fixture-first pattern with TestContext and fixture chain instead. See test_push_authorized_by_recursive_maintainer_state for example."
1483)]
1484pub async fn setup_repo_for_recursive_maintainer(
1485 client: &crate::AuditClient,
1486 git_data_dir: &Path,
1487 relay_domain: &str,
1488) -> Result<RepoSetup, String> {
1489 use nostr_sdk::prelude::TagKind;
1490
1491 let ctx = TestContext::new(client);
1492
1493 // Get RepoState fixture (includes owner's repo announcement and state event)
1494 let state_event = ctx.get_fixture(FixtureKind::RepoState).await
1495 .map_err(|e| format!("Failed to create repo state fixture: {}", e))?;
1496
1497 // Get MaintainerAnnouncement fixture (maintainer's repo announcement with recursive maintainer in maintainers tag)
1498 let _maintainer_announcement = ctx.get_fixture(FixtureKind::MaintainerAnnouncement).await
1499 .map_err(|e| format!("Failed to create maintainer announcement fixture: {}", e))?;
1500
1501 // Get MaintainerState fixture (maintainer's state event)
1502 let _maintainer_state = ctx.get_fixture(FixtureKind::MaintainerState).await
1503 .map_err(|e| format!("Failed to create maintainer state fixture: {}", e))?;
1504
1505 // Get RecursiveMaintainerRepoAndState fixture (completes 3-level delegation chain)
1506 let _recursive_maintainer_state = ctx.get_fixture(FixtureKind::RecursiveMaintainerRepoAndState).await
1507 .map_err(|e| format!("Failed to create recursive maintainer repo state fixture: {}", e))?;
1508
1509 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1510
1511 // Extract repo_id from owner's state event
1512 let repo_id = state_event.tags.iter().find(|t| t.kind() == TagKind::d())
1513 .and_then(|t| t.content())
1514 .ok_or("Missing repo_id")?
1515 .to_string();
1516
1517 // The npub is from the owner keys (the signer of the state event)
1518 let npub = state_event.pubkey.to_bech32()
1519 .map_err(|e| format!("Failed to convert owner pubkey to bech32: {}", e))?;
1520
1521 // Verify repo exists
1522 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
1523 if !repo_path.exists() {
1524 return Err(format!("Owner repo not found: {}", repo_path.display()));
1525 }
1526
1527 // Clone repo using owner's npub
1528 let clone_path = clone_repo(relay_domain, &npub, &repo_id)?;
1529
1530 // Create recursive maintainer deterministic commit locally (this will be the root commit with no parent)
1531 let commit_hash = create_deterministic_commit_with_variant(&clone_path, CommitVariant::RecursiveMaintainer)
1532 .map_err(|e| {
1533 let _ = fs::remove_dir_all(&clone_path);
1534 e
1535 })?;
1536
1537 // Verify commit hash matches expected recursive maintainer deterministic hash
1538 if commit_hash != RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH {
1539 let _ = fs::remove_dir_all(&clone_path);
1540 return Err(format!(
1541 "Recursive maintainer commit hash mismatch: got {}, expected {}",
1542 commit_hash, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH
1543 ));
1544 }
1545
1546 // Create main branch pointing to our deterministic commit
1547 let branch_output = Command::new("git")
1548 .args(["branch", "main"])
1549 .current_dir(&clone_path)
1550 .output()
1551 .map_err(|e| {
1552 let _ = fs::remove_dir_all(&clone_path);
1553 format!("Failed to create main branch: {}", e)
1554 })?;
1555
1556 if !branch_output.status.success() {
1557 let _ = fs::remove_dir_all(&clone_path);
1558 return Err(format!(
1559 "Failed to create main branch: {}",
1560 String::from_utf8_lossy(&branch_output.stderr)
1561 ));
1562 }
1563
1564 // Checkout main branch
1565 let checkout_output = Command::new("git")
1566 .args(["checkout", "main"])
1567 .current_dir(&clone_path)
1568 .output()
1569 .map_err(|e| {
1570 let _ = fs::remove_dir_all(&clone_path);
1571 format!("Failed to checkout main branch: {}", e)
1572 })?;
1573
1574 if !checkout_output.status.success() {
1575 let _ = fs::remove_dir_all(&clone_path);
1576 return Err(format!(
1577 "Failed to checkout main branch: {}",
1578 String::from_utf8_lossy(&checkout_output.stderr)
1579 ));
1580 }
1581
1582 // Push the commit to the server so the bare repo matches the state event
1583 let push_output = Command::new("git")
1584 .args(["push", "origin", "main"])
1585 .current_dir(&clone_path)
1586 .env("GIT_TERMINAL_PROMPT", "0")
1587 .output()
1588 .map_err(|e| {
1589 let _ = fs::remove_dir_all(&clone_path);
1590 format!("Failed to push to server: {}", e)
1591 })?;
1592
1593 if !push_output.status.success() {
1594 let _ = fs::remove_dir_all(&clone_path);
1595 return Err(format!(
1596 "Failed to push to server: {}",
1597 String::from_utf8_lossy(&push_output.stderr)
1598 ));
1599 }
1600
1601 Ok(RepoSetup {
1602 clone_path,
1603 repo_id,
1604 npub,
1605 commit_hash,
1606 })
1607}
1608
1609/// Attempt a git push and return success/failure 1089/// Attempt a git push and return success/failure
1610/// 1090///
1611/// # Arguments 1091/// # Arguments
diff --git a/grasp-audit/src/lib.rs b/grasp-audit/src/lib.rs
index 2d5531b..5eb9b71 100644
--- a/grasp-audit/src/lib.rs
+++ b/grasp-audit/src/lib.rs
@@ -43,10 +43,8 @@ pub use fixtures::{
43 try_push, 43 try_push,
44 // Verification helpers 44 // Verification helpers
45 send_and_verify_accepted, send_and_verify_rejected, 45 send_and_verify_accepted, send_and_verify_rejected,
46 // Repo setup helpers
47 setup_repo_for_maintainer, setup_repo_for_recursive_maintainer, setup_repo_with_deterministic_commit,
48 // Types and constants 46 // Types and constants
49 CommitVariant, ContextMode, FixtureKind, RepoSetup, TestContext, 47 CommitVariant, ContextMode, FixtureKind, TestContext,
50 DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH, 48 DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH,
51 RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, 49 RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH,
52}; 50};
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs
index f1d6970..fad77fb 100644
--- a/grasp-audit/src/specs/grasp01/push_authorization.rs
+++ b/grasp-audit/src/specs/grasp01/push_authorization.rs
@@ -18,9 +18,9 @@
18 18
19use crate::{ 19use crate::{
20 clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, 20 clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant,
21 setup_repo_with_deterministic_commit, try_push, 21 try_push, AuditClient, CommitVariant, FixtureKind, TestContext, TestResult,
22 AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, DETERMINISTIC_COMMIT_HASH, 22 DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH,
23 MAINTAINER_DETERMINISTIC_COMMIT_HASH, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, 23 RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH,
24}; 24};
25use nostr_sdk::prelude::*; 25use nostr_sdk::prelude::*;
26use std::fs; 26use std::fs;
@@ -258,50 +258,202 @@ impl PushAuthorizationTests {
258 258
259 /// Test that push is rejected when commit doesn't match state event 259 /// Test that push is rejected when commit doesn't match state event
260 /// 260 ///
261 /// This test verifies that the relay enforces state event authorization. 261 /// GRASP-01: "MUST accept pushes via this service that match the latest repo state announcement"
262 /// The state event (from fixture) points to the deterministic commit which is 262 /// (Conversely, MUST reject pushes that don't match)
263 /// already on the server. We create a new commit locally and try to push it. 263 ///
264 /// The push should be rejected because the new commit doesn't match what the 264 /// ## Fixture-First Pattern
265 /// state event announces. 265 ///
266 /// 1. **Generate**: Create TestContext and get RepoState fixture
267 /// (repo announcement + state event pointing to deterministic commit)
268 /// 2. **Send**: Clone repo, create deterministic commit, push (establishes state on relay)
269 /// 3. **Test**: Create a NEW commit locally, try to push
270 /// 4. **Verify**: Push should be rejected because new commit doesn't match state event
266 pub async fn test_push_rejected_wrong_commit( 271 pub async fn test_push_rejected_wrong_commit(
267 client: &AuditClient, 272 client: &AuditClient,
268 git_data_dir: &Path, 273 git_data_dir: &Path,
269 relay_domain: &str, 274 relay_domain: &str,
270 ) -> TestResult { 275 ) -> TestResult {
276 use std::process::Command;
277
271 let test_name = "test_push_rejected_wrong_commit"; 278 let test_name = "test_push_rejected_wrong_commit";
272 279
273 // Set up repository with deterministic commit 280 // ============================================================
274 // This creates a state event pointing to DETERMINISTIC_COMMIT_HASH and pushes that commit 281 // Step 1: GENERATE - Create TestContext and get RepoState fixture
275 let setup = match setup_repo_with_deterministic_commit(client, git_data_dir, relay_domain).await { 282 // ============================================================
276 Ok(s) => s, 283 let ctx = TestContext::new(client);
284
285 let state_event = match ctx.get_fixture(FixtureKind::RepoState).await {
286 Ok(e) => e,
277 Err(e) => { 287 Err(e) => {
278 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 288 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
279 .fail(&format!("Setup failed: {}", e)) 289 .fail(&format!("Failed to create RepoState fixture: {}", e));
290 }
291 };
292
293 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
294
295 // Extract repo_id and npub from state event
296 let repo_id = match state_event
297 .tags
298 .iter()
299 .find(|t| t.kind() == TagKind::d())
300 .and_then(|t| t.content())
301 {
302 Some(id) => id.to_string(),
303 None => {
304 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
305 .fail("Missing repo_id in state event");
306 }
307 };
308
309 let npub = match state_event.pubkey.to_bech32() {
310 Ok(n) => n,
311 Err(e) => {
312 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
313 .fail(&format!("Failed to convert pubkey to bech32: {}", e));
280 } 314 }
281 }; 315 };
282 316
283 // Create a new commit locally - this is NOT announced in any state event 317 // Verify repo exists on disk
284 let new_commit = match create_commit(&setup.clone_path, "Unauthorized commit") { 318 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
319 if !repo_path.exists() {
320 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
321 .fail(&format!("Repo not found: {}", repo_path.display()));
322 }
323
324 // ============================================================
325 // Step 2: SEND - Clone repo, create deterministic commit, push
326 // (establishes the state on the relay)
327 // ============================================================
328 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
329 Ok(p) => p,
330 Err(e) => {
331 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
332 .fail(&format!("Failed to clone repo: {}", e));
333 }
334 };
335
336 // Cleanup helper
337 let cleanup = || {
338 let _ = fs::remove_dir_all(&clone_path);
339 };
340
341 // Create deterministic commit locally
342 let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") {
285 Ok(h) => h, 343 Ok(h) => h,
286 Err(e) => { 344 Err(e) => {
345 cleanup();
287 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 346 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
288 .fail(&format!("Failed to create commit: {}", e)) 347 .fail(&format!("Failed to create deterministic commit: {}", e));
289 } 348 }
290 }; 349 };
291 350
292 // Try to push the new commit 351 // Verify commit hash matches expected
293 // This should be REJECTED because: 352 if commit_hash != DETERMINISTIC_COMMIT_HASH {
294 // - The state event still points to the deterministic commit (setup.commit_hash) 353 cleanup();
295 // - We're trying to push new_commit which is different 354 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
296 // - The relay MUST reject pushes that don't match the announced state 355 .fail(&format!(
297 let push_result = try_push(&setup.clone_path); 356 "Commit hash mismatch: got {}, expected {}",
357 commit_hash, DETERMINISTIC_COMMIT_HASH
358 ));
359 }
360
361 // Create main branch pointing to our deterministic commit
362 let branch_output = Command::new("git")
363 .args(["branch", "main"])
364 .current_dir(&clone_path)
365 .output();
366
367 match branch_output {
368 Err(e) => {
369 cleanup();
370 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
371 .fail(&format!("Failed to create main branch: {}", e));
372 }
373 Ok(output) if !output.status.success() => {
374 cleanup();
375 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
376 .fail(&format!(
377 "Failed to create main branch: {}",
378 String::from_utf8_lossy(&output.stderr)
379 ));
380 }
381 _ => {}
382 }
383
384 // Checkout main branch
385 let checkout_output = Command::new("git")
386 .args(["checkout", "main"])
387 .current_dir(&clone_path)
388 .output();
389
390 match checkout_output {
391 Err(e) => {
392 cleanup();
393 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
394 .fail(&format!("Failed to checkout main branch: {}", e));
395 }
396 Ok(output) if !output.status.success() => {
397 cleanup();
398 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
399 .fail(&format!(
400 "Failed to checkout main branch: {}",
401 String::from_utf8_lossy(&output.stderr)
402 ));
403 }
404 _ => {}
405 }
406
407 // Push the deterministic commit to establish state on relay
408 let push_output = Command::new("git")
409 .args(["push", "origin", "main"])
410 .current_dir(&clone_path)
411 .env("GIT_TERMINAL_PROMPT", "0")
412 .output();
413
414 match push_output {
415 Err(e) => {
416 cleanup();
417 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
418 .fail(&format!("Failed to push initial commit: {}", e));
419 }
420 Ok(output) if !output.status.success() => {
421 cleanup();
422 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
423 .fail(&format!(
424 "Failed to push initial commit: {}",
425 String::from_utf8_lossy(&output.stderr)
426 ));
427 }
428 _ => {}
429 }
430
431 // ============================================================
432 // Step 3: TEST - Create a NEW commit that is NOT announced
433 // in any state event
434 // ============================================================
435 let new_commit = match create_commit(&clone_path, "Unauthorized commit") {
436 Ok(h) => h,
437 Err(e) => {
438 cleanup();
439 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
440 .fail(&format!("Failed to create commit: {}", e));
441 }
442 };
443
444 // ============================================================
445 // Step 4: VERIFY - Push should be rejected because new commit
446 // doesn't match state event
447 // ============================================================
448 let push_result = try_push(&clone_path);
449 cleanup();
298 450
299 match push_result { 451 match push_result {
300 Ok(false) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").pass(), 452 Ok(false) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").pass(),
301 Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 453 Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
302 .fail(&format!( 454 .fail(&format!(
303 "Push accepted but should be rejected. State event points to {}, but pushed {}", 455 "Push accepted but should be rejected. State event points to {}, but pushed {}",
304 setup.commit_hash, new_commit 456 DETERMINISTIC_COMMIT_HASH, new_commit
305 )), 457 )),
306 Err(e) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").fail(&e), 458 Err(e) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").fail(&e),
307 } 459 }
@@ -826,32 +978,187 @@ impl PushAuthorizationTests {
826 978
827 /// Test that non-maintainer state event is ignored 979 /// Test that non-maintainer state event is ignored
828 /// 980 ///
829 /// This test verifies that the relay ignores state events from non-maintainers. 981 /// GRASP-01: "respecting the recursive maintainer set"
830 /// We set up a valid repo, then create a rogue state event signed by a different 982 /// (Conversely, state events from non-maintainers MUST be ignored)
831 /// keypair (not the repo maintainer) that announces a different commit. The push 983 ///
832 /// should be rejected because the rogue state event is not authorized. 984 /// ## Fixture-First Pattern
985 ///
986 /// 1. **Generate**: Create TestContext and get RepoState fixture
987 /// (repo announcement + state event pointing to deterministic commit)
988 /// 2. **Send**: Clone repo, create deterministic commit, push (establishes state on relay)
989 /// 3. **Attack**: Create a rogue state event signed by a non-maintainer
990 /// 4. **Test**: Create a new commit and try to push
991 /// 5. **Verify**: Push should be rejected because rogue state event is ignored
833 pub async fn test_non_maintainer_state_rejected( 992 pub async fn test_non_maintainer_state_rejected(
834 client: &AuditClient, 993 client: &AuditClient,
835 git_data_dir: &Path, 994 git_data_dir: &Path,
836 relay_domain: &str, 995 relay_domain: &str,
837 ) -> TestResult { 996 ) -> TestResult {
997 use std::process::Command;
998
838 let test_name = "test_non_maintainer_state_rejected"; 999 let test_name = "test_non_maintainer_state_rejected";
839 1000
840 // Set up repository with deterministic commit (signed by maintainer) 1001 // ============================================================
841 let setup = match setup_repo_with_deterministic_commit(client, git_data_dir, relay_domain).await { 1002 // Step 1: GENERATE - Create TestContext and get RepoState fixture
842 Ok(s) => s, 1003 // ============================================================
1004 let ctx = TestContext::new(client);
1005
1006 let state_event = match ctx.get_fixture(FixtureKind::RepoState).await {
1007 Ok(e) => e,
1008 Err(e) => {
1009 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1010 .fail(&format!("Failed to create RepoState fixture: {}", e));
1011 }
1012 };
1013
1014 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1015
1016 // Extract repo_id and npub from state event
1017 let repo_id = match state_event
1018 .tags
1019 .iter()
1020 .find(|t| t.kind() == TagKind::d())
1021 .and_then(|t| t.content())
1022 {
1023 Some(id) => id.to_string(),
1024 None => {
1025 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1026 .fail("Missing repo_id in state event");
1027 }
1028 };
1029
1030 let npub = match state_event.pubkey.to_bech32() {
1031 Ok(n) => n,
1032 Err(e) => {
1033 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1034 .fail(&format!("Failed to convert pubkey to bech32: {}", e));
1035 }
1036 };
1037
1038 // Verify repo exists on disk
1039 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
1040 if !repo_path.exists() {
1041 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1042 .fail(&format!("Repo not found: {}", repo_path.display()));
1043 }
1044
1045 // ============================================================
1046 // Step 2: SEND - Clone repo, create deterministic commit, push
1047 // (establishes the state on the relay)
1048 // ============================================================
1049 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
1050 Ok(p) => p,
1051 Err(e) => {
1052 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1053 .fail(&format!("Failed to clone repo: {}", e));
1054 }
1055 };
1056
1057 // Cleanup helper
1058 let cleanup = || {
1059 let _ = fs::remove_dir_all(&clone_path);
1060 };
1061
1062 // Create deterministic commit locally
1063 let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") {
1064 Ok(h) => h,
843 Err(e) => { 1065 Err(e) => {
1066 cleanup();
844 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1067 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
845 .fail(&format!("Setup failed: {}", e)) 1068 .fail(&format!("Failed to create deterministic commit: {}", e));
846 } 1069 }
847 }; 1070 };
848 1071
849 // Create a new commit locally that we want to push 1072 // Verify commit hash matches expected
850 let new_commit = match create_commit(&setup.clone_path, "New commit to push") { 1073 if commit_hash != DETERMINISTIC_COMMIT_HASH {
1074 cleanup();
1075 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1076 .fail(&format!(
1077 "Commit hash mismatch: got {}, expected {}",
1078 commit_hash, DETERMINISTIC_COMMIT_HASH
1079 ));
1080 }
1081
1082 // Create main branch pointing to our deterministic commit
1083 let branch_output = Command::new("git")
1084 .args(["branch", "main"])
1085 .current_dir(&clone_path)
1086 .output();
1087
1088 match branch_output {
1089 Err(e) => {
1090 cleanup();
1091 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1092 .fail(&format!("Failed to create main branch: {}", e));
1093 }
1094 Ok(output) if !output.status.success() => {
1095 cleanup();
1096 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1097 .fail(&format!(
1098 "Failed to create main branch: {}",
1099 String::from_utf8_lossy(&output.stderr)
1100 ));
1101 }
1102 _ => {}
1103 }
1104
1105 // Checkout main branch
1106 let checkout_output = Command::new("git")
1107 .args(["checkout", "main"])
1108 .current_dir(&clone_path)
1109 .output();
1110
1111 match checkout_output {
1112 Err(e) => {
1113 cleanup();
1114 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1115 .fail(&format!("Failed to checkout main branch: {}", e));
1116 }
1117 Ok(output) if !output.status.success() => {
1118 cleanup();
1119 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1120 .fail(&format!(
1121 "Failed to checkout main branch: {}",
1122 String::from_utf8_lossy(&output.stderr)
1123 ));
1124 }
1125 _ => {}
1126 }
1127
1128 // Push the deterministic commit to establish state on relay
1129 let push_output = Command::new("git")
1130 .args(["push", "origin", "main"])
1131 .current_dir(&clone_path)
1132 .env("GIT_TERMINAL_PROMPT", "0")
1133 .output();
1134
1135 match push_output {
1136 Err(e) => {
1137 cleanup();
1138 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1139 .fail(&format!("Failed to push initial commit: {}", e));
1140 }
1141 Ok(output) if !output.status.success() => {
1142 cleanup();
1143 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1144 .fail(&format!(
1145 "Failed to push initial commit: {}",
1146 String::from_utf8_lossy(&output.stderr)
1147 ));
1148 }
1149 _ => {}
1150 }
1151
1152 // ============================================================
1153 // Step 3: ATTACK - Create a new commit and a rogue state event
1154 // from a non-maintainer
1155 // ============================================================
1156 let new_commit = match create_commit(&clone_path, "New commit to push") {
851 Ok(h) => h, 1157 Ok(h) => h,
852 Err(e) => { 1158 Err(e) => {
1159 cleanup();
853 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1160 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
854 .fail(&format!("Failed to create commit: {}", e)) 1161 .fail(&format!("Failed to create commit: {}", e));
855 } 1162 }
856 }; 1163 };
857 1164
@@ -862,7 +1169,7 @@ impl PushAuthorizationTests {
862 // This event has the correct repo_id but is signed by a non-maintainer 1169 // This event has the correct repo_id but is signed by a non-maintainer
863 let rogue_state = match client 1170 let rogue_state = match client
864 .event_builder(Kind::Custom(30618), "") 1171 .event_builder(Kind::Custom(30618), "")
865 .tag(Tag::identifier(&setup.repo_id)) 1172 .tag(Tag::identifier(&repo_id))
866 .tag(Tag::custom( 1173 .tag(Tag::custom(
867 TagKind::custom("refs/heads/main"), 1174 TagKind::custom("refs/heads/main"),
868 vec![new_commit.clone()], 1175 vec![new_commit.clone()],
@@ -871,13 +1178,15 @@ impl PushAuthorizationTests {
871 { 1178 {
872 Ok(e) => e, 1179 Ok(e) => e,
873 Err(e) => { 1180 Err(e) => {
1181 cleanup();
874 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1182 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
875 .fail(&format!("Failed to build rogue state event: {}", e)) 1183 .fail(&format!("Failed to build rogue state event: {}", e));
876 } 1184 }
877 }; 1185 };
878 1186
879 // Send the rogue state event using the raw client to bypass AuditClient's key check 1187 // Send the rogue state event using the raw client to bypass AuditClient's key check
880 if let Err(e) = client.client().send_event(&rogue_state).await { 1188 if let Err(e) = client.client().send_event(&rogue_state).await {
1189 cleanup();
881 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1190 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
882 .fail(&format!("Failed to send rogue state event: {}", e)); 1191 .fail(&format!("Failed to send rogue state event: {}", e));
883 } 1192 }
@@ -885,14 +1194,12 @@ impl PushAuthorizationTests {
885 // Wait for event to propagate 1194 // Wait for event to propagate
886 tokio::time::sleep(std::time::Duration::from_millis(200)).await; 1195 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
887 1196
888 // Try to push the new commit 1197 // ============================================================
889 // This should be REJECTED because: 1198 // Step 4 & 5: VERIFY - Push should be rejected because rogue
890 // - The rogue state event announces new_commit 1199 // state event is ignored
891 // - But the rogue state event is NOT signed by the maintainer 1200 // ============================================================
892 // - The relay should ignore the rogue state event 1201 let push_result = try_push(&clone_path);
893 // - The valid state event (from setup) still points to the deterministic commit 1202 cleanup();
894 // - Therefore pushing new_commit should fail
895 let push_result = try_push(&setup.clone_path);
896 1203
897 match push_result { 1204 match push_result {
898 Ok(false) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").pass(), 1205 Ok(false) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").pass(),