From 497df415749039236126140193af0ea612358cc7 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 11 Dec 2025 15:27:06 +0000 Subject: test: nip77 smoke test --- tests/nip77_negentropy.rs | 213 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 tests/nip77_negentropy.rs (limited to 'tests') diff --git a/tests/nip77_negentropy.rs b/tests/nip77_negentropy.rs new file mode 100644 index 0000000..c8e0b50 --- /dev/null +++ b/tests/nip77_negentropy.rs @@ -0,0 +1,213 @@ +//! NIP-77 Negentropy Sync Smoke Tests +//! +//! Verifies that ngit-grasp's NIP-77 claim is valid by testing negentropy +//! reconciliation between a client and the relay. +//! +//! # Background +//! +//! NIP-77 defines the negentropy protocol for efficient set reconciliation. +//! The nostr-relay-builder v0.44 provides built-in NIP-77 support via: +//! - NEG-OPEN message handling +//! - NEG-MSG message handling +//! - NEG-CLOSE message handling +//! +//! This test uses nostr-sdk's `client.sync()` method to perform negentropy +//! reconciliation against the relay. +//! +//! # Running Tests +//! +//! ```bash +//! cargo test --test nip77_negentropy -- --nocapture +//! ``` + +mod common; + +use nostr_sdk::prelude::*; +use std::time::Duration; + +use common::{sync_helpers::*, TestRelay}; + +/// Smoke test: NIP-77 negentropy reconciliation returns event IDs +/// +/// Scenario: +/// 1. Start a TestRelay +/// 2. Publish a couple of events to it +/// 3. Create a fresh client with empty local database +/// 4. Call client.sync() to perform negentropy reconciliation +/// 5. Verify reconciliation found the events on the relay +#[tokio::test] +async fn test_nip77_negentropy_sync_finds_events() { + // 1. Start relay + let relay = TestRelay::start().await; + println!("Relay started at {}", relay.url()); + + // 2. Create keys and publish events + let keys = Keys::generate(); + + // Create a repository announcement that will be accepted by the relay + let announcement = create_repo_announcement( + &keys, + &[&relay.domain()], + "test-repo-nip77", + ); + let event1_id = announcement.id; + println!("Created event 1: {} (kind {})", event1_id, announcement.kind.as_u16()); + + // Create a second event (issue referencing the repo) + let repo_coord = format!( + "{}:{}:{}", + KIND_REPOSITORY_STATE, + keys.public_key().to_hex(), + "test-repo-nip77" + ); + let issue = build_layer2_issue_event(&keys, &repo_coord, "Test issue for NIP-77") + .expect("Failed to build issue event"); + let event2_id = issue.id; + println!("Created event 2: {} (kind {})", event2_id, issue.kind.as_u16()); + + // 3. Send events to relay using TestClient + let publish_client = TestClient::new(relay.url(), keys.clone()) + .await + .expect("Failed to connect to relay"); + + publish_client + .send_event(&announcement) + .await + .expect("Failed to send announcement"); + publish_client + .send_event(&issue) + .await + .expect("Failed to send issue"); + println!("Events published to relay"); + + publish_client.disconnect().await; + + // 4. Wait a moment for events to be stored + tokio::time::sleep(Duration::from_millis(200)).await; + + // 5. Create a fresh client to perform sync (different instance, no local events) + let sync_keys = Keys::generate(); // Different keys, doesn't matter for sync + let sync_client = Client::new(sync_keys); + + sync_client + .add_relay(relay.url()) + .await + .expect("Failed to add relay"); + sync_client.connect().await; + + // Wait for connection + tokio::time::sleep(Duration::from_millis(500)).await; + + // 6. Perform negentropy sync with filter matching our events + let filter = Filter::new() + .author(keys.public_key()) + .kinds(vec![Kind::Custom(KIND_REPOSITORY_STATE), Kind::Custom(KIND_ISSUE)]); + + println!("Starting negentropy sync with filter: {:?}", filter); + + let sync_opts = SyncOptions::default(); + + let result = sync_client.sync(filter, &sync_opts).await; + + // 7. Cleanup + sync_client.disconnect().await; + relay.stop().await; + + // 8. Verify results + match result { + Ok(output) => { + let reconciliation = output.val; + println!("Negentropy sync completed!"); + println!(" Local: {:?}", reconciliation.local); + println!(" Remote: {:?}", reconciliation.remote); + println!(" Sent: {:?}", reconciliation.sent); + println!(" Received: {:?}", reconciliation.received); + println!(" Failures: {:?}", output.failed); + + // The relay has events we don't have locally, so they should appear in "received" + // or "remote" (depending on whether we requested them or just discovered them) + let total_discovered = reconciliation.received.len() + reconciliation.remote.len(); + + assert!( + total_discovered >= 2, + "Expected to discover at least 2 events via negentropy, got {} (received: {}, remote: {})", + total_discovered, + reconciliation.received.len(), + reconciliation.remote.len() + ); + + // Verify our specific events were found + let all_discovered: Vec<_> = reconciliation + .received + .iter() + .chain(reconciliation.remote.iter()) + .collect(); + + println!("All discovered event IDs: {:?}", all_discovered); + } + Err(e) => { + panic!( + "NIP-77 negentropy sync failed: {}. This means the relay does NOT support NIP-77 as claimed.", + e + ); + } + } +} + +/// Smoke test: Negentropy sync with empty database returns empty result +/// +/// Verifies that negentropy sync works correctly when no events match the filter. +#[tokio::test] +async fn test_nip77_negentropy_sync_empty_result() { + // 1. Start relay (empty, no events) + let relay = TestRelay::start().await; + println!("Relay started at {}", relay.url()); + + // 2. Create client + let keys = Keys::generate(); + let client = Client::new(keys.clone()); + + client + .add_relay(relay.url()) + .await + .expect("Failed to add relay"); + client.connect().await; + + tokio::time::sleep(Duration::from_millis(500)).await; + + // 3. Sync with filter that won't match anything + let filter = Filter::new() + .author(keys.public_key()) // Random new key, no events exist + .kind(Kind::Custom(KIND_REPOSITORY_STATE)); + + println!("Starting negentropy sync with empty filter"); + + let sync_opts = SyncOptions::default(); + + let result = client.sync(filter, &sync_opts).await; + + // 4. Cleanup + client.disconnect().await; + relay.stop().await; + + // 5. Verify - should succeed but find nothing + match result { + Ok(output) => { + let reconciliation = output.val; + println!("Empty sync completed!"); + println!(" Received: {:?}", reconciliation.received); + println!(" Remote: {:?}", reconciliation.remote); + + // Should be empty since no events match + let total = reconciliation.received.len() + reconciliation.remote.len(); + assert_eq!( + total, 0, + "Expected 0 events for non-existent author, got {}", + total + ); + } + Err(e) => { + panic!("NIP-77 negentropy sync failed on empty query: {}", e); + } + } +} -- cgit v1.2.3