From 5d963a8d006bbf17db6dec0bdff4d3a4d82aafce Mon Sep 17 00:00:00 2001 From: Duncan Date: Tue, 13 Oct 2026 15:37:32 -0400 Subject: [PATCH] test(relay): add public live control and search case for 44300 deletions The non-owner live subscription now must receive a public deletion, so suppressing all kind:5 delivery to non-authors fails. Shared deletion helpers take the relay address from the including suite, so one binary never mixes two relays. Indexed deletion search now covers a k=44300 deletion. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- .../tests/e2e_author_only_deletion.rs | 35 ++++++++++++++- .../tests/e2e_instructions_versions.rs | 43 ++++++++++--------- .../tests/support/author_only_deletion.rs | 6 ++- 3 files changed, 60 insertions(+), 24 deletions(-) diff --git a/crates/buzz-test-client/tests/e2e_author_only_deletion.rs b/crates/buzz-test-client/tests/e2e_author_only_deletion.rs index 34b235cc92..35e9aed6f8 100644 --- a/crates/buzz-test-client/tests/e2e_author_only_deletion.rs +++ b/crates/buzz-test-client/tests/e2e_author_only_deletion.rs @@ -34,7 +34,9 @@ use serde_json::Value; mod support; use support::*; +const DEFAULT_RELAY_URL: &str = "ws://localhost:3001"; const KIND_EVENT_REMINDER: u16 = 30300; +const KIND_INSTRUCTIONS_VERSION: u16 = 44300; fn build_reminder(keys: &Keys) -> nostr::Event { EventBuilder::new( @@ -68,6 +70,30 @@ async fn store_deleted_reminder(client: &Client, author: &Keys) -> (nostr::Event (reminder, deletion) } +/// Store and delete an instructions version (kind:44300) with a `k`-tagged +/// deletion carrying `author`'s search term as its reason. +async fn store_deleted_instructions_version(client: &Client, author: &Keys) -> nostr::Event { + let subject = Keys::generate().public_key().to_hex(); + let version = EventBuilder::new( + Kind::Custom(KIND_INSTRUCTIONS_VERSION), + "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + ) + .tags(vec![tag(&["p", &subject])]) + .sign_with_keys(author) + .unwrap(); + submit_ok(client, author, &version).await; + let deletion = build_deletion_with_reason( + author, + vec![ + tag(&["e", &version.id.to_hex()]), + tag(&["k", &KIND_INSTRUCTIONS_VERSION.to_string()]), + ], + &search_term(author), + ); + submit_ok(client, author, &deletion).await; + deletion +} + /// A search term unique to `author`'s deletion reason. fn search_term(author: &Keys) -> String { format!("aodreason{}", &author.public_key().to_hex()[..16]) @@ -104,6 +130,7 @@ async fn author_only_deletion_search_reaches_only_author() { let client = http_client(); let author = Keys::generate(); let (_, private) = store_deleted_reminder(&client, &author).await; + let instructions = store_deleted_instructions_version(&client, &author).await; // Public control: an ordinary deletion carrying the same reason word. let note = EventBuilder::text_note("public note") .sign_with_keys(&author) @@ -119,14 +146,18 @@ async fn author_only_deletion_search_reaches_only_author() { let search = Filter::new() .kind(Kind::EventDeletion) .search(search_term(&author)); - let (private, public) = (private.id.to_hex(), public.id.to_hex()); + let (private, instructions, public) = ( + private.id.to_hex(), + instructions.id.to_hex(), + public.id.to_hex(), + ); let strict = search_indexes_deletions(); let mut mismatches = Vec::new(); for (reader, name, allowed) in [ ( &author, "author", - BTreeSet::from([private.clone(), public.clone()]), + BTreeSet::from([private.clone(), instructions.clone(), public.clone()]), ), (&Keys::generate(), "other", BTreeSet::from([public.clone()])), ] { diff --git a/crates/buzz-test-client/tests/e2e_instructions_versions.rs b/crates/buzz-test-client/tests/e2e_instructions_versions.rs index ebf4ebb943..a27e65fadc 100644 --- a/crates/buzz-test-client/tests/e2e_instructions_versions.rs +++ b/crates/buzz-test-client/tests/e2e_instructions_versions.rs @@ -33,21 +33,12 @@ use serde_json::{json, Value}; #[path = "support/author_only_deletion.rs"] mod aod; +const DEFAULT_RELAY_URL: &str = "ws://localhost:3000"; const INSTRUCTIONS_VERSION_KIND: u16 = 44300; const MAX_CONTENT_BYTES: usize = 218_548; const CIPHERTEXT: &str = "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; -fn relay_url() -> String { - std::env::var("RELAY_URL").unwrap_or_else(|_| "ws://localhost:3000".to_string()) -} - -fn relay_http_url() -> String { - relay_url() - .replace("wss://", "https://") - .replace("ws://", "http://") - .trim_end_matches('/') - .to_string() -} +use aod::{relay_http_url, relay_url}; fn sub_id(name: &str) -> String { format!("e2e-instr-{name}-{}", uuid::Uuid::new_v4()) @@ -739,7 +730,8 @@ async fn test_instructions_version_deletion_reads_are_author_only() { } /// Subscribed while the version still exists, the owner receives its -/// deletion live and another reader does not. +/// deletion live and another reader does not. A public deletion by the same +/// owner is the positive control: both subscribers must receive it. #[tokio::test] #[ignore] async fn test_instructions_version_deletion_live_fanout_is_author_only() { @@ -774,23 +766,34 @@ async fn test_instructions_version_deletion_live_fanout_is_author_only() { ], ); aod::submit_ok(&client, &owner, &deletion).await; + let note = EventBuilder::text_note("public note") + .sign_with_keys(&owner) + .unwrap(); + aod::submit_ok(&client, &owner, ¬e).await; + let public = aod::build_deletion( + &owner, + vec![aod::tag(&["e", ¬e.id.to_hex()]), aod::tag(&["k", "1"])], + ); + aod::submit_ok(&client, &owner, &public).await; for (i, (mut ws, sid)) in subscribers.into_iter().enumerate() { - let delivered = aod::live_event_ids(&mut ws, &sid) - .await - .contains(&deletion.id); + let delivered = aod::live_event_ids(&mut ws, &sid).await; + assert!( + delivered.contains(&public.id), + "public deletion must reach subscriber {i} (0 = owner)" + ); assert_eq!( - delivered, + delivered.contains(&deletion.id), i == 0, - "live delivery to subscriber {i} (0 = owner)" + "private deletion delivery to subscriber {i} (0 = owner)" ); ws.disconnect().await.expect("disconnect"); } } -/// A deletion of a version must carry `["k","44300"]`: one without it, or -/// with another kind, is rejected whether the version is live or already -/// deleted. +/// A deletion of a version must carry `["k","44300"]`. While the version is +/// live, a missing, wrong or noncanonical `k` is rejected; once it is +/// deleted, a deletion without `k` is still rejected. #[tokio::test] #[ignore] async fn test_instructions_version_deletion_requires_k_tag() { diff --git a/crates/buzz-test-client/tests/support/author_only_deletion.rs b/crates/buzz-test-client/tests/support/author_only_deletion.rs index 9736352b5d..59dc001153 100644 --- a/crates/buzz-test-client/tests/support/author_only_deletion.rs +++ b/crates/buzz-test-client/tests/support/author_only_deletion.rs @@ -1,5 +1,7 @@ //! Shared helpers for NIP-09 deletion e2e tests of author-only kinds. Each -//! test binary covering a kind in `AUTHOR_ONLY_KINDS` includes this module. +//! test binary covering a kind in `AUTHOR_ONLY_KINDS` includes this module +//! and defines `DEFAULT_RELAY_URL`, so the helpers and the including suite +//! always talk to the same relay. #![allow(dead_code)] use std::time::Duration; @@ -10,7 +12,7 @@ use reqwest::Client; use serde_json::Value; pub fn relay_url() -> String { - std::env::var("RELAY_URL").unwrap_or_else(|_| "ws://localhost:3001".to_string()) + std::env::var("RELAY_URL").unwrap_or_else(|_| super::DEFAULT_RELAY_URL.to_string()) } pub fn relay_http_url() -> String {