mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 23:18:24 +00:00
test(nip09): add coverage for addressable event newer than deletion request
NIP-09 specifies that an a-coordinate deletion only deletes versions of the coordinate with created_at up to the deletion request's created_at. A newer version published after the deletion must be accepted and served. The existing nip09_validation suite covered a-tag deletion of an older target but had no end-to-end test for the timestamp guard protecting a newer version. Add newer_addressable_event_survives_a_tag_deletion. To keep the test focused on the NIP-09 timestamp rule (and away from the purgatory/git-promotion machinery that kind-30617 announcements require), the addressable event under test is a generic kind-30078 event that references the promoted repo by its a coordinate. Like an issue it is a Layer-2 event served immediately, but unlike an issue it is addressable, so the NIP-09 created_at rule for a-tag deletions applies. This exercises the resubmission gate's is_coordinate_deleted timestamp check end-to-end, complementing the purgatory-only unit test test_deletion_by_coordinate_respects_created_at.
This commit is contained in:
+159
-1
@@ -18,7 +18,10 @@
|
||||
//! - deletion of a non-existent target is accepted (NIP-09 pre-emptive delete);
|
||||
//! - a pre-emptive delete that arrives *before* its target lays down a
|
||||
//! tombstone, so the later-arriving target is rejected and never served;
|
||||
//! - malformed `a` coordinates are silently treated as no-ops (accepted).
|
||||
//! - malformed `a` coordinates are silently treated as no-ops (accepted);
|
||||
//! - the NIP-09 timestamp rule for `a`-coordinate deletions: a version of an
|
||||
//! addressable event *newer* than the deletion request is accepted and served
|
||||
//! (the deletion only applies up to its own `created_at`).
|
||||
//!
|
||||
//! `normal_mode_honours_deletion` in `nip09_disrespector.rs` already covers the
|
||||
//! "announcement deleted by `a` coordinate is removed" path, so it is NOT
|
||||
@@ -739,3 +742,158 @@ async fn empty_deletion_currently_accepted() {
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
/// 7. NIP-09 timestamp rule for addressable events: an `a`-coordinate deletion
|
||||
/// only deletes versions of that coordinate with `created_at` *up to* the
|
||||
/// deletion request's `created_at`. A NEWER version of the same coordinate,
|
||||
/// published after the deletion, must be accepted and served — the deletion
|
||||
/// must not censor it.
|
||||
///
|
||||
/// To keep this focused on the NIP-09 timestamp rule (and away from the
|
||||
/// purgatory/git-promotion machinery that kind-30617 announcements require), the
|
||||
/// addressable event under test is a generic kind-30078 (NIP-78
|
||||
/// application-specific data) event that references the promoted repo by its `a`
|
||||
/// coordinate. Like an issue, a referencing event is a Layer-2 event served
|
||||
/// immediately — but unlike an issue it is *addressable*, so the NIP-09
|
||||
/// `created_at` rule for `a`-tag deletions applies to it.
|
||||
///
|
||||
/// Flow:
|
||||
/// 1. Promote a repo announcement (served) to anchor referencing events.
|
||||
/// 2. Publish addressable event v1 (kind 30078, own `d`) referencing the repo;
|
||||
/// it is served immediately.
|
||||
/// 3. Delete it by its own `a` coordinate (`30078:<pubkey>:<d>`), with the
|
||||
/// deletion's `created_at == now`.
|
||||
/// 4. Verify v1 is gone.
|
||||
/// 5. Publish v2 of the SAME coordinate with a strictly newer `created_at` and
|
||||
/// assert it is accepted and served — the deletion must not block it.
|
||||
///
|
||||
/// This is the served/main-DB counterpart of the purgatory-only unit test
|
||||
/// `test_deletion_by_coordinate_respects_created_at` in
|
||||
/// `src/nostr/policy/deletion.rs`, and exercises the resubmission gate's
|
||||
/// `is_coordinate_deleted` timestamp check (a newer event must NOT be blocked).
|
||||
#[tokio::test]
|
||||
async fn newer_addressable_event_survives_a_tag_deletion() {
|
||||
// A generic addressable (parameterized-replaceable) kind in the 30000-39999
|
||||
// range. NIP-78 application-specific data; any addressable kind works.
|
||||
const ADDRESSABLE_KIND: u16 = 30078;
|
||||
|
||||
let relay = TestRelay::start().await;
|
||||
let client = AuditClient::new(relay.url(), AuditConfig::isolated())
|
||||
.await
|
||||
.expect("create audit client");
|
||||
|
||||
// Stage 1: promote a repo so referencing events are accepted/served.
|
||||
let (announcement, repo_id) = publish_served_repo(&client, "newer-survives").await;
|
||||
let repo_coord = announcement_coordinate(&announcement, &repo_id);
|
||||
|
||||
// Build the addressable coordinate under test: `30078:<pubkey>:<d>`.
|
||||
let addr_d = format!("note-{}", &repo_id);
|
||||
let addr_coord = format!(
|
||||
"{}:{}:{}",
|
||||
ADDRESSABLE_KIND,
|
||||
client.public_key().to_hex(),
|
||||
addr_d
|
||||
);
|
||||
|
||||
// Helper: build a kind-30078 addressable event referencing the repo's `a`
|
||||
// coordinate, with the given content and created_at.
|
||||
let build_addressable = |content: &str, ts: Timestamp| {
|
||||
client
|
||||
.event_builder(Kind::from(ADDRESSABLE_KIND), content)
|
||||
.tag(Tag::identifier(&addr_d))
|
||||
.tag(Tag::custom("a", vec![repo_coord.clone()]))
|
||||
.custom_time(ts)
|
||||
.build(client.keys())
|
||||
.expect("build addressable event")
|
||||
};
|
||||
|
||||
// Stage 2: publish addressable v1 (older). It references the promoted repo,
|
||||
// so it is a Layer-2 event served immediately.
|
||||
let v1 = build_addressable("version 1", Timestamp::now());
|
||||
let v1_id = v1.id;
|
||||
client
|
||||
.send_event(v1.clone())
|
||||
.await
|
||||
.expect("relay should accept addressable event referencing the repo");
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
assert!(
|
||||
client
|
||||
.is_event_on_relay(v1_id)
|
||||
.await
|
||||
.expect("query v1 before deletion"),
|
||||
"addressable v1 should be served before deletion"
|
||||
);
|
||||
|
||||
// Stage 3: delete v1 by its own `a` coordinate. The deletion's created_at is
|
||||
// "now", so only versions at or before now are deleted.
|
||||
let deletion = build_deletion(&client, &[], &[addr_coord.clone()]);
|
||||
let deletion_ts = deletion.created_at;
|
||||
client
|
||||
.send_event(deletion)
|
||||
.await
|
||||
.expect("relay should accept deletion by coordinate");
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
// Stage 4: v1 must be gone.
|
||||
assert!(
|
||||
!client
|
||||
.is_event_on_relay(v1_id)
|
||||
.await
|
||||
.expect("query v1 after deletion"),
|
||||
"addressable v1 {} should be deleted by its a coordinate",
|
||||
v1_id
|
||||
);
|
||||
|
||||
// Stage 5: publish v2 of the SAME coordinate with a strictly newer
|
||||
// created_at than the deletion request. The resubmission gate must NOT block
|
||||
// it (it is newer than the deletion), so it is accepted and served.
|
||||
let newer_ts = Timestamp::from(deletion_ts.as_secs() + 60);
|
||||
let v2 = build_addressable("version 2", newer_ts);
|
||||
let v2_id = v2.id;
|
||||
assert!(
|
||||
v2.created_at > deletion_ts,
|
||||
"v2 must be strictly newer than the deletion request"
|
||||
);
|
||||
|
||||
client
|
||||
.send_event(v2.clone())
|
||||
.await
|
||||
.expect("relay should accept an addressable event newer than the deletion request");
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
let served_by_id = client
|
||||
.is_event_on_relay(v2_id)
|
||||
.await
|
||||
.expect("query v2 by id");
|
||||
|
||||
// Also confirm it is served via a real NIP-01 addressable-event query
|
||||
// (author + kind + d identifier), not just by id.
|
||||
let coord_filter = Filter::new()
|
||||
.kind(Kind::from(ADDRESSABLE_KIND))
|
||||
.author(client.public_key())
|
||||
.identifier(&addr_d);
|
||||
let served_by_coord = client
|
||||
.query(coord_filter)
|
||||
.await
|
||||
.expect("query v2 by coordinate")
|
||||
.iter()
|
||||
.any(|e| e.id == v2_id);
|
||||
|
||||
relay.stop().await;
|
||||
|
||||
assert!(
|
||||
served_by_id,
|
||||
"newer addressable v2 {} must be served (by-id): a NIP-09 deletion \
|
||||
must not censor a version newer than the deletion request",
|
||||
v2_id
|
||||
);
|
||||
assert!(
|
||||
served_by_coord,
|
||||
"newer addressable v2 {} must be served (by author+kind+identifier): \
|
||||
a NIP-09 deletion must not censor a version newer than the deletion request",
|
||||
v2_id
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user