198 lines
6.8 KiB
Go
198 lines
6.8 KiB
Go
package healtheventspublisher_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"galaxy/rtmanager/internal/adapters/healtheventspublisher"
|
|
"galaxy/rtmanager/internal/domain/health"
|
|
"galaxy/rtmanager/internal/ports"
|
|
|
|
"github.com/alicebob/miniredis/v2"
|
|
"github.com/redis/go-redis/v9"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// fakeSnapshots captures Upsert invocations for assertions.
|
|
type fakeSnapshots struct {
|
|
mu sync.Mutex
|
|
upserts []health.HealthSnapshot
|
|
upsertErr error
|
|
}
|
|
|
|
func (s *fakeSnapshots) Upsert(_ context.Context, snapshot health.HealthSnapshot) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.upsertErr != nil {
|
|
return s.upsertErr
|
|
}
|
|
s.upserts = append(s.upserts, snapshot)
|
|
return nil
|
|
}
|
|
|
|
func (s *fakeSnapshots) Get(_ context.Context, _ string) (health.HealthSnapshot, error) {
|
|
return health.HealthSnapshot{}, nil
|
|
}
|
|
|
|
func newPublisher(t *testing.T, snapshots ports.HealthSnapshotStore) (*healtheventspublisher.Publisher, *miniredis.Miniredis, *redis.Client) {
|
|
t.Helper()
|
|
server := miniredis.RunT(t)
|
|
client := redis.NewClient(&redis.Options{Addr: server.Addr()})
|
|
t.Cleanup(func() { _ = client.Close() })
|
|
|
|
publisher, err := healtheventspublisher.NewPublisher(healtheventspublisher.Config{
|
|
Client: client,
|
|
Snapshots: snapshots,
|
|
Stream: "runtime:health_events",
|
|
})
|
|
require.NoError(t, err)
|
|
return publisher, server, client
|
|
}
|
|
|
|
func TestNewPublisherRejectsMissingCollaborators(t *testing.T) {
|
|
_, err := healtheventspublisher.NewPublisher(healtheventspublisher.Config{})
|
|
require.Error(t, err)
|
|
|
|
_, err = healtheventspublisher.NewPublisher(healtheventspublisher.Config{
|
|
Client: redis.NewClient(&redis.Options{Addr: "127.0.0.1:0"}),
|
|
})
|
|
require.Error(t, err)
|
|
|
|
_, err = healtheventspublisher.NewPublisher(healtheventspublisher.Config{
|
|
Client: redis.NewClient(&redis.Options{Addr: "127.0.0.1:0"}),
|
|
Snapshots: &fakeSnapshots{},
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestPublishContainerStartedUpsertsHealthyAndXAdds(t *testing.T) {
|
|
snapshots := &fakeSnapshots{}
|
|
publisher, _, client := newPublisher(t, snapshots)
|
|
|
|
occurredAt := time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC)
|
|
envelope := ports.HealthEventEnvelope{
|
|
GameID: "game-1",
|
|
ContainerID: "c-1",
|
|
EventType: health.EventTypeContainerStarted,
|
|
OccurredAt: occurredAt,
|
|
Details: json.RawMessage(`{"image_ref":"galaxy/game:1.2.3"}`),
|
|
}
|
|
require.NoError(t, publisher.Publish(context.Background(), envelope))
|
|
|
|
require.Len(t, snapshots.upserts, 1)
|
|
snapshot := snapshots.upserts[0]
|
|
assert.Equal(t, "game-1", snapshot.GameID)
|
|
assert.Equal(t, "c-1", snapshot.ContainerID)
|
|
assert.Equal(t, health.SnapshotStatusHealthy, snapshot.Status)
|
|
assert.Equal(t, health.SnapshotSourceDockerEvent, snapshot.Source)
|
|
assert.JSONEq(t, `{"image_ref":"galaxy/game:1.2.3"}`, string(snapshot.Details))
|
|
assert.Equal(t, occurredAt, snapshot.ObservedAt)
|
|
|
|
entries, err := client.XRange(context.Background(), "runtime:health_events", "-", "+").Result()
|
|
require.NoError(t, err)
|
|
require.Len(t, entries, 1)
|
|
values := entries[0].Values
|
|
assert.Equal(t, "game-1", values["game_id"])
|
|
assert.Equal(t, "c-1", values["container_id"])
|
|
assert.Equal(t, "container_started", values["event_type"])
|
|
assert.Equal(t, strconv.FormatInt(occurredAt.UnixMilli(), 10), values["occurred_at_ms"])
|
|
assert.JSONEq(t, `{"image_ref":"galaxy/game:1.2.3"}`, values["details"].(string))
|
|
}
|
|
|
|
func TestPublishMapsEveryEventTypeToASnapshot(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
eventType health.EventType
|
|
expectStatus health.SnapshotStatus
|
|
expectSource health.SnapshotSource
|
|
}{
|
|
{health.EventTypeContainerStarted, health.SnapshotStatusHealthy, health.SnapshotSourceDockerEvent},
|
|
{health.EventTypeContainerExited, health.SnapshotStatusExited, health.SnapshotSourceDockerEvent},
|
|
{health.EventTypeContainerOOM, health.SnapshotStatusOOM, health.SnapshotSourceDockerEvent},
|
|
{health.EventTypeContainerDisappeared, health.SnapshotStatusContainerDisappeared, health.SnapshotSourceDockerEvent},
|
|
{health.EventTypeInspectUnhealthy, health.SnapshotStatusInspectUnhealthy, health.SnapshotSourceInspect},
|
|
{health.EventTypeProbeFailed, health.SnapshotStatusProbeFailed, health.SnapshotSourceProbe},
|
|
{health.EventTypeProbeRecovered, health.SnapshotStatusHealthy, health.SnapshotSourceProbe},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(string(tc.eventType), func(t *testing.T) {
|
|
t.Parallel()
|
|
snapshots := &fakeSnapshots{}
|
|
publisher, _, _ := newPublisher(t, snapshots)
|
|
require.NoError(t, publisher.Publish(context.Background(), ports.HealthEventEnvelope{
|
|
GameID: "g",
|
|
ContainerID: "c",
|
|
EventType: tc.eventType,
|
|
OccurredAt: time.Now().UTC(),
|
|
Details: json.RawMessage(`{}`),
|
|
}))
|
|
require.Len(t, snapshots.upserts, 1)
|
|
assert.Equal(t, tc.expectStatus, snapshots.upserts[0].Status)
|
|
assert.Equal(t, tc.expectSource, snapshots.upserts[0].Source)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPublishEmptyDetailsBecomesEmptyObject(t *testing.T) {
|
|
snapshots := &fakeSnapshots{}
|
|
publisher, _, client := newPublisher(t, snapshots)
|
|
|
|
envelope := ports.HealthEventEnvelope{
|
|
GameID: "g",
|
|
ContainerID: "c",
|
|
EventType: health.EventTypeContainerDisappeared,
|
|
OccurredAt: time.Now().UTC(),
|
|
}
|
|
require.NoError(t, publisher.Publish(context.Background(), envelope))
|
|
|
|
require.Len(t, snapshots.upserts, 1)
|
|
assert.JSONEq(t, "{}", string(snapshots.upserts[0].Details))
|
|
|
|
entries, err := client.XRange(context.Background(), "runtime:health_events", "-", "+").Result()
|
|
require.NoError(t, err)
|
|
require.Len(t, entries, 1)
|
|
assert.JSONEq(t, "{}", entries[0].Values["details"].(string))
|
|
}
|
|
|
|
func TestPublishRejectsInvalidEnvelope(t *testing.T) {
|
|
snapshots := &fakeSnapshots{}
|
|
publisher, _, client := newPublisher(t, snapshots)
|
|
|
|
require.Error(t, publisher.Publish(context.Background(), ports.HealthEventEnvelope{}))
|
|
|
|
entries, err := client.XRange(context.Background(), "runtime:health_events", "-", "+").Result()
|
|
require.NoError(t, err)
|
|
assert.Empty(t, entries)
|
|
assert.Empty(t, snapshots.upserts)
|
|
}
|
|
|
|
func TestPublishSurfacesSnapshotErrorWithoutXAdd(t *testing.T) {
|
|
snapshots := &fakeSnapshots{upsertErr: assertSentinelErr}
|
|
publisher, _, client := newPublisher(t, snapshots)
|
|
|
|
err := publisher.Publish(context.Background(), ports.HealthEventEnvelope{
|
|
GameID: "g",
|
|
ContainerID: "c",
|
|
EventType: health.EventTypeContainerStarted,
|
|
OccurredAt: time.Now().UTC(),
|
|
Details: json.RawMessage(`{"image_ref":"x"}`),
|
|
})
|
|
require.Error(t, err)
|
|
|
|
entries, err := client.XRange(context.Background(), "runtime:health_events", "-", "+").Result()
|
|
require.NoError(t, err)
|
|
assert.Empty(t, entries, "xadd must not run when snapshot upsert fails")
|
|
}
|
|
|
|
// assertSentinelErr is a sentinel for snapshot-failure assertions.
|
|
var assertSentinelErr = sentinelError("snapshot upsert failure")
|
|
|
|
type sentinelError string
|
|
|
|
func (s sentinelError) Error() string { return string(s) }
|