vibed request manager.
This commit is contained in:
+854
@@ -0,0 +1,854 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user