package listmyapplications_test import ( "context" "io" "log/slog" "testing" "time" "galaxy/lobby/internal/adapters/applicationstub" "galaxy/lobby/internal/adapters/gamestub" "galaxy/lobby/internal/domain/application" "galaxy/lobby/internal/domain/common" "galaxy/lobby/internal/domain/game" "galaxy/lobby/internal/ports" "galaxy/lobby/internal/service/listmyapplications" "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 applications *applicationstub.Store svc *listmyapplications.Service } func newFixture(t *testing.T) *fixture { t.Helper() games := gamestub.NewStore() apps := applicationstub.NewStore() svc, err := listmyapplications.NewService(listmyapplications.Dependencies{ Games: games, Applications: apps, Logger: silentLogger(), }) require.NoError(t, err) return &fixture{games: games, applications: apps, svc: svc} } func seedGame( t *testing.T, store *gamestub.Store, id common.GameID, gameType game.GameType, name string, now time.Time, ) { t.Helper() owner := "" if gameType == game.GameTypePrivate { owner = "user-owner" } record, err := game.New(game.NewGameInput{ GameID: id, GameName: name, GameType: gameType, OwnerUserID: owner, 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) require.NoError(t, store.Save(context.Background(), record)) } func seedApplication( t *testing.T, store *applicationstub.Store, id common.ApplicationID, gameID common.GameID, userID string, status application.Status, createdAt time.Time, ) { t.Helper() record, err := application.New(application.NewApplicationInput{ ApplicationID: id, GameID: gameID, ApplicantUserID: userID, RaceName: "Race-" + userID, Now: createdAt, }) require.NoError(t, err) require.NoError(t, store.Save(context.Background(), record)) if status != application.StatusSubmitted { require.NoError(t, store.UpdateStatus(context.Background(), ports.UpdateApplicationStatusInput{ ApplicationID: id, ExpectedFrom: application.StatusSubmitted, To: status, At: createdAt.Add(time.Minute), })) } } func TestHandleReturnsSubmittedOnly(t *testing.T) { t.Parallel() fix := newFixture(t) now := time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC) seedGame(t, fix.games, "game-1", game.GameTypePublic, "Hyperion", now) seedGame(t, fix.games, "game-2", game.GameTypePublic, "Endymion", now) seedApplication(t, fix.applications, "application-submitted", "game-1", "user-1", application.StatusSubmitted, now) seedApplication(t, fix.applications, "application-rejected", "game-2", "user-1", application.StatusRejected, now) out, err := fix.svc.Handle(context.Background(), listmyapplications.Input{ Actor: shared.NewUserActor("user-1"), }) require.NoError(t, err) require.Len(t, out.Items, 1) require.Equal(t, common.ApplicationID("application-submitted"), out.Items[0].Application.ApplicationID) require.Equal(t, "Hyperion", out.Items[0].GameName) require.Equal(t, game.GameTypePublic, out.Items[0].GameType) } func TestHandleAdminForbidden(t *testing.T) { t.Parallel() fix := newFixture(t) _, err := fix.svc.Handle(context.Background(), listmyapplications.Input{ Actor: shared.NewAdminActor(), }) require.ErrorIs(t, err, shared.ErrForbidden) } func TestHandleSkipsApplicationsWithMissingGame(t *testing.T) { t.Parallel() fix := newFixture(t) now := time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC) // Note: seed only application-1's game; application-2 will reference a missing game. seedGame(t, fix.games, "game-1", game.GameTypePublic, "G1", now) seedApplication(t, fix.applications, "application-1", "game-1", "user-1", application.StatusSubmitted, now) seedApplication(t, fix.applications, "application-2", "game-missing", "user-1", application.StatusSubmitted, now) out, err := fix.svc.Handle(context.Background(), listmyapplications.Input{ Actor: shared.NewUserActor("user-1"), }) require.NoError(t, err) require.Len(t, out.Items, 1) require.Equal(t, common.ApplicationID("application-1"), out.Items[0].Application.ApplicationID) } func TestHandlePagination(t *testing.T) { t.Parallel() fix := newFixture(t) now := time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC) for i := range 4 { gid := common.GameID("game-page-" + string(rune('a'+i))) seedGame(t, fix.games, gid, game.GameTypePublic, "G", now) seedApplication(t, fix.applications, common.ApplicationID("application-page-"+string(rune('a'+i))), gid, "user-1", application.StatusSubmitted, now.Add(time.Duration(i)*time.Minute), ) } first, err := fix.svc.Handle(context.Background(), listmyapplications.Input{ Actor: shared.NewUserActor("user-1"), Page: shared.Page{Size: 2}, }) require.NoError(t, err) require.Len(t, first.Items, 2) require.NotEmpty(t, first.NextPageToken) // Most recent first. require.Equal(t, common.ApplicationID("application-page-d"), first.Items[0].Application.ApplicationID) require.Equal(t, common.ApplicationID("application-page-c"), first.Items[1].Application.ApplicationID) } func TestNewServiceRejectsMissingDeps(t *testing.T) { t.Parallel() cases := []struct { name string deps listmyapplications.Dependencies }{ {"nil games", listmyapplications.Dependencies{Applications: applicationstub.NewStore()}}, {"nil applications", listmyapplications.Dependencies{Games: gamestub.NewStore()}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() _, err := listmyapplications.NewService(tc.deps) require.Error(t, err) }) } }