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:
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> {