refactor: village - cursor pagination (#16)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details

Reviewed-on: twhelp/corev3#16
This commit is contained in:
Dawid Wysokiński 2024-03-05 06:14:41 +00:00
parent e3bb2eb5c4
commit 847cf220da
16 changed files with 667 additions and 277 deletions

View File

@ -76,17 +76,25 @@ func (repo *VillageBunRepository) CreateOrUpdate(ctx context.Context, params ...
return nil return nil
} }
func (repo *VillageBunRepository) List(ctx context.Context, params domain.ListVillagesParams) (domain.Villages, error) { func (repo *VillageBunRepository) List(
ctx context.Context,
params domain.ListVillagesParams,
) (domain.ListVillagesResult, error) {
var villages bunmodel.Villages var villages bunmodel.Villages
if err := repo.db.NewSelect(). if err := repo.db.NewSelect().
Model(&villages). Model(&villages).
Apply(listVillagesParamsApplier{params: params}.apply). Apply(listVillagesParamsApplier{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 villages from the db: %w", err) return domain.ListVillagesResult{}, fmt.Errorf("couldn't select villages from the db: %w", err)
} }
return villages.ToDomain() converted, err := villages.ToDomain()
if err != nil {
return domain.ListVillagesResult{}, err
}
return domain.NewListVillagesResult(separateListResultAndNext(converted, params.Limit()))
} }
func (repo *VillageBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error { func (repo *VillageBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error {
@ -116,10 +124,6 @@ func (a listVillagesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
q = q.Where("village.id IN (?)", bun.In(ids)) q = q.Where("village.id IN (?)", bun.In(ids))
} }
if idGT := a.params.IDGT(); idGT.Valid {
q = q.Where("village.id > ?", idGT.V)
}
if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 { if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 {
q = q.Where("village.server_key IN (?)", bun.In(serverKeys)) q = q.Where("village.server_key IN (?)", bun.In(serverKeys))
} }
@ -139,5 +143,49 @@ func (a listVillagesParamsApplier) 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)
}
func (a listVillagesParamsApplier) 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 el cursorPaginationApplierDataElement
switch s {
case domain.VillageSortIDASC:
el.value = cursor.ID()
el.unique = true
el.column = "village.id"
el.direction = sortDirectionASC
case domain.VillageSortIDDESC:
el.value = cursor.ID()
el.unique = true
el.column = "village.id"
el.direction = sortDirectionDESC
case domain.VillageSortServerKeyASC:
el.value = cursor.ServerKey()
el.column = "village.server_key"
el.direction = sortDirectionASC
case domain.VillageSortServerKeyDESC:
el.value = cursor.ServerKey()
el.column = "village.server_key"
el.direction = sortDirectionDESC
default:
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
cursorApplier.data = append(cursorApplier.data, el)
}
return q.Apply(cursorApplier.apply)
} }

View File

@ -371,7 +371,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)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, len(res.Players())) require.NotEmpty(t, res.Players())
randPlayer := res.Players()[0] randPlayer := res.Players()[0]
require.NoError(t, params.SetIDs([]int{randPlayer.ID()})) require.NoError(t, params.SetIDs([]int{randPlayer.ID()}))
@ -406,7 +406,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)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, len(res.Players())) require.NotEmpty(t, res.Players())
randPlayer := res.Players()[0] randPlayer := res.Players()[0]
require.NoError(t, params.SetNames([]string{randPlayer.Name()})) require.NoError(t, params.SetNames([]string{randPlayer.Name()}))
@ -544,7 +544,7 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
players := res.Players() players := res.Players()
assert.NotEmpty(t, len(players)) assert.NotEmpty(t, len(players))
for _, p := range res.Players() { for _, p := range players {
assert.GreaterOrEqual(t, p.ID(), params.Cursor().ID()) assert.GreaterOrEqual(t, p.ID(), params.Cursor().ID())
assert.True(t, slices.Contains(serverKeys, p.ServerKey())) assert.True(t, slices.Contains(serverKeys, p.ServerKey()))
} }
@ -590,7 +590,7 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
) )
})) }))
assert.GreaterOrEqual(t, players[0].ID(), params.Cursor().ID()) assert.GreaterOrEqual(t, players[0].ID(), params.Cursor().ID())
for _, p := range res.Players() { for _, p := range players {
assert.GreaterOrEqual(t, p.ServerKey(), params.Cursor().ServerKey()) assert.GreaterOrEqual(t, p.ServerKey(), params.Cursor().ServerKey())
} }
}, },
@ -632,7 +632,7 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
) * -1 ) * -1
})) }))
assert.LessOrEqual(t, players[0].ID(), params.Cursor().ID()) assert.LessOrEqual(t, players[0].ID(), params.Cursor().ID())
for _, p := range res.Players() { for _, p := range players {
assert.LessOrEqual(t, p.ServerKey(), params.Cursor().ServerKey()) assert.LessOrEqual(t, p.ServerKey(), params.Cursor().ServerKey())
} }
}, },
@ -652,6 +652,8 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) { assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper() t.Helper()
assert.Len(t, res.Players(), params.Limit()) assert.Len(t, res.Players(), params.Limit())
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()

View File

@ -488,6 +488,7 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) { assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper() t.Helper()
assert.Len(t, res.Servers(), params.Limit()) assert.Len(t, res.Servers(), params.Limit())
assert.False(t, res.Self().IsZero())
assert.False(t, res.Next().IsZero()) assert.False(t, res.Next().IsZero())
}, },
assertError: func(t *testing.T, err error) { assertError: func(t *testing.T, err error) {

View File

@ -37,7 +37,7 @@ type playerRepository interface {
type villageRepository interface { type villageRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateVillageParams) error CreateOrUpdate(ctx context.Context, params ...domain.CreateVillageParams) error
List(ctx context.Context, params domain.ListVillagesParams) (domain.Villages, error) List(ctx context.Context, params domain.ListVillagesParams) (domain.ListVillagesResult, error)
Delete(ctx context.Context, serverKey string, ids ...int) error Delete(ctx context.Context, serverKey string, ids ...int) error
} }

View File

@ -434,7 +434,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
res, err := repos.tribe.List(ctx, params) res, err := repos.tribe.List(ctx, params)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, len(res.Tribes())) require.NotEmpty(t, res.Tribes())
randTribe := res.Tribes()[0] randTribe := res.Tribes()[0]
require.NoError(t, params.SetIDs([]int{randTribe.ID()})) require.NoError(t, params.SetIDs([]int{randTribe.ID()}))
@ -469,7 +469,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
res, err := repos.tribe.List(ctx, params) res, err := repos.tribe.List(ctx, params)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, len(res.Tribes())) require.NotEmpty(t, res.Tribes())
randTribe := res.Tribes()[0] randTribe := res.Tribes()[0]
require.NoError(t, params.SetTags([]string{randTribe.Tag()})) require.NoError(t, params.SetTags([]string{randTribe.Tag()}))
@ -615,7 +615,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
) )
})) }))
assert.GreaterOrEqual(t, tribes[0].ID(), params.Cursor().ID()) assert.GreaterOrEqual(t, tribes[0].ID(), params.Cursor().ID())
for _, tr := range res.Tribes() { for _, tr := range tribes {
assert.GreaterOrEqual(t, tr.ServerKey(), params.Cursor().ServerKey()) assert.GreaterOrEqual(t, tr.ServerKey(), params.Cursor().ServerKey())
} }
}, },
@ -657,7 +657,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
) * -1 ) * -1
})) }))
assert.LessOrEqual(t, tribes[0].ID(), params.Cursor().ID()) assert.LessOrEqual(t, tribes[0].ID(), params.Cursor().ID())
for _, tr := range res.Tribes() { for _, tr := range tribes {
assert.LessOrEqual(t, tr.ServerKey(), params.Cursor().ServerKey()) assert.LessOrEqual(t, tr.ServerKey(), params.Cursor().ServerKey())
} }
}, },
@ -703,7 +703,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
})) }))
assert.GreaterOrEqual(t, tribes[0].ID(), params.Cursor().ID()) assert.GreaterOrEqual(t, tribes[0].ID(), params.Cursor().ID())
assert.GreaterOrEqual(t, tribes[0].ServerKey(), params.Cursor().ServerKey()) assert.GreaterOrEqual(t, tribes[0].ServerKey(), params.Cursor().ServerKey())
for _, tr := range res.Tribes() { for _, tr := range tribes {
assert.LessOrEqual(t, tr.Points(), params.Cursor().Points()) assert.LessOrEqual(t, tr.Points(), params.Cursor().Points())
} }
}, },
@ -749,7 +749,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
})) }))
assert.GreaterOrEqual(t, tribes[0].ID(), params.Cursor().ID()) assert.GreaterOrEqual(t, tribes[0].ID(), params.Cursor().ID())
assert.GreaterOrEqual(t, tribes[0].ServerKey(), params.Cursor().ServerKey()) assert.GreaterOrEqual(t, tribes[0].ServerKey(), params.Cursor().ServerKey())
for _, tr := range res.Tribes() { for _, tr := range tribes {
assert.GreaterOrEqual(t, tr.DeletedAt(), params.Cursor().DeletedAt()) assert.GreaterOrEqual(t, tr.DeletedAt(), params.Cursor().DeletedAt())
} }
}, },
@ -769,6 +769,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
assertResult: func(t *testing.T, params domain.ListTribesParams, res domain.ListTribesResult) { assertResult: func(t *testing.T, params domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper() t.Helper()
assert.Len(t, res.Tribes(), params.Limit()) assert.Len(t, res.Tribes(), params.Limit())
assert.False(t, res.Self().IsZero())
assert.False(t, res.Next().IsZero()) assert.False(t, res.Next().IsZero())
}, },
assertError: func(t *testing.T, err error) { assertError: func(t *testing.T, err error) {

View File

@ -175,6 +175,7 @@ func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositorie
assertResult: func(t *testing.T, params domain.ListVersionsParams, res domain.ListVersionsResult) { assertResult: func(t *testing.T, params domain.ListVersionsParams, res domain.ListVersionsResult) {
t.Helper() t.Helper()
assert.Len(t, res.Versions(), params.Limit()) assert.Len(t, res.Versions(), params.Limit())
assert.False(t, res.Self().IsZero())
assert.False(t, res.Next().IsZero()) assert.False(t, res.Next().IsZero())
}, },
assertError: func(t *testing.T, err error) { assertError: func(t *testing.T, err error) {

View File

@ -3,7 +3,6 @@ package adapter_test
import ( import (
"cmp" "cmp"
"context" "context"
"fmt"
"math" "math"
"slices" "slices"
"testing" "testing"
@ -38,8 +37,9 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
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()}))
villages, err := repos.village.List(ctx, listParams) res, err := repos.village.List(ctx, listParams)
require.NoError(t, err) require.NoError(t, err)
villages := res.Villages()
assert.Len(t, villages, len(params)) assert.Len(t, villages, len(params))
for i, p := range params { for i, p := range params {
idx := slices.IndexFunc(villages, func(village domain.Village) bool { idx := slices.IndexFunc(villages, func(village domain.Village) bool {
@ -97,17 +97,11 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
repos := newRepos(t) repos := newRepos(t)
villages, listVillagesErr := repos.village.List(ctx, domain.NewListVillagesParams())
require.NoError(t, listVillagesErr)
require.NotEmpty(t, villages)
randVillage := villages[0]
tests := []struct { tests := []struct {
name string name string
params func(t *testing.T) domain.ListVillagesParams params func(t *testing.T) domain.ListVillagesParams
assertVillages func(t *testing.T, params domain.ListVillagesParams, villages domain.Villages) assertResult func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult)
assertError func(t *testing.T, err error) assertError func(t *testing.T, err error)
assertTotal func(t *testing.T, params domain.ListVillagesParams, total int)
}{ }{
{ {
name: "OK: default params", name: "OK: default params",
@ -115,8 +109,9 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
t.Helper() t.Helper()
return domain.NewListVillagesParams() return domain.NewListVillagesParams()
}, },
assertVillages: func(t *testing.T, _ domain.ListVillagesParams, villages domain.Villages) { assertResult: func(t *testing.T, _ domain.ListVillagesParams, res domain.ListVillagesResult) {
t.Helper() t.Helper()
villages := res.Villages()
assert.NotEmpty(t, len(villages)) assert.NotEmpty(t, len(villages))
assert.True(t, slices.IsSortedFunc(villages, func(a, b domain.Village) int { assert.True(t, slices.IsSortedFunc(villages, func(a, b domain.Village) int {
return cmp.Or( return cmp.Or(
@ -124,15 +119,13 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
cmp.Compare(a.ID(), b.ID()), cmp.Compare(a.ID(), b.ID()),
) )
})) }))
assert.False(t, res.Self().IsZero())
assert.True(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.ListVillagesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: "OK: sort=[serverKey DESC, id DESC]", name: "OK: sort=[serverKey DESC, id DESC]",
@ -142,8 +135,9 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
require.NoError(t, params.SetSort([]domain.VillageSort{domain.VillageSortServerKeyDESC, domain.VillageSortIDDESC})) require.NoError(t, params.SetSort([]domain.VillageSort{domain.VillageSortServerKeyDESC, domain.VillageSortIDDESC}))
return params return params
}, },
assertVillages: func(t *testing.T, _ domain.ListVillagesParams, villages domain.Villages) { assertResult: func(t *testing.T, _ domain.ListVillagesParams, res domain.ListVillagesResult) {
t.Helper() t.Helper()
villages := res.Villages()
assert.NotEmpty(t, len(villages)) assert.NotEmpty(t, len(villages))
assert.True(t, slices.IsSortedFunc(villages, func(a, b domain.Village) int { assert.True(t, slices.IsSortedFunc(villages, func(a, b domain.Village) int {
return cmp.Or( return cmp.Or(
@ -156,26 +150,32 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
t.Helper() t.Helper()
require.NoError(t, err) require.NoError(t, err)
}, },
assertTotal: func(t *testing.T, _ domain.ListVillagesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: fmt.Sprintf("OK: ids=[%d] serverKeys=[%s]", randVillage.ID(), randVillage.ServerKey()), name: "OK: ids serverKeys",
params: func(t *testing.T) domain.ListVillagesParams { params: func(t *testing.T) domain.ListVillagesParams {
t.Helper() t.Helper()
params := domain.NewListVillagesParams() params := domain.NewListVillagesParams()
res, err := repos.village.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, res.Villages())
randVillage := res.Villages()[0]
require.NoError(t, params.SetIDs([]int{randVillage.ID()})) require.NoError(t, params.SetIDs([]int{randVillage.ID()}))
require.NoError(t, params.SetServerKeys([]string{randVillage.ServerKey()})) require.NoError(t, params.SetServerKeys([]string{randVillage.ServerKey()}))
return params return params
}, },
assertVillages: func(t *testing.T, params domain.ListVillagesParams, villages domain.Villages) { assertResult: func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult) {
t.Helper() t.Helper()
ids := params.IDs() ids := params.IDs()
serverKeys := params.ServerKeys() serverKeys := params.ServerKeys()
villages := res.Villages()
assert.Len(t, villages, len(ids))
for _, v := range villages { for _, v := range villages {
assert.True(t, slices.Contains(ids, v.ID())) assert.True(t, slices.Contains(ids, v.ID()))
assert.True(t, slices.Contains(serverKeys, v.ServerKey())) assert.True(t, slices.Contains(serverKeys, v.ServerKey()))
@ -185,58 +185,147 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
t.Helper() t.Helper()
require.NoError(t, err) require.NoError(t, err)
}, },
assertTotal: func(t *testing.T, _ domain.ListVillagesParams, total int) { },
{
name: "OK: cursor serverKeys sort=[id ASC]",
params: func(t *testing.T) domain.ListVillagesParams {
t.Helper() t.Helper()
assert.NotEmpty(t, total)
params := domain.NewListVillagesParams()
res, err := repos.village.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Villages()), 2)
require.NoError(t, params.SetSort([]domain.VillageSort{domain.VillageSortIDASC}))
require.NoError(t, params.SetServerKeys([]string{res.Villages()[1].ServerKey()}))
require.NoError(t, params.SetCursor(domaintest.NewVillageCursor(t, func(cfg *domaintest.VillageCursorConfig) {
cfg.ID = res.Villages()[1].ID()
})))
return params
},
assertResult: func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult) {
t.Helper()
serverKeys := params.ServerKeys()
villages := res.Villages()
assert.NotEmpty(t, len(villages))
for _, v := range villages {
assert.GreaterOrEqual(t, v.ID(), params.Cursor().ID())
assert.True(t, slices.Contains(serverKeys, v.ServerKey()))
}
assert.True(t, slices.IsSortedFunc(villages, func(a, b domain.Village) int {
return cmp.Compare(a.ID(), b.ID())
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
}, },
}, },
{ {
name: fmt.Sprintf("OK: idGT=%d", randVillage.ID()), name: "OK: cursor sort=[serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListVillagesParams { params: func(t *testing.T) domain.ListVillagesParams {
t.Helper() t.Helper()
params := domain.NewListVillagesParams() params := domain.NewListVillagesParams()
require.NoError(t, params.SetIDGT(domain.NullInt{ require.NoError(t, params.SetSort([]domain.VillageSort{
V: randVillage.ID(), domain.VillageSortServerKeyASC,
Valid: true, domain.VillageSortIDASC,
})) }))
res, err := repos.village.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Villages()), 2)
require.NoError(t, params.SetCursor(domaintest.NewVillageCursor(t, func(cfg *domaintest.VillageCursorConfig) {
cfg.ID = res.Villages()[1].ID()
cfg.ServerKey = res.Villages()[1].ServerKey()
})))
return params return params
}, },
assertVillages: func(t *testing.T, params domain.ListVillagesParams, villages domain.Villages) { assertResult: func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult) {
t.Helper() t.Helper()
assert.NotEmpty(t, villages) villages := res.Villages()
assert.NotEmpty(t, len(villages))
assert.True(t, slices.IsSortedFunc(villages, func(a, b domain.Village) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
assert.GreaterOrEqual(t, villages[0].ID(), params.Cursor().ID())
for _, v := range villages { for _, v := range villages {
assert.Greater(t, v.ID(), params.IDGT().V, v.ID()) assert.GreaterOrEqual(t, v.ServerKey(), params.Cursor().ServerKey())
} }
}, },
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.ListVillagesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: "OK: offset=1 limit=2", name: "OK: cursor sort=[serverKey DESC, id DESC]",
params: func(t *testing.T) domain.ListVillagesParams { params: func(t *testing.T) domain.ListVillagesParams {
t.Helper() t.Helper()
params := domain.NewListVillagesParams() params := domain.NewListVillagesParams()
require.NoError(t, params.SetOffset(1)) require.NoError(t, params.SetSort([]domain.VillageSort{
require.NoError(t, params.SetLimit(2)) domain.VillageSortServerKeyDESC,
domain.VillageSortIDDESC,
}))
res, err := repos.village.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Villages()), 2)
require.NoError(t, params.SetCursor(domaintest.NewVillageCursor(t, func(cfg *domaintest.VillageCursorConfig) {
cfg.ID = res.Villages()[1].ID()
cfg.ServerKey = res.Villages()[1].ServerKey()
})))
return params return params
}, },
assertVillages: func(t *testing.T, params domain.ListVillagesParams, villages domain.Villages) { assertResult: func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult) {
t.Helper() t.Helper()
assert.Len(t, villages, params.Limit()) villages := res.Villages()
assert.NotEmpty(t, len(villages))
assert.True(t, slices.IsSortedFunc(villages, func(a, b domain.Village) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
) * -1
}))
assert.LessOrEqual(t, villages[0].ID(), params.Cursor().ID())
for _, v := range villages {
assert.LessOrEqual(t, v.ServerKey(), params.Cursor().ServerKey())
}
}, },
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.ListVillagesParams, total int) { },
{
name: "OK: limit=2",
params: func(t *testing.T) domain.ListVillagesParams {
t.Helper() t.Helper()
assert.NotEmpty(t, total) params := domain.NewListVillagesParams()
require.NoError(t, params.SetLimit(2))
return params
},
assertResult: func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult) {
t.Helper()
assert.Len(t, res.Villages(), params.Limit())
assert.False(t, res.Self().IsZero())
assert.False(t, res.Next().IsZero())
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
}, },
}, },
} }
@ -249,7 +338,7 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
res, err := repos.village.List(ctx, params) res, err := repos.village.List(ctx, params)
tt.assertError(t, err) tt.assertError(t, err)
tt.assertVillages(t, params, res) tt.assertResult(t, params, res)
}) })
} }
}) })
@ -276,8 +365,9 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
listVillagesParams := domain.NewListVillagesParams() listVillagesParams := domain.NewListVillagesParams()
require.NoError(t, listVillagesParams.SetServerKeys(serverKeys)) require.NoError(t, listVillagesParams.SetServerKeys(serverKeys))
villagesBeforeDelete, err := repos.village.List(ctx, listVillagesParams) res, err := repos.village.List(ctx, listVillagesParams)
require.NoError(t, err) require.NoError(t, err)
villagesBeforeDelete := res.Villages()
var serverKey string var serverKey string
var ids []int var ids []int
@ -296,8 +386,9 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
require.NoError(t, repos.village.Delete(ctx, serverKey, idsToDelete...)) require.NoError(t, repos.village.Delete(ctx, serverKey, idsToDelete...))
villagesAfterDelete, err := repos.village.List(ctx, listVillagesParams) res, err = repos.village.List(ctx, listVillagesParams)
require.NoError(t, err) require.NoError(t, err)
villagesAfterDelete := res.Villages()
assert.Len(t, villagesAfterDelete, len(villagesBeforeDelete)-len(idsToDelete)) assert.Len(t, villagesAfterDelete, len(villagesBeforeDelete)-len(idsToDelete))
for _, v := range villagesAfterDelete { for _, v := range villagesAfterDelete {
if v.ServerKey() == serverKey && slices.Contains(ids, v.ID()) { if v.ServerKey() == serverKey && slices.Contains(ids, v.ID()) {

View File

@ -9,7 +9,7 @@ import (
type VillageRepository interface { type VillageRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateVillageParams) error CreateOrUpdate(ctx context.Context, params ...domain.CreateVillageParams) error
List(ctx context.Context, params domain.ListVillagesParams) (domain.Villages, error) List(ctx context.Context, params domain.ListVillagesParams) (domain.ListVillagesResult, error)
Delete(ctx context.Context, serverKey string, ids ...int) error Delete(ctx context.Context, serverKey string, ids ...int) error
} }
@ -94,24 +94,28 @@ func (svc *VillageService) delete(ctx context.Context, serverKey string, village
var toDelete []int var toDelete []int
for { for {
storedVillages, 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(storedVillages) == 0 { toDelete = append(toDelete, res.Villages().Delete(serverKey, villages)...)
if res.Next().IsZero() {
break break
} }
toDelete = append(toDelete, storedVillages.Delete(serverKey, villages)...) if err = listParams.SetCursor(res.Next()); err != nil {
if err = listParams.SetIDGT(domain.NullInt{
V: storedVillages[len(storedVillages)-1].ID(),
Valid: true,
}); err != nil {
return err return err
} }
} }
return svc.repo.Delete(ctx, serverKey, toDelete...) return svc.repo.Delete(ctx, serverKey, toDelete...)
} }
func (svc *VillageService) List(
ctx context.Context,
params domain.ListVillagesParams,
) (domain.ListVillagesResult, error) {
return svc.repo.List(ctx, params)
}

View File

@ -8,6 +8,32 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type VillageCursorConfig struct {
ID int
ServerKey string
}
func NewVillageCursor(tb TestingTB, opts ...func(cfg *VillageCursorConfig)) domain.VillageCursor {
tb.Helper()
cfg := &VillageCursorConfig{
ID: RandID(),
ServerKey: RandServerKey(),
}
for _, opt := range opts {
opt(cfg)
}
pc, err := domain.NewVillageCursor(
cfg.ID,
cfg.ServerKey,
)
require.NoError(tb, err)
return pc
}
type VillageConfig struct { type VillageConfig struct {
ID int ID int
ServerKey string ServerKey string

View File

@ -412,6 +412,33 @@ const (
PlayerSortDeletedAtDESC PlayerSortDeletedAtDESC
) )
func newPlayerSortFromString(s string) (PlayerSort, error) {
allowed := []PlayerSort{
PlayerSortODScoreAttASC,
PlayerSortODScoreAttDESC,
PlayerSortODScoreDefASC,
PlayerSortODScoreDefDESC,
PlayerSortODScoreSupASC,
PlayerSortODScoreSupDESC,
PlayerSortODScoreTotalASC,
PlayerSortODScoreTotalDESC,
PlayerSortPointsASC,
PlayerSortPointsDESC,
PlayerSortDeletedAtASC,
PlayerSortDeletedAtDESC,
}
for _, a := range allowed {
if strings.EqualFold(a.String(), s) {
return a, nil
}
}
return 0, UnsupportedSortStringError{
Sort: s,
}
}
// IsInConflict returns true if two sorts can't be used together (e.g. PlayerSortIDASC and PlayerSortIDDESC). // IsInConflict returns true if two sorts can't be used together (e.g. PlayerSortIDASC and PlayerSortIDDESC).
func (s PlayerSort) IsInConflict(s2 PlayerSort) bool { func (s PlayerSort) IsInConflict(s2 PlayerSort) bool {
ss := []PlayerSort{s, s2} ss := []PlayerSort{s, s2}
@ -460,33 +487,6 @@ func (s PlayerSort) String() string {
} }
} }
func newPlayerSortFromString(s string) (PlayerSort, error) {
allowed := []PlayerSort{
PlayerSortODScoreAttASC,
PlayerSortODScoreAttDESC,
PlayerSortODScoreDefASC,
PlayerSortODScoreDefDESC,
PlayerSortODScoreSupASC,
PlayerSortODScoreSupDESC,
PlayerSortODScoreTotalASC,
PlayerSortODScoreTotalDESC,
PlayerSortPointsASC,
PlayerSortPointsDESC,
PlayerSortDeletedAtASC,
PlayerSortDeletedAtDESC,
}
for _, a := range allowed {
if strings.EqualFold(a.String(), s) {
return a, nil
}
}
return 0, UnsupportedSortStringError{
Sort: s,
}
}
type PlayerCursor struct { type PlayerCursor struct {
id int id int
serverKey string serverKey string

View File

@ -510,7 +510,7 @@ func TestNewPlayerCursor(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
tc, err := domain.NewPlayerCursor( pc, err := domain.NewPlayerCursor(
tt.args.id, tt.args.id,
tt.args.serverKey, tt.args.serverKey,
tt.args.odScoreAtt, tt.args.odScoreAtt,
@ -524,15 +524,15 @@ func TestNewPlayerCursor(t *testing.T) {
if tt.expectedErr != nil { if tt.expectedErr != nil {
return return
} }
assert.Equal(t, tt.args.id, tc.ID()) assert.Equal(t, tt.args.id, pc.ID())
assert.Equal(t, tt.args.serverKey, tc.ServerKey()) assert.Equal(t, tt.args.serverKey, pc.ServerKey())
assert.Equal(t, tt.args.odScoreAtt, tc.ODScoreAtt()) assert.Equal(t, tt.args.odScoreAtt, pc.ODScoreAtt())
assert.Equal(t, tt.args.odScoreDef, tc.ODScoreDef()) assert.Equal(t, tt.args.odScoreDef, pc.ODScoreDef())
assert.Equal(t, tt.args.odScoreSup, tc.ODScoreSup()) assert.Equal(t, tt.args.odScoreSup, pc.ODScoreSup())
assert.Equal(t, tt.args.odScoreTotal, tc.ODScoreTotal()) assert.Equal(t, tt.args.odScoreTotal, pc.ODScoreTotal())
assert.Equal(t, tt.args.points, tc.Points()) assert.Equal(t, tt.args.points, pc.Points())
assert.Equal(t, tt.args.deletedAt, tc.DeletedAt()) assert.Equal(t, tt.args.deletedAt, pc.DeletedAt())
assert.NotEmpty(t, tc.Encode()) assert.NotEmpty(t, pc.Encode())
}) })
} }
} }

View File

@ -450,6 +450,33 @@ const (
TribeSortDeletedAtDESC TribeSortDeletedAtDESC
) )
func newTribeSortFromString(s string) (TribeSort, error) {
allowed := []TribeSort{
TribeSortODScoreAttASC,
TribeSortODScoreAttDESC,
TribeSortODScoreDefASC,
TribeSortODScoreDefDESC,
TribeSortODScoreTotalASC,
TribeSortODScoreTotalDESC,
TribeSortPointsASC,
TribeSortPointsDESC,
TribeSortDominanceASC,
TribeSortDominanceDESC,
TribeSortDeletedAtASC,
TribeSortDeletedAtDESC,
}
for _, a := range allowed {
if strings.EqualFold(a.String(), s) {
return a, nil
}
}
return 0, UnsupportedSortStringError{
Sort: s,
}
}
// IsInConflict returns true if two sorts can't be used together (e.g. TribeSortIDASC and TribeSortIDDESC). // IsInConflict returns true if two sorts can't be used together (e.g. TribeSortIDASC and TribeSortIDDESC).
func (s TribeSort) IsInConflict(s2 TribeSort) bool { func (s TribeSort) IsInConflict(s2 TribeSort) bool {
ss := []TribeSort{s, s2} ss := []TribeSort{s, s2}
@ -498,33 +525,6 @@ func (s TribeSort) String() string {
} }
} }
func newTribeSortFromString(s string) (TribeSort, error) {
allowed := []TribeSort{
TribeSortODScoreAttASC,
TribeSortODScoreAttDESC,
TribeSortODScoreDefASC,
TribeSortODScoreDefDESC,
TribeSortODScoreTotalASC,
TribeSortODScoreTotalDESC,
TribeSortPointsASC,
TribeSortPointsDESC,
TribeSortDominanceASC,
TribeSortDominanceDESC,
TribeSortDeletedAtASC,
TribeSortDeletedAtDESC,
}
for _, a := range allowed {
if strings.EqualFold(a.String(), s) {
return a, nil
}
}
return 0, UnsupportedSortStringError{
Sort: s,
}
}
type TribeCursor struct { type TribeCursor struct {
id int id int
serverKey string serverKey string

View File

@ -31,7 +31,7 @@ type keyValuePair struct {
value any value any
} }
func (kvp keyValuePair) valueString() string { func (kvp keyValuePair) valueToString() string {
switch v := kvp.value.(type) { switch v := kvp.value.(type) {
case string: case string:
return v return v
@ -59,7 +59,7 @@ func encodeCursor(kvps []keyValuePair) string {
n := len(cursorSeparator) * (len(kvps) - 1) n := len(cursorSeparator) * (len(kvps) - 1)
for i, kvp := range kvps { for i, kvp := range kvps {
value := kvp.valueString() value := kvp.valueToString()
n += len(kvp.key) + len(value) + len(cursorKeyValueSeparator) n += len(kvp.key) + len(value) + len(cursorKeyValueSeparator)
values[i] = value values[i] = value
} }

View File

@ -147,6 +147,10 @@ func (v Village) Base() BaseVillage {
} }
} }
func (v Village) IsZero() bool {
return v == Village{}
}
type Villages []Village type Villages []Village
// Delete finds all villages with the given serverKey that are not in the given slice with active villages // Delete finds all villages with the given serverKey that are not in the given slice with active villages
@ -248,13 +252,93 @@ func (s VillageSort) String() string {
} }
} }
type VillageCursor struct {
id int
serverKey string
}
const villageCursorModelName = "VillageCursor"
func NewVillageCursor(id int, serverKey string) (VillageCursor, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return VillageCursor{}, ValidationError{
Model: villageCursorModelName,
Field: "id",
Err: err,
}
}
if err := validateServerKey(serverKey); err != nil {
return VillageCursor{}, ValidationError{
Model: villageCursorModelName,
Field: "serverKey",
Err: err,
}
}
return VillageCursor{
id: id,
serverKey: serverKey,
}, nil
}
//nolint:gocyclo
func decodeVillageCursor(encoded string) (VillageCursor, error) {
m, err := decodeCursor(encoded)
if err != nil {
return VillageCursor{}, err
}
id, err := m.int("id")
if err != nil {
return VillageCursor{}, ErrInvalidCursor
}
serverKey, err := m.string("serverKey")
if err != nil {
return VillageCursor{}, ErrInvalidCursor
}
vc, err := NewVillageCursor(
id,
serverKey,
)
if err != nil {
return VillageCursor{}, ErrInvalidCursor
}
return vc, nil
}
func (vc VillageCursor) ID() int {
return vc.id
}
func (vc VillageCursor) ServerKey() string {
return vc.serverKey
}
func (vc VillageCursor) IsZero() bool {
return vc == VillageCursor{}
}
func (vc VillageCursor) Encode() string {
if vc.IsZero() {
return ""
}
return encodeCursor([]keyValuePair{
{"id", vc.id},
{"serverKey", vc.serverKey},
})
}
type ListVillagesParams struct { type ListVillagesParams struct {
ids []int ids []int
idGT NullInt
serverKeys []string serverKeys []string
sort []VillageSort sort []VillageSort
cursor VillageCursor
limit int limit int
offset int
} }
const ( const (
@ -293,26 +377,6 @@ func (params *ListVillagesParams) SetIDs(ids []int) error {
return nil return nil
} }
func (params *ListVillagesParams) IDGT() NullInt {
return params.idGT
}
func (params *ListVillagesParams) SetIDGT(idGT NullInt) error {
if idGT.Valid {
if err := validateIntInRange(idGT.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: listVillagesParamsModelName,
Field: "idGT",
Err: err,
}
}
}
params.idGT = idGT
return nil
}
func (params *ListVillagesParams) ServerKeys() []string { func (params *ListVillagesParams) ServerKeys() []string {
return params.serverKeys return params.serverKeys
} }
@ -357,6 +421,30 @@ func (params *ListVillagesParams) SetSort(sort []VillageSort) error {
return nil return nil
} }
func (params *ListVillagesParams) Cursor() VillageCursor {
return params.cursor
}
func (params *ListVillagesParams) SetCursor(cursor VillageCursor) error {
params.cursor = cursor
return nil
}
func (params *ListVillagesParams) SetEncodedCursor(encoded string) error {
decoded, err := decodeVillageCursor(encoded)
if err != nil {
return ValidationError{
Model: listVillagesParamsModelName,
Field: "cursor",
Err: err,
}
}
params.cursor = decoded
return nil
}
func (params *ListVillagesParams) Limit() int { func (params *ListVillagesParams) Limit() int {
return params.limit return params.limit
} }
@ -375,20 +463,53 @@ func (params *ListVillagesParams) SetLimit(limit int) error {
return nil return nil
} }
func (params *ListVillagesParams) Offset() int { type ListVillagesResult struct {
return params.offset villages Villages
self VillageCursor
next VillageCursor
} }
func (params *ListVillagesParams) SetOffset(offset int) error { const listVillagesResultModelName = "ListVillagesResult"
if err := validateIntInRange(offset, 0, math.MaxInt); err != nil {
return ValidationError{ func NewListVillagesResult(villages Villages, next Village) (ListVillagesResult, error) {
Model: listVillagesParamsModelName, var err error
Field: "offset", res := ListVillagesResult{
Err: err, villages: villages,
}
if len(villages) > 0 {
res.self, err = NewVillageCursor(villages[0].ID(), villages[0].ServerKey())
if err != nil {
return ListVillagesResult{}, ValidationError{
Model: listVillagesResultModelName,
Field: "self",
Err: err,
}
} }
} }
params.offset = offset if !next.IsZero() {
res.next, err = NewVillageCursor(next.ID(), next.ServerKey())
if err != nil {
return ListVillagesResult{}, ValidationError{
Model: listVillagesResultModelName,
Field: "next",
Err: err,
}
}
}
return nil return res, nil
}
func (res ListVillagesResult) Villages() Villages {
return res.villages
}
func (res ListVillagesResult) Self() VillageCursor {
return res.self
}
func (res ListVillagesResult) Next() VillageCursor {
return res.next
} }

View File

@ -8,6 +8,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/v7"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -146,6 +147,82 @@ func TestVillageSort_IsInConflict(t *testing.T) {
} }
} }
func TestNewVillageCursor(t *testing.T) {
t.Parallel()
validVillageCursor := domaintest.NewVillageCursor(t)
type args struct {
id int
serverKey string
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
id: validVillageCursor.ID(),
serverKey: validVillageCursor.ServerKey(),
},
expectedErr: nil,
},
{
name: "ERR: id < 1",
args: args{
id: 0,
serverKey: validVillageCursor.ServerKey(),
},
expectedErr: domain.ValidationError{
Model: "VillageCursor",
Field: "id",
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
id: validVillageCursor.ID(),
serverKey: serverKeyTest.key,
},
expectedErr: domain.ValidationError{
Model: "VillageCursor",
Field: "serverKey",
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
vc, err := domain.NewVillageCursor(
tt.args.id,
tt.args.serverKey,
)
require.ErrorIs(t, err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.id, vc.ID())
assert.Equal(t, tt.args.serverKey, vc.ServerKey())
assert.NotEmpty(t, vc.Encode())
})
}
}
func TestListVillagesParams_SetIDs(t *testing.T) { func TestListVillagesParams_SetIDs(t *testing.T) {
t.Parallel() t.Parallel()
@ -206,61 +283,6 @@ func TestListVillagesParams_SetIDs(t *testing.T) {
} }
} }
func TestListVillagesParams_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{
V: domaintest.RandID(),
Valid: true,
},
},
},
{
name: "ERR: value < 0",
args: args{
idGT: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "ListVillagesParams",
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.NewListVillagesParams()
require.ErrorIs(t, params.SetIDGT(tt.args.idGT), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.idGT, params.IDGT())
})
}
}
func TestListVillagesParams_SetServerKeys(t *testing.T) { func TestListVillagesParams_SetServerKeys(t *testing.T) {
t.Parallel() t.Parallel()
@ -403,6 +425,87 @@ func TestListVillagesParams_SetSort(t *testing.T) {
} }
} }
func TestListVillagesParams_SetEncodedCursor(t *testing.T) {
t.Parallel()
validCursor := domaintest.NewVillageCursor(t)
type args struct {
cursor string
}
tests := []struct {
name string
args args
expectedCursor domain.VillageCursor
expectedErr error
}{
{
name: "OK",
args: args{
cursor: validCursor.Encode(),
},
expectedCursor: validCursor,
},
{
name: "ERR: len(cursor) < 1",
args: args{
cursor: "",
},
expectedErr: domain.ValidationError{
Model: "ListVillagesParams",
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: "ListVillagesParams",
Field: "cursor",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: 1001,
},
},
},
{
name: "ERR: malformed base64",
args: args{
cursor: "112345",
},
expectedErr: domain.ValidationError{
Model: "ListVillagesParams",
Field: "cursor",
Err: domain.ErrInvalidCursor,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListVillagesParams()
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 TestListVillagesParams_SetLimit(t *testing.T) { func TestListVillagesParams_SetLimit(t *testing.T) {
t.Parallel() t.Parallel()
@ -466,51 +569,46 @@ func TestListVillagesParams_SetLimit(t *testing.T) {
} }
} }
func TestListVillagesParams_SetOffset(t *testing.T) { func TestNewListVillagesResult(t *testing.T) {
t.Parallel() t.Parallel()
type args struct { villages := domain.Villages{
offset int domaintest.NewVillage(t),
domaintest.NewVillage(t),
domaintest.NewVillage(t),
} }
next := domaintest.NewVillage(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: "ListVillagesParams",
Field: "offset",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests { res, err := domain.NewListVillagesResult(villages, next)
t.Run(tt.name, func(t *testing.T) { require.NoError(t, err)
t.Parallel() assert.Equal(t, villages, res.Villages())
assert.Equal(t, villages[0].ID(), res.Self().ID())
assert.Equal(t, villages[0].ServerKey(), res.Self().ServerKey())
assert.Equal(t, next.ID(), res.Next().ID())
assert.Equal(t, next.ServerKey(), res.Next().ServerKey())
})
params := domain.NewListVillagesParams() t.Run("OK: without next", func(t *testing.T) {
t.Parallel()
require.ErrorIs(t, params.SetOffset(tt.args.offset), tt.expectedErr) res, err := domain.NewListVillagesResult(villages, domain.Village{})
if tt.expectedErr != nil { require.NoError(t, err)
return assert.Equal(t, villages, res.Villages())
} assert.Equal(t, villages[0].ID(), res.Self().ID())
assert.Equal(t, tt.args.offset, params.Offset()) assert.Equal(t, villages[0].ServerKey(), res.Self().ServerKey())
}) assert.True(t, res.Next().IsZero())
} })
t.Run("OK: 0 villages", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListVillagesResult(nil, domain.Village{})
require.NoError(t, err)
assert.Zero(t, res.Villages())
assert.True(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
})
} }

View File

@ -421,19 +421,16 @@ func TestDataSync(t *testing.T) {
allVillages := make(domain.Villages, 0, len(expectedVillages)) allVillages := make(domain.Villages, 0, len(expectedVillages))
for { for {
villages, err := villageRepo.List(ctx, listParams) res, err := villageRepo.List(ctx, listParams)
require.NoError(collect, err) require.NoError(collect, err)
if len(villages) == 0 { allVillages = append(allVillages, res.Villages()...)
if res.Next().IsZero() {
break break
} }
allVillages = append(allVillages, villages...) require.NoError(collect, listParams.SetCursor(res.Next()))
require.NoError(collect, listParams.SetIDGT(domain.NullInt{
V: villages[len(villages)-1].ID(),
Valid: true,
}))
} }
if !assert.Len(collect, allVillages, len(expectedVillages)) { if !assert.Len(collect, allVillages, len(expectedVillages)) {