upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/specs/nip01_smoke.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-05 20:13:22 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-05 20:25:38 +0000
commit396da002fefeeb4549e11ff51abf824e91a6ed88 (patch)
treed5a61d081d97b52a38f7ce8f4a28d8c200eeede3 /grasp-audit/src/specs/nip01_smoke.rs
parentb22cb23928ef799b0a5d362003d3084d2ab267b4 (diff)
restructure grasp01 audit tests and add event acceptance
Diffstat (limited to 'grasp-audit/src/specs/nip01_smoke.rs')
-rw-r--r--grasp-audit/src/specs/nip01_smoke.rs311
1 files changed, 0 insertions, 311 deletions
diff --git a/grasp-audit/src/specs/nip01_smoke.rs b/grasp-audit/src/specs/nip01_smoke.rs
deleted file mode 100644
index 9ed0f56..0000000
--- a/grasp-audit/src/specs/nip01_smoke.rs
+++ /dev/null
@@ -1,311 +0,0 @@
1//! NIP-01 Smoke Tests
2//!
3//! These tests verify basic Nostr relay functionality.
4//! We don't comprehensively test NIP-01 because rust-nostr already has 1000+ tests.
5//! These are just smoke tests to ensure the relay is working at all.
6
7use crate::{AuditClient, AuditResult, TestResult};
8use nostr_sdk::prelude::*;
9
10pub struct Nip01SmokeTests;
11
12impl Nip01SmokeTests {
13 /// Run all NIP-01 smoke tests
14 pub async fn run_all(client: &AuditClient) -> AuditResult {
15 let mut results = AuditResult::new("NIP-01 Smoke Tests");
16
17 // Run tests sequentially to avoid future type issues
18 results.add(Self::test_websocket_connection(client).await);
19 results.add(Self::test_send_receive_event(client).await);
20 results.add(Self::test_create_subscription(client).await);
21 results.add(Self::test_close_subscription(client).await);
22 results.add(Self::test_reject_invalid_signature(client).await);
23 results.add(Self::test_reject_invalid_event_id(client).await);
24
25 results
26 }
27
28 /// Test 1: Can establish WebSocket connection
29 ///
30 /// Spec: NIP-01 basic requirement
31 /// Requirement: MUST serve a relay at / via WebSocket
32 async fn test_websocket_connection(client: &AuditClient) -> TestResult {
33 TestResult::new(
34 "websocket_connection",
35 "NIP-01:basic",
36 "Can establish WebSocket connection to /",
37 )
38 .run(|| async {
39 if !client.is_connected().await {
40 return Err("Failed to connect to relay".to_string());
41 }
42
43 Ok(())
44 })
45 .await
46 }
47
48 /// Test 2: Can send EVENT and receive OK response
49 ///
50 /// Spec: NIP-01 EVENT message
51 /// Requirement: Relay MUST accept valid EVENT messages
52 ///
53 /// For GRASP servers, we send a NIP-34 repository announcement that lists
54 /// the GRASP server in clone and relays tags (required for acceptance).
55 async fn test_send_receive_event(client: &AuditClient) -> TestResult {
56 TestResult::new(
57 "send_receive_event",
58 "NIP-01:event-message",
59 "Can send EVENT and receive OK response",
60 )
61 .run(|| async {
62 // Create a NIP-34 announcement event
63 let event = client.create_repo_announcement("send_receive_event").await
64 .map_err(|e| format!("Failed to create announcement: {}", e))?;
65
66 // Send event
67 let event_id = client
68 .send_event(event.clone())
69 .await
70 .map_err(|e| format!("Failed to send event: {}", e))?;
71
72 // Verify we got an event ID back
73 if event_id != event.id {
74 return Err(format!(
75 "Event ID mismatch: sent {}, got {}",
76 event.id, event_id
77 ));
78 }
79
80 // Wait a bit for event to be indexed
81 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
82
83 // Try to query it back
84 let filter = Filter::new()
85 .kind(Kind::Custom(30617))
86 .id(event_id);
87
88 let events = client
89 .query(filter)
90 .await
91 .map_err(|e| format!("Failed to query event: {}", e))?;
92
93 if events.is_empty() {
94 // Debug: try querying without audit client filtering
95 eprintln!("Event not found with audit client query, trying direct client query...");
96 let direct_filter = Filter::new().kind(Kind::Custom(30617)).id(event_id);
97 let direct_events = client.client().fetch_events(direct_filter, std::time::Duration::from_secs(5)).await
98 .map_err(|e| format!("Direct query failed: {}", e))?;
99 let direct_vec: Vec<Event> = direct_events.into_iter().collect();
100 eprintln!("Direct query found {} events", direct_vec.len());
101 if !direct_vec.is_empty() {
102 eprintln!("Event tags: {:?}", direct_vec[0].tags);
103 }
104 return Err(format!("Event not found after sending (direct query found {})", direct_vec.len()));
105 }
106
107 if events[0].id != event_id {
108 return Err("Retrieved event has different ID".to_string());
109 }
110
111 Ok(())
112 })
113 .await
114 }
115
116 /// Test 3: Can create subscription with REQ
117 ///
118 /// Spec: NIP-01 REQ message
119 /// Requirement: Relay MUST support REQ subscriptions
120 async fn test_create_subscription(client: &AuditClient) -> TestResult {
121 TestResult::new(
122 "create_subscription",
123 "NIP-01:req-message",
124 "Can create subscription with REQ and receive EOSE",
125 )
126 .run(|| async {
127 // Create a NIP-34 announcement event (accepted by GRASP relays)
128 let event = client.create_repo_announcement("create_subscription").await
129 .map_err(|e| format!("Failed to create announcement: {}", e))?;
130
131 let event_id = client
132 .send_event(event.clone())
133 .await
134 .map_err(|e| format!("Failed to send event: {}", e))?;
135
136 // Subscribe to NIP-34 announcements from this author
137 let filter = Filter::new()
138 .kind(Kind::Custom(30617))
139 .author(client.public_key());
140
141 let events = client
142 .subscribe(vec![filter], Some(std::time::Duration::from_secs(5)))
143 .await
144 .map_err(|e| format!("Failed to subscribe: {}", e))?;
145
146 // Should have at least our event
147 if events.is_empty() {
148 return Err("No events received from subscription".to_string());
149 }
150
151 Ok(())
152 })
153 .await
154 }
155
156 /// Test 4: Can close subscription with CLOSE
157 ///
158 /// Spec: NIP-01 CLOSE message
159 /// Requirement: Relay MUST support CLOSE to end subscriptions
160 async fn test_close_subscription(client: &AuditClient) -> TestResult {
161 TestResult::new(
162 "close_subscription",
163 "NIP-01:close-message",
164 "Can close subscriptions",
165 )
166 .run(|| async {
167 // For now, we just verify we can query events
168 // Full subscription management with CLOSE would require
169 // lower-level WebSocket access
170
171 let filter = Filter::new()
172 .kind(Kind::TextNote)
173 .limit(1);
174
175 let _events = client
176 .subscribe(vec![filter], Some(std::time::Duration::from_secs(2)))
177 .await
178 .map_err(|e| format!("Failed to subscribe: {}", e))?;
179
180 // If we got here, subscription worked
181 Ok(())
182 })
183 .await
184 }
185
186 /// Test 5: Rejects events with invalid signatures
187 ///
188 /// Spec: NIP-01 event validation
189 /// Requirement: Relay MUST reject events with invalid signatures
190 async fn test_reject_invalid_signature(client: &AuditClient) -> TestResult {
191 TestResult::new(
192 "reject_invalid_signature",
193 "NIP-01:validation",
194 "Rejects events with invalid signatures",
195 )
196 .run(|| async {
197 // Create a valid event
198 let event = client
199 .event_builder(Kind::TextNote, "Invalid signature test")
200 .build(client.keys())
201 .map_err(|e| format!("Failed to build event: {}", e))?;
202
203 // Corrupt the signature by creating a new event with wrong sig
204 // We'll use a different key to sign, creating an invalid signature
205 let wrong_keys = Keys::generate();
206 let wrong_event = EventBuilder::new(event.kind, event.content.clone())
207 .tags(event.tags.clone())
208 .sign_with_keys(&wrong_keys)
209 .map_err(|e| format!("Failed to build wrong event: {}", e))?;
210
211 // Create event JSON with mismatched pubkey and signature
212 // This should be rejected by the relay
213 let invalid_event_json = serde_json::json!({
214 "id": event.id.to_hex(),
215 "pubkey": event.pubkey.to_hex(),
216 "created_at": event.created_at.as_u64(),
217 "kind": event.kind.as_u16(),
218 "tags": event.tags,
219 "content": event.content,
220 "sig": wrong_event.sig.to_string(), // Wrong signature!
221 });
222
223 // Parse it back to an Event
224 let invalid_event: Event = serde_json::from_value(invalid_event_json)
225 .map_err(|e| format!("Failed to create invalid event: {}", e))?;
226
227 // Try to send the invalid event
228 let result = client.send_event(invalid_event).await;
229
230 // We expect this to fail
231 if result.is_ok() {
232 return Err("Relay accepted event with invalid signature".to_string());
233 }
234
235 Ok(())
236 })
237 .await
238 }
239
240 /// Test 6: Rejects events with invalid event IDs
241 ///
242 /// Spec: NIP-01 event ID validation
243 /// Requirement: Relay MUST reject events where ID doesn't match hash
244 async fn test_reject_invalid_event_id(client: &AuditClient) -> TestResult {
245 TestResult::new(
246 "reject_invalid_event_id",
247 "NIP-01:validation",
248 "Rejects events with invalid event IDs",
249 )
250 .run(|| async {
251 // Create a valid event
252 let event = client
253 .event_builder(Kind::TextNote, "Invalid ID test")
254 .build(client.keys())
255 .map_err(|e| format!("Failed to build event: {}", e))?;
256
257 // Create event JSON with corrupted ID
258 let invalid_event_json = serde_json::json!({
259 "id": EventId::all_zeros().to_hex(), // Wrong ID!
260 "pubkey": event.pubkey.to_hex(),
261 "created_at": event.created_at.as_u64(),
262 "kind": event.kind.as_u16(),
263 "tags": event.tags,
264 "content": event.content,
265 "sig": event.sig.to_string(),
266 });
267
268 // Parse it back to an Event
269 let invalid_event: Event = serde_json::from_value(invalid_event_json)
270 .map_err(|e| format!("Failed to create invalid event: {}", e))?;
271
272 // Try to send the invalid event
273 let result = client.send_event(invalid_event).await;
274
275 // We expect this to fail
276 if result.is_ok() {
277 return Err("Relay accepted event with invalid ID".to_string());
278 }
279
280 Ok(())
281 })
282 .await
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use crate::AuditConfig;
290
291 // Note: These tests require a running relay
292 // They are integration tests, not unit tests
293
294 #[tokio::test]
295 #[ignore] // Ignore by default since it needs a running relay
296 async fn test_smoke_tests_against_relay() {
297 // RELAY_URL env var must be set - no default fallback
298 let relay_url = std::env::var("RELAY_URL")
299 .expect("RELAY_URL environment variable must be set for integration tests");
300
301 let config = AuditConfig::ci();
302 let client = AuditClient::new(&relay_url, config)
303 .await
304 .expect("Failed to connect to relay");
305
306 let results = Nip01SmokeTests::run_all(&client).await;
307 results.print_report();
308
309 assert!(results.all_passed(), "Some smoke tests failed");
310 }
311}