feat: use postgres

This commit is contained in:
Ilia Denisov
2026-04-26 20:34:39 +02:00
committed by GitHub
parent 48b0056b49
commit fe829285a6
365 changed files with 29223 additions and 24049 deletions
@@ -0,0 +1,193 @@
package racenamedir_test
import (
"context"
"database/sql"
"strconv"
"testing"
"time"
"galaxy/lobby/internal/adapters/postgres/internal/pgtest"
"galaxy/lobby/internal/adapters/postgres/racenamedir"
"galaxy/lobby/internal/domain/racename"
"galaxy/lobby/internal/ports"
"galaxy/lobby/internal/ports/racenamedirtest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestMain wires the per-package PostgreSQL container shared by every
// store test in this module.
func TestMain(m *testing.M) { pgtest.RunMain(m) }
// newDirectory builds one Race Name Directory adapter against a freshly
// truncated lobby schema. now selects between the deterministic clock the
// shared suite supplies and the default time.Now.
func newDirectory(t *testing.T, now func() time.Time) *racenamedir.Directory {
t.Helper()
pgtest.TruncateAll(t)
policy, err := racename.NewPolicy()
require.NoError(t, err)
cfg := racenamedir.Config{
DB: pgtest.Ensure(t).Pool(),
OperationTimeout: pgtest.OperationTimeout,
Policy: policy,
}
if now != nil {
cfg.Clock = now
}
directory, err := racenamedir.New(cfg)
require.NoError(t, err)
return directory
}
// TestRaceNameDirectoryContract runs the shared behavioural suite that
// every ports.RaceNameDirectory implementation must pass.
func TestRaceNameDirectoryContract(t *testing.T) {
racenamedirtest.Run(t, func(now func() time.Time) ports.RaceNameDirectory {
return newDirectory(t, now)
})
}
func TestNewRejectsNilDB(t *testing.T) {
policy, err := racename.NewPolicy()
require.NoError(t, err)
_, err = racenamedir.New(racenamedir.Config{
OperationTimeout: pgtest.OperationTimeout,
Policy: policy,
})
require.Error(t, err)
}
func TestNewRejectsNilPolicy(t *testing.T) {
_, err := racenamedir.New(racenamedir.Config{
DB: pgtest.Ensure(t).Pool(),
OperationTimeout: pgtest.OperationTimeout,
})
require.Error(t, err)
}
func TestNewRejectsNonPositiveTimeout(t *testing.T) {
policy, err := racename.NewPolicy()
require.NoError(t, err)
_, err = racenamedir.New(racenamedir.Config{
DB: pgtest.Ensure(t).Pool(),
Policy: policy,
})
require.Error(t, err)
}
// TestRegisteredRowShape validates the on-disk shape of a registered
// binding so future schema migrations have an explicit anchor.
func TestRegisteredRowShape(t *testing.T) {
now := time.Date(2026, 5, 1, 12, 0, 0, 0, time.UTC)
directory := newDirectory(t, func() time.Time { return now })
ctx := context.Background()
const (
gameID = "game-shape-1"
userID = "user-shape-1"
raceName = "PilotNova"
)
require.NoError(t, directory.Reserve(ctx, gameID, userID, raceName))
require.NoError(t, directory.MarkPendingRegistration(ctx, gameID, userID, raceName, now.Add(time.Hour)))
require.NoError(t, directory.Register(ctx, gameID, userID, raceName))
pool := pgtest.Ensure(t).Pool()
canonical, err := directory.Canonicalize(raceName)
require.NoError(t, err)
row := pool.QueryRowContext(ctx, `
SELECT canonical_key, game_id, holder_user_id, race_name, binding_kind,
source_game_id, reserved_at_ms, eligible_until_ms, registered_at_ms
FROM race_names
WHERE canonical_key = $1
`, canonical)
var (
canonicalKey string
storedGameID string
holderUserID string
raceNameCol string
bindingKind string
sourceGameID string
reservedAtMs int64
eligibleAtMs sql.NullInt64
registeredAtMs sql.NullInt64
)
require.NoError(t, row.Scan(
&canonicalKey,
&storedGameID,
&holderUserID,
&raceNameCol,
&bindingKind,
&sourceGameID,
&reservedAtMs,
&eligibleAtMs,
&registeredAtMs,
))
assert.Equal(t, canonical, canonicalKey)
assert.Equal(t, "", storedGameID, "registered rows store game_id = ''")
assert.Equal(t, userID, holderUserID)
assert.Equal(t, raceName, raceNameCol)
assert.Equal(t, ports.KindRegistered, bindingKind)
assert.Equal(t, gameID, sourceGameID)
assert.True(t, registeredAtMs.Valid)
assert.Equal(t, now.UTC().UnixMilli(), registeredAtMs.Int64)
assert.False(t, eligibleAtMs.Valid, "registered rows null out eligible_until_ms")
assert.Equal(t, now.UTC().UnixMilli(), reservedAtMs, "reserved_at_ms is preserved across promote+register")
}
// TestRegisteredPartialUniqueIndex confirms that a second user cannot
// register the same canonical key, even when they own a separate
// reservation row at a different (canonical_key, game_id) PK.
func TestRegisteredPartialUniqueIndex(t *testing.T) {
now := time.Date(2026, 5, 1, 12, 0, 0, 0, time.UTC)
directory := newDirectory(t, func() time.Time { return now })
ctx := context.Background()
const (
raceName = "PilotNova"
gameA = "game-unique-a"
userA = "user-unique-a"
userB = "user-unique-b"
)
require.NoError(t, directory.Reserve(ctx, gameA, userA, raceName))
require.NoError(t, directory.MarkPendingRegistration(ctx, gameA, userA, raceName, now.Add(time.Hour)))
require.NoError(t, directory.Register(ctx, gameA, userA, raceName))
err := directory.Reserve(ctx, gameA, userB, raceName)
require.ErrorIs(t, err, ports.ErrNameTaken)
}
// TestExpirePendingRegistrationsBatched seeds two pending entries with
// distinct canonical keys and asserts both are released by a single pass
// even when the worker iterates via separate advisory locks.
func TestExpirePendingRegistrationsBatched(t *testing.T) {
now := time.Date(2026, 5, 1, 12, 0, 0, 0, time.UTC)
directory := newDirectory(t, func() time.Time { return now })
ctx := context.Background()
for index := range 3 {
gameID := "game-batch-" + strconv.Itoa(index)
userID := "user-batch-" + strconv.Itoa(index)
raceName := "PilotBatch" + strconv.Itoa(index)
require.NoError(t, directory.Reserve(ctx, gameID, userID, raceName))
require.NoError(t, directory.MarkPendingRegistration(ctx, gameID, userID, raceName, now.Add(time.Hour)))
}
expired, err := directory.ExpirePendingRegistrations(ctx, now.Add(2*time.Hour))
require.NoError(t, err)
require.Len(t, expired, 3)
expired, err = directory.ExpirePendingRegistrations(ctx, now.Add(2*time.Hour))
require.NoError(t, err)
assert.Empty(t, expired, "second pass releases nothing")
}