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