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 <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
Duncan
2026-10-13 15:37:32 -04:00
committed by Hayt
parent 8be8f99b31
commit 5d963a8d00
3 changed files with 60 additions and 24 deletions

View File

@@ -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()])),
] {

View File

@@ -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, &note).await;
let public = aod::build_deletion(
&owner,
vec![aod::tag(&["e", &note.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() {

View File

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