feat: gamemaster
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"galaxy/gamemaster/internal/domain/engineversion"
|
||||
"galaxy/gamemaster/internal/domain/operation"
|
||||
"galaxy/gamemaster/internal/domain/runtime"
|
||||
engineversionsvc "galaxy/gamemaster/internal/service/engineversion"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMapErrorCodeToStatusCoversEveryDocumentedCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := map[string]int{
|
||||
errorCodeInvalidRequest: http.StatusBadRequest,
|
||||
errorCodeForbidden: http.StatusForbidden,
|
||||
errorCodeRuntimeNotFound: http.StatusNotFound,
|
||||
errorCodeEngineVersionNotFound: http.StatusNotFound,
|
||||
errorCodeConflict: http.StatusConflict,
|
||||
errorCodeRuntimeNotRunning: http.StatusConflict,
|
||||
errorCodeSemverPatchOnly: http.StatusConflict,
|
||||
errorCodeEngineVersionInUse: http.StatusConflict,
|
||||
errorCodeEngineUnreachable: http.StatusBadGateway,
|
||||
errorCodeEngineValidationError: http.StatusBadGateway,
|
||||
errorCodeEngineProtocolError: http.StatusBadGateway,
|
||||
errorCodeServiceUnavailable: http.StatusServiceUnavailable,
|
||||
errorCodeInternal: http.StatusInternalServerError,
|
||||
"unknown_code": http.StatusInternalServerError,
|
||||
}
|
||||
|
||||
for code, expected := range cases {
|
||||
assert.Equalf(t, expected, mapErrorCodeToStatus(code), "code %q", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapServiceErrorMapsEverySentinel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
err error
|
||||
status int
|
||||
code string
|
||||
}{
|
||||
{engineversionsvc.ErrInvalidRequest, http.StatusBadRequest, errorCodeInvalidRequest},
|
||||
{engineversionsvc.ErrNotFound, http.StatusNotFound, errorCodeEngineVersionNotFound},
|
||||
{engineversionsvc.ErrConflict, http.StatusConflict, errorCodeConflict},
|
||||
{engineversionsvc.ErrInUse, http.StatusConflict, errorCodeEngineVersionInUse},
|
||||
{engineversionsvc.ErrServiceUnavailable, http.StatusServiceUnavailable, errorCodeServiceUnavailable},
|
||||
{errors.New("plain go error"), http.StatusInternalServerError, errorCodeInternal},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
status, code, _ := mapServiceError(tc.err)
|
||||
assert.Equalf(t, tc.status, status, "status for %v", tc.err)
|
||||
assert.Equalf(t, tc.code, code, "code for %v", tc.err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveOpSourceMapsCallerHeader(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := map[string]operation.OpSource{
|
||||
"": operation.OpSourceAdminRest,
|
||||
"unknown": operation.OpSourceAdminRest,
|
||||
"GATEWAY": operation.OpSourceGatewayPlayer,
|
||||
" lobby ": operation.OpSourceLobbyInternal,
|
||||
"admin": operation.OpSourceAdminRest,
|
||||
}
|
||||
|
||||
for value, expected := range cases {
|
||||
request := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
if value != "" {
|
||||
request.Header.Set(callerHeader, value)
|
||||
}
|
||||
assert.Equalf(t, expected, resolveOpSource(request), "header %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestSourceRefReadsXRequestID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
assert.Empty(t, requestSourceRef(request))
|
||||
|
||||
request.Header.Set(requestIDHeader, " trace-123 ")
|
||||
assert.Equal(t, "trace-123", requestSourceRef(request))
|
||||
}
|
||||
|
||||
func TestDecodeStrictJSONRejectsUnknownFieldsAndTrailingContent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type input struct {
|
||||
Field string `json:"field"`
|
||||
}
|
||||
|
||||
var ok input
|
||||
require.NoError(t, decodeStrictJSON(strings.NewReader(`{"field":"value"}`), &ok))
|
||||
assert.Equal(t, "value", ok.Field)
|
||||
|
||||
var rejected input
|
||||
err := decodeStrictJSON(strings.NewReader(`{"field":"v","extra":1}`), &rejected)
|
||||
require.Error(t, err)
|
||||
|
||||
var trailing input
|
||||
err = decodeStrictJSON(strings.NewReader(`{"field":"v"}{"another":true}`), &trailing)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestReadRawJSONBodyValidatesPayload(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
body, err := readRawJSONBody(strings.NewReader(`{"commands":[]}`))
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, `{"commands":[]}`, string(body))
|
||||
|
||||
_, err = readRawJSONBody(strings.NewReader(""))
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = readRawJSONBody(strings.NewReader("not json"))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEncodeRuntimeRecordIncludesEveryRequiredField(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
moment := time.Date(2026, 5, 1, 9, 30, 0, 0, time.UTC)
|
||||
next := moment.Add(time.Minute)
|
||||
record := runtime.RuntimeRecord{
|
||||
GameID: "game-1",
|
||||
Status: runtime.StatusRunning,
|
||||
EngineEndpoint: "http://example:8080",
|
||||
CurrentImageRef: "galaxy/game:1.2.3",
|
||||
CurrentEngineVersion: "1.2.3",
|
||||
TurnSchedule: "0 18 * * *",
|
||||
CurrentTurn: 7,
|
||||
NextGenerationAt: &next,
|
||||
SkipNextTick: true,
|
||||
EngineHealth: "healthy",
|
||||
CreatedAt: moment,
|
||||
UpdatedAt: moment,
|
||||
StartedAt: &moment,
|
||||
}
|
||||
|
||||
encoded := encodeRuntimeRecord(record)
|
||||
assert.Equal(t, "game-1", encoded.GameID)
|
||||
assert.Equal(t, "running", encoded.RuntimeStatus)
|
||||
assert.Equal(t, moment.UnixMilli(), encoded.CreatedAt)
|
||||
assert.Equal(t, next.UnixMilli(), encoded.NextGenerationAt)
|
||||
require.NotNil(t, encoded.StartedAt)
|
||||
assert.Equal(t, moment.UnixMilli(), *encoded.StartedAt)
|
||||
assert.Nil(t, encoded.StoppedAt)
|
||||
assert.Nil(t, encoded.FinishedAt)
|
||||
}
|
||||
|
||||
func TestEncodeRuntimeRecordZerosNextGenerationWhenNil(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
moment := time.Date(2026, 5, 1, 9, 30, 0, 0, time.UTC)
|
||||
record := runtime.RuntimeRecord{
|
||||
GameID: "game-1",
|
||||
Status: runtime.StatusStarting,
|
||||
EngineEndpoint: "http://example:8080",
|
||||
CurrentImageRef: "galaxy/game:1.2.3",
|
||||
CurrentEngineVersion: "1.2.3",
|
||||
TurnSchedule: "0 18 * * *",
|
||||
CreatedAt: moment,
|
||||
UpdatedAt: moment,
|
||||
}
|
||||
|
||||
encoded := encodeRuntimeRecord(record)
|
||||
assert.Equal(t, int64(0), encoded.NextGenerationAt)
|
||||
assert.Nil(t, encoded.StartedAt)
|
||||
}
|
||||
|
||||
func TestEncodeEngineVersionDefaultsEmptyOptionsToObject(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
moment := time.Date(2026, 5, 1, 9, 30, 0, 0, time.UTC)
|
||||
encoded := encodeEngineVersion(engineversion.EngineVersion{
|
||||
Version: "1.2.3",
|
||||
ImageRef: "galaxy/game:1.2.3",
|
||||
Status: engineversion.StatusActive,
|
||||
CreatedAt: moment,
|
||||
UpdatedAt: moment,
|
||||
})
|
||||
assert.Equal(t, "{}", string(encoded.Options))
|
||||
assert.Equal(t, "active", encoded.Status)
|
||||
}
|
||||
|
||||
func TestEncodeRuntimeListAlwaysReturnsNonNilSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
resp := encodeRuntimeList(nil)
|
||||
require.NotNil(t, resp.Runtimes)
|
||||
assert.Empty(t, resp.Runtimes)
|
||||
}
|
||||
Reference in New Issue
Block a user