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>2026-01-07 15:58:37 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-07 15:58:37 +0000
commite557d07ddbb1ea1a8ca6604f9ba945f359f54ce7 (patch)
tree7b8f3b64499b8917274394a80a8f5532d54a6491
parent049cff14fa731c95b9b0074f67469df3af19870b (diff)
Add integration test for concurrent state and PR event sync
Implements test_concurrent_state_and_pr_sync which verifies that when both a state event and a PR event for the same repository enter purgatory, both are correctly synced from a remote relay and released when git data arrives. The test: 1. Creates a source relay with a repo containing two commits 2. Sends both state event (referencing main branch) and PR event to source 3. Pushes git data to source relay, releasing both events from purgatory 4. Starts syncing relay that connects to source 5. Verifies both events sync to the syncing relay and are served 6. Confirms refs are correct (main branch and refs/nostr/<event-id>) This validates that the purgatory sync mechanism handles multiple concurrent events for the same repository without race conditions or conflicts.
-rw-r--r--tests/purgatory_sync.rs243
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 @@
28mod common; 28mod common;
29 29
30use common::{ 30use 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};
36use nostr_sdk::prelude::*; 36use nostr_sdk::prelude::*;
37use std::time::Duration; 37use 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]
455async 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}