upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests/common/mock_relay.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-01-07 20:41:01 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-07 20:41:01 +0000
commit7467aa9ace94b4e132eedd03c9daddb2d59813c4 (patch)
tree32c4571a7376138eb429017a48dbde7ad8b15be6 /tests/common/mock_relay.rs
parente7c18cf2a96b1f45e5f21a83ee1fe2e18a6dc7e2 (diff)
test: added purgatory git data sync intergration tests agregating from mulitple git servers
Diffstat (limited to 'tests/common/mock_relay.rs')
-rw-r--r--tests/common/mock_relay.rs336
1 files changed, 336 insertions, 0 deletions
diff --git a/tests/common/mock_relay.rs b/tests/common/mock_relay.rs
new file mode 100644
index 0000000..123c29e
--- /dev/null
+++ b/tests/common/mock_relay.rs
@@ -0,0 +1,336 @@
1//! Mock Nostr Relay for Testing
2//!
3//! Provides a simple Nostr relay that accepts all events without validation.
4//! Uses rust-nostr's `LocalRelayBuilder` to create an in-memory relay.
5//!
6//! # Usage
7//!
8//! ```ignore
9//! use common::MockRelay;
10//!
11//! #[tokio::test]
12//! async fn test_mock_relay() {
13//! // Start the mock relay
14//! let mock = MockRelay::start().await;
15//!
16//! // Use mock.url() for WebSocket connections
17//! let client = Client::new(keys);
18//! client.add_relay(mock.url()).await.unwrap();
19//!
20//! // All events are accepted without validation
21//! client.send_event(&event).await.unwrap();
22//!
23//! // Cleanup
24//! mock.stop().await;
25//! }
26//! ```
27//!
28//! # How It Works
29//!
30//! The mock relay:
31//! - Uses `LocalRelayBuilder::default().build()` which accepts all events
32//! - Runs an HTTP server with WebSocket upgrade support
33//! - Stores events in an in-memory database
34//! - Does NOT perform any GRASP validation (no purgatory, no git data checks)
35
36use std::net::SocketAddr;
37use std::sync::Arc;
38
39use http_body_util::Full;
40use hyper::body::Bytes;
41use hyper::header::{CONNECTION, SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_KEY, UPGRADE};
42use hyper::server::conn::http1;
43use hyper::service::service_fn;
44use hyper::{Request, Response, StatusCode};
45use hyper_util::rt::TokioIo;
46use nostr_relay_builder::prelude::*;
47use tokio::net::TcpListener;
48use tokio::sync::oneshot;
49
50/// Mock Nostr relay that accepts all events without validation.
51///
52/// This relay is useful for testing scenarios where you need a relay
53/// that serves events without GRASP validation (no purgatory, no git checks).
54pub struct MockRelay {
55 /// Shutdown signal sender
56 shutdown_tx: Option<oneshot::Sender<()>>,
57 /// Server task handle
58 handle: Option<tokio::task::JoinHandle<()>>,
59 /// Server URL (ws://127.0.0.1:<port>)
60 url: String,
61 /// Server port
62 #[allow(dead_code)]
63 port: u16,
64 /// The underlying LocalRelay (kept alive for the server lifetime)
65 #[allow(dead_code)]
66 relay: LocalRelay,
67}
68
69impl MockRelay {
70 /// Start a mock relay on a random free port.
71 ///
72 /// The relay accepts all events without validation and stores them
73 /// in an in-memory database.
74 pub async fn start() -> Self {
75 let port = find_free_port();
76 Self::start_on_port(port).await
77 }
78
79 /// Start a mock relay on a specific port.
80 pub async fn start_on_port(port: u16) -> Self {
81 let addr: SocketAddr = ([127, 0, 0, 1], port).into();
82
83 // Create a simple relay with no write policy (accepts all events)
84 let relay = LocalRelayBuilder::default().build();
85
86 // Create shutdown channel
87 let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
88
89 // Clone relay for the server task
90 let server_relay = relay.clone();
91
92 // Start the HTTP/WebSocket server
93 let listener = TcpListener::bind(addr)
94 .await
95 .expect("Failed to bind to address");
96
97 let handle = tokio::spawn(async move {
98 loop {
99 tokio::select! {
100 accept_result = listener.accept() => {
101 match accept_result {
102 Ok((stream, remote_addr)) => {
103 let relay = server_relay.clone();
104 let io = TokioIo::new(stream);
105
106 tokio::spawn(async move {
107 let service = service_fn(move |req| {
108 let relay = relay.clone();
109 async move { handle_request(req, relay, remote_addr).await }
110 });
111
112 if let Err(e) = http1::Builder::new()
113 .serve_connection(io, service)
114 .with_upgrades()
115 .await
116 {
117 // Connection errors are expected when client disconnects
118 if !e.to_string().contains("connection") {
119 eprintln!("MockRelay connection error: {}", e);
120 }
121 }
122 });
123 }
124 Err(e) => {
125 eprintln!("MockRelay accept error: {}", e);
126 }
127 }
128 }
129 _ = &mut shutdown_rx => {
130 // Shutdown signal received
131 break;
132 }
133 }
134 }
135 });
136
137 let url = format!("ws://127.0.0.1:{}", port);
138
139 // Wait for server to be ready
140 wait_for_server_ready(port).await;
141
142 Self {
143 shutdown_tx: Some(shutdown_tx),
144 handle: Some(handle),
145 url,
146 port,
147 relay,
148 }
149 }
150
151 /// Get the relay WebSocket URL.
152 pub fn url(&self) -> &str {
153 &self.url
154 }
155
156 /// Stop the mock relay.
157 pub async fn stop(mut self) {
158 // Send shutdown signal
159 if let Some(tx) = self.shutdown_tx.take() {
160 let _ = tx.send(());
161 }
162
163 // Wait for server task to complete
164 if let Some(handle) = self.handle.take() {
165 let _ = handle.await;
166 }
167 }
168}
169
170impl Drop for MockRelay {
171 fn drop(&mut self) {
172 // Send shutdown signal if not already sent
173 if let Some(tx) = self.shutdown_tx.take() {
174 let _ = tx.send(());
175 }
176 }
177}
178
179/// Handle an HTTP request, upgrading to WebSocket if requested.
180async fn handle_request(
181 req: Request<hyper::body::Incoming>,
182 relay: LocalRelay,
183 addr: SocketAddr,
184) -> Result<Response<Full<Bytes>>, hyper::Error> {
185 // Check for WebSocket upgrade request
186 let is_websocket = req
187 .headers()
188 .get(UPGRADE)
189 .map(|v| v.to_str().unwrap_or("").to_lowercase() == "websocket")
190 .unwrap_or(false);
191
192 if is_websocket {
193 // Get the Sec-WebSocket-Key header
194 let key = req
195 .headers()
196 .get(SEC_WEBSOCKET_KEY)
197 .and_then(|k| k.to_str().ok())
198 .map(|k| k.to_string());
199
200 if let Some(key) = key {
201 let accept_key = derive_accept_key(key.as_bytes());
202
203 // Spawn task to handle the upgraded connection
204 tokio::spawn(async move {
205 match hyper::upgrade::on(req).await {
206 Ok(upgraded) => {
207 if let Err(e) = relay.take_connection(TokioIo::new(upgraded), addr).await {
208 eprintln!("MockRelay WebSocket error: {}", e);
209 }
210 }
211 Err(e) => eprintln!("MockRelay upgrade error: {}", e),
212 }
213 });
214
215 // Return 101 Switching Protocols
216 return Ok(Response::builder()
217 .status(StatusCode::SWITCHING_PROTOCOLS)
218 .header(CONNECTION, "upgrade")
219 .header(UPGRADE, "websocket")
220 .header(SEC_WEBSOCKET_ACCEPT, accept_key)
221 .body(Full::new(Bytes::new()))
222 .unwrap());
223 }
224 }
225
226 // Non-WebSocket request - return simple response
227 Ok(Response::builder()
228 .status(StatusCode::OK)
229 .header("Content-Type", "text/plain")
230 .body(Full::new(Bytes::from("MockRelay - Nostr test relay")))
231 .unwrap())
232}
233
234/// Derive the Sec-WebSocket-Accept key from the request key.
235fn derive_accept_key(request_key: &[u8]) -> String {
236 use nostr_sdk::hashes::sha1::Hash as Sha1Hash;
237 use nostr_sdk::hashes::{Hash, HashEngine};
238
239 const WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
240
241 let mut engine = Sha1Hash::engine();
242 engine.input(request_key);
243 engine.input(WS_GUID);
244 let hash = Sha1Hash::from_engine(engine);
245 base64::Engine::encode(&base64::engine::general_purpose::STANDARD, hash.as_byte_array())
246}
247
248/// Find a free port to use for the server.
249fn find_free_port() -> u16 {
250 use std::net::TcpListener;
251
252 let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to random port");
253 let port = listener
254 .local_addr()
255 .expect("Failed to get local addr")
256 .port();
257 drop(listener);
258 port
259}
260
261/// Wait for the server to be ready to accept connections.
262async fn wait_for_server_ready(port: u16) {
263 let max_attempts = 50; // 5 seconds total
264 let delay = std::time::Duration::from_millis(100);
265
266 for attempt in 0..max_attempts {
267 match tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port)).await {
268 Ok(_) => {
269 // Connection successful, server is ready
270 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
271 return;
272 }
273 Err(_) => {
274 if attempt == max_attempts - 1 {
275 panic!(
276 "MockRelay failed to start after {} attempts",
277 max_attempts
278 );
279 }
280 tokio::time::sleep(delay).await;
281 }
282 }
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use nostr_sdk::prelude::*;
290 use std::time::Duration;
291
292 #[tokio::test]
293 async fn test_mock_relay_starts_and_stops() {
294 let mock = MockRelay::start().await;
295
296 // Verify URL is set
297 assert!(mock.url().starts_with("ws://127.0.0.1:"));
298
299 mock.stop().await;
300 }
301
302 #[tokio::test]
303 async fn test_mock_relay_accepts_events() {
304 let mock = MockRelay::start().await;
305
306 // Create a client and connect
307 let keys = Keys::generate();
308 let client = Client::new(keys.clone());
309 client.add_relay(mock.url()).await.expect("Failed to add relay");
310 client.connect().await;
311
312 // Wait for connection
313 tokio::time::sleep(Duration::from_millis(500)).await;
314
315 // Create and send a simple event
316 let event = EventBuilder::text_note("Test note from MockRelay test")
317 .sign_with_keys(&keys)
318 .expect("Failed to sign event");
319
320 let result = client.send_event(&event).await;
321 assert!(result.is_ok(), "MockRelay should accept events");
322
323 // Verify event was stored by fetching it back
324 let filter = Filter::new().id(event.id);
325 let events = client
326 .fetch_events(filter, Duration::from_secs(2))
327 .await
328 .expect("Failed to fetch events");
329
330 assert!(!events.is_empty(), "Event should be stored and retrievable");
331 assert_eq!(events.first().unwrap().id, event.id);
332
333 client.disconnect().await;
334 mock.stop().await;
335 }
336}