187 lines
6.4 KiB
Go
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)
|
|
}
|