Add relay testing framework and utilities
- 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.
This commit is contained in:
280
relay-tester/client.go
Normal file
280
relay-tester/client.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
130
relay-tester/keys.go
Normal file
130
relay-tester/keys.go
Normal file
@@ -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)
|
||||
}
|
||||
261
relay-tester/test.go
Normal file
261
relay-tester/test.go
Normal file
@@ -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
|
||||
}
|
||||
547
relay-tester/tests.go
Normal file
547
relay-tester/tests.go
Normal file
@@ -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}
|
||||
}
|
||||
Reference in New Issue
Block a user