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-20 21:45:45 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-20 21:45:45 +0000
commitca50f5b98f30d0933a510c05db86b608afee73a0 (patch)
treecdd694a82ba413220d80082056c34de444904f59 /tests
parent89c69eae8e75d2b00794087d9ef74fd4856d0f88 (diff)
replace tests to use grasp-audit lib as much as possible
Diffstat (limited to 'tests')
-rw-r--r--tests/nip01_compliance.rs80
-rw-r--r--tests/nip34_announcements.rs580
2 files changed, 125 insertions, 535 deletions
diff --git a/tests/nip01_compliance.rs b/tests/nip01_compliance.rs
index 05957fd..4cb2af4 100644
--- a/tests/nip01_compliance.rs
+++ b/tests/nip01_compliance.rs
@@ -1,14 +1,13 @@
1//! NIP-01 Compliance Integration Tests 1//! NIP-01 Compliance Integration Tests
2//! 2//!
3//! These tests verify that ngit-grasp relay implements NIP-01 correctly 3//! Tests ngit-grasp relay's NIP-01 compliance using grasp-audit library.
4//! by using the grasp-audit library to run compliance tests. 4//! Avoids code duplication by delegating to grasp-audit's test suite.
5//! 5//!
6//! # Test Strategy 6//! # Test Strategy
7//! 7//!
8//! - Uses grasp-audit as a library (not CLI) 8//! - Uses TestRelay fixture for ngit-grasp relay lifecycle management
9//! - Automatically manages relay lifecycle 9//! - Uses grasp-audit's Nip01SmokeTests for actual test logic
10//! - Reuses test specs from grasp-audit (single source of truth) 10//! - Minimal duplication - single source of truth in grasp-audit
11//! - Pure Rust, no shell scripts
12//! 11//!
13//! # Running Tests 12//! # Running Tests
14//! 13//!
@@ -30,17 +29,20 @@ use grasp_audit::*;
30 29
31/// Test NIP-01 smoke tests against ngit-grasp relay 30/// Test NIP-01 smoke tests against ngit-grasp relay
32/// 31///
33/// This test: 32/// This test runs all NIP-01 smoke tests from grasp-audit against
34/// 1. Starts a fresh ngit-grasp relay instance 33/// the ngit-grasp relay implementation.
35/// 2. Runs all NIP-01 smoke tests from grasp-audit 34///
36/// 3. Verifies all tests pass 35/// Tests cover:
37/// 4. Shuts down the relay 36/// - WebSocket connection
37/// - Event send/receive
38/// - Subscriptions (REQ/CLOSE)
39/// - Event validation (signature, ID)
38#[tokio::test] 40#[tokio::test]
39async fn test_nip01_smoke() { 41async fn test_nip01_smoke() {
40 // Start test relay 42 // Start test relay
41 let relay = TestRelay::start().await; 43 let relay = TestRelay::start().await;
42 44
43 // Create audit client in CI mode (isolated, no cleanup needed) 45 // Create audit client in CI mode (isolated testing)
44 let config = AuditConfig::ci(); 46 let config = AuditConfig::ci();
45 let client = AuditClient::new(relay.url(), config) 47 let client = AuditClient::new(relay.url(), config)
46 .await 48 .await
@@ -64,34 +66,12 @@ async fn test_nip01_smoke() {
64 ); 66 );
65} 67}
66 68
67/// Test individual NIP-01 tests can be run separately 69/// Test that relay properly validates events
68///
69/// This demonstrates that we can run individual tests from the specs
70/// for more granular testing or debugging.
71#[tokio::test]
72async fn test_nip01_individual_tests() {
73 use grasp_audit::specs::grasp01::Nip01SmokeTests;
74
75 let relay = TestRelay::start().await;
76 let config = AuditConfig::ci();
77 let client = AuditClient::new(relay.url(), config)
78 .await
79 .expect("Failed to create audit client");
80
81 // We can't call private methods, so we'll run the full suite
82 // This test is mainly to show the pattern
83 let all_results = Nip01SmokeTests::run_all(&client).await;
84
85 relay.stop().await;
86
87 // Verify
88 assert!(all_results.all_passed());
89}
90
91/// Test that relay rejects invalid events
92/// 70///
93/// This is a critical security test - we want to ensure the relay 71/// Critical security test - ensures relay validates:
94/// properly validates events before accepting them. 72/// - Event signatures
73/// - Event IDs
74/// - Other NIP-01 requirements
95#[tokio::test] 75#[tokio::test]
96async fn test_relay_validates_events() { 76async fn test_relay_validates_events() {
97 let relay = TestRelay::start().await; 77 let relay = TestRelay::start().await;
@@ -100,29 +80,29 @@ async fn test_relay_validates_events() {
100 .await 80 .await
101 .expect("Failed to create audit client"); 81 .expect("Failed to create audit client");
102 82
103 // The validation tests are part of the smoke tests 83 // Run smoke tests which include validation tests
104 let results = specs::Nip01SmokeTests::run_all(&client).await; 84 let results = specs::Nip01SmokeTests::run_all(&client).await;
105 85
106 // Check that validation tests exist and pass 86 relay.stop().await;
87
88 // Filter to validation tests
107 let validation_tests: Vec<_> = results 89 let validation_tests: Vec<_> = results
108 .results 90 .results
109 .iter() 91 .iter()
110 .filter(|t| t.spec_ref.contains("validation")) 92 .filter(|t| t.name.contains("reject") || t.name.contains("invalid"))
111 .collect(); 93 .collect();
112 94
113 relay.stop().await;
114
115 // Should have validation tests 95 // Should have validation tests
116 assert!( 96 assert!(
117 !validation_tests.is_empty(), 97 !validation_tests.is_empty(),
118 "No validation tests found in NIP-01 smoke tests" 98 "No validation tests found (these are critical for security)"
119 ); 99 );
120 100
121 // All validation tests should pass 101 // All validation tests should pass
122 for test in validation_tests { 102 for test in validation_tests {
123 assert!( 103 assert!(
124 test.passed, 104 test.passed,
125 "Validation test failed: {} - {}", 105 "Validation test failed: {} - {}\nThis is a security issue!",
126 test.name, 106 test.name,
127 test.error.as_deref().unwrap_or("unknown error") 107 test.error.as_deref().unwrap_or("unknown error")
128 ); 108 );
@@ -131,7 +111,7 @@ async fn test_relay_validates_events() {
131 111
132/// Test relay lifecycle management 112/// Test relay lifecycle management
133/// 113///
134/// Ensures our test fixture properly manages relay lifecycle 114/// Verifies TestRelay fixture properly manages relay lifecycle
135#[tokio::test] 115#[tokio::test]
136async fn test_relay_lifecycle() { 116async fn test_relay_lifecycle() {
137 // Start relay 117 // Start relay
@@ -148,15 +128,11 @@ async fn test_relay_lifecycle() {
148 128
149 // Stop relay 129 // Stop relay
150 relay.stop().await; 130 relay.stop().await;
151
152 // Note: We can't easily verify disconnection without modifying grasp-audit
153 // to expose connection state after relay shutdown. That's okay - the
154 // important part is that the relay starts and stops cleanly.
155} 131}
156 132
157/// Test multiple relays can run in parallel 133/// Test multiple relays can run in parallel
158/// 134///
159/// This ensures our random port selection works correctly 135/// Ensures random port selection avoids conflicts
160#[tokio::test] 136#[tokio::test]
161async fn test_parallel_relays() { 137async fn test_parallel_relays() {
162 // Start two relays simultaneously 138 // Start two relays simultaneously
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}