diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-23 15:41:32 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-23 15:41:32 +0000 |
| commit | c54ce061d6d278cce8362d5af085808ca60c239b (patch) | |
| tree | ec967d6195d9f7ec4f061449596611afe3a0950f /tests/common | |
| parent | e0ad39a489b3398f8208713bf728db0cb11475b0 (diff) | |
| parent | 113928aa84894ea8f65c247d9987527e792b32a9 (diff) | |
feat: announcement purgatory
Extends purgatory to hold repository announcements until git data arrives,
preventing empty repositories from being served to clients.
When an announcement is received, a bare repo is created immediately and the
announcement is held in purgatory. It is only promoted and served once a git
push confirms real content exists. If no push arrives before expiry, the bare
repo is deleted and the announcement is silently discarded.
Key behaviours:
- Soft expiry: announcements are hidden from clients but kept alive while git
pushes are in progress, reviving on successful push
- Expiry is extended when a matching state event or git push is observed
- NIP-09 deletion events remove announcements from purgatory
- Purgatory state (announcements, state events, PR events, expired set) is
persisted to disk on graceful shutdown and restored on startup, with elapsed
downtime subtracted from expiry deadlines
- Purgatory announcements drive StateOnly sync in the sync system so state
events are fetched from listed relays before promotion
- SyncLevel added to RepoSyncIndex to distinguish purgatory repos (StateOnly)
from promoted repos (Full L2+L3 sync)
Diffstat (limited to 'tests/common')
| -rw-r--r-- | tests/common/purgatory_helpers.rs | 38 | ||||
| -rw-r--r-- | tests/common/relay.rs | 13 | ||||
| -rw-r--r-- | tests/common/sync_helpers.rs | 423 |
3 files changed, 432 insertions, 42 deletions
diff --git a/tests/common/purgatory_helpers.rs b/tests/common/purgatory_helpers.rs index 1d06f22..cfcea1c 100644 --- a/tests/common/purgatory_helpers.rs +++ b/tests/common/purgatory_helpers.rs | |||
| @@ -338,6 +338,44 @@ pub fn build_repo_coord(keys: &Keys, identifier: &str) -> String { | |||
| 338 | format!("30617:{}:{}", keys.public_key().to_hex(), identifier) | 338 | format!("30617:{}:{}", keys.public_key().to_hex(), identifier) |
| 339 | } | 339 | } |
| 340 | 340 | ||
| 341 | /// Create a repository announcement event (kind 30617) for purgatory tests. | ||
| 342 | /// | ||
| 343 | /// Creates a minimal but valid NIP-34 repository announcement with a `d` tag, | ||
| 344 | /// optional `clone` URLs, and optional `relays` URLs. | ||
| 345 | /// | ||
| 346 | /// # Arguments | ||
| 347 | /// * `keys` - Keys for signing | ||
| 348 | /// * `identifier` - Repository identifier (d-tag) | ||
| 349 | /// * `clone_urls` - Clone URLs to include (may be empty) | ||
| 350 | /// * `relay_urls` - Relay URLs to include (may be empty) | ||
| 351 | /// | ||
| 352 | /// # Returns | ||
| 353 | /// * `Ok(Event)` - Signed announcement event | ||
| 354 | /// * `Err(String)` - If signing fails | ||
| 355 | pub fn create_announcement_event( | ||
| 356 | keys: &Keys, | ||
| 357 | identifier: &str, | ||
| 358 | clone_urls: &[&str], | ||
| 359 | relay_urls: &[&str], | ||
| 360 | ) -> Result<Event, String> { | ||
| 361 | let mut tags = vec![Tag::identifier(identifier)]; | ||
| 362 | |||
| 363 | if !clone_urls.is_empty() { | ||
| 364 | let urls: Vec<String> = clone_urls.iter().map(|s| s.to_string()).collect(); | ||
| 365 | tags.push(Tag::custom(TagKind::custom("clone"), urls)); | ||
| 366 | } | ||
| 367 | |||
| 368 | if !relay_urls.is_empty() { | ||
| 369 | let urls: Vec<String> = relay_urls.iter().map(|s| s.to_string()).collect(); | ||
| 370 | tags.push(Tag::custom(TagKind::custom("relays"), urls)); | ||
| 371 | } | ||
| 372 | |||
| 373 | EventBuilder::new(Kind::GitRepoAnnouncement, "") | ||
| 374 | .tags(tags) | ||
| 375 | .sign_with_keys(keys) | ||
| 376 | .map_err(|e| format!("Failed to sign announcement event: {}", e)) | ||
| 377 | } | ||
| 378 | |||
| 341 | /// Wait for an event to be served by a relay (not in purgatory). | 379 | /// Wait for an event to be served by a relay (not in purgatory). |
| 342 | /// | 380 | /// |
| 343 | /// Polls the relay until the event is queryable, indicating it has | 381 | /// Polls the relay until the event is queryable, indicating it has |
diff --git a/tests/common/relay.rs b/tests/common/relay.rs index 227849a..b1e96cf 100644 --- a/tests/common/relay.rs +++ b/tests/common/relay.rs | |||
| @@ -204,7 +204,7 @@ impl TestRelay { | |||
| 204 | .env("NGIT_GIT_DATA_PATH", git_data_dir.path()) | 204 | .env("NGIT_GIT_DATA_PATH", git_data_dir.path()) |
| 205 | .env("NGIT_DATABASE_BACKEND", "memory") // Force in-memory database for isolation | 205 | .env("NGIT_DATABASE_BACKEND", "memory") // Force in-memory database for isolation |
| 206 | .env("NGIT_OWNER_NPUB", &test_npub) | 206 | .env("NGIT_OWNER_NPUB", &test_npub) |
| 207 | .env("NGIT_SYNC_BATCH_WINDOW_MS", "200") // Fast batch window for tests (200ms instead of 5s default) | 207 | .env("NGIT_TEST", "1") // Enable test mode: fast timers (200ms batch window, 200ms purgatory sync) |
| 208 | .env("NGIT_SYNC_STARTUP_DELAY_SECS", "0") // No startup delay for faster tests | 208 | .env("NGIT_SYNC_STARTUP_DELAY_SECS", "0") // No startup delay for faster tests |
| 209 | .env("NGIT_SYNC_STARTUP_JITTER_MS", "0") // No jitter for tests | 209 | .env("NGIT_SYNC_STARTUP_JITTER_MS", "0") // No jitter for tests |
| 210 | .env("NGIT_SYNC_DISCONNECT_CHECK_INTERVAL_SECS", "1") // Fast reconnect attempts for tests | 210 | .env("NGIT_SYNC_DISCONNECT_CHECK_INTERVAL_SECS", "1") // Fast reconnect attempts for tests |
| @@ -213,8 +213,15 @@ impl TestRelay { | |||
| 213 | "RUST_LOG", | 213 | "RUST_LOG", |
| 214 | std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()), | 214 | std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()), |
| 215 | ) // Use RUST_LOG from environment or default to info | 215 | ) // Use RUST_LOG from environment or default to info |
| 216 | .stdout(Stdio::null()) // Suppress stdout for cleaner test output | 216 | .stdout( |
| 217 | .stderr(Stdio::null()); // Suppress stderr for cleaner test output | 217 | std::fs::OpenOptions::new() |
| 218 | .create(true) | ||
| 219 | .append(true) | ||
| 220 | .open(format!("/tmp/relay-{}.log", port)) | ||
| 221 | .map(Stdio::from) | ||
| 222 | .unwrap_or(Stdio::null()), | ||
| 223 | ) | ||
| 224 | .stderr(Stdio::inherit()); // Inherit stderr for test output | ||
| 218 | 225 | ||
| 219 | // Add bootstrap relay URL if provided | 226 | // Add bootstrap relay URL if provided |
| 220 | if let Some(ref bootstrap_url) = bootstrap_relay_url { | 227 | if let Some(ref bootstrap_url) = bootstrap_relay_url { |
diff --git a/tests/common/sync_helpers.rs b/tests/common/sync_helpers.rs index 5fc2ad7..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; | 527 | |
| 534 | return false; | 528 | // Wait for connection |
| 535 | } | 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 | } | ||
| 536 | 538 | ||
| 537 | // Fetch events with the provided timeout | 539 | if connected { |
| 538 | let result = client.fetch_events(filter, timeout).await; | 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; | ||
| 539 | 544 | ||
| 540 | client.disconnect().await; | 545 | match result { |
| 546 | Ok(events) if !events.is_empty() => return true, | ||
| 547 | _ => {} | ||
| 548 | } | ||
| 549 | } else { | ||
| 550 | client.disconnect().await; | ||
| 551 | } | ||
| 541 | 552 | ||
| 542 | match result { | 553 | if tokio::time::Instant::now() >= deadline { |
| 543 | Ok(events) => !events.is_empty(), | 554 | return false; |
| 544 | Err(_) => 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]; |
| @@ -1071,12 +1088,16 @@ pub struct SyncTestResult { | |||
| 1071 | pub syncing_relay: TestRelay, | 1088 | pub syncing_relay: TestRelay, |
| 1072 | pub maintainer_keys: Keys, | 1089 | pub maintainer_keys: Keys, |
| 1073 | pub repo_coord: String, | 1090 | pub repo_coord: String, |
| 1091 | // Keep SmartGitServer alive for the test duration | ||
| 1092 | _git_server: Option<super::git_server::SmartGitServer>, | ||
| 1093 | // Keep temp dir alive for the test duration | ||
| 1094 | _git_temp_dir: Option<tempfile::TempDir>, | ||
| 1074 | } | 1095 | } |
| 1075 | 1096 | ||
| 1076 | /// Helper to send an event to a relay | 1097 | /// Helper to send an event to a relay |
| 1077 | /// | 1098 | /// |
| 1078 | /// Creates a temporary client, sends the event, and disconnects. | 1099 | /// Creates a temporary client, sends the event, and disconnects. |
| 1079 | async fn send_to_relay(relay: &TestRelay, event: &Event) -> Result<(), String> { | 1100 | pub async fn send_to_relay(relay: &TestRelay, event: &Event) -> Result<(), String> { |
| 1080 | let temp_keys = Keys::generate(); | 1101 | let temp_keys = Keys::generate(); |
| 1081 | let client = TestClient::new(relay.url(), temp_keys).await?; | 1102 | let client = TestClient::new(relay.url(), temp_keys).await?; |
| 1082 | client.send_event(event).await?; | 1103 | client.send_event(event).await?; |
| @@ -1084,6 +1105,270 @@ async fn send_to_relay(relay: &TestRelay, event: &Event) -> Result<(), String> { | |||
| 1084 | Ok(()) | 1105 | Ok(()) |
| 1085 | } | 1106 | } |
| 1086 | 1107 | ||
| 1108 | /// Helper to send an event to a relay by URL | ||
| 1109 | /// | ||
| 1110 | /// Creates a temporary client, sends the event, and disconnects. | ||
| 1111 | pub async fn send_to_relay_url(relay_url: &str, event: &Event) -> Result<(), String> { | ||
| 1112 | let temp_keys = Keys::generate(); | ||
| 1113 | let client = TestClient::new(relay_url, temp_keys).await?; | ||
| 1114 | client.send_event(event).await?; | ||
| 1115 | client.disconnect().await; | ||
| 1116 | Ok(()) | ||
| 1117 | } | ||
| 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 | |||
| 1087 | /// Unified sync test helper that automatically determines sync mode. | 1372 | /// Unified sync test helper that automatically determines sync mode. |
| 1088 | /// | 1373 | /// |
| 1089 | /// 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 |
| @@ -1119,6 +1404,10 @@ async fn send_to_relay(relay: &TestRelay, event: &Event) -> Result<(), String> { | |||
| 1119 | /// // Assert comment synced to result.syncing_relay | 1404 | /// // Assert comment synced to result.syncing_relay |
| 1120 | /// ``` | 1405 | /// ``` |
| 1121 | pub async fn run_sync_test(historic_events: &[Event], live_events: &[Event]) -> SyncTestResult { | 1406 | pub async fn run_sync_test(historic_events: &[Event], live_events: &[Event]) -> SyncTestResult { |
| 1407 | use super::purgatory_helpers::{ | ||
| 1408 | create_state_event, create_test_repo_with_commit, push_to_relay, CommitVariant, | ||
| 1409 | }; | ||
| 1410 | |||
| 1122 | // Validate usage - cannot provide events in both slices | 1411 | // Validate usage - cannot provide events in both slices |
| 1123 | let historic_mode = !historic_events.is_empty(); | 1412 | let historic_mode = !historic_events.is_empty(); |
| 1124 | let live_mode = !live_events.is_empty(); | 1413 | let live_mode = !live_events.is_empty(); |
| @@ -1137,39 +1426,93 @@ pub async fn run_sync_test(historic_events: &[Event], live_events: &[Event]) -> | |||
| 1137 | // 2. Start source relay | 1426 | // 2. Start source relay |
| 1138 | let source = TestRelay::start().await; | 1427 | let source = TestRelay::start().await; |
| 1139 | 1428 | ||
| 1140 | // 3. Create keys and announcement listing both relays | 1429 | // 3. Create local git repo with a commit |
| 1430 | let git_temp_dir = tempfile::tempdir().expect("Failed to create temp dir for git repo"); | ||
| 1431 | let commit_hash = create_test_repo_with_commit(git_temp_dir.path(), CommitVariant::StateTest) | ||
| 1432 | .expect("Failed to create test git repo"); | ||
| 1433 | |||
| 1434 | // 4. Create keys and build URLs | ||
| 1141 | let keys = Keys::generate(); | 1435 | let keys = Keys::generate(); |
| 1142 | let announcement = | 1436 | let npub = keys |
| 1143 | create_repo_announcement(&keys, &[&source.domain(), &syncing_domain], "test-repo"); | 1437 | .public_key() |
| 1438 | .to_bech32() | ||
| 1439 | .expect("Failed to convert public key to npub"); | ||
| 1440 | |||
| 1441 | // Clone URLs: source relay HTTP endpoint is where git data lives | ||
| 1442 | // The syncing relay's purgatory will fetch from source's clone URL | ||
| 1443 | let clone_url_source = format!("http://{}/{}/{}.git", source.domain(), npub, "test-repo"); | ||
| 1444 | let clone_url_syncing = format!("http://{}/{}/{}.git", syncing_domain, npub, "test-repo"); | ||
| 1144 | 1445 | ||
| 1145 | // 4. Send announcement + historic events to source BEFORE syncing relay starts | 1446 | let clone_urls = vec![clone_url_source.clone(), clone_url_syncing.clone()]; |
| 1447 | let relay_urls = vec![ | ||
| 1448 | format!("ws://{}", source.domain()), | ||
| 1449 | format!("ws://{}", syncing_domain), | ||
| 1450 | ]; | ||
| 1451 | |||
| 1452 | let announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "Repository state") | ||
| 1453 | .tags(vec![ | ||
| 1454 | Tag::identifier("test-repo"), | ||
| 1455 | Tag::custom(TagKind::custom("clone"), clone_urls.clone()), | ||
| 1456 | Tag::custom(TagKind::custom("relays"), relay_urls.clone()), | ||
| 1457 | ]) | ||
| 1458 | .sign_with_keys(&keys) | ||
| 1459 | .expect("Failed to sign repo announcement"); | ||
| 1460 | |||
| 1461 | // 5. Create state event referencing the commit | ||
| 1462 | let state_event = create_state_event( | ||
| 1463 | &keys, | ||
| 1464 | "test-repo", | ||
| 1465 | &[("main", &commit_hash)], | ||
| 1466 | &[], | ||
| 1467 | &clone_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(), | ||
| 1468 | &relay_urls.iter().map(|s| s.as_str()).collect::<Vec<_>>(), | ||
| 1469 | ) | ||
| 1470 | .expect("Failed to create state event"); | ||
| 1471 | |||
| 1472 | // 6. Send announcement + state event to source (both go to purgatory) | ||
| 1146 | send_to_relay(&source, &announcement) | 1473 | send_to_relay(&source, &announcement) |
| 1147 | .await | 1474 | .await |
| 1148 | .expect("Failed to send announcement"); | 1475 | .expect("Failed to send announcement"); |
| 1476 | send_to_relay(&source, &state_event) | ||
| 1477 | .await | ||
| 1478 | .expect("Failed to send state event"); | ||
| 1479 | |||
| 1480 | // 7. Git push to source relay → releases both announcement and state event from purgatory | ||
| 1481 | push_to_relay(git_temp_dir.path(), &source.domain(), &npub, "test-repo") | ||
| 1482 | .expect("Failed to push git data to source relay"); | ||
| 1483 | |||
| 1484 | // 8. Wait for source relay to process the push and release events from purgatory | ||
| 1485 | tokio::time::sleep(Duration::from_secs(2)).await; | ||
| 1486 | |||
| 1487 | // 9. Send historic events to source BEFORE syncing relay starts | ||
| 1149 | for event in historic_events { | 1488 | for event in historic_events { |
| 1150 | send_to_relay(&source, event) | 1489 | send_to_relay(&source, event) |
| 1151 | .await | 1490 | .await |
| 1152 | .expect("Failed to send historic event"); | 1491 | .expect("Failed to send historic event"); |
| 1153 | } | 1492 | } |
| 1154 | 1493 | ||
| 1155 | // 5. Start syncing relay (connects to source) | 1494 | // 10. Start syncing relay (connects to source) |
| 1156 | let syncing = | 1495 | let syncing = |
| 1157 | TestRelay::start_on_port_with_options(syncing_port, Some(source.url().into()), false).await; | 1496 | TestRelay::start_on_port_with_options(syncing_port, Some(source.url().into()), false).await; |
| 1158 | 1497 | ||
| 1159 | // 6. Wait for sync connection to establish | 1498 | // 11. Wait for sync connection to establish |
| 1160 | let _ = wait_for_sync_connection(syncing.url(), 1, Duration::from_secs(5)).await; | 1499 | let _ = wait_for_sync_connection(syncing.url(), 1, Duration::from_secs(5)).await; |
| 1161 | 1500 | ||
| 1162 | // 7. Send live events AFTER connection established | 1501 | // 12. Send live events AFTER connection established |
| 1163 | for event in live_events { | 1502 | for event in live_events { |
| 1164 | send_to_relay(&source, event) | 1503 | send_to_relay(&source, event) |
| 1165 | .await | 1504 | .await |
| 1166 | .expect("Failed to send live event"); | 1505 | .expect("Failed to send live event"); |
| 1167 | } | 1506 | } |
| 1168 | 1507 | ||
| 1169 | // 8. Allow sync to complete | 1508 | // 13. Allow sync + purgatory promotion to complete on the syncing relay. |
| 1170 | tokio::time::sleep(Duration::from_millis(100)).await; | 1509 | // The syncing relay receives the announcement (goes to purgatory) and state event. |
| 1510 | // The purgatory sync loop (1s interval) fetches git data from source's clone URL | ||
| 1511 | // (http://source-domain/npub/test-repo.git) and releases the announcement. | ||
| 1512 | // We wait up to 8s to allow time for this. | ||
| 1513 | tokio::time::sleep(Duration::from_secs(8)).await; | ||
| 1171 | 1514 | ||
| 1172 | // 9. Compute repo coordinate before moving keys | 1515 | // 14. Compute repo coordinate before moving keys |
| 1173 | let coordinate = repo_coord(&keys, "test-repo"); | 1516 | let coordinate = repo_coord(&keys, "test-repo"); |
| 1174 | 1517 | ||
| 1175 | SyncTestResult { | 1518 | SyncTestResult { |
| @@ -1177,6 +1520,8 @@ pub async fn run_sync_test(historic_events: &[Event], live_events: &[Event]) -> | |||
| 1177 | syncing_relay: syncing, | 1520 | syncing_relay: syncing, |
| 1178 | maintainer_keys: keys, | 1521 | maintainer_keys: keys, |
| 1179 | repo_coord: coordinate, | 1522 | repo_coord: coordinate, |
| 1523 | _git_server: None, | ||
| 1524 | _git_temp_dir: Some(git_temp_dir), | ||
| 1180 | } | 1525 | } |
| 1181 | } | 1526 | } |
| 1182 | 1527 | ||