refactor: isolate each grasp-audit lib test with minimal boilerplate

- Add isolated_test! macro pattern to nip34_announcements.rs and nip01_compliance.rs
- Each test runs with its own fresh relay instance for complete isolation
- Make all individual test functions public in grasp-audit library (nip01_smoke.rs, event_acceptance_policy.rs)
- Eliminates 122 lines of boilerplate across integration tests
- Tests: 15 GRASP-01 event acceptance policy tests + 6 NIP-01 smoke tests
- Ensures tests don't interfere with each other, preventing flakiness
This commit is contained in:
DanConwayDev
2025-11-20 22:20:28 +00:00
parent ca50f5b98f
commit 519fdc6693
4 changed files with 99 additions and 221 deletions
@@ -143,7 +143,7 @@ impl EventAcceptancePolicyTests {
/// **Using TestContext pattern:**
/// - In CI mode: Creates fresh repo for full isolation
/// - In Production mode: Reuses cached repo to minimize events
async fn test_accept_valid_repo_announcement(client: &AuditClient) -> TestResult {
pub async fn test_accept_valid_repo_announcement(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_valid_repo_announcement",
"GRASP-01:nostr-relay:3-5",
@@ -246,7 +246,7 @@ impl EventAcceptancePolicyTests {
///
/// Spec: Line 5 of ../grasp/01.md
/// Requirement: MUST reject announcements not listing service (unless GRASP-05)
async fn test_reject_repo_announcement_missing_clone_tag(client: &AuditClient) -> TestResult {
pub async fn test_reject_repo_announcement_missing_clone_tag(client: &AuditClient) -> TestResult {
TestResult::new(
"reject_repo_announcement_missing_clone_tag",
"GRASP-01:nostr-relay:5",
@@ -320,7 +320,7 @@ impl EventAcceptancePolicyTests {
///
/// Spec: Line 5 of ../grasp/01.md
/// Requirement: MUST reject announcements not listing service in relays
async fn test_reject_repo_announcement_missing_relays_tag(client: &AuditClient) -> TestResult {
pub async fn test_reject_repo_announcement_missing_relays_tag(client: &AuditClient) -> TestResult {
TestResult::new(
"reject_repo_announcement_missing_relays_tag",
"GRASP-01:nostr-relay:5",
@@ -412,7 +412,7 @@ impl EventAcceptancePolicyTests {
/// This test demonstrates the new TestContext pattern:
/// - In CI mode: Creates fresh repo for full isolation
/// - In Production mode: Reuses cached repo to minimize events
async fn test_accept_valid_repo_state_announcement(client: &AuditClient) -> TestResult {
pub async fn test_accept_valid_repo_state_announcement(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_valid_repo_state_announcement",
"GRASP-01:nostr-relay:6-7",
@@ -579,7 +579,7 @@ impl EventAcceptancePolicyTests {
///
/// **EXAMPLE: Using TestContext for prerequisite events**
/// Demonstrates how TestContext simplifies test setup while supporting dual modes
async fn test_accept_issue_via_a_tag(client: &AuditClient) -> TestResult {
pub async fn test_accept_issue_via_a_tag(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_issue_via_a_tag",
"GRASP-01:event-acceptance:1.1",
@@ -614,7 +614,7 @@ impl EventAcceptancePolicyTests {
/// **Using TestContext pattern:**
/// - In CI mode: Creates fresh repo for full isolation
/// - In Production mode: Reuses cached repo to minimize events
async fn test_accept_comment_via_capital_a_tag(client: &AuditClient) -> TestResult {
pub async fn test_accept_comment_via_capital_a_tag(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_comment_via_A_tag",
"GRASP-01:event-acceptance:1.2",
@@ -666,7 +666,7 @@ impl EventAcceptancePolicyTests {
/// **Using TestContext pattern:**
/// - In CI mode: Creates fresh repo for full isolation
/// - In Production mode: Reuses cached repo to minimize events
async fn test_accept_kind1_via_q_tag(client: &AuditClient) -> TestResult {
pub async fn test_accept_kind1_via_q_tag(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_kind1_via_q_tag",
"GRASP-01:event-acceptance:1.3",
@@ -715,7 +715,7 @@ impl EventAcceptancePolicyTests {
/// **Using TestContext pattern:**
/// - In CI mode: Creates fresh repo+issue for full isolation
/// - In Production mode: Reuses cached repo+issue to minimize events
async fn test_accept_issue_quoting_issue_via_q(client: &AuditClient) -> TestResult {
pub async fn test_accept_issue_quoting_issue_via_q(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_issue_quoting_issue_via_q",
"GRASP-01:event-acceptance:2.1",
@@ -761,7 +761,7 @@ impl EventAcceptancePolicyTests {
/// **Using TestContext pattern:**
/// - In CI mode: Creates fresh repo+issue for full isolation
/// - In Production mode: Reuses cached repo+issue to minimize events
async fn test_accept_comment_via_capital_e_tag(client: &AuditClient) -> TestResult {
pub async fn test_accept_comment_via_capital_e_tag(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_comment_via_E_tag",
"GRASP-01:event-acceptance:2.2",
@@ -799,7 +799,7 @@ impl EventAcceptancePolicyTests {
/// **Using TestContext pattern:**
/// - In CI mode: Creates fresh repo for full isolation
/// - In Production mode: Reuses cached repo to minimize events
async fn test_accept_kind1_via_e_tag(client: &AuditClient) -> TestResult {
pub async fn test_accept_kind1_via_e_tag(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_kind1_via_e_tag",
"GRASP-01:event-acceptance:2.3",
@@ -859,7 +859,7 @@ impl EventAcceptancePolicyTests {
/// **Using TestContext pattern:**
/// - In CI mode: Creates fresh repo for full isolation
/// - In Production mode: Reuses cached repo to minimize events
async fn test_accept_kind1_referenced_in_issue(client: &AuditClient) -> TestResult {
pub async fn test_accept_kind1_referenced_in_issue(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_kind1_referenced_in_issue",
"GRASP-01:event-acceptance:3.1",
@@ -928,7 +928,7 @@ impl EventAcceptancePolicyTests {
/// **Using TestContext pattern:**
/// - In CI mode: Creates fresh repo+issue for full isolation
/// - In Production mode: Reuses cached repo+issue to minimize events
async fn test_accept_comment_referenced_in_comment(client: &AuditClient) -> TestResult {
pub async fn test_accept_comment_referenced_in_comment(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_comment_referenced_in_comment",
"GRASP-01:event-acceptance:3.2",
@@ -1010,7 +1010,7 @@ impl EventAcceptancePolicyTests {
/// **Using TestContext pattern:**
/// - In CI mode: Creates fresh repo for full isolation
/// - In Production mode: Reuses cached repo to minimize events
async fn test_accept_kind1_referenced_in_kind1(client: &AuditClient) -> TestResult {
pub async fn test_accept_kind1_referenced_in_kind1(client: &AuditClient) -> TestResult {
TestResult::new(
"accept_kind1_referenced_in_kind1",
"GRASP-01:event-acceptance:3.3",
@@ -1070,7 +1070,7 @@ impl EventAcceptancePolicyTests {
// ============================================================
/// Test 4.1: Issue referencing unaccepted repo should be rejected
async fn test_reject_orphan_issue(client: &AuditClient) -> TestResult {
pub async fn test_reject_orphan_issue(client: &AuditClient) -> TestResult {
TestResult::new(
"reject_orphan_issue",
"GRASP-01:event-acceptance:4.1",
@@ -1098,7 +1098,7 @@ impl EventAcceptancePolicyTests {
}
/// Test 4.2: Generic kind 1 note with no repo references should be rejected
async fn test_reject_orphan_kind1(client: &AuditClient) -> TestResult {
pub async fn test_reject_orphan_kind1(client: &AuditClient) -> TestResult {
TestResult::new(
"reject_orphan_kind1",
"GRASP-01:event-acceptance:4.2",
@@ -1126,7 +1126,7 @@ impl EventAcceptancePolicyTests {
/// - In CI mode: Creates fresh accepted repo for full isolation
/// - In Production mode: Reuses cached accepted repo to minimize events
/// - Note: Unaccepted repo B is always created fresh (not cached) since it must remain unaccepted
async fn test_reject_comment_quoting_other_repo(client: &AuditClient) -> TestResult {
pub async fn test_reject_comment_quoting_other_repo(client: &AuditClient) -> TestResult {
TestResult::new(
"reject_comment_quoting_other_repo",
"GRASP-01:event-acceptance:4.3",
+6 -6
View File
@@ -29,7 +29,7 @@ impl Nip01SmokeTests {
///
/// Spec: NIP-01 basic requirement
/// Requirement: MUST serve a relay at / via WebSocket
async fn test_websocket_connection(client: &AuditClient) -> TestResult {
pub async fn test_websocket_connection(client: &AuditClient) -> TestResult {
TestResult::new(
"websocket_connection",
"NIP-01:basic",
@@ -52,7 +52,7 @@ impl Nip01SmokeTests {
///
/// For GRASP servers, we send a NIP-34 repository announcement that lists
/// the GRASP server in clone and relays tags (required for acceptance).
async fn test_send_receive_event(client: &AuditClient) -> TestResult {
pub async fn test_send_receive_event(client: &AuditClient) -> TestResult {
TestResult::new(
"send_receive_event",
"NIP-01:event-message",
@@ -123,7 +123,7 @@ impl Nip01SmokeTests {
///
/// Spec: NIP-01 REQ message
/// Requirement: Relay MUST support REQ subscriptions
async fn test_create_subscription(client: &AuditClient) -> TestResult {
pub async fn test_create_subscription(client: &AuditClient) -> TestResult {
TestResult::new(
"create_subscription",
"NIP-01:req-message",
@@ -165,7 +165,7 @@ impl Nip01SmokeTests {
///
/// Spec: NIP-01 CLOSE message
/// Requirement: Relay MUST support CLOSE to end subscriptions
async fn test_close_subscription(client: &AuditClient) -> TestResult {
pub async fn test_close_subscription(client: &AuditClient) -> TestResult {
TestResult::new(
"close_subscription",
"NIP-01:close-message",
@@ -193,7 +193,7 @@ impl Nip01SmokeTests {
///
/// Spec: NIP-01 event validation
/// Requirement: Relay MUST reject events with invalid signatures
async fn test_reject_invalid_signature(client: &AuditClient) -> TestResult {
pub async fn test_reject_invalid_signature(client: &AuditClient) -> TestResult {
TestResult::new(
"reject_invalid_signature",
"NIP-01:validation",
@@ -247,7 +247,7 @@ impl Nip01SmokeTests {
///
/// Spec: NIP-01 event ID validation
/// Requirement: Relay MUST reject events where ID doesn't match hash
async fn test_reject_invalid_event_id(client: &AuditClient) -> TestResult {
pub async fn test_reject_invalid_event_id(client: &AuditClient) -> TestResult {
TestResult::new(
"reject_invalid_event_id",
"NIP-01:validation",
+34 -80
View File
@@ -1,13 +1,13 @@
//! NIP-01 Compliance Integration Tests
//!
//! Tests ngit-grasp relay's NIP-01 compliance using grasp-audit library.
//! Avoids code duplication by delegating to grasp-audit's test suite.
//! Uses isolated test pattern for complete test independence.
//!
//! # Test Strategy
//!
//! - Uses TestRelay fixture for ngit-grasp relay lifecycle management
//! - Uses grasp-audit's Nip01SmokeTests for actual test logic
//! - Minimal duplication - single source of truth in grasp-audit
//! - Each test runs in complete isolation with its own fresh relay instance
//! - Uses macro to eliminate boilerplate while maintaining test isolation
//! - Calls individual test methods from grasp-audit for minimal duplication
//!
//! # Running Tests
//!
@@ -16,7 +16,7 @@
//! cargo test --test nip01_compliance
//!
//! # Run specific test
//! cargo test --test nip01_compliance test_nip01_smoke
//! cargo test --test nip01_compliance test_websocket_connection
//!
//! # With output
//! cargo test --test nip01_compliance -- --nocapture
@@ -27,87 +27,41 @@ mod common;
use common::TestRelay;
use grasp_audit::*;
/// Test NIP-01 smoke tests against ngit-grasp relay
/// Macro to generate isolated integration tests
///
/// This test runs all NIP-01 smoke tests from grasp-audit against
/// the ngit-grasp relay implementation.
///
/// Tests cover:
/// - WebSocket connection
/// - Event send/receive
/// - Subscriptions (REQ/CLOSE)
/// - Event validation (signature, ID)
#[tokio::test]
async fn test_nip01_smoke() {
// Start test relay
let relay = TestRelay::start().await;
/// Each test runs with its own fresh relay instance to ensure complete isolation.
/// This eliminates flakiness and ensures tests don't interfere with each other.
macro_rules! isolated_test {
($test_name:ident) => {
#[tokio::test]
async fn $test_name() {
let relay = TestRelay::start().await;
let config = AuditConfig::ci();
let client = AuditClient::new(relay.url(), config)
.await
.expect("Failed to create audit client");
// Create audit client in CI mode (isolated testing)
let config = AuditConfig::ci();
let client = AuditClient::new(relay.url(), config)
.await
.expect("Failed to create audit client");
let result = specs::Nip01SmokeTests::$test_name(&client).await;
// Run all NIP-01 smoke tests
let results = specs::Nip01SmokeTests::run_all(&client).await;
relay.stop().await;
// Print detailed report
results.print_report();
// Stop relay
relay.stop().await;
// Assert all tests passed
assert!(
results.all_passed(),
"NIP-01 smoke tests failed: {}/{} passed",
results.passed_count(),
results.total_count()
);
assert!(
result.passed,
"{} failed: {}",
stringify!($test_name),
result.error.as_deref().unwrap_or("unknown error")
);
}
};
}
/// Test that relay properly validates events
///
/// Critical security test - ensures relay validates:
/// - Event signatures
/// - Event IDs
/// - Other NIP-01 requirements
#[tokio::test]
async fn test_relay_validates_events() {
let relay = TestRelay::start().await;
let config = AuditConfig::ci();
let client = AuditClient::new(relay.url(), config)
.await
.expect("Failed to create audit client");
// Run smoke tests which include validation tests
let results = specs::Nip01SmokeTests::run_all(&client).await;
relay.stop().await;
// Filter to validation tests
let validation_tests: Vec<_> = results
.results
.iter()
.filter(|t| t.name.contains("reject") || t.name.contains("invalid"))
.collect();
// Should have validation tests
assert!(
!validation_tests.is_empty(),
"No validation tests found (these are critical for security)"
);
// All validation tests should pass
for test in validation_tests {
assert!(
test.passed,
"Validation test failed: {} - {}\nThis is a security issue!",
test.name,
test.error.as_deref().unwrap_or("unknown error")
);
}
}
// Generate isolated tests for all NIP-01 smoke tests
isolated_test!(test_websocket_connection);
isolated_test!(test_send_receive_event);
isolated_test!(test_create_subscription);
isolated_test!(test_close_subscription);
isolated_test!(test_reject_invalid_signature);
isolated_test!(test_reject_invalid_event_id);
/// Test relay lifecycle management
///
+43 -119
View File
@@ -5,9 +5,9 @@
//!
//! # Test Strategy
//!
//! - Uses TestRelay fixture for ngit-grasp relay lifecycle management
//! - Uses grasp-audit's EventAcceptancePolicyTests for actual test logic
//! - Minimal duplication - single source of truth in grasp-audit
//! - Each test runs in complete isolation with its own fresh relay instance
//! - Uses macro to eliminate boilerplate while maintaining test isolation
//! - Calls individual test methods from grasp-audit for minimal duplication
//!
//! # Running Tests
//!
@@ -16,7 +16,7 @@
//! cargo test --test nip34_announcements
//!
//! # Run specific test
//! cargo test --test nip34_announcements test_grasp01_event_acceptance
//! cargo test --test nip34_announcements test_reject_orphan_kind1
//!
//! # With output
//! cargo test --test nip34_announcements -- --nocapture
@@ -27,124 +27,48 @@ mod common;
use common::TestRelay;
use grasp_audit::*;
/// Test GRASP-01 event acceptance policy against ngit-grasp relay
/// Macro to generate isolated integration tests
///
/// This test runs all GRASP-01 event acceptance policy tests from grasp-audit
/// against the ngit-grasp relay implementation.
///
/// Tests cover:
/// - Repository announcement acceptance/rejection
/// - Repository state announcement acceptance
/// - Events tagging accepted repositories
/// - Transitive event acceptance (events tagging accepted events)
/// - Forward reference acceptance (events tagged by accepted events)
/// - Rejection of unrelated events
#[tokio::test]
async fn test_grasp01_event_acceptance() {
// Start test relay
let relay = TestRelay::start().await;
/// Each test runs with its own fresh relay instance to ensure complete isolation.
/// This eliminates rate-limiting issues and ensures tests don't interfere with each other.
macro_rules! isolated_test {
($test_name:ident) => {
#[tokio::test]
async fn $test_name() {
let relay = TestRelay::start().await;
let config = AuditConfig::ci();
let client = AuditClient::new(relay.url(), config)
.await
.expect("Failed to create audit client");
// Create audit client in CI mode (isolated testing)
let config = AuditConfig::ci();
let client = AuditClient::new(relay.url(), config)
.await
.expect("Failed to create audit client");
let result = specs::EventAcceptancePolicyTests::$test_name(&client).await;
// Run all GRASP-01 event acceptance policy tests
let results = specs::EventAcceptancePolicyTests::run_all(&client).await;
relay.stop().await;
// Print detailed report
results.print_report();
// Stop relay
relay.stop().await;
// Assert all tests passed
assert!(
results.all_passed(),
"GRASP-01 event acceptance tests failed: {}/{} passed",
results.passed_count(),
results.total_count()
);
assert!(
result.passed,
"{} failed: {}",
stringify!($test_name),
result.error.as_deref().unwrap_or("unknown error")
);
}
};
}
/// Test that relay accepts valid repository announcements
///
/// Demonstrates running individual test categories from the suite
#[tokio::test]
async fn test_accepts_repository_announcements() {
let relay = TestRelay::start().await;
let config = AuditConfig::ci();
let client = AuditClient::new(relay.url(), config)
.await
.expect("Failed to create audit client");
// Run all tests
let results = specs::EventAcceptancePolicyTests::run_all(&client).await;
relay.stop().await;
// Filter to only repository announcement tests
let announcement_tests: Vec<_> = results
.results
.iter()
.filter(|t| {
t.spec_ref.contains("repo") || t.name.contains("announcement") || t.name.contains("state")
})
.collect();
// Verify we have announcement tests
assert!(
!announcement_tests.is_empty(),
"No repository announcement tests found"
);
// All should pass
for test in announcement_tests {
assert!(
test.passed,
"Repository test failed: {} - {}",
test.name,
test.error.as_deref().unwrap_or("unknown error")
);
}
}
/// Test that relay properly validates clone and relays tags
///
/// This is a critical security requirement for GRASP-01
#[tokio::test]
async fn test_validates_service_tags() {
let relay = TestRelay::start().await;
let config = AuditConfig::ci();
let client = AuditClient::new(relay.url(), config)
.await
.expect("Failed to create audit client");
let results = specs::EventAcceptancePolicyTests::run_all(&client).await;
relay.stop().await;
// Filter to rejection tests (these verify tag validation)
let rejection_tests: Vec<_> = results
.results
.iter()
.filter(|t| t.name.contains("reject"))
.collect();
// Should have rejection tests
assert!(
!rejection_tests.is_empty(),
"No rejection tests found (these are critical for security)"
);
// All rejection tests should pass
for test in rejection_tests {
assert!(
test.passed,
"Rejection test failed: {} - {}\nThis is a security issue!",
test.name,
test.error.as_deref().unwrap_or("unknown error")
);
}
}
// Generate isolated tests for all GRASP-01 event acceptance policy tests
isolated_test!(test_accept_valid_repo_announcement);
isolated_test!(test_reject_repo_announcement_missing_clone_tag);
isolated_test!(test_reject_repo_announcement_missing_relays_tag);
isolated_test!(test_accept_valid_repo_state_announcement);
isolated_test!(test_accept_issue_via_a_tag);
isolated_test!(test_accept_comment_via_capital_a_tag);
isolated_test!(test_accept_kind1_via_q_tag);
isolated_test!(test_accept_issue_quoting_issue_via_q);
isolated_test!(test_accept_comment_via_capital_e_tag);
isolated_test!(test_accept_kind1_via_e_tag);
isolated_test!(test_accept_kind1_referenced_in_issue);
isolated_test!(test_accept_comment_referenced_in_comment);
isolated_test!(test_accept_kind1_referenced_in_kind1);
isolated_test!(test_reject_orphan_issue);
isolated_test!(test_reject_orphan_kind1);
isolated_test!(test_reject_comment_quoting_other_repo);