upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/specs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-05 06:37:21 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-05 06:37:21 +0000
commit5cd47079ee762125817612d2bf82a0bca07da3ad (patch)
tree89f490b9cb981467c27467a0a826bdbfa229cfd9 /grasp-audit/src/specs
parent9ae69e7f6854d44f75ebd16f11ba5c95a45a56bf (diff)
preparing to build grasp-audit against git-relay
Diffstat (limited to 'grasp-audit/src/specs')
-rw-r--r--grasp-audit/src/specs/grasp01_nostr_relay.rs577
1 files changed, 577 insertions, 0 deletions
diff --git a/grasp-audit/src/specs/grasp01_nostr_relay.rs b/grasp-audit/src/specs/grasp01_nostr_relay.rs
new file mode 100644
index 0000000..1d1de79
--- /dev/null
+++ b/grasp-audit/src/specs/grasp01_nostr_relay.rs
@@ -0,0 +1,577 @@
1//! GRASP-01 Nostr Relay Tests
2//!
3//! Tests for GRASP-01 Nostr relay requirements (lines 1-14 of ../grasp/01.md)
4//!
5//! These tests validate that a GRASP-01 compliant relay:
6//! - Accepts valid NIP-34 repository announcements and state announcements
7//! - Rejects announcements that don't list the service
8//! - Accepts related events (issues, patches, PRs)
9//! - Serves proper NIP-11 relay information document
10
11use crate::{AuditClient, AuditResult, TestResult};
12use nostr_sdk::prelude::*;
13
14pub struct Grasp01NostrRelayTests;
15
16impl Grasp01NostrRelayTests {
17 /// Run all GRASP-01 Nostr relay tests
18 pub async fn run_all(client: &AuditClient) -> AuditResult {
19 let mut results = AuditResult::new("GRASP-01 Nostr Relay Tests");
20
21 // Repository announcement acceptance tests
22 results.add(Self::test_accept_valid_repo_announcement(client).await);
23 results.add(Self::test_reject_repo_announcement_missing_clone_tag(client).await);
24 results.add(Self::test_reject_repo_announcement_missing_relays_tag(client).await);
25
26 // Repository state announcement tests
27 results.add(Self::test_accept_valid_repo_state_announcement(client).await);
28 results.add(Self::test_accept_state_announcement_multiple_refs(client).await);
29 results.add(Self::test_accept_state_announcement_no_refs(client).await);
30
31 // Related event acceptance tests
32 results.add(Self::test_accept_event_tagging_repo_announcement(client).await);
33 results.add(Self::test_accept_event_tagged_by_repo(client).await);
34 results.add(Self::test_accept_patch_for_repo(client).await);
35 results.add(Self::test_accept_pull_request_for_repo(client).await);
36 results.add(Self::test_accept_issue_for_repo(client).await);
37 results.add(Self::test_accept_reply_to_issue(client).await);
38
39 // NIP-11 relay information tests
40 results.add(Self::test_nip11_document_exists(client).await);
41 results.add(Self::test_nip11_supported_grasps_field(client).await);
42 results.add(Self::test_nip11_repo_acceptance_criteria_field(client).await);
43 results.add(Self::test_nip11_curation_field(client).await);
44
45 // Policy tests (document behavior)
46 results.add(Self::test_custom_rejection_allowed(client).await);
47 results.add(Self::test_spam_prevention_allowed(client).await);
48
49 results
50 }
51
52 // =========================================================================
53 // Repository Announcement Acceptance Tests
54 // =========================================================================
55
56 /// Test: Accept valid repository announcements
57 ///
58 /// Spec: Lines 3-5 of ../grasp/01.md
59 /// Requirement: MUST accept repo announcements listing service in clone & relays tags
60 async fn test_accept_valid_repo_announcement(client: &AuditClient) -> TestResult {
61 TestResult::new(
62 "accept_valid_repo_announcement",
63 "GRASP-01:nostr-relay:3-5",
64 "Accept valid repository announcements with service in clone and relays tags",
65 )
66 .run(|| async {
67 // TODO: Implementation
68 // 1. Get service URL from client config
69 // 2. Create kind 30617 event with:
70 // - d tag: "test-repo-{timestamp}"
71 // - name tag: "Test Repository"
72 // - description tag: "Test repository for GRASP-01 compliance"
73 // - clone tag: "{service_url}/{npub}/test-repo.git"
74 // - relays tag: "{service_ws_url}"
75 // 3. Send event to relay
76 // 4. Verify OK response (not rejected)
77 // 5. Query back with filter on kind 30617, author, d tag
78 // 6. Verify event is stored and matches what we sent
79
80 Err("Not implemented yet".to_string())
81 })
82 .await
83 }
84
85 /// Test: Reject repo announcements not listing service in clone tag
86 ///
87 /// Spec: Line 5 of ../grasp/01.md
88 /// Requirement: MUST reject announcements not listing service (unless GRASP-05)
89 async fn test_reject_repo_announcement_missing_clone_tag(client: &AuditClient) -> TestResult {
90 TestResult::new(
91 "reject_repo_announcement_missing_clone_tag",
92 "GRASP-01:nostr-relay:5",
93 "Reject repository announcements without service in clone tag",
94 )
95 .run(|| async {
96 // TODO: Implementation
97 // 1. Create kind 30617 event with:
98 // - d tag: "test-repo-no-clone"
99 // - clone tag: "https://github.com/user/repo.git" (NOT this service)
100 // - relays tag: "{service_url}" (correct)
101 // 2. Send event to relay
102 // 3. Verify rejection (error in OK response or event not stored)
103 // 4. Query to confirm event is NOT in relay
104 // 5. Document expected error message if available
105
106 Err("Not implemented yet".to_string())
107 })
108 .await
109 }
110
111 /// Test: Reject repo announcements not listing service in relays tag
112 ///
113 /// Spec: Line 5 of ../grasp/01.md
114 /// Requirement: MUST reject announcements not listing service in relays
115 async fn test_reject_repo_announcement_missing_relays_tag(client: &AuditClient) -> TestResult {
116 TestResult::new(
117 "reject_repo_announcement_missing_relays_tag",
118 "GRASP-01:nostr-relay:5",
119 "Reject repository announcements without service in relays tag",
120 )
121 .run(|| async {
122 // TODO: Implementation
123 // 1. Create kind 30617 event with:
124 // - d tag: "test-repo-no-relays"
125 // - clone tag: "{service_url}/{npub}/test-repo.git" (correct)
126 // - relays tag: "wss://relay.damus.io" (NOT this service)
127 // 2. Send event to relay
128 // 3. Verify rejection
129 // 4. Query to confirm event is NOT in relay
130
131 Err("Not implemented yet".to_string())
132 })
133 .await
134 }
135
136 // =========================================================================
137 // Repository State Announcement Tests
138 // =========================================================================
139
140 /// Test: Accept valid repository state announcements
141 ///
142 /// Spec: Line 3 of ../grasp/01.md
143 /// Requirement: MUST accept repo state announcements
144 async fn test_accept_valid_repo_state_announcement(client: &AuditClient) -> TestResult {
145 TestResult::new(
146 "accept_valid_repo_state_announcement",
147 "GRASP-01:nostr-relay:3",
148 "Accept valid repository state announcements",
149 )
150 .run(|| async {
151 // TODO: Implementation
152 // 1. First send valid kind 30617 (repo announcement) - prerequisite
153 // 2. Create kind 30618 event with:
154 // - d tag: same as repo announcement
155 // - refs/heads/main tag: "{commit-sha}"
156 // - HEAD tag: "ref: refs/heads/main"
157 // 3. Send state announcement
158 // 4. Verify acceptance
159 // 5. Query back to confirm stored
160 // 6. Verify all tags are preserved
161
162 Err("Not implemented yet".to_string())
163 })
164 .await
165 }
166
167 /// Test: Accept state announcement with multiple refs
168 ///
169 /// Spec: Line 3 of ../grasp/01.md
170 /// Requirement: MUST accept state announcements with multiple refs
171 async fn test_accept_state_announcement_multiple_refs(client: &AuditClient) -> TestResult {
172 TestResult::new(
173 "accept_state_announcement_multiple_refs",
174 "GRASP-01:nostr-relay:3",
175 "Accept state announcements with multiple branch and tag refs",
176 )
177 .run(|| async {
178 // TODO: Implementation
179 // 1. Send valid kind 30617 repo announcement
180 // 2. Create kind 30618 with multiple refs:
181 // - refs/heads/main: "{commit-sha-1}"
182 // - refs/heads/develop: "{commit-sha-2}"
183 // - refs/tags/v1.0.0: "{commit-sha-3}"
184 // - refs/tags/v2.0.0: "{commit-sha-4}"
185 // - HEAD: "ref: refs/heads/main"
186 // 3. Send and verify acceptance
187 // 4. Query back and verify all refs are stored
188
189 Err("Not implemented yet".to_string())
190 })
191 .await
192 }
193
194 /// Test: Accept state announcement with no refs (stop tracking)
195 ///
196 /// Spec: NIP-34 repository state announcements
197 /// Requirement: Support stopping state tracking by sending event with no refs
198 async fn test_accept_state_announcement_no_refs(client: &AuditClient) -> TestResult {
199 TestResult::new(
200 "accept_state_announcement_no_refs",
201 "GRASP-01:nostr-relay:3",
202 "Accept state announcements with no refs (stop tracking)",
203 )
204 .run(|| async {
205 // TODO: Implementation
206 // 1. Send valid kind 30617 repo announcement
207 // 2. Send kind 30618 with refs (establish state)
208 // 3. Send kind 30618 with ONLY d tag (no refs)
209 // 4. Verify acceptance (allows author to stop tracking)
210 // 5. Query to confirm latest state has no refs
211
212 Err("Not implemented yet".to_string())
213 })
214 .await
215 }
216
217 // =========================================================================
218 // Related Event Acceptance Tests
219 // =========================================================================
220
221 /// Test: Accept events tagging accepted repo announcements
222 ///
223 /// Spec: Lines 7-9 of ../grasp/01.md
224 /// Requirement: MUST accept events that tag accepted repo announcements
225 async fn test_accept_event_tagging_repo_announcement(client: &AuditClient) -> TestResult {
226 TestResult::new(
227 "accept_event_tagging_repo_announcement",
228 "GRASP-01:nostr-relay:7-9",
229 "Accept events that tag accepted repository announcements",
230 )
231 .run(|| async {
232 // TODO: Implementation
233 // 1. Create and send kind 30617 repo announcement
234 // 2. Create kind 1621 (issue) event with:
235 // - a tag: "30617:{pubkey}:{d-tag}"
236 // - p tag: repo owner pubkey
237 // - subject tag: "Test Issue"
238 // - content: "This is a test issue"
239 // 3. Send issue event
240 // 4. Verify acceptance
241 // 5. Query to confirm issue is stored
242
243 Err("Not implemented yet".to_string())
244 })
245 .await
246 }
247
248 /// Test: Accept events tagged by repo announcements
249 ///
250 /// Spec: Lines 7-9 of ../grasp/01.md
251 /// Requirement: MUST accept events tagged by accepted announcements
252 async fn test_accept_event_tagged_by_repo(client: &AuditClient) -> TestResult {
253 TestResult::new(
254 "accept_event_tagged_by_repo",
255 "GRASP-01:nostr-relay:7-9",
256 "Accept events that are tagged by accepted repository announcements",
257 )
258 .run(|| async {
259 // TODO: Implementation
260 // 1. Create kind 1 note event (regular note)
261 // 2. Send the note
262 // 3. Create kind 30617 repo announcement that tags the note
263 // - Include e tag pointing to note event ID
264 // 4. Send repo announcement
265 // 5. Verify both events are stored
266 // 6. This tests that related events are retained
267
268 Err("Not implemented yet".to_string())
269 })
270 .await
271 }
272
273 /// Test: Accept patches (kind 1617) for accepted repos
274 ///
275 /// Spec: Lines 8-9 of ../grasp/01.md
276 /// Requirement: MUST accept patches for accepted repos
277 async fn test_accept_patch_for_repo(client: &AuditClient) -> TestResult {
278 TestResult::new(
279 "accept_patch_for_repo",
280 "GRASP-01:nostr-relay:8-9",
281 "Accept patch events (kind 1617) for accepted repositories",
282 )
283 .run(|| async {
284 // TODO: Implementation
285 // 1. Create and send kind 30617 repo announcement
286 // 2. Create kind 1617 patch event with:
287 // - a tag: "30617:{pubkey}:{d-tag}"
288 // - p tag: repo owner
289 // - r tag: earliest-unique-commit-id
290 // - t tag: "root" (first patch in series)
291 // - content: actual git format-patch output
292 // 3. Send patch event
293 // 4. Verify acceptance
294 // 5. Query to confirm patch is stored
295
296 Err("Not implemented yet".to_string())
297 })
298 .await
299 }
300
301 /// Test: Accept pull requests (kind 1618) for accepted repos
302 ///
303 /// Spec: Lines 8-9 of ../grasp/01.md
304 /// Requirement: MUST accept PRs for accepted repos
305 async fn test_accept_pull_request_for_repo(client: &AuditClient) -> TestResult {
306 TestResult::new(
307 "accept_pull_request_for_repo",
308 "GRASP-01:nostr-relay:8-9",
309 "Accept pull request events (kind 1618) for accepted repositories",
310 )
311 .run(|| async {
312 // TODO: Implementation
313 // 1. Create and send kind 30617 repo announcement
314 // 2. Create kind 1618 PR event with:
315 // - a tag: "30617:{pubkey}:{d-tag}"
316 // - p tag: repo owner
317 // - r tag: earliest-unique-commit-id
318 // - subject tag: "Add feature X"
319 // - c tag: commit SHA of PR tip
320 // - clone tag: URL where commit can be fetched
321 // - content: PR description
322 // 3. Send PR event
323 // 4. Verify acceptance
324 // 5. Query to confirm PR is stored
325
326 Err("Not implemented yet".to_string())
327 })
328 .await
329 }
330
331 /// Test: Accept issues (kind 1621) for accepted repos
332 ///
333 /// Spec: Lines 8-9 of ../grasp/01.md
334 /// Requirement: MUST accept issues for accepted repos
335 async fn test_accept_issue_for_repo(client: &AuditClient) -> TestResult {
336 TestResult::new(
337 "accept_issue_for_repo",
338 "GRASP-01:nostr-relay:8-9",
339 "Accept issue events (kind 1621) for accepted repositories",
340 )
341 .run(|| async {
342 // TODO: Implementation
343 // 1. Create and send kind 30617 repo announcement
344 // 2. Create kind 1621 issue event with:
345 // - a tag: "30617:{pubkey}:{d-tag}"
346 // - p tag: repo owner
347 // - subject tag: "Bug: Something is broken"
348 // - t tag: "bug" (label)
349 // - content: issue description
350 // 3. Send issue event
351 // 4. Verify acceptance
352 // 5. Query to confirm issue is stored
353
354 Err("Not implemented yet".to_string())
355 })
356 .await
357 }
358
359 /// Test: Accept replies to accepted patches/PRs/issues
360 ///
361 /// Spec: Lines 8-9 of ../grasp/01.md
362 /// Requirement: MUST accept replies to accepted events
363 async fn test_accept_reply_to_issue(client: &AuditClient) -> TestResult {
364 TestResult::new(
365 "accept_reply_to_issue",
366 "GRASP-01:nostr-relay:8-9",
367 "Accept reply events to accepted issues/patches/PRs",
368 )
369 .run(|| async {
370 // TODO: Implementation
371 // 1. Create and send kind 30617 repo announcement
372 // 2. Create and send kind 1621 issue
373 // 3. Create NIP-22 comment (kind 1111) replying to issue:
374 // - E tag: issue event ID
375 // - P tag: issue author
376 // - content: reply text
377 // 4. Send reply event
378 // 5. Verify acceptance
379 // 6. Query to confirm reply is stored
380
381 Err("Not implemented yet".to_string())
382 })
383 .await
384 }
385
386 // =========================================================================
387 // NIP-11 Relay Information Tests
388 // =========================================================================
389
390 /// Test: Serve NIP-11 document
391 ///
392 /// Spec: Line 11 of ../grasp/01.md
393 /// Requirement: MUST serve NIP-11 document
394 async fn test_nip11_document_exists(client: &AuditClient) -> TestResult {
395 TestResult::new(
396 "nip11_document_exists",
397 "GRASP-01:nostr-relay:11",
398 "Serve NIP-11 relay information document",
399 )
400 .run(|| async {
401 // TODO: Implementation
402 // 1. Extract HTTP(S) URL from client's WebSocket URL
403 // - ws://localhost:8081 -> http://localhost:8081
404 // - wss://relay.example.com -> https://relay.example.com
405 // 2. HTTP GET to base URL with header:
406 // - Accept: application/nostr+json
407 // 3. Verify 200 OK response
408 // 4. Verify response is valid JSON
409 // 5. Parse as NIP-11 document
410 // 6. Verify has required fields (name, description, etc.)
411
412 Err("Not implemented yet".to_string())
413 })
414 .await
415 }
416
417 /// Test: NIP-11 includes supported_grasps field
418 ///
419 /// Spec: Line 12 of ../grasp/01.md
420 /// Requirement: MUST list supported GRASPs as string array
421 async fn test_nip11_supported_grasps_field(client: &AuditClient) -> TestResult {
422 TestResult::new(
423 "nip11_supported_grasps_field",
424 "GRASP-01:nostr-relay:12",
425 "NIP-11 document includes supported_grasps field with GRASP-01",
426 )
427 .run(|| async {
428 // TODO: Implementation
429 // 1. Fetch NIP-11 document (same as above)
430 // 2. Verify `supported_grasps` field exists
431 // 3. Verify it's a JSON array of strings
432 // 4. Verify array includes "GRASP-01"
433 // 5. Verify format: each entry matches pattern "GRASP-\d{2}"
434 // 6. Document other GRASPs found (for info)
435
436 Err("Not implemented yet".to_string())
437 })
438 .await
439 }
440
441 /// Test: NIP-11 includes repo_acceptance_criteria field
442 ///
443 /// Spec: Line 13 of ../grasp/01.md
444 /// Requirement: MUST list repository acceptance criteria
445 async fn test_nip11_repo_acceptance_criteria_field(client: &AuditClient) -> TestResult {
446 TestResult::new(
447 "nip11_repo_acceptance_criteria_field",
448 "GRASP-01:nostr-relay:13",
449 "NIP-11 document includes repo_acceptance_criteria field",
450 )
451 .run(|| async {
452 // TODO: Implementation
453 // 1. Fetch NIP-11 document
454 // 2. Verify `repo_acceptance_criteria` field exists
455 // 3. Verify it's a string (human-readable)
456 // 4. Verify non-empty
457 // 5. Document the criteria (for info)
458 // Examples: "Must list this relay in clone and relays tags"
459 // "Pre-payment required via Lightning invoice"
460
461 Err("Not implemented yet".to_string())
462 })
463 .await
464 }
465
466 /// Test: NIP-11 curation field handling
467 ///
468 /// Spec: Line 14 of ../grasp/01.md
469 /// Requirement: MUST include curation if curated, omit otherwise
470 async fn test_nip11_curation_field(client: &AuditClient) -> TestResult {
471 TestResult::new(
472 "nip11_curation_field",
473 "GRASP-01:nostr-relay:14",
474 "NIP-11 curation field present if curated, absent otherwise",
475 )
476 .run(|| async {
477 // TODO: Implementation
478 // 1. Fetch NIP-11 document
479 // 2. Check if `curation` field exists
480 // 3. If present:
481 // - Verify it's a non-empty string
482 // - Document the curation policy
483 // 4. If absent:
484 // - Document that no curation beyond SPAM prevention
485 // 5. Both cases are valid per spec
486
487 Err("Not implemented yet".to_string())
488 })
489 .await
490 }
491
492 // =========================================================================
493 // Policy Tests (Document Allowed Behavior)
494 // =========================================================================
495
496 /// Test: Custom rejection criteria allowed
497 ///
498 /// Spec: Line 6 of ../grasp/01.md
499 /// Requirement: MAY reject based on custom criteria (document behavior)
500 async fn test_custom_rejection_allowed(client: &AuditClient) -> TestResult {
501 TestResult::new(
502 "custom_rejection_allowed",
503 "GRASP-01:nostr-relay:6",
504 "Document that custom rejection criteria are allowed",
505 )
506 .run(|| async {
507 // TODO: Implementation
508 // This is a policy test, not a functional test
509 //
510 // The spec says relay MAY reject based on:
511 // - Pre-payment
512 // - Quotas
513 // - WoT (Web of Trust)
514 // - Whitelist
515 // - SPAM prevention
516 // - etc.
517 //
518 // This test should:
519 // 1. Document that such rejections are allowed
520 // 2. Check NIP-11 repo_acceptance_criteria for policy
521 // 3. Optionally test if relay enforces any criteria
522 // 4. Mark as PASS (this is permissive, not mandatory)
523
524 Ok(()) // This is always allowed
525 })
526 .await
527 }
528
529 /// Test: SPAM prevention allowed
530 ///
531 /// Spec: Line 10 of ../grasp/01.md
532 /// Requirement: MAY reject/delete for SPAM prevention
533 async fn test_spam_prevention_allowed(client: &AuditClient) -> TestResult {
534 TestResult::new(
535 "spam_prevention_allowed",
536 "GRASP-01:nostr-relay:10",
537 "Document that SPAM prevention is allowed",
538 )
539 .run(|| async {
540 // TODO: Implementation
541 // Similar to above - this is permissive
542 //
543 // The spec says relay MAY reject or delete events for:
544 // - Generic SPAM prevention
545 // - Curation (WoT, whitelist, user bans, banned topics)
546 //
547 // This test should:
548 // 1. Document that SPAM prevention is allowed
549 // 2. Check NIP-11 curation field for policy
550 // 3. Mark as PASS (this is implementation-specific)
551
552 Ok(()) // This is always allowed
553 })
554 .await
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561 use crate::AuditConfig;
562
563 #[tokio::test]
564 #[ignore] // Requires running relay
565 async fn test_grasp01_nostr_relay_against_relay() {
566 let config = AuditConfig::ci();
567 let client = AuditClient::new("ws://localhost:8081", config)
568 .await
569 .expect("Failed to connect to relay");
570
571 let results = Grasp01NostrRelayTests::run_all(&client).await;
572 results.print_report();
573
574 // Don't assert all passed yet - tests not implemented
575 // assert!(results.all_passed(), "Some GRASP-01 Nostr relay tests failed");
576 }
577}