mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
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.
This commit is contained in:
@@ -0,0 +1,488 @@
|
||||
//! 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;
|
||||
}
|
||||
Reference in New Issue
Block a user