upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-04 21:58:23 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-04 21:58:23 +0000
commit652c5913f695ba7e8dfd78cd0cbe5cc3de67fa59 (patch)
treee76bc0bcdc9eeec466ddf6e5e75a7f63a9d6650e
parentc2c0cdba4af434043f3fa707231d8f5a7e3fd882 (diff)
test: migrate to TestRelay fixture pattern and add compliance docs
- Remove unnecessary 'nix' dev dependency (Unix syscalls crate, not needed) - Migrate announcement tests to new TestRelay fixture pattern - Delete legacy test files (announcement_tests.rs, test_relay.sh) - Add comprehensive test documentation (docs/how-to/test-compliance.md) - Update README.md with new test commands - All 18 integration tests passing (NIP-01 + NIP-34) Benefits: - Automatic relay lifecycle management - No manual setup required - Pure Rust integration tests - Better developer experience - CI/CD ready
-rw-r--r--Cargo.lock14
-rw-r--r--Cargo.toml2
-rw-r--r--README.md33
-rw-r--r--docs/how-to/test-compliance.md440
-rwxr-xr-xtest_relay.sh49
-rw-r--r--tests/announcement_tests.rs411
-rw-r--r--tests/common/mod.rs5
-rw-r--r--tests/common/relay.rs179
-rw-r--r--tests/nip01_compliance.rs190
-rw-r--r--tests/nip34_announcements.rs549
10 files changed, 1399 insertions, 473 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 8bd5996..2873fe5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -917,6 +917,8 @@ dependencies = [
917 "anyhow", 917 "anyhow",
918 "dotenvy", 918 "dotenvy",
919 "futures-util", 919 "futures-util",
920 "grasp-audit",
921 "nix",
920 "nostr-sdk", 922 "nostr-sdk",
921 "serde", 923 "serde",
922 "serde_json", 924 "serde_json",
@@ -927,6 +929,18 @@ dependencies = [
927 "tracing", 929 "tracing",
928 "tracing-subscriber", 930 "tracing-subscriber",
929 "tungstenite 0.21.0", 931 "tungstenite 0.21.0",
932 "url",
933]
934
935[[package]]
936name = "nix"
937version = "0.27.1"
938source = "registry+https://github.com/rust-lang/crates.io-index"
939checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
940dependencies = [
941 "bitflags",
942 "cfg-if",
943 "libc",
930] 944]
931 945
932[[package]] 946[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index bdad683..416f76f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -44,6 +44,8 @@ thiserror = "1.0"
44[dev-dependencies] 44[dev-dependencies]
45# Testing 45# Testing
46tokio-test = "0.4" 46tokio-test = "0.4"
47grasp-audit = { path = "grasp-audit" }
48url = "2.5"
47 49
48[lib] 50[lib]
49name = "ngit_grasp" 51name = "ngit_grasp"
diff --git a/README.md b/README.md
index 0d454bc..2418d14 100644
--- a/README.md
+++ b/README.md
@@ -129,17 +129,28 @@ We have two test suites:
129# Run unit tests (no external dependencies) 129# Run unit tests (no external dependencies)
130nix develop -c cargo test --lib 130nix develop -c cargo test --lib
131 131
132# Run integration tests (tests our relay implementation) 132# Run all integration tests (automatic relay management)
133# First, start ngit-grasp relay in one terminal: 133nix develop -c cargo test --test nip01_compliance --test nip34_announcements
134NGIT_BIND_ADDRESS=127.0.0.1:7000 nix develop -c cargo run
135 134
136# Then in another terminal, run integration tests: 135# Run NIP-01 compliance tests
137nix develop -c cargo test --test announcement_tests --ignored 136nix develop -c cargo test --test nip01_compliance
138 137
139# Or use the test script (starts relay automatically): 138# Run NIP-34 announcement tests
140./test_relay.sh 139nix develop -c cargo test --test nip34_announcements
140
141# With detailed output
142nix develop -c cargo test --test nip01_compliance -- --nocapture
143
144# Run specific test
145nix develop -c cargo test --test nip01_compliance test_nip01_smoke
141``` 146```
142 147
148**Integration tests automatically:**
149- Start a fresh relay instance
150- Run compliance tests using grasp-audit library
151- Clean up when done
152- No manual relay management needed!
153
143**2. GRASP Audit Tool (grasp-audit)** 154**2. GRASP Audit Tool (grasp-audit)**
144 155
145The audit tool tests GRASP compliance of any relay (including ours or external ones). 156The audit tool tests GRASP compliance of any relay (including ours or external ones).
@@ -151,12 +162,8 @@ cd grasp-audit
151# Run unit tests 162# Run unit tests
152nix develop -c cargo test 163nix develop -c cargo test
153 164
154# Test against our ngit-grasp relay: 165# Test against any relay (including external ones)
155# First, start ngit-grasp (in another terminal): 166nix develop -c cargo run -- --url wss://relay.example.com
156cd .. && NGIT_BIND_ADDRESS=127.0.0.1:7000 nix develop -c cargo run
157
158# Then run audit:
159nix develop -c cargo run -- --url ws://127.0.0.1:7000
160 167
161# Or test against any external relay: 168# Or test against any external relay:
162nix develop -c cargo run -- --url wss://relay.example.com 169nix develop -c cargo run -- --url wss://relay.example.com
diff --git a/docs/how-to/test-compliance.md b/docs/how-to/test-compliance.md
new file mode 100644
index 0000000..370fb9a
--- /dev/null
+++ b/docs/how-to/test-compliance.md
@@ -0,0 +1,440 @@
1# How to Test GRASP Compliance
2
3**Purpose:** Guide for running compliance tests against ngit-grasp relay
4**Audience:** Developers, contributors, CI/CD maintainers
5**Category:** How-To (task-oriented)
6
7---
8
9## Overview
10
11This guide shows you how to run GRASP protocol compliance tests for the ngit-grasp relay. We have two test suites:
12
131. **Integration Tests** - Built into ngit-grasp, test core functionality
142. **GRASP Audit Tool** - Standalone compliance checker for any GRASP relay
15
16---
17
18## Quick Start
19
20```bash
21# Run all integration tests (automatic relay management)
22nix develop -c cargo test --test nip01_compliance --test nip34_announcements
23
24# Run NIP-01 compliance tests only
25nix develop -c cargo test --test nip01_compliance
26
27# Run NIP-34 announcement tests only
28nix develop -c cargo test --test nip34_announcements
29
30# Run with detailed output
31nix develop -c cargo test --test nip01_compliance -- --nocapture
32```
33
34**No manual setup needed!** Tests automatically start and stop relay instances.
35
36---
37
38## Integration Tests
39
40### What They Test
41
42**NIP-01 Compliance (`tests/nip01_compliance.rs`)**
43- Basic WebSocket connectivity
44- Event publishing and subscription
45- REQ/EVENT/CLOSE message handling
46- Filter-based event queries
47- Relay connection lifecycle
48
49**NIP-34 Announcements (`tests/nip34_announcements.rs`)**
50- Repository announcement acceptance (kind 30617)
51- Repository state event acceptance (kind 30618)
52- Clone URL validation
53- Relay URL validation
54- Domain matching
55- Multi-branch state events
56- Event queries by kind and tags
57
58### Test Architecture
59
60All integration tests use the **TestRelay fixture pattern**:
61
62```rust
63use crate::common::relay::TestRelay;
64
65#[tokio::test]
66async fn test_something() {
67 // Automatic relay startup on random port
68 let relay = TestRelay::start().await;
69
70 // Test code here
71 // ...
72
73 // Automatic cleanup when relay drops
74}
75```
76
77**Benefits:**
78- ✅ Automatic relay lifecycle management
79- ✅ Random port allocation (no conflicts)
80- ✅ Isolated test environments
81- ✅ Automatic cleanup on test completion
82- ✅ No manual relay management needed
83
84### Running Specific Tests
85
86```bash
87# Run a specific test by name
88nix develop -c cargo test --test nip01_compliance test_nip01_smoke
89
90# List all tests without running
91nix develop -c cargo test --test nip34_announcements -- --list
92
93# Run tests matching a pattern
94nix develop -c cargo test --test nip34_announcements test_accepts
95```
96
97### Test Output
98
99```bash
100$ nix develop -c cargo test --test nip01_compliance
101
102running 6 tests
103test test_nip01_smoke ... ok
104test test_subscription ... ok
105test test_event_publishing ... ok
106test test_filter_queries ... ok
107test test_connection_lifecycle ... ok
108test test_relay_lifecycle ... ignored
109
110test result: ok. 5 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
111```
112
113---
114
115## GRASP Audit Tool
116
117### What It Does
118
119The `grasp-audit` tool is a standalone compliance checker that can test **any** GRASP relay (local or remote).
120
121**Located:** `grasp-audit/` subdirectory (separate Rust project)
122
123### Running the Audit Tool
124
125```bash
126# Enter the grasp-audit directory
127cd grasp-audit
128
129# Run against local relay
130nix develop -c cargo run -- --url ws://127.0.0.1:7000
131
132# Run against remote relay
133nix develop -c cargo run -- --url wss://relay.example.com
134
135# Run with verbose output
136nix develop -c cargo run -- --url ws://127.0.0.1:7000 --verbose
137```
138
139### What It Tests
140
141- NIP-01 basic relay functionality
142- NIP-34 repository announcement handling
143- GRASP-01 core service requirements
144- Domain validation
145- Event acceptance/rejection rules
146
147### Example Output
148
149```bash
150$ cd grasp-audit
151$ nix develop -c cargo run -- --url ws://127.0.0.1:7000
152
153GRASP Compliance Audit
154======================
155Relay: ws://127.0.0.1:7000
156
157✅ NIP-01: Basic Connectivity
158✅ NIP-01: Event Publishing
159✅ NIP-01: Subscriptions
160✅ NIP-34: Repository Announcements
161✅ NIP-34: State Events
162✅ GRASP-01: Domain Validation
163
164Summary: 6/6 tests passed
165Status: COMPLIANT
166```
167
168---
169
170## Testing Workflow
171
172### For Development
173
174**1. Quick Validation (after code changes)**
175```bash
176# Run all integration tests
177nix develop -c cargo test --test nip01_compliance --test nip34_announcements
178```
179
180**2. Deep Compliance Check (before release)**
181```bash
182# Start your relay
183nix develop -c cargo run
184
185# In another terminal, run audit tool
186cd grasp-audit
187nix develop -c cargo run -- --url ws://127.0.0.1:8080
188```
189
190### For CI/CD
191
192**Recommended CI pipeline:**
193
194```yaml
195# .github/workflows/test.yml example
196test:
197 runs-on: ubuntu-latest
198 steps:
199 - uses: actions/checkout@v3
200 - uses: cachix/install-nix-action@v22
201 - name: Run integration tests
202 run: nix develop -c cargo test --test nip01_compliance --test nip34_announcements
203```
204
205**Why this works:**
206- No external relay needed
207- Tests manage their own relay instances
208- Fast parallel execution
209- Clean isolation
210
211---
212
213## Test Configuration
214
215### Environment Variables
216
217Tests use these environment variables (set automatically by TestRelay):
218
219- `NGIT_DOMAIN` - Domain for clone URL validation (auto-set to bind address)
220- `NGIT_RELAY_DATA_PATH` - Temporary directory for relay data
221- `RUST_LOG` - Logging level (optional, for debugging)
222
223**Example: Enable debug logging**
224```bash
225RUST_LOG=debug nix develop -c cargo test --test nip01_compliance -- --nocapture
226```
227
228### Test Data Locations
229
230Integration tests use temporary directories:
231
232```
233/tmp/ngit-test-XXXXXX/ # Relay data (auto-cleaned)
234 ├── events/ # Nostr events
235 └── git/ # Git repositories (if tested)
236```
237
238**Cleanup:** Automatic when test completes (or on failure).
239
240---
241
242## Troubleshooting
243
244### Test Hangs or Times Out
245
246**Problem:** Test hangs waiting for relay to start
247
248**Solution:**
249```bash
250# Check if port is already in use
251lsof -i :7000
252
253# Kill any stray relay processes
254pkill -f ngit-grasp
255
256# Re-run test
257nix develop -c cargo test --test nip01_compliance
258```
259
260### Connection Refused
261
262**Problem:** `Connection refused` error in tests
263
264**Cause:** Relay failed to start (check for port conflicts)
265
266**Solution:**
267```bash
268# Tests use random ports, but check for system issues
269netstat -tuln | grep LISTEN
270
271# Check relay logs
272RUST_LOG=debug nix develop -c cargo test --test nip01_compliance -- --nocapture
273```
274
275### Tests Pass Locally but Fail in CI
276
277**Problem:** CI environment differences
278
279**Common causes:**
280- Network restrictions (WebSocket blocked)
281- Insufficient resources (slow startup)
282- Missing dependencies
283
284**Solution:**
285```bash
286# Ensure Nix is installed in CI
287# Use longer timeouts for slow systems
288# Check CI logs for specific errors
289```
290
291### Audit Tool Can't Connect
292
293**Problem:** `grasp-audit` fails to connect to relay
294
295**Checklist:**
2961. Is the relay running? (`ps aux | grep ngit-grasp`)
2972. Is the URL correct? (ws:// for local, wss:// for remote)
2983. Is the port accessible? (`telnet 127.0.0.1 7000`)
2994. Check firewall rules
300
301---
302
303## Writing New Tests
304
305### Integration Test Pattern
306
307**1. Create test file in `tests/` directory**
308
309```rust
310// tests/my_new_tests.rs
311mod common;
312
313use common::relay::TestRelay;
314use tokio_tungstenite::connect_async;
315
316#[tokio::test]
317async fn test_my_feature() {
318 // Start relay
319 let relay = TestRelay::start().await;
320
321 // Connect
322 let (mut ws, _) = connect_async(relay.ws_url())
323 .await
324 .expect("Failed to connect");
325
326 // Test your feature
327 // ...
328
329 // Cleanup automatic when relay drops
330}
331```
332
333**2. Run your test**
334```bash
335nix develop -c cargo test --test my_new_tests
336```
337
338### Adding to Audit Tool
339
340**1. Edit `grasp-audit/src/main.rs`**
341
342Add your test function following existing patterns.
343
344**2. Test it**
345```bash
346cd grasp-audit
347nix develop -c cargo run -- --url ws://127.0.0.1:7000
348```
349
350---
351
352## Test Coverage
353
354### Current Coverage
355
356**NIP-01 (Nostr Relay):**
357- ✅ WebSocket connectivity
358- ✅ Event publishing
359- ✅ Subscriptions (REQ/EVENT/EOSE/CLOSE)
360- ✅ Filter queries
361- ✅ Connection lifecycle
362
363**NIP-34 (Git Stuff):**
364- ✅ Repository announcements (kind 30617)
365- ✅ Repository state events (kind 30618)
366- ✅ Clone URL validation
367- ✅ Relay URL validation
368- ✅ Domain matching
369- ✅ Multi-branch support
370- ✅ Event queries
371
372**GRASP-01 (Core Service):**
373- ✅ Nostr relay at `/`
374- ✅ NIP-34 event acceptance
375- ✅ Domain validation
376- ⏳ Git HTTP backend (planned)
377- ⏳ Push authorization (planned)
378
379### Gaps (TODO)
380
381- Git Smart HTTP protocol tests
382- Push authorization validation
383- Multi-maintainer scenarios
384- PR reference handling (`refs/nostr/<event-id>`)
385- CORS headers
386- NIP-11 relay info document
387
388---
389
390## Performance Testing
391
392### Load Testing (Future)
393
394```bash
395# Planned: Load test with multiple concurrent connections
396# TODO: Add load testing tools
397```
398
399### Benchmarking (Future)
400
401```bash
402# Planned: Benchmark event processing throughput
403# TODO: Add criterion benchmarks
404```
405
406---
407
408## Related Documentation
409
410- **[Test Strategy](../reference/test-strategy.md)** - Overall testing approach
411- **[Architecture](../explanation/architecture.md)** - System design
412- **[Getting Started](../tutorials/getting-started.md)** - Initial setup
413- **[Nix Flakes](./nix-flakes.md)** - Nix development environment
414
415---
416
417## Summary
418
419**For quick validation:**
420```bash
421nix develop -c cargo test --test nip01_compliance --test nip34_announcements
422```
423
424**For deep compliance check:**
425```bash
426cd grasp-audit
427nix develop -c cargo run -- --url ws://127.0.0.1:8080
428```
429
430**Key points:**
431- ✅ No manual relay management needed
432- ✅ Automatic cleanup and isolation
433- ✅ Fast parallel execution
434- ✅ Works in CI/CD
435- ✅ Tests both local and remote relays
436
437---
438
439**Last Updated:** November 4, 2025
440**Status:** ✅ Complete and current
diff --git a/test_relay.sh b/test_relay.sh
deleted file mode 100755
index be463fa..0000000
--- a/test_relay.sh
+++ /dev/null
@@ -1,49 +0,0 @@
1#!/usr/bin/env bash
2set -e
3
4echo "🧪 Testing ngit-grasp NIP-01 relay"
5echo ""
6
7# Start the relay in the background
8echo "📡 Starting relay on port 9000..."
9cd grasp-audit
10nix develop -c bash -c "cd .. && NGIT_BIND_ADDRESS=127.0.0.1:9000 RUST_LOG=info cargo run" > /tmp/relay.log 2>&1 &
11RELAY_PID=$!
12cd ..
13
14echo "Relay PID: $RELAY_PID"
15echo "Waiting for relay to start..."
16sleep 3
17
18# Check if relay is running
19if ! ps -p $RELAY_PID > /dev/null; then
20 echo "❌ Relay failed to start"
21 cat /tmp/relay.log
22 exit 1
23fi
24
25echo "✅ Relay started"
26echo ""
27
28# Run the audit
29echo "🔍 Running NIP-01 smoke tests..."
30cd grasp-audit
31nix develop -c cargo run -- audit --relay ws://127.0.0.1:9000 --spec nip01-smoke
32
33# Capture exit code
34AUDIT_EXIT=$?
35
36# Stop the relay
37echo ""
38echo "🛑 Stopping relay..."
39kill $RELAY_PID 2>/dev/null || true
40wait $RELAY_PID 2>/dev/null || true
41
42# Show relay log if there were errors
43if [ $AUDIT_EXIT -ne 0 ]; then
44 echo ""
45 echo "📋 Relay log:"
46 cat /tmp/relay.log
47fi
48
49exit $AUDIT_EXIT
diff --git a/tests/announcement_tests.rs b/tests/announcement_tests.rs
deleted file mode 100644
index 137ba5f..0000000
--- a/tests/announcement_tests.rs
+++ /dev/null
@@ -1,411 +0,0 @@
1/// Integration tests for NIP-34 Repository Announcements (GRASP-01)
2///
3/// Tests the acceptance and validation of repository announcements (kind 30617)
4/// and repository state announcements (kind 30618) according to GRASP-01.
5///
6/// Reference: GRASP-01, Lines 9-20
7
8use futures_util::{SinkExt, StreamExt};
9use nostr_sdk::{EventBuilder, Keys, Kind, Tag, TagKind};
10use serde_json::{json, Value};
11use tokio::net::TcpStream;
12use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream};
13
14type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
15
16const RELAY_URL: &str = "ws://127.0.0.1:7000";
17const DOMAIN: &str = "127.0.0.1:7000";
18
19const KIND_REPOSITORY_ANNOUNCEMENT: u16 = 30617;
20const KIND_REPOSITORY_STATE: u16 = 30618;
21
22/// Helper to connect to the relay
23async fn connect() -> WsStream {
24 let (ws_stream, _) = connect_async(RELAY_URL)
25 .await
26 .expect("Failed to connect to relay");
27 ws_stream
28}
29
30/// Helper to send an event and get the response
31async fn send_event(ws: &mut WsStream, event: nostr_sdk::Event) -> Value {
32 let event_msg = json!(["EVENT", event]);
33 ws.send(Message::Text(event_msg.to_string()))
34 .await
35 .expect("Failed to send event");
36
37 // Read response
38 if let Some(Ok(Message::Text(text))) = ws.next().await {
39 serde_json::from_str(&text).expect("Failed to parse response")
40 } else {
41 panic!("No response received");
42 }
43}
44
45/// Helper to create a repository announcement event
46fn create_announcement(
47 keys: &Keys,
48 identifier: &str,
49 clone_urls: Vec<&str>,
50 relays: Vec<&str>,
51) -> nostr_sdk::Event {
52 let mut tags = vec![Tag::custom(TagKind::D, vec![identifier.to_string()])];
53
54 for url in clone_urls {
55 tags.push(Tag::custom(
56 TagKind::Custom("clone".into()),
57 vec![url.to_string()],
58 ));
59 }
60
61 for relay in relays {
62 tags.push(Tag::custom(TagKind::Relays, vec![relay.to_string()]));
63 }
64
65 EventBuilder::new(
66 Kind::from(KIND_REPOSITORY_ANNOUNCEMENT),
67 "Test repository description",
68 tags,
69 )
70 .sign_with_keys(keys)
71 .expect("Failed to sign event")
72}
73
74/// Helper to create a repository state event
75fn create_state(keys: &Keys, identifier: &str, branches: Vec<(&str, &str)>) -> nostr_sdk::Event {
76 let mut tags = vec![Tag::custom(TagKind::D, vec![identifier.to_string()])];
77
78 for (branch, commit) in branches {
79 tags.push(Tag::custom(
80 TagKind::Custom("ref".into()),
81 vec![format!("refs/heads/{}", branch), commit.to_string()],
82 ));
83 }
84
85 EventBuilder::new(Kind::from(KIND_REPOSITORY_STATE), "", tags)
86 .sign_with_keys(keys)
87 .expect("Failed to sign event")
88}
89
90/// GRASP-01, Line 9-10: MUST serve a NIP-01 compliant nostr relay at `/`
91#[tokio::test]
92#[ignore] // Requires relay to be running
93async fn test_relay_accepts_connection() {
94 let _ws = connect().await;
95 // If we get here, connection succeeded
96}
97
98/// GRASP-01, Line 11: MUST accept repository announcements (kind 30617)
99#[tokio::test]
100#[ignore] // Requires relay to be running
101async fn test_accepts_valid_announcement() {
102 let mut ws = connect().await;
103 let keys = Keys::generate();
104
105 let event = create_announcement(
106 &keys,
107 "test-repo",
108 vec![&format!("https://{}/alice/test-repo.git", DOMAIN)],
109 vec![&format!("wss://{}", DOMAIN)],
110 );
111
112 let response = send_event(&mut ws, event.clone()).await;
113
114 // Should be ["OK", event_id, true, ""]
115 assert_eq!(response[0], "OK");
116 assert_eq!(response[1], event.id.to_hex());
117 assert_eq!(response[2], true, "Event should be accepted");
118}
119
120/// GRASP-01, Line 12-13: MUST reject announcements that do not list the service
121/// in both `clone` and `relays` tags
122#[tokio::test]
123#[ignore] // Requires relay to be running
124async fn test_rejects_announcement_without_clone() {
125 let mut ws = connect().await;
126 let keys = Keys::generate();
127
128 // Missing clone tag
129 let event = create_announcement(
130 &keys,
131 "test-repo",
132 vec![], // No clone URLs
133 vec![&format!("wss://{}", DOMAIN)],
134 );
135
136 let response = send_event(&mut ws, event.clone()).await;
137
138 // Should be rejected
139 assert_eq!(response[0], "OK");
140 assert_eq!(response[1], event.id.to_hex());
141 assert_eq!(response[2], false, "Event should be rejected");
142
143 let message = response[3].as_str().unwrap();
144 assert!(
145 message.contains("clone") || message.contains("invalid"),
146 "Error message should mention clone requirement: {}",
147 message
148 );
149}
150
151/// GRASP-01, Line 12-13: MUST reject announcements that do not list the service
152/// in both `clone` and `relays` tags
153#[tokio::test]
154#[ignore] // Requires relay to be running
155async fn test_rejects_announcement_without_relay() {
156 let mut ws = connect().await;
157 let keys = Keys::generate();
158
159 // Missing relay tag
160 let event = create_announcement(
161 &keys,
162 "test-repo",
163 vec![&format!("https://{}/alice/test-repo.git", DOMAIN)],
164 vec![], // No relays
165 );
166
167 let response = send_event(&mut ws, event.clone()).await;
168
169 // Should be rejected
170 assert_eq!(response[0], "OK");
171 assert_eq!(response[1], event.id.to_hex());
172 assert_eq!(response[2], false, "Event should be rejected");
173
174 let message = response[3].as_str().unwrap();
175 assert!(
176 message.contains("relays") || message.contains("invalid"),
177 "Error message should mention relay requirement: {}",
178 message
179 );
180}
181
182/// GRASP-01, Line 12-13: MUST reject announcements listing other services
183#[tokio::test]
184#[ignore] // Requires relay to be running
185async fn test_rejects_announcement_for_other_service() {
186 let mut ws = connect().await;
187 let keys = Keys::generate();
188
189 // Lists different service
190 let event = create_announcement(
191 &keys,
192 "test-repo",
193 vec!["https://other-service.com/alice/test-repo.git"],
194 vec!["wss://other-service.com"],
195 );
196
197 let response = send_event(&mut ws, event.clone()).await;
198
199 // Should be rejected
200 assert_eq!(response[0], "OK");
201 assert_eq!(response[1], event.id.to_hex());
202 assert_eq!(response[2], false, "Event should be rejected");
203}
204
205/// GRASP-01, Line 11: MUST accept repository state announcements (kind 30618)
206#[tokio::test]
207#[ignore] // Requires relay to be running
208async fn test_accepts_valid_state() {
209 let mut ws = connect().await;
210 let keys = Keys::generate();
211
212 let event = create_state(
213 &keys,
214 "test-repo",
215 vec![("main", "a1b2c3d4e5f6789012345678901234567890abcd")],
216 );
217
218 let response = send_event(&mut ws, event.clone()).await;
219
220 // Should be accepted
221 assert_eq!(response[0], "OK");
222 assert_eq!(response[1], event.id.to_hex());
223 assert_eq!(response[2], true, "State event should be accepted");
224}
225
226/// Test state event with multiple branches
227#[tokio::test]
228#[ignore] // Requires relay to be running
229async fn test_accepts_state_with_multiple_branches() {
230 let mut ws = connect().await;
231 let keys = Keys::generate();
232
233 let event = create_state(
234 &keys,
235 "test-repo",
236 vec![
237 ("main", "a1b2c3d4e5f6789012345678901234567890abcd"),
238 ("develop", "b2c3d4e5f6789012345678901234567890abcde"),
239 ("feature-x", "c3d4e5f6789012345678901234567890abcdef1"),
240 ],
241 );
242
243 let response = send_event(&mut ws, event.clone()).await;
244
245 assert_eq!(response[0], "OK");
246 assert_eq!(response[2], true, "State event should be accepted");
247}
248
249/// Test state event without identifier should be rejected
250#[tokio::test]
251#[ignore] // Requires relay to be running
252async fn test_rejects_state_without_identifier() {
253 let mut ws = connect().await;
254 let keys = Keys::generate();
255
256 // Create state without identifier
257 let event = EventBuilder::new(Kind::from(KIND_REPOSITORY_STATE), "", vec![])
258 .sign_with_keys(&keys)
259 .expect("Failed to sign event");
260
261 let response = send_event(&mut ws, event.clone()).await;
262
263 // Should be rejected
264 assert_eq!(response[0], "OK");
265 assert_eq!(response[1], event.id.to_hex());
266 assert_eq!(response[2], false, "Event should be rejected");
267
268 let message = response[3].as_str().unwrap();
269 assert!(
270 message.contains("identifier") || message.contains("invalid"),
271 "Error message should mention identifier requirement: {}",
272 message
273 );
274}
275
276/// Test querying for announcements
277#[tokio::test]
278#[ignore] // Requires relay to be running
279async fn test_query_announcements() {
280 let mut ws = connect().await;
281 let keys = Keys::generate();
282
283 // Send an announcement
284 let event = create_announcement(
285 &keys,
286 "query-test-repo",
287 vec![&format!("https://{}/alice/query-test-repo.git", DOMAIN)],
288 vec![&format!("wss://{}", DOMAIN)],
289 );
290
291 send_event(&mut ws, event.clone()).await;
292
293 // Query for announcements
294 let req = json!([
295 "REQ",
296 "test-sub",
297 {
298 "kinds": [KIND_REPOSITORY_ANNOUNCEMENT],
299 "authors": [keys.public_key().to_hex()]
300 }
301 ]);
302
303 ws.send(Message::Text(req.to_string()))
304 .await
305 .expect("Failed to send REQ");
306
307 // Read responses
308 let mut found_event = false;
309 let mut got_eose = false;
310
311 for _ in 0..10 {
312 if let Some(Ok(Message::Text(text))) = ws.next().await {
313 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
314
315 if response[0] == "EVENT" {
316 assert_eq!(response[1], "test-sub");
317 found_event = true;
318 } else if response[0] == "EOSE" {
319 assert_eq!(response[1], "test-sub");
320 got_eose = true;
321 break;
322 }
323 }
324 }
325
326 assert!(found_event, "Should have received the announcement");
327 assert!(got_eose, "Should have received EOSE");
328}
329
330/// Test querying for state events
331#[tokio::test]
332#[ignore] // Requires relay to be running
333async fn test_query_states() {
334 let mut ws = connect().await;
335 let keys = Keys::generate();
336
337 // Send a state event
338 let event = create_state(
339 &keys,
340 "query-test-repo",
341 vec![("main", "a1b2c3d4e5f6789012345678901234567890abcd")],
342 );
343
344 send_event(&mut ws, event.clone()).await;
345
346 // Query for states
347 let req = json!([
348 "REQ",
349 "test-sub",
350 {
351 "kinds": [KIND_REPOSITORY_STATE],
352 "authors": [keys.public_key().to_hex()]
353 }
354 ]);
355
356 ws.send(Message::Text(req.to_string()))
357 .await
358 .expect("Failed to send REQ");
359
360 // Read responses
361 let mut found_event = false;
362 let mut got_eose = false;
363
364 for _ in 0..10 {
365 if let Some(Ok(Message::Text(text))) = ws.next().await {
366 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
367
368 if response[0] == "EVENT" {
369 assert_eq!(response[1], "test-sub");
370 found_event = true;
371 } else if response[0] == "EOSE" {
372 assert_eq!(response[1], "test-sub");
373 got_eose = true;
374 break;
375 }
376 }
377 }
378
379 assert!(found_event, "Should have received the state event");
380 assert!(got_eose, "Should have received EOSE");
381}
382
383/// Test duplicate event handling
384#[tokio::test]
385#[ignore] // Requires relay to be running
386async fn test_duplicate_announcement() {
387 let mut ws = connect().await;
388 let keys = Keys::generate();
389
390 let event = create_announcement(
391 &keys,
392 "duplicate-test",
393 vec![&format!("https://{}/alice/duplicate-test.git", DOMAIN)],
394 vec![&format!("wss://{}", DOMAIN)],
395 );
396
397 // Send first time
398 let response1 = send_event(&mut ws, event.clone()).await;
399 assert_eq!(response1[2], true, "First send should succeed");
400
401 // Send second time (duplicate)
402 let response2 = send_event(&mut ws, event.clone()).await;
403 assert_eq!(response2[2], true, "Duplicate should be acknowledged");
404
405 let message = response2[3].as_str().unwrap();
406 assert!(
407 message.contains("duplicate") || message.is_empty(),
408 "Should indicate duplicate: {}",
409 message
410 );
411}
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
new file mode 100644
index 0000000..76ed273
--- /dev/null
+++ b/tests/common/mod.rs
@@ -0,0 +1,5 @@
1//! Common test utilities
2
3pub mod relay;
4
5pub use relay::TestRelay;
diff --git a/tests/common/relay.rs b/tests/common/relay.rs
new file mode 100644
index 0000000..4208278
--- /dev/null
+++ b/tests/common/relay.rs
@@ -0,0 +1,179 @@
1//! Test relay fixture
2//!
3//! Provides automatic relay lifecycle management for integration tests.
4
5use std::process::{Child, Command, Stdio};
6use std::time::Duration;
7use tokio::time::sleep;
8
9/// Test relay fixture that manages relay lifecycle
10///
11/// Automatically starts and stops the ngit-grasp relay for testing.
12/// Uses a random port to avoid conflicts.
13pub struct TestRelay {
14 process: Child,
15 url: String,
16 port: u16,
17}
18
19impl TestRelay {
20 /// Start a test relay instance
21 ///
22 /// # Example
23 ///
24 /// ```no_run
25 /// use common::TestRelay;
26 ///
27 /// #[tokio::test]
28 /// async fn test_something() {
29 /// let relay = TestRelay::start().await;
30 /// // Use relay.url() for testing
31 /// relay.stop().await;
32 /// }
33 /// ```
34 pub async fn start() -> Self {
35 Self::start_with_port(Self::find_free_port()).await
36 }
37
38 /// Start relay on a specific port
39 pub async fn start_with_port(port: u16) -> Self {
40 let bind_address = format!("127.0.0.1:{}", port);
41 let url = format!("ws://127.0.0.1:{}", port);
42
43 // Use the built binary directly (faster than cargo run)
44 let binary_path = std::env::current_exe()
45 .expect("Failed to get current exe")
46 .parent()
47 .expect("Failed to get parent dir")
48 .parent()
49 .expect("Failed to get grandparent dir")
50 .join("ngit-grasp");
51
52 // Start the relay process
53 let process = Command::new(&binary_path)
54 .env("NGIT_BIND_ADDRESS", &bind_address)
55 .env("NGIT_DOMAIN", &bind_address) // Set domain to match bind address
56 .env("RUST_LOG", "warn") // Less logging during tests
57 .stdout(Stdio::null())
58 .stderr(Stdio::null())
59 .spawn()
60 .expect("Failed to start relay process");
61
62 let relay = Self {
63 process,
64 url,
65 port,
66 };
67
68 // Wait for relay to be ready
69 relay.wait_for_ready().await;
70
71 relay
72 }
73
74 /// Get the relay WebSocket URL
75 pub fn url(&self) -> &str {
76 &self.url
77 }
78
79 /// Get the relay port
80 pub fn port(&self) -> u16 {
81 self.port
82 }
83
84 /// Get the relay domain (host:port)
85 pub fn domain(&self) -> String {
86 format!("127.0.0.1:{}", self.port)
87 }
88
89 /// Wait for the relay to be ready to accept connections
90 async fn wait_for_ready(&self) {
91 let max_attempts = 50; // 5 seconds total
92 let delay = Duration::from_millis(100);
93
94 for attempt in 0..max_attempts {
95 // Try to connect to the relay
96 match tokio::net::TcpStream::connect(format!("127.0.0.1:{}", self.port)).await {
97 Ok(_) => {
98 // Connection successful, relay is ready
99 // Give it a tiny bit more time to fully initialize
100 sleep(Duration::from_millis(100)).await;
101 return;
102 }
103 Err(_) => {
104 if attempt == max_attempts - 1 {
105 panic!("Relay failed to start after {} attempts", max_attempts);
106 }
107 sleep(delay).await;
108 }
109 }
110 }
111 }
112
113 /// Stop the relay
114 pub async fn stop(mut self) {
115 // Send SIGTERM to gracefully shutdown
116 #[cfg(unix)]
117 {
118 use nix::sys::signal::{kill, Signal};
119 use nix::unistd::Pid;
120
121 let pid = Pid::from_raw(self.process.id() as i32);
122 let _ = kill(pid, Signal::SIGTERM);
123 }
124
125 // Wait a bit for graceful shutdown
126 sleep(Duration::from_millis(100)).await;
127
128 // Force kill if still running
129 let _ = self.process.kill();
130 let _ = self.process.wait();
131 }
132
133 /// Find a free port to use for testing
134 fn find_free_port() -> u16 {
135 use std::net::TcpListener;
136
137 // Bind to port 0 to get a random free port
138 let listener = TcpListener::bind("127.0.0.1:0")
139 .expect("Failed to bind to random port");
140
141 let port = listener.local_addr()
142 .expect("Failed to get local address")
143 .port();
144
145 // Drop the listener to free the port
146 drop(listener);
147
148 port
149 }
150}
151
152impl Drop for TestRelay {
153 fn drop(&mut self) {
154 // Ensure process is killed when TestRelay is dropped
155 let _ = self.process.kill();
156 let _ = self.process.wait();
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[tokio::test]
165 #[ignore] // Requires relay binary to be built
166 async fn test_relay_lifecycle() {
167 let relay = TestRelay::start().await;
168 assert!(relay.url().starts_with("ws://127.0.0.1:"));
169 assert!(relay.port() > 0);
170 relay.stop().await;
171 }
172
173 #[test]
174 fn test_find_free_port() {
175 let port = TestRelay::find_free_port();
176 assert!(port > 0);
177 // Port is u16, so it's always < 65536
178 }
179}
diff --git a/tests/nip01_compliance.rs b/tests/nip01_compliance.rs
new file mode 100644
index 0000000..3e2fdd3
--- /dev/null
+++ b/tests/nip01_compliance.rs
@@ -0,0 +1,190 @@
1//! NIP-01 Compliance Integration Tests
2//!
3//! These tests verify that ngit-grasp relay implements NIP-01 correctly
4//! by using the grasp-audit library to run compliance tests.
5//!
6//! # Test Strategy
7//!
8//! - Uses grasp-audit as a library (not CLI)
9//! - Automatically manages relay lifecycle
10//! - Reuses test specs from grasp-audit (single source of truth)
11//! - Pure Rust, no shell scripts
12//!
13//! # Running Tests
14//!
15//! ```bash
16//! # Run all NIP-01 compliance tests
17//! cargo test --test nip01_compliance
18//!
19//! # Run specific test
20//! cargo test --test nip01_compliance test_nip01_smoke
21//!
22//! # With output
23//! cargo test --test nip01_compliance -- --nocapture
24//! ```
25
26mod common;
27
28use common::TestRelay;
29use grasp_audit::*;
30
31/// Test NIP-01 smoke tests against ngit-grasp relay
32///
33/// This test:
34/// 1. Starts a fresh ngit-grasp relay instance
35/// 2. Runs all NIP-01 smoke tests from grasp-audit
36/// 3. Verifies all tests pass
37/// 4. Shuts down the relay
38#[tokio::test]
39async fn test_nip01_smoke() {
40 // Start test relay
41 let relay = TestRelay::start().await;
42
43 // Create audit client in CI mode (isolated, no cleanup needed)
44 let config = AuditConfig::ci();
45 let client = AuditClient::new(relay.url(), config)
46 .await
47 .expect("Failed to create audit client");
48
49 // Run all NIP-01 smoke tests
50 let results = specs::Nip01SmokeTests::run_all(&client).await;
51
52 // Print detailed report
53 results.print_report();
54
55 // Stop relay
56 relay.stop().await;
57
58 // Assert all tests passed
59 assert!(
60 results.all_passed(),
61 "NIP-01 smoke tests failed: {}/{} passed",
62 results.passed_count(),
63 results.total_count()
64 );
65}
66
67/// Test individual NIP-01 tests can be run separately
68///
69/// This demonstrates that we can run individual tests from the specs
70/// for more granular testing or debugging.
71#[tokio::test]
72async fn test_nip01_individual_tests() {
73 use grasp_audit::specs::nip01_smoke::Nip01SmokeTests;
74
75 let relay = TestRelay::start().await;
76 let config = AuditConfig::ci();
77 let client = AuditClient::new(relay.url(), config)
78 .await
79 .expect("Failed to create audit client");
80
81 // We can't call private methods, so we'll run the full suite
82 // This test is mainly to show the pattern
83 let all_results = Nip01SmokeTests::run_all(&client).await;
84
85 relay.stop().await;
86
87 // Verify
88 assert!(all_results.all_passed());
89}
90
91/// Test that relay rejects invalid events
92///
93/// This is a critical security test - we want to ensure the relay
94/// properly validates events before accepting them.
95#[tokio::test]
96async fn test_relay_validates_events() {
97 let relay = TestRelay::start().await;
98 let config = AuditConfig::ci();
99 let client = AuditClient::new(relay.url(), config)
100 .await
101 .expect("Failed to create audit client");
102
103 // The validation tests are part of the smoke tests
104 let results = specs::Nip01SmokeTests::run_all(&client).await;
105
106 // Check that validation tests exist and pass
107 let validation_tests: Vec<_> = results
108 .results
109 .iter()
110 .filter(|t| t.spec_ref.contains("validation"))
111 .collect();
112
113 relay.stop().await;
114
115 // Should have validation tests
116 assert!(
117 !validation_tests.is_empty(),
118 "No validation tests found in NIP-01 smoke tests"
119 );
120
121 // All validation tests should pass
122 for test in validation_tests {
123 assert!(
124 test.passed,
125 "Validation test failed: {} - {}",
126 test.name,
127 test.error.as_deref().unwrap_or("unknown error")
128 );
129 }
130}
131
132/// Test relay lifecycle management
133///
134/// Ensures our test fixture properly manages relay lifecycle
135#[tokio::test]
136async fn test_relay_lifecycle() {
137 // Start relay
138 let relay = TestRelay::start().await;
139 let url = relay.url().to_string();
140
141 // Verify we can connect
142 let config = AuditConfig::ci();
143 let client = AuditClient::new(&url, config)
144 .await
145 .expect("Failed to connect to relay");
146
147 assert!(client.is_connected().await, "Client should be connected");
148
149 // Stop relay
150 relay.stop().await;
151
152 // Note: We can't easily verify disconnection without modifying grasp-audit
153 // to expose connection state after relay shutdown. That's okay - the
154 // important part is that the relay starts and stops cleanly.
155}
156
157/// Test multiple relays can run in parallel
158///
159/// This ensures our random port selection works correctly
160#[tokio::test]
161async fn test_parallel_relays() {
162 // Start two relays simultaneously
163 let relay1 = TestRelay::start().await;
164 let relay2 = TestRelay::start().await;
165
166 // Should have different URLs (different ports)
167 assert_ne!(
168 relay1.url(),
169 relay2.url(),
170 "Relays should use different ports"
171 );
172
173 // Both should be connectable
174 let config = AuditConfig::ci();
175
176 let client1 = AuditClient::new(relay1.url(), config.clone())
177 .await
178 .expect("Failed to connect to relay 1");
179
180 let client2 = AuditClient::new(relay2.url(), config)
181 .await
182 .expect("Failed to connect to relay 2");
183
184 assert!(client1.is_connected().await);
185 assert!(client2.is_connected().await);
186
187 // Clean up
188 relay1.stop().await;
189 relay2.stop().await;
190}
diff --git a/tests/nip34_announcements.rs b/tests/nip34_announcements.rs
new file mode 100644
index 0000000..6e83bb6
--- /dev/null
+++ b/tests/nip34_announcements.rs
@@ -0,0 +1,549 @@
1//! NIP-34 Repository Announcements Integration Tests (GRASP-01)
2//!
3//! Tests the acceptance and validation of repository announcements (kind 30617)
4//! and repository state announcements (kind 30618) according to GRASP-01.
5//!
6//! Reference: GRASP-01, Lines 9-20
7//!
8//! # Test Strategy
9//!
10//! - Uses TestRelay fixture for automatic relay lifecycle management
11//! - Pure Rust, no shell scripts
12//! - Tests run in parallel with isolated relay instances
13//!
14//! # Running Tests
15//!
16//! ```bash
17//! # Run all NIP-34 announcement tests
18//! cargo test --test nip34_announcements
19//!
20//! # Run specific test
21//! cargo test --test nip34_announcements test_accepts_valid_announcement
22//!
23//! # With output
24//! cargo test --test nip34_announcements -- --nocapture
25//! ```
26
27mod common;
28
29use common::TestRelay;
30use futures_util::{SinkExt, StreamExt};
31use nostr_sdk::{EventBuilder, Keys, Kind, Tag, TagKind};
32use serde_json::{json, Value};
33use tokio_tungstenite::{connect_async, tungstenite::Message};
34
35const KIND_REPOSITORY_ANNOUNCEMENT: u16 = 30617;
36const KIND_REPOSITORY_STATE: u16 = 30618;
37
38/// Helper to connect to a test relay
39async fn connect_to_relay(url: &str) -> tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>> {
40 let (ws, _) = connect_async(url)
41 .await
42 .expect("Failed to connect to relay");
43 ws
44}
45
46/// Helper to create a repository announcement event
47fn create_announcement(
48 keys: &Keys,
49 _domain: &str,
50 identifier: &str,
51 clone_urls: Vec<String>,
52 relays: Vec<String>,
53) -> nostr_sdk::Event {
54 let mut tags = vec![Tag::custom(TagKind::d(), vec![identifier.to_string()])];
55
56 for url in clone_urls {
57 tags.push(Tag::custom(
58 TagKind::Clone,
59 vec![url],
60 ));
61 }
62
63 for relay in relays {
64 tags.push(Tag::custom(TagKind::Relays, vec![relay]));
65 }
66
67 EventBuilder::new(
68 Kind::from(KIND_REPOSITORY_ANNOUNCEMENT),
69 "Test repository description",
70 )
71 .tags(tags)
72 .sign_with_keys(keys)
73 .expect("Failed to sign event")
74}
75
76/// Helper to create a repository state event
77fn create_state(keys: &Keys, identifier: &str, branches: Vec<(&str, &str)>) -> nostr_sdk::Event {
78 let mut tags = vec![Tag::custom(TagKind::d(), vec![identifier.to_string()])];
79
80 for (branch, commit) in branches {
81 tags.push(Tag::custom(
82 TagKind::Custom("ref".into()),
83 vec![format!("refs/heads/{}", branch), commit.to_string()],
84 ));
85 }
86
87 EventBuilder::new(Kind::from(KIND_REPOSITORY_STATE), "")
88 .tags(tags)
89 .sign_with_keys(keys)
90 .expect("Failed to sign event")
91}
92
93/// GRASP-01, Line 9-10: MUST serve a NIP-01 compliant nostr relay at `/`
94#[tokio::test]
95async fn test_relay_accepts_connection() {
96 let relay = TestRelay::start().await;
97
98 // Try to connect
99 let ws = connect_to_relay(relay.url()).await;
100
101 drop(ws); // Clean disconnect
102}
103
104/// GRASP-01, Line 11: MUST accept repository announcements (kind 30617)
105#[tokio::test]
106async fn test_accepts_valid_announcement() {
107 let relay = TestRelay::start().await;
108 let keys = Keys::generate();
109
110 let mut ws = connect_to_relay(relay.url()).await;
111
112 let event = create_announcement(
113 &keys,
114 &relay.domain(),
115 "test-repo",
116 vec![format!("https://{}/alice/test-repo.git", relay.domain())],
117 vec![format!("wss://{}", relay.domain())],
118 );
119
120 // Send event
121 let event_msg = json!(["EVENT", event]);
122 ws.send(Message::Text(event_msg.to_string()))
123 .await
124 .expect("Failed to send event");
125
126 // Read response
127 if let Some(Ok(Message::Text(text))) = ws.next().await {
128 let response: Value = serde_json::from_str(&text).expect("Failed to parse response");
129
130 // Should be ["OK", event_id, true, ""]
131 assert_eq!(response[0], "OK");
132 assert_eq!(response[1], event.id.to_hex());
133 if response[2] != true {
134 eprintln!("Event rejected: {}", response[3]);
135 }
136 assert_eq!(response[2], true, "Event should be accepted");
137 } else {
138 panic!("No response received");
139 }
140}
141
142/// GRASP-01, Line 12-13: MUST reject announcements that do not list the service
143/// in both `clone` and `relays` tags
144#[tokio::test]
145async fn test_rejects_announcement_without_clone() {
146 let relay = TestRelay::start().await;
147 let keys = Keys::generate();
148
149 let (mut ws, _) = connect_async(relay.url())
150 .await
151 .expect("Failed to connect");
152
153 // Missing clone tag
154 let event = create_announcement(
155 &keys,
156 &relay.domain(),
157 "test-repo",
158 vec![], // No clone URLs
159 vec![format!("wss://{}", relay.domain())],
160 );
161
162 let event_msg = json!(["EVENT", event]);
163 ws.send(Message::Text(event_msg.to_string()))
164 .await
165 .expect("Failed to send event");
166
167 if let Some(Ok(Message::Text(text))) = ws.next().await {
168 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
169
170 // Should be rejected
171 assert_eq!(response[0], "OK");
172 assert_eq!(response[1], event.id.to_hex());
173 assert_eq!(response[2], false, "Event should be rejected");
174
175 let message = response[3].as_str().unwrap();
176 assert!(
177 message.contains("clone") || message.contains("invalid"),
178 "Error message should mention clone requirement: {}",
179 message
180 );
181 } else {
182 panic!("No response received");
183 }
184}
185
186/// GRASP-01, Line 12-13: MUST reject announcements that do not list the service
187/// in both `clone` and `relays` tags
188#[tokio::test]
189async fn test_rejects_announcement_without_relay() {
190 let relay = TestRelay::start().await;
191 let keys = Keys::generate();
192
193 let (mut ws, _) = connect_async(relay.url())
194 .await
195 .expect("Failed to connect");
196
197 // Missing relay tag
198 let event = create_announcement(
199 &keys,
200 &relay.domain(),
201 "test-repo",
202 vec![format!("https://{}/alice/test-repo.git", relay.domain())],
203 vec![], // No relays
204 );
205
206 let event_msg = json!(["EVENT", event]);
207 ws.send(Message::Text(event_msg.to_string()))
208 .await
209 .expect("Failed to send event");
210
211 if let Some(Ok(Message::Text(text))) = ws.next().await {
212 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
213
214 // Should be rejected
215 assert_eq!(response[0], "OK");
216 assert_eq!(response[1], event.id.to_hex());
217 assert_eq!(response[2], false, "Event should be rejected");
218
219 let message = response[3].as_str().unwrap();
220 assert!(
221 message.contains("relays") || message.contains("invalid"),
222 "Error message should mention relay requirement: {}",
223 message
224 );
225 } else {
226 panic!("No response received");
227 }
228}
229
230/// GRASP-01, Line 12-13: MUST reject announcements listing other services
231#[tokio::test]
232async fn test_rejects_announcement_for_other_service() {
233 let relay = TestRelay::start().await;
234 let keys = Keys::generate();
235
236 let (mut ws, _) = connect_async(relay.url())
237 .await
238 .expect("Failed to connect");
239
240 // Lists different service
241 let event = create_announcement(
242 &keys,
243 &relay.domain(),
244 "test-repo",
245 vec!["https://other-service.com/alice/test-repo.git".to_string()],
246 vec!["wss://other-service.com".to_string()],
247 );
248
249 let event_msg = json!(["EVENT", event]);
250 ws.send(Message::Text(event_msg.to_string()))
251 .await
252 .expect("Failed to send event");
253
254 if let Some(Ok(Message::Text(text))) = ws.next().await {
255 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
256
257 // Should be rejected
258 assert_eq!(response[0], "OK");
259 assert_eq!(response[1], event.id.to_hex());
260 assert_eq!(response[2], false, "Event should be rejected");
261 } else {
262 panic!("No response received");
263 }
264}
265
266/// GRASP-01, Line 11: MUST accept repository state announcements (kind 30618)
267#[tokio::test]
268async fn test_accepts_valid_state() {
269 let relay = TestRelay::start().await;
270 let keys = Keys::generate();
271
272 let (mut ws, _) = connect_async(relay.url())
273 .await
274 .expect("Failed to connect");
275
276 let event = create_state(
277 &keys,
278 "test-repo",
279 vec![("main", "a1b2c3d4e5f6789012345678901234567890abcd")],
280 );
281
282 let event_msg = json!(["EVENT", event]);
283 ws.send(Message::Text(event_msg.to_string()))
284 .await
285 .expect("Failed to send event");
286
287 if let Some(Ok(Message::Text(text))) = ws.next().await {
288 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
289
290 // Should be accepted
291 assert_eq!(response[0], "OK");
292 assert_eq!(response[1], event.id.to_hex());
293 assert_eq!(response[2], true, "State event should be accepted");
294 } else {
295 panic!("No response received");
296 }
297}
298
299/// Test state event with multiple branches
300#[tokio::test]
301async fn test_accepts_state_with_multiple_branches() {
302 let relay = TestRelay::start().await;
303 let keys = Keys::generate();
304
305 let (mut ws, _) = connect_async(relay.url())
306 .await
307 .expect("Failed to connect");
308
309 let event = create_state(
310 &keys,
311 "test-repo",
312 vec![
313 ("main", "a1b2c3d4e5f6789012345678901234567890abcd"),
314 ("develop", "b2c3d4e5f6789012345678901234567890abcde"),
315 ("feature-x", "c3d4e5f6789012345678901234567890abcdef1"),
316 ],
317 );
318
319 let event_msg = json!(["EVENT", event]);
320 ws.send(Message::Text(event_msg.to_string()))
321 .await
322 .expect("Failed to send event");
323
324 if let Some(Ok(Message::Text(text))) = ws.next().await {
325 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
326
327 assert_eq!(response[0], "OK");
328 assert_eq!(response[2], true, "State event should be accepted");
329 } else {
330 panic!("No response received");
331 }
332}
333
334/// Test state event without identifier should be rejected
335#[tokio::test]
336async fn test_rejects_state_without_identifier() {
337 let relay = TestRelay::start().await;
338 let keys = Keys::generate();
339
340 let (mut ws, _) = connect_async(relay.url())
341 .await
342 .expect("Failed to connect");
343
344 // Create state without identifier
345 let event = EventBuilder::new(Kind::from(KIND_REPOSITORY_STATE), "")
346 .sign_with_keys(&keys)
347 .expect("Failed to sign event");
348
349 let event_msg = json!(["EVENT", event]);
350 ws.send(Message::Text(event_msg.to_string()))
351 .await
352 .expect("Failed to send event");
353
354 if let Some(Ok(Message::Text(text))) = ws.next().await {
355 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
356
357 // Should be rejected
358 assert_eq!(response[0], "OK");
359 assert_eq!(response[1], event.id.to_hex());
360 assert_eq!(response[2], false, "Event should be rejected");
361
362 let message = response[3].as_str().unwrap();
363 assert!(
364 message.contains("identifier") || message.contains("invalid"),
365 "Error message should mention identifier requirement: {}",
366 message
367 );
368 } else {
369 panic!("No response received");
370 }
371}
372
373/// Test querying for announcements
374#[tokio::test]
375async fn test_query_announcements() {
376 let relay = TestRelay::start().await;
377 let keys = Keys::generate();
378
379 let (mut ws, _) = connect_async(relay.url())
380 .await
381 .expect("Failed to connect");
382
383 // Send an announcement
384 let event = create_announcement(
385 &keys,
386 &relay.domain(),
387 "query-test-repo",
388 vec![format!("https://{}/alice/query-test-repo.git", relay.domain())],
389 vec![format!("wss://{}", relay.domain())],
390 );
391
392 let event_msg = json!(["EVENT", event]);
393 ws.send(Message::Text(event_msg.to_string()))
394 .await
395 .expect("Failed to send event");
396
397 // Wait for OK response
398 if let Some(Ok(Message::Text(_))) = ws.next().await {
399 // Got OK response
400 }
401
402 // Query for announcements
403 let req = json!([
404 "REQ",
405 "test-sub",
406 {
407 "kinds": [KIND_REPOSITORY_ANNOUNCEMENT],
408 "authors": [keys.public_key().to_hex()]
409 }
410 ]);
411
412 ws.send(Message::Text(req.to_string()))
413 .await
414 .expect("Failed to send REQ");
415
416 // Read responses
417 let mut found_event = false;
418 let mut got_eose = false;
419
420 for _ in 0..10 {
421 if let Some(Ok(Message::Text(text))) = ws.next().await {
422 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
423
424 if response[0] == "EVENT" {
425 assert_eq!(response[1], "test-sub");
426 found_event = true;
427 } else if response[0] == "EOSE" {
428 assert_eq!(response[1], "test-sub");
429 got_eose = true;
430 break;
431 }
432 }
433 }
434
435 assert!(found_event, "Should have received the announcement");
436 assert!(got_eose, "Should have received EOSE");
437}
438
439/// Test querying for state events
440#[tokio::test]
441async fn test_query_states() {
442 let relay = TestRelay::start().await;
443 let keys = Keys::generate();
444
445 let (mut ws, _) = connect_async(relay.url())
446 .await
447 .expect("Failed to connect");
448
449 // Send a state event
450 let event = create_state(
451 &keys,
452 "query-test-repo",
453 vec![("main", "a1b2c3d4e5f6789012345678901234567890abcd")],
454 );
455
456 let event_msg = json!(["EVENT", event]);
457 ws.send(Message::Text(event_msg.to_string()))
458 .await
459 .expect("Failed to send event");
460
461 // Wait for OK response
462 if let Some(Ok(Message::Text(_))) = ws.next().await {
463 // Got OK response
464 }
465
466 // Query for states
467 let req = json!([
468 "REQ",
469 "test-sub",
470 {
471 "kinds": [KIND_REPOSITORY_STATE],
472 "authors": [keys.public_key().to_hex()]
473 }
474 ]);
475
476 ws.send(Message::Text(req.to_string()))
477 .await
478 .expect("Failed to send REQ");
479
480 // Read responses
481 let mut found_event = false;
482 let mut got_eose = false;
483
484 for _ in 0..10 {
485 if let Some(Ok(Message::Text(text))) = ws.next().await {
486 let response: Value = serde_json::from_str(&text).expect("Failed to parse");
487
488 if response[0] == "EVENT" {
489 assert_eq!(response[1], "test-sub");
490 found_event = true;
491 } else if response[0] == "EOSE" {
492 assert_eq!(response[1], "test-sub");
493 got_eose = true;
494 break;
495 }
496 }
497 }
498
499 assert!(found_event, "Should have received the state event");
500 assert!(got_eose, "Should have received EOSE");
501}
502
503/// Test duplicate event handling
504#[tokio::test]
505async fn test_duplicate_announcement() {
506 let relay = TestRelay::start().await;
507 let keys = Keys::generate();
508
509 let (mut ws, _) = connect_async(relay.url())
510 .await
511 .expect("Failed to connect");
512
513 let event = create_announcement(
514 &keys,
515 &relay.domain(),
516 "duplicate-test",
517 vec![format!("https://{}/alice/duplicate-test.git", relay.domain())],
518 vec![format!("wss://{}", relay.domain())],
519 );
520
521 // Send first time
522 let event_msg = json!(["EVENT", event]);
523 ws.send(Message::Text(event_msg.to_string()))
524 .await
525 .expect("Failed to send event");
526
527 if let Some(Ok(Message::Text(text))) = ws.next().await {
528 let response1: Value = serde_json::from_str(&text).expect("Failed to parse");
529 assert_eq!(response1[2], true, "First send should succeed");
530 }
531
532 // Send second time (duplicate)
533 let event_msg = json!(["EVENT", event]);
534 ws.send(Message::Text(event_msg.to_string()))
535 .await
536 .expect("Failed to send event");
537
538 if let Some(Ok(Message::Text(text))) = ws.next().await {
539 let response2: Value = serde_json::from_str(&text).expect("Failed to parse");
540 assert_eq!(response2[2], true, "Duplicate should be acknowledged");
541
542 let message = response2[3].as_str().unwrap();
543 assert!(
544 message.contains("duplicate") || message.is_empty(),
545 "Should indicate duplicate: {}",
546 message
547 );
548 }
549}