mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 23:18:24 +00:00
444 lines
15 KiB
Rust
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()
|
|
);
|
|
}
|