6 changed files with 346 additions and 1 deletions
@ -0,0 +1,71 @@ |
|||||||
|
# relay-tester |
||||||
|
|
||||||
|
A command-line tool for testing Nostr relay implementations against the NIP-01 specification and related NIPs. |
||||||
|
|
||||||
|
## Usage |
||||||
|
|
||||||
|
```bash |
||||||
|
relay-tester -url <relay-url> [options] |
||||||
|
``` |
||||||
|
|
||||||
|
## Options |
||||||
|
|
||||||
|
- `-url` (required): Relay websocket URL (e.g., `ws://127.0.0.1:3334` or `wss://relay.example.com`) |
||||||
|
- `-test <name>`: Run a specific test by name (default: run all tests) |
||||||
|
- `-json`: Output results in JSON format |
||||||
|
- `-v`: Verbose output (shows additional info for each test) |
||||||
|
- `-list`: List all available tests and exit |
||||||
|
|
||||||
|
## Examples |
||||||
|
|
||||||
|
### Run all tests against a local relay: |
||||||
|
```bash |
||||||
|
relay-tester -url ws://127.0.0.1:3334 |
||||||
|
``` |
||||||
|
|
||||||
|
### Run all tests with verbose output: |
||||||
|
```bash |
||||||
|
relay-tester -url ws://127.0.0.1:3334 -v |
||||||
|
``` |
||||||
|
|
||||||
|
### Run a specific test: |
||||||
|
```bash |
||||||
|
relay-tester -url ws://127.0.0.1:3334 -test "Publishes basic event" |
||||||
|
``` |
||||||
|
|
||||||
|
### Output results as JSON: |
||||||
|
```bash |
||||||
|
relay-tester -url ws://127.0.0.1:3334 -json |
||||||
|
``` |
||||||
|
|
||||||
|
### List all available tests: |
||||||
|
```bash |
||||||
|
relay-tester -list |
||||||
|
``` |
||||||
|
|
||||||
|
## Exit Codes |
||||||
|
|
||||||
|
- `0`: All required tests passed |
||||||
|
- `1`: One or more required tests failed, or an error occurred |
||||||
|
|
||||||
|
## Test Categories |
||||||
|
|
||||||
|
The relay-tester runs tests covering: |
||||||
|
|
||||||
|
- **Basic Event Operations**: Publishing, finding by ID/author/kind/tags |
||||||
|
- **Filtering**: Time ranges, limits, multiple filters, scrape queries |
||||||
|
- **Replaceable Events**: Metadata and contact list replacement |
||||||
|
- **Parameterized Replaceable Events**: Addressable events with `d` tags |
||||||
|
- **Event Deletion**: Deletion events (NIP-09) |
||||||
|
- **Ephemeral Events**: Event handling for ephemeral kinds |
||||||
|
- **EOSE Handling**: End of stored events signaling |
||||||
|
- **Event Validation**: Signature verification, ID hash verification |
||||||
|
- **JSON Compliance**: NIP-01 JSON escape sequences |
||||||
|
|
||||||
|
## Notes |
||||||
|
|
||||||
|
- Tests are run in dependency order (some tests depend on others) |
||||||
|
- Required tests must pass for the relay to be considered compliant |
||||||
|
- Optional tests may fail without affecting overall compliance |
||||||
|
- The tool connects to the relay using WebSocket and runs tests sequentially |
||||||
|
|
||||||
@ -0,0 +1,160 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"flag" |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"lol.mleku.dev/log" |
||||||
|
relaytester "next.orly.dev/relay-tester" |
||||||
|
) |
||||||
|
|
||||||
|
func main() { |
||||||
|
var ( |
||||||
|
relayURL = flag.String("url", "", "relay websocket URL (required, e.g., ws://127.0.0.1:3334)") |
||||||
|
testName = flag.String("test", "", "run specific test by name (default: run all tests)") |
||||||
|
jsonOut = flag.Bool("json", false, "output results in JSON format") |
||||||
|
verbose = flag.Bool("v", false, "verbose output") |
||||||
|
listTests = flag.Bool("list", false, "list all available tests and exit") |
||||||
|
) |
||||||
|
flag.Parse() |
||||||
|
|
||||||
|
if *listTests { |
||||||
|
listAllTests() |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if *relayURL == "" { |
||||||
|
log.E.F("required flag: -url (relay websocket URL)") |
||||||
|
flag.Usage() |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
|
||||||
|
// Validate URL format
|
||||||
|
if !strings.HasPrefix(*relayURL, "ws://") && !strings.HasPrefix(*relayURL, "wss://") { |
||||||
|
log.E.F("URL must start with ws:// or wss://") |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
|
||||||
|
// Create test suite
|
||||||
|
if *verbose { |
||||||
|
log.I.F("Creating test suite for %s...", *relayURL) |
||||||
|
} |
||||||
|
suite, err := relaytester.NewTestSuite(*relayURL) |
||||||
|
if err != nil { |
||||||
|
log.E.F("failed to create test suite: %v", err) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
|
||||||
|
// Run tests
|
||||||
|
var results []relaytester.TestResult |
||||||
|
if *testName != "" { |
||||||
|
if *verbose { |
||||||
|
log.I.F("Running test: %s", *testName) |
||||||
|
} |
||||||
|
result, err := suite.RunTest(*testName) |
||||||
|
if err != nil { |
||||||
|
log.E.F("failed to run test %s: %v", *testName, err) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
results = []relaytester.TestResult{result} |
||||||
|
} else { |
||||||
|
if *verbose { |
||||||
|
log.I.F("Running all tests...") |
||||||
|
} |
||||||
|
if results, err = suite.Run(); err != nil { |
||||||
|
log.E.F("failed to run tests: %v", err) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Output results
|
||||||
|
if *jsonOut { |
||||||
|
jsonOutput, err := relaytester.FormatJSON(results) |
||||||
|
if err != nil { |
||||||
|
log.E.F("failed to format JSON: %v", err) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
fmt.Println(jsonOutput) |
||||||
|
} else { |
||||||
|
outputResults(results, *verbose) |
||||||
|
} |
||||||
|
|
||||||
|
// Check exit code
|
||||||
|
hasRequiredFailures := false |
||||||
|
for _, result := range results { |
||||||
|
if result.Required && !result.Pass { |
||||||
|
hasRequiredFailures = true |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if hasRequiredFailures { |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func outputResults(results []relaytester.TestResult, verbose bool) { |
||||||
|
passed := 0 |
||||||
|
failed := 0 |
||||||
|
requiredFailed := 0 |
||||||
|
|
||||||
|
for _, result := range results { |
||||||
|
if result.Pass { |
||||||
|
passed++ |
||||||
|
if verbose { |
||||||
|
fmt.Printf("PASS: %s", result.Name) |
||||||
|
if result.Info != "" { |
||||||
|
fmt.Printf(" - %s", result.Info) |
||||||
|
} |
||||||
|
fmt.Println() |
||||||
|
} else { |
||||||
|
fmt.Printf("PASS: %s\n", result.Name) |
||||||
|
} |
||||||
|
} else { |
||||||
|
failed++ |
||||||
|
if result.Required { |
||||||
|
requiredFailed++ |
||||||
|
fmt.Printf("FAIL (required): %s", result.Name) |
||||||
|
} else { |
||||||
|
fmt.Printf("FAIL (optional): %s", result.Name) |
||||||
|
} |
||||||
|
if result.Info != "" { |
||||||
|
fmt.Printf(" - %s", result.Info) |
||||||
|
} |
||||||
|
fmt.Println() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fmt.Println() |
||||||
|
fmt.Println("Test Summary:") |
||||||
|
fmt.Printf(" Total: %d\n", len(results)) |
||||||
|
fmt.Printf(" Passed: %d\n", passed) |
||||||
|
fmt.Printf(" Failed: %d\n", failed) |
||||||
|
fmt.Printf(" Required Failed: %d\n", requiredFailed) |
||||||
|
} |
||||||
|
|
||||||
|
func listAllTests() { |
||||||
|
// Create a dummy test suite to get the list of tests
|
||||||
|
suite, err := relaytester.NewTestSuite("ws://127.0.0.1:0") |
||||||
|
if err != nil { |
||||||
|
log.E.F("failed to create test suite: %v", err) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
|
||||||
|
fmt.Println("Available tests:") |
||||||
|
fmt.Println() |
||||||
|
|
||||||
|
testNames := suite.ListTests() |
||||||
|
testInfo := suite.GetTestNames() |
||||||
|
|
||||||
|
for _, name := range testNames { |
||||||
|
required := "" |
||||||
|
if testInfo[name] { |
||||||
|
required = " (required)" |
||||||
|
} |
||||||
|
fmt.Printf(" - %s%s\n", name, required) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
Loading…
Reference in new issue