refactor: tribe snapshot - cursor pagination
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details

This commit is contained in:
Dawid Wysokiński 2024-03-16 08:19:30 +01:00
parent 41cb1d042e
commit 5fa98ce0b6
Signed by: Kichiyaki
GPG Key ID: B5445E357FB8B892
9 changed files with 969 additions and 91 deletions

View File

@ -58,17 +58,22 @@ func (repo *TribeSnapshotBunRepository) Create(ctx context.Context, params ...do
func (repo *TribeSnapshotBunRepository) List(
ctx context.Context,
params domain.ListTribeSnapshotsParams,
) (domain.TribeSnapshots, error) {
) (domain.ListTribeSnapshotsResult, error) {
var tribeSnapshots bunmodel.TribeSnapshots
if err := repo.db.NewSelect().
Model(&tribeSnapshots).
Apply(listTribeSnapshotsParamsApplier{params: params}.apply).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("couldn't select tribe snapshots from the db: %w", err)
return domain.ListTribeSnapshotsResult{}, fmt.Errorf("couldn't select tribe snapshots from the db: %w", err)
}
return tribeSnapshots.ToDomain()
converted, err := tribeSnapshots.ToDomain()
if err != nil {
return domain.ListTribeSnapshotsResult{}, err
}
return domain.NewListTribeSnapshotsResult(separateListResultAndNext(converted, params.Limit()))
}
type listTribeSnapshotsParamsApplier struct {
@ -80,6 +85,10 @@ func (a listTribeSnapshotsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQu
q = q.Where("ts.server_key IN (?)", bun.In(serverKeys))
}
if tribeIDs := a.params.TribeIDs(); len(tribeIDs) > 0 {
q = q.Where("ts.tribe_id IN (?)", bun.In(tribeIDs))
}
for _, s := range a.params.Sort() {
column, dir, err := a.sortToColumnAndDirection(s)
if err != nil {
@ -89,7 +98,49 @@ func (a listTribeSnapshotsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQu
q.OrderExpr("? ?", column, dir.Bun())
}
return q.Limit(a.params.Limit()).Offset(a.params.Offset())
return q.Limit(a.params.Limit() + 1).Apply(a.applyCursor)
}
func (a listTribeSnapshotsParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuery {
cursor := a.params.Cursor()
if cursor.IsZero() {
return q
}
sort := a.params.Sort()
cursorApplier := cursorPaginationApplier{
data: make([]cursorPaginationApplierDataElement, 0, len(sort)),
}
for _, s := range sort {
var err error
var el cursorPaginationApplierDataElement
el.column, el.direction, err = a.sortToColumnAndDirection(s)
if err != nil {
return q.Err(err)
}
switch s {
case domain.TribeSnapshotSortIDASC,
domain.TribeSnapshotSortIDDESC:
el.value = cursor.ID()
el.unique = true
case domain.TribeSnapshotSortServerKeyASC,
domain.TribeSnapshotSortServerKeyDESC:
el.value = cursor.ServerKey()
case domain.TribeSnapshotSortDateASC,
domain.TribeSnapshotSortDateDESC:
el.value = cursor.Date()
default:
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
cursorApplier.data = append(cursorApplier.data, el)
}
return q.Apply(cursorApplier.apply)
}
func (a listTribeSnapshotsParamsApplier) sortToColumnAndDirection(

View File

@ -296,6 +296,115 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo
}
},
},
{
name: "OK: cursor serverKeys sort=[id ASC]",
params: func(t *testing.T) domain.ListPlayerSnapshotsParams {
t.Helper()
params := domain.NewListPlayerSnapshotsParams()
res, err := repos.playerSnapshot.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.PlayerSnapshots()), 2)
require.NoError(t, params.SetSort([]domain.PlayerSnapshotSort{domain.PlayerSnapshotSortIDASC}))
require.NoError(t, params.SetServerKeys([]string{res.PlayerSnapshots()[1].ServerKey()}))
cursor, err := res.PlayerSnapshots()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
assertResult: func(t *testing.T, params domain.ListPlayerSnapshotsParams, res domain.ListPlayerSnapshotsResult) {
t.Helper()
serverKeys := params.ServerKeys()
playerSnapshots := res.PlayerSnapshots()
assert.NotEmpty(t, len(playerSnapshots))
for _, ps := range playerSnapshots {
assert.GreaterOrEqual(t, ps.ID(), params.Cursor().ID())
assert.True(t, slices.Contains(serverKeys, ps.ServerKey()))
}
assert.True(t, slices.IsSortedFunc(playerSnapshots, func(a, b domain.PlayerSnapshot) int {
return cmp.Compare(a.ID(), b.ID())
}))
},
},
{
name: "OK: cursor sort=[serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListPlayerSnapshotsParams {
t.Helper()
params := domain.NewListPlayerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.PlayerSnapshotSort{
domain.PlayerSnapshotSortServerKeyASC,
domain.PlayerSnapshotSortIDASC,
}))
res, err := repos.playerSnapshot.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.PlayerSnapshots()), 2)
cursor, err := res.PlayerSnapshots()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
assertResult: func(t *testing.T, params domain.ListPlayerSnapshotsParams, res domain.ListPlayerSnapshotsResult) {
t.Helper()
playerSnapshots := res.PlayerSnapshots()
assert.NotEmpty(t, len(playerSnapshots))
assert.True(t, slices.IsSortedFunc(playerSnapshots, func(a, b domain.PlayerSnapshot) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
assert.GreaterOrEqual(t, playerSnapshots[0].ID(), params.Cursor().ID())
for _, ps := range playerSnapshots {
assert.GreaterOrEqual(t, ps.ServerKey(), params.Cursor().ServerKey())
}
},
},
{
name: "OK: cursor sort=[serverKey DESC, id DESC]",
params: func(t *testing.T) domain.ListPlayerSnapshotsParams {
t.Helper()
params := domain.NewListPlayerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.PlayerSnapshotSort{
domain.PlayerSnapshotSortServerKeyDESC,
domain.PlayerSnapshotSortIDDESC,
}))
res, err := repos.playerSnapshot.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.PlayerSnapshots()), 2)
cursor, err := res.PlayerSnapshots()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
assertResult: func(t *testing.T, params domain.ListPlayerSnapshotsParams, res domain.ListPlayerSnapshotsResult) {
t.Helper()
playerSnapshots := res.PlayerSnapshots()
assert.NotEmpty(t, len(playerSnapshots))
assert.True(t, slices.IsSortedFunc(playerSnapshots, func(a, b domain.PlayerSnapshot) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
) * -1
}))
assert.LessOrEqual(t, playerSnapshots[0].ID(), params.Cursor().ID())
for _, ps := range playerSnapshots {
assert.LessOrEqual(t, ps.ServerKey(), params.Cursor().ServerKey())
}
},
},
{
name: "OK: limit=2",
params: func(t *testing.T) domain.ListPlayerSnapshotsParams {

View File

@ -65,7 +65,7 @@ type tribeChangeRepository interface {
type tribeSnapshotRepository interface {
Create(ctx context.Context, params ...domain.CreateTribeSnapshotParams) error
List(ctx context.Context, params domain.ListTribeSnapshotsParams) (domain.TribeSnapshots, error)
List(ctx context.Context, params domain.ListTribeSnapshotsParams) (domain.ListTribeSnapshotsResult, error)
}
type playerSnapshotRepository interface {

View File

@ -30,7 +30,8 @@ func testTribeSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repos
require.NotEmpty(t, params)
tribeSnapshots, err := repos.tribeSnapshot.List(ctx, domain.NewListTribeSnapshotsParams())
res, err := repos.tribeSnapshot.List(ctx, domain.NewListTribeSnapshotsParams())
tribeSnapshots := res.TribeSnapshots()
require.NoError(t, err)
for i, p := range params {
date := p.Date().Format(dateFormat)
@ -62,8 +63,9 @@ func testTribeSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repos
require.NotEmpty(t, params)
tribeSnapshots, err := repos.tribeSnapshot.List(ctx, domain.NewListTribeSnapshotsParams())
res, err := repos.tribeSnapshot.List(ctx, domain.NewListTribeSnapshotsParams())
require.NoError(t, err)
tribeSnapshots := res.TribeSnapshots()
m := make(map[string][]int)
@ -120,16 +122,11 @@ func testTribeSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repos
repos := newRepos(t)
tribeSnapshots, listTribeSnapshotsErr := repos.tribeSnapshot.List(ctx, domain.NewListTribeSnapshotsParams())
require.NoError(t, listTribeSnapshotsErr)
require.NotEmpty(t, tribeSnapshots)
randTribeSnapshot := tribeSnapshots[0]
tests := []struct {
name string
params func(t *testing.T) domain.ListTribeSnapshotsParams
assertTribeSnapshots func(t *testing.T, params domain.ListTribeSnapshotsParams, tribeSnapshots domain.TribeSnapshots)
assertError func(t *testing.T, err error)
name string
params func(t *testing.T) domain.ListTribeSnapshotsParams
assertResult func(t *testing.T, params domain.ListTribeSnapshotsParams, res domain.ListTribeSnapshotsResult)
assertError func(t *testing.T, err error)
}{
{
name: "OK: default params",
@ -137,12 +134,13 @@ func testTribeSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repos
t.Helper()
return domain.NewListTribeSnapshotsParams()
},
assertTribeSnapshots: func(
assertResult: func(
t *testing.T,
_ domain.ListTribeSnapshotsParams,
tribeSnapshots domain.TribeSnapshots,
res domain.ListTribeSnapshotsResult,
) {
t.Helper()
tribeSnapshots := res.TribeSnapshots()
assert.NotEmpty(t, len(tribeSnapshots))
assert.True(t, slices.IsSortedFunc(tribeSnapshots, func(a, b domain.TribeSnapshot) int {
return cmp.Or(
@ -151,6 +149,8 @@ func testTribeSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repos
cmp.Compare(a.ID(), b.ID()),
)
}))
assert.False(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
},
},
{
@ -164,12 +164,13 @@ func testTribeSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repos
}))
return params
},
assertTribeSnapshots: func(
assertResult: func(
t *testing.T,
_ domain.ListTribeSnapshotsParams,
tribeSnapshots domain.TribeSnapshots,
res domain.ListTribeSnapshotsResult,
) {
t.Helper()
tribeSnapshots := res.TribeSnapshots()
assert.NotEmpty(t, len(tribeSnapshots))
assert.True(t, slices.IsSortedFunc(tribeSnapshots, func(a, b domain.TribeSnapshot) int {
return cmp.Or(
@ -189,12 +190,13 @@ func testTribeSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repos
}))
return params
},
assertTribeSnapshots: func(
assertResult: func(
t *testing.T,
_ domain.ListTribeSnapshotsParams,
tribeSnapshots domain.TribeSnapshots,
res domain.ListTribeSnapshotsResult,
) {
t.Helper()
tribeSnapshots := res.TribeSnapshots()
assert.NotEmpty(t, len(tribeSnapshots))
assert.True(t, slices.IsSortedFunc(tribeSnapshots, func(a, b domain.TribeSnapshot) int {
return cmp.Compare(a.ID(), b.ID())
@ -211,12 +213,13 @@ func testTribeSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repos
}))
return params
},
assertTribeSnapshots: func(
assertResult: func(
t *testing.T,
_ domain.ListTribeSnapshotsParams,
tribeSnapshots domain.TribeSnapshots,
res domain.ListTribeSnapshotsResult,
) {
t.Helper()
tribeSnapshots := res.TribeSnapshots()
assert.NotEmpty(t, len(tribeSnapshots))
assert.True(t, slices.IsSortedFunc(tribeSnapshots, func(a, b domain.TribeSnapshot) int {
return cmp.Compare(a.ID(), b.ID()) * -1
@ -224,43 +227,198 @@ func testTribeSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repos
},
},
{
name: fmt.Sprintf("OK: serverKeys=[%s]", randTribeSnapshot.ServerKey()),
name: "OK: serverKeys",
params: func(t *testing.T) domain.ListTribeSnapshotsParams {
t.Helper()
params := domain.NewListTribeSnapshotsParams()
res, err := repos.tribeSnapshot.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, res.TribeSnapshots())
randTribeSnapshot := res.TribeSnapshots()[0]
require.NoError(t, params.SetServerKeys([]string{randTribeSnapshot.ServerKey()}))
return params
},
assertTribeSnapshots: func(
assertResult: func(
t *testing.T,
params domain.ListTribeSnapshotsParams,
tribeSnapshots domain.TribeSnapshots,
res domain.ListTribeSnapshotsResult,
) {
t.Helper()
serverKeys := params.ServerKeys()
tribeSnapshots := res.TribeSnapshots()
assert.NotZero(t, tribeSnapshots)
for _, ts := range tribeSnapshots {
assert.True(t, slices.Contains(serverKeys, ts.ServerKey()))
}
},
},
{
name: "OK: offset=1 limit=2",
name: "OK: tribeIDs serverKeys",
params: func(t *testing.T) domain.ListTribeSnapshotsParams {
t.Helper()
params := domain.NewListTribeSnapshotsParams()
res, err := repos.tribeSnapshot.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, res.TribeSnapshots())
randTribeSnapshot := res.TribeSnapshots()[0]
require.NoError(t, params.SetServerKeys([]string{randTribeSnapshot.ServerKey()}))
require.NoError(t, params.SetTribeIDs([]int{randTribeSnapshot.TribeID()}))
return params
},
assertResult: func(
t *testing.T,
params domain.ListTribeSnapshotsParams,
res domain.ListTribeSnapshotsResult,
) {
t.Helper()
serverKeys := params.ServerKeys()
tribeIDs := params.TribeIDs()
tribeSnapshots := res.TribeSnapshots()
assert.NotZero(t, tribeSnapshots)
for _, ts := range tribeSnapshots {
assert.True(t, slices.Contains(serverKeys, ts.ServerKey()))
assert.True(t, slices.Contains(tribeIDs, ts.TribeID()))
}
},
},
{
name: "OK: cursor serverKeys sort=[id ASC]",
params: func(t *testing.T) domain.ListTribeSnapshotsParams {
t.Helper()
params := domain.NewListTribeSnapshotsParams()
res, err := repos.tribeSnapshot.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.TribeSnapshots()), 2)
require.NoError(t, params.SetSort([]domain.TribeSnapshotSort{domain.TribeSnapshotSortIDASC}))
require.NoError(t, params.SetServerKeys([]string{res.TribeSnapshots()[1].ServerKey()}))
cursor, err := res.TribeSnapshots()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
assertResult: func(t *testing.T, params domain.ListTribeSnapshotsParams, res domain.ListTribeSnapshotsResult) {
t.Helper()
serverKeys := params.ServerKeys()
tribeSnapshots := res.TribeSnapshots()
assert.NotEmpty(t, len(tribeSnapshots))
for _, ts := range tribeSnapshots {
assert.GreaterOrEqual(t, ts.ID(), params.Cursor().ID())
assert.True(t, slices.Contains(serverKeys, ts.ServerKey()))
}
assert.True(t, slices.IsSortedFunc(tribeSnapshots, func(a, b domain.TribeSnapshot) int {
return cmp.Compare(a.ID(), b.ID())
}))
},
},
{
name: "OK: cursor sort=[serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListTribeSnapshotsParams {
t.Helper()
params := domain.NewListTribeSnapshotsParams()
require.NoError(t, params.SetSort([]domain.TribeSnapshotSort{
domain.TribeSnapshotSortServerKeyASC,
domain.TribeSnapshotSortIDASC,
}))
res, err := repos.tribeSnapshot.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.TribeSnapshots()), 2)
cursor, err := res.TribeSnapshots()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
assertResult: func(t *testing.T, params domain.ListTribeSnapshotsParams, res domain.ListTribeSnapshotsResult) {
t.Helper()
tribeSnapshots := res.TribeSnapshots()
assert.NotEmpty(t, len(tribeSnapshots))
assert.True(t, slices.IsSortedFunc(tribeSnapshots, func(a, b domain.TribeSnapshot) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
assert.GreaterOrEqual(t, tribeSnapshots[0].ID(), params.Cursor().ID())
for _, ts := range tribeSnapshots {
assert.GreaterOrEqual(t, ts.ServerKey(), params.Cursor().ServerKey())
}
},
},
{
name: "OK: cursor sort=[serverKey DESC, id DESC]",
params: func(t *testing.T) domain.ListTribeSnapshotsParams {
t.Helper()
params := domain.NewListTribeSnapshotsParams()
require.NoError(t, params.SetSort([]domain.TribeSnapshotSort{
domain.TribeSnapshotSortServerKeyDESC,
domain.TribeSnapshotSortIDDESC,
}))
res, err := repos.tribeSnapshot.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.TribeSnapshots()), 2)
cursor, err := res.TribeSnapshots()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
assertResult: func(t *testing.T, params domain.ListTribeSnapshotsParams, res domain.ListTribeSnapshotsResult) {
t.Helper()
tribeSnapshots := res.TribeSnapshots()
assert.NotEmpty(t, len(tribeSnapshots))
assert.True(t, slices.IsSortedFunc(tribeSnapshots, func(a, b domain.TribeSnapshot) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
) * -1
}))
assert.LessOrEqual(t, tribeSnapshots[0].ID(), params.Cursor().ID())
for _, ts := range tribeSnapshots {
assert.LessOrEqual(t, ts.ServerKey(), params.Cursor().ServerKey())
}
},
},
{
name: "OK: limit=2",
params: func(t *testing.T) domain.ListTribeSnapshotsParams {
t.Helper()
params := domain.NewListTribeSnapshotsParams()
require.NoError(t, params.SetOffset(1))
require.NoError(t, params.SetLimit(2))
return params
},
assertTribeSnapshots: func(
assertResult: func(
t *testing.T,
params domain.ListTribeSnapshotsParams,
tribeSnapshots domain.TribeSnapshots,
res domain.ListTribeSnapshotsResult,
) {
t.Helper()
assert.Len(t, tribeSnapshots, params.Limit())
assert.Len(t, res.TribeSnapshots(), params.Limit())
assert.False(t, res.Self().IsZero())
assert.False(t, res.Next().IsZero())
},
},
}
@ -281,7 +439,7 @@ func testTribeSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repos
res, err := repos.tribeSnapshot.List(ctx, params)
assertError(t, err)
tt.assertTribeSnapshots(t, params, res)
tt.assertResult(t, params, res)
})
}
})

View File

@ -0,0 +1,96 @@
package domaintest
import (
"time"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/brianvoe/gofakeit/v7"
"github.com/stretchr/testify/require"
)
type TribeSnapshotCursorConfig struct {
ID int
ServerKey string
Date time.Time
}
func NewTribeSnapshotCursor(tb TestingTB, opts ...func(cfg *TribeSnapshotCursorConfig)) domain.TribeSnapshotCursor {
tb.Helper()
cfg := &TribeSnapshotCursorConfig{
ID: RandID(),
ServerKey: RandServerKey(),
Date: gofakeit.Date(),
}
for _, opt := range opts {
opt(cfg)
}
psc, err := domain.NewTribeSnapshotCursor(
cfg.ID,
cfg.ServerKey,
cfg.Date,
)
require.NoError(tb, err)
return psc
}
type TribeSnapshotConfig struct {
ID int
TribeID int
ServerKey string
NumMembers int
NumVillages int
Points int
AllPoints int
Rank int
OD domain.OpponentsDefeated
Dominance float64
Date time.Time
CreatedAt time.Time
}
func NewTribeSnapshot(tb TestingTB, opts ...func(cfg *TribeSnapshotConfig)) domain.TribeSnapshot {
tb.Helper()
now := time.Now()
cfg := &TribeSnapshotConfig{
ID: RandID(),
TribeID: RandID(),
ServerKey: RandServerKey(),
NumMembers: gofakeit.IntRange(1, 10000),
NumVillages: gofakeit.IntRange(1, 10000),
Points: gofakeit.IntRange(1, 10000),
AllPoints: gofakeit.IntRange(1, 10000),
Rank: gofakeit.IntRange(1, 10000),
OD: NewOpponentsDefeated(tb),
Dominance: gofakeit.Float64Range(0.1, 100),
Date: now,
CreatedAt: now,
}
for _, opt := range opts {
opt(cfg)
}
ts, err := domain.UnmarshalTribeSnapshotFromDatabase(
cfg.ID,
cfg.TribeID,
cfg.ServerKey,
cfg.NumMembers,
cfg.NumVillages,
cfg.Points,
cfg.AllPoints,
cfg.Rank,
cfg.OD,
cfg.Dominance,
cfg.Date,
cfg.CreatedAt,
)
require.NoError(tb, err)
return ts
}

View File

@ -245,6 +245,66 @@ func TestListPlayerSnapshotsParams_SetServerKeys(t *testing.T) {
}
}
func TestListPlayerSnapshotsParams_SetPlayerIDs(t *testing.T) {
t.Parallel()
type args struct {
playerIDs []int
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
playerIDs: []int{
domaintest.RandID(),
domaintest.RandID(),
domaintest.RandID(),
},
},
},
{
name: "ERR: value < 1",
args: args{
playerIDs: []int{
domaintest.RandID(),
domaintest.RandID(),
domaintest.RandID(),
0,
domaintest.RandID(),
},
},
expectedErr: domain.SliceElementValidationError{
Model: "ListPlayerSnapshotsParams",
Field: "playerIDs",
Index: 3,
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListPlayerSnapshotsParams()
require.ErrorIs(t, params.SetPlayerIDs(tt.args.playerIDs), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.playerIDs, params.PlayerIDs())
})
}
}
func TestListPlayerSnapshotsParams_SetSort(t *testing.T) {
t.Parallel()

View File

@ -129,6 +129,14 @@ func (ts TribeSnapshot) CreatedAt() time.Time {
return ts.createdAt
}
func (ts TribeSnapshot) ToCursor() (TribeSnapshotCursor, error) {
return NewTribeSnapshotCursor(ts.id, ts.serverKey, ts.date)
}
func (ts TribeSnapshot) IsZero() bool {
return ts == TribeSnapshot{}
}
type TribeSnapshots []TribeSnapshot
type CreateTribeSnapshotParams struct {
@ -250,11 +258,106 @@ func (s TribeSnapshotSort) String() string {
}
}
type TribeSnapshotCursor struct {
id int
serverKey string
date time.Time
}
const tribeSnapshotCursorModelName = "TribeSnapshotCursor"
func NewTribeSnapshotCursor(id int, serverKey string, date time.Time) (TribeSnapshotCursor, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return TribeSnapshotCursor{}, ValidationError{
Model: tribeSnapshotCursorModelName,
Field: "id",
Err: err,
}
}
if err := validateServerKey(serverKey); err != nil {
return TribeSnapshotCursor{}, ValidationError{
Model: tribeSnapshotCursorModelName,
Field: "serverKey",
Err: err,
}
}
return TribeSnapshotCursor{
id: id,
serverKey: serverKey,
date: date,
}, nil
}
//nolint:gocyclo
func decodeTribeSnapshotCursor(encoded string) (TribeSnapshotCursor, error) {
m, err := decodeCursor(encoded)
if err != nil {
return TribeSnapshotCursor{}, err
}
id, err := m.int("id")
if err != nil {
return TribeSnapshotCursor{}, ErrInvalidCursor
}
serverKey, err := m.string("serverKey")
if err != nil {
return TribeSnapshotCursor{}, ErrInvalidCursor
}
date, err := m.time("date")
if err != nil {
return TribeSnapshotCursor{}, ErrInvalidCursor
}
tsc, err := NewTribeSnapshotCursor(
id,
serverKey,
date,
)
if err != nil {
return TribeSnapshotCursor{}, ErrInvalidCursor
}
return tsc, nil
}
func (tsc TribeSnapshotCursor) ID() int {
return tsc.id
}
func (tsc TribeSnapshotCursor) ServerKey() string {
return tsc.serverKey
}
func (tsc TribeSnapshotCursor) Date() time.Time {
return tsc.date
}
func (tsc TribeSnapshotCursor) IsZero() bool {
return tsc == TribeSnapshotCursor{}
}
func (tsc TribeSnapshotCursor) Encode() string {
if tsc.IsZero() {
return ""
}
return encodeCursor([]keyValuePair{
{"id", tsc.id},
{"serverKey", tsc.serverKey},
{"date", tsc.date},
})
}
type ListTribeSnapshotsParams struct {
serverKeys []string
tribeIDs []int
sort []TribeSnapshotSort
cursor TribeSnapshotCursor
limit int
offset int
}
const (
@ -294,6 +397,27 @@ func (params *ListTribeSnapshotsParams) SetServerKeys(serverKeys []string) error
return nil
}
func (params *ListTribeSnapshotsParams) TribeIDs() []int {
return params.tribeIDs
}
func (params *ListTribeSnapshotsParams) SetTribeIDs(tribeIDs []int) error {
for i, id := range tribeIDs {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listTribeSnapshotsParamsModelName,
Field: "tribeIDs",
Index: i,
Err: err,
}
}
}
params.tribeIDs = tribeIDs
return nil
}
func (params *ListTribeSnapshotsParams) Sort() []TribeSnapshotSort {
return params.sort
}
@ -317,6 +441,30 @@ func (params *ListTribeSnapshotsParams) SetSort(sort []TribeSnapshotSort) error
return nil
}
func (params *ListTribeSnapshotsParams) Cursor() TribeSnapshotCursor {
return params.cursor
}
func (params *ListTribeSnapshotsParams) SetCursor(cursor TribeSnapshotCursor) error {
params.cursor = cursor
return nil
}
func (params *ListTribeSnapshotsParams) SetEncodedCursor(encoded string) error {
decoded, err := decodeTribeSnapshotCursor(encoded)
if err != nil {
return ValidationError{
Model: listTribeSnapshotsParamsModelName,
Field: "cursor",
Err: err,
}
}
params.cursor = decoded
return nil
}
func (params *ListTribeSnapshotsParams) Limit() int {
return params.limit
}
@ -335,20 +483,56 @@ func (params *ListTribeSnapshotsParams) SetLimit(limit int) error {
return nil
}
func (params *ListTribeSnapshotsParams) Offset() int {
return params.offset
type ListTribeSnapshotsResult struct {
snapshots TribeSnapshots
self TribeSnapshotCursor
next TribeSnapshotCursor
}
func (params *ListTribeSnapshotsParams) SetOffset(offset int) error {
if err := validateIntInRange(offset, 0, math.MaxInt); err != nil {
return ValidationError{
Model: listTribeSnapshotsParamsModelName,
Field: "offset",
Err: err,
const listTribeSnapshotsResultModelName = "ListTribeSnapshotsResult"
func NewListTribeSnapshotsResult(
snapshots TribeSnapshots,
next TribeSnapshot,
) (ListTribeSnapshotsResult, error) {
var err error
res := ListTribeSnapshotsResult{
snapshots: snapshots,
}
if len(snapshots) > 0 {
res.self, err = snapshots[0].ToCursor()
if err != nil {
return ListTribeSnapshotsResult{}, ValidationError{
Model: listTribeSnapshotsResultModelName,
Field: "self",
Err: err,
}
}
}
params.offset = offset
if !next.IsZero() {
res.next, err = next.ToCursor()
if err != nil {
return ListTribeSnapshotsResult{}, ValidationError{
Model: listTribeSnapshotsResultModelName,
Field: "next",
Err: err,
}
}
}
return nil
return res, nil
}
func (res ListTribeSnapshotsResult) TribeSnapshots() TribeSnapshots {
return res.snapshots
}
func (res ListTribeSnapshotsResult) Self() TribeSnapshotCursor {
return res.self
}
func (res ListTribeSnapshotsResult) Next() TribeSnapshotCursor {
return res.next
}

View File

@ -8,6 +8,7 @@ import (
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
"github.com/brianvoe/gofakeit/v7"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -111,6 +112,87 @@ func TestTribeSnapshotSort_IsInConflict(t *testing.T) {
}
}
func TestNewTribeSnapshotCursor(t *testing.T) {
t.Parallel()
validTribeSnapshotCursor := domaintest.NewTribeSnapshotCursor(t)
type args struct {
id int
serverKey string
date time.Time
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
id: validTribeSnapshotCursor.ID(),
serverKey: validTribeSnapshotCursor.ServerKey(),
date: validTribeSnapshotCursor.Date(),
},
expectedErr: nil,
},
{
name: "ERR: id < 1",
args: args{
id: 0,
serverKey: validTribeSnapshotCursor.ServerKey(),
date: validTribeSnapshotCursor.Date(),
},
expectedErr: domain.ValidationError{
Model: "TribeSnapshotCursor",
Field: "id",
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
id: validTribeSnapshotCursor.ID(),
serverKey: serverKeyTest.key,
},
expectedErr: domain.ValidationError{
Model: "TribeSnapshotCursor",
Field: "serverKey",
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
psc, err := domain.NewTribeSnapshotCursor(
tt.args.id,
tt.args.serverKey,
tt.args.date,
)
require.ErrorIs(t, err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.id, psc.ID())
assert.Equal(t, tt.args.serverKey, psc.ServerKey())
assert.Equal(t, tt.args.date, psc.Date())
assert.NotEmpty(t, psc.Encode())
})
}
}
func TestListTribeSnapshotsParams_SetServerKeys(t *testing.T) {
t.Parallel()
@ -165,6 +247,66 @@ func TestListTribeSnapshotsParams_SetServerKeys(t *testing.T) {
}
}
func TestListTribeSnapshotsParams_SetTribeIDs(t *testing.T) {
t.Parallel()
type args struct {
tribeIDs []int
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
tribeIDs: []int{
domaintest.RandID(),
domaintest.RandID(),
domaintest.RandID(),
},
},
},
{
name: "ERR: value < 1",
args: args{
tribeIDs: []int{
domaintest.RandID(),
domaintest.RandID(),
domaintest.RandID(),
0,
domaintest.RandID(),
},
},
expectedErr: domain.SliceElementValidationError{
Model: "ListTribeSnapshotsParams",
Field: "tribeIDs",
Index: 3,
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListTribeSnapshotsParams()
require.ErrorIs(t, params.SetTribeIDs(tt.args.tribeIDs), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.tribeIDs, params.TribeIDs())
})
}
}
func TestListTribeSnapshotsParams_SetSort(t *testing.T) {
t.Parallel()
@ -254,6 +396,86 @@ func TestListTribeSnapshotsParams_SetSort(t *testing.T) {
}
}
func TestListTribeSnapshotsParams_SetEncodedCursor(t *testing.T) {
t.Parallel()
validCursor := domaintest.NewTribeSnapshotCursor(t)
type args struct {
cursor string
}
tests := []struct {
name string
args args
expectedCursor domain.TribeSnapshotCursor
expectedErr error
}{
{
name: "OK",
args: args{
cursor: validCursor.Encode(),
},
expectedCursor: validCursor,
},
{
name: "ERR: len(cursor) < 1",
args: args{
cursor: "",
},
expectedErr: domain.ValidationError{
Model: "ListTribeSnapshotsParams",
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: "ListTribeSnapshotsParams",
Field: "cursor",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: 1001,
},
},
},
{
name: "ERR: malformed base64",
args: args{
cursor: "112345",
},
expectedErr: domain.ValidationError{
Model: "ListTribeSnapshotsParams",
Field: "cursor",
Err: domain.ErrInvalidCursor,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListTribeSnapshotsParams()
require.ErrorIs(t, params.SetEncodedCursor(tt.args.cursor), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.cursor, params.Cursor().Encode())
})
}
}
func TestListTribeSnapshotsParams_SetLimit(t *testing.T) {
t.Parallel()
@ -317,51 +539,49 @@ func TestListTribeSnapshotsParams_SetLimit(t *testing.T) {
}
}
func TestListTribeSnapshotsParams_SetOffset(t *testing.T) {
func TestNewListTribeSnapshotsResult(t *testing.T) {
t.Parallel()
type args struct {
offset int
snapshots := domain.TribeSnapshots{
domaintest.NewTribeSnapshot(t),
domaintest.NewTribeSnapshot(t),
domaintest.NewTribeSnapshot(t),
}
next := domaintest.NewTribeSnapshot(t)
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
offset: 100,
},
},
{
name: "ERR: offset < 0",
args: args{
offset: -1,
},
expectedErr: domain.ValidationError{
Model: "ListTribeSnapshotsParams",
Field: "offset",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
t.Run("OK: with next", func(t *testing.T) {
t.Parallel()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
res, err := domain.NewListTribeSnapshotsResult(snapshots, next)
require.NoError(t, err)
assert.Equal(t, snapshots, res.TribeSnapshots())
assert.Equal(t, snapshots[0].ID(), res.Self().ID())
assert.Equal(t, snapshots[0].ServerKey(), res.Self().ServerKey())
assert.Equal(t, snapshots[0].Date(), res.Self().Date())
assert.Equal(t, next.ID(), res.Next().ID())
assert.Equal(t, next.ServerKey(), res.Next().ServerKey())
assert.Equal(t, next.Date(), res.Next().Date())
})
params := domain.NewListTribeSnapshotsParams()
t.Run("OK: without next", func(t *testing.T) {
t.Parallel()
require.ErrorIs(t, params.SetOffset(tt.args.offset), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.offset, params.Offset())
})
}
res, err := domain.NewListTribeSnapshotsResult(snapshots, domain.TribeSnapshot{})
require.NoError(t, err)
assert.Equal(t, snapshots, res.TribeSnapshots())
assert.Equal(t, snapshots[0].ID(), res.Self().ID())
assert.Equal(t, snapshots[0].ServerKey(), res.Self().ServerKey())
assert.Equal(t, snapshots[0].Date(), res.Self().Date())
assert.True(t, res.Next().IsZero())
})
t.Run("OK: 0 snapshots", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListTribeSnapshotsResult(nil, domain.TribeSnapshot{})
require.NoError(t, err)
assert.Zero(t, res.TribeSnapshots())
assert.True(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
})
}

View File

@ -225,14 +225,10 @@ func TestSnapshotCreation(t *testing.T) {
cnt := 0
for {
snapshots, err := tribeSnapshotRepo.List(ctx, listSnapshotsParams)
res, err := tribeSnapshotRepo.List(ctx, listSnapshotsParams)
require.NoError(collect, err)
if len(snapshots) == 0 {
break
}
for _, ts := range snapshots {
for _, ts := range res.TribeSnapshots() {
cnt++
msg := fmt.Sprintf("TribeID=%d,ServerKey=%s", ts.TribeID(), ts.ServerKey())
@ -264,7 +260,11 @@ func TestSnapshotCreation(t *testing.T) {
assert.WithinDuration(collect, time.Now(), ts.Date(), 24*time.Hour, msg)
}
require.NoError(collect, listSnapshotsParams.SetOffset(listSnapshotsParams.Offset()+listSnapshotsParams.Limit()))
if res.Next().IsZero() {
break
}
require.NoError(collect, listSnapshotsParams.SetCursor(res.Next()))
}
//nolint:testifylint