//! Discovery Sync Tests //! //! Tests for relay discovery from announcement events. //! When a relay receives an announcement listing another relay, //! it should discover and connect to that relay to sync events. //! //! # Tests //! - Test 2: Direct Layer 3 discovery from Layer 2 //! - Test 3: Recursive multi-hop Layer 3 discovery use std::time::Duration; use nostr_sdk::prelude::*; use crate::common::{sync_helpers::*, TestRelay}; /// Kind 1617 - Patch event (NIP-34) const KIND_PATCH: u16 = 1617; /// Create a valid repository announcement event for testing sync. /// /// This creates a kind 30617 event with required clone and relays tags. fn create_repo_announcement(keys: &Keys, domains: &[&str], identifier: &str) -> Event { let clone_urls: Vec = domains .iter() .map(|d| format!("http://{}/{}.git", d, identifier)) .collect(); let relay_urls: Vec = domains.iter().map(|d| format!("ws://{}", d)).collect(); let tags = vec![ Tag::identifier(identifier), Tag::custom(TagKind::custom("clone"), clone_urls), Tag::custom(TagKind::custom("relays"), relay_urls), ]; EventBuilder::new(Kind::Custom(KIND_REPOSITORY_STATE), "Repository state") .tags(tags) .sign_with_keys(keys) .expect("Failed to sign repo announcement") } /// Create an event referencing a repository coordinate via 'a' tag. /// /// Used to create Layer 2 events like patches that reference a repository. fn create_event_referencing_repo(keys: &Keys, repo_coord: &str, kind: u16, content: &str) -> Event { let tags = vec![Tag::custom( TagKind::custom("a"), vec![repo_coord.to_string()], )]; EventBuilder::new(Kind::Custom(kind), content) .tags(tags) .sign_with_keys(keys) .expect("Failed to sign event") } /// Test 2: Relay discovers another relay via announcement and syncs Layer 2 events /// /// Scenario: /// 1. relay_a has announcement + patch event (Layer 2) /// 2. relay_b (sync enabled, NO bootstrap) receives the announcement directly /// 3. relay_b discovers relay_a from the announcement's relays tag /// 4. relay_b connects to relay_a and syncs the patch event /// /// This tests dynamic relay discovery from direct submissions. #[tokio::test] async fn test_discovers_layer3_via_layer2() { // 1. Start relay_a (source) with the patch event let relay_a = TestRelay::start().await; println!( "relay_a started at {} (domain: {})", relay_a.url(), relay_a.domain() ); // 2. Start relay_b: sync enabled but NO bootstrap relay - will discover relay_a let relay_b = TestRelay::start_with_sync(None).await; println!( "relay_b started at {} (domain: {})", relay_b.url(), relay_b.domain() ); // 3. Create test keys let keys = Keys::generate(); // 4. Create a repository announcement that lists BOTH relays let announcement = create_repo_announcement( &keys, &[&relay_a.domain(), &relay_b.domain()], "test-repo-discovery", ); let announcement_id = announcement.id; println!( "Created announcement {} (kind {})", announcement_id, announcement.kind.as_u16() ); for tag in announcement.tags.iter() { println!(" Tag: {:?}", tag.as_slice()); } // 5. Build the repo coordinate for the 'a' tag in the patch let repo_coord = format!( "{}:{}:{}", KIND_REPOSITORY_STATE, keys.public_key().to_hex(), "test-repo-discovery" ); // 6. Create a patch event (Layer 2) that references the announcement let patch = create_event_referencing_repo(&keys, &repo_coord, KIND_PATCH, "Test patch proposal"); let patch_id = patch.id; println!("Created patch {} (kind {})", patch_id, patch.kind.as_u16()); for tag in patch.tags.iter() { println!(" Tag: {:?}", tag.as_slice()); } // 7. Send announcement and patch to relay_a ONLY let client_a = TestClient::new(relay_a.url(), keys.clone()) .await .expect("Failed to connect to relay_a"); client_a .send_event(&announcement) .await .expect("Failed to send announcement to relay_a"); println!("Announcement sent to relay_a"); client_a .send_event(&patch) .await .expect("Failed to send patch to relay_a"); println!("Patch sent to relay_a"); client_a.disconnect().await; // 8. Send announcement to relay_b directly (triggers discovery of relay_a) let client_b = TestClient::new(relay_b.url(), keys.clone()) .await .expect("Failed to connect to relay_b"); client_b .send_event(&announcement) .await .expect("Failed to send announcement to relay_b"); println!("Announcement sent to relay_b (should trigger discovery of relay_a)"); client_b.disconnect().await; // 9. Wait for relay_b to discover relay_a and sync the patch println!("Waiting 3s for relay_b to discover relay_a and sync patch..."); tokio::time::sleep(Duration::from_secs(3)).await; // 10. Verify patch was synced to relay_b let filter = Filter::new() .kind(Kind::Custom(KIND_PATCH)) .author(keys.public_key()); let patch_synced = wait_for_event_on_relay(relay_b.url(), filter, Duration::from_secs(5)).await; if patch_synced { println!( "Patch {} found on relay_b (synced from discovered relay_a)", patch_id ); } else { println!("Patch {} NOT found on relay_b", patch_id); } // 11. Cleanup relay_b.stop().await; relay_a.stop().await; assert!( patch_synced, "Patch {} should have been synced to relay_b from discovered relay_a", patch_id ); } /// Test 3: Layer 2 discovery with full event chain /// /// Scenario: /// 1. relay_a has: announcement → issue (Layer 2) /// 2. relay_b receives announcement directly /// 3. relay_b discovers relay_a and syncs the issue (Layer 2) /// /// This tests that Layer 2 events (issues/patches) are synced when their /// parent repository is discovered. The chain is: /// Layer 1 (30617): Repository announcement /// Layer 2 (1618): Issue referencing repo /// /// Note: Layer 3 (comments on issues) sync is tracked separately and may /// be implemented in future phases. This test focuses on Layer 2 discovery. #[tokio::test] async fn test_layer2_discovery_with_chain() { // 1. Start relay_a (source) with the event chain let relay_a = TestRelay::start().await; println!( "relay_a started at {} (domain: {})", relay_a.url(), relay_a.domain() ); // 2. Start relay_b: sync enabled but NO bootstrap relay let relay_b = TestRelay::start_with_sync(None).await; println!( "relay_b started at {} (domain: {})", relay_b.url(), relay_b.domain() ); // 3. Create test keys let keys = Keys::generate(); // 4. Create the event chain on relay_a: // Layer 1: Repository announcement let announcement = create_repo_announcement( &keys, &[&relay_a.domain(), &relay_b.domain()], "test-repo-chain", ); let announcement_id = announcement.id; println!("Created announcement {} (Layer 1)", announcement_id); // Build repo coordinate for Layer 2 reference let repo_coord = repo_coord(&keys, "test-repo-chain"); // Layer 2: Issue referencing the repo let issue = build_layer2_issue_event(&keys, &repo_coord, "Test issue for chain discovery") .expect("Failed to create issue"); let issue_id = issue.id; println!("Created issue {} (Layer 2)", issue_id); // 5. Send all events to relay_a let client_a = TestClient::new(relay_a.url(), keys.clone()) .await .expect("Failed to connect to relay_a"); client_a .send_event(&announcement) .await .expect("Failed to send announcement"); client_a .send_event(&issue) .await .expect("Failed to send issue"); println!("Events sent to relay_a"); client_a.disconnect().await; // 6. Send only the announcement to relay_b (triggers discovery) let client_b = TestClient::new(relay_b.url(), keys.clone()) .await .expect("Failed to connect to relay_b"); client_b .send_event(&announcement) .await .expect("Failed to send announcement to relay_b"); println!("Announcement sent to relay_b (should trigger discovery)"); client_b.disconnect().await; // 7. Wait for sync println!("Waiting 3s for Layer 2 sync..."); tokio::time::sleep(Duration::from_secs(3)).await; // 8. Verify Layer 2 event synced to relay_b let issue_filter = Filter::new() .kind(Kind::Custom(KIND_ISSUE)) .author(keys.public_key()); let issue_synced = wait_for_event_on_relay(relay_b.url(), issue_filter, Duration::from_secs(5)).await; println!("Sync result:"); println!(" Issue {} synced: {}", issue_id, issue_synced); // 9. Cleanup relay_b.stop().await; relay_a.stop().await; // 10. Assert Layer 2 event synced assert!( issue_synced, "Issue {} (Layer 2) should have synced to relay_b via discovery", issue_id ); }