notedeck

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

commit 20f1499c73345e23742d76d3f75a58f478a110fd
parent 6f0bf48c70ffc9fe4cd24ef8c5e5f38dfe6992dd
Author: kernelkind <kernelkind@gmail.com>
Date:   Mon,  2 Feb 2026 12:46:18 -0500

test(outbox): `OutboxSession`

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

Diffstat:
Mcrates/enostr/src/relay/outbox/session.rs | 323+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/enostr/src/relay/test_utils.rs | 10++++++++++
2 files changed, 333 insertions(+), 0 deletions(-)

diff --git a/crates/enostr/src/relay/outbox/session.rs b/crates/enostr/src/relay/outbox/session.rs @@ -171,3 +171,326 @@ impl OutboxSession { fn filters_prune_empty(filters: &mut Vec<Filter>) { filters.retain(|f| f.num_elements() != 0); } + +#[cfg(test)] +mod tests { + use crate::relay::test_utils::{expect_task, trivial_filter}; + + use super::*; + + // ==================== OutboxSession tests ==================== + + /// Verifies a freshly created session has no pending tasks. + #[test] + fn outbox_session_default_empty() { + let session = OutboxSession::default(); + assert!(session.tasks.is_empty()); + } + + /// Drops subscribe/oneshot requests that lack meaningful filters/relays. + #[test] + fn outbox_session_subscribe_empty() { + let mut session = OutboxSession::default(); + let urls = RelayUrlPkgs::new(HashSet::new()); + + session.subscribe(OutboxSubId(0), vec![Filter::new().build()], urls.clone()); + assert!(session.tasks.is_empty()); + + session.subscribe(OutboxSubId(0), vec![], urls.clone()); + assert!(session.tasks.is_empty()); + + session.oneshot(OutboxSubId(0), vec![Filter::new().build()], urls.clone()); + assert!(session.tasks.is_empty()); + + session.oneshot(OutboxSubId(0), vec![], urls); + assert!(session.tasks.is_empty()); + } + + /// Stores subscribe tasks when filters and relays are provided. + #[test] + fn outbox_session_subscribe() { + let mut session = OutboxSession::default(); + let urls = RelayUrlPkgs::new(HashSet::new()); + + session.subscribe(OutboxSubId(0), trivial_filter(), urls); + + assert!(matches!( + expect_task(&session, OutboxSubId(0)), + OutboxTask::Subscribe(_) + )); + } + + /// Stores oneshot tasks when filters and relays are provided. + #[test] + fn outbox_session_oneshot() { + let mut session = OutboxSession::default(); + let urls = RelayUrlPkgs::new(HashSet::new()); + + session.oneshot(OutboxSubId(0), trivial_filter(), urls); + + assert!(matches!( + expect_task(&session, OutboxSubId(0)), + OutboxTask::Oneshot(_) + )); + } + + /// Records unsubscribe operations on demand. + #[test] + fn outbox_session_unsubscribe() { + let mut session = OutboxSession::default(); + + session.unsubscribe(OutboxSubId(42)); + + assert!(matches!( + expect_task(&session, OutboxSubId(42)), + OutboxTask::Unsubscribe + )); + } + + /// Pushing filters first results in a Modify(Filters) task. + #[test] + fn outbox_session_new_filters_creates_modify_filters() { + let mut session = OutboxSession::default(); + + session.new_filters(OutboxSubId(0), trivial_filter()); + + assert!(matches!( + expect_task(&session, OutboxSubId(0)), + OutboxTask::Modify(ModifyTask::Filters(_)) + )); + } + + /// Pushing relays first results in a Modify(Relays) task. + #[test] + fn outbox_session_new_relays_creates_modify_relays() { + let mut session = OutboxSession::default(); + + session.new_relays(OutboxSubId(0), HashSet::new()); + + assert!(matches!( + expect_task(&session, OutboxSubId(0)), + OutboxTask::Modify(ModifyTask::Relays(_)) + )); + } + + /// Mixing filters then relays converges to a Modify(Full) task. + #[test] + fn outbox_session_merges_filters_and_relays_to_full_modification() { + let mut session = OutboxSession::default(); + + // First add filters + session.new_filters(OutboxSubId(0), trivial_filter()); + + // Then add relays - should merge to Full modification + session.new_relays(OutboxSubId(0), HashSet::new()); + + assert!(matches!( + expect_task(&session, OutboxSubId(0)), + OutboxTask::Modify(ModifyTask::Full(_)) + )); + } + + /// Mixing relays then filters also converges to a Modify(Full) task. + #[test] + fn outbox_session_merges_relays_and_filters_to_full_modification() { + let mut session = OutboxSession::default(); + + // First add relays + session.new_relays(OutboxSubId(0), HashSet::new()); + + // Then add filters - should merge to Full modification + session.new_filters(OutboxSubId(0), trivial_filter()); + + assert!(matches!( + expect_task(&session, OutboxSubId(0)), + OutboxTask::Modify(ModifyTask::Full(_)) + )); + } + + // this should never happen in practice though + /// Subscribe commands override previously staged filter changes. + #[test] + fn outbox_session_subscribe_overwrites_modify_filters() { + let mut session = OutboxSession::default(); + let urls = RelayUrlPkgs::new(HashSet::new()); + + session.new_filters(OutboxSubId(0), trivial_filter()); + session.subscribe( + OutboxSubId(0), + vec![Filter::new().kinds(vec![3]).build()], + urls, + ); + + assert!(matches!( + expect_task(&session, OutboxSubId(0)), + OutboxTask::Subscribe(_) + )); + } + + /// Unsubscribe issued after subscribe should take precedence. + #[test] + fn outbox_session_unsubscribe_after_subscribe() { + let mut session = OutboxSession::default(); + let urls = RelayUrlPkgs::new(HashSet::new()); + + session.subscribe(OutboxSubId(0), trivial_filter(), urls); + session.unsubscribe(OutboxSubId(0)); + + assert!(matches!( + expect_task(&session, OutboxSubId(0)), + OutboxTask::Unsubscribe + )); + } + + /// Adding filters after an unsubscribe restarts the task as Modify(Filters). + #[test] + fn outbox_session_new_filters_after_unsubscribe() { + let mut session = OutboxSession::default(); + + session.unsubscribe(OutboxSubId(0)); + session.new_filters(OutboxSubId(0), trivial_filter()); + + // Filters should overwrite unsubscribe + assert!(matches!( + expect_task(&session, OutboxSubId(0)), + OutboxTask::Modify(ModifyTask::Filters(_)) + )); + } + + /// Updating filters of a Full modification replaces its filter list. + #[test] + fn outbox_session_update_full_modification_filters() { + let mut session = OutboxSession::default(); + + // Create full modification + session.new_filters(OutboxSubId(0), trivial_filter()); + session.new_relays(OutboxSubId(0), HashSet::new()); + + // Update filters on the full modification + session.new_filters( + OutboxSubId(0), + vec![ + Filter::new().kinds(vec![3]).build(), + Filter::new().kinds(vec![1]).build(), + ], + ); + + match expect_task(&session, OutboxSubId(0)) { + OutboxTask::Modify(ModifyTask::Full(fm)) => { + assert_eq!(fm.filters.len(), 2); + } + _ => panic!("Expected Modify(Full)"), + } + } + + /// Updating relays of a Full modification replaces its relay set. + #[test] + fn outbox_session_update_full_modification_relays() { + let mut session = OutboxSession::default(); + + // Create full modification + session.new_filters(OutboxSubId(0), trivial_filter()); + session.new_relays(OutboxSubId(0), HashSet::new()); + + // Update relays on the full modification + let mut new_urls = HashSet::new(); + new_urls.insert(NormRelayUrl::new("wss://relay.example.com").unwrap()); + session.new_relays(OutboxSubId(0), new_urls); + + match expect_task(&session, OutboxSubId(0)) { + OutboxTask::Modify(ModifyTask::Full(fm)) => { + assert!(!fm.relays.is_empty()); + } + _ => panic!("Expected Modify(Full)"), + } + } + + /// Attempting to modify oneshot filters leaves them unchanged. + #[test] + fn outbox_session_update_oneshot_filters() { + let mut session = OutboxSession::default(); + let urls = RelayUrlPkgs::new(HashSet::new()); + + session.oneshot(OutboxSubId(0), trivial_filter(), urls); + session.new_filters( + OutboxSubId(0), + vec![ + Filter::new().kinds([1]).build(), + Filter::new().kinds([3]).build(), + ], + ); + + match expect_task(&session, OutboxSubId(0)) { + OutboxTask::Oneshot(task) => { + assert_eq!(task.filters.len(), 1); + } + _ => panic!("Expected Oneshot task"), + } + } + + /// Updating filters on a Subscribe task replaces the stored filters. + #[test] + fn outbox_session_update_subscribe_filters() { + let mut session = OutboxSession::default(); + let urls = RelayUrlPkgs::new(HashSet::new()); + + session.subscribe(OutboxSubId(0), trivial_filter(), urls); + session.new_filters( + OutboxSubId(0), + vec![ + Filter::new().kinds([1]).build(), + Filter::new().kinds([3]).build(), + ], + ); + + match expect_task(&session, OutboxSubId(0)) { + OutboxTask::Subscribe(task) => { + assert_eq!(task.filters.len(), 2); + } + _ => panic!("Expected Subscribe task"), + } + } + + /// Updating relays on a Subscribe task replaces the stored relays. + #[test] + fn outbox_session_update_subscribe_relays() { + let mut session = OutboxSession::default(); + let urls = RelayUrlPkgs::new(HashSet::new()); + + session.subscribe(OutboxSubId(0), trivial_filter(), urls); + + let mut new_urls = HashSet::new(); + new_urls.insert(NormRelayUrl::new("wss://relay.example.com").unwrap()); + session.new_relays(OutboxSubId(0), new_urls); + + match expect_task(&session, OutboxSubId(0)) { + OutboxTask::Subscribe(task) => { + assert!(!task.relays.urls.is_empty()); + } + _ => panic!("Expected Subscribe task"), + } + } + + /// Attempting to modify oneshot relays leaves them unchanged. + #[test] + fn outbox_session_update_oneshot_relays() { + let mut session = OutboxSession::default(); + let urls = RelayUrlPkgs::new(HashSet::new()); + + session.oneshot(OutboxSubId(0), trivial_filter(), urls); + + let mut new_urls = HashSet::new(); + new_urls.insert(NormRelayUrl::new("wss://relay.example.com").unwrap()); + session.new_relays(OutboxSubId(0), new_urls); + + match expect_task(&session, OutboxSubId(0)) { + OutboxTask::Oneshot(task) => { + assert!( + task.relays.urls.is_empty(), + "cannot make modifications on oneshot" + ); + } + _ => panic!("Expected Oneshot task"), + } + } +} diff --git a/crates/enostr/src/relay/test_utils.rs b/crates/enostr/src/relay/test_utils.rs @@ -5,6 +5,7 @@ use nostrdb::Filter; +use crate::relay::{OutboxSession, OutboxSubId, OutboxTask}; use crate::Wakeup; /// A mock Wakeup implementation that tracks how many times wake() was called. @@ -31,6 +32,15 @@ impl Wakeup for MockWakeup { fn wake(&self) {} } +/// Returns a task for `id`, panicking when the task is missing. +#[track_caller] +pub fn expect_task<'a>(session: &'a OutboxSession, id: OutboxSubId) -> &'a OutboxTask { + session + .tasks + .get(&id) + .unwrap_or_else(|| panic!("Expected task for {:?}", id)) +} + // ==================== SubRegistry tests ==================== pub fn trivial_filter() -> Vec<Filter> {