upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests/nip34_announcements.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-20 21:45:45 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-20 21:45:45 +0000
commitca50f5b98f30d0933a510c05db86b608afee73a0 (patch)
treecdd694a82ba413220d80082056c34de444904f59 /tests/nip34_announcements.rs
parent89c69eae8e75d2b00794087d9ef74fd4856d0f88 (diff)
replace tests to use grasp-audit lib as much as possible
Diffstat (limited to 'tests/nip34_announcements.rs')
-rw-r--r--tests/nip34_announcements.rs580
1 files changed, 97 insertions, 483 deletions
diff --git a/tests/nip34_announcements.rs b/tests/nip34_announcements.rs
index 535425d..f1cbd05 100644
--- a/tests/nip34_announcements.rs
+++ b/tests/nip34_announcements.rs
@@ -1,24 +1,22 @@
1//! NIP-34 Repository Announcements Integration Tests (GRASP-01) 1//! GRASP-01 Repository Event Acceptance Integration Tests
2//! 2//!
3//! Tests the acceptance and validation of repository announcements (kind 30617) 3//! Tests ngit-grasp relay's implementation of GRASP-01 repository event acceptance policy.
4//! and repository state announcements (kind 30618) according to GRASP-01. 4//! Uses grasp-audit library to avoid code duplication.
5//!
6//! Reference: GRASP-01, Lines 9-20
7//! 5//!
8//! # Test Strategy 6//! # Test Strategy
9//! 7//!
10//! - Uses TestRelay fixture for automatic relay lifecycle management 8//! - Uses TestRelay fixture for ngit-grasp relay lifecycle management
11//! - Pure Rust, no shell scripts 9//! - Uses grasp-audit's EventAcceptancePolicyTests for actual test logic
12//! - Tests run in parallel with isolated relay instances 10//! - Minimal duplication - single source of truth in grasp-audit
13//! 11//!
14//! # Running Tests 12//! # Running Tests
15//! 13//!
16//! ```bash 14//! ```bash
17//! # Run all NIP-34 announcement tests 15//! # Run all GRASP-01 tests
18//! cargo test --test nip34_announcements 16//! cargo test --test nip34_announcements
19//! 17//!
20//! # Run specific test 18//! # Run specific test
21//! cargo test --test nip34_announcements test_accepts_valid_announcement 19//! cargo test --test nip34_announcements test_grasp01_event_acceptance
22//! 20//!
23//! # With output 21//! # With output
24//! cargo test --test nip34_announcements -- --nocapture 22//! cargo test --test nip34_announcements -- --nocapture
@@ -27,510 +25,126 @@
27mod common; 25mod common;
28 26
29use common::TestRelay; 27use common::TestRelay;
30use futures_util::{SinkExt, StreamExt}; 28use grasp_audit::*;
31use nostr_sdk::{EventBuilder, Keys, Kind, Tag, TagKind}; 29
32use serde_json::{json, Value}; 30/// Test GRASP-01 event acceptance policy against ngit-grasp relay
33use tokio_tungstenite::{connect_async, tungstenite::Message}; 31///
34 32/// This test runs all GRASP-01 event acceptance policy tests from grasp-audit
35const KIND_REPOSITORY_ANNOUNCEMENT: u16 = 30617; 33/// against the ngit-grasp relay implementation.
36const KIND_REPOSITORY_STATE: u16 = 30618; 34///
37 35/// Tests cover:
38/// Helper to connect to a test relay 36/// - Repository announcement acceptance/rejection
39async fn connect_to_relay( 37/// - Repository state announcement acceptance
40 url: &str, 38/// - Events tagging accepted repositories
41) -> tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>> { 39/// - Transitive event acceptance (events tagging accepted events)
42 let (ws, _) = connect_async(url) 40/// - Forward reference acceptance (events tagged by accepted events)
43 .await 41/// - Rejection of unrelated events
44 .expect("Failed to connect to relay");
45 ws
46}
47
48/// Helper to create a repository announcement event
49fn create_announcement(
50 keys: &Keys,
51 _domain: &str,
52 identifier: &str,
53 clone_urls: Vec<String>,
54 relays: Vec<String>,
55) -> nostr_sdk::Event {
56 let mut tags = vec![Tag::custom(TagKind::d(), vec![identifier.to_string()])];
57
58 for url in clone_urls {
59 tags.push(Tag::custom(TagKind::Clone, vec![url]));
60 }
61
62 for relay in relays {
63 tags.push(Tag::custom(TagKind::Relays, vec![relay]));
64 }
65
66 EventBuilder::new(
67 Kind::from(KIND_REPOSITORY_ANNOUNCEMENT),
68 "Test repository description",
69 )
70 .tags(tags)
71 .sign_with_keys(keys)
72 .expect("Failed to sign event")
73}
74
75/// Helper to create a repository state event
76fn create_state(keys: &Keys, identifier: &str, branches: Vec<(&str, &str)>) -> nostr_sdk::Event {
77 let mut tags = vec![Tag::custom(TagKind::d(), vec![identifier.to_string()])];
78
79 for (branch, commit) in branches {
80 tags.push(Tag::custom(
81 TagKind::Custom("ref".into()),
82 vec![format!("refs/heads/{}", branch), commit.to_string()],
83 ));
84 }
85
86 EventBuilder::new(Kind::from(KIND_REPOSITORY_STATE), "")
87 .tags(tags)
88 .sign_with_keys(keys)
89 .expect("Failed to sign event")
90}
91
92/// GRASP-01, Line 9-10: MUST serve a NIP-01 compliant nostr relay at `/`
93#[tokio::test] 42#[tokio::test]
94async fn test_relay_accepts_connection() { 43async fn test_grasp01_event_acceptance() {
44 // Start test relay
95 let relay = TestRelay::start().await; 45 let relay = TestRelay::start().await;
96 46
97 // Try to connect 47 // Create audit client in CI mode (isolated testing)
98 let ws = connect_to_relay(relay.url()).await; 48 let config = AuditConfig::ci();
99 49 let client = AuditClient::new(relay.url(), config)
100 drop(ws); // Clean disconnect
101}
102
103/// GRASP-01, Line 11: MUST accept repository announcements (kind 30617)
104#[tokio::test]
105async fn test_accepts_valid_announcement() {
106 let relay = TestRelay::start().await;
107 let keys = Keys::generate();
108
109 let mut ws = connect_to_relay(relay.url()).await;
110
111 let event = create_announcement(
112 &keys,
113 &relay.domain(),
114 "test-repo",
115 vec![format!("https://{}/alice/test-repo.git", relay.domain())],
116 vec![format!("wss://{}", relay.domain())],
117 );
118
119 // Send event
120 let event_msg = json!(["EVENT", event]);
121 ws.send(Message::Text(event_msg.to_string().into()))
122 .await 50 .await
123 .expect("Failed to send event"); 51 .expect("Failed to create audit client");
124 52
125 // Read response 53 // Run all GRASP-01 event acceptance policy tests
126 if let Some(Ok(Message::Text(text))) = ws.next().await { 54 let results = specs::EventAcceptancePolicyTests::run_all(&client).await;
127 let response: Value = serde_json::from_str(&text).expect("Failed to parse response");
128 55
129 // Should be ["OK", event_id, true, ""] 56 // Print detailed report
130 assert_eq!(response[0], "OK"); 57 results.print_report();
131 assert_eq!(response[1], event.id.to_hex());
132 if response[2] != true {
133 eprintln!("Event rejected: {}", response[3]);
134 }
135 assert_eq!(response[2], true, "Event should be accepted");
136 } else {
137 panic!("No response received");
138 }
139}
140
141/// GRASP-01, Line 12-13: MUST reject announcements that do not list the service
142/// in both `clone` and `relays` tags
143#[tokio::test]
144async fn test_rejects_announcement_without_clone() {
145 let relay = TestRelay::start().await;
146 let keys = Keys::generate();
147 58
148 let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect"); 59 // Stop relay
60 relay.stop().await;
149 61
150 // Missing clone tag 62 // Assert all tests passed
151 let event = create_announcement( 63 assert!(
152 &keys, 64 results.all_passed(),
153 &relay.domain(), 65 "GRASP-01 event acceptance tests failed: {}/{} passed",
154 "test-repo", 66 results.passed_count(),
155 vec![], // No clone URLs 67 results.total_count()
156 vec![format!("wss://{}", relay.domain())],
157 ); 68 );
158
159 let event_msg = json!(["EVENT", event]);
160 ws.send(Message::Text(event_msg.to_string().into()))
161 .await
162 .expect("Failed to send event");
163
164 if let Some(Ok(Message::Text(text))) = ws.next().await {
165 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
166
167 // Should be rejected
168 assert_eq!(response[0], "OK");
169 assert_eq!(response[1], event.id.to_hex());
170 assert_eq!(response[2], false, "Event should be rejected");
171
172 let message = response[3].as_str().unwrap();
173 assert!(
174 message.contains("clone") || message.contains("invalid"),
175 "Error message should mention clone requirement: {}",
176 message
177 );
178 } else {
179 panic!("No response received");
180 }
181}
182
183/// GRASP-01, Line 12-13: MUST reject announcements that do not list the service
184/// in both `clone` and `relays` tags
185#[tokio::test]
186async fn test_rejects_announcement_without_relay() {
187 let relay = TestRelay::start().await;
188 let keys = Keys::generate();
189
190 let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect");
191
192 // Missing relay tag
193 let event = create_announcement(
194 &keys,
195 &relay.domain(),
196 "test-repo",
197 vec![format!("https://{}/alice/test-repo.git", relay.domain())],
198 vec![], // No relays
199 );
200
201 let event_msg = json!(["EVENT", event]);
202 ws.send(Message::Text(event_msg.to_string().into()))
203 .await
204 .expect("Failed to send event");
205
206 if let Some(Ok(Message::Text(text))) = ws.next().await {
207 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
208
209 // Should be rejected
210 assert_eq!(response[0], "OK");
211 assert_eq!(response[1], event.id.to_hex());
212 assert_eq!(response[2], false, "Event should be rejected");
213
214 let message = response[3].as_str().unwrap();
215 assert!(
216 message.contains("relays") || message.contains("invalid"),
217 "Error message should mention relay requirement: {}",
218 message
219 );
220 } else {
221 panic!("No response received");
222 }
223} 69}
224 70
225/// GRASP-01, Line 12-13: MUST reject announcements listing other services 71/// Test that relay accepts valid repository announcements
72///
73/// Demonstrates running individual test categories from the suite
226#[tokio::test] 74#[tokio::test]
227async fn test_rejects_announcement_for_other_service() { 75async fn test_accepts_repository_announcements() {
228 let relay = TestRelay::start().await; 76 let relay = TestRelay::start().await;
229 let keys = Keys::generate(); 77 let config = AuditConfig::ci();
230 78 let client = AuditClient::new(relay.url(), config)
231 let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect");
232
233 // Lists different service
234 let event = create_announcement(
235 &keys,
236 &relay.domain(),
237 "test-repo",
238 vec!["https://other-service.com/alice/test-repo.git".to_string()],
239 vec!["wss://other-service.com".to_string()],
240 );
241
242 let event_msg = json!(["EVENT", event]);
243 ws.send(Message::Text(event_msg.to_string().into()))
244 .await 79 .await
245 .expect("Failed to send event"); 80 .expect("Failed to create audit client");
246 81
247 if let Some(Ok(Message::Text(text))) = ws.next().await { 82 // Run all tests
248 let response: Value = serde_json::from_str(&text).expect("Failed to parse"); 83 let results = specs::EventAcceptancePolicyTests::run_all(&client).await;
249 84
250 // Should be rejected 85 relay.stop().await;
251 assert_eq!(response[0], "OK"); 86
252 assert_eq!(response[1], event.id.to_hex()); 87 // Filter to only repository announcement tests
253 assert_eq!(response[2], false, "Event should be rejected"); 88 let announcement_tests: Vec<_> = results
254 } else { 89 .results
255 panic!("No response received"); 90 .iter()
256 } 91 .filter(|t| {
257} 92 t.spec_ref.contains("repo") || t.name.contains("announcement") || t.name.contains("state")
258 93 })
259/// GRASP-01, Line 11: MUST accept repository state announcements (kind 30618) 94 .collect();
260#[tokio::test] 95
261async fn test_accepts_valid_state() { 96 // Verify we have announcement tests
262 let relay = TestRelay::start().await; 97 assert!(
263 let keys = Keys::generate(); 98 !announcement_tests.is_empty(),
264 99 "No repository announcement tests found"
265 let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect");
266
267 let event = create_state(
268 &keys,
269 "test-repo",
270 vec![("main", "a1b2c3d4e5f6789012345678901234567890abcd")],
271 ); 100 );
272 101
273 let event_msg = json!(["EVENT", event]); 102 // All should pass
274 ws.send(Message::Text(event_msg.to_string().into())) 103 for test in announcement_tests {
275 .await
276 .expect("Failed to send event");
277
278 if let Some(Ok(Message::Text(text))) = ws.next().await {
279 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
280
281 // Should be accepted
282 assert_eq!(response[0], "OK");
283 assert_eq!(response[1], event.id.to_hex());
284 assert_eq!(response[2], true, "State event should be accepted");
285 } else {
286 panic!("No response received");
287 }
288}
289
290/// Test state event with multiple branches
291#[tokio::test]
292async fn test_accepts_state_with_multiple_branches() {
293 let relay = TestRelay::start().await;
294 let keys = Keys::generate();
295
296 let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect");
297
298 let event = create_state(
299 &keys,
300 "test-repo",
301 vec![
302 ("main", "a1b2c3d4e5f6789012345678901234567890abcd"),
303 ("develop", "b2c3d4e5f6789012345678901234567890abcde"),
304 ("feature-x", "c3d4e5f6789012345678901234567890abcdef1"),
305 ],
306 );
307
308 let event_msg = json!(["EVENT", event]);
309 ws.send(Message::Text(event_msg.to_string().into()))
310 .await
311 .expect("Failed to send event");
312
313 if let Some(Ok(Message::Text(text))) = ws.next().await {
314 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
315
316 assert_eq!(response[0], "OK");
317 assert_eq!(response[2], true, "State event should be accepted");
318 } else {
319 panic!("No response received");
320 }
321}
322
323/// Test state event without identifier should be rejected
324#[tokio::test]
325async fn test_rejects_state_without_identifier() {
326 let relay = TestRelay::start().await;
327 let keys = Keys::generate();
328
329 let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect");
330
331 // Create state without identifier
332 let event = EventBuilder::new(Kind::from(KIND_REPOSITORY_STATE), "")
333 .sign_with_keys(&keys)
334 .expect("Failed to sign event");
335
336 let event_msg = json!(["EVENT", event]);
337 ws.send(Message::Text(event_msg.to_string().into()))
338 .await
339 .expect("Failed to send event");
340
341 if let Some(Ok(Message::Text(text))) = ws.next().await {
342 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
343
344 // Should be rejected
345 assert_eq!(response[0], "OK");
346 assert_eq!(response[1], event.id.to_hex());
347 assert_eq!(response[2], false, "Event should be rejected");
348
349 let message = response[3].as_str().unwrap();
350 assert!( 104 assert!(
351 message.contains("identifier") || message.contains("invalid"), 105 test.passed,
352 "Error message should mention identifier requirement: {}", 106 "Repository test failed: {} - {}",
353 message 107 test.name,
108 test.error.as_deref().unwrap_or("unknown error")
354 ); 109 );
355 } else {
356 panic!("No response received");
357 }
358}
359
360/// Test querying for announcements
361#[tokio::test]
362async fn test_query_announcements() {
363 let relay = TestRelay::start().await;
364 let keys = Keys::generate();
365
366 let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect");
367
368 // Send an announcement
369 let event = create_announcement(
370 &keys,
371 &relay.domain(),
372 "query-test-repo",
373 vec![format!(
374 "https://{}/alice/query-test-repo.git",
375 relay.domain()
376 )],
377 vec![format!("wss://{}", relay.domain())],
378 );
379
380 let event_msg = json!(["EVENT", event]);
381 ws.send(Message::Text(event_msg.to_string().into()))
382 .await
383 .expect("Failed to send event");
384
385 // Wait for OK response
386 if let Some(Ok(Message::Text(_))) = ws.next().await {
387 // Got OK response
388 } 110 }
389
390 // Query for announcements
391 let req = json!([
392 "REQ",
393 "test-sub",
394 {
395 "kinds": [KIND_REPOSITORY_ANNOUNCEMENT],
396 "authors": [keys.public_key().to_hex()]
397 }
398 ]);
399
400 ws.send(Message::Text(req.to_string().into()))
401 .await
402 .expect("Failed to send REQ");
403
404 // Read responses
405 let mut found_event = false;
406 let mut got_eose = false;
407
408 for _ in 0..10 {
409 if let Some(Ok(Message::Text(text))) = ws.next().await {
410 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
411
412 if response[0] == "EVENT" {
413 assert_eq!(response[1], "test-sub");
414 found_event = true;
415 } else if response[0] == "EOSE" {
416 assert_eq!(response[1], "test-sub");
417 got_eose = true;
418 break;
419 }
420 }
421 }
422
423 assert!(found_event, "Should have received the announcement");
424 assert!(got_eose, "Should have received EOSE");
425} 111}
426 112
427/// Test querying for state events 113/// Test that relay properly validates clone and relays tags
114///
115/// This is a critical security requirement for GRASP-01
428#[tokio::test] 116#[tokio::test]
429async fn test_query_states() { 117async fn test_validates_service_tags() {
430 let relay = TestRelay::start().await; 118 let relay = TestRelay::start().await;
431 let keys = Keys::generate(); 119 let config = AuditConfig::ci();
432 120 let client = AuditClient::new(relay.url(), config)
433 let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect");
434
435 // Send a state event
436 let event = create_state(
437 &keys,
438 "query-test-repo",
439 vec![("main", "a1b2c3d4e5f6789012345678901234567890abcd")],
440 );
441
442 let event_msg = json!(["EVENT", event]);
443 ws.send(Message::Text(event_msg.to_string().into()))
444 .await
445 .expect("Failed to send event");
446
447 // Wait for OK response
448 if let Some(Ok(Message::Text(_))) = ws.next().await {
449 // Got OK response
450 }
451
452 // Query for states
453 let req = json!([
454 "REQ",
455 "test-sub",
456 {
457 "kinds": [KIND_REPOSITORY_STATE],
458 "authors": [keys.public_key().to_hex()]
459 }
460 ]);
461
462 ws.send(Message::Text(req.to_string().into()))
463 .await 121 .await
464 .expect("Failed to send REQ"); 122 .expect("Failed to create audit client");
465 123
466 // Read responses 124 let results = specs::EventAcceptancePolicyTests::run_all(&client).await;
467 let mut found_event = false;
468 let mut got_eose = false;
469 125
470 for _ in 0..10 { 126 relay.stop().await;
471 if let Some(Ok(Message::Text(text))) = ws.next().await {
472 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
473
474 if response[0] == "EVENT" {
475 assert_eq!(response[1], "test-sub");
476 found_event = true;
477 } else if response[0] == "EOSE" {
478 assert_eq!(response[1], "test-sub");
479 got_eose = true;
480 break;
481 }
482 }
483 }
484 127
485 assert!(found_event, "Should have received the state event"); 128 // Filter to rejection tests (these verify tag validation)
486 assert!(got_eose, "Should have received EOSE"); 129 let rejection_tests: Vec<_> = results
487} 130 .results
488 131 .iter()
489/// Test duplicate event handling 132 .filter(|t| t.name.contains("reject"))
490#[tokio::test] 133 .collect();
491async fn test_duplicate_announcement() {
492 let relay = TestRelay::start().await;
493 let keys = Keys::generate();
494 134
495 let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect"); 135 // Should have rejection tests
496 136 assert!(
497 let event = create_announcement( 137 !rejection_tests.is_empty(),
498 &keys, 138 "No rejection tests found (these are critical for security)"
499 &relay.domain(),
500 "duplicate-test",
501 vec![format!(
502 "https://{}/alice/duplicate-test.git",
503 relay.domain()
504 )],
505 vec![format!("wss://{}", relay.domain())],
506 ); 139 );
507 140
508 // Send first time 141 // All rejection tests should pass
509 let event_msg = json!(["EVENT", event]); 142 for test in rejection_tests {
510 ws.send(Message::Text(event_msg.to_string().into()))
511 .await
512 .expect("Failed to send event");
513
514 if let Some(Ok(Message::Text(text))) = ws.next().await {
515 let response1: Value = serde_json::from_str(&text).expect("Failed to parse");
516 assert_eq!(response1[2], true, "First send should succeed");
517 }
518
519 // Send second time (duplicate)
520 let event_msg = json!(["EVENT", event]);
521 ws.send(Message::Text(event_msg.to_string().into()))
522 .await
523 .expect("Failed to send event");
524
525 if let Some(Ok(Message::Text(text))) = ws.next().await {
526 let response2: Value = serde_json::from_str(&text).expect("Failed to parse");
527 assert_eq!(response2[2], true, "Duplicate should be acknowledged");
528
529 let message = response2[3].as_str().unwrap();
530 assert!( 143 assert!(
531 message.contains("duplicate") || message.is_empty(), 144 test.passed,
532 "Should indicate duplicate: {}", 145 "Rejection test failed: {} - {}\nThis is a security issue!",
533 message 146 test.name,
147 test.error.as_deref().unwrap_or("unknown error")
534 ); 148 );
535 } 149 }
536} 150}