upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests/purgatory_sync.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-01-07 20:41:01 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-07 20:41:01 +0000
commit7467aa9ace94b4e132eedd03c9daddb2d59813c4 (patch)
tree32c4571a7376138eb429017a48dbde7ad8b15be6 /tests/purgatory_sync.rs
parente7c18cf2a96b1f45e5f21a83ee1fe2e18a6dc7e2 (diff)
test: added purgatory git data sync intergration tests agregating from mulitple git servers
Diffstat (limited to 'tests/purgatory_sync.rs')
-rw-r--r--tests/purgatory_sync.rs496
1 files changed, 477 insertions, 19 deletions
diff --git a/tests/purgatory_sync.rs b/tests/purgatory_sync.rs
index 0b4d864..fe03455 100644
--- a/tests/purgatory_sync.rs
+++ b/tests/purgatory_sync.rs
@@ -29,9 +29,10 @@ mod common;
29 29
30use common::{ 30use common::{
31 add_commit_to_repo, build_repo_coord, check_ref_at_commit, create_pr_event, 31 add_commit_to_repo, build_repo_coord, check_ref_at_commit, create_pr_event,
32 create_repo_announcement, create_state_event, create_test_repo_with_commit, push_ref_to_relay, 32 create_pr_event_with_clone, create_repo_announcement, create_state_event,
33 push_to_relay, verify_event_not_served, wait_for_event_served, wait_for_sync_connection, 33 create_test_repo_with_commit, push_ref_to_relay, push_to_relay, verify_event_not_served,
34 CommitVariant, TestRelay, 34 wait_for_event_served, wait_for_sync_connection, CommitVariant, MockRelay, SimpleGitServer,
35 TestRelay,
35}; 36};
36use nostr_sdk::prelude::*; 37use nostr_sdk::prelude::*;
37use std::time::Duration; 38use std::time::Duration;
@@ -55,9 +56,8 @@ async fn test_push_triggers_unified_processing() {
55 56
56 // 2. Create test repository locally with deterministic commit 57 // 2. Create test repository locally with deterministic commit
57 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); 58 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
58 let commit_hash = 59 let commit_hash = create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest)
59 create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) 60 .expect("Failed to create test repo");
60 .expect("Failed to create test repo");
61 61
62 // 3. Create and send announcement 62 // 3. Create and send announcement
63 let announcement = create_repo_announcement(&keys, &[&relay.domain()], identifier); 63 let announcement = create_repo_announcement(&keys, &[&relay.domain()], identifier);
@@ -343,8 +343,13 @@ async fn test_pr_event_syncs_from_remote() {
343 // The PR event goes to purgatory on source relay, which authorizes the push 343 // The PR event goes to purgatory on source relay, which authorizes the push
344 let repo_coord = build_repo_coord(&owner_keys, identifier); 344 let repo_coord = build_repo_coord(&owner_keys, identifier);
345 345
346 let pr_event = create_pr_event(&pr_author_keys, &repo_coord, &commit_hash, "Test PR for sync") 346 let pr_event = create_pr_event(
347 .expect("Failed to create PR event"); 347 &pr_author_keys,
348 &repo_coord,
349 &commit_hash,
350 "Test PR for sync",
351 )
352 .expect("Failed to create PR event");
348 353
349 let pr_event_id = pr_event.id; 354 let pr_event_id = pr_event.id;
350 355
@@ -418,15 +423,10 @@ async fn test_pr_event_syncs_from_remote() {
418 ); 423 );
419 424
420 // 8. Verify refs/nostr/<event-id> was created on syncing relay 425 // 8. Verify refs/nostr/<event-id> was created on syncing relay
421 let ref_correct = check_ref_at_commit( 426 let ref_correct =
422 &syncing_domain, 427 check_ref_at_commit(&syncing_domain, &npub, identifier, &ref_name, &commit_hash)
423 &npub, 428 .await
424 identifier, 429 .expect("Failed to check PR ref");
425 &ref_name,
426 &commit_hash,
427 )
428 .await
429 .expect("Failed to check PR ref");
430 430
431 assert!( 431 assert!(
432 ref_correct, 432 ref_correct,
@@ -624,8 +624,8 @@ async fn test_concurrent_state_and_pr_sync() {
624 state_found.err() 624 state_found.err()
625 ); 625 );
626 626
627 let pr_found = wait_for_event_served(syncing_relay.url(), &pr_event_id, Duration::from_secs(30)) 627 let pr_found =
628 .await; 628 wait_for_event_served(syncing_relay.url(), &pr_event_id, Duration::from_secs(30)).await;
629 629
630 assert!( 630 assert!(
631 pr_found.is_ok(), 631 pr_found.is_ok(),
@@ -674,3 +674,461 @@ async fn test_concurrent_state_and_pr_sync() {
674 syncing_relay.stop().await; 674 syncing_relay.stop().await;
675 source_relay.stop().await; 675 source_relay.stop().await;
676} 676}
677
678/// Test PR event clone tag sync with relay discovery from announcement tags and partial git data sync
679/// from multiple servers (state and pr git data from different places)
680///
681/// This comprehensive test verifies:
682/// 1. Relay discovery: syncing_relay discovers other relays from announcement's `relays` tag
683/// 2. PR clone tag sync: PR events with `clone` tags have their URLs used during purgatory sync
684/// 3. OID aggregation: OIDs can be aggregated from multiple sources when no single server has all data
685///
686/// ## Key Difference from Bootstrap-Based Sync
687///
688/// Unlike tests that use bootstrap relay configuration, this test:
689/// - Starts syncing_relay with NO bootstrap relay
690/// - Publishes announcement DIRECTLY to syncing_relay
691/// - syncing_relay discovers source_grasp and mock_relay from announcement's `relays` tag
692///
693/// This validates the relay discovery mechanism that allows GRASP relays to find
694/// and sync from other relays listed in repository announcements.
695///
696/// ## Architecture
697///
698/// ```text
699/// ┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────────┐
700/// │ source_grasp │ │ mock_relay │ │ git_server │
701/// │ (GRASP relay) │ │ (rust-nostr relay) │ │ (SimpleGitServer) │
702/// │ │ │ │ │ │
703/// │ Has: │ │ Has: │ │ Has: │
704/// │ - Announcement │ │ - PR event │ │ - PR commit (commit_b) │
705/// │ - State event (served) │ │ (served immediately, │ │ at refs/heads/main │
706/// │ - refs/heads/main │ │ no purgatory) │ │ │
707/// │ → commit_a │ │ │ │ Does NOT have: │
708/// │ │ │ PR event has clone tag │ │ - commit_a │
709/// │ Does NOT have: │ │ pointing to git_server │ │ │
710/// │ - PR commit (commit_b) │ │ │ │ │
711/// └─────────────────────────┘ └─────────────────────────┘ └─────────────────────────┘
712/// │ │ │
713/// └───────────────────────────────┼───────────────────────────────┘
714/// ▼
715/// ┌─────────────────────────────────────────────────────────────────────────────────────────┐
716/// │ syncing_relay (GRASP relay under test) │
717/// │ │
718/// │ Flow: │
719/// │ 1. Started with NO bootstrap relay (sync enabled but no initial connections) │
720/// │ 2. Announcement published DIRECTLY to syncing_relay │
721/// │ 3. Relay discovers source_grasp and mock_relay from announcement's `relays` tag │
722/// │ 4. Syncs state event from source_grasp → purgatory (no commit_a locally) │
723/// │ 5. Syncs PR event from mock_relay → purgatory (no commit_b locally) │
724/// │ 6. Purgatory sync triggers │
725/// │ 7. Fetches commit_a from source_grasp clone URL (from announcement clone tag) │
726/// │ 8. Fetches commit_b from git_server (from PR event's clone tag) │
727/// │ 9. Both events released when all OIDs available │
728/// │ │
729/// │ Result: │
730/// │ - State event served │
731/// │ - PR event served │
732/// │ - refs/heads/main → commit_a (from source_grasp) │
733/// │ - refs/nostr/<event-id> → commit_b (from git_server via PR clone tag) │
734/// └─────────────────────────────────────────────────────────────────────────────────────────┘
735/// ```
736#[tokio::test]
737async fn test_pr_event_clone_tag_sync_with_partial_oid_aggregation_from_multiple_server() {
738 // ========================================================================
739 // Step 1: Setup Repositories
740 // ========================================================================
741
742 // Repo A: main branch with commit_a (for state event)
743 let repo_a = tempfile::tempdir().expect("Failed to create temp dir for repo_a");
744 let commit_a = create_test_repo_with_commit(repo_a.path(), CommitVariant::StateTest)
745 .expect("Failed to create commit_a");
746
747 // Repo B: PR commit (commit_b) - different content
748 let repo_b = tempfile::tempdir().expect("Failed to create temp dir for repo_b");
749 let commit_b = create_test_repo_with_commit(repo_b.path(), CommitVariant::PrTest)
750 .expect("Failed to create commit_b");
751
752 // ========================================================================
753 // Step 2: Start Servers
754 // ========================================================================
755
756 // 1. source_grasp - GRASP relay with main branch data
757 let source_grasp = TestRelay::start().await;
758
759 // 2. mock_relay - rust-nostr relay for PR event (no validation, no purgatory)
760 let mock_relay = MockRelay::start().await;
761
762 // 3. git_server - SimpleGitServer with PR commit only
763 let git_server = SimpleGitServer::start(repo_b.path()).await;
764
765 // 4. Pre-allocate syncing_relay port for announcement tags
766 let syncing_port = TestRelay::find_free_port();
767 let syncing_domain = format!("127.0.0.1:{}", syncing_port);
768
769 // ========================================================================
770 // Step 3: Setup source_grasp with announcement and state event
771 // ========================================================================
772
773 let owner_keys = Keys::generate();
774 let pr_author_keys = Keys::generate();
775 let identifier = "pr-clone-partial-oid-test";
776 let npub = owner_keys
777 .public_key()
778 .to_bech32()
779 .expect("Failed to get npub");
780
781 // Build URLs for announcement
782 // - clone tag: ONLY source_grasp (has main branch data)
783 // - relays tag: source_grasp + mock_relay (mock_relay will serve PR event)
784 let clone_url_source = format!(
785 "http://{}/{}/{}.git",
786 source_grasp.domain(),
787 npub,
788 identifier
789 );
790 let clone_url_syncing = format!("http://{}/{}/{}.git", syncing_domain, npub, identifier);
791
792 // Create announcement with custom clone/relay URLs
793 // Clone URLs: source_grasp + syncing (NOT git_server - PR commit only via PR's clone tag)
794 // Relay URLs: source_grasp + mock_relay + syncing
795 let announcement = nostr_sdk::EventBuilder::new(
796 nostr_sdk::Kind::Custom(30617),
797 "Repository for PR clone tag + partial OID test",
798 )
799 .tags(vec![
800 nostr_sdk::Tag::identifier(identifier),
801 nostr_sdk::Tag::custom(
802 nostr_sdk::TagKind::custom("clone"),
803 vec![clone_url_source.clone(), clone_url_syncing.clone()],
804 ),
805 nostr_sdk::Tag::custom(
806 nostr_sdk::TagKind::custom("relays"),
807 vec![
808 source_grasp.url().to_string(),
809 mock_relay.url().to_string(),
810 format!("ws://{}", syncing_domain),
811 ],
812 ),
813 ])
814 .sign_with_keys(&owner_keys)
815 .expect("Failed to sign announcement");
816
817 // Connect to source_grasp and send announcement
818 let source_client = Client::new(owner_keys.clone());
819 source_client
820 .add_relay(source_grasp.url())
821 .await
822 .expect("Failed to add source_grasp relay");
823 source_client.connect().await;
824 tokio::time::sleep(Duration::from_millis(500)).await;
825
826 source_client
827 .send_event(&announcement)
828 .await
829 .expect("Failed to send announcement to source_grasp");
830 tokio::time::sleep(Duration::from_millis(200)).await;
831
832 // Create state event referencing commit_a
833 let state_event = create_state_event(
834 &owner_keys,
835 identifier,
836 &[("main", &commit_a)],
837 &[],
838 &[&clone_url_source, &clone_url_syncing],
839 &[
840 source_grasp.url(),
841 mock_relay.url(),
842 &format!("ws://{}", syncing_domain),
843 ],
844 )
845 .expect("Failed to create state event");
846
847 let state_event_id = state_event.id;
848
849 // Send state event to source_grasp (goes to purgatory - no git data yet)
850 source_client
851 .send_event(&state_event)
852 .await
853 .expect("Failed to send state event to source_grasp");
854 tokio::time::sleep(Duration::from_millis(200)).await;
855
856 // Push main branch (commit_a) to source_grasp - releases state event
857 push_to_relay(repo_a.path(), &source_grasp.domain(), &npub, identifier)
858 .expect("Push to source_grasp should succeed");
859
860 // Verify state event is served on source_grasp
861 wait_for_event_served(source_grasp.url(), &state_event_id, Duration::from_secs(5))
862 .await
863 .expect("State event should be served on source_grasp after push");
864
865 // ========================================================================
866 // Step 4: Setup mock_relay with PR event
867 // ========================================================================
868
869 // First, send announcement to mock_relay so it has the repo context
870 // This is needed because the sync system filters events based on whether
871 // they reference repos that list our relay
872 let mock_client = Client::new(owner_keys.clone());
873 mock_client
874 .add_relay(mock_relay.url())
875 .await
876 .expect("Failed to add mock_relay for announcement");
877 mock_client.connect().await;
878 tokio::time::sleep(Duration::from_millis(500)).await;
879
880 mock_client
881 .send_event(&announcement)
882 .await
883 .expect("Failed to send announcement to mock_relay");
884 tokio::time::sleep(Duration::from_millis(200)).await;
885
886 let repo_coord = build_repo_coord(&owner_keys, identifier);
887
888 // Create PR event with clone tag pointing to git_server
889 // This is the KEY part - the PR's clone tag provides the URL for commit_b
890 let pr_event = create_pr_event_with_clone(
891 &pr_author_keys,
892 &repo_coord,
893 &commit_b,
894 "Test PR for partial OID aggregation",
895 &[git_server.url()], // Clone URL points to SimpleGitServer
896 )
897 .expect("Failed to create PR event");
898
899 let pr_event_id = pr_event.id;
900
901 // Send PR event to mock_relay
902 // MockRelay accepts all events without validation (no purgatory)
903 let pr_client = Client::new(pr_author_keys.clone());
904 pr_client
905 .add_relay(mock_relay.url())
906 .await
907 .expect("Failed to add mock_relay");
908 pr_client.connect().await;
909 tokio::time::sleep(Duration::from_millis(500)).await;
910
911 pr_client
912 .send_event(&pr_event)
913 .await
914 .expect("Failed to send PR event to mock_relay");
915
916 // Verify PR event is served on mock_relay (immediate, no purgatory)
917 wait_for_event_served(mock_relay.url(), &pr_event_id, Duration::from_secs(5))
918 .await
919 .expect("PR event should be served on mock_relay immediately");
920
921 // ========================================================================
922 // Step 5: Start syncing_relay WITHOUT bootstrap and publish announcement directly
923 // ========================================================================
924
925 // Start syncing_relay with sync enabled but NO bootstrap relay
926 // This tests relay discovery from announcement's `relays` tag
927 // Note: We disable negentropy because MockRelay doesn't support NIP-77,
928 // and the sync system doesn't properly fall back to REQ+EOSE when negentropy fails.
929 let syncing_relay = TestRelay::start_on_port_with_options(
930 syncing_port,
931 None, // NO bootstrap - relay discovery via announcement tags
932 true, // Disable negentropy - MockRelay doesn't support NIP-77
933 )
934 .await;
935
936 // Publish announcement DIRECTLY to syncing_relay
937 // This triggers relay discovery from the announcement's `relays` tag
938 let syncing_client = Client::new(owner_keys.clone());
939 syncing_client
940 .add_relay(syncing_relay.url())
941 .await
942 .expect("Failed to add syncing_relay");
943 syncing_client.connect().await;
944 tokio::time::sleep(Duration::from_millis(500)).await;
945
946 syncing_client
947 .send_event(&announcement)
948 .await
949 .expect("Failed to send announcement to syncing_relay");
950 tokio::time::sleep(Duration::from_millis(200)).await;
951
952 // Wait for relay discovery and sync connections to establish
953 // syncing_relay should discover source_grasp and mock_relay from announcement's relays tag
954 println!("=== Waiting for sync connections ===");
955 println!("syncing_relay URL: {}", syncing_relay.url());
956 println!("source_grasp URL: {}", source_grasp.url());
957 println!("mock_relay URL: {}", mock_relay.url());
958 println!("git_server URL: {}", git_server.url());
959
960 wait_for_sync_connection(syncing_relay.url(), 2, Duration::from_secs(10))
961 .await
962 .expect(
963 "Sync connections should establish to discovered relays (source_grasp + mock_relay)",
964 );
965 println!("Sync connections established!");
966
967 // Debug: Check metrics to see what relays are connected
968 let metrics_url = syncing_relay
969 .url()
970 .replace("ws://", "http://")
971 .replace("/", "")
972 + "/metrics";
973 println!("Checking metrics at: {}", metrics_url);
974 if let Ok(response) = reqwest::get(&metrics_url).await {
975 if let Ok(metrics) = response.text().await {
976 // Print sync-related metrics
977 for line in metrics.lines() {
978 if line.contains("sync") && !line.starts_with('#') {
979 println!(" {}", line);
980 }
981 }
982 }
983 }
984
985 // Give some time for sync to happen
986 println!("Waiting 10s for events to sync...");
987 tokio::time::sleep(Duration::from_secs(10)).await;
988
989 // Check metrics again after waiting
990 println!("=== Checking metrics after sync wait ===");
991 if let Ok(response) = reqwest::get(&metrics_url).await {
992 if let Ok(metrics) = response.text().await {
993 for line in metrics.lines() {
994 if line.contains("sync") && !line.starts_with('#') {
995 println!(" {}", line);
996 }
997 }
998 }
999 }
1000
1001 // Debug: Check if PR event is still on mock_relay
1002 println!("=== Debug: Checking PR event on mock_relay ===");
1003 let pr_on_mock =
1004 wait_for_event_served(mock_relay.url(), &pr_event_id, Duration::from_secs(2)).await;
1005 println!("PR event on mock_relay: {:?}", pr_on_mock.is_ok());
1006 if let Ok(ref pr) = pr_on_mock {
1007 println!("PR event tags:");
1008 for tag in pr.tags.iter() {
1009 println!(" {:?}", tag.as_slice());
1010 }
1011 }
1012
1013 // Debug: Check repo coordinate
1014 let repo_coord = build_repo_coord(&owner_keys, identifier);
1015 println!("Expected repo coordinate: {}", repo_coord);
1016
1017 // Debug: Test if mock_relay responds to tag-based filter (Layer 2 style)
1018 println!("=== Debug: Testing mock_relay tag filter response ===");
1019 let test_client = Client::new(Keys::generate());
1020 test_client
1021 .add_relay(mock_relay.url())
1022 .await
1023 .expect("Failed to add mock_relay");
1024 test_client.connect().await;
1025 tokio::time::sleep(Duration::from_millis(500)).await;
1026
1027 // Build a Layer 2 style filter (by 'a' tag)
1028 let tag_filter =
1029 Filter::new().custom_tag(SingleLetterTag::lowercase(Alphabet::A), repo_coord.as_str());
1030 println!("Tag filter: {:?}", tag_filter);
1031
1032 let tag_results = test_client
1033 .fetch_events(tag_filter, Duration::from_secs(5))
1034 .await;
1035 match tag_results {
1036 Ok(events) => {
1037 println!("Tag filter returned {} events", events.len());
1038 for event in events.iter() {
1039 println!(" Event ID: {}, Kind: {}", event.id, event.kind.as_u16());
1040 }
1041 }
1042 Err(e) => {
1043 println!("Tag filter query failed: {:?}", e);
1044 }
1045 }
1046 test_client.disconnect().await;
1047
1048 // The syncing relay will:
1049 // 1. Receive announcement directly (creates bare repo)
1050 // 2. Discover source_grasp and mock_relay from announcement's `relays` tag
1051 // 3. Connect to discovered relays
1052 // 4. Sync state event from source_grasp → purgatory (no commit_a locally)
1053 // 5. Sync PR event from mock_relay → purgatory (no commit_b locally)
1054 // 6. Purgatory sync triggers
1055 // 7. Fetches commit_a from source_grasp clone URL (from announcement clone tag)
1056 // 8. Fetches commit_b from git_server (from PR event's clone tag)
1057 // 9. Both events released when all OIDs available
1058
1059 // ========================================================================
1060 // Step 6: Verify Results
1061 // ========================================================================
1062
1063 println!("=== Step 6: Verify Results ===");
1064 println!("State event ID: {}", state_event_id);
1065 println!("PR event ID: {}", pr_event_id);
1066 println!("commit_a: {}", commit_a);
1067 println!("commit_b: {}", commit_b);
1068
1069 // Wait for state event to be served on syncing_relay
1070 println!("Waiting for state event on syncing_relay...");
1071 let state_found = wait_for_event_served(
1072 syncing_relay.url(),
1073 &state_event_id,
1074 Duration::from_secs(30),
1075 )
1076 .await;
1077 println!("State event result: {:?}", state_found);
1078 assert!(
1079 state_found.is_ok(),
1080 "State event should be served on syncing_relay: {:?}",
1081 state_found.err()
1082 );
1083
1084 // Wait for PR event to be served on syncing_relay
1085 println!("Waiting for PR event on syncing_relay...");
1086 let pr_found =
1087 wait_for_event_served(syncing_relay.url(), &pr_event_id, Duration::from_secs(30)).await;
1088 println!("PR event result: {:?}", pr_found);
1089 assert!(
1090 pr_found.is_ok(),
1091 "PR event should be served on syncing_relay (fetched commit_b from git_server via PR clone tag): {:?}",
1092 pr_found.err()
1093 );
1094
1095 // Verify refs/heads/main → commit_a (from source_grasp)
1096 let main_correct = check_ref_at_commit(
1097 &syncing_domain,
1098 &npub,
1099 identifier,
1100 "refs/heads/main",
1101 &commit_a,
1102 )
1103 .await
1104 .expect("Failed to check main ref");
1105 assert!(
1106 main_correct,
1107 "main should point to commit_a ({}) from source_grasp",
1108 commit_a
1109 );
1110
1111 // Verify refs/nostr/<event-id> → commit_b (from git_server via PR clone tag)
1112 let pr_ref = format!("refs/nostr/{}", pr_event_id.to_hex());
1113 let pr_correct = check_ref_at_commit(&syncing_domain, &npub, identifier, &pr_ref, &commit_b)
1114 .await
1115 .expect("Failed to check PR ref");
1116 assert!(
1117 pr_correct,
1118 "PR ref should point to commit_b ({}) fetched from git_server via PR clone tag",
1119 commit_b
1120 );
1121
1122 // ========================================================================
1123 // Step 7: Cleanup
1124 // ========================================================================
1125
1126 source_client.disconnect().await;
1127 mock_client.disconnect().await;
1128 pr_client.disconnect().await;
1129 syncing_client.disconnect().await;
1130 git_server.stop().await;
1131 mock_relay.stop().await;
1132 syncing_relay.stop().await;
1133 source_grasp.stop().await;
1134}