Files
galaxy-game/lobby/internal/service/manualreadytostart/service_test.go
T
2026-04-28 20:39:18 +02:00

282 lines
8.5 KiB
Go

package manualreadytostart_test
import (
"context"
"io"
"log/slog"
"sync"
"testing"
"time"
"galaxy/lobby/internal/adapters/gameinmem"
"galaxy/lobby/internal/adapters/inviteinmem"
"galaxy/lobby/internal/adapters/membershipinmem"
"galaxy/lobby/internal/adapters/mocks"
"galaxy/lobby/internal/domain/common"
"galaxy/lobby/internal/domain/game"
"galaxy/lobby/internal/domain/invite"
"galaxy/lobby/internal/domain/membership"
"galaxy/lobby/internal/service/manualreadytostart"
"galaxy/lobby/internal/service/shared"
"galaxy/notificationintent"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
type intentRec struct {
mu sync.Mutex
published []notificationintent.Intent
}
func (r *intentRec) 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 *intentRec) snapshot() []notificationintent.Intent {
r.mu.Lock()
defer r.mu.Unlock()
return append([]notificationintent.Intent(nil), r.published...)
}
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
}
const (
publicGameID = common.GameID("game-public")
privateGameID = common.GameID("game-private")
ownerUserID = "user-owner"
)
func silentLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) }
func fixedClock(at time.Time) func() time.Time { return func() time.Time { return at } }
type fixture struct {
now time.Time
games *gameinmem.Store
invites *inviteinmem.Store
memberships *membershipinmem.Store
intentRec *intentRec
intents *mocks.MockIntentPublisher
}
func newFixture(t *testing.T) *fixture {
t.Helper()
now := time.Date(2026, 4, 25, 10, 0, 0, 0, time.UTC)
rec := &intentRec{}
f := &fixture{
now: now,
games: gameinmem.NewStore(),
invites: inviteinmem.NewStore(),
memberships: membershipinmem.NewStore(),
intentRec: rec,
}
f.intents = newIntentMock(t, rec)
return f
}
func (f *fixture) addGame(t *testing.T, gameID common.GameID, gameType game.GameType, owner string, minPlayers int) game.Game {
t.Helper()
rec, err := game.New(game.NewGameInput{
GameID: gameID,
GameName: "Test Game",
GameType: gameType,
OwnerUserID: owner,
MinPlayers: minPlayers,
MaxPlayers: minPlayers + 2,
StartGapHours: 2,
StartGapPlayers: 1,
EnrollmentEndsAt: f.now.Add(24 * time.Hour),
TurnSchedule: "0 */6 * * *",
TargetEngineVersion: "1.0.0",
Now: f.now,
})
require.NoError(t, err)
rec.Status = game.StatusEnrollmentOpen
require.NoError(t, f.games.Save(context.Background(), rec))
return rec
}
func (f *fixture) addActiveMember(t *testing.T, membershipID common.MembershipID, gameID common.GameID, userID string) {
t.Helper()
mem, err := membership.New(membership.NewMembershipInput{
MembershipID: membershipID,
GameID: gameID,
UserID: userID,
RaceName: "Race " + userID,
CanonicalKey: "race-" + userID,
Now: f.now,
})
require.NoError(t, err)
require.NoError(t, f.memberships.Save(context.Background(), mem))
}
func (f *fixture) addCreatedInvite(t *testing.T, inviteID common.InviteID, gameID common.GameID, owner, invitee string) {
t.Helper()
rec, err := invite.New(invite.NewInviteInput{
InviteID: inviteID,
GameID: gameID,
InviterUserID: owner,
InviteeUserID: invitee,
Now: f.now,
ExpiresAt: f.now.Add(24 * time.Hour),
})
require.NoError(t, err)
require.NoError(t, f.invites.Save(context.Background(), rec))
}
func (f *fixture) newService(t *testing.T) *manualreadytostart.Service {
t.Helper()
svc, err := manualreadytostart.NewService(manualreadytostart.Dependencies{
Games: f.games,
Memberships: f.memberships,
Invites: f.invites,
Intents: f.intents,
Clock: fixedClock(f.now),
Logger: silentLogger(),
})
require.NoError(t, err)
return svc
}
func TestHandleAdminClosesPublicEnrollment(t *testing.T) {
t.Parallel()
f := newFixture(t)
f.addGame(t, publicGameID, game.GameTypePublic, "", 2)
f.addActiveMember(t, "membership-1", publicGameID, "user-a")
f.addActiveMember(t, "membership-2", publicGameID, "user-b")
got, err := f.newService(t).Handle(context.Background(), manualreadytostart.Input{
Actor: shared.NewAdminActor(),
GameID: publicGameID,
})
require.NoError(t, err)
assert.Equal(t, game.StatusReadyToStart, got.Status)
}
func TestHandleOwnerClosesPrivateEnrollmentAndExpiresInvites(t *testing.T) {
t.Parallel()
f := newFixture(t)
f.addGame(t, privateGameID, game.GameTypePrivate, ownerUserID, 2)
f.addActiveMember(t, "membership-1", privateGameID, "user-a")
f.addActiveMember(t, "membership-2", privateGameID, "user-b")
f.addCreatedInvite(t, "invite-1", privateGameID, ownerUserID, "user-c")
f.addCreatedInvite(t, "invite-2", privateGameID, ownerUserID, "user-d")
got, err := f.newService(t).Handle(context.Background(), manualreadytostart.Input{
Actor: shared.NewUserActor(ownerUserID),
GameID: privateGameID,
})
require.NoError(t, err)
assert.Equal(t, game.StatusReadyToStart, got.Status)
for _, id := range []common.InviteID{"invite-1", "invite-2"} {
rec, err := f.invites.Get(context.Background(), id)
require.NoError(t, err)
assert.Equal(t, invite.StatusExpired, rec.Status)
}
intents := f.intentRec.snapshot()
require.Len(t, intents, 2)
for _, intent := range intents {
assert.Equal(t, notificationintent.NotificationTypeLobbyInviteExpired, intent.NotificationType)
assert.Equal(t, []string{ownerUserID}, intent.RecipientUserIDs)
}
}
func TestHandleNonOwnerOnPrivateForbidden(t *testing.T) {
t.Parallel()
f := newFixture(t)
f.addGame(t, privateGameID, game.GameTypePrivate, ownerUserID, 2)
f.addActiveMember(t, "membership-1", privateGameID, "user-a")
f.addActiveMember(t, "membership-2", privateGameID, "user-b")
_, err := f.newService(t).Handle(context.Background(), manualreadytostart.Input{
Actor: shared.NewUserActor("user-impostor"),
GameID: privateGameID,
})
require.ErrorIs(t, err, shared.ErrForbidden)
}
func TestHandleUserOnPublicForbidden(t *testing.T) {
t.Parallel()
f := newFixture(t)
f.addGame(t, publicGameID, game.GameTypePublic, "", 2)
f.addActiveMember(t, "membership-1", publicGameID, "user-a")
f.addActiveMember(t, "membership-2", publicGameID, "user-b")
_, err := f.newService(t).Handle(context.Background(), manualreadytostart.Input{
Actor: shared.NewUserActor("user-a"),
GameID: publicGameID,
})
require.ErrorIs(t, err, shared.ErrForbidden)
}
func TestHandleGameNotFound(t *testing.T) {
t.Parallel()
f := newFixture(t)
_, err := f.newService(t).Handle(context.Background(), manualreadytostart.Input{
Actor: shared.NewAdminActor(),
GameID: publicGameID,
})
require.ErrorIs(t, err, game.ErrNotFound)
}
func TestHandleStatusNotEnrollmentOpenConflict(t *testing.T) {
t.Parallel()
f := newFixture(t)
rec := f.addGame(t, publicGameID, game.GameTypePublic, "", 2)
rec.Status = game.StatusDraft
require.NoError(t, f.games.Save(context.Background(), rec))
_, err := f.newService(t).Handle(context.Background(), manualreadytostart.Input{
Actor: shared.NewAdminActor(),
GameID: publicGameID,
})
require.ErrorIs(t, err, game.ErrConflict)
}
func TestHandleBelowMinPlayersConflict(t *testing.T) {
t.Parallel()
f := newFixture(t)
f.addGame(t, publicGameID, game.GameTypePublic, "", 3)
f.addActiveMember(t, "membership-1", publicGameID, "user-a")
f.addActiveMember(t, "membership-2", publicGameID, "user-b")
_, err := f.newService(t).Handle(context.Background(), manualreadytostart.Input{
Actor: shared.NewAdminActor(),
GameID: publicGameID,
})
require.ErrorIs(t, err, game.ErrConflict)
current, err := f.games.Get(context.Background(), publicGameID)
require.NoError(t, err)
assert.Equal(t, game.StatusEnrollmentOpen, current.Status)
assert.Empty(t, f.intentRec.snapshot())
}
func TestHandleEmptyInvitesProducesNoNotifications(t *testing.T) {
t.Parallel()
f := newFixture(t)
f.addGame(t, privateGameID, game.GameTypePrivate, ownerUserID, 2)
f.addActiveMember(t, "membership-1", privateGameID, "user-a")
f.addActiveMember(t, "membership-2", privateGameID, "user-b")
_, err := f.newService(t).Handle(context.Background(), manualreadytostart.Input{
Actor: shared.NewUserActor(ownerUserID),
GameID: privateGameID,
})
require.NoError(t, err)
assert.Empty(t, f.intentRec.snapshot())
}