notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 9b869c489e3795e27a635a51b4abc167f43ef595
parent 5b2c8bc45472ef1e36bdedaebb099536aece9a42
Author: kernelkind <kernelkind@gmail.com>
Date:   Mon,  2 Feb 2026 13:56:35 -0500

test(outbox): `OutboxPool`

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/enostr/src/relay/outbox/mod.rs | 371+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 371 insertions(+), 0 deletions(-)

diff --git a/crates/enostr/src/relay/outbox/mod.rs b/crates/enostr/src/relay/outbox/mod.rs @@ -507,3 +507,374 @@ where wakeup, ) } + +#[cfg(test)] +mod tests { + use hashbrown::HashSet; + use nostrdb::Filter; + + use super::*; + use crate::relay::{ + coordinator::CoordinationTask, + test_utils::{filters_json, trivial_filter, MockWakeup}, + RelayUrlPkgs, + }; + + /// Ensures the subscription registry always yields unique IDs. + #[test] + fn registry_generates_unique_ids() { + let mut registry = SubRegistry { next_request_id: 0 }; + + let id1 = registry.next(); + let id2 = registry.next(); + let id3 = registry.next(); + + assert_ne!(id1, id2); + assert_ne!(id2, id3); + assert_ne!(id1, id3); + } + + // ==================== OutboxPool tests ==================== + + /// Default pool has no relays or subscriptions. + #[test] + fn outbox_pool_default_empty() { + let pool = OutboxPool::default(); + assert!(pool.relays.is_empty()); + // Verify no subscriptions by checking that a lookup returns empty status + assert!(pool.status(&OutboxSubId(0)).is_empty()); + } + + /// has_eose returns false when no relays are tracking the request. + #[test] + fn outbox_pool_has_eose_false_when_empty() { + let pool = OutboxPool::default(); + assert!(!pool.has_eose(&OutboxSubId(0))); + } + + /// status() returns empty map for unknown request IDs. + #[test] + fn outbox_pool_status_empty_for_unknown() { + let pool = OutboxPool::default(); + let status = pool.status(&OutboxSubId(999)); + assert!(status.is_empty()); + } + + /// websocket_statuses() is empty before any relays connect. + #[test] + fn outbox_pool_websocket_statuses_empty_initially() { + let pool = OutboxPool::default(); + let statuses = pool.websocket_statuses(); + assert!(statuses.is_empty()); + } + + /// Full modifications should unsubscribe old relays and resubscribe new ones using the updated filters. + #[test] + fn full_modification_updates_sessions_with_new_filters() { + let mut pool = OutboxPool::default(); + let wakeup = MockWakeup::default(); + let relay_a = NormRelayUrl::new("wss://relay-a.example.com").unwrap(); + let relay_b = NormRelayUrl::new("wss://relay-b.example.com").unwrap(); + + let mut urls = HashSet::new(); + urls.insert(relay_a.clone()); + let new_sub_id = { + let mut handler = pool.start_session(wakeup.clone()); + handler.subscribe(trivial_filter(), RelayUrlPkgs::new(urls)) + }; + + { + let sub = pool + .subs + .get_mut(&new_sub_id) + .expect("subscription should be registered"); + assert_eq!(sub.relays.len(), 1); + assert!(sub.relays.contains(&relay_a)); + assert!(!sub.is_oneshot); + assert_eq!(sub.relay_type, RelayType::Compaction); + } + + let sessions = { + let mut updated_relays = HashSet::new(); + updated_relays.insert(relay_b.clone()); + + let mut handler = pool.start_session(wakeup); + handler.modify_filters( + new_sub_id, + vec![Filter::new().kinds(vec![3]).limit(1).build()], + ); + handler.modify_relays(new_sub_id, updated_relays); + let session = handler.export(); + pool.collect_sessions(session) + }; + + let old_task = sessions + .get(&relay_a) + .and_then(|session| session.tasks.get(&new_sub_id)) + .expect("expected a task for relay relay_a"); + assert!(matches!(old_task, CoordinationTask::Unsubscribe)); + + let new_task = sessions + .get(&relay_b) + .and_then(|session| session.tasks.get(&new_sub_id)) + .expect("expected a task for relay relay_b"); + assert!(matches!(new_task, CoordinationTask::CompactionSub)); + } + + /// Oneshot requests route to compaction mode by default. + #[test] + fn oneshot_routes_to_compaction() { + let mut pool = OutboxPool::default(); + let relay = NormRelayUrl::new("wss://relay-oneshot.example.com").unwrap(); + let mut relays = HashSet::new(); + relays.insert(relay.clone()); + let filters = vec![Filter::new().kinds(vec![1]).limit(2).build()]; + let id = OutboxSubId(42); + + let mut session = OutboxSession::default(); + session.oneshot(id, filters.clone(), RelayUrlPkgs::new(relays)); + + let sessions = pool.collect_sessions(session); + + let relay_task = sessions + .get(&relay) + .and_then(|session| session.tasks.get(&id)) + .expect("expected task for oneshot relay"); + assert!(matches!(relay_task, CoordinationTask::CompactionSub)); + } + + /// Unsubscribing from a multi-relay subscription emits unsubscribe tasks for each relay. + #[test] + fn unsubscribe_targets_all_relays() { + let mut pool = OutboxPool::default(); + let relay_a = NormRelayUrl::new("wss://relay-a.example.com").unwrap(); + let relay_b = NormRelayUrl::new("wss://relay-b.example.com").unwrap(); + let id = OutboxSubId(42); + + // Subscribe to both relays + let mut urls = HashSet::new(); + urls.insert(relay_a.clone()); + urls.insert(relay_b.clone()); + + let mut session = OutboxSession::default(); + session.subscribe(id, trivial_filter(), RelayUrlPkgs::new(urls)); + pool.collect_sessions(session); + + // Unsubscribe + let mut session = OutboxSession::default(); + session.unsubscribe(id); + let sessions = pool.collect_sessions(session); + + // Both relays should receive unsubscribe tasks + let task_a = sessions.get(&relay_a).and_then(|s| s.tasks.get(&id)); + let task_b = sessions.get(&relay_b).and_then(|s| s.tasks.get(&id)); + + assert!(matches!(task_a, Some(CoordinationTask::Unsubscribe))); + assert!(matches!(task_b, Some(CoordinationTask::Unsubscribe))); + } + + /// Subscriptions with use_transparent=true route to transparent mode. + #[test] + fn subscribe_transparent_mode() { + let mut pool = OutboxPool::default(); + let relay = NormRelayUrl::new("wss://relay-transparent.example.com").unwrap(); + let id = OutboxSubId(5); + + let mut urls = HashSet::new(); + urls.insert(relay.clone()); + let mut pkgs = RelayUrlPkgs::new(urls); + pkgs.use_transparent = true; + + let mut session = OutboxSession::default(); + session.subscribe(id, trivial_filter(), pkgs); + let sessions = pool.collect_sessions(session); + + let task = sessions.get(&relay).and_then(|s| s.tasks.get(&id)); + assert!(matches!(task, Some(CoordinationTask::TransparentSub))); + } + + /// Modifying filters should re-subscribe the routed relays with the new filters. + #[test] + fn modify_filters_reissues_subscribe_for_existing_relays() { + let mut pool = OutboxPool::default(); + let wakeup = MockWakeup::default(); + let relay = NormRelayUrl::new("wss://relay-modify.example.com").unwrap(); + + let mut urls = HashSet::new(); + urls.insert(relay.clone()); + let sub_id = { + let mut handler = pool.start_session(wakeup.clone()); + handler.subscribe(trivial_filter(), RelayUrlPkgs::new(urls)) + }; + + let (sessions, expected_json) = { + let mut handler = pool.start_session(wakeup); + let updated_filters = vec![Filter::new().kinds(vec![7]).limit(2).build()]; + let expected_json = filters_json(&updated_filters); + handler.modify_filters(sub_id, updated_filters); + let session = handler.export(); + (pool.collect_sessions(session), expected_json) + }; + + let view = pool.subs.view(&sub_id).expect("updated subscription view"); + let stored_json = filters_json(view.filters.get_filters()); + assert_eq!(stored_json, expected_json); + + let task = sessions + .get(&relay) + .and_then(|session| session.tasks.get(&sub_id)) + .expect("expected coordination task"); + assert!(matches!(task, CoordinationTask::CompactionSub)); + } + + /// Modifying relays should unsubscribe removed relays and subscribe new ones. + #[test] + fn modify_relays_differs_routing_sets() { + let mut pool = OutboxPool::default(); + let wakeup = MockWakeup::default(); + let relay_a = NormRelayUrl::new("wss://relay-diff-a.example.com").unwrap(); + let relay_b = NormRelayUrl::new("wss://relay-diff-b.example.com").unwrap(); + + let mut urls = HashSet::new(); + urls.insert(relay_a.clone()); + let sub_id = { + let mut handler = pool.start_session(wakeup.clone()); + handler.subscribe(trivial_filter(), RelayUrlPkgs::new(urls)) + }; + + let sessions = { + let mut handler = pool.start_session(wakeup); + let mut new_urls = HashSet::new(); + new_urls.insert(relay_b.clone()); + handler.modify_relays(sub_id, new_urls); + let session = handler.export(); + pool.collect_sessions(session) + }; + + let unsub_task = sessions + .get(&relay_a) + .and_then(|session| session.tasks.get(&sub_id)) + .expect("missing relay_a task"); + assert!(matches!(unsub_task, CoordinationTask::Unsubscribe)); + + let sub_task = sessions + .get(&relay_b) + .and_then(|session| session.tasks.get(&sub_id)) + .expect("missing relay_b task"); + assert!(matches!(sub_task, CoordinationTask::CompactionSub)); + } + + /// Full modifications that end up with no relays should drop the subscription entirely. + #[test] + fn modify_full_with_empty_relays_removes_subscription() { + let mut pool = OutboxPool::default(); + let wakeup = MockWakeup::default(); + let relay = NormRelayUrl::new("wss://relay-empty.example.com").unwrap(); + + let mut urls = HashSet::new(); + urls.insert(relay.clone()); + let sub_id = { + let mut handler = pool.start_session(wakeup.clone()); + handler.subscribe(trivial_filter(), RelayUrlPkgs::new(urls)) + }; + + let sessions = { + let mut handler = pool.start_session(wakeup); + handler.modify_filters(sub_id, vec![Filter::new().kinds(vec![9]).limit(1).build()]); + handler.modify_relays(sub_id, HashSet::new()); + let session = handler.export(); + pool.collect_sessions(session) + }; + + let task = sessions + .get(&relay) + .and_then(|session| session.tasks.get(&sub_id)) + .expect("expected unsubscribe for relay"); + assert!(matches!(task, CoordinationTask::Unsubscribe)); + assert!( + pool.subs.get_mut(&sub_id).is_none(), + "subscription metadata should be removed" + ); + } + + // ==================== OutboxSessionHandler tests ==================== + + /// The first subscribe issued via handler should return SubRequestId(0). + #[test] + fn outbox_session_handler_subscribe_returns_id() { + let mut pool = OutboxPool::default(); + let wakeup = MockWakeup::default(); + + let id = { + let mut handler = pool.start_session(wakeup); + handler.subscribe(trivial_filter(), RelayUrlPkgs::new(HashSet::new())) + }; + + assert_eq!(id, OutboxSubId(0)); + } + + /// Separate sessions should continue incrementing subscription IDs globally. + #[test] + fn outbox_session_handler_multiple_subscribes_unique_ids() { + let mut pool = OutboxPool::default(); + let wakeup = MockWakeup::default(); + + let id1 = { + let mut handler = pool.start_session(wakeup.clone()); + handler.subscribe(trivial_filter(), RelayUrlPkgs::new(HashSet::new())) + }; + + let id2 = { + let mut handler = pool.start_session(wakeup); + handler.subscribe(trivial_filter(), RelayUrlPkgs::new(HashSet::new())) + }; + + assert_ne!(id1, id2); + assert_eq!(id1, OutboxSubId(0)); + assert_eq!(id2, OutboxSubId(1)); + } + + /// Exporting/importing a session should carry over any pending tasks intact. + #[test] + fn outbox_session_handler_export_and_import() { + let mut pool = OutboxPool::default(); + let wakeup = MockWakeup::default(); + + // Create a handler and export its session + let handler = pool.start_session(wakeup.clone()); + let session = handler.export(); + + // Should be empty since we didn't do anything + assert!(session.tasks.is_empty()); + + // Import the session back + let _handler = OutboxSessionHandler::import(&mut pool, session, wakeup); + } + + // ==================== get_session tests ==================== + + /// get_session should create a new coordination entry when missing. + #[test] + fn get_session_creates_new_if_missing() { + let mut map: HashMap<NormRelayUrl, CoordinationSession> = HashMap::new(); + let url = NormRelayUrl::new("wss://relay.example.com").unwrap(); + + let _session = get_session(&mut map, &url); + + // Should have created a new session + assert!(map.contains_key(&url)); + } + + /// get_session returns the pre-existing coordination session. + #[test] + fn get_session_returns_existing() { + let mut map: HashMap<NormRelayUrl, CoordinationSession> = HashMap::new(); + let url = NormRelayUrl::new("wss://relay.example.com").unwrap(); + + let session = get_session(&mut map, &url); + session.subscribe(OutboxSubId(0), false); + + // Map should still have exactly one entry + assert_eq!(map.len(), 1); + } +}