From b7aa7b72c189290b45fb388ec1826862bc8dda49 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 5 Dec 2025 14:51:16 +0000 Subject: test: fix proactive sync basic tests --- tests/proactive_sync_basic.rs | 360 ++++++++++++++++-------------------------- 1 file changed, 135 insertions(+), 225 deletions(-) (limited to 'tests/proactive_sync_basic.rs') diff --git a/tests/proactive_sync_basic.rs b/tests/proactive_sync_basic.rs index 9af5a2d..b789cb6 100644 --- a/tests/proactive_sync_basic.rs +++ b/tests/proactive_sync_basic.rs @@ -21,6 +21,86 @@ use nostr_sdk::prelude::*; /// Kind 30617 - Repository State (NIP-34) const KIND_REPOSITORY_STATE: u16 = 30617; +/// Result of checking if an event syncs between relays +#[derive(Debug)] +struct SyncCheckResult { + /// Whether the event was successfully stored on the source relay + stored_on_source: bool, + /// Whether the event was synced to the target relay + synced_to_target: bool, +} + +/// Helper to check if an event syncs from source relay to target relay +/// +/// This function: +/// 1. Sends the event to the source relay +/// 2. Verifies if it was stored on the source relay +/// 3. Waits for potential sync +/// 4. Checks if the event appears on the target relay +/// +/// Note: The sync subscription must already be established before calling this. +async fn check_event_syncs( + source_relay: &TestRelay, + target_relay: &TestRelay, + event: &Event, + keys: &Keys, +) -> SyncCheckResult { + let event_id = event.id; + + // Create client and connect to source relay + let client_source = create_connected_client(source_relay.url(), keys.clone()) + .await + .expect("Failed to connect to source relay"); + + // Send event to source relay + let send_result = send_event_reliably(&client_source, event).await; + let stored_on_source = send_result.is_ok(); + + if stored_on_source { + println!("Event {} stored on source relay", event_id); + } else { + println!( + "Event {} NOT stored on source relay: {:?}", + event_id, + send_result.err() + ); + } + + // Wait for sync to occur + tokio::time::sleep(Duration::from_secs(1)).await; + + // Check if event exists on target relay + let client_target = create_connected_client(target_relay.url(), Keys::generate()) + .await + .expect("Failed to connect to target relay"); + + let filter = Filter::new() + .kind(event.kind) + .author(keys.public_key()); + + let events_on_target = client_target + .fetch_events(filter, Duration::from_secs(3)) + .await + .expect("Failed to fetch from target relay"); + + let synced_to_target = events_on_target.iter().any(|e| e.id == event_id); + + if synced_to_target { + println!("Event {} found on target relay (synced)", event_id); + } else { + println!("Event {} NOT found on target relay", event_id); + } + + // Clean up + client_source.disconnect().await; + client_target.disconnect().await; + + SyncCheckResult { + stored_on_source, + synced_to_target, + } +} + /// Create a client with keys, connect to relay, and wait for connection async fn create_connected_client(relay_url: &str, keys: Keys) -> Result { let client = Client::new(keys); @@ -72,31 +152,10 @@ async fn send_event_reliably(client: &Client, event: &Event) -> Result Event { - // Build tags for repository announcement using custom tag kinds (as grasp-audit does) - let tags = vec![ - Tag::identifier(identifier), - Tag::custom( - TagKind::custom("clone"), - vec![format!("http://{}/{}.git", domain, identifier)], - ), - Tag::custom(TagKind::custom("relays"), vec![format!("ws://{}", domain)]), - ]; - - EventBuilder::new(Kind::Custom(KIND_REPOSITORY_STATE), "Repository state") - .tags(tags) - .sign_with_keys(keys) - .expect("Failed to sign event") -} - -/// Create a valid repository announcement event listing multiple relays -/// -/// This creates a kind 30617 event with clone/relays tags referencing multiple domains, -/// which is necessary for sync tests where the event needs to be accepted by both relays. -/// Uses TagKind::custom("clone") and TagKind::custom("relays") to match grasp-audit patterns. -fn create_shared_repo_announcement(keys: &Keys, domains: &[&str], identifier: &str) -> Event { +fn create_repo_announcement(keys: &Keys, domains: &[&str], identifier: &str) -> Event { // Build clone URLs for all domains (with .git suffix) let clone_urls: Vec = domains .iter() @@ -157,25 +216,17 @@ async fn test_valid_event_syncs_to_relay() { relay_b.domain() ); - // Create test keys that will be used for both client and event signing + // Create test keys let keys = Keys::generate(); // Wait for relay_b's sync connection to establish - // With NGIT_SYNC_STARTUP_JITTER_MS=0 (set by TestRelay), sync connects immediately. - // A brief wait allows the WebSocket connection and Layer 1 subscription to be set up. println!("Waiting 1s for relay_b sync connection to establish..."); tokio::time::sleep(Duration::from_secs(1)).await; - // Create a client with our keys and connect to relay_a - let client_a = create_connected_client(relay_a.url(), keys.clone()) - .await - .expect("Failed to connect to relay_a"); - println!("client_a connected to relay_a"); - // Create a repository announcement that lists BOTH relays // This is required for sync - the event must reference both the source relay // and the syncing relay for the write policy to accept it on both sides - let event = create_shared_repo_announcement( + let event = create_repo_announcement( &keys, &[&relay_a.domain(), &relay_b.domain()], "test-repo", @@ -188,226 +239,85 @@ async fn test_valid_event_syncs_to_relay() { println!(" Tag: {:?}", tag.as_slice()); } - // Submit event to relay_a AFTER relay_b's subscription is established - // This ensures the event is received via the live subscription - println!("Sending event to relay_a..."); - send_event_reliably(&client_a, &event) - .await - .expect("Failed to send event to relay_a"); - println!("Event sent successfully"); - - // Verify event is stored on relay_a first - let filter_a = Filter::new() - .kind(Kind::Custom(KIND_REPOSITORY_STATE)) - .author(keys.public_key()); - - let events_on_a = client_a - .fetch_events(filter_a.clone(), Duration::from_secs(5)) - .await - .expect("Failed to fetch events from relay_a"); + // Use helper to send and check sync + let result = check_event_syncs(&relay_a, &relay_b, &event, &keys).await; - println!( - "Events on relay_a: {} (looking for {})", - events_on_a.len(), - event_id - ); - for e in events_on_a.iter() { - println!(" Found event: {} (kind {})", e.id, e.kind.as_u16()); - } + // Clean up + relay_b.stop().await; + relay_a.stop().await; - let found_on_a = events_on_a.iter().any(|e| e.id == event_id); assert!( - found_on_a, + result.stored_on_source, "Event {} was not stored on relay_a! This is a prerequisite for sync.", event_id ); - println!("✓ Event confirmed on relay_a"); - - // Wait for sync to occur (event processing and storage) - println!("Waiting 1s for sync to occur..."); - tokio::time::sleep(Duration::from_secs(1)).await; - - // Query relay_b to verify the event was synced - let client_b = create_connected_client(relay_b.url(), Keys::generate()) - .await - .expect("Failed to connect to relay_b"); - - // Create filter to find our event - let filter_b = Filter::new() - .kind(Kind::Custom(KIND_REPOSITORY_STATE)) - .author(keys.public_key()); - - let events_on_b = client_b - .fetch_events(filter_b, Duration::from_secs(5)) - .await - .expect("Failed to fetch events from relay_b"); - - println!( - "Events on relay_b: {} (looking for {})", - events_on_b.len(), - event_id - ); - for e in events_on_b.iter() { - println!(" Found event: {} (kind {})", e.id, e.kind.as_u16()); - } - - // Check if our event was synced - let found_on_b = events_on_b.iter().any(|e| e.id == event_id); - - // Clean up - client_a.disconnect().await; - client_b.disconnect().await; - relay_b.stop().await; - relay_a.stop().await; - assert!( - found_on_b, - "Event {} was not synced to relay_b. Found {} events on relay_b", - event_id, - events_on_b.len() + result.synced_to_target, + "Event {} was not synced to relay_b", + event_id ); } -/// Test that invalid events are rejected by syncing relay validation +/// Test that events not listing relay_b in their relays tag are NOT synced +/// +/// This verifies that relay_b's write policy correctly rejects events during sync +/// if they don't list relay_b as one of their relays. #[tokio::test] -async fn test_invalid_event_rejected_by_sync_validation() { - // Start source relay (relay_a) - this is a simple relay without GRASP validation - // For this test, we'll use a second ngit-grasp relay, but the key insight is that - // the syncing relay should reject events that don't pass its own validation - +async fn test_event_not_listing_target_relay_is_not_synced() { + // Start source relay (relay_a) let relay_a = TestRelay::start().await; - let relay_b = TestRelay::start_with_sync(relay_a.url()).await; + println!( + "relay_a started at {} (domain: {})", + relay_a.url(), + relay_a.domain() + ); - // Give time for connection - tokio::time::sleep(Duration::from_millis(500)).await; + // Start syncing relay (relay_b) configured to sync from relay_a + let relay_b = TestRelay::start_with_sync(relay_a.url()).await; + println!( + "relay_b started at {} (domain: {})", + relay_b.url(), + relay_b.domain() + ); // Create test keys let keys = Keys::generate(); - // Create an INVALID repository announcement (missing clone tag) - let tags = vec![ - Tag::identifier("test-invalid-repo"), - // Missing required "clone" tag! - Tag::custom( - TagKind::custom("relays"), - vec![format!("ws://{}", relay_a.domain())], - ), - ]; - - let invalid_event = EventBuilder::new(Kind::Custom(KIND_REPOSITORY_STATE), "Invalid repo") - .tags(tags) - .sign_with_keys(&keys) - .expect("Failed to sign event"); - - let invalid_event_id = invalid_event.id; - - // Submit invalid event to relay_a - // Note: relay_a will also reject it due to GRASP validation - let client_a = Client::default(); - client_a - .add_relay(relay_a.url()) - .await - .expect("Failed to add relay_a"); - client_a.connect().await; - - // This will likely fail since relay_a also validates, but let's try - let _ = client_a.send_event(&invalid_event).await; - - // Wait for potential sync + // Wait for relay_b's sync connection to establish + println!("Waiting 1s for relay_b sync connection to establish..."); tokio::time::sleep(Duration::from_secs(1)).await; - // Query relay_b - the event should NOT be present - let client_b = Client::default(); - client_b - .add_relay(relay_b.url()) - .await - .expect("Failed to add relay_b"); - client_b.connect().await; - - let filter = Filter::new() - .kind(Kind::Custom(KIND_REPOSITORY_STATE)) - .author(keys.public_key()); + // Create a repository announcement that lists ONLY relay_a (NOT relay_b) + // This event is valid and will be accepted by relay_a, but should be + // rejected by relay_b's write policy during sync + let event = create_repo_announcement(&keys, &[&relay_a.domain()], "test-repo-only-a"); + let event_id = event.id; - let events = client_b - .fetch_events(filter, Duration::from_secs(3)) - .await - .expect("Failed to fetch events from relay_b"); + // Print event details for debugging + println!("Created event {} (kind {})", event_id, event.kind.as_u16()); + for tag in event.tags.iter() { + println!(" Tag: {:?}", tag.as_slice()); + } + println!("Note: This event only lists relay_a, not relay_b"); - let found = events.iter().any(|e| e.id == invalid_event_id); + // Use helper to send and check sync + let result = check_event_syncs(&relay_a, &relay_b, &event, &keys).await; // Clean up - client_a.disconnect().await; - client_b.disconnect().await; relay_b.stop().await; relay_a.stop().await; + // Event should be stored on relay_a (it lists relay_a) assert!( - !found, - "Invalid event {} should NOT have been synced to relay_b", - invalid_event_id + result.stored_on_source, + "Event {} should have been stored on relay_a (it lists relay_a)", + event_id ); -} - -/// Test that syncing relay maintains its own validation policy -#[tokio::test] -async fn test_sync_respects_local_validation() { - // This test verifies that synced events go through the local Nip34WritePolicy - // by testing that orphan events (events referencing non-existent repos) are rejected - - let relay_a = TestRelay::start().await; - let relay_b = TestRelay::start_with_sync(relay_a.url()).await; - - tokio::time::sleep(Duration::from_millis(500)).await; - - let keys = Keys::generate(); - - // First, create a VALID repository announcement and submit it - let valid_event = create_valid_repo_announcement(&keys, &relay_a.domain(), "valid-repo"); - let valid_event_id = valid_event.id; - - let client_a = Client::default(); - client_a - .add_relay(relay_a.url()) - .await - .expect("Failed to add relay_a"); - client_a.connect().await; - - client_a - .send_event(&valid_event) - .await - .expect("Failed to send valid event"); - - // Wait for sync - tokio::time::sleep(Duration::from_secs(2)).await; - - // Query relay_b to verify the valid event was synced - let client_b = Client::default(); - client_b - .add_relay(relay_b.url()) - .await - .expect("Failed to add relay_b"); - client_b.connect().await; - - let filter = Filter::new() - .kind(Kind::Custom(KIND_REPOSITORY_STATE)) - .author(keys.public_key()); - - let events = client_b - .fetch_events(filter, Duration::from_secs(5)) - .await - .expect("Failed to fetch events from relay_b"); - - let found = events.iter().any(|e| e.id == valid_event_id); - - // Clean up - client_a.disconnect().await; - client_b.disconnect().await; - relay_b.stop().await; - relay_a.stop().await; + // Event should NOT be synced to relay_b (it doesn't list relay_b) assert!( - found, - "Valid event {} should have been synced to relay_b", - valid_event_id + !result.synced_to_target, + "Event {} should NOT have been synced to relay_b (it doesn't list relay_b)", + event_id ); } -- cgit v1.2.3