Files
galaxy-game/gamemaster/internal/service/livenessreply/service_test.go
T
2026-05-03 07:59:03 +02:00

176 lines
5.4 KiB
Go

package livenessreply_test
import (
"context"
"errors"
"sync"
"testing"
"time"
"galaxy/gamemaster/internal/domain/runtime"
"galaxy/gamemaster/internal/ports"
"galaxy/gamemaster/internal/service/livenessreply"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type fakeRuntimeRecords struct {
mu sync.Mutex
stored map[string]runtime.RuntimeRecord
getErr error
}
func newFakeRuntimeRecords() *fakeRuntimeRecords {
return &fakeRuntimeRecords{stored: map[string]runtime.RuntimeRecord{}}
}
func (s *fakeRuntimeRecords) seed(record runtime.RuntimeRecord) {
s.mu.Lock()
defer s.mu.Unlock()
s.stored[record.GameID] = record
}
func (s *fakeRuntimeRecords) Get(_ context.Context, gameID string) (runtime.RuntimeRecord, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.getErr != nil {
return runtime.RuntimeRecord{}, s.getErr
}
record, ok := s.stored[gameID]
if !ok {
return runtime.RuntimeRecord{}, runtime.ErrNotFound
}
return record, nil
}
func (s *fakeRuntimeRecords) Insert(context.Context, runtime.RuntimeRecord) error {
return errors.New("not used")
}
func (s *fakeRuntimeRecords) UpdateStatus(context.Context, ports.UpdateStatusInput) error {
return errors.New("not used")
}
func (s *fakeRuntimeRecords) UpdateScheduling(context.Context, ports.UpdateSchedulingInput) error {
return errors.New("not used")
}
func (s *fakeRuntimeRecords) UpdateImage(context.Context, ports.UpdateImageInput) error {
return errors.New("not used")
}
func (s *fakeRuntimeRecords) UpdateEngineHealth(context.Context, ports.UpdateEngineHealthInput) error {
return errors.New("not used")
}
func (s *fakeRuntimeRecords) Delete(context.Context, string) error {
return errors.New("not used")
}
func (s *fakeRuntimeRecords) ListDueRunning(context.Context, time.Time) ([]runtime.RuntimeRecord, error) {
return nil, errors.New("not used")
}
func (s *fakeRuntimeRecords) ListByStatus(context.Context, runtime.Status) ([]runtime.RuntimeRecord, error) {
return nil, errors.New("not used")
}
func (s *fakeRuntimeRecords) List(context.Context) ([]runtime.RuntimeRecord, error) {
return nil, errors.New("not used")
}
func newService(t *testing.T, store *fakeRuntimeRecords) *livenessreply.Service {
t.Helper()
service, err := livenessreply.NewService(livenessreply.Dependencies{
RuntimeRecords: store,
})
require.NoError(t, err)
return service
}
func runningRecord(gameID string) runtime.RuntimeRecord {
now := time.Date(2026, time.May, 1, 12, 0, 0, 0, time.UTC)
return runtime.RuntimeRecord{
GameID: gameID,
Status: runtime.StatusRunning,
EngineEndpoint: "http://galaxy-game-" + gameID + ":8080",
CurrentImageRef: "ghcr.io/galaxy/game:v1.2.3",
CurrentEngineVersion: "v1.2.3",
TurnSchedule: "0 18 * * *",
CurrentTurn: 5,
CreatedAt: now,
UpdatedAt: now,
}
}
func TestNewServiceRejectsNilRuntimeRecords(t *testing.T) {
_, err := livenessreply.NewService(livenessreply.Dependencies{})
require.Error(t, err)
}
func TestHandleRunningReturnsReadyTrue(t *testing.T) {
store := newFakeRuntimeRecords()
store.seed(runningRecord("game-001"))
service := newService(t, store)
result, err := service.Handle(context.Background(), livenessreply.Input{GameID: "game-001"})
require.NoError(t, err)
assert.True(t, result.Ready)
assert.Equal(t, runtime.StatusRunning, result.Status)
}
func TestHandleNonRunningReturnsReadyFalseWithStatus(t *testing.T) {
cases := []runtime.Status{
runtime.StatusStarting,
runtime.StatusGenerationInProgress,
runtime.StatusGenerationFailed,
runtime.StatusEngineUnreachable,
runtime.StatusStopped,
runtime.StatusFinished,
}
for _, status := range cases {
t.Run(string(status), func(t *testing.T) {
store := newFakeRuntimeRecords()
rec := runningRecord("game-001")
rec.Status = status
store.seed(rec)
service := newService(t, store)
result, err := service.Handle(context.Background(), livenessreply.Input{GameID: "game-001"})
require.NoError(t, err)
assert.False(t, result.Ready)
assert.Equal(t, status, result.Status)
})
}
}
func TestHandleRuntimeNotFoundReturnsEmptyStatus(t *testing.T) {
store := newFakeRuntimeRecords()
service := newService(t, store)
result, err := service.Handle(context.Background(), livenessreply.Input{GameID: "missing"})
require.NoError(t, err, "runtime_not_found is absorbed into 200 response per Stage 17 D5")
assert.False(t, result.Ready)
assert.Equal(t, runtime.Status(""), result.Status)
}
func TestHandleStoreReadFailureReturnsServiceUnavailable(t *testing.T) {
store := newFakeRuntimeRecords()
store.getErr = errors.New("connection refused")
service := newService(t, store)
_, err := service.Handle(context.Background(), livenessreply.Input{GameID: "game-001"})
require.Error(t, err)
assert.Contains(t, err.Error(), livenessreply.ErrorCodeServiceUnavailable)
}
func TestHandleEmptyGameIDReturnsInvalidRequest(t *testing.T) {
store := newFakeRuntimeRecords()
service := newService(t, store)
_, err := service.Handle(context.Background(), livenessreply.Input{GameID: ""})
require.Error(t, err)
assert.Contains(t, err.Error(), livenessreply.ErrorCodeInvalidRequest)
}
func TestHandleNilContextReturnsError(t *testing.T) {
store := newFakeRuntimeRecords()
service := newService(t, store)
_, err := service.Handle(nil, livenessreply.Input{GameID: "game-001"}) //nolint:staticcheck // guard test
require.Error(t, err)
}