feat: gamemaster
This commit is contained in:
@@ -0,0 +1,631 @@
|
||||
package engineversion_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"galaxy/gamemaster/internal/adapters/mocks"
|
||||
domainengineversion "galaxy/gamemaster/internal/domain/engineversion"
|
||||
"galaxy/gamemaster/internal/domain/operation"
|
||||
"galaxy/gamemaster/internal/ports"
|
||||
"galaxy/gamemaster/internal/service/engineversion"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// fakeOperationLogs is a thread-safe stub recorder for the few
|
||||
// operation_log entries the engine-version service writes per call.
|
||||
// Using a stub keeps the operation_log assertions table-driven without
|
||||
// introducing the verbosity of a gomock recorder for every entry.
|
||||
type fakeOperationLogs struct {
|
||||
mu sync.Mutex
|
||||
entries []operation.OperationEntry
|
||||
err error
|
||||
}
|
||||
|
||||
func newFakeOperationLogs() *fakeOperationLogs {
|
||||
return &fakeOperationLogs{}
|
||||
}
|
||||
|
||||
func (s *fakeOperationLogs) Append(_ context.Context, entry operation.OperationEntry) (int64, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.err != nil {
|
||||
return 0, s.err
|
||||
}
|
||||
s.entries = append(s.entries, entry)
|
||||
return int64(len(s.entries)), nil
|
||||
}
|
||||
|
||||
func (s *fakeOperationLogs) ListByGame(_ context.Context, _ string, _ int) ([]operation.OperationEntry, error) {
|
||||
return nil, errors.New("not used in engineversion tests")
|
||||
}
|
||||
|
||||
func (s *fakeOperationLogs) snapshot() []operation.OperationEntry {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
out := make([]operation.OperationEntry, len(s.entries))
|
||||
copy(out, s.entries)
|
||||
return out
|
||||
}
|
||||
|
||||
type harness struct {
|
||||
ctrl *gomock.Controller
|
||||
store *mocks.MockEngineVersionStore
|
||||
oplog *fakeOperationLogs
|
||||
clock time.Time
|
||||
service *engineversion.Service
|
||||
}
|
||||
|
||||
func newHarness(t *testing.T) *harness {
|
||||
t.Helper()
|
||||
ctrl := gomock.NewController(t)
|
||||
store := mocks.NewMockEngineVersionStore(ctrl)
|
||||
oplog := newFakeOperationLogs()
|
||||
clock := time.Date(2026, time.April, 30, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
service, err := engineversion.NewService(engineversion.Dependencies{
|
||||
EngineVersions: store,
|
||||
OperationLogs: oplog,
|
||||
Clock: func() time.Time { return clock },
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return &harness{
|
||||
ctrl: ctrl,
|
||||
store: store,
|
||||
oplog: oplog,
|
||||
clock: clock,
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServiceRejectsMissingDeps(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
store := mocks.NewMockEngineVersionStore(ctrl)
|
||||
oplog := newFakeOperationLogs()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
deps engineversion.Dependencies
|
||||
}{
|
||||
{"nil store", engineversion.Dependencies{OperationLogs: oplog}},
|
||||
{"nil oplog", engineversion.Dependencies{EngineVersions: store}},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s, err := engineversion.NewService(tc.deps)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServiceDefaultsClockAndLogger(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
service, err := engineversion.NewService(engineversion.Dependencies{
|
||||
EngineVersions: mocks.NewMockEngineVersionStore(ctrl),
|
||||
OperationLogs: newFakeOperationLogs(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, service)
|
||||
}
|
||||
|
||||
// --- List ------------------------------------------------------------
|
||||
|
||||
func TestListNoFilter(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
rows := []domainengineversion.EngineVersion{
|
||||
{Version: "v1.2.3", ImageRef: "ghcr.io/galaxy/game:v1.2.3", Status: domainengineversion.StatusActive},
|
||||
{Version: "v1.3.0", ImageRef: "ghcr.io/galaxy/game:v1.3.0", Status: domainengineversion.StatusDeprecated},
|
||||
}
|
||||
h.store.EXPECT().List(gomock.Any(), nil).Return(rows, nil)
|
||||
|
||||
got, err := h.service.List(context.Background(), nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rows, got)
|
||||
}
|
||||
|
||||
func TestListWithStatusFilter(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
active := domainengineversion.StatusActive
|
||||
expected := []domainengineversion.EngineVersion{
|
||||
{Version: "v1.2.3", ImageRef: "ghcr.io/galaxy/game:v1.2.3", Status: active},
|
||||
}
|
||||
h.store.EXPECT().List(gomock.Any(), &active).Return(expected, nil)
|
||||
|
||||
got, err := h.service.List(context.Background(), &active)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, got)
|
||||
}
|
||||
|
||||
func TestListRejectsUnknownStatusFilter(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
exotic := domainengineversion.Status("exotic")
|
||||
got, err := h.service.List(context.Background(), &exotic)
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrInvalidRequest))
|
||||
assert.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestListWrapsStoreErrorAsServiceUnavailable(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
storeErr := errors.New("pg down")
|
||||
h.store.EXPECT().List(gomock.Any(), nil).Return(nil, storeErr)
|
||||
|
||||
_, err := h.service.List(context.Background(), nil)
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrServiceUnavailable))
|
||||
}
|
||||
|
||||
// --- Get -------------------------------------------------------------
|
||||
|
||||
func TestGetHappyPath(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
row := domainengineversion.EngineVersion{
|
||||
Version: "v1.2.3", ImageRef: "ghcr.io/galaxy/game:v1.2.3", Status: domainengineversion.StatusActive,
|
||||
}
|
||||
h.store.EXPECT().Get(gomock.Any(), "v1.2.3").Return(row, nil)
|
||||
|
||||
got, err := h.service.Get(context.Background(), "v1.2.3")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, row, got)
|
||||
}
|
||||
|
||||
func TestGetNotFound(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.store.EXPECT().Get(gomock.Any(), "v9.9.9").Return(domainengineversion.EngineVersion{}, domainengineversion.ErrNotFound)
|
||||
|
||||
_, err := h.service.Get(context.Background(), "v9.9.9")
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrNotFound))
|
||||
}
|
||||
|
||||
func TestGetRejectsEmptyVersion(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
_, err := h.service.Get(context.Background(), " ")
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrInvalidRequest))
|
||||
}
|
||||
|
||||
func TestGetWrapsStoreError(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.store.EXPECT().Get(gomock.Any(), "v1.2.3").Return(domainengineversion.EngineVersion{}, errors.New("pg down"))
|
||||
|
||||
_, err := h.service.Get(context.Background(), "v1.2.3")
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrServiceUnavailable))
|
||||
}
|
||||
|
||||
// --- ResolveImageRef -------------------------------------------------
|
||||
|
||||
func TestResolveImageRefHappyPath(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.store.EXPECT().Get(gomock.Any(), "v1.2.3").Return(domainengineversion.EngineVersion{
|
||||
Version: "v1.2.3", ImageRef: "ghcr.io/galaxy/game:v1.2.3", Status: domainengineversion.StatusActive,
|
||||
}, nil)
|
||||
|
||||
got, err := h.service.ResolveImageRef(context.Background(), "v1.2.3")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "ghcr.io/galaxy/game:v1.2.3", got)
|
||||
}
|
||||
|
||||
func TestResolveImageRefSeededTable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
seedVersion string
|
||||
seedRef string
|
||||
}{
|
||||
{"v1.0.0", "v1.0.0", "ghcr.io/galaxy/game:v1.0.0"},
|
||||
{"v1.2.3 with prerelease metadata", "v1.2.3-rc1", "ghcr.io/galaxy/game:v1.2.3-rc1"},
|
||||
{"v2.0.0 fully-qualified", "v2.0.0", "registry.galaxy.local/game:v2.0.0"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.store.EXPECT().Get(gomock.Any(), tc.seedVersion).Return(domainengineversion.EngineVersion{
|
||||
Version: tc.seedVersion, ImageRef: tc.seedRef, Status: domainengineversion.StatusActive,
|
||||
}, nil)
|
||||
got, err := h.service.ResolveImageRef(context.Background(), tc.seedVersion)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.seedRef, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveImageRefNotFound(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.store.EXPECT().Get(gomock.Any(), "v9.9.9").Return(domainengineversion.EngineVersion{}, domainengineversion.ErrNotFound)
|
||||
|
||||
_, err := h.service.ResolveImageRef(context.Background(), "v9.9.9")
|
||||
require.True(t, errors.Is(err, engineversion.ErrNotFound))
|
||||
}
|
||||
|
||||
// --- Create ----------------------------------------------------------
|
||||
|
||||
func TestCreateHappyPath(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.store.EXPECT().Insert(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(_ context.Context, record domainengineversion.EngineVersion) error {
|
||||
assert.Equal(t, "v1.2.3", record.Version)
|
||||
assert.Equal(t, "ghcr.io/galaxy/game:v1.2.3", record.ImageRef)
|
||||
assert.Equal(t, domainengineversion.StatusActive, record.Status)
|
||||
assert.Equal(t, h.clock, record.CreatedAt)
|
||||
assert.Equal(t, h.clock, record.UpdatedAt)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
got, err := h.service.Create(context.Background(), engineversion.CreateInput{
|
||||
Version: "1.2.3",
|
||||
ImageRef: "ghcr.io/galaxy/game:v1.2.3",
|
||||
Options: []byte(`{"max_planets":120}`),
|
||||
OpSource: operation.OpSourceAdminRest,
|
||||
SourceRef: "request-1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "v1.2.3", got.Version)
|
||||
|
||||
entries := h.oplog.snapshot()
|
||||
require.Len(t, entries, 1)
|
||||
assert.Equal(t, operation.OpKindEngineVersionCreate, entries[0].OpKind)
|
||||
assert.Equal(t, "v1.2.3", entries[0].GameID)
|
||||
assert.Equal(t, operation.OutcomeSuccess, entries[0].Outcome)
|
||||
assert.Equal(t, operation.OpSourceAdminRest, entries[0].OpSource)
|
||||
assert.Equal(t, "request-1", entries[0].SourceRef)
|
||||
}
|
||||
|
||||
func TestCreateRejectsInvalidSemver(t *testing.T) {
|
||||
tests := []string{"", " ", "not-a-version", "v1.2", "1.2"}
|
||||
for _, version := range tests {
|
||||
t.Run(version, func(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
_, err := h.service.Create(context.Background(), engineversion.CreateInput{
|
||||
Version: version,
|
||||
ImageRef: "ghcr.io/galaxy/game:v1.2.3",
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrInvalidRequest))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAuditFailureForBadImageRef(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
_, err := h.service.Create(context.Background(), engineversion.CreateInput{
|
||||
Version: "v1.2.3",
|
||||
ImageRef: " ",
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrInvalidRequest))
|
||||
entries := h.oplog.snapshot()
|
||||
require.Len(t, entries, 1)
|
||||
assert.Equal(t, operation.OpKindEngineVersionCreate, entries[0].OpKind)
|
||||
assert.Equal(t, "v1.2.3", entries[0].GameID)
|
||||
assert.Equal(t, operation.OutcomeFailure, entries[0].Outcome)
|
||||
assert.Equal(t, engineversion.ErrorCodeInvalidRequest, entries[0].ErrorCode)
|
||||
}
|
||||
|
||||
func TestCreateRejectsBadDockerReference(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
_, err := h.service.Create(context.Background(), engineversion.CreateInput{
|
||||
Version: "v1.2.3",
|
||||
ImageRef: "BAD//Ref::",
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrInvalidRequest))
|
||||
}
|
||||
|
||||
func TestCreateRejectsNonObjectOptions(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
_, err := h.service.Create(context.Background(), engineversion.CreateInput{
|
||||
Version: "v1.2.3",
|
||||
ImageRef: "ghcr.io/galaxy/game:v1.2.3",
|
||||
Options: []byte(`[1,2,3]`),
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrInvalidRequest))
|
||||
}
|
||||
|
||||
func TestCreateAcceptsEmptyOptionsAsNil(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.store.EXPECT().Insert(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(_ context.Context, record domainengineversion.EngineVersion) error {
|
||||
assert.Empty(t, record.Options, "expected empty options pass-through (adapter writes default {})")
|
||||
return nil
|
||||
},
|
||||
)
|
||||
_, err := h.service.Create(context.Background(), engineversion.CreateInput{
|
||||
Version: "v1.2.3",
|
||||
ImageRef: "ghcr.io/galaxy/game:v1.2.3",
|
||||
Options: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCreateConflict(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.store.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(domainengineversion.ErrConflict)
|
||||
_, err := h.service.Create(context.Background(), engineversion.CreateInput{
|
||||
Version: "v1.2.3",
|
||||
ImageRef: "ghcr.io/galaxy/game:v1.2.3",
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrConflict))
|
||||
entries := h.oplog.snapshot()
|
||||
require.Len(t, entries, 1)
|
||||
assert.Equal(t, operation.OutcomeFailure, entries[0].Outcome)
|
||||
assert.Equal(t, engineversion.ErrorCodeConflict, entries[0].ErrorCode)
|
||||
}
|
||||
|
||||
func TestCreateUnknownStoreError(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.store.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(errors.New("pg down"))
|
||||
_, err := h.service.Create(context.Background(), engineversion.CreateInput{
|
||||
Version: "v1.2.3",
|
||||
ImageRef: "ghcr.io/galaxy/game:v1.2.3",
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrServiceUnavailable))
|
||||
}
|
||||
|
||||
// --- Update ----------------------------------------------------------
|
||||
|
||||
func TestUpdateHappyPath(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
newRef := "ghcr.io/galaxy/game:v1.2.4"
|
||||
deprecated := domainengineversion.StatusDeprecated
|
||||
|
||||
gomock.InOrder(
|
||||
h.store.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(_ context.Context, input ports.UpdateEngineVersionInput) error {
|
||||
require.NotNil(t, input.ImageRef)
|
||||
assert.Equal(t, newRef, *input.ImageRef)
|
||||
require.NotNil(t, input.Status)
|
||||
assert.Equal(t, deprecated, *input.Status)
|
||||
assert.Equal(t, h.clock, input.Now)
|
||||
return nil
|
||||
},
|
||||
),
|
||||
h.store.EXPECT().Get(gomock.Any(), "v1.2.3").Return(domainengineversion.EngineVersion{
|
||||
Version: "v1.2.3", ImageRef: newRef, Status: deprecated, UpdatedAt: h.clock,
|
||||
}, nil),
|
||||
)
|
||||
|
||||
got, err := h.service.Update(context.Background(), engineversion.UpdateInput{
|
||||
Version: "v1.2.3",
|
||||
ImageRef: &newRef,
|
||||
Status: &deprecated,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, deprecated, got.Status)
|
||||
|
||||
entries := h.oplog.snapshot()
|
||||
require.Len(t, entries, 1)
|
||||
assert.Equal(t, operation.OpKindEngineVersionUpdate, entries[0].OpKind)
|
||||
assert.Equal(t, operation.OutcomeSuccess, entries[0].Outcome)
|
||||
}
|
||||
|
||||
func TestUpdateRejectsEmptyVersion(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
newRef := "ghcr.io/galaxy/game:v1.2.4"
|
||||
_, err := h.service.Update(context.Background(), engineversion.UpdateInput{
|
||||
Version: " ",
|
||||
ImageRef: &newRef,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrInvalidRequest))
|
||||
}
|
||||
|
||||
func TestUpdateRejectsEmptyPatch(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
_, err := h.service.Update(context.Background(), engineversion.UpdateInput{Version: "v1.2.3"})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrInvalidRequest))
|
||||
}
|
||||
|
||||
func TestUpdateRejectsBadImageRef(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
bad := "BAD//Ref::"
|
||||
_, err := h.service.Update(context.Background(), engineversion.UpdateInput{
|
||||
Version: "v1.2.3",
|
||||
ImageRef: &bad,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrInvalidRequest))
|
||||
}
|
||||
|
||||
func TestUpdateRejectsUnknownStatus(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
bad := domainengineversion.Status("exotic")
|
||||
_, err := h.service.Update(context.Background(), engineversion.UpdateInput{
|
||||
Version: "v1.2.3",
|
||||
Status: &bad,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrInvalidRequest))
|
||||
}
|
||||
|
||||
func TestUpdateRejectsBadOptions(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
bad := []byte(`"not-an-object"`)
|
||||
_, err := h.service.Update(context.Background(), engineversion.UpdateInput{
|
||||
Version: "v1.2.3",
|
||||
Options: &bad,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrInvalidRequest))
|
||||
}
|
||||
|
||||
func TestUpdateNotFound(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
newRef := "ghcr.io/galaxy/game:v1.2.4"
|
||||
h.store.EXPECT().Update(gomock.Any(), gomock.Any()).Return(domainengineversion.ErrNotFound)
|
||||
_, err := h.service.Update(context.Background(), engineversion.UpdateInput{
|
||||
Version: "v1.2.3",
|
||||
ImageRef: &newRef,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrNotFound))
|
||||
entries := h.oplog.snapshot()
|
||||
require.Len(t, entries, 1)
|
||||
assert.Equal(t, engineversion.ErrorCodeEngineVersionNotFound, entries[0].ErrorCode)
|
||||
}
|
||||
|
||||
// --- Deprecate -------------------------------------------------------
|
||||
|
||||
func TestDeprecateHappyPath(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.store.EXPECT().Deprecate(gomock.Any(), "v1.2.3", h.clock).Return(nil)
|
||||
|
||||
err := h.service.Deprecate(context.Background(), engineversion.DeprecateInput{Version: "v1.2.3"})
|
||||
require.NoError(t, err)
|
||||
entries := h.oplog.snapshot()
|
||||
require.Len(t, entries, 1)
|
||||
assert.Equal(t, operation.OpKindEngineVersionDeprecate, entries[0].OpKind)
|
||||
assert.Equal(t, operation.OutcomeSuccess, entries[0].Outcome)
|
||||
}
|
||||
|
||||
func TestDeprecateRejectsEmptyVersion(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
err := h.service.Deprecate(context.Background(), engineversion.DeprecateInput{})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrInvalidRequest))
|
||||
}
|
||||
|
||||
func TestDeprecateNotFound(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.store.EXPECT().Deprecate(gomock.Any(), "v9.9.9", h.clock).Return(domainengineversion.ErrNotFound)
|
||||
err := h.service.Deprecate(context.Background(), engineversion.DeprecateInput{Version: "v9.9.9"})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrNotFound))
|
||||
entries := h.oplog.snapshot()
|
||||
require.Len(t, entries, 1)
|
||||
assert.Equal(t, operation.OutcomeFailure, entries[0].Outcome)
|
||||
assert.Equal(t, engineversion.ErrorCodeEngineVersionNotFound, entries[0].ErrorCode)
|
||||
}
|
||||
|
||||
func TestDeprecateUnknownStoreError(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.store.EXPECT().Deprecate(gomock.Any(), "v1.2.3", h.clock).Return(errors.New("pg down"))
|
||||
err := h.service.Deprecate(context.Background(), engineversion.DeprecateInput{Version: "v1.2.3"})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrServiceUnavailable))
|
||||
}
|
||||
|
||||
// --- Delete ----------------------------------------------------------
|
||||
|
||||
func TestDeleteHappyPath(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
gomock.InOrder(
|
||||
h.store.EXPECT().IsReferencedByActiveRuntime(gomock.Any(), "v1.2.3").Return(false, nil),
|
||||
h.store.EXPECT().Delete(gomock.Any(), "v1.2.3").Return(nil),
|
||||
)
|
||||
err := h.service.Delete(context.Background(), engineversion.DeleteInput{
|
||||
Version: "v1.2.3",
|
||||
OpSource: operation.OpSourceAdminRest,
|
||||
SourceRef: "ticket-42",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
entries := h.oplog.snapshot()
|
||||
require.Len(t, entries, 1)
|
||||
assert.Equal(t, operation.OpKindEngineVersionDelete, entries[0].OpKind)
|
||||
assert.Equal(t, operation.OutcomeSuccess, entries[0].Outcome)
|
||||
assert.Equal(t, "ticket-42", entries[0].SourceRef)
|
||||
}
|
||||
|
||||
func TestDeleteRejectsEmptyVersion(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
err := h.service.Delete(context.Background(), engineversion.DeleteInput{})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrInvalidRequest))
|
||||
}
|
||||
|
||||
func TestDeleteRejectedWhenReferenced(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.store.EXPECT().IsReferencedByActiveRuntime(gomock.Any(), "v1.2.3").Return(true, nil)
|
||||
// Delete must not be called when the row is referenced.
|
||||
|
||||
err := h.service.Delete(context.Background(), engineversion.DeleteInput{Version: "v1.2.3"})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrInUse))
|
||||
entries := h.oplog.snapshot()
|
||||
require.Len(t, entries, 1)
|
||||
assert.Equal(t, operation.OutcomeFailure, entries[0].Outcome)
|
||||
assert.Equal(t, engineversion.ErrorCodeEngineVersionInUse, entries[0].ErrorCode)
|
||||
}
|
||||
|
||||
func TestDeleteIsReferencedProbeError(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.store.EXPECT().IsReferencedByActiveRuntime(gomock.Any(), "v1.2.3").Return(false, errors.New("pg down"))
|
||||
|
||||
err := h.service.Delete(context.Background(), engineversion.DeleteInput{Version: "v1.2.3"})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrServiceUnavailable))
|
||||
}
|
||||
|
||||
func TestDeleteNotFound(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
gomock.InOrder(
|
||||
h.store.EXPECT().IsReferencedByActiveRuntime(gomock.Any(), "v9.9.9").Return(false, nil),
|
||||
h.store.EXPECT().Delete(gomock.Any(), "v9.9.9").Return(domainengineversion.ErrNotFound),
|
||||
)
|
||||
err := h.service.Delete(context.Background(), engineversion.DeleteInput{Version: "v9.9.9"})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrNotFound))
|
||||
}
|
||||
|
||||
func TestDeleteUnknownStoreError(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
gomock.InOrder(
|
||||
h.store.EXPECT().IsReferencedByActiveRuntime(gomock.Any(), "v1.2.3").Return(false, nil),
|
||||
h.store.EXPECT().Delete(gomock.Any(), "v1.2.3").Return(errors.New("pg down")),
|
||||
)
|
||||
err := h.service.Delete(context.Background(), engineversion.DeleteInput{Version: "v1.2.3"})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, engineversion.ErrServiceUnavailable))
|
||||
}
|
||||
|
||||
// --- guard rails -----------------------------------------------------
|
||||
|
||||
func TestNilContextReturnsError(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
|
||||
t.Run("List", func(t *testing.T) {
|
||||
_, err := h.service.List(nil, nil) //nolint:staticcheck // intentional nil context
|
||||
require.Error(t, err)
|
||||
})
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
_, err := h.service.Get(nil, "v1.2.3") //nolint:staticcheck // intentional nil context
|
||||
require.Error(t, err)
|
||||
})
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
_, err := h.service.Create(nil, engineversion.CreateInput{}) //nolint:staticcheck // intentional nil context
|
||||
require.Error(t, err)
|
||||
})
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
_, err := h.service.Update(nil, engineversion.UpdateInput{}) //nolint:staticcheck // intentional nil context
|
||||
require.Error(t, err)
|
||||
})
|
||||
t.Run("Deprecate", func(t *testing.T) {
|
||||
err := h.service.Deprecate(nil, engineversion.DeprecateInput{}) //nolint:staticcheck // intentional nil context
|
||||
require.Error(t, err)
|
||||
})
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
err := h.service.Delete(nil, engineversion.DeleteInput{}) //nolint:staticcheck // intentional nil context
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNilServiceReturnsError(t *testing.T) {
|
||||
var s *engineversion.Service
|
||||
_, err := s.Get(context.Background(), "v1.2.3")
|
||||
require.Error(t, err)
|
||||
_, err = s.Create(context.Background(), engineversion.CreateInput{})
|
||||
require.Error(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user