9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
213 lines
7.3 KiB
Rust
213 lines
7.3 KiB
Rust
//! End-to-end tests for kind:30176 team events (NIP-AP).
|
|
//!
|
|
//! These tests verify the relay accepts and addresses team events the same way
|
|
//! it does personas:
|
|
//! - Accepts a valid team event and queries it back by NIP-33 coordinate.
|
|
//! - Enforces NIP-33 replacement semantics (same d-tag, newer timestamp wins).
|
|
//! - Honors a NIP-09 a-tag tombstone, removing the team coordinate.
|
|
//!
|
|
//! The team `d`-tag is the team's stable id (a slug-like string), not a 64-hex
|
|
//! pubkey — it exercises the generic parameterized-replaceable path that
|
|
//! enforces only `D_TAG_MAX_LEN`, distinct from the persona slug envelope.
|
|
//!
|
|
//! # Running
|
|
//!
|
|
//! Start the relay, then run:
|
|
//!
|
|
//! ```text
|
|
//! RELAY_URL=ws://localhost:3000 cargo test --test e2e_team -- --ignored
|
|
//! ```
|
|
|
|
use std::time::Duration;
|
|
|
|
use buzz_test_client::BuzzTestClient;
|
|
use nostr::{Alphabet, EventBuilder, Filter, Keys, Kind, SingleLetterTag, Tag, Timestamp};
|
|
|
|
const TEAM_KIND: u16 = 30176;
|
|
|
|
fn relay_url() -> String {
|
|
std::env::var("RELAY_URL").unwrap_or_else(|_| "ws://localhost:3000".to_string())
|
|
}
|
|
|
|
fn sub_id(name: &str) -> String {
|
|
format!("e2e-team-{name}-{}", uuid::Uuid::new_v4())
|
|
}
|
|
|
|
/// Build a team event whose `d`-tag is the team id, mirroring the desktop
|
|
/// `build_team_event` shape.
|
|
fn team_event(keys: &Keys, d_tag: &str, content: &str) -> nostr::Event {
|
|
EventBuilder::new(Kind::Custom(TEAM_KIND), content)
|
|
.tags(vec![Tag::parse(["d", d_tag]).unwrap()])
|
|
.sign_with_keys(keys)
|
|
.unwrap()
|
|
}
|
|
|
|
fn team_event_at(keys: &Keys, d_tag: &str, content: &str, created_at: u64) -> nostr::Event {
|
|
EventBuilder::new(Kind::Custom(TEAM_KIND), content)
|
|
.tags(vec![Tag::parse(["d", d_tag]).unwrap()])
|
|
.custom_created_at(Timestamp::from(created_at))
|
|
.sign_with_keys(keys)
|
|
.unwrap()
|
|
}
|
|
|
|
/// Build a NIP-09 a-tag-only deletion at the team's NIP-33 coordinate,
|
|
/// mirroring the desktop `build_team_delete` shape (no `e`-tag, so the relay
|
|
/// takes the coordinate-delete path rather than the event-id path).
|
|
fn team_delete_event(keys: &Keys, d_tag: &str) -> nostr::Event {
|
|
let coord = format!("{TEAM_KIND}:{}:{d_tag}", keys.public_key().to_hex());
|
|
EventBuilder::new(Kind::Custom(5), "")
|
|
.tags(vec![Tag::parse(["a", coord.as_str()]).unwrap()])
|
|
.sign_with_keys(keys)
|
|
.unwrap()
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore]
|
|
async fn test_team_publish_and_query() {
|
|
let url = relay_url();
|
|
let keys = Keys::generate();
|
|
let d_tag = format!("team-{}", &uuid::Uuid::new_v4().to_string()[..8]);
|
|
|
|
let content = serde_json::json!({
|
|
"name": "Test Team",
|
|
"description": "A test team for E2E validation",
|
|
"persona_ids": ["p1", "p2"]
|
|
})
|
|
.to_string();
|
|
|
|
let mut client = BuzzTestClient::connect(&url, &keys).await.expect("connect");
|
|
|
|
let event = team_event(&keys, &d_tag, &content);
|
|
let ok = client.send_event(event).await.expect("send team");
|
|
assert!(ok.accepted, "relay rejected team event: {}", ok.message);
|
|
|
|
let sid = sub_id("query");
|
|
let filter = Filter::new()
|
|
.kind(Kind::Custom(TEAM_KIND))
|
|
.author(keys.public_key())
|
|
.custom_tags(SingleLetterTag::lowercase(Alphabet::D), [d_tag.as_str()]);
|
|
|
|
client
|
|
.subscribe(&sid, vec![filter])
|
|
.await
|
|
.expect("subscribe");
|
|
|
|
let events = client
|
|
.collect_until_eose(&sid, Duration::from_secs(5))
|
|
.await
|
|
.expect("collect events");
|
|
|
|
assert_eq!(events.len(), 1, "expected exactly one team event");
|
|
let ev = &events[0];
|
|
assert_eq!(ev.content, content);
|
|
assert_eq!(ev.pubkey, keys.public_key());
|
|
assert_eq!(ev.kind, Kind::Custom(TEAM_KIND));
|
|
|
|
client.disconnect().await.expect("disconnect");
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore]
|
|
async fn test_team_nip33_replacement_newer_wins() {
|
|
let url = relay_url();
|
|
let keys = Keys::generate();
|
|
let d_tag = format!("team-replace-{}", &uuid::Uuid::new_v4().to_string()[..8]);
|
|
|
|
let mut client = BuzzTestClient::connect(&url, &keys).await.expect("connect");
|
|
|
|
let now = Timestamp::now().as_secs();
|
|
let old_content = r#"{"name":"Old Team","persona_ids":["p1"]}"#;
|
|
let old_event = team_event_at(&keys, &d_tag, old_content, now - 100);
|
|
let ok = client.send_event(old_event).await.expect("send old");
|
|
assert!(ok.accepted, "relay rejected old event: {}", ok.message);
|
|
|
|
let new_content = r#"{"name":"New Team","persona_ids":["p1","p2"]}"#;
|
|
let new_event = team_event_at(&keys, &d_tag, new_content, now);
|
|
let ok = client.send_event(new_event).await.expect("send new");
|
|
assert!(ok.accepted, "relay rejected new event: {}", ok.message);
|
|
|
|
let sid = sub_id("replace");
|
|
let filter = Filter::new()
|
|
.kind(Kind::Custom(TEAM_KIND))
|
|
.author(keys.public_key())
|
|
.custom_tags(SingleLetterTag::lowercase(Alphabet::D), [d_tag.as_str()]);
|
|
|
|
client
|
|
.subscribe(&sid, vec![filter])
|
|
.await
|
|
.expect("subscribe");
|
|
|
|
let events = client
|
|
.collect_until_eose(&sid, Duration::from_secs(5))
|
|
.await
|
|
.expect("collect");
|
|
|
|
assert_eq!(events.len(), 1, "NIP-33: only newest event should remain");
|
|
assert_eq!(
|
|
events[0].content, new_content,
|
|
"should be the newer version"
|
|
);
|
|
|
|
client.disconnect().await.expect("disconnect");
|
|
}
|
|
|
|
/// The a-tag tombstone is the only state-destroying op in the team flow: it
|
|
/// removes the team for every client and across reboots. This proves the relay
|
|
/// acts on it — publish a team, confirm it is live, publish the a-tag-only
|
|
/// tombstone at its coordinate, then assert the query returns it gone.
|
|
#[tokio::test]
|
|
#[ignore]
|
|
async fn test_team_tombstone_deletes_coordinate() {
|
|
let url = relay_url();
|
|
let keys = Keys::generate();
|
|
let d_tag = format!("team-tombstone-{}", &uuid::Uuid::new_v4().to_string()[..8]);
|
|
|
|
let content = r#"{"name":"Doomed Team","persona_ids":["p1"]}"#;
|
|
|
|
let mut client = BuzzTestClient::connect(&url, &keys).await.expect("connect");
|
|
|
|
let event = team_event(&keys, &d_tag, content);
|
|
let ok = client.send_event(event).await.expect("send team");
|
|
assert!(ok.accepted, "relay rejected team event: {}", ok.message);
|
|
|
|
let filter = || {
|
|
Filter::new()
|
|
.kind(Kind::Custom(TEAM_KIND))
|
|
.author(keys.public_key())
|
|
.custom_tags(SingleLetterTag::lowercase(Alphabet::D), [d_tag.as_str()])
|
|
};
|
|
|
|
let sid = sub_id("tombstone-pre");
|
|
client
|
|
.subscribe(&sid, vec![filter()])
|
|
.await
|
|
.expect("subscribe pre");
|
|
let before = client
|
|
.collect_until_eose(&sid, Duration::from_secs(5))
|
|
.await
|
|
.expect("collect pre");
|
|
assert_eq!(before.len(), 1, "team should be live before deletion");
|
|
|
|
let tombstone = team_delete_event(&keys, &d_tag);
|
|
let ok = client.send_event(tombstone).await.expect("send tombstone");
|
|
assert!(ok.accepted, "relay rejected tombstone: {}", ok.message);
|
|
|
|
let sid = sub_id("tombstone-post");
|
|
client
|
|
.subscribe(&sid, vec![filter()])
|
|
.await
|
|
.expect("subscribe post");
|
|
let after = client
|
|
.collect_until_eose(&sid, Duration::from_secs(5))
|
|
.await
|
|
.expect("collect post");
|
|
assert_eq!(
|
|
after.len(),
|
|
0,
|
|
"tombstone should remove the team coordinate, got {} event(s)",
|
|
after.len()
|
|
);
|
|
|
|
client.disconnect().await.expect("disconnect");
|
|
}
|