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 | |
| 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')
| -rw-r--r-- | tests/archive_grasp_services.rs | 225 | ||||
| -rw-r--r-- | tests/archive_read_only.rs | 368 | ||||
| -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 | ||||
| -rw-r--r-- | tests/nip77_negentropy.rs | 69 | ||||
| -rw-r--r-- | tests/purgatory.rs | 89 | ||||
| -rw-r--r-- | tests/purgatory_persistence.rs | 157 | ||||
| -rw-r--r-- | tests/purgatory_sync.rs | 365 | ||||
| -rw-r--r-- | tests/sync/discovery.rs | 259 | ||||
| -rw-r--r-- | tests/sync/historic_sync.rs | 32 | ||||
| -rw-r--r-- | tests/sync/live_sync.rs | 119 | ||||
| -rw-r--r-- | tests/sync/maintainer_reprocessing.rs | 278 | ||||
| -rw-r--r-- | tests/sync/metrics.rs | 139 | ||||
| -rw-r--r-- | tests/sync/mod.rs | 10 | ||||
| -rw-r--r-- | tests/sync/tag_variations.rs | 244 |
16 files changed, 1536 insertions, 1292 deletions
diff --git a/tests/archive_grasp_services.rs b/tests/archive_grasp_services.rs index a47fc55..9f13d2a 100644 --- a/tests/archive_grasp_services.rs +++ b/tests/archive_grasp_services.rs | |||
| @@ -29,7 +29,11 @@ | |||
| 29 | 29 | ||
| 30 | mod common; | 30 | mod common; |
| 31 | 31 | ||
| 32 | use common::TestRelay; | 32 | use common::{ |
| 33 | check_ref_at_commit, create_repo_announcement, create_state_event, | ||
| 34 | create_test_repo_with_commit, push_to_relay, wait_for_event_served, wait_for_sync_connection, | ||
| 35 | CommitVariant, TestRelay, | ||
| 36 | }; | ||
| 33 | use nostr_sdk::prelude::*; | 37 | use nostr_sdk::prelude::*; |
| 34 | use std::path::PathBuf; | 38 | use std::path::PathBuf; |
| 35 | use std::process::{Child, Command, Stdio}; | 39 | use std::process::{Child, Command, Stdio}; |
| @@ -376,3 +380,222 @@ async fn test_archive_multiple_grasp_services() { | |||
| 376 | let _ = process.kill(); | 380 | let _ = process.kill(); |
| 377 | let _ = process.wait(); | 381 | let _ = process.wait(); |
| 378 | } | 382 | } |
| 383 | |||
| 384 | /// Test that archive_read_only mode creates bare git repositories and syncs data | ||
| 385 | /// via relay-to-relay sync (purgatory sync infrastructure). | ||
| 386 | /// | ||
| 387 | /// Scenario: | ||
| 388 | /// 1. Start source relay with full repository (announcement + state + git data) | ||
| 389 | /// 2. Start archive relay with archive_all=true, archive_read_only=true, syncing from source | ||
| 390 | /// 3. Archive relay syncs announcement and state events from source | ||
| 391 | /// 4. State events trigger purgatory sync which fetches git data from source's clone URL | ||
| 392 | /// 5. Verify bare repository is created and git data is synced | ||
| 393 | /// 6. Verify git pushes are rejected (read-only mode) | ||
| 394 | #[tokio::test] | ||
| 395 | async fn test_archive_read_only_creates_bare_repo() { | ||
| 396 | // 1. Start source relay | ||
| 397 | let source_relay = TestRelay::start().await; | ||
| 398 | let keys = Keys::generate(); | ||
| 399 | let identifier = "archive-test-repo"; | ||
| 400 | |||
| 401 | // Pre-allocate archive relay port so we can include it in announcement | ||
| 402 | let archive_port = TestRelay::find_free_port(); | ||
| 403 | let archive_domain = format!("127.0.0.1:{}", archive_port); | ||
| 404 | |||
| 405 | // 2. Create test repository locally with deterministic commit | ||
| 406 | let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); | ||
| 407 | let commit_hash = create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) | ||
| 408 | .expect("Failed to create test repo"); | ||
| 409 | |||
| 410 | let npub = keys.public_key().to_bech32().expect("Failed to get npub"); | ||
| 411 | |||
| 412 | // 3. Create and send announcement listing BOTH relays | ||
| 413 | // This ensures the archive relay will accept the state event when it syncs | ||
| 414 | let announcement = create_repo_announcement( | ||
| 415 | &keys, | ||
| 416 | &[&source_relay.domain(), &archive_domain], | ||
| 417 | identifier, | ||
| 418 | ); | ||
| 419 | |||
| 420 | let source_client = Client::new(keys.clone()); | ||
| 421 | source_client | ||
| 422 | .add_relay(source_relay.url()) | ||
| 423 | .await | ||
| 424 | .expect("Failed to add source relay"); | ||
| 425 | source_client.connect().await; | ||
| 426 | |||
| 427 | // Wait for connection | ||
| 428 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 429 | |||
| 430 | // Send announcement to source relay | ||
| 431 | source_client | ||
| 432 | .send_event(&announcement) | ||
| 433 | .await | ||
| 434 | .expect("Failed to send announcement to source"); | ||
| 435 | |||
| 436 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 437 | |||
| 438 | // 4. Create and send state event | ||
| 439 | let clone_urls = [ | ||
| 440 | format!( | ||
| 441 | "http://{}/{}/{}.git", | ||
| 442 | source_relay.domain(), | ||
| 443 | npub, | ||
| 444 | identifier | ||
| 445 | ), | ||
| 446 | format!("http://{}/{}/{}.git", archive_domain, npub, identifier), | ||
| 447 | ]; | ||
| 448 | let relay_urls = [ | ||
| 449 | source_relay.url().to_string(), | ||
| 450 | format!("ws://{}", archive_domain), | ||
| 451 | ]; | ||
| 452 | |||
| 453 | let state_event = create_state_event( | ||
| 454 | &keys, | ||
| 455 | identifier, | ||
| 456 | &[("main", &commit_hash)], | ||
| 457 | &[], | ||
| 458 | &[&clone_urls[0], &clone_urls[1]], | ||
| 459 | &[&relay_urls[0], &relay_urls[1]], | ||
| 460 | ) | ||
| 461 | .expect("Failed to create state event"); | ||
| 462 | |||
| 463 | let state_event_id = state_event.id; | ||
| 464 | |||
| 465 | // Send state event to source relay (goes to purgatory - no git data yet) | ||
| 466 | source_client | ||
| 467 | .send_event(&state_event) | ||
| 468 | .await | ||
| 469 | .expect("Failed to send state event to source"); | ||
| 470 | |||
| 471 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 472 | |||
| 473 | // 5. Push git data to source relay | ||
| 474 | // The state event in purgatory authorizes this push | ||
| 475 | push_to_relay(temp_dir.path(), &source_relay.domain(), &npub, identifier) | ||
| 476 | .expect("Push to source should succeed"); | ||
| 477 | |||
| 478 | // After push, state event should be released from purgatory on source relay | ||
| 479 | wait_for_event_served(source_relay.url(), &state_event_id, Duration::from_secs(5)) | ||
| 480 | .await | ||
| 481 | .expect("State event should be served on source relay after push"); | ||
| 482 | |||
| 483 | // 6. Start archive relay with archive_all=true, archive_read_only=true, syncing from source | ||
| 484 | let archive_relay = TestRelay::start_with_archive_and_sync( | ||
| 485 | archive_port, | ||
| 486 | Some(source_relay.url().to_string()), | ||
| 487 | false, // negentropy enabled | ||
| 488 | true, // archive_all | ||
| 489 | true, // archive_read_only | ||
| 490 | ) | ||
| 491 | .await; | ||
| 492 | |||
| 493 | // Wait for sync connection to establish | ||
| 494 | wait_for_sync_connection(archive_relay.url(), 1, Duration::from_secs(5)) | ||
| 495 | .await | ||
| 496 | .expect("Sync connection should establish"); | ||
| 497 | |||
| 498 | // 7. Wait for state event to be released on archive relay | ||
| 499 | // The sync should: | ||
| 500 | // a) Fetch the announcement and state event from source relay | ||
| 501 | // b) Accept announcement (creates bare repo structure) - via archive mode | ||
| 502 | // c) Put state event in purgatory (git data missing on archive relay) | ||
| 503 | // d) Fetch git data from source relay's clone URL | ||
| 504 | // e) Release the state event from purgatory | ||
| 505 | |||
| 506 | let found = wait_for_event_served( | ||
| 507 | archive_relay.url(), | ||
| 508 | &state_event_id, | ||
| 509 | Duration::from_secs(30), // Allow time for sync + git fetch | ||
| 510 | ) | ||
| 511 | .await; | ||
| 512 | |||
| 513 | assert!( | ||
| 514 | found.is_ok(), | ||
| 515 | "State event should be served after sync fetches git data: {:?}", | ||
| 516 | found.err() | ||
| 517 | ); | ||
| 518 | |||
| 519 | // 8. Verify bare repository was created | ||
| 520 | let repo_path = archive_relay | ||
| 521 | .git_data_path() | ||
| 522 | .join(format!("{}/{}.git", npub, identifier)); | ||
| 523 | |||
| 524 | assert!( | ||
| 525 | repo_path.exists(), | ||
| 526 | "Bare repository should be created at {:?} for archive announcement", | ||
| 527 | repo_path | ||
| 528 | ); | ||
| 529 | |||
| 530 | // 9. Verify it's a bare repository (check for config file with bare = true) | ||
| 531 | let config_path = repo_path.join("config"); | ||
| 532 | assert!( | ||
| 533 | config_path.exists(), | ||
| 534 | "Git config should exist at {:?}", | ||
| 535 | config_path | ||
| 536 | ); | ||
| 537 | |||
| 538 | let config_content = tokio::fs::read_to_string(&config_path) | ||
| 539 | .await | ||
| 540 | .expect("Should read git config"); | ||
| 541 | assert!( | ||
| 542 | config_content.contains("bare = true"), | ||
| 543 | "Repository at {:?} should be bare (config should contain 'bare = true')", | ||
| 544 | repo_path | ||
| 545 | ); | ||
| 546 | |||
| 547 | // 10. Verify refs are correct on archive relay | ||
| 548 | let ref_correct = check_ref_at_commit( | ||
| 549 | &archive_domain, | ||
| 550 | &npub, | ||
| 551 | identifier, | ||
| 552 | "refs/heads/main", | ||
| 553 | &commit_hash, | ||
| 554 | ) | ||
| 555 | .await | ||
| 556 | .expect("Failed to check ref"); | ||
| 557 | |||
| 558 | assert!(ref_correct, "main branch should point to correct commit"); | ||
| 559 | |||
| 560 | // 11. Verify git pushes are rejected (read-only mode) | ||
| 561 | // Create a new commit in the source repo | ||
| 562 | tokio::fs::write(temp_dir.path().join("new_file.txt"), "new content") | ||
| 563 | .await | ||
| 564 | .expect("Failed to write new file"); | ||
| 565 | |||
| 566 | let output = tokio::process::Command::new("git") | ||
| 567 | .args(["add", "."]) | ||
| 568 | .current_dir(temp_dir.path()) | ||
| 569 | .output() | ||
| 570 | .await | ||
| 571 | .expect("Failed to git add"); | ||
| 572 | assert!(output.status.success()); | ||
| 573 | |||
| 574 | let output = tokio::process::Command::new("git") | ||
| 575 | .args(["commit", "-m", "New commit for push test"]) | ||
| 576 | .current_dir(temp_dir.path()) | ||
| 577 | .output() | ||
| 578 | .await | ||
| 579 | .expect("Failed to git commit"); | ||
| 580 | assert!(output.status.success()); | ||
| 581 | |||
| 582 | // Try to push to archive relay (should fail in read-only mode) | ||
| 583 | let push_url = format!("http://{}/{}/{}.git", archive_domain, npub, identifier); | ||
| 584 | let output = tokio::process::Command::new("git") | ||
| 585 | .args(["push", &push_url, "main"]) | ||
| 586 | .current_dir(temp_dir.path()) | ||
| 587 | .output() | ||
| 588 | .await | ||
| 589 | .expect("Failed to run git push"); | ||
| 590 | |||
| 591 | assert!( | ||
| 592 | !output.status.success(), | ||
| 593 | "Git push should be rejected in archive_read_only mode. stderr: {}", | ||
| 594 | String::from_utf8_lossy(&output.stderr) | ||
| 595 | ); | ||
| 596 | |||
| 597 | // Cleanup | ||
| 598 | source_client.disconnect().await; | ||
| 599 | archive_relay.stop().await; | ||
| 600 | source_relay.stop().await; | ||
| 601 | } | ||
diff --git a/tests/archive_read_only.rs b/tests/archive_read_only.rs deleted file mode 100644 index be6959b..0000000 --- a/tests/archive_read_only.rs +++ /dev/null | |||
| @@ -1,368 +0,0 @@ | |||
| 1 | //! Archive Read-Only Mode Integration Tests | ||
| 2 | //! | ||
| 3 | //! Tests that verify archive_read_only mode behavior: | ||
| 4 | //! - Bare git repositories are created for announcements | ||
| 5 | //! - Git data is synced via relay-to-relay sync (purgatory sync) | ||
| 6 | //! - Git pushes are rejected (read-only mode) | ||
| 7 | //! | ||
| 8 | //! # Test Strategy | ||
| 9 | //! | ||
| 10 | //! These tests verify the GRASP-05 archive mode with read_only flag: | ||
| 11 | //! 1. Source relay has full repository (announcement + state events + git data) | ||
| 12 | //! 2. Archive relay syncs from source relay (relay-to-relay sync) | ||
| 13 | //! 3. State events trigger purgatory sync which fetches git data | ||
| 14 | //! 4. Git data is validated against Nostr state events | ||
| 15 | //! 5. Git pushes are rejected (read-only enforcement) | ||
| 16 | //! | ||
| 17 | //! # Security Model | ||
| 18 | //! | ||
| 19 | //! Archive mode uses the existing purgatory sync infrastructure to ensure: | ||
| 20 | //! - Git data is validated against Nostr state events | ||
| 21 | //! - "Naughty git servers" can't provide incorrect state | ||
| 22 | //! - Same security guarantees as normal relay operation | ||
| 23 | //! | ||
| 24 | //! # Running Tests | ||
| 25 | //! | ||
| 26 | //! ```bash | ||
| 27 | //! # Run all archive read-only tests | ||
| 28 | //! cargo test --test archive_read_only | ||
| 29 | //! | ||
| 30 | //! # Run specific test | ||
| 31 | //! cargo test --test archive_read_only test_archive_read_only_creates_bare_repo | ||
| 32 | //! | ||
| 33 | //! # With output for debugging | ||
| 34 | //! cargo test --test archive_read_only -- --nocapture | ||
| 35 | //! ``` | ||
| 36 | |||
| 37 | mod common; | ||
| 38 | |||
| 39 | use common::{ | ||
| 40 | check_ref_at_commit, create_repo_announcement, create_state_event, | ||
| 41 | create_test_repo_with_commit, push_to_relay, wait_for_event_served, wait_for_sync_connection, | ||
| 42 | CommitVariant, TestRelay, | ||
| 43 | }; | ||
| 44 | use nostr_sdk::prelude::*; | ||
| 45 | use std::time::Duration; | ||
| 46 | |||
| 47 | /// Test that archive_read_only mode creates bare git repositories and syncs data | ||
| 48 | /// via relay-to-relay sync (purgatory sync infrastructure). | ||
| 49 | /// | ||
| 50 | /// Scenario: | ||
| 51 | /// 1. Start source relay with full repository (announcement + state + git data) | ||
| 52 | /// 2. Start archive relay with archive_all=true, archive_read_only=true, syncing from source | ||
| 53 | /// 3. Archive relay syncs announcement and state events from source | ||
| 54 | /// 4. State events trigger purgatory sync which fetches git data from source's clone URL | ||
| 55 | /// 5. Verify bare repository is created and git data is synced | ||
| 56 | /// 6. Verify git pushes are rejected (read-only mode) | ||
| 57 | #[tokio::test] | ||
| 58 | async fn test_archive_read_only_creates_bare_repo() { | ||
| 59 | // 1. Start source relay | ||
| 60 | let source_relay = TestRelay::start().await; | ||
| 61 | let keys = Keys::generate(); | ||
| 62 | let identifier = "archive-test-repo"; | ||
| 63 | |||
| 64 | // Pre-allocate archive relay port so we can include it in announcement | ||
| 65 | let archive_port = TestRelay::find_free_port(); | ||
| 66 | let archive_domain = format!("127.0.0.1:{}", archive_port); | ||
| 67 | |||
| 68 | // 2. Create test repository locally with deterministic commit | ||
| 69 | let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); | ||
| 70 | let commit_hash = create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) | ||
| 71 | .expect("Failed to create test repo"); | ||
| 72 | |||
| 73 | let npub = keys.public_key().to_bech32().expect("Failed to get npub"); | ||
| 74 | |||
| 75 | // 3. Create and send announcement listing BOTH relays | ||
| 76 | // This ensures the archive relay will accept the state event when it syncs | ||
| 77 | let announcement = create_repo_announcement( | ||
| 78 | &keys, | ||
| 79 | &[&source_relay.domain(), &archive_domain], | ||
| 80 | identifier, | ||
| 81 | ); | ||
| 82 | |||
| 83 | let source_client = Client::new(keys.clone()); | ||
| 84 | source_client | ||
| 85 | .add_relay(source_relay.url()) | ||
| 86 | .await | ||
| 87 | .expect("Failed to add source relay"); | ||
| 88 | source_client.connect().await; | ||
| 89 | |||
| 90 | // Wait for connection | ||
| 91 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 92 | |||
| 93 | // Send announcement to source relay | ||
| 94 | source_client | ||
| 95 | .send_event(&announcement) | ||
| 96 | .await | ||
| 97 | .expect("Failed to send announcement to source"); | ||
| 98 | |||
| 99 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 100 | |||
| 101 | // 4. Create and send state event | ||
| 102 | let clone_urls = [ | ||
| 103 | format!( | ||
| 104 | "http://{}/{}/{}.git", | ||
| 105 | source_relay.domain(), | ||
| 106 | npub, | ||
| 107 | identifier | ||
| 108 | ), | ||
| 109 | format!("http://{}/{}/{}.git", archive_domain, npub, identifier), | ||
| 110 | ]; | ||
| 111 | let relay_urls = [ | ||
| 112 | source_relay.url().to_string(), | ||
| 113 | format!("ws://{}", archive_domain), | ||
| 114 | ]; | ||
| 115 | |||
| 116 | let state_event = create_state_event( | ||
| 117 | &keys, | ||
| 118 | identifier, | ||
| 119 | &[("main", &commit_hash)], | ||
| 120 | &[], | ||
| 121 | &[&clone_urls[0], &clone_urls[1]], | ||
| 122 | &[&relay_urls[0], &relay_urls[1]], | ||
| 123 | ) | ||
| 124 | .expect("Failed to create state event"); | ||
| 125 | |||
| 126 | let state_event_id = state_event.id; | ||
| 127 | |||
| 128 | // Send state event to source relay (goes to purgatory - no git data yet) | ||
| 129 | source_client | ||
| 130 | .send_event(&state_event) | ||
| 131 | .await | ||
| 132 | .expect("Failed to send state event to source"); | ||
| 133 | |||
| 134 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 135 | |||
| 136 | // 5. Push git data to source relay | ||
| 137 | // The state event in purgatory authorizes this push | ||
| 138 | push_to_relay(temp_dir.path(), &source_relay.domain(), &npub, identifier) | ||
| 139 | .expect("Push to source should succeed"); | ||
| 140 | |||
| 141 | // After push, state event should be released from purgatory on source relay | ||
| 142 | wait_for_event_served(source_relay.url(), &state_event_id, Duration::from_secs(5)) | ||
| 143 | .await | ||
| 144 | .expect("State event should be served on source relay after push"); | ||
| 145 | |||
| 146 | // 6. Start archive relay with archive_all=true, archive_read_only=true, syncing from source | ||
| 147 | let archive_relay = TestRelay::start_with_archive_and_sync( | ||
| 148 | archive_port, | ||
| 149 | Some(source_relay.url().to_string()), | ||
| 150 | false, // negentropy enabled | ||
| 151 | true, // archive_all | ||
| 152 | true, // archive_read_only | ||
| 153 | ) | ||
| 154 | .await; | ||
| 155 | |||
| 156 | // Wait for sync connection to establish | ||
| 157 | wait_for_sync_connection(archive_relay.url(), 1, Duration::from_secs(5)) | ||
| 158 | .await | ||
| 159 | .expect("Sync connection should establish"); | ||
| 160 | |||
| 161 | // 7. Wait for state event to be released on archive relay | ||
| 162 | // The sync should: | ||
| 163 | // a) Fetch the announcement and state event from source relay | ||
| 164 | // b) Accept announcement (creates bare repo structure) - via archive mode | ||
| 165 | // c) Put state event in purgatory (git data missing on archive relay) | ||
| 166 | // d) Fetch git data from source relay's clone URL | ||
| 167 | // e) Release the state event from purgatory | ||
| 168 | let found = wait_for_event_served( | ||
| 169 | archive_relay.url(), | ||
| 170 | &state_event_id, | ||
| 171 | Duration::from_secs(30), // Allow time for sync + git fetch | ||
| 172 | ) | ||
| 173 | .await; | ||
| 174 | |||
| 175 | assert!( | ||
| 176 | found.is_ok(), | ||
| 177 | "State event should be served after sync fetches git data: {:?}", | ||
| 178 | found.err() | ||
| 179 | ); | ||
| 180 | |||
| 181 | // 8. Verify bare repository was created | ||
| 182 | let repo_path = archive_relay | ||
| 183 | .git_data_path() | ||
| 184 | .join(format!("{}/{}.git", npub, identifier)); | ||
| 185 | |||
| 186 | assert!( | ||
| 187 | repo_path.exists(), | ||
| 188 | "Bare repository should be created at {:?} for archive announcement", | ||
| 189 | repo_path | ||
| 190 | ); | ||
| 191 | |||
| 192 | // 9. Verify it's a bare repository (check for config file with bare = true) | ||
| 193 | let config_path = repo_path.join("config"); | ||
| 194 | assert!( | ||
| 195 | config_path.exists(), | ||
| 196 | "Git config should exist at {:?}", | ||
| 197 | config_path | ||
| 198 | ); | ||
| 199 | |||
| 200 | let config_content = tokio::fs::read_to_string(&config_path) | ||
| 201 | .await | ||
| 202 | .expect("Should read git config"); | ||
| 203 | assert!( | ||
| 204 | config_content.contains("bare = true"), | ||
| 205 | "Repository at {:?} should be bare (config should contain 'bare = true')", | ||
| 206 | repo_path | ||
| 207 | ); | ||
| 208 | |||
| 209 | // 10. Verify refs are correct on archive relay | ||
| 210 | let ref_correct = check_ref_at_commit( | ||
| 211 | &archive_domain, | ||
| 212 | &npub, | ||
| 213 | identifier, | ||
| 214 | "refs/heads/main", | ||
| 215 | &commit_hash, | ||
| 216 | ) | ||
| 217 | .await | ||
| 218 | .expect("Failed to check ref"); | ||
| 219 | |||
| 220 | assert!(ref_correct, "main branch should point to correct commit"); | ||
| 221 | |||
| 222 | // 11. Verify git pushes are rejected (read-only mode) | ||
| 223 | // Create a new commit in the source repo | ||
| 224 | tokio::fs::write(temp_dir.path().join("new_file.txt"), "new content") | ||
| 225 | .await | ||
| 226 | .expect("Failed to write new file"); | ||
| 227 | |||
| 228 | let output = tokio::process::Command::new("git") | ||
| 229 | .args(["add", "."]) | ||
| 230 | .current_dir(temp_dir.path()) | ||
| 231 | .output() | ||
| 232 | .await | ||
| 233 | .expect("Failed to git add"); | ||
| 234 | assert!(output.status.success()); | ||
| 235 | |||
| 236 | let output = tokio::process::Command::new("git") | ||
| 237 | .args(["commit", "-m", "New commit for push test"]) | ||
| 238 | .current_dir(temp_dir.path()) | ||
| 239 | .output() | ||
| 240 | .await | ||
| 241 | .expect("Failed to git commit"); | ||
| 242 | assert!(output.status.success()); | ||
| 243 | |||
| 244 | // Try to push to archive relay (should fail in read-only mode) | ||
| 245 | let push_url = format!("http://{}/{}/{}.git", archive_domain, npub, identifier); | ||
| 246 | let output = tokio::process::Command::new("git") | ||
| 247 | .args(["push", &push_url, "main"]) | ||
| 248 | .current_dir(temp_dir.path()) | ||
| 249 | .output() | ||
| 250 | .await | ||
| 251 | .expect("Failed to run git push"); | ||
| 252 | |||
| 253 | assert!( | ||
| 254 | !output.status.success(), | ||
| 255 | "Git push should be rejected in archive_read_only mode. stderr: {}", | ||
| 256 | String::from_utf8_lossy(&output.stderr) | ||
| 257 | ); | ||
| 258 | |||
| 259 | // Cleanup | ||
| 260 | source_client.disconnect().await; | ||
| 261 | archive_relay.stop().await; | ||
| 262 | source_relay.stop().await; | ||
| 263 | } | ||
| 264 | |||
| 265 | /// Test that archive mode without state events does NOT sync git data. | ||
| 266 | /// | ||
| 267 | /// This verifies the security model: archive mode only syncs git data | ||
| 268 | /// when there are state events to validate against. | ||
| 269 | /// | ||
| 270 | /// Scenario: | ||
| 271 | /// 1. Start source relay with announcement only (no state events) | ||
| 272 | /// 2. Start archive relay syncing from source | ||
| 273 | /// 3. Archive relay syncs announcement (creates bare repo) | ||
| 274 | /// 4. Verify git data is NOT synced (no state events to trigger purgatory sync) | ||
| 275 | #[tokio::test] | ||
| 276 | async fn test_archive_without_state_events_does_not_sync_git() { | ||
| 277 | // 1. Start source relay | ||
| 278 | let source_relay = TestRelay::start().await; | ||
| 279 | let keys = Keys::generate(); | ||
| 280 | let identifier = "archive-no-state-repo"; | ||
| 281 | |||
| 282 | // Pre-allocate archive relay port | ||
| 283 | let archive_port = TestRelay::find_free_port(); | ||
| 284 | let archive_domain = format!("127.0.0.1:{}", archive_port); | ||
| 285 | |||
| 286 | // 2. Create test repository locally | ||
| 287 | let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); | ||
| 288 | let commit_hash = create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) | ||
| 289 | .expect("Failed to create test repo"); | ||
| 290 | |||
| 291 | let npub = keys.public_key().to_bech32().expect("Failed to get npub"); | ||
| 292 | |||
| 293 | // 3. Create and send announcement listing BOTH relays (but NO state event) | ||
| 294 | let announcement = create_repo_announcement( | ||
| 295 | &keys, | ||
| 296 | &[&source_relay.domain(), &archive_domain], | ||
| 297 | identifier, | ||
| 298 | ); | ||
| 299 | |||
| 300 | let source_client = Client::new(keys.clone()); | ||
| 301 | source_client | ||
| 302 | .add_relay(source_relay.url()) | ||
| 303 | .await | ||
| 304 | .expect("Failed to add source relay"); | ||
| 305 | source_client.connect().await; | ||
| 306 | |||
| 307 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 308 | |||
| 309 | // Send announcement to source relay | ||
| 310 | source_client | ||
| 311 | .send_event(&announcement) | ||
| 312 | .await | ||
| 313 | .expect("Failed to send announcement to source"); | ||
| 314 | |||
| 315 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 316 | |||
| 317 | // 4. Push git data to source relay (but no state event to authorize it) | ||
| 318 | // This push will fail because there's no state event in purgatory | ||
| 319 | // That's expected - we're testing that archive mode doesn't blindly fetch git data | ||
| 320 | |||
| 321 | // 5. Start archive relay | ||
| 322 | let archive_relay = TestRelay::start_with_archive_and_sync( | ||
| 323 | archive_port, | ||
| 324 | Some(source_relay.url().to_string()), | ||
| 325 | false, | ||
| 326 | true, | ||
| 327 | true, | ||
| 328 | ) | ||
| 329 | .await; | ||
| 330 | |||
| 331 | // Wait for sync | ||
| 332 | wait_for_sync_connection(archive_relay.url(), 1, Duration::from_secs(5)) | ||
| 333 | .await | ||
| 334 | .expect("Sync connection should establish"); | ||
| 335 | |||
| 336 | // Give time for any potential git sync to happen | ||
| 337 | tokio::time::sleep(Duration::from_secs(3)).await; | ||
| 338 | |||
| 339 | // 6. Verify bare repository was created (announcement was accepted) | ||
| 340 | let repo_path = archive_relay | ||
| 341 | .git_data_path() | ||
| 342 | .join(format!("{}/{}.git", npub, identifier)); | ||
| 343 | |||
| 344 | assert!( | ||
| 345 | repo_path.exists(), | ||
| 346 | "Bare repository should be created for archive announcement" | ||
| 347 | ); | ||
| 348 | |||
| 349 | // 7. Verify git data was NOT synced (no state events to trigger purgatory sync) | ||
| 350 | // Check that the commit does NOT exist in the archive relay's repo | ||
| 351 | let output = tokio::process::Command::new("git") | ||
| 352 | .args(["cat-file", "-t", &commit_hash]) | ||
| 353 | .current_dir(&repo_path) | ||
| 354 | .output() | ||
| 355 | .await; | ||
| 356 | |||
| 357 | let commit_exists = output.map(|o| o.status.success()).unwrap_or(false); | ||
| 358 | |||
| 359 | assert!( | ||
| 360 | !commit_exists, | ||
| 361 | "Git data should NOT be synced without state events (security: validates against Nostr state)" | ||
| 362 | ); | ||
| 363 | |||
| 364 | // Cleanup | ||
| 365 | source_client.disconnect().await; | ||
| 366 | archive_relay.stop().await; | ||
| 367 | source_relay.stop().await; | ||
| 368 | } | ||
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 | ||
diff --git a/tests/nip77_negentropy.rs b/tests/nip77_negentropy.rs index fccfe67..29e62d8 100644 --- a/tests/nip77_negentropy.rs +++ b/tests/nip77_negentropy.rs | |||
| @@ -35,56 +35,67 @@ use common::{sync_helpers::*, TestRelay}; | |||
| 35 | /// 3. Create a fresh client with empty local database | 35 | /// 3. Create a fresh client with empty local database |
| 36 | /// 4. Call client.sync() to perform negentropy reconciliation | 36 | /// 4. Call client.sync() to perform negentropy reconciliation |
| 37 | /// 5. Verify reconciliation found the events on the relay | 37 | /// 5. Verify reconciliation found the events on the relay |
| 38 | /// | ||
| 39 | /// Uses kind 10317 (GitUserGraspList) events which are unconditionally accepted | ||
| 40 | /// by the relay without requiring a promoted repository. This avoids the | ||
| 41 | /// announcements-purgatory system which holds kind 30617 events until git data | ||
| 42 | /// arrives, meaning announcement events are not stored in the DB and would not | ||
| 43 | /// appear in negentropy sync results. | ||
| 38 | #[tokio::test] | 44 | #[tokio::test] |
| 39 | async fn test_nip77_negentropy_sync_finds_events() { | 45 | async fn test_nip77_negentropy_sync_finds_events() { |
| 40 | // 1. Start relay | 46 | // 1. Start relay |
| 41 | let relay = TestRelay::start().await; | 47 | let relay = TestRelay::start().await; |
| 42 | println!("Relay started at {}", relay.url()); | 48 | println!("Relay started at {}", relay.url()); |
| 43 | 49 | ||
| 44 | // 2. Create keys and publish events | 50 | // 2. Create two distinct keypairs - each publishes a kind 10317 event. |
| 45 | let keys = Keys::generate(); | 51 | // Kind 10317 (GitUserGraspList) is unconditionally accepted and stored in |
| 46 | 52 | // the relay DB, unlike kind 30617 announcements which go to purgatory. | |
| 47 | // Create a repository announcement that will be accepted by the relay | 53 | let keys1 = Keys::generate(); |
| 48 | let announcement = create_repo_announcement(&keys, &[&relay.domain()], "test-repo-nip77"); | 54 | let keys2 = Keys::generate(); |
| 49 | let event1_id = announcement.id; | 55 | |
| 56 | // Build kind 10317 events (replaceable per pubkey, so two keys = two stored events) | ||
| 57 | let event1 = EventBuilder::new(Kind::GitUserGraspList, "") | ||
| 58 | .tags(vec![Tag::identifier("grasp-list-nip77-a")]) | ||
| 59 | .sign_with_keys(&keys1) | ||
| 60 | .expect("Failed to sign event 1"); | ||
| 61 | let event1_id = event1.id; | ||
| 50 | println!( | 62 | println!( |
| 51 | "Created event 1: {} (kind {})", | 63 | "Created event 1: {} (kind {})", |
| 52 | event1_id, | 64 | event1_id, |
| 53 | announcement.kind.as_u16() | 65 | event1.kind.as_u16() |
| 54 | ); | 66 | ); |
| 55 | 67 | ||
| 56 | // Create a second event (issue referencing the repo) | 68 | let event2 = EventBuilder::new(Kind::GitUserGraspList, "") |
| 57 | let repo_coord = format!( | 69 | .tags(vec![Tag::identifier("grasp-list-nip77-b")]) |
| 58 | "{}:{}:{}", | 70 | .sign_with_keys(&keys2) |
| 59 | Kind::GitRepoAnnouncement.as_u16(), | 71 | .expect("Failed to sign event 2"); |
| 60 | keys.public_key().to_hex(), | 72 | let event2_id = event2.id; |
| 61 | "test-repo-nip77" | ||
| 62 | ); | ||
| 63 | let issue = build_layer2_issue_event(&keys, &repo_coord, "Test issue for NIP-77") | ||
| 64 | .expect("Failed to build issue event"); | ||
| 65 | let event2_id = issue.id; | ||
| 66 | println!( | 73 | println!( |
| 67 | "Created event 2: {} (kind {})", | 74 | "Created event 2: {} (kind {})", |
| 68 | event2_id, | 75 | event2_id, |
| 69 | issue.kind.as_u16() | 76 | event2.kind.as_u16() |
| 70 | ); | 77 | ); |
| 71 | 78 | ||
| 72 | // 3. Send events to relay using TestClient | 79 | // 3. Send events to relay using TestClient |
| 73 | let publish_client = TestClient::new(relay.url(), keys.clone()) | 80 | let publish_client1 = TestClient::new(relay.url(), keys1.clone()) |
| 74 | .await | 81 | .await |
| 75 | .expect("Failed to connect to relay"); | 82 | .expect("Failed to connect to relay"); |
| 83 | publish_client1 | ||
| 84 | .send_event(&event1) | ||
| 85 | .await | ||
| 86 | .expect("Failed to send event 1"); | ||
| 87 | publish_client1.disconnect().await; | ||
| 76 | 88 | ||
| 77 | publish_client | 89 | let publish_client2 = TestClient::new(relay.url(), keys2.clone()) |
| 78 | .send_event(&announcement) | ||
| 79 | .await | 90 | .await |
| 80 | .expect("Failed to send announcement"); | 91 | .expect("Failed to connect to relay"); |
| 81 | publish_client | 92 | publish_client2 |
| 82 | .send_event(&issue) | 93 | .send_event(&event2) |
| 83 | .await | 94 | .await |
| 84 | .expect("Failed to send issue"); | 95 | .expect("Failed to send event 2"); |
| 85 | println!("Events published to relay"); | 96 | publish_client2.disconnect().await; |
| 86 | 97 | ||
| 87 | publish_client.disconnect().await; | 98 | println!("Events published to relay"); |
| 88 | 99 | ||
| 89 | // 4. Wait a moment for events to be stored | 100 | // 4. Wait a moment for events to be stored |
| 90 | tokio::time::sleep(Duration::from_millis(200)).await; | 101 | tokio::time::sleep(Duration::from_millis(200)).await; |
| @@ -104,8 +115,8 @@ async fn test_nip77_negentropy_sync_finds_events() { | |||
| 104 | 115 | ||
| 105 | // 6. Perform negentropy sync with filter matching our events | 116 | // 6. Perform negentropy sync with filter matching our events |
| 106 | let filter = Filter::new() | 117 | let filter = Filter::new() |
| 107 | .author(keys.public_key()) | 118 | .authors(vec![keys1.public_key(), keys2.public_key()]) |
| 108 | .kinds(vec![Kind::GitRepoAnnouncement, Kind::GitIssue]); | 119 | .kind(Kind::GitUserGraspList); |
| 109 | 120 | ||
| 110 | println!("Starting negentropy sync with filter: {:?}", filter); | 121 | println!("Starting negentropy sync with filter: {:?}", filter); |
| 111 | 122 | ||
diff --git a/tests/purgatory.rs b/tests/purgatory.rs new file mode 100644 index 0000000..73f85ca --- /dev/null +++ b/tests/purgatory.rs | |||
| @@ -0,0 +1,89 @@ | |||
| 1 | //! Purgatory Integration Tests | ||
| 2 | //! | ||
| 3 | //! Tests ngit-grasp relay's implementation of GRASP-01 purgatory behavior. | ||
| 4 | //! Uses grasp-audit library to avoid code duplication. | ||
| 5 | //! | ||
| 6 | //! # Test Strategy | ||
| 7 | //! | ||
| 8 | //! - Each test runs in complete isolation with its own fresh relay instance | ||
| 9 | //! - Uses macro to eliminate boilerplate while maintaining test isolation | ||
| 10 | //! - Calls individual test methods from grasp-audit for minimal duplication | ||
| 11 | //! - Automatic cleanup via TestRelay fixture (removes container and temp dirs) | ||
| 12 | //! | ||
| 13 | //! # Running Tests | ||
| 14 | //! | ||
| 15 | //! ```bash | ||
| 16 | //! # Run all purgatory tests | ||
| 17 | //! cargo test --test purgatory | ||
| 18 | //! | ||
| 19 | //! # Run specific test | ||
| 20 | //! cargo test --test purgatory test_state_event_not_served_before_git_data | ||
| 21 | //! | ||
| 22 | //! # With output | ||
| 23 | //! cargo test --test purgatory -- --nocapture | ||
| 24 | //! ``` | ||
| 25 | |||
| 26 | mod common; | ||
| 27 | |||
| 28 | use common::TestRelay; | ||
| 29 | use grasp_audit::specs::grasp01::PurgatoryTests; | ||
| 30 | use grasp_audit::{AuditClient, AuditConfig}; | ||
| 31 | |||
| 32 | /// Macro to generate isolated integration tests for purgatory | ||
| 33 | /// | ||
| 34 | /// Each test runs with its own fresh relay instance to ensure complete isolation. | ||
| 35 | /// This eliminates issues with leftover repositories and ensures clean state. | ||
| 36 | macro_rules! isolated_purgatory_test { | ||
| 37 | ($test_name:ident) => { | ||
| 38 | #[tokio::test] | ||
| 39 | async fn $test_name() { | ||
| 40 | let relay = TestRelay::start().await; | ||
| 41 | let config = AuditConfig::isolated(); | ||
| 42 | let client = AuditClient::new(relay.url(), config) | ||
| 43 | .await | ||
| 44 | .expect("Failed to create audit client"); | ||
| 45 | |||
| 46 | let result = PurgatoryTests::$test_name(&client).await; | ||
| 47 | |||
| 48 | relay.stop().await; | ||
| 49 | |||
| 50 | assert!( | ||
| 51 | result.passed, | ||
| 52 | "{} failed: {}", | ||
| 53 | stringify!($test_name), | ||
| 54 | result.error.as_deref().unwrap_or("unknown error") | ||
| 55 | ); | ||
| 56 | } | ||
| 57 | }; | ||
| 58 | } | ||
| 59 | |||
| 60 | // ============================================================ | ||
| 61 | // Announcement Purgatory Tests | ||
| 62 | // ============================================================ | ||
| 63 | |||
| 64 | isolated_purgatory_test!(test_announcement_not_served_before_git_data); | ||
| 65 | isolated_purgatory_test!(test_announcement_served_after_git_push); | ||
| 66 | isolated_purgatory_test!(test_bare_repo_exists_for_purgatory_announcement); | ||
| 67 | isolated_purgatory_test!(test_state_event_accepted_for_purgatory_announcement); | ||
| 68 | |||
| 69 | // ============================================================ | ||
| 70 | // Deletion Event Tests (NIP-09) | ||
| 71 | // ============================================================ | ||
| 72 | |||
| 73 | isolated_purgatory_test!(test_deletion_by_event_id_removes_purgatory_state_event); | ||
| 74 | isolated_purgatory_test!(test_deletion_by_coordinate_removes_purgatory_state_event); | ||
| 75 | |||
| 76 | // ============================================================ | ||
| 77 | // State Event Purgatory Tests (already implemented) | ||
| 78 | // ============================================================ | ||
| 79 | |||
| 80 | isolated_purgatory_test!(test_state_event_not_served_before_git_data); | ||
| 81 | isolated_purgatory_test!(test_state_event_served_after_git_push); | ||
| 82 | |||
| 83 | // ============================================================ | ||
| 84 | // PR Purgatory Tests | ||
| 85 | // ============================================================ | ||
| 86 | |||
| 87 | isolated_purgatory_test!(test_pr_event_accepted_into_purgatory_and_isnt_served); | ||
| 88 | isolated_purgatory_test!(test_pr_event_in_purgatory_git_push_accepted); | ||
| 89 | isolated_purgatory_test!(test_pr_event_served_after_git_push); | ||
diff --git a/tests/purgatory_persistence.rs b/tests/purgatory_persistence.rs index 4dc5e94..655b0d9 100644 --- a/tests/purgatory_persistence.rs +++ b/tests/purgatory_persistence.rs | |||
| @@ -31,9 +31,11 @@ | |||
| 31 | 31 | ||
| 32 | mod common; | 32 | mod common; |
| 33 | 33 | ||
| 34 | use common::purgatory_helpers::create_announcement_event; | ||
| 34 | use ngit_grasp::purgatory::Purgatory; | 35 | use ngit_grasp::purgatory::Purgatory; |
| 35 | use ngit_grasp::sync::rejected_index::{EventType, RejectedEventsIndex, RejectionReason}; | 36 | use ngit_grasp::sync::rejected_index::{EventType, RejectedEventsIndex, RejectionReason}; |
| 36 | use nostr_sdk::prelude::*; | 37 | use nostr_sdk::prelude::*; |
| 38 | use std::collections::HashSet; | ||
| 37 | use std::time::Duration; | 39 | use std::time::Duration; |
| 38 | 40 | ||
| 39 | /// Helper to create a test event | 41 | /// Helper to create a test event |
| @@ -120,11 +122,31 @@ async fn test_full_purgatory_save_restore_cycle() { | |||
| 120 | // Add a PR placeholder (git-data-first scenario) | 122 | // Add a PR placeholder (git-data-first scenario) |
| 121 | purgatory.add_pr_placeholder("placeholder-id".to_string(), "commit-xyz".to_string()); | 123 | purgatory.add_pr_placeholder("placeholder-id".to_string(), "commit-xyz".to_string()); |
| 122 | 124 | ||
| 123 | // Note: We can't directly test expired events without accessing private fields, | 125 | // Add an announcement to purgatory (requires a real directory for the repo path) |
| 124 | // so we'll focus on testing state and PR events persistence | 126 | let repo_dir = temp_dir.path().join("repo.git"); |
| 127 | std::fs::create_dir_all(&repo_dir).unwrap(); | ||
| 128 | let ann_keys = Keys::generate(); | ||
| 129 | let ann_event = create_announcement_event( | ||
| 130 | &ann_keys, | ||
| 131 | "my-repo", | ||
| 132 | &["http://example.com/my-repo.git"], | ||
| 133 | &["wss://relay.example.com"], | ||
| 134 | ) | ||
| 135 | .unwrap(); | ||
| 136 | let ann_event_id = ann_event.id; | ||
| 137 | let mut ann_relays = HashSet::new(); | ||
| 138 | ann_relays.insert("wss://relay.example.com".to_string()); | ||
| 139 | purgatory.add_announcement( | ||
| 140 | ann_event, | ||
| 141 | "my-repo".to_string(), | ||
| 142 | ann_keys.public_key(), | ||
| 143 | repo_dir.clone(), | ||
| 144 | ann_relays, | ||
| 145 | ); | ||
| 125 | 146 | ||
| 126 | // Verify initial counts | 147 | // Verify initial counts |
| 127 | let (state_count, pr_count) = purgatory.count(); | 148 | let (announcement_count, state_count, pr_count) = purgatory.count(); |
| 149 | assert_eq!(announcement_count, 1, "Should have 1 announcement"); | ||
| 128 | assert_eq!(state_count, 2, "Should have 2 state events"); | 150 | assert_eq!(state_count, 2, "Should have 2 state events"); |
| 129 | assert_eq!( | 151 | assert_eq!( |
| 130 | pr_count, 3, | 152 | pr_count, 3, |
| @@ -146,13 +168,23 @@ async fn test_full_purgatory_save_restore_cycle() { | |||
| 146 | ); | 168 | ); |
| 147 | 169 | ||
| 148 | // Verify all data was restored | 170 | // Verify all data was restored |
| 149 | let (state_count2, pr_count2) = purgatory2.count(); | 171 | let (announcement_count2, state_count2, pr_count2) = purgatory2.count(); |
| 172 | assert_eq!(announcement_count2, 1, "Should have 1 announcement after restore"); | ||
| 150 | assert_eq!(state_count2, 2, "Should have 2 state events after restore"); | 173 | assert_eq!(state_count2, 2, "Should have 2 state events after restore"); |
| 151 | assert_eq!( | 174 | assert_eq!( |
| 152 | pr_count2, 3, | 175 | pr_count2, 3, |
| 153 | "Should have 3 PR events after restore (2 events + 1 placeholder)" | 176 | "Should have 3 PR events after restore (2 events + 1 placeholder)" |
| 154 | ); | 177 | ); |
| 155 | 178 | ||
| 179 | // Verify announcement was restored correctly | ||
| 180 | let restored_ann = purgatory2 | ||
| 181 | .find_announcement(&ann_keys.public_key(), "my-repo") | ||
| 182 | .expect("Announcement should be restored"); | ||
| 183 | assert_eq!(restored_ann.event.id, ann_event_id); | ||
| 184 | assert_eq!(restored_ann.identifier, "my-repo"); | ||
| 185 | assert_eq!(restored_ann.repo_path, repo_dir); | ||
| 186 | assert!(!restored_ann.soft_expired); | ||
| 187 | |||
| 156 | // Verify specific state events | 188 | // Verify specific state events |
| 157 | let repo1_states = purgatory2.find_state("repo1"); | 189 | let repo1_states = purgatory2.find_state("repo1"); |
| 158 | assert_eq!(repo1_states.len(), 1); | 190 | assert_eq!(repo1_states.len(), 1); |
| @@ -284,7 +316,7 @@ async fn test_purgatory_downtime_adjustment() { | |||
| 284 | purgatory2.restore_from_disk(&state_path).unwrap(); | 316 | purgatory2.restore_from_disk(&state_path).unwrap(); |
| 285 | 317 | ||
| 286 | // Verify event is still there (downtime was accounted for) | 318 | // Verify event is still there (downtime was accounted for) |
| 287 | let (state_count, _) = purgatory2.count(); | 319 | let (_, state_count, _) = purgatory2.count(); |
| 288 | assert_eq!(state_count, 1); | 320 | assert_eq!(state_count, 1); |
| 289 | 321 | ||
| 290 | let repo1_states = purgatory2.find_state("repo1"); | 322 | let repo1_states = purgatory2.find_state("repo1"); |
| @@ -410,7 +442,7 @@ async fn test_purgatory_restore_missing_file() { | |||
| 410 | assert!(result.is_err(), "Should error on missing file"); | 442 | assert!(result.is_err(), "Should error on missing file"); |
| 411 | 443 | ||
| 412 | // Purgatory should still be usable (empty state) | 444 | // Purgatory should still be usable (empty state) |
| 413 | let (state_count, pr_count) = purgatory.count(); | 445 | let (_, state_count, pr_count) = purgatory.count(); |
| 414 | assert_eq!(state_count, 0); | 446 | assert_eq!(state_count, 0); |
| 415 | assert_eq!(pr_count, 0); | 447 | assert_eq!(pr_count, 0); |
| 416 | 448 | ||
| @@ -419,7 +451,7 @@ async fn test_purgatory_restore_missing_file() { | |||
| 419 | let event = create_test_event(&keys, "test").await; | 451 | let event = create_test_event(&keys, "test").await; |
| 420 | purgatory.add_state(event, "repo1".to_string(), keys.public_key(), false); | 452 | purgatory.add_state(event, "repo1".to_string(), keys.public_key(), false); |
| 421 | 453 | ||
| 422 | let (state_count, _) = purgatory.count(); | 454 | let (_, state_count, _) = purgatory.count(); |
| 423 | assert_eq!(state_count, 1); | 455 | assert_eq!(state_count, 1); |
| 424 | } | 456 | } |
| 425 | 457 | ||
| @@ -470,7 +502,7 @@ async fn test_purgatory_restore_corrupted_file() { | |||
| 470 | assert!(result.is_err(), "Should error on corrupted file"); | 502 | assert!(result.is_err(), "Should error on corrupted file"); |
| 471 | 503 | ||
| 472 | // Purgatory should still be usable | 504 | // Purgatory should still be usable |
| 473 | let (state_count, pr_count) = purgatory.count(); | 505 | let (_, state_count, pr_count) = purgatory.count(); |
| 474 | assert_eq!(state_count, 0); | 506 | assert_eq!(state_count, 0); |
| 475 | assert_eq!(pr_count, 0); | 507 | assert_eq!(pr_count, 0); |
| 476 | } | 508 | } |
| @@ -513,7 +545,7 @@ async fn test_empty_purgatory_save_restore() { | |||
| 513 | purgatory2.restore_from_disk(&state_path).unwrap(); | 545 | purgatory2.restore_from_disk(&state_path).unwrap(); |
| 514 | 546 | ||
| 515 | // Verify empty state | 547 | // Verify empty state |
| 516 | let (state_count, pr_count) = purgatory2.count(); | 548 | let (_, state_count, pr_count) = purgatory2.count(); |
| 517 | assert_eq!(state_count, 0); | 549 | assert_eq!(state_count, 0); |
| 518 | assert_eq!(pr_count, 0); | 550 | assert_eq!(pr_count, 0); |
| 519 | assert_eq!(purgatory2.expired_count(), 0); | 551 | assert_eq!(purgatory2.expired_count(), 0); |
| @@ -620,7 +652,7 @@ async fn test_purgatory_continues_working_after_restore() { | |||
| 620 | ); | 652 | ); |
| 621 | 653 | ||
| 622 | // Verify both old and new events work | 654 | // Verify both old and new events work |
| 623 | let (state_count, _) = purgatory2.count(); | 655 | let (_, state_count, _) = purgatory2.count(); |
| 624 | assert_eq!(state_count, 2); | 656 | assert_eq!(state_count, 2); |
| 625 | 657 | ||
| 626 | let repo1_states = purgatory2.find_state("repo1"); | 658 | let repo1_states = purgatory2.find_state("repo1"); |
| @@ -632,7 +664,7 @@ async fn test_purgatory_continues_working_after_restore() { | |||
| 632 | assert_eq!(repo2_states[0].event.id, event2.id); | 664 | assert_eq!(repo2_states[0].event.id, event2.id); |
| 633 | 665 | ||
| 634 | // Verify cleanup still works | 666 | // Verify cleanup still works |
| 635 | let (state_removed, pr_removed) = purgatory2.cleanup(); | 667 | let (_, state_removed, pr_removed) = purgatory2.cleanup(); |
| 636 | // Nothing should be expired yet | 668 | // Nothing should be expired yet |
| 637 | assert_eq!(state_removed, 0); | 669 | assert_eq!(state_removed, 0); |
| 638 | assert_eq!(pr_removed, 0); | 670 | assert_eq!(pr_removed, 0); |
| @@ -713,15 +745,15 @@ async fn test_purgatory_entries_expired_during_downtime() { | |||
| 713 | purgatory2.restore_from_disk(&state_path).unwrap(); | 745 | purgatory2.restore_from_disk(&state_path).unwrap(); |
| 714 | 746 | ||
| 715 | // Event should be restored | 747 | // Event should be restored |
| 716 | let (state_count, _) = purgatory2.count(); | 748 | let (_, state_count, _) = purgatory2.count(); |
| 717 | assert_eq!(state_count, 1); | 749 | assert_eq!(state_count, 1); |
| 718 | 750 | ||
| 719 | // Cleanup should work (even if nothing is expired yet) | 751 | // Cleanup should work (even if nothing is expired yet) |
| 720 | let (state_removed, _) = purgatory2.cleanup(); | 752 | let (_, state_removed, _) = purgatory2.cleanup(); |
| 721 | // Nothing expired yet since we didn't wait 30 minutes | 753 | // Nothing expired yet since we didn't wait 30 minutes |
| 722 | assert_eq!(state_removed, 0); | 754 | assert_eq!(state_removed, 0); |
| 723 | 755 | ||
| 724 | let (state_count, _) = purgatory2.count(); | 756 | let (_, state_count, _) = purgatory2.count(); |
| 725 | assert_eq!(state_count, 1); | 757 | assert_eq!(state_count, 1); |
| 726 | } | 758 | } |
| 727 | 759 | ||
| @@ -775,3 +807,100 @@ async fn test_rejected_cache_entries_expired_during_downtime() { | |||
| 775 | assert_eq!(index2.hot_cache_len(), 0); | 807 | assert_eq!(index2.hot_cache_len(), 0); |
| 776 | assert_eq!(index2.cold_index_len(), 1); | 808 | assert_eq!(index2.cold_index_len(), 1); |
| 777 | } | 809 | } |
| 810 | |||
| 811 | /// Test 18: Announcement events are saved and restored across restarts | ||
| 812 | #[tokio::test] | ||
| 813 | async fn test_announcement_save_restore_cycle() { | ||
| 814 | let temp_dir = tempfile::tempdir().unwrap(); | ||
| 815 | let git_data_path = temp_dir.path().join("git"); | ||
| 816 | let state_path = temp_dir.path().join("purgatory.json"); | ||
| 817 | |||
| 818 | // Create a real bare repo directory (restore skips entries whose path is missing) | ||
| 819 | let repo_dir = temp_dir.path().join("owner.git"); | ||
| 820 | std::fs::create_dir_all(&repo_dir).unwrap(); | ||
| 821 | |||
| 822 | let purgatory = Purgatory::new(&git_data_path); | ||
| 823 | let keys = Keys::generate(); | ||
| 824 | |||
| 825 | let ann_event = create_announcement_event( | ||
| 826 | &keys, | ||
| 827 | "my-repo", | ||
| 828 | &["http://example.com/my-repo.git"], | ||
| 829 | &["wss://relay.example.com"], | ||
| 830 | ) | ||
| 831 | .unwrap(); | ||
| 832 | let ann_event_id = ann_event.id; | ||
| 833 | |||
| 834 | let mut relays = HashSet::new(); | ||
| 835 | relays.insert("wss://relay.example.com".to_string()); | ||
| 836 | |||
| 837 | purgatory.add_announcement( | ||
| 838 | ann_event, | ||
| 839 | "my-repo".to_string(), | ||
| 840 | keys.public_key(), | ||
| 841 | repo_dir.clone(), | ||
| 842 | relays.clone(), | ||
| 843 | ); | ||
| 844 | |||
| 845 | let (ann_count, _, _) = purgatory.count(); | ||
| 846 | assert_eq!(ann_count, 1); | ||
| 847 | |||
| 848 | // Save to disk | ||
| 849 | purgatory.save_to_disk(&state_path).unwrap(); | ||
| 850 | assert!(state_path.exists()); | ||
| 851 | |||
| 852 | // Restore into a fresh purgatory | ||
| 853 | let purgatory2 = Purgatory::new(&git_data_path); | ||
| 854 | purgatory2.restore_from_disk(&state_path).unwrap(); | ||
| 855 | |||
| 856 | assert!(!state_path.exists(), "State file should be deleted after restore"); | ||
| 857 | |||
| 858 | let (ann_count2, _, _) = purgatory2.count(); | ||
| 859 | assert_eq!(ann_count2, 1, "Announcement should be restored"); | ||
| 860 | |||
| 861 | let restored = purgatory2 | ||
| 862 | .find_announcement(&keys.public_key(), "my-repo") | ||
| 863 | .expect("Announcement should be findable after restore"); | ||
| 864 | |||
| 865 | assert_eq!(restored.event.id, ann_event_id); | ||
| 866 | assert_eq!(restored.identifier, "my-repo"); | ||
| 867 | assert_eq!(restored.owner, keys.public_key()); | ||
| 868 | assert_eq!(restored.repo_path, repo_dir); | ||
| 869 | assert_eq!(restored.relays, relays); | ||
| 870 | assert!(!restored.soft_expired); | ||
| 871 | } | ||
| 872 | |||
| 873 | /// Test 19: Announcement with missing repo path is skipped on restore | ||
| 874 | #[tokio::test] | ||
| 875 | async fn test_announcement_missing_repo_skipped_on_restore() { | ||
| 876 | let temp_dir = tempfile::tempdir().unwrap(); | ||
| 877 | let git_data_path = temp_dir.path().join("git"); | ||
| 878 | let state_path = temp_dir.path().join("purgatory.json"); | ||
| 879 | |||
| 880 | // Point to a path that does NOT exist on disk | ||
| 881 | let missing_repo = temp_dir.path().join("nonexistent.git"); | ||
| 882 | |||
| 883 | let purgatory = Purgatory::new(&git_data_path); | ||
| 884 | let keys = Keys::generate(); | ||
| 885 | |||
| 886 | let ann_event = create_announcement_event(&keys, "my-repo", &[], &[]).unwrap(); | ||
| 887 | |||
| 888 | purgatory.add_announcement( | ||
| 889 | ann_event, | ||
| 890 | "my-repo".to_string(), | ||
| 891 | keys.public_key(), | ||
| 892 | missing_repo, | ||
| 893 | HashSet::new(), | ||
| 894 | ); | ||
| 895 | |||
| 896 | purgatory.save_to_disk(&state_path).unwrap(); | ||
| 897 | |||
| 898 | let purgatory2 = Purgatory::new(&git_data_path); | ||
| 899 | purgatory2.restore_from_disk(&state_path).unwrap(); | ||
| 900 | |||
| 901 | let (ann_count, _, _) = purgatory2.count(); | ||
| 902 | assert_eq!( | ||
| 903 | ann_count, 0, | ||
| 904 | "Announcement with missing repo path must be skipped" | ||
| 905 | ); | ||
| 906 | } | ||
diff --git a/tests/purgatory_sync.rs b/tests/purgatory_sync.rs index 72f3d81..eefd6bc 100644 --- a/tests/purgatory_sync.rs +++ b/tests/purgatory_sync.rs | |||
| @@ -282,15 +282,20 @@ async fn test_state_event_syncs_from_remote() { | |||
| 282 | /// Test that a PR event entering purgatory triggers remote commit fetch | 282 | /// Test that a PR event entering purgatory triggers remote commit fetch |
| 283 | /// and is released once the commit is available. | 283 | /// and is released once the commit is available. |
| 284 | /// | 284 | /// |
| 285 | /// Scenario: | 285 | /// Flow on source relay: |
| 286 | /// 1. Start source relay with repository announcement | 286 | /// 1. Send announcement → purgatory (StateOnly - no git data yet) |
| 287 | /// 2. Create PR event (goes to purgatory - no git data yet) | 287 | /// 2. Send state event → purgatory (refs point to non-existent commits) |
| 288 | /// 3. Push commit to refs/nostr/<event-id> (authorized by PR event in purgatory) | 288 | /// 3. Push git data → promotes announcement to Full + releases state event |
| 289 | /// 4. PR event gets released from purgatory on source relay | 289 | /// 4. Send PR event → purgatory (announcement now Full, so PR events accepted) |
| 290 | /// 5. Start syncing relay | 290 | /// 5. Push PR commit → releases PR event |
| 291 | /// 6. Syncing relay syncs PR event (goes to purgatory - no local git data) | 291 | /// |
| 292 | /// 7. Syncing relay fetches commit from source's clone URL | 292 | /// Flow on syncing relay: |
| 293 | /// 8. Verify PR event is released and refs/nostr/<event-id> created on syncing relay | 293 | /// 6. Start syncing relay |
| 294 | /// 7. Syncs announcement → purgatory (StateOnly) | ||
| 295 | /// 8. Syncs state event → purgatory | ||
| 296 | /// 9. Fetches git data → promotes announcement (Full) + releases state event | ||
| 297 | /// 10. Syncs PR event → purgatory (announcement now Full) | ||
| 298 | /// 11. Fetches PR commit → releases PR event | ||
| 294 | #[tokio::test] | 299 | #[tokio::test] |
| 295 | async fn test_pr_event_syncs_from_remote() { | 300 | async fn test_pr_event_syncs_from_remote() { |
| 296 | // 1. Start source relay | 301 | // 1. Start source relay |
| @@ -313,8 +318,7 @@ async fn test_pr_event_syncs_from_remote() { | |||
| 313 | .to_bech32() | 318 | .to_bech32() |
| 314 | .expect("Failed to get npub"); | 319 | .expect("Failed to get npub"); |
| 315 | 320 | ||
| 316 | // 3. Create and send announcement listing BOTH relays | 321 | // 3. Create announcement listing BOTH relays |
| 317 | // This ensures the syncing relay will accept the PR event when it syncs | ||
| 318 | let announcement = create_repo_announcement( | 322 | let announcement = create_repo_announcement( |
| 319 | &owner_keys, | 323 | &owner_keys, |
| 320 | &[&source_relay.domain(), &syncing_domain], | 324 | &[&source_relay.domain(), &syncing_domain], |
| @@ -331,7 +335,7 @@ async fn test_pr_event_syncs_from_remote() { | |||
| 331 | // Wait for connection | 335 | // Wait for connection |
| 332 | tokio::time::sleep(Duration::from_millis(500)).await; | 336 | tokio::time::sleep(Duration::from_millis(500)).await; |
| 333 | 337 | ||
| 334 | // Send announcement to source relay (creates bare repo) | 338 | // Step 1: Send announcement to source relay → purgatory (StateOnly) |
| 335 | source_client | 339 | source_client |
| 336 | .send_event(&announcement) | 340 | .send_event(&announcement) |
| 337 | .await | 341 | .await |
| @@ -339,8 +343,52 @@ async fn test_pr_event_syncs_from_remote() { | |||
| 339 | 343 | ||
| 340 | tokio::time::sleep(Duration::from_millis(200)).await; | 344 | tokio::time::sleep(Duration::from_millis(200)).await; |
| 341 | 345 | ||
| 342 | // 4. Create and send PR event BEFORE pushing | 346 | // Step 2: Create and send state event → purgatory (no git data yet) |
| 343 | // The PR event goes to purgatory on source relay, which authorizes the push | 347 | let clone_urls = [ |
| 348 | format!( | ||
| 349 | "http://{}/{}/{}.git", | ||
| 350 | source_relay.domain(), | ||
| 351 | npub, | ||
| 352 | identifier | ||
| 353 | ), | ||
| 354 | format!("http://{}/{}/{}.git", syncing_domain, npub, identifier), | ||
| 355 | ]; | ||
| 356 | let relay_urls = [ | ||
| 357 | source_relay.url().to_string(), | ||
| 358 | format!("ws://{}", syncing_domain), | ||
| 359 | ]; | ||
| 360 | |||
| 361 | let state_event = create_state_event( | ||
| 362 | &owner_keys, | ||
| 363 | identifier, | ||
| 364 | &[("main", &commit_hash)], | ||
| 365 | &[], | ||
| 366 | &[&clone_urls[0], &clone_urls[1]], | ||
| 367 | &[&relay_urls[0], &relay_urls[1]], | ||
| 368 | ) | ||
| 369 | .expect("Failed to create state event"); | ||
| 370 | |||
| 371 | let state_event_id = state_event.id; | ||
| 372 | |||
| 373 | source_client | ||
| 374 | .send_event(&state_event) | ||
| 375 | .await | ||
| 376 | .expect("Failed to send state event to source"); | ||
| 377 | |||
| 378 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 379 | |||
| 380 | // Step 3: Push git data to source relay | ||
| 381 | // This promotes the announcement from StateOnly to Full AND releases state event | ||
| 382 | push_to_relay(temp_dir.path(), &source_relay.domain(), &npub, identifier) | ||
| 383 | .expect("Push to source should succeed"); | ||
| 384 | |||
| 385 | // Wait for state event to be released from purgatory on source relay | ||
| 386 | wait_for_event_served(source_relay.url(), &state_event_id, Duration::from_secs(5)) | ||
| 387 | .await | ||
| 388 | .expect("State event should be served on source relay after push"); | ||
| 389 | |||
| 390 | // Step 4: Create and send PR event → purgatory | ||
| 391 | // NOW the announcement is promoted (Full), so PR events are accepted | ||
| 344 | let repo_coord = build_repo_coord(&owner_keys, identifier); | 392 | let repo_coord = build_repo_coord(&owner_keys, identifier); |
| 345 | 393 | ||
| 346 | let pr_event = create_pr_event( | 394 | let pr_event = create_pr_event( |
| @@ -367,11 +415,10 @@ async fn test_pr_event_syncs_from_remote() { | |||
| 367 | .await | 415 | .await |
| 368 | .expect("Failed to send PR event to source"); | 416 | .expect("Failed to send PR event to source"); |
| 369 | 417 | ||
| 370 | // Small delay to ensure PR event is processed into purgatory | ||
| 371 | tokio::time::sleep(Duration::from_millis(200)).await; | 418 | tokio::time::sleep(Duration::from_millis(200)).await; |
| 372 | 419 | ||
| 373 | // 5. Push commit to refs/nostr/<event-id> on source relay | 420 | // Step 5: Push PR commit to refs/nostr/<event-id> on source relay |
| 374 | // The PR event in purgatory authorizes this push | 421 | // This releases the PR event from purgatory |
| 375 | let ref_name = format!("refs/nostr/{}", pr_event_id.to_hex()); | 422 | let ref_name = format!("refs/nostr/{}", pr_event_id.to_hex()); |
| 376 | push_ref_to_relay( | 423 | push_ref_to_relay( |
| 377 | temp_dir.path(), | 424 | temp_dir.path(), |
| @@ -383,12 +430,12 @@ async fn test_pr_event_syncs_from_remote() { | |||
| 383 | ) | 430 | ) |
| 384 | .expect("Push to refs/nostr/<event-id> should succeed"); | 431 | .expect("Push to refs/nostr/<event-id> should succeed"); |
| 385 | 432 | ||
| 386 | // After push, PR event should be released from purgatory on source relay | 433 | // Wait for PR event to be released from purgatory on source relay |
| 387 | wait_for_event_served(source_relay.url(), &pr_event_id, Duration::from_secs(5)) | 434 | wait_for_event_served(source_relay.url(), &pr_event_id, Duration::from_secs(5)) |
| 388 | .await | 435 | .await |
| 389 | .expect("PR event should be served on source relay after push"); | 436 | .expect("PR event should be served on source relay after push"); |
| 390 | 437 | ||
| 391 | // 6. Start syncing relay (syncs from source) | 438 | // Step 6: Start syncing relay (syncs from source) |
| 392 | let syncing_relay = TestRelay::start_on_port_with_options( | 439 | let syncing_relay = TestRelay::start_on_port_with_options( |
| 393 | syncing_port, | 440 | syncing_port, |
| 394 | Some(source_relay.url().to_string()), | 441 | Some(source_relay.url().to_string()), |
| @@ -401,14 +448,13 @@ async fn test_pr_event_syncs_from_remote() { | |||
| 401 | .await | 448 | .await |
| 402 | .expect("Sync connection should establish"); | 449 | .expect("Sync connection should establish"); |
| 403 | 450 | ||
| 404 | // 7. Wait for PR event to be released on syncing relay | 451 | // Steps 7-11: Syncing relay syncs events |
| 405 | // The sync should: | 452 | // The sync should: |
| 406 | // a) Fetch the announcement and PR event from source relay | 453 | // a) Sync announcement → purgatory (StateOnly) |
| 407 | // b) Accept announcement (creates bare repo structure) | 454 | // b) Sync state event → purgatory |
| 408 | // c) Put PR event in purgatory (commit missing on syncing relay) | 455 | // c) Fetch git data → promotes announcement (Full) + releases state event |
| 409 | // d) Fetch commit from source relay's clone URL | 456 | // d) Sync PR event → purgatory (announcement now Full) |
| 410 | // e) Release the PR event from purgatory | 457 | // e) Fetch PR commit → releases PR event |
| 411 | // f) Create refs/nostr/<event-id> pointing to the commit | ||
| 412 | let found = wait_for_event_served( | 458 | let found = wait_for_event_served( |
| 413 | syncing_relay.url(), | 459 | syncing_relay.url(), |
| 414 | &pr_event_id, | 460 | &pr_event_id, |
| @@ -422,7 +468,7 @@ async fn test_pr_event_syncs_from_remote() { | |||
| 422 | found.err() | 468 | found.err() |
| 423 | ); | 469 | ); |
| 424 | 470 | ||
| 425 | // 8. Verify refs/nostr/<event-id> was created on syncing relay | 471 | // Verify refs/nostr/<event-id> was created on syncing relay |
| 426 | let ref_correct = | 472 | let ref_correct = |
| 427 | check_ref_at_commit(&syncing_domain, &npub, identifier, &ref_name, &commit_hash) | 473 | check_ref_at_commit(&syncing_domain, &npub, identifier, &ref_name, &commit_hash) |
| 428 | .await | 474 | .await |
| @@ -443,14 +489,20 @@ async fn test_pr_event_syncs_from_remote() { | |||
| 443 | /// Test that concurrent state and PR events for the same repository | 489 | /// Test that concurrent state and PR events for the same repository |
| 444 | /// both sync correctly. | 490 | /// both sync correctly. |
| 445 | /// | 491 | /// |
| 446 | /// Scenario: | 492 | /// Flow on source relay: |
| 447 | /// 1. Start source relay with repo containing two commits (main branch + PR commit) | 493 | /// 1. Send announcement → purgatory (StateOnly - no git data yet) |
| 448 | /// 2. Create and push both commits to source relay | 494 | /// 2. Send state event → purgatory (refs point to non-existent commits) |
| 449 | /// 3. Send both state event and PR event to source relay | 495 | /// 3. Push git data → promotes announcement to Full + releases state event |
| 450 | /// 4. Start syncing relay | 496 | /// 4. THEN send PR event → purgatory (announcement now Full, so PR events accepted) |
| 451 | /// 5. Wait for sync to fetch git data and release both events | 497 | /// 5. Push PR commit → releases PR event |
| 452 | /// 6. Verify both state event and PR event are served | 498 | /// |
| 453 | /// 7. Verify refs are correct for both (main branch and refs/nostr/<event-id>) | 499 | /// Flow on syncing relay: |
| 500 | /// 6. Start syncing relay | ||
| 501 | /// 7. Syncs announcement → purgatory (StateOnly) | ||
| 502 | /// 8. Syncs state event → purgatory | ||
| 503 | /// 9. Fetches git data → promotes announcement (Full) + releases state event | ||
| 504 | /// 10. Syncs PR event → purgatory (announcement now Full) | ||
| 505 | /// 11. Fetches PR commit → releases PR event | ||
| 454 | #[tokio::test] | 506 | #[tokio::test] |
| 455 | async fn test_concurrent_state_and_pr_sync() { | 507 | async fn test_concurrent_state_and_pr_sync() { |
| 456 | // 1. Start source relay | 508 | // 1. Start source relay |
| @@ -464,15 +516,13 @@ async fn test_concurrent_state_and_pr_sync() { | |||
| 464 | let syncing_domain = format!("127.0.0.1:{}", syncing_port); | 516 | let syncing_domain = format!("127.0.0.1:{}", syncing_port); |
| 465 | 517 | ||
| 466 | // 2. Create test repository with two commits | 518 | // 2. Create test repository with two commits |
| 467 | // First commit establishes the repo, second commit is used for both state and PR events | 519 | // First commit establishes the repo (for state event), second commit is for PR |
| 468 | let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); | 520 | let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); |
| 469 | let _first_commit = create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) | 521 | let _state_commit = create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) |
| 470 | .expect("Failed to create test repo"); | 522 | .expect("Failed to create test repo"); |
| 471 | 523 | ||
| 472 | // Add second commit - this becomes HEAD of main and is referenced by both events | 524 | // Add second commit - this is used for the PR event |
| 473 | // In a real scenario, the state event would reference the current branch state, | 525 | let pr_commit = |
| 474 | // and the PR would propose changes (which happen to be the same commit here for simplicity) | ||
| 475 | let head_commit = | ||
| 476 | add_commit_to_repo(temp_dir.path(), CommitVariant::PrTest).expect("Failed to add commit"); | 526 | add_commit_to_repo(temp_dir.path(), CommitVariant::PrTest).expect("Failed to add commit"); |
| 477 | 527 | ||
| 478 | let npub = owner_keys | 528 | let npub = owner_keys |
| @@ -480,7 +530,7 @@ async fn test_concurrent_state_and_pr_sync() { | |||
| 480 | .to_bech32() | 530 | .to_bech32() |
| 481 | .expect("Failed to get npub"); | 531 | .expect("Failed to get npub"); |
| 482 | 532 | ||
| 483 | // 3. Create and send announcement listing BOTH relays | 533 | // 3. Create announcement listing BOTH relays |
| 484 | let announcement = create_repo_announcement( | 534 | let announcement = create_repo_announcement( |
| 485 | &owner_keys, | 535 | &owner_keys, |
| 486 | &[&source_relay.domain(), &syncing_domain], | 536 | &[&source_relay.domain(), &syncing_domain], |
| @@ -497,7 +547,7 @@ async fn test_concurrent_state_and_pr_sync() { | |||
| 497 | // Wait for connection | 547 | // Wait for connection |
| 498 | tokio::time::sleep(Duration::from_millis(500)).await; | 548 | tokio::time::sleep(Duration::from_millis(500)).await; |
| 499 | 549 | ||
| 500 | // Send announcement to source relay (creates bare repo) | 550 | // Step 1: Send announcement to source relay → purgatory (StateOnly) |
| 501 | source_client | 551 | source_client |
| 502 | .send_event(&announcement) | 552 | .send_event(&announcement) |
| 503 | .await | 553 | .await |
| @@ -505,8 +555,7 @@ async fn test_concurrent_state_and_pr_sync() { | |||
| 505 | 555 | ||
| 506 | tokio::time::sleep(Duration::from_millis(200)).await; | 556 | tokio::time::sleep(Duration::from_millis(200)).await; |
| 507 | 557 | ||
| 508 | // 4. Create state event referencing the HEAD commit (pr_commit) | 558 | // Step 2: Create and send state event → purgatory (no git data yet) |
| 509 | // After add_commit_to_repo, main points to pr_commit (which includes state_commit in history) | ||
| 510 | let clone_urls = [ | 559 | let clone_urls = [ |
| 511 | format!( | 560 | format!( |
| 512 | "http://{}/{}/{}.git", | 561 | "http://{}/{}/{}.git", |
| @@ -521,11 +570,13 @@ async fn test_concurrent_state_and_pr_sync() { | |||
| 521 | format!("ws://{}", syncing_domain), | 570 | format!("ws://{}", syncing_domain), |
| 522 | ]; | 571 | ]; |
| 523 | 572 | ||
| 524 | // State event references main at head_commit (the current HEAD) | 573 | // State event references main at pr_commit (HEAD after add_commit_to_repo). |
| 574 | // push_to_relay uses `git push --all` which pushes main -> pr_commit (HEAD), | ||
| 575 | // so the state event must reference pr_commit for push validation to succeed. | ||
| 525 | let state_event = create_state_event( | 576 | let state_event = create_state_event( |
| 526 | &owner_keys, | 577 | &owner_keys, |
| 527 | identifier, | 578 | identifier, |
| 528 | &[("main", &head_commit)], | 579 | &[("main", &pr_commit)], |
| 529 | &[], | 580 | &[], |
| 530 | &[&clone_urls[0], &clone_urls[1]], | 581 | &[&clone_urls[0], &clone_urls[1]], |
| 531 | &[&relay_urls[0], &relay_urls[1]], | 582 | &[&relay_urls[0], &relay_urls[1]], |
| @@ -534,20 +585,31 @@ async fn test_concurrent_state_and_pr_sync() { | |||
| 534 | 585 | ||
| 535 | let state_event_id = state_event.id; | 586 | let state_event_id = state_event.id; |
| 536 | 587 | ||
| 537 | // Send state event to source relay (goes to purgatory - no git data yet) | ||
| 538 | source_client | 588 | source_client |
| 539 | .send_event(&state_event) | 589 | .send_event(&state_event) |
| 540 | .await | 590 | .await |
| 541 | .expect("Failed to send state event to source"); | 591 | .expect("Failed to send state event to source"); |
| 542 | 592 | ||
| 543 | // 5. Create PR event referencing the same commit (head_commit) | 593 | tokio::time::sleep(Duration::from_millis(200)).await; |
| 544 | // This simulates a PR that proposes the changes in head_commit | 594 | |
| 595 | // Step 3: Push git data to source relay | ||
| 596 | // This promotes the announcement from StateOnly to Full AND releases state event | ||
| 597 | push_to_relay(temp_dir.path(), &source_relay.domain(), &npub, identifier) | ||
| 598 | .expect("Push to source should succeed"); | ||
| 599 | |||
| 600 | // Wait for state event to be released from purgatory on source relay | ||
| 601 | wait_for_event_served(source_relay.url(), &state_event_id, Duration::from_secs(5)) | ||
| 602 | .await | ||
| 603 | .expect("State event should be served on source relay after push"); | ||
| 604 | |||
| 605 | // Step 4: Create and send PR event → purgatory | ||
| 606 | // NOW the announcement is promoted (Full), so PR events are accepted | ||
| 545 | let repo_coord = build_repo_coord(&owner_keys, identifier); | 607 | let repo_coord = build_repo_coord(&owner_keys, identifier); |
| 546 | 608 | ||
| 547 | let pr_event = create_pr_event( | 609 | let pr_event = create_pr_event( |
| 548 | &pr_author_keys, | 610 | &pr_author_keys, |
| 549 | &repo_coord, | 611 | &repo_coord, |
| 550 | &head_commit, | 612 | &pr_commit, |
| 551 | "Test PR for concurrent sync", | 613 | "Test PR for concurrent sync", |
| 552 | ) | 614 | ) |
| 553 | .expect("Failed to create PR event"); | 615 | .expect("Failed to create PR event"); |
| @@ -570,33 +632,25 @@ async fn test_concurrent_state_and_pr_sync() { | |||
| 570 | 632 | ||
| 571 | tokio::time::sleep(Duration::from_millis(200)).await; | 633 | tokio::time::sleep(Duration::from_millis(200)).await; |
| 572 | 634 | ||
| 573 | // 6. Push git data to source relay | 635 | // Step 5: Push PR commit to refs/nostr/<event-id> on source relay |
| 574 | // Push all branches (main contains both commits due to linear history) | 636 | // This releases the PR event from purgatory |
| 575 | push_to_relay(temp_dir.path(), &source_relay.domain(), &npub, identifier) | ||
| 576 | .expect("Push to source should succeed"); | ||
| 577 | |||
| 578 | // Also push the PR ref | ||
| 579 | let pr_ref_name = format!("refs/nostr/{}", pr_event_id.to_hex()); | 637 | let pr_ref_name = format!("refs/nostr/{}", pr_event_id.to_hex()); |
| 580 | push_ref_to_relay( | 638 | push_ref_to_relay( |
| 581 | temp_dir.path(), | 639 | temp_dir.path(), |
| 582 | &source_relay.domain(), | 640 | &source_relay.domain(), |
| 583 | &npub, | 641 | &npub, |
| 584 | identifier, | 642 | identifier, |
| 585 | &head_commit, | 643 | &pr_commit, |
| 586 | &pr_ref_name, | 644 | &pr_ref_name, |
| 587 | ) | 645 | ) |
| 588 | .expect("Push PR ref to source should succeed"); | 646 | .expect("Push PR ref to source should succeed"); |
| 589 | 647 | ||
| 590 | // After push, both events should be released from purgatory on source relay | 648 | // Wait for PR event to be released from purgatory on source relay |
| 591 | wait_for_event_served(source_relay.url(), &state_event_id, Duration::from_secs(5)) | ||
| 592 | .await | ||
| 593 | .expect("State event should be served on source relay after push"); | ||
| 594 | |||
| 595 | wait_for_event_served(source_relay.url(), &pr_event_id, Duration::from_secs(5)) | 649 | wait_for_event_served(source_relay.url(), &pr_event_id, Duration::from_secs(5)) |
| 596 | .await | 650 | .await |
| 597 | .expect("PR event should be served on source relay after push"); | 651 | .expect("PR event should be served on source relay after push"); |
| 598 | 652 | ||
| 599 | // 7. Start syncing relay (syncs from source) | 653 | // Step 6: Start syncing relay (syncs from source) |
| 600 | let syncing_relay = TestRelay::start_on_port_with_options( | 654 | let syncing_relay = TestRelay::start_on_port_with_options( |
| 601 | syncing_port, | 655 | syncing_port, |
| 602 | Some(source_relay.url().to_string()), | 656 | Some(source_relay.url().to_string()), |
| @@ -609,8 +663,13 @@ async fn test_concurrent_state_and_pr_sync() { | |||
| 609 | .await | 663 | .await |
| 610 | .expect("Sync connection should establish"); | 664 | .expect("Sync connection should establish"); |
| 611 | 665 | ||
| 612 | // 8. Wait for BOTH events to be released on syncing relay | 666 | // Steps 7-11: Syncing relay syncs events |
| 613 | // The sync should fetch git data and release both events | 667 | // The sync should: |
| 668 | // a) Sync announcement → purgatory (StateOnly) | ||
| 669 | // b) Sync state event → purgatory | ||
| 670 | // c) Fetch git data → promotes announcement (Full) + releases state event | ||
| 671 | // d) Sync PR event → purgatory (announcement now Full) | ||
| 672 | // e) Fetch PR commit → releases PR event | ||
| 614 | let state_found = wait_for_event_served( | 673 | let state_found = wait_for_event_served( |
| 615 | syncing_relay.url(), | 674 | syncing_relay.url(), |
| 616 | &state_event_id, | 675 | &state_event_id, |
| @@ -629,18 +688,18 @@ async fn test_concurrent_state_and_pr_sync() { | |||
| 629 | 688 | ||
| 630 | assert!( | 689 | assert!( |
| 631 | pr_found.is_ok(), | 690 | pr_found.is_ok(), |
| 632 | "PR event should be served after sync fetches git data: {:?}", | 691 | "PR event should be served after sync fetches commit: {:?}", |
| 633 | pr_found.err() | 692 | pr_found.err() |
| 634 | ); | 693 | ); |
| 635 | 694 | ||
| 636 | // 9. Verify refs are correct on syncing relay | 695 | // Verify refs are correct on syncing relay |
| 637 | // Check main branch points to head_commit (the HEAD) | 696 | // Check main branch points to pr_commit (HEAD after both commits) |
| 638 | let main_ref_correct = check_ref_at_commit( | 697 | let main_ref_correct = check_ref_at_commit( |
| 639 | &syncing_domain, | 698 | &syncing_domain, |
| 640 | &npub, | 699 | &npub, |
| 641 | identifier, | 700 | identifier, |
| 642 | "refs/heads/main", | 701 | "refs/heads/main", |
| 643 | &head_commit, | 702 | &pr_commit, // After push, main points to pr_commit (HEAD) |
| 644 | ) | 703 | ) |
| 645 | .await | 704 | .await |
| 646 | .expect("Failed to check main ref"); | 705 | .expect("Failed to check main ref"); |
| @@ -648,24 +707,24 @@ async fn test_concurrent_state_and_pr_sync() { | |||
| 648 | assert!( | 707 | assert!( |
| 649 | main_ref_correct, | 708 | main_ref_correct, |
| 650 | "main branch should point to HEAD commit ({})", | 709 | "main branch should point to HEAD commit ({})", |
| 651 | head_commit | 710 | pr_commit |
| 652 | ); | 711 | ); |
| 653 | 712 | ||
| 654 | // Check refs/nostr/<event-id> points to the same commit | 713 | // Check refs/nostr/<event-id> points to pr_commit |
| 655 | let pr_ref_correct = check_ref_at_commit( | 714 | let pr_ref_correct = check_ref_at_commit( |
| 656 | &syncing_domain, | 715 | &syncing_domain, |
| 657 | &npub, | 716 | &npub, |
| 658 | identifier, | 717 | identifier, |
| 659 | &pr_ref_name, | 718 | &pr_ref_name, |
| 660 | &head_commit, | 719 | &pr_commit, |
| 661 | ) | 720 | ) |
| 662 | .await | 721 | .await |
| 663 | .expect("Failed to check PR ref"); | 722 | .expect("Failed to check PR ref"); |
| 664 | 723 | ||
| 665 | assert!( | 724 | assert!( |
| 666 | pr_ref_correct, | 725 | pr_ref_correct, |
| 667 | "refs/nostr/<event-id> should point to commit ({})", | 726 | "refs/nostr/<event-id> should point to PR commit ({})", |
| 668 | head_commit | 727 | pr_commit |
| 669 | ); | 728 | ); |
| 670 | 729 | ||
| 671 | // Cleanup | 730 | // Cleanup |
| @@ -921,162 +980,43 @@ async fn test_pr_event_clone_tag_sync_with_partial_oid_aggregation_from_multiple | |||
| 921 | .expect("PR event should be served on mock_relay immediately"); | 980 | .expect("PR event should be served on mock_relay immediately"); |
| 922 | 981 | ||
| 923 | // ======================================================================== | 982 | // ======================================================================== |
| 924 | // Step 5: Start syncing_relay WITHOUT bootstrap and publish announcement directly | 983 | // Step 5: Start syncing_relay with source_grasp as bootstrap |
| 925 | // ======================================================================== | 984 | // ======================================================================== |
| 926 | 985 | ||
| 927 | // Start syncing_relay with sync enabled but NO bootstrap relay | 986 | // Start syncing_relay with source_grasp as bootstrap relay. |
| 928 | // This tests relay discovery from announcement's `relays` tag | 987 | // Negentropy is disabled because MockRelay doesn't support NIP-77, and the |
| 929 | // Note: We disable negentropy because MockRelay doesn't support NIP-77, | 988 | // sync system doesn't properly fall back to REQ+EOSE when negentropy fails. |
| 930 | // and the sync system doesn't properly fall back to REQ+EOSE when negentropy fails. | 989 | // |
| 990 | // We do NOT publish the announcement directly to syncing_relay. Instead, | ||
| 991 | // syncing_relay discovers it via the bootstrap connection to source_grasp, | ||
| 992 | // which has the promoted announcement in its database. | ||
| 931 | let syncing_relay = TestRelay::start_on_port_with_options( | 993 | let syncing_relay = TestRelay::start_on_port_with_options( |
| 932 | syncing_port, | 994 | syncing_port, |
| 933 | None, // NO bootstrap - relay discovery via announcement tags | 995 | Some(source_grasp.url().to_string()), // Bootstrap from source_grasp |
| 934 | true, // Disable negentropy - MockRelay doesn't support NIP-77 | 996 | true, // Disable negentropy - MockRelay doesn't support NIP-77 |
| 935 | ) | 997 | ) |
| 936 | .await; | 998 | .await; |
| 937 | 999 | ||
| 938 | // Publish announcement DIRECTLY to syncing_relay | ||
| 939 | // This triggers relay discovery from the announcement's `relays` tag | ||
| 940 | let syncing_client = Client::new(owner_keys.clone()); | ||
| 941 | syncing_client | ||
| 942 | .add_relay(syncing_relay.url()) | ||
| 943 | .await | ||
| 944 | .expect("Failed to add syncing_relay"); | ||
| 945 | syncing_client.connect().await; | ||
| 946 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 947 | |||
| 948 | syncing_client | ||
| 949 | .send_event(&announcement) | ||
| 950 | .await | ||
| 951 | .expect("Failed to send announcement to syncing_relay"); | ||
| 952 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 953 | |||
| 954 | // Wait for relay discovery and sync connections to establish | ||
| 955 | // syncing_relay should discover source_grasp and mock_relay from announcement's relays tag | ||
| 956 | println!("=== Waiting for sync connections ==="); | ||
| 957 | println!("syncing_relay URL: {}", syncing_relay.url()); | ||
| 958 | println!("source_grasp URL: {}", source_grasp.url()); | ||
| 959 | println!("mock_relay URL: {}", mock_relay.url()); | ||
| 960 | println!("git_server URL: {}", git_server.url()); | ||
| 961 | |||
| 962 | wait_for_sync_connection(syncing_relay.url(), 2, Duration::from_secs(10)) | ||
| 963 | .await | ||
| 964 | .expect( | ||
| 965 | "Sync connections should establish to discovered relays (source_grasp + mock_relay)", | ||
| 966 | ); | ||
| 967 | println!("Sync connections established!"); | ||
| 968 | |||
| 969 | // Debug: Check metrics to see what relays are connected | ||
| 970 | let metrics_url = syncing_relay | ||
| 971 | .url() | ||
| 972 | .replace("ws://", "http://") | ||
| 973 | .replace("/", "") | ||
| 974 | + "/metrics"; | ||
| 975 | println!("Checking metrics at: {}", metrics_url); | ||
| 976 | if let Ok(response) = reqwest::get(&metrics_url).await { | ||
| 977 | if let Ok(metrics) = response.text().await { | ||
| 978 | // Print sync-related metrics | ||
| 979 | for line in metrics.lines() { | ||
| 980 | if line.contains("sync") && !line.starts_with('#') { | ||
| 981 | println!(" {}", line); | ||
| 982 | } | ||
| 983 | } | ||
| 984 | } | ||
| 985 | } | ||
| 986 | |||
| 987 | // Give some time for sync to happen | ||
| 988 | println!("Waiting 10s for events to sync..."); | ||
| 989 | tokio::time::sleep(Duration::from_secs(10)).await; | ||
| 990 | |||
| 991 | // Check metrics again after waiting | ||
| 992 | println!("=== Checking metrics after sync wait ==="); | ||
| 993 | if let Ok(response) = reqwest::get(&metrics_url).await { | ||
| 994 | if let Ok(metrics) = response.text().await { | ||
| 995 | for line in metrics.lines() { | ||
| 996 | if line.contains("sync") && !line.starts_with('#') { | ||
| 997 | println!(" {}", line); | ||
| 998 | } | ||
| 999 | } | ||
| 1000 | } | ||
| 1001 | } | ||
| 1002 | |||
| 1003 | // Debug: Check if PR event is still on mock_relay | ||
| 1004 | println!("=== Debug: Checking PR event on mock_relay ==="); | ||
| 1005 | let pr_on_mock = | ||
| 1006 | wait_for_event_served(mock_relay.url(), &pr_event_id, Duration::from_secs(2)).await; | ||
| 1007 | println!("PR event on mock_relay: {:?}", pr_on_mock.is_ok()); | ||
| 1008 | if let Ok(ref pr) = pr_on_mock { | ||
| 1009 | println!("PR event tags:"); | ||
| 1010 | for tag in pr.tags.iter() { | ||
| 1011 | println!(" {:?}", tag.as_slice()); | ||
| 1012 | } | ||
| 1013 | } | ||
| 1014 | |||
| 1015 | // Debug: Check repo coordinate | ||
| 1016 | let repo_coord = build_repo_coord(&owner_keys, identifier); | ||
| 1017 | println!("Expected repo coordinate: {}", repo_coord); | ||
| 1018 | |||
| 1019 | // Debug: Test if mock_relay responds to tag-based filter (Layer 2 style) | ||
| 1020 | println!("=== Debug: Testing mock_relay tag filter response ==="); | ||
| 1021 | let test_client = Client::new(Keys::generate()); | ||
| 1022 | test_client | ||
| 1023 | .add_relay(mock_relay.url()) | ||
| 1024 | .await | ||
| 1025 | .expect("Failed to add mock_relay"); | ||
| 1026 | test_client.connect().await; | ||
| 1027 | tokio::time::sleep(Duration::from_millis(500)).await; | ||
| 1028 | |||
| 1029 | // Build a Layer 2 style filter (by 'a' tag) | ||
| 1030 | let tag_filter = | ||
| 1031 | Filter::new().custom_tag(SingleLetterTag::lowercase(Alphabet::A), repo_coord.as_str()); | ||
| 1032 | println!("Tag filter: {:?}", tag_filter); | ||
| 1033 | |||
| 1034 | let tag_results = test_client | ||
| 1035 | .fetch_events(tag_filter, Duration::from_secs(5)) | ||
| 1036 | .await; | ||
| 1037 | match tag_results { | ||
| 1038 | Ok(events) => { | ||
| 1039 | println!("Tag filter returned {} events", events.len()); | ||
| 1040 | for event in events.iter() { | ||
| 1041 | println!(" Event ID: {}, Kind: {}", event.id, event.kind.as_u16()); | ||
| 1042 | } | ||
| 1043 | } | ||
| 1044 | Err(e) => { | ||
| 1045 | println!("Tag filter query failed: {:?}", e); | ||
| 1046 | } | ||
| 1047 | } | ||
| 1048 | test_client.disconnect().await; | ||
| 1049 | |||
| 1050 | // The syncing relay will: | 1000 | // The syncing relay will: |
| 1051 | // 1. Receive announcement directly (creates bare repo) | 1001 | // 1. Sync promoted announcement from source_grasp via bootstrap connection → purgatory (no local git data) |
| 1052 | // 2. Discover source_grasp and mock_relay from announcement's `relays` tag | 1002 | // 2. EOSE triggers StateOnly subscription → syncs state event from source_grasp → purgatory sync |
| 1053 | // 3. Connect to discovered relays | 1003 | // 3. Purgatory sync fetches commit_a from source_grasp clone URL → announcement + state promoted |
| 1054 | // 4. Sync state event from source_grasp → purgatory (no commit_a locally) | 1004 | // 4. SelfSubscriber sees promoted announcement → upgrades to Full → connects to mock_relay |
| 1055 | // 5. Sync PR event from mock_relay → purgatory (no commit_b locally) | 1005 | // 5. Syncs PR event from mock_relay → purgatory (no commit_b locally) |
| 1056 | // 6. Purgatory sync triggers | 1006 | // 6. Purgatory sync fetches commit_b from git_server via PR clone tag |
| 1057 | // 7. Fetches commit_a from source_grasp clone URL (from announcement clone tag) | 1007 | // 7. PR event promoted → served |
| 1058 | // 8. Fetches commit_b from git_server (from PR event's clone tag) | ||
| 1059 | // 9. Both events released when all OIDs available | ||
| 1060 | 1008 | ||
| 1061 | // ======================================================================== | 1009 | // ======================================================================== |
| 1062 | // Step 6: Verify Results | 1010 | // Step 6: Verify Results |
| 1063 | // ======================================================================== | 1011 | // ======================================================================== |
| 1064 | 1012 | ||
| 1065 | println!("=== Step 6: Verify Results ==="); | ||
| 1066 | println!("State event ID: {}", state_event_id); | ||
| 1067 | println!("PR event ID: {}", pr_event_id); | ||
| 1068 | println!("commit_a: {}", commit_a); | ||
| 1069 | println!("commit_b: {}", commit_b); | ||
| 1070 | |||
| 1071 | // Wait for state event to be served on syncing_relay | 1013 | // Wait for state event to be served on syncing_relay |
| 1072 | println!("Waiting for state event on syncing_relay..."); | ||
| 1073 | let state_found = wait_for_event_served( | 1014 | let state_found = wait_for_event_served( |
| 1074 | syncing_relay.url(), | 1015 | syncing_relay.url(), |
| 1075 | &state_event_id, | 1016 | &state_event_id, |
| 1076 | Duration::from_secs(30), | 1017 | Duration::from_secs(30), |
| 1077 | ) | 1018 | ) |
| 1078 | .await; | 1019 | .await; |
| 1079 | println!("State event result: {:?}", state_found); | ||
| 1080 | assert!( | 1020 | assert!( |
| 1081 | state_found.is_ok(), | 1021 | state_found.is_ok(), |
| 1082 | "State event should be served on syncing_relay: {:?}", | 1022 | "State event should be served on syncing_relay: {:?}", |
| @@ -1084,10 +1024,8 @@ async fn test_pr_event_clone_tag_sync_with_partial_oid_aggregation_from_multiple | |||
| 1084 | ); | 1024 | ); |
| 1085 | 1025 | ||
| 1086 | // Wait for PR event to be served on syncing_relay | 1026 | // Wait for PR event to be served on syncing_relay |
| 1087 | println!("Waiting for PR event on syncing_relay..."); | ||
| 1088 | let pr_found = | 1027 | let pr_found = |
| 1089 | wait_for_event_served(syncing_relay.url(), &pr_event_id, Duration::from_secs(30)).await; | 1028 | wait_for_event_served(syncing_relay.url(), &pr_event_id, Duration::from_secs(30)).await; |
| 1090 | println!("PR event result: {:?}", pr_found); | ||
| 1091 | assert!( | 1029 | assert!( |
| 1092 | pr_found.is_ok(), | 1030 | pr_found.is_ok(), |
| 1093 | "PR event should be served on syncing_relay (fetched commit_b from git_server via PR clone tag): {:?}", | 1031 | "PR event should be served on syncing_relay (fetched commit_b from git_server via PR clone tag): {:?}", |
| @@ -1128,7 +1066,6 @@ async fn test_pr_event_clone_tag_sync_with_partial_oid_aggregation_from_multiple | |||
| 1128 | source_client.disconnect().await; | 1066 | source_client.disconnect().await; |
| 1129 | mock_client.disconnect().await; | 1067 | mock_client.disconnect().await; |
| 1130 | pr_client.disconnect().await; | 1068 | pr_client.disconnect().await; |
| 1131 | syncing_client.disconnect().await; | ||
| 1132 | git_server.stop().await; | 1069 | git_server.stop().await; |
| 1133 | mock_relay.stop().await; | 1070 | mock_relay.stop().await; |
| 1134 | syncing_relay.stop().await; | 1071 | syncing_relay.stop().await; |
diff --git a/tests/sync/discovery.rs b/tests/sync/discovery.rs index 8ed80b5..d45a290 100644 --- a/tests/sync/discovery.rs +++ b/tests/sync/discovery.rs | |||
| @@ -3,10 +3,6 @@ | |||
| 3 | //! Tests for relay discovery from announcement events. | 3 | //! Tests for relay discovery from announcement events. |
| 4 | //! When a relay receives an announcement listing another relay, | 4 | //! When a relay receives an announcement listing another relay, |
| 5 | //! it should discover and connect to that relay to sync events. | 5 | //! it should discover and connect to that relay to sync events. |
| 6 | //! | ||
| 7 | //! # Tests | ||
| 8 | //! - Test 2: Direct Layer 3 discovery from Layer 2 | ||
| 9 | //! - Test 3: Recursive multi-hop Layer 3 discovery | ||
| 10 | 6 | ||
| 11 | use std::time::Duration; | 7 | use std::time::Duration; |
| 12 | 8 | ||
| @@ -62,29 +58,26 @@ async fn test_discovers_layer3_via_layer2() { | |||
| 62 | // 3. Create test keys | 58 | // 3. Create test keys |
| 63 | let keys = Keys::generate(); | 59 | let keys = Keys::generate(); |
| 64 | 60 | ||
| 65 | // 4. Create a repository announcement that lists BOTH relays | 61 | // 4. Set up repository announcement on relay_a with git data |
| 66 | let announcement = create_repo_announcement( | 62 | // (purgatory requires git data before announcements are accepted) |
| 67 | &keys, | 63 | let repo_id = "test-repo-discovery"; |
| 68 | &[&relay_a.domain(), &relay_b.domain()], | 64 | let domains = vec![relay_a.domain(), relay_b.domain()]; |
| 69 | "test-repo-discovery", | 65 | let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect(); |
| 70 | ); | ||
| 71 | let announcement_id = announcement.id; | ||
| 72 | 66 | ||
| 67 | let (announcement, _git_dir_a) = | ||
| 68 | setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await; | ||
| 69 | let announcement_id = announcement.id; | ||
| 73 | println!( | 70 | println!( |
| 74 | "Created announcement {} (kind {})", | 71 | "Announcement {} set up on relay_a with git data", |
| 75 | announcement_id, | 72 | announcement_id |
| 76 | announcement.kind.as_u16() | ||
| 77 | ); | 73 | ); |
| 78 | for tag in announcement.tags.iter() { | ||
| 79 | println!(" Tag: {:?}", tag.as_slice()); | ||
| 80 | } | ||
| 81 | 74 | ||
| 82 | // 5. Build the repo coordinate for the 'a' tag in the patch | 75 | // 5. Build the repo coordinate for the 'a' tag in the patch |
| 83 | let repo_coord = format!( | 76 | let repo_coord = format!( |
| 84 | "{}:{}:{}", | 77 | "{}:{}:{}", |
| 85 | Kind::GitRepoAnnouncement.as_u16(), | 78 | Kind::GitRepoAnnouncement.as_u16(), |
| 86 | keys.public_key().to_hex(), | 79 | keys.public_key().to_hex(), |
| 87 | "test-repo-discovery" | 80 | repo_id |
| 88 | ); | 81 | ); |
| 89 | 82 | ||
| 90 | // 6. Create a patch event (Layer 2) that references the announcement | 83 | // 6. Create a patch event (Layer 2) that references the announcement |
| @@ -97,22 +90,13 @@ async fn test_discovers_layer3_via_layer2() { | |||
| 97 | let patch_id = patch.id; | 90 | let patch_id = patch.id; |
| 98 | 91 | ||
| 99 | println!("Created patch {} (kind {})", patch_id, patch.kind.as_u16()); | 92 | println!("Created patch {} (kind {})", patch_id, patch.kind.as_u16()); |
| 100 | for tag in patch.tags.iter() { | ||
| 101 | println!(" Tag: {:?}", tag.as_slice()); | ||
| 102 | } | ||
| 103 | 93 | ||
| 104 | // 7. Send announcement and patch to relay_a ONLY | 94 | // 7. Send patch to relay_a |
| 105 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | 95 | let client_a = TestClient::new(relay_a.url(), keys.clone()) |
| 106 | .await | 96 | .await |
| 107 | .expect("Failed to connect to relay_a"); | 97 | .expect("Failed to connect to relay_a"); |
| 108 | 98 | ||
| 109 | client_a | 99 | client_a |
| 110 | .send_event(&announcement) | ||
| 111 | .await | ||
| 112 | .expect("Failed to send announcement to relay_a"); | ||
| 113 | println!("Announcement sent to relay_a"); | ||
| 114 | |||
| 115 | client_a | ||
| 116 | .send_event(&patch) | 100 | .send_event(&patch) |
| 117 | .await | 101 | .await |
| 118 | .expect("Failed to send patch to relay_a"); | 102 | .expect("Failed to send patch to relay_a"); |
| @@ -120,18 +104,10 @@ async fn test_discovers_layer3_via_layer2() { | |||
| 120 | 104 | ||
| 121 | client_a.disconnect().await; | 105 | client_a.disconnect().await; |
| 122 | 106 | ||
| 123 | // 8. Send announcement to relay_b directly (triggers discovery of relay_a) | 107 | // 8. Set up announcement on relay_b (triggers discovery of relay_a) |
| 124 | let client_b = TestClient::new(relay_b.url(), keys.clone()) | 108 | let (_announcement_b, _git_dir_b) = |
| 125 | .await | 109 | setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await; |
| 126 | .expect("Failed to connect to relay_b"); | 110 | println!("Announcement set up on relay_b (should trigger discovery of relay_a)"); |
| 127 | |||
| 128 | client_b | ||
| 129 | .send_event(&announcement) | ||
| 130 | .await | ||
| 131 | .expect("Failed to send announcement to relay_b"); | ||
| 132 | println!("Announcement sent to relay_b (should trigger discovery of relay_a)"); | ||
| 133 | |||
| 134 | client_b.disconnect().await; | ||
| 135 | 111 | ||
| 136 | // 9. Wait for relay_b to discover relay_a and sync the patch | 112 | // 9. Wait for relay_b to discover relay_a and sync the patch |
| 137 | println!("Waiting 3s for relay_b to discover relay_a and sync patch..."); | 113 | println!("Waiting 3s for relay_b to discover relay_a and sync patch..."); |
| @@ -197,19 +173,20 @@ async fn test_relay_discovery_via_announcements_with_historic_sync() { | |||
| 197 | // 3. Create test keys | 173 | // 3. Create test keys |
| 198 | let keys = Keys::generate(); | 174 | let keys = Keys::generate(); |
| 199 | 175 | ||
| 200 | // 4. Create the event chain on relay_a: | 176 | // 4. Set up repository on relay_a with git data and a Layer 2 issue |
| 201 | 177 | ||
| 202 | // Layer 1: Repository announcement | 178 | // Layer 1: Set up announcement with git data |
| 203 | let announcement = create_repo_announcement( | 179 | let domains = vec![relay_a.domain(), relay_b.domain()]; |
| 204 | &keys, | 180 | let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect(); |
| 205 | &[&relay_a.domain(), &relay_b.domain()], | 181 | let repo_id = "test-repo-chain"; |
| 206 | "test-repo-chain", | 182 | |
| 207 | ); | 183 | let (announcement, _git_dir_a) = |
| 184 | setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await; | ||
| 208 | let announcement_id = announcement.id; | 185 | let announcement_id = announcement.id; |
| 209 | println!("Created announcement {} (Layer 1)", announcement_id); | 186 | println!("Announcement {} set up on relay_a with git data (Layer 1)", announcement_id); |
| 210 | 187 | ||
| 211 | // Build repo coordinate for Layer 2 reference | 188 | // Build repo coordinate for Layer 2 reference |
| 212 | let repo_coord = repo_coord(&keys, "test-repo-chain"); | 189 | let repo_coord = repo_coord(&keys, repo_id); |
| 213 | 190 | ||
| 214 | // Layer 2: Issue referencing the repo | 191 | // Layer 2: Issue referencing the repo |
| 215 | let issue = build_layer2_issue_event(&keys, &repo_coord, "Test issue for chain discovery") | 192 | let issue = build_layer2_issue_event(&keys, &repo_coord, "Test issue for chain discovery") |
| @@ -217,35 +194,23 @@ async fn test_relay_discovery_via_announcements_with_historic_sync() { | |||
| 217 | let issue_id = issue.id; | 194 | let issue_id = issue.id; |
| 218 | println!("Created issue {} (Layer 2)", issue_id); | 195 | println!("Created issue {} (Layer 2)", issue_id); |
| 219 | 196 | ||
| 220 | // 5. Send all events to relay_a | 197 | // 5. Send issue to relay_a |
| 221 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | 198 | let client_a = TestClient::new(relay_a.url(), keys.clone()) |
| 222 | .await | 199 | .await |
| 223 | .expect("Failed to connect to relay_a"); | 200 | .expect("Failed to connect to relay_a"); |
| 224 | 201 | ||
| 225 | client_a | 202 | client_a |
| 226 | .send_event(&announcement) | ||
| 227 | .await | ||
| 228 | .expect("Failed to send announcement"); | ||
| 229 | client_a | ||
| 230 | .send_event(&issue) | 203 | .send_event(&issue) |
| 231 | .await | 204 | .await |
| 232 | .expect("Failed to send issue"); | 205 | .expect("Failed to send issue"); |
| 233 | 206 | ||
| 234 | println!("Events sent to relay_a"); | 207 | println!("Issue sent to relay_a"); |
| 235 | client_a.disconnect().await; | 208 | client_a.disconnect().await; |
| 236 | 209 | ||
| 237 | // 6. Send only the announcement to relay_b (triggers discovery) | 210 | // 6. Set up announcement on relay_b (triggers discovery of relay_a) |
| 238 | let client_b = TestClient::new(relay_b.url(), keys.clone()) | 211 | let (_announcement_b, _git_dir_b) = |
| 239 | .await | 212 | setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await; |
| 240 | .expect("Failed to connect to relay_b"); | 213 | println!("Announcement set up on relay_b (should trigger discovery of relay_a)"); |
| 241 | |||
| 242 | client_b | ||
| 243 | .send_event(&announcement) | ||
| 244 | .await | ||
| 245 | .expect("Failed to send announcement to relay_b"); | ||
| 246 | println!("Announcement sent to relay_b (should trigger discovery)"); | ||
| 247 | |||
| 248 | client_b.disconnect().await; | ||
| 249 | 214 | ||
| 250 | // 7. Wait for sync | 215 | // 7. Wait for sync |
| 251 | println!("Waiting 3s for Layer 2 sync..."); | 216 | println!("Waiting 3s for Layer 2 sync..."); |
| @@ -271,163 +236,3 @@ async fn test_relay_discovery_via_announcements_with_historic_sync() { | |||
| 271 | ); | 236 | ); |
| 272 | } | 237 | } |
| 273 | 238 | ||
| 274 | /// Test 3: 3-relay recursive discovery - relay discovers third relay through bootstrap | ||
| 275 | /// | ||
| 276 | /// Scenario: | ||
| 277 | /// ```text | ||
| 278 | /// relay_a (SUT) relay_b (bootstrap) relay_c (discovered) | ||
| 279 | /// │ │ │ | ||
| 280 | /// │ │ has announcement_x │ has announcement_y | ||
| 281 | /// │ │ listing A+B+C │ listing A+C | ||
| 282 | /// │ │ │ | ||
| 283 | /// ├────connect──────────► │ | ||
| 284 | /// │◄───sync announcement_x─────────────────────── | ||
| 285 | /// │ │ | ||
| 286 | /// │ discovers relay_c from announcement_x │ | ||
| 287 | /// │ │ | ||
| 288 | /// ├─────────────connect─────────────────────────► | ||
| 289 | /// │◄────────────sync announcement_y─────────────┘ | ||
| 290 | /// ``` | ||
| 291 | /// | ||
| 292 | /// This tests that relay_a: | ||
| 293 | /// 1. Connects to relay_b (configured as bootstrap) | ||
| 294 | /// 2. Receives announcement_x which lists relay_c | ||
| 295 | /// 3. Discovers and connects to relay_c | ||
| 296 | /// 4. Syncs announcement_y from relay_c | ||
| 297 | /// | ||
| 298 | #[tokio::test] | ||
| 299 | async fn test_recursive_relay_discovery_via_announcements_with_historic_sync() { | ||
| 300 | // 1. Start all three relays | ||
| 301 | |||
| 302 | // relay_b - will be the bootstrap relay, has announcement_x | ||
| 303 | let relay_b = TestRelay::start().await; | ||
| 304 | println!( | ||
| 305 | "relay_b (bootstrap) started at {} (domain: {})", | ||
| 306 | relay_b.url(), | ||
| 307 | relay_b.domain() | ||
| 308 | ); | ||
| 309 | |||
| 310 | // relay_c - will be discovered via announcement_x, has announcement_y | ||
| 311 | let relay_c = TestRelay::start().await; | ||
| 312 | println!( | ||
| 313 | "relay_c (to be discovered) started at {} (domain: {})", | ||
| 314 | relay_c.url(), | ||
| 315 | relay_c.domain() | ||
| 316 | ); | ||
| 317 | |||
| 318 | // relay_a - SUT, starts with relay_b as bootstrap | ||
| 319 | let relay_a = TestRelay::start_with_sync(Some(relay_b.url().to_string())).await; | ||
| 320 | println!( | ||
| 321 | "relay_a (SUT) started at {} (domain: {})", | ||
| 322 | relay_a.url(), | ||
| 323 | relay_a.domain() | ||
| 324 | ); | ||
| 325 | |||
| 326 | // 2. Create test keys (one for each announcement) | ||
| 327 | let keys_x = Keys::generate(); | ||
| 328 | let keys_y = Keys::generate(); | ||
| 329 | |||
| 330 | // 3. Create announcement_x on relay_b (lists all three relays: A+B+C) | ||
| 331 | let announcement_x = create_repo_announcement( | ||
| 332 | &keys_x, | ||
| 333 | &[&relay_a.domain(), &relay_b.domain(), &relay_c.domain()], | ||
| 334 | "repo-x-all-relays", | ||
| 335 | ); | ||
| 336 | let announcement_x_id = announcement_x.id; | ||
| 337 | println!("Created announcement_x {} listing A+B+C", announcement_x_id); | ||
| 338 | for tag in announcement_x.tags.iter() { | ||
| 339 | println!(" Tag: {:?}", tag.as_slice()); | ||
| 340 | } | ||
| 341 | |||
| 342 | // 4. Create announcement_y on relay_c (lists only A+C, NOT B) | ||
| 343 | let announcement_y = create_repo_announcement( | ||
| 344 | &keys_y, | ||
| 345 | &[&relay_a.domain(), &relay_c.domain()], | ||
| 346 | "repo-y-ac-only", | ||
| 347 | ); | ||
| 348 | let announcement_y_id = announcement_y.id; | ||
| 349 | println!( | ||
| 350 | "Created announcement_y {} listing A+C only", | ||
| 351 | announcement_y_id | ||
| 352 | ); | ||
| 353 | for tag in announcement_y.tags.iter() { | ||
| 354 | println!(" Tag: {:?}", tag.as_slice()); | ||
| 355 | } | ||
| 356 | |||
| 357 | // 5. Send announcement_x to relay_b only | ||
| 358 | let client_b = TestClient::new(relay_b.url(), keys_x.clone()) | ||
| 359 | .await | ||
| 360 | .expect("Failed to connect to relay_b"); | ||
| 361 | |||
| 362 | client_b | ||
| 363 | .send_event(&announcement_x) | ||
| 364 | .await | ||
| 365 | .expect("Failed to send announcement_x to relay_b"); | ||
| 366 | println!("announcement_x sent to relay_b"); | ||
| 367 | |||
| 368 | client_b.disconnect().await; | ||
| 369 | |||
| 370 | // 6. Send announcement_y to relay_c only | ||
| 371 | let client_c = TestClient::new(relay_c.url(), keys_y.clone()) | ||
| 372 | .await | ||
| 373 | .expect("Failed to connect to relay_c"); | ||
| 374 | |||
| 375 | client_c | ||
| 376 | .send_event(&announcement_y) | ||
| 377 | .await | ||
| 378 | .expect("Failed to send announcement_y to relay_c"); | ||
| 379 | println!("announcement_y sent to relay_c"); | ||
| 380 | |||
| 381 | client_c.disconnect().await; | ||
| 382 | |||
| 383 | // 7. Wait for relay_a to: | ||
| 384 | // - Sync from bootstrap relay_b (gets announcement_x) | ||
| 385 | // - Discover relay_c from announcement_x's relays tag | ||
| 386 | // - Connect to relay_c and sync announcement_y | ||
| 387 | println!("Waiting 5s for recursive relay discovery..."); | ||
| 388 | tokio::time::sleep(Duration::from_secs(5)).await; | ||
| 389 | |||
| 390 | // 8. Verify announcement_x was synced to relay_a (from bootstrap relay_b) | ||
| 391 | let filter_x = Filter::new() | ||
| 392 | .kind(Kind::GitRepoAnnouncement) | ||
| 393 | .author(keys_x.public_key()); | ||
| 394 | |||
| 395 | let announcement_x_synced = | ||
| 396 | wait_for_event_on_relay(relay_a.url(), filter_x, Duration::from_secs(5)).await; | ||
| 397 | |||
| 398 | println!( | ||
| 399 | "announcement_x {} synced to relay_a: {}", | ||
| 400 | announcement_x_id, announcement_x_synced | ||
| 401 | ); | ||
| 402 | |||
| 403 | // 9. Verify announcement_y was synced to relay_a (from discovered relay_c) | ||
| 404 | let filter_y = Filter::new() | ||
| 405 | .kind(Kind::GitRepoAnnouncement) | ||
| 406 | .author(keys_y.public_key()); | ||
| 407 | |||
| 408 | let announcement_y_synced = | ||
| 409 | wait_for_event_on_relay(relay_a.url(), filter_y, Duration::from_secs(5)).await; | ||
| 410 | |||
| 411 | println!( | ||
| 412 | "announcement_y {} synced to relay_a: {}", | ||
| 413 | announcement_y_id, announcement_y_synced | ||
| 414 | ); | ||
| 415 | |||
| 416 | // 10. Cleanup | ||
| 417 | relay_a.stop().await; | ||
| 418 | relay_b.stop().await; | ||
| 419 | relay_c.stop().await; | ||
| 420 | |||
| 421 | // 11. Assertions | ||
| 422 | assert!( | ||
| 423 | announcement_x_synced, | ||
| 424 | "announcement_x {} should have synced from bootstrap relay_b to relay_a", | ||
| 425 | announcement_x_id | ||
| 426 | ); | ||
| 427 | |||
| 428 | assert!( | ||
| 429 | announcement_y_synced, | ||
| 430 | "announcement_y {} should have synced from discovered relay_c to relay_a (recursive discovery)", | ||
| 431 | announcement_y_id | ||
| 432 | ); | ||
| 433 | } | ||
diff --git a/tests/sync/historic_sync.rs b/tests/sync/historic_sync.rs index aec2819..723b776 100644 --- a/tests/sync/historic_sync.rs +++ b/tests/sync/historic_sync.rs | |||
| @@ -224,34 +224,24 @@ async fn test_history_sync_without_negentropy() { | |||
| 224 | // Create keys | 224 | // Create keys |
| 225 | let keys = Keys::generate(); | 225 | let keys = Keys::generate(); |
| 226 | 226 | ||
| 227 | // Create announcement listing BOTH relay domains | 227 | // Set up announcement on source with git data |
| 228 | // This event will exist on source BEFORE syncing relay ever connects | 228 | // (purgatory requires git data before announcements are accepted) |
| 229 | let announcement = create_repo_announcement( | 229 | let domains = vec![source.domain(), syncing_domain.clone()]; |
| 230 | let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect(); | ||
| 231 | let (announcement, _git_dir) = setup_announcement_on_relay( | ||
| 232 | &source, | ||
| 230 | &keys, | 233 | &keys, |
| 231 | &[&source.domain(), &syncing_domain], | 234 | &domain_refs, |
| 232 | "test-repo-history-no-negentropy", | 235 | "test-repo-history-no-negentropy", |
| 233 | ); | 236 | ) |
| 237 | .await; | ||
| 234 | let announcement_id = announcement.id; | 238 | let announcement_id = announcement.id; |
| 235 | 239 | ||
| 236 | println!( | 240 | println!( |
| 237 | "Created announcement {} (kind {})", | 241 | "Announcement {} set up on source with git data (event exists BEFORE syncing relay connects)", |
| 238 | announcement_id, | 242 | announcement_id |
| 239 | announcement.kind.as_u16() | ||
| 240 | ); | 243 | ); |
| 241 | 244 | ||
| 242 | // Send announcement to source (event now exists BEFORE syncing relay connects) | ||
| 243 | let client = TestClient::new(source.url(), keys.clone()) | ||
| 244 | .await | ||
| 245 | .expect("Failed to connect to source"); | ||
| 246 | |||
| 247 | client | ||
| 248 | .send_event(&announcement) | ||
| 249 | .await | ||
| 250 | .expect("Failed to send announcement to source"); | ||
| 251 | println!("Announcement sent to source (event exists BEFORE syncing relay connects)"); | ||
| 252 | |||
| 253 | client.disconnect().await; | ||
| 254 | |||
| 255 | // Wait to ensure event is stored | 245 | // Wait to ensure event is stored |
| 256 | tokio::time::sleep(Duration::from_millis(500)).await; | 246 | tokio::time::sleep(Duration::from_millis(500)).await; |
| 257 | 247 | ||
diff --git a/tests/sync/live_sync.rs b/tests/sync/live_sync.rs index 8ee3119..4289004 100644 --- a/tests/sync/live_sync.rs +++ b/tests/sync/live_sync.rs | |||
| @@ -56,43 +56,24 @@ async fn test_live_sync_layer2_events() { | |||
| 56 | // 3. Create test keys | 56 | // 3. Create test keys |
| 57 | let keys = Keys::generate(); | 57 | let keys = Keys::generate(); |
| 58 | 58 | ||
| 59 | // 4. Create a repository announcement that lists BOTH relays | 59 | // 4. Create a repository announcement on both relays with git data |
| 60 | // (purgatory requires git data before announcements are accepted) | ||
| 60 | let repo_id = "test-repo-live-l2"; | 61 | let repo_id = "test-repo-live-l2"; |
| 61 | let announcement = | 62 | let domains = vec![relay_a.domain(), relay_b.domain()]; |
| 62 | create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id); | 63 | let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect(); |
| 63 | 64 | ||
| 64 | println!( | 65 | let (_announcement, _git_dir_a) = |
| 65 | "Created announcement {} (kind {})", | 66 | setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await; |
| 66 | announcement.id, | 67 | println!("Announcement set up on relay_a with git data"); |
| 67 | announcement.kind.as_u16() | ||
| 68 | ); | ||
| 69 | |||
| 70 | // 5. Send announcement to relay_a | ||
| 71 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | ||
| 72 | .await | ||
| 73 | .expect("Failed to connect to relay_a"); | ||
| 74 | |||
| 75 | client_a | ||
| 76 | .send_event(&announcement) | ||
| 77 | .await | ||
| 78 | .expect("Failed to send announcement to relay_a"); | ||
| 79 | println!("Announcement sent to relay_a"); | ||
| 80 | |||
| 81 | // 6. Send announcement to relay_b (triggers discovery of relay_a) | ||
| 82 | let client_b = TestClient::new(relay_b.url(), keys.clone()) | ||
| 83 | .await | ||
| 84 | .expect("Failed to connect to relay_b"); | ||
| 85 | 68 | ||
| 86 | client_b | 69 | let (_announcement_b, _git_dir_b) = |
| 87 | .send_event(&announcement) | 70 | setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await; |
| 88 | .await | 71 | println!("Announcement set up on relay_b with git data (triggers discovery)"); |
| 89 | .expect("Failed to send announcement to relay_b"); | ||
| 90 | println!("Announcement sent to relay_b (triggers discovery)"); | ||
| 91 | 72 | ||
| 92 | // 7. Wait for discovery to complete | 73 | // 5. Wait for discovery to complete |
| 93 | tokio::time::sleep(Duration::from_secs(1)).await; | 74 | tokio::time::sleep(Duration::from_secs(1)).await; |
| 94 | 75 | ||
| 95 | // 8. Create and send a Layer 2 issue event (using helper) | 76 | // 6. Create and send a Layer 2 issue event (using helper) |
| 96 | let repo_coordinate = repo_coord(&keys, repo_id); | 77 | let repo_coordinate = repo_coord(&keys, repo_id); |
| 97 | let issue = build_layer2_issue_event(&keys, &repo_coordinate, "Test Issue for Live Sync") | 78 | let issue = build_layer2_issue_event(&keys, &repo_coordinate, "Test Issue for Live Sync") |
| 98 | .expect("Failed to create issue event"); | 79 | .expect("Failed to create issue event"); |
| @@ -104,6 +85,10 @@ async fn test_live_sync_layer2_events() { | |||
| 104 | } | 85 | } |
| 105 | 86 | ||
| 106 | // Send issue to relay_a only | 87 | // Send issue to relay_a only |
| 88 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | ||
| 89 | .await | ||
| 90 | .expect("Failed to connect to relay_a"); | ||
| 91 | |||
| 107 | client_a | 92 | client_a |
| 108 | .send_event(&issue) | 93 | .send_event(&issue) |
| 109 | .await | 94 | .await |
| @@ -111,7 +96,6 @@ async fn test_live_sync_layer2_events() { | |||
| 111 | println!("Issue sent to relay_a"); | 96 | println!("Issue sent to relay_a"); |
| 112 | 97 | ||
| 113 | client_a.disconnect().await; | 98 | client_a.disconnect().await; |
| 114 | client_b.disconnect().await; | ||
| 115 | 99 | ||
| 116 | // 9. Wait and verify event syncs to relay_b | 100 | // 9. Wait and verify event syncs to relay_b |
| 117 | let filter = Filter::new() | 101 | let filter = Filter::new() |
| @@ -166,30 +150,19 @@ async fn test_live_sync_layer3_events() { | |||
| 166 | 150 | ||
| 167 | let keys = Keys::generate(); | 151 | let keys = Keys::generate(); |
| 168 | 152 | ||
| 169 | // 2. Create and send repository announcement to both relays | 153 | // 2. Create and send repository announcement to both relays with git data |
| 154 | // (purgatory requires git data before announcements are accepted) | ||
| 170 | let repo_id = "test-repo-live-l3"; | 155 | let repo_id = "test-repo-live-l3"; |
| 171 | let announcement = | 156 | let domains = vec![relay_a.domain(), relay_b.domain()]; |
| 172 | create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id); | 157 | let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect(); |
| 173 | 158 | ||
| 174 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | 159 | let (_announcement, _git_dir_a) = |
| 175 | .await | 160 | setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await; |
| 176 | .expect("Failed to connect to relay_a"); | 161 | println!("Announcement set up on relay_a with git data"); |
| 177 | 162 | ||
| 178 | let client_b = TestClient::new(relay_b.url(), keys.clone()) | 163 | let (_announcement_b, _git_dir_b) = |
| 179 | .await | 164 | setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await; |
| 180 | .expect("Failed to connect to relay_b"); | 165 | println!("Announcement set up on relay_b with git data (triggers discovery)"); |
| 181 | |||
| 182 | client_a | ||
| 183 | .send_event(&announcement) | ||
| 184 | .await | ||
| 185 | .expect("Failed to send announcement to relay_a"); | ||
| 186 | println!("Announcement sent to relay_a"); | ||
| 187 | |||
| 188 | client_b | ||
| 189 | .send_event(&announcement) | ||
| 190 | .await | ||
| 191 | .expect("Failed to send announcement to relay_b"); | ||
| 192 | println!("Announcement sent to relay_b (triggers discovery)"); | ||
| 193 | 166 | ||
| 194 | // 3. Wait for discovery | 167 | // 3. Wait for discovery |
| 195 | tokio::time::sleep(Duration::from_secs(1)).await; | 168 | tokio::time::sleep(Duration::from_secs(1)).await; |
| @@ -200,6 +173,10 @@ async fn test_live_sync_layer3_events() { | |||
| 200 | .expect("Failed to create issue"); | 173 | .expect("Failed to create issue"); |
| 201 | let issue_id = issue.id; | 174 | let issue_id = issue.id; |
| 202 | 175 | ||
| 176 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | ||
| 177 | .await | ||
| 178 | .expect("Failed to connect to relay_a"); | ||
| 179 | |||
| 203 | client_a | 180 | client_a |
| 204 | .send_event(&issue) | 181 | .send_event(&issue) |
| 205 | .await | 182 | .await |
| @@ -243,7 +220,6 @@ async fn test_live_sync_layer3_events() { | |||
| 243 | println!("Issue synced to relay_b: {}", issue_synced); | 220 | println!("Issue synced to relay_b: {}", issue_synced); |
| 244 | 221 | ||
| 245 | client_a.disconnect().await; | 222 | client_a.disconnect().await; |
| 246 | client_b.disconnect().await; | ||
| 247 | 223 | ||
| 248 | // 7. Wait and verify comment syncs to relay_b | 224 | // 7. Wait and verify comment syncs to relay_b |
| 249 | let comment_filter = Filter::new() | 225 | let comment_filter = Filter::new() |
| @@ -343,29 +319,17 @@ async fn test_live_sync_event_ordering() { | |||
| 343 | 319 | ||
| 344 | let keys = Keys::generate(); | 320 | let keys = Keys::generate(); |
| 345 | 321 | ||
| 346 | // 2. Create and send repository announcement to both relays | 322 | // 2. Create and send repository announcement to both relays with git data |
| 323 | // (purgatory requires git data before announcements are accepted) | ||
| 347 | let repo_id = "test-repo-ordering"; | 324 | let repo_id = "test-repo-ordering"; |
| 348 | let announcement = | 325 | let domains = vec![relay_a.domain(), relay_b.domain()]; |
| 349 | create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id); | 326 | let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect(); |
| 350 | 327 | ||
| 351 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | 328 | let (_announcement, _git_dir_a) = |
| 352 | .await | 329 | setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await; |
| 353 | .expect("Failed to connect to relay_a"); | 330 | let (_announcement_b, _git_dir_b) = |
| 354 | 331 | setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await; | |
| 355 | let client_b = TestClient::new(relay_b.url(), keys.clone()) | 332 | println!("Announcements set up on both relays with git data"); |
| 356 | .await | ||
| 357 | .expect("Failed to connect to relay_b"); | ||
| 358 | |||
| 359 | client_a | ||
| 360 | .send_event(&announcement) | ||
| 361 | .await | ||
| 362 | .expect("Failed to send announcement to relay_a"); | ||
| 363 | |||
| 364 | client_b | ||
| 365 | .send_event(&announcement) | ||
| 366 | .await | ||
| 367 | .expect("Failed to send announcement to relay_b"); | ||
| 368 | println!("Announcements sent to both relays"); | ||
| 369 | 333 | ||
| 370 | // 3. Wait for discovery | 334 | // 3. Wait for discovery |
| 371 | tokio::time::sleep(Duration::from_secs(1)).await; | 335 | tokio::time::sleep(Duration::from_secs(1)).await; |
| @@ -375,6 +339,10 @@ async fn test_live_sync_event_ordering() { | |||
| 375 | let mut issue_ids = Vec::new(); | 339 | let mut issue_ids = Vec::new(); |
| 376 | let mut expected_order_timestamps = Vec::new(); | 340 | let mut expected_order_timestamps = Vec::new(); |
| 377 | 341 | ||
| 342 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | ||
| 343 | .await | ||
| 344 | .expect("Failed to connect to relay_a"); | ||
| 345 | |||
| 378 | for i in 1..=3 { | 346 | for i in 1..=3 { |
| 379 | let issue = build_layer2_issue_event( | 347 | let issue = build_layer2_issue_event( |
| 380 | &keys, | 348 | &keys, |
| @@ -402,7 +370,6 @@ async fn test_live_sync_event_ordering() { | |||
| 402 | } | 370 | } |
| 403 | 371 | ||
| 404 | client_a.disconnect().await; | 372 | client_a.disconnect().await; |
| 405 | client_b.disconnect().await; | ||
| 406 | 373 | ||
| 407 | // 5. Wait for all events to sync | 374 | // 5. Wait for all events to sync |
| 408 | tokio::time::sleep(Duration::from_secs(3)).await; | 375 | tokio::time::sleep(Duration::from_secs(3)).await; |
diff --git a/tests/sync/maintainer_reprocessing.rs b/tests/sync/maintainer_reprocessing.rs index df1bf78..ff1eb43 100644 --- a/tests/sync/maintainer_reprocessing.rs +++ b/tests/sync/maintainer_reprocessing.rs | |||
| @@ -2,6 +2,25 @@ | |||
| 2 | //! | 2 | //! |
| 3 | //! Tests the two-tier rejected events index and immediate re-processing of | 3 | //! Tests the two-tier rejected events index and immediate re-processing of |
| 4 | //! maintainer announcements when owner announcements are accepted. | 4 | //! maintainer announcements when owner announcements are accepted. |
| 5 | //! | ||
| 6 | //! ## Test design | ||
| 7 | //! | ||
| 8 | //! Announcements now require git data before they are released from purgatory and | ||
| 9 | //! served to other relays. The hot-cache re-processing path we want to exercise is: | ||
| 10 | //! | ||
| 11 | //! relay_b syncs maintainer announcement from relay_a | ||
| 12 | //! → write policy rejects it (no owner announcement in DB yet) | ||
| 13 | //! → event stored in hot cache | ||
| 14 | //! owner git push to relay_b promotes owner announcement from purgatory | ||
| 15 | //! → our new code calls rejected_events_index.invalidate_and_get() | ||
| 16 | //! → maintainer announcement re-processed and accepted | ||
| 17 | //! | ||
| 18 | //! To guarantee the maintainer announcements arrive at relay_b *before* the owner | ||
| 19 | //! git push, relay_b is started with relay_a as its bootstrap relay. That way | ||
| 20 | //! relay_b's SyncManager connects to relay_a immediately and syncs whatever is | ||
| 21 | //! already in relay_a's DB. We push the maintainer git data first (so the | ||
| 22 | //! announcements are in relay_a's DB), wait briefly for the sync round-trip, then | ||
| 23 | //! send the owner announcement + git push. | ||
| 5 | 24 | ||
| 6 | use std::time::Duration; | 25 | use std::time::Duration; |
| 7 | 26 | ||
| @@ -9,66 +28,91 @@ use nostr_sdk::prelude::*; | |||
| 9 | 28 | ||
| 10 | use crate::common::{sync_helpers::*, TestRelay}; | 29 | use crate::common::{sync_helpers::*, TestRelay}; |
| 11 | 30 | ||
| 12 | /// Test that maintainer announcements are re-processed immediately when owner announcement accepted | 31 | /// Test that a maintainer announcement is re-processed immediately when the owner |
| 32 | /// announcement is promoted from purgatory via a git push. | ||
| 13 | /// | 33 | /// |
| 14 | /// Flow: | 34 | /// Flow: |
| 15 | /// 1. relay_a: Maintainer sends announcement (gets rejected - doesn't list relay_b) | 35 | /// 1. relay_a: Maintainer sends announcement + git data → accepted into relay_a's DB |
| 16 | /// 2. relay_b: Owner sends announcement (lists relay_a + maintainer) | 36 | /// 2. relay_b (bootstrapped from relay_a): SyncManager syncs maintainer announcement |
| 17 | /// 3. relay_b syncs from relay_a, maintainer announcement enters rejected index | 37 | /// → rejected by write policy (no owner in DB) → stored in hot cache |
| 18 | /// 4. relay_b processes owner announcement, invalidates and re-processes maintainer announcement | 38 | /// 3. relay_b: Owner sends announcement → purgatory (no git data yet) |
| 39 | /// 4. relay_b: Owner git push → owner announcement promoted from purgatory | ||
| 40 | /// → hot-cache re-processing fires → maintainer announcement accepted | ||
| 19 | /// 5. Both announcements should be in relay_b's database | 41 | /// 5. Both announcements should be in relay_b's database |
| 20 | /// | ||
| 21 | /// Expected time: <5 seconds (vs 24 hours without hot cache) | ||
| 22 | #[tokio::test] | 42 | #[tokio::test] |
| 23 | async fn test_maintainer_announcement_reprocessed_immediately() { | 43 | async fn test_maintainer_announcement_reprocessed_immediately() { |
| 24 | // Start relay_a (where maintainer announcement will be sent) | 44 | // Start relay_a (where maintainer announcement will be sent) |
| 25 | let relay_a = TestRelay::start().await; | 45 | let relay_a = TestRelay::start().await; |
| 26 | println!("relay_a started at {}", relay_a.url()); | 46 | println!("relay_a started at {}", relay_a.url()); |
| 27 | 47 | ||
| 28 | // Start relay_b with sync enabled (will sync from relay_a) | ||
| 29 | let relay_b = TestRelay::start_with_sync(None).await; | ||
| 30 | println!("relay_b started at {}", relay_b.url()); | ||
| 31 | |||
| 32 | // Create keys | 48 | // Create keys |
| 33 | let owner_keys = Keys::generate(); | 49 | let owner_keys = Keys::generate(); |
| 34 | let maintainer_keys = Keys::generate(); | 50 | let maintainer_keys = Keys::generate(); |
| 35 | |||
| 36 | let identifier = "test-repo"; | 51 | let identifier = "test-repo"; |
| 37 | 52 | ||
| 38 | let start = std::time::Instant::now(); | 53 | // Step 1: Send maintainer announcement to relay_a then push git data so it lands in |
| 39 | 54 | // relay_a's DB. The announcement lists relay_a only (not relay_b), so relay_b's write | |
| 40 | // Step 1: Send maintainer announcement to relay_a (will be rejected - doesn't list relay_b) | 55 | // policy will reject it when it arrives via sync. |
| 41 | let client_a = TestClient::new(relay_a.url(), maintainer_keys.clone()) | 56 | let maintainer_npub = maintainer_keys |
| 42 | .await | 57 | .public_key() |
| 43 | .expect("Failed to connect to relay_a"); | 58 | .to_bech32() |
| 44 | 59 | .expect("Failed to get npub"); | |
| 45 | let maintainer_announcement = | 60 | let maintainer_announcement = |
| 46 | EventBuilder::new(Kind::GitRepoAnnouncement, "Maintainer's repository") | 61 | EventBuilder::new(Kind::GitRepoAnnouncement, "Maintainer's repository") |
| 47 | .tags(vec![ | 62 | .tags(vec![ |
| 48 | Tag::identifier(identifier), | 63 | Tag::identifier(identifier), |
| 49 | Tag::custom( | 64 | Tag::custom( |
| 50 | TagKind::custom("clone"), | 65 | TagKind::custom("clone"), |
| 51 | vec![format!("https://{}/{}.git", relay_a.domain(), identifier)], | 66 | vec![format!( |
| 67 | "http://{}/{}/{}.git", | ||
| 68 | relay_a.domain(), | ||
| 69 | maintainer_npub, | ||
| 70 | identifier | ||
| 71 | )], | ||
| 72 | ), | ||
| 73 | Tag::custom( | ||
| 74 | TagKind::custom("relays"), | ||
| 75 | vec![relay_a.url().to_string()], | ||
| 52 | ), | 76 | ), |
| 53 | Tag::custom(TagKind::custom("relays"), vec![relay_a.url().to_string()]), | ||
| 54 | ]) | 77 | ]) |
| 55 | .sign_with_keys(&maintainer_keys) | 78 | .sign_with_keys(&maintainer_keys) |
| 56 | .unwrap(); | 79 | .unwrap(); |
| 80 | send_to_relay(&relay_a, &maintainer_announcement).await.unwrap(); | ||
| 81 | let _git_dir_maintainer = | ||
| 82 | push_git_data_to_relay(&relay_a, &maintainer_keys, identifier, &[&relay_a.domain()]) | ||
| 83 | .await; | ||
| 84 | println!("✓ Maintainer announcement + git data pushed to relay_a"); | ||
| 85 | |||
| 86 | // Step 2: Start relay_b with relay_a as bootstrap so its SyncManager connects immediately. | ||
| 87 | // relay_b's initial negentropy sync will pick up the maintainer announcement and reject it | ||
| 88 | // (no owner announcement in relay_b's DB yet), storing it in the hot cache. | ||
| 89 | let relay_b = TestRelay::start_with_sync(Some(relay_a.url().to_string())).await; | ||
| 90 | println!("relay_b started at {}", relay_b.url()); | ||
| 57 | 91 | ||
| 58 | client_a.send_event(&maintainer_announcement).await.unwrap(); | 92 | // Give relay_b's SyncManager time to complete the initial negentropy sync with relay_a. |
| 59 | println!("✓ Maintainer announcement sent to relay_a"); | 93 | tokio::time::sleep(Duration::from_secs(3)).await; |
| 94 | println!("✓ relay_b synced from relay_a (maintainer announcement should be in hot cache)"); | ||
| 60 | 95 | ||
| 61 | // Step 2: Send owner announcement to relay_b (lists relay_a + maintainer) | 96 | let start = std::time::Instant::now(); |
| 62 | let client_b = TestClient::new(relay_b.url(), owner_keys.clone()) | 97 | |
| 63 | .await | 98 | // Step 3: Send owner announcement to relay_b → goes to purgatory (no git data yet). |
| 64 | .expect("Failed to connect to relay_b"); | 99 | // The announcement lists relay_a + relay_b and names the maintainer. |
| 100 | let owner_npub = owner_keys | ||
| 101 | .public_key() | ||
| 102 | .to_bech32() | ||
| 103 | .expect("Failed to get npub"); | ||
| 65 | 104 | ||
| 66 | let owner_announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "Owner's repository") | 105 | let owner_announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "Owner's repository") |
| 67 | .tags(vec![ | 106 | .tags(vec![ |
| 68 | Tag::identifier(identifier), | 107 | Tag::identifier(identifier), |
| 69 | Tag::custom( | 108 | Tag::custom( |
| 70 | TagKind::custom("clone"), | 109 | TagKind::custom("clone"), |
| 71 | vec![format!("https://{}/{}.git", relay_b.domain(), identifier)], | 110 | vec![format!( |
| 111 | "http://{}/{}/{}.git", | ||
| 112 | relay_b.domain(), | ||
| 113 | owner_npub, | ||
| 114 | identifier | ||
| 115 | )], | ||
| 72 | ), | 116 | ), |
| 73 | Tag::custom( | 117 | Tag::custom( |
| 74 | TagKind::custom("relays"), | 118 | TagKind::custom("relays"), |
| @@ -82,15 +126,22 @@ async fn test_maintainer_announcement_reprocessed_immediately() { | |||
| 82 | .sign_with_keys(&owner_keys) | 126 | .sign_with_keys(&owner_keys) |
| 83 | .unwrap(); | 127 | .unwrap(); |
| 84 | 128 | ||
| 85 | client_b.send_event(&owner_announcement).await.unwrap(); | 129 | send_to_relay(&relay_b, &owner_announcement).await.unwrap(); |
| 86 | println!("✓ Owner announcement sent to relay_b"); | 130 | println!("✓ Owner announcement sent to relay_b (now in purgatory)"); |
| 87 | 131 | ||
| 88 | // Step 3: Wait for sync and re-processing (relay_b discovers relay_a, syncs, re-processes) | 132 | // Step 4: Push owner git data to relay_b. |
| 89 | tokio::time::sleep(Duration::from_secs(3)).await; | 133 | // This promotes the owner announcement from purgatory, which triggers hot-cache |
| 134 | // re-processing of the maintainer announcement via our new code path. | ||
| 135 | let _git_dir_owner = | ||
| 136 | push_git_data_to_relay(&relay_b, &owner_keys, identifier, &[&relay_b.domain()]).await; | ||
| 137 | println!("✓ Owner git data pushed to relay_b (owner announcement promoted, hot cache re-processed)"); | ||
| 138 | |||
| 139 | // Step 5: Wait briefly for async processing to complete. | ||
| 140 | tokio::time::sleep(Duration::from_secs(1)).await; | ||
| 90 | 141 | ||
| 91 | let elapsed = start.elapsed(); | 142 | let elapsed = start.elapsed(); |
| 92 | 143 | ||
| 93 | // Step 4: Verify both announcements are in relay_b's database | 144 | // Step 6: Verify both announcements are in relay_b's database. |
| 94 | let owner_filter = Filter::new() | 145 | let owner_filter = Filter::new() |
| 95 | .kind(Kind::GitRepoAnnouncement) | 146 | .kind(Kind::GitRepoAnnouncement) |
| 96 | .author(owner_keys.public_key()) | 147 | .author(owner_keys.public_key()) |
| @@ -112,17 +163,14 @@ async fn test_maintainer_announcement_reprocessed_immediately() { | |||
| 112 | "Maintainer announcement should be re-processed and accepted in relay_b" | 163 | "Maintainer announcement should be re-processed and accepted in relay_b" |
| 113 | ); | 164 | ); |
| 114 | 165 | ||
| 115 | // Step 5: Verify it happened quickly (not 24 hours!) | ||
| 116 | assert!( | 166 | assert!( |
| 117 | elapsed.as_secs() < 10, | 167 | elapsed.as_secs() < 15, |
| 118 | "Re-processing should happen in <10 seconds, took {:?}", | 168 | "Re-processing should happen in <15 seconds, took {:?}", |
| 119 | elapsed | 169 | elapsed |
| 120 | ); | 170 | ); |
| 121 | 171 | ||
| 122 | println!("✅ Maintainer announcement re-processed in {:?}", elapsed); | 172 | println!("✅ Maintainer announcement re-processed in {:?}", elapsed); |
| 123 | 173 | ||
| 124 | client_a.disconnect().await; | ||
| 125 | client_b.disconnect().await; | ||
| 126 | relay_a.stop().await; | 174 | relay_a.stop().await; |
| 127 | relay_b.stop().await; | 175 | relay_b.stop().await; |
| 128 | } | 176 | } |
| @@ -227,13 +275,16 @@ async fn test_maintainer_announcement_cold_index_prevents_refetch() { | |||
| 227 | relay.stop().await; | 275 | relay.stop().await; |
| 228 | } | 276 | } |
| 229 | 277 | ||
| 230 | /// Test multiple maintainers are all re-processed when owner announcement accepted | 278 | /// Test that all maintainer announcements are re-processed when the owner announcement |
| 279 | /// is promoted from purgatory via a git push. | ||
| 231 | /// | 280 | /// |
| 232 | /// Flow: | 281 | /// Flow: |
| 233 | /// 1. relay_a: Three maintainers send announcements (get rejected - don't list relay_b) | 282 | /// 1. relay_a: Three maintainers send announcements + git data → in relay_a's DB |
| 234 | /// 2. relay_b: Owner sends announcement (lists relay_a + all three maintainers) | 283 | /// 2. relay_b (bootstrapped from relay_a): SyncManager syncs all three maintainer |
| 235 | /// 3. relay_b syncs from relay_a, all maintainer announcements enter rejected index | 284 | /// announcements → all rejected (no owner in DB) → all in hot cache |
| 236 | /// 4. relay_b processes owner announcement, invalidates and re-processes all maintainer announcements | 285 | /// 3. relay_b: Owner sends announcement → purgatory |
| 286 | /// 4. relay_b: Owner git push → owner promoted → hot-cache re-processing fires for | ||
| 287 | /// all three maintainers | ||
| 237 | /// 5. All four announcements should be in relay_b's database | 288 | /// 5. All four announcements should be in relay_b's database |
| 238 | #[tokio::test] | 289 | #[tokio::test] |
| 239 | async fn test_multiple_maintainers_all_reprocessed() { | 290 | async fn test_multiple_maintainers_all_reprocessed() { |
| @@ -241,57 +292,113 @@ async fn test_multiple_maintainers_all_reprocessed() { | |||
| 241 | let relay_a = TestRelay::start().await; | 292 | let relay_a = TestRelay::start().await; |
| 242 | println!("relay_a started at {}", relay_a.url()); | 293 | println!("relay_a started at {}", relay_a.url()); |
| 243 | 294 | ||
| 244 | // Start relay_b with sync enabled (will sync from relay_a) | ||
| 245 | let relay_b = TestRelay::start_with_sync(None).await; | ||
| 246 | println!("relay_b started at {}", relay_b.url()); | ||
| 247 | |||
| 248 | // Create keys | 295 | // Create keys |
| 249 | let owner_keys = Keys::generate(); | 296 | let owner_keys = Keys::generate(); |
| 250 | let maintainer1_keys = Keys::generate(); | 297 | let maintainer1_keys = Keys::generate(); |
| 251 | let maintainer2_keys = Keys::generate(); | 298 | let maintainer2_keys = Keys::generate(); |
| 252 | let maintainer3_keys = Keys::generate(); | 299 | let maintainer3_keys = Keys::generate(); |
| 253 | 300 | ||
| 254 | let identifier = "multi-maintainer-repo"; | 301 | // Use a unique identifier per test run to avoid cross-test interference when |
| 255 | 302 | // tests run in parallel (each test gets its own namespace on relay_a). | |
| 256 | // Step 1: Send three maintainer announcements to relay_a | 303 | let identifier = &format!( |
| 257 | let client_a = TestClient::new(relay_a.url(), maintainer1_keys.clone()) | 304 | "multi-maintainer-repo-{}", |
| 258 | .await | 305 | owner_keys.public_key().to_hex()[..8].to_string() |
| 259 | .expect("Failed to connect to relay_a"); | 306 | ); |
| 260 | 307 | ||
| 308 | // Step 1: Send each maintainer announcement to relay_a then push git data so all three | ||
| 309 | // land in relay_a's DB. Each announcement lists relay_a only, so relay_b will reject | ||
| 310 | // them when syncing (no owner announcement in relay_b's DB yet). | ||
| 311 | let mut git_dirs = Vec::new(); | ||
| 261 | for (idx, maintainer_keys) in [&maintainer1_keys, &maintainer2_keys, &maintainer3_keys] | 312 | for (idx, maintainer_keys) in [&maintainer1_keys, &maintainer2_keys, &maintainer3_keys] |
| 262 | .iter() | 313 | .iter() |
| 263 | .enumerate() | 314 | .enumerate() |
| 264 | { | 315 | { |
| 316 | let m_npub = maintainer_keys | ||
| 317 | .public_key() | ||
| 318 | .to_bech32() | ||
| 319 | .expect("Failed to get npub"); | ||
| 265 | let announcement = EventBuilder::new( | 320 | let announcement = EventBuilder::new( |
| 266 | Kind::GitRepoAnnouncement, | 321 | Kind::GitRepoAnnouncement, |
| 267 | format!("Maintainer {} repository", idx + 1), | 322 | format!("Maintainer {} repository", idx + 1), |
| 268 | ) | 323 | ) |
| 269 | .tags(vec![ | 324 | .tags(vec![ |
| 270 | Tag::identifier(identifier), | 325 | Tag::identifier(identifier.as_str()), |
| 271 | Tag::custom( | 326 | Tag::custom( |
| 272 | TagKind::custom("clone"), | 327 | TagKind::custom("clone"), |
| 273 | vec![format!("https://{}/{}.git", relay_a.domain(), identifier)], | 328 | vec![format!( |
| 329 | "http://{}/{}/{}.git", | ||
| 330 | relay_a.domain(), | ||
| 331 | m_npub, | ||
| 332 | identifier | ||
| 333 | )], | ||
| 274 | ), | 334 | ), |
| 275 | Tag::custom(TagKind::custom("relays"), vec![relay_a.url().to_string()]), | 335 | Tag::custom(TagKind::custom("relays"), vec![relay_a.url().to_string()]), |
| 276 | ]) | 336 | ]) |
| 277 | .sign_with_keys(maintainer_keys) | 337 | .sign_with_keys(maintainer_keys) |
| 278 | .unwrap(); | 338 | .unwrap(); |
| 339 | send_to_relay(&relay_a, &announcement).await.unwrap(); | ||
| 340 | // Use push_unique_git_data_to_relay so each maintainer gets a distinct commit | ||
| 341 | // hash. Identical hashes cause git to skip pack transfer when the object | ||
| 342 | // already exists on the server, leaving the announcement in purgatory. | ||
| 343 | let git_dir = push_unique_git_data_to_relay( | ||
| 344 | &relay_a, | ||
| 345 | maintainer_keys, | ||
| 346 | identifier, | ||
| 347 | &[&relay_a.domain()], | ||
| 348 | &m_npub, | ||
| 349 | ) | ||
| 350 | .await; | ||
| 351 | git_dirs.push(git_dir); | ||
| 352 | } | ||
| 353 | println!("✓ Three maintainer announcements + git data pushed to relay_a"); | ||
| 279 | 354 | ||
| 280 | client_a.send_event(&announcement).await.unwrap(); | 355 | // Confirm all three announcements are queryable on relay_a before starting relay_b. |
| 356 | // This eliminates the race between relay_a's DB writes and relay_b's initial negentropy sync. | ||
| 357 | for (name, keys) in [ | ||
| 358 | ("maintainer1", &maintainer1_keys), | ||
| 359 | ("maintainer2", &maintainer2_keys), | ||
| 360 | ("maintainer3", &maintainer3_keys), | ||
| 361 | ] { | ||
| 362 | let filter = Filter::new() | ||
| 363 | .kind(Kind::GitRepoAnnouncement) | ||
| 364 | .author(keys.public_key()) | ||
| 365 | .identifier(identifier); | ||
| 366 | let found = | ||
| 367 | wait_for_event_on_relay(relay_a.url(), filter, Duration::from_secs(10)).await; | ||
| 368 | assert!(found, "{} announcement should be in relay_a before starting relay_b", name); | ||
| 281 | } | 369 | } |
| 282 | println!("✓ Three maintainer announcements sent to relay_a"); | 370 | println!("✓ All three maintainer announcements confirmed in relay_a's DB"); |
| 283 | 371 | ||
| 284 | // Step 2: Send owner announcement to relay_b (lists relay_a + all three maintainers) | 372 | // Step 2: Start relay_b with relay_a as bootstrap so its SyncManager connects immediately. |
| 285 | let client_b = TestClient::new(relay_b.url(), owner_keys.clone()) | 373 | // Because all three maintainer announcements are confirmed in relay_a's DB, relay_b's |
| 286 | .await | 374 | // initial negentropy sync will pick them all up and reject them (no owner announcement |
| 287 | .expect("Failed to connect to relay_b"); | 375 | // in relay_b's DB yet), storing them in the hot cache. |
| 376 | let relay_b = TestRelay::start_with_sync(Some(relay_a.url().to_string())).await; | ||
| 377 | println!("relay_b started at {}", relay_b.url()); | ||
| 378 | |||
| 379 | // Give relay_b's SyncManager time to complete the initial negentropy sync with relay_a. | ||
| 380 | // The negentropy sync completes within ~200ms (NGIT_TEST=1 sets batch window to 200ms), but we | ||
| 381 | // allow extra time for slow CI environments. | ||
| 382 | tokio::time::sleep(Duration::from_secs(3)).await; | ||
| 383 | println!("✓ relay_b synced from relay_a (maintainer announcements should be in hot cache)"); | ||
| 384 | |||
| 385 | // Step 3: Send owner announcement to relay_b → goes to purgatory. | ||
| 386 | let owner_npub = owner_keys | ||
| 387 | .public_key() | ||
| 388 | .to_bech32() | ||
| 389 | .expect("Failed to get npub"); | ||
| 288 | 390 | ||
| 289 | let owner_announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "Owner's repository") | 391 | let owner_announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "Owner's repository") |
| 290 | .tags(vec![ | 392 | .tags(vec![ |
| 291 | Tag::identifier(identifier), | 393 | Tag::identifier(identifier), |
| 292 | Tag::custom( | 394 | Tag::custom( |
| 293 | TagKind::custom("clone"), | 395 | TagKind::custom("clone"), |
| 294 | vec![format!("https://{}/{}.git", relay_b.domain(), identifier)], | 396 | vec![format!( |
| 397 | "http://{}/{}/{}.git", | ||
| 398 | relay_b.domain(), | ||
| 399 | owner_npub, | ||
| 400 | identifier | ||
| 401 | )], | ||
| 295 | ), | 402 | ), |
| 296 | Tag::custom( | 403 | Tag::custom( |
| 297 | TagKind::custom("relays"), | 404 | TagKind::custom("relays"), |
| @@ -309,13 +416,20 @@ async fn test_multiple_maintainers_all_reprocessed() { | |||
| 309 | .sign_with_keys(&owner_keys) | 416 | .sign_with_keys(&owner_keys) |
| 310 | .unwrap(); | 417 | .unwrap(); |
| 311 | 418 | ||
| 312 | client_b.send_event(&owner_announcement).await.unwrap(); | 419 | send_to_relay(&relay_b, &owner_announcement).await.unwrap(); |
| 313 | println!("✓ Owner announcement sent to relay_b"); | 420 | println!("✓ Owner announcement sent to relay_b (now in purgatory)"); |
| 314 | 421 | ||
| 315 | // Step 3: Wait for sync and re-processing | 422 | // Step 4: Push owner git data to relay_b. |
| 316 | tokio::time::sleep(Duration::from_secs(3)).await; | 423 | // This promotes the owner announcement from purgatory and triggers hot-cache |
| 424 | // re-processing for all three maintainer announcements. | ||
| 425 | let _git_dir_owner = | ||
| 426 | push_git_data_to_relay(&relay_b, &owner_keys, identifier, &[&relay_b.domain()]).await; | ||
| 427 | println!("✓ Owner git data pushed to relay_b (hot-cache re-processing should fire)"); | ||
| 428 | |||
| 429 | // Step 5: Wait briefly for async processing to complete. | ||
| 430 | tokio::time::sleep(Duration::from_secs(1)).await; | ||
| 317 | 431 | ||
| 318 | // Step 4: Verify all four announcements are in relay_b's database | 432 | // Step 6: Verify all four announcements are in relay_b's database. |
| 319 | for (name, keys) in [ | 433 | for (name, keys) in [ |
| 320 | ("owner", &owner_keys), | 434 | ("owner", &owner_keys), |
| 321 | ("maintainer1", &maintainer1_keys), | 435 | ("maintainer1", &maintainer1_keys), |
| @@ -333,8 +447,6 @@ async fn test_multiple_maintainers_all_reprocessed() { | |||
| 333 | 447 | ||
| 334 | println!("✅ All three maintainer announcements re-processed successfully"); | 448 | println!("✅ All three maintainer announcements re-processed successfully"); |
| 335 | 449 | ||
| 336 | client_a.disconnect().await; | ||
| 337 | client_b.disconnect().await; | ||
| 338 | relay_a.stop().await; | 450 | relay_a.stop().await; |
| 339 | relay_b.stop().await; | 451 | relay_b.stop().await; |
| 340 | } | 452 | } |
| @@ -342,10 +454,10 @@ async fn test_multiple_maintainers_all_reprocessed() { | |||
| 342 | /// Test that invalid maintainer public keys don't cause panics | 454 | /// Test that invalid maintainer public keys don't cause panics |
| 343 | /// | 455 | /// |
| 344 | /// Flow: | 456 | /// Flow: |
| 345 | /// 1. Maintainer announcement arrives → Rejected | 457 | /// 1. Maintainer announcement arrives → Rejected (doesn't list our relay) |
| 346 | /// 2. Owner announcement arrives with INVALID maintainer hex → Should handle gracefully | 458 | /// 2. Owner announcement + git push → accepted, with INVALID maintainer hex in maintainers tag |
| 347 | /// 3. Owner announcement should still be accepted | 459 | /// 3. Owner announcement should be accepted |
| 348 | /// 4. Maintainer announcement should NOT be re-processed (invalid pubkey) | 460 | /// 4. Maintainer announcement should NOT be re-processed (invalid pubkey can't be parsed) |
| 349 | #[tokio::test] | 461 | #[tokio::test] |
| 350 | async fn test_invalid_maintainer_pubkey_handled_gracefully() { | 462 | async fn test_invalid_maintainer_pubkey_handled_gracefully() { |
| 351 | let relay = TestRelay::start().await; | 463 | let relay = TestRelay::start().await; |
| @@ -382,13 +494,25 @@ async fn test_invalid_maintainer_pubkey_handled_gracefully() { | |||
| 382 | let _ = client.send_event(&maintainer_announcement).await; | 494 | let _ = client.send_event(&maintainer_announcement).await; |
| 383 | tokio::time::sleep(Duration::from_millis(200)).await; | 495 | tokio::time::sleep(Duration::from_millis(200)).await; |
| 384 | 496 | ||
| 385 | // Step 2: Send owner announcement with INVALID maintainer hex | 497 | // Step 2: Send owner announcement with INVALID maintainer hex, then push git data. |
| 498 | // The announcement goes to purgatory first; the git push promotes it. | ||
| 499 | // The invalid maintainer hex should be handled gracefully (no panic). | ||
| 500 | let owner_npub = owner_keys | ||
| 501 | .public_key() | ||
| 502 | .to_bech32() | ||
| 503 | .expect("Failed to get npub"); | ||
| 504 | |||
| 386 | let owner_announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "Owner's repository") | 505 | let owner_announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "Owner's repository") |
| 387 | .tags(vec![ | 506 | .tags(vec![ |
| 388 | Tag::identifier(identifier), | 507 | Tag::identifier(identifier), |
| 389 | Tag::custom( | 508 | Tag::custom( |
| 390 | TagKind::custom("clone"), | 509 | TagKind::custom("clone"), |
| 391 | vec![format!("https://{}/{}.git", relay.domain(), identifier)], | 510 | vec![format!( |
| 511 | "http://{}/{}/{}.git", | ||
| 512 | relay.domain(), | ||
| 513 | owner_npub, | ||
| 514 | identifier | ||
| 515 | )], | ||
| 392 | ), | 516 | ), |
| 393 | Tag::custom(TagKind::custom("relays"), vec![relay.url().to_string()]), | 517 | Tag::custom(TagKind::custom("relays"), vec![relay.url().to_string()]), |
| 394 | Tag::custom( | 518 | Tag::custom( |
| @@ -399,7 +523,9 @@ async fn test_invalid_maintainer_pubkey_handled_gracefully() { | |||
| 399 | .sign_with_keys(&owner_keys) | 523 | .sign_with_keys(&owner_keys) |
| 400 | .unwrap(); | 524 | .unwrap(); |
| 401 | 525 | ||
| 402 | client.send_event(&owner_announcement).await.unwrap(); | 526 | send_to_relay(&relay, &owner_announcement).await.unwrap(); |
| 527 | let _git_dir = | ||
| 528 | push_git_data_to_relay(&relay, &owner_keys, identifier, &[&relay.domain()]).await; | ||
| 403 | tokio::time::sleep(Duration::from_millis(500)).await; | 529 | tokio::time::sleep(Duration::from_millis(500)).await; |
| 404 | 530 | ||
| 405 | // Step 3: Verify owner announcement accepted, maintainer not re-processed | 531 | // Step 3: Verify owner announcement accepted, maintainer not re-processed |
diff --git a/tests/sync/metrics.rs b/tests/sync/metrics.rs index e8c75c7..e973bbb 100644 --- a/tests/sync/metrics.rs +++ b/tests/sync/metrics.rs | |||
| @@ -16,8 +16,8 @@ use nostr_sdk::prelude::*; | |||
| 16 | 16 | ||
| 17 | use crate::common::{ | 17 | use crate::common::{ |
| 18 | sync_helpers::{ | 18 | sync_helpers::{ |
| 19 | create_repo_announcement, fetch_metrics, wait_for_sync_connection, MetricsTestHarness, | 19 | create_repo_announcement, fetch_metrics, setup_announcement_on_relay, |
| 20 | ParsedMetrics, TestClient, | 20 | wait_for_sync_connection, MetricsTestHarness, ParsedMetrics, TestClient, |
| 21 | }, | 21 | }, |
| 22 | TestRelay, | 22 | TestRelay, |
| 23 | }; | 23 | }; |
| @@ -224,16 +224,17 @@ async fn test_startup_sync_event_count() { | |||
| 224 | // 3. Create test keys | 224 | // 3. Create test keys |
| 225 | let keys = Keys::generate(); | 225 | let keys = Keys::generate(); |
| 226 | 226 | ||
| 227 | // 4. Create an announcement that lists BOTH relays (required for discovery) | 227 | // 4. Set up announcement on SOURCE relay with git data |
| 228 | let announcement = create_repo_announcement( | 228 | // (purgatory requires git data before announcements are accepted) |
| 229 | &keys, | 229 | let repo_id = "test-repo-metrics"; |
| 230 | &[&source_relay.domain(), &syncing_relay.domain()], | 230 | let domains = vec![source_relay.domain(), syncing_relay.domain()]; |
| 231 | "test-repo-metrics", | 231 | let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect(); |
| 232 | ); | 232 | |
| 233 | let (announcement, _git_dir_source) = | ||
| 234 | setup_announcement_on_relay(&source_relay, &keys, &domain_refs, repo_id).await; | ||
| 233 | println!( | 235 | println!( |
| 234 | "Created announcement {} (kind {})", | 236 | "Announcement {} set up on source relay with git data", |
| 235 | announcement.id, | 237 | announcement.id |
| 236 | announcement.kind.as_u16() | ||
| 237 | ); | 238 | ); |
| 238 | 239 | ||
| 239 | // 5. Build the repo coordinate for the 'a' tag in the patches | 240 | // 5. Build the repo coordinate for the 'a' tag in the patches |
| @@ -241,7 +242,7 @@ async fn test_startup_sync_event_count() { | |||
| 241 | "{}:{}:{}", | 242 | "{}:{}:{}", |
| 242 | Kind::GitRepoAnnouncement.as_u16(), | 243 | Kind::GitRepoAnnouncement.as_u16(), |
| 243 | keys.public_key().to_hex(), | 244 | keys.public_key().to_hex(), |
| 244 | "test-repo-metrics" | 245 | repo_id |
| 245 | ); | 246 | ); |
| 246 | 247 | ||
| 247 | // 6. Create 3 patch events (Layer 2) that reference the announcement | 248 | // 6. Create 3 patch events (Layer 2) that reference the announcement |
| @@ -257,17 +258,11 @@ async fn test_startup_sync_event_count() { | |||
| 257 | .collect(); | 258 | .collect(); |
| 258 | println!("Created {} patches", patches.len()); | 259 | println!("Created {} patches", patches.len()); |
| 259 | 260 | ||
| 260 | // 7. Send announcement + patches to SOURCE relay ONLY | 261 | // 7. Send patches to SOURCE relay |
| 261 | let source_client = TestClient::new(source_relay.url(), keys.clone()) | 262 | let source_client = TestClient::new(source_relay.url(), keys.clone()) |
| 262 | .await | 263 | .await |
| 263 | .expect("Failed to connect to source relay"); | 264 | .expect("Failed to connect to source relay"); |
| 264 | 265 | ||
| 265 | source_client | ||
| 266 | .send_event(&announcement) | ||
| 267 | .await | ||
| 268 | .expect("Failed to send announcement to source"); | ||
| 269 | println!("Announcement sent to source relay"); | ||
| 270 | |||
| 271 | for patch in &patches { | 266 | for patch in &patches { |
| 272 | source_client | 267 | source_client |
| 273 | .send_event(patch) | 268 | .send_event(patch) |
| @@ -277,17 +272,10 @@ async fn test_startup_sync_event_count() { | |||
| 277 | println!("Patches sent to source relay"); | 272 | println!("Patches sent to source relay"); |
| 278 | source_client.disconnect().await; | 273 | source_client.disconnect().await; |
| 279 | 274 | ||
| 280 | // 8. Send announcement to SYNCING relay (triggers discovery of source relay) | 275 | // 8. Set up announcement on SYNCING relay (triggers discovery of source relay) |
| 281 | let syncing_client = TestClient::new(syncing_relay.url(), keys.clone()) | 276 | let (_announcement_syncing, _git_dir_syncing) = |
| 282 | .await | 277 | setup_announcement_on_relay(&syncing_relay, &keys, &domain_refs, repo_id).await; |
| 283 | .expect("Failed to connect to syncing relay"); | 278 | println!("Announcement set up on syncing relay (triggers discovery of source)"); |
| 284 | |||
| 285 | syncing_client | ||
| 286 | .send_event(&announcement) | ||
| 287 | .await | ||
| 288 | .expect("Failed to send announcement to syncing relay"); | ||
| 289 | println!("Announcement sent to syncing relay (triggers discovery of source)"); | ||
| 290 | syncing_client.disconnect().await; | ||
| 291 | 279 | ||
| 292 | // 9. Wait for discovery + sync to complete | 280 | // 9. Wait for discovery + sync to complete |
| 293 | println!("Waiting 5s for discovery and sync..."); | 281 | println!("Waiting 5s for discovery and sync..."); |
| @@ -404,18 +392,35 @@ async fn test_connection_failure_increments_counter() { | |||
| 404 | /// Test that live sync events are counted in metrics. | 392 | /// Test that live sync events are counted in metrics. |
| 405 | /// | 393 | /// |
| 406 | /// This test validates that events received via live subscription | 394 | /// This test validates that events received via live subscription |
| 407 | /// (after sync connection is established) are counted separately | 395 | /// (after sync connection is established) are counted in metrics. |
| 408 | /// from startup/bootstrap events. | 396 | /// Uses Layer 2 patch events (not announcements) to avoid purgatory, |
| 397 | /// since Layer 2 events are accepted directly to the DB. | ||
| 409 | #[tokio::test] | 398 | #[tokio::test] |
| 410 | async fn test_live_sync_event_count() { | 399 | async fn test_live_sync_event_count() { |
| 411 | let mut harness = MetricsTestHarness::with_sources(1).await; | ||
| 412 | |||
| 413 | // Pre-allocate syncing relay port to include in announcements | 400 | // Pre-allocate syncing relay port to include in announcements |
| 414 | let sync_port = TestRelay::find_free_port(); | 401 | let sync_port = TestRelay::find_free_port(); |
| 415 | let sync_domain = format!("127.0.0.1:{}", sync_port); | 402 | let sync_domain = format!("127.0.0.1:{}", sync_port); |
| 416 | 403 | ||
| 404 | // Start source relay | ||
| 405 | let source_relay = TestRelay::start().await; | ||
| 406 | println!("Source relay started at {}", source_relay.url()); | ||
| 407 | |||
| 408 | // Set up announcement on source relay BEFORE starting syncing relay | ||
| 409 | // This allows discovery when syncing relay connects | ||
| 410 | let keys = Keys::generate(); | ||
| 411 | let repo_id = "live-metrics-repo"; | ||
| 412 | let domains = vec![source_relay.domain(), sync_domain.clone()]; | ||
| 413 | let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect(); | ||
| 414 | |||
| 415 | let (_announcement, _git_dir) = | ||
| 416 | setup_announcement_on_relay(&source_relay, &keys, &domain_refs, repo_id).await; | ||
| 417 | println!("Announcement set up on source relay with git data"); | ||
| 418 | |||
| 417 | // Start syncing relay with pre-allocated port | 419 | // Start syncing relay with pre-allocated port |
| 418 | harness.start_syncing_relay_on_port(0, sync_port).await; | 420 | let syncing_relay = |
| 421 | TestRelay::start_on_port_with_options(sync_port, Some(source_relay.url().to_string()), false) | ||
| 422 | .await; | ||
| 423 | println!("Syncing relay started at {}", syncing_relay.url()); | ||
| 419 | 424 | ||
| 420 | // Wait for sync connection to be fully established with EOSE received | 425 | // Wait for sync connection to be fully established with EOSE received |
| 421 | // This ensures we're in "live" mode before submitting test events | 426 | // This ensures we're in "live" mode before submitting test events |
| @@ -424,33 +429,61 @@ async fn test_live_sync_event_count() { | |||
| 424 | .await | 429 | .await |
| 425 | .expect("Sync connection should be established"); | 430 | .expect("Sync connection should be established"); |
| 426 | 431 | ||
| 427 | // Additional small delay to ensure EOSE has been processed | 432 | // Additional delay to ensure purgatory promotion completes on syncing relay |
| 428 | tokio::time::sleep(Duration::from_millis(500)).await; | 433 | tokio::time::sleep(Duration::from_secs(4)).await; |
| 429 | 434 | ||
| 430 | // Now add events - these should be "live" not "startup" | 435 | // Now add Layer 2 patch events (not announcements) - these are accepted immediately |
| 431 | // Include BOTH domains so events are accepted by both relays | 436 | // (Layer 2 events are accepted directly to DB, no purgatory) |
| 432 | let keys = Keys::generate(); | 437 | let repo_coord_str = format!( |
| 433 | let events: Vec<_> = (0..2) | 438 | "{}:{}:{}", |
| 434 | .map(|i| { | 439 | Kind::GitRepoAnnouncement.as_u16(), |
| 435 | create_repo_announcement( | 440 | keys.public_key().to_hex(), |
| 436 | &keys, | 441 | repo_id |
| 437 | &[&harness.source_domain(0), &sync_domain], | 442 | ); |
| 438 | &format!("live-{}", i), | 443 | |
| 439 | ) | 444 | let patch1 = create_event_referencing_repo( |
| 440 | }) | 445 | &keys, |
| 441 | .collect(); | 446 | &repo_coord_str, |
| 442 | harness.submit_events(0, &events).await.unwrap(); | 447 | Kind::GitPatch.as_u16(), |
| 448 | "Live test patch 1", | ||
| 449 | ); | ||
| 450 | let patch2 = create_event_referencing_repo( | ||
| 451 | &keys, | ||
| 452 | &repo_coord_str, | ||
| 453 | Kind::GitPatch.as_u16(), | ||
| 454 | "Live test patch 2", | ||
| 455 | ); | ||
| 456 | |||
| 457 | // Send patches to source AFTER sync connection established (live mode) | ||
| 458 | let client = TestClient::new(source_relay.url(), keys.clone()) | ||
| 459 | .await | ||
| 460 | .expect("Failed to connect to source"); | ||
| 461 | client.send_event(&patch1).await.expect("Failed to send patch 1"); | ||
| 462 | client.send_event(&patch2).await.expect("Failed to send patch 2"); | ||
| 463 | client.disconnect().await; | ||
| 464 | println!("Two patches sent to source relay (live mode)"); | ||
| 443 | 465 | ||
| 444 | // Wait for live events to be processed and metrics updated | 466 | // Wait for live events to be processed and metrics updated |
| 445 | tokio::time::sleep(Duration::from_secs(4)).await; | 467 | tokio::time::sleep(Duration::from_secs(4)).await; |
| 446 | let metrics = harness.get_metrics().await.unwrap(); | 468 | |
| 469 | // Fetch metrics from syncing relay | ||
| 470 | let raw_metrics = fetch_metrics(&sync_url) | ||
| 471 | .await | ||
| 472 | .expect("Failed to fetch metrics"); | ||
| 473 | let metrics = ParsedMetrics::parse(&raw_metrics); | ||
| 447 | 474 | ||
| 448 | let synced_count = metrics.events_synced_total(); | 475 | let synced_count = metrics.events_synced_total(); |
| 449 | println!("Events synced total: {:?}", synced_count); | 476 | println!("Events synced total: {:?}", synced_count); |
| 450 | 477 | ||
| 451 | assert_eq!(synced_count, Some(2), "Should have 2 synced events"); | 478 | // Cleanup |
| 479 | syncing_relay.stop().await; | ||
| 480 | source_relay.stop().await; | ||
| 452 | 481 | ||
| 453 | harness.stop_all().await; | 482 | assert!( |
| 483 | synced_count.is_some() && synced_count.unwrap() >= 2, | ||
| 484 | "Should have synced at least 2 events, got {:?}", | ||
| 485 | synced_count | ||
| 486 | ); | ||
| 454 | } | 487 | } |
| 455 | 488 | ||
| 456 | /// Test that relay connected status is tracked in metrics. | 489 | /// Test that relay connected status is tracked in metrics. |
diff --git a/tests/sync/mod.rs b/tests/sync/mod.rs index 400341f..70c6981 100644 --- a/tests/sync/mod.rs +++ b/tests/sync/mod.rs | |||
| @@ -82,14 +82,12 @@ | |||
| 82 | //! **Example from `discovery.rs`:** | 82 | //! **Example from `discovery.rs`:** |
| 83 | //! ```rust | 83 | //! ```rust |
| 84 | //! #[tokio::test] | 84 | //! #[tokio::test] |
| 85 | //! async fn test_recursive_relay_discovery() { | 85 | //! async fn test_discovers_layer3_via_layer2() { |
| 86 | //! // Multi-relay orchestration | 86 | //! // Multi-relay orchestration |
| 87 | //! let relay1 = TestRelay::start().await; | 87 | //! let relay_a = TestRelay::start().await; |
| 88 | //! let relay2 = TestRelay::start().await; | 88 | //! let relay_b = TestRelay::start_with_sync(None).await; |
| 89 | //! let relay3 = TestRelay::start().await; | ||
| 90 | //! | 89 | //! |
| 91 | //! // relay1 announces relay2, relay2 announces relay3 | 90 | //! // relay_b receives announcement listing relay_a, discovers and syncs from it |
| 92 | //! // Verify relay1 discovers relay3 through chain | ||
| 93 | //! } | 91 | //! } |
| 94 | //! ``` | 92 | //! ``` |
| 95 | //! | 93 | //! |
diff --git a/tests/sync/tag_variations.rs b/tests/sync/tag_variations.rs index 46b1203..021ad0e 100644 --- a/tests/sync/tag_variations.rs +++ b/tests/sync/tag_variations.rs | |||
| @@ -55,30 +55,19 @@ async fn test_layer2_sync_with_lowercase_a_tag() { | |||
| 55 | 55 | ||
| 56 | let keys = Keys::generate(); | 56 | let keys = Keys::generate(); |
| 57 | 57 | ||
| 58 | // 2. Create and send repository announcement to both relays | 58 | // 2. Create and send repository announcement to both relays with git data |
| 59 | // (purgatory requires git data before announcements are accepted) | ||
| 59 | let repo_id = "test-repo-tag-8a"; | 60 | let repo_id = "test-repo-tag-8a"; |
| 60 | let announcement = | 61 | let domains = vec![relay_a.domain(), relay_b.domain()]; |
| 61 | create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id); | 62 | let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect(); |
| 62 | 63 | ||
| 63 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | 64 | let (_announcement, _git_dir_a) = |
| 64 | .await | 65 | setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await; |
| 65 | .expect("Failed to connect to relay_a"); | 66 | println!("Announcement set up on relay_a with git data"); |
| 66 | |||
| 67 | let client_b = TestClient::new(relay_b.url(), keys.clone()) | ||
| 68 | .await | ||
| 69 | .expect("Failed to connect to relay_b"); | ||
| 70 | 67 | ||
| 71 | client_a | 68 | let (_announcement_b, _git_dir_b) = |
| 72 | .send_event(&announcement) | 69 | setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await; |
| 73 | .await | 70 | println!("Announcement set up on relay_b with git data (triggers discovery)"); |
| 74 | .expect("Failed to send announcement to relay_a"); | ||
| 75 | println!("Announcement sent to relay_a"); | ||
| 76 | |||
| 77 | client_b | ||
| 78 | .send_event(&announcement) | ||
| 79 | .await | ||
| 80 | .expect("Failed to send announcement to relay_b"); | ||
| 81 | println!("Announcement sent to relay_b (triggers discovery)"); | ||
| 82 | 71 | ||
| 83 | // 3. Wait for discovery | 72 | // 3. Wait for discovery |
| 84 | tokio::time::sleep(Duration::from_secs(1)).await; | 73 | tokio::time::sleep(Duration::from_secs(1)).await; |
| @@ -95,9 +84,10 @@ async fn test_layer2_sync_with_lowercase_a_tag() { | |||
| 95 | issue_id, | 84 | issue_id, |
| 96 | issue.kind.as_u16() | 85 | issue.kind.as_u16() |
| 97 | ); | 86 | ); |
| 98 | for tag in issue.tags.iter() { | 87 | |
| 99 | println!(" Tag: {:?}", tag.as_slice()); | 88 | let client_a = TestClient::new(relay_a.url(), keys.clone()) |
| 100 | } | 89 | .await |
| 90 | .expect("Failed to connect to relay_a"); | ||
| 101 | 91 | ||
| 102 | client_a | 92 | client_a |
| 103 | .send_event(&issue) | 93 | .send_event(&issue) |
| @@ -106,7 +96,6 @@ async fn test_layer2_sync_with_lowercase_a_tag() { | |||
| 106 | println!("Issue sent to relay_a"); | 96 | println!("Issue sent to relay_a"); |
| 107 | 97 | ||
| 108 | client_a.disconnect().await; | 98 | client_a.disconnect().await; |
| 109 | client_b.disconnect().await; | ||
| 110 | 99 | ||
| 111 | // 5. Wait and verify event syncs to relay_b | 100 | // 5. Wait and verify event syncs to relay_b |
| 112 | let filter = Filter::new() | 101 | let filter = Filter::new() |
| @@ -154,30 +143,18 @@ async fn test_layer2_sync_with_uppercase_a_tag() { | |||
| 154 | 143 | ||
| 155 | let keys = Keys::generate(); | 144 | let keys = Keys::generate(); |
| 156 | 145 | ||
| 157 | // 2. Create and send repository announcement to both relays | 146 | // 2. Create and send repository announcement to both relays with git data |
| 158 | let repo_id = "test-repo-tag-8b"; | 147 | let repo_id = "test-repo-tag-8b"; |
| 159 | let announcement = | 148 | let domains = vec![relay_a.domain(), relay_b.domain()]; |
| 160 | create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id); | 149 | let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect(); |
| 161 | |||
| 162 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | ||
| 163 | .await | ||
| 164 | .expect("Failed to connect to relay_a"); | ||
| 165 | 150 | ||
| 166 | let client_b = TestClient::new(relay_b.url(), keys.clone()) | 151 | let (_announcement, _git_dir_a) = |
| 167 | .await | 152 | setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await; |
| 168 | .expect("Failed to connect to relay_b"); | 153 | println!("Announcement set up on relay_a with git data"); |
| 169 | 154 | ||
| 170 | client_a | 155 | let (_announcement_b, _git_dir_b) = |
| 171 | .send_event(&announcement) | 156 | setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await; |
| 172 | .await | 157 | println!("Announcement set up on relay_b with git data (triggers discovery)"); |
| 173 | .expect("Failed to send announcement to relay_a"); | ||
| 174 | println!("Announcement sent to relay_a"); | ||
| 175 | |||
| 176 | client_b | ||
| 177 | .send_event(&announcement) | ||
| 178 | .await | ||
| 179 | .expect("Failed to send announcement to relay_b"); | ||
| 180 | println!("Announcement sent to relay_b (triggers discovery)"); | ||
| 181 | 158 | ||
| 182 | // 3. Wait for discovery | 159 | // 3. Wait for discovery |
| 183 | tokio::time::sleep(Duration::from_secs(1)).await; | 160 | tokio::time::sleep(Duration::from_secs(1)).await; |
| @@ -197,9 +174,10 @@ async fn test_layer2_sync_with_uppercase_a_tag() { | |||
| 197 | issue_id, | 174 | issue_id, |
| 198 | issue.kind.as_u16() | 175 | issue.kind.as_u16() |
| 199 | ); | 176 | ); |
| 200 | for tag in issue.tags.iter() { | 177 | |
| 201 | println!(" Tag: {:?}", tag.as_slice()); | 178 | let client_a = TestClient::new(relay_a.url(), keys.clone()) |
| 202 | } | 179 | .await |
| 180 | .expect("Failed to connect to relay_a"); | ||
| 203 | 181 | ||
| 204 | client_a | 182 | client_a |
| 205 | .send_event(&issue) | 183 | .send_event(&issue) |
| @@ -208,7 +186,6 @@ async fn test_layer2_sync_with_uppercase_a_tag() { | |||
| 208 | println!("Issue sent to relay_a"); | 186 | println!("Issue sent to relay_a"); |
| 209 | 187 | ||
| 210 | client_a.disconnect().await; | 188 | client_a.disconnect().await; |
| 211 | client_b.disconnect().await; | ||
| 212 | 189 | ||
| 213 | // 5. Wait and verify event syncs to relay_b | 190 | // 5. Wait and verify event syncs to relay_b |
| 214 | let filter = Filter::new() | 191 | let filter = Filter::new() |
| @@ -255,30 +232,18 @@ async fn test_layer2_sync_with_q_tag() { | |||
| 255 | 232 | ||
| 256 | let keys = Keys::generate(); | 233 | let keys = Keys::generate(); |
| 257 | 234 | ||
| 258 | // 2. Create and send repository announcement to both relays | 235 | // 2. Create and send repository announcement to both relays with git data |
| 259 | let repo_id = "test-repo-tag-8c"; | 236 | let repo_id = "test-repo-tag-8c"; |
| 260 | let announcement = | 237 | let domains = vec![relay_a.domain(), relay_b.domain()]; |
| 261 | create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id); | 238 | let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect(); |
| 262 | 239 | ||
| 263 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | 240 | let (_announcement, _git_dir_a) = |
| 264 | .await | 241 | setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await; |
| 265 | .expect("Failed to connect to relay_a"); | 242 | println!("Announcement set up on relay_a with git data"); |
| 266 | 243 | ||
| 267 | let client_b = TestClient::new(relay_b.url(), keys.clone()) | 244 | let (_announcement_b, _git_dir_b) = |
| 268 | .await | 245 | setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await; |
| 269 | .expect("Failed to connect to relay_b"); | 246 | println!("Announcement set up on relay_b with git data (triggers discovery)"); |
| 270 | |||
| 271 | client_a | ||
| 272 | .send_event(&announcement) | ||
| 273 | .await | ||
| 274 | .expect("Failed to send announcement to relay_a"); | ||
| 275 | println!("Announcement sent to relay_a"); | ||
| 276 | |||
| 277 | client_b | ||
| 278 | .send_event(&announcement) | ||
| 279 | .await | ||
| 280 | .expect("Failed to send announcement to relay_b"); | ||
| 281 | println!("Announcement sent to relay_b (triggers discovery)"); | ||
| 282 | 247 | ||
| 283 | // 3. Wait for discovery | 248 | // 3. Wait for discovery |
| 284 | tokio::time::sleep(Duration::from_secs(1)).await; | 249 | tokio::time::sleep(Duration::from_secs(1)).await; |
| @@ -294,9 +259,10 @@ async fn test_layer2_sync_with_q_tag() { | |||
| 294 | issue_id, | 259 | issue_id, |
| 295 | issue.kind.as_u16() | 260 | issue.kind.as_u16() |
| 296 | ); | 261 | ); |
| 297 | for tag in issue.tags.iter() { | 262 | |
| 298 | println!(" Tag: {:?}", tag.as_slice()); | 263 | let client_a = TestClient::new(relay_a.url(), keys.clone()) |
| 299 | } | 264 | .await |
| 265 | .expect("Failed to connect to relay_a"); | ||
| 300 | 266 | ||
| 301 | client_a | 267 | client_a |
| 302 | .send_event(&issue) | 268 | .send_event(&issue) |
| @@ -305,7 +271,6 @@ async fn test_layer2_sync_with_q_tag() { | |||
| 305 | println!("Issue sent to relay_a"); | 271 | println!("Issue sent to relay_a"); |
| 306 | 272 | ||
| 307 | client_a.disconnect().await; | 273 | client_a.disconnect().await; |
| 308 | client_b.disconnect().await; | ||
| 309 | 274 | ||
| 310 | // 5. Wait and verify event syncs to relay_b | 275 | // 5. Wait and verify event syncs to relay_b |
| 311 | let filter = Filter::new() | 276 | let filter = Filter::new() |
| @@ -362,30 +327,18 @@ async fn test_layer3_sync_with_lowercase_e_tag() { | |||
| 362 | 327 | ||
| 363 | let keys = Keys::generate(); | 328 | let keys = Keys::generate(); |
| 364 | 329 | ||
| 365 | // 2. Create and send repository announcement to both relays | 330 | // 2. Create and send repository announcement to both relays with git data |
| 366 | let repo_id = "test-repo-tag-9a"; | 331 | let repo_id = "test-repo-tag-9a"; |
| 367 | let announcement = | 332 | let domains = vec![relay_a.domain(), relay_b.domain()]; |
| 368 | create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id); | 333 | let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect(); |
| 369 | 334 | ||
| 370 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | 335 | let (_announcement, _git_dir_a) = |
| 371 | .await | 336 | setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await; |
| 372 | .expect("Failed to connect to relay_a"); | 337 | println!("Announcement set up on relay_a with git data"); |
| 373 | |||
| 374 | let client_b = TestClient::new(relay_b.url(), keys.clone()) | ||
| 375 | .await | ||
| 376 | .expect("Failed to connect to relay_b"); | ||
| 377 | 338 | ||
| 378 | client_a | 339 | let (_announcement_b, _git_dir_b) = |
| 379 | .send_event(&announcement) | 340 | setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await; |
| 380 | .await | 341 | println!("Announcement set up on relay_b with git data (triggers discovery)"); |
| 381 | .expect("Failed to send announcement to relay_a"); | ||
| 382 | println!("Announcement sent to relay_a"); | ||
| 383 | |||
| 384 | client_b | ||
| 385 | .send_event(&announcement) | ||
| 386 | .await | ||
| 387 | .expect("Failed to send announcement to relay_b"); | ||
| 388 | println!("Announcement sent to relay_b (triggers discovery)"); | ||
| 389 | 342 | ||
| 390 | // 3. Wait for discovery | 343 | // 3. Wait for discovery |
| 391 | tokio::time::sleep(Duration::from_secs(1)).await; | 344 | tokio::time::sleep(Duration::from_secs(1)).await; |
| @@ -396,6 +349,10 @@ async fn test_layer3_sync_with_lowercase_e_tag() { | |||
| 396 | .expect("Failed to create issue"); | 349 | .expect("Failed to create issue"); |
| 397 | let issue_id = issue.id; | 350 | let issue_id = issue.id; |
| 398 | 351 | ||
| 352 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | ||
| 353 | .await | ||
| 354 | .expect("Failed to connect to relay_a"); | ||
| 355 | |||
| 399 | client_a | 356 | client_a |
| 400 | .send_event(&issue) | 357 | .send_event(&issue) |
| 401 | .await | 358 | .await |
| @@ -410,11 +367,6 @@ async fn test_layer3_sync_with_lowercase_e_tag() { | |||
| 410 | assert!(issue_synced, "Layer 2 issue should sync first"); | 367 | assert!(issue_synced, "Layer 2 issue should sync first"); |
| 411 | 368 | ||
| 412 | // Wait for Layer 3 subscriptions to be established | 369 | // Wait for Layer 3 subscriptions to be established |
| 413 | // After issue syncs, relay_b's SelfSubscriber needs time to: | ||
| 414 | // 1. Receive the synced issue via notify_event broadcast | ||
| 415 | // 2. Batch timer to tick (up to 200ms in tests) | ||
| 416 | // 3. Process batch and create Layer 3 filters | ||
| 417 | // 4. Subscribe to relay_a with Layer 3 filters | ||
| 418 | tokio::time::sleep(Duration::from_millis(500)).await; | 370 | tokio::time::sleep(Duration::from_millis(500)).await; |
| 419 | 371 | ||
| 420 | // 6. Create and send Layer 3 reply with lowercase 'e' tag (kind 1) | 372 | // 6. Create and send Layer 3 reply with lowercase 'e' tag (kind 1) |
| @@ -427,9 +379,6 @@ async fn test_layer3_sync_with_lowercase_e_tag() { | |||
| 427 | reply_id, | 379 | reply_id, |
| 428 | reply.kind.as_u16() | 380 | reply.kind.as_u16() |
| 429 | ); | 381 | ); |
| 430 | for tag in reply.tags.iter() { | ||
| 431 | println!(" Tag: {:?}", tag.as_slice()); | ||
| 432 | } | ||
| 433 | 382 | ||
| 434 | client_a | 383 | client_a |
| 435 | .send_event(&reply) | 384 | .send_event(&reply) |
| @@ -438,7 +387,6 @@ async fn test_layer3_sync_with_lowercase_e_tag() { | |||
| 438 | println!("Layer 3 reply {} sent to relay_a", reply_id); | 387 | println!("Layer 3 reply {} sent to relay_a", reply_id); |
| 439 | 388 | ||
| 440 | client_a.disconnect().await; | 389 | client_a.disconnect().await; |
| 441 | client_b.disconnect().await; | ||
| 442 | 390 | ||
| 443 | // 7. Wait and verify reply syncs to relay_b | 391 | // 7. Wait and verify reply syncs to relay_b |
| 444 | let reply_filter = Filter::new() | 392 | let reply_filter = Filter::new() |
| @@ -486,30 +434,18 @@ async fn test_layer3_sync_with_uppercase_e_tag() { | |||
| 486 | 434 | ||
| 487 | let keys = Keys::generate(); | 435 | let keys = Keys::generate(); |
| 488 | 436 | ||
| 489 | // 2. Create and send repository announcement to both relays | 437 | // 2. Create and send repository announcement to both relays with git data |
| 490 | let repo_id = "test-repo-tag-9b"; | 438 | let repo_id = "test-repo-tag-9b"; |
| 491 | let announcement = | 439 | let domains = vec![relay_a.domain(), relay_b.domain()]; |
| 492 | create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id); | 440 | let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect(); |
| 493 | |||
| 494 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | ||
| 495 | .await | ||
| 496 | .expect("Failed to connect to relay_a"); | ||
| 497 | 441 | ||
| 498 | let client_b = TestClient::new(relay_b.url(), keys.clone()) | 442 | let (_announcement, _git_dir_a) = |
| 499 | .await | 443 | setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await; |
| 500 | .expect("Failed to connect to relay_b"); | 444 | println!("Announcement set up on relay_a with git data"); |
| 501 | 445 | ||
| 502 | client_a | 446 | let (_announcement_b, _git_dir_b) = |
| 503 | .send_event(&announcement) | 447 | setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await; |
| 504 | .await | 448 | println!("Announcement set up on relay_b with git data (triggers discovery)"); |
| 505 | .expect("Failed to send announcement to relay_a"); | ||
| 506 | println!("Announcement sent to relay_a"); | ||
| 507 | |||
| 508 | client_b | ||
| 509 | .send_event(&announcement) | ||
| 510 | .await | ||
| 511 | .expect("Failed to send announcement to relay_b"); | ||
| 512 | println!("Announcement sent to relay_b (triggers discovery)"); | ||
| 513 | 449 | ||
| 514 | // 3. Wait for discovery | 450 | // 3. Wait for discovery |
| 515 | tokio::time::sleep(Duration::from_secs(1)).await; | 451 | tokio::time::sleep(Duration::from_secs(1)).await; |
| @@ -520,6 +456,10 @@ async fn test_layer3_sync_with_uppercase_e_tag() { | |||
| 520 | .expect("Failed to create issue"); | 456 | .expect("Failed to create issue"); |
| 521 | let issue_id = issue.id; | 457 | let issue_id = issue.id; |
| 522 | 458 | ||
| 459 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | ||
| 460 | .await | ||
| 461 | .expect("Failed to connect to relay_a"); | ||
| 462 | |||
| 523 | client_a | 463 | client_a |
| 524 | .send_event(&issue) | 464 | .send_event(&issue) |
| 525 | .await | 465 | .await |
| @@ -534,11 +474,6 @@ async fn test_layer3_sync_with_uppercase_e_tag() { | |||
| 534 | assert!(issue_synced, "Layer 2 issue should sync first"); | 474 | assert!(issue_synced, "Layer 2 issue should sync first"); |
| 535 | 475 | ||
| 536 | // Wait for Layer 3 subscriptions to be established | 476 | // Wait for Layer 3 subscriptions to be established |
| 537 | // After issue syncs, relay_b's SelfSubscriber needs time to: | ||
| 538 | // 1. Receive the synced issue via notify_event broadcast | ||
| 539 | // 2. Batch timer to tick (up to 200ms in tests) | ||
| 540 | // 3. Process batch and create Layer 3 filters | ||
| 541 | // 4. Subscribe to relay_a with Layer 3 filters | ||
| 542 | tokio::time::sleep(Duration::from_millis(500)).await; | 477 | tokio::time::sleep(Duration::from_millis(500)).await; |
| 543 | 478 | ||
| 544 | // 6. Create and send Layer 3 comment with uppercase 'E' tag (kind 1111) | 479 | // 6. Create and send Layer 3 comment with uppercase 'E' tag (kind 1111) |
| @@ -552,9 +487,6 @@ async fn test_layer3_sync_with_uppercase_e_tag() { | |||
| 552 | comment_id, | 487 | comment_id, |
| 553 | comment.kind.as_u16() | 488 | comment.kind.as_u16() |
| 554 | ); | 489 | ); |
| 555 | for tag in comment.tags.iter() { | ||
| 556 | println!(" Tag: {:?}", tag.as_slice()); | ||
| 557 | } | ||
| 558 | 490 | ||
| 559 | client_a | 491 | client_a |
| 560 | .send_event(&comment) | 492 | .send_event(&comment) |
| @@ -563,7 +495,6 @@ async fn test_layer3_sync_with_uppercase_e_tag() { | |||
| 563 | println!("Layer 3 comment {} sent to relay_a", comment_id); | 495 | println!("Layer 3 comment {} sent to relay_a", comment_id); |
| 564 | 496 | ||
| 565 | client_a.disconnect().await; | 497 | client_a.disconnect().await; |
| 566 | client_b.disconnect().await; | ||
| 567 | 498 | ||
| 568 | // 7. Wait and verify comment syncs to relay_b | 499 | // 7. Wait and verify comment syncs to relay_b |
| 569 | let comment_filter = Filter::new() | 500 | let comment_filter = Filter::new() |
| @@ -614,30 +545,18 @@ async fn test_layer3_sync_with_q_tag() { | |||
| 614 | 545 | ||
| 615 | let keys = Keys::generate(); | 546 | let keys = Keys::generate(); |
| 616 | 547 | ||
| 617 | // 2. Create and send repository announcement to both relays | 548 | // 2. Create and send repository announcement to both relays with git data |
| 618 | let repo_id = "test-repo-tag-9c"; | 549 | let repo_id = "test-repo-tag-9c"; |
| 619 | let announcement = | 550 | let domains = vec![relay_a.domain(), relay_b.domain()]; |
| 620 | create_repo_announcement(&keys, &[&relay_a.domain(), &relay_b.domain()], repo_id); | 551 | let domain_refs: Vec<&str> = domains.iter().map(|s| s.as_str()).collect(); |
| 621 | 552 | ||
| 622 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | 553 | let (_announcement, _git_dir_a) = |
| 623 | .await | 554 | setup_announcement_on_relay(&relay_a, &keys, &domain_refs, repo_id).await; |
| 624 | .expect("Failed to connect to relay_a"); | 555 | println!("Announcement set up on relay_a with git data"); |
| 625 | 556 | ||
| 626 | let client_b = TestClient::new(relay_b.url(), keys.clone()) | 557 | let (_announcement_b, _git_dir_b) = |
| 627 | .await | 558 | setup_announcement_on_relay(&relay_b, &keys, &domain_refs, repo_id).await; |
| 628 | .expect("Failed to connect to relay_b"); | 559 | println!("Announcement set up on relay_b with git data (triggers discovery)"); |
| 629 | |||
| 630 | client_a | ||
| 631 | .send_event(&announcement) | ||
| 632 | .await | ||
| 633 | .expect("Failed to send announcement to relay_a"); | ||
| 634 | println!("Announcement sent to relay_a"); | ||
| 635 | |||
| 636 | client_b | ||
| 637 | .send_event(&announcement) | ||
| 638 | .await | ||
| 639 | .expect("Failed to send announcement to relay_b"); | ||
| 640 | println!("Announcement sent to relay_b (triggers discovery)"); | ||
| 641 | 560 | ||
| 642 | // 3. Wait for discovery | 561 | // 3. Wait for discovery |
| 643 | tokio::time::sleep(Duration::from_secs(1)).await; | 562 | tokio::time::sleep(Duration::from_secs(1)).await; |
| @@ -648,6 +567,10 @@ async fn test_layer3_sync_with_q_tag() { | |||
| 648 | .expect("Failed to create issue"); | 567 | .expect("Failed to create issue"); |
| 649 | let issue_id = issue.id; | 568 | let issue_id = issue.id; |
| 650 | 569 | ||
| 570 | let client_a = TestClient::new(relay_a.url(), keys.clone()) | ||
| 571 | .await | ||
| 572 | .expect("Failed to connect to relay_a"); | ||
| 573 | |||
| 651 | client_a | 574 | client_a |
| 652 | .send_event(&issue) | 575 | .send_event(&issue) |
| 653 | .await | 576 | .await |
| @@ -662,11 +585,6 @@ async fn test_layer3_sync_with_q_tag() { | |||
| 662 | assert!(issue_synced, "Layer 2 issue should sync first"); | 585 | assert!(issue_synced, "Layer 2 issue should sync first"); |
| 663 | 586 | ||
| 664 | // Wait for Layer 3 subscriptions to be established | 587 | // Wait for Layer 3 subscriptions to be established |
| 665 | // After issue syncs, relay_b's SelfSubscriber needs time to: | ||
| 666 | // 1. Receive the synced issue via notify_event broadcast | ||
| 667 | // 2. Batch timer to tick (up to 200ms in tests) | ||
| 668 | // 3. Process batch and create Layer 3 filters | ||
| 669 | // 4. Subscribe to relay_a with Layer 3 filters | ||
| 670 | tokio::time::sleep(Duration::from_millis(500)).await; | 588 | tokio::time::sleep(Duration::from_millis(500)).await; |
| 671 | 589 | ||
| 672 | // 6. Create and send Layer 3 quote with 'q' tag (kind 1) | 590 | // 6. Create and send Layer 3 quote with 'q' tag (kind 1) |
| @@ -679,9 +597,6 @@ async fn test_layer3_sync_with_q_tag() { | |||
| 679 | quote_id, | 597 | quote_id, |
| 680 | quote.kind.as_u16() | 598 | quote.kind.as_u16() |
| 681 | ); | 599 | ); |
| 682 | for tag in quote.tags.iter() { | ||
| 683 | println!(" Tag: {:?}", tag.as_slice()); | ||
| 684 | } | ||
| 685 | 600 | ||
| 686 | client_a | 601 | client_a |
| 687 | .send_event("e) | 602 | .send_event("e) |
| @@ -690,7 +605,6 @@ async fn test_layer3_sync_with_q_tag() { | |||
| 690 | println!("Layer 3 quote {} sent to relay_a", quote_id); | 605 | println!("Layer 3 quote {} sent to relay_a", quote_id); |
| 691 | 606 | ||
| 692 | client_a.disconnect().await; | 607 | client_a.disconnect().await; |
| 693 | client_b.disconnect().await; | ||
| 694 | 608 | ||
| 695 | // 7. Wait and verify quote syncs to relay_b | 609 | // 7. Wait and verify quote syncs to relay_b |
| 696 | let quote_filter = Filter::new() | 610 | let quote_filter = Filter::new() |