notedeck

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

commit 8387a6683aa06d5a0bd089a35a523cb71173d9f4
parent b0f187fee6732628b481cbfa85732e8ea95a3154
Author: kernelkind <kernelkind@gmail.com>
Date:   Thu, 19 Feb 2026 13:30:57 -0500

test(scoped-subs): validate relay targeting for oneshot and publish APIs

Add unit tests asserting oneshot uses selected account read relays and publish targets either explicit relays or selected account write relays as requested.

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

Diffstat:
Mcrates/notedeck/src/oneshot_api.rs | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck/src/publish.rs | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 159 insertions(+), 0 deletions(-)

diff --git a/crates/notedeck/src/oneshot_api.rs b/crates/notedeck/src/oneshot_api.rs @@ -25,3 +25,62 @@ impl<'o, 'a> OneshotApi<'o, 'a> { ); } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{EguiWakeup, UnknownIds, FALLBACK_PUBKEY}; + use enostr::{OutboxPool, OutboxSessionHandler, OutboxSubId}; + use nostrdb::{Config, Ndb, Transaction}; + use tempfile::TempDir; + + fn test_accounts_with_forced_relay(relay: &str) -> (TempDir, crate::Accounts) { + let tmp = TempDir::new().expect("tmp dir"); + let mut ndb = Ndb::new(tmp.path().to_str().expect("path"), &Config::new()).expect("ndb"); + let txn = Transaction::new(&ndb).expect("txn"); + let mut unknown_ids = UnknownIds::default(); + + let accounts = crate::Accounts::new( + None, + vec![relay.to_owned()], + FALLBACK_PUBKEY(), + &mut ndb, + &txn, + &mut unknown_ids, + ); + + (tmp, accounts) + } + + /// Verifies oneshot requests are routed to the selected account's read relays + /// and the expected filters are staged in outbox. + #[test] + fn oneshot_uses_selected_account_read_relays() { + let (_tmp, accounts) = test_accounts_with_forced_relay("wss://relay-read.example.com"); + let expected_relays = accounts.selected_account_read_relays(); + assert!(!expected_relays.is_empty()); + + let mut pool = OutboxPool::default(); + let filter = Filter::new().kinds(vec![1]).limit(1).build(); + + { + let mut outbox = + OutboxSessionHandler::new(&mut pool, EguiWakeup::new(egui::Context::default())); + let mut oneshot = OneshotApi::new(&mut outbox, &accounts); + oneshot.oneshot(vec![filter.clone()]); + } + + let request_id = OutboxSubId(0); + let status = pool.status(&request_id); + let status_relays: hashbrown::HashSet<enostr::NormRelayUrl> = + status.keys().map(|url| (*url).clone()).collect(); + assert_eq!(status_relays, expected_relays); + + let stored_filters = pool.filters(&request_id).expect("oneshot filters"); + assert_eq!(stored_filters.len(), 1); + assert_eq!( + stored_filters[0].json().expect("filter json"), + filter.json().expect("filter json") + ); + } +} diff --git a/crates/notedeck/src/publish.rs b/crates/notedeck/src/publish.rs @@ -72,3 +72,103 @@ impl<'o, 'a> PublishApi<'o, 'a> { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{EguiWakeup, UnknownIds, FALLBACK_PUBKEY}; + use enostr::{FullKeypair, NormRelayUrl, OutboxPool, OutboxSessionHandler}; + use nostrdb::{Config, Ndb, Note, NoteBuilder, Transaction}; + use tempfile::TempDir; + + fn test_accounts_with_forced_relay(relay: &str) -> (TempDir, crate::Accounts) { + let tmp = TempDir::new().expect("tmp dir"); + let mut ndb = Ndb::new(tmp.path().to_str().expect("path"), &Config::new()).expect("ndb"); + let txn = Transaction::new(&ndb).expect("txn"); + let mut unknown_ids = UnknownIds::default(); + + let accounts = crate::Accounts::new( + None, + vec![relay.to_owned()], + FALLBACK_PUBKEY(), + &mut ndb, + &txn, + &mut unknown_ids, + ); + + (tmp, accounts) + } + + fn signed_note() -> Note<'static> { + let keypair = FullKeypair::generate(); + let seckey = keypair.secret_key.to_secret_bytes(); + + NoteBuilder::new() + .kind(1) + .content("publish-test") + .sign(&seckey) + .build() + .expect("note") + } + + /// Verifies explicit relay publishing targets only the provided relay set. + #[test] + fn publish_note_explicit_targets_requested_relay() { + let (_tmp, accounts) = test_accounts_with_forced_relay("wss://relay-write.example.com"); + let note = signed_note(); + let relay = NormRelayUrl::new("wss://relay-explicit.example.com").expect("relay"); + let mut expected = hashbrown::HashSet::new(); + expected.insert(relay.clone()); + + let mut pool = OutboxPool::default(); + { + let mut outbox = + OutboxSessionHandler::new(&mut pool, EguiWakeup::new(egui::Context::default())); + let mut publish = PublishApi::new(&mut outbox, &accounts); + + publish.publish_note( + &note, + RelayType::Explicit(vec![RelayId::Websocket(relay.clone())]), + ); + } + let actual: hashbrown::HashSet<NormRelayUrl> = pool + .websocket_statuses() + .keys() + .map(|url| (*url).clone()) + .collect(); + assert_eq!(actual, expected); + } + + /// Verifies account-write publishing targets the selected account's write relays. + #[test] + fn publish_note_accounts_write_targets_selected_account_relays() { + let (_tmp, accounts) = + test_accounts_with_forced_relay("wss://relay-accounts-write.example.com"); + let note = signed_note(); + let expected_relays: hashbrown::HashSet<NormRelayUrl> = accounts + .selected_account_write_relays() + .into_iter() + .filter_map(|relay| match relay { + RelayId::Websocket(url) => Some(url), + RelayId::Multicast => None, + }) + .collect(); + assert!(!expected_relays.is_empty()); + + let mut pool = OutboxPool::default(); + { + let mut outbox = + OutboxSessionHandler::new(&mut pool, EguiWakeup::new(egui::Context::default())); + let mut publish = PublishApi::new(&mut outbox, &accounts); + + publish.publish_note(&note, RelayType::AccountsWrite); + } + + let actual_relays: hashbrown::HashSet<NormRelayUrl> = pool + .websocket_statuses() + .keys() + .map(|url| (*url).clone()) + .collect(); + assert_eq!(actual_relays, expected_relays); + } +}