upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-13 13:24:46 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-13 17:29:23 +0000
commit1d09e4bdea7e328cf2740818df9df660c5532a99 (patch)
treedcb758a70a2e9b84709df247cc685a2f6423094e /tests
parenta2a99d5a4137b57e4141cf2840f2f51b38035cfa (diff)
feat: implement announcement purgatory core (breaks archive sync test)
Route new announcements to purgatory instead of accepting immediately. Announcements are promoted to the database when git data arrives, ensuring we only serve announcements for repos with actual content. Implemented: - AnnouncementPurgatoryEntry type and DashMap store - Route new announcements to purgatory (replacement announcements skip) - Promote announcements on git data arrival (process_purgatory_announcements) - Authorization checks purgatory announcements (fetch_repository_data_with_purgatory) - State policy uses purgatory announcements for maintainer validation - Cleanup task handles announcement expiry - Updated count()/cleanup() to 3-tuples Known broken: - test_archive_read_only_creates_bare_repo fails: sync module does not treat purgatory announcements as confirmed repos, so per-repo sync (state events, PRs) is never triggered for purgatory announcements - Announcement persistence (save/restore) not implemented - SyncLevel (StateOnly vs Full) not implemented - Soft expiry two-phase not implemented - Expiry extension on state event / git auth not wired up
Diffstat (limited to 'tests')
-rw-r--r--tests/archive_read_only.rs59
-rw-r--r--tests/purgatory.rs4
-rw-r--r--tests/purgatory_persistence.rs26
3 files changed, 61 insertions, 28 deletions
diff --git a/tests/archive_read_only.rs b/tests/archive_read_only.rs
index be6959b..e39b4b2 100644
--- a/tests/archive_read_only.rs
+++ b/tests/archive_read_only.rs
@@ -165,6 +165,7 @@ async fn test_archive_read_only_creates_bare_repo() {
165 // c) Put state event in purgatory (git data missing on archive relay) 165 // c) Put state event in purgatory (git data missing on archive relay)
166 // d) Fetch git data from source relay's clone URL 166 // d) Fetch git data from source relay's clone URL
167 // e) Release the state event from purgatory 167 // e) Release the state event from purgatory
168
168 let found = wait_for_event_served( 169 let found = wait_for_event_served(
169 archive_relay.url(), 170 archive_relay.url(),
170 &state_event_id, 171 &state_event_id,
@@ -267,11 +268,13 @@ async fn test_archive_read_only_creates_bare_repo() {
267/// This verifies the security model: archive mode only syncs git data 268/// This verifies the security model: archive mode only syncs git data
268/// when there are state events to validate against. 269/// when there are state events to validate against.
269/// 270///
270/// Scenario: 271/// With announcement purgatory, the flow is:
271/// 1. Start source relay with announcement only (no state events) 272/// 1. Send announcement to source relay (goes to purgatory)
272/// 2. Start archive relay syncing from source 273/// 2. Send state event to source relay (goes to purgatory)
273/// 3. Archive relay syncs announcement (creates bare repo) 274/// 3. Push git data to source relay (promotes announcement and state event)
274/// 4. Verify git data is NOT synced (no state events to trigger purgatory sync) 275/// 4. Start archive relay with sync from source
276/// 5. Archive relay syncs the promoted announcement
277/// 6. Verify git data is NOT synced (archive has no state event to authorize git fetch)
275#[tokio::test] 278#[tokio::test]
276async fn test_archive_without_state_events_does_not_sync_git() { 279async fn test_archive_without_state_events_does_not_sync_git() {
277 // 1. Start source relay 280 // 1. Start source relay
@@ -290,7 +293,7 @@ async fn test_archive_without_state_events_does_not_sync_git() {
290 293
291 let npub = keys.public_key().to_bech32().expect("Failed to get npub"); 294 let npub = keys.public_key().to_bech32().expect("Failed to get npub");
292 295
293 // 3. Create and send announcement listing BOTH relays (but NO state event) 296 // 3. Create and send announcement listing BOTH relays
294 let announcement = create_repo_announcement( 297 let announcement = create_repo_announcement(
295 &keys, 298 &keys,
296 &[&source_relay.domain(), &archive_domain], 299 &[&source_relay.domain(), &archive_domain],
@@ -306,7 +309,7 @@ async fn test_archive_without_state_events_does_not_sync_git() {
306 309
307 tokio::time::sleep(Duration::from_millis(500)).await; 310 tokio::time::sleep(Duration::from_millis(500)).await;
308 311
309 // Send announcement to source relay 312 // Send announcement to source relay (goes to purgatory)
310 source_client 313 source_client
311 .send_event(&announcement) 314 .send_event(&announcement)
312 .await 315 .await
@@ -314,11 +317,39 @@ async fn test_archive_without_state_events_does_not_sync_git() {
314 317
315 tokio::time::sleep(Duration::from_millis(200)).await; 318 tokio::time::sleep(Duration::from_millis(200)).await;
316 319
317 // 4. Push git data to source relay (but no state event to authorize it) 320 // 4. Create and send state event to source relay (goes to purgatory)
318 // This push will fail because there's no state event in purgatory 321 let clone_url = format!(
319 // That's expected - we're testing that archive mode doesn't blindly fetch git data 322 "http://{}/{}/{}.git",
323 source_relay.domain(),
324 npub,
325 identifier
326 );
327 let relay_url = source_relay.url().to_string();
328
329 let state_event = create_state_event(
330 &keys,
331 identifier,
332 &[("main", &commit_hash)],
333 &[],
334 &[&clone_url],
335 &[&relay_url],
336 )
337 .expect("Failed to create state event");
338
339 source_client
340 .send_event(&state_event)
341 .await
342 .expect("Failed to send state event to source");
343
344 tokio::time::sleep(Duration::from_millis(200)).await;
345
346 // 5. Push git data to source relay (promotes announcement and state event)
347 push_to_relay(temp_dir.path(), &source_relay.domain(), &npub, identifier)
348 .expect("Push to source should succeed");
349
350 tokio::time::sleep(Duration::from_millis(500)).await;
320 351
321 // 5. Start archive relay 352 // 6. Start archive relay (without state event - we don't send state event to archive)
322 let archive_relay = TestRelay::start_with_archive_and_sync( 353 let archive_relay = TestRelay::start_with_archive_and_sync(
323 archive_port, 354 archive_port,
324 Some(source_relay.url().to_string()), 355 Some(source_relay.url().to_string()),
@@ -333,10 +364,10 @@ async fn test_archive_without_state_events_does_not_sync_git() {
333 .await 364 .await
334 .expect("Sync connection should establish"); 365 .expect("Sync connection should establish");
335 366
336 // Give time for any potential git sync to happen 367 // Give time for sync to fetch announcement
337 tokio::time::sleep(Duration::from_secs(3)).await; 368 tokio::time::sleep(Duration::from_secs(3)).await;
338 369
339 // 6. Verify bare repository was created (announcement was accepted) 370 // 7. Verify bare repository was created (announcement was synced and accepted to purgatory)
340 let repo_path = archive_relay 371 let repo_path = archive_relay
341 .git_data_path() 372 .git_data_path()
342 .join(format!("{}/{}.git", npub, identifier)); 373 .join(format!("{}/{}.git", npub, identifier));
@@ -346,7 +377,7 @@ async fn test_archive_without_state_events_does_not_sync_git() {
346 "Bare repository should be created for archive announcement" 377 "Bare repository should be created for archive announcement"
347 ); 378 );
348 379
349 // 7. Verify git data was NOT synced (no state events to trigger purgatory sync) 380 // 8. Verify git data was NOT synced (no state events on archive to trigger git fetch)
350 // Check that the commit does NOT exist in the archive relay's repo 381 // Check that the commit does NOT exist in the archive relay's repo
351 let output = tokio::process::Command::new("git") 382 let output = tokio::process::Command::new("git")
352 .args(["cat-file", "-t", &commit_hash]) 383 .args(["cat-file", "-t", &commit_hash])
diff --git a/tests/purgatory.rs b/tests/purgatory.rs
index e99540b..efc28c9 100644
--- a/tests/purgatory.rs
+++ b/tests/purgatory.rs
@@ -58,10 +58,10 @@ macro_rules! isolated_purgatory_test {
58} 58}
59 59
60// ============================================================ 60// ============================================================
61// Announcement Purgatory Tests (commented out - feature not yet implemented) 61// Announcement Purgatory Tests
62// ============================================================ 62// ============================================================
63 63
64// isolated_purgatory_test!(test_announcement_not_served_before_git_data); 64isolated_purgatory_test!(test_announcement_not_served_before_git_data);
65isolated_purgatory_test!(test_announcement_served_after_git_push); 65isolated_purgatory_test!(test_announcement_served_after_git_push);
66isolated_purgatory_test!(test_bare_repo_exists_for_purgatory_announcement); 66isolated_purgatory_test!(test_bare_repo_exists_for_purgatory_announcement);
67isolated_purgatory_test!(test_state_event_accepted_for_purgatory_announcement); 67isolated_purgatory_test!(test_state_event_accepted_for_purgatory_announcement);
diff --git a/tests/purgatory_persistence.rs b/tests/purgatory_persistence.rs
index fe37c33..5abbf15 100644
--- a/tests/purgatory_persistence.rs
+++ b/tests/purgatory_persistence.rs
@@ -120,7 +120,8 @@ async fn test_full_purgatory_save_restore_cycle() {
120 // so we'll focus on testing state and PR events persistence 120 // so we'll focus on testing state and PR events persistence
121 121
122 // Verify initial counts 122 // Verify initial counts
123 let (state_count, pr_count) = purgatory.count(); 123 let (announcement_count, state_count, pr_count) = purgatory.count();
124 assert_eq!(announcement_count, 0, "Should have 0 announcements");
124 assert_eq!(state_count, 2, "Should have 2 state events"); 125 assert_eq!(state_count, 2, "Should have 2 state events");
125 assert_eq!( 126 assert_eq!(
126 pr_count, 3, 127 pr_count, 3,
@@ -142,7 +143,8 @@ async fn test_full_purgatory_save_restore_cycle() {
142 ); 143 );
143 144
144 // Verify all data was restored 145 // Verify all data was restored
145 let (state_count2, pr_count2) = purgatory2.count(); 146 let (announcement_count2, state_count2, pr_count2) = purgatory2.count();
147 assert_eq!(announcement_count2, 0, "Should have 0 announcements after restore");
146 assert_eq!(state_count2, 2, "Should have 2 state events after restore"); 148 assert_eq!(state_count2, 2, "Should have 2 state events after restore");
147 assert_eq!( 149 assert_eq!(
148 pr_count2, 3, 150 pr_count2, 3,
@@ -275,7 +277,7 @@ async fn test_purgatory_downtime_adjustment() {
275 purgatory2.restore_from_disk(&state_path).unwrap(); 277 purgatory2.restore_from_disk(&state_path).unwrap();
276 278
277 // Verify event is still there (downtime was accounted for) 279 // Verify event is still there (downtime was accounted for)
278 let (state_count, _) = purgatory2.count(); 280 let (_, state_count, _) = purgatory2.count();
279 assert_eq!(state_count, 1); 281 assert_eq!(state_count, 1);
280 282
281 let repo1_states = purgatory2.find_state("repo1"); 283 let repo1_states = purgatory2.find_state("repo1");
@@ -401,7 +403,7 @@ async fn test_purgatory_restore_missing_file() {
401 assert!(result.is_err(), "Should error on missing file"); 403 assert!(result.is_err(), "Should error on missing file");
402 404
403 // Purgatory should still be usable (empty state) 405 // Purgatory should still be usable (empty state)
404 let (state_count, pr_count) = purgatory.count(); 406 let (_, state_count, pr_count) = purgatory.count();
405 assert_eq!(state_count, 0); 407 assert_eq!(state_count, 0);
406 assert_eq!(pr_count, 0); 408 assert_eq!(pr_count, 0);
407 409
@@ -410,7 +412,7 @@ async fn test_purgatory_restore_missing_file() {
410 let event = create_test_event(&keys, "test").await; 412 let event = create_test_event(&keys, "test").await;
411 purgatory.add_state(event, "repo1".to_string(), keys.public_key()); 413 purgatory.add_state(event, "repo1".to_string(), keys.public_key());
412 414
413 let (state_count, _) = purgatory.count(); 415 let (_, state_count, _) = purgatory.count();
414 assert_eq!(state_count, 1); 416 assert_eq!(state_count, 1);
415} 417}
416 418
@@ -461,7 +463,7 @@ async fn test_purgatory_restore_corrupted_file() {
461 assert!(result.is_err(), "Should error on corrupted file"); 463 assert!(result.is_err(), "Should error on corrupted file");
462 464
463 // Purgatory should still be usable 465 // Purgatory should still be usable
464 let (state_count, pr_count) = purgatory.count(); 466 let (_, state_count, pr_count) = purgatory.count();
465 assert_eq!(state_count, 0); 467 assert_eq!(state_count, 0);
466 assert_eq!(pr_count, 0); 468 assert_eq!(pr_count, 0);
467} 469}
@@ -504,7 +506,7 @@ async fn test_empty_purgatory_save_restore() {
504 purgatory2.restore_from_disk(&state_path).unwrap(); 506 purgatory2.restore_from_disk(&state_path).unwrap();
505 507
506 // Verify empty state 508 // Verify empty state
507 let (state_count, pr_count) = purgatory2.count(); 509 let (_, state_count, pr_count) = purgatory2.count();
508 assert_eq!(state_count, 0); 510 assert_eq!(state_count, 0);
509 assert_eq!(pr_count, 0); 511 assert_eq!(pr_count, 0);
510 assert_eq!(purgatory2.expired_count(), 0); 512 assert_eq!(purgatory2.expired_count(), 0);
@@ -591,7 +593,7 @@ async fn test_purgatory_continues_working_after_restore() {
591 purgatory2.add_state(event2.clone(), "repo2".to_string(), keys.public_key()); 593 purgatory2.add_state(event2.clone(), "repo2".to_string(), keys.public_key());
592 594
593 // Verify both old and new events work 595 // Verify both old and new events work
594 let (state_count, _) = purgatory2.count(); 596 let (_, state_count, _) = purgatory2.count();
595 assert_eq!(state_count, 2); 597 assert_eq!(state_count, 2);
596 598
597 let repo1_states = purgatory2.find_state("repo1"); 599 let repo1_states = purgatory2.find_state("repo1");
@@ -603,7 +605,7 @@ async fn test_purgatory_continues_working_after_restore() {
603 assert_eq!(repo2_states[0].event.id, event2.id); 605 assert_eq!(repo2_states[0].event.id, event2.id);
604 606
605 // Verify cleanup still works 607 // Verify cleanup still works
606 let (state_removed, pr_removed) = purgatory2.cleanup(); 608 let (_, state_removed, pr_removed) = purgatory2.cleanup();
607 // Nothing should be expired yet 609 // Nothing should be expired yet
608 assert_eq!(state_removed, 0); 610 assert_eq!(state_removed, 0);
609 assert_eq!(pr_removed, 0); 611 assert_eq!(pr_removed, 0);
@@ -684,15 +686,15 @@ async fn test_purgatory_entries_expired_during_downtime() {
684 purgatory2.restore_from_disk(&state_path).unwrap(); 686 purgatory2.restore_from_disk(&state_path).unwrap();
685 687
686 // Event should be restored 688 // Event should be restored
687 let (state_count, _) = purgatory2.count(); 689 let (_, state_count, _) = purgatory2.count();
688 assert_eq!(state_count, 1); 690 assert_eq!(state_count, 1);
689 691
690 // Cleanup should work (even if nothing is expired yet) 692 // Cleanup should work (even if nothing is expired yet)
691 let (state_removed, _) = purgatory2.cleanup(); 693 let (_, state_removed, _) = purgatory2.cleanup();
692 // Nothing expired yet since we didn't wait 30 minutes 694 // Nothing expired yet since we didn't wait 30 minutes
693 assert_eq!(state_removed, 0); 695 assert_eq!(state_removed, 0);
694 696
695 let (state_count, _) = purgatory2.count(); 697 let (_, state_count, _) = purgatory2.count();
696 assert_eq!(state_count, 1); 698 assert_eq!(state_count, 1);
697} 699}
698 700