Files
galaxy-game/lobby/internal/adapters/postgres/invitestore/store_test.go
T
2026-04-26 20:34:39 +02:00

200 lines
6.2 KiB
Go

package invitestore_test
import (
"context"
"testing"
"time"
"galaxy/lobby/internal/adapters/postgres/gamestore"
"galaxy/lobby/internal/adapters/postgres/internal/pgtest"
"galaxy/lobby/internal/adapters/postgres/invitestore"
"galaxy/lobby/internal/domain/common"
"galaxy/lobby/internal/domain/game"
"galaxy/lobby/internal/domain/invite"
"galaxy/lobby/internal/ports"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) { pgtest.RunMain(m) }
func newStores(t *testing.T) (*gamestore.Store, *invitestore.Store) {
t.Helper()
pgtest.TruncateAll(t)
gs, err := gamestore.New(gamestore.Config{
DB: pgtest.Ensure(t).Pool(), OperationTimeout: pgtest.OperationTimeout,
})
require.NoError(t, err)
is, err := invitestore.New(invitestore.Config{
DB: pgtest.Ensure(t).Pool(), OperationTimeout: pgtest.OperationTimeout,
})
require.NoError(t, err)
return gs, is
}
func seedPrivateGame(t *testing.T, gs *gamestore.Store, id, ownerID string) game.Game {
t.Helper()
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
g, err := game.New(game.NewGameInput{
GameID: common.GameID(id),
GameName: "Private " + id,
GameType: game.GameTypePrivate,
OwnerUserID: ownerID,
MinPlayers: 2,
MaxPlayers: 6,
StartGapHours: 12,
StartGapPlayers: 2,
EnrollmentEndsAt: now.Add(7 * 24 * time.Hour),
TurnSchedule: "0 18 * * *",
TargetEngineVersion: "v1.0.0",
Now: now,
})
require.NoError(t, err)
require.NoError(t, gs.Save(context.Background(), g))
return g
}
func newInvite(t *testing.T, id, gameID, inviter, invitee string) invite.Invite {
t.Helper()
now := time.Date(2026, 4, 23, 12, 0, 0, 0, time.UTC)
rec, err := invite.New(invite.NewInviteInput{
InviteID: common.InviteID(id),
GameID: common.GameID(gameID),
InviterUserID: inviter,
InviteeUserID: invitee,
Now: now,
ExpiresAt: now.Add(7 * 24 * time.Hour),
})
require.NoError(t, err)
return rec
}
func TestSaveAndGet(t *testing.T) {
ctx := context.Background()
gs, is := newStores(t)
seedPrivateGame(t, gs, "game-001", "owner-1")
rec := newInvite(t, "invite-001", "game-001", "owner-1", "invitee-1")
require.NoError(t, is.Save(ctx, rec))
got, err := is.Get(ctx, rec.InviteID)
require.NoError(t, err)
assert.Equal(t, rec.InviteID, got.InviteID)
assert.Equal(t, invite.StatusCreated, got.Status)
assert.Equal(t, "invitee-1", got.InviteeUserID)
assert.True(t, got.ExpiresAt.Equal(rec.ExpiresAt))
}
func TestSaveRejectsNonCreated(t *testing.T) {
ctx := context.Background()
gs, is := newStores(t)
seedPrivateGame(t, gs, "game-001", "owner-1")
rec := newInvite(t, "invite-001", "game-001", "owner-1", "invitee-1")
rec.Status = invite.StatusRedeemed
require.Error(t, is.Save(ctx, rec))
}
func TestSaveDuplicateReturnsConflict(t *testing.T) {
ctx := context.Background()
gs, is := newStores(t)
seedPrivateGame(t, gs, "game-001", "owner-1")
rec := newInvite(t, "invite-001", "game-001", "owner-1", "invitee-1")
require.NoError(t, is.Save(ctx, rec))
err := is.Save(ctx, rec)
require.ErrorIs(t, err, invite.ErrConflict)
}
func TestUpdateStatusRedeemSetsRaceName(t *testing.T) {
ctx := context.Background()
gs, is := newStores(t)
seedPrivateGame(t, gs, "game-001", "owner-1")
rec := newInvite(t, "invite-001", "game-001", "owner-1", "invitee-1")
require.NoError(t, is.Save(ctx, rec))
require.NoError(t, is.UpdateStatus(ctx, ports.UpdateInviteStatusInput{
InviteID: rec.InviteID,
ExpectedFrom: invite.StatusCreated,
To: invite.StatusRedeemed,
At: rec.CreatedAt.Add(time.Minute),
RaceName: "PilotRedeemed",
}))
got, err := is.Get(ctx, rec.InviteID)
require.NoError(t, err)
assert.Equal(t, invite.StatusRedeemed, got.Status)
assert.Equal(t, "PilotRedeemed", got.RaceName)
require.NotNil(t, got.DecidedAt)
}
func TestUpdateStatusReturnsConflictOnExpectedFromMismatch(t *testing.T) {
ctx := context.Background()
gs, is := newStores(t)
seedPrivateGame(t, gs, "game-001", "owner-1")
rec := newInvite(t, "invite-001", "game-001", "owner-1", "invitee-1")
require.NoError(t, is.Save(ctx, rec))
// Move row out of `created` so the next attempt's `WHERE status = ?`
// fails on persistence even though the (created → revoked) transition is
// itself valid in the domain table.
require.NoError(t, is.UpdateStatus(ctx, ports.UpdateInviteStatusInput{
InviteID: rec.InviteID,
ExpectedFrom: invite.StatusCreated,
To: invite.StatusDeclined,
At: rec.CreatedAt.Add(time.Minute),
}))
err := is.UpdateStatus(ctx, ports.UpdateInviteStatusInput{
InviteID: rec.InviteID,
ExpectedFrom: invite.StatusCreated,
To: invite.StatusRevoked,
At: rec.CreatedAt.Add(2 * time.Minute),
})
require.ErrorIs(t, err, invite.ErrConflict)
}
func TestUpdateStatusReturnsNotFoundForMissing(t *testing.T) {
ctx := context.Background()
_, is := newStores(t)
err := is.UpdateStatus(ctx, ports.UpdateInviteStatusInput{
InviteID: common.InviteID("invite-missing"),
ExpectedFrom: invite.StatusCreated,
To: invite.StatusDeclined,
At: time.Now().UTC(),
})
require.ErrorIs(t, err, invite.ErrNotFound)
}
func TestGetByGameUserInviter(t *testing.T) {
ctx := context.Background()
gs, is := newStores(t)
seedPrivateGame(t, gs, "game-001", "owner-1")
seedPrivateGame(t, gs, "game-002", "owner-2")
require.NoError(t, is.Save(ctx, newInvite(t, "invite-001", "game-001", "owner-1", "invitee-1")))
require.NoError(t, is.Save(ctx, newInvite(t, "invite-002", "game-001", "owner-1", "invitee-2")))
require.NoError(t, is.Save(ctx, newInvite(t, "invite-003", "game-002", "owner-2", "invitee-1")))
g1, err := is.GetByGame(ctx, common.GameID("game-001"))
require.NoError(t, err)
assert.Len(t, g1, 2)
user1, err := is.GetByUser(ctx, "invitee-1")
require.NoError(t, err)
assert.Len(t, user1, 2)
by1, err := is.GetByInviter(ctx, "owner-1")
require.NoError(t, err)
assert.Len(t, by1, 2)
}
func TestGetMissingReturnsNotFound(t *testing.T) {
ctx := context.Background()
_, is := newStores(t)
_, err := is.Get(ctx, common.InviteID("invite-missing"))
require.ErrorIs(t, err, invite.ErrNotFound)
}