mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 23:18:24 +00:00
fix: rewrite deletion integration tests to avoid shared-state side effects
The previous tests deleted purgatory announcements (kind 30617) and checked for bare-repo absence via git ls-remote, which would corrupt shared-mode test state by destroying repos other tests depend on. New approach tests deletion of purgatory state events (kind 30618) instead: - e-tag test: promotes a repo, creates a unique commit locally, submits a state event pointing to it (enters purgatory), deletes the state event by event ID, then verifies git push of that commit is rejected. - a-tag coordinate test: promotes a repo, generates a fresh maintainer keypair, sends a replacement announcement adding that maintainer, submits a state event signed by the new maintainer (enters purgatory), deletes by coordinate 30618:<new_maintainer_pubkey>:<identifier>, then verifies git push is rejected. Also extends DeletionPolicy to handle kind 30618 state events in purgatory for both e-tag (event ID) and a-tag (coordinate) deletion paths.
This commit is contained in:
@@ -27,9 +27,11 @@
|
||||
//! - `test_pr_event_in_purgatory_git_push_accepted` - Git push to refs/nostr/<event-id> succeeds
|
||||
//! - `test_pr_event_served_after_git_push` - Event becomes queryable after git data
|
||||
|
||||
use crate::fixtures::{clone_repo, create_commit, try_push};
|
||||
use crate::specs::grasp01::SpecRef;
|
||||
use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult};
|
||||
use nostr_sdk::prelude::*;
|
||||
use std::fs;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Test suite for GRASP-01 purgatory behavior
|
||||
@@ -47,9 +49,9 @@ impl PurgatoryTests {
|
||||
results.add(Self::test_state_event_accepted_for_purgatory_announcement(client).await);
|
||||
|
||||
// Deletion event tests (NIP-09)
|
||||
results.add(Self::test_deletion_by_event_id_removes_purgatory_announcement(client).await);
|
||||
results.add(Self::test_deletion_by_event_id_removes_purgatory_state_event(client).await);
|
||||
results.add(
|
||||
Self::test_deletion_by_coordinate_removes_purgatory_announcement(client).await,
|
||||
Self::test_deletion_by_coordinate_removes_purgatory_state_event(client).await,
|
||||
);
|
||||
|
||||
// State event purgatory tests (already implemented)
|
||||
@@ -656,192 +658,293 @@ impl PurgatoryTests {
|
||||
// Deletion Event Tests (NIP-09)
|
||||
// ============================================================
|
||||
|
||||
/// Test: Kind 5 deletion event by event ID removes purgatory announcement
|
||||
/// Test: Kind 5 deletion event by event ID removes a purgatory state event
|
||||
///
|
||||
/// Spec: NIP-09
|
||||
/// "A special event with kind 5... having a list of one or more `e` or `a` tags,
|
||||
/// each referencing an event the author is requesting to be deleted."
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. Send a valid repository announcement (enters purgatory)
|
||||
/// 2. Send a kind 5 deletion event referencing the announcement by event ID
|
||||
/// 3. The announcement is no longer in purgatory (git push would fail)
|
||||
/// 4. The deletion event itself is accepted by the relay
|
||||
pub async fn test_deletion_by_event_id_removes_purgatory_announcement(
|
||||
/// 1. Get a promoted repo (OwnerStateDataPushed) so git pushes are possible
|
||||
/// 2. Clone the repo and create a unique commit (not yet pushed)
|
||||
/// 3. Submit a state event pointing to that unique commit (enters purgatory)
|
||||
/// 4. Send a kind 5 deletion event referencing the state event by event ID
|
||||
/// 5. Attempt to push the unique commit — MUST be rejected (no authorized state event)
|
||||
pub async fn test_deletion_by_event_id_removes_purgatory_state_event(
|
||||
client: &AuditClient,
|
||||
) -> TestResult {
|
||||
TestResult::new(
|
||||
"deletion_by_event_id_removes_purgatory_announcement",
|
||||
"deletion_by_event_id_removes_purgatory_state_event",
|
||||
SpecRef::PurgatoryAcceptUntilGitData,
|
||||
"Kind 5 deletion by event ID SHOULD remove a purgatory announcement",
|
||||
"Kind 5 deletion by event ID SHOULD remove a purgatory state event, causing push rejection",
|
||||
)
|
||||
.run(|| async {
|
||||
let ctx = TestContext::new(client);
|
||||
|
||||
// Send announcement to purgatory
|
||||
let repo = ctx
|
||||
.get_fixture(FixtureKind::ValidRepoSent)
|
||||
// Stage 1: get a promoted repo with git data already on the relay
|
||||
let existing_state = ctx
|
||||
.get_fixture(FixtureKind::OwnerStateDataPushed)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create repo announcement: {}", e))?;
|
||||
.map_err(|e| format!("Failed to get promoted repo: {}", e))?;
|
||||
|
||||
let repo_id = repo
|
||||
let repo_id = existing_state
|
||||
.tags
|
||||
.iter()
|
||||
.find(|t| t.kind() == TagKind::d())
|
||||
.and_then(|t| t.content())
|
||||
.ok_or("Missing d tag in repo announcement")?
|
||||
.ok_or("Missing d tag in state event")?
|
||||
.to_string();
|
||||
|
||||
// Verify it's in purgatory (not served)
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
if client.is_event_on_relay(repo.id).await.map_err(|e| e.to_string())? {
|
||||
return Err(
|
||||
"Announcement was served immediately - purgatory not working".to_string(),
|
||||
);
|
||||
}
|
||||
let relay_domain = client
|
||||
.relay_url()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.trim_start_matches("ws://")
|
||||
.trim_start_matches("wss://")
|
||||
.to_string();
|
||||
|
||||
// Build and send kind 5 deletion event referencing the announcement by event ID
|
||||
let deletion = client
|
||||
.event_builder(Kind::EventDeletion, "")
|
||||
.tag(Tag::event(repo.id))
|
||||
let npub = client
|
||||
.public_key()
|
||||
.to_bech32()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Stage 2: clone the repo and create a unique commit (not pushed yet)
|
||||
let clone_path = clone_repo(&relay_domain, &npub, &repo_id)
|
||||
.map_err(|e| format!("Failed to clone repo: {}", e))?;
|
||||
|
||||
let cleanup = || { let _ = fs::remove_dir_all(&clone_path); };
|
||||
|
||||
let unique_commit = match create_commit(&clone_path, "deletion test unique commit") {
|
||||
Ok(h) => h,
|
||||
Err(e) => { cleanup(); return Err(format!("Failed to create commit: {}", e)); }
|
||||
};
|
||||
|
||||
// Stage 3: submit a state event pointing to the unique commit (enters purgatory)
|
||||
let state_event = client
|
||||
.event_builder(Kind::RepoState, "")
|
||||
.tag(Tag::identifier(&repo_id))
|
||||
.tag(Tag::custom(
|
||||
TagKind::custom("k"),
|
||||
vec!["30617"],
|
||||
TagKind::custom("refs/heads/main"),
|
||||
vec![unique_commit.clone()],
|
||||
))
|
||||
.tag(Tag::custom(
|
||||
TagKind::custom("HEAD"),
|
||||
vec!["ref: refs/heads/main".to_string()],
|
||||
))
|
||||
.build(client.keys())
|
||||
.map_err(|e| format!("Failed to build deletion event: {}", e))?;
|
||||
.map_err(|e| { cleanup(); format!("Failed to build state event: {}", e) })?;
|
||||
|
||||
let (_, in_purgatory) = client
|
||||
.send_event_and_note_purgatory(state_event.clone())
|
||||
.await
|
||||
.map_err(|e| { cleanup(); format!("Failed to send state event: {}", e) })?;
|
||||
|
||||
if !in_purgatory {
|
||||
cleanup();
|
||||
return Err(format!(
|
||||
"State event was served immediately (not in purgatory). \
|
||||
Commit {} may already exist on relay.",
|
||||
unique_commit
|
||||
));
|
||||
}
|
||||
|
||||
// Stage 4: send kind 5 deletion event referencing the state event by event ID
|
||||
let deletion = client
|
||||
.event_builder(Kind::EventDeletion, "")
|
||||
.tag(Tag::event(state_event.id))
|
||||
.tag(Tag::custom(TagKind::custom("k"), vec!["30618"]))
|
||||
.build(client.keys())
|
||||
.map_err(|e| { cleanup(); format!("Failed to build deletion event: {}", e) })?;
|
||||
|
||||
client
|
||||
.send_event(deletion)
|
||||
.await
|
||||
.map_err(|e| format!("Relay rejected deletion event: {}", e))?;
|
||||
.map_err(|e| { cleanup(); format!("Relay rejected deletion event: {}", e) })?;
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
// Verify the announcement can no longer be promoted by attempting a git push.
|
||||
// We check this indirectly: if the purgatory entry was removed, a subsequent
|
||||
// git push to the repo path should fail (no bare repo).
|
||||
// For the integration test we verify the announcement is still not served
|
||||
// (it was never promoted) and that the deletion event was accepted.
|
||||
// The bare-repo deletion is verified by attempting a git clone.
|
||||
let http_url = AuditClient::ws_to_http_url(&client.relay_url().await.map_err(|e| e.to_string())?)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let clone_url = format!(
|
||||
"{}/{}/{}.git",
|
||||
http_url,
|
||||
client.public_key().to_bech32().map_err(|e| e.to_string())?,
|
||||
repo_id
|
||||
);
|
||||
// Stage 5: attempt to push the unique commit — must be rejected
|
||||
let push_result = try_push(&clone_path);
|
||||
cleanup();
|
||||
|
||||
// git ls-remote should fail (bare repo deleted)
|
||||
let output = std::process::Command::new("git")
|
||||
.args(["ls-remote", &clone_url])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run git ls-remote: {}", e))?;
|
||||
|
||||
if output.status.success() {
|
||||
return Err(format!(
|
||||
"Bare repo still exists after deletion event. \
|
||||
Expected git ls-remote to fail for {}",
|
||||
clone_url
|
||||
));
|
||||
match push_result {
|
||||
Ok(false) => Ok(()), // push rejected as expected
|
||||
Ok(true) => Err(format!(
|
||||
"Push was accepted but should have been rejected. \
|
||||
The state event (id={}) was deleted, so commit {} \
|
||||
should not be authorized.",
|
||||
state_event.id, unique_commit
|
||||
)),
|
||||
Err(e) => Err(format!("Git push error: {}", e)),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Test: Kind 5 deletion event by `a` tag coordinate removes purgatory announcement
|
||||
/// Test: Kind 5 deletion event by `a` tag coordinate removes a purgatory state event
|
||||
///
|
||||
/// Spec: NIP-09
|
||||
/// "When an `a` tag is used, relays SHOULD delete all versions of the replaceable
|
||||
/// event up to the `created_at` timestamp of the deletion request event."
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. Send a valid repository announcement (enters purgatory)
|
||||
/// 2. Send a kind 5 deletion event referencing the announcement by coordinate
|
||||
/// (`30617:<pubkey>:<identifier>`)
|
||||
/// 3. The announcement is no longer in purgatory
|
||||
pub async fn test_deletion_by_coordinate_removes_purgatory_announcement(
|
||||
/// 1. Get a promoted repo (OwnerStateDataPushed) so git pushes are possible
|
||||
/// 2. Generate a fresh keypair for a new maintainer
|
||||
/// 3. Send a replacement owner announcement adding the new maintainer (goes to DB)
|
||||
/// 4. Send a state event signed by the new maintainer pointing to a unique commit
|
||||
/// (enters purgatory — maintainer is authorized but commit doesn't exist yet)
|
||||
/// 5. Delete by coordinate `30618:<new_maintainer_pubkey>:<identifier>`
|
||||
/// 6. Clone repo, create that unique commit, attempt to push — MUST be rejected
|
||||
/// (the state event was deleted, so the commit is no longer authorized)
|
||||
pub async fn test_deletion_by_coordinate_removes_purgatory_state_event(
|
||||
client: &AuditClient,
|
||||
) -> TestResult {
|
||||
TestResult::new(
|
||||
"deletion_by_coordinate_removes_purgatory_announcement",
|
||||
"deletion_by_coordinate_removes_purgatory_state_event",
|
||||
SpecRef::PurgatoryAcceptUntilGitData,
|
||||
"Kind 5 deletion by `a` coordinate SHOULD remove a purgatory announcement",
|
||||
"Kind 5 deletion by `a` coordinate SHOULD remove a purgatory state event, causing push rejection",
|
||||
)
|
||||
.run(|| async {
|
||||
let ctx = TestContext::new(client);
|
||||
|
||||
// Send announcement to purgatory
|
||||
let repo = ctx
|
||||
.get_fixture(FixtureKind::ValidRepoSent)
|
||||
// Stage 1: get a promoted repo with git data already on the relay
|
||||
let existing_state = ctx
|
||||
.get_fixture(FixtureKind::OwnerStateDataPushed)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create repo announcement: {}", e))?;
|
||||
.map_err(|e| format!("Failed to get promoted repo: {}", e))?;
|
||||
|
||||
let repo_id = repo
|
||||
let repo_id = existing_state
|
||||
.tags
|
||||
.iter()
|
||||
.find(|t| t.kind() == TagKind::d())
|
||||
.and_then(|t| t.content())
|
||||
.ok_or("Missing d tag in repo announcement")?
|
||||
.ok_or("Missing d tag in state event")?
|
||||
.to_string();
|
||||
|
||||
// Verify it's in purgatory (not served)
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
if client.is_event_on_relay(repo.id).await.map_err(|e| e.to_string())? {
|
||||
return Err(
|
||||
"Announcement was served immediately - purgatory not working".to_string(),
|
||||
);
|
||||
// Stage 2: generate a fresh keypair for a new maintainer
|
||||
let new_maintainer_keys = Keys::generate();
|
||||
let new_maintainer_hex = new_maintainer_keys.public_key().to_hex();
|
||||
|
||||
// Stage 3: send a replacement owner announcement that adds the new maintainer.
|
||||
// This is a replacement (same pubkey + identifier already in DB) so it goes
|
||||
// straight to the database without entering purgatory.
|
||||
let relay_url = client
|
||||
.relay_url()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let http_url = relay_url
|
||||
.replace("ws://", "http://")
|
||||
.replace("wss://", "https://");
|
||||
let npub = client
|
||||
.public_key()
|
||||
.to_bech32()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let replacement_announcement = client
|
||||
.event_builder(Kind::GitRepoAnnouncement, "")
|
||||
.tag(Tag::identifier(&repo_id))
|
||||
.tag(Tag::custom(
|
||||
TagKind::custom("clone"),
|
||||
vec![format!("{}/{}/{}.git", http_url, npub, repo_id)],
|
||||
))
|
||||
.tag(Tag::custom(
|
||||
TagKind::custom("relays"),
|
||||
vec![relay_url.clone()],
|
||||
))
|
||||
.tag(Tag::custom(
|
||||
TagKind::custom("maintainers"),
|
||||
vec![new_maintainer_hex.clone()],
|
||||
))
|
||||
.build(client.keys())
|
||||
.map_err(|e| format!("Failed to build replacement announcement: {}", e))?;
|
||||
|
||||
client
|
||||
.send_event(replacement_announcement)
|
||||
.await
|
||||
.map_err(|e| format!("Relay rejected replacement announcement: {}", e))?;
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
// Stage 4: clone the repo and create a unique commit (not pushed yet)
|
||||
let relay_domain = relay_url
|
||||
.trim_start_matches("ws://")
|
||||
.trim_start_matches("wss://")
|
||||
.to_string();
|
||||
|
||||
let clone_path = clone_repo(&relay_domain, &npub, &repo_id)
|
||||
.map_err(|e| format!("Failed to clone repo: {}", e))?;
|
||||
|
||||
let cleanup = || { let _ = fs::remove_dir_all(&clone_path); };
|
||||
|
||||
let unique_commit = match create_commit(&clone_path, "deletion coordinate test unique commit") {
|
||||
Ok(h) => h,
|
||||
Err(e) => { cleanup(); return Err(format!("Failed to create commit: {}", e)); }
|
||||
};
|
||||
|
||||
// Stage 5: submit a state event signed by the new maintainer pointing to the
|
||||
// unique commit. The new maintainer is now authorized (listed in the replacement
|
||||
// announcement), so the state event should enter purgatory (commit doesn't exist).
|
||||
let state_event = client
|
||||
.event_builder(Kind::RepoState, "")
|
||||
.tag(Tag::identifier(&repo_id))
|
||||
.tag(Tag::custom(
|
||||
TagKind::custom("refs/heads/main"),
|
||||
vec![unique_commit.clone()],
|
||||
))
|
||||
.tag(Tag::custom(
|
||||
TagKind::custom("HEAD"),
|
||||
vec!["ref: refs/heads/main".to_string()],
|
||||
))
|
||||
.build(&new_maintainer_keys)
|
||||
.map_err(|e| { cleanup(); format!("Failed to build state event: {}", e) })?;
|
||||
|
||||
let (_, in_purgatory) = client
|
||||
.send_event_and_note_purgatory(state_event.clone())
|
||||
.await
|
||||
.map_err(|e| { cleanup(); format!("Failed to send state event: {}", e) })?;
|
||||
|
||||
if !in_purgatory {
|
||||
cleanup();
|
||||
return Err(format!(
|
||||
"State event was served immediately (not in purgatory). \
|
||||
Commit {} may already exist on relay.",
|
||||
unique_commit
|
||||
));
|
||||
}
|
||||
|
||||
// Build coordinate: `30617:<pubkey_hex>:<identifier>`
|
||||
let coord = format!(
|
||||
"30617:{}:{}",
|
||||
client.public_key().to_hex(),
|
||||
repo_id
|
||||
);
|
||||
// Stage 6: send kind 5 deletion event signed by the new maintainer,
|
||||
// referencing their state event by coordinate `30618:<pubkey>:<identifier>`
|
||||
let coord = format!("30618:{}:{}", new_maintainer_hex, repo_id);
|
||||
|
||||
// Build and send kind 5 deletion event referencing by coordinate
|
||||
let deletion = client
|
||||
.event_builder(Kind::EventDeletion, "")
|
||||
.tag(Tag::custom(TagKind::custom("a"), vec![coord]))
|
||||
.tag(Tag::custom(TagKind::custom("k"), vec!["30617"]))
|
||||
.build(client.keys())
|
||||
.map_err(|e| format!("Failed to build deletion event: {}", e))?;
|
||||
.tag(Tag::custom(TagKind::custom("k"), vec!["30618"]))
|
||||
.build(&new_maintainer_keys)
|
||||
.map_err(|e| { cleanup(); format!("Failed to build deletion event: {}", e) })?;
|
||||
|
||||
client
|
||||
.send_event(deletion)
|
||||
.await
|
||||
.map_err(|e| format!("Relay rejected deletion event: {}", e))?;
|
||||
.map_err(|e| { cleanup(); format!("Relay rejected deletion event: {}", e) })?;
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
// Verify bare repo was deleted
|
||||
let http_url = AuditClient::ws_to_http_url(&client.relay_url().await.map_err(|e| e.to_string())?)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let clone_url = format!(
|
||||
"{}/{}/{}.git",
|
||||
http_url,
|
||||
client.public_key().to_bech32().map_err(|e| e.to_string())?,
|
||||
repo_id
|
||||
);
|
||||
// Stage 7: attempt to push the unique commit — must be rejected because
|
||||
// the new maintainer's state event was deleted from purgatory
|
||||
let push_result = try_push(&clone_path);
|
||||
cleanup();
|
||||
|
||||
let output = std::process::Command::new("git")
|
||||
.args(["ls-remote", &clone_url])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run git ls-remote: {}", e))?;
|
||||
|
||||
if output.status.success() {
|
||||
return Err(format!(
|
||||
"Bare repo still exists after deletion event. \
|
||||
Expected git ls-remote to fail for {}",
|
||||
clone_url
|
||||
));
|
||||
match push_result {
|
||||
Ok(false) => Ok(()), // push rejected as expected
|
||||
Ok(true) => Err(format!(
|
||||
"Push was accepted but should have been rejected. \
|
||||
The new maintainer's state event (id={}) was deleted by coordinate, \
|
||||
so commit {} should not be authorized.",
|
||||
state_event.id, unique_commit
|
||||
)),
|
||||
Err(e) => Err(format!("Git push error: {}", e)),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/// Deletion Policy - NIP-09 event deletion request handling
|
||||
///
|
||||
/// Handles kind 5 (EventDeletion) events that request removal of repository
|
||||
/// announcements (kind 30617) from purgatory.
|
||||
/// Handles kind 5 (EventDeletion) events that request removal of purgatory entries
|
||||
/// for repository announcements (kind 30617) and state events (kind 30618).
|
||||
///
|
||||
/// ## NIP-09 Rules Enforced
|
||||
///
|
||||
@@ -13,9 +13,9 @@
|
||||
///
|
||||
/// ## Purgatory Interaction
|
||||
///
|
||||
/// When a valid deletion request targets a kind 30617 announcement that is currently
|
||||
/// in purgatory (not yet promoted to the database), the purgatory entry is removed
|
||||
/// and the bare repository is deleted from disk.
|
||||
/// - Kind 30617 (announcement) in purgatory: entry removed, bare repo deleted from disk
|
||||
/// - Kind 30618 (state event) in purgatory: matching state event(s) removed by event ID
|
||||
/// or by (author, identifier) coordinate
|
||||
use nostr_relay_builder::prelude::{Event, WritePolicyResult};
|
||||
|
||||
use super::PolicyContext;
|
||||
@@ -48,13 +48,13 @@ impl DeletionPolicy {
|
||||
WritePolicyResult::Accept
|
||||
}
|
||||
|
||||
/// Remove any purgatory announcements targeted by this deletion event.
|
||||
/// Remove any purgatory entries targeted by this deletion event.
|
||||
///
|
||||
/// Handles both reference styles from NIP-09:
|
||||
/// - `e` tags: event ID references — match against purgatory entry event IDs
|
||||
/// - `a` tags: addressable coordinate references — `30617:<pubkey>:<identifier>`
|
||||
/// - `e` tags: event ID references — match against announcement or state event IDs
|
||||
/// - `a` tags: addressable coordinate references — `30617:…` or `30618:…`
|
||||
///
|
||||
/// Only removes entries where the purgatory entry's owner matches the deletion
|
||||
/// Only removes entries where the purgatory entry's author matches the deletion
|
||||
/// event's pubkey (enforces author-only deletion).
|
||||
fn remove_purgatory_targets(&self, event: &Event) {
|
||||
let author = &event.pubkey;
|
||||
@@ -81,17 +81,19 @@ impl DeletionPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a purgatory announcement matched by event ID.
|
||||
/// Remove a purgatory entry (announcement or state event) matched by event ID.
|
||||
///
|
||||
/// Scans all purgatory announcements owned by `author` and removes the one
|
||||
/// whose event ID hex matches `target_id_hex`.
|
||||
fn remove_by_event_id(&self, author: &nostr_relay_builder::prelude::PublicKey, target_id_hex: &str, _deletion_created_at: u64) {
|
||||
// Scan announcements owned by this author for a matching event ID
|
||||
// We use get_announcements_by_identifier would require knowing the identifier,
|
||||
// so instead we iterate via find_announcement after collecting all entries.
|
||||
/// Checks announcements first (kind 30617), then state events (kind 30618).
|
||||
/// Only removes entries whose author matches `author`.
|
||||
fn remove_by_event_id(
|
||||
&self,
|
||||
author: &nostr_relay_builder::prelude::PublicKey,
|
||||
target_id_hex: &str,
|
||||
_deletion_created_at: u64,
|
||||
) {
|
||||
// --- Check announcements (kind 30617) ---
|
||||
// The DashMap doesn't expose a direct "find by event ID" method, so we use
|
||||
// the announcements_for_sync snapshot to get all (repo_id, _) pairs and then
|
||||
// look up each one.
|
||||
// the announcements_for_sync snapshot to enumerate all (repo_id, _) pairs.
|
||||
let all = self.ctx.purgatory.announcements_for_sync();
|
||||
for (repo_id, _) in all {
|
||||
// repo_id format: "30617:{pubkey_hex}:{identifier}"
|
||||
@@ -102,7 +104,6 @@ impl DeletionPolicy {
|
||||
let entry_pubkey_hex = parts[1];
|
||||
let identifier = parts[2];
|
||||
|
||||
// Only check entries owned by the deletion event author
|
||||
if entry_pubkey_hex != author.to_hex() {
|
||||
continue;
|
||||
}
|
||||
@@ -116,18 +117,37 @@ impl DeletionPolicy {
|
||||
"Deletion request: removing purgatory announcement by event ID"
|
||||
);
|
||||
self.evict_purgatory_entry(author, identifier);
|
||||
return; // event IDs are unique, no need to continue
|
||||
return; // event IDs are unique
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Check state events (kind 30618) ---
|
||||
// State events are keyed by identifier; scan all identifiers for a match.
|
||||
let state_identifiers = self.ctx.purgatory.get_all_identifiers();
|
||||
for identifier in state_identifiers {
|
||||
let entries = self.ctx.purgatory.find_state(&identifier);
|
||||
for entry in entries {
|
||||
if entry.author == *author && entry.event.id.to_hex() == target_id_hex {
|
||||
tracing::info!(
|
||||
event_id = %target_id_hex,
|
||||
identifier = %identifier,
|
||||
author = %author.to_hex(),
|
||||
"Deletion request: removing purgatory state event by event ID"
|
||||
);
|
||||
self.ctx.purgatory.remove_state_event(&identifier, &entry.event.id);
|
||||
return; // event IDs are unique
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a purgatory announcement matched by addressable coordinate.
|
||||
/// Remove a purgatory entry matched by addressable coordinate.
|
||||
///
|
||||
/// The coordinate format is `<kind>:<pubkey>:<d-identifier>`. Only kind 30617
|
||||
/// coordinates are relevant here. Per NIP-09, all versions up to `deletion_created_at`
|
||||
/// are considered deleted — since purgatory entries are always a single event per
|
||||
/// (owner, identifier), we delete if the entry's `created_at` ≤ `deletion_created_at`.
|
||||
/// The coordinate format is `<kind>:<pubkey>:<d-identifier>`.
|
||||
/// Handles kind 30617 (announcements) and kind 30618 (state events).
|
||||
///
|
||||
/// Per NIP-09, all versions up to `deletion_created_at` are considered deleted.
|
||||
fn remove_by_coordinate(
|
||||
&self,
|
||||
author: &nostr_relay_builder::prelude::PublicKey,
|
||||
@@ -144,11 +164,6 @@ impl DeletionPolicy {
|
||||
let coord_pubkey_hex = parts[1];
|
||||
let identifier = parts[2];
|
||||
|
||||
// Only handle kind 30617 (GitRepoAnnouncement)
|
||||
if kind_str != "30617" {
|
||||
return;
|
||||
}
|
||||
|
||||
// The coordinate pubkey must match the deletion event author
|
||||
if coord_pubkey_hex != author.to_hex() {
|
||||
tracing::debug!(
|
||||
@@ -159,25 +174,50 @@ impl DeletionPolicy {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(entry) = self.ctx.purgatory.find_announcement(author, identifier) {
|
||||
// Per NIP-09: delete all versions up to deletion_created_at
|
||||
if entry.event.created_at.as_secs() <= deletion_created_at {
|
||||
tracing::info!(
|
||||
identifier = %identifier,
|
||||
author = %author.to_hex(),
|
||||
entry_created_at = entry.event.created_at.as_secs(),
|
||||
deletion_created_at = %deletion_created_at,
|
||||
"Deletion request: removing purgatory announcement by coordinate"
|
||||
);
|
||||
self.evict_purgatory_entry(author, identifier);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
identifier = %identifier,
|
||||
author = %author.to_hex(),
|
||||
entry_created_at = entry.event.created_at.as_secs(),
|
||||
deletion_created_at = %deletion_created_at,
|
||||
"Ignoring deletion: purgatory entry is newer than deletion request"
|
||||
);
|
||||
match kind_str {
|
||||
"30617" => {
|
||||
// Announcement purgatory entry
|
||||
if let Some(entry) = self.ctx.purgatory.find_announcement(author, identifier) {
|
||||
if entry.event.created_at.as_secs() <= deletion_created_at {
|
||||
tracing::info!(
|
||||
identifier = %identifier,
|
||||
author = %author.to_hex(),
|
||||
"Deletion request: removing purgatory announcement by coordinate"
|
||||
);
|
||||
self.evict_purgatory_entry(author, identifier);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
identifier = %identifier,
|
||||
author = %author.to_hex(),
|
||||
"Ignoring deletion: purgatory announcement is newer than deletion request"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
"30618" => {
|
||||
// State event purgatory entries for this (author, identifier).
|
||||
// Remove all entries authored by `author` with created_at ≤ deletion_created_at.
|
||||
let entries = self.ctx.purgatory.find_state(identifier);
|
||||
let mut removed = 0usize;
|
||||
for entry in entries {
|
||||
if entry.author == *author
|
||||
&& entry.event.created_at.as_secs() <= deletion_created_at
|
||||
{
|
||||
self.ctx.purgatory.remove_state_event(identifier, &entry.event.id);
|
||||
removed += 1;
|
||||
}
|
||||
}
|
||||
if removed > 0 {
|
||||
tracing::info!(
|
||||
identifier = %identifier,
|
||||
author = %author.to_hex(),
|
||||
removed = %removed,
|
||||
"Deletion request: removed purgatory state event(s) by coordinate"
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Other kinds not handled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -70,8 +70,8 @@ isolated_purgatory_test!(test_state_event_accepted_for_purgatory_announcement);
|
||||
// Deletion Event Tests (NIP-09)
|
||||
// ============================================================
|
||||
|
||||
isolated_purgatory_test!(test_deletion_by_event_id_removes_purgatory_announcement);
|
||||
isolated_purgatory_test!(test_deletion_by_coordinate_removes_purgatory_announcement);
|
||||
isolated_purgatory_test!(test_deletion_by_event_id_removes_purgatory_state_event);
|
||||
isolated_purgatory_test!(test_deletion_by_coordinate_removes_purgatory_state_event);
|
||||
|
||||
// ============================================================
|
||||
// State Event Purgatory Tests (already implemented)
|
||||
|
||||
Reference in New Issue
Block a user