refactor: player - cursor pagination (#5)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline failed Details

Reviewed-on: twhelp/corev3#5
This commit is contained in:
Dawid Wysokiński 2024-02-26 06:44:39 +00:00
parent 3fd654d2ce
commit 084bb5aa85
18 changed files with 930 additions and 268 deletions

View File

@ -89,17 +89,25 @@ func (repo *PlayerBunRepository) CreateOrUpdate(ctx context.Context, params ...d
return nil return nil
} }
func (repo *PlayerBunRepository) List(ctx context.Context, params domain.ListPlayersParams) (domain.Players, error) { func (repo *PlayerBunRepository) List(
ctx context.Context,
params domain.ListPlayersParams,
) (domain.ListPlayersResult, error) {
var players bunmodel.Players var players bunmodel.Players
if err := repo.db.NewSelect(). if err := repo.db.NewSelect().
Model(&players). Model(&players).
Apply(listPlayersParamsApplier{params: params}.apply). Apply(listPlayersParamsApplier{params: params}.apply).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) { Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("couldn't select players from the db: %w", err) return domain.ListPlayersResult{}, fmt.Errorf("couldn't select players from the db: %w", err)
} }
return players.ToDomain() converted, err := players.ToDomain()
if err != nil {
return domain.ListPlayersResult{}, err
}
return domain.NewListPlayersResult(separateListResultAndNext(converted, params.Limit()))
} }
func (repo *PlayerBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error { func (repo *PlayerBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error {
@ -132,10 +140,6 @@ func (a listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
q = q.Where("player.id IN (?)", bun.In(ids)) q = q.Where("player.id IN (?)", bun.In(ids))
} }
if idGT := a.params.IDGT(); idGT.Valid {
q = q.Where("player.id > ?", idGT.Value)
}
if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 { if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 {
q = q.Where("player.server_key IN (?)", bun.In(serverKeys)) q = q.Where("player.server_key IN (?)", bun.In(serverKeys))
} }
@ -163,5 +167,90 @@ func (a listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
} }
} }
return q.Limit(a.params.Limit()).Offset(a.params.Offset()) return q.Limit(a.params.Limit() + 1).Apply(a.applyCursor)
}
//nolint:gocyclo
func (a listPlayersParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuery {
if a.params.Cursor().IsZero() {
return q
}
q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
cursor := a.params.Cursor()
cursorID := cursor.ID()
cursorServerKey := cursor.ServerKey()
sort := a.params.Sort()
sortLen := len(sort)
// based on https://github.com/prisma/prisma/issues/19159#issuecomment-1713389245
switch {
case sortLen == 1:
switch sort[0] {
case domain.PlayerSortIDASC:
q = q.Where("player.id >= ?", cursorID)
case domain.PlayerSortIDDESC:
q = q.Where("player.id <= ?", cursorID)
case domain.PlayerSortServerKeyASC,
domain.PlayerSortServerKeyDESC:
return q.Err(errSortNoUniqueField)
default:
return q.Err(errUnsupportedSortValue)
}
case sortLen > 1:
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery {
for i := 0; i < sortLen; i++ {
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery {
current := sort[i]
for j := 0; j < i; j++ {
s := sort[j]
switch s {
case domain.PlayerSortIDASC,
domain.PlayerSortIDDESC:
q = q.Where("player.id = ?", cursorID)
case domain.PlayerSortServerKeyASC,
domain.PlayerSortServerKeyDESC:
q = q.Where("player.server_key = ?", cursorServerKey)
default:
return q.Err(errUnsupportedSortValue)
}
}
greaterSymbol := bun.Safe(">")
lessSymbol := bun.Safe("<")
if i == sortLen-1 {
greaterSymbol = ">="
lessSymbol = "<="
}
switch current {
case domain.PlayerSortIDASC:
q = q.Where("player.id ? ?", greaterSymbol, cursorID)
case domain.PlayerSortIDDESC:
q = q.Where("player.id ? ?", lessSymbol, cursorID)
case domain.PlayerSortServerKeyASC:
q = q.Where("player.server_key ? ?", greaterSymbol, cursorServerKey)
case domain.PlayerSortServerKeyDESC:
q = q.Where("player.server_key ? ?", lessSymbol, cursorServerKey)
default:
return q.Err(errUnsupportedSortValue)
}
return q
})
}
return q
})
}
return q
})
return q
} }

View File

@ -116,7 +116,7 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit
}) })
}) })
t.Run("List & ListCount", func(t *testing.T) { t.Run("List", func(t *testing.T) {
t.Parallel() t.Parallel()
repos := newRepos(t) repos := newRepos(t)

View File

@ -90,8 +90,9 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo
})) }))
require.NoError(t, listPlayersParams.SetLimit(domain.PlayerSnapshotListMaxLimit/2)) require.NoError(t, listPlayersParams.SetLimit(domain.PlayerSnapshotListMaxLimit/2))
players, err := repos.player.List(ctx, listPlayersParams) res, err := repos.player.List(ctx, listPlayersParams)
require.NoError(t, err) require.NoError(t, err)
players := res.Players()
require.NotEmpty(t, players) require.NotEmpty(t, players)
date := time.Now() date := time.Now()
@ -113,7 +114,7 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo
}) })
}) })
t.Run("List & ListCount", func(t *testing.T) { t.Run("List", func(t *testing.T) {
t.Parallel() t.Parallel()
repos := newRepos(t) repos := newRepos(t)

View File

@ -3,7 +3,6 @@ package adapter_test
import ( import (
"cmp" "cmp"
"context" "context"
"fmt"
"math" "math"
"slices" "slices"
"testing" "testing"
@ -39,8 +38,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
require.NoError(t, listParams.SetIDs(ids)) require.NoError(t, listParams.SetIDs(ids))
require.NoError(t, listParams.SetServerKeys([]string{params[0].ServerKey()})) require.NoError(t, listParams.SetServerKeys([]string{params[0].ServerKey()}))
players, err := repos.player.List(ctx, listParams) res, err := repos.player.List(ctx, listParams)
require.NoError(t, err) require.NoError(t, err)
players := res.Players()
assert.Len(t, players, len(params)) assert.Len(t, players, len(params))
for i, p := range params { for i, p := range params {
idx := slices.IndexFunc(players, func(player domain.Player) bool { idx := slices.IndexFunc(players, func(player domain.Player) bool {
@ -100,22 +100,16 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
}) })
}) })
t.Run("List & ListCount", func(t *testing.T) { t.Run("List", func(t *testing.T) {
t.Parallel() t.Parallel()
repos := newRepos(t) repos := newRepos(t)
players, listPlayersErr := repos.player.List(ctx, domain.NewListPlayersParams())
require.NoError(t, listPlayersErr)
require.NotEmpty(t, players)
randPlayer := players[0]
tests := []struct { tests := []struct {
name string name string
params func(t *testing.T) domain.ListPlayersParams params func(t *testing.T) domain.ListPlayersParams
assertPlayers func(t *testing.T, params domain.ListPlayersParams, players domain.Players) assertResult func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult)
assertError func(t *testing.T, err error) assertError func(t *testing.T, err error)
assertTotal func(t *testing.T, params domain.ListPlayersParams, total int)
}{ }{
{ {
name: "OK: default params", name: "OK: default params",
@ -123,8 +117,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Helper() t.Helper()
return domain.NewListPlayersParams() return domain.NewListPlayersParams()
}, },
assertPlayers: func(t *testing.T, _ domain.ListPlayersParams, players domain.Players) { assertResult: func(t *testing.T, _ domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper() t.Helper()
players := res.Players()
assert.NotEmpty(t, len(players)) assert.NotEmpty(t, len(players))
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int { assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
return cmp.Or( return cmp.Or(
@ -132,15 +127,13 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
cmp.Compare(a.ID(), b.ID()), cmp.Compare(a.ID(), b.ID()),
) )
})) }))
assert.False(t, res.Self().IsZero())
assert.False(t, res.Next().IsZero())
}, },
assertError: func(t *testing.T, err error) { assertError: func(t *testing.T, err error) {
t.Helper() t.Helper()
require.NoError(t, err) require.NoError(t, err)
}, },
assertTotal: func(t *testing.T, _ domain.ListPlayersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: "OK: sort=[serverKey DESC, id DESC]", name: "OK: sort=[serverKey DESC, id DESC]",
@ -150,8 +143,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
require.NoError(t, params.SetSort([]domain.PlayerSort{domain.PlayerSortServerKeyDESC, domain.PlayerSortIDDESC})) require.NoError(t, params.SetSort([]domain.PlayerSort{domain.PlayerSortServerKeyDESC, domain.PlayerSortIDDESC}))
return params return params
}, },
assertPlayers: func(t *testing.T, _ domain.ListPlayersParams, players domain.Players) { assertResult: func(t *testing.T, _ domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper() t.Helper()
players := res.Players()
assert.NotEmpty(t, len(players)) assert.NotEmpty(t, len(players))
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int { assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
return cmp.Or( return cmp.Or(
@ -164,26 +158,32 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Helper() t.Helper()
require.NoError(t, err) require.NoError(t, err)
}, },
assertTotal: func(t *testing.T, _ domain.ListPlayersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: fmt.Sprintf("OK: ids=[%d] serverKeys=[%s]", randPlayer.ID(), randPlayer.ServerKey()), name: "OK: ids serverKeys",
params: func(t *testing.T) domain.ListPlayersParams { params: func(t *testing.T) domain.ListPlayersParams {
t.Helper() t.Helper()
params := domain.NewListPlayersParams() params := domain.NewListPlayersParams()
res, err := repos.player.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, len(res.Players()))
randPlayer := res.Players()[0]
require.NoError(t, params.SetIDs([]int{randPlayer.ID()})) require.NoError(t, params.SetIDs([]int{randPlayer.ID()}))
require.NoError(t, params.SetServerKeys([]string{randPlayer.ServerKey()})) require.NoError(t, params.SetServerKeys([]string{randPlayer.ServerKey()}))
return params return params
}, },
assertPlayers: func(t *testing.T, params domain.ListPlayersParams, players domain.Players) { assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper() t.Helper()
ids := params.IDs() ids := params.IDs()
serverKeys := params.ServerKeys() serverKeys := params.ServerKeys()
players := res.Players()
assert.NotEmpty(t, players)
for _, p := range players { for _, p := range players {
assert.True(t, slices.Contains(ids, p.ID())) assert.True(t, slices.Contains(ids, p.ID()))
assert.True(t, slices.Contains(serverKeys, p.ServerKey())) assert.True(t, slices.Contains(serverKeys, p.ServerKey()))
@ -193,37 +193,6 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Helper() t.Helper()
require.NoError(t, err) require.NoError(t, err)
}, },
assertTotal: func(t *testing.T, _ domain.ListPlayersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: fmt.Sprintf("OK: idGT=%d", randPlayer.ID()),
params: func(t *testing.T) domain.ListPlayersParams {
t.Helper()
params := domain.NewListPlayersParams()
require.NoError(t, params.SetIDGT(domain.NullInt{
Value: randPlayer.ID(),
Valid: true,
}))
return params
},
assertPlayers: func(t *testing.T, params domain.ListPlayersParams, players domain.Players) {
t.Helper()
assert.NotEmpty(t, players)
for _, p := range players {
assert.Greater(t, p.ID(), params.IDGT().Value, p.ID())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListPlayersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: "OK: deleted=true", name: "OK: deleted=true",
@ -236,8 +205,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
})) }))
return params return params
}, },
assertPlayers: func(t *testing.T, _ domain.ListPlayersParams, players domain.Players) { assertResult: func(t *testing.T, _ domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper() t.Helper()
players := res.Players()
assert.NotEmpty(t, players) assert.NotEmpty(t, players)
for _, s := range players { for _, s := range players {
assert.True(t, s.IsDeleted()) assert.True(t, s.IsDeleted())
@ -247,10 +217,6 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Helper() t.Helper()
require.NoError(t, err) require.NoError(t, err)
}, },
assertTotal: func(t *testing.T, _ domain.ListPlayersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: "OK: deleted=false", name: "OK: deleted=false",
@ -263,8 +229,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
})) }))
return params return params
}, },
assertPlayers: func(t *testing.T, _ domain.ListPlayersParams, players domain.Players) { assertResult: func(t *testing.T, _ domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper() t.Helper()
players := res.Players()
assert.NotEmpty(t, players) assert.NotEmpty(t, players)
for _, s := range players { for _, s := range players {
assert.False(t, s.IsDeleted()) assert.False(t, s.IsDeleted())
@ -274,31 +241,145 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Helper() t.Helper()
require.NoError(t, err) require.NoError(t, err)
}, },
assertTotal: func(t *testing.T, _ domain.ListPlayersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: "OK: offset=1 limit=2", name: "OK: cursor serverKeys sort=[id ASC]",
params: func(t *testing.T) domain.ListPlayersParams { params: func(t *testing.T) domain.ListPlayersParams {
t.Helper() t.Helper()
params := domain.NewListPlayersParams() params := domain.NewListPlayersParams()
require.NoError(t, params.SetOffset(1))
require.NoError(t, params.SetLimit(2)) res, err := repos.player.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Players()), 2)
require.NoError(t, params.SetSort([]domain.PlayerSort{domain.PlayerSortIDASC}))
require.NoError(t, params.SetServerKeys([]string{res.Players()[1].ServerKey()}))
require.NoError(t, params.SetCursor(domaintest.NewPlayerCursor(t, func(cfg *domaintest.PlayerCursorConfig) {
cfg.ID = res.Players()[1].ID()
})))
return params return params
}, },
assertPlayers: func(t *testing.T, params domain.ListPlayersParams, players domain.Players) { assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper() t.Helper()
assert.Len(t, players, params.Limit())
serverKeys := params.ServerKeys()
players := res.Players()
assert.NotEmpty(t, len(players))
for _, p := range res.Players() {
assert.GreaterOrEqual(t, p.ID(), params.Cursor().ID())
assert.True(t, slices.Contains(serverKeys, p.ServerKey()))
}
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
return cmp.Compare(a.ID(), b.ID())
}))
}, },
assertError: func(t *testing.T, err error) { assertError: func(t *testing.T, err error) {
t.Helper() t.Helper()
require.NoError(t, err) require.NoError(t, err)
}, },
assertTotal: func(t *testing.T, _ domain.ListPlayersParams, total int) { },
{
name: "OK: cursor sort=[serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListPlayersParams {
t.Helper() t.Helper()
assert.NotEmpty(t, total)
params := domain.NewListPlayersParams()
require.NoError(t, params.SetSort([]domain.PlayerSort{
domain.PlayerSortServerKeyASC,
domain.PlayerSortIDASC,
}))
res, err := repos.player.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Players()), 2)
require.NoError(t, params.SetCursor(domaintest.NewPlayerCursor(t, func(cfg *domaintest.PlayerCursorConfig) {
cfg.ID = res.Players()[1].ID()
cfg.ServerKey = res.Players()[1].ServerKey()
})))
return params
},
assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper()
players := res.Players()
assert.NotEmpty(t, len(players))
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
assert.GreaterOrEqual(t, players[0].ID(), params.Cursor().ID())
for _, p := range res.Players() {
assert.GreaterOrEqual(t, p.ServerKey(), params.Cursor().ServerKey())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: cursor sort=[serverKey DESC, id DESC]",
params: func(t *testing.T) domain.ListPlayersParams {
t.Helper()
params := domain.NewListPlayersParams()
require.NoError(t, params.SetSort([]domain.PlayerSort{
domain.PlayerSortServerKeyDESC,
domain.PlayerSortIDDESC,
}))
res, err := repos.player.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Players()), 2)
require.NoError(t, params.SetCursor(domaintest.NewPlayerCursor(t, func(cfg *domaintest.PlayerCursorConfig) {
cfg.ID = res.Players()[1].ID()
cfg.ServerKey = res.Players()[1].ServerKey()
})))
return params
},
assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper()
players := res.Players()
assert.NotEmpty(t, len(players))
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
) * -1
}))
assert.LessOrEqual(t, players[0].ID(), params.Cursor().ID())
for _, p := range res.Players() {
assert.LessOrEqual(t, p.ServerKey(), params.Cursor().ServerKey())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: limit=2",
params: func(t *testing.T) domain.ListPlayersParams {
t.Helper()
params := domain.NewListPlayersParams()
require.NoError(t, params.SetLimit(2))
return params
},
assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper()
assert.Len(t, res.Players(), params.Limit())
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
}, },
}, },
} }
@ -311,7 +392,7 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
res, err := repos.player.List(ctx, params) res, err := repos.player.List(ctx, params)
tt.assertError(t, err) tt.assertError(t, err)
tt.assertPlayers(t, params, res) tt.assertResult(t, params, res)
}) })
} }
}) })
@ -339,8 +420,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
require.NoError(t, listPlayersParams.SetDeleted(domain.NullBool{Value: false, Valid: true})) require.NoError(t, listPlayersParams.SetDeleted(domain.NullBool{Value: false, Valid: true}))
require.NoError(t, listPlayersParams.SetServerKeys(serverKeys)) require.NoError(t, listPlayersParams.SetServerKeys(serverKeys))
players, err := repos.player.List(ctx, listPlayersParams) res, err := repos.player.List(ctx, listPlayersParams)
require.NoError(t, err) require.NoError(t, err)
players := res.Players()
var serverKey string var serverKey string
var ids []int var ids []int
@ -363,9 +445,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
require.NoError(t, listPlayersParams.SetDeleted(domain.NullBool{Valid: false})) require.NoError(t, listPlayersParams.SetDeleted(domain.NullBool{Valid: false}))
require.NoError(t, listPlayersParams.SetServerKeys(serverKeys)) require.NoError(t, listPlayersParams.SetServerKeys(serverKeys))
players, err = repos.player.List(ctx, listPlayersParams) res, err = repos.player.List(ctx, listPlayersParams)
require.NoError(t, err) require.NoError(t, err)
for _, p := range players { for _, p := range res.Players() {
if p.ServerKey() == serverKey && slices.Contains(ids, p.ID()) { if p.ServerKey() == serverKey && slices.Contains(ids, p.ID()) {
if slices.Contains(idsToDelete, p.ID()) { if slices.Contains(idsToDelete, p.ID()) {
assert.WithinDuration(t, time.Now(), p.DeletedAt(), time.Minute) assert.WithinDuration(t, time.Now(), p.DeletedAt(), time.Minute)

View File

@ -30,7 +30,7 @@ type tribeRepository interface {
type playerRepository interface { type playerRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error
List(ctx context.Context, params domain.ListPlayersParams) (domain.Players, error) List(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersResult, error)
Delete(ctx context.Context, serverKey string, ids ...int) error Delete(ctx context.Context, serverKey string, ids ...int) error
} }

View File

@ -84,8 +84,9 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
t.Run("OK", func(t *testing.T) { t.Run("OK", func(t *testing.T) {
t.Parallel() t.Parallel()
players, err := repos.player.List(ctx, domain.NewListPlayersParams()) res, err := repos.player.List(ctx, domain.NewListPlayersParams())
require.NoError(t, err) require.NoError(t, err)
players := res.Players()
require.NotEmpty(t, players) require.NotEmpty(t, players)
player := players[0] player := players[0]
@ -133,7 +134,7 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
}) })
}) })
t.Run("List & ListCount", func(t *testing.T) { t.Run("List", func(t *testing.T) {
t.Parallel() t.Parallel()
repos := newRepos(t) repos := newRepos(t)

View File

@ -115,7 +115,7 @@ func testTribeSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repos
}) })
}) })
t.Run("List & ListCount", func(t *testing.T) { t.Run("List", func(t *testing.T) {
t.Parallel() t.Parallel()
repos := newRepos(t) repos := newRepos(t)

View File

@ -163,7 +163,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
} }
}) })
t.Run("List & ListCount", func(t *testing.T) { t.Run("List", func(t *testing.T) {
t.Parallel() t.Parallel()
repos := newRepos(t) repos := newRepos(t)

View File

@ -92,7 +92,7 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
}) })
}) })
t.Run("List & ListCount", func(t *testing.T) { t.Run("List", func(t *testing.T) {
t.Parallel() t.Parallel()
repos := newRepos(t) repos := newRepos(t)

View File

@ -9,7 +9,7 @@ import (
type PlayerRepository interface { type PlayerRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error
List(ctx context.Context, params domain.ListPlayersParams) (domain.Players, error) List(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersResult, error)
// Delete marks players with the given serverKey and ids as deleted (sets deleted at to now). // Delete marks players with the given serverKey and ids as deleted (sets deleted at to now).
// In addition, Delete sets TribeID to null. // In addition, Delete sets TribeID to null.
// //
@ -104,10 +104,11 @@ func (svc *PlayerService) createOrUpdateChunk(ctx context.Context, serverKey str
return err return err
} }
storedPlayers, err := svc.repo.List(ctx, listParams) res, err := svc.repo.List(ctx, listParams)
if err != nil { if err != nil {
return err return err
} }
storedPlayers := res.Players()
createParams, err := domain.NewCreatePlayerParams(serverKey, players, storedPlayers) createParams, err := domain.NewCreatePlayerParams(serverKey, players, storedPlayers)
if err != nil { if err != nil {
@ -149,16 +150,12 @@ func (svc *PlayerService) delete(ctx context.Context, serverKey string, players
var tribeChangesParams []domain.CreateTribeChangeParams var tribeChangesParams []domain.CreateTribeChangeParams
for { for {
storedPlayers, err := svc.repo.List(ctx, listParams) res, err := svc.repo.List(ctx, listParams)
if err != nil { if err != nil {
return err return err
} }
if len(storedPlayers) == 0 { ids, params, err := res.Players().Delete(serverKey, players)
break
}
ids, params, err := storedPlayers.Delete(serverKey, players)
if err != nil { if err != nil {
return err return err
} }
@ -166,10 +163,11 @@ func (svc *PlayerService) delete(ctx context.Context, serverKey string, players
toDelete = append(toDelete, ids...) toDelete = append(toDelete, ids...)
tribeChangesParams = append(tribeChangesParams, params...) tribeChangesParams = append(tribeChangesParams, params...)
if err = listParams.SetIDGT(domain.NullInt{ if res.Next().IsZero() {
Value: storedPlayers[len(storedPlayers)-1].ID(), break
Valid: true, }
}); err != nil {
if err = listParams.SetCursor(res.Next()); err != nil {
return err return err
} }
} }
@ -181,6 +179,6 @@ func (svc *PlayerService) delete(ctx context.Context, serverKey string, players
return svc.tribeChangeSvc.Create(ctx, tribeChangesParams...) return svc.tribeChangeSvc.Create(ctx, tribeChangesParams...)
} }
func (svc *PlayerService) List(ctx context.Context, params domain.ListPlayersParams) (domain.Players, error) { func (svc *PlayerService) List(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersResult, error) {
return svc.repo.List(ctx, params) return svc.repo.List(ctx, params)
} }

View File

@ -53,10 +53,11 @@ func (svc *PlayerSnapshotService) Create(
} }
for { for {
players, err := svc.playerSvc.List(ctx, listPlayersParams) res, err := svc.playerSvc.List(ctx, listPlayersParams)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", serverKey, err) return fmt.Errorf("%s: %w", serverKey, err)
} }
players := res.Players()
if len(players) == 0 { if len(players) == 0 {
break break
@ -71,10 +72,11 @@ func (svc *PlayerSnapshotService) Create(
return fmt.Errorf("%s: %w", serverKey, err) return fmt.Errorf("%s: %w", serverKey, err)
} }
if err = listPlayersParams.SetIDGT(domain.NullInt{ if res.Next().IsZero() {
Value: players[len(players)-1].ID(), break
Valid: true, }
}); err != nil {
if err = listPlayersParams.SetCursor(res.Next()); err != nil {
return fmt.Errorf("%s: %w", serverKey, err) return fmt.Errorf("%s: %w", serverKey, err)
} }
} }

View File

@ -1,6 +1,7 @@
package domaintest package domaintest
import ( import (
"math"
"time" "time"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain" "gitea.dwysokinski.me/twhelp/corev3/internal/domain"
@ -8,6 +9,47 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type PlayerCursorConfig struct {
ID int
ServerKey string
ODScoreAtt int
ODScoreDef int
ODScoreTotal int
Points int
DeletedAt time.Time
}
func NewPlayerCursor(tb TestingTB, opts ...func(cfg *PlayerCursorConfig)) domain.PlayerCursor {
tb.Helper()
cfg := &PlayerCursorConfig{
ID: RandID(),
ServerKey: RandServerKey(),
ODScoreAtt: gofakeit.IntRange(0, math.MaxInt),
ODScoreDef: gofakeit.IntRange(0, math.MaxInt),
ODScoreTotal: gofakeit.IntRange(0, math.MaxInt),
Points: gofakeit.IntRange(0, math.MaxInt),
DeletedAt: time.Time{},
}
for _, opt := range opts {
opt(cfg)
}
pc, err := domain.NewPlayerCursor(
cfg.ID,
cfg.ServerKey,
cfg.ODScoreAtt,
cfg.ODScoreDef,
cfg.ODScoreTotal,
cfg.Points,
cfg.DeletedAt,
)
require.NoError(tb, err)
return pc
}
type PlayerConfig struct { type PlayerConfig struct {
ID int ID int
Points int Points int

View File

@ -373,14 +373,199 @@ const (
PlayerSortServerKeyDESC PlayerSortServerKeyDESC
) )
type PlayerCursor struct {
id int
serverKey string
odScoreAtt int
odScoreDef int
odScoreTotal int
points int
deletedAt time.Time
}
const playerCursorModelName = "PlayerCursor"
func NewPlayerCursor(
id int,
serverKey string,
odScoreAtt int,
odScoreDef int,
odScoreTotal int,
points int,
deletedAt time.Time,
) (PlayerCursor, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return PlayerCursor{}, ValidationError{
Model: playerCursorModelName,
Field: "id",
Err: err,
}
}
if err := validateServerKey(serverKey); err != nil {
return PlayerCursor{}, ValidationError{
Model: playerCursorModelName,
Field: "serverKey",
Err: err,
}
}
if err := validateIntInRange(odScoreAtt, 0, math.MaxInt); err != nil {
return PlayerCursor{}, ValidationError{
Model: playerCursorModelName,
Field: "odScoreAtt",
Err: err,
}
}
if err := validateIntInRange(odScoreDef, 0, math.MaxInt); err != nil {
return PlayerCursor{}, ValidationError{
Model: playerCursorModelName,
Field: "odScoreDef",
Err: err,
}
}
if err := validateIntInRange(odScoreTotal, 0, math.MaxInt); err != nil {
return PlayerCursor{}, ValidationError{
Model: playerCursorModelName,
Field: "odScoreTotal",
Err: err,
}
}
if err := validateIntInRange(points, 0, math.MaxInt); err != nil {
return PlayerCursor{}, ValidationError{
Model: playerCursorModelName,
Field: "points",
Err: err,
}
}
return PlayerCursor{
id: id,
serverKey: serverKey,
odScoreAtt: odScoreAtt,
odScoreDef: odScoreDef,
odScoreTotal: odScoreTotal,
points: points,
deletedAt: deletedAt,
}, nil
}
//nolint:gocyclo
func decodePlayerCursor(encoded string) (PlayerCursor, error) {
m, err := decodeCursor(encoded)
if err != nil {
return PlayerCursor{}, err
}
id, err := m.int("id")
if err != nil {
return PlayerCursor{}, ErrInvalidCursor
}
serverKey, err := m.string("serverKey")
if err != nil {
return PlayerCursor{}, ErrInvalidCursor
}
odScoreAtt, err := m.int("odScoreAtt")
if err != nil {
return PlayerCursor{}, ErrInvalidCursor
}
odScoreDef, err := m.int("odScoreDef")
if err != nil {
return PlayerCursor{}, ErrInvalidCursor
}
odScoreTotal, err := m.int("odScoreTotal")
if err != nil {
return PlayerCursor{}, ErrInvalidCursor
}
points, err := m.int("points")
if err != nil {
return PlayerCursor{}, ErrInvalidCursor
}
deletedAt, err := m.time("deletedAt")
if err != nil {
return PlayerCursor{}, ErrInvalidCursor
}
pc, err := NewPlayerCursor(
id,
serverKey,
odScoreAtt,
odScoreDef,
odScoreTotal,
points,
deletedAt,
)
if err != nil {
return PlayerCursor{}, ErrInvalidCursor
}
return pc, nil
}
func (pc PlayerCursor) ID() int {
return pc.id
}
func (pc PlayerCursor) ServerKey() string {
return pc.serverKey
}
func (pc PlayerCursor) ODScoreAtt() int {
return pc.odScoreAtt
}
func (pc PlayerCursor) ODScoreDef() int {
return pc.odScoreDef
}
func (pc PlayerCursor) ODScoreTotal() int {
return pc.odScoreTotal
}
func (pc PlayerCursor) Points() int {
return pc.points
}
func (pc PlayerCursor) DeletedAt() time.Time {
return pc.deletedAt
}
func (pc PlayerCursor) IsZero() bool {
return pc == PlayerCursor{}
}
func (pc PlayerCursor) Encode() string {
if pc.IsZero() {
return ""
}
return encodeCursor([]keyValuePair{
{"id", pc.id},
{"serverKey", pc.serverKey},
{"odScoreAtt", pc.odScoreAtt},
{"odScoreDef", pc.odScoreDef},
{"odScoreTotal", pc.odScoreTotal},
{"points", pc.points},
{"deletedAt", pc.deletedAt},
})
}
type ListPlayersParams struct { type ListPlayersParams struct {
ids []int ids []int
idGT NullInt
serverKeys []string serverKeys []string
deleted NullBool deleted NullBool
sort []PlayerSort sort []PlayerSort
cursor PlayerCursor
limit int limit int
offset int
} }
const ( const (
@ -419,26 +604,6 @@ func (params *ListPlayersParams) SetIDs(ids []int) error {
return nil return nil
} }
func (params *ListPlayersParams) IDGT() NullInt {
return params.idGT
}
func (params *ListPlayersParams) SetIDGT(idGT NullInt) error {
if idGT.Valid {
if err := validateIntInRange(idGT.Value, 0, math.MaxInt); err != nil {
return ValidationError{
Model: listPlayersParamsModelName,
Field: "idGT",
Err: err,
}
}
}
params.idGT = idGT
return nil
}
func (params *ListPlayersParams) ServerKeys() []string { func (params *ListPlayersParams) ServerKeys() []string {
return params.serverKeys return params.serverKeys
} }
@ -480,6 +645,30 @@ func (params *ListPlayersParams) SetSort(sort []PlayerSort) error {
return nil return nil
} }
func (params *ListPlayersParams) Cursor() PlayerCursor {
return params.cursor
}
func (params *ListPlayersParams) SetCursor(cursor PlayerCursor) error {
params.cursor = cursor
return nil
}
func (params *ListPlayersParams) SetEncodedCursor(encoded string) error {
decoded, err := decodePlayerCursor(encoded)
if err != nil {
return ValidationError{
Model: listPlayersParamsModelName,
Field: "cursor",
Err: err,
}
}
params.cursor = decoded
return nil
}
func (params *ListPlayersParams) Limit() int { func (params *ListPlayersParams) Limit() int {
return params.limit return params.limit
} }
@ -498,20 +687,71 @@ func (params *ListPlayersParams) SetLimit(limit int) error {
return nil return nil
} }
func (params *ListPlayersParams) Offset() int { type ListPlayersResult struct {
return params.offset players Players
self PlayerCursor
next PlayerCursor
} }
func (params *ListPlayersParams) SetOffset(offset int) error { const listPlayersResultModelName = "ListPlayersResult"
if err := validateIntInRange(offset, 0, math.MaxInt); err != nil {
return ValidationError{ func NewListPlayersResult(players Players, next Player) (ListPlayersResult, error) {
Model: listPlayersParamsModelName, var err error
Field: "offset", res := ListPlayersResult{
Err: err, players: players,
}
if len(players) > 0 {
od := players[0].OD()
res.self, err = NewPlayerCursor(
players[0].ID(),
players[0].ServerKey(),
od.ScoreAtt(),
od.ScoreDef(),
od.ScoreTotal(),
players[0].Points(),
players[0].DeletedAt(),
)
if err != nil {
return ListPlayersResult{}, ValidationError{
Model: listPlayersResultModelName,
Field: "self",
Err: err,
}
} }
} }
params.offset = offset if !next.IsZero() {
od := next.OD()
res.next, err = NewPlayerCursor(
next.ID(),
next.ServerKey(),
od.ScoreAtt(),
od.ScoreDef(),
od.ScoreTotal(),
next.Points(),
next.DeletedAt(),
)
if err != nil {
return ListPlayersResult{}, ValidationError{
Model: listPlayersResultModelName,
Field: "next",
Err: err,
}
}
}
return nil return res, nil
}
func (res ListPlayersResult) Players() Players {
return res.players
}
func (res ListPlayersResult) Self() PlayerCursor {
return res.self
}
func (res ListPlayersResult) Next() PlayerCursor {
return res.next
} }

View File

@ -9,6 +9,7 @@ import (
"gitea.dwysokinski.me/twhelp/corev3/internal/domain" "gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest" "gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -227,6 +228,192 @@ func TestNewCreatePlayerParams(t *testing.T) {
} }
} }
func TestNewPlayerCursor(t *testing.T) {
t.Parallel()
validPlayerCursor := domaintest.NewPlayerCursor(t)
type args struct {
id int
serverKey string
odScoreAtt int
odScoreDef int
odScoreTotal int
points int
deletedAt time.Time
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
id: validPlayerCursor.ID(),
serverKey: validPlayerCursor.ServerKey(),
odScoreAtt: validPlayerCursor.ODScoreAtt(),
odScoreDef: validPlayerCursor.ODScoreDef(),
odScoreTotal: validPlayerCursor.ODScoreTotal(),
points: validPlayerCursor.Points(),
deletedAt: validPlayerCursor.DeletedAt(),
},
expectedErr: nil,
},
{
name: "ERR: id < 1",
args: args{
id: 0,
serverKey: validPlayerCursor.ServerKey(),
odScoreAtt: validPlayerCursor.ODScoreAtt(),
odScoreDef: validPlayerCursor.ODScoreDef(),
odScoreTotal: validPlayerCursor.ODScoreTotal(),
points: validPlayerCursor.Points(),
deletedAt: validPlayerCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "PlayerCursor",
Field: "id",
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
{
name: "ERR: odScoreAtt < 0",
args: args{
id: validPlayerCursor.ID(),
serverKey: validPlayerCursor.ServerKey(),
odScoreAtt: -1,
odScoreDef: validPlayerCursor.ODScoreDef(),
odScoreTotal: validPlayerCursor.ODScoreTotal(),
points: validPlayerCursor.Points(),
deletedAt: validPlayerCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "PlayerCursor",
Field: "odScoreAtt",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: odScoreDef < 0",
args: args{
id: validPlayerCursor.ID(),
serverKey: validPlayerCursor.ServerKey(),
odScoreAtt: validPlayerCursor.ODScoreAtt(),
odScoreDef: -1,
odScoreTotal: validPlayerCursor.ODScoreTotal(),
points: validPlayerCursor.Points(),
deletedAt: validPlayerCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "PlayerCursor",
Field: "odScoreDef",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: odScoreTotal < 0",
args: args{
id: validPlayerCursor.ID(),
serverKey: validPlayerCursor.ServerKey(),
odScoreAtt: validPlayerCursor.ODScoreAtt(),
odScoreDef: validPlayerCursor.ODScoreDef(),
odScoreTotal: -1,
points: validPlayerCursor.Points(),
deletedAt: validPlayerCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "PlayerCursor",
Field: "odScoreTotal",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: points < 0",
args: args{
id: validPlayerCursor.ID(),
serverKey: validPlayerCursor.ServerKey(),
odScoreAtt: validPlayerCursor.ODScoreAtt(),
odScoreDef: validPlayerCursor.ODScoreDef(),
odScoreTotal: validPlayerCursor.ODScoreTotal(),
points: -1,
deletedAt: validPlayerCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "PlayerCursor",
Field: "points",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
id: validPlayerCursor.ID(),
serverKey: serverKeyTest.key,
odScoreAtt: validPlayerCursor.ODScoreAtt(),
odScoreDef: validPlayerCursor.ODScoreDef(),
odScoreTotal: validPlayerCursor.ODScoreTotal(),
points: validPlayerCursor.Points(),
deletedAt: validPlayerCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "PlayerCursor",
Field: "serverKey",
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tc, err := domain.NewPlayerCursor(
tt.args.id,
tt.args.serverKey,
tt.args.odScoreAtt,
tt.args.odScoreDef,
tt.args.odScoreTotal,
tt.args.points,
tt.args.deletedAt,
)
require.ErrorIs(t, err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.id, tc.ID())
assert.Equal(t, tt.args.serverKey, tc.ServerKey())
assert.Equal(t, tt.args.odScoreAtt, tc.ODScoreAtt())
assert.Equal(t, tt.args.odScoreDef, tc.ODScoreDef())
assert.Equal(t, tt.args.odScoreTotal, tc.ODScoreTotal())
assert.Equal(t, tt.args.points, tc.Points())
assert.Equal(t, tt.args.deletedAt, tc.DeletedAt())
assert.NotEmpty(t, tc.Encode())
})
}
}
func TestListPlayersParams_SetIDs(t *testing.T) { func TestListPlayersParams_SetIDs(t *testing.T) {
t.Parallel() t.Parallel()
@ -287,61 +474,6 @@ func TestListPlayersParams_SetIDs(t *testing.T) {
} }
} }
func TestListPlayersParams_SetIDGT(t *testing.T) {
t.Parallel()
type args struct {
idGT domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
idGT: domain.NullInt{
Value: domaintest.RandID(),
Valid: true,
},
},
},
{
name: "ERR: value < 0",
args: args{
idGT: domain.NullInt{
Value: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "ListPlayersParams",
Field: "idGT",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListPlayersParams()
require.ErrorIs(t, params.SetIDGT(tt.args.idGT), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.idGT, params.IDGT())
})
}
}
func TestListPlayersParams_SetSort(t *testing.T) { func TestListPlayersParams_SetSort(t *testing.T) {
t.Parallel() t.Parallel()
@ -414,6 +546,87 @@ func TestListPlayersParams_SetSort(t *testing.T) {
} }
} }
func TestListPlayersParams_SetEncodedCursor(t *testing.T) {
t.Parallel()
validCursor := domaintest.NewPlayerCursor(t)
type args struct {
cursor string
}
tests := []struct {
name string
args args
expectedCursor domain.PlayerCursor
expectedErr error
}{
{
name: "OK",
args: args{
cursor: validCursor.Encode(),
},
expectedCursor: validCursor,
},
{
name: "ERR: len(cursor) < 1",
args: args{
cursor: "",
},
expectedErr: domain.ValidationError{
Model: "ListPlayersParams",
Field: "cursor",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: 0,
},
},
},
{
name: "ERR: len(cursor) > 1000",
args: args{
cursor: gofakeit.LetterN(1001),
},
expectedErr: domain.ValidationError{
Model: "ListPlayersParams",
Field: "cursor",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: 1001,
},
},
},
{
name: "ERR: malformed base64",
args: args{
cursor: "112345",
},
expectedErr: domain.ValidationError{
Model: "ListPlayersParams",
Field: "cursor",
Err: domain.ErrInvalidCursor,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListPlayersParams()
require.ErrorIs(t, params.SetEncodedCursor(tt.args.cursor), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.expectedCursor, params.Cursor())
assert.Equal(t, tt.args.cursor, params.Cursor().Encode())
})
}
}
func TestListPlayersParams_SetLimit(t *testing.T) { func TestListPlayersParams_SetLimit(t *testing.T) {
t.Parallel() t.Parallel()
@ -477,51 +690,46 @@ func TestListPlayersParams_SetLimit(t *testing.T) {
} }
} }
func TestListPlayersParams_SetOffset(t *testing.T) { func TestNewListPlayersResult(t *testing.T) {
t.Parallel() t.Parallel()
type args struct { players := domain.Players{
offset int domaintest.NewPlayer(t),
domaintest.NewPlayer(t),
domaintest.NewPlayer(t),
} }
next := domaintest.NewPlayer(t)
tests := []struct { t.Run("OK: with next", func(t *testing.T) {
name string t.Parallel()
args args
expectedErr error
}{
{
name: "OK",
args: args{
offset: 100,
},
},
{
name: "ERR: offset < 0",
args: args{
offset: -1,
},
expectedErr: domain.ValidationError{
Model: "ListPlayersParams",
Field: "offset",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests { res, err := domain.NewListPlayersResult(players, next)
t.Run(tt.name, func(t *testing.T) { require.NoError(t, err)
t.Parallel() assert.Equal(t, players, res.Players())
assert.Equal(t, players[0].ID(), res.Self().ID())
assert.Equal(t, players[0].ServerKey(), res.Self().ServerKey())
assert.Equal(t, next.ID(), res.Next().ID())
assert.Equal(t, next.ServerKey(), res.Next().ServerKey())
})
params := domain.NewListPlayersParams() t.Run("OK: without next", func(t *testing.T) {
t.Parallel()
require.ErrorIs(t, params.SetOffset(tt.args.offset), tt.expectedErr) res, err := domain.NewListPlayersResult(players, domain.Player{})
if tt.expectedErr != nil { require.NoError(t, err)
return assert.Equal(t, players, res.Players())
} assert.Equal(t, players[0].ID(), res.Self().ID())
assert.Equal(t, tt.args.offset, params.Offset()) assert.Equal(t, players[0].ServerKey(), res.Self().ServerKey())
}) assert.True(t, res.Next().IsZero())
} })
t.Run("OK: 0 players", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListPlayersResult(nil, domain.Player{})
require.NoError(t, err)
assert.Zero(t, res.Players())
assert.True(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
})
} }

View File

@ -489,7 +489,7 @@ func TestNewListServersResult(t *testing.T) {
assert.True(t, res.Next().IsZero()) assert.True(t, res.Next().IsZero())
}) })
t.Run("OK: 0 versions", func(t *testing.T) { t.Run("OK: 0 servers", func(t *testing.T) {
t.Parallel() t.Parallel()
res, err := domain.NewListServersResult(nil, domain.Server{}) res, err := domain.NewListServersResult(nil, domain.Server{})

View File

@ -365,6 +365,12 @@ func TestNewTribeCursor(t *testing.T) {
} }
assert.Equal(t, tt.args.id, tc.ID()) assert.Equal(t, tt.args.id, tc.ID())
assert.Equal(t, tt.args.serverKey, tc.ServerKey()) assert.Equal(t, tt.args.serverKey, tc.ServerKey())
assert.Equal(t, tt.args.odScoreAtt, tc.ODScoreAtt())
assert.Equal(t, tt.args.odScoreDef, tc.ODScoreDef())
assert.Equal(t, tt.args.odScoreTotal, tc.ODScoreTotal())
assert.Equal(t, tt.args.points, tc.Points())
assert.InDelta(t, tt.args.dominance, tc.Dominance(), 0.001)
assert.Equal(t, tt.args.deletedAt, tc.DeletedAt())
assert.NotEmpty(t, tc.Encode()) assert.NotEmpty(t, tc.Encode())
}) })
} }
@ -835,8 +841,7 @@ func TestListTribesParams_SetEncodedCursor(t *testing.T) {
if tt.expectedErr != nil { if tt.expectedErr != nil {
return return
} }
assert.Equal(t, tt.expectedCursor.ID(), params.Cursor().ID()) assert.Equal(t, tt.expectedCursor, params.Cursor())
assert.Equal(t, tt.expectedCursor.ServerKey(), params.Cursor().ServerKey())
assert.Equal(t, tt.args.cursor, params.Cursor().Encode()) assert.Equal(t, tt.args.cursor, params.Cursor().Encode())
}) })
} }
@ -908,7 +913,7 @@ func TestListTribesParams_SetLimit(t *testing.T) {
func TestNewListTribesResult(t *testing.T) { func TestNewListTribesResult(t *testing.T) {
t.Parallel() t.Parallel()
servers := domain.Tribes{ tribes := domain.Tribes{
domaintest.NewTribe(t), domaintest.NewTribe(t),
domaintest.NewTribe(t), domaintest.NewTribe(t),
domaintest.NewTribe(t), domaintest.NewTribe(t),
@ -918,11 +923,11 @@ func TestNewListTribesResult(t *testing.T) {
t.Run("OK: with next", func(t *testing.T) { t.Run("OK: with next", func(t *testing.T) {
t.Parallel() t.Parallel()
res, err := domain.NewListTribesResult(servers, next) res, err := domain.NewListTribesResult(tribes, next)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, servers, res.Tribes()) assert.Equal(t, tribes, res.Tribes())
assert.Equal(t, servers[0].ID(), res.Self().ID()) assert.Equal(t, tribes[0].ID(), res.Self().ID())
assert.Equal(t, servers[0].ServerKey(), res.Self().ServerKey()) assert.Equal(t, tribes[0].ServerKey(), res.Self().ServerKey())
assert.Equal(t, next.ID(), res.Next().ID()) assert.Equal(t, next.ID(), res.Next().ID())
assert.Equal(t, next.ServerKey(), res.Next().ServerKey()) assert.Equal(t, next.ServerKey(), res.Next().ServerKey())
}) })
@ -930,15 +935,15 @@ func TestNewListTribesResult(t *testing.T) {
t.Run("OK: without next", func(t *testing.T) { t.Run("OK: without next", func(t *testing.T) {
t.Parallel() t.Parallel()
res, err := domain.NewListTribesResult(servers, domain.Tribe{}) res, err := domain.NewListTribesResult(tribes, domain.Tribe{})
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, servers, res.Tribes()) assert.Equal(t, tribes, res.Tribes())
assert.Equal(t, servers[0].ID(), res.Self().ID()) assert.Equal(t, tribes[0].ID(), res.Self().ID())
assert.Equal(t, servers[0].ServerKey(), res.Self().ServerKey()) assert.Equal(t, tribes[0].ServerKey(), res.Self().ServerKey())
assert.True(t, res.Next().IsZero()) assert.True(t, res.Next().IsZero())
}) })
t.Run("OK: 0 versions", func(t *testing.T) { t.Run("OK: 0 tribes", func(t *testing.T) {
t.Parallel() t.Parallel()
res, err := domain.NewListTribesResult(nil, domain.Tribe{}) res, err := domain.NewListTribesResult(nil, domain.Tribe{})

View File

@ -357,19 +357,16 @@ func TestDataSync(t *testing.T) {
allPlayers := make(domain.Players, 0, len(expectedPlayers)) allPlayers := make(domain.Players, 0, len(expectedPlayers))
for { for {
players, err := playerRepo.List(ctx, listParams) res, err := playerRepo.List(ctx, listParams)
require.NoError(collect, err) require.NoError(collect, err)
if len(players) == 0 { allPlayers = append(allPlayers, res.Players()...)
if res.Next().IsZero() {
break break
} }
allPlayers = append(allPlayers, players...) require.NoError(collect, listParams.SetCursor(res.Next()))
require.NoError(collect, listParams.SetIDGT(domain.NullInt{
Value: players[len(players)-1].ID(),
Valid: true,
}))
} }
if !assert.Len(collect, allPlayers, len(expectedPlayers)) { if !assert.Len(collect, allPlayers, len(expectedPlayers)) {

View File

@ -276,19 +276,16 @@ func TestSnapshotCreation(t *testing.T) {
var allPlayers domain.Players var allPlayers domain.Players
for { for {
players, err := playerRepo.List(ctx, listPlayersParams) res, err := playerRepo.List(ctx, listPlayersParams)
require.NoError(collect, err) require.NoError(collect, err)
if len(players) == 0 { allPlayers = append(allPlayers, res.Players()...)
if res.Next().IsZero() {
break break
} }
allPlayers = append(allPlayers, players...) require.NoError(collect, listPlayersParams.SetCursor(res.Next()))
require.NoError(collect, listPlayersParams.SetIDGT(domain.NullInt{
Value: players[len(players)-1].ID(),
Valid: true,
}))
} }
listSnapshotsParams := domain.NewListPlayerSnapshotsParams() listSnapshotsParams := domain.NewListPlayerSnapshotsParams()