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-05 13:37:26 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-05 13:37:26 +0000
commitebdf1779697e1b3d0fa0e1068af4340a16eabf2b (patch)
tree1a59fc412346274e5ee1538b976166720644e30d /grasp-audit/src
parent5f137994850773114d8a4f8ba70f34aaf2eb1992 (diff)
feat(grasp-audit): implement tests #3 and #4 for GRASP-01 compliance
- Add test_reject_repo_announcement_missing_relays_tag (test #3) Verifies that repo announcements without required 'relays' tag are rejected - Add test_accept_valid_repo_state_announcement (test #4) Verifies that valid repo state announcements with all required tags are accepted Both tests verified passing with test-ngit-relay.sh
Diffstat (limited to 'grasp-audit/src')
-rw-r--r--grasp-audit/src/specs/grasp01_nostr_relay.rs147
1 files changed, 123 insertions, 24 deletions
diff --git a/grasp-audit/src/specs/grasp01_nostr_relay.rs b/grasp-audit/src/specs/grasp01_nostr_relay.rs
index 837434b..19322cb 100644
--- a/grasp-audit/src/specs/grasp01_nostr_relay.rs
+++ b/grasp-audit/src/specs/grasp01_nostr_relay.rs
@@ -212,16 +212,55 @@ impl Grasp01NostrRelayTests {
212 "Reject repository announcements without service in relays tag", 212 "Reject repository announcements without service in relays tag",
213 ) 213 )
214 .run(|| async { 214 .run(|| async {
215 // TODO: Implementation 215 // Get relay URL from client
216 // 1. Create kind 30617 event with: 216 let relay_url = client.client().relays().await
217 // - d tag: "test-repo-no-relays" 217 .keys()
218 // - clone tag: "{service_url}/{npub}/test-repo.git" (correct) 218 .next()
219 // - relays tag: "wss://relay.damus.io" (NOT this service) 219 .ok_or("No relay connected - client has no active relay connections")?
220 // 2. Send event to relay 220 .to_string();
221 // 3. Verify rejection
222 // 4. Query to confirm event is NOT in relay
223 221
224 Err("Not implemented yet".to_string()) 222 // Convert WebSocket URL to HTTP URL for clone tag
223 let http_url = relay_url
224 .replace("ws://", "http://")
225 .replace("wss://", "https://");
226
227 // Create unique repository identifier
228 let timestamp = Timestamp::now().as_u64();
229 let repo_id = format!("test-repo-no-relays-{}", timestamp);
230
231 // Create repo announcement WITHOUT service in relays tag
232 let event = client.event_builder(Kind::GitRepoAnnouncement, "")
233 .tag(Tag::identifier(&repo_id))
234 .tag(Tag::custom(TagKind::custom("name"), vec!["Test Repo No Relays"]))
235 .tag(Tag::custom(TagKind::custom("clone"), vec![format!("{}/{}/test-repo.git", http_url, client.public_key())])) // Correct clone
236 .tag(Tag::custom(TagKind::custom("relays"), vec!["wss://relay.damus.io"])) // NOT this service
237 .build(client.keys())
238 .map_err(|e| format!("Failed to build event: {}", e))?;
239
240 let event_id = event.id;
241
242 // Send event - expect rejection
243 let _send_result = client.send_event(event.clone()).await;
244
245 // Query to verify event is NOT stored
246 let filter = Filter::new()
247 .kind(Kind::GitRepoAnnouncement)
248 .author(client.public_key())
249 .identifier(&repo_id);
250
251 let events = client.query(filter).await
252 .map_err(|e| format!("Failed to query events from relay: {}", e))?;
253
254 // Verify event was rejected (not stored)
255 if events.iter().any(|e| e.id == event_id) {
256 return Err(format!(
257 "Relay incorrectly accepted announcement without service in relays tag. \
258 Event ID: {}, Relays URL: wss://relay.damus.io (should require {})",
259 event_id, relay_url
260 ));
261 }
262
263 Ok(())
225 }) 264 })
226 .await 265 .await
227 } 266 }
@@ -232,27 +271,87 @@ impl Grasp01NostrRelayTests {
232 271
233 /// Test: Accept valid repository state announcements 272 /// Test: Accept valid repository state announcements
234 /// 273 ///
235 /// Spec: Line 3 of ../grasp/01.md 274 /// Spec: Lines 6-7 of ../grasp/01.md
236 /// Requirement: MUST accept repo state announcements 275 /// Requirement: MUST accept repo state announcements with d, maintainers, and r tags
237 async fn test_accept_valid_repo_state_announcement(client: &AuditClient) -> TestResult { 276 async fn test_accept_valid_repo_state_announcement(client: &AuditClient) -> TestResult {
238 TestResult::new( 277 TestResult::new(
239 "accept_valid_repo_state_announcement", 278 "accept_valid_repo_state_announcement",
240 "GRASP-01:nostr-relay:3", 279 "GRASP-01:nostr-relay:6-7",
241 "Accept valid repository state announcements", 280 "Accept valid repository state announcements with required tags",
242 ) 281 )
243 .run(|| async { 282 .run(|| async {
244 // TODO: Implementation 283 // Create unique repository identifier
245 // 1. First send valid kind 30617 (repo announcement) - prerequisite 284 let timestamp = Timestamp::now().as_u64();
246 // 2. Create kind 30618 event with: 285 let repo_id = format!("test-repo-state-{}", timestamp);
247 // - d tag: same as repo announcement
248 // - refs/heads/main tag: "{commit-sha}"
249 // - HEAD tag: "ref: refs/heads/main"
250 // 3. Send state announcement
251 // 4. Verify acceptance
252 // 5. Query back to confirm stored
253 // 6. Verify all tags are preserved
254 286
255 Err("Not implemented yet".to_string()) 287 // Create kind 30618 repository state announcement with required tags
288 let npub = client.public_key().to_bech32()
289 .map_err(|e| format!("Failed to convert public key to bech32: {}", e))?;
290
291 let event = client.event_builder(Kind::Custom(30618), "")
292 .tag(Tag::identifier(&repo_id))
293 .tag(Tag::custom(TagKind::custom("maintainers"), vec![npub]))
294 .tag(Tag::custom(TagKind::custom("r"), vec!["refs/heads/main".to_string()]))
295 .build(client.keys())
296 .map_err(|e| format!("Failed to build repository state announcement: {}", e))?;
297
298 let event_id = event.id;
299
300 // Send the event
301 client.send_event(event.clone()).await
302 .map_err(|e| format!("Failed to send repository state announcement to relay: {}", e))?;
303
304 // Query back to verify it was accepted and stored
305 let filter = Filter::new()
306 .kind(Kind::Custom(30618))
307 .author(client.public_key())
308 .identifier(&repo_id);
309
310 let events = client.query(filter).await
311 .map_err(|e| format!("Failed to query events from relay: {}", e))?;
312
313 // Verify we got the event back
314 if events.is_empty() {
315 return Err(format!(
316 "Event was not stored in relay (possibly rejected). Event ID: {}, Repo ID: {}",
317 event_id, repo_id
318 ));
319 }
320
321 // Verify it's the same event
322 let stored_event = events.iter()
323 .find(|e| e.id == event_id)
324 .ok_or(format!(
325 "Stored event ID doesn't match sent event. Expected: {}, Got {} events",
326 event_id, events.len()
327 ))?;
328
329 // Verify required tags are present
330 let has_d_tag = stored_event.tags.iter()
331 .any(|t| t.kind() == TagKind::d() && t.content() == Some(&repo_id));
332
333 let has_maintainers_tag = stored_event.tags.iter()
334 .any(|t| t.kind() == TagKind::custom("maintainers"));
335
336 let has_r_tag = stored_event.tags.iter()
337 .any(|t| {
338 t.kind() == TagKind::custom("r") &&
339 t.content().map(|c| c.contains("refs/heads/main")).unwrap_or(false)
340 });
341
342 if !has_d_tag {
343 return Err(format!("Stored event missing d tag with repo identifier ({})", repo_id));
344 }
345
346 if !has_maintainers_tag {
347 return Err("Stored event missing maintainers tag".to_string());
348 }
349
350 if !has_r_tag {
351 return Err("Stored event missing r tag with git reference".to_string());
352 }
353
354 Ok(())
256 }) 355 })
257 .await 356 .await
258 } 357 }