Files
galaxy-game/gamemaster/internal/adapters/lobbyeventspublisher/publisher_test.go
T
2026-05-03 07:59:03 +02:00

187 lines
6.4 KiB
Go

package lobbyeventspublisher
import (
"context"
"encoding/json"
"strconv"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"galaxy/gamemaster/internal/domain/runtime"
"galaxy/gamemaster/internal/ports"
)
const testStream = "gm:lobby_events"
func newTestPublisher(t *testing.T) (*Publisher, *redis.Client) {
t.Helper()
server := miniredis.RunT(t)
client := redis.NewClient(&redis.Options{Addr: server.Addr()})
t.Cleanup(func() { _ = client.Close() })
publisher, err := NewPublisher(Config{Client: client, Stream: testStream})
require.NoError(t, err)
return publisher, client
}
func TestNewPublisherValidation(t *testing.T) {
t.Run("nil client", func(t *testing.T) {
_, err := NewPublisher(Config{Stream: testStream})
require.Error(t, err)
})
t.Run("empty stream", func(t *testing.T) {
client := redis.NewClient(&redis.Options{Addr: "127.0.0.1:0"})
t.Cleanup(func() { _ = client.Close() })
_, err := NewPublisher(Config{Client: client})
require.Error(t, err)
})
}
func TestPublishSnapshotUpdateHappyPath(t *testing.T) {
publisher, client := newTestPublisher(t)
occurredAt := time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC)
msg := ports.RuntimeSnapshotUpdate{
GameID: "game-1",
CurrentTurn: 17,
RuntimeStatus: runtime.StatusRunning,
EngineHealthSummary: "healthy",
PlayerTurnStats: []ports.PlayerTurnStats{
{UserID: "user-1", Planets: 4, Population: 12000},
{UserID: "user-2", Planets: 3, Population: 9000},
},
OccurredAt: occurredAt,
}
require.NoError(t, publisher.PublishSnapshotUpdate(context.Background(), msg))
entries, err := client.XRange(context.Background(), testStream, "-", "+").Result()
require.NoError(t, err)
require.Len(t, entries, 1)
values := entries[0].Values
assert.Equal(t, "runtime_snapshot_update", values[fieldEventType])
assert.Equal(t, "game-1", values[fieldGameID])
assert.Equal(t, "17", values[fieldCurrentTurn])
assert.Equal(t, "running", values[fieldRuntimeStatus])
assert.Equal(t, "healthy", values[fieldEngineHealthSummary])
assert.Equal(t, strconv.FormatInt(occurredAt.UnixMilli(), 10), values[fieldOccurredAtMS])
statsRaw, ok := values[fieldPlayerTurnStats].(string)
require.True(t, ok)
var stats []playerTurnStatEnvelope
require.NoError(t, json.Unmarshal([]byte(statsRaw), &stats))
assert.Equal(t, []playerTurnStatEnvelope{
{UserID: "user-1", Planets: 4, Population: 12000},
{UserID: "user-2", Planets: 3, Population: 9000},
}, stats)
}
func TestPublishSnapshotUpdateEmptyStatsBecomesArray(t *testing.T) {
publisher, client := newTestPublisher(t)
msg := ports.RuntimeSnapshotUpdate{
GameID: "g",
CurrentTurn: 0,
RuntimeStatus: runtime.StatusStarting,
EngineHealthSummary: "",
OccurredAt: time.Now().UTC(),
}
require.NoError(t, publisher.PublishSnapshotUpdate(context.Background(), msg))
entries, err := client.XRange(context.Background(), testStream, "-", "+").Result()
require.NoError(t, err)
require.Len(t, entries, 1)
assert.Equal(t, "[]", entries[0].Values[fieldPlayerTurnStats])
}
func TestPublishSnapshotUpdateRejectsInvalid(t *testing.T) {
publisher, client := newTestPublisher(t)
require.Error(t, publisher.PublishSnapshotUpdate(context.Background(), ports.RuntimeSnapshotUpdate{}))
entries, err := client.XRange(context.Background(), testStream, "-", "+").Result()
require.NoError(t, err)
assert.Empty(t, entries, "invalid messages must not reach the stream")
}
func TestPublishGameFinishedHappyPath(t *testing.T) {
publisher, client := newTestPublisher(t)
finishedAt := time.Date(2026, 4, 28, 8, 30, 0, 0, time.UTC)
msg := ports.GameFinished{
GameID: "game-1",
FinalTurnNumber: 42,
RuntimeStatus: runtime.StatusFinished,
PlayerTurnStats: []ports.PlayerTurnStats{
{UserID: "user-1", Planets: 6, Population: 25000},
{UserID: "user-2", Planets: 0, Population: 0},
},
FinishedAt: finishedAt,
}
require.NoError(t, publisher.PublishGameFinished(context.Background(), msg))
entries, err := client.XRange(context.Background(), testStream, "-", "+").Result()
require.NoError(t, err)
require.Len(t, entries, 1)
values := entries[0].Values
assert.Equal(t, "game_finished", values[fieldEventType])
assert.Equal(t, "game-1", values[fieldGameID])
assert.Equal(t, "42", values[fieldFinalTurnNumber])
assert.Equal(t, "finished", values[fieldRuntimeStatus])
assert.Equal(t, strconv.FormatInt(finishedAt.UnixMilli(), 10), values[fieldFinishedAtMS])
_, hasOccurred := values[fieldOccurredAtMS]
assert.False(t, hasOccurred, "game_finished must not carry occurred_at_ms")
_, hasCurrentTurn := values[fieldCurrentTurn]
assert.False(t, hasCurrentTurn, "game_finished must not carry current_turn")
_, hasHealth := values[fieldEngineHealthSummary]
assert.False(t, hasHealth, "game_finished must not carry engine_health_summary")
}
func TestPublishGameFinishedRejectsBadStatus(t *testing.T) {
publisher, client := newTestPublisher(t)
require.Error(t, publisher.PublishGameFinished(context.Background(), ports.GameFinished{
GameID: "g",
FinalTurnNumber: 1,
RuntimeStatus: runtime.StatusRunning, // wrong status
FinishedAt: time.Now().UTC(),
}))
entries, err := client.XRange(context.Background(), testStream, "-", "+").Result()
require.NoError(t, err)
assert.Empty(t, entries)
}
func TestTimestampsNormalisedToUTC(t *testing.T) {
publisher, client := newTestPublisher(t)
loc, err := time.LoadLocation("Asia/Tokyo")
require.NoError(t, err)
msg := ports.RuntimeSnapshotUpdate{
GameID: "g",
CurrentTurn: 1,
RuntimeStatus: runtime.StatusRunning,
OccurredAt: time.Date(2026, 4, 27, 21, 0, 0, 0, loc),
}
require.NoError(t, publisher.PublishSnapshotUpdate(context.Background(), msg))
entries, err := client.XRange(context.Background(), testStream, "-", "+").Result()
require.NoError(t, err)
require.Len(t, entries, 1)
wantMs := msg.OccurredAt.UTC().UnixMilli()
assert.Equal(t, strconv.FormatInt(wantMs, 10), entries[0].Values[fieldOccurredAtMS])
}
func TestRejectsNilContext(t *testing.T) {
publisher, _ := newTestPublisher(t)
//nolint:staticcheck // explicitly testing nil-context rejection.
err := publisher.PublishSnapshotUpdate(nil, ports.RuntimeSnapshotUpdate{
GameID: "g",
CurrentTurn: 0,
RuntimeStatus: runtime.StatusStarting,
OccurredAt: time.Now().UTC(),
})
require.Error(t, err)
}