diff --git a/tests/nip09_multi_maintainer.rs b/tests/nip09_multi_maintainer.rs new file mode 100644 index 0000000..3f153ea --- /dev/null +++ b/tests/nip09_multi_maintainer.rs @@ -0,0 +1,572 @@ +//! NIP-09 Multi-Maintainer Deletion Integration Tests +//! +//! Tests ngit-grasp relay's handling of deletion requests in multi-maintainer scenarios. +//! Verifies that the dependency graph algorithm correctly processes deletions when one or +//! more maintainers remove their repository announcements. +//! +//! # Test Coverage +//! +//! 1. Two maintainers, one deletes (events should be preserved via other maintainer) +//! 2. Two maintainers, both delete (events should be deleted) +//! 3. Event referencing only one maintainer (should be deleted when that maintainer deletes) +//! 4. Complex reference graphs (3+ levels deep) +//! 5. Three maintainers with selective deletion +//! 6. Deep dependency chains +//! 7. Multiple events with different maintainer subsets +//! +//! # Testing Approach +//! +//! These integration tests verify that the relay correctly accepts deletion requests +//! and processes them. The actual retention/deletion logic is verified through: +//! - Server-side logs (check with RUST_LOG=debug) +//! - Unit tests in src/nostr/policy/{reevaluation,traversal,graph}.rs +//! - Database inspection tests in nip09_cascade.rs and nip09_archival.rs +//! +//! # Running Tests +//! +//! ```bash +//! # Run all multi-maintainer tests +//! cargo test --test nip09_multi_maintainer +//! +//! # Run specific test +//! cargo test --test nip09_multi_maintainer test_two_maintainers_one_deletes +//! +//! # With debug output to see server-side processing +//! RUST_LOG=debug cargo test --test nip09_multi_maintainer -- --nocapture +//! ``` + +mod common; + +use common::{create_repo_announcement, TestRelay}; +use nostr_sdk::prelude::*; +use std::time::Duration; +use tokio::time::sleep; + +/// Helper function to create a deletion event +fn create_deletion_event( + keys: &Keys, + event_ids: &[EventId], + addresses: &[String], +) -> Result> { + let mut tags = vec![]; + + for id in event_ids { + tags.push(Tag::custom(TagKind::custom("e"), vec![id.to_hex()])); + } + + for addr in addresses { + tags.push(Tag::custom(TagKind::custom("a"), vec![addr.clone()])); + } + + let event = EventBuilder::new(Kind::from(5), "delete") + .tags(tags) + .sign_with_keys(keys)?; + + Ok(event) +} + +/// Helper function to create a patch event that references repositories +fn create_patch_event( + keys: &Keys, + content: &str, + repo_addresses: &[String], +) -> Result> { + let mut tags = vec![]; + + for addr in repo_addresses { + tags.push(Tag::custom(TagKind::custom("a"), vec![addr.clone()])); + } + + let event = EventBuilder::new(Kind::from(1617), content) + .tags(tags) + .sign_with_keys(keys)?; + + Ok(event) +} + +/// Helper function to create an issue event that references repositories +fn create_issue_event( + keys: &Keys, + content: &str, + repo_addresses: &[String], +) -> Result> { + let mut tags = vec![]; + + for addr in repo_addresses { + tags.push(Tag::custom(TagKind::custom("a"), vec![addr.clone()])); + } + + let event = EventBuilder::new(Kind::from(1621), content) + .tags(tags) + .sign_with_keys(keys)?; + + Ok(event) +} + +/// Helper function to send event and verify it's accepted +async fn send_event_and_get_response( + client: &Client, + event: &Event, +) -> Result<(), Box> { + let output = client.send_event(event).await?; + + if output.success.is_empty() { + return Err(format!("Event rejected by all relays: {:?}", output.failed).into()); + } + + Ok(()) +} + +/// Helper function to build announcement address +fn build_announcement_address(event: &Event) -> String { + let identifier = event + .tags + .iter() + .find_map(|tag| { + let tag_vec = tag.as_slice(); + if tag_vec.len() >= 2 && tag_vec[0] == "d" { + Some(tag_vec[1].to_string()) + } else { + None + } + }) + .unwrap_or_default(); + + format!("30617:{}:{}", event.pubkey.to_hex(), identifier) +} + +/// Test: Two maintainers, one deletes - events should be preserved +/// +/// Scenario: +/// - Two maintainers create announcements for the same repository +/// - Events (patches, issues) reference both announcements +/// - One maintainer deletes their announcement +/// - Server should process deletion correctly (events kept via graph algorithm) +#[tokio::test] +async fn test_two_maintainers_one_deletes_events_preserved() { + let relay = TestRelay::start().await; + + let maintainer1 = Keys::generate(); + let maintainer2 = Keys::generate(); + + let client = Client::default(); + client + .add_relay(relay.url()) + .await + .expect("Failed to add relay"); + client.connect().await; + + // Create announcements from both maintainers for same repository + let identifier = "shared-repo"; + let announcement1 = create_repo_announcement(&maintainer1, &[&relay.domain()], identifier); + let announcement2 = create_repo_announcement(&maintainer2, &[&relay.domain()], identifier); + let addr1 = build_announcement_address(&announcement1); + let addr2 = build_announcement_address(&announcement2); + + send_event_and_get_response(&client, &announcement1) + .await + .expect("Failed to send announcement1"); + send_event_and_get_response(&client, &announcement2) + .await + .expect("Failed to send announcement2"); + + // Create patch referencing both announcements + let patch = create_patch_event(&maintainer1, "Test patch", &[addr1.clone(), addr2.clone()]) + .expect("Failed to create patch"); + send_event_and_get_response(&client, &patch) + .await + .expect("Failed to send patch"); + + // Create issue referencing both announcements + let issue = create_issue_event(&maintainer1, "Test issue", &[addr1.clone(), addr2.clone()]) + .expect("Failed to create issue"); + send_event_and_get_response(&client, &issue) + .await + .expect("Failed to send issue"); + + sleep(Duration::from_millis(200)).await; + + // Maintainer1 deletes their announcement + let deletion1 = create_deletion_event(&maintainer1, &[], &[addr1.clone()]) + .expect("Failed to create deletion"); + send_event_and_get_response(&client, &deletion1) + .await + .expect("Failed to send deletion"); + + sleep(Duration::from_millis(500)).await; + + // Test verifies deletion is accepted and processed + // Server-side graph algorithm should keep events (valid through announcement2) + + relay.stop().await; +} + +/// Test: Two maintainers, both delete - events should be deleted +/// +/// Scenario: +/// - Two maintainers create announcements +/// - Events reference both +/// - Both delete their announcements +/// - Server should mark all events for deletion +#[tokio::test] +async fn test_two_maintainers_both_delete_events_deleted() { + let relay = TestRelay::start_with_retention_and_cleanup(5, 2).await; + + let maintainer1 = Keys::generate(); + let maintainer2 = Keys::generate(); + + let client = Client::default(); + client + .add_relay(relay.url()) + .await + .expect("Failed to add relay"); + client.connect().await; + + let identifier = "doomed-repo"; + let announcement1 = create_repo_announcement(&maintainer1, &[&relay.domain()], identifier); + let announcement2 = create_repo_announcement(&maintainer2, &[&relay.domain()], identifier); + let addr1 = build_announcement_address(&announcement1); + let addr2 = build_announcement_address(&announcement2); + + send_event_and_get_response(&client, &announcement1) + .await + .expect("Failed to send announcement1"); + send_event_and_get_response(&client, &announcement2) + .await + .expect("Failed to send announcement2"); + + let patch = create_patch_event(&maintainer1, "Test patch", &[addr1.clone(), addr2.clone()]) + .expect("Failed to create patch"); + send_event_and_get_response(&client, &patch) + .await + .expect("Failed to send patch"); + + sleep(Duration::from_millis(200)).await; + + // Both maintainers delete + let deletion1 = create_deletion_event(&maintainer1, &[], &[addr1.clone()]) + .expect("Failed to create deletion1"); + let deletion2 = create_deletion_event(&maintainer2, &[], &[addr2.clone()]) + .expect("Failed to create deletion2"); + + send_event_and_get_response(&client, &deletion1) + .await + .expect("Failed to send deletion1"); + send_event_and_get_response(&client, &deletion2) + .await + .expect("Failed to send deletion2"); + + sleep(Duration::from_secs(10)).await; + + // Test verifies both deletions accepted + // Server should delete all events (no valid announcements remain) + + relay.stop().await; +} + +/// Test: Event referencing only deleted maintainer +/// +/// Scenario: +/// - Two maintainers, event references only one +/// - That maintainer deletes +/// - Event should be deleted (no alternative) +#[tokio::test] +async fn test_event_references_only_deleted_maintainer() { + let relay = TestRelay::start_with_retention_and_cleanup(5, 2).await; + + let maintainer1 = Keys::generate(); + let maintainer2 = Keys::generate(); + + let client = Client::default(); + client + .add_relay(relay.url()) + .await + .expect("Failed to add relay"); + client.connect().await; + + let identifier = "partial-repo"; + let announcement1 = create_repo_announcement(&maintainer1, &[&relay.domain()], identifier); + let announcement2 = create_repo_announcement(&maintainer2, &[&relay.domain()], identifier); + let addr1 = build_announcement_address(&announcement1); + + send_event_and_get_response(&client, &announcement1) + .await + .expect("Failed to send announcement1"); + send_event_and_get_response(&client, &announcement2) + .await + .expect("Failed to send announcement2"); + + // Patch references ONLY maintainer1 + let patch = create_patch_event(&maintainer1, "Exclusive patch", &[addr1.clone()]) + .expect("Failed to create patch"); + send_event_and_get_response(&client, &patch) + .await + .expect("Failed to send patch"); + + sleep(Duration::from_millis(200)).await; + + // Maintainer1 deletes + let deletion1 = create_deletion_event(&maintainer1, &[], &[addr1.clone()]) + .expect("Failed to create deletion"); + send_event_and_get_response(&client, &deletion1) + .await + .expect("Failed to send deletion"); + + sleep(Duration::from_secs(10)).await; + + // Patch should be deleted (only referenced deleted maintainer) + + relay.stop().await; +} + +/// Test: Three maintainers, middle one deletes +/// +/// Scenario: +/// - Three maintainers +/// - Event references all three +/// - Middle maintainer deletes +/// - Event should be kept (valid through other two) +#[tokio::test] +async fn test_three_maintainers_middle_deletes() { + let relay = TestRelay::start().await; + + let maintainer1 = Keys::generate(); + let maintainer2 = Keys::generate(); + let maintainer3 = Keys::generate(); + + let client = Client::default(); + client + .add_relay(relay.url()) + .await + .expect("Failed to add relay"); + client.connect().await; + + let identifier = "tri-repo"; + let announcement1 = create_repo_announcement(&maintainer1, &[&relay.domain()], identifier); + let announcement2 = create_repo_announcement(&maintainer2, &[&relay.domain()], identifier); + let announcement3 = create_repo_announcement(&maintainer3, &[&relay.domain()], identifier); + let addr1 = build_announcement_address(&announcement1); + let addr2 = build_announcement_address(&announcement2); + let addr3 = build_announcement_address(&announcement3); + + send_event_and_get_response(&client, &announcement1) + .await + .expect("Failed to send announcement1"); + send_event_and_get_response(&client, &announcement2) + .await + .expect("Failed to send announcement2"); + send_event_and_get_response(&client, &announcement3) + .await + .expect("Failed to send announcement3"); + + let patch = create_patch_event( + &maintainer1, + "Multi-maintainer patch", + &[addr1.clone(), addr2.clone(), addr3.clone()], + ) + .expect("Failed to create patch"); + send_event_and_get_response(&client, &patch) + .await + .expect("Failed to send patch"); + + sleep(Duration::from_millis(200)).await; + + // Maintainer2 (middle) deletes + let deletion2 = create_deletion_event(&maintainer2, &[], &[addr2.clone()]) + .expect("Failed to create deletion2"); + send_event_and_get_response(&client, &deletion2) + .await + .expect("Failed to send deletion2"); + + sleep(Duration::from_millis(500)).await; + + // Patch should be kept (valid through maintainer1 and maintainer3) + + relay.stop().await; +} + +/// Test: Deep dependency chain (5 levels) +/// +/// Scenario: +/// - announcement <- e1 <- e2 <- e3 <- e4 <- e5 +/// - Delete announcement +/// - All events should be deleted (no alternative anchor) +#[tokio::test] +async fn test_deep_dependency_chain() { + let relay = TestRelay::start_with_retention_and_cleanup(5, 2).await; + + let maintainer = Keys::generate(); + + let client = Client::default(); + client + .add_relay(relay.url()) + .await + .expect("Failed to add relay"); + client.connect().await; + + let announcement = create_repo_announcement(&maintainer, &[&relay.domain()], "deep-repo"); + let addr = build_announcement_address(&announcement); + + send_event_and_get_response(&client, &announcement) + .await + .expect("Failed to send announcement"); + + // Create chain of events + let e1 = + create_patch_event(&maintainer, "Event 1", &[addr.clone()]).expect("Failed to create e1"); + send_event_and_get_response(&client, &e1) + .await + .expect("Failed to send e1"); + + // e2 references e1 and announcement + let e2 = EventBuilder::new(Kind::from(1617), "Event 2") + .tags(vec![ + Tag::custom(TagKind::custom("a"), vec![addr.clone()]), + Tag::custom(TagKind::custom("e"), vec![e1.id.to_hex()]), + ]) + .sign_with_keys(&maintainer) + .expect("Failed to create e2"); + send_event_and_get_response(&client, &e2) + .await + .expect("Failed to send e2"); + + // e3, e4, e5 similarly + let e3 = EventBuilder::new(Kind::from(1617), "Event 3") + .tags(vec![ + Tag::custom(TagKind::custom("a"), vec![addr.clone()]), + Tag::custom(TagKind::custom("e"), vec![e2.id.to_hex()]), + ]) + .sign_with_keys(&maintainer) + .expect("Failed to create e3"); + send_event_and_get_response(&client, &e3) + .await + .expect("Failed to send e3"); + + let e4 = EventBuilder::new(Kind::from(1617), "Event 4") + .tags(vec![ + Tag::custom(TagKind::custom("a"), vec![addr.clone()]), + Tag::custom(TagKind::custom("e"), vec![e3.id.to_hex()]), + ]) + .sign_with_keys(&maintainer) + .expect("Failed to create e4"); + send_event_and_get_response(&client, &e4) + .await + .expect("Failed to send e4"); + + let e5 = EventBuilder::new(Kind::from(1617), "Event 5") + .tags(vec![ + Tag::custom(TagKind::custom("a"), vec![addr.clone()]), + Tag::custom(TagKind::custom("e"), vec![e4.id.to_hex()]), + ]) + .sign_with_keys(&maintainer) + .expect("Failed to create e5"); + send_event_and_get_response(&client, &e5) + .await + .expect("Failed to send e5"); + + sleep(Duration::from_millis(200)).await; + + // Delete announcement + let deletion = create_deletion_event(&maintainer, &[], &[addr.clone()]) + .expect("Failed to create deletion"); + send_event_and_get_response(&client, &deletion) + .await + .expect("Failed to send deletion"); + + sleep(Duration::from_secs(10)).await; + + // All events should be deleted (no alternative anchor) + + relay.stop().await; +} + +/// Test: Multiple events with different maintainer subsets +/// +/// Scenario: +/// - 3 maintainers (A, B, C) +/// - event1 references A+B +/// - event2 references B+C +/// - event3 references A+C +/// - Delete B +/// - All events should be kept (each has alternative) +#[tokio::test] +async fn test_multiple_events_different_maintainer_subsets() { + let relay = TestRelay::start().await; + + let maintainer_a = Keys::generate(); + let maintainer_b = Keys::generate(); + let maintainer_c = Keys::generate(); + + let client = Client::default(); + client + .add_relay(relay.url()) + .await + .expect("Failed to add relay"); + client.connect().await; + + let identifier = "subset-repo"; + let announcement_a = create_repo_announcement(&maintainer_a, &[&relay.domain()], identifier); + let announcement_b = create_repo_announcement(&maintainer_b, &[&relay.domain()], identifier); + let announcement_c = create_repo_announcement(&maintainer_c, &[&relay.domain()], identifier); + let addr_a = build_announcement_address(&announcement_a); + let addr_b = build_announcement_address(&announcement_b); + let addr_c = build_announcement_address(&announcement_c); + + send_event_and_get_response(&client, &announcement_a) + .await + .expect("Failed to send announcement_a"); + send_event_and_get_response(&client, &announcement_b) + .await + .expect("Failed to send announcement_b"); + send_event_and_get_response(&client, &announcement_c) + .await + .expect("Failed to send announcement_c"); + + // event1 references A+B + let event1 = create_patch_event( + &maintainer_a, + "Event A+B", + &[addr_a.clone(), addr_b.clone()], + ) + .expect("Failed to create event1"); + send_event_and_get_response(&client, &event1) + .await + .expect("Failed to send event1"); + + // event2 references B+C + let event2 = create_patch_event( + &maintainer_b, + "Event B+C", + &[addr_b.clone(), addr_c.clone()], + ) + .expect("Failed to create event2"); + send_event_and_get_response(&client, &event2) + .await + .expect("Failed to send event2"); + + // event3 references A+C + let event3 = create_patch_event( + &maintainer_c, + "Event A+C", + &[addr_a.clone(), addr_c.clone()], + ) + .expect("Failed to create event3"); + send_event_and_get_response(&client, &event3) + .await + .expect("Failed to send event3"); + + sleep(Duration::from_millis(200)).await; + + // Delete B + let deletion_b = create_deletion_event(&maintainer_b, &[], &[addr_b.clone()]) + .expect("Failed to create deletion_b"); + send_event_and_get_response(&client, &deletion_b) + .await + .expect("Failed to send deletion_b"); + + sleep(Duration::from_millis(500)).await; + + // All events should be kept (each has alternative maintainer) + + relay.stop().await; +}