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>
274 lines
8.9 KiB
Rust
274 lines
8.9 KiB
Rust
//! End-to-end tests for NIP-38 user statuses (kind:30315).
|
|
//!
|
|
//! These tests require a running relay instance. By default they are marked
|
|
//! `#[ignore]` so that `cargo test` does not fail in CI when the relay is not
|
|
//! available.
|
|
//!
|
|
//! # Running
|
|
//!
|
|
//! Start the relay, then run:
|
|
//!
|
|
//! ```text
|
|
//! cargo test --test e2e_user_status -- --ignored
|
|
//! ```
|
|
//!
|
|
//! Override the relay URL with the `RELAY_URL` environment variable:
|
|
//!
|
|
//! ```text
|
|
//! RELAY_URL=ws://relay.example.com cargo test --test e2e_user_status -- --ignored
|
|
//! ```
|
|
|
|
use std::time::Duration;
|
|
|
|
use buzz_test_client::BuzzTestClient;
|
|
use nostr::{Alphabet, EventBuilder, Filter, Keys, Kind, SingleLetterTag, Tag, Timestamp};
|
|
|
|
const KIND_USER_STATUS: u16 = 30315;
|
|
|
|
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-{name}-{}", uuid::Uuid::new_v4())
|
|
}
|
|
|
|
/// Build a kind:30315 event with a d-tag and content.
|
|
fn build_user_status_event(
|
|
keys: &Keys,
|
|
d_tag: &str,
|
|
content: &str,
|
|
extra_tags: Vec<Tag>,
|
|
) -> nostr::Event {
|
|
let mut tags = vec![Tag::parse(["d", d_tag]).unwrap()];
|
|
tags.extend(extra_tags);
|
|
EventBuilder::new(Kind::Custom(KIND_USER_STATUS), content)
|
|
.tags(tags)
|
|
.sign_with_keys(keys)
|
|
.unwrap()
|
|
}
|
|
|
|
/// kind:30315 events are accepted by the relay.
|
|
#[tokio::test]
|
|
#[ignore]
|
|
async fn test_user_status_accepted() {
|
|
let url = relay_url();
|
|
let keys = Keys::generate();
|
|
let mut client = BuzzTestClient::connect(&url, &keys).await.expect("connect");
|
|
|
|
let event = build_user_status_event(&keys, "general", "Working on NIP-38 support", vec![]);
|
|
|
|
let ok = client.send_event(event).await.expect("send event");
|
|
assert!(
|
|
ok.accepted,
|
|
"relay should accept kind:30315: {}",
|
|
ok.message
|
|
);
|
|
|
|
client.disconnect().await.expect("disconnect");
|
|
}
|
|
|
|
/// kind:30315 events are retrievable via REQ with kinds filter.
|
|
#[tokio::test]
|
|
#[ignore]
|
|
async fn test_user_status_retrievable() {
|
|
let url = relay_url();
|
|
let keys = Keys::generate();
|
|
let mut client = BuzzTestClient::connect(&url, &keys).await.expect("connect");
|
|
|
|
let d_tag = format!("retrieve-{}", uuid::Uuid::new_v4().simple());
|
|
let event = build_user_status_event(&keys, &d_tag, "Currently online", vec![]);
|
|
let event_id = event.id;
|
|
|
|
let ok = client.send_event(event).await.expect("send event");
|
|
assert!(ok.accepted, "relay should accept: {}", ok.message);
|
|
|
|
// Query back by kind + author
|
|
let sid = sub_id("retrieve");
|
|
let filter = Filter::new()
|
|
.kind(Kind::Custom(KIND_USER_STATUS))
|
|
.author(keys.public_key());
|
|
client
|
|
.subscribe(&sid, vec![filter])
|
|
.await
|
|
.expect("subscribe");
|
|
|
|
let events = client
|
|
.collect_until_eose(&sid, Duration::from_secs(5))
|
|
.await
|
|
.expect("collect");
|
|
|
|
assert!(
|
|
events.iter().any(|e| e.id == event_id),
|
|
"should find the published user status in query results"
|
|
);
|
|
|
|
client.disconnect().await.expect("disconnect");
|
|
}
|
|
|
|
/// NIP-33 replacement: publishing a newer kind:30315 with the same d-tag replaces the old one.
|
|
#[tokio::test]
|
|
#[ignore]
|
|
async fn test_user_status_nip33_replacement() {
|
|
let url = relay_url();
|
|
let keys = Keys::generate();
|
|
let mut client = BuzzTestClient::connect(&url, &keys).await.expect("connect");
|
|
|
|
let d_tag = format!("replace-{}", uuid::Uuid::new_v4().simple());
|
|
|
|
// Publish v1
|
|
let v1 = build_user_status_event(&keys, &d_tag, "Status v1", vec![]);
|
|
let ok1 = client.send_event(v1).await.expect("send v1");
|
|
assert!(ok1.accepted, "v1 should be accepted: {}", ok1.message);
|
|
|
|
// Small delay to ensure different created_at timestamps
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
|
|
// Publish v2 with the same d-tag
|
|
let v2 = build_user_status_event(&keys, &d_tag, "Status v2 — updated", vec![]);
|
|
let v2_id = v2.id;
|
|
let ok2 = client.send_event(v2).await.expect("send v2");
|
|
assert!(ok2.accepted, "v2 should be accepted: {}", ok2.message);
|
|
|
|
// Query — should only get v2 (v1 replaced)
|
|
let sid = sub_id("replace");
|
|
let filter = Filter::new()
|
|
.kind(Kind::Custom(KIND_USER_STATUS))
|
|
.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,
|
|
"should have exactly one event after replacement"
|
|
);
|
|
assert_eq!(events[0].id, v2_id, "surviving event should be v2");
|
|
assert!(events[0].content.contains("v2"), "content should be v2");
|
|
|
|
client.disconnect().await.expect("disconnect");
|
|
}
|
|
|
|
/// NIP-38: multiple d-tags coexist — "general" and "music" are independent status slots.
|
|
#[tokio::test]
|
|
#[ignore]
|
|
async fn test_user_status_multiple_d_tags_coexist() {
|
|
let url = relay_url();
|
|
let keys = Keys::generate();
|
|
let mut client = BuzzTestClient::connect(&url, &keys).await.expect("connect");
|
|
|
|
let general_d = format!("general-{}", uuid::Uuid::new_v4().simple());
|
|
let music_d = format!("music-{}", uuid::Uuid::new_v4().simple());
|
|
|
|
// Publish general status
|
|
let general = build_user_status_event(&keys, &general_d, "Working on code", vec![]);
|
|
let general_id = general.id;
|
|
let ok1 = client.send_event(general).await.expect("send general");
|
|
assert!(ok1.accepted, "general should be accepted: {}", ok1.message);
|
|
|
|
// Publish music status
|
|
let music = build_user_status_event(&keys, &music_d, "Listening to jazz", vec![]);
|
|
let music_id = music.id;
|
|
let ok2 = client.send_event(music).await.expect("send music");
|
|
assert!(ok2.accepted, "music should be accepted: {}", ok2.message);
|
|
|
|
// Query by kind + author — both should be returned
|
|
let sid = sub_id("multi-dtag");
|
|
let filter = Filter::new()
|
|
.kind(Kind::Custom(KIND_USER_STATUS))
|
|
.author(keys.public_key());
|
|
client
|
|
.subscribe(&sid, vec![filter])
|
|
.await
|
|
.expect("subscribe");
|
|
|
|
let events = client
|
|
.collect_until_eose(&sid, Duration::from_secs(5))
|
|
.await
|
|
.expect("collect");
|
|
|
|
assert!(
|
|
events.iter().any(|e| e.id == general_id),
|
|
"general status should be present"
|
|
);
|
|
assert!(
|
|
events.iter().any(|e| e.id == music_id),
|
|
"music status should be present"
|
|
);
|
|
|
|
client.disconnect().await.expect("disconnect");
|
|
}
|
|
|
|
/// NIP-33 stale-write protection: an older event cannot replace a newer one.
|
|
#[tokio::test]
|
|
#[ignore]
|
|
async fn test_user_status_stale_write_rejected() {
|
|
let url = relay_url();
|
|
let keys = Keys::generate();
|
|
let mut client = BuzzTestClient::connect(&url, &keys).await.expect("connect");
|
|
|
|
let d_tag = format!("stale-{}", uuid::Uuid::new_v4().simple());
|
|
|
|
// Publish the "newer" event first (with a future-ish timestamp)
|
|
let newer = {
|
|
let tags = vec![Tag::parse(["d", &d_tag]).unwrap()];
|
|
EventBuilder::new(Kind::Custom(KIND_USER_STATUS), "Newer status")
|
|
.tags(tags)
|
|
.custom_created_at(Timestamp::from(nostr::Timestamp::now().as_secs() + 100))
|
|
.sign_with_keys(&keys)
|
|
.unwrap()
|
|
};
|
|
let newer_id = newer.id;
|
|
let ok1 = client.send_event(newer).await.expect("send newer");
|
|
assert!(ok1.accepted, "newer should be accepted: {}", ok1.message);
|
|
|
|
// Now try to publish an "older" event with the same d-tag but earlier timestamp
|
|
let older = {
|
|
let tags = vec![Tag::parse(["d", &d_tag]).unwrap()];
|
|
EventBuilder::new(Kind::Custom(KIND_USER_STATUS), "Older status")
|
|
.tags(tags)
|
|
.custom_created_at(Timestamp::from(nostr::Timestamp::now().as_secs() - 100))
|
|
.sign_with_keys(&keys)
|
|
.unwrap()
|
|
};
|
|
let _ok2 = client.send_event(older).await.expect("send older");
|
|
// Stale write may be rejected or accepted-as-duplicate — either way,
|
|
// the older event must NOT replace the newer one.
|
|
|
|
// Query — should still have the newer event
|
|
let sid = sub_id("stale");
|
|
let filter = Filter::new()
|
|
.kind(Kind::Custom(KIND_USER_STATUS))
|
|
.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, "should have exactly one event");
|
|
assert_eq!(
|
|
events[0].id, newer_id,
|
|
"surviving event should be the newer one"
|
|
);
|
|
assert!(
|
|
events[0].content.contains("Newer"),
|
|
"content should be from the newer event"
|
|
);
|
|
|
|
client.disconnect().await.expect("disconnect");
|
|
}
|