611 lines
20 KiB
Go
611 lines
20 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"galaxy/rtmanager/internal/api/internalhttp/handlers/mocks"
|
|
"galaxy/rtmanager/internal/domain/operation"
|
|
"galaxy/rtmanager/internal/domain/runtime"
|
|
"galaxy/rtmanager/internal/service/cleanupcontainer"
|
|
"galaxy/rtmanager/internal/service/patchruntime"
|
|
"galaxy/rtmanager/internal/service/restartruntime"
|
|
"galaxy/rtmanager/internal/service/startruntime"
|
|
"galaxy/rtmanager/internal/service/stopruntime"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/mock/gomock"
|
|
)
|
|
|
|
// Tests for the mutating handlers (start, stop, restart, patch,
|
|
// cleanup). Each handler delegates to one lifecycle service through a
|
|
// narrow `mockgen`-backed interface; the handler layer is responsible
|
|
// for input parsing, the `X-Galaxy-Caller` → `op_source` mapping, and
|
|
// the canonical `ErrorCode` → HTTP status table documented in
|
|
// `rtmanager/docs/services.md` §18.
|
|
|
|
// --- start ---
|
|
|
|
func TestStartHandlerReturnsRecordOnSuccess(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockStartService(ctrl)
|
|
|
|
record := sampleRunningRecord(t)
|
|
mock.EXPECT().
|
|
Handle(gomock.Any(), gomock.AssignableToTypeOf(startruntime.Input{})).
|
|
DoAndReturn(func(_ context.Context, in startruntime.Input) (startruntime.Result, error) {
|
|
assert.Equal(t, "game-test", in.GameID)
|
|
assert.Equal(t, "galaxy/game:v1.2.3", in.ImageRef)
|
|
assert.Equal(t, operation.OpSourceAdminRest, in.OpSource)
|
|
return startruntime.Result{Record: record, Outcome: operation.OutcomeSuccess}, nil
|
|
})
|
|
|
|
deps := Dependencies{StartRuntime: mock}
|
|
rec := drive(t, deps, http.MethodPost, "/api/v1/internal/runtimes/game-test/start",
|
|
jsonHeaders(),
|
|
strReader(`{"image_ref":"galaxy/game:v1.2.3"}`),
|
|
)
|
|
|
|
resp := decodeRecordResponse(t, rec)
|
|
assert.Equal(t, "game-test", resp.GameID)
|
|
assert.Equal(t, "running", resp.Status)
|
|
}
|
|
|
|
func TestStartHandlerReturnsRecordOnReplayNoOp(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockStartService(ctrl)
|
|
|
|
record := sampleRunningRecord(t)
|
|
mock.EXPECT().
|
|
Handle(gomock.Any(), gomock.Any()).
|
|
Return(startruntime.Result{
|
|
Record: record,
|
|
Outcome: operation.OutcomeSuccess,
|
|
ErrorCode: startruntime.ErrorCodeReplayNoOp,
|
|
}, nil)
|
|
|
|
rec := drive(t, Dependencies{StartRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/start",
|
|
jsonHeaders(),
|
|
strReader(`{"image_ref":"galaxy/game:v1.2.3"}`),
|
|
)
|
|
|
|
resp := decodeRecordResponse(t, rec)
|
|
assert.Equal(t, "game-test", resp.GameID)
|
|
}
|
|
|
|
func TestStartHandlerMapsServiceFailures(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
errorCode string
|
|
wantStatus int
|
|
}{
|
|
{"start_config_invalid", startruntime.ErrorCodeStartConfigInvalid, http.StatusBadRequest},
|
|
{"image_pull_failed", startruntime.ErrorCodeImagePullFailed, http.StatusInternalServerError},
|
|
{"container_start_failed", startruntime.ErrorCodeContainerStartFailed, http.StatusInternalServerError},
|
|
{"conflict", startruntime.ErrorCodeConflict, http.StatusConflict},
|
|
{"service_unavailable", startruntime.ErrorCodeServiceUnavailable, http.StatusServiceUnavailable},
|
|
{"internal_error", startruntime.ErrorCodeInternal, http.StatusInternalServerError},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockStartService(ctrl)
|
|
|
|
mock.EXPECT().
|
|
Handle(gomock.Any(), gomock.Any()).
|
|
Return(startruntime.Result{
|
|
Outcome: operation.OutcomeFailure,
|
|
ErrorCode: tc.errorCode,
|
|
ErrorMessage: "synthetic " + tc.name,
|
|
}, nil)
|
|
|
|
rec := drive(t, Dependencies{StartRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/start",
|
|
jsonHeaders(),
|
|
strReader(`{"image_ref":"galaxy/game:v1.2.3"}`),
|
|
)
|
|
|
|
body := decodeErrorBody(t, rec, tc.wantStatus)
|
|
assert.Equal(t, tc.errorCode, body.Code)
|
|
assert.Equal(t, "synthetic "+tc.name, body.Message)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStartHandlerRejectsUnknownJSONFields(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockStartService(ctrl)
|
|
|
|
rec := drive(t, Dependencies{StartRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/start",
|
|
jsonHeaders(),
|
|
strReader(`{"image_ref":"x","extra":"y"}`),
|
|
)
|
|
|
|
body := decodeErrorBody(t, rec, http.StatusBadRequest)
|
|
assert.Equal(t, "invalid_request", body.Code)
|
|
}
|
|
|
|
func TestStartHandlerRejectsMalformedJSON(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockStartService(ctrl)
|
|
|
|
rec := drive(t, Dependencies{StartRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/start",
|
|
jsonHeaders(),
|
|
strReader(`{"image_ref":`),
|
|
)
|
|
|
|
body := decodeErrorBody(t, rec, http.StatusBadRequest)
|
|
assert.Equal(t, "invalid_request", body.Code)
|
|
}
|
|
|
|
func TestStartHandlerHonoursXGalaxyCallerHeader(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
header string
|
|
want operation.OpSource
|
|
hdrLabel string
|
|
}{
|
|
{"gm", operation.OpSourceGMRest, "gm"},
|
|
{"GM", operation.OpSourceGMRest, "uppercase gm"},
|
|
{"admin", operation.OpSourceAdminRest, "admin"},
|
|
{"unknown", operation.OpSourceAdminRest, "unknown value"},
|
|
{"", operation.OpSourceAdminRest, "missing header"},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.hdrLabel, func(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockStartService(ctrl)
|
|
|
|
record := sampleRunningRecord(t)
|
|
mock.EXPECT().
|
|
Handle(gomock.Any(), gomock.AssignableToTypeOf(startruntime.Input{})).
|
|
DoAndReturn(func(_ context.Context, in startruntime.Input) (startruntime.Result, error) {
|
|
assert.Equal(t, tc.want, in.OpSource)
|
|
return startruntime.Result{Record: record, Outcome: operation.OutcomeSuccess}, nil
|
|
})
|
|
|
|
headers := jsonHeaders()
|
|
if tc.header != "" {
|
|
headers = withCaller(headers, tc.header)
|
|
}
|
|
rec := drive(t, Dependencies{StartRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/start",
|
|
headers,
|
|
strReader(`{"image_ref":"galaxy/game:v1.2.3"}`),
|
|
)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStartHandlerForwardsXRequestIDAsSourceRef(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockStartService(ctrl)
|
|
|
|
mock.EXPECT().
|
|
Handle(gomock.Any(), gomock.AssignableToTypeOf(startruntime.Input{})).
|
|
DoAndReturn(func(_ context.Context, in startruntime.Input) (startruntime.Result, error) {
|
|
assert.Equal(t, "req-42", in.SourceRef)
|
|
return startruntime.Result{Record: sampleRunningRecord(t), Outcome: operation.OutcomeSuccess}, nil
|
|
})
|
|
|
|
headers := jsonHeaders()
|
|
headers.Set("X-Request-ID", "req-42")
|
|
rec := drive(t, Dependencies{StartRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/start",
|
|
headers,
|
|
strReader(`{"image_ref":"galaxy/game:v1.2.3"}`),
|
|
)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
}
|
|
|
|
func TestStartHandlerReturnsInternalErrorWhenServiceErrors(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockStartService(ctrl)
|
|
|
|
mock.EXPECT().
|
|
Handle(gomock.Any(), gomock.Any()).
|
|
Return(startruntime.Result{}, assert.AnError)
|
|
|
|
rec := drive(t, Dependencies{StartRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/start",
|
|
jsonHeaders(),
|
|
strReader(`{"image_ref":"galaxy/game:v1.2.3"}`),
|
|
)
|
|
|
|
body := decodeErrorBody(t, rec, http.StatusInternalServerError)
|
|
assert.Equal(t, "internal_error", body.Code)
|
|
}
|
|
|
|
func TestStartHandlerReturnsInternalErrorWhenServiceNotWired(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rec := drive(t, Dependencies{}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/start",
|
|
jsonHeaders(),
|
|
strReader(`{"image_ref":"galaxy/game:v1.2.3"}`),
|
|
)
|
|
|
|
body := decodeErrorBody(t, rec, http.StatusInternalServerError)
|
|
assert.Equal(t, "internal_error", body.Code)
|
|
}
|
|
|
|
// --- stop ---
|
|
|
|
func TestStopHandlerReturnsRecordOnSuccess(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockStopService(ctrl)
|
|
|
|
record := sampleStoppedRecord(t)
|
|
mock.EXPECT().
|
|
Handle(gomock.Any(), gomock.AssignableToTypeOf(stopruntime.Input{})).
|
|
DoAndReturn(func(_ context.Context, in stopruntime.Input) (stopruntime.Result, error) {
|
|
assert.Equal(t, "game-test", in.GameID)
|
|
assert.Equal(t, stopruntime.StopReasonAdminRequest, in.Reason)
|
|
assert.Equal(t, operation.OpSourceAdminRest, in.OpSource)
|
|
return stopruntime.Result{Record: record, Outcome: operation.OutcomeSuccess}, nil
|
|
})
|
|
|
|
rec := drive(t, Dependencies{StopRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/stop",
|
|
jsonHeaders(),
|
|
strReader(`{"reason":"admin_request"}`),
|
|
)
|
|
|
|
resp := decodeRecordResponse(t, rec)
|
|
assert.Equal(t, "stopped", resp.Status)
|
|
}
|
|
|
|
func TestStopHandlerMapsServiceFailures(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
name string
|
|
errorCode string
|
|
wantStatus int
|
|
}{
|
|
{"not_found", startruntime.ErrorCodeNotFound, http.StatusNotFound},
|
|
{"conflict", startruntime.ErrorCodeConflict, http.StatusConflict},
|
|
{"invalid_request", startruntime.ErrorCodeInvalidRequest, http.StatusBadRequest},
|
|
{"service_unavailable", startruntime.ErrorCodeServiceUnavailable, http.StatusServiceUnavailable},
|
|
{"internal_error", startruntime.ErrorCodeInternal, http.StatusInternalServerError},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockStopService(ctrl)
|
|
mock.EXPECT().Handle(gomock.Any(), gomock.Any()).Return(stopruntime.Result{
|
|
Outcome: operation.OutcomeFailure, ErrorCode: tc.errorCode, ErrorMessage: tc.name,
|
|
}, nil)
|
|
|
|
rec := drive(t, Dependencies{StopRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/stop",
|
|
jsonHeaders(),
|
|
strReader(`{"reason":"admin_request"}`),
|
|
)
|
|
body := decodeErrorBody(t, rec, tc.wantStatus)
|
|
assert.Equal(t, tc.errorCode, body.Code)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStopHandlerRejectsUnknownJSONFields(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockStopService(ctrl)
|
|
|
|
rec := drive(t, Dependencies{StopRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/stop",
|
|
jsonHeaders(),
|
|
strReader(`{"reason":"admin_request","extra":1}`),
|
|
)
|
|
body := decodeErrorBody(t, rec, http.StatusBadRequest)
|
|
assert.Equal(t, "invalid_request", body.Code)
|
|
}
|
|
|
|
func TestStopHandlerHonoursXGalaxyCallerHeader(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockStopService(ctrl)
|
|
|
|
mock.EXPECT().
|
|
Handle(gomock.Any(), gomock.AssignableToTypeOf(stopruntime.Input{})).
|
|
DoAndReturn(func(_ context.Context, in stopruntime.Input) (stopruntime.Result, error) {
|
|
assert.Equal(t, operation.OpSourceGMRest, in.OpSource)
|
|
return stopruntime.Result{Record: sampleStoppedRecord(t), Outcome: operation.OutcomeSuccess}, nil
|
|
})
|
|
|
|
rec := drive(t, Dependencies{StopRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/stop",
|
|
withCaller(jsonHeaders(), "gm"),
|
|
strReader(`{"reason":"cancelled"}`),
|
|
)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
}
|
|
|
|
func TestStopHandlerReturnsInternalErrorWhenServiceNotWired(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rec := drive(t, Dependencies{}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/stop",
|
|
jsonHeaders(),
|
|
strReader(`{"reason":"admin_request"}`),
|
|
)
|
|
body := decodeErrorBody(t, rec, http.StatusInternalServerError)
|
|
assert.Equal(t, "internal_error", body.Code)
|
|
}
|
|
|
|
// --- restart ---
|
|
|
|
func TestRestartHandlerReturnsRecordOnSuccess(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockRestartService(ctrl)
|
|
|
|
record := sampleRunningRecord(t)
|
|
mock.EXPECT().
|
|
Handle(gomock.Any(), gomock.AssignableToTypeOf(restartruntime.Input{})).
|
|
DoAndReturn(func(_ context.Context, in restartruntime.Input) (restartruntime.Result, error) {
|
|
assert.Equal(t, "game-test", in.GameID)
|
|
assert.Equal(t, operation.OpSourceAdminRest, in.OpSource)
|
|
return restartruntime.Result{Record: record, Outcome: operation.OutcomeSuccess}, nil
|
|
})
|
|
|
|
rec := drive(t, Dependencies{RestartRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/restart", nil, nil,
|
|
)
|
|
resp := decodeRecordResponse(t, rec)
|
|
assert.Equal(t, "running", resp.Status)
|
|
}
|
|
|
|
func TestRestartHandlerMapsServiceFailures(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
name string
|
|
errorCode string
|
|
wantStatus int
|
|
}{
|
|
{"not_found", startruntime.ErrorCodeNotFound, http.StatusNotFound},
|
|
{"conflict", startruntime.ErrorCodeConflict, http.StatusConflict},
|
|
{"service_unavailable", startruntime.ErrorCodeServiceUnavailable, http.StatusServiceUnavailable},
|
|
{"internal_error", startruntime.ErrorCodeInternal, http.StatusInternalServerError},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockRestartService(ctrl)
|
|
mock.EXPECT().Handle(gomock.Any(), gomock.Any()).Return(restartruntime.Result{
|
|
Outcome: operation.OutcomeFailure, ErrorCode: tc.errorCode, ErrorMessage: tc.name,
|
|
}, nil)
|
|
|
|
rec := drive(t, Dependencies{RestartRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/restart", nil, nil,
|
|
)
|
|
body := decodeErrorBody(t, rec, tc.wantStatus)
|
|
assert.Equal(t, tc.errorCode, body.Code)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRestartHandlerHonoursXGalaxyCallerHeader(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockRestartService(ctrl)
|
|
|
|
mock.EXPECT().
|
|
Handle(gomock.Any(), gomock.AssignableToTypeOf(restartruntime.Input{})).
|
|
DoAndReturn(func(_ context.Context, in restartruntime.Input) (restartruntime.Result, error) {
|
|
assert.Equal(t, operation.OpSourceGMRest, in.OpSource)
|
|
return restartruntime.Result{Record: sampleRunningRecord(t), Outcome: operation.OutcomeSuccess}, nil
|
|
})
|
|
|
|
rec := drive(t, Dependencies{RestartRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/restart",
|
|
withCaller(http.Header{}, "gm"), nil,
|
|
)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
}
|
|
|
|
func TestRestartHandlerReturnsInternalErrorWhenServiceNotWired(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rec := drive(t, Dependencies{}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/restart", nil, nil,
|
|
)
|
|
body := decodeErrorBody(t, rec, http.StatusInternalServerError)
|
|
assert.Equal(t, "internal_error", body.Code)
|
|
}
|
|
|
|
// --- patch ---
|
|
|
|
func TestPatchHandlerReturnsRecordOnSuccess(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockPatchService(ctrl)
|
|
|
|
record := sampleRunningRecord(t)
|
|
mock.EXPECT().
|
|
Handle(gomock.Any(), gomock.AssignableToTypeOf(patchruntime.Input{})).
|
|
DoAndReturn(func(_ context.Context, in patchruntime.Input) (patchruntime.Result, error) {
|
|
assert.Equal(t, "game-test", in.GameID)
|
|
assert.Equal(t, "galaxy/game:v1.2.4", in.NewImageRef)
|
|
return patchruntime.Result{Record: record, Outcome: operation.OutcomeSuccess}, nil
|
|
})
|
|
|
|
rec := drive(t, Dependencies{PatchRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/patch",
|
|
jsonHeaders(),
|
|
strReader(`{"image_ref":"galaxy/game:v1.2.4"}`),
|
|
)
|
|
resp := decodeRecordResponse(t, rec)
|
|
assert.Equal(t, "running", resp.Status)
|
|
}
|
|
|
|
func TestPatchHandlerMapsServiceFailures(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
name string
|
|
errorCode string
|
|
wantStatus int
|
|
}{
|
|
{"image_ref_not_semver", startruntime.ErrorCodeImageRefNotSemver, http.StatusBadRequest},
|
|
{"semver_patch_only", startruntime.ErrorCodeSemverPatchOnly, http.StatusConflict},
|
|
{"not_found", startruntime.ErrorCodeNotFound, http.StatusNotFound},
|
|
{"conflict", startruntime.ErrorCodeConflict, http.StatusConflict},
|
|
{"service_unavailable", startruntime.ErrorCodeServiceUnavailable, http.StatusServiceUnavailable},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockPatchService(ctrl)
|
|
mock.EXPECT().Handle(gomock.Any(), gomock.Any()).Return(patchruntime.Result{
|
|
Outcome: operation.OutcomeFailure, ErrorCode: tc.errorCode, ErrorMessage: tc.name,
|
|
}, nil)
|
|
|
|
rec := drive(t, Dependencies{PatchRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/patch",
|
|
jsonHeaders(),
|
|
strReader(`{"image_ref":"galaxy/game:v1.2.4"}`),
|
|
)
|
|
body := decodeErrorBody(t, rec, tc.wantStatus)
|
|
assert.Equal(t, tc.errorCode, body.Code)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPatchHandlerRejectsUnknownJSONFields(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockPatchService(ctrl)
|
|
|
|
rec := drive(t, Dependencies{PatchRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/patch",
|
|
jsonHeaders(),
|
|
strReader(`{"image_ref":"x","unexpected":true}`),
|
|
)
|
|
body := decodeErrorBody(t, rec, http.StatusBadRequest)
|
|
assert.Equal(t, "invalid_request", body.Code)
|
|
}
|
|
|
|
func TestPatchHandlerHonoursXGalaxyCallerHeader(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockPatchService(ctrl)
|
|
|
|
mock.EXPECT().
|
|
Handle(gomock.Any(), gomock.AssignableToTypeOf(patchruntime.Input{})).
|
|
DoAndReturn(func(_ context.Context, in patchruntime.Input) (patchruntime.Result, error) {
|
|
assert.Equal(t, operation.OpSourceGMRest, in.OpSource)
|
|
return patchruntime.Result{Record: sampleRunningRecord(t), Outcome: operation.OutcomeSuccess}, nil
|
|
})
|
|
|
|
rec := drive(t, Dependencies{PatchRuntime: mock}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/patch",
|
|
withCaller(jsonHeaders(), "gm"),
|
|
strReader(`{"image_ref":"galaxy/game:v1.2.4"}`),
|
|
)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
}
|
|
|
|
func TestPatchHandlerReturnsInternalErrorWhenServiceNotWired(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rec := drive(t, Dependencies{}, http.MethodPost,
|
|
"/api/v1/internal/runtimes/game-test/patch",
|
|
jsonHeaders(),
|
|
strReader(`{"image_ref":"galaxy/game:v1.2.4"}`),
|
|
)
|
|
body := decodeErrorBody(t, rec, http.StatusInternalServerError)
|
|
assert.Equal(t, "internal_error", body.Code)
|
|
}
|
|
|
|
// --- cleanup ---
|
|
|
|
func TestCleanupHandlerReturnsRecordOnSuccess(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockCleanupService(ctrl)
|
|
|
|
record := sampleStoppedRecord(t)
|
|
record.Status = runtime.StatusRemoved
|
|
record.CurrentContainerID = ""
|
|
removed := record.LastOpAt
|
|
record.RemovedAt = &removed
|
|
|
|
mock.EXPECT().
|
|
Handle(gomock.Any(), gomock.AssignableToTypeOf(cleanupcontainer.Input{})).
|
|
DoAndReturn(func(_ context.Context, in cleanupcontainer.Input) (cleanupcontainer.Result, error) {
|
|
assert.Equal(t, "game-stopped", in.GameID)
|
|
assert.Equal(t, operation.OpSourceAdminRest, in.OpSource)
|
|
return cleanupcontainer.Result{Record: record, Outcome: operation.OutcomeSuccess}, nil
|
|
})
|
|
|
|
rec := drive(t, Dependencies{CleanupContainer: mock}, http.MethodDelete,
|
|
"/api/v1/internal/runtimes/game-stopped/container", nil, nil,
|
|
)
|
|
resp := decodeRecordResponse(t, rec)
|
|
assert.Equal(t, "removed", resp.Status)
|
|
assert.Nil(t, resp.CurrentContainerID, "container id must be null after cleanup")
|
|
}
|
|
|
|
func TestCleanupHandlerMapsServiceFailures(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
name string
|
|
errorCode string
|
|
wantStatus int
|
|
}{
|
|
{"not_found", startruntime.ErrorCodeNotFound, http.StatusNotFound},
|
|
{"conflict", startruntime.ErrorCodeConflict, http.StatusConflict},
|
|
{"service_unavailable", startruntime.ErrorCodeServiceUnavailable, http.StatusServiceUnavailable},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
ctrl := gomock.NewController(t)
|
|
mock := mocks.NewMockCleanupService(ctrl)
|
|
mock.EXPECT().Handle(gomock.Any(), gomock.Any()).Return(cleanupcontainer.Result{
|
|
Outcome: operation.OutcomeFailure, ErrorCode: tc.errorCode, ErrorMessage: tc.name,
|
|
}, nil)
|
|
|
|
rec := drive(t, Dependencies{CleanupContainer: mock}, http.MethodDelete,
|
|
"/api/v1/internal/runtimes/game-test/container", nil, nil,
|
|
)
|
|
body := decodeErrorBody(t, rec, tc.wantStatus)
|
|
assert.Equal(t, tc.errorCode, body.Code)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCleanupHandlerReturnsInternalErrorWhenServiceNotWired(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rec := drive(t, Dependencies{}, http.MethodDelete,
|
|
"/api/v1/internal/runtimes/game-test/container", nil, nil,
|
|
)
|
|
body := decodeErrorBody(t, rec, http.StatusInternalServerError)
|
|
assert.Equal(t, "internal_error", body.Code)
|
|
}
|