176 lines
5.4 KiB
Go
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)
|
|
}
|