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-28 12:40:31 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-28 12:40:31 +0000
commitb94262161df99966fbb8aa6861fb46603039111f (patch)
treef0194656783d05263be2d940f4e182b1bec75070 /grasp-audit/src
parentbf51a082ad54815f108bb255cf258fcae4a9bb4f (diff)
allow push to ref/nostr/<event-id>
Diffstat (limited to 'grasp-audit/src')
-rw-r--r--grasp-audit/src/fixtures.rs38
-rw-r--r--grasp-audit/src/lib.rs2
-rw-r--r--grasp-audit/src/specs/grasp01/push_authorization.rs242
3 files changed, 280 insertions, 2 deletions
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs
index 8cee964..b6fbc79 100644
--- a/grasp-audit/src/fixtures.rs
+++ b/grasp-audit/src/fixtures.rs
@@ -1208,6 +1208,44 @@ pub fn try_push(clone_path: &Path) -> Result<bool, String> {
1208 Ok(output.status.success()) 1208 Ok(output.status.success())
1209} 1209}
1210 1210
1211/// Attempt a git push to a specific ref and return success/failure
1212///
1213/// This is used for testing refs/nostr/<event-id> push validation.
1214///
1215/// # Arguments
1216/// * `clone_path` - Path to the git repository
1217/// * `ref_name` - The ref to push to (e.g., "refs/nostr/<event-id>")
1218///
1219/// # Returns
1220/// * `Ok(true)` - Push succeeded
1221/// * `Ok(false)` - Push was rejected
1222/// * `Err(String)` - Error executing git push
1223///
1224/// # Example
1225/// ```no_run
1226/// # use grasp_audit::*;
1227/// # use std::path::Path;
1228/// # fn example() -> Result<(), String> {
1229/// let success = try_push_to_ref(Path::new("/tmp/my-repo"), "refs/nostr/abc123")?;
1230/// if success {
1231/// println!("Push to refs/nostr/abc123 succeeded");
1232/// } else {
1233/// println!("Push was rejected");
1234/// }
1235/// # Ok(())
1236/// # }
1237/// ```
1238pub fn try_push_to_ref(clone_path: &Path, ref_name: &str) -> Result<bool, String> {
1239 let output = Command::new("git")
1240 .args(["push", "origin", &format!("HEAD:{}", ref_name)])
1241 .current_dir(clone_path)
1242 .env("GIT_TERMINAL_PROMPT", "0")
1243 .output()
1244 .map_err(|e| format!("Failed to execute git push: {}", e))?;
1245
1246 Ok(output.status.success())
1247}
1248
1211#[cfg(test)] 1249#[cfg(test)]
1212mod tests { 1250mod tests {
1213 use super::*; 1251 use super::*;
diff --git a/grasp-audit/src/lib.rs b/grasp-audit/src/lib.rs
index 3d11bd8..5ee93b3 100644
--- a/grasp-audit/src/lib.rs
+++ b/grasp-audit/src/lib.rs
@@ -40,7 +40,7 @@ pub use client::AuditClient;
40pub use fixtures::{ 40pub use fixtures::{
41 // Git operation helpers 41 // Git operation helpers
42 clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, 42 clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant,
43 try_push, 43 try_push, try_push_to_ref,
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 // Types and constants 46 // Types and constants
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs
index 0a5b1ec..0170f6e 100644
--- a/grasp-audit/src/specs/grasp01/push_authorization.rs
+++ b/grasp-audit/src/specs/grasp01/push_authorization.rs
@@ -18,7 +18,7 @@
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 try_push, AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, 21 try_push, try_push_to_ref, AuditClient, CommitVariant, FixtureKind, TestContext, TestResult,
22 DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH, 22 DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH,
23 RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, 23 RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH,
24}; 24};
@@ -41,6 +41,8 @@ impl PushAuthorizationTests {
41 results.add(Self::test_push_rejected_wrong_commit(client, relay_domain).await); 41 results.add(Self::test_push_rejected_wrong_commit(client, relay_domain).await);
42 results.add(Self::test_push_authorized_by_maintainer_state_only(client, relay_domain).await); 42 results.add(Self::test_push_authorized_by_maintainer_state_only(client, relay_domain).await);
43 results.add(Self::test_push_authorized_by_recursive_maintainer_state(client, relay_domain).await); 43 results.add(Self::test_push_authorized_by_recursive_maintainer_state(client, relay_domain).await);
44 results.add(Self::test_push_to_refs_nostr_valid_event_id(client, relay_domain).await);
45 results.add(Self::test_push_to_refs_nostr_invalid_event_id(client, relay_domain).await);
44 46
45 results 47 results
46 } 48 }
@@ -1029,6 +1031,244 @@ impl PushAuthorizationTests {
1029 Err(e) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").fail(&e), 1031 Err(e) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").fail(&e),
1030 } 1032 }
1031 } 1033 }
1034
1035 /// Test that push to refs/nostr/<event-id> succeeds with valid EventId format
1036 ///
1037 /// GRASP-01: "MUST accept pushes via this service to `refs/nostr/<event-id>`"
1038 /// The event_id must parse as a valid rust-nostr EventId (64-char hex string).
1039 /// This does NOT require the ref to be listed in any state event - it's purely format validation.
1040 ///
1041 /// ## Fixture-First Pattern
1042 ///
1043 /// 1. **Generate**: Create repo with ValidRepo fixture (no state event needed)
1044 /// 2. **Send**: Clone repo, create commit, push to refs/nostr/<valid-event-id>
1045 /// 3. **Verify**: Push should succeed because event-id format is valid
1046 pub async fn test_push_to_refs_nostr_valid_event_id(
1047 client: &AuditClient,
1048 relay_domain: &str,
1049 ) -> TestResult {
1050 let test_name = "test_push_to_refs_nostr_valid_event_id";
1051
1052 // ============================================================
1053 // Step 1: GENERATE - Create repo (no state event needed for refs/nostr/)
1054 // ============================================================
1055 let ctx = TestContext::new(client);
1056
1057 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
1058 Ok(r) => r,
1059 Err(e) => {
1060 return TestResult::new(
1061 test_name,
1062 "GRASP-01",
1063 "Push to refs/nostr/<valid-event-id> accepted",
1064 )
1065 .fail(&format!("Failed to create repo: {}", e));
1066 }
1067 };
1068
1069 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1070
1071 let repo_id = repo
1072 .tags
1073 .iter()
1074 .find(|t| t.kind() == TagKind::d())
1075 .and_then(|t| t.content())
1076 .unwrap()
1077 .to_string();
1078 let npub = repo.pubkey.to_bech32().unwrap();
1079
1080 // ============================================================
1081 // Step 2: SEND - Clone repo, create commit, push to refs/nostr/<event-id>
1082 // ============================================================
1083 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
1084 Ok(p) => p,
1085 Err(e) => {
1086 return TestResult::new(
1087 test_name,
1088 "GRASP-01",
1089 "Push to refs/nostr/<valid-event-id> accepted",
1090 )
1091 .fail(&e);
1092 }
1093 };
1094 let cleanup = || {
1095 let _ = fs::remove_dir_all(&clone_path);
1096 };
1097
1098 // Create a unique commit
1099 if let Err(e) = create_commit(&clone_path, "Test commit for refs/nostr push") {
1100 cleanup();
1101 return TestResult::new(
1102 test_name,
1103 "GRASP-01",
1104 "Push to refs/nostr/<valid-event-id> accepted",
1105 )
1106 .fail(&e);
1107 }
1108
1109 // Generate a random event to get a valid EventId
1110 let keys = Keys::generate();
1111 let event = match EventBuilder::text_note("test")
1112 .sign(&keys)
1113 .await
1114 {
1115 Ok(e) => e,
1116 Err(e) => {
1117 cleanup();
1118 return TestResult::new(
1119 test_name,
1120 "GRASP-01",
1121 "Push to refs/nostr/<valid-event-id> accepted",
1122 )
1123 .fail(&format!("Failed to create test event: {}", e));
1124 }
1125 };
1126
1127 // Use the event id as the refs/nostr/ target
1128 let ref_name = format!("refs/nostr/{}", event.id);
1129
1130 // ============================================================
1131 // Step 3: VERIFY - Push should succeed with valid event-id format
1132 // ============================================================
1133 let push_result = try_push_to_ref(&clone_path, &ref_name);
1134 cleanup();
1135
1136 match push_result {
1137 Ok(true) => TestResult::new(
1138 test_name,
1139 "GRASP-01",
1140 "Push to refs/nostr/<valid-event-id> accepted",
1141 )
1142 .pass(),
1143 Ok(false) => TestResult::new(
1144 test_name,
1145 "GRASP-01",
1146 "Push to refs/nostr/<valid-event-id> accepted",
1147 )
1148 .fail(&format!(
1149 "Push to {} was rejected but should be accepted. \
1150 The event-id '{}' is a valid 64-character hex string (EventId format).",
1151 ref_name, event.id
1152 )),
1153 Err(e) => TestResult::new(
1154 test_name,
1155 "GRASP-01",
1156 "Push to refs/nostr/<valid-event-id> accepted",
1157 )
1158 .fail(&format!("Push error: {}", e)),
1159 }
1160 }
1161
1162 /// Test that push to refs/nostr/<invalid> is rejected with invalid EventId format
1163 ///
1164 /// GRASP-01: "MUST accept pushes via this service to `refs/nostr/<event-id>`"
1165 /// The event_id must parse as a valid rust-nostr EventId (64-char hex string).
1166 /// Invalid formats (too short, non-hex, etc.) should be rejected.
1167 ///
1168 /// ## Fixture-First Pattern
1169 ///
1170 /// 1. **Generate**: Create repo with ValidRepo fixture (no state event needed)
1171 /// 2. **Send**: Clone repo, create commit, try to push to refs/nostr/123 (invalid)
1172 /// 3. **Verify**: Push should be rejected because event-id format is invalid
1173 pub async fn test_push_to_refs_nostr_invalid_event_id(
1174 client: &AuditClient,
1175 relay_domain: &str,
1176 ) -> TestResult {
1177 let test_name = "test_push_to_refs_nostr_invalid_event_id";
1178
1179 // ============================================================
1180 // Step 1: GENERATE - Create repo (no state event needed for refs/nostr/)
1181 // ============================================================
1182 let ctx = TestContext::new(client);
1183
1184 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
1185 Ok(r) => r,
1186 Err(e) => {
1187 return TestResult::new(
1188 test_name,
1189 "GRASP-01",
1190 "Push to refs/nostr/<invalid-event-id> rejected",
1191 )
1192 .fail(&format!("Failed to create repo: {}", e));
1193 }
1194 };
1195
1196 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1197
1198 let repo_id = repo
1199 .tags
1200 .iter()
1201 .find(|t| t.kind() == TagKind::d())
1202 .and_then(|t| t.content())
1203 .unwrap()
1204 .to_string();
1205 let npub = repo.pubkey.to_bech32().unwrap();
1206
1207 // ============================================================
1208 // Step 2: SEND - Clone repo, create commit, try push to invalid ref
1209 // ============================================================
1210 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
1211 Ok(p) => p,
1212 Err(e) => {
1213 return TestResult::new(
1214 test_name,
1215 "GRASP-01",
1216 "Push to refs/nostr/<invalid-event-id> rejected",
1217 )
1218 .fail(&e);
1219 }
1220 };
1221 let cleanup = || {
1222 let _ = fs::remove_dir_all(&clone_path);
1223 };
1224
1225 // Create a unique commit
1226 if let Err(e) = create_commit(&clone_path, "Test commit for invalid refs/nostr push") {
1227 cleanup();
1228 return TestResult::new(
1229 test_name,
1230 "GRASP-01",
1231 "Push to refs/nostr/<invalid-event-id> rejected",
1232 )
1233 .fail(&e);
1234 }
1235
1236 // Use an invalid event-id (too short, not a valid 64-char hex)
1237 let invalid_event_id = "123";
1238 let ref_name = format!("refs/nostr/{}", invalid_event_id);
1239
1240 // ============================================================
1241 // Step 3: VERIFY - Push should be rejected with invalid event-id format
1242 // ============================================================
1243 let push_result = try_push_to_ref(&clone_path, &ref_name);
1244 cleanup();
1245
1246 match push_result {
1247 Ok(false) => TestResult::new(
1248 test_name,
1249 "GRASP-01",
1250 "Push to refs/nostr/<invalid-event-id> rejected",
1251 )
1252 .pass(),
1253 Ok(true) => TestResult::new(
1254 test_name,
1255 "GRASP-01",
1256 "Push to refs/nostr/<invalid-event-id> rejected",
1257 )
1258 .fail(&format!(
1259 "Push to {} was accepted but should be rejected. \
1260 The event-id '{}' is NOT a valid 64-character hex string (EventId format). \
1261 The relay should reject pushes to refs/nostr/ with invalid event-id format.",
1262 ref_name, invalid_event_id
1263 )),
1264 Err(e) => TestResult::new(
1265 test_name,
1266 "GRASP-01",
1267 "Push to refs/nostr/<invalid-event-id> rejected",
1268 )
1269 .fail(&format!("Push error: {}", e)),
1270 }
1271 }
1032} 1272}
1033 1273
1034#[cfg(test)] 1274#[cfg(test)]