Files
ngit-grasp/tests/lifecycle/nip62_validation.rs
T

444 lines
15 KiB
Rust

//! NIP-62 request-to-vanish standard-behaviour integration tests
//!
//! This suite is the canonical, end-to-end characterisation of the relay's
//! kind-62 (`RequestToVanish`) handling **as specified by NIP-62**, driven
//! against a live ngit-grasp instance via the [`TestRelay`] fixture.
//!
//! These tests are the NIP-62 counterpart of `nip09_validation.rs`. They lock
//! in spec-compliant `handle_vanish` / `deletion_gate` behaviour
//! (`src/nostr/builder.rs`) and the persistent tombstone store
//! (`src/nostr/tombstones.rs`):
//!
//! SCOPE — STANDARD NIP-62 BEHAVIOUR ONLY. Every test in this file must assert
//! plain, spec-compliant NIP-62 request-to-vanish semantics. This file is the
//! reference that proves ngit-grasp still behaves like a vanilla NIP-62 relay.
//!
//! DO NOT add tests here for any custom, non-standard, or ngit-grasp-specific
//! vanish/deletion behaviour (e.g. cascade deletion, multi-maintainer deletion
//! graphs, holding-DB / archival / recovery, or any extension layered on top of
//! NIP-62). Such behaviour is deliberately out of scope and, when it is built,
//! belongs in its own dedicated test file so this suite remains a pure
//! standard-conformance baseline. If a change you are testing diverges from
//! what a stock NIP-62 relay would do, it does NOT belong in this file.
//!
//! - a kind-62 targeting this relay (via `["relay", "ALL_RELAYS"]` or this
//! relay's URL) is accepted, removes the author's served events, and lays
//! down a vanish tombstone;
//! - once a pubkey has vanished, any *new* event it submits is rejected by the
//! deletion/vanish gate with a message mentioning the vanish;
//! - a vanish request is author-bound: author B's vanish must never delete or
//! block author A's events;
//! - a vanish request that names a *different* relay URL (not this one) is
//! accepted but NOT actioned — the author's events remain served and the
//! author may keep publishing;
//! - the kind-62 request event itself is accepted (the author signs their own
//! vanish, so author validation is inherent).
//!
//! The disrespector-specific NIP-62 behaviour (NIP-11 omits NIP-62 in archival
//! mode) lives in `nip09_disrespector.rs`.
//!
//! Shared helpers (`publish_served_repo`, `announcement_served_by_coordinate`,
//! `announcement_coordinate`, `build_vanish`) live in
//! `tests/common/nip09_helpers.rs`.
//!
//! # Running
//!
//! ```bash
//! cargo test --test nip62_validation
//! cargo test --test nip62_validation -- --nocapture
//! ```
#[path = "../common/mod.rs"]
mod common;
use common::{announcement_served_by_coordinate, build_vanish, publish_served_repo, TestRelay};
use grasp_audit::{AuditClient, AuditConfig};
use std::time::Duration;
/// 1. A kind-62 vanish targeting this relay (`ALL_RELAYS`) is accepted and the
/// author's served events are removed.
#[tokio::test]
async fn vanish_removes_authors_served_events() {
let relay = TestRelay::start().await;
let client = AuditClient::new(relay.url(), AuditConfig::isolated())
.await
.expect("create audit client");
let (announcement, repo_id) = publish_served_repo(&client, "vanish-removes").await;
// A served Layer-2 issue referencing the promoted repo.
let issue = client
.create_issue(&announcement, "Issue to vanish", "bye", vec![])
.expect("build issue");
let issue_id = issue.id;
client
.send_event(issue)
.await
.expect("relay should accept issue");
tokio::time::sleep(Duration::from_millis(200)).await;
assert!(
client
.is_event_on_relay(issue_id)
.await
.expect("query issue before vanish"),
"issue should be served before the vanish request"
);
assert!(
client
.is_event_on_relay(announcement.id)
.await
.expect("query announcement before vanish"),
"announcement should be served before the vanish request"
);
// Send the vanish request (targets ALL_RELAYS).
let vanish = build_vanish(&client, None);
client
.send_event(vanish)
.await
.expect("relay should accept a well-formed vanish request");
tokio::time::sleep(Duration::from_millis(300)).await;
let issue_served = client
.is_event_on_relay(issue_id)
.await
.expect("query issue after vanish");
let announcement_served_by_id = client
.is_event_on_relay(announcement.id)
.await
.expect("query announcement by id after vanish");
let announcement_served_by_coord =
announcement_served_by_coordinate(&client, &announcement, &repo_id).await;
relay.stop().await;
assert!(
!issue_served,
"issue {} should be deleted after the author requested to vanish",
issue_id
);
assert!(
!announcement_served_by_id,
"announcement {} should be deleted after the author vanished (by-id check)",
announcement.id
);
assert!(
!announcement_served_by_coord,
"announcement {} should be deleted after the author vanished \
(author+kind+identifier check)",
announcement.id
);
}
/// 2. After a pubkey has vanished, any NEW event it submits is rejected by the
/// deletion/vanish gate with a message mentioning the vanish.
#[tokio::test]
async fn events_from_vanished_pubkey_are_rejected() {
let relay = TestRelay::start().await;
let client = AuditClient::new(relay.url(), AuditConfig::isolated())
.await
.expect("create audit client");
// Promote a repo so we have a well-formed announcement to build issues
// against later (the issue itself is built after the vanish).
let (announcement, _repo_id) = publish_served_repo(&client, "vanished-blocked").await;
// The author vanishes.
let vanish = build_vanish(&client, None);
client
.send_event(vanish)
.await
.expect("relay should accept the vanish request");
tokio::time::sleep(Duration::from_millis(300)).await;
// A brand-new event from the now-vanished pubkey must be rejected.
let issue = client
.create_issue(
&announcement,
"Post-vanish issue",
"should be blocked",
vec![],
)
.expect("build issue");
let issue_id = issue.id;
let result = client.send_event(issue).await;
tokio::time::sleep(Duration::from_millis(200)).await;
let served = client
.is_event_on_relay(issue_id)
.await
.expect("query post-vanish issue");
relay.stop().await;
assert!(
result.is_err(),
"an event from a vanished pubkey must be rejected, but it was accepted"
);
let msg = result.err().unwrap().to_string().to_lowercase();
assert!(
msg.contains("vanish"),
"rejection message should mention the pubkey requested to vanish; got: {}",
msg
);
assert!(
!served,
"event {} from a vanished pubkey must never be served",
issue_id
);
}
/// 3. A vanish request is author-bound: author B vanishing must never delete or
/// block author A's events.
#[tokio::test]
async fn vanish_does_not_affect_other_authors() {
let relay = TestRelay::start().await;
let author_a = AuditClient::new(relay.url(), AuditConfig::isolated())
.await
.expect("create author A client");
let author_b = AuditClient::new(relay.url(), AuditConfig::isolated())
.await
.expect("create author B client");
// Author A publishes a repo + a served issue.
let (announcement_a, repo_id) = publish_served_repo(&author_a, "vanish-other-author").await;
let issue = author_a
.create_issue(&announcement_a, "A's issue", "owned by A", vec![])
.expect("build A's issue");
let issue_id = issue.id;
author_a
.send_event(issue)
.await
.expect("relay should accept A's issue");
tokio::time::sleep(Duration::from_millis(200)).await;
assert!(
author_a
.is_event_on_relay(issue_id)
.await
.expect("query A's issue before B vanishes"),
"A's issue should be served before B's vanish"
);
// Author B requests to vanish (ALL_RELAYS).
let vanish = build_vanish(&author_b, None);
author_b
.send_event(vanish)
.await
.expect("relay should accept B's vanish request");
tokio::time::sleep(Duration::from_millis(300)).await;
// A's events must be untouched.
let issue_served = author_a
.is_event_on_relay(issue_id)
.await
.expect("query A's issue after B vanishes");
let announcement_served_by_coord =
announcement_served_by_coordinate(&author_a, &announcement_a, &repo_id).await;
// A may still publish freely: B's vanish must not have blocked A.
let new_issue = author_a
.create_issue(&announcement_a, "A's later issue", "still here", vec![])
.expect("build A's later issue");
let new_issue_id = new_issue.id;
let new_result = author_a.send_event(new_issue).await;
tokio::time::sleep(Duration::from_millis(200)).await;
let new_served = author_a
.is_event_on_relay(new_issue_id)
.await
.expect("query A's later issue");
relay.stop().await;
assert!(
issue_served,
"A's issue {} must remain served after B's vanish (vanish is author-bound)",
issue_id
);
assert!(
announcement_served_by_coord,
"A's announcement {} must remain served after B's vanish",
announcement_a.id
);
assert!(
new_result.is_ok(),
"A must be able to keep publishing after B's vanish, but submission was \
rejected: {:?}",
new_result.err()
);
assert!(
new_served,
"A's later issue {} must be served: B's vanish must not block A",
new_issue_id
);
}
/// 4. A vanish request that names this relay's URL is accepted and actioned.
#[tokio::test]
async fn vanish_targeting_this_relay_url_is_actioned() {
let relay = TestRelay::start().await;
let client = AuditClient::new(relay.url(), AuditConfig::isolated())
.await
.expect("create audit client");
let (announcement, repo_id) = publish_served_repo(&client, "vanish-this-relay-url").await;
let issue = client
.create_issue(&announcement, "Issue to vanish", "bye", vec![])
.expect("build issue");
let issue_id = issue.id;
client
.send_event(issue)
.await
.expect("relay should accept issue");
tokio::time::sleep(Duration::from_millis(200)).await;
assert!(
client
.is_event_on_relay(issue_id)
.await
.expect("query issue before vanish"),
"issue should be served before the targeted vanish request"
);
let vanish = build_vanish(&client, Some(relay.url()));
client
.send_event(vanish)
.await
.expect("relay should accept a vanish request that targets it by URL");
tokio::time::sleep(Duration::from_millis(300)).await;
let issue_served = client
.is_event_on_relay(issue_id)
.await
.expect("query issue after targeted vanish");
let announcement_served_by_coord =
announcement_served_by_coordinate(&client, &announcement, &repo_id).await;
relay.stop().await;
assert!(
!issue_served,
"issue {} should be deleted after a vanish targeting this relay URL",
issue_id
);
assert!(
!announcement_served_by_coord,
"announcement {} should be deleted after a vanish targeting this relay URL",
announcement.id
);
}
/// 5. A vanish request that names a DIFFERENT relay (not this one) is accepted
/// but NOT actioned: the author's events remain served and the author may
/// keep publishing.
#[tokio::test]
async fn vanish_targeting_other_relay_is_not_actioned() {
let relay = TestRelay::start().await;
let client = AuditClient::new(relay.url(), AuditConfig::isolated())
.await
.expect("create audit client");
let (announcement, repo_id) = publish_served_repo(&client, "vanish-other-relay").await;
let issue = client
.create_issue(&announcement, "Issue kept", "stays", vec![])
.expect("build issue");
let issue_id = issue.id;
client
.send_event(issue)
.await
.expect("relay should accept issue");
tokio::time::sleep(Duration::from_millis(200)).await;
assert!(
client
.is_event_on_relay(issue_id)
.await
.expect("query issue before vanish"),
"issue should be served before the vanish request"
);
// Vanish request naming an unrelated relay — must not be actioned here.
let vanish = build_vanish(&client, Some("wss://some.other.relay.example"));
client
.send_event(vanish)
.await
.expect("relay should accept a vanish request that does not target it");
tokio::time::sleep(Duration::from_millis(300)).await;
let issue_served = client
.is_event_on_relay(issue_id)
.await
.expect("query issue after non-targeting vanish");
let announcement_served_by_coord =
announcement_served_by_coordinate(&client, &announcement, &repo_id).await;
// The author must still be able to publish: no vanish tombstone was laid.
let later = client
.create_issue(&announcement, "Later issue", "still allowed", vec![])
.expect("build later issue");
let later_id = later.id;
let later_result = client.send_event(later).await;
tokio::time::sleep(Duration::from_millis(200)).await;
let later_served = client
.is_event_on_relay(later_id)
.await
.expect("query later issue");
relay.stop().await;
assert!(
issue_served,
"issue {} must remain served: a vanish naming another relay must not be actioned",
issue_id
);
assert!(
announcement_served_by_coord,
"announcement {} must remain served: a vanish naming another relay must not be actioned",
announcement.id
);
assert!(
later_result.is_ok(),
"author must keep publishing after a non-targeting vanish, but submission was \
rejected: {:?}",
later_result.err()
);
assert!(
later_served,
"later issue {} must be served: a non-targeting vanish must not block the author",
later_id
);
}
/// 6. The kind-62 vanish request event itself is accepted (the author signs
/// their own vanish, so authorship is inherent and no target is required).
#[tokio::test]
async fn vanish_request_event_is_accepted() {
let relay = TestRelay::start().await;
let client = AuditClient::new(relay.url(), AuditConfig::isolated())
.await
.expect("create audit client");
let vanish = build_vanish(&client, None);
let result = client.send_event(vanish).await;
relay.stop().await;
assert!(
result.is_ok(),
"a well-formed kind-62 vanish request must be accepted; got: {:?}",
result.err()
);
}