feat: gamemaster
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
// Package playermappingstore implements the PostgreSQL-backed adapter
|
||||
// for `ports.PlayerMappingStore`.
|
||||
//
|
||||
// The package owns the on-disk shape of the `player_mappings` table
|
||||
// defined in
|
||||
// `galaxy/gamemaster/internal/adapters/postgres/migrations/00001_init.sql`
|
||||
// and translates the schema-agnostic `ports.PlayerMappingStore`
|
||||
// interface declared in `internal/ports/playermappingstore.go` into
|
||||
// concrete go-jet/v2 statements driven by the pgx driver.
|
||||
//
|
||||
// BulkInsert ships every row in a single multi-row INSERT so the
|
||||
// operation is atomic — any unique-constraint violation rolls back the
|
||||
// whole batch and is mapped to playermapping.ErrConflict.
|
||||
package playermappingstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/gamemaster/internal/adapters/postgres/internal/sqlx"
|
||||
pgtable "galaxy/gamemaster/internal/adapters/postgres/jet/gamemaster/table"
|
||||
"galaxy/gamemaster/internal/domain/playermapping"
|
||||
"galaxy/gamemaster/internal/ports"
|
||||
|
||||
pg "github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
// Config configures one PostgreSQL-backed player-mapping store.
|
||||
type Config struct {
|
||||
DB *sql.DB
|
||||
OperationTimeout time.Duration
|
||||
}
|
||||
|
||||
// Store persists Game Master player mappings in PostgreSQL.
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
operationTimeout time.Duration
|
||||
}
|
||||
|
||||
// New constructs one PostgreSQL-backed player-mapping store from cfg.
|
||||
func New(cfg Config) (*Store, error) {
|
||||
if cfg.DB == nil {
|
||||
return nil, errors.New("new postgres player mapping store: db must not be nil")
|
||||
}
|
||||
if cfg.OperationTimeout <= 0 {
|
||||
return nil, errors.New("new postgres player mapping store: operation timeout must be positive")
|
||||
}
|
||||
return &Store{
|
||||
db: cfg.DB,
|
||||
operationTimeout: cfg.OperationTimeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// playerMappingSelectColumns matches scanRow's column order.
|
||||
var playerMappingSelectColumns = pg.ColumnList{
|
||||
pgtable.PlayerMappings.GameID,
|
||||
pgtable.PlayerMappings.UserID,
|
||||
pgtable.PlayerMappings.RaceName,
|
||||
pgtable.PlayerMappings.EnginePlayerUUID,
|
||||
pgtable.PlayerMappings.CreatedAt,
|
||||
}
|
||||
|
||||
// BulkInsert installs every mapping in records using a single
|
||||
// multi-row INSERT. Either every row is persisted or none of them is.
|
||||
// Any PostgreSQL unique-violation
|
||||
// (`(game_id, user_id)` PK or `(game_id, race_name)` UNIQUE) is mapped
|
||||
// to playermapping.ErrConflict.
|
||||
func (store *Store) BulkInsert(ctx context.Context, records []playermapping.PlayerMapping) error {
|
||||
if store == nil || store.db == nil {
|
||||
return errors.New("bulk insert player mappings: nil store")
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
for index, record := range records {
|
||||
if err := record.Validate(); err != nil {
|
||||
return fmt.Errorf("bulk insert player mappings: record %d: %w", index, err)
|
||||
}
|
||||
}
|
||||
|
||||
operationCtx, cancel, err := sqlx.WithTimeout(ctx, "bulk insert player mappings", store.operationTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
stmt := pgtable.PlayerMappings.INSERT(
|
||||
pgtable.PlayerMappings.GameID,
|
||||
pgtable.PlayerMappings.UserID,
|
||||
pgtable.PlayerMappings.RaceName,
|
||||
pgtable.PlayerMappings.EnginePlayerUUID,
|
||||
pgtable.PlayerMappings.CreatedAt,
|
||||
)
|
||||
for _, record := range records {
|
||||
stmt = stmt.VALUES(
|
||||
record.GameID,
|
||||
record.UserID,
|
||||
record.RaceName,
|
||||
record.EnginePlayerUUID,
|
||||
record.CreatedAt.UTC(),
|
||||
)
|
||||
}
|
||||
|
||||
query, args := stmt.Sql()
|
||||
if _, err := store.db.ExecContext(operationCtx, query, args...); err != nil {
|
||||
if sqlx.IsUniqueViolation(err) {
|
||||
return fmt.Errorf("bulk insert player mappings: %w", playermapping.ErrConflict)
|
||||
}
|
||||
return fmt.Errorf("bulk insert player mappings: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the mapping identified by (gameID, userID).
|
||||
func (store *Store) Get(ctx context.Context, gameID, userID string) (playermapping.PlayerMapping, error) {
|
||||
if store == nil || store.db == nil {
|
||||
return playermapping.PlayerMapping{}, errors.New("get player mapping: nil store")
|
||||
}
|
||||
if strings.TrimSpace(gameID) == "" {
|
||||
return playermapping.PlayerMapping{}, fmt.Errorf("get player mapping: game id must not be empty")
|
||||
}
|
||||
if strings.TrimSpace(userID) == "" {
|
||||
return playermapping.PlayerMapping{}, fmt.Errorf("get player mapping: user id must not be empty")
|
||||
}
|
||||
|
||||
operationCtx, cancel, err := sqlx.WithTimeout(ctx, "get player mapping", store.operationTimeout)
|
||||
if err != nil {
|
||||
return playermapping.PlayerMapping{}, err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
stmt := pg.SELECT(playerMappingSelectColumns).
|
||||
FROM(pgtable.PlayerMappings).
|
||||
WHERE(pg.AND(
|
||||
pgtable.PlayerMappings.GameID.EQ(pg.String(gameID)),
|
||||
pgtable.PlayerMappings.UserID.EQ(pg.String(userID)),
|
||||
))
|
||||
|
||||
query, args := stmt.Sql()
|
||||
row := store.db.QueryRowContext(operationCtx, query, args...)
|
||||
got, err := scanRow(row)
|
||||
if sqlx.IsNoRows(err) {
|
||||
return playermapping.PlayerMapping{}, playermapping.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return playermapping.PlayerMapping{}, fmt.Errorf("get player mapping: %w", err)
|
||||
}
|
||||
return got, nil
|
||||
}
|
||||
|
||||
// GetByRace returns the mapping identified by (gameID, raceName).
|
||||
func (store *Store) GetByRace(ctx context.Context, gameID, raceName string) (playermapping.PlayerMapping, error) {
|
||||
if store == nil || store.db == nil {
|
||||
return playermapping.PlayerMapping{}, errors.New("get player mapping by race: nil store")
|
||||
}
|
||||
if strings.TrimSpace(gameID) == "" {
|
||||
return playermapping.PlayerMapping{}, fmt.Errorf("get player mapping by race: game id must not be empty")
|
||||
}
|
||||
if strings.TrimSpace(raceName) == "" {
|
||||
return playermapping.PlayerMapping{}, fmt.Errorf("get player mapping by race: race name must not be empty")
|
||||
}
|
||||
|
||||
operationCtx, cancel, err := sqlx.WithTimeout(ctx, "get player mapping by race", store.operationTimeout)
|
||||
if err != nil {
|
||||
return playermapping.PlayerMapping{}, err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
stmt := pg.SELECT(playerMappingSelectColumns).
|
||||
FROM(pgtable.PlayerMappings).
|
||||
WHERE(pg.AND(
|
||||
pgtable.PlayerMappings.GameID.EQ(pg.String(gameID)),
|
||||
pgtable.PlayerMappings.RaceName.EQ(pg.String(raceName)),
|
||||
))
|
||||
|
||||
query, args := stmt.Sql()
|
||||
row := store.db.QueryRowContext(operationCtx, query, args...)
|
||||
got, err := scanRow(row)
|
||||
if sqlx.IsNoRows(err) {
|
||||
return playermapping.PlayerMapping{}, playermapping.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return playermapping.PlayerMapping{}, fmt.Errorf("get player mapping by race: %w", err)
|
||||
}
|
||||
return got, nil
|
||||
}
|
||||
|
||||
// ListByGame returns every mapping owned by gameID, ordered by user_id
|
||||
// ascending.
|
||||
func (store *Store) ListByGame(ctx context.Context, gameID string) ([]playermapping.PlayerMapping, error) {
|
||||
if store == nil || store.db == nil {
|
||||
return nil, errors.New("list player mappings by game: nil store")
|
||||
}
|
||||
if strings.TrimSpace(gameID) == "" {
|
||||
return nil, fmt.Errorf("list player mappings by game: game id must not be empty")
|
||||
}
|
||||
|
||||
operationCtx, cancel, err := sqlx.WithTimeout(ctx, "list player mappings by game", store.operationTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
stmt := pg.SELECT(playerMappingSelectColumns).
|
||||
FROM(pgtable.PlayerMappings).
|
||||
WHERE(pgtable.PlayerMappings.GameID.EQ(pg.String(gameID))).
|
||||
ORDER_BY(pgtable.PlayerMappings.UserID.ASC())
|
||||
|
||||
query, args := stmt.Sql()
|
||||
rows, err := store.db.QueryContext(operationCtx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list player mappings by game: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
mappings := make([]playermapping.PlayerMapping, 0)
|
||||
for rows.Next() {
|
||||
got, err := scanRow(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list player mappings by game: scan: %w", err)
|
||||
}
|
||||
mappings = append(mappings, got)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("list player mappings by game: %w", err)
|
||||
}
|
||||
if len(mappings) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return mappings, nil
|
||||
}
|
||||
|
||||
// DeleteByGame removes every mapping owned by gameID. The call is
|
||||
// idempotent: it returns nil even when no rows were deleted.
|
||||
func (store *Store) DeleteByGame(ctx context.Context, gameID string) error {
|
||||
if store == nil || store.db == nil {
|
||||
return errors.New("delete player mappings by game: nil store")
|
||||
}
|
||||
if strings.TrimSpace(gameID) == "" {
|
||||
return fmt.Errorf("delete player mappings by game: game id must not be empty")
|
||||
}
|
||||
|
||||
operationCtx, cancel, err := sqlx.WithTimeout(ctx, "delete player mappings by game", store.operationTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
stmt := pgtable.PlayerMappings.DELETE().
|
||||
WHERE(pgtable.PlayerMappings.GameID.EQ(pg.String(gameID)))
|
||||
|
||||
query, args := stmt.Sql()
|
||||
if _, err := store.db.ExecContext(operationCtx, query, args...); err != nil {
|
||||
return fmt.Errorf("delete player mappings by game: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// rowScanner abstracts *sql.Row and *sql.Rows so scanRow can be shared
|
||||
// across single-row and iterated reads.
|
||||
type rowScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
// scanRow scans one player_mappings row from rs.
|
||||
func scanRow(rs rowScanner) (playermapping.PlayerMapping, error) {
|
||||
var (
|
||||
gameID string
|
||||
userID string
|
||||
raceName string
|
||||
enginePlayerUUID string
|
||||
createdAt time.Time
|
||||
)
|
||||
if err := rs.Scan(&gameID, &userID, &raceName, &enginePlayerUUID, &createdAt); err != nil {
|
||||
return playermapping.PlayerMapping{}, err
|
||||
}
|
||||
return playermapping.PlayerMapping{
|
||||
GameID: gameID,
|
||||
UserID: userID,
|
||||
RaceName: raceName,
|
||||
EnginePlayerUUID: enginePlayerUUID,
|
||||
CreatedAt: createdAt.UTC(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Ensure Store satisfies the ports.PlayerMappingStore interface at
|
||||
// compile time.
|
||||
var _ ports.PlayerMappingStore = (*Store)(nil)
|
||||
@@ -0,0 +1,264 @@
|
||||
package playermappingstore_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"galaxy/gamemaster/internal/adapters/postgres/internal/pgtest"
|
||||
"galaxy/gamemaster/internal/adapters/postgres/playermappingstore"
|
||||
"galaxy/gamemaster/internal/domain/playermapping"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) { pgtest.RunMain(m) }
|
||||
|
||||
func newStore(t *testing.T) *playermappingstore.Store {
|
||||
t.Helper()
|
||||
pgtest.TruncateAll(t)
|
||||
store, err := playermappingstore.New(playermappingstore.Config{
|
||||
DB: pgtest.Ensure(t).Pool(),
|
||||
OperationTimeout: pgtest.OperationTimeout,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return store
|
||||
}
|
||||
|
||||
func mapping(gameID, userID, raceName, uuid string, createdAt time.Time) playermapping.PlayerMapping {
|
||||
return playermapping.PlayerMapping{
|
||||
GameID: gameID,
|
||||
UserID: userID,
|
||||
RaceName: raceName,
|
||||
EnginePlayerUUID: uuid,
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRejectsInvalidConfig(t *testing.T) {
|
||||
_, err := playermappingstore.New(playermappingstore.Config{})
|
||||
require.Error(t, err)
|
||||
|
||||
store, err := playermappingstore.New(playermappingstore.Config{
|
||||
DB: pgtest.Ensure(t).Pool(),
|
||||
OperationTimeout: 0,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, store)
|
||||
}
|
||||
|
||||
func TestBulkInsertHappy(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
now := time.Date(2026, time.April, 27, 12, 0, 0, 0, time.UTC)
|
||||
records := []playermapping.PlayerMapping{
|
||||
mapping("game-001", "user-1", "Aelinari", "uuid-1", now),
|
||||
mapping("game-001", "user-2", "Drazi", "uuid-2", now),
|
||||
mapping("game-001", "user-3", "Voltori", "uuid-3", now),
|
||||
}
|
||||
require.NoError(t, store.BulkInsert(ctx, records))
|
||||
|
||||
for _, want := range records {
|
||||
got, err := store.Get(ctx, want.GameID, want.UserID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want.RaceName, got.RaceName)
|
||||
assert.Equal(t, want.EnginePlayerUUID, got.EnginePlayerUUID)
|
||||
assert.True(t, got.CreatedAt.Equal(now))
|
||||
assert.Equal(t, time.UTC, got.CreatedAt.Location())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBulkInsertEmpty(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
require.NoError(t, store.BulkInsert(ctx, nil))
|
||||
require.NoError(t, store.BulkInsert(ctx, []playermapping.PlayerMapping{}))
|
||||
|
||||
got, err := store.ListByGame(ctx, "game-001")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, got)
|
||||
}
|
||||
|
||||
func TestBulkInsertAtomicConflictRaceName(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
now := time.Date(2026, time.April, 27, 12, 0, 0, 0, time.UTC)
|
||||
// user-2 reuses Aelinari (already taken by user-1) inside the same
|
||||
// game — the unique (game_id, race_name) index must reject the
|
||||
// whole batch.
|
||||
records := []playermapping.PlayerMapping{
|
||||
mapping("game-001", "user-1", "Aelinari", "uuid-1", now),
|
||||
mapping("game-001", "user-2", "Drazi", "uuid-2", now),
|
||||
mapping("game-001", "user-3", "Aelinari", "uuid-3", now),
|
||||
}
|
||||
err := store.BulkInsert(ctx, records)
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, playermapping.ErrConflict), "want ErrConflict, got %v", err)
|
||||
|
||||
got, err := store.ListByGame(ctx, "game-001")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, got, "atomic batch must roll back every row when any row fails")
|
||||
}
|
||||
|
||||
func TestBulkInsertAtomicConflictUserID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
now := time.Date(2026, time.April, 27, 12, 0, 0, 0, time.UTC)
|
||||
records := []playermapping.PlayerMapping{
|
||||
mapping("game-001", "user-1", "Aelinari", "uuid-1", now),
|
||||
mapping("game-001", "user-1", "Drazi", "uuid-2", now), // user-1 twice
|
||||
}
|
||||
err := store.BulkInsert(ctx, records)
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, playermapping.ErrConflict))
|
||||
}
|
||||
|
||||
func TestBulkInsertConflictAcrossCalls(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
now := time.Date(2026, time.April, 27, 12, 0, 0, 0, time.UTC)
|
||||
require.NoError(t, store.BulkInsert(ctx, []playermapping.PlayerMapping{
|
||||
mapping("game-001", "user-1", "Aelinari", "uuid-1", now),
|
||||
}))
|
||||
|
||||
err := store.BulkInsert(ctx, []playermapping.PlayerMapping{
|
||||
mapping("game-001", "user-1", "DifferentRace", "uuid-2", now),
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, playermapping.ErrConflict))
|
||||
}
|
||||
|
||||
func TestBulkInsertRejectsInvalid(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
now := time.Date(2026, time.April, 27, 12, 0, 0, 0, time.UTC)
|
||||
bad := []playermapping.PlayerMapping{
|
||||
mapping("game-001", "user-1", "Aelinari", "uuid-1", now),
|
||||
{GameID: "game-001", UserID: "", RaceName: "Drazi", EnginePlayerUUID: "uuid-2", CreatedAt: now},
|
||||
}
|
||||
err := store.BulkInsert(ctx, bad)
|
||||
require.Error(t, err)
|
||||
require.False(t, errors.Is(err, playermapping.ErrConflict))
|
||||
|
||||
got, err := store.ListByGame(ctx, "game-001")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, got, "validation rejection must not insert any row")
|
||||
}
|
||||
|
||||
func TestGetMissingReturnsNotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
_, err := store.Get(ctx, "game-001", "user-1")
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, playermapping.ErrNotFound))
|
||||
}
|
||||
|
||||
func TestGetByRace(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
now := time.Date(2026, time.April, 27, 12, 0, 0, 0, time.UTC)
|
||||
require.NoError(t, store.BulkInsert(ctx, []playermapping.PlayerMapping{
|
||||
mapping("game-001", "user-1", "Aelinari", "uuid-1", now),
|
||||
mapping("game-001", "user-2", "Drazi", "uuid-2", now),
|
||||
}))
|
||||
|
||||
got, err := store.GetByRace(ctx, "game-001", "Aelinari")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "user-1", got.UserID)
|
||||
|
||||
_, err = store.GetByRace(ctx, "game-001", "Voltori")
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, playermapping.ErrNotFound))
|
||||
}
|
||||
|
||||
func TestListByGameSortedByUserID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
now := time.Date(2026, time.April, 27, 12, 0, 0, 0, time.UTC)
|
||||
require.NoError(t, store.BulkInsert(ctx, []playermapping.PlayerMapping{
|
||||
mapping("game-001", "user-c", "Aelinari", "uuid-1", now),
|
||||
mapping("game-001", "user-a", "Drazi", "uuid-2", now),
|
||||
mapping("game-001", "user-b", "Voltori", "uuid-3", now),
|
||||
// other game's mappings must not leak
|
||||
mapping("game-002", "user-z", "Outsider", "uuid-4", now),
|
||||
}))
|
||||
|
||||
got, err := store.ListByGame(ctx, "game-001")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 3)
|
||||
assert.Equal(t, "user-a", got[0].UserID)
|
||||
assert.Equal(t, "user-b", got[1].UserID)
|
||||
assert.Equal(t, "user-c", got[2].UserID)
|
||||
}
|
||||
|
||||
func TestListByGameUnknown(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
got, err := store.ListByGame(ctx, "unknown-game")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, got)
|
||||
}
|
||||
|
||||
func TestDeleteByGameIdempotent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
now := time.Date(2026, time.April, 27, 12, 0, 0, 0, time.UTC)
|
||||
require.NoError(t, store.BulkInsert(ctx, []playermapping.PlayerMapping{
|
||||
mapping("game-001", "user-1", "Aelinari", "uuid-1", now),
|
||||
mapping("game-001", "user-2", "Drazi", "uuid-2", now),
|
||||
}))
|
||||
|
||||
require.NoError(t, store.DeleteByGame(ctx, "game-001"))
|
||||
got, err := store.ListByGame(ctx, "game-001")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, got)
|
||||
|
||||
// Second call must be a no-op.
|
||||
require.NoError(t, store.DeleteByGame(ctx, "game-001"))
|
||||
}
|
||||
|
||||
func TestGetRejectsEmptyArgs(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
_, err := store.Get(ctx, "", "user-1")
|
||||
require.Error(t, err)
|
||||
_, err = store.Get(ctx, "game-001", "")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGetByRaceRejectsEmptyArgs(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
_, err := store.GetByRace(ctx, "", "Aelinari")
|
||||
require.Error(t, err)
|
||||
_, err = store.GetByRace(ctx, "game-001", "")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestListByGameRejectsEmpty(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
_, err := store.ListByGame(ctx, "")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteByGameRejectsEmpty(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
err := store.DeleteByGame(ctx, "")
|
||||
require.Error(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user