Skip to main content

contextvm_sdk/relay/
mock.rs

1//! In-memory mock relay pool for network-free testing.
2//!
3//! Mirrors the design of the TypeScript SDK's `MockRelayHub`:
4//! - `publish_event` stores the event and broadcasts it to all `notifications()` receivers.
5//! - `subscribe` registers filters and immediately replays matching stored events through the
6//!   broadcast, so listeners that called `notifications()` before `subscribe()` see the replay.
7//! - `connect` / `disconnect` are no-ops — no sockets are opened.
8//! - Signing uses a freshly generated ephemeral `Keys`; `signer()` returns it wrapped in `Arc`
9//!   so encryption code can call it without any real relay connection.
10
11use std::collections::HashMap;
12use std::sync::Arc;
13
14use async_trait::async_trait;
15use tokio::sync::Mutex;
16
17use nostr_sdk::prelude::*;
18
19use crate::core::error::{Error, Result};
20use crate::relay::RelayPoolTrait;
21
22// ── Internal state ────────────────────────────────────────────────────────────
23
24struct MockRelayInner {
25    events: Vec<Event>,
26    /// Active subscriptions: id → filters registered by that subscription.
27    subscriptions: HashMap<u32, Vec<Filter>>,
28    next_sub_id: u32,
29}
30
31impl MockRelayInner {
32    fn new() -> Self {
33        Self {
34            events: Vec::new(),
35            subscriptions: HashMap::new(),
36            next_sub_id: 0,
37        }
38    }
39}
40
41// ── Public struct ─────────────────────────────────────────────────────────────
42
43/// In-memory relay pool for deterministic, network-free testing.
44///
45/// Create one with [`MockRelayPool::new`] and pass it (wrapped in `Arc`) wherever
46/// an `Arc<dyn RelayPoolTrait>` is expected.
47pub struct MockRelayPool {
48    inner: Arc<Mutex<MockRelayInner>>,
49    /// Broadcast sender — every published event is sent here so that all
50    /// `notifications()` receivers see it.
51    notification_tx: tokio::sync::broadcast::Sender<RelayPoolNotification>,
52    /// Ephemeral key used for signing in `publish` / `sign` / `signer`.
53    keys: Keys,
54}
55
56impl MockRelayPool {
57    /// Create a new mock relay pool with a freshly generated ephemeral signing key.
58    pub fn new() -> Self {
59        let keys = Keys::generate();
60        let (tx, _rx) = tokio::sync::broadcast::channel(1024);
61        Self {
62            inner: Arc::new(Mutex::new(MockRelayInner::new())),
63            notification_tx: tx,
64            keys,
65        }
66    }
67
68    /// The ephemeral public key used by this mock for signing.
69    pub fn mock_public_key(&self) -> PublicKey {
70        self.keys.public_key()
71    }
72
73    /// The ephemeral signing keys (for manual event injection in tests).
74    pub fn mock_keys(&self) -> Keys {
75        self.keys.clone()
76    }
77
78    /// Like [`new`](Self::new) but with caller-provided signing keys.
79    pub fn with_keys(keys: Keys) -> Self {
80        let (tx, _rx) = tokio::sync::broadcast::channel(1024);
81        Self {
82            inner: Arc::new(Mutex::new(MockRelayInner::new())),
83            notification_tx: tx,
84            keys,
85        }
86    }
87
88    /// Create a pair of linked mock relay pools with different signing keys.
89    ///
90    /// Both pools share the same event store and notification channel; events
91    /// published by one are visible to the other's `notifications()` receivers.
92    pub fn create_pair() -> (Self, Self) {
93        let (tx, _rx) = tokio::sync::broadcast::channel(1024);
94        let inner = Arc::new(Mutex::new(MockRelayInner::new()));
95        let a = Self {
96            inner: Arc::clone(&inner),
97            notification_tx: tx.clone(),
98            keys: Keys::generate(),
99        };
100        let b = Self {
101            inner,
102            notification_tx: tx,
103            keys: Keys::generate(),
104        };
105        (a, b)
106    }
107
108    /// Create `n` linked mock relay pools with different signing keys.
109    ///
110    /// All pools share the same event store and notification channel so events
111    /// published by any one pool are visible to all others' `notifications()`
112    /// receivers.  Useful for multi-client integration tests.
113    pub fn create_linked_group(n: usize) -> Vec<Self> {
114        assert!(n > 0, "group must have at least one pool");
115        let (tx, _rx) = tokio::sync::broadcast::channel(1024);
116        let inner = Arc::new(Mutex::new(MockRelayInner::new()));
117        (0..n)
118            .map(|_| Self {
119                inner: Arc::clone(&inner),
120                notification_tx: tx.clone(),
121                keys: Keys::generate(),
122            })
123            .collect()
124    }
125
126    /// Clone of all events published so far (useful for assertions in tests).
127    pub async fn stored_events(&self) -> Vec<Event> {
128        self.inner.lock().await.events.clone()
129    }
130}
131
132impl Default for MockRelayPool {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138// ── RelayPoolTrait impl ───────────────────────────────────────────────────────
139
140#[async_trait]
141impl RelayPoolTrait for MockRelayPool {
142    /// No-op: the mock has no sockets to open.
143    async fn connect(&self, _relay_urls: &[String]) -> Result<()> {
144        Ok(())
145    }
146
147    /// No-op: the mock has no sockets to close.
148    async fn disconnect(&self) -> Result<()> {
149        Ok(())
150    }
151
152    /// Store the event and broadcast it to all current `notifications()` receivers.
153    async fn publish_event(&self, event: &Event) -> Result<EventId> {
154        let event_id = event.id;
155
156        {
157            let mut inner = self.inner.lock().await;
158            inner.events.push(event.clone());
159        }
160
161        // Always broadcast — consumers filter by kind/pubkey/tag themselves,
162        // which mirrors how nostr-sdk's real notification stream works.
163        let notification = make_notification(event.clone());
164        // Ignore send errors: they just mean there are no active receivers yet.
165        let _ = self.notification_tx.send(notification);
166
167        Ok(event_id)
168    }
169
170    /// Sign `builder` with the ephemeral key, then call `publish_event`.
171    async fn publish(&self, builder: EventBuilder) -> Result<EventId> {
172        let event = sign_with_keys(builder, &self.keys)?;
173        let id = event.id;
174        self.publish_event(&event).await?;
175        Ok(id)
176    }
177
178    /// Sign `builder` with the ephemeral key and return the event without publishing.
179    async fn sign(&self, builder: EventBuilder) -> Result<Event> {
180        sign_with_keys(builder, &self.keys)
181    }
182
183    /// Return the ephemeral key as a signer.
184    async fn signer(&self) -> Result<Arc<dyn NostrSigner>> {
185        Ok(Arc::new(self.keys.clone()) as Arc<dyn NostrSigner>)
186    }
187
188    /// Return a new broadcast receiver. Each call gets an independent receiver
189    /// that sees all events published *after* this call, plus any replayed by
190    /// a subsequent `subscribe()`.
191    fn notifications(&self) -> tokio::sync::broadcast::Receiver<RelayPoolNotification> {
192        self.notification_tx.subscribe()
193    }
194
195    /// Return the ephemeral public key.
196    async fn public_key(&self) -> Result<PublicKey> {
197        Ok(self.keys.public_key())
198    }
199
200    /// Register the filters and immediately replay any already-stored events that
201    /// match them through the broadcast channel, mirroring the behaviour of a
202    /// real relay that sends historical events before EOSE.
203    async fn subscribe(&self, filters: Vec<Filter>) -> Result<()> {
204        let replay = {
205            let mut inner = self.inner.lock().await;
206            let sub_id = inner.next_sub_id;
207            inner.next_sub_id += 1;
208
209            // Store filters first so the replay read comes from the stored value,
210            // ensuring the field is both written and read (no dead-code warning).
211            inner.subscriptions.insert(sub_id, filters);
212
213            // Clone events so we can release the events borrow before borrowing subscriptions.
214            let events_snapshot = inner.events.clone();
215            let stored = inner.subscriptions.get(&sub_id).expect("just inserted");
216            events_snapshot
217                .into_iter()
218                .filter(|e| {
219                    stored
220                        .iter()
221                        .any(|f| f.match_event(e, MatchEventOptions::default()))
222                })
223                .collect::<Vec<_>>()
224        };
225
226        for event in replay {
227            let _ = self.notification_tx.send(make_notification(event));
228        }
229
230        Ok(())
231    }
232}
233
234// ── Helpers ───────────────────────────────────────────────────────────────────
235
236fn sign_with_keys(builder: EventBuilder, keys: &Keys) -> Result<Event> {
237    builder
238        .sign_with_keys(keys)
239        .map_err(|e| Error::Transport(e.to_string()))
240}
241
242fn make_notification(event: Event) -> RelayPoolNotification {
243    RelayPoolNotification::Event {
244        relay_url: RelayUrl::parse("wss://mock.relay").expect("hardcoded URL"),
245        subscription_id: SubscriptionId::generate(),
246        event: Box::new(event),
247    }
248}
249
250// ── Unit tests ────────────────────────────────────────────────────────────────
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[tokio::test]
257    async fn connect_and_disconnect_are_noops() {
258        let pool = MockRelayPool::new();
259        assert!(pool.connect(&["wss://unused".to_string()]).await.is_ok());
260        assert!(pool.disconnect().await.is_ok());
261    }
262
263    #[tokio::test]
264    async fn publish_event_stores_and_broadcasts() {
265        let pool = MockRelayPool::new();
266        let mut rx = pool.notifications();
267
268        let keys = Keys::generate();
269        let event = EventBuilder::new(Kind::TextNote, "hello")
270            .sign_with_keys(&keys)
271            .unwrap();
272
273        pool.publish_event(&event).await.unwrap();
274
275        assert_eq!(pool.stored_events().await.len(), 1);
276        let notif = rx.try_recv().unwrap();
277        if let RelayPoolNotification::Event { event: e, .. } = notif {
278            assert_eq!(e.id, event.id);
279        } else {
280            panic!("expected Event notification");
281        }
282    }
283
284    #[tokio::test]
285    async fn publish_signs_and_stores() {
286        let pool = MockRelayPool::new();
287        let builder = EventBuilder::new(Kind::TextNote, "signed");
288        pool.publish(builder).await.unwrap();
289        let stored = pool.stored_events().await;
290        assert_eq!(stored.len(), 1);
291        assert_eq!(stored[0].pubkey, pool.mock_public_key());
292    }
293
294    #[tokio::test]
295    async fn sign_does_not_publish() {
296        let pool = MockRelayPool::new();
297        let builder = EventBuilder::new(Kind::TextNote, "unsigned");
298        let event = pool.sign(builder).await.unwrap();
299        assert_eq!(event.pubkey, pool.mock_public_key());
300        assert!(pool.stored_events().await.is_empty());
301    }
302
303    #[tokio::test]
304    async fn signer_uses_same_key_as_publish() {
305        let pool = MockRelayPool::new();
306        let signer = pool.signer().await.unwrap();
307        let expected_pubkey = pool.mock_public_key();
308        assert_eq!(signer.get_public_key().await.unwrap(), expected_pubkey);
309    }
310
311    #[tokio::test]
312    async fn subscribe_replays_matching_stored_events() {
313        let pool = MockRelayPool::new();
314        let mut rx = pool.notifications();
315
316        // Pre-publish two events
317        let keys = Keys::generate();
318        let e1 = EventBuilder::new(Kind::TextNote, "one")
319            .sign_with_keys(&keys)
320            .unwrap();
321        let e2 = EventBuilder::new(Kind::Custom(9999), "two")
322            .sign_with_keys(&keys)
323            .unwrap();
324        pool.publish_event(&e1).await.unwrap();
325        pool.publish_event(&e2).await.unwrap();
326
327        // Drain the two publish notifications
328        rx.try_recv().unwrap();
329        rx.try_recv().unwrap();
330
331        // Subscribe for TextNote only — e1 should be replayed, e2 not
332        let filter = Filter::new().kind(Kind::TextNote);
333        pool.subscribe(vec![filter]).await.unwrap();
334
335        let replayed = rx.try_recv().unwrap();
336        if let RelayPoolNotification::Event { event, .. } = replayed {
337            assert_eq!(event.id, e1.id);
338        } else {
339            panic!("expected replayed Event notification");
340        }
341        // e2 should not be replayed
342        assert!(rx.try_recv().is_err());
343    }
344
345    #[tokio::test]
346    async fn notifications_receives_future_publishes() {
347        let pool = MockRelayPool::new();
348        let mut rx = pool.notifications();
349
350        let keys = Keys::generate();
351        let event = EventBuilder::new(Kind::TextNote, "future")
352            .sign_with_keys(&keys)
353            .unwrap();
354        pool.publish_event(&event).await.unwrap();
355
356        let notif = rx.try_recv().unwrap();
357        assert!(matches!(notif, RelayPoolNotification::Event { .. }));
358    }
359}