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
@@ -5,15 +5,15 @@ import (
"errors"
"io"
"log/slog"
"sync"
"testing"
"time"
"galaxy/lobby/internal/adapters/applicationstub"
"galaxy/lobby/internal/adapters/gamestub"
"galaxy/lobby/internal/adapters/intentpubstub"
"galaxy/lobby/internal/adapters/membershipstub"
"galaxy/lobby/internal/adapters/racenamestub"
"galaxy/lobby/internal/adapters/userservicestub"
"galaxy/lobby/internal/adapters/applicationinmem"
"galaxy/lobby/internal/adapters/gameinmem"
"galaxy/lobby/internal/adapters/membershipinmem"
"galaxy/lobby/internal/adapters/mocks"
"galaxy/lobby/internal/adapters/racenameinmem"
"galaxy/lobby/internal/domain/application"
"galaxy/lobby/internal/domain/common"
"galaxy/lobby/internal/domain/game"
@@ -25,8 +25,87 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
type intentRec struct {
mu sync.Mutex
published []notificationintent.Intent
err error
}
func (r *intentRec) record(_ context.Context, intent notificationintent.Intent) (string, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.err != nil {
return "", r.err
}
r.published = append(r.published, intent)
return "1", nil
}
func (r *intentRec) snapshot() []notificationintent.Intent {
r.mu.Lock()
defer r.mu.Unlock()
return append([]notificationintent.Intent(nil), r.published...)
}
func (r *intentRec) setErr(err error) {
r.mu.Lock()
defer r.mu.Unlock()
r.err = err
}
type userRec struct {
mu sync.Mutex
elig map[string]ports.Eligibility
failures map[string]error
}
func (r *userRec) 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 *userRec) 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 *userRec) 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 newIntentMock(t *testing.T, rec *intentRec) *mocks.MockIntentPublisher {
t.Helper()
m := mocks.NewMockIntentPublisher(gomock.NewController(t))
m.EXPECT().Publish(gomock.Any(), gomock.Any()).DoAndReturn(rec.record).AnyTimes()
return m
}
func newUserMock(t *testing.T, rec *userRec) *mocks.MockUserService {
t.Helper()
m := mocks.NewMockUserService(gomock.NewController(t))
m.EXPECT().GetEligibility(gomock.Any(), gomock.Any()).DoAndReturn(rec.record).AnyTimes()
return m
}
const (
defaultRaceName = "SolarPilot"
otherRaceName = "VoidRunner"
@@ -58,12 +137,14 @@ func (f fixedIDs) NewMembershipID() (common.MembershipID, error) {
type fixture struct {
now time.Time
games *gamestub.Store
memberships *membershipstub.Store
applications *applicationstub.Store
directory *racenamestub.Directory
users *userservicestub.Service
intents *intentpubstub.Publisher
games *gameinmem.Store
memberships *membershipinmem.Store
applications *applicationinmem.Store
directory *racenameinmem.Directory
users *userRec
usersMock *mocks.MockUserService
intents *intentRec
intentsMock *mocks.MockIntentPublisher
ids fixedIDs
openPublicGameID common.GameID
defaultUserID string
@@ -72,13 +153,13 @@ type fixture struct {
func newFixture(t *testing.T) *fixture {
t.Helper()
now := time.Date(2026, 4, 25, 10, 0, 0, 0, time.UTC)
dir, err := racenamestub.NewDirectory(racenamestub.WithClock(fixedClock(now)))
dir, err := racenameinmem.NewDirectory(racenameinmem.WithClock(fixedClock(now)))
require.NoError(t, err)
users := userservicestub.NewService()
users.SetEligibility("user-1", ports.Eligibility{Exists: true, CanLogin: true, CanJoinGame: true})
games := gamestub.NewStore()
memberships := membershipstub.NewStore()
applications := applicationstub.NewStore()
users := &userRec{}
users.setEligibility("user-1", ports.Eligibility{Exists: true, CanLogin: true, CanJoinGame: true})
games := gameinmem.NewStore()
memberships := membershipinmem.NewStore()
applications := applicationinmem.NewStore()
gameRecord, err := game.New(game.NewGameInput{
GameID: "game-public",
@@ -97,6 +178,7 @@ func newFixture(t *testing.T) *fixture {
gameRecord.Status = game.StatusEnrollmentOpen
require.NoError(t, games.Save(context.Background(), gameRecord))
intents := &intentRec{}
return &fixture{
now: now,
games: games,
@@ -104,7 +186,9 @@ func newFixture(t *testing.T) *fixture {
applications: applications,
directory: dir,
users: users,
intents: intentpubstub.NewPublisher(),
usersMock: newUserMock(t, users),
intents: intents,
intentsMock: newIntentMock(t, intents),
ids: fixedIDs{applicationID: "application-fixed", membershipID: "membership-fixed"},
openPublicGameID: gameRecord.GameID,
defaultUserID: "user-1",
@@ -117,9 +201,9 @@ func newService(t *testing.T, f *fixture) *submitapplication.Service {
Games: f.games,
Memberships: f.memberships,
Applications: f.applications,
Users: f.users,
Users: f.usersMock,
Directory: f.directory,
Intents: f.intents,
Intents: f.intentsMock,
IDs: f.ids,
Clock: fixedClock(f.now),
Logger: silentLogger(),
@@ -147,7 +231,7 @@ func TestHandleHappyPath(t *testing.T) {
assert.Equal(t, common.ApplicationID("application-fixed"), got.ApplicationID)
assert.Equal(t, defaultRaceName, got.RaceName)
intents := f.intents.Published()
intents := f.intents.snapshot()
require.Len(t, intents, 1)
assert.Equal(t, notificationintent.NotificationTypeLobbyApplicationSubmitted, intents[0].NotificationType)
assert.Equal(t, notificationintent.AudienceKindAdminEmail, intents[0].AudienceKind)
@@ -236,7 +320,7 @@ func TestHandleUserMissingEligibilityDenied(t *testing.T) {
func TestHandleCanJoinGameFalseEligibilityDenied(t *testing.T) {
t.Parallel()
f := newFixture(t)
f.users.SetEligibility("user-blocked", ports.Eligibility{Exists: true, CanLogin: true, CanJoinGame: false})
f.users.setEligibility("user-blocked", ports.Eligibility{Exists: true, CanLogin: true, CanJoinGame: false})
svc := newService(t, f)
input := defaultInput(f)
input.Actor = shared.NewUserActor("user-blocked")
@@ -248,7 +332,7 @@ func TestHandleCanJoinGameFalseEligibilityDenied(t *testing.T) {
func TestHandleUserServiceUnavailable(t *testing.T) {
t.Parallel()
f := newFixture(t)
f.users.SetFailure(f.defaultUserID, ports.ErrUserServiceUnavailable)
f.users.setFailure(f.defaultUserID, ports.ErrUserServiceUnavailable)
svc := newService(t, f)
_, err := svc.Handle(context.Background(), defaultInput(f))
@@ -322,7 +406,7 @@ func TestHandleDuplicateActiveApplicationConflict(t *testing.T) {
func TestHandlePublishFailureDoesNotRollback(t *testing.T) {
t.Parallel()
f := newFixture(t)
f.intents.SetError(errors.New("publish failed"))
f.intents.setErr(errors.New("publish failed"))
svc := newService(t, f)
got, err := svc.Handle(context.Background(), defaultInput(f))