mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
All sync tests now create a local git repo, send announcement + state event to the source relay, and push git data to release both from purgatory before the syncing relay starts bootstrap sync.
502 lines
18 KiB
Rust
502 lines
18 KiB
Rust
//! Integration tests for GRASP-02 PR3: Maintainer Announcement Re-Processing
|
|
//!
|
|
//! Tests the two-tier rejected events index and immediate re-processing of
|
|
//! maintainer announcements when owner announcements are accepted.
|
|
|
|
use std::time::Duration;
|
|
|
|
use nostr_sdk::prelude::*;
|
|
|
|
use crate::common::{
|
|
sync_helpers::*,
|
|
TestRelay,
|
|
};
|
|
|
|
/// Test that maintainer announcements are re-processed immediately when owner announcement accepted
|
|
///
|
|
/// Flow:
|
|
/// 1. relay_a: Maintainer sends announcement (gets rejected - doesn't list relay_b)
|
|
/// 2. relay_b: Owner sends announcement (lists relay_a + maintainer)
|
|
/// 3. relay_b syncs from relay_a, maintainer announcement enters rejected index
|
|
/// 4. relay_b processes owner announcement, invalidates and re-processes maintainer announcement
|
|
/// 5. Both announcements should be in relay_b's database
|
|
///
|
|
/// Expected time: <5 seconds (vs 24 hours without hot cache)
|
|
#[tokio::test]
|
|
async fn test_maintainer_announcement_reprocessed_immediately() {
|
|
// Start relay_a (where maintainer announcement will be sent)
|
|
let relay_a = TestRelay::start().await;
|
|
println!("relay_a started at {}", relay_a.url());
|
|
|
|
// Start relay_b with sync enabled (will sync from relay_a)
|
|
let relay_b = TestRelay::start_with_sync(None).await;
|
|
println!("relay_b started at {}", relay_b.url());
|
|
|
|
// Create keys
|
|
let owner_keys = Keys::generate();
|
|
let maintainer_keys = Keys::generate();
|
|
|
|
let identifier = "test-repo";
|
|
|
|
let start = std::time::Instant::now();
|
|
|
|
// Step 1: Send maintainer announcement to relay_a (will be rejected by relay_b - doesn't list relay_b)
|
|
// Use HTTP clone URL pointing to relay_a's git endpoint so it can be released from purgatory
|
|
let maintainer_npub = maintainer_keys
|
|
.public_key()
|
|
.to_bech32()
|
|
.expect("Failed to get npub");
|
|
|
|
let maintainer_announcement =
|
|
EventBuilder::new(Kind::GitRepoAnnouncement, "Maintainer's repository")
|
|
.tags(vec![
|
|
Tag::identifier(identifier),
|
|
Tag::custom(
|
|
TagKind::custom("clone"),
|
|
vec![format!(
|
|
"http://{}/{}/{}.git",
|
|
relay_a.domain(),
|
|
maintainer_npub,
|
|
identifier
|
|
)],
|
|
),
|
|
Tag::custom(TagKind::custom("relays"), vec![relay_a.url().to_string()]),
|
|
])
|
|
.sign_with_keys(&maintainer_keys)
|
|
.unwrap();
|
|
|
|
send_to_relay(&relay_a, &maintainer_announcement)
|
|
.await
|
|
.unwrap();
|
|
println!("✓ Maintainer announcement sent to relay_a");
|
|
|
|
// Push git data for maintainer's repo to relay_a → releases maintainer announcement from purgatory
|
|
let _git_dir_maintainer = push_git_data_to_relay(
|
|
&relay_a,
|
|
&maintainer_keys,
|
|
identifier,
|
|
&[&relay_a.domain()],
|
|
)
|
|
.await;
|
|
println!("✓ Maintainer git data pushed to relay_a (announcement released from purgatory)");
|
|
|
|
// Step 2: Set up owner announcement on relay_b (lists relay_a + maintainer) with git data
|
|
let owner_npub = owner_keys
|
|
.public_key()
|
|
.to_bech32()
|
|
.expect("Failed to get npub");
|
|
|
|
let owner_announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "Owner's repository")
|
|
.tags(vec![
|
|
Tag::identifier(identifier),
|
|
Tag::custom(
|
|
TagKind::custom("clone"),
|
|
vec![format!(
|
|
"http://{}/{}/{}.git",
|
|
relay_b.domain(),
|
|
owner_npub,
|
|
identifier
|
|
)],
|
|
),
|
|
Tag::custom(
|
|
TagKind::custom("relays"),
|
|
vec![relay_a.url().to_string(), relay_b.url().to_string()],
|
|
),
|
|
Tag::custom(
|
|
TagKind::custom("maintainers"),
|
|
vec![maintainer_keys.public_key().to_hex()],
|
|
),
|
|
])
|
|
.sign_with_keys(&owner_keys)
|
|
.unwrap();
|
|
|
|
send_to_relay(&relay_b, &owner_announcement).await.unwrap();
|
|
println!("✓ Owner announcement sent to relay_b");
|
|
|
|
// Push git data for owner's repo to relay_b → releases owner announcement from purgatory
|
|
let _git_dir_owner =
|
|
push_git_data_to_relay(&relay_b, &owner_keys, identifier, &[&relay_b.domain()]).await;
|
|
println!("✓ Owner git data pushed to relay_b (announcement released from purgatory)");
|
|
|
|
// Step 3: Wait for sync and re-processing (relay_b discovers relay_a, syncs, re-processes)
|
|
tokio::time::sleep(Duration::from_secs(3)).await;
|
|
|
|
let elapsed = start.elapsed();
|
|
|
|
// Step 4: Verify both announcements are in relay_b's database
|
|
let owner_filter = Filter::new()
|
|
.kind(Kind::GitRepoAnnouncement)
|
|
.author(owner_keys.public_key())
|
|
.identifier(identifier);
|
|
|
|
let owner_found =
|
|
wait_for_event_on_relay(relay_b.url(), owner_filter, Duration::from_secs(2)).await;
|
|
assert!(owner_found, "Owner announcement should be in relay_b");
|
|
|
|
let maintainer_filter = Filter::new()
|
|
.kind(Kind::GitRepoAnnouncement)
|
|
.author(maintainer_keys.public_key())
|
|
.identifier(identifier);
|
|
|
|
let maintainer_found =
|
|
wait_for_event_on_relay(relay_b.url(), maintainer_filter, Duration::from_secs(2)).await;
|
|
assert!(
|
|
maintainer_found,
|
|
"Maintainer announcement should be re-processed and accepted in relay_b"
|
|
);
|
|
|
|
// Step 5: Verify it happened quickly (not 24 hours!)
|
|
assert!(
|
|
elapsed.as_secs() < 15,
|
|
"Re-processing should happen in <15 seconds, took {:?}",
|
|
elapsed
|
|
);
|
|
|
|
println!("✅ Maintainer announcement re-processed in {:?}", elapsed);
|
|
|
|
relay_a.stop().await;
|
|
relay_b.stop().await;
|
|
}
|
|
|
|
/// Test that maintainer announcements NOT in hot cache are still prevented from re-fetching
|
|
///
|
|
/// Flow:
|
|
/// 1. Maintainer announcement arrives → Rejected (added to hot cache + cold index)
|
|
/// 2. Wait for hot cache to expire (2+ minutes)
|
|
/// 3. Owner announcement arrives → Invalidates cold index
|
|
/// 4. Maintainer announcement should NOT be re-fetched (cold index prevents)
|
|
/// 5. Only owner announcement should be in database
|
|
///
|
|
/// This test verifies the cold index prevents repeated downloads after hot cache expiry.
|
|
/// Note: This test is slow (2+ minutes) so we'll skip it in normal test runs.
|
|
#[tokio::test]
|
|
#[ignore] // Skip by default due to 2+ minute duration
|
|
async fn test_maintainer_announcement_cold_index_prevents_refetch() {
|
|
let relay = TestRelay::start().await;
|
|
|
|
// Create keys
|
|
let owner_keys = Keys::generate();
|
|
let maintainer_keys = Keys::generate();
|
|
|
|
let identifier = "test-repo-cold";
|
|
|
|
// Create client using TestClient helper
|
|
let client = TestClient::new(relay.url(), maintainer_keys.clone())
|
|
.await
|
|
.expect("Failed to connect to relay");
|
|
|
|
// Step 1: Send maintainer announcement (will be rejected - doesn't list our relay)
|
|
let maintainer_announcement =
|
|
EventBuilder::new(Kind::GitRepoAnnouncement, "Maintainer's repository")
|
|
.tags(vec![
|
|
Tag::identifier(identifier),
|
|
Tag::custom(
|
|
TagKind::custom("clone"),
|
|
vec![format!("https://example.com/{}.git", identifier)],
|
|
),
|
|
Tag::custom(
|
|
TagKind::custom("relays"),
|
|
vec!["wss://example.com".to_string()],
|
|
),
|
|
])
|
|
.sign_with_keys(&maintainer_keys)
|
|
.unwrap();
|
|
|
|
// Send maintainer announcement - expect it to be rejected
|
|
let _ = client.send_event(&maintainer_announcement).await;
|
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
|
|
// Step 2: Wait for hot cache to expire (default: 120 seconds)
|
|
println!("⏳ Waiting for hot cache to expire (120 seconds)...");
|
|
tokio::time::sleep(Duration::from_secs(125)).await;
|
|
|
|
// Step 3: Send owner announcement (lists maintainer)
|
|
let owner_announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "Owner's repository")
|
|
.tags(vec![
|
|
Tag::identifier(identifier),
|
|
Tag::custom(
|
|
TagKind::custom("clone"),
|
|
vec![format!("https://{}/{}.git", relay.domain(), identifier)],
|
|
),
|
|
Tag::custom(TagKind::custom("relays"), vec![relay.url().to_string()]),
|
|
Tag::custom(
|
|
TagKind::custom("maintainers"),
|
|
vec![maintainer_keys.public_key().to_hex()],
|
|
),
|
|
])
|
|
.sign_with_keys(&owner_keys)
|
|
.unwrap();
|
|
|
|
client.send_event(&owner_announcement).await.unwrap();
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
|
|
// Step 4: Verify only owner announcement is in database
|
|
let owner_filter = Filter::new()
|
|
.kind(Kind::GitRepoAnnouncement)
|
|
.author(owner_keys.public_key())
|
|
.identifier(identifier);
|
|
|
|
let owner_found =
|
|
wait_for_event_on_relay(relay.url(), owner_filter, Duration::from_secs(2)).await;
|
|
assert!(owner_found, "Owner announcement should be accepted");
|
|
|
|
let maintainer_filter = Filter::new()
|
|
.kind(Kind::GitRepoAnnouncement)
|
|
.author(maintainer_keys.public_key())
|
|
.identifier(identifier);
|
|
|
|
let maintainer_found =
|
|
wait_for_event_on_relay(relay.url(), maintainer_filter, Duration::from_millis(500)).await;
|
|
assert!(
|
|
!maintainer_found,
|
|
"Maintainer announcement should NOT be re-processed (hot cache expired)"
|
|
);
|
|
|
|
println!("✅ Cold index prevented re-fetch after hot cache expiry");
|
|
|
|
client.disconnect().await;
|
|
relay.stop().await;
|
|
}
|
|
|
|
/// Test multiple maintainers are all re-processed when owner announcement accepted
|
|
///
|
|
/// Flow:
|
|
/// 1. relay_a: Three maintainers send announcements (get rejected - don't list relay_b)
|
|
/// 2. relay_b: Owner sends announcement (lists relay_a + all three maintainers)
|
|
/// 3. relay_b syncs from relay_a, all maintainer announcements enter rejected index
|
|
/// 4. relay_b processes owner announcement, invalidates and re-processes all maintainer announcements
|
|
/// 5. All four announcements should be in relay_b's database
|
|
#[tokio::test]
|
|
async fn test_multiple_maintainers_all_reprocessed() {
|
|
// Start relay_a (where maintainer announcements will be sent)
|
|
let relay_a = TestRelay::start().await;
|
|
println!("relay_a started at {}", relay_a.url());
|
|
|
|
// Start relay_b with sync enabled (will sync from relay_a)
|
|
let relay_b = TestRelay::start_with_sync(None).await;
|
|
println!("relay_b started at {}", relay_b.url());
|
|
|
|
// Create keys
|
|
let owner_keys = Keys::generate();
|
|
let maintainer1_keys = Keys::generate();
|
|
let maintainer2_keys = Keys::generate();
|
|
let maintainer3_keys = Keys::generate();
|
|
|
|
let identifier = "multi-maintainer-repo";
|
|
|
|
// Step 1: Send three maintainer announcements to relay_a with git data
|
|
// (purgatory requires git data before announcements are accepted)
|
|
let mut git_dirs_maintainers = Vec::new();
|
|
for (idx, maintainer_keys) in [&maintainer1_keys, &maintainer2_keys, &maintainer3_keys]
|
|
.iter()
|
|
.enumerate()
|
|
{
|
|
let m_npub = maintainer_keys
|
|
.public_key()
|
|
.to_bech32()
|
|
.expect("Failed to get npub");
|
|
|
|
let announcement = EventBuilder::new(
|
|
Kind::GitRepoAnnouncement,
|
|
format!("Maintainer {} repository", idx + 1),
|
|
)
|
|
.tags(vec![
|
|
Tag::identifier(identifier),
|
|
Tag::custom(
|
|
TagKind::custom("clone"),
|
|
vec![format!(
|
|
"http://{}/{}/{}.git",
|
|
relay_a.domain(),
|
|
m_npub,
|
|
identifier
|
|
)],
|
|
),
|
|
Tag::custom(TagKind::custom("relays"), vec![relay_a.url().to_string()]),
|
|
])
|
|
.sign_with_keys(maintainer_keys)
|
|
.unwrap();
|
|
|
|
send_to_relay(&relay_a, &announcement).await.unwrap();
|
|
|
|
// Push git data to release each maintainer's announcement from purgatory
|
|
let git_dir =
|
|
push_git_data_to_relay(&relay_a, maintainer_keys, identifier, &[&relay_a.domain()])
|
|
.await;
|
|
git_dirs_maintainers.push(git_dir);
|
|
}
|
|
println!("✓ Three maintainer announcements sent to relay_a with git data");
|
|
|
|
// Step 2: Send owner announcement to relay_b (lists relay_a + all three maintainers)
|
|
let owner_npub = owner_keys
|
|
.public_key()
|
|
.to_bech32()
|
|
.expect("Failed to get npub");
|
|
|
|
let owner_announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "Owner's repository")
|
|
.tags(vec![
|
|
Tag::identifier(identifier),
|
|
Tag::custom(
|
|
TagKind::custom("clone"),
|
|
vec![format!(
|
|
"http://{}/{}/{}.git",
|
|
relay_b.domain(),
|
|
owner_npub,
|
|
identifier
|
|
)],
|
|
),
|
|
Tag::custom(
|
|
TagKind::custom("relays"),
|
|
vec![relay_a.url().to_string(), relay_b.url().to_string()],
|
|
),
|
|
Tag::custom(
|
|
TagKind::custom("maintainers"),
|
|
vec![
|
|
maintainer1_keys.public_key().to_hex(),
|
|
maintainer2_keys.public_key().to_hex(),
|
|
maintainer3_keys.public_key().to_hex(),
|
|
],
|
|
),
|
|
])
|
|
.sign_with_keys(&owner_keys)
|
|
.unwrap();
|
|
|
|
send_to_relay(&relay_b, &owner_announcement).await.unwrap();
|
|
println!("✓ Owner announcement sent to relay_b");
|
|
|
|
// Push git data for owner to relay_b → releases owner announcement from purgatory
|
|
let _git_dir_owner =
|
|
push_git_data_to_relay(&relay_b, &owner_keys, identifier, &[&relay_b.domain()]).await;
|
|
println!("✓ Owner git data pushed to relay_b (announcement released from purgatory)");
|
|
|
|
// Step 3: Wait for sync and re-processing
|
|
tokio::time::sleep(Duration::from_secs(3)).await;
|
|
|
|
// Step 4: Verify all four announcements are in relay_b's database
|
|
for (name, keys) in [
|
|
("owner", &owner_keys),
|
|
("maintainer1", &maintainer1_keys),
|
|
("maintainer2", &maintainer2_keys),
|
|
("maintainer3", &maintainer3_keys),
|
|
] {
|
|
let filter = Filter::new()
|
|
.kind(Kind::GitRepoAnnouncement)
|
|
.author(keys.public_key())
|
|
.identifier(identifier);
|
|
|
|
let found = wait_for_event_on_relay(relay_b.url(), filter, Duration::from_secs(2)).await;
|
|
assert!(found, "{} announcement should be in relay_b", name);
|
|
}
|
|
|
|
println!("✅ All three maintainer announcements re-processed successfully");
|
|
|
|
relay_a.stop().await;
|
|
relay_b.stop().await;
|
|
}
|
|
|
|
/// Test that invalid maintainer public keys don't cause panics
|
|
///
|
|
/// Flow:
|
|
/// 1. Maintainer announcement arrives → Rejected
|
|
/// 2. Owner announcement arrives with INVALID maintainer hex → Should handle gracefully
|
|
/// 3. Owner announcement should still be accepted
|
|
/// 4. Maintainer announcement should NOT be re-processed (invalid pubkey)
|
|
#[tokio::test]
|
|
async fn test_invalid_maintainer_pubkey_handled_gracefully() {
|
|
let relay = TestRelay::start().await;
|
|
|
|
// Create keys
|
|
let owner_keys = Keys::generate();
|
|
let maintainer_keys = Keys::generate();
|
|
|
|
let identifier = "invalid-maintainer-repo";
|
|
|
|
// Step 1: Send maintainer announcement (will be rejected - doesn't list our relay)
|
|
// This one uses example.com clone URL - it goes to purgatory on relay, never promoted
|
|
let maintainer_announcement =
|
|
EventBuilder::new(Kind::GitRepoAnnouncement, "Maintainer's repository")
|
|
.tags(vec![
|
|
Tag::identifier(identifier),
|
|
Tag::custom(
|
|
TagKind::custom("clone"),
|
|
vec![format!("https://example.com/{}.git", identifier)],
|
|
),
|
|
Tag::custom(
|
|
TagKind::custom("relays"),
|
|
vec!["wss://example.com".to_string()],
|
|
),
|
|
])
|
|
.sign_with_keys(&maintainer_keys)
|
|
.unwrap();
|
|
|
|
// Send maintainer announcement - expect it to be rejected (purgatory / policy)
|
|
send_to_relay(&relay, &maintainer_announcement).await.ok();
|
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
|
|
// Step 2: Set up owner announcement with INVALID maintainer hex and git data
|
|
// Use HTTP clone URL to relay's git endpoint so it can be released from purgatory
|
|
let owner_npub = owner_keys
|
|
.public_key()
|
|
.to_bech32()
|
|
.expect("Failed to get npub");
|
|
|
|
let owner_announcement = EventBuilder::new(Kind::GitRepoAnnouncement, "Owner's repository")
|
|
.tags(vec![
|
|
Tag::identifier(identifier),
|
|
Tag::custom(
|
|
TagKind::custom("clone"),
|
|
vec![format!(
|
|
"http://{}/{}/{}.git",
|
|
relay.domain(),
|
|
owner_npub,
|
|
identifier
|
|
)],
|
|
),
|
|
Tag::custom(TagKind::custom("relays"), vec![relay.url().to_string()]),
|
|
Tag::custom(
|
|
TagKind::custom("maintainers"),
|
|
vec!["invalid-hex-not-a-pubkey".to_string()],
|
|
),
|
|
])
|
|
.sign_with_keys(&owner_keys)
|
|
.unwrap();
|
|
|
|
send_to_relay(&relay, &owner_announcement).await.unwrap();
|
|
|
|
// Push git data to relay → releases owner announcement from purgatory
|
|
let _git_dir =
|
|
push_git_data_to_relay(&relay, &owner_keys, identifier, &[&relay.domain()]).await;
|
|
println!("✓ Owner git data pushed to relay (announcement released from purgatory)");
|
|
|
|
// Wait for processing
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
|
|
// Step 3: Verify owner announcement accepted, maintainer not re-processed
|
|
let owner_filter = Filter::new()
|
|
.kind(Kind::GitRepoAnnouncement)
|
|
.author(owner_keys.public_key())
|
|
.identifier(identifier);
|
|
|
|
let owner_found =
|
|
wait_for_event_on_relay(relay.url(), owner_filter, Duration::from_secs(2)).await;
|
|
assert!(
|
|
owner_found,
|
|
"Owner announcement should be accepted despite invalid maintainer"
|
|
);
|
|
|
|
let maintainer_filter = Filter::new()
|
|
.kind(Kind::GitRepoAnnouncement)
|
|
.author(maintainer_keys.public_key())
|
|
.identifier(identifier);
|
|
|
|
let maintainer_found =
|
|
wait_for_event_on_relay(relay.url(), maintainer_filter, Duration::from_millis(500)).await;
|
|
assert!(
|
|
!maintainer_found,
|
|
"Maintainer announcement should NOT be re-processed (invalid pubkey)"
|
|
);
|
|
|
|
println!("✅ Invalid maintainer pubkey handled gracefully without panic");
|
|
|
|
relay.stop().await;
|
|
}
|