upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-05 14:51:16 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-05 14:51:16 +0000
commitb7aa7b72c189290b45fb388ec1826862bc8dda49 (patch)
tree5bb7bccd3a39109cdedbb412c2c9dac508a6b523
parent339b0c02f2cec25ae804dc882f5ce7d1dd58b9a6 (diff)
test: fix proactive sync basic tests
-rw-r--r--tests/common/relay.rs9
-rw-r--r--tests/proactive_sync_basic.rs360
2 files changed, 142 insertions, 227 deletions
diff --git a/tests/common/relay.rs b/tests/common/relay.rs
index dbfd8de..3d160f2 100644
--- a/tests/common/relay.rs
+++ b/tests/common/relay.rs
@@ -58,7 +58,11 @@ impl TestRelay {
58 /// } 58 /// }
59 /// ``` 59 /// ```
60 pub async fn start_with_sync(bootstrap_relay_url: &str) -> Self { 60 pub async fn start_with_sync(bootstrap_relay_url: &str) -> Self {
61 Self::start_with_options(Self::find_free_port(), Some(bootstrap_relay_url.to_string())).await 61 Self::start_with_options(
62 Self::find_free_port(),
63 Some(bootstrap_relay_url.to_string()),
64 )
65 .await
62 } 66 }
63 67
64 /// Start relay with options 68 /// Start relay with options
@@ -91,11 +95,12 @@ impl TestRelay {
91 cmd.env("NGIT_BIND_ADDRESS", &bind_address) 95 cmd.env("NGIT_BIND_ADDRESS", &bind_address)
92 .env("NGIT_DOMAIN", &bind_address) // Set domain to match bind address 96 .env("NGIT_DOMAIN", &bind_address) // Set domain to match bind address
93 .env("NGIT_GIT_DATA_PATH", git_data_dir.path()) 97 .env("NGIT_GIT_DATA_PATH", git_data_dir.path())
98 .env("NGIT_DATABASE_BACKEND", "memory") // Force in-memory database for isolation
94 .env("NGIT_OWNER_NPUB", &test_npub) 99 .env("NGIT_OWNER_NPUB", &test_npub)
95 .env("NGIT_SYNC_STARTUP_JITTER_MS", "0") // Disable jitter for tests 100 .env("NGIT_SYNC_STARTUP_JITTER_MS", "0") // Disable jitter for tests
96 .env("RUST_LOG", "warn") // Less logging during tests 101 .env("RUST_LOG", "warn") // Less logging during tests
97 .stdout(Stdio::null()) 102 .stdout(Stdio::null())
98 .stderr(Stdio::null()); 103 .stderr(Stdio::null()); // Disable stderr for cleaner test output
99 104
100 // Add bootstrap relay URL if provided 105 // Add bootstrap relay URL if provided
101 if let Some(ref bootstrap_url) = bootstrap_relay_url { 106 if let Some(ref bootstrap_url) = bootstrap_relay_url {
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::*;
21/// Kind 30617 - Repository State (NIP-34) 21/// Kind 30617 - Repository State (NIP-34)
22const KIND_REPOSITORY_STATE: u16 = 30617; 22const KIND_REPOSITORY_STATE: u16 = 30617;
23 23
24/// Result of checking if an event syncs between relays
25#[derive(Debug)]
26struct SyncCheckResult {
27 /// Whether the event was successfully stored on the source relay
28 stored_on_source: bool,
29 /// Whether the event was synced to the target relay
30 synced_to_target: bool,
31}
32
33/// Helper to check if an event syncs from source relay to target relay
34///
35/// This function:
36/// 1. Sends the event to the source relay
37/// 2. Verifies if it was stored on the source relay
38/// 3. Waits for potential sync
39/// 4. Checks if the event appears on the target relay
40///
41/// Note: The sync subscription must already be established before calling this.
42async fn check_event_syncs(
43 source_relay: &TestRelay,
44 target_relay: &TestRelay,
45 event: &Event,
46 keys: &Keys,
47) -> SyncCheckResult {
48 let event_id = event.id;
49
50 // Create client and connect to source relay
51 let client_source = create_connected_client(source_relay.url(), keys.clone())
52 .await
53 .expect("Failed to connect to source relay");
54
55 // Send event to source relay
56 let send_result = send_event_reliably(&client_source, event).await;
57 let stored_on_source = send_result.is_ok();
58
59 if stored_on_source {
60 println!("Event {} stored on source relay", event_id);
61 } else {
62 println!(
63 "Event {} NOT stored on source relay: {:?}",
64 event_id,
65 send_result.err()
66 );
67 }
68
69 // Wait for sync to occur
70 tokio::time::sleep(Duration::from_secs(1)).await;
71
72 // Check if event exists on target relay
73 let client_target = create_connected_client(target_relay.url(), Keys::generate())
74 .await
75 .expect("Failed to connect to target relay");
76
77 let filter = Filter::new()
78 .kind(event.kind)
79 .author(keys.public_key());
80
81 let events_on_target = client_target
82 .fetch_events(filter, Duration::from_secs(3))
83 .await
84 .expect("Failed to fetch from target relay");
85
86 let synced_to_target = events_on_target.iter().any(|e| e.id == event_id);
87
88 if synced_to_target {
89 println!("Event {} found on target relay (synced)", event_id);
90 } else {
91 println!("Event {} NOT found on target relay", event_id);
92 }
93
94 // Clean up
95 client_source.disconnect().await;
96 client_target.disconnect().await;
97
98 SyncCheckResult {
99 stored_on_source,
100 synced_to_target,
101 }
102}
103
24/// Create a client with keys, connect to relay, and wait for connection 104/// Create a client with keys, connect to relay, and wait for connection
25async fn create_connected_client(relay_url: &str, keys: Keys) -> Result<Client, String> { 105async fn create_connected_client(relay_url: &str, keys: Keys) -> Result<Client, String> {
26 let client = Client::new(keys); 106 let client = Client::new(keys);
@@ -72,31 +152,10 @@ async fn send_event_reliably(client: &Client, event: &Event) -> Result<EventId,
72/// Create a valid repository announcement event for testing 152/// Create a valid repository announcement event for testing
73/// 153///
74/// This creates a kind 30617 event with required clone and relays tags. 154/// This creates a kind 30617 event with required clone and relays tags.
155/// Accepts one or more domains - for sync tests, include all relay domains
156/// so the event will be accepted by each relay's write policy.
75/// Uses TagKind::custom("clone") and TagKind::custom("relays") to match grasp-audit patterns. 157/// Uses TagKind::custom("clone") and TagKind::custom("relays") to match grasp-audit patterns.
76#[allow(dead_code)] 158fn create_repo_announcement(keys: &Keys, domains: &[&str], identifier: &str) -> Event {
77fn create_valid_repo_announcement(keys: &Keys, domain: &str, identifier: &str) -> Event {
78 // Build tags for repository announcement using custom tag kinds (as grasp-audit does)
79 let tags = vec![
80 Tag::identifier(identifier),
81 Tag::custom(
82 TagKind::custom("clone"),
83 vec![format!("http://{}/{}.git", domain, identifier)],
84 ),
85 Tag::custom(TagKind::custom("relays"), vec![format!("ws://{}", domain)]),
86 ];
87
88 EventBuilder::new(Kind::Custom(KIND_REPOSITORY_STATE), "Repository state")
89 .tags(tags)
90 .sign_with_keys(keys)
91 .expect("Failed to sign event")
92}
93
94/// Create a valid repository announcement event listing multiple relays
95///
96/// This creates a kind 30617 event with clone/relays tags referencing multiple domains,
97/// which is necessary for sync tests where the event needs to be accepted by both relays.
98/// Uses TagKind::custom("clone") and TagKind::custom("relays") to match grasp-audit patterns.
99fn create_shared_repo_announcement(keys: &Keys, domains: &[&str], identifier: &str) -> Event {
100 // Build clone URLs for all domains (with .git suffix) 159 // Build clone URLs for all domains (with .git suffix)
101 let clone_urls: Vec<String> = domains 160 let clone_urls: Vec<String> = domains
102 .iter() 161 .iter()
@@ -157,25 +216,17 @@ async fn test_valid_event_syncs_to_relay() {
157 relay_b.domain() 216 relay_b.domain()
158 ); 217 );
159 218
160 // Create test keys that will be used for both client and event signing 219 // Create test keys
161 let keys = Keys::generate(); 220 let keys = Keys::generate();
162 221
163 // Wait for relay_b's sync connection to establish 222 // Wait for relay_b's sync connection to establish
164 // With NGIT_SYNC_STARTUP_JITTER_MS=0 (set by TestRelay), sync connects immediately.
165 // A brief wait allows the WebSocket connection and Layer 1 subscription to be set up.
166 println!("Waiting 1s for relay_b sync connection to establish..."); 223 println!("Waiting 1s for relay_b sync connection to establish...");
167 tokio::time::sleep(Duration::from_secs(1)).await; 224 tokio::time::sleep(Duration::from_secs(1)).await;
168 225
169 // Create a client with our keys and connect to relay_a
170 let client_a = create_connected_client(relay_a.url(), keys.clone())
171 .await
172 .expect("Failed to connect to relay_a");
173 println!("client_a connected to relay_a");
174
175 // Create a repository announcement that lists BOTH relays 226 // Create a repository announcement that lists BOTH relays
176 // This is required for sync - the event must reference both the source relay 227 // This is required for sync - the event must reference both the source relay
177 // and the syncing relay for the write policy to accept it on both sides 228 // and the syncing relay for the write policy to accept it on both sides
178 let event = create_shared_repo_announcement( 229 let event = create_repo_announcement(
179 &keys, 230 &keys,
180 &[&relay_a.domain(), &relay_b.domain()], 231 &[&relay_a.domain(), &relay_b.domain()],
181 "test-repo", 232 "test-repo",
@@ -188,226 +239,85 @@ async fn test_valid_event_syncs_to_relay() {
188 println!(" Tag: {:?}", tag.as_slice()); 239 println!(" Tag: {:?}", tag.as_slice());
189 } 240 }
190 241
191 // Submit event to relay_a AFTER relay_b's subscription is established 242 // Use helper to send and check sync
192 // This ensures the event is received via the live subscription 243 let result = check_event_syncs(&relay_a, &relay_b, &event, &keys).await;
193 println!("Sending event to relay_a...");
194 send_event_reliably(&client_a, &event)
195 .await
196 .expect("Failed to send event to relay_a");
197 println!("Event sent successfully");
198
199 // Verify event is stored on relay_a first
200 let filter_a = Filter::new()
201 .kind(Kind::Custom(KIND_REPOSITORY_STATE))
202 .author(keys.public_key());
203
204 let events_on_a = client_a
205 .fetch_events(filter_a.clone(), Duration::from_secs(5))
206 .await
207 .expect("Failed to fetch events from relay_a");
208 244
209 println!( 245 // Clean up
210 "Events on relay_a: {} (looking for {})", 246 relay_b.stop().await;
211 events_on_a.len(), 247 relay_a.stop().await;
212 event_id
213 );
214 for e in events_on_a.iter() {
215 println!(" Found event: {} (kind {})", e.id, e.kind.as_u16());
216 }
217 248
218 let found_on_a = events_on_a.iter().any(|e| e.id == event_id);
219 assert!( 249 assert!(
220 found_on_a, 250 result.stored_on_source,
221 "Event {} was not stored on relay_a! This is a prerequisite for sync.", 251 "Event {} was not stored on relay_a! This is a prerequisite for sync.",
222 event_id 252 event_id
223 ); 253 );
224 println!("✓ Event confirmed on relay_a");
225
226 // Wait for sync to occur (event processing and storage)
227 println!("Waiting 1s for sync to occur...");
228 tokio::time::sleep(Duration::from_secs(1)).await;
229
230 // Query relay_b to verify the event was synced
231 let client_b = create_connected_client(relay_b.url(), Keys::generate())
232 .await
233 .expect("Failed to connect to relay_b");
234
235 // Create filter to find our event
236 let filter_b = Filter::new()
237 .kind(Kind::Custom(KIND_REPOSITORY_STATE))
238 .author(keys.public_key());
239
240 let events_on_b = client_b
241 .fetch_events(filter_b, Duration::from_secs(5))
242 .await
243 .expect("Failed to fetch events from relay_b");
244
245 println!(
246 "Events on relay_b: {} (looking for {})",
247 events_on_b.len(),
248 event_id
249 );
250 for e in events_on_b.iter() {
251 println!(" Found event: {} (kind {})", e.id, e.kind.as_u16());
252 }
253
254 // Check if our event was synced
255 let found_on_b = events_on_b.iter().any(|e| e.id == event_id);
256
257 // Clean up
258 client_a.disconnect().await;
259 client_b.disconnect().await;
260 relay_b.stop().await;
261 relay_a.stop().await;
262
263 assert!( 254 assert!(
264 found_on_b, 255 result.synced_to_target,
265 "Event {} was not synced to relay_b. Found {} events on relay_b", 256 "Event {} was not synced to relay_b",
266 event_id, 257 event_id
267 events_on_b.len()
268 ); 258 );
269} 259}
270 260
271/// Test that invalid events are rejected by syncing relay validation 261/// Test that events not listing relay_b in their relays tag are NOT synced
262///
263/// This verifies that relay_b's write policy correctly rejects events during sync
264/// if they don't list relay_b as one of their relays.
272#[tokio::test] 265#[tokio::test]
273async fn test_invalid_event_rejected_by_sync_validation() { 266async fn test_event_not_listing_target_relay_is_not_synced() {
274 // Start source relay (relay_a) - this is a simple relay without GRASP validation 267 // Start source relay (relay_a)
275 // For this test, we'll use a second ngit-grasp relay, but the key insight is that
276 // the syncing relay should reject events that don't pass its own validation
277
278 let relay_a = TestRelay::start().await; 268 let relay_a = TestRelay::start().await;
279 let relay_b = TestRelay::start_with_sync(relay_a.url()).await; 269 println!(
270 "relay_a started at {} (domain: {})",
271 relay_a.url(),
272 relay_a.domain()
273 );
280 274
281 // Give time for connection 275 // Start syncing relay (relay_b) configured to sync from relay_a
282 tokio::time::sleep(Duration::from_millis(500)).await; 276 let relay_b = TestRelay::start_with_sync(relay_a.url()).await;
277 println!(
278 "relay_b started at {} (domain: {})",
279 relay_b.url(),
280 relay_b.domain()
281 );
283 282
284 // Create test keys 283 // Create test keys
285 let keys = Keys::generate(); 284 let keys = Keys::generate();
286 285
287 // Create an INVALID repository announcement (missing clone tag) 286 // Wait for relay_b's sync connection to establish
288 let tags = vec![ 287 println!("Waiting 1s for relay_b sync connection to establish...");
289 Tag::identifier("test-invalid-repo"),
290 // Missing required "clone" tag!
291 Tag::custom(
292 TagKind::custom("relays"),
293 vec![format!("ws://{}", relay_a.domain())],
294 ),
295 ];
296
297 let invalid_event = EventBuilder::new(Kind::Custom(KIND_REPOSITORY_STATE), "Invalid repo")
298 .tags(tags)
299 .sign_with_keys(&keys)
300 .expect("Failed to sign event");
301
302 let invalid_event_id = invalid_event.id;
303
304 // Submit invalid event to relay_a
305 // Note: relay_a will also reject it due to GRASP validation
306 let client_a = Client::default();
307 client_a
308 .add_relay(relay_a.url())
309 .await
310 .expect("Failed to add relay_a");
311 client_a.connect().await;
312
313 // This will likely fail since relay_a also validates, but let's try
314 let _ = client_a.send_event(&invalid_event).await;
315
316 // Wait for potential sync
317 tokio::time::sleep(Duration::from_secs(1)).await; 288 tokio::time::sleep(Duration::from_secs(1)).await;
318 289
319 // Query relay_b - the event should NOT be present 290 // Create a repository announcement that lists ONLY relay_a (NOT relay_b)
320 let client_b = Client::default(); 291 // This event is valid and will be accepted by relay_a, but should be
321 client_b 292 // rejected by relay_b's write policy during sync
322 .add_relay(relay_b.url()) 293 let event = create_repo_announcement(&keys, &[&relay_a.domain()], "test-repo-only-a");
323 .await 294 let event_id = event.id;
324 .expect("Failed to add relay_b");
325 client_b.connect().await;
326
327 let filter = Filter::new()
328 .kind(Kind::Custom(KIND_REPOSITORY_STATE))
329 .author(keys.public_key());
330 295
331 let events = client_b 296 // Print event details for debugging
332 .fetch_events(filter, Duration::from_secs(3)) 297 println!("Created event {} (kind {})", event_id, event.kind.as_u16());
333 .await 298 for tag in event.tags.iter() {
334 .expect("Failed to fetch events from relay_b"); 299 println!(" Tag: {:?}", tag.as_slice());
300 }
301 println!("Note: This event only lists relay_a, not relay_b");
335 302
336 let found = events.iter().any(|e| e.id == invalid_event_id); 303 // Use helper to send and check sync
304 let result = check_event_syncs(&relay_a, &relay_b, &event, &keys).await;
337 305
338 // Clean up 306 // Clean up
339 client_a.disconnect().await;
340 client_b.disconnect().await;
341 relay_b.stop().await; 307 relay_b.stop().await;
342 relay_a.stop().await; 308 relay_a.stop().await;
343 309
310 // Event should be stored on relay_a (it lists relay_a)
344 assert!( 311 assert!(
345 !found, 312 result.stored_on_source,
346 "Invalid event {} should NOT have been synced to relay_b", 313 "Event {} should have been stored on relay_a (it lists relay_a)",
347 invalid_event_id 314 event_id
348 ); 315 );
349}
350
351/// Test that syncing relay maintains its own validation policy
352#[tokio::test]
353async fn test_sync_respects_local_validation() {
354 // This test verifies that synced events go through the local Nip34WritePolicy
355 // by testing that orphan events (events referencing non-existent repos) are rejected
356
357 let relay_a = TestRelay::start().await;
358 let relay_b = TestRelay::start_with_sync(relay_a.url()).await;
359
360 tokio::time::sleep(Duration::from_millis(500)).await;
361
362 let keys = Keys::generate();
363
364 // First, create a VALID repository announcement and submit it
365 let valid_event = create_valid_repo_announcement(&keys, &relay_a.domain(), "valid-repo");
366 let valid_event_id = valid_event.id;
367
368 let client_a = Client::default();
369 client_a
370 .add_relay(relay_a.url())
371 .await
372 .expect("Failed to add relay_a");
373 client_a.connect().await;
374
375 client_a
376 .send_event(&valid_event)
377 .await
378 .expect("Failed to send valid event");
379
380 // Wait for sync
381 tokio::time::sleep(Duration::from_secs(2)).await;
382
383 // Query relay_b to verify the valid event was synced
384 let client_b = Client::default();
385 client_b
386 .add_relay(relay_b.url())
387 .await
388 .expect("Failed to add relay_b");
389 client_b.connect().await;
390
391 let filter = Filter::new()
392 .kind(Kind::Custom(KIND_REPOSITORY_STATE))
393 .author(keys.public_key());
394
395 let events = client_b
396 .fetch_events(filter, Duration::from_secs(5))
397 .await
398 .expect("Failed to fetch events from relay_b");
399
400 let found = events.iter().any(|e| e.id == valid_event_id);
401
402 // Clean up
403 client_a.disconnect().await;
404 client_b.disconnect().await;
405 relay_b.stop().await;
406 relay_a.stop().await;
407 316
317 // Event should NOT be synced to relay_b (it doesn't list relay_b)
408 assert!( 318 assert!(
409 found, 319 !result.synced_to_target,
410 "Valid event {} should have been synced to relay_b", 320 "Event {} should NOT have been synced to relay_b (it doesn't list relay_b)",
411 valid_event_id 321 event_id
412 ); 322 );
413} 323}