feat: gamemaster

This commit is contained in:
Ilia Denisov
2026-05-03 07:59:03 +02:00
committed by GitHub
parent a7cee15115
commit 3e2622757e
229 changed files with 41521 additions and 1098 deletions
@@ -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)
}