tests

package
v0.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 3, 2026 License: MIT Imports: 14 Imported by: 0

README

GRAIN Testing

Integration test suite for GRAIN relay using Docker test environment.

Note: All commands in this README should be run from the tests/ directory.

Quick Start

Run the full test suite with automatic cleanup:

make test

This will:

  1. Start Docker test environment (GRAIN + MongoDB)
  2. Wait for services to be ready
  3. Run all integration tests
  4. Automatically stop environment and collect logs

For development work where you want to keep the environment running:

make test-interactive

This provides the old behavior with prompts to keep the environment up.

Manual Testing

Step-by-step testing:
# Navigate to tests directory first
cd tests/

# 1. Start test environment
make test-start

# 2. Run all tests (can repeat multiple times)
make test-run

# 3. Run specific test function
make test-single TEST=TestBasicConnection

# 4. Run all tests in a file
make test-file FILE=relay_test.go

# 5. Stop environment and collect logs
make test-stop

Test Structure

tests/                     # Run all commands from this directory
├── Makefile               # Test commands
├── README.md              # This file
├── helpers.go             # Test utilities
├── logs/                  # Generated test logs and results
├── docker/                # Test environment
│   ├── Dockerfile         # Test container build
│   └── docker-compose.yml # Test services
├── integration/           # Integration tests
│   ├── relay_test.go      # Core relay functionality
│   ├── websocket_test.go  # WebSocket connection tests
│   └── api_test.go        # HTTP API tests
└── review/                # Code quality tests
    └── codeQuality_test.go # Code review and standards

Available Commands

Command Description
make test Complete test cycle (start, test, stop, cleanup)
make test-interactive Interactive mode (keeps environment running)
make test-all Run integration + code review tests with cleanup
make test-review Run code quality review tests only
make test-start Start test environment only
make test-run Run integration tests only
make test-single TEST=TestName Run specific test function by name
make test-file FILE=file.go Run all tests in a single go file
make test-stop Stop environment and collect logs
make test-clean-logs Remove all log files
make help Show all available commands
Test Modes

Production/CI Mode (automatic cleanup):

make test      # Complete integration tests
make test-all  # Integration + code review tests

Development Mode (interactive):

make test-interactive  # Keeps environment up for multiple test runs

Manual Mode (step-by-step):

make test-start        # Start environment
make test-run          # Run tests
make test-single TEST=TestName  # Run specific tests
make test-stop         # Clean up when done

Note on test names: The TEST= parameter takes the actual Go test function name (e.g., TestRelayBasics, TestWebSocketConnection, TestEventPublishing).

Test Types

Integration Tests

Located in integration/ directory:

  • relay_test.go - Core relay functionality and connections
  • websocket_test.go - WebSocket protocol testing
  • api_test.go - HTTP API endpoint testing
Code Quality Tests

Located in review/ directory:

  • codeQuality_test.go - Code formatting, linting, and standards
  • Automatically run as part of make test-all
  • Can be run independently with make test-review

Test Environment

The test environment uses:

  • GRAIN: localhost:8181
  • MongoDB: localhost:27017
  • Environment: Ephemeral (no data persistence)
  • Containers: grain-test-relay, grain-test-mongo

Logs and Results

Test results and logs are automatically saved to the logs/ directory with timestamps:

  • test-results-YYYYMMDD_HHMMSS.log - Full test run output
  • test-TestName-YYYYMMDD_HHMMSS.log - Individual test output
  • test-file-filename-YYYYMMDD_HHMMSS.log - File-specific test output
  • review-YYYYMMDD_HHMMSS.log - Code review test results
  • grain-YYYYMMDD_HHMMSS.log - Container logs
  • debug-YYYYMMDD_HHMMSS.log - Application debug logs

Use make test-clean-logs to remove all log files when needed.

Debugging

Viewing Container Logs

While the test environment is running, you can monitor logs in real-time:

# View GRAIN logs
cd docker && docker-compose logs -f grain

# View MongoDB logs
cd docker && docker-compose logs -f mongo
Viewing GRAIN debug.log

GRAIN writes detailed logs to debug.log inside the container:

# View debug log in real-time
cd docker && docker-compose exec grain tail -f debug.log
Editing Configuration During Testing

Option 1: Edit files inside the container

# Access the container shell
cd docker && docker-compose exec grain sh

# Edit configuration files using vi
vi config.yml
vi whitelist.yml
vi blacklist.yml
vi relay_metadata.json

# To edit and save in vi:
# Press 'i' to enter insert mode, make changes
# Press 'Esc', then type ':wq' and Enter to save

Option 2: Edit locally (recommended)

# Copy config files from container to local directory
cd docker && docker-compose cp grain:/app/config.yml ./config.yml
cd docker && docker-compose cp grain:/app/whitelist.yml ./whitelist.yml
cd docker && docker-compose cp grain:/app/blacklist.yml ./blacklist.yml
cd docker && docker-compose cp grain:/app/relay_metadata.json ./relay_metadata.json

# Edit locally with your preferred editor
nano config.yml    # or code config.yml, vim config.yml, etc.

# Copy edited files back to container
cd docker && docker-compose cp ./config.yml grain:/app/config.yml
cd docker && docker-compose cp ./whitelist.yml grain:/app/whitelist.yml
cd docker && docker-compose cp ./blacklist.yml grain:/app/blacklist.yml
cd docker && docker-compose cp ./relay_metadata.json grain:/app/relay_metadata.json

Note: GRAIN watches configuration files and automatically restarts when changes are saved. No need to restart the container manually.

Database Operations
# Access MongoDB shell
cd docker && docker-compose exec mongo mongosh grain

# View database stats
cd docker && docker-compose exec mongo mongosh --eval "use grain; db.stats()"

# Backup database during testing
cd docker && docker-compose exec mongo mongodump --db grain --archive | gzip > "../logs/test-backup-$(date +%Y%m%d_%H%M%S).gz"

Writing Tests

Integration tests should:

  • Test against http://localhost:8181 (relay endpoint)
  • Test against ws://localhost:8181 (WebSocket endpoint)
  • Use helpers.go utilities for common test operations
  • Clean up any test data created

Example test structure:

func TestBasicConnection(t *testing.T) {
	client := tests.NewTestClient(t)
	defer client.Close()

	t.Log("✅ Successfully connected to relay")
}

Cross-Platform Compatibility

This test suite works on:

  • Windows (PowerShell, Command Prompt, Git Bash)
  • macOS (Terminal, iTerm2)
  • Linux (Bash, Zsh)

All Makefile commands are cross-platform compatible and handle path differences automatically.

Documentation

Index

Constants

View Source
const (
	TestRelayURL = "ws://127.0.0.1:8182"
	TestHTTPURL  = "http://127.0.0.1:8182"
)

Default (legacy) endpoints — used by the happy-path tests in relay_test.go, api_test.go, websocket_test.go.

We use 127.0.0.1 instead of "localhost" on purpose: docker-compose's port publishing on Linux binds to IPv4 0.0.0.0 only, but Go's resolver on some hosts (notably GitHub Actions ubuntu runners) returns ::1 for "localhost" first, producing intermittent "connection refused" errors as the test harness dials an IPv6 address docker never bound.

View Source
const (
	DefaultRelayURL    = "ws://127.0.0.1:8182"
	RateLimitRelayURL  = "ws://127.0.0.1:8183"
	BlacklistRelayURL  = "ws://127.0.0.1:8184"
	WhitelistRelayURL  = "ws://127.0.0.1:8185"
	AuthRelayURL       = "ws://127.0.0.1:8186"
	TimeCheckRelayURL  = "ws://127.0.0.1:8187"
	EventPurgeRelayURL = "ws://127.0.0.1:8188"
	HotReloadRelayURL  = "ws://127.0.0.1:8189"
	NIP86RelayURL      = "ws://127.0.0.1:8190"

	DefaultHTTPURL    = "http://127.0.0.1:8182"
	RateLimitHTTPURL  = "http://127.0.0.1:8183"
	BlacklistHTTPURL  = "http://127.0.0.1:8184"
	WhitelistHTTPURL  = "http://127.0.0.1:8185"
	AuthHTTPURL       = "http://127.0.0.1:8186"
	TimeCheckHTTPURL  = "http://127.0.0.1:8187"
	EventPurgeHTTPURL = "http://127.0.0.1:8188"
	HotReloadHTTPURL  = "http://127.0.0.1:8189"
	NIP86HTTPURL      = "http://127.0.0.1:8190"
)

Per-scenario relay endpoints. Each URL targets a dedicated docker-compose service backed by a config fixture in tests/docker/configs/.

View Source
const (
	NIP86OwnerSeed   = "grain-test-nip86-owner"
	NIP86AllowedSeed = "grain-test-nip86-allowed"
	NIP86BannedSeed  = "grain-test-nip86-banned"
)

NIP-86 fixture seeds. The genconfigs tool plants the pubkeys derived from these seeds into nip86-*.yml / nip86-relay_metadata.json so the integration tests can use NewDeterministicKeypair to recover the same keys. Keep these in sync with tests/genconfigs/main.go.

View Source
const WhitelistSeed = "grain-test-whitelist-allowed"

WhitelistSeed MUST match tests/genconfigs/main.go.WhitelistSeed — the whitelist-rules.yml fixture contains the pubkey derived from this seed.

Variables

AllRelayHTTPURLs is used by TestMain readiness checks.

Functions

func ContainsAny added in v0.5.0

func ContainsAny(s string, subs ...string) bool

ContainsAny reports whether s contains any of the given substrings. Handy for matching reject messages against alternatives.

func RandomSubID added in v0.5.0

func RandomSubID() string

RandomSubID generates a random subscription ID for tests.

func WaitForAllRelaysReady added in v0.5.0

func WaitForAllRelaysReady(maxAttempts int) error

WaitForAllRelaysReady pings every per-scenario relay endpoint in parallel so one slow-starting container can't block the others. Progress is logged to stderr for CI visibility.

func WaitForRelayReady

func WaitForRelayReady(t *testing.T, maxAttempts int)

WaitForRelayReady waits for the default relay to be ready.

func WaitForRelayReadyAt added in v0.5.0

func WaitForRelayReadyAt(t *testing.T, httpURL string, maxAttempts int)

WaitForRelayReadyAt waits for a specific relay endpoint to be ready. Returns an error (rather than t.Fatalf) so callers in TestMain — where no real *testing.T exists — can handle the failure themselves.

Types

type TestClient

type TestClient struct {
	// contains filtered or unexported fields
}

TestClient wraps a WebSocket connection for testing

func NewTestClient

func NewTestClient(t *testing.T) *TestClient

NewTestClient creates a new WebSocket connection to the default test relay.

func NewTestClientAt added in v0.5.0

func NewTestClientAt(t *testing.T, url string) *TestClient

NewTestClientAt creates a new WebSocket connection to the given relay URL.

func (*TestClient) Close

func (c *TestClient) Close()

Close closes the WebSocket connection

func (*TestClient) ExpectAuthChallenge added in v0.5.0

func (c *TestClient) ExpectAuthChallenge(timeout time.Duration) string

ExpectAuthChallenge returns the NIP-42 AUTH challenge. GRAIN sends the challenge proactively on connect, which NewTestClientAt captures into initialChallenge; this method returns that captured value. If none was captured (older relays or a reconnect), it falls back to reading frames until an AUTH arrives.

func (*TestClient) ExpectClosed added in v0.5.0

func (c *TestClient) ExpectClosed(subID string, timeout time.Duration) string

ExpectClosed reads messages until a CLOSED for subID is received.

func (*TestClient) ExpectEOSE added in v0.5.0

func (c *TestClient) ExpectEOSE(subID string, timeout time.Duration) []map[string]interface{}

ExpectEOSE reads messages until it gets an EOSE for the given subscription ID. Returns any EVENT messages received before the EOSE.

func (*TestClient) ExpectNotice added in v0.5.0

func (c *TestClient) ExpectNotice(timeout time.Duration) string

ExpectNotice reads messages until a NOTICE is received, then returns its message text. Fails if one doesn't arrive before timeout.

func (*TestClient) ExpectOK added in v0.5.0

func (c *TestClient) ExpectOK(eventID string, timeout time.Duration) (bool, string)

ExpectOK reads messages until it gets an OK for the given event ID. Returns (accepted bool, message string).

func (*TestClient) PerformAuth added in v0.5.0

func (c *TestClient) PerformAuth(kp *TestKeypair, relayURL string, timeout time.Duration) (bool, string)

PerformAuth executes a NIP-42 challenge/response exchange: reads the AUTH challenge, signs a kind-22242 event with the required relay + challenge tags, sends it, and waits for OK. Returns the OK message text.

func (*TestClient) ReadMessage

func (c *TestClient) ReadMessage(timeout time.Duration) []interface{}

ReadMessage reads a message from the relay with timeout

func (*TestClient) ReadMessageRaw added in v0.5.0

func (c *TestClient) ReadMessageRaw(timeout time.Duration) json.RawMessage

ReadMessageRaw reads a raw JSON message from the relay

func (*TestClient) SendEvent added in v0.5.0

func (c *TestClient) SendEvent(evt nostr.Event)

SendEvent publishes an EVENT message to the relay

func (*TestClient) SendMessage

func (c *TestClient) SendMessage(msg interface{})

SendMessage sends a message to the relay

func (*TestClient) Subscribe added in v0.5.0

func (c *TestClient) Subscribe(subID string, filters ...map[string]interface{})

Subscribe sends a REQ with the given subscription ID and filters

func (*TestClient) TryReadMessage added in v0.5.0

func (c *TestClient) TryReadMessage(timeout time.Duration) ([]interface{}, error)

TryReadMessage reads a message from the relay, returning nil and the error if the read times out or fails. Unlike ReadMessage it does not call t.Fatalf.

type TestKeypair added in v0.5.0

type TestKeypair struct {
	PrivKey *btcec.PrivateKey
	PubKey  string // hex-encoded x-only public key
}

TestKeypair holds a private key and its corresponding public key hex for testing.

func NewDeterministicKeypair added in v0.5.0

func NewDeterministicKeypair(seed string) *TestKeypair

NewDeterministicKeypair derives a keypair from a fixed seed. The same seed always produces the same pubkey, so config YAML fixtures generated by tests/genconfigs/main.go can reference it. MUST stay in sync with tests/genconfigs/main.go.DerivePubKeyHex.

func NewTestKeypair added in v0.5.0

func NewTestKeypair() *TestKeypair

NewTestKeypair generates a random keypair for testing.

func (*TestKeypair) SignEvent added in v0.5.0

func (kp *TestKeypair) SignEvent(kind int, content string, tags [][]string) nostr.Event

SignEvent creates a properly signed Nostr event.

func (*TestKeypair) SignEventAt added in v0.5.0

func (kp *TestKeypair) SignEventAt(kind int, content string, tags [][]string, createdAt int64) nostr.Event

SignEventAt signs an event with a custom created_at (for drift tests).

Directories

Path Synopsis
genconfigs writes dynamic test fixtures that depend on deterministically derived keypairs (so test Go code and config YAML agree on the same pubkey).
genconfigs writes dynamic test fixtures that depend on deterministically derived keypairs (so test Go code and config YAML agree on the same pubkey).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL