diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-18 23:17:08 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-23 12:05:05 +0000 |
| commit | 49b9405dfcbb872686acdd7abc12dc9c94adc2ab (patch) | |
| tree | 2bd54765aad3853dddd68119c9143626ba3bfdaa /tests/common/sync_helpers.rs | |
| parent | 63865548b07e44d69321af3b03ca2c29aa60d74d (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.rs | 336 |
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 | /// ``` |
| 509 | pub async fn wait_for_event_on_relay(relay_url: &str, filter: Filter, timeout: Duration) -> bool { | 509 | pub 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 | ||
| 1133 | pub 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 | ||
| 1200 | pub 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. | ||
| 1306 | pub 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![ |