mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 23:18:24 +00:00
test(nip09): dedupe helpers into common, consolidate basic tests
Extract the duplicated publish_served_repo, announcement_served_by_coordinate, announcement_coordinate and build_deletion helpers (~250 lines copied verbatim across both NIP-09 suites) into tests/common/nip09_helpers.rs. Move the normal_mode_honours_deletion baseline from nip09_disrespector.rs into nip09_validation.rs so all basic NIP-09 validation tests live together; the disrespector suite now owns only the disrespector-specific behaviour (accept-but-ignore, NIP-11 advertisement).
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
pub mod git_server;
|
||||
pub mod mock_relay;
|
||||
pub mod nip09_helpers;
|
||||
pub mod port;
|
||||
pub mod purgatory_helpers;
|
||||
pub mod relay;
|
||||
@@ -11,6 +12,7 @@ pub mod sync_helpers;
|
||||
|
||||
pub use git_server::{SimpleGitServer, SmartGitServer};
|
||||
pub use mock_relay::MockRelay;
|
||||
pub use nip09_helpers::*;
|
||||
pub use port::{reserve_port, PortReservation};
|
||||
pub use purgatory_helpers::*;
|
||||
pub use relay::TestRelay;
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
//! Shared NIP-09 deletion-request test helpers.
|
||||
//!
|
||||
//! These utilities are shared by the NIP-09 integration test suites
|
||||
//! (`nip09_validation.rs` and `nip09_disrespector.rs`) so the announcement
|
||||
//! promotion dance, coordinate-query helper and kind-5 deletion builder are
|
||||
//! defined exactly once.
|
||||
//!
|
||||
//! All helpers drive a live ngit-grasp instance via [`AuditClient`].
|
||||
|
||||
use grasp_audit::{
|
||||
clone_repo, create_deterministic_commit, try_push, AuditClient, DETERMINISTIC_COMMIT_HASH,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Publish a repo announcement, submit a matching state event, and push the
|
||||
/// deterministic git data so the announcement is promoted out of purgatory and
|
||||
/// becomes queryable.
|
||||
///
|
||||
/// Returns the announcement event (kind 30617) and its repo identifier.
|
||||
///
|
||||
/// A kind-30617 announcement is NOT queryable until git data is pushed; the
|
||||
/// tests that target the announcement therefore need it promoted first. Layer-2
|
||||
/// events (issues/notes) that reference the promoted repo are served
|
||||
/// immediately and do not need this dance.
|
||||
pub async fn publish_served_repo(client: &AuditClient, test_name: &str) -> (Event, String) {
|
||||
let relay_url = client
|
||||
.relay_url()
|
||||
.await
|
||||
.expect("client should have a relay");
|
||||
let relay_domain = relay_url
|
||||
.trim_start_matches("ws://")
|
||||
.trim_start_matches("wss://")
|
||||
.to_string();
|
||||
let http_url = format!("http://{}", relay_domain);
|
||||
|
||||
let npub = client.public_key().to_bech32().expect("pubkey to bech32");
|
||||
|
||||
let repo_id = format!(
|
||||
"{}-{}",
|
||||
test_name,
|
||||
&Keys::generate().public_key().to_hex()[..8]
|
||||
);
|
||||
|
||||
// Stage 1: announcement (enters purgatory until git data arrives)
|
||||
let announcement = client
|
||||
.event_builder(Kind::GitRepoAnnouncement, "")
|
||||
.tag(Tag::identifier(&repo_id))
|
||||
.tag(Tag::custom("name", vec![repo_id.clone()]))
|
||||
.tag(Tag::custom(
|
||||
"clone",
|
||||
vec![format!("{}/{}/{}.git", http_url, npub, repo_id)],
|
||||
))
|
||||
.tag(Tag::custom("relays", vec![relay_url.clone()]))
|
||||
.build(client.keys())
|
||||
.expect("build announcement");
|
||||
|
||||
client
|
||||
.send_event(announcement.clone())
|
||||
.await
|
||||
.expect("relay should accept announcement");
|
||||
|
||||
// Stage 2: state event pointing at the deterministic commit (purgatory)
|
||||
let state_event = client
|
||||
.event_builder(Kind::RepoState, "")
|
||||
.tag(Tag::identifier(&repo_id))
|
||||
.tag(Tag::custom(
|
||||
"refs/heads/main",
|
||||
vec![DETERMINISTIC_COMMIT_HASH.to_string()],
|
||||
))
|
||||
.tag(Tag::custom(
|
||||
"HEAD",
|
||||
vec!["ref: refs/heads/main".to_string()],
|
||||
))
|
||||
.build(client.keys())
|
||||
.expect("build state event");
|
||||
|
||||
client
|
||||
.send_event_and_note_purgatory(state_event.clone())
|
||||
.await
|
||||
.expect("relay should accept state event");
|
||||
|
||||
// Stage 3: clone, create the deterministic commit, push — promotes the repo
|
||||
let clone_path =
|
||||
clone_repo(&relay_domain, &npub, &repo_id).expect("clone purgatory repo over git http");
|
||||
|
||||
let cleanup = |path: &PathBuf| {
|
||||
let _ = std::fs::remove_dir_all(path);
|
||||
};
|
||||
|
||||
let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
cleanup(&clone_path);
|
||||
panic!("failed to create deterministic commit: {}", e);
|
||||
}
|
||||
};
|
||||
assert_eq!(
|
||||
commit_hash, DETERMINISTIC_COMMIT_HASH,
|
||||
"deterministic commit hash mismatch"
|
||||
);
|
||||
|
||||
let _ = std::process::Command::new("git")
|
||||
.args(["branch", "main"])
|
||||
.current_dir(&clone_path)
|
||||
.output();
|
||||
let _ = std::process::Command::new("git")
|
||||
.args(["checkout", "main"])
|
||||
.current_dir(&clone_path)
|
||||
.output();
|
||||
|
||||
let pushed = try_push(&clone_path);
|
||||
cleanup(&clone_path);
|
||||
match pushed {
|
||||
Ok(true) => {}
|
||||
Ok(false) => panic!("git push rejected while promoting repo out of purgatory"),
|
||||
Err(e) => panic!("git push error while promoting repo: {}", e),
|
||||
}
|
||||
|
||||
// Give the relay a moment to promote the events out of purgatory.
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
assert!(
|
||||
client
|
||||
.is_event_on_relay(announcement.id)
|
||||
.await
|
||||
.expect("query announcement"),
|
||||
"announcement should be served after git data arrives"
|
||||
);
|
||||
|
||||
(announcement, repo_id)
|
||||
}
|
||||
|
||||
/// Query the relay for a served repo announcement using a real client's access
|
||||
/// pattern: author + kind + `d` identifier (a NIP-01 addressable-event query),
|
||||
/// rather than a by-id lookup. Returns whether the relay serves it.
|
||||
pub async fn announcement_served_by_coordinate(
|
||||
client: &AuditClient,
|
||||
announcement: &Event,
|
||||
repo_id: &str,
|
||||
) -> bool {
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::GitRepoAnnouncement)
|
||||
.author(announcement.pubkey)
|
||||
.identifier(repo_id);
|
||||
|
||||
client
|
||||
.query(filter)
|
||||
.await
|
||||
.expect("query announcement by coordinate")
|
||||
.iter()
|
||||
.any(|e| e.id == announcement.id)
|
||||
}
|
||||
|
||||
/// Coordinate string for a kind-30617 announcement: `30617:<pubkey-hex>:<d>`.
|
||||
pub fn announcement_coordinate(announcement: &Event, repo_id: &str) -> String {
|
||||
format!(
|
||||
"{}:{}:{}",
|
||||
Kind::GitRepoAnnouncement.as_u16(),
|
||||
announcement.pubkey.to_hex(),
|
||||
repo_id
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a kind-5 deletion request signed by `client` carrying the supplied
|
||||
/// `e`-tag event ids and `a`-tag coordinate strings.
|
||||
pub fn build_deletion(client: &AuditClient, event_ids: &[EventId], coordinates: &[String]) -> Event {
|
||||
let mut builder = client.event_builder(Kind::EventDeletion, "delete");
|
||||
|
||||
for id in event_ids {
|
||||
builder = builder.tag(Tag::event(*id));
|
||||
}
|
||||
for coord in coordinates {
|
||||
builder = builder.tag(Tag::custom("a", vec![coord.clone()]));
|
||||
}
|
||||
|
||||
builder.build(client.keys()).expect("build deletion event")
|
||||
}
|
||||
+15
-218
@@ -7,15 +7,19 @@
|
||||
//! These tests deliberately assert only on observable contract, not on any
|
||||
//! internal "holding database" — the current implementation is tombstone /
|
||||
//! purgatory based, so the disrespector simply accepts the kind-5 request and
|
||||
//! does nothing with it. The three properties under test are:
|
||||
//! does nothing with it. The two properties under test are:
|
||||
//!
|
||||
//! 1. **Disrespector accepts but ignores deletion** — a kind-5 request gets an
|
||||
//! `OK`, yet the targeted announcement remains queryable afterwards.
|
||||
//! 2. **Normal mode honours deletion** — the same flow against a default relay
|
||||
//! removes the target.
|
||||
//! 3. **NIP-11 advertisement** — a disrespector relay omits `9` and `62` from
|
||||
//! 2. **NIP-11 advertisement** — a disrespector relay omits `9` and `62` from
|
||||
//! `supported_nips`; a normal relay advertises both.
|
||||
//!
|
||||
//! The normal-mode baseline ("a default relay honours the deletion") lives in
|
||||
//! `nip09_validation.rs` as `normal_mode_honours_deletion`. Shared helpers
|
||||
//! (`publish_served_repo`, `announcement_served_by_coordinate`,
|
||||
//! `announcement_coordinate`, `build_deletion`) live in
|
||||
//! `tests/common/nip09_helpers.rs`.
|
||||
//!
|
||||
//! # Running
|
||||
//!
|
||||
//! ```bash
|
||||
@@ -25,177 +29,12 @@
|
||||
|
||||
mod common;
|
||||
|
||||
use common::TestRelay;
|
||||
|
||||
use grasp_audit::{
|
||||
clone_repo, create_deterministic_commit, try_push, AuditClient, AuditConfig,
|
||||
DETERMINISTIC_COMMIT_HASH,
|
||||
use common::{
|
||||
announcement_coordinate, announcement_served_by_coordinate, build_deletion,
|
||||
publish_served_repo, TestRelay,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Publish a repo announcement, submit a matching state event, and push the
|
||||
/// deterministic git data so the announcement is promoted out of purgatory and
|
||||
/// becomes queryable.
|
||||
///
|
||||
/// Returns the announcement event (kind 30617) and its repo identifier.
|
||||
///
|
||||
/// This is a self-contained replica of the `PurgatoryOwnerStateDataPushed`
|
||||
/// fixture lifecycle. It is inlined here (rather than reusing the fixture)
|
||||
/// because the disrespector tests need the *announcement* event itself to
|
||||
/// target with the kind-5 deletion, whereas the fixture returns the state
|
||||
/// event.
|
||||
async fn publish_served_repo(client: &AuditClient, test_name: &str) -> (Event, String) {
|
||||
let relay_url = client
|
||||
.relay_url()
|
||||
.await
|
||||
.expect("client should have a relay");
|
||||
let relay_domain = relay_url
|
||||
.trim_start_matches("ws://")
|
||||
.trim_start_matches("wss://")
|
||||
.to_string();
|
||||
let http_url = format!("http://{}", relay_domain);
|
||||
|
||||
let npub = client.public_key().to_bech32().expect("pubkey to bech32");
|
||||
|
||||
let repo_id = format!(
|
||||
"{}-{}",
|
||||
test_name,
|
||||
&Keys::generate().public_key().to_hex()[..8]
|
||||
);
|
||||
|
||||
// Stage 1: announcement (enters purgatory until git data arrives)
|
||||
let announcement = client
|
||||
.event_builder(Kind::GitRepoAnnouncement, "")
|
||||
.tag(Tag::identifier(&repo_id))
|
||||
.tag(Tag::custom("name", vec![repo_id.clone()]))
|
||||
.tag(Tag::custom(
|
||||
"clone",
|
||||
vec![format!("{}/{}/{}.git", http_url, npub, repo_id)],
|
||||
))
|
||||
.tag(Tag::custom("relays", vec![relay_url.clone()]))
|
||||
.build(client.keys())
|
||||
.expect("build announcement");
|
||||
|
||||
client
|
||||
.send_event(announcement.clone())
|
||||
.await
|
||||
.expect("relay should accept announcement");
|
||||
|
||||
// Stage 2: state event pointing at the deterministic commit (purgatory)
|
||||
let state_event = client
|
||||
.event_builder(Kind::RepoState, "")
|
||||
.tag(Tag::identifier(&repo_id))
|
||||
.tag(Tag::custom(
|
||||
"refs/heads/main",
|
||||
vec![DETERMINISTIC_COMMIT_HASH.to_string()],
|
||||
))
|
||||
.tag(Tag::custom(
|
||||
"HEAD",
|
||||
vec!["ref: refs/heads/main".to_string()],
|
||||
))
|
||||
.build(client.keys())
|
||||
.expect("build state event");
|
||||
|
||||
client
|
||||
.send_event_and_note_purgatory(state_event.clone())
|
||||
.await
|
||||
.expect("relay should accept state event");
|
||||
|
||||
// Stage 3: clone, create the deterministic commit, push — promotes the repo
|
||||
let clone_path =
|
||||
clone_repo(&relay_domain, &npub, &repo_id).expect("clone purgatory repo over git http");
|
||||
|
||||
let cleanup = |path: &PathBuf| {
|
||||
let _ = std::fs::remove_dir_all(path);
|
||||
};
|
||||
|
||||
let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
cleanup(&clone_path);
|
||||
panic!("failed to create deterministic commit: {}", e);
|
||||
}
|
||||
};
|
||||
assert_eq!(
|
||||
commit_hash, DETERMINISTIC_COMMIT_HASH,
|
||||
"deterministic commit hash mismatch"
|
||||
);
|
||||
|
||||
let _ = std::process::Command::new("git")
|
||||
.args(["branch", "main"])
|
||||
.current_dir(&clone_path)
|
||||
.output();
|
||||
let _ = std::process::Command::new("git")
|
||||
.args(["checkout", "main"])
|
||||
.current_dir(&clone_path)
|
||||
.output();
|
||||
|
||||
let pushed = try_push(&clone_path);
|
||||
cleanup(&clone_path);
|
||||
match pushed {
|
||||
Ok(true) => {}
|
||||
Ok(false) => panic!("git push rejected while promoting repo out of purgatory"),
|
||||
Err(e) => panic!("git push error while promoting repo: {}", e),
|
||||
}
|
||||
|
||||
// Give the relay a moment to promote the events out of purgatory.
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
assert!(
|
||||
client
|
||||
.is_event_on_relay(announcement.id)
|
||||
.await
|
||||
.expect("query announcement"),
|
||||
"announcement should be served after git data arrives"
|
||||
);
|
||||
|
||||
(announcement, repo_id)
|
||||
}
|
||||
|
||||
/// Query the relay for a served repo announcement using a real client's access
|
||||
/// pattern: author + kind + `d` identifier (a NIP-01 addressable-event query),
|
||||
/// rather than a by-id lookup. Returns whether the relay serves it.
|
||||
async fn announcement_served_by_coordinate(
|
||||
client: &AuditClient,
|
||||
announcement: &Event,
|
||||
repo_id: &str,
|
||||
) -> bool {
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::GitRepoAnnouncement)
|
||||
.author(announcement.pubkey)
|
||||
.identifier(repo_id);
|
||||
|
||||
client
|
||||
.query(filter)
|
||||
.await
|
||||
.expect("query announcement by coordinate")
|
||||
.iter()
|
||||
.any(|e| e.id == announcement.id)
|
||||
}
|
||||
|
||||
/// Build a kind-5 deletion request from the announcement's author targeting the
|
||||
/// announcement both by event id and by `a` coordinate (replaceable event).
|
||||
fn build_deletion(client: &AuditClient, announcement: &Event, repo_id: &str) -> Event {
|
||||
let coordinate = format!(
|
||||
"{}:{}:{}",
|
||||
Kind::GitRepoAnnouncement.as_u16(),
|
||||
announcement.pubkey.to_hex(),
|
||||
repo_id
|
||||
);
|
||||
|
||||
client
|
||||
.event_builder(Kind::EventDeletion, "")
|
||||
.tag(Tag::event(announcement.id))
|
||||
.tag(Tag::custom("a", vec![coordinate]))
|
||||
.tag(Tag::custom(
|
||||
"k",
|
||||
vec![Kind::GitRepoAnnouncement.as_u16().to_string()],
|
||||
))
|
||||
.build(client.keys())
|
||||
.expect("build deletion event")
|
||||
}
|
||||
use grasp_audit::{AuditClient, AuditConfig};
|
||||
|
||||
/// Disrespector mode: the kind-5 deletion is accepted (OK) but ignored — the
|
||||
/// targeted announcement remains queryable.
|
||||
@@ -208,7 +47,8 @@ async fn disrespector_accepts_but_ignores_deletion() {
|
||||
|
||||
let (announcement, repo_id) = publish_served_repo(&client, "disrespector-ignores").await;
|
||||
|
||||
let deletion = build_deletion(&client, &announcement, &repo_id);
|
||||
let coordinate = announcement_coordinate(&announcement, &repo_id);
|
||||
let deletion = build_deletion(&client, &[announcement.id], &[coordinate]);
|
||||
let deletion_id = deletion.id;
|
||||
|
||||
// The relay MUST accept the deletion (archival mode still stores/acks it).
|
||||
@@ -217,7 +57,7 @@ async fn disrespector_accepts_but_ignores_deletion() {
|
||||
.await
|
||||
.expect("disrespector relay should OK the kind-5 deletion");
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
|
||||
|
||||
// Contract part 1: the kind-5 request itself MUST be stored/served — the
|
||||
// archival relay accepts and preserves the request rather than dropping it.
|
||||
@@ -258,49 +98,6 @@ async fn disrespector_accepts_but_ignores_deletion() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Normal mode (default relay) honours the deletion: after a kind-5 request the
|
||||
/// targeted announcement is gone.
|
||||
#[tokio::test]
|
||||
async fn normal_mode_honours_deletion() {
|
||||
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, "normal-honours").await;
|
||||
|
||||
let deletion = build_deletion(&client, &announcement, &repo_id);
|
||||
|
||||
client
|
||||
.send_event(deletion)
|
||||
.await
|
||||
.expect("normal relay should OK the kind-5 deletion");
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
let served_by_id = client
|
||||
.is_event_on_relay(announcement.id)
|
||||
.await
|
||||
.expect("query announcement by id after deletion");
|
||||
let served_by_coordinate =
|
||||
announcement_served_by_coordinate(&client, &announcement, &repo_id).await;
|
||||
|
||||
relay.stop().await;
|
||||
|
||||
assert!(
|
||||
!served_by_id,
|
||||
"normal relay must honour the deletion: announcement {} should no longer be \
|
||||
queryable by id",
|
||||
announcement.id
|
||||
);
|
||||
assert!(
|
||||
!served_by_coordinate,
|
||||
"normal relay must honour the deletion: announcement {} should no longer be served \
|
||||
for an author+kind+identifier query",
|
||||
announcement.id
|
||||
);
|
||||
}
|
||||
|
||||
/// Fetch the NIP-11 relay information document via HTTP and return the list of
|
||||
/// advertised `supported_nips`.
|
||||
///
|
||||
|
||||
+63
-179
@@ -23,9 +23,15 @@
|
||||
//! 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
|
||||
//! duplicated here.
|
||||
//! The "normal mode honours deletion" baseline (a default relay removing an
|
||||
//! announcement deleted by its `a` coordinate) lives here as a basic NIP-09
|
||||
//! test (`normal_mode_honours_deletion`). The disrespector-specific
|
||||
//! counterparts (accept-but-ignore, NIP-11 advertisement) live in
|
||||
//! `nip09_disrespector.rs`.
|
||||
//!
|
||||
//! Shared helpers (`publish_served_repo`, `announcement_served_by_coordinate`,
|
||||
//! `announcement_coordinate`, `build_deletion`) live in
|
||||
//! `tests/common/nip09_helpers.rs` and are used by both suites.
|
||||
//!
|
||||
//! # Running
|
||||
//!
|
||||
@@ -36,186 +42,15 @@
|
||||
|
||||
mod common;
|
||||
|
||||
use common::TestRelay;
|
||||
|
||||
use grasp_audit::{
|
||||
clone_repo, create_deterministic_commit, try_push, AuditClient, AuditConfig,
|
||||
DETERMINISTIC_COMMIT_HASH,
|
||||
use common::{
|
||||
announcement_coordinate, announcement_served_by_coordinate, build_deletion,
|
||||
publish_served_repo, TestRelay,
|
||||
};
|
||||
|
||||
use grasp_audit::{AuditClient, AuditConfig};
|
||||
use nostr_sdk::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Publish a repo announcement, submit a matching state event, and push the
|
||||
/// deterministic git data so the announcement is promoted out of purgatory and
|
||||
/// becomes queryable.
|
||||
///
|
||||
/// Returns the announcement event (kind 30617) and its repo identifier.
|
||||
///
|
||||
/// Copied (inline) from `nip09_disrespector.rs` so this file is self-contained.
|
||||
/// A kind-30617 announcement is NOT queryable until git data is pushed; the
|
||||
/// tests that target the announcement therefore need it promoted first. Layer-2
|
||||
/// events (issues/notes) that reference the promoted repo are served
|
||||
/// immediately and do not need this dance.
|
||||
async fn publish_served_repo(client: &AuditClient, test_name: &str) -> (Event, String) {
|
||||
let relay_url = client
|
||||
.relay_url()
|
||||
.await
|
||||
.expect("client should have a relay");
|
||||
let relay_domain = relay_url
|
||||
.trim_start_matches("ws://")
|
||||
.trim_start_matches("wss://")
|
||||
.to_string();
|
||||
let http_url = format!("http://{}", relay_domain);
|
||||
|
||||
let npub = client.public_key().to_bech32().expect("pubkey to bech32");
|
||||
|
||||
let repo_id = format!(
|
||||
"{}-{}",
|
||||
test_name,
|
||||
&Keys::generate().public_key().to_hex()[..8]
|
||||
);
|
||||
|
||||
// Stage 1: announcement (enters purgatory until git data arrives)
|
||||
let announcement = client
|
||||
.event_builder(Kind::GitRepoAnnouncement, "")
|
||||
.tag(Tag::identifier(&repo_id))
|
||||
.tag(Tag::custom("name", vec![repo_id.clone()]))
|
||||
.tag(Tag::custom(
|
||||
"clone",
|
||||
vec![format!("{}/{}/{}.git", http_url, npub, repo_id)],
|
||||
))
|
||||
.tag(Tag::custom("relays", vec![relay_url.clone()]))
|
||||
.build(client.keys())
|
||||
.expect("build announcement");
|
||||
|
||||
client
|
||||
.send_event(announcement.clone())
|
||||
.await
|
||||
.expect("relay should accept announcement");
|
||||
|
||||
// Stage 2: state event pointing at the deterministic commit (purgatory)
|
||||
let state_event = client
|
||||
.event_builder(Kind::RepoState, "")
|
||||
.tag(Tag::identifier(&repo_id))
|
||||
.tag(Tag::custom(
|
||||
"refs/heads/main",
|
||||
vec![DETERMINISTIC_COMMIT_HASH.to_string()],
|
||||
))
|
||||
.tag(Tag::custom(
|
||||
"HEAD",
|
||||
vec!["ref: refs/heads/main".to_string()],
|
||||
))
|
||||
.build(client.keys())
|
||||
.expect("build state event");
|
||||
|
||||
client
|
||||
.send_event_and_note_purgatory(state_event.clone())
|
||||
.await
|
||||
.expect("relay should accept state event");
|
||||
|
||||
// Stage 3: clone, create the deterministic commit, push — promotes the repo
|
||||
let clone_path =
|
||||
clone_repo(&relay_domain, &npub, &repo_id).expect("clone purgatory repo over git http");
|
||||
|
||||
let cleanup = |path: &PathBuf| {
|
||||
let _ = std::fs::remove_dir_all(path);
|
||||
};
|
||||
|
||||
let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
cleanup(&clone_path);
|
||||
panic!("failed to create deterministic commit: {}", e);
|
||||
}
|
||||
};
|
||||
assert_eq!(
|
||||
commit_hash, DETERMINISTIC_COMMIT_HASH,
|
||||
"deterministic commit hash mismatch"
|
||||
);
|
||||
|
||||
let _ = std::process::Command::new("git")
|
||||
.args(["branch", "main"])
|
||||
.current_dir(&clone_path)
|
||||
.output();
|
||||
let _ = std::process::Command::new("git")
|
||||
.args(["checkout", "main"])
|
||||
.current_dir(&clone_path)
|
||||
.output();
|
||||
|
||||
let pushed = try_push(&clone_path);
|
||||
cleanup(&clone_path);
|
||||
match pushed {
|
||||
Ok(true) => {}
|
||||
Ok(false) => panic!("git push rejected while promoting repo out of purgatory"),
|
||||
Err(e) => panic!("git push error while promoting repo: {}", e),
|
||||
}
|
||||
|
||||
// Give the relay a moment to promote the events out of purgatory.
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
assert!(
|
||||
client
|
||||
.is_event_on_relay(announcement.id)
|
||||
.await
|
||||
.expect("query announcement"),
|
||||
"announcement should be served after git data arrives"
|
||||
);
|
||||
|
||||
(announcement, repo_id)
|
||||
}
|
||||
|
||||
/// Query the relay for a served repo announcement using a real client's access
|
||||
/// pattern: author + kind + `d` identifier (a NIP-01 addressable-event query),
|
||||
/// rather than a by-id lookup. Returns whether the relay serves it.
|
||||
///
|
||||
/// Copied (inline) from `nip09_disrespector.rs`.
|
||||
async fn announcement_served_by_coordinate(
|
||||
client: &AuditClient,
|
||||
announcement: &Event,
|
||||
repo_id: &str,
|
||||
) -> bool {
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::GitRepoAnnouncement)
|
||||
.author(announcement.pubkey)
|
||||
.identifier(repo_id);
|
||||
|
||||
client
|
||||
.query(filter)
|
||||
.await
|
||||
.expect("query announcement by coordinate")
|
||||
.iter()
|
||||
.any(|e| e.id == announcement.id)
|
||||
}
|
||||
|
||||
/// Build a kind-5 deletion request signed by `client` carrying the supplied
|
||||
/// `e`-tag event ids and `a`-tag coordinate strings.
|
||||
///
|
||||
/// A more general form of the `nip09_disrespector.rs` `build_deletion` helper;
|
||||
/// kept inline here so this file is self-contained.
|
||||
fn build_deletion(client: &AuditClient, event_ids: &[EventId], coordinates: &[String]) -> Event {
|
||||
let mut builder = client.event_builder(Kind::EventDeletion, "delete");
|
||||
|
||||
for id in event_ids {
|
||||
builder = builder.tag(Tag::event(*id));
|
||||
}
|
||||
for coord in coordinates {
|
||||
builder = builder.tag(Tag::custom("a", vec![coord.clone()]));
|
||||
}
|
||||
|
||||
builder.build(client.keys()).expect("build deletion event")
|
||||
}
|
||||
|
||||
/// Coordinate string for a kind-30617 announcement: `30617:<pubkey-hex>:<d>`.
|
||||
fn announcement_coordinate(announcement: &Event, repo_id: &str) -> String {
|
||||
format!(
|
||||
"{}:{}:{}",
|
||||
Kind::GitRepoAnnouncement.as_u16(),
|
||||
announcement.pubkey.to_hex(),
|
||||
repo_id
|
||||
)
|
||||
}
|
||||
|
||||
/// 1. A deletion targeting an existing, author-owned event by `e` tag is
|
||||
/// accepted and the target is no longer served.
|
||||
#[tokio::test]
|
||||
@@ -897,3 +732,52 @@ async fn newer_addressable_event_survives_a_tag_deletion() {
|
||||
v2_id
|
||||
);
|
||||
}
|
||||
|
||||
/// 8. Baseline: a default (normal-mode) relay honours a kind-5 deletion that
|
||||
/// targets a promoted announcement by its `a` coordinate — the announcement
|
||||
/// is no longer served, neither by id nor by author+kind+identifier.
|
||||
///
|
||||
/// This is the deletion-honoured counterpart to the disrespector mode's
|
||||
/// "accept but ignore" behaviour (`disrespector_accepts_but_ignores_deletion`
|
||||
/// in `nip09_disrespector.rs`), kept here as a basic NIP-09 validation test.
|
||||
#[tokio::test]
|
||||
async fn normal_mode_honours_deletion() {
|
||||
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, "normal-honours").await;
|
||||
|
||||
let coordinate = announcement_coordinate(&announcement, &repo_id);
|
||||
let deletion = build_deletion(&client, &[announcement.id], &[coordinate]);
|
||||
|
||||
client
|
||||
.send_event(deletion)
|
||||
.await
|
||||
.expect("normal relay should OK the kind-5 deletion");
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
let served_by_id = client
|
||||
.is_event_on_relay(announcement.id)
|
||||
.await
|
||||
.expect("query announcement by id after deletion");
|
||||
let served_by_coordinate =
|
||||
announcement_served_by_coordinate(&client, &announcement, &repo_id).await;
|
||||
|
||||
relay.stop().await;
|
||||
|
||||
assert!(
|
||||
!served_by_id,
|
||||
"normal relay must honour the deletion: announcement {} should no longer be \
|
||||
queryable by id",
|
||||
announcement.id
|
||||
);
|
||||
assert!(
|
||||
!served_by_coordinate,
|
||||
"normal relay must honour the deletion: announcement {} should no longer be served \
|
||||
for an author+kind+identifier query",
|
||||
announcement.id
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user