test(e2e): add disabled-backend fail-closed search test (gate #2)

Adds test_nip50_search_disabled_fails_closed: posts a message that the
postgres/typesense backends provably return, then asserts the relay
delivers EOSE with zero events. Proves BUZZ_SEARCH_BACKEND=disabled fails
closed — NIP-50 search leaks nothing regardless of how well content would
otherwise match.

Introduces a test_backend() helper reading BUZZ_TEST_BACKEND; the
disabled test early-returns (skips) unless the relay-under-test is the
disabled backend, so the full suite stays green against all three
backends.

Matrix verified green: typesense 9/9, postgres 9/9 (identical parity),
disabled 6/6 incl. the fail-closed assertion.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d
2026-06-24 19:27:36 -04:00
co-authored by Tyler Longwell
parent 5dfeb7fbe0
commit bba6d20bb2
@@ -42,6 +42,15 @@ fn sub_id(name: &str) -> String {
format!("e2e-{name}-{}", uuid::Uuid::new_v4())
}
/// The search backend the relay-under-test is configured with, surfaced to the
/// test process via `BUZZ_TEST_BACKEND` (`typesense` | `postgres` | `disabled`).
/// The matrix runner sets this to match the relay's `BUZZ_SEARCH_BACKEND`.
/// Backend-specific tests early-return when it does not match, so the same
/// suite is safe to run against any backend.
fn test_backend() -> String {
std::env::var("BUZZ_TEST_BACKEND").unwrap_or_else(|_| "postgres".to_string())
}
/// Create a real channel in the DB via REST so the relay accepts events for it.
async fn create_test_channel(keys: &Keys) -> String {
create_channel_with_visibility(keys, "open").await
@@ -365,6 +374,75 @@ async fn test_nip50_search_returns_results_and_eose() {
client.disconnect().await.expect("disconnect");
}
/// Gate #2 — `BUZZ_SEARCH_BACKEND=disabled` fails closed.
///
/// Posts a message that the postgres/typesense backends provably return (it is
/// the exact same setup as `test_nip50_search_returns_results_and_eose`), then
/// asserts the relay delivers EOSE with **zero** events. A disabled backend must
/// never leak content: NIP-50 search returns empty for every query, regardless
/// of how well it would otherwise match.
///
/// Only meaningful against a relay configured with the disabled backend, so it
/// early-returns (skips) unless `BUZZ_TEST_BACKEND=disabled`. The matrix runner
/// sets that env to match the relay's `BUZZ_SEARCH_BACKEND`.
#[tokio::test]
#[ignore]
async fn test_nip50_search_disabled_fails_closed() {
if test_backend() != "disabled" {
eprintln!(
"skipping test_nip50_search_disabled_fails_closed: BUZZ_TEST_BACKEND={} (need `disabled`)",
test_backend()
);
return;
}
let url = relay_url();
let keys = Keys::generate();
let channel = create_test_channel(&keys).await;
// Post a message that WOULD match under a real backend — same shape as the
// positive "returns results" test, so a non-empty result here would prove a
// working index, and an empty result proves the disabled backend held closed.
let unique_token = format!("searchtoken_{}", uuid::Uuid::new_v4().simple());
let content = format!("Hello world {unique_token}");
let mut client = BuzzTestClient::connect(&url, &keys).await.expect("connect");
let ok = client
.send_text_message(&keys, &channel, &content, 9)
.await
.expect("send message");
assert!(ok.accepted, "relay rejected message: {}", ok.message);
// Generous settle window — under a real backend this is when indexing lands.
tokio::time::sleep(Duration::from_millis(800)).await;
let sid = sub_id("nip50-disabled");
let filter = Filter::new()
.kind(Kind::Custom(9))
.search(&unique_token)
.custom_tags(SingleLetterTag::lowercase(Alphabet::H), [channel.as_str()]);
client
.subscribe(&sid, vec![filter])
.await
.expect("subscribe");
let events = client
.collect_until_eose(&sid, Duration::from_secs(10))
.await
.expect("collect until EOSE");
assert!(
events.is_empty(),
"disabled backend returned {} search hit(s) — search did NOT fail closed. events: {:?}",
events.len(),
events.iter().map(|e| &e.content).collect::<Vec<_>>()
);
client.disconnect().await.expect("disconnect");
}
/// Subscribe with mixed search + non-search filters.
/// Verify: relay sends CLOSED with error message containing "mixed".
#[tokio::test]