package listmygames_test import ( "context" "io" "log/slog" "testing" "time" "galaxy/lobby/internal/adapters/gamestub" "galaxy/lobby/internal/adapters/membershipstub" "galaxy/lobby/internal/domain/common" "galaxy/lobby/internal/domain/game" "galaxy/lobby/internal/domain/membership" "galaxy/lobby/internal/ports" "galaxy/lobby/internal/service/listmygames" "galaxy/lobby/internal/service/shared" "github.com/stretchr/testify/require" ) func silentLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) } type fixture struct { games *gamestub.Store memberships *membershipstub.Store svc *listmygames.Service } func newFixture(t *testing.T) *fixture { t.Helper() games := gamestub.NewStore() memberships := membershipstub.NewStore() svc, err := listmygames.NewService(listmygames.Dependencies{ Games: games, Memberships: memberships, Logger: silentLogger(), }) require.NoError(t, err) return &fixture{games: games, memberships: memberships, svc: svc} } func seedGameWithStatus( t *testing.T, store *gamestub.Store, id common.GameID, status game.Status, now time.Time, ) { t.Helper() record, err := game.New(game.NewGameInput{ GameID: id, GameName: "Seed", GameType: game.GameTypePublic, MinPlayers: 2, MaxPlayers: 4, StartGapHours: 4, StartGapPlayers: 1, EnrollmentEndsAt: now.Add(24 * time.Hour), TurnSchedule: "0 */6 * * *", TargetEngineVersion: "1.0.0", Now: now, }) require.NoError(t, err) if status != game.StatusDraft { record.Status = status record.UpdatedAt = now.Add(time.Minute) if status == game.StatusRunning || status == game.StatusPaused || status == game.StatusFinished { started := now.Add(time.Minute) record.StartedAt = &started } } require.NoError(t, store.Save(context.Background(), record)) } func seedMembership( t *testing.T, store *membershipstub.Store, gameID common.GameID, userID string, status membership.Status, joinedAt time.Time, ) { t.Helper() record, err := membership.New(membership.NewMembershipInput{ MembershipID: common.MembershipID("membership-" + userID + "-" + gameID.String()), GameID: gameID, UserID: userID, RaceName: "Race-" + userID, CanonicalKey: "race-" + userID, Now: joinedAt, }) require.NoError(t, err) require.NoError(t, store.Save(context.Background(), record)) if status != membership.StatusActive { require.NoError(t, store.UpdateStatus(context.Background(), ports.UpdateMembershipStatusInput{ MembershipID: record.MembershipID, ExpectedFrom: membership.StatusActive, To: status, At: joinedAt.Add(time.Minute), })) } } func TestHandleReturnsRunningAndPaused(t *testing.T) { t.Parallel() fix := newFixture(t) now := time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC) seedGameWithStatus(t, fix.games, "game-running", game.StatusRunning, now) seedGameWithStatus(t, fix.games, "game-paused", game.StatusPaused, now) seedGameWithStatus(t, fix.games, "game-finished", game.StatusFinished, now) seedGameWithStatus(t, fix.games, "game-enroll", game.StatusEnrollmentOpen, now) for _, id := range []common.GameID{"game-running", "game-paused", "game-finished", "game-enroll"} { seedMembership(t, fix.memberships, id, "user-1", membership.StatusActive, now) } out, err := fix.svc.Handle(context.Background(), listmygames.Input{ Actor: shared.NewUserActor("user-1"), }) require.NoError(t, err) require.Len(t, out.Items, 2) gotIDs := []common.GameID{out.Items[0].GameID, out.Items[1].GameID} require.Contains(t, gotIDs, common.GameID("game-running")) require.Contains(t, gotIDs, common.GameID("game-paused")) } func TestHandleIgnoresRemovedAndBlockedMemberships(t *testing.T) { t.Parallel() fix := newFixture(t) now := time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC) seedGameWithStatus(t, fix.games, "game-removed", game.StatusRunning, now) seedGameWithStatus(t, fix.games, "game-blocked", game.StatusRunning, now) seedGameWithStatus(t, fix.games, "game-active", game.StatusRunning, now) seedMembership(t, fix.memberships, "game-removed", "user-1", membership.StatusRemoved, now) seedMembership(t, fix.memberships, "game-blocked", "user-1", membership.StatusBlocked, now) seedMembership(t, fix.memberships, "game-active", "user-1", membership.StatusActive, now) out, err := fix.svc.Handle(context.Background(), listmygames.Input{ Actor: shared.NewUserActor("user-1"), }) require.NoError(t, err) require.Len(t, out.Items, 1) require.Equal(t, common.GameID("game-active"), out.Items[0].GameID) } func TestHandleAdminForbidden(t *testing.T) { t.Parallel() fix := newFixture(t) _, err := fix.svc.Handle(context.Background(), listmygames.Input{ Actor: shared.NewAdminActor(), }) require.ErrorIs(t, err, shared.ErrForbidden) } func TestHandlePaginationAndOrder(t *testing.T) { t.Parallel() fix := newFixture(t) now := time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC) for i := range 4 { id := common.GameID("game-page-" + string(rune('a'+i))) seedGameWithStatus(t, fix.games, id, game.StatusRunning, now.Add(time.Duration(i)*time.Minute)) seedMembership(t, fix.memberships, id, "user-1", membership.StatusActive, now) } out, err := fix.svc.Handle(context.Background(), listmygames.Input{ Actor: shared.NewUserActor("user-1"), Page: shared.Page{Size: 2}, }) require.NoError(t, err) require.Len(t, out.Items, 2) require.NotEmpty(t, out.NextPageToken) // Most recently started first. require.Equal(t, common.GameID("game-page-d"), out.Items[0].GameID) require.Equal(t, common.GameID("game-page-c"), out.Items[1].GameID) } func TestNewServiceRejectsMissingDeps(t *testing.T) { t.Parallel() cases := []struct { name string deps listmygames.Dependencies }{ {"nil games", listmygames.Dependencies{Memberships: membershipstub.NewStore()}}, {"nil memberships", listmygames.Dependencies{Games: gamestub.NewStore()}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() _, err := listmygames.NewService(tc.deps) require.Error(t, err) }) } }