277 lines
7.6 KiB
Go
277 lines
7.6 KiB
Go
package gameinmem
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"galaxy/lobby/internal/domain/common"
|
|
"galaxy/lobby/internal/domain/game"
|
|
"galaxy/lobby/internal/ports"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func newDraftRecord(t *testing.T, id common.GameID, createdAt time.Time) game.Game {
|
|
t.Helper()
|
|
|
|
record, err := game.New(game.NewGameInput{
|
|
GameID: id,
|
|
GameName: "Test Game",
|
|
GameType: game.GameTypePublic,
|
|
OwnerUserID: "",
|
|
MinPlayers: 2,
|
|
MaxPlayers: 4,
|
|
StartGapHours: 4,
|
|
StartGapPlayers: 1,
|
|
EnrollmentEndsAt: createdAt.Add(24 * time.Hour),
|
|
TurnSchedule: "0 */6 * * *",
|
|
TargetEngineVersion: "1.0.0",
|
|
Now: createdAt,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return record
|
|
}
|
|
|
|
func TestStoreSaveGetRoundtrip(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := NewStore()
|
|
ctx := context.Background()
|
|
|
|
record := newDraftRecord(t, "game-alpha", time.Date(2026, 4, 24, 10, 0, 0, 0, time.UTC))
|
|
require.NoError(t, store.Save(ctx, record))
|
|
|
|
loaded, err := store.Get(ctx, "game-alpha")
|
|
require.NoError(t, err)
|
|
require.Equal(t, record.GameID, loaded.GameID)
|
|
require.Equal(t, record.Status, loaded.Status)
|
|
require.Equal(t, record.UpdatedAt.UTC(), loaded.UpdatedAt)
|
|
}
|
|
|
|
func TestStoreGetMissing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := NewStore()
|
|
|
|
_, err := store.Get(context.Background(), "game-missing")
|
|
require.ErrorIs(t, err, game.ErrNotFound)
|
|
}
|
|
|
|
func TestStoreGetByStatusOrderedByCreatedAt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := NewStore()
|
|
ctx := context.Background()
|
|
|
|
earlier := time.Date(2026, 4, 24, 9, 0, 0, 0, time.UTC)
|
|
later := earlier.Add(30 * time.Minute)
|
|
|
|
a := newDraftRecord(t, "game-a", earlier)
|
|
b := newDraftRecord(t, "game-b", later)
|
|
require.NoError(t, store.Save(ctx, b))
|
|
require.NoError(t, store.Save(ctx, a))
|
|
|
|
records, err := store.GetByStatus(ctx, game.StatusDraft)
|
|
require.NoError(t, err)
|
|
require.Len(t, records, 2)
|
|
require.Equal(t, common.GameID("game-a"), records[0].GameID)
|
|
require.Equal(t, common.GameID("game-b"), records[1].GameID)
|
|
}
|
|
|
|
func TestStoreCountByStatusReturnsAllStatusBuckets(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := NewStore()
|
|
ctx := context.Background()
|
|
|
|
createdAt := time.Date(2026, 4, 25, 10, 0, 0, 0, time.UTC)
|
|
require.NoError(t, store.Save(ctx, newDraftRecord(t, "game-a", createdAt)))
|
|
require.NoError(t, store.Save(ctx, newDraftRecord(t, "game-b", createdAt)))
|
|
|
|
counts, err := store.CountByStatus(ctx)
|
|
require.NoError(t, err)
|
|
|
|
for _, status := range game.AllStatuses() {
|
|
_, present := counts[status]
|
|
require.True(t, present, "expected %s bucket", status)
|
|
}
|
|
require.Equal(t, 2, counts[game.StatusDraft])
|
|
require.Equal(t, 0, counts[game.StatusRunning])
|
|
}
|
|
|
|
func TestStoreUpdateStatusHappyPath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := NewStore()
|
|
ctx := context.Background()
|
|
|
|
created := time.Date(2026, 4, 24, 10, 0, 0, 0, time.UTC)
|
|
record := newDraftRecord(t, "game-open", created)
|
|
require.NoError(t, store.Save(ctx, record))
|
|
|
|
at := created.Add(time.Hour)
|
|
err := store.UpdateStatus(ctx, ports.UpdateStatusInput{
|
|
GameID: "game-open",
|
|
ExpectedFrom: game.StatusDraft,
|
|
To: game.StatusEnrollmentOpen,
|
|
Trigger: game.TriggerCommand,
|
|
At: at,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
loaded, err := store.Get(ctx, "game-open")
|
|
require.NoError(t, err)
|
|
require.Equal(t, game.StatusEnrollmentOpen, loaded.Status)
|
|
require.Equal(t, at.UTC(), loaded.UpdatedAt)
|
|
}
|
|
|
|
func TestStoreUpdateStatusInvalidTransition(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := NewStore()
|
|
ctx := context.Background()
|
|
|
|
record := newDraftRecord(t, "game-invalid", time.Date(2026, 4, 24, 10, 0, 0, 0, time.UTC))
|
|
require.NoError(t, store.Save(ctx, record))
|
|
|
|
err := store.UpdateStatus(ctx, ports.UpdateStatusInput{
|
|
GameID: "game-invalid",
|
|
ExpectedFrom: game.StatusDraft,
|
|
To: game.StatusRunning,
|
|
Trigger: game.TriggerCommand,
|
|
At: time.Now().UTC(),
|
|
})
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, game.ErrInvalidTransition)
|
|
}
|
|
|
|
func TestStoreUpdateStatusCASMismatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := NewStore()
|
|
ctx := context.Background()
|
|
|
|
created := time.Date(2026, 4, 24, 10, 0, 0, 0, time.UTC)
|
|
record := newDraftRecord(t, "game-cas", created)
|
|
require.NoError(t, store.Save(ctx, record))
|
|
|
|
err := store.UpdateStatus(ctx, ports.UpdateStatusInput{
|
|
GameID: "game-cas",
|
|
ExpectedFrom: game.StatusEnrollmentOpen,
|
|
To: game.StatusReadyToStart,
|
|
Trigger: game.TriggerManual,
|
|
At: created.Add(time.Hour),
|
|
})
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, game.ErrConflict)
|
|
}
|
|
|
|
func TestStoreUpdateStatusMissing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := NewStore()
|
|
|
|
err := store.UpdateStatus(context.Background(), ports.UpdateStatusInput{
|
|
GameID: "game-nope",
|
|
ExpectedFrom: game.StatusDraft,
|
|
To: game.StatusEnrollmentOpen,
|
|
Trigger: game.TriggerCommand,
|
|
At: time.Now().UTC(),
|
|
})
|
|
require.ErrorIs(t, err, game.ErrNotFound)
|
|
}
|
|
|
|
func TestStoreUpdateRuntimeSnapshot(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := NewStore()
|
|
ctx := context.Background()
|
|
|
|
created := time.Date(2026, 4, 24, 10, 0, 0, 0, time.UTC)
|
|
record := newDraftRecord(t, "game-snap", created)
|
|
require.NoError(t, store.Save(ctx, record))
|
|
|
|
err := store.UpdateRuntimeSnapshot(ctx, ports.UpdateRuntimeSnapshotInput{
|
|
GameID: "game-snap",
|
|
Snapshot: game.RuntimeSnapshot{
|
|
CurrentTurn: 7,
|
|
RuntimeStatus: "alive",
|
|
EngineHealthSummary: "ok",
|
|
},
|
|
At: created.Add(2 * time.Hour),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
loaded, err := store.Get(ctx, "game-snap")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 7, loaded.RuntimeSnapshot.CurrentTurn)
|
|
require.Equal(t, "alive", loaded.RuntimeSnapshot.RuntimeStatus)
|
|
require.Equal(t, game.StatusDraft, loaded.Status, "snapshot update must not alter status")
|
|
}
|
|
|
|
func TestStoreValidateInputs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := NewStore()
|
|
ctx := context.Background()
|
|
|
|
err := store.UpdateStatus(ctx, ports.UpdateStatusInput{GameID: ""})
|
|
require.Error(t, err)
|
|
|
|
err = store.UpdateRuntimeSnapshot(ctx, ports.UpdateRuntimeSnapshotInput{GameID: ""})
|
|
require.Error(t, err)
|
|
|
|
_, err = store.GetByStatus(ctx, game.Status("ghost"))
|
|
require.Error(t, err)
|
|
|
|
require.True(t, errors.Is(game.ErrNotFound, game.ErrNotFound))
|
|
}
|
|
|
|
func TestStoreUpdateStatusSetsStartedAtAndFinishedAt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := NewStore()
|
|
ctx := context.Background()
|
|
|
|
created := time.Date(2026, 4, 24, 10, 0, 0, 0, time.UTC)
|
|
record := newDraftRecord(t, "game-timeline", created)
|
|
record.Status = game.StatusStarting
|
|
record.UpdatedAt = created.Add(time.Hour)
|
|
require.NoError(t, store.Save(ctx, record))
|
|
|
|
runningAt := created.Add(2 * time.Hour)
|
|
err := store.UpdateStatus(ctx, ports.UpdateStatusInput{
|
|
GameID: "game-timeline",
|
|
ExpectedFrom: game.StatusStarting,
|
|
To: game.StatusRunning,
|
|
Trigger: game.TriggerRuntimeEvent,
|
|
At: runningAt,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
loaded, err := store.Get(ctx, "game-timeline")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, loaded.StartedAt)
|
|
require.Equal(t, runningAt.UTC(), loaded.StartedAt.UTC())
|
|
require.Nil(t, loaded.FinishedAt)
|
|
|
|
finishAt := runningAt.Add(5 * time.Hour)
|
|
err = store.UpdateStatus(ctx, ports.UpdateStatusInput{
|
|
GameID: "game-timeline",
|
|
ExpectedFrom: game.StatusRunning,
|
|
To: game.StatusFinished,
|
|
Trigger: game.TriggerRuntimeEvent,
|
|
At: finishAt,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
loaded, err = store.Get(ctx, "game-timeline")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, loaded.FinishedAt)
|
|
require.Equal(t, finishAt.UTC(), loaded.FinishedAt.UTC())
|
|
require.Equal(t, runningAt.UTC(), loaded.StartedAt.UTC(), "StartedAt must be preserved")
|
|
}
|