feat: game lobby service
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user