feat: runtime manager

This commit is contained in:
Ilia Denisov
2026-04-28 20:39:18 +02:00
committed by GitHub
parent e0a99b346b
commit a7cee15115
289 changed files with 45660 additions and 2207 deletions
+95 -25
View File
@@ -4,44 +4,114 @@ import (
"context"
"encoding/json"
"net/http"
"sync"
"testing"
"time"
"galaxy/lobby/internal/adapters/gamestub"
"galaxy/lobby/internal/adapters/intentpubstub"
"galaxy/lobby/internal/adapters/racenamestub"
"galaxy/lobby/internal/adapters/userservicestub"
"galaxy/lobby/internal/adapters/gameinmem"
"galaxy/lobby/internal/adapters/mocks"
"galaxy/lobby/internal/adapters/racenameinmem"
"galaxy/lobby/internal/domain/common"
"galaxy/lobby/internal/domain/game"
"galaxy/lobby/internal/ports"
"galaxy/lobby/internal/service/listmyracenames"
"galaxy/lobby/internal/service/registerracename"
"galaxy/notificationintent"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
type publishedIntentRec struct {
mu sync.Mutex
published []notificationintent.Intent
}
func (r *publishedIntentRec) record(_ context.Context, intent notificationintent.Intent) (string, error) {
r.mu.Lock()
defer r.mu.Unlock()
r.published = append(r.published, intent)
return "1", nil
}
func (r *publishedIntentRec) snapshot() []notificationintent.Intent {
r.mu.Lock()
defer r.mu.Unlock()
return append([]notificationintent.Intent(nil), r.published...)
}
type userEligibilityRec struct {
mu sync.Mutex
elig map[string]ports.Eligibility
failures map[string]error
}
func (r *userEligibilityRec) record(_ context.Context, userID string) (ports.Eligibility, error) {
r.mu.Lock()
defer r.mu.Unlock()
if err, ok := r.failures[userID]; ok {
return ports.Eligibility{}, err
}
if e, ok := r.elig[userID]; ok {
return e, nil
}
return ports.Eligibility{Exists: false}, nil
}
func (r *userEligibilityRec) setEligibility(userID string, e ports.Eligibility) {
r.mu.Lock()
defer r.mu.Unlock()
if r.elig == nil {
r.elig = make(map[string]ports.Eligibility)
}
r.elig[userID] = e
}
func (r *userEligibilityRec) setFailure(userID string, err error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.failures == nil {
r.failures = make(map[string]error)
}
r.failures[userID] = err
}
func newPublishedIntentMock(t *testing.T, rec *publishedIntentRec) *mocks.MockIntentPublisher {
t.Helper()
m := mocks.NewMockIntentPublisher(gomock.NewController(t))
m.EXPECT().Publish(gomock.Any(), gomock.Any()).DoAndReturn(rec.record).AnyTimes()
return m
}
func newUserEligibilityMock(t *testing.T, rec *userEligibilityRec) *mocks.MockUserService {
t.Helper()
m := mocks.NewMockUserService(gomock.NewController(t))
m.EXPECT().GetEligibility(gomock.Any(), gomock.Any()).DoAndReturn(rec.record).AnyTimes()
return m
}
type raceNameFixture struct {
now time.Time
directory *racenamestub.Directory
users *userservicestub.Service
intents *intentpubstub.Publisher
directory *racenameinmem.Directory
users *userEligibilityRec
intents *publishedIntentRec
handler http.Handler
}
func newRaceNameFixture(t *testing.T) *raceNameFixture {
t.Helper()
now := time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC)
directory, err := racenamestub.NewDirectory(racenamestub.WithClock(func() time.Time { return now }))
directory, err := racenameinmem.NewDirectory(racenameinmem.WithClock(func() time.Time { return now }))
require.NoError(t, err)
users := userservicestub.NewService()
intents := intentpubstub.NewPublisher()
usersRec := &userEligibilityRec{}
intentsRec := &publishedIntentRec{}
logger := silentLogger()
svc, err := registerracename.NewService(registerracename.Dependencies{
Directory: directory,
Users: users,
Intents: intents,
Users: newUserEligibilityMock(t, usersRec),
Intents: newPublishedIntentMock(t, intentsRec),
Clock: func() time.Time { return now },
Logger: logger,
})
@@ -50,8 +120,8 @@ func newRaceNameFixture(t *testing.T) *raceNameFixture {
return &raceNameFixture{
now: now,
directory: directory,
users: users,
intents: intents,
users: usersRec,
intents: intentsRec,
handler: newHandler(Dependencies{Logger: logger, RegisterRaceName: svc}, logger),
}
}
@@ -66,7 +136,7 @@ func TestHandleRegisterRaceNameHappyPath(t *testing.T) {
t.Parallel()
f := newRaceNameFixture(t)
f.users.SetEligibility("user-1", ports.Eligibility{Exists: true, MaxRegisteredRaceNames: 2})
f.users.setEligibility("user-1", ports.Eligibility{Exists: true, MaxRegisteredRaceNames: 2})
f.seedPending(t, "game-1", "user-1", "Stellaris", f.now.Add(7*24*time.Hour))
rec := doRequest(t, f.handler, http.MethodPost, registerRaceNamePath, "user-1", registerRaceNameRequest{
@@ -82,7 +152,7 @@ func TestHandleRegisterRaceNameHappyPath(t *testing.T) {
assert.Equal(t, f.now.UnixMilli(), resp.RegisteredAtMs)
assert.NotEmpty(t, resp.CanonicalKey)
require.Len(t, f.intents.Published(), 1)
require.Len(t, f.intents.snapshot(), 1)
}
func TestHandleRegisterRaceNameRejectsMissingUserHeader(t *testing.T) {
@@ -120,7 +190,7 @@ func TestHandleRegisterRaceNamePendingMissing(t *testing.T) {
t.Parallel()
f := newRaceNameFixture(t)
f.users.SetEligibility("user-1", ports.Eligibility{Exists: true, MaxRegisteredRaceNames: 2})
f.users.setEligibility("user-1", ports.Eligibility{Exists: true, MaxRegisteredRaceNames: 2})
rec := doRequest(t, f.handler, http.MethodPost, registerRaceNamePath, "user-1", registerRaceNameRequest{
RaceName: "Stellaris",
@@ -137,7 +207,7 @@ func TestHandleRegisterRaceNamePendingExpired(t *testing.T) {
t.Parallel()
f := newRaceNameFixture(t)
f.users.SetEligibility("user-1", ports.Eligibility{Exists: true, MaxRegisteredRaceNames: 2})
f.users.setEligibility("user-1", ports.Eligibility{Exists: true, MaxRegisteredRaceNames: 2})
f.seedPending(t, "game-1", "user-1", "Stellaris", f.now.Add(-time.Minute))
rec := doRequest(t, f.handler, http.MethodPost, registerRaceNamePath, "user-1", registerRaceNameRequest{
@@ -155,7 +225,7 @@ func TestHandleRegisterRaceNameQuotaExceeded(t *testing.T) {
t.Parallel()
f := newRaceNameFixture(t)
f.users.SetEligibility("user-1", ports.Eligibility{Exists: true, MaxRegisteredRaceNames: 1})
f.users.setEligibility("user-1", ports.Eligibility{Exists: true, MaxRegisteredRaceNames: 1})
// pre-existing registered race name to exhaust quota
f.seedPending(t, "game-old", "user-1", "OldName", f.now.Add(24*time.Hour))
require.NoError(t, f.directory.Register(context.Background(), "game-old", "user-1", "OldName"))
@@ -177,7 +247,7 @@ func TestHandleRegisterRaceNamePermanentBlock(t *testing.T) {
t.Parallel()
f := newRaceNameFixture(t)
f.users.SetEligibility("user-1", ports.Eligibility{
f.users.setEligibility("user-1", ports.Eligibility{
Exists: true,
PermanentBlocked: true,
MaxRegisteredRaceNames: 2,
@@ -199,7 +269,7 @@ func TestHandleRegisterRaceNameUserServiceUnavailable(t *testing.T) {
t.Parallel()
f := newRaceNameFixture(t)
f.users.SetFailure("user-1", ports.ErrUserServiceUnavailable)
f.users.setFailure("user-1", ports.ErrUserServiceUnavailable)
f.seedPending(t, "game-1", "user-1", "Stellaris", f.now.Add(24*time.Hour))
rec := doRequest(t, f.handler, http.MethodPost, registerRaceNamePath, "user-1", registerRaceNameRequest{
@@ -218,17 +288,17 @@ func TestHandleRegisterRaceNameUserServiceUnavailable(t *testing.T) {
// silent logger.
type myRaceNamesFixture struct {
now time.Time
directory *racenamestub.Directory
games *gamestub.Store
directory *racenameinmem.Directory
games *gameinmem.Store
handler http.Handler
}
func newMyRaceNamesFixture(t *testing.T) *myRaceNamesFixture {
t.Helper()
now := time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC)
directory, err := racenamestub.NewDirectory(racenamestub.WithClock(func() time.Time { return now }))
directory, err := racenameinmem.NewDirectory(racenameinmem.WithClock(func() time.Time { return now }))
require.NoError(t, err)
games := gamestub.NewStore()
games := gameinmem.NewStore()
logger := silentLogger()
svc, err := listmyracenames.NewService(listmyracenames.Dependencies{