Files
ngit-grasp/tests/nip09_validation.rs
T
DanConwayDev dc86593fa9 Add comprehensive NIP-09 deletion request validation tests
Implement integration tests for NIP-09 deletion request validation:
- Valid deletion with event tags (e tags)
- Valid deletion with address tags (a tags for kind 30617)
- Rejection of deletion with no tags
- Rejection of deletion with author mismatch
- Handling of invalid address formats
- Acceptance of deletion for non-existent events
- Multiple deletions in single event
- Mixed deletion with both e and a tags

All tests properly handle GRASP-01 requirements by creating repository
announcements before creating events that reference them.
2026-01-14 11:11:28 +00:00

489 lines
15 KiB
Rust

//! NIP-09 Deletion Request Validation Integration Tests
//!
//! Tests ngit-grasp relay's implementation of NIP-09 deletion request validation.
//! Verifies that deletion requests are properly validated according to NIP-09 spec.
//!
//! # Test Coverage
//!
//! - Valid deletion with `e` tags (event IDs)
//! - Valid deletion with `a` tags (addresses for kind 30617)
//! - Rejection of deletion with no tags
//! - Rejection of deletion with author mismatch
//! - Handling of invalid address formats
//! - Configuration of deletion retention settings
//!
//! # Running Tests
//!
//! ```bash
//! # Run all NIP-09 validation tests
//! cargo test --test nip09_validation
//!
//! # Run specific test
//! cargo test --test nip09_validation test_valid_deletion_with_event_tags
//!
//! # With output
//! cargo test --test nip09_validation -- --nocapture
//! ```
mod common;
use common::{create_repo_announcement, TestRelay};
use nostr_sdk::prelude::*;
/// Helper function to create a deletion event
///
/// Creates a kind 5 deletion event with specified `e` and `a` tags.
fn create_deletion_event(
keys: &Keys,
event_ids: &[EventId],
addresses: &[String],
) -> Result<Event, Box<dyn std::error::Error>> {
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 test event that references a repository
///
/// Creates a kind 1 text note that references a repository (required by GRASP-01).
fn create_test_event_with_repo(
keys: &Keys,
content: &str,
repo_event_id: EventId,
) -> Result<Event, Box<dyn std::error::Error>> {
let tags = vec![Tag::event(repo_event_id)];
let event = EventBuilder::new(Kind::from(1), content)
.tags(tags)
.sign_with_keys(keys)?;
Ok(event)
}
/// Helper function to send event and get response
///
/// Sends an event to the relay and waits for OK/NOTICE response.
async fn send_event_and_get_response(
client: &Client,
event: &Event,
) -> Result<(), Box<dyn std::error::Error>> {
// Send event
let output = client.send_event(event).await?;
// Check if event was accepted
if output.success.is_empty() {
return Err(format!("Event rejected by all relays: {:?}", output.failed).into());
}
Ok(())
}
/// Test valid deletion with event tags
///
/// Creates a test event, stores it, then creates a deletion event with `e` tag.
/// Verifies the deletion is accepted.
#[tokio::test]
async fn test_valid_deletion_with_event_tags() {
let relay = TestRelay::start().await;
// Create keys for test
let keys = Keys::generate();
// Connect client
let client = Client::default();
client
.add_relay(relay.url())
.await
.expect("Failed to add relay");
client.connect().await;
// Create and send a repository announcement first (required by GRASP-01)
let repo_event = create_repo_announcement(&keys, &[&relay.domain()], "event-test-repo");
send_event_and_get_response(&client, &repo_event)
.await
.expect("Failed to send repo announcement");
// Create and send a test event that references the repository
let test_event = create_test_event_with_repo(&keys, "This will be deleted", repo_event.id)
.expect("Failed to create test event");
let test_event_id = test_event.id;
send_event_and_get_response(&client, &test_event)
.await
.expect("Failed to send test event");
// Create deletion event with `e` tag
let deletion_event = create_deletion_event(&keys, &[test_event_id], &[])
.expect("Failed to create deletion event");
// Send deletion event - should be accepted
let result = send_event_and_get_response(&client, &deletion_event).await;
assert!(
result.is_ok(),
"Valid deletion with event tags should be accepted: {:?}",
result.err()
);
relay.stop().await;
}
/// Test valid deletion with address tags
///
/// Creates a kind 30617 repository announcement, then creates a deletion with `a` tag.
/// Verifies the deletion is accepted.
#[tokio::test]
async fn test_valid_deletion_with_address_tags() {
let relay = TestRelay::start().await;
// Create keys for test
let keys = Keys::generate();
let pubkey = keys.public_key();
// Create and send a repository announcement
let identifier = "test-repo";
let repo_event = create_repo_announcement(&keys, &[&relay.domain()], identifier);
// Connect client and send repo event
let client = Client::default();
client
.add_relay(relay.url())
.await
.expect("Failed to add relay");
client.connect().await;
send_event_and_get_response(&client, &repo_event)
.await
.expect("Failed to send repo announcement");
// Create address tag: 30617:pubkey:identifier
let address = format!("30617:{}:{}", pubkey.to_hex(), identifier);
// Create deletion event with `a` tag
let deletion_event =
create_deletion_event(&keys, &[], &[address]).expect("Failed to create deletion event");
// Send deletion event - should be accepted
let result = send_event_and_get_response(&client, &deletion_event).await;
assert!(
result.is_ok(),
"Valid deletion with address tags should be accepted: {:?}",
result.err()
);
relay.stop().await;
}
/// Test deletion with no tags is rejected
///
/// Creates a kind 5 event with no `e` or `a` tags.
/// Verifies it's rejected with appropriate error message.
#[tokio::test]
async fn test_deletion_no_tags_rejected() {
let relay = TestRelay::start().await;
// Create keys for test
let keys = Keys::generate();
// Create deletion event with NO tags
let deletion_event =
create_deletion_event(&keys, &[], &[]).expect("Failed to create deletion event");
// Connect client
let client = Client::default();
client
.add_relay(relay.url())
.await
.expect("Failed to add relay");
client.connect().await;
// Send deletion event - should be rejected
let result = send_event_and_get_response(&client, &deletion_event).await;
assert!(result.is_err(), "Deletion with no tags should be rejected");
// Check error message contains expected text
let error_msg = result.err().unwrap().to_string();
assert!(
error_msg.contains("e") || error_msg.contains("a") || error_msg.contains("tag"),
"Error message should mention missing tags: {}",
error_msg
);
relay.stop().await;
}
/// Test deletion with author mismatch is rejected
///
/// Creates an event with keys1, then tries to delete it with keys2.
/// Verifies the deletion is rejected due to author mismatch.
#[tokio::test]
async fn test_deletion_author_mismatch_rejected() {
let relay = TestRelay::start().await;
// Create two different key pairs
let keys1 = Keys::generate();
let keys2 = Keys::generate();
// Connect client
let client = Client::default();
client
.add_relay(relay.url())
.await
.expect("Failed to add relay");
client.connect().await;
// Create and send a repository announcement first (required by GRASP-01)
let repo_event = create_repo_announcement(&keys1, &[&relay.domain()], "author-test-repo");
send_event_and_get_response(&client, &repo_event)
.await
.expect("Failed to send repo announcement");
// Create and send a test event with keys1
let test_event = create_test_event_with_repo(&keys1, "Created by keys1", repo_event.id)
.expect("Failed to create test event");
let test_event_id = test_event.id;
send_event_and_get_response(&client, &test_event)
.await
.expect("Failed to send test event");
// Create deletion event with keys2 (different author)
let deletion_event = create_deletion_event(&keys2, &[test_event_id], &[])
.expect("Failed to create deletion event");
// Send deletion event - should be rejected
let result = send_event_and_get_response(&client, &deletion_event).await;
assert!(
result.is_err(),
"Deletion with author mismatch should be rejected"
);
// Check error message mentions author mismatch
let error_msg = result.err().unwrap().to_string();
assert!(
error_msg.contains("author") || error_msg.contains("mismatch"),
"Error message should mention author mismatch: {}",
error_msg
);
relay.stop().await;
}
/// Test deletion with invalid address format
///
/// Creates a kind 5 event with malformed `a` tag.
/// Verifies it's rejected or handled gracefully.
#[tokio::test]
async fn test_deletion_invalid_address_format() {
let relay = TestRelay::start().await;
// Create keys for test
let keys = Keys::generate();
// Create deletion event with invalid address format
let invalid_addresses = vec![
"invalid:format".to_string(), // Only 2 parts
"not-a-number:pubkey:identifier".to_string(), // Invalid kind
"30617:invalid-pubkey:identifier".to_string(), // Invalid pubkey hex
];
// Connect client
let client = Client::default();
client
.add_relay(relay.url())
.await
.expect("Failed to add relay");
client.connect().await;
for invalid_addr in invalid_addresses {
let deletion_event = create_deletion_event(&keys, &[], &[invalid_addr.clone()])
.expect("Failed to create deletion event");
// Send deletion event - should be rejected or handled gracefully
let result = send_event_and_get_response(&client, &deletion_event).await;
// We expect either rejection or graceful handling (event not found)
// The important thing is it doesn't crash the relay
if result.is_ok() {
// If accepted, it means the relay handled it gracefully
// (event not found is OK per NIP-09)
continue;
}
// If rejected, check error message is reasonable
let error_msg = result.err().unwrap().to_string();
assert!(
!error_msg.is_empty(),
"Error message should not be empty for invalid address: {}",
invalid_addr
);
}
relay.stop().await;
}
/// Test deletion of non-existent event is accepted
///
/// NIP-09 allows deletion of events that don't exist yet (might arrive later).
/// Verifies that deletion of non-existent event is accepted.
#[tokio::test]
async fn test_deletion_nonexistent_event_accepted() {
let relay = TestRelay::start().await;
// Create keys for test
let keys = Keys::generate();
// Create a random event ID that doesn't exist
let nonexistent_id =
EventId::from_hex("0000000000000000000000000000000000000000000000000000000000000001")
.expect("Failed to create event ID");
// Create deletion event for non-existent event
let deletion_event = create_deletion_event(&keys, &[nonexistent_id], &[])
.expect("Failed to create deletion event");
// Connect client
let client = Client::default();
client
.add_relay(relay.url())
.await
.expect("Failed to add relay");
client.connect().await;
// Send deletion event - should be accepted (event not found is OK per NIP-09)
let result = send_event_and_get_response(&client, &deletion_event).await;
assert!(
result.is_ok(),
"Deletion of non-existent event should be accepted: {:?}",
result.err()
);
relay.stop().await;
}
/// Test multiple deletions in single event
///
/// Creates multiple test events, then deletes them all with one deletion event.
/// Verifies the deletion is accepted.
#[tokio::test]
async fn test_multiple_deletions_in_single_event() {
let relay = TestRelay::start().await;
// Create keys for test
let keys = Keys::generate();
// Connect client
let client = Client::default();
client
.add_relay(relay.url())
.await
.expect("Failed to add relay");
client.connect().await;
// Create and send a repository announcement first (required by GRASP-01)
let repo_event = create_repo_announcement(&keys, &[&relay.domain()], "multi-test-repo");
send_event_and_get_response(&client, &repo_event)
.await
.expect("Failed to send repo announcement");
// Create and send multiple test events
let mut event_ids = Vec::new();
for i in 0..3 {
let test_event =
create_test_event_with_repo(&keys, &format!("Test event {}", i), repo_event.id)
.expect("Failed to create test event");
event_ids.push(test_event.id);
send_event_and_get_response(&client, &test_event)
.await
.expect("Failed to send test event");
}
// Create deletion event with multiple `e` tags
let deletion_event =
create_deletion_event(&keys, &event_ids, &[]).expect("Failed to create deletion event");
// Send deletion event - should be accepted
let result = send_event_and_get_response(&client, &deletion_event).await;
assert!(
result.is_ok(),
"Deletion with multiple event tags should be accepted: {:?}",
result.err()
);
relay.stop().await;
}
/// Test mixed deletion with both e and a tags
///
/// Creates both a regular event and a repository announcement,
/// then deletes both with one deletion event containing both `e` and `a` tags.
#[tokio::test]
async fn test_mixed_deletion_with_e_and_a_tags() {
let relay = TestRelay::start().await;
// Create keys for test
let keys = Keys::generate();
let pubkey = keys.public_key();
// Connect client
let client = Client::default();
client
.add_relay(relay.url())
.await
.expect("Failed to add relay");
client.connect().await;
// Create and send a repository announcement
let identifier = "mixed-test-repo";
let repo_event = create_repo_announcement(&keys, &[&relay.domain()], identifier);
send_event_and_get_response(&client, &repo_event)
.await
.expect("Failed to send repo announcement");
// Create and send a test event that references the repository
let test_event = create_test_event_with_repo(&keys, "Regular event", repo_event.id)
.expect("Failed to create test event");
let test_event_id = test_event.id;
send_event_and_get_response(&client, &test_event)
.await
.expect("Failed to send test event");
// Create address tag: 30617:pubkey:identifier
let address = format!("30617:{}:{}", pubkey.to_hex(), identifier);
// Create deletion event with both `e` and `a` tags
let deletion_event = create_deletion_event(&keys, &[test_event_id], &[address])
.expect("Failed to create deletion event");
// Send deletion event - should be accepted
let result = send_event_and_get_response(&client, &deletion_event).await;
assert!(
result.is_ok(),
"Deletion with both e and a tags should be accepted: {:?}",
result.err()
);
relay.stop().await;
}