855 lines
18 KiB
Go
855 lines
18 KiB
Go
package prism
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.wisehodl.dev/jay/go-honeybee"
|
|
"git.wisehodl.dev/jay/go-roots-ws"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Session
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func TestSession_SendFailure_Terminates(t *testing.T) {
|
|
sendFn := func(data []byte) error {
|
|
return fmt.Errorf("write error")
|
|
}
|
|
|
|
terminateCalled := make(chan struct{})
|
|
terminate := func() { close(terminateCalled) }
|
|
|
|
done := make(chan struct{})
|
|
s := newSession("TESTID01", []byte(`["REQ"]`), sendFn, done, terminate)
|
|
go s.run()
|
|
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-terminateCalled:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "terminate should be called on send failure")
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Query
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func TestQuery_ReturnsEventsBeforeEOSE(t *testing.T) {
|
|
h := newManagerHarness(t)
|
|
h.connect()
|
|
Eventually(t, h.envoy.IsConnected, "envoy should be connected")
|
|
|
|
filters := [][]byte{[]byte(`{"kinds":[1]}`)}
|
|
|
|
var events []ReqEvent
|
|
var queryDone = make(chan struct{})
|
|
go func() {
|
|
events, _ = h.manager.Query(filters, TestTimeout)
|
|
close(queryDone)
|
|
}()
|
|
|
|
// Intercept the outbound REQ to learn the sub-ID.
|
|
var req []byte
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case req = <-h.sent:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "manager should send REQ")
|
|
|
|
subID, _, err := envelope.FindReq(req)
|
|
assert.NoError(t, err)
|
|
|
|
h.sendEvent(subID, []byte(`{"id":"aaa"}`))
|
|
h.sendEvent(subID, []byte(`{"id":"bbb"}`))
|
|
h.sendEOSE(subID)
|
|
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-queryDone:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "query should return after EOSE")
|
|
|
|
assert.Len(t, events, 2)
|
|
}
|
|
|
|
func TestQuery_EOSE_SendsCLOSEAndReturns(t *testing.T) {
|
|
h := newManagerHarness(t)
|
|
h.connect()
|
|
Eventually(t, h.envoy.IsConnected, "envoy should be connected")
|
|
|
|
filters := [][]byte{[]byte(`{"kinds":[1]}`)}
|
|
|
|
var events []ReqEvent
|
|
queryDone := make(chan struct{})
|
|
go func() {
|
|
events, _ = h.manager.Query(filters, TestTimeout)
|
|
close(queryDone)
|
|
}()
|
|
|
|
var req []byte
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case req = <-h.sent:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "manager should send REQ")
|
|
|
|
subID, _, err := envelope.FindReq(req)
|
|
assert.NoError(t, err)
|
|
|
|
h.sendEOSE(subID)
|
|
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-queryDone:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "query should return after EOSE")
|
|
|
|
assert.Empty(t, events)
|
|
|
|
// Verify a CLOSE was sent after EOSE.
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case msg := <-h.sent:
|
|
closedID, err := envelope.FindClose(msg)
|
|
return err == nil && closedID == subID
|
|
default:
|
|
return false
|
|
}
|
|
}, "manager should send CLOSE after EOSE")
|
|
}
|
|
|
|
func TestQuery_CLOSED_ReturnsMessageAndEmptyEvents(t *testing.T) {
|
|
h := newManagerHarness(t)
|
|
h.connect()
|
|
Eventually(t, h.envoy.IsConnected, "envoy should be connected")
|
|
|
|
filters := [][]byte{[]byte(`{"kinds":[1]}`)}
|
|
|
|
var retEvents []ReqEvent
|
|
var retClosed *ReqClosed
|
|
queryDone := make(chan struct{})
|
|
go func() {
|
|
retEvents, retClosed = h.manager.Query(filters, TestTimeout)
|
|
close(queryDone)
|
|
}()
|
|
|
|
var req []byte
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case req = <-h.sent:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "manager should send REQ")
|
|
|
|
subID, _, err := envelope.FindReq(req)
|
|
assert.NoError(t, err)
|
|
|
|
h.sendClosed(subID, "rate limited: too many subscriptions")
|
|
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-queryDone:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "query should return after CLOSED")
|
|
|
|
assert.Empty(t, retEvents)
|
|
assert.NotNil(t, retClosed)
|
|
assert.Equal(t, "rate limited: too many subscriptions", retClosed.Data)
|
|
}
|
|
|
|
func TestQuery_Timeout_ReturnsPartialEvents(t *testing.T) {
|
|
h := newManagerHarness(t)
|
|
h.connect()
|
|
Eventually(t, h.envoy.IsConnected, "envoy should be connected")
|
|
|
|
filters := [][]byte{[]byte(`{"kinds":[1]}`)}
|
|
|
|
var retEvents []ReqEvent
|
|
queryDone := make(chan struct{})
|
|
go func() {
|
|
retEvents, _ = h.manager.Query(filters, 200*time.Millisecond)
|
|
close(queryDone)
|
|
}()
|
|
|
|
var req []byte
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case req = <-h.sent:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "manager should send REQ")
|
|
|
|
subID, _, err := envelope.FindReq(req)
|
|
assert.NoError(t, err)
|
|
|
|
h.sendEvent(subID, []byte(`{"id":"aaa"}`))
|
|
// No EOSE — let the timeout fire.
|
|
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-queryDone:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "query should return after timeout")
|
|
|
|
assert.Len(t, retEvents, 1)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Disconnect / Reconnect
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func TestStream_Disconnect_TerminatesSession(t *testing.T) {
|
|
h := newManagerHarness(t)
|
|
h.connect()
|
|
Eventually(t, h.envoy.IsConnected, "envoy should be connected")
|
|
|
|
filters := [][]byte{[]byte(`{"kinds":[1]}`)}
|
|
_, _, _ = h.manager.Stream(filters)
|
|
|
|
// Wait for the REQ to be sent so we know the session is running.
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-h.sent:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "manager should send REQ")
|
|
|
|
h.disconnect()
|
|
|
|
// After disconnect, reqWg should reach zero, meaning the session exited.
|
|
wgDone := make(chan struct{})
|
|
go func() {
|
|
h.manager.reqWg.Wait()
|
|
close(wgDone)
|
|
}()
|
|
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-wgDone:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "reqWg should reach zero after disconnect")
|
|
}
|
|
|
|
func TestStream_Disconnect_PreservesRegistration(t *testing.T) {
|
|
h := newManagerHarness(t)
|
|
h.connect()
|
|
Eventually(t, h.envoy.IsConnected, "envoy should be connected")
|
|
|
|
filters := [][]byte{[]byte(`{"kinds":[1]}`)}
|
|
id, _, _ := h.manager.Stream(filters)
|
|
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-h.sent:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "manager should send REQ")
|
|
|
|
h.disconnect()
|
|
|
|
wgDone := make(chan struct{})
|
|
go func() {
|
|
h.manager.reqWg.Wait()
|
|
close(wgDone)
|
|
}()
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-wgDone:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "reqWg should clear")
|
|
|
|
h.manager.mu.RLock()
|
|
_, regExists := h.manager.regs[id]
|
|
h.manager.mu.RUnlock()
|
|
|
|
assert.True(t, regExists, "registration should survive disconnect")
|
|
}
|
|
|
|
func TestStream_Reconnect_ResendsREQ(t *testing.T) {
|
|
h := newManagerHarness(t)
|
|
h.connect()
|
|
Eventually(t, h.envoy.IsConnected, "envoy should be connected")
|
|
|
|
filters := [][]byte{[]byte(`{"kinds":[1]}`)}
|
|
_, _, _ = h.manager.Stream(filters)
|
|
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-h.sent:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "first REQ should be sent")
|
|
|
|
h.disconnect()
|
|
wgDone := make(chan struct{})
|
|
go func() { h.manager.reqWg.Wait(); close(wgDone) }()
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-wgDone:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "reqWg should clear after disconnect")
|
|
|
|
h.connect()
|
|
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-h.sent:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "REQ should be resent after reconnect")
|
|
}
|
|
|
|
func TestStream_Reconnect_ResumesForwardingEvents(t *testing.T) {
|
|
h := newManagerHarness(t)
|
|
h.connect()
|
|
Eventually(t, h.envoy.IsConnected, "envoy should be connected")
|
|
|
|
filters := [][]byte{[]byte(`{"kinds":[1]}`)}
|
|
_, eventsCh, _ := h.manager.Stream(filters)
|
|
|
|
var firstREQ []byte
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case firstREQ = <-h.sent:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "first REQ should be sent")
|
|
|
|
firstSubID, _, err := envelope.FindReq(firstREQ)
|
|
assert.NoError(t, err)
|
|
|
|
h.sendEvent(firstSubID, []byte(`{"id":"before"}`))
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-eventsCh:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "event before disconnect should arrive")
|
|
|
|
h.disconnect()
|
|
wgDone := make(chan struct{})
|
|
go func() { h.manager.reqWg.Wait(); close(wgDone) }()
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-wgDone:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "reqWg should clear after disconnect")
|
|
|
|
h.connect()
|
|
|
|
var secondREQ []byte
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case secondREQ = <-h.sent:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "second REQ should be sent after reconnect")
|
|
|
|
secondSubID, _, err := envelope.FindReq(secondREQ)
|
|
assert.NoError(t, err)
|
|
|
|
h.sendEvent(secondSubID, []byte(`{"id":"after"}`))
|
|
var gotAfter ReqEvent
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case gotAfter = <-eventsCh:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "event after reconnect should arrive on same channel")
|
|
assert.Equal(t, []byte(`{"id":"after"}`), gotAfter.Data)
|
|
}
|
|
|
|
func TestQuery_Disconnect_ReturnsEmpty(t *testing.T) {
|
|
h := newManagerHarness(t)
|
|
h.connect()
|
|
Eventually(t, h.envoy.IsConnected, "envoy should be connected")
|
|
|
|
filters := [][]byte{[]byte(`{"kinds":[1]}`)}
|
|
|
|
var retEvents []ReqEvent
|
|
queryDone := make(chan struct{})
|
|
go func() {
|
|
retEvents, _ = h.manager.Query(filters, TestTimeout)
|
|
close(queryDone)
|
|
}()
|
|
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-h.sent:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "manager should send REQ")
|
|
|
|
h.disconnect()
|
|
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-queryDone:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "query should return after disconnect")
|
|
|
|
assert.Empty(t, retEvents)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Inbox routing
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func TestRouteInbox_RoutesEventToCorrectSession(t *testing.T) {
|
|
h := newManagerHarness(t)
|
|
h.connect()
|
|
Eventually(t, h.envoy.IsConnected, "envoy should be connected")
|
|
|
|
idA, evA, _ := h.manager.Stream([][]byte{[]byte(`{"kinds":[1]}`)})
|
|
idB, evB, _ := h.manager.Stream([][]byte{[]byte(`{"kinds":[2]}`)})
|
|
|
|
// Collect both outbound REQs (order is non-deterministic).
|
|
reqs := make(map[string]bool)
|
|
Eventually(t, func() bool {
|
|
for {
|
|
select {
|
|
case msg := <-h.sent:
|
|
subID, _, err := envelope.FindReq(msg)
|
|
if err == nil {
|
|
reqs[subID] = true
|
|
}
|
|
default:
|
|
return len(reqs) >= 2
|
|
}
|
|
}
|
|
}, "both REQs should be sent")
|
|
|
|
assert.Contains(t, reqs, idA)
|
|
assert.Contains(t, reqs, idB)
|
|
|
|
// Send an event targeted at idA's subscription.
|
|
h.sendEvent(idA, []byte(`{"id":"for-a"}`))
|
|
|
|
var gotA ReqEvent
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case gotA = <-evA:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "event for idA should arrive on evA")
|
|
assert.Equal(t, []byte(`{"id":"for-a"}`), gotA.Data)
|
|
|
|
Never(t, func() bool {
|
|
select {
|
|
case <-evB:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "event for idA should not appear on evB")
|
|
}
|
|
|
|
func TestRouteInbox_DropsUnknownSubID(t *testing.T) {
|
|
h := newManagerHarness(t)
|
|
h.connect()
|
|
Eventually(t, h.envoy.IsConnected, "envoy should be connected")
|
|
|
|
// Send an EVENT for an ID no session knows about.
|
|
h.sendEvent("UNKNOWN1", []byte(`{"id":"ghost"}`))
|
|
|
|
// No panic, no block, manager continues operating.
|
|
_, evCh, _ := h.manager.Stream([][]byte{[]byte(`{"kinds":[1]}`)})
|
|
|
|
var req []byte
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case req = <-h.sent:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "REQ should be sent")
|
|
|
|
subID, _, err := envelope.FindReq(req)
|
|
assert.NoError(t, err)
|
|
|
|
h.sendEvent(subID, []byte(`{"id":"real"}`))
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-evCh:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "real event should still arrive after unknown drop")
|
|
}
|
|
|
|
func TestRouteInbox_DropsUnparseable(t *testing.T) {
|
|
h := newManagerHarness(t)
|
|
h.connect()
|
|
Eventually(t, h.envoy.IsConnected, "envoy should be connected")
|
|
|
|
// Inject a malformed message directly into the inbox.
|
|
h.inbox <- honeybee.InboxMessage{
|
|
ID: "wss://test",
|
|
Data: []byte(`not valid json at all`),
|
|
ReceivedAt: time.Now(),
|
|
}
|
|
|
|
// Manager should still be alive and routing correctly.
|
|
_, evCh, _ := h.manager.Stream([][]byte{[]byte(`{"kinds":[1]}`)})
|
|
|
|
var req []byte
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case req = <-h.sent:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "REQ should be sent after malformed message")
|
|
|
|
subID, _, err := envelope.FindReq(req)
|
|
assert.NoError(t, err)
|
|
|
|
h.sendEvent(subID, []byte(`{"id":"ok"}`))
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-evCh:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "event should arrive after malformed message was dropped")
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Manager lifecycle
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func TestManager_StreamWhileDisconnected_SessionSpawnedOnConnect(t *testing.T) {
|
|
h := newManagerHarness(t)
|
|
// Do NOT connect yet.
|
|
|
|
filters := [][]byte{[]byte(`{"kinds":[1]}`)}
|
|
_, _, _ = h.manager.Stream(filters)
|
|
|
|
// No REQ should be sent while disconnected.
|
|
Never(t, func() bool {
|
|
select {
|
|
case <-h.sent:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "no REQ should be sent before connect")
|
|
|
|
// Now connect — session should spawn and REQ should be sent.
|
|
h.connect()
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-h.sent:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "REQ should be sent after connect")
|
|
}
|
|
|
|
func TestManager_QueryWhileDisconnected_ReturnsEmpty(t *testing.T) {
|
|
h := newManagerHarness(t)
|
|
// Do NOT connect.
|
|
|
|
filters := [][]byte{[]byte(`{"kinds":[1]}`)}
|
|
events, closed := h.manager.Query(filters, TestTimeout)
|
|
|
|
assert.Empty(t, events)
|
|
assert.Nil(t, closed)
|
|
}
|
|
|
|
func TestManager_Close_TerminatesAllSessions(t *testing.T) {
|
|
h := newManagerHarness(t)
|
|
h.connect()
|
|
Eventually(t, h.envoy.IsConnected, "envoy should be connected")
|
|
|
|
h.manager.Stream([][]byte{[]byte(`{"kinds":[1]}`)})
|
|
h.manager.Stream([][]byte{[]byte(`{"kinds":[2]}`)})
|
|
|
|
// Wait for both REQs.
|
|
Eventually(t, func() bool {
|
|
for {
|
|
select {
|
|
case <-h.sent:
|
|
default:
|
|
return len(h.manager.sessions) >= 2
|
|
}
|
|
}
|
|
}, "both sessions should be active")
|
|
|
|
closed := make(chan struct{})
|
|
go func() {
|
|
h.manager.Close()
|
|
close(closed)
|
|
}()
|
|
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-closed:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "manager.Close() should return")
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Stream (unit-level session tests)
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func TestStream_ForwardsEvents(t *testing.T) {
|
|
si := newSessionInbox()
|
|
done := make(chan struct{})
|
|
terminate := func() {}
|
|
|
|
eventsCh := make(chan ReqEvent, 4)
|
|
|
|
s := newSession(
|
|
"TESTID01",
|
|
[]byte(`["REQ"]`),
|
|
func([]byte) error { return nil },
|
|
done,
|
|
terminate,
|
|
withSessionInbox(si),
|
|
withForwardEvents(eventsCh),
|
|
)
|
|
go s.run()
|
|
|
|
eventData := []byte(`{"id":"abc"}`)
|
|
si.events <- inboxEvent{
|
|
subID: "TESTID01",
|
|
data: eventData,
|
|
receivedAt: time.Now(),
|
|
}
|
|
|
|
Eventually(t, func() bool { return len(eventsCh) > 0 }, "event should be forwarded")
|
|
|
|
got := <-eventsCh
|
|
assert.Equal(t, eventData, got.Data)
|
|
|
|
close(done)
|
|
}
|
|
|
|
func TestStream_IgnoresEOSE(t *testing.T) {
|
|
si := newSessionInbox()
|
|
done := make(chan struct{})
|
|
terminateCalled := make(chan struct{})
|
|
terminate := func() { close(terminateCalled) }
|
|
|
|
s := newSession(
|
|
"TESTID01",
|
|
[]byte(`["REQ"]`),
|
|
func([]byte) error { return nil },
|
|
done,
|
|
terminate,
|
|
withSessionInbox(si),
|
|
)
|
|
go s.run()
|
|
|
|
si.eose <- inboxEOSE{subID: "TESTID01", receivedAt: time.Now()}
|
|
|
|
Never(t, func() bool {
|
|
select {
|
|
case <-terminateCalled:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "stream session should not terminate on EOSE")
|
|
|
|
close(done)
|
|
}
|
|
|
|
func TestStream_CLOSED_ForwardsMessageAndTerminates(t *testing.T) {
|
|
si := newSessionInbox()
|
|
done := make(chan struct{})
|
|
terminateCalled := make(chan struct{})
|
|
deregisterCalled := make(chan struct{})
|
|
terminate := func() { close(terminateCalled) }
|
|
deregister := func() { close(deregisterCalled) }
|
|
|
|
forwardedClosed := make(chan ReqClosed, 1)
|
|
|
|
s := newSession(
|
|
"TESTID01",
|
|
[]byte(`["REQ"]`),
|
|
func([]byte) error { return nil },
|
|
done,
|
|
terminate,
|
|
withSessionInbox(si),
|
|
withDeregister(deregister),
|
|
withForwardClosed(forwardedClosed),
|
|
)
|
|
go s.run()
|
|
|
|
si.closed <- inboxClosed{
|
|
subID: "TESTID01",
|
|
message: "rate limited: too many subscriptions",
|
|
receivedAt: time.Now(),
|
|
}
|
|
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-terminateCalled:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "terminate should be called on CLOSED")
|
|
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-deregisterCalled:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "deregister should be called on CLOSED")
|
|
|
|
assert.Len(t, forwardedClosed, 1)
|
|
got := <-forwardedClosed
|
|
assert.Equal(t, "rate limited: too many subscriptions", got.Data)
|
|
}
|
|
|
|
func TestStream_Cancel_SendsCLOSEAndTerminates(t *testing.T) {
|
|
si := newSessionInbox()
|
|
done := make(chan struct{})
|
|
terminateCalled := make(chan struct{})
|
|
terminate := func() { close(terminateCalled) }
|
|
|
|
var sentMessages [][]byte
|
|
var sentMu sync.Mutex
|
|
sendFn := func(data []byte) error {
|
|
sentMu.Lock()
|
|
sentMessages = append(sentMessages, data)
|
|
sentMu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
id := "TESTID01"
|
|
s := newSession(
|
|
id,
|
|
[]byte(`["REQ"]`),
|
|
sendFn,
|
|
done,
|
|
terminate,
|
|
withSessionInbox(si),
|
|
)
|
|
go s.run()
|
|
|
|
// Wait for REQ to be sent before cancelling.
|
|
Eventually(t, func() bool {
|
|
sentMu.Lock()
|
|
defer sentMu.Unlock()
|
|
return len(sentMessages) > 0
|
|
}, "REQ should be sent before cancel")
|
|
|
|
s.Close()
|
|
|
|
Eventually(t, func() bool {
|
|
select {
|
|
case <-terminateCalled:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, "terminate should be called on cancel")
|
|
|
|
sentMu.Lock()
|
|
defer sentMu.Unlock()
|
|
expectedCLOSE := []byte(envelope.EncloseClose(id))
|
|
assert.Contains(t, sentMessages, expectedCLOSE)
|
|
}
|
|
|
|
func TestSession_SendsREQOnStart(t *testing.T) {
|
|
sent := make(chan []byte, 1)
|
|
sendFn := func(data []byte) error {
|
|
sent <- data
|
|
return nil
|
|
}
|
|
|
|
id := "TESTID01"
|
|
filters := [][]byte{[]byte(`{"kinds":[1]}`)}
|
|
req := mustEncloseReq(id, filters)
|
|
done := make(chan struct{})
|
|
terminate := func() {}
|
|
|
|
s := newSession(id, req, sendFn, done, terminate)
|
|
go s.run()
|
|
|
|
Eventually(t, func() bool { return len(sent) > 0 }, "session should send REQ")
|
|
|
|
got := <-sent
|
|
assert.Equal(t, req, got)
|
|
|
|
close(done)
|
|
}
|