feat: runtime manager
This commit is contained in:
@@ -0,0 +1,610 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user