13 KiB
GRASP Audit
A reusable audit and compliance testing tool for GRASP protocol implementations.
Features
- ✅ Isolated Testing: Tests run in parallel with unique audit IDs
- ✅ Production Audit: Test live services with minimal impact
- ✅ Clean Audit Events: Special tags for easy cleanup (no deletion trails)
- ✅ Spec-Mirrored Tests: Test structure matches GRASP protocol exactly
- ✅ Reusable: Can test any GRASP implementation (Rust, Go, Python, etc.)
Quick Start
The fastest way to run GRASP-01 compliance tests:
# Run the test suite against ngit-relay
cd grasp-audit
nix develop -c bash test-ngit-relay.sh --mode test
This automatically:
- ✅ Starts ngit-relay in an isolated Docker container
- ✅ Runs all GRASP-01 compliance tests
- ✅ Cleans up resources when finished
For more options:
./test-ngit-relay.sh --help
Usage Examples
As a Library
use grasp_audit::*;
#[tokio::main]
async fn main() -> Result<()> {
// Create audit client for CI testing
let config = AuditConfig::ci();
let client = AuditClient::new("ws://localhost:7000", config).await?;
// Run NIP-01 smoke tests
let results = specs::Nip01SmokeTests::run_all(&client).await;
results.print_report();
if !results.all_passed() {
std::process::exit(1);
}
Ok(())
}
As a CLI Tool
# Install
cargo install --path .
# Run smoke tests against local relay
grasp-audit audit --relay ws://localhost:7000 --mode ci --spec nip01-smoke
# Audit production server
grasp-audit audit --relay wss://grasp.example.com --mode production --spec all
Test Specifications
NIP-01 Smoke Tests (6 tests)
Basic Nostr relay functionality:
websocket_connection- Can connect to /send_receive_event- Can send EVENT, get OKcreate_subscription- Can subscribe with REQclose_subscription- Can close subscriptionsreject_invalid_signature- Rejects bad signaturesreject_invalid_event_id- Rejects wrong IDs
Why only smoke tests? rust-nostr already has 1000+ tests for NIP-01 compliance. We focus on GRASP-specific behavior.
GRASP-01 Tests (Coming Soon)
- Repository announcement acceptance
- State event handling
- Policy enforcement
- And more...
Audit Event Strategy
All audit events automatically include special tags for isolation and cleanup:
{
"tags": [
["t", "grasp-audit-test-event"],
["t", "audit-ci-a1b2c3d4-e5f6-7890-abcd-ef1234567890"],
["t", "audit-cleanup-after-1730822334"]
]
}
Tag Format:
["t", "grasp-audit-test-event"]- Identifies all audit-related events["t", "audit-{run_id}"]- Unique identifier for each audit run- CI mode:
audit-ci-{uuid} - Production mode:
audit-prod-audit-{timestamp}
- CI mode:
["t", "audit-cleanup-after-{unix_timestamp}"]- Cleanup scheduling- CI mode: Current time + 3600 seconds (1 hour)
- Production mode: Current time + 300 seconds (5 minutes)
Benefits:
- Automatic: Tags added automatically to all events via
AuditEventBuilder - Isolation: Each test run has unique ID for event filtering
- Cleanup: Events marked for cleanup after timestamp (direct database cleanup)
- No deletion trails: No NIP-09 deletion events needed
- Discovery: Easy to query all audit events via hashtag
Modes
CI Mode (Default)
- Tests are isolated by unique run ID
- Tests only see their own events
- Full read/write access
- Cleanup after 1 hour
let config = AuditConfig::ci();
Production Mode
- Tests see all events (including real ones)
- Read-only by default (minimal impact)
- Cleanup after 5 minutes
let config = AuditConfig::production();
Examples
See examples/ directory:
# Simple audit example
cargo run --example simple_audit
Testing
TL;DR: See the Quick Start section for the fastest way to run tests.
Unit Tests
# Enter dev environment (NixOS)
nix develop
# Run unit tests (no relay required)
cargo test
Integration Tests
The recommended approach is test-ngit-relay.sh, which handles all relay lifecycle management automatically.
See the Quick Start section for common usage patterns.
Advanced: Manual Relay Setup
Click to expand manual testing instructions
For advanced use cases where you need direct control over the relay:
# Start relay on a specific port (example uses 18081)
docker run --rm -p 18081:8081 ghcr.io/danconwaydev/ngit-relay:latest
# In another terminal, run tests with RELAY_URL
grasp-audit audit --relay ws://localhost:18081 --mode ci
# or run all ignored tests via cargo
RELAY_URL="ws://localhost:18081" cargo test --lib -- --ignored --nocapture
# or run specific test via cargo
RELAY_URL="ws://localhost:18081" cargo test --lib test_grasp01_nostr_relay_against_relay -- --ignored --nocapture
Test Design Pattern: Fixture-First
To prevent rate-limiting from production relays during testing, we use a fixture-first approach that minimizes relay interactions.
Quick Start for New Tests
- Create TestContext at test start
- Get prerequisites via
ctx.get_fixture(FixtureKind::...) - Build test-specific events using fixtures as base
- Verify outcomes via
send_and_verify_accepted/rejected
Pattern Template
pub async fn test_something(client: &AuditClient) -> TestResult {
TestResult::new(...)
.run(|| async {
// 1. Context
let ctx = TestContext::new(client);
// 2. Prerequisites (cached per-TestContext)
let repo = ctx.get_fixture(FixtureKind::ValidRepo).await?;
// 3. Test-specific event
let my_event = client.create_issue(&repo, "Title", "Content", vec![])?;
// 4. Verify
send_and_verify_accepted(client, my_event, "description").await?;
Ok(())
})
.await
}
Three-Layer Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Layer 3: Test Functions │
│ Create TestContext, get fixtures, build scenarios, verify │
├─────────────────────────────────────────────────────────────────┤
│ Layer 2: FixtureKind + TestContext │
│ ValidRepo, RepoState, OwnerStateDataPushed, etc. │
│ Mode-aware caching within TestContext │
├─────────────────────────────────────────────────────────────────┤
│ Layer 1: AuditClient │
│ event_builder, create_repo_announcement, send_event │
└─────────────────────────────────────────────────────────────────┘
Available Fixtures
| FixtureKind | Provides | Use When |
|---|---|---|
ValidRepo |
Accepted repo announcement (kind 30617). Signed by owner keys, lists maintainer in maintainers tag. | Need a repo as prerequisite |
RepoWithIssue |
Repo + accepted issue (kind 1621) | Testing issue-dependent events |
RepoWithComment |
Repo + issue + comment (kind 1111) | Testing comment-dependent events |
RepoState |
Repo + state event (kind 30618). Signed by owner, points to DETERMINISTIC_COMMIT_HASH. |
Testing owner state events |
PREvent |
Repo + PR event (kind 1618). Signed by PR author, points to PR_TEST_COMMIT_HASH. |
Testing PR-dependent events |
PREventGenerated |
PR event built but NOT sent to relay. | Need PR event ID before publishing |
PRWrongCommitPushedBeforeEvent |
Wrong commit pushed to refs/nostr/<pr-event-id> before PR event sent. Returns unsent PR event. |
Testing pre-event ref cleanup |
PREventSentAfterWrongPush |
PR event sent after wrong commit was pushed. Tests cleanup behavior. | Testing post-event ref cleanup |
OwnerStateDataPushed |
Full owner push flow: state event + git data pushed. Points to DETERMINISTIC_COMMIT_HASH. |
Testing owner push authorization |
MaintainerStateDataPushed |
Full maintainer push flow: force-pushes over owner's data. Points to MAINTAINER_DETERMINISTIC_COMMIT_HASH. |
Testing maintainer push authorization |
RecursiveMaintainerStateDataPushed |
Full recursive maintainer push flow: Owner → Maintainer → RecursiveMaintainer chain. Points to RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH. |
Testing recursive maintainer authorization |
HeadSetToDevelopBranch |
State event with HEAD=refs/heads/develop. Depends on RecursiveMaintainerStateDataPushed. | Testing HEAD branch switching |
Deterministic Commit Hashes
Fixtures use deterministic commit hashes for reproducible testing:
| Constant | Hash | Used By |
|---|---|---|
DETERMINISTIC_COMMIT_HASH |
64ea71d79a57a7acb334cd9651f8aec067c0ce5d |
Owner fixtures (RepoState, OwnerStateDataPushed) |
MAINTAINER_DETERMINISTIC_COMMIT_HASH |
1c2d472c9b71ed51968a66500281a3c4a6840464 |
MaintainerStateDataPushed |
RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH |
05939b82de66fbdb9c077d0a64fc68522f3cb8e0 |
RecursiveMaintainerStateDataPushed |
PR_TEST_COMMIT_HASH |
5d40fb1555a0c28bf4d650515a73aaa54d4d9bfb |
PR fixtures (PREvent, PREventGenerated) |
Fixture Dependencies
Fixtures automatically resolve their dependencies:
ValidRepo (base)
├── RepoWithIssue → RepoWithComment
├── RepoState
├── PREventGenerated → PRWrongCommitPushedBeforeEvent → PREventSentAfterWrongPush
├── PREvent
└── OwnerStateDataPushed
└── MaintainerStateDataPushed
└── RecursiveMaintainerStateDataPushed
└── HeadSetToDevelopBranch
Fixture Lifecycle: Generate → Send → Verify → DataPushed
Every fixture follows a lifecycle (some stop earlier):
- GENERATE: Build event via
AuditClient.event_builder()(in memory only) - SEND:
client.send_event(event)transmits to relay (rate-limited operation) - VERIFY: Query relay to confirm acceptance/rejection
- DATA_PUSHED: (DataPushed variants only) Clone repo, create commit, push to git server
Caching happens after the fixture completes - same fixture request returns cached Event.
Note: Some fixtures handle their own event sending (e.g., OwnerStateDataPushed, MaintainerStateDataPushed). These are marked with sends_own_events() -> true.
How TestContext Correlates Events
Each TestContext shares a run_id with all events:
// All events in a TestContext get these tags automatically:
["t", "grasp-audit-test-event"] // Identifies test events
["t", "audit-{run_id}"] // Unique ID for this run
["t", "audit-cleanup-after-{ts}"] // Cleanup timestamp
This enables:
- Event correlation within a test run
- Production relay cleanup scripts
- Test isolation between runs
When NOT to Use Fixtures
Use direct event building (NOT fixtures) when:
- Testing event REJECTION - Build invalid events directly
- Testing signature/ID validation - Need malformed events
- One-off connectivity tests - No prerequisites needed
// Example: Testing rejection (build invalid event directly)
let invalid_event = client.event_builder(Kind::GitRepoAnnouncement, "")
.tag(Tag::identifier("test"))
// Missing required 'clone' tag - should be rejected
.build(client.keys())?;
send_and_verify_rejected(client, invalid_event, "missing clone tag").await?;
Anti-Patterns to Avoid
❌ Creating TestContext inside helper functions - Tests lose cache control
❌ Monolithic setup functions - Mix fixture retrieval with git operations
❌ Direct event creation when fixture exists - Misses caching opportunity
✅ Each test creates own TestContext - Isolation guaranteed
✅ Use fixtures for prerequisites - Caching minimizes relay calls
✅ Build invalid events directly - Only for rejection tests
Architecture
grasp-audit/
├── src/
│ ├── lib.rs # Public API
│ ├── audit.rs # Audit config and event tagging
│ ├── client.rs # Audit client
│ ├── fixtures.rs # TestContext and FixtureKind
│ ├── result.rs # Test result types
│ ├── isolation.rs # Test isolation utilities
│ └── specs/
│ ├── mod.rs
│ └── nip01_smoke.rs # NIP-01 smoke tests
├── examples/
│ └── simple_audit.rs # Example usage
└── bin/
└── grasp-audit.rs # CLI tool
Development Status
- ✅ Audit framework
- ✅ NIP-01 smoke tests (6 tests)
- 🚧 GRASP-01 relay tests (planned)
- 🚧 GRASP-01 git tests (planned)
- 🚧 Cleanup utilities (planned)
Contributing
This tool is designed to be reusable by any GRASP implementation. Contributions welcome!
License
MIT