diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-07 15:50:50 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-07 15:50:50 +0000 |
| commit | 049cff14fa731c95b9b0074f67469df3af19870b (patch) | |
| tree | 78f87b52385eaaaa1d221745cdf367ee81b598ba | |
| parent | 1db877d53c4ff45971c69fecc5165c352ec316c9 (diff) | |
test: add test_pr_event_syncs_from_remote
| -rw-r--r-- | tests/common/purgatory_helpers.rs | 67 | ||||
| -rw-r--r-- | tests/purgatory_sync.rs | 168 |
2 files changed, 232 insertions, 3 deletions
diff --git a/tests/common/purgatory_helpers.rs b/tests/common/purgatory_helpers.rs index e61e2e2..7d8e908 100644 --- a/tests/common/purgatory_helpers.rs +++ b/tests/common/purgatory_helpers.rs | |||
| @@ -529,6 +529,73 @@ pub fn push_to_relay( | |||
| 529 | Ok(()) | 529 | Ok(()) |
| 530 | } | 530 | } |
| 531 | 531 | ||
| 532 | /// Push a specific ref to a relay. | ||
| 533 | /// | ||
| 534 | /// This is used for pushing to refs/nostr/<event-id> for PR events. | ||
| 535 | /// Unlike `push_to_relay` which pushes all refs, this pushes a specific | ||
| 536 | /// commit to a specific ref name. | ||
| 537 | /// | ||
| 538 | /// # Arguments | ||
| 539 | /// * `local_path` - Path to local git repository | ||
| 540 | /// * `relay_domain` - The relay domain (e.g., "127.0.0.1:8080") | ||
| 541 | /// * `npub` - Owner's npub | ||
| 542 | /// * `repo_id` - Repository identifier | ||
| 543 | /// * `commit_hash` - The commit to push | ||
| 544 | /// * `ref_name` - The ref name to push to (e.g., "refs/nostr/<event-id>") | ||
| 545 | /// | ||
| 546 | /// # Returns | ||
| 547 | /// * `Ok(())` - Push successful | ||
| 548 | /// * `Err(String)` - Push failed | ||
| 549 | pub fn push_ref_to_relay( | ||
| 550 | local_path: &Path, | ||
| 551 | relay_domain: &str, | ||
| 552 | npub: &str, | ||
| 553 | repo_id: &str, | ||
| 554 | commit_hash: &str, | ||
| 555 | ref_name: &str, | ||
| 556 | ) -> Result<(), String> { | ||
| 557 | let remote_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id); | ||
| 558 | |||
| 559 | // Check if origin already exists | ||
| 560 | let check_output = Command::new("git") | ||
| 561 | .args(["remote", "get-url", "origin"]) | ||
| 562 | .current_dir(local_path) | ||
| 563 | .output() | ||
| 564 | .map_err(|e| format!("Failed to check remote: {}", e))?; | ||
| 565 | |||
| 566 | if check_output.status.success() { | ||
| 567 | // Remote exists, update it | ||
| 568 | let _ = Command::new("git") | ||
| 569 | .args(["remote", "set-url", "origin", &remote_url]) | ||
| 570 | .current_dir(local_path) | ||
| 571 | .output(); | ||
| 572 | } else { | ||
| 573 | // Add new remote | ||
| 574 | let _ = Command::new("git") | ||
| 575 | .args(["remote", "add", "origin", &remote_url]) | ||
| 576 | .current_dir(local_path) | ||
| 577 | .output(); | ||
| 578 | } | ||
| 579 | |||
| 580 | // Push specific commit to specific ref | ||
| 581 | // Format: git push origin <commit>:<ref> | ||
| 582 | let refspec = format!("{}:{}", commit_hash, ref_name); | ||
| 583 | let output = Command::new("git") | ||
| 584 | .args(["push", "origin", &refspec]) | ||
| 585 | .current_dir(local_path) | ||
| 586 | .output() | ||
| 587 | .map_err(|e| format!("Failed to run git push: {}", e))?; | ||
| 588 | |||
| 589 | if !output.status.success() { | ||
| 590 | return Err(format!( | ||
| 591 | "git push failed: {}", | ||
| 592 | String::from_utf8_lossy(&output.stderr) | ||
| 593 | )); | ||
| 594 | } | ||
| 595 | |||
| 596 | Ok(()) | ||
| 597 | } | ||
| 598 | |||
| 532 | #[cfg(test)] | 599 | #[cfg(test)] |
| 533 | mod tests { | 600 | mod tests { |
| 534 | use super::*; | 601 | use super::*; |
diff --git a/tests/purgatory_sync.rs b/tests/purgatory_sync.rs index 3da086c..bb99f46 100644 --- a/tests/purgatory_sync.rs +++ b/tests/purgatory_sync.rs | |||
| @@ -28,9 +28,10 @@ | |||
| 28 | mod common; | 28 | mod common; |
| 29 | 29 | ||
| 30 | use common::{ | 30 | use common::{ |
| 31 | check_ref_at_commit, create_repo_announcement, create_state_event, | 31 | build_repo_coord, check_ref_at_commit, create_pr_event, create_repo_announcement, |
| 32 | create_test_repo_with_commit, push_to_relay, verify_event_not_served, wait_for_event_served, | 32 | create_state_event, create_test_repo_with_commit, push_ref_to_relay, push_to_relay, |
| 33 | wait_for_sync_connection, CommitVariant, TestRelay, | 33 | verify_event_not_served, wait_for_event_served, wait_for_sync_connection, CommitVariant, |
| 34 | TestRelay, | ||
| 34 | }; | 35 | }; |
| 35 | use nostr_sdk::prelude::*; | 36 | use nostr_sdk::prelude::*; |
| 36 | use std::time::Duration; | 37 | use std::time::Duration; |
| @@ -277,3 +278,164 @@ async fn test_state_event_syncs_from_remote() { | |||
| 277 | syncing_relay.stop().await; | 278 | syncing_relay.stop().await; |
| 278 | source_relay.stop().await; | 279 | source_relay.stop().await; |
| 279 | } | 280 | } |
| 281 | |||
| 282 | /// Test that a PR event entering purgatory triggers remote commit fetch | ||
| 283 | /// and is released once the commit is available. | ||
| 284 | /// | ||
| 285 | /// Scenario: | ||
| 286 | /// 1. Start source relay with repository announcement | ||
| 287 | /// 2. Create PR event (goes to purgatory - no git data yet) | ||
| 288 | /// 3. Push commit to refs/nostr/<event-id> (authorized by PR event in purgatory) | ||
| 289 | /// 4. PR event gets released from purgatory on source relay | ||
| 290 | /// 5. Start syncing relay | ||
| 291 | /// 6. Syncing relay syncs PR event (goes to purgatory - no local git data) | ||
| 292 | /// 7. Syncing relay fetches commit from source's clone URL | ||
| 293 | /// 8. Verify PR event is released and refs/nostr/<event-id> created on syncing relay | ||
| 294 | #[tokio::test] | ||
| 295 | async fn test_pr_event_syncs_from_remote() { | ||
| 296 | // 1. Start source relay | ||
| 297 | let source_relay = TestRelay::start().await; | ||
| 298 | let owner_keys = Keys::generate(); | ||
| 299 | let pr_author_keys = Keys::generate(); | ||
| 300 | let identifier = "pr-sync-test-repo"; | ||
| 301 | |||
| 302 | // Pre-allocate syncing relay port so we can include it in announcement | ||
| 303 | let syncing_port = TestRelay::find_free_port(); | ||
| 304 | let syncing_domain = format!("127.0.0.1:{}", syncing_port); | ||
| 305 | |||
| 306 | // 2. Create test repository locally with PR commit | ||
| 307 | let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); | ||
| 308 | let commit_hash = create_test_repo_with_commit(temp_dir.path(), CommitVariant::PrTest) | ||
| 309 | .expect("Failed to create test repo"); | ||
| 310 | |||
| 311 | let npub = owner_keys | ||
| 312 | .public_key() | ||
| 313 | .to_bech32() | ||
| 314 | .expect("Failed to get npub"); | ||
| 315 | |||
| 316 | // 3. Create and send announcement listing BOTH relays | ||
| 317 | // This ensures the syncing relay will accept the PR event when it syncs | ||
| 318 | let announcement = create_repo_announcement( | ||
| 319 | &owner_keys, | ||
| 320 | &[&source_relay.domain(), &syncing_domain], | ||
| 321 | identifier, | ||
| 322 | ); | ||
| 323 | |||
| 324 | let source_client = Client::new(owner_keys.clone()); | ||
| 325 | source_client | ||
| 326 | .add_relay(source_relay.url()) | ||
| 327 | .await | ||
| 328 | .expect("Failed to add source relay"); | ||
| 329 | source_client.connect().await; | ||
| 330 | |||
| 331 | // Wait for connection | ||
| 332 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 333 | |||
| 334 | // Send announcement to source relay (creates bare repo) | ||
| 335 | source_client | ||
| 336 | .send_event(&announcement) | ||
| 337 | .await | ||
| 338 | .expect("Failed to send announcement to source"); | ||
| 339 | |||
| 340 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 341 | |||
| 342 | // 4. Create and send PR event BEFORE pushing | ||
| 343 | // The PR event goes to purgatory on source relay, which authorizes the push | ||
| 344 | let repo_coord = build_repo_coord(&owner_keys, identifier); | ||
| 345 | |||
| 346 | let pr_event = create_pr_event(&pr_author_keys, &repo_coord, &commit_hash, "Test PR for sync") | ||
| 347 | .expect("Failed to create PR event"); | ||
| 348 | |||
| 349 | let pr_event_id = pr_event.id; | ||
| 350 | |||
| 351 | // Send PR event to source relay using PR author's client | ||
| 352 | let pr_client = Client::new(pr_author_keys.clone()); | ||
| 353 | pr_client | ||
| 354 | .add_relay(source_relay.url()) | ||
| 355 | .await | ||
| 356 | .expect("Failed to add source relay for PR"); | ||
| 357 | pr_client.connect().await; | ||
| 358 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 359 | |||
| 360 | pr_client | ||
| 361 | .send_event(&pr_event) | ||
| 362 | .await | ||
| 363 | .expect("Failed to send PR event to source"); | ||
| 364 | |||
| 365 | // Small delay to ensure PR event is processed into purgatory | ||
| 366 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 367 | |||
| 368 | // 5. Push commit to refs/nostr/<event-id> on source relay | ||
| 369 | // The PR event in purgatory authorizes this push | ||
| 370 | let ref_name = format!("refs/nostr/{}", pr_event_id.to_hex()); | ||
| 371 | push_ref_to_relay( | ||
| 372 | temp_dir.path(), | ||
| 373 | &source_relay.domain(), | ||
| 374 | &npub, | ||
| 375 | identifier, | ||
| 376 | &commit_hash, | ||
| 377 | &ref_name, | ||
| 378 | ) | ||
| 379 | .expect("Push to refs/nostr/<event-id> should succeed"); | ||
| 380 | |||
| 381 | // After push, PR event should be released from purgatory on source relay | ||
| 382 | wait_for_event_served(source_relay.url(), &pr_event_id, Duration::from_secs(5)) | ||
| 383 | .await | ||
| 384 | .expect("PR event should be served on source relay after push"); | ||
| 385 | |||
| 386 | // 6. Start syncing relay (syncs from source) | ||
| 387 | let syncing_relay = TestRelay::start_on_port_with_options( | ||
| 388 | syncing_port, | ||
| 389 | Some(source_relay.url().to_string()), | ||
| 390 | false, | ||
| 391 | ) | ||
| 392 | .await; | ||
| 393 | |||
| 394 | // Wait for sync connection to establish | ||
| 395 | wait_for_sync_connection(syncing_relay.url(), 1, Duration::from_secs(5)) | ||
| 396 | .await | ||
| 397 | .expect("Sync connection should establish"); | ||
| 398 | |||
| 399 | // 7. Wait for PR event to be released on syncing relay | ||
| 400 | // The sync should: | ||
| 401 | // a) Fetch the announcement and PR event from source relay | ||
| 402 | // b) Accept announcement (creates bare repo structure) | ||
| 403 | // c) Put PR event in purgatory (commit missing on syncing relay) | ||
| 404 | // d) Fetch commit from source relay's clone URL | ||
| 405 | // e) Release the PR event from purgatory | ||
| 406 | // f) Create refs/nostr/<event-id> pointing to the commit | ||
| 407 | let found = wait_for_event_served( | ||
| 408 | syncing_relay.url(), | ||
| 409 | &pr_event_id, | ||
| 410 | Duration::from_secs(30), // Allow time for sync + git fetch | ||
| 411 | ) | ||
| 412 | .await; | ||
| 413 | |||
| 414 | assert!( | ||
| 415 | found.is_ok(), | ||
| 416 | "PR event should be served after sync fetches commit: {:?}", | ||
| 417 | found.err() | ||
| 418 | ); | ||
| 419 | |||
| 420 | // 8. Verify refs/nostr/<event-id> was created on syncing relay | ||
| 421 | let ref_correct = check_ref_at_commit( | ||
| 422 | &syncing_domain, | ||
| 423 | &npub, | ||
| 424 | identifier, | ||
| 425 | &ref_name, | ||
| 426 | &commit_hash, | ||
| 427 | ) | ||
| 428 | .await | ||
| 429 | .expect("Failed to check PR ref"); | ||
| 430 | |||
| 431 | assert!( | ||
| 432 | ref_correct, | ||
| 433 | "refs/nostr/<event-id> should point to PR commit" | ||
| 434 | ); | ||
| 435 | |||
| 436 | // Cleanup | ||
| 437 | source_client.disconnect().await; | ||
| 438 | pr_client.disconnect().await; | ||
| 439 | syncing_relay.stop().await; | ||
| 440 | source_relay.stop().await; | ||
| 441 | } | ||