Files
galaxy-game/lobby/internal/domain/game/model_test.go
T
2026-04-25 23:20:55 +02:00

235 lines
6.8 KiB
Go

package game
import (
"strings"
"testing"
"time"
"galaxy/lobby/internal/domain/common"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func validNewGameInput(now time.Time) NewGameInput {
return NewGameInput{
GameID: common.GameID("game-42"),
GameName: "Spring Classic",
Description: "optional",
GameType: GameTypePublic,
OwnerUserID: "",
MinPlayers: 4,
MaxPlayers: 8,
StartGapHours: 24,
StartGapPlayers: 2,
EnrollmentEndsAt: now.Add(7 * 24 * time.Hour),
TurnSchedule: "0 18 * * *",
TargetEngineVersion: "v1.2.3",
Now: now,
}
}
func TestNewGameSucceedsOnHappyPath(t *testing.T) {
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
input := validNewGameInput(now)
record, err := New(input)
require.NoError(t, err)
assert.Equal(t, StatusDraft, record.Status)
assert.Equal(t, input.GameID, record.GameID)
assert.Equal(t, now.UTC(), record.CreatedAt)
assert.Equal(t, now.UTC(), record.UpdatedAt)
assert.Nil(t, record.StartedAt)
assert.Nil(t, record.FinishedAt)
assert.Equal(t, 0, record.RuntimeSnapshot.CurrentTurn)
assert.Empty(t, record.RuntimeSnapshot.RuntimeStatus)
assert.Empty(t, record.RuntimeSnapshot.EngineHealthSummary)
}
func TestNewGameValidatesOwnerBinding(t *testing.T) {
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
privateMissingOwner := validNewGameInput(now)
privateMissingOwner.GameType = GameTypePrivate
privateMissingOwner.OwnerUserID = ""
_, err := New(privateMissingOwner)
require.Error(t, err)
assert.Contains(t, err.Error(), "owner user id must not be empty for private games")
publicWithOwner := validNewGameInput(now)
publicWithOwner.OwnerUserID = "user-1"
_, err = New(publicWithOwner)
require.Error(t, err)
assert.Contains(t, err.Error(), "owner user id must be empty for public games")
privateWithOwner := validNewGameInput(now)
privateWithOwner.GameType = GameTypePrivate
privateWithOwner.OwnerUserID = "user-1"
_, err = New(privateWithOwner)
require.NoError(t, err)
}
func TestNewGameRejectsInvalidSizing(t *testing.T) {
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
cases := map[string]func(*NewGameInput){
"min_players_zero": func(i *NewGameInput) { i.MinPlayers = 0 },
"min_players_negative": func(i *NewGameInput) { i.MinPlayers = -1 },
"max_players_zero": func(i *NewGameInput) { i.MaxPlayers = 0 },
"max_less_than_min": func(i *NewGameInput) { i.MaxPlayers = i.MinPlayers - 1 },
"start_gap_hours_zero": func(i *NewGameInput) { i.StartGapHours = 0 },
"start_gap_players_zero": func(i *NewGameInput) { i.StartGapPlayers = 0 },
}
for name, mutate := range cases {
t.Run(name, func(t *testing.T) {
input := validNewGameInput(now)
mutate(&input)
_, err := New(input)
require.Error(t, err)
})
}
}
func TestNewGameRejectsInvalidEnrollmentDeadline(t *testing.T) {
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
past := validNewGameInput(now)
past.EnrollmentEndsAt = now.Add(-time.Hour)
_, err := New(past)
require.Error(t, err)
zero := validNewGameInput(now)
zero.EnrollmentEndsAt = time.Time{}
_, err = New(zero)
require.Error(t, err)
}
func TestNewGameRejectsInvalidTurnSchedule(t *testing.T) {
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
input := validNewGameInput(now)
input.TurnSchedule = "not a cron"
_, err := New(input)
require.Error(t, err)
assert.Contains(t, err.Error(), "turn schedule")
}
func TestNewGameRejectsInvalidEngineVersion(t *testing.T) {
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
input := validNewGameInput(now)
input.TargetEngineVersion = "not-semver"
_, err := New(input)
require.Error(t, err)
assert.Contains(t, err.Error(), "target engine version")
input = validNewGameInput(now)
input.TargetEngineVersion = ""
_, err = New(input)
require.Error(t, err)
assert.Contains(t, err.Error(), "target engine version")
}
func TestNewGameRejectsEmptyName(t *testing.T) {
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
input := validNewGameInput(now)
input.GameName = " "
_, err := New(input)
require.Error(t, err)
assert.Contains(t, err.Error(), "game name must not be empty")
}
func TestNewGameRejectsInvalidGameID(t *testing.T) {
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
input := validNewGameInput(now)
input.GameID = common.GameID("bogus")
_, err := New(input)
require.Error(t, err)
assert.Contains(t, err.Error(), "game id")
}
func TestGameValidateAcceptsCanonicalSemverWithoutPrefix(t *testing.T) {
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
input := validNewGameInput(now)
input.TargetEngineVersion = "2.0.0"
record, err := New(input)
require.NoError(t, err)
assert.Equal(t, "2.0.0", record.TargetEngineVersion)
}
func TestGameValidateRuntimeBinding(t *testing.T) {
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
input := validNewGameInput(now)
record, err := New(input)
require.NoError(t, err)
bound := now.Add(time.Minute)
record.RuntimeBinding = &RuntimeBinding{
ContainerID: "container-1",
EngineEndpoint: "engine.local:9000",
RuntimeJobID: "1700000000000-0",
BoundAt: bound,
}
require.NoError(t, record.Validate())
cases := map[string]func(binding *RuntimeBinding){
"empty_container_id": func(b *RuntimeBinding) { b.ContainerID = "" },
"empty_engine_endpoint": func(b *RuntimeBinding) { b.EngineEndpoint = "" },
"empty_runtime_job_id": func(b *RuntimeBinding) { b.RuntimeJobID = "" },
"zero_bound_at": func(b *RuntimeBinding) { b.BoundAt = time.Time{} },
}
for name, mutate := range cases {
t.Run(name, func(t *testing.T) {
input := validNewGameInput(now)
rec, err := New(input)
require.NoError(t, err)
binding := RuntimeBinding{
ContainerID: "container-1",
EngineEndpoint: "engine.local:9000",
RuntimeJobID: "1700000000000-0",
BoundAt: bound,
}
mutate(&binding)
rec.RuntimeBinding = &binding
require.Error(t, rec.Validate())
})
}
beforeCreated := validNewGameInput(now)
rec, err := New(beforeCreated)
require.NoError(t, err)
earlier := now.Add(-time.Hour)
rec.RuntimeBinding = &RuntimeBinding{
ContainerID: "container-1",
EngineEndpoint: "engine.local:9000",
RuntimeJobID: "1700000000000-0",
BoundAt: earlier,
}
err = rec.Validate()
require.Error(t, err)
assert.Contains(t, strings.ToLower(err.Error()), "runtime binding bound at must not be before created at")
}
func TestGameValidateDetectsStartedBeforeCreated(t *testing.T) {
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
input := validNewGameInput(now)
record, err := New(input)
require.NoError(t, err)
earlier := now.Add(-time.Minute)
record.StartedAt = &earlier
err = record.Validate()
require.Error(t, err)
assert.Contains(t, strings.ToLower(err.Error()), "started at")
}