notedeck

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

commit f8cacf085ab0289977ca449a5b5fc65ecbc28d9e
parent 5f3f1f01191e6c94234d7a6e0722f8ba327d9a17
Author: kernelkind <kernelkind@gmail.com>
Date:   Thu,  5 Feb 2026 16:22:57 -0500

test(outbox): `OutboxSubscriptions`

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

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

diff --git a/crates/enostr/src/relay/subscription.rs b/crates/enostr/src/relay/subscription.rs @@ -147,3 +147,297 @@ pub struct SubscribeTask { pub filters: Vec<Filter>, pub relays: RelayUrlPkgs, } + +#[cfg(test)] +mod tests { + use super::*; + use crate::relay::RelayUrlPkgs; + use crate::relay::{FullModificationTask, ModifyFiltersTask}; + + fn subscribe_task(filters: Vec<Filter>, urls: RelayUrlPkgs) -> SubscribeTask { + SubscribeTask { + filters, + relays: urls, + } + } + + fn relay_urls(url: &str) -> HashSet<NormRelayUrl> { + let mut urls = HashSet::new(); + let relay = NormRelayUrl::new(url).unwrap(); + urls.insert(relay); + urls + } + + /// new_subscription should persist relay metadata and expose it via view(). + #[test] + fn new_subscription_records_metadata() { + let mut subs = OutboxSubscriptions::default(); + let mut pkgs = RelayUrlPkgs::new(relay_urls("wss://relay-meta.example.com")); + pkgs.use_transparent = true; + let filters = vec![Filter::new().kinds(vec![1]).limit(4).build()]; + let id = OutboxSubId(7); + + subs.new_subscription(id, subscribe_task(filters.clone(), pkgs), true); + + let view = subs.view(&id).expect("subscription view"); + assert_eq!(view.id, id); + assert!(view.is_oneshot); + assert_eq!(view.filters.get_filters().len(), filters.len()); + assert!(view.json_size > 0); + + let sub = subs.get_mut(&id).expect("subscription metadata"); + assert_eq!(sub.relays.len(), 1); + assert_eq!(sub.relay_type, RelayType::Transparent); + } + + /// subset_oneshot should only return IDs corresponding to oneshot subscriptions. + #[test] + fn subset_oneshot_filters_ids() { + let mut subs = OutboxSubscriptions::default(); + let filters = vec![Filter::new().kinds(vec![1]).build()]; + let id_a = OutboxSubId(1); + let id_b = OutboxSubId(2); + subs.new_subscription( + id_a, + subscribe_task( + filters.clone(), + RelayUrlPkgs::new(relay_urls("wss://relay-a.example")), + ), + false, + ); + subs.new_subscription( + id_b, + subscribe_task( + filters, + RelayUrlPkgs::new(relay_urls("wss://relay-b.example")), + ), + true, + ); + + let mut ids = HashSet::new(); + ids.insert(id_a); + ids.insert(id_b); + + let oneshots = subs.subset_oneshot(&ids); + let expected = { + let mut s = HashSet::new(); + s.insert(id_b); + s + }; + assert_eq!(oneshots, expected); + } + + /// json_size_sum aggregates the JSON payload size for the requested subscriptions. + #[test] + fn json_size_sum_accumulates_sizes() { + let mut subs = OutboxSubscriptions::default(); + let filters = vec![Filter::new().kinds(vec![1]).build()]; + let id_a = OutboxSubId(1); + let id_b = OutboxSubId(2); + subs.new_subscription( + id_a, + subscribe_task( + filters.clone(), + RelayUrlPkgs::new(relay_urls("wss://relay-json-a.example")), + ), + false, + ); + subs.new_subscription( + id_b, + subscribe_task( + filters, + RelayUrlPkgs::new(relay_urls("wss://relay-json-b.example")), + ), + false, + ); + + let mut ids = HashSet::new(); + ids.insert(id_a); + ids.insert(id_b); + + let sum = subs.json_size_sum(&ids); + let expected = subs.json_size(&id_a).unwrap() + subs.json_size(&id_b).unwrap(); + assert_eq!(sum, expected); + } + + /// see_all should mark every filter as seen at the provided timestamp. + #[test] + fn see_all_marks_filters() { + let mut subs = OutboxSubscriptions::default(); + let id = OutboxSubId(8); + subs.new_subscription( + id, + subscribe_task( + vec![ + Filter::new().kinds(vec![1]).limit(2).build(), + Filter::new().kinds(vec![4]).limit(1).build(), + ], + RelayUrlPkgs::new(relay_urls("wss://relay-see.example")), + ), + false, + ); + + let timestamp = 12345; + let sub = subs.get_mut(&id).expect("subscription metadata"); + sub.see_all(timestamp); + + assert!(sub + .filters + .iter() + .all(|(_, meta)| meta.last_seen == Some(timestamp))); + } + + /// ingest_task should update json_size when filters are modified. + #[test] + fn ingest_task_updates_json_size_on_filter_change() { + let mut subs = OutboxSubscriptions::default(); + let id = OutboxSubId(9); + let small_filters = vec![Filter::new().kinds(vec![1]).build()]; + subs.new_subscription( + id, + subscribe_task( + small_filters, + RelayUrlPkgs::new(relay_urls("wss://relay-ingest.example")), + ), + false, + ); + + let original_size = subs.json_size(&id).unwrap(); + + // Modify with larger filters + let large_filters = vec![ + Filter::new().kinds(vec![1, 2, 3, 4, 5]).limit(100).build(), + Filter::new().kinds(vec![6, 7, 8]).limit(50).build(), + ]; + let sub = subs.get_mut(&id).unwrap(); + sub.ingest_task(ModifyTask::Filters(ModifyFiltersTask(large_filters))); + + let new_size = subs.json_size(&id).unwrap(); + assert_ne!( + original_size, new_size, + "json_size should change after filter modification" + ); + assert!( + new_size > original_size, + "larger filters should have larger json_size" + ); + } + + /// ingest_task with Full modification should update json_size. + #[test] + fn ingest_task_updates_json_size_on_full_change() { + let mut subs = OutboxSubscriptions::default(); + let id = OutboxSubId(10); + let small_filters = vec![Filter::new().kinds(vec![1]).build()]; + subs.new_subscription( + id, + subscribe_task( + small_filters, + RelayUrlPkgs::new(relay_urls("wss://relay-full.example")), + ), + false, + ); + + let original_size = subs.json_size(&id).unwrap(); + + // Full modification with larger filters + let large_filters = vec![ + Filter::new().kinds(vec![1, 2, 3, 4, 5]).limit(100).build(), + Filter::new().kinds(vec![6, 7, 8]).limit(50).build(), + ]; + let sub = subs.get_mut(&id).unwrap(); + sub.ingest_task(ModifyTask::Full(FullModificationTask { + filters: large_filters, + relays: relay_urls("wss://new-relay.example"), + })); + + let new_size = subs.json_size(&id).unwrap(); + assert_ne!( + original_size, new_size, + "json_size should change after full modification" + ); + assert!( + new_size > original_size, + "larger filters should have larger json_size" + ); + } + + fn filter_has_since(filter: &Filter, expected: u64) -> bool { + let json = filter.json().expect("filter json"); + json.contains(&format!("\"since\":{}", expected)) + } + + /// Full flow: see_all sets last_seen, then since_optimize applies it to filters. + #[test] + fn see_all_then_since_optimize_applies_since_to_filters() { + let mut subs = OutboxSubscriptions::default(); + let id = OutboxSubId(11); + let filters = vec![ + Filter::new().kinds(vec![1]).build(), + Filter::new().kinds(vec![2]).build(), + ]; + subs.new_subscription( + id, + subscribe_task( + filters, + RelayUrlPkgs::new(relay_urls("wss://relay-since.example")), + ), + false, + ); + + // Verify filters don't have since initially + let view = subs.view(&id).unwrap(); + for filter in view.filters.get_filters() { + let json = filter.json().expect("filter json"); + assert!( + !json.contains("\"since\""), + "filter should not have since initially" + ); + } + + let timestamp = 1700000000u64; + let sub = subs.get_mut(&id).unwrap(); + sub.see_all(timestamp); + sub.filters.since_optimize(); + + // Verify filters now have since + let view = subs.view(&id).unwrap(); + for filter in view.filters.get_filters() { + assert!( + filter_has_since(filter, timestamp), + "filter should have since after see_all + since_optimize" + ); + } + } + + /// Filters accessed via view() should have since after optimization. + #[test] + fn view_returns_optimized_filters() { + let mut subs = OutboxSubscriptions::default(); + let id = OutboxSubId(12); + let filters = vec![Filter::new().kinds(vec![1]).build()]; + subs.new_subscription( + id, + subscribe_task( + filters, + RelayUrlPkgs::new(relay_urls("wss://relay-view.example")), + ), + false, + ); + + let timestamp = 1234567890u64; + { + let sub = subs.get_mut(&id).unwrap(); + sub.see_all(timestamp); + sub.filters.since_optimize(); + } + + // Access via view - should see the optimized filters + let view = subs.view(&id).unwrap(); + let filter = &view.filters.get_filters()[0]; + assert!( + filter_has_since(filter, timestamp), + "view should return filters with since applied" + ); + } +}