package listmyinvites_test import ( "context" "io" "log/slog" "testing" "time" "galaxy/lobby/internal/adapters/gameinmem" "galaxy/lobby/internal/adapters/inviteinmem" "galaxy/lobby/internal/adapters/membershipinmem" "galaxy/lobby/internal/domain/common" "galaxy/lobby/internal/domain/game" "galaxy/lobby/internal/domain/invite" "galaxy/lobby/internal/domain/membership" "galaxy/lobby/internal/ports" "galaxy/lobby/internal/service/listmyinvites" "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 *gameinmem.Store invites *inviteinmem.Store memberships *membershipinmem.Store svc *listmyinvites.Service } func newFixture(t *testing.T) *fixture { t.Helper() games := gameinmem.NewStore() invites := inviteinmem.NewStore() memberships := membershipinmem.NewStore() svc, err := listmyinvites.NewService(listmyinvites.Dependencies{ Games: games, Invites: invites, Memberships: memberships, Logger: silentLogger(), }) require.NoError(t, err) return &fixture{games: games, invites: invites, memberships: memberships, svc: svc} } func seedPrivateGame( t *testing.T, store *gameinmem.Store, id common.GameID, owner string, name string, now time.Time, ) { t.Helper() record, err := game.New(game.NewGameInput{ GameID: id, GameName: name, GameType: game.GameTypePrivate, 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 seedInvite( t *testing.T, store *inviteinmem.Store, id common.InviteID, gameID common.GameID, inviter, invitee string, status invite.Status, createdAt time.Time, ) { t.Helper() record, err := invite.New(invite.NewInviteInput{ InviteID: id, GameID: gameID, InviterUserID: inviter, InviteeUserID: invitee, Now: createdAt, ExpiresAt: createdAt.Add(48 * time.Hour), }) require.NoError(t, err) require.NoError(t, store.Save(context.Background(), record)) if status != invite.StatusCreated { input := ports.UpdateInviteStatusInput{ InviteID: id, ExpectedFrom: invite.StatusCreated, To: status, At: createdAt.Add(time.Minute), } if status == invite.StatusRedeemed { input.RaceName = "Race-" + invitee } require.NoError(t, store.UpdateStatus(context.Background(), input)) } } func seedActiveMembership( t *testing.T, store *membershipinmem.Store, gameID common.GameID, userID, raceName string, now time.Time, ) { t.Helper() record, err := membership.New(membership.NewMembershipInput{ MembershipID: common.MembershipID("membership-" + userID + "-" + gameID.String()), GameID: gameID, UserID: userID, RaceName: raceName, CanonicalKey: raceName, Now: now, }) require.NoError(t, err) require.NoError(t, store.Save(context.Background(), record)) } func TestHandleReturnsCreatedOnly(t *testing.T) { t.Parallel() fix := newFixture(t) now := time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC) seedPrivateGame(t, fix.games, "game-1", "user-owner", "Hyperion", now) seedInvite(t, fix.invites, "invite-created", "game-1", "user-owner", "user-invitee", invite.StatusCreated, now) seedInvite(t, fix.invites, "invite-declined", "game-1", "user-owner", "user-invitee", invite.StatusDeclined, now) seedInvite(t, fix.invites, "invite-revoked", "game-1", "user-owner", "user-invitee", invite.StatusRevoked, now) out, err := fix.svc.Handle(context.Background(), listmyinvites.Input{ Actor: shared.NewUserActor("user-invitee"), }) require.NoError(t, err) require.Len(t, out.Items, 1) require.Equal(t, common.InviteID("invite-created"), out.Items[0].Invite.InviteID) require.Equal(t, "Hyperion", out.Items[0].GameName) } func TestHandleInviterNameUsesRaceWhenAvailable(t *testing.T) { t.Parallel() fix := newFixture(t) now := time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC) seedPrivateGame(t, fix.games, "game-1", "user-owner", "G", now) seedActiveMembership(t, fix.memberships, "game-1", "user-owner", "Centauri", now) seedInvite(t, fix.invites, "invite-1", "game-1", "user-owner", "user-invitee", invite.StatusCreated, now) out, err := fix.svc.Handle(context.Background(), listmyinvites.Input{ Actor: shared.NewUserActor("user-invitee"), }) require.NoError(t, err) require.Len(t, out.Items, 1) require.Equal(t, "Centauri", out.Items[0].InviterName) } func TestHandleInviterNameFallsBackToUserID(t *testing.T) { t.Parallel() fix := newFixture(t) now := time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC) seedPrivateGame(t, fix.games, "game-1", "user-owner", "G", now) // no membership for inviter seedInvite(t, fix.invites, "invite-1", "game-1", "user-owner", "user-invitee", invite.StatusCreated, now) out, err := fix.svc.Handle(context.Background(), listmyinvites.Input{ Actor: shared.NewUserActor("user-invitee"), }) require.NoError(t, err) require.Len(t, out.Items, 1) require.Equal(t, "user-owner", out.Items[0].InviterName) } func TestHandleAdminForbidden(t *testing.T) { t.Parallel() fix := newFixture(t) _, err := fix.svc.Handle(context.Background(), listmyinvites.Input{ Actor: shared.NewAdminActor(), }) require.ErrorIs(t, err, shared.ErrForbidden) } 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))) seedPrivateGame(t, fix.games, gid, "user-owner", "G", now) seedInvite(t, fix.invites, common.InviteID("invite-page-"+string(rune('a'+i))), gid, "user-owner", "user-invitee", invite.StatusCreated, now.Add(time.Duration(i)*time.Minute), ) } first, err := fix.svc.Handle(context.Background(), listmyinvites.Input{ Actor: shared.NewUserActor("user-invitee"), Page: shared.Page{Size: 2}, }) require.NoError(t, err) require.Len(t, first.Items, 2) require.NotEmpty(t, first.NextPageToken) require.Equal(t, common.InviteID("invite-page-d"), first.Items[0].Invite.InviteID) require.Equal(t, common.InviteID("invite-page-c"), first.Items[1].Invite.InviteID) } func TestNewServiceRejectsMissingDeps(t *testing.T) { t.Parallel() cases := []struct { name string deps listmyinvites.Dependencies }{ {"nil games", listmyinvites.Dependencies{Invites: inviteinmem.NewStore(), Memberships: membershipinmem.NewStore()}}, {"nil invites", listmyinvites.Dependencies{Games: gameinmem.NewStore(), Memberships: membershipinmem.NewStore()}}, {"nil memberships", listmyinvites.Dependencies{Games: gameinmem.NewStore(), Invites: inviteinmem.NewStore()}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() _, err := listmyinvites.NewService(tc.deps) require.Error(t, err) }) } }