Browse Source
- Introduced a new `relaytester` package to facilitate testing of relay functionalities. - Implemented a `TestSuite` structure to manage and execute various test cases against the relay. - Added multiple test cases for event publishing, retrieval, and validation, ensuring comprehensive coverage of relay behavior. - Created utility functions for generating key pairs and events, enhancing test reliability and maintainability. - Established a WebSocket client for interacting with the relay during tests, including subscription and message handling. - Included JSON formatting for test results to improve output readability. - This commit lays the groundwork for robust integration testing of relay features.main
5 changed files with 1425 additions and 0 deletions
@ -0,0 +1,280 @@
@@ -0,0 +1,280 @@
|
||||
package relaytester |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/gorilla/websocket" |
||||
"lol.mleku.dev/errorf" |
||||
"next.orly.dev/pkg/encoders/event" |
||||
"next.orly.dev/pkg/encoders/hex" |
||||
) |
||||
|
||||
// Client wraps a WebSocket connection to a relay for testing.
|
||||
type Client struct { |
||||
conn *websocket.Conn |
||||
url string |
||||
mu sync.Mutex |
||||
subs map[string]chan []byte |
||||
ctx context.Context |
||||
cancel context.CancelFunc |
||||
} |
||||
|
||||
// NewClient creates a new test client connected to the relay.
|
||||
func NewClient(url string) (c *Client, err error) { |
||||
ctx, cancel := context.WithCancel(context.Background()) |
||||
var conn *websocket.Conn |
||||
dialer := websocket.Dialer{ |
||||
HandshakeTimeout: 5 * time.Second, |
||||
} |
||||
if conn, _, err = dialer.Dial(url, nil); err != nil { |
||||
cancel() |
||||
return |
||||
} |
||||
c = &Client{ |
||||
conn: conn, |
||||
url: url, |
||||
subs: make(map[string]chan []byte), |
||||
ctx: ctx, |
||||
cancel: cancel, |
||||
} |
||||
go c.readLoop() |
||||
return |
||||
} |
||||
|
||||
// Close closes the client connection.
|
||||
func (c *Client) Close() error { |
||||
c.cancel() |
||||
return c.conn.Close() |
||||
} |
||||
|
||||
// Send sends a JSON message to the relay.
|
||||
func (c *Client) Send(msg interface{}) (err error) { |
||||
c.mu.Lock() |
||||
defer c.mu.Unlock() |
||||
var data []byte |
||||
if data, err = json.Marshal(msg); err != nil { |
||||
return errorf.E("failed to marshal message: %w", err) |
||||
} |
||||
if err = c.conn.WriteMessage(websocket.TextMessage, data); err != nil { |
||||
return errorf.E("failed to write message: %w", err) |
||||
} |
||||
return |
||||
} |
||||
|
||||
// readLoop reads messages from the relay and routes them to subscriptions.
|
||||
func (c *Client) readLoop() { |
||||
defer c.conn.Close() |
||||
for { |
||||
select { |
||||
case <-c.ctx.Done(): |
||||
return |
||||
default: |
||||
} |
||||
_, msg, err := c.conn.ReadMessage() |
||||
if err != nil { |
||||
return |
||||
} |
||||
var raw []interface{} |
||||
if err = json.Unmarshal(msg, &raw); err != nil { |
||||
continue |
||||
} |
||||
if len(raw) < 2 { |
||||
continue |
||||
} |
||||
typ, ok := raw[0].(string) |
||||
if !ok { |
||||
continue |
||||
} |
||||
c.mu.Lock() |
||||
switch typ { |
||||
case "EVENT": |
||||
if len(raw) >= 2 { |
||||
if subID, ok := raw[1].(string); ok { |
||||
if ch, exists := c.subs[subID]; exists { |
||||
select { |
||||
case ch <- msg: |
||||
default: |
||||
} |
||||
} |
||||
} |
||||
} |
||||
case "EOSE": |
||||
if len(raw) >= 2 { |
||||
if subID, ok := raw[1].(string); ok { |
||||
if ch, exists := c.subs[subID]; exists { |
||||
close(ch) |
||||
} |
||||
} |
||||
} |
||||
case "OK": |
||||
// OK messages are handled by WaitForOK
|
||||
case "NOTICE": |
||||
// Notice messages are logged
|
||||
case "CLOSED": |
||||
// Closed messages indicate subscription ended
|
||||
case "AUTH": |
||||
// Auth challenge messages
|
||||
} |
||||
c.mu.Unlock() |
||||
} |
||||
} |
||||
|
||||
// Subscribe creates a subscription and returns a channel for events.
|
||||
func (c *Client) Subscribe(subID string, filters []interface{}) (ch chan []byte, err error) { |
||||
req := []interface{}{"REQ", subID} |
||||
req = append(req, filters...) |
||||
if err = c.Send(req); err != nil { |
||||
return |
||||
} |
||||
c.mu.Lock() |
||||
ch = make(chan []byte, 100) |
||||
c.subs[subID] = ch |
||||
c.mu.Unlock() |
||||
return |
||||
} |
||||
|
||||
// Unsubscribe closes a subscription.
|
||||
func (c *Client) Unsubscribe(subID string) error { |
||||
c.mu.Lock() |
||||
if ch, exists := c.subs[subID]; exists { |
||||
close(ch) |
||||
delete(c.subs, subID) |
||||
} |
||||
c.mu.Unlock() |
||||
return c.Send([]interface{}{"CLOSE", subID}) |
||||
} |
||||
|
||||
// Publish sends an EVENT message to the relay.
|
||||
func (c *Client) Publish(ev *event.E) (err error) { |
||||
evJSON, err := json.Marshal(ev.Serialize()) |
||||
if err != nil { |
||||
return errorf.E("failed to marshal event: %w", err) |
||||
} |
||||
var evMap map[string]interface{} |
||||
if err = json.Unmarshal(evJSON, &evMap); err != nil { |
||||
return errorf.E("failed to unmarshal event: %w", err) |
||||
} |
||||
return c.Send([]interface{}{"EVENT", evMap}) |
||||
} |
||||
|
||||
// WaitForOK waits for an OK response for the given event ID.
|
||||
func (c *Client) WaitForOK(eventID []byte, timeout time.Duration) (accepted bool, reason string, err error) { |
||||
ctx, cancel := context.WithTimeout(c.ctx, timeout) |
||||
defer cancel() |
||||
idStr := hex.Enc(eventID) |
||||
for { |
||||
select { |
||||
case <-ctx.Done(): |
||||
return false, "", errorf.E("timeout waiting for OK response") |
||||
default: |
||||
} |
||||
var msg []byte |
||||
_, msg, err = c.conn.ReadMessage() |
||||
if err != nil { |
||||
return false, "", errorf.E("connection closed: %w", err) |
||||
} |
||||
var raw []interface{} |
||||
if err = json.Unmarshal(msg, &raw); err != nil { |
||||
continue |
||||
} |
||||
if len(raw) < 3 { |
||||
continue |
||||
} |
||||
if typ, ok := raw[0].(string); ok && typ == "OK" { |
||||
if id, ok := raw[1].(string); ok && id == idStr { |
||||
accepted, _ = raw[2].(bool) |
||||
if len(raw) > 3 { |
||||
reason, _ = raw[3].(string) |
||||
} |
||||
return |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Count sends a COUNT request and returns the count.
|
||||
func (c *Client) Count(filters []interface{}) (count int64, err error) { |
||||
req := []interface{}{"COUNT", "count-sub"} |
||||
req = append(req, filters...) |
||||
if err = c.Send(req); err != nil { |
||||
return |
||||
} |
||||
ctx, cancel := context.WithTimeout(c.ctx, 5*time.Second) |
||||
defer cancel() |
||||
for { |
||||
select { |
||||
case <-ctx.Done(): |
||||
return 0, errorf.E("timeout waiting for COUNT response") |
||||
default: |
||||
} |
||||
_, msg, err := c.conn.ReadMessage() |
||||
if err != nil { |
||||
return 0, errorf.E("connection closed: %w", err) |
||||
} |
||||
var raw []interface{} |
||||
if err = json.Unmarshal(msg, &raw); err != nil { |
||||
continue |
||||
} |
||||
if len(raw) >= 3 { |
||||
if typ, ok := raw[0].(string); ok && typ == "COUNT" { |
||||
if subID, ok := raw[1].(string); ok && subID == "count-sub" { |
||||
if countObj, ok := raw[2].(map[string]interface{}); ok { |
||||
if c, ok := countObj["count"].(float64); ok { |
||||
return int64(c), nil |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Auth sends an AUTH message with the signed event.
|
||||
func (c *Client) Auth(ev *event.E) error { |
||||
evJSON, err := json.Marshal(ev.Serialize()) |
||||
if err != nil { |
||||
return errorf.E("failed to marshal event: %w", err) |
||||
} |
||||
var evMap map[string]interface{} |
||||
if err = json.Unmarshal(evJSON, &evMap); err != nil { |
||||
return errorf.E("failed to unmarshal event: %w", err) |
||||
} |
||||
return c.Send([]interface{}{"AUTH", evMap}) |
||||
} |
||||
|
||||
// GetEvents collects all events from a subscription until EOSE.
|
||||
func (c *Client) GetEvents(subID string, filters []interface{}, timeout time.Duration) (events []*event.E, err error) { |
||||
ch, err := c.Subscribe(subID, filters) |
||||
if err != nil { |
||||
return |
||||
} |
||||
defer c.Unsubscribe(subID) |
||||
ctx, cancel := context.WithTimeout(c.ctx, timeout) |
||||
defer cancel() |
||||
for { |
||||
select { |
||||
case <-ctx.Done(): |
||||
return events, nil |
||||
case msg, ok := <-ch: |
||||
if !ok { |
||||
return events, nil |
||||
} |
||||
var raw []interface{} |
||||
if err = json.Unmarshal(msg, &raw); err != nil { |
||||
continue |
||||
} |
||||
if len(raw) >= 3 && raw[0] == "EVENT" { |
||||
if evData, ok := raw[2].(map[string]interface{}); ok { |
||||
evJSON, _ := json.Marshal(evData) |
||||
ev := event.New() |
||||
if _, err = ev.Unmarshal(evJSON); err == nil { |
||||
events = append(events, ev) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,130 @@
@@ -0,0 +1,130 @@
|
||||
package relaytester |
||||
|
||||
import ( |
||||
"crypto/rand" |
||||
"fmt" |
||||
"time" |
||||
|
||||
"lol.mleku.dev/chk" |
||||
"next.orly.dev/pkg/crypto/p256k" |
||||
"next.orly.dev/pkg/encoders/bech32encoding" |
||||
"next.orly.dev/pkg/encoders/event" |
||||
"next.orly.dev/pkg/encoders/hex" |
||||
"next.orly.dev/pkg/encoders/kind" |
||||
"next.orly.dev/pkg/encoders/tag" |
||||
) |
||||
|
||||
// KeyPair represents a test keypair.
|
||||
type KeyPair struct { |
||||
Secret *p256k.Signer |
||||
Pubkey []byte |
||||
Nsec string |
||||
Npub string |
||||
} |
||||
|
||||
// GenerateKeyPair generates a new keypair for testing.
|
||||
func GenerateKeyPair() (kp *KeyPair, err error) { |
||||
kp = &KeyPair{} |
||||
kp.Secret = &p256k.Signer{} |
||||
if err = kp.Secret.Generate(); chk.E(err) { |
||||
return |
||||
} |
||||
kp.Pubkey = kp.Secret.Pub() |
||||
nsecBytes, err := bech32encoding.BinToNsec(kp.Secret.Sec()) |
||||
if chk.E(err) { |
||||
return |
||||
} |
||||
kp.Nsec = string(nsecBytes) |
||||
npubBytes, err := bech32encoding.BinToNpub(kp.Pubkey) |
||||
if chk.E(err) { |
||||
return |
||||
} |
||||
kp.Npub = string(npubBytes) |
||||
return |
||||
} |
||||
|
||||
// CreateEvent creates a signed event with the given parameters.
|
||||
func CreateEvent(signer *p256k.Signer, kindNum uint16, content string, tags *tag.S) (ev *event.E, err error) { |
||||
ev = event.New() |
||||
ev.CreatedAt = time.Now().Unix() |
||||
ev.Kind = kindNum |
||||
ev.Content = []byte(content) |
||||
if tags != nil { |
||||
ev.Tags = tags |
||||
} else { |
||||
ev.Tags = tag.NewS() |
||||
} |
||||
if err = ev.Sign(signer); chk.E(err) { |
||||
return |
||||
} |
||||
return |
||||
} |
||||
|
||||
// CreateEventWithTags creates an event with specific tags.
|
||||
func CreateEventWithTags(signer *p256k.Signer, kindNum uint16, content string, tagPairs [][]string) (ev *event.E, err error) { |
||||
tags := tag.NewS() |
||||
for _, pair := range tagPairs { |
||||
if len(pair) >= 2 { |
||||
// Build tag fields as []byte variadic arguments
|
||||
tagFields := make([][]byte, len(pair)) |
||||
tagFields[0] = []byte(pair[0]) |
||||
for i := 1; i < len(pair); i++ { |
||||
tagFields[i] = []byte(pair[i]) |
||||
} |
||||
tags.Append(tag.NewFromBytesSlice(tagFields...)) |
||||
} |
||||
} |
||||
return CreateEvent(signer, kindNum, content, tags) |
||||
} |
||||
|
||||
// CreateReplaceableEvent creates a replaceable event (kind 0-3, 10000-19999).
|
||||
func CreateReplaceableEvent(signer *p256k.Signer, kindNum uint16, content string) (ev *event.E, err error) { |
||||
return CreateEvent(signer, kindNum, content, nil) |
||||
} |
||||
|
||||
// CreateEphemeralEvent creates an ephemeral event (kind 20000-29999).
|
||||
func CreateEphemeralEvent(signer *p256k.Signer, kindNum uint16, content string) (ev *event.E, err error) { |
||||
return CreateEvent(signer, kindNum, content, nil) |
||||
} |
||||
|
||||
// CreateDeleteEvent creates a deletion event (kind 5).
|
||||
func CreateDeleteEvent(signer *p256k.Signer, eventIDs [][]byte, reason string) (ev *event.E, err error) { |
||||
tags := tag.NewS() |
||||
for _, id := range eventIDs { |
||||
tags.Append(tag.NewFromBytesSlice([]byte("e"), id)) |
||||
} |
||||
if reason != "" { |
||||
tags.Append(tag.NewFromBytesSlice([]byte("content"), []byte(reason))) |
||||
} |
||||
return CreateEvent(signer, kind.EventDeletion.K, reason, tags) |
||||
} |
||||
|
||||
// CreateParameterizedReplaceableEvent creates a parameterized replaceable event (kind 30000-39999).
|
||||
func CreateParameterizedReplaceableEvent(signer *p256k.Signer, kindNum uint16, content string, dTag string) (ev *event.E, err error) { |
||||
tags := tag.NewS() |
||||
tags.Append(tag.NewFromBytesSlice([]byte("d"), []byte(dTag))) |
||||
return CreateEvent(signer, kindNum, content, tags) |
||||
} |
||||
|
||||
// RandomID generates a random 32-byte ID.
|
||||
func RandomID() (id []byte, err error) { |
||||
id = make([]byte, 32) |
||||
if _, err = rand.Read(id); err != nil { |
||||
return nil, fmt.Errorf("failed to generate random ID: %w", err) |
||||
} |
||||
return |
||||
} |
||||
|
||||
// MustHex decodes a hex string or panics.
|
||||
func MustHex(s string) []byte { |
||||
b, err := hex.Dec(s) |
||||
if err != nil { |
||||
panic(fmt.Sprintf("invalid hex: %s", s)) |
||||
} |
||||
return b |
||||
} |
||||
|
||||
// HexID returns the hex-encoded event ID.
|
||||
func HexID(ev *event.E) string { |
||||
return hex.Enc(ev.ID) |
||||
} |
||||
@ -0,0 +1,261 @@
@@ -0,0 +1,261 @@
|
||||
package relaytester |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"time" |
||||
|
||||
"lol.mleku.dev/errorf" |
||||
) |
||||
|
||||
// TestResult represents the result of a test.
|
||||
type TestResult struct { |
||||
Name string `json:"test"` |
||||
Pass bool `json:"pass"` |
||||
Required bool `json:"required"` |
||||
Info string `json:"info,omitempty"` |
||||
} |
||||
|
||||
// TestFunc is a function that runs a test case.
|
||||
type TestFunc func(client *Client, key1, key2 *KeyPair) (result TestResult) |
||||
|
||||
// TestCase represents a test case with dependencies.
|
||||
type TestCase struct { |
||||
Name string |
||||
Required bool |
||||
Func TestFunc |
||||
Dependencies []string // Names of tests that must run before this one
|
||||
} |
||||
|
||||
// TestSuite runs all tests against a relay.
|
||||
type TestSuite struct { |
||||
relayURL string |
||||
key1 *KeyPair |
||||
key2 *KeyPair |
||||
tests map[string]*TestCase |
||||
results map[string]TestResult |
||||
order []string |
||||
} |
||||
|
||||
// NewTestSuite creates a new test suite.
|
||||
func NewTestSuite(relayURL string) (suite *TestSuite, err error) { |
||||
suite = &TestSuite{ |
||||
relayURL: relayURL, |
||||
tests: make(map[string]*TestCase), |
||||
results: make(map[string]TestResult), |
||||
} |
||||
if suite.key1, err = GenerateKeyPair(); err != nil { |
||||
return |
||||
} |
||||
if suite.key2, err = GenerateKeyPair(); err != nil { |
||||
return |
||||
} |
||||
suite.registerTests() |
||||
return |
||||
} |
||||
|
||||
// AddTest adds a test case to the suite.
|
||||
func (s *TestSuite) AddTest(tc *TestCase) { |
||||
s.tests[tc.Name] = tc |
||||
} |
||||
|
||||
// registerTests registers all test cases.
|
||||
func (s *TestSuite) registerTests() { |
||||
allTests := []*TestCase{ |
||||
{ |
||||
Name: "Publishes basic event", |
||||
Required: true, |
||||
Func: testPublishBasicEvent, |
||||
}, |
||||
{ |
||||
Name: "Finds event by ID", |
||||
Required: true, |
||||
Func: testFindByID, |
||||
Dependencies: []string{"Publishes basic event"}, |
||||
}, |
||||
{ |
||||
Name: "Finds event by author", |
||||
Required: true, |
||||
Func: testFindByAuthor, |
||||
Dependencies: []string{"Publishes basic event"}, |
||||
}, |
||||
{ |
||||
Name: "Finds event by kind", |
||||
Required: true, |
||||
Func: testFindByKind, |
||||
Dependencies: []string{"Publishes basic event"}, |
||||
}, |
||||
{ |
||||
Name: "Finds event by tags", |
||||
Required: true, |
||||
Func: testFindByTags, |
||||
Dependencies: []string{"Publishes basic event"}, |
||||
}, |
||||
{ |
||||
Name: "Finds by multiple tags", |
||||
Required: true, |
||||
Func: testFindByMultipleTags, |
||||
Dependencies: []string{"Publishes basic event"}, |
||||
}, |
||||
{ |
||||
Name: "Finds by time range", |
||||
Required: true, |
||||
Func: testFindByTimeRange, |
||||
Dependencies: []string{"Publishes basic event"}, |
||||
}, |
||||
{ |
||||
Name: "Rejects invalid signature", |
||||
Required: true, |
||||
Func: testRejectInvalidSignature, |
||||
}, |
||||
{ |
||||
Name: "Rejects future event", |
||||
Required: true, |
||||
Func: testRejectFutureEvent, |
||||
}, |
||||
{ |
||||
Name: "Rejects expired event", |
||||
Required: false, |
||||
Func: testRejectExpiredEvent, |
||||
}, |
||||
{ |
||||
Name: "Handles replaceable events", |
||||
Required: true, |
||||
Func: testReplaceableEvents, |
||||
}, |
||||
{ |
||||
Name: "Handles ephemeral events", |
||||
Required: false, |
||||
Func: testEphemeralEvents, |
||||
}, |
||||
{ |
||||
Name: "Handles parameterized replaceable events", |
||||
Required: true, |
||||
Func: testParameterizedReplaceableEvents, |
||||
}, |
||||
{ |
||||
Name: "Handles deletion events", |
||||
Required: true, |
||||
Func: testDeletionEvents, |
||||
Dependencies: []string{"Publishes basic event"}, |
||||
}, |
||||
{ |
||||
Name: "Handles COUNT request", |
||||
Required: true, |
||||
Func: testCountRequest, |
||||
Dependencies: []string{"Publishes basic event"}, |
||||
}, |
||||
{ |
||||
Name: "Handles limit parameter", |
||||
Required: true, |
||||
Func: testLimitParameter, |
||||
Dependencies: []string{"Publishes basic event"}, |
||||
}, |
||||
{ |
||||
Name: "Handles multiple filters", |
||||
Required: true, |
||||
Func: testMultipleFilters, |
||||
Dependencies: []string{"Publishes basic event"}, |
||||
}, |
||||
{ |
||||
Name: "Handles subscription close", |
||||
Required: true, |
||||
Func: testSubscriptionClose, |
||||
}, |
||||
} |
||||
for _, tc := range allTests { |
||||
s.AddTest(tc) |
||||
} |
||||
s.topologicalSort() |
||||
} |
||||
|
||||
// topologicalSort orders tests based on dependencies.
|
||||
func (s *TestSuite) topologicalSort() { |
||||
visited := make(map[string]bool) |
||||
temp := make(map[string]bool) |
||||
var visit func(name string) |
||||
visit = func(name string) { |
||||
if temp[name] { |
||||
return |
||||
} |
||||
if visited[name] { |
||||
return |
||||
} |
||||
temp[name] = true |
||||
if tc, exists := s.tests[name]; exists { |
||||
for _, dep := range tc.Dependencies { |
||||
visit(dep) |
||||
} |
||||
} |
||||
temp[name] = false |
||||
visited[name] = true |
||||
s.order = append(s.order, name) |
||||
} |
||||
for name := range s.tests { |
||||
if !visited[name] { |
||||
visit(name) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Run runs all tests in the suite.
|
||||
func (s *TestSuite) Run() (results []TestResult, err error) { |
||||
client, err := NewClient(s.relayURL) |
||||
if err != nil { |
||||
return nil, errorf.E("failed to connect to relay: %w", err) |
||||
} |
||||
defer client.Close() |
||||
for _, name := range s.order { |
||||
tc := s.tests[name] |
||||
if tc == nil { |
||||
continue |
||||
} |
||||
result := tc.Func(client, s.key1, s.key2) |
||||
result.Name = name |
||||
result.Required = tc.Required |
||||
s.results[name] = result |
||||
results = append(results, result) |
||||
time.Sleep(100 * time.Millisecond) // Small delay between tests
|
||||
} |
||||
return |
||||
} |
||||
|
||||
// RunTest runs a specific test by name.
|
||||
func (s *TestSuite) RunTest(testName string) (result TestResult, err error) { |
||||
tc, exists := s.tests[testName] |
||||
if !exists { |
||||
return result, errorf.E("test %s not found", testName) |
||||
} |
||||
// Check dependencies
|
||||
for _, dep := range tc.Dependencies { |
||||
if _, exists := s.results[dep]; !exists { |
||||
return result, errorf.E("test %s depends on %s which has not been run", testName, dep) |
||||
} |
||||
if !s.results[dep].Pass { |
||||
return result, errorf.E("test %s depends on %s which failed", testName, dep) |
||||
} |
||||
} |
||||
client, err := NewClient(s.relayURL) |
||||
if err != nil { |
||||
return result, errorf.E("failed to connect to relay: %w", err) |
||||
} |
||||
defer client.Close() |
||||
result = tc.Func(client, s.key1, s.key2) |
||||
result.Name = testName |
||||
result.Required = tc.Required |
||||
s.results[testName] = result |
||||
return |
||||
} |
||||
|
||||
// GetResults returns all test results.
|
||||
func (s *TestSuite) GetResults() map[string]TestResult { |
||||
return s.results |
||||
} |
||||
|
||||
// FormatJSON formats results as JSON.
|
||||
func FormatJSON(results []TestResult) (output string, err error) { |
||||
var data []byte |
||||
if data, err = json.Marshal(results); err != nil { |
||||
return |
||||
} |
||||
return string(data), nil |
||||
} |
||||
@ -0,0 +1,547 @@
@@ -0,0 +1,547 @@
|
||||
package relaytester |
||||
|
||||
import ( |
||||
"fmt" |
||||
"time" |
||||
|
||||
"next.orly.dev/pkg/encoders/hex" |
||||
"next.orly.dev/pkg/encoders/kind" |
||||
"next.orly.dev/pkg/encoders/tag" |
||||
) |
||||
|
||||
// Test implementations - these are referenced by test.go
|
||||
|
||||
func testPublishBasicEvent(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "test content", nil) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
if err = client.Publish(ev); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, reason, err := client.WaitForOK(ev.ID, 5*time.Second) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
||||
} |
||||
if !accepted { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("event rejected: %s", reason)} |
||||
} |
||||
return TestResult{Pass: true} |
||||
} |
||||
|
||||
func testFindByID(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "find by id test", nil) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
if err = client.Publish(ev); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
||||
if err != nil || !accepted { |
||||
return TestResult{Pass: false, Info: "event not accepted"} |
||||
} |
||||
time.Sleep(200 * time.Millisecond) |
||||
filter := map[string]interface{}{ |
||||
"ids": []string{hex.Enc(ev.ID)}, |
||||
} |
||||
events, err := client.GetEvents("test-sub", []interface{}{filter}, 2*time.Second) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
||||
} |
||||
found := false |
||||
for _, e := range events { |
||||
if string(e.ID) == string(ev.ID) { |
||||
found = true |
||||
break |
||||
} |
||||
} |
||||
if !found { |
||||
return TestResult{Pass: false, Info: "event not found by ID"} |
||||
} |
||||
return TestResult{Pass: true} |
||||
} |
||||
|
||||
func testFindByAuthor(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "find by author test", nil) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
if err = client.Publish(ev); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
||||
if err != nil || !accepted { |
||||
return TestResult{Pass: false, Info: "event not accepted"} |
||||
} |
||||
time.Sleep(200 * time.Millisecond) |
||||
filter := map[string]interface{}{ |
||||
"authors": []string{hex.Enc(key1.Pubkey)}, |
||||
} |
||||
events, err := client.GetEvents("test-author", []interface{}{filter}, 2*time.Second) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
||||
} |
||||
found := false |
||||
for _, e := range events { |
||||
if string(e.ID) == string(ev.ID) { |
||||
found = true |
||||
break |
||||
} |
||||
} |
||||
if !found { |
||||
return TestResult{Pass: false, Info: "event not found by author"} |
||||
} |
||||
return TestResult{Pass: true} |
||||
} |
||||
|
||||
func testFindByKind(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "find by kind test", nil) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
if err = client.Publish(ev); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
||||
if err != nil || !accepted { |
||||
return TestResult{Pass: false, Info: "event not accepted"} |
||||
} |
||||
time.Sleep(200 * time.Millisecond) |
||||
filter := map[string]interface{}{ |
||||
"kinds": []int{int(kind.TextNote.K)}, |
||||
} |
||||
events, err := client.GetEvents("test-kind", []interface{}{filter}, 2*time.Second) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
||||
} |
||||
found := false |
||||
for _, e := range events { |
||||
if string(e.ID) == string(ev.ID) { |
||||
found = true |
||||
break |
||||
} |
||||
} |
||||
if !found { |
||||
return TestResult{Pass: false, Info: "event not found by kind"} |
||||
} |
||||
return TestResult{Pass: true} |
||||
} |
||||
|
||||
func testFindByTags(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
tags := tag.NewS(tag.NewFromBytesSlice([]byte("t"), []byte("test-tag"))) |
||||
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "find by tags test", tags) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
if err = client.Publish(ev); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
||||
if err != nil || !accepted { |
||||
return TestResult{Pass: false, Info: "event not accepted"} |
||||
} |
||||
time.Sleep(200 * time.Millisecond) |
||||
filter := map[string]interface{}{ |
||||
"#t": []string{"test-tag"}, |
||||
} |
||||
events, err := client.GetEvents("test-tags", []interface{}{filter}, 2*time.Second) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
||||
} |
||||
found := false |
||||
for _, e := range events { |
||||
if string(e.ID) == string(ev.ID) { |
||||
found = true |
||||
break |
||||
} |
||||
} |
||||
if !found { |
||||
return TestResult{Pass: false, Info: "event not found by tags"} |
||||
} |
||||
return TestResult{Pass: true} |
||||
} |
||||
|
||||
func testFindByMultipleTags(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
tags := tag.NewS( |
||||
tag.NewFromBytesSlice([]byte("t"), []byte("multi-tag-1")), |
||||
tag.NewFromBytesSlice([]byte("t"), []byte("multi-tag-2")), |
||||
) |
||||
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "find by multiple tags test", tags) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
if err = client.Publish(ev); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
||||
if err != nil || !accepted { |
||||
return TestResult{Pass: false, Info: "event not accepted"} |
||||
} |
||||
time.Sleep(200 * time.Millisecond) |
||||
filter := map[string]interface{}{ |
||||
"#t": []string{"multi-tag-1", "multi-tag-2"}, |
||||
} |
||||
events, err := client.GetEvents("test-multi-tags", []interface{}{filter}, 2*time.Second) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
||||
} |
||||
found := false |
||||
for _, e := range events { |
||||
if string(e.ID) == string(ev.ID) { |
||||
found = true |
||||
break |
||||
} |
||||
} |
||||
if !found { |
||||
return TestResult{Pass: false, Info: "event not found by multiple tags"} |
||||
} |
||||
return TestResult{Pass: true} |
||||
} |
||||
|
||||
func testFindByTimeRange(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "find by time range test", nil) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
if err = client.Publish(ev); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
||||
if err != nil || !accepted { |
||||
return TestResult{Pass: false, Info: "event not accepted"} |
||||
} |
||||
time.Sleep(200 * time.Millisecond) |
||||
now := time.Now().Unix() |
||||
filter := map[string]interface{}{ |
||||
"since": now - 3600, |
||||
"until": now + 3600, |
||||
} |
||||
events, err := client.GetEvents("test-time", []interface{}{filter}, 2*time.Second) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
||||
} |
||||
found := false |
||||
for _, e := range events { |
||||
if string(e.ID) == string(ev.ID) { |
||||
found = true |
||||
break |
||||
} |
||||
} |
||||
if !found { |
||||
return TestResult{Pass: false, Info: "event not found by time range"} |
||||
} |
||||
return TestResult{Pass: true} |
||||
} |
||||
|
||||
func testRejectInvalidSignature(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "invalid sig test", nil) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
// Corrupt the signature
|
||||
ev.Sig[0] ^= 0xFF |
||||
if err = client.Publish(ev); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, reason, err := client.WaitForOK(ev.ID, 5*time.Second) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
||||
} |
||||
if accepted { |
||||
return TestResult{Pass: false, Info: "invalid signature was accepted"} |
||||
} |
||||
_ = reason |
||||
return TestResult{Pass: true} |
||||
} |
||||
|
||||
func testRejectFutureEvent(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "future event test", nil) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
ev.CreatedAt = time.Now().Unix() + 3600 // 1 hour in the future
|
||||
// Re-sign with new timestamp
|
||||
if err = ev.Sign(key1.Secret); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to re-sign: %v", err)} |
||||
} |
||||
if err = client.Publish(ev); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, reason, err := client.WaitForOK(ev.ID, 5*time.Second) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
||||
} |
||||
if accepted { |
||||
return TestResult{Pass: false, Info: "future event was accepted"} |
||||
} |
||||
_ = reason |
||||
return TestResult{Pass: true} |
||||
} |
||||
|
||||
func testRejectExpiredEvent(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "expired event test", nil) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
ev.CreatedAt = time.Now().Unix() - 86400*365 // 1 year ago
|
||||
// Re-sign with new timestamp
|
||||
if err = ev.Sign(key1.Secret); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to re-sign: %v", err)} |
||||
} |
||||
if err = client.Publish(ev); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
||||
} |
||||
// Some relays may accept old events, so this is optional
|
||||
if !accepted { |
||||
return TestResult{Pass: true, Info: "expired event rejected (expected)"} |
||||
} |
||||
return TestResult{Pass: true, Info: "expired event accepted (relay allows old events)"} |
||||
} |
||||
|
||||
func testReplaceableEvents(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
ev1, err := CreateReplaceableEvent(key1.Secret, kind.ProfileMetadata.K, "first version") |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
if err = client.Publish(ev1); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, _, err := client.WaitForOK(ev1.ID, 5*time.Second) |
||||
if err != nil || !accepted { |
||||
return TestResult{Pass: false, Info: "first event not accepted"} |
||||
} |
||||
time.Sleep(200 * time.Millisecond) |
||||
ev2, err := CreateReplaceableEvent(key1.Secret, kind.ProfileMetadata.K, "second version") |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
if err = client.Publish(ev2); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, _, err = client.WaitForOK(ev2.ID, 5*time.Second) |
||||
if err != nil || !accepted { |
||||
return TestResult{Pass: false, Info: "second event not accepted"} |
||||
} |
||||
time.Sleep(200 * time.Millisecond) |
||||
filter := map[string]interface{}{ |
||||
"kinds": []int{int(kind.ProfileMetadata.K)}, |
||||
"authors": []string{hex.Enc(key1.Pubkey)}, |
||||
} |
||||
events, err := client.GetEvents("test-replaceable", []interface{}{filter}, 2*time.Second) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
||||
} |
||||
foundSecond := false |
||||
for _, e := range events { |
||||
if string(e.ID) == string(ev2.ID) { |
||||
foundSecond = true |
||||
break |
||||
} |
||||
} |
||||
if !foundSecond { |
||||
return TestResult{Pass: false, Info: "second replaceable event not found"} |
||||
} |
||||
return TestResult{Pass: true} |
||||
} |
||||
|
||||
func testEphemeralEvents(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
ev, err := CreateEphemeralEvent(key1.Secret, 20000, "ephemeral test") |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
if err = client.Publish(ev); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
||||
if err != nil || !accepted { |
||||
return TestResult{Pass: false, Info: "ephemeral event not accepted"} |
||||
} |
||||
// Ephemeral events should not be stored, so query should not find them
|
||||
time.Sleep(200 * time.Millisecond) |
||||
filter := map[string]interface{}{ |
||||
"kinds": []int{20000}, |
||||
} |
||||
events, err := client.GetEvents("test-ephemeral", []interface{}{filter}, 2*time.Second) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
||||
} |
||||
// Ephemeral events should not be queryable
|
||||
for _, e := range events { |
||||
if string(e.ID) == string(ev.ID) { |
||||
return TestResult{Pass: false, Info: "ephemeral event was stored (should not be)"} |
||||
} |
||||
} |
||||
return TestResult{Pass: true} |
||||
} |
||||
|
||||
func testParameterizedReplaceableEvents(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
ev1, err := CreateParameterizedReplaceableEvent(key1.Secret, 30023, "first list", "test-list") |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
if err = client.Publish(ev1); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, _, err := client.WaitForOK(ev1.ID, 5*time.Second) |
||||
if err != nil || !accepted { |
||||
return TestResult{Pass: false, Info: "first event not accepted"} |
||||
} |
||||
time.Sleep(200 * time.Millisecond) |
||||
ev2, err := CreateParameterizedReplaceableEvent(key1.Secret, 30023, "second list", "test-list") |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
if err = client.Publish(ev2); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, _, err = client.WaitForOK(ev2.ID, 5*time.Second) |
||||
if err != nil || !accepted { |
||||
return TestResult{Pass: false, Info: "second event not accepted"} |
||||
} |
||||
return TestResult{Pass: true} |
||||
} |
||||
|
||||
func testDeletionEvents(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
// First create an event to delete
|
||||
targetEv, err := CreateEvent(key1.Secret, kind.TextNote.K, "event to delete", nil) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
if err = client.Publish(targetEv); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, _, err := client.WaitForOK(targetEv.ID, 5*time.Second) |
||||
if err != nil || !accepted { |
||||
return TestResult{Pass: false, Info: "target event not accepted"} |
||||
} |
||||
time.Sleep(200 * time.Millisecond) |
||||
// Now create deletion event
|
||||
deleteEv, err := CreateDeleteEvent(key1.Secret, [][]byte{targetEv.ID}, "deletion reason") |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create delete event: %v", err)} |
||||
} |
||||
if err = client.Publish(deleteEv); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, _, err = client.WaitForOK(deleteEv.ID, 5*time.Second) |
||||
if err != nil || !accepted { |
||||
return TestResult{Pass: false, Info: "delete event not accepted"} |
||||
} |
||||
return TestResult{Pass: true} |
||||
} |
||||
|
||||
func testCountRequest(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "count test", nil) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
if err = client.Publish(ev); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
||||
if err != nil || !accepted { |
||||
return TestResult{Pass: false, Info: "event not accepted"} |
||||
} |
||||
time.Sleep(200 * time.Millisecond) |
||||
filter := map[string]interface{}{ |
||||
"kinds": []int{int(kind.TextNote.K)}, |
||||
} |
||||
count, err := client.Count([]interface{}{filter}) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("COUNT failed: %v", err)} |
||||
} |
||||
if count < 1 { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("COUNT returned %d, expected at least 1", count)} |
||||
} |
||||
return TestResult{Pass: true} |
||||
} |
||||
|
||||
func testLimitParameter(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
// Publish multiple events
|
||||
for i := 0; i < 5; i++ { |
||||
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, fmt.Sprintf("limit test %d", i), nil) |
||||
if err != nil { |
||||
continue |
||||
} |
||||
client.Publish(ev) |
||||
client.WaitForOK(ev.ID, 2*time.Second) |
||||
} |
||||
time.Sleep(500 * time.Millisecond) |
||||
filter := map[string]interface{}{ |
||||
"limit": 2, |
||||
} |
||||
events, err := client.GetEvents("test-limit", []interface{}{filter}, 2*time.Second) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
||||
} |
||||
// Limit should be respected (though exact count may vary)
|
||||
if len(events) > 10 { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("got %d events, limit may not be working", len(events))} |
||||
} |
||||
return TestResult{Pass: true} |
||||
} |
||||
|
||||
func testMultipleFilters(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
ev1, err := CreateEvent(key1.Secret, kind.TextNote.K, "filter 1", nil) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
ev2, err := CreateEvent(key2.Secret, kind.TextNote.K, "filter 2", nil) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
||||
} |
||||
if err = client.Publish(ev1); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
if err = client.Publish(ev2); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
||||
} |
||||
accepted, _, err := client.WaitForOK(ev1.ID, 2*time.Second) |
||||
if err != nil || !accepted { |
||||
return TestResult{Pass: false, Info: "event 1 not accepted"} |
||||
} |
||||
accepted, _, err = client.WaitForOK(ev2.ID, 2*time.Second) |
||||
if err != nil || !accepted { |
||||
return TestResult{Pass: false, Info: "event 2 not accepted"} |
||||
} |
||||
time.Sleep(300 * time.Millisecond) |
||||
filter1 := map[string]interface{}{ |
||||
"authors": []string{hex.Enc(key1.Pubkey)}, |
||||
} |
||||
filter2 := map[string]interface{}{ |
||||
"authors": []string{hex.Enc(key2.Pubkey)}, |
||||
} |
||||
events, err := client.GetEvents("test-multi-filter", []interface{}{filter1, filter2}, 2*time.Second) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
||||
} |
||||
if len(events) < 2 { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("got %d events, expected at least 2", len(events))} |
||||
} |
||||
return TestResult{Pass: true} |
||||
} |
||||
|
||||
func testSubscriptionClose(client *Client, key1, key2 *KeyPair) (result TestResult) { |
||||
ch, err := client.Subscribe("close-test", []interface{}{map[string]interface{}{}}) |
||||
if err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to subscribe: %v", err)} |
||||
} |
||||
if err = client.Unsubscribe("close-test"); err != nil { |
||||
return TestResult{Pass: false, Info: fmt.Sprintf("failed to unsubscribe: %v", err)} |
||||
} |
||||
// Channel should be closed
|
||||
select { |
||||
case _, ok := <-ch: |
||||
if ok { |
||||
return TestResult{Pass: false, Info: "subscription channel not closed"} |
||||
} |
||||
default: |
||||
// Channel already closed, which is fine
|
||||
} |
||||
return TestResult{Pass: true} |
||||
} |
||||
@ -0,0 +1,207 @@
@@ -0,0 +1,207 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
"os/signal" |
||||
"path/filepath" |
||||
"syscall" |
||||
"testing" |
||||
"time" |
||||
|
||||
lol "lol.mleku.dev" |
||||
"next.orly.dev/app/config" |
||||
"next.orly.dev/pkg/run" |
||||
relaytester "next.orly.dev/relay-tester" |
||||
) |
||||
|
||||
var ( |
||||
testRelayURL string |
||||
testName string |
||||
testJSON bool |
||||
keepDataDir bool |
||||
relayPort int |
||||
relayDataDir string |
||||
) |
||||
|
||||
func TestRelay(t *testing.T) { |
||||
var err error |
||||
var relay *run.Relay |
||||
var relayURL string |
||||
|
||||
// Determine relay URL
|
||||
if testRelayURL != "" { |
||||
relayURL = testRelayURL |
||||
} else { |
||||
// Start local relay for testing
|
||||
if relay, err = startTestRelay(); err != nil { |
||||
t.Fatalf("Failed to start test relay: %v", err) |
||||
} |
||||
defer func() { |
||||
if stopErr := relay.Stop(); stopErr != nil { |
||||
t.Logf("Error stopping relay: %v", stopErr) |
||||
} |
||||
}() |
||||
port := relayPort |
||||
if port == 0 { |
||||
port = 3334 // Default port
|
||||
} |
||||
relayURL = fmt.Sprintf("ws://127.0.0.1:%d", port) |
||||
// Wait for relay to be ready
|
||||
time.Sleep(2 * time.Second) |
||||
} |
||||
|
||||
// Create test suite
|
||||
suite, err := relaytester.NewTestSuite(relayURL) |
||||
if err != nil { |
||||
t.Fatalf("Failed to create test suite: %v", err) |
||||
} |
||||
|
||||
// Run tests
|
||||
var results []relaytester.TestResult |
||||
if testName != "" { |
||||
// Run specific test
|
||||
result, err := suite.RunTest(testName) |
||||
if err != nil { |
||||
t.Fatalf("Failed to run test %s: %v", testName, err) |
||||
} |
||||
results = []relaytester.TestResult{result} |
||||
} else { |
||||
// Run all tests
|
||||
if results, err = suite.Run(); err != nil { |
||||
t.Fatalf("Failed to run tests: %v", err) |
||||
} |
||||
} |
||||
|
||||
// Output results
|
||||
if testJSON { |
||||
jsonOutput, err := relaytester.FormatJSON(results) |
||||
if err != nil { |
||||
t.Fatalf("Failed to format JSON: %v", err) |
||||
} |
||||
fmt.Println(jsonOutput) |
||||
} else { |
||||
outputResults(results, t) |
||||
} |
||||
|
||||
// Check if any required tests failed
|
||||
for _, result := range results { |
||||
if result.Required && !result.Pass { |
||||
t.Errorf("Required test '%s' failed: %s", result.Name, result.Info) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func startTestRelay() (relay *run.Relay, err error) { |
||||
cfg := &config.C{ |
||||
AppName: "ORLY-TEST", |
||||
DataDir: relayDataDir, |
||||
Listen: "127.0.0.1", |
||||
Port: relayPort, |
||||
LogLevel: "warn", |
||||
DBLogLevel: "warn", |
||||
ACLMode: "none", |
||||
} |
||||
|
||||
// Set default port if not specified
|
||||
if cfg.Port == 0 { |
||||
cfg.Port = 3334 |
||||
} |
||||
|
||||
// Set default data dir if not specified
|
||||
if cfg.DataDir == "" { |
||||
tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("orly-test-%d", time.Now().UnixNano())) |
||||
cfg.DataDir = tmpDir |
||||
} |
||||
|
||||
// Set up logging
|
||||
lol.SetLogLevel(cfg.LogLevel) |
||||
|
||||
// Create options
|
||||
cleanup := !keepDataDir |
||||
opts := &run.Options{ |
||||
CleanupDataDir: &cleanup, |
||||
} |
||||
|
||||
// Start relay
|
||||
if relay, err = run.Start(cfg, opts); err != nil { |
||||
return nil, fmt.Errorf("failed to start relay: %w", err) |
||||
} |
||||
|
||||
// Set up signal handling for graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1) |
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) |
||||
go func() { |
||||
<-sigChan |
||||
if relay != nil { |
||||
relay.Stop() |
||||
} |
||||
os.Exit(0) |
||||
}() |
||||
|
||||
return relay, nil |
||||
} |
||||
|
||||
func outputResults(results []relaytester.TestResult, t *testing.T) { |
||||
passed := 0 |
||||
failed := 0 |
||||
requiredFailed := 0 |
||||
|
||||
for _, result := range results { |
||||
if result.Pass { |
||||
passed++ |
||||
t.Logf("PASS: %s", result.Name) |
||||
} else { |
||||
failed++ |
||||
if result.Required { |
||||
requiredFailed++ |
||||
t.Errorf("FAIL (required): %s - %s", result.Name, result.Info) |
||||
} else { |
||||
t.Logf("FAIL (optional): %s - %s", result.Name, result.Info) |
||||
} |
||||
} |
||||
} |
||||
|
||||
t.Logf("\nTest Summary:") |
||||
t.Logf(" Total: %d", len(results)) |
||||
t.Logf(" Passed: %d", passed) |
||||
t.Logf(" Failed: %d", failed) |
||||
t.Logf(" Required Failed: %d", requiredFailed) |
||||
} |
||||
|
||||
// TestMain allows custom test setup/teardown
|
||||
func TestMain(m *testing.M) { |
||||
// Manually parse our custom flags to avoid conflicts with Go's test flags
|
||||
for i := 1; i < len(os.Args); i++ { |
||||
arg := os.Args[i] |
||||
switch arg { |
||||
case "-relay-url": |
||||
if i+1 < len(os.Args) { |
||||
testRelayURL = os.Args[i+1] |
||||
i++ |
||||
} |
||||
case "-test-name": |
||||
if i+1 < len(os.Args) { |
||||
testName = os.Args[i+1] |
||||
i++ |
||||
} |
||||
case "-json": |
||||
testJSON = true |
||||
case "-keep-data": |
||||
keepDataDir = true |
||||
case "-port": |
||||
if i+1 < len(os.Args) { |
||||
fmt.Sscanf(os.Args[i+1], "%d", &relayPort) |
||||
i++ |
||||
} |
||||
case "-data-dir": |
||||
if i+1 < len(os.Args) { |
||||
relayDataDir = os.Args[i+1] |
||||
i++ |
||||
} |
||||
} |
||||
} |
||||
|
||||
code := m.Run() |
||||
os.Exit(code) |
||||
} |
||||
Loading…
Reference in new issue