diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/purgatory_sync.rs | 243 |
1 files changed, 239 insertions, 4 deletions
diff --git a/tests/purgatory_sync.rs b/tests/purgatory_sync.rs index bb99f46..0b4d864 100644 --- a/tests/purgatory_sync.rs +++ b/tests/purgatory_sync.rs | |||
| @@ -28,10 +28,10 @@ | |||
| 28 | mod common; | 28 | mod common; |
| 29 | 29 | ||
| 30 | use common::{ | 30 | use common::{ |
| 31 | build_repo_coord, check_ref_at_commit, create_pr_event, create_repo_announcement, | 31 | add_commit_to_repo, build_repo_coord, check_ref_at_commit, create_pr_event, |
| 32 | create_state_event, create_test_repo_with_commit, push_ref_to_relay, push_to_relay, | 32 | create_repo_announcement, create_state_event, create_test_repo_with_commit, push_ref_to_relay, |
| 33 | verify_event_not_served, wait_for_event_served, wait_for_sync_connection, CommitVariant, | 33 | push_to_relay, verify_event_not_served, wait_for_event_served, wait_for_sync_connection, |
| 34 | TestRelay, | 34 | CommitVariant, TestRelay, |
| 35 | }; | 35 | }; |
| 36 | use nostr_sdk::prelude::*; | 36 | use nostr_sdk::prelude::*; |
| 37 | use std::time::Duration; | 37 | use std::time::Duration; |
| @@ -439,3 +439,238 @@ async fn test_pr_event_syncs_from_remote() { | |||
| 439 | syncing_relay.stop().await; | 439 | syncing_relay.stop().await; |
| 440 | source_relay.stop().await; | 440 | source_relay.stop().await; |
| 441 | } | 441 | } |
| 442 | |||
| 443 | /// Test that concurrent state and PR events for the same repository | ||
| 444 | /// both sync correctly. | ||
| 445 | /// | ||
| 446 | /// Scenario: | ||
| 447 | /// 1. Start source relay with repo containing two commits (main branch + PR commit) | ||
| 448 | /// 2. Create and push both commits to source relay | ||
| 449 | /// 3. Send both state event and PR event to source relay | ||
| 450 | /// 4. Start syncing relay | ||
| 451 | /// 5. Wait for sync to fetch git data and release both events | ||
| 452 | /// 6. Verify both state event and PR event are served | ||
| 453 | /// 7. Verify refs are correct for both (main branch and refs/nostr/<event-id>) | ||
| 454 | #[tokio::test] | ||
| 455 | async fn test_concurrent_state_and_pr_sync() { | ||
| 456 | // 1. Start source relay | ||
| 457 | let source_relay = TestRelay::start().await; | ||
| 458 | let owner_keys = Keys::generate(); | ||
| 459 | let pr_author_keys = Keys::generate(); | ||
| 460 | let identifier = "concurrent-sync-test-repo"; | ||
| 461 | |||
| 462 | // Pre-allocate syncing relay port so we can include it in announcement | ||
| 463 | let syncing_port = TestRelay::find_free_port(); | ||
| 464 | let syncing_domain = format!("127.0.0.1:{}", syncing_port); | ||
| 465 | |||
| 466 | // 2. Create test repository with two commits | ||
| 467 | // First commit establishes the repo, second commit is used for both state and PR events | ||
| 468 | let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); | ||
| 469 | let _first_commit = create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) | ||
| 470 | .expect("Failed to create test repo"); | ||
| 471 | |||
| 472 | // Add second commit - this becomes HEAD of main and is referenced by both events | ||
| 473 | // In a real scenario, the state event would reference the current branch state, | ||
| 474 | // and the PR would propose changes (which happen to be the same commit here for simplicity) | ||
| 475 | let head_commit = | ||
| 476 | add_commit_to_repo(temp_dir.path(), CommitVariant::PrTest).expect("Failed to add commit"); | ||
| 477 | |||
| 478 | let npub = owner_keys | ||
| 479 | .public_key() | ||
| 480 | .to_bech32() | ||
| 481 | .expect("Failed to get npub"); | ||
| 482 | |||
| 483 | // 3. Create and send announcement listing BOTH relays | ||
| 484 | let announcement = create_repo_announcement( | ||
| 485 | &owner_keys, | ||
| 486 | &[&source_relay.domain(), &syncing_domain], | ||
| 487 | identifier, | ||
| 488 | ); | ||
| 489 | |||
| 490 | let source_client = Client::new(owner_keys.clone()); | ||
| 491 | source_client | ||
| 492 | .add_relay(source_relay.url()) | ||
| 493 | .await | ||
| 494 | .expect("Failed to add source relay"); | ||
| 495 | source_client.connect().await; | ||
| 496 | |||
| 497 | // Wait for connection | ||
| 498 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 499 | |||
| 500 | // Send announcement to source relay (creates bare repo) | ||
| 501 | source_client | ||
| 502 | .send_event(&announcement) | ||
| 503 | .await | ||
| 504 | .expect("Failed to send announcement to source"); | ||
| 505 | |||
| 506 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 507 | |||
| 508 | // 4. Create state event referencing the HEAD commit (pr_commit) | ||
| 509 | // After add_commit_to_repo, main points to pr_commit (which includes state_commit in history) | ||
| 510 | let clone_urls = [ | ||
| 511 | format!( | ||
| 512 | "http://{}/{}/{}.git", | ||
| 513 | source_relay.domain(), | ||
| 514 | npub, | ||
| 515 | identifier | ||
| 516 | ), | ||
| 517 | format!("http://{}/{}/{}.git", syncing_domain, npub, identifier), | ||
| 518 | ]; | ||
| 519 | let relay_urls = [ | ||
| 520 | source_relay.url().to_string(), | ||
| 521 | format!("ws://{}", syncing_domain), | ||
| 522 | ]; | ||
| 523 | |||
| 524 | // State event references main at head_commit (the current HEAD) | ||
| 525 | let state_event = create_state_event( | ||
| 526 | &owner_keys, | ||
| 527 | identifier, | ||
| 528 | &[("main", &head_commit)], | ||
| 529 | &[], | ||
| 530 | &[&clone_urls[0], &clone_urls[1]], | ||
| 531 | &[&relay_urls[0], &relay_urls[1]], | ||
| 532 | ) | ||
| 533 | .expect("Failed to create state event"); | ||
| 534 | |||
| 535 | let state_event_id = state_event.id; | ||
| 536 | |||
| 537 | // Send state event to source relay (goes to purgatory - no git data yet) | ||
| 538 | source_client | ||
| 539 | .send_event(&state_event) | ||
| 540 | .await | ||
| 541 | .expect("Failed to send state event to source"); | ||
| 542 | |||
| 543 | // 5. Create PR event referencing the same commit (head_commit) | ||
| 544 | // This simulates a PR that proposes the changes in head_commit | ||
| 545 | let repo_coord = build_repo_coord(&owner_keys, identifier); | ||
| 546 | |||
| 547 | let pr_event = create_pr_event( | ||
| 548 | &pr_author_keys, | ||
| 549 | &repo_coord, | ||
| 550 | &head_commit, | ||
| 551 | "Test PR for concurrent sync", | ||
| 552 | ) | ||
| 553 | .expect("Failed to create PR event"); | ||
| 554 | |||
| 555 | let pr_event_id = pr_event.id; | ||
| 556 | |||
| 557 | // Send PR event to source relay using PR author's client | ||
| 558 | let pr_client = Client::new(pr_author_keys.clone()); | ||
| 559 | pr_client | ||
| 560 | .add_relay(source_relay.url()) | ||
| 561 | .await | ||
| 562 | .expect("Failed to add source relay for PR"); | ||
| 563 | pr_client.connect().await; | ||
| 564 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 565 | |||
| 566 | pr_client | ||
| 567 | .send_event(&pr_event) | ||
| 568 | .await | ||
| 569 | .expect("Failed to send PR event to source"); | ||
| 570 | |||
| 571 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 572 | |||
| 573 | // 6. Push git data to source relay | ||
| 574 | // Push all branches (main contains both commits due to linear history) | ||
| 575 | push_to_relay(temp_dir.path(), &source_relay.domain(), &npub, identifier) | ||
| 576 | .expect("Push to source should succeed"); | ||
| 577 | |||
| 578 | // Also push the PR ref | ||
| 579 | let pr_ref_name = format!("refs/nostr/{}", pr_event_id.to_hex()); | ||
| 580 | push_ref_to_relay( | ||
| 581 | temp_dir.path(), | ||
| 582 | &source_relay.domain(), | ||
| 583 | &npub, | ||
| 584 | identifier, | ||
| 585 | &head_commit, | ||
| 586 | &pr_ref_name, | ||
| 587 | ) | ||
| 588 | .expect("Push PR ref to source should succeed"); | ||
| 589 | |||
| 590 | // After push, both events should be released from purgatory on source relay | ||
| 591 | wait_for_event_served(source_relay.url(), &state_event_id, Duration::from_secs(5)) | ||
| 592 | .await | ||
| 593 | .expect("State event should be served on source relay after push"); | ||
| 594 | |||
| 595 | wait_for_event_served(source_relay.url(), &pr_event_id, Duration::from_secs(5)) | ||
| 596 | .await | ||
| 597 | .expect("PR event should be served on source relay after push"); | ||
| 598 | |||
| 599 | // 7. Start syncing relay (syncs from source) | ||
| 600 | let syncing_relay = TestRelay::start_on_port_with_options( | ||
| 601 | syncing_port, | ||
| 602 | Some(source_relay.url().to_string()), | ||
| 603 | false, | ||
| 604 | ) | ||
| 605 | .await; | ||
| 606 | |||
| 607 | // Wait for sync connection to establish | ||
| 608 | wait_for_sync_connection(syncing_relay.url(), 1, Duration::from_secs(5)) | ||
| 609 | .await | ||
| 610 | .expect("Sync connection should establish"); | ||
| 611 | |||
| 612 | // 8. Wait for BOTH events to be released on syncing relay | ||
| 613 | // The sync should fetch git data and release both events | ||
| 614 | let state_found = wait_for_event_served( | ||
| 615 | syncing_relay.url(), | ||
| 616 | &state_event_id, | ||
| 617 | Duration::from_secs(30), | ||
| 618 | ) | ||
| 619 | .await; | ||
| 620 | |||
| 621 | assert!( | ||
| 622 | state_found.is_ok(), | ||
| 623 | "State event should be served after sync fetches git data: {:?}", | ||
| 624 | state_found.err() | ||
| 625 | ); | ||
| 626 | |||
| 627 | let pr_found = wait_for_event_served(syncing_relay.url(), &pr_event_id, Duration::from_secs(30)) | ||
| 628 | .await; | ||
| 629 | |||
| 630 | assert!( | ||
| 631 | pr_found.is_ok(), | ||
| 632 | "PR event should be served after sync fetches git data: {:?}", | ||
| 633 | pr_found.err() | ||
| 634 | ); | ||
| 635 | |||
| 636 | // 9. Verify refs are correct on syncing relay | ||
| 637 | // Check main branch points to head_commit (the HEAD) | ||
| 638 | let main_ref_correct = check_ref_at_commit( | ||
| 639 | &syncing_domain, | ||
| 640 | &npub, | ||
| 641 | identifier, | ||
| 642 | "refs/heads/main", | ||
| 643 | &head_commit, | ||
| 644 | ) | ||
| 645 | .await | ||
| 646 | .expect("Failed to check main ref"); | ||
| 647 | |||
| 648 | assert!( | ||
| 649 | main_ref_correct, | ||
| 650 | "main branch should point to HEAD commit ({})", | ||
| 651 | head_commit | ||
| 652 | ); | ||
| 653 | |||
| 654 | // Check refs/nostr/<event-id> points to the same commit | ||
| 655 | let pr_ref_correct = check_ref_at_commit( | ||
| 656 | &syncing_domain, | ||
| 657 | &npub, | ||
| 658 | identifier, | ||
| 659 | &pr_ref_name, | ||
| 660 | &head_commit, | ||
| 661 | ) | ||
| 662 | .await | ||
| 663 | .expect("Failed to check PR ref"); | ||
| 664 | |||
| 665 | assert!( | ||
| 666 | pr_ref_correct, | ||
| 667 | "refs/nostr/<event-id> should point to commit ({})", | ||
| 668 | head_commit | ||
| 669 | ); | ||
| 670 | |||
| 671 | // Cleanup | ||
| 672 | source_client.disconnect().await; | ||
| 673 | pr_client.disconnect().await; | ||
| 674 | syncing_relay.stop().await; | ||
| 675 | source_relay.stop().await; | ||
| 676 | } | ||