diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-07 20:41:01 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-07 20:41:01 +0000 |
| commit | 7467aa9ace94b4e132eedd03c9daddb2d59813c4 (patch) | |
| tree | 32c4571a7376138eb429017a48dbde7ad8b15be6 /tests/common/mock_relay.rs | |
| parent | e7c18cf2a96b1f45e5f21a83ee1fe2e18a6dc7e2 (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.rs | 336 |
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 | |||
| 36 | use std::net::SocketAddr; | ||
| 37 | use std::sync::Arc; | ||
| 38 | |||
| 39 | use http_body_util::Full; | ||
| 40 | use hyper::body::Bytes; | ||
| 41 | use hyper::header::{CONNECTION, SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_KEY, UPGRADE}; | ||
| 42 | use hyper::server::conn::http1; | ||
| 43 | use hyper::service::service_fn; | ||
| 44 | use hyper::{Request, Response, StatusCode}; | ||
| 45 | use hyper_util::rt::TokioIo; | ||
| 46 | use nostr_relay_builder::prelude::*; | ||
| 47 | use tokio::net::TcpListener; | ||
| 48 | use 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). | ||
| 54 | pub 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 | |||
| 69 | impl 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 | |||
| 170 | impl 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. | ||
| 180 | async 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. | ||
| 235 | fn 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. | ||
| 249 | fn 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. | ||
| 262 | async 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)] | ||
| 287 | mod 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 | } | ||