mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-06 15:38:25 +00:00
test: fix proactive sync basic tests
This commit is contained in:
@@ -58,7 +58,11 @@ impl TestRelay {
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn start_with_sync(bootstrap_relay_url: &str) -> Self {
|
||||
Self::start_with_options(Self::find_free_port(), Some(bootstrap_relay_url.to_string())).await
|
||||
Self::start_with_options(
|
||||
Self::find_free_port(),
|
||||
Some(bootstrap_relay_url.to_string()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Start relay with options
|
||||
@@ -91,11 +95,12 @@ impl TestRelay {
|
||||
cmd.env("NGIT_BIND_ADDRESS", &bind_address)
|
||||
.env("NGIT_DOMAIN", &bind_address) // Set domain to match bind address
|
||||
.env("NGIT_GIT_DATA_PATH", git_data_dir.path())
|
||||
.env("NGIT_DATABASE_BACKEND", "memory") // Force in-memory database for isolation
|
||||
.env("NGIT_OWNER_NPUB", &test_npub)
|
||||
.env("NGIT_SYNC_STARTUP_JITTER_MS", "0") // Disable jitter for tests
|
||||
.env("RUST_LOG", "warn") // Less logging during tests
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
.stderr(Stdio::null()); // Disable stderr for cleaner test output
|
||||
|
||||
// Add bootstrap relay URL if provided
|
||||
if let Some(ref bootstrap_url) = bootstrap_relay_url {
|
||||
|
||||
+139
-229
@@ -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<Client, String> {
|
||||
let client = Client::new(keys);
|
||||
@@ -72,31 +152,10 @@ async fn send_event_reliably(client: &Client, event: &Event) -> Result<EventId,
|
||||
/// Create a valid repository announcement event for testing
|
||||
///
|
||||
/// This creates a kind 30617 event with required clone and relays tags.
|
||||
/// Accepts one or more domains - for sync tests, include all relay domains
|
||||
/// so the event will be accepted by each relay's write policy.
|
||||
/// Uses TagKind::custom("clone") and TagKind::custom("relays") to match grasp-audit patterns.
|
||||
#[allow(dead_code)]
|
||||
fn create_valid_repo_announcement(keys: &Keys, domain: &str, identifier: &str) -> 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<String> = 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");
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
let found_on_a = events_on_a.iter().any(|e| e.id == event_id);
|
||||
assert!(
|
||||
found_on_a,
|
||||
"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);
|
||||
// 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;
|
||||
|
||||
assert!(
|
||||
found_on_b,
|
||||
"Event {} was not synced to relay_b. Found {} events on relay_b",
|
||||
event_id,
|
||||
events_on_b.len()
|
||||
result.stored_on_source,
|
||||
"Event {} was not stored on relay_a! This is a prerequisite for sync.",
|
||||
event_id
|
||||
);
|
||||
assert!(
|
||||
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;
|
||||
// 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 filter = Filter::new()
|
||||
.kind(Kind::Custom(KIND_REPOSITORY_STATE))
|
||||
.author(keys.public_key());
|
||||
// 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 events = client_b
|
||||
.fetch_events(filter, Duration::from_secs(3))
|
||||
.await
|
||||
.expect("Failed to fetch events from 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
|
||||
);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
assert!(
|
||||
found,
|
||||
"Valid event {} should have been synced to relay_b",
|
||||
valid_event_id
|
||||
result.stored_on_source,
|
||||
"Event {} should have been stored on relay_a (it lists relay_a)",
|
||||
event_id
|
||||
);
|
||||
|
||||
// Event should NOT be synced to relay_b (it doesn't list relay_b)
|
||||
assert!(
|
||||
!result.synced_to_target,
|
||||
"Event {} should NOT have been synced to relay_b (it doesn't list relay_b)",
|
||||
event_id
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user