Update version to v0.21.0 and enhance relay client functionality
- Bumped version from v0.20.6 to v0.21.0. - Added a `complete` map in the Client struct to track subscription completion status. - Improved event handling in the read loop to manage EOSE messages and subscription closures. - Introduced new tests for filtering, event ordering, and subscription behaviors, enhancing test coverage and reliability.
This commit is contained in:
@@ -1 +1 @@
|
|||||||
v0.20.6
|
v0.21.0
|
||||||
@@ -14,14 +14,15 @@ import (
|
|||||||
|
|
||||||
// Client wraps a WebSocket connection to a relay for testing.
|
// Client wraps a WebSocket connection to a relay for testing.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
url string
|
url string
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
subs map[string]chan []byte
|
subs map[string]chan []byte
|
||||||
okCh chan []byte // Channel for OK messages
|
complete map[string]bool // Track if subscription is complete (e.g., by ID)
|
||||||
countCh chan []byte // Channel for COUNT messages
|
okCh chan []byte // Channel for OK messages
|
||||||
ctx context.Context
|
countCh chan []byte // Channel for COUNT messages
|
||||||
cancel context.CancelFunc
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient creates a new test client connected to the relay.
|
// NewClient creates a new test client connected to the relay.
|
||||||
@@ -36,13 +37,14 @@ func NewClient(url string) (c *Client, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
c = &Client{
|
c = &Client{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
url: url,
|
url: url,
|
||||||
subs: make(map[string]chan []byte),
|
subs: make(map[string]chan []byte),
|
||||||
okCh: make(chan []byte, 100),
|
complete: make(map[string]bool),
|
||||||
countCh: make(chan []byte, 100),
|
okCh: make(chan []byte, 100),
|
||||||
ctx: ctx,
|
countCh: make(chan []byte, 100),
|
||||||
cancel: cancel,
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
}
|
}
|
||||||
go c.readLoop()
|
go c.readLoop()
|
||||||
return
|
return
|
||||||
@@ -109,8 +111,17 @@ func (c *Client) readLoop() {
|
|||||||
if len(raw) >= 2 {
|
if len(raw) >= 2 {
|
||||||
if subID, ok := raw[1].(string); ok {
|
if subID, ok := raw[1].(string); ok {
|
||||||
if ch, exists := c.subs[subID]; exists {
|
if ch, exists := c.subs[subID]; exists {
|
||||||
close(ch)
|
// Send EOSE message to channel
|
||||||
delete(c.subs, subID)
|
select {
|
||||||
|
case ch <- msg:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
// For complete subscriptions (by ID), close the channel after EOSE
|
||||||
|
if c.complete[subID] {
|
||||||
|
close(ch)
|
||||||
|
delete(c.subs, subID)
|
||||||
|
delete(c.complete, subID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +158,19 @@ func (c *Client) Subscribe(subID string, filters []interface{}) (ch chan []byte,
|
|||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
ch = make(chan []byte, 100)
|
ch = make(chan []byte, 100)
|
||||||
c.subs[subID] = ch
|
c.subs[subID] = ch
|
||||||
|
// Check if subscription is complete (has 'ids' filter)
|
||||||
|
isComplete := false
|
||||||
|
for _, f := range filters {
|
||||||
|
if fMap, ok := f.(map[string]interface{}); ok {
|
||||||
|
if ids, exists := fMap["ids"]; exists {
|
||||||
|
if idList, ok := ids.([]string); ok && len(idList) > 0 {
|
||||||
|
isComplete = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.complete[subID] = isComplete
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -165,6 +189,7 @@ func (c *Client) Unsubscribe(subID string) error {
|
|||||||
close(ch)
|
close(ch)
|
||||||
}()
|
}()
|
||||||
delete(c.subs, subID)
|
delete(c.subs, subID)
|
||||||
|
delete(c.complete, subID)
|
||||||
}
|
}
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
return c.Send([]interface{}{"CLOSE", subID})
|
return c.Send([]interface{}{"CLOSE", subID})
|
||||||
@@ -269,14 +294,27 @@ func (c *Client) GetEvents(subID string, filters []interface{}, timeout time.Dur
|
|||||||
if err = json.Unmarshal(msg, &raw); err != nil {
|
if err = json.Unmarshal(msg, &raw); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(raw) >= 3 && raw[0] == "EVENT" {
|
if len(raw) < 2 {
|
||||||
if evData, ok := raw[2].(map[string]interface{}); ok {
|
continue
|
||||||
evJSON, _ := json.Marshal(evData)
|
}
|
||||||
ev := event.New()
|
typ, ok := raw[0].(string)
|
||||||
if _, err = ev.Unmarshal(evJSON); err == nil {
|
if !ok {
|
||||||
events = append(events, ev)
|
continue
|
||||||
|
}
|
||||||
|
switch typ {
|
||||||
|
case "EVENT":
|
||||||
|
if len(raw) >= 3 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "EOSE":
|
||||||
|
// End of stored events - return what we have
|
||||||
|
return events, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,6 +161,175 @@ func (s *TestSuite) registerTests() {
|
|||||||
Required: true,
|
Required: true,
|
||||||
Func: testSubscriptionClose,
|
Func: testSubscriptionClose,
|
||||||
},
|
},
|
||||||
|
// Filter tests
|
||||||
|
{
|
||||||
|
Name: "Since and until filters are inclusive",
|
||||||
|
Required: true,
|
||||||
|
Func: testSinceUntilAreInclusive,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Limit zero works",
|
||||||
|
Required: true,
|
||||||
|
Func: testLimitZero,
|
||||||
|
},
|
||||||
|
// Find tests
|
||||||
|
{
|
||||||
|
Name: "Events are ordered from newest to oldest",
|
||||||
|
Required: true,
|
||||||
|
Func: testEventsOrderedFromNewestToOldest,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Newest events are returned when filter is limited",
|
||||||
|
Required: true,
|
||||||
|
Func: testNewestEventsWhenLimited,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Finds by pubkey and kind",
|
||||||
|
Required: true,
|
||||||
|
Func: testFindByPubkeyAndKind,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Finds by pubkey and tags",
|
||||||
|
Required: true,
|
||||||
|
Func: testFindByPubkeyAndTags,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Finds by kind and tags",
|
||||||
|
Required: true,
|
||||||
|
Func: testFindByKindAndTags,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Finds by scrape",
|
||||||
|
Required: true,
|
||||||
|
Func: testFindByScrape,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
// Replaceable event tests
|
||||||
|
{
|
||||||
|
Name: "Replaces metadata",
|
||||||
|
Required: true,
|
||||||
|
Func: testReplacesMetadata,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Replaces contact list",
|
||||||
|
Required: true,
|
||||||
|
Func: testReplacesContactList,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Replaced events are still available by ID",
|
||||||
|
Required: false,
|
||||||
|
Func: testReplacedEventsStillAvailableByID,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Replaceable events replace older ones",
|
||||||
|
Required: true,
|
||||||
|
Func: testReplaceableEventRemovesPrevious,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Replaceable events rejected if a newer one exists",
|
||||||
|
Required: true,
|
||||||
|
Func: testReplaceableEventRejectedIfFuture,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Addressable events replace older ones",
|
||||||
|
Required: true,
|
||||||
|
Func: testAddressableEventRemovesPrevious,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Addressable events rejected if a newer one exists",
|
||||||
|
Required: true,
|
||||||
|
Func: testAddressableEventRejectedIfFuture,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
// Deletion tests
|
||||||
|
{
|
||||||
|
Name: "Deletes by a-tag address",
|
||||||
|
Required: true,
|
||||||
|
Func: testDeleteByAddr,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Delete by a-tag deletes older but not newer",
|
||||||
|
Required: true,
|
||||||
|
Func: testDeleteByAddrOnlyDeletesOlder,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Delete by a-tag is bound by a-tag",
|
||||||
|
Required: true,
|
||||||
|
Func: testDeleteByAddrIsBoundByTag,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
// Ephemeral tests
|
||||||
|
{
|
||||||
|
Name: "Ephemeral subscriptions work",
|
||||||
|
Required: false,
|
||||||
|
Func: testEphemeralSubscriptionsWork,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Persists ephemeral events",
|
||||||
|
Required: false,
|
||||||
|
Func: testPersistsEphemeralEvents,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
// EOSE tests
|
||||||
|
{
|
||||||
|
Name: "Supports EOSE",
|
||||||
|
Required: true,
|
||||||
|
Func: testSupportsEose,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Closes complete subscriptions after EOSE",
|
||||||
|
Required: false,
|
||||||
|
Func: testClosesCompleteSubscriptionsAfterEose,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Keeps open incomplete subscriptions after EOSE",
|
||||||
|
Required: true,
|
||||||
|
Func: testKeepsOpenIncompleteSubscriptionsAfterEose,
|
||||||
|
},
|
||||||
|
// JSON tests
|
||||||
|
{
|
||||||
|
Name: "Accepts events with empty tags",
|
||||||
|
Required: false,
|
||||||
|
Func: testAcceptsEventsWithEmptyTags,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Accepts NIP-01 JSON escape sequences",
|
||||||
|
Required: true,
|
||||||
|
Func: testAcceptsNip1JsonEscapeSequences,
|
||||||
|
Dependencies: []string{"Publishes basic event"},
|
||||||
|
},
|
||||||
|
// Registration tests
|
||||||
|
{
|
||||||
|
Name: "Sends OK after EVENT",
|
||||||
|
Required: true,
|
||||||
|
Func: testSendsOkAfterEvent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Verifies event signatures",
|
||||||
|
Required: true,
|
||||||
|
Func: testVerifiesSignatures,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Verifies event ID hashes",
|
||||||
|
Required: true,
|
||||||
|
Func: testVerifiesIdHashes,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range allTests {
|
for _, tc := range allTests {
|
||||||
s.AddTest(tc)
|
s.AddTest(tc)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user