From ca50f5b98f30d0933a510c05db86b608afee73a0 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 20 Nov 2025 21:45:45 +0000 Subject: replace tests to use grasp-audit lib as much as possible --- tests/nip01_compliance.rs | 80 +++--- tests/nip34_announcements.rs | 580 ++++++++----------------------------------- 2 files changed, 125 insertions(+), 535 deletions(-) (limited to 'tests') diff --git a/tests/nip01_compliance.rs b/tests/nip01_compliance.rs index 05957fd..4cb2af4 100644 --- a/tests/nip01_compliance.rs +++ b/tests/nip01_compliance.rs @@ -1,14 +1,13 @@ //! NIP-01 Compliance Integration Tests //! -//! These tests verify that ngit-grasp relay implements NIP-01 correctly -//! by using the grasp-audit library to run compliance tests. +//! Tests ngit-grasp relay's NIP-01 compliance using grasp-audit library. +//! Avoids code duplication by delegating to grasp-audit's test suite. //! //! # Test Strategy //! -//! - Uses grasp-audit as a library (not CLI) -//! - Automatically manages relay lifecycle -//! - Reuses test specs from grasp-audit (single source of truth) -//! - Pure Rust, no shell scripts +//! - 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 //! //! # Running Tests //! @@ -30,17 +29,20 @@ use grasp_audit::*; /// Test NIP-01 smoke tests against ngit-grasp relay /// -/// This test: -/// 1. Starts a fresh ngit-grasp relay instance -/// 2. Runs all NIP-01 smoke tests from grasp-audit -/// 3. Verifies all tests pass -/// 4. Shuts down the relay +/// 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; - // Create audit client in CI mode (isolated, no cleanup needed) + // Create audit client in CI mode (isolated testing) let config = AuditConfig::ci(); let client = AuditClient::new(relay.url(), config) .await @@ -64,34 +66,12 @@ async fn test_nip01_smoke() { ); } -/// Test individual NIP-01 tests can be run separately -/// -/// This demonstrates that we can run individual tests from the specs -/// for more granular testing or debugging. -#[tokio::test] -async fn test_nip01_individual_tests() { - use grasp_audit::specs::grasp01::Nip01SmokeTests; - - let relay = TestRelay::start().await; - let config = AuditConfig::ci(); - let client = AuditClient::new(relay.url(), config) - .await - .expect("Failed to create audit client"); - - // We can't call private methods, so we'll run the full suite - // This test is mainly to show the pattern - let all_results = Nip01SmokeTests::run_all(&client).await; - - relay.stop().await; - - // Verify - assert!(all_results.all_passed()); -} - -/// Test that relay rejects invalid events +/// Test that relay properly validates events /// -/// This is a critical security test - we want to ensure the relay -/// properly validates events before accepting them. +/// 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; @@ -100,29 +80,29 @@ async fn test_relay_validates_events() { .await .expect("Failed to create audit client"); - // The validation tests are part of the smoke tests + // Run smoke tests which include validation tests let results = specs::Nip01SmokeTests::run_all(&client).await; - // Check that validation tests exist and pass + relay.stop().await; + + // Filter to validation tests let validation_tests: Vec<_> = results .results .iter() - .filter(|t| t.spec_ref.contains("validation")) + .filter(|t| t.name.contains("reject") || t.name.contains("invalid")) .collect(); - relay.stop().await; - // Should have validation tests assert!( !validation_tests.is_empty(), - "No validation tests found in NIP-01 smoke tests" + "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: {} - {}", + "Validation test failed: {} - {}\nThis is a security issue!", test.name, test.error.as_deref().unwrap_or("unknown error") ); @@ -131,7 +111,7 @@ async fn test_relay_validates_events() { /// Test relay lifecycle management /// -/// Ensures our test fixture properly manages relay lifecycle +/// Verifies TestRelay fixture properly manages relay lifecycle #[tokio::test] async fn test_relay_lifecycle() { // Start relay @@ -148,15 +128,11 @@ async fn test_relay_lifecycle() { // Stop relay relay.stop().await; - - // Note: We can't easily verify disconnection without modifying grasp-audit - // to expose connection state after relay shutdown. That's okay - the - // important part is that the relay starts and stops cleanly. } /// Test multiple relays can run in parallel /// -/// This ensures our random port selection works correctly +/// Ensures random port selection avoids conflicts #[tokio::test] async fn test_parallel_relays() { // Start two relays simultaneously diff --git a/tests/nip34_announcements.rs b/tests/nip34_announcements.rs index 535425d..f1cbd05 100644 --- a/tests/nip34_announcements.rs +++ b/tests/nip34_announcements.rs @@ -1,24 +1,22 @@ -//! NIP-34 Repository Announcements Integration Tests (GRASP-01) +//! GRASP-01 Repository Event Acceptance Integration Tests //! -//! Tests the acceptance and validation of repository announcements (kind 30617) -//! and repository state announcements (kind 30618) according to GRASP-01. -//! -//! Reference: GRASP-01, Lines 9-20 +//! Tests ngit-grasp relay's implementation of GRASP-01 repository event acceptance policy. +//! Uses grasp-audit library to avoid code duplication. //! //! # Test Strategy //! -//! - Uses TestRelay fixture for automatic relay lifecycle management -//! - Pure Rust, no shell scripts -//! - Tests run in parallel with isolated relay instances +//! - 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 //! //! # Running Tests //! //! ```bash -//! # Run all NIP-34 announcement tests +//! # Run all GRASP-01 tests //! cargo test --test nip34_announcements //! //! # Run specific test -//! cargo test --test nip34_announcements test_accepts_valid_announcement +//! cargo test --test nip34_announcements test_grasp01_event_acceptance //! //! # With output //! cargo test --test nip34_announcements -- --nocapture @@ -27,510 +25,126 @@ mod common; use common::TestRelay; -use futures_util::{SinkExt, StreamExt}; -use nostr_sdk::{EventBuilder, Keys, Kind, Tag, TagKind}; -use serde_json::{json, Value}; -use tokio_tungstenite::{connect_async, tungstenite::Message}; - -const KIND_REPOSITORY_ANNOUNCEMENT: u16 = 30617; -const KIND_REPOSITORY_STATE: u16 = 30618; - -/// Helper to connect to a test relay -async fn connect_to_relay( - url: &str, -) -> tokio_tungstenite::WebSocketStream> { - let (ws, _) = connect_async(url) - .await - .expect("Failed to connect to relay"); - ws -} - -/// Helper to create a repository announcement event -fn create_announcement( - keys: &Keys, - _domain: &str, - identifier: &str, - clone_urls: Vec, - relays: Vec, -) -> nostr_sdk::Event { - let mut tags = vec![Tag::custom(TagKind::d(), vec![identifier.to_string()])]; - - for url in clone_urls { - tags.push(Tag::custom(TagKind::Clone, vec![url])); - } - - for relay in relays { - tags.push(Tag::custom(TagKind::Relays, vec![relay])); - } - - EventBuilder::new( - Kind::from(KIND_REPOSITORY_ANNOUNCEMENT), - "Test repository description", - ) - .tags(tags) - .sign_with_keys(keys) - .expect("Failed to sign event") -} - -/// Helper to create a repository state event -fn create_state(keys: &Keys, identifier: &str, branches: Vec<(&str, &str)>) -> nostr_sdk::Event { - let mut tags = vec![Tag::custom(TagKind::d(), vec![identifier.to_string()])]; - - for (branch, commit) in branches { - tags.push(Tag::custom( - TagKind::Custom("ref".into()), - vec![format!("refs/heads/{}", branch), commit.to_string()], - )); - } - - EventBuilder::new(Kind::from(KIND_REPOSITORY_STATE), "") - .tags(tags) - .sign_with_keys(keys) - .expect("Failed to sign event") -} - -/// GRASP-01, Line 9-10: MUST serve a NIP-01 compliant nostr relay at `/` +use grasp_audit::*; + +/// Test GRASP-01 event acceptance policy against ngit-grasp relay +/// +/// 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_relay_accepts_connection() { +async fn test_grasp01_event_acceptance() { + // Start test relay let relay = TestRelay::start().await; - // Try to connect - let ws = connect_to_relay(relay.url()).await; - - drop(ws); // Clean disconnect -} - -/// GRASP-01, Line 11: MUST accept repository announcements (kind 30617) -#[tokio::test] -async fn test_accepts_valid_announcement() { - let relay = TestRelay::start().await; - let keys = Keys::generate(); - - let mut ws = connect_to_relay(relay.url()).await; - - let event = create_announcement( - &keys, - &relay.domain(), - "test-repo", - vec![format!("https://{}/alice/test-repo.git", relay.domain())], - vec![format!("wss://{}", relay.domain())], - ); - - // Send event - let event_msg = json!(["EVENT", event]); - ws.send(Message::Text(event_msg.to_string().into())) + // Create audit client in CI mode (isolated testing) + let config = AuditConfig::ci(); + let client = AuditClient::new(relay.url(), config) .await - .expect("Failed to send event"); + .expect("Failed to create audit client"); - // Read response - if let Some(Ok(Message::Text(text))) = ws.next().await { - let response: Value = serde_json::from_str(&text).expect("Failed to parse response"); + // Run all GRASP-01 event acceptance policy tests + let results = specs::EventAcceptancePolicyTests::run_all(&client).await; - // Should be ["OK", event_id, true, ""] - assert_eq!(response[0], "OK"); - assert_eq!(response[1], event.id.to_hex()); - if response[2] != true { - eprintln!("Event rejected: {}", response[3]); - } - assert_eq!(response[2], true, "Event should be accepted"); - } else { - panic!("No response received"); - } -} - -/// GRASP-01, Line 12-13: MUST reject announcements that do not list the service -/// in both `clone` and `relays` tags -#[tokio::test] -async fn test_rejects_announcement_without_clone() { - let relay = TestRelay::start().await; - let keys = Keys::generate(); + // Print detailed report + results.print_report(); - let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect"); + // Stop relay + relay.stop().await; - // Missing clone tag - let event = create_announcement( - &keys, - &relay.domain(), - "test-repo", - vec![], // No clone URLs - vec![format!("wss://{}", relay.domain())], + // Assert all tests passed + assert!( + results.all_passed(), + "GRASP-01 event acceptance tests failed: {}/{} passed", + results.passed_count(), + results.total_count() ); - - let event_msg = json!(["EVENT", event]); - ws.send(Message::Text(event_msg.to_string().into())) - .await - .expect("Failed to send event"); - - if let Some(Ok(Message::Text(text))) = ws.next().await { - let response: Value = serde_json::from_str(&text).expect("Failed to parse"); - - // Should be rejected - assert_eq!(response[0], "OK"); - assert_eq!(response[1], event.id.to_hex()); - assert_eq!(response[2], false, "Event should be rejected"); - - let message = response[3].as_str().unwrap(); - assert!( - message.contains("clone") || message.contains("invalid"), - "Error message should mention clone requirement: {}", - message - ); - } else { - panic!("No response received"); - } -} - -/// GRASP-01, Line 12-13: MUST reject announcements that do not list the service -/// in both `clone` and `relays` tags -#[tokio::test] -async fn test_rejects_announcement_without_relay() { - let relay = TestRelay::start().await; - let keys = Keys::generate(); - - let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect"); - - // Missing relay tag - let event = create_announcement( - &keys, - &relay.domain(), - "test-repo", - vec![format!("https://{}/alice/test-repo.git", relay.domain())], - vec![], // No relays - ); - - let event_msg = json!(["EVENT", event]); - ws.send(Message::Text(event_msg.to_string().into())) - .await - .expect("Failed to send event"); - - if let Some(Ok(Message::Text(text))) = ws.next().await { - let response: Value = serde_json::from_str(&text).expect("Failed to parse"); - - // Should be rejected - assert_eq!(response[0], "OK"); - assert_eq!(response[1], event.id.to_hex()); - assert_eq!(response[2], false, "Event should be rejected"); - - let message = response[3].as_str().unwrap(); - assert!( - message.contains("relays") || message.contains("invalid"), - "Error message should mention relay requirement: {}", - message - ); - } else { - panic!("No response received"); - } } -/// GRASP-01, Line 12-13: MUST reject announcements listing other services +/// Test that relay accepts valid repository announcements +/// +/// Demonstrates running individual test categories from the suite #[tokio::test] -async fn test_rejects_announcement_for_other_service() { +async fn test_accepts_repository_announcements() { let relay = TestRelay::start().await; - let keys = Keys::generate(); - - let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect"); - - // Lists different service - let event = create_announcement( - &keys, - &relay.domain(), - "test-repo", - vec!["https://other-service.com/alice/test-repo.git".to_string()], - vec!["wss://other-service.com".to_string()], - ); - - let event_msg = json!(["EVENT", event]); - ws.send(Message::Text(event_msg.to_string().into())) + let config = AuditConfig::ci(); + let client = AuditClient::new(relay.url(), config) .await - .expect("Failed to send event"); - - if let Some(Ok(Message::Text(text))) = ws.next().await { - let response: Value = serde_json::from_str(&text).expect("Failed to parse"); - - // Should be rejected - assert_eq!(response[0], "OK"); - assert_eq!(response[1], event.id.to_hex()); - assert_eq!(response[2], false, "Event should be rejected"); - } else { - panic!("No response received"); - } -} - -/// GRASP-01, Line 11: MUST accept repository state announcements (kind 30618) -#[tokio::test] -async fn test_accepts_valid_state() { - let relay = TestRelay::start().await; - let keys = Keys::generate(); - - let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect"); - - let event = create_state( - &keys, - "test-repo", - vec![("main", "a1b2c3d4e5f6789012345678901234567890abcd")], + .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" ); - let event_msg = json!(["EVENT", event]); - ws.send(Message::Text(event_msg.to_string().into())) - .await - .expect("Failed to send event"); - - if let Some(Ok(Message::Text(text))) = ws.next().await { - let response: Value = serde_json::from_str(&text).expect("Failed to parse"); - - // Should be accepted - assert_eq!(response[0], "OK"); - assert_eq!(response[1], event.id.to_hex()); - assert_eq!(response[2], true, "State event should be accepted"); - } else { - panic!("No response received"); - } -} - -/// Test state event with multiple branches -#[tokio::test] -async fn test_accepts_state_with_multiple_branches() { - let relay = TestRelay::start().await; - let keys = Keys::generate(); - - let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect"); - - let event = create_state( - &keys, - "test-repo", - vec![ - ("main", "a1b2c3d4e5f6789012345678901234567890abcd"), - ("develop", "b2c3d4e5f6789012345678901234567890abcde"), - ("feature-x", "c3d4e5f6789012345678901234567890abcdef1"), - ], - ); - - let event_msg = json!(["EVENT", event]); - ws.send(Message::Text(event_msg.to_string().into())) - .await - .expect("Failed to send event"); - - if let Some(Ok(Message::Text(text))) = ws.next().await { - let response: Value = serde_json::from_str(&text).expect("Failed to parse"); - - assert_eq!(response[0], "OK"); - assert_eq!(response[2], true, "State event should be accepted"); - } else { - panic!("No response received"); - } -} - -/// Test state event without identifier should be rejected -#[tokio::test] -async fn test_rejects_state_without_identifier() { - let relay = TestRelay::start().await; - let keys = Keys::generate(); - - let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect"); - - // Create state without identifier - let event = EventBuilder::new(Kind::from(KIND_REPOSITORY_STATE), "") - .sign_with_keys(&keys) - .expect("Failed to sign event"); - - let event_msg = json!(["EVENT", event]); - ws.send(Message::Text(event_msg.to_string().into())) - .await - .expect("Failed to send event"); - - if let Some(Ok(Message::Text(text))) = ws.next().await { - let response: Value = serde_json::from_str(&text).expect("Failed to parse"); - - // Should be rejected - assert_eq!(response[0], "OK"); - assert_eq!(response[1], event.id.to_hex()); - assert_eq!(response[2], false, "Event should be rejected"); - - let message = response[3].as_str().unwrap(); + // All should pass + for test in announcement_tests { assert!( - message.contains("identifier") || message.contains("invalid"), - "Error message should mention identifier requirement: {}", - message + test.passed, + "Repository test failed: {} - {}", + test.name, + test.error.as_deref().unwrap_or("unknown error") ); - } else { - panic!("No response received"); - } -} - -/// Test querying for announcements -#[tokio::test] -async fn test_query_announcements() { - let relay = TestRelay::start().await; - let keys = Keys::generate(); - - let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect"); - - // Send an announcement - let event = create_announcement( - &keys, - &relay.domain(), - "query-test-repo", - vec![format!( - "https://{}/alice/query-test-repo.git", - relay.domain() - )], - vec![format!("wss://{}", relay.domain())], - ); - - let event_msg = json!(["EVENT", event]); - ws.send(Message::Text(event_msg.to_string().into())) - .await - .expect("Failed to send event"); - - // Wait for OK response - if let Some(Ok(Message::Text(_))) = ws.next().await { - // Got OK response } - - // Query for announcements - let req = json!([ - "REQ", - "test-sub", - { - "kinds": [KIND_REPOSITORY_ANNOUNCEMENT], - "authors": [keys.public_key().to_hex()] - } - ]); - - ws.send(Message::Text(req.to_string().into())) - .await - .expect("Failed to send REQ"); - - // Read responses - let mut found_event = false; - let mut got_eose = false; - - for _ in 0..10 { - if let Some(Ok(Message::Text(text))) = ws.next().await { - let response: Value = serde_json::from_str(&text).expect("Failed to parse"); - - if response[0] == "EVENT" { - assert_eq!(response[1], "test-sub"); - found_event = true; - } else if response[0] == "EOSE" { - assert_eq!(response[1], "test-sub"); - got_eose = true; - break; - } - } - } - - assert!(found_event, "Should have received the announcement"); - assert!(got_eose, "Should have received EOSE"); } -/// Test querying for state events +/// Test that relay properly validates clone and relays tags +/// +/// This is a critical security requirement for GRASP-01 #[tokio::test] -async fn test_query_states() { +async fn test_validates_service_tags() { let relay = TestRelay::start().await; - let keys = Keys::generate(); - - let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect"); - - // Send a state event - let event = create_state( - &keys, - "query-test-repo", - vec![("main", "a1b2c3d4e5f6789012345678901234567890abcd")], - ); - - let event_msg = json!(["EVENT", event]); - ws.send(Message::Text(event_msg.to_string().into())) - .await - .expect("Failed to send event"); - - // Wait for OK response - if let Some(Ok(Message::Text(_))) = ws.next().await { - // Got OK response - } - - // Query for states - let req = json!([ - "REQ", - "test-sub", - { - "kinds": [KIND_REPOSITORY_STATE], - "authors": [keys.public_key().to_hex()] - } - ]); - - ws.send(Message::Text(req.to_string().into())) + let config = AuditConfig::ci(); + let client = AuditClient::new(relay.url(), config) .await - .expect("Failed to send REQ"); + .expect("Failed to create audit client"); - // Read responses - let mut found_event = false; - let mut got_eose = false; + let results = specs::EventAcceptancePolicyTests::run_all(&client).await; - for _ in 0..10 { - if let Some(Ok(Message::Text(text))) = ws.next().await { - let response: Value = serde_json::from_str(&text).expect("Failed to parse"); - - if response[0] == "EVENT" { - assert_eq!(response[1], "test-sub"); - found_event = true; - } else if response[0] == "EOSE" { - assert_eq!(response[1], "test-sub"); - got_eose = true; - break; - } - } - } + relay.stop().await; - assert!(found_event, "Should have received the state event"); - assert!(got_eose, "Should have received EOSE"); -} - -/// Test duplicate event handling -#[tokio::test] -async fn test_duplicate_announcement() { - let relay = TestRelay::start().await; - let keys = Keys::generate(); + // Filter to rejection tests (these verify tag validation) + let rejection_tests: Vec<_> = results + .results + .iter() + .filter(|t| t.name.contains("reject")) + .collect(); - let (mut ws, _) = connect_async(relay.url()).await.expect("Failed to connect"); - - let event = create_announcement( - &keys, - &relay.domain(), - "duplicate-test", - vec![format!( - "https://{}/alice/duplicate-test.git", - relay.domain() - )], - vec![format!("wss://{}", relay.domain())], + // Should have rejection tests + assert!( + !rejection_tests.is_empty(), + "No rejection tests found (these are critical for security)" ); - // Send first time - let event_msg = json!(["EVENT", event]); - ws.send(Message::Text(event_msg.to_string().into())) - .await - .expect("Failed to send event"); - - if let Some(Ok(Message::Text(text))) = ws.next().await { - let response1: Value = serde_json::from_str(&text).expect("Failed to parse"); - assert_eq!(response1[2], true, "First send should succeed"); - } - - // Send second time (duplicate) - let event_msg = json!(["EVENT", event]); - ws.send(Message::Text(event_msg.to_string().into())) - .await - .expect("Failed to send event"); - - if let Some(Ok(Message::Text(text))) = ws.next().await { - let response2: Value = serde_json::from_str(&text).expect("Failed to parse"); - assert_eq!(response2[2], true, "Duplicate should be acknowledged"); - - let message = response2[3].as_str().unwrap(); + // All rejection tests should pass + for test in rejection_tests { assert!( - message.contains("duplicate") || message.is_empty(), - "Should indicate duplicate: {}", - message + test.passed, + "Rejection test failed: {} - {}\nThis is a security issue!", + test.name, + test.error.as_deref().unwrap_or("unknown error") ); } } -- cgit v1.2.3