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:
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"
+ );
+ }
+}