upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-04 14:33:18 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-04 14:33:18 +0000
commitc2c0cdba4af434043f3fa707231d8f5a7e3fd882 (patch)
tree02adae65fba476e5fcdf1fadcd0ce9efa4f1fbcd /tests
parent4d89f4537c325f60571cc6339df0708ee8161514 (diff)
add announcement tests
Diffstat (limited to 'tests')
-rw-r--r--tests/announcement_tests.rs411
1 files changed, 411 insertions, 0 deletions
diff --git a/tests/announcement_tests.rs b/tests/announcement_tests.rs
new file mode 100644
index 0000000..137ba5f
--- /dev/null
+++ b/tests/announcement_tests.rs
@@ -0,0 +1,411 @@
1/// Integration tests for NIP-34 Repository Announcements (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
8use futures_util::{SinkExt, StreamExt};
9use nostr_sdk::{EventBuilder, Keys, Kind, Tag, TagKind};
10use serde_json::{json, Value};
11use tokio::net::TcpStream;
12use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream};
13
14type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
15
16const RELAY_URL: &str = "ws://127.0.0.1:7000";
17const DOMAIN: &str = "127.0.0.1:7000";
18
19const KIND_REPOSITORY_ANNOUNCEMENT: u16 = 30617;
20const KIND_REPOSITORY_STATE: u16 = 30618;
21
22/// Helper to connect to the relay
23async fn connect() -> WsStream {
24 let (ws_stream, _) = connect_async(RELAY_URL)
25 .await
26 .expect("Failed to connect to relay");
27 ws_stream
28}
29
30/// Helper to send an event and get the response
31async fn send_event(ws: &mut WsStream, event: nostr_sdk::Event) -> Value {
32 let event_msg = json!(["EVENT", event]);
33 ws.send(Message::Text(event_msg.to_string()))
34 .await
35 .expect("Failed to send event");
36
37 // Read response
38 if let Some(Ok(Message::Text(text))) = ws.next().await {
39 serde_json::from_str(&text).expect("Failed to parse response")
40 } else {
41 panic!("No response received");
42 }
43}
44
45/// Helper to create a repository announcement event
46fn create_announcement(
47 keys: &Keys,
48 identifier: &str,
49 clone_urls: Vec<&str>,
50 relays: Vec<&str>,
51) -> nostr_sdk::Event {
52 let mut tags = vec![Tag::custom(TagKind::D, vec![identifier.to_string()])];
53
54 for url in clone_urls {
55 tags.push(Tag::custom(
56 TagKind::Custom("clone".into()),
57 vec![url.to_string()],
58 ));
59 }
60
61 for relay in relays {
62 tags.push(Tag::custom(TagKind::Relays, vec![relay.to_string()]));
63 }
64
65 EventBuilder::new(
66 Kind::from(KIND_REPOSITORY_ANNOUNCEMENT),
67 "Test repository description",
68 tags,
69 )
70 .sign_with_keys(keys)
71 .expect("Failed to sign event")
72}
73
74/// Helper to create a repository state event
75fn create_state(keys: &Keys, identifier: &str, branches: Vec<(&str, &str)>) -> nostr_sdk::Event {
76 let mut tags = vec![Tag::custom(TagKind::D, vec![identifier.to_string()])];
77
78 for (branch, commit) in branches {
79 tags.push(Tag::custom(
80 TagKind::Custom("ref".into()),
81 vec![format!("refs/heads/{}", branch), commit.to_string()],
82 ));
83 }
84
85 EventBuilder::new(Kind::from(KIND_REPOSITORY_STATE), "", tags)
86 .sign_with_keys(keys)
87 .expect("Failed to sign event")
88}
89
90/// GRASP-01, Line 9-10: MUST serve a NIP-01 compliant nostr relay at `/`
91#[tokio::test]
92#[ignore] // Requires relay to be running
93async fn test_relay_accepts_connection() {
94 let _ws = connect().await;
95 // If we get here, connection succeeded
96}
97
98/// GRASP-01, Line 11: MUST accept repository announcements (kind 30617)
99#[tokio::test]
100#[ignore] // Requires relay to be running
101async fn test_accepts_valid_announcement() {
102 let mut ws = connect().await;
103 let keys = Keys::generate();
104
105 let event = create_announcement(
106 &keys,
107 "test-repo",
108 vec![&format!("https://{}/alice/test-repo.git", DOMAIN)],
109 vec![&format!("wss://{}", DOMAIN)],
110 );
111
112 let response = send_event(&mut ws, event.clone()).await;
113
114 // Should be ["OK", event_id, true, ""]
115 assert_eq!(response[0], "OK");
116 assert_eq!(response[1], event.id.to_hex());
117 assert_eq!(response[2], true, "Event should be accepted");
118}
119
120/// GRASP-01, Line 12-13: MUST reject announcements that do not list the service
121/// in both `clone` and `relays` tags
122#[tokio::test]
123#[ignore] // Requires relay to be running
124async fn test_rejects_announcement_without_clone() {
125 let mut ws = connect().await;
126 let keys = Keys::generate();
127
128 // Missing clone tag
129 let event = create_announcement(
130 &keys,
131 "test-repo",
132 vec![], // No clone URLs
133 vec![&format!("wss://{}", DOMAIN)],
134 );
135
136 let response = send_event(&mut ws, event.clone()).await;
137
138 // Should be rejected
139 assert_eq!(response[0], "OK");
140 assert_eq!(response[1], event.id.to_hex());
141 assert_eq!(response[2], false, "Event should be rejected");
142
143 let message = response[3].as_str().unwrap();
144 assert!(
145 message.contains("clone") || message.contains("invalid"),
146 "Error message should mention clone requirement: {}",
147 message
148 );
149}
150
151/// GRASP-01, Line 12-13: MUST reject announcements that do not list the service
152/// in both `clone` and `relays` tags
153#[tokio::test]
154#[ignore] // Requires relay to be running
155async fn test_rejects_announcement_without_relay() {
156 let mut ws = connect().await;
157 let keys = Keys::generate();
158
159 // Missing relay tag
160 let event = create_announcement(
161 &keys,
162 "test-repo",
163 vec![&format!("https://{}/alice/test-repo.git", DOMAIN)],
164 vec![], // No relays
165 );
166
167 let response = send_event(&mut ws, event.clone()).await;
168
169 // Should be rejected
170 assert_eq!(response[0], "OK");
171 assert_eq!(response[1], event.id.to_hex());
172 assert_eq!(response[2], false, "Event should be rejected");
173
174 let message = response[3].as_str().unwrap();
175 assert!(
176 message.contains("relays") || message.contains("invalid"),
177 "Error message should mention relay requirement: {}",
178 message
179 );
180}
181
182/// GRASP-01, Line 12-13: MUST reject announcements listing other services
183#[tokio::test]
184#[ignore] // Requires relay to be running
185async fn test_rejects_announcement_for_other_service() {
186 let mut ws = connect().await;
187 let keys = Keys::generate();
188
189 // Lists different service
190 let event = create_announcement(
191 &keys,
192 "test-repo",
193 vec!["https://other-service.com/alice/test-repo.git"],
194 vec!["wss://other-service.com"],
195 );
196
197 let response = send_event(&mut ws, event.clone()).await;
198
199 // Should be rejected
200 assert_eq!(response[0], "OK");
201 assert_eq!(response[1], event.id.to_hex());
202 assert_eq!(response[2], false, "Event should be rejected");
203}
204
205/// GRASP-01, Line 11: MUST accept repository state announcements (kind 30618)
206#[tokio::test]
207#[ignore] // Requires relay to be running
208async fn test_accepts_valid_state() {
209 let mut ws = connect().await;
210 let keys = Keys::generate();
211
212 let event = create_state(
213 &keys,
214 "test-repo",
215 vec![("main", "a1b2c3d4e5f6789012345678901234567890abcd")],
216 );
217
218 let response = send_event(&mut ws, event.clone()).await;
219
220 // Should be accepted
221 assert_eq!(response[0], "OK");
222 assert_eq!(response[1], event.id.to_hex());
223 assert_eq!(response[2], true, "State event should be accepted");
224}
225
226/// Test state event with multiple branches
227#[tokio::test]
228#[ignore] // Requires relay to be running
229async fn test_accepts_state_with_multiple_branches() {
230 let mut ws = connect().await;
231 let keys = Keys::generate();
232
233 let event = create_state(
234 &keys,
235 "test-repo",
236 vec![
237 ("main", "a1b2c3d4e5f6789012345678901234567890abcd"),
238 ("develop", "b2c3d4e5f6789012345678901234567890abcde"),
239 ("feature-x", "c3d4e5f6789012345678901234567890abcdef1"),
240 ],
241 );
242
243 let response = send_event(&mut ws, event.clone()).await;
244
245 assert_eq!(response[0], "OK");
246 assert_eq!(response[2], true, "State event should be accepted");
247}
248
249/// Test state event without identifier should be rejected
250#[tokio::test]
251#[ignore] // Requires relay to be running
252async fn test_rejects_state_without_identifier() {
253 let mut ws = connect().await;
254 let keys = Keys::generate();
255
256 // Create state without identifier
257 let event = EventBuilder::new(Kind::from(KIND_REPOSITORY_STATE), "", vec![])
258 .sign_with_keys(&keys)
259 .expect("Failed to sign event");
260
261 let response = send_event(&mut ws, event.clone()).await;
262
263 // Should be rejected
264 assert_eq!(response[0], "OK");
265 assert_eq!(response[1], event.id.to_hex());
266 assert_eq!(response[2], false, "Event should be rejected");
267
268 let message = response[3].as_str().unwrap();
269 assert!(
270 message.contains("identifier") || message.contains("invalid"),
271 "Error message should mention identifier requirement: {}",
272 message
273 );
274}
275
276/// Test querying for announcements
277#[tokio::test]
278#[ignore] // Requires relay to be running
279async fn test_query_announcements() {
280 let mut ws = connect().await;
281 let keys = Keys::generate();
282
283 // Send an announcement
284 let event = create_announcement(
285 &keys,
286 "query-test-repo",
287 vec![&format!("https://{}/alice/query-test-repo.git", DOMAIN)],
288 vec![&format!("wss://{}", DOMAIN)],
289 );
290
291 send_event(&mut ws, event.clone()).await;
292
293 // Query for announcements
294 let req = json!([
295 "REQ",
296 "test-sub",
297 {
298 "kinds": [KIND_REPOSITORY_ANNOUNCEMENT],
299 "authors": [keys.public_key().to_hex()]
300 }
301 ]);
302
303 ws.send(Message::Text(req.to_string()))
304 .await
305 .expect("Failed to send REQ");
306
307 // Read responses
308 let mut found_event = false;
309 let mut got_eose = false;
310
311 for _ in 0..10 {
312 if let Some(Ok(Message::Text(text))) = ws.next().await {
313 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
314
315 if response[0] == "EVENT" {
316 assert_eq!(response[1], "test-sub");
317 found_event = true;
318 } else if response[0] == "EOSE" {
319 assert_eq!(response[1], "test-sub");
320 got_eose = true;
321 break;
322 }
323 }
324 }
325
326 assert!(found_event, "Should have received the announcement");
327 assert!(got_eose, "Should have received EOSE");
328}
329
330/// Test querying for state events
331#[tokio::test]
332#[ignore] // Requires relay to be running
333async fn test_query_states() {
334 let mut ws = connect().await;
335 let keys = Keys::generate();
336
337 // Send a state event
338 let event = create_state(
339 &keys,
340 "query-test-repo",
341 vec![("main", "a1b2c3d4e5f6789012345678901234567890abcd")],
342 );
343
344 send_event(&mut ws, event.clone()).await;
345
346 // Query for states
347 let req = json!([
348 "REQ",
349 "test-sub",
350 {
351 "kinds": [KIND_REPOSITORY_STATE],
352 "authors": [keys.public_key().to_hex()]
353 }
354 ]);
355
356 ws.send(Message::Text(req.to_string()))
357 .await
358 .expect("Failed to send REQ");
359
360 // Read responses
361 let mut found_event = false;
362 let mut got_eose = false;
363
364 for _ in 0..10 {
365 if let Some(Ok(Message::Text(text))) = ws.next().await {
366 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
367
368 if response[0] == "EVENT" {
369 assert_eq!(response[1], "test-sub");
370 found_event = true;
371 } else if response[0] == "EOSE" {
372 assert_eq!(response[1], "test-sub");
373 got_eose = true;
374 break;
375 }
376 }
377 }
378
379 assert!(found_event, "Should have received the state event");
380 assert!(got_eose, "Should have received EOSE");
381}
382
383/// Test duplicate event handling
384#[tokio::test]
385#[ignore] // Requires relay to be running
386async fn test_duplicate_announcement() {
387 let mut ws = connect().await;
388 let keys = Keys::generate();
389
390 let event = create_announcement(
391 &keys,
392 "duplicate-test",
393 vec![&format!("https://{}/alice/duplicate-test.git", DOMAIN)],
394 vec![&format!("wss://{}", DOMAIN)],
395 );
396
397 // Send first time
398 let response1 = send_event(&mut ws, event.clone()).await;
399 assert_eq!(response1[2], true, "First send should succeed");
400
401 // Send second time (duplicate)
402 let response2 = send_event(&mut ws, event.clone()).await;
403 assert_eq!(response2[2], true, "Duplicate should be acknowledged");
404
405 let message = response2[3].as_str().unwrap();
406 assert!(
407 message.contains("duplicate") || message.is_empty(),
408 "Should indicate duplicate: {}",
409 message
410 );
411}