290 lines
7.7 KiB
Go
290 lines
7.7 KiB
Go
package events
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"galaxy/gateway/internal/config"
|
|
"galaxy/gateway/internal/push"
|
|
"galaxy/gateway/internal/testutil"
|
|
|
|
"github.com/alicebob/miniredis/v2"
|
|
"github.com/redis/go-redis/v9"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestRedisClientEventSubscriberPublishesValidEvent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := miniredis.RunT(t)
|
|
publisher := &recordingClientEventPublisher{}
|
|
subscriber := newTestRedisClientEventSubscriber(t, server, publisher)
|
|
running := runTestClientEventSubscriber(t, subscriber)
|
|
defer running.stop(t)
|
|
|
|
addClientEvent(t, server, "gateway:client_events", map[string]any{
|
|
"user_id": "user-123",
|
|
"device_session_id": "device-session-123",
|
|
"event_type": "fleet.updated",
|
|
"event_id": "event-123",
|
|
"payload_bytes": []byte("payload-123"),
|
|
"request_id": "request-123",
|
|
"trace_id": "trace-123",
|
|
})
|
|
|
|
require.Eventually(t, func() bool {
|
|
return len(publisher.events()) == 1
|
|
}, time.Second, 10*time.Millisecond)
|
|
|
|
assert.Equal(t, []push.Event{{
|
|
UserID: "user-123",
|
|
DeviceSessionID: "device-session-123",
|
|
EventType: "fleet.updated",
|
|
EventID: "event-123",
|
|
PayloadBytes: []byte("payload-123"),
|
|
RequestID: "request-123",
|
|
TraceID: "trace-123",
|
|
}}, publisher.events())
|
|
}
|
|
|
|
func TestRedisClientEventSubscriberSkipsMalformedEventAndContinues(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := miniredis.RunT(t)
|
|
publisher := &recordingClientEventPublisher{}
|
|
subscriber := newTestRedisClientEventSubscriber(t, server, publisher)
|
|
running := runTestClientEventSubscriber(t, subscriber)
|
|
defer running.stop(t)
|
|
|
|
addClientEvent(t, server, "gateway:client_events", map[string]any{
|
|
"user_id": "user-123",
|
|
"event_type": "fleet.updated",
|
|
"event_id": "event-bad",
|
|
"payload_bytes": []byte("payload-bad"),
|
|
"unexpected": "boom",
|
|
})
|
|
addClientEvent(t, server, "gateway:client_events", map[string]any{
|
|
"user_id": "user-123",
|
|
"event_type": "fleet.updated",
|
|
"event_id": "event-good",
|
|
"payload_bytes": []byte("payload-good"),
|
|
})
|
|
|
|
require.Eventually(t, func() bool {
|
|
events := publisher.events()
|
|
return len(events) == 1 && events[0].EventID == "event-good"
|
|
}, time.Second, 10*time.Millisecond)
|
|
}
|
|
|
|
func TestRedisClientEventSubscriberStartsFromCurrentTail(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := miniredis.RunT(t)
|
|
publisher := &recordingClientEventPublisher{}
|
|
|
|
addClientEvent(t, server, "gateway:client_events", map[string]any{
|
|
"user_id": "user-123",
|
|
"event_type": "fleet.updated",
|
|
"event_id": "event-old",
|
|
"payload_bytes": []byte("payload-old"),
|
|
})
|
|
|
|
subscriber := newTestRedisClientEventSubscriber(t, server, publisher)
|
|
running := runTestClientEventSubscriber(t, subscriber)
|
|
defer running.stop(t)
|
|
|
|
assert.Never(t, func() bool {
|
|
return len(publisher.events()) > 0
|
|
}, 100*time.Millisecond, 10*time.Millisecond)
|
|
|
|
addClientEvent(t, server, "gateway:client_events", map[string]any{
|
|
"user_id": "user-123",
|
|
"event_type": "fleet.updated",
|
|
"event_id": "event-new",
|
|
"payload_bytes": []byte("payload-new"),
|
|
})
|
|
|
|
require.Eventually(t, func() bool {
|
|
events := publisher.events()
|
|
return len(events) == 1 && events[0].EventID == "event-new"
|
|
}, time.Second, 10*time.Millisecond)
|
|
}
|
|
|
|
func TestRedisClientEventSubscriberShutdownInterruptsBlockingRead(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := miniredis.RunT(t)
|
|
publisher := &recordingClientEventPublisher{}
|
|
subscriber := newTestRedisClientEventSubscriber(t, server, publisher)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
resultCh := make(chan error, 1)
|
|
go func() {
|
|
resultCh <- subscriber.Run(ctx)
|
|
}()
|
|
|
|
select {
|
|
case <-subscriber.started:
|
|
case <-time.After(time.Second):
|
|
require.FailNow(t, "subscriber did not start")
|
|
}
|
|
|
|
cancel()
|
|
require.NoError(t, subscriber.Shutdown(context.Background()))
|
|
|
|
select {
|
|
case err := <-resultCh:
|
|
require.ErrorIs(t, err, context.Canceled)
|
|
case <-time.After(time.Second):
|
|
require.FailNow(t, "subscriber did not stop after shutdown")
|
|
}
|
|
}
|
|
|
|
func TestRedisClientEventSubscriberLogsAndCountsMalformedEvents(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := miniredis.RunT(t)
|
|
publisher := &recordingClientEventPublisher{}
|
|
logger, logBuffer := testutil.NewObservedLogger(t)
|
|
telemetryRuntime := testutil.NewTelemetryRuntime(t, logger)
|
|
|
|
subscriber, err := NewRedisClientEventSubscriberWithObservability(
|
|
newTestRedisClient(t, server),
|
|
config.SessionCacheRedisConfig{
|
|
KeyPrefix: "gateway:session:",
|
|
LookupTimeout: 250 * time.Millisecond,
|
|
},
|
|
config.ClientEventsRedisConfig{
|
|
Stream: "gateway:client_events",
|
|
ReadBlockTimeout: 25 * time.Millisecond,
|
|
},
|
|
publisher,
|
|
logger,
|
|
telemetryRuntime,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
running := runTestClientEventSubscriber(t, subscriber)
|
|
defer running.stop(t)
|
|
|
|
addClientEvent(t, server, "gateway:client_events", map[string]any{
|
|
"user_id": "user-123",
|
|
"event_type": "fleet.updated",
|
|
"event_id": "event-bad",
|
|
"payload_bytes": []byte("payload-bad"),
|
|
"unexpected": "boom",
|
|
})
|
|
|
|
require.Eventually(t, func() bool {
|
|
return strings.Contains(logBuffer.String(), "dropped malformed client event")
|
|
}, time.Second, 10*time.Millisecond)
|
|
|
|
metricsText := testutil.ScrapeMetrics(t, telemetryRuntime.Handler())
|
|
assert.Contains(t, metricsText, `gateway_internal_event_drops_total`)
|
|
assert.Contains(t, metricsText, `component="client_event_subscriber"`)
|
|
assert.Contains(t, metricsText, `reason="malformed_event"`)
|
|
}
|
|
|
|
func newTestRedisClientEventSubscriber(t *testing.T, server *miniredis.Miniredis, publisher ClientEventPublisher) *RedisClientEventSubscriber {
|
|
t.Helper()
|
|
|
|
subscriber, err := NewRedisClientEventSubscriber(
|
|
newTestRedisClient(t, server),
|
|
config.SessionCacheRedisConfig{
|
|
KeyPrefix: "gateway:session:",
|
|
LookupTimeout: 250 * time.Millisecond,
|
|
},
|
|
config.ClientEventsRedisConfig{
|
|
Stream: "gateway:client_events",
|
|
ReadBlockTimeout: 25 * time.Millisecond,
|
|
},
|
|
publisher,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
return subscriber
|
|
}
|
|
|
|
func addClientEvent(t *testing.T, server *miniredis.Miniredis, stream string, values map[string]any) {
|
|
t.Helper()
|
|
|
|
client := redis.NewClient(&redis.Options{
|
|
Addr: server.Addr(),
|
|
Protocol: 2,
|
|
DisableIdentity: true,
|
|
})
|
|
defer func() {
|
|
assert.NoError(t, client.Close())
|
|
}()
|
|
|
|
err := client.XAdd(context.Background(), &redis.XAddArgs{
|
|
Stream: stream,
|
|
Values: values,
|
|
}).Err()
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
type runningClientEventSubscriber struct {
|
|
cancel context.CancelFunc
|
|
resultCh chan error
|
|
}
|
|
|
|
func runTestClientEventSubscriber(t *testing.T, subscriber *RedisClientEventSubscriber) runningClientEventSubscriber {
|
|
t.Helper()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
resultCh := make(chan error, 1)
|
|
go func() {
|
|
resultCh <- subscriber.Run(ctx)
|
|
}()
|
|
|
|
select {
|
|
case <-subscriber.started:
|
|
case <-time.After(time.Second):
|
|
require.FailNow(t, "subscriber did not start")
|
|
}
|
|
|
|
return runningClientEventSubscriber{
|
|
cancel: cancel,
|
|
resultCh: resultCh,
|
|
}
|
|
}
|
|
|
|
func (r runningClientEventSubscriber) stop(t *testing.T) {
|
|
t.Helper()
|
|
|
|
r.cancel()
|
|
|
|
select {
|
|
case err := <-r.resultCh:
|
|
require.ErrorIs(t, err, context.Canceled)
|
|
case <-time.After(time.Second):
|
|
require.FailNow(t, "subscriber did not stop")
|
|
}
|
|
}
|
|
|
|
type recordingClientEventPublisher struct {
|
|
mu sync.Mutex
|
|
records []push.Event
|
|
}
|
|
|
|
func (p *recordingClientEventPublisher) Publish(event push.Event) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
p.records = append(p.records, event)
|
|
}
|
|
|
|
func (p *recordingClientEventPublisher) events() []push.Event {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
cloned := make([]push.Event, len(p.records))
|
|
copy(cloned, p.records)
|
|
return cloned
|
|
}
|