upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests/common/sync_helpers.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-18 23:17:08 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-23 12:05:05 +0000
commit49b9405dfcbb872686acdd7abc12dc9c94adc2ab (patch)
tree2bd54765aad3853dddd68119c9143626ba3bfdaa /tests/common/sync_helpers.rs
parent63865548b07e44d69321af3b03ca2c29aa60d74d (diff)
test: update sync tests to set up git data for purgatory flow
All sync tests now create a local git repo, send announcement + state event to the source relay, and push git data to release both from purgatory before the syncing relay starts bootstrap sync.
Diffstat (limited to 'tests/common/sync_helpers.rs')
-rw-r--r--tests/common/sync_helpers.rs336
1 files changed, 301 insertions, 35 deletions
diff --git a/tests/common/sync_helpers.rs b/tests/common/sync_helpers.rs
index daa684b..af51e78 100644
--- a/tests/common/sync_helpers.rs
+++ b/tests/common/sync_helpers.rs
@@ -507,41 +507,53 @@ fn check_sync_connections_in_metrics(metrics: &str, expected: usize) -> bool {
507/// assert!(found, "Expected event {} to sync to relay", event.id); 507/// assert!(found, "Expected event {} to sync to relay", event.id);
508/// ``` 508/// ```
509pub async fn wait_for_event_on_relay(relay_url: &str, filter: Filter, timeout: Duration) -> bool { 509pub async fn wait_for_event_on_relay(relay_url: &str, filter: Filter, timeout: Duration) -> bool {
510 // Create a temporary client for querying 510 let deadline = tokio::time::Instant::now() + timeout;
511 let temp_keys = Keys::generate(); 511 let poll_interval = Duration::from_millis(200);
512 let client = Client::new(temp_keys);
513
514 // Try to connect
515 if client.add_relay(relay_url).await.is_err() {
516 return false;
517 }
518 512
519 client.connect().await; 513 loop {
514 // Create a fresh client for each poll attempt (avoids stale connection state)
515 let temp_keys = Keys::generate();
516 let client = Client::new(temp_keys);
520 517
521 // Wait for connection (brief timeout) 518 if client.add_relay(relay_url).await.is_err() {
522 let mut connected = false; 519 if tokio::time::Instant::now() >= deadline {
523 for _ in 0..10 { 520 return false;
524 tokio::time::sleep(Duration::from_millis(100)).await; 521 }
525 let relays = client.relays().await; 522 tokio::time::sleep(poll_interval).await;
526 if relays.values().any(|r| r.is_connected()) { 523 continue;
527 connected = true;
528 break;
529 } 524 }
530 }
531 525
532 if !connected { 526 client.connect().await;
533 client.disconnect().await;
534 return false;
535 }
536 527
537 // Fetch events with the provided timeout 528 // Wait for connection
538 let result = client.fetch_events(filter, timeout).await; 529 let mut connected = false;
530 for _ in 0..10 {
531 tokio::time::sleep(Duration::from_millis(100)).await;
532 let relays = client.relays().await;
533 if relays.values().any(|r| r.is_connected()) {
534 connected = true;
535 break;
536 }
537 }
539 538
540 client.disconnect().await; 539 if connected {
540 // Use a short fetch window — if the event is there, EOSE comes back quickly
541 let fetch_timeout = Duration::from_millis(500);
542 let result = client.fetch_events(filter.clone(), fetch_timeout).await;
543 client.disconnect().await;
541 544
542 match result { 545 match result {
543 Ok(events) => !events.is_empty(), 546 Ok(events) if !events.is_empty() => return true,
544 Err(_) => false, 547 _ => {}
548 }
549 } else {
550 client.disconnect().await;
551 }
552
553 if tokio::time::Instant::now() >= deadline {
554 return false;
555 }
556 tokio::time::sleep(poll_interval).await;
545 } 557 }
546} 558}
547 559
@@ -774,6 +786,11 @@ impl MetricsTestHarness {
774 self.source_relays[idx].domain() 786 self.source_relays[idx].domain()
775 } 787 }
776 788
789 /// Get a reference to a source relay (for advanced test operations)
790 pub fn source_relay(&self, idx: usize) -> &TestRelay {
791 &self.source_relays[idx]
792 }
793
777 /// Submit events to a specific source relay 794 /// Submit events to a specific source relay
778 pub async fn submit_events(&self, source_idx: usize, events: &[Event]) -> Result<(), String> { 795 pub async fn submit_events(&self, source_idx: usize, events: &[Event]) -> Result<(), String> {
779 let relay = &self.source_relays[source_idx]; 796 let relay = &self.source_relays[source_idx];
@@ -1099,6 +1116,259 @@ pub async fn send_to_relay_url(relay_url: &str, event: &Event) -> Result<(), Str
1099 Ok(()) 1116 Ok(())
1100} 1117}
1101 1118
1119/// Push git repository data to a relay to release a purgatory-held announcement.
1120///
1121/// Creates a local git repo, sends a state event, and pushes to the relay.
1122/// Use this when you need to build a custom announcement but still need the
1123/// relay to accept it (i.e., release it from purgatory).
1124///
1125/// # Arguments
1126/// * `relay` - The relay to push to
1127/// * `keys` - Keys of the repository owner
1128/// * `identifier` - Repository identifier
1129/// * `domains` - All domains in the announcement (for state event URLs)
1130///
1131/// # Returns
1132/// `tempfile::TempDir` - Keep alive for test duration
1133pub async fn push_git_data_to_relay(
1134 relay: &TestRelay,
1135 keys: &Keys,
1136 identifier: &str,
1137 domains: &[&str],
1138) -> tempfile::TempDir {
1139 use super::purgatory_helpers::{
1140 create_state_event, create_test_repo_with_commit, push_to_relay, CommitVariant,
1141 };
1142
1143 let npub = keys
1144 .public_key()
1145 .to_bech32()
1146 .expect("Failed to convert public key to npub");
1147
1148 // Create local git repo
1149 let git_temp_dir = tempfile::tempdir().expect("Failed to create temp dir for git repo");
1150 let commit_hash = create_test_repo_with_commit(git_temp_dir.path(), CommitVariant::StateTest)
1151 .expect("Failed to create test git repo");
1152
1153 let clone_urls: Vec<String> = domains
1154 .iter()
1155 .map(|d| format!("http://{}/{}/{}.git", d, npub, identifier))
1156 .collect();
1157 let relay_urls: Vec<String> = domains.iter().map(|d| format!("ws://{}", d)).collect();
1158
1159 // Build and send state event with all domains' clone URLs
1160 let state_event = create_state_event(
1161 keys,
1162 identifier,
1163 &[("main", &commit_hash)],
1164 &[],
1165 &clone_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1166 &relay_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1167 )
1168 .expect("Failed to create state event");
1169
1170 send_to_relay(relay, &state_event)
1171 .await
1172 .expect("Failed to send state event");
1173
1174 // Git push to relay → releases state event from purgatory, authorizes push
1175 push_to_relay(git_temp_dir.path(), &relay.domain(), &npub, identifier)
1176 .expect("Failed to push git data to relay");
1177
1178 // Brief wait for push processing
1179 tokio::time::sleep(Duration::from_millis(500)).await;
1180
1181 git_temp_dir
1182}
1183
1184/// Like `push_git_data_to_relay` but writes a unique marker file so each call
1185/// produces a distinct commit hash.
1186///
1187/// Use this when multiple callers push to the same relay with the same identifier
1188/// but different keys — identical commit hashes cause git to skip pack transfer,
1189/// which can leave the announcement in purgatory.
1190///
1191/// # Arguments
1192/// * `relay` - The relay to push to
1193/// * `keys` - Keys of the repository owner
1194/// * `identifier` - Repository identifier
1195/// * `domains` - All domains in the announcement (for state event URLs)
1196/// * `unique_seed` - A string written into a `.unique` file to differentiate commits
1197///
1198/// # Returns
1199/// `tempfile::TempDir` - Keep alive for test duration
1200pub async fn push_unique_git_data_to_relay(
1201 relay: &TestRelay,
1202 keys: &Keys,
1203 identifier: &str,
1204 domains: &[&str],
1205 unique_seed: &str,
1206) -> tempfile::TempDir {
1207 use super::purgatory_helpers::{create_state_event, push_to_relay};
1208
1209 let npub = keys
1210 .public_key()
1211 .to_bech32()
1212 .expect("Failed to convert public key to npub");
1213
1214 let git_temp_dir = tempfile::tempdir().expect("Failed to create temp dir for git repo");
1215 let path = git_temp_dir.path();
1216
1217 fn git(path: &std::path::Path, args: &[&str]) {
1218 let status = std::process::Command::new("git")
1219 .args(args)
1220 .current_dir(path)
1221 .env("GIT_AUTHOR_NAME", "Test User")
1222 .env("GIT_AUTHOR_EMAIL", "test@example.com")
1223 .env("GIT_COMMITTER_NAME", "Test User")
1224 .env("GIT_COMMITTER_EMAIL", "test@example.com")
1225 .env("GIT_AUTHOR_DATE", "2024-01-01T00:00:00+00:00")
1226 .env("GIT_COMMITTER_DATE", "2024-01-01T00:00:00+00:00")
1227 .output()
1228 .unwrap_or_else(|e| panic!("git {:?} failed to spawn: {}", args, e));
1229 assert!(
1230 status.status.success(),
1231 "git {:?} failed: {}",
1232 args,
1233 String::from_utf8_lossy(&status.stderr)
1234 );
1235 }
1236
1237 git(path, &["init", "--initial-branch=main"]);
1238 git(path, &["config", "user.email", "test@example.com"]);
1239 git(path, &["config", "user.name", "Test User"]);
1240 git(path, &["config", "commit.gpgsign", "false"]);
1241
1242 // Write a unique file so each maintainer gets a distinct commit hash
1243 std::fs::write(path.join("state_test.txt"), "State test content for purgatory sync")
1244 .expect("write state_test.txt");
1245 std::fs::write(path.join(".unique"), unique_seed).expect("write .unique");
1246 git(path, &["add", "."]);
1247 git(path, &["commit", "-m", "State test commit"]);
1248
1249 let commit_hash = {
1250 let out = std::process::Command::new("git")
1251 .args(["rev-parse", "HEAD"])
1252 .current_dir(path)
1253 .output()
1254 .expect("git rev-parse");
1255 String::from_utf8_lossy(&out.stdout).trim().to_string()
1256 };
1257
1258 let clone_urls: Vec<String> = domains
1259 .iter()
1260 .map(|d| format!("http://{}/{}/{}.git", d, npub, identifier))
1261 .collect();
1262 let relay_urls: Vec<String> = domains.iter().map(|d| format!("ws://{}", d)).collect();
1263
1264 let state_event = create_state_event(
1265 keys,
1266 identifier,
1267 &[("main", &commit_hash)],
1268 &[],
1269 &clone_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1270 &relay_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1271 )
1272 .expect("Failed to create state event");
1273
1274 send_to_relay(relay, &state_event)
1275 .await
1276 .expect("Failed to send state event");
1277
1278 push_to_relay(path, &relay.domain(), &npub, identifier)
1279 .expect("Failed to push git data to relay");
1280
1281 tokio::time::sleep(Duration::from_millis(500)).await;
1282
1283 git_temp_dir
1284}
1285
1286/// Set up a repository announcement on a relay with git data so it passes purgatory.
1287///
1288/// With the announcement purgatory feature, announcements (kind 30617) require git
1289/// data before they are promoted to the relay's main DB. This helper:
1290///
1291/// 1. Creates a local git repo with a commit
1292/// 2. Builds an announcement and state event (kind 30618) pointing to the relay
1293/// 3. Sends both to the relay (they go to purgatory)
1294/// 4. Git pushes to the relay → releases both from purgatory immediately
1295/// 5. Returns the announcement event and temp dir (keep alive for test duration)
1296///
1297/// # Arguments
1298/// * `relay` - The relay to set up the announcement on
1299/// * `keys` - Keys to sign the announcement with (repo owner)
1300/// * `domains` - All domains that should be listed in the announcement (including relay.domain())
1301/// * `identifier` - Repository identifier (d-tag)
1302///
1303/// # Returns
1304/// `(Event, tempfile::TempDir)` - The announcement event and temp dir.
1305/// The temp dir MUST be kept alive for the duration of the test.
1306pub async fn setup_announcement_on_relay(
1307 relay: &TestRelay,
1308 keys: &Keys,
1309 domains: &[&str],
1310 identifier: &str,
1311) -> (Event, tempfile::TempDir) {
1312 use super::purgatory_helpers::{
1313 create_state_event, create_test_repo_with_commit, push_to_relay, CommitVariant,
1314 };
1315
1316 let npub = keys
1317 .public_key()
1318 .to_bech32()
1319 .expect("Failed to convert public key to npub");
1320
1321 // Create local git repo with a commit
1322 let git_temp_dir = tempfile::tempdir().expect("Failed to create temp dir for git repo");
1323 let commit_hash = create_test_repo_with_commit(git_temp_dir.path(), CommitVariant::StateTest)
1324 .expect("Failed to create test git repo");
1325
1326 // Build clone URLs and relay URLs from domains
1327 let clone_urls: Vec<String> = domains
1328 .iter()
1329 .map(|d| format!("http://{}/{}/{}.git", d, npub, identifier))
1330 .collect();
1331 let relay_urls: Vec<String> = domains.iter().map(|d| format!("ws://{}", d)).collect();
1332
1333 // Build announcement event (lists ALL domains for relay discovery)
1334 let announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "Repository state")
1335 .tags(vec![
1336 Tag::identifier(identifier),
1337 Tag::custom(TagKind::custom("clone"), clone_urls.clone()),
1338 Tag::custom(TagKind::custom("relays"), relay_urls.clone()),
1339 ])
1340 .sign_with_keys(keys)
1341 .expect("Failed to sign repo announcement");
1342
1343 // Build state event with all domains' clone URLs
1344 let state_event = create_state_event(
1345 keys,
1346 identifier,
1347 &[("main", &commit_hash)],
1348 &[],
1349 &clone_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1350 &relay_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1351 )
1352 .expect("Failed to create state event");
1353
1354 // Send announcement and state event to relay (both go to purgatory)
1355 send_to_relay(relay, &announcement)
1356 .await
1357 .expect("Failed to send announcement");
1358 send_to_relay(relay, &state_event)
1359 .await
1360 .expect("Failed to send state event");
1361
1362 // Git push to relay → releases both from purgatory
1363 push_to_relay(git_temp_dir.path(), &relay.domain(), &npub, identifier)
1364 .expect("Failed to push git data to relay");
1365
1366 // Brief wait for push processing
1367 tokio::time::sleep(Duration::from_millis(500)).await;
1368
1369 (announcement, git_temp_dir)
1370}
1371
1102/// Unified sync test helper that automatically determines sync mode. 1372/// Unified sync test helper that automatically determines sync mode.
1103/// 1373///
1104/// This function sets up a complete sync test environment by determining whether 1374/// This function sets up a complete sync test environment by determining whether
@@ -1158,9 +1428,8 @@ pub async fn run_sync_test(historic_events: &[Event], live_events: &[Event]) ->
1158 1428
1159 // 3. Create local git repo with a commit 1429 // 3. Create local git repo with a commit
1160 let git_temp_dir = tempfile::tempdir().expect("Failed to create temp dir for git repo"); 1430 let git_temp_dir = tempfile::tempdir().expect("Failed to create temp dir for git repo");
1161 let commit_hash = 1431 let commit_hash = create_test_repo_with_commit(git_temp_dir.path(), CommitVariant::StateTest)
1162 create_test_repo_with_commit(git_temp_dir.path(), CommitVariant::StateTest) 1432 .expect("Failed to create test git repo");
1163 .expect("Failed to create test git repo");
1164 1433
1165 // 4. Create keys and build URLs 1434 // 4. Create keys and build URLs
1166 let keys = Keys::generate(); 1435 let keys = Keys::generate();
@@ -1172,10 +1441,7 @@ pub async fn run_sync_test(historic_events: &[Event], live_events: &[Event]) ->
1172 // Clone URLs: source relay HTTP endpoint is where git data lives 1441 // Clone URLs: source relay HTTP endpoint is where git data lives
1173 // The syncing relay's purgatory will fetch from source's clone URL 1442 // The syncing relay's purgatory will fetch from source's clone URL
1174 let clone_url_source = format!("http://{}/{}/{}.git", source.domain(), npub, "test-repo"); 1443 let clone_url_source = format!("http://{}/{}/{}.git", source.domain(), npub, "test-repo");
1175 let clone_url_syncing = format!( 1444 let clone_url_syncing = format!("http://{}/{}/{}.git", syncing_domain, npub, "test-repo");
1176 "http://{}/{}/{}.git",
1177 syncing_domain, npub, "test-repo"
1178 );
1179 1445
1180 let clone_urls = vec![clone_url_source.clone(), clone_url_syncing.clone()]; 1446 let clone_urls = vec![clone_url_source.clone(), clone_url_syncing.clone()];
1181 let relay_urls = vec![ 1447 let relay_urls = vec![