206 lines
6.3 KiB
Go
206 lines
6.3 KiB
Go
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)
|
|
}
|