refactor: server - cursor pagination (#56)

Reviewed-on: twhelp/corev3#56
This commit is contained in:
Dawid Wysokiński 2024-02-07 07:17:47 +00:00
parent b6d55b1741
commit f07451351b
17 changed files with 600 additions and 280 deletions

View File

@ -64,18 +64,25 @@ func (repo *ServerBunRepository) CreateOrUpdate(ctx context.Context, params ...d
return nil return nil
} }
func (repo *ServerBunRepository) List(ctx context.Context, params domain.ListServersParams) (domain.Servers, error) { func (repo *ServerBunRepository) List(
ctx context.Context,
params domain.ListServersParams,
) (domain.ListServersResult, error) {
var servers bunmodel.Servers var servers bunmodel.Servers
if err := repo.baseListQuery(params).Model(&servers).Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) { if err := repo.db.NewSelect().
return nil, fmt.Errorf("couldn't select servers from the db: %w", err) Model(&servers).
Apply(listServersParamsApplier{params: params}.apply).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return domain.ListServersResult{}, fmt.Errorf("couldn't select servers from the db: %w", err)
} }
return servers.ToDomain() converted, err := servers.ToDomain()
} if err != nil {
return domain.ListServersResult{}, err
}
func (repo *ServerBunRepository) baseListQuery(params domain.ListServersParams) *bun.SelectQuery { return domain.NewListServersResult(separateListResultAndNext(converted, params.Limit()))
return repo.db.NewSelect().Apply(listServersParamsApplier{params: params}.apply)
} }
func (repo *ServerBunRepository) Update(ctx context.Context, key string, params domain.UpdateServerParams) error { func (repo *ServerBunRepository) Update(ctx context.Context, key string, params domain.UpdateServerParams) error {
@ -184,10 +191,6 @@ func (a listServersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
q = q.Where("server.key IN (?)", bun.In(keys)) q = q.Where("server.key IN (?)", bun.In(keys))
} }
if keyGT := a.params.KeyGT(); keyGT.Valid {
q = q.Where("server.key > ?", keyGT.Value)
}
if versionCodes := a.params.VersionCodes(); len(versionCodes) > 0 { if versionCodes := a.params.VersionCodes(); len(versionCodes) > 0 {
q = q.Where("server.version_code IN (?)", bun.In(versionCodes)) q = q.Where("server.version_code IN (?)", bun.In(versionCodes))
} }
@ -229,5 +232,29 @@ func (a listServersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
} }
} }
return q.Limit(a.params.Limit()).Offset(a.params.Offset()) if !a.params.Cursor().IsZero() {
q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
cursorKey := a.params.Cursor().Key()
cursorOpen := a.params.Cursor().Open()
for _, s := range a.params.Sort() {
switch s {
case domain.ServerSortKeyASC:
q = q.Where("server.key >= ?", cursorKey)
case domain.ServerSortKeyDESC:
q = q.Where("server.key <= ?", cursorKey)
case domain.ServerSortOpenASC:
q = q.Where("server.open >= ?", cursorOpen)
case domain.ServerSortOpenDESC:
q = q.Where("server.open <= ?", cursorOpen)
default:
return q.Err(errors.New("unsupported sort value"))
}
}
return q
})
}
return q.Limit(a.params.Limit() + 1)
} }

View File

@ -89,10 +89,10 @@ func testEnnoblementRepository(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()
servers, err := repos.server.List(ctx, domain.NewListServersParams()) listServersRes, err := repos.server.List(ctx, domain.NewListServersParams())
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, servers) require.NotEmpty(t, listServersRes)
server := servers[0] server := listServersRes.Servers()[0]
ennoblementsToCreate := domain.BaseEnnoblements{ ennoblementsToCreate := domain.BaseEnnoblements{
domaintest.NewBaseEnnoblement(t), domaintest.NewBaseEnnoblement(t),

View File

@ -64,10 +64,10 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Run("OK", func(t *testing.T) { t.Run("OK", func(t *testing.T) {
t.Parallel() t.Parallel()
servers, err := repos.server.List(ctx, domain.NewListServersParams()) listServersRes, err := repos.server.List(ctx, domain.NewListServersParams())
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, servers) require.NotEmpty(t, listServersRes)
server := servers[0] server := listServersRes.Servers()[0]
playersToCreate := domain.BasePlayers{ playersToCreate := domain.BasePlayers{
domaintest.NewBasePlayer(t), domaintest.NewBasePlayer(t),
@ -325,15 +325,15 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
listServersParams := domain.NewListServersParams() listServersParams := domain.NewListServersParams()
require.NoError(t, listServersParams.SetSpecial(domain.NullBool{Value: false, Valid: true})) require.NoError(t, listServersParams.SetSpecial(domain.NullBool{Value: false, Valid: true}))
servers, listServersErr := repos.server.List(ctx, listServersParams) listServersRes, listServersErr := repos.server.List(ctx, listServersParams)
require.NoError(t, listServersErr) require.NoError(t, listServersErr)
require.NotEmpty(t, servers) require.NotEmpty(t, listServersRes)
t.Run("OK", func(t *testing.T) { t.Run("OK", func(t *testing.T) {
t.Parallel() t.Parallel()
serverKeys := make([]string, 0, len(servers)) serverKeys := make([]string, 0, len(listServersRes.Servers()))
for _, s := range servers { for _, s := range listServersRes.Servers() {
serverKeys = append(serverKeys, s.Key()) serverKeys = append(serverKeys, s.Key())
} }
@ -386,7 +386,7 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Run("OK: len(ids) == 0", func(t *testing.T) { t.Run("OK: len(ids) == 0", func(t *testing.T) {
t.Parallel() t.Parallel()
require.NoError(t, repos.player.Delete(ctx, servers[0].Key())) require.NoError(t, repos.player.Delete(ctx, listServersRes.Servers()[0].Key()))
}) })
}) })
} }

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 testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
listParams := domain.NewListServersParams() listParams := domain.NewListServersParams()
require.NoError(t, listParams.SetKeys(keys)) require.NoError(t, listParams.SetKeys(keys))
servers, err := repos.server.List(ctx, listParams) res, err := repos.server.List(ctx, listParams)
require.NoError(t, err) require.NoError(t, err)
servers := res.Servers()
require.Len(t, servers, len(params)) require.Len(t, servers, len(params))
for i, p := range params { for i, p := range params {
idx := slices.IndexFunc(servers, func(server domain.Server) bool { idx := slices.IndexFunc(servers, func(server domain.Server) bool {
@ -99,11 +99,6 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
repos := newRepos(t) repos := newRepos(t)
servers, listServersErr := repos.server.List(ctx, domain.NewListServersParams())
require.NoError(t, listServersErr)
require.NotEmpty(t, servers)
randServer := servers[0]
snapshotsCreatedAtLT := time.Date( snapshotsCreatedAtLT := time.Date(
2022, 2022,
time.March, time.March,
@ -116,11 +111,10 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
) )
tests := []struct { tests := []struct {
name string name string
params func(t *testing.T) domain.ListServersParams params func(t *testing.T) domain.ListServersParams
assertServers func(t *testing.T, params domain.ListServersParams, servers domain.Servers) assertResult func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult)
assertError func(t *testing.T, err error) assertError func(t *testing.T, err error)
assertTotal func(t *testing.T, params domain.ListServersParams, total int)
}{ }{
{ {
name: "OK: default params", name: "OK: default params",
@ -128,8 +122,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Helper() t.Helper()
return domain.NewListServersParams() return domain.NewListServersParams()
}, },
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) { assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper() t.Helper()
servers := res.Servers()
assert.NotEmpty(t, len(servers)) assert.NotEmpty(t, len(servers))
assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int { assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int {
return cmp.Compare(a.Key(), b.Key()) return cmp.Compare(a.Key(), b.Key())
@ -139,10 +134,6 @@ func testServerRepository(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, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: "OK: sort=[open ASC, key ASC]", name: "OK: sort=[open ASC, key ASC]",
@ -152,8 +143,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
require.NoError(t, params.SetSort([]domain.ServerSort{domain.ServerSortOpenASC, domain.ServerSortKeyASC})) require.NoError(t, params.SetSort([]domain.ServerSort{domain.ServerSortOpenASC, domain.ServerSortKeyASC}))
return params return params
}, },
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) { assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper() t.Helper()
servers := res.Servers()
assert.NotEmpty(t, len(servers)) assert.NotEmpty(t, len(servers))
assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int { assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int {
if a.Open() && !b.Open() { if a.Open() && !b.Open() {
@ -171,10 +163,6 @@ func testServerRepository(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, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: "OK: sort=[open DESC, key DESC]", name: "OK: sort=[open DESC, key DESC]",
@ -184,8 +172,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
require.NoError(t, params.SetSort([]domain.ServerSort{domain.ServerSortOpenDESC, domain.ServerSortKeyDESC})) require.NoError(t, params.SetSort([]domain.ServerSort{domain.ServerSortOpenDESC, domain.ServerSortKeyDESC}))
return params return params
}, },
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) { assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper() t.Helper()
servers := res.Servers()
assert.NotEmpty(t, len(servers)) assert.NotEmpty(t, len(servers))
assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int { assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int {
if a.Open() && !b.Open() { if a.Open() && !b.Open() {
@ -203,22 +192,26 @@ func testServerRepository(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, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: fmt.Sprintf("OK: keys=[%s]", randServer.Key()), name: "OK: keys",
params: func(t *testing.T) domain.ListServersParams { params: func(t *testing.T) domain.ListServersParams {
t.Helper() t.Helper()
params := domain.NewListServersParams() params := domain.NewListServersParams()
require.NoError(t, params.SetKeys([]string{randServer.Key()}))
res, err := repos.server.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, len(res.Servers()))
require.NoError(t, params.SetKeys([]string{res.Servers()[0].Key()}))
return params return params
}, },
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) { assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper() t.Helper()
servers := res.Servers()
keys := params.Keys() keys := params.Keys()
assert.Len(t, servers, len(keys)) assert.Len(t, servers, len(keys))
@ -232,22 +225,26 @@ func testServerRepository(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, params domain.ListServersParams, total int) {
t.Helper()
assert.Equal(t, len(params.Keys()), total) //nolint:testifylint
},
}, },
{ {
name: fmt.Sprintf("OK: versionCodes=[%s]", randServer.VersionCode()), name: "OK: versionCodes",
params: func(t *testing.T) domain.ListServersParams { params: func(t *testing.T) domain.ListServersParams {
t.Helper() t.Helper()
params := domain.NewListServersParams() params := domain.NewListServersParams()
require.NoError(t, params.SetVersionCodes([]string{randServer.VersionCode()}))
res, err := repos.server.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, len(res.Servers()))
require.NoError(t, params.SetVersionCodes([]string{res.Servers()[0].VersionCode()}))
return params return params
}, },
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) { assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper() t.Helper()
servers := res.Servers()
versionCodes := params.VersionCodes() versionCodes := params.VersionCodes()
assert.NotEmpty(t, servers) assert.NotEmpty(t, servers)
@ -261,37 +258,6 @@ func testServerRepository(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, params domain.ListServersParams, total int) {
t.Helper()
assert.Equal(t, len(params.Keys()), total) //nolint:testifylint
},
},
{
name: fmt.Sprintf("OK: keyGT=%s", randServer.Key()),
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
params := domain.NewListServersParams()
require.NoError(t, params.SetKeyGT(domain.NullString{
Value: randServer.Key(),
Valid: true,
}))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
t.Helper()
assert.NotEmpty(t, len(servers))
for _, s := range servers {
assert.Greater(t, s.Key(), params.KeyGT().Value, s.Key())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: "OK: special=true", name: "OK: special=true",
@ -304,8 +270,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
})) }))
return params return params
}, },
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) { assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper() t.Helper()
servers := res.Servers()
assert.NotEmpty(t, len(servers)) assert.NotEmpty(t, len(servers))
for _, s := range servers { for _, s := range servers {
assert.True(t, s.Special(), s.Key()) assert.True(t, s.Special(), s.Key())
@ -315,10 +282,6 @@ func testServerRepository(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, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: "OK: open=false", name: "OK: open=false",
@ -331,8 +294,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
})) }))
return params return params
}, },
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) { assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper() t.Helper()
servers := res.Servers()
assert.NotEmpty(t, len(servers)) assert.NotEmpty(t, len(servers))
for _, s := range servers { for _, s := range servers {
assert.False(t, s.Open(), s.Key()) assert.False(t, s.Open(), s.Key())
@ -342,10 +306,6 @@ func testServerRepository(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, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: "OK: playerSnapshotsCreatedAtLt=" + snapshotsCreatedAtLT.Format(time.RFC3339), name: "OK: playerSnapshotsCreatedAtLt=" + snapshotsCreatedAtLT.Format(time.RFC3339),
@ -358,8 +318,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
})) }))
return params return params
}, },
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) { assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper() t.Helper()
servers := res.Servers()
assert.NotEmpty(t, len(servers)) assert.NotEmpty(t, len(servers))
for _, s := range servers { for _, s := range servers {
assert.True(t, s.PlayerSnapshotsCreatedAt().Before(snapshotsCreatedAtLT)) assert.True(t, s.PlayerSnapshotsCreatedAt().Before(snapshotsCreatedAtLT))
@ -369,10 +330,6 @@ func testServerRepository(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, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: "OK: tribeSnapshotsCreatedAtLt=" + snapshotsCreatedAtLT.Format(time.RFC3339), name: "OK: tribeSnapshotsCreatedAtLt=" + snapshotsCreatedAtLT.Format(time.RFC3339),
@ -385,8 +342,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
})) }))
return params return params
}, },
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) { assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper() t.Helper()
servers := res.Servers()
assert.NotEmpty(t, len(servers)) assert.NotEmpty(t, len(servers))
for _, s := range servers { for _, s := range servers {
assert.True(t, s.TribeSnapshotsCreatedAt().Before(snapshotsCreatedAtLT)) assert.True(t, s.TribeSnapshotsCreatedAt().Before(snapshotsCreatedAtLT))
@ -396,31 +354,108 @@ func testServerRepository(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, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: "OK: offset=1 limit=2", name: "OK: cursor sort=[open ASC, key ASC]",
params: func(t *testing.T) domain.ListServersParams { params: func(t *testing.T) domain.ListServersParams {
t.Helper() t.Helper()
params := domain.NewListServersParams() params := domain.NewListServersParams()
require.NoError(t, params.SetOffset(1)) require.NoError(t, params.SetSort([]domain.ServerSort{domain.ServerSortOpenASC, domain.ServerSortKeyASC}))
require.NoError(t, params.SetLimit(2))
res, err := repos.server.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Servers()), 2)
require.NoError(t, params.SetCursor(domaintest.NewServerCursor(t, func(cfg *domaintest.ServerCursorConfig) {
cfg.Key = res.Servers()[1].Key()
cfg.Open = res.Servers()[1].Open()
})))
return params return params
}, },
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) { assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper() t.Helper()
assert.Len(t, servers, params.Limit()) servers := res.Servers()
assert.NotEmpty(t, len(servers))
assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int {
if a.Open() && !b.Open() {
return 1
}
if !a.Open() && b.Open() {
return -1
}
return cmp.Compare(a.Key(), b.Key())
}))
for _, s := range res.Servers() {
assert.GreaterOrEqual(t, s.Key(), params.Cursor().Key(), s.Key())
}
}, },
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, params domain.ListServersParams, total int) { },
{
name: "OK: cursor sort=[open DESC, key DESC]",
params: func(t *testing.T) domain.ListServersParams {
t.Helper() t.Helper()
assert.NotEmpty(t, total)
params := domain.NewListServersParams()
require.NoError(t, params.SetSort([]domain.ServerSort{domain.ServerSortOpenDESC, domain.ServerSortKeyDESC}))
res, err := repos.server.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Servers()), 2)
require.NoError(t, params.SetCursor(domaintest.NewServerCursor(t, func(cfg *domaintest.ServerCursorConfig) {
cfg.Key = res.Servers()[1].Key()
cfg.Open = res.Servers()[1].Open()
})))
return params
},
assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
servers := res.Servers()
assert.NotEmpty(t, len(servers))
assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int {
if a.Open() && !b.Open() {
return -1
}
if !a.Open() && b.Open() {
return 1
}
return cmp.Compare(a.Key(), b.Key()) * -1
}))
for _, s := range res.Servers() {
assert.LessOrEqual(t, s.Key(), params.Cursor().Key(), s.Key())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: limit=2",
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
params := domain.NewListServersParams()
require.NoError(t, params.SetLimit(2))
return params
},
assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
assert.Len(t, res.Servers(), params.Limit())
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
}, },
}, },
} }
@ -435,7 +470,7 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
res, err := repos.server.List(ctx, params) res, err := repos.server.List(ctx, params)
tt.assertError(t, err) tt.assertError(t, err)
tt.assertServers(t, params, res) tt.assertResult(t, params, res)
}) })
} }
}) })
@ -454,6 +489,7 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
serversBeforeUpdate, err := repos.server.List(ctx, listServersBeforeUpdateParams) serversBeforeUpdate, err := repos.server.List(ctx, listServersBeforeUpdateParams)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, serversBeforeUpdate) require.NotEmpty(t, serversBeforeUpdate)
serverBeforeUpdate := serversBeforeUpdate.Servers()[0]
var updateParams domain.UpdateServerParams var updateParams domain.UpdateServerParams
require.NoError(t, updateParams.SetConfig(domain.NullServerConfig{ require.NoError(t, updateParams.SetConfig(domain.NullServerConfig{
@ -517,58 +553,59 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
Valid: true, Valid: true,
})) }))
require.NoError(t, repos.server.Update(ctx, serversBeforeUpdate[0].Key(), updateParams)) require.NoError(t, repos.server.Update(ctx, serverBeforeUpdate.Key(), updateParams))
listServersAfterUpdateParams := domain.NewListServersParams() listServersAfterUpdateParams := domain.NewListServersParams()
require.NoError(t, listServersAfterUpdateParams.SetLimit(1)) require.NoError(t, listServersAfterUpdateParams.SetLimit(1))
require.NoError(t, listServersAfterUpdateParams.SetKeys([]string{serversBeforeUpdate[0].Key()})) require.NoError(t, listServersAfterUpdateParams.SetKeys([]string{serverBeforeUpdate.Key()}))
serversAfterUpdate, err := repos.server.List(ctx, listServersAfterUpdateParams) serversAfterUpdate, err := repos.server.List(ctx, listServersAfterUpdateParams)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, serversAfterUpdate) require.NotEmpty(t, serversAfterUpdate)
assert.Equal(t, updateParams.Config().Value, serversAfterUpdate[0].Config()) serverAfterUpdate := serversAfterUpdate.Servers()[0]
assert.Equal(t, updateParams.UnitInfo().Value, serversAfterUpdate[0].UnitInfo()) assert.Equal(t, updateParams.Config().Value, serverAfterUpdate.Config())
assert.Equal(t, updateParams.BuildingInfo().Value, serversAfterUpdate[0].BuildingInfo()) assert.Equal(t, updateParams.UnitInfo().Value, serverAfterUpdate.UnitInfo())
assert.Equal(t, updateParams.NumTribes().Value, serversAfterUpdate[0].NumTribes()) assert.Equal(t, updateParams.BuildingInfo().Value, serverAfterUpdate.BuildingInfo())
assert.Equal(t, updateParams.NumTribes().Value, serverAfterUpdate.NumTribes())
assert.WithinDuration( assert.WithinDuration(
t, t,
updateParams.TribeDataSyncedAt().Value, updateParams.TribeDataSyncedAt().Value,
serversAfterUpdate[0].TribeDataSyncedAt(), serverAfterUpdate.TribeDataSyncedAt(),
time.Minute, time.Minute,
) )
assert.Equal(t, updateParams.NumPlayers().Value, serversAfterUpdate[0].NumPlayers()) assert.Equal(t, updateParams.NumPlayers().Value, serverAfterUpdate.NumPlayers())
assert.WithinDuration( assert.WithinDuration(
t, t,
updateParams.PlayerDataSyncedAt().Value, updateParams.PlayerDataSyncedAt().Value,
serversAfterUpdate[0].PlayerDataSyncedAt(), serverAfterUpdate.PlayerDataSyncedAt(),
time.Minute, time.Minute,
) )
assert.Equal(t, updateParams.NumVillages().Value, serversAfterUpdate[0].NumVillages()) assert.Equal(t, updateParams.NumVillages().Value, serverAfterUpdate.NumVillages())
assert.Equal(t, updateParams.NumPlayerVillages().Value, serversAfterUpdate[0].NumPlayerVillages()) assert.Equal(t, updateParams.NumPlayerVillages().Value, serverAfterUpdate.NumPlayerVillages())
assert.Equal(t, updateParams.NumBarbarianVillages().Value, serversAfterUpdate[0].NumBarbarianVillages()) assert.Equal(t, updateParams.NumBarbarianVillages().Value, serverAfterUpdate.NumBarbarianVillages())
assert.Equal(t, updateParams.NumBonusVillages().Value, serversAfterUpdate[0].NumBonusVillages()) assert.Equal(t, updateParams.NumBonusVillages().Value, serverAfterUpdate.NumBonusVillages())
assert.WithinDuration( assert.WithinDuration(
t, t,
updateParams.VillageDataSyncedAt().Value, updateParams.VillageDataSyncedAt().Value,
serversAfterUpdate[0].VillageDataSyncedAt(), serverAfterUpdate.VillageDataSyncedAt(),
time.Minute, time.Minute,
) )
assert.WithinDuration( assert.WithinDuration(
t, t,
updateParams.EnnoblementDataSyncedAt().Value, updateParams.EnnoblementDataSyncedAt().Value,
serversAfterUpdate[0].EnnoblementDataSyncedAt(), serverAfterUpdate.EnnoblementDataSyncedAt(),
time.Minute, time.Minute,
) )
assert.WithinDuration( assert.WithinDuration(
t, t,
updateParams.TribeSnapshotsCreatedAt().Value, updateParams.TribeSnapshotsCreatedAt().Value,
serversAfterUpdate[0].TribeSnapshotsCreatedAt(), serverAfterUpdate.TribeSnapshotsCreatedAt(),
time.Minute, time.Minute,
) )
assert.WithinDuration( assert.WithinDuration(
t, t,
updateParams.PlayerSnapshotsCreatedAt().Value, updateParams.PlayerSnapshotsCreatedAt().Value,
serversAfterUpdate[0].PlayerSnapshotsCreatedAt(), serverAfterUpdate.PlayerSnapshotsCreatedAt(),
time.Minute, time.Minute,
) )
}) })

View File

@ -17,7 +17,7 @@ type versionRepository interface {
type serverRepository interface { type serverRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateServerParams) error CreateOrUpdate(ctx context.Context, params ...domain.CreateServerParams) error
List(ctx context.Context, params domain.ListServersParams) (domain.Servers, error) List(ctx context.Context, params domain.ListServersParams) (domain.ListServersResult, error)
Update(ctx context.Context, key string, params domain.UpdateServerParams) error Update(ctx context.Context, key string, params domain.UpdateServerParams) error
} }

View File

@ -63,10 +63,10 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
t.Run("OK", func(t *testing.T) { t.Run("OK", func(t *testing.T) {
t.Parallel() t.Parallel()
servers, err := repos.server.List(ctx, domain.NewListServersParams()) listServersRes, err := repos.server.List(ctx, domain.NewListServersParams())
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, servers) require.NotEmpty(t, listServersRes)
server := servers[0] server := listServersRes.Servers()[0]
tribesToCreate := domain.BaseTribes{ tribesToCreate := domain.BaseTribes{
domaintest.NewBaseTribe(t), domaintest.NewBaseTribe(t),
@ -114,9 +114,9 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
Valid: true, Valid: true,
})) }))
servers, listServersErr := repos.server.List(ctx, listServersParams) listServersRes, listServersErr := repos.server.List(ctx, listServersParams)
require.NoError(t, listServersErr) require.NoError(t, listServersErr)
require.GreaterOrEqual(t, len(servers), 2) require.GreaterOrEqual(t, len(listServersRes.Servers()), 2)
tests := []struct { tests := []struct {
name string name string
@ -125,12 +125,12 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
}{ }{
{ {
name: "numPlayerVillages=0", name: "numPlayerVillages=0",
serverKey: servers[0].Key(), serverKey: listServersRes.Servers()[0].Key(),
numPlayerVillages: 0, numPlayerVillages: 0,
}, },
{ {
name: "numPlayerVillages=35000", name: "numPlayerVillages=35000",
serverKey: servers[1].Key(), serverKey: listServersRes.Servers()[1].Key(),
numPlayerVillages: 35000, numPlayerVillages: 35000,
}, },
} }
@ -390,15 +390,15 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
listServersParams := domain.NewListServersParams() listServersParams := domain.NewListServersParams()
require.NoError(t, listServersParams.SetSpecial(domain.NullBool{Value: false, Valid: true})) require.NoError(t, listServersParams.SetSpecial(domain.NullBool{Value: false, Valid: true}))
servers, listServersErr := repos.server.List(ctx, listServersParams) listServersRes, listServersErr := repos.server.List(ctx, listServersParams)
require.NoError(t, listServersErr) require.NoError(t, listServersErr)
require.NotEmpty(t, servers) require.NotEmpty(t, listServersRes)
t.Run("OK", func(t *testing.T) { t.Run("OK", func(t *testing.T) {
t.Parallel() t.Parallel()
serverKeys := make([]string, 0, len(servers)) serverKeys := make([]string, 0, len(listServersRes.Servers()))
for _, s := range servers { for _, s := range listServersRes.Servers() {
serverKeys = append(serverKeys, s.Key()) serverKeys = append(serverKeys, s.Key())
} }
@ -450,7 +450,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
t.Run("OK: len(ids) == 0", func(t *testing.T) { t.Run("OK: len(ids) == 0", func(t *testing.T) {
t.Parallel() t.Parallel()
require.NoError(t, repos.tribe.Delete(ctx, servers[0].Key())) require.NoError(t, repos.tribe.Delete(ctx, listServersRes.Servers()[0].Key()))
}) })
}) })
} }

View File

@ -56,10 +56,10 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
t.Run("OK", func(t *testing.T) { t.Run("OK", func(t *testing.T) {
t.Parallel() t.Parallel()
servers, err := repos.server.List(ctx, domain.NewListServersParams()) listServersRes, err := repos.server.List(ctx, domain.NewListServersParams())
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, servers) require.NotEmpty(t, listServersRes)
server := servers[0] server := listServersRes.Servers()[0]
villagesToCreate := domain.BaseVillages{ villagesToCreate := domain.BaseVillages{
domaintest.NewBaseVillage(t), domaintest.NewBaseVillage(t),
@ -263,15 +263,15 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
listServersParams := domain.NewListServersParams() listServersParams := domain.NewListServersParams()
require.NoError(t, listServersParams.SetSpecial(domain.NullBool{Value: false, Valid: true})) require.NoError(t, listServersParams.SetSpecial(domain.NullBool{Value: false, Valid: true}))
servers, listServersErr := repos.server.List(ctx, listServersParams) listServersRes, listServersErr := repos.server.List(ctx, listServersParams)
require.NoError(t, listServersErr) require.NoError(t, listServersErr)
require.NotEmpty(t, servers) require.NotEmpty(t, listServersRes)
t.Run("OK", func(t *testing.T) { t.Run("OK", func(t *testing.T) {
t.Parallel() t.Parallel()
serverKeys := make([]string, 0, len(servers)) serverKeys := make([]string, 0, len(listServersRes.Servers()))
for _, s := range servers { for _, s := range listServersRes.Servers() {
serverKeys = append(serverKeys, s.Key()) serverKeys = append(serverKeys, s.Key())
} }
@ -311,7 +311,7 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
t.Run("OK: len(ids) == 0", func(t *testing.T) { t.Run("OK: len(ids) == 0", func(t *testing.T) {
t.Parallel() t.Parallel()
require.NoError(t, repos.village.Delete(ctx, servers[0].Key())) require.NoError(t, repos.village.Delete(ctx, listServersRes.Servers()[0].Key()))
}) })
}) })
} }

View File

@ -10,7 +10,7 @@ import (
type ServerRepository interface { type ServerRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateServerParams) error CreateOrUpdate(ctx context.Context, params ...domain.CreateServerParams) error
List(ctx context.Context, params domain.ListServersParams) (domain.Servers, error) List(ctx context.Context, params domain.ListServersParams) (domain.ListServersResult, error)
Update(ctx context.Context, key string, params domain.UpdateServerParams) error Update(ctx context.Context, key string, params domain.UpdateServerParams) error
} }
@ -101,11 +101,8 @@ func (svc *ServerService) ListAllOpen(ctx context.Context, versionCode string) (
} }
// ListAll retrieves all servers from the database based on the given params in an optimal way. // ListAll retrieves all servers from the database based on the given params in an optimal way.
// You can't specify a custom limit/offset/sort/keyGT for this operation. // You can't specify a custom limit/cursor/sort for this operation.
func (svc *ServerService) ListAll(ctx context.Context, params domain.ListServersParams) (domain.Servers, error) { func (svc *ServerService) ListAll(ctx context.Context, params domain.ListServersParams) (domain.Servers, error) {
if err := params.SetOffset(0); err != nil {
return nil, err
}
if err := params.SetLimit(domain.ServerListMaxLimit); err != nil { if err := params.SetLimit(domain.ServerListMaxLimit); err != nil {
return nil, err return nil, err
} }
@ -116,21 +113,18 @@ func (svc *ServerService) ListAll(ctx context.Context, params domain.ListServers
var servers domain.Servers var servers domain.Servers
for { for {
ss, err := svc.repo.List(ctx, params) res, err := svc.repo.List(ctx, params)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(ss) == 0 { servers = append(servers, res.Servers()...)
if res.Next().IsZero() {
return servers, nil return servers, nil
} }
servers = append(servers, ss...) if err = params.SetCursor(res.Next()); err != nil {
if err = params.SetKeyGT(domain.NullString{
Value: ss[len(ss)-1].Key(),
Valid: true,
}); err != nil {
return nil, err return nil, err
} }
} }

View File

@ -13,6 +13,29 @@ func RandServerKey() string {
return gofakeit.LetterN(5) return gofakeit.LetterN(5)
} }
type ServerCursorConfig struct {
Key string
Open bool
}
func NewServerCursor(tb TestingTB, opts ...func(cfg *ServerCursorConfig)) domain.ServerCursor {
tb.Helper()
cfg := &ServerCursorConfig{
Key: RandServerKey(),
Open: gofakeit.Bool(),
}
for _, opt := range opts {
opt(cfg)
}
vc, err := domain.NewServerCursor(cfg.Key, cfg.Open)
require.NoError(tb, err)
return vc
}
type ServerConfig struct { type ServerConfig struct {
Key string Key string
VersionCode string VersionCode string

View File

@ -5,6 +5,7 @@ import (
"math" "math"
"net/url" "net/url"
"slices" "slices"
"strconv"
"time" "time"
) )
@ -200,6 +201,10 @@ func (s Server) EnnoblementDataSyncedAt() time.Time {
return s.ennoblementDataSyncedAt return s.ennoblementDataSyncedAt
} }
func (s Server) IsZero() bool {
return s == Server{}
}
func (s Server) Base() BaseServer { func (s Server) Base() BaseServer {
return BaseServer{ return BaseServer{
key: s.key, key: s.key,
@ -464,17 +469,77 @@ const (
ServerSortOpenDESC ServerSortOpenDESC
) )
type ServerCursor struct {
key string
open bool
}
const serverCursorModelName = "ServerCursor"
func NewServerCursor(key string, open bool) (ServerCursor, error) {
if err := validateServerKey(key); err != nil {
return ServerCursor{}, ValidationError{
Model: serverCursorModelName,
Field: "key",
Err: err,
}
}
return ServerCursor{key: key, open: open}, nil
}
func decodeServerCursor(encoded string) (ServerCursor, error) {
m, err := decodeCursor(encoded)
if err != nil {
return ServerCursor{}, err
}
open, err := strconv.ParseBool(m["open"])
if err != nil {
return ServerCursor{}, ErrInvalidCursor
}
vc, err := NewServerCursor(m["key"], open)
if err != nil {
return ServerCursor{}, ErrInvalidCursor
}
return vc, nil
}
func (sc ServerCursor) Key() string {
return sc.key
}
func (sc ServerCursor) Open() bool {
return sc.open
}
func (sc ServerCursor) IsZero() bool {
return sc == ServerCursor{}
}
func (sc ServerCursor) Encode() string {
if sc.IsZero() {
return ""
}
return encodeCursor([][2]string{
{"key", sc.key},
{"open", strconv.FormatBool(sc.open)},
})
}
type ListServersParams struct { type ListServersParams struct {
keys []string keys []string
keyGT NullString
versionCodes []string versionCodes []string
open NullBool open NullBool
special NullBool special NullBool
tribeSnapshotsCreatedAtLT NullTime tribeSnapshotsCreatedAtLT NullTime
playerSnapshotsCreatedAtLT NullTime playerSnapshotsCreatedAtLT NullTime
sort []ServerSort sort []ServerSort
cursor ServerCursor
limit int limit int
offset int
} }
const ( const (
@ -502,15 +567,6 @@ func (params *ListServersParams) SetKeys(keys []string) error {
return nil return nil
} }
func (params *ListServersParams) KeyGT() NullString {
return params.keyGT
}
func (params *ListServersParams) SetKeyGT(keyGT NullString) error {
params.keyGT = keyGT
return nil
}
func (params *ListServersParams) VersionCodes() []string { func (params *ListServersParams) VersionCodes() []string {
return params.versionCodes return params.versionCodes
} }
@ -579,6 +635,30 @@ func (params *ListServersParams) SetSort(sort []ServerSort) error {
return nil return nil
} }
func (params *ListServersParams) Cursor() ServerCursor {
return params.cursor
}
func (params *ListServersParams) SetCursor(cursor ServerCursor) error {
params.cursor = cursor
return nil
}
func (params *ListServersParams) SetEncodedCursor(encoded string) error {
decoded, err := decodeServerCursor(encoded)
if err != nil {
return ValidationError{
Model: listServersParamsModelName,
Field: "cursor",
Err: err,
}
}
params.cursor = decoded
return nil
}
func (params *ListServersParams) Limit() int { func (params *ListServersParams) Limit() int {
return params.limit return params.limit
} }
@ -597,22 +677,55 @@ func (params *ListServersParams) SetLimit(limit int) error {
return nil return nil
} }
func (params *ListServersParams) Offset() int { type ListServersResult struct {
return params.offset servers Servers
self ServerCursor
next ServerCursor
} }
func (params *ListServersParams) SetOffset(offset int) error { const listServersResultModelName = "ListServersResult"
if err := validateIntInRange(offset, 0, math.MaxInt); err != nil {
return ValidationError{ func NewListServersResult(servers Servers, next Server) (ListServersResult, error) {
Model: listServersParamsModelName, var err error
Field: "offset", res := ListServersResult{
Err: err, servers: servers,
}
if len(servers) > 0 {
res.self, err = NewServerCursor(servers[0].Key(), servers[0].Open())
if err != nil {
return ListServersResult{}, ValidationError{
Model: listServersResultModelName,
Field: "self",
Err: err,
}
} }
} }
params.offset = offset if !next.IsZero() {
res.next, err = NewServerCursor(next.Key(), next.Open())
if err != nil {
return ListServersResult{}, ValidationError{
Model: listServersResultModelName,
Field: "next",
Err: err,
}
}
}
return nil return res, nil
}
func (res ListServersResult) Servers() Servers {
return res.servers
}
func (res ListServersResult) Self() ServerCursor {
return res.self
}
func (res ListServersResult) Next() ServerCursor {
return res.next
} }
type ServerNotFoundError struct { type ServerNotFoundError struct {

View File

@ -187,6 +187,65 @@ func TestUpdateServerParams_SetNumTribes(t *testing.T) {
} }
} }
func TestNewServerCursor(t *testing.T) {
t.Parallel()
validServerCursor := domaintest.NewServerCursor(t)
type args struct {
key string
open bool
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
key: validServerCursor.Key(),
open: validServerCursor.Open(),
},
expectedErr: nil,
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
key: serverKeyTest.key,
},
expectedErr: domain.ValidationError{
Model: "ServerCursor",
Field: "key",
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
sc, err := domain.NewServerCursor(tt.args.key, tt.args.open)
require.ErrorIs(t, err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.key, sc.Key())
assert.Equal(t, tt.args.open, sc.Open())
assert.NotEmpty(t, sc.Encode())
})
}
}
func TestListServersParams_SetSort(t *testing.T) { func TestListServersParams_SetSort(t *testing.T) {
t.Parallel() t.Parallel()
@ -260,6 +319,90 @@ func TestListServersParams_SetSort(t *testing.T) {
} }
} }
func TestListServersParams_SetEncodedCursor(t *testing.T) {
t.Parallel()
validCursor := domaintest.NewServerCursor(t)
type args struct {
cursor string
}
tests := []struct {
name string
args args
expectedCursor domain.ServerCursor
expectedErr error
}{
{
name: "OK",
args: args{
cursor: validCursor.Encode(),
},
expectedCursor: validCursor,
},
{
name: "ERR: len(cursor) < 1",
args: args{
cursor: "",
},
expectedErr: domain.ValidationError{
Model: "ListServersParams",
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: "ListServersParams",
Field: "cursor",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: 1001,
},
},
},
{
name: "ERR: malformed base64",
args: args{
cursor: "112345",
},
expectedErr: domain.ValidationError{
Model: "ListServersParams",
Field: "cursor",
Err: domain.ErrInvalidCursor,
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListServersParams()
require.ErrorIs(t, params.SetEncodedCursor(tt.args.cursor), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.expectedCursor.Key(), params.Cursor().Key())
assert.Equal(t, tt.expectedCursor.Open(), params.Cursor().Open())
assert.Equal(t, tt.args.cursor, params.Cursor().Encode())
})
}
}
func TestListServersParams_SetLimit(t *testing.T) { func TestListServersParams_SetLimit(t *testing.T) {
t.Parallel() t.Parallel()
@ -325,55 +468,48 @@ func TestListServersParams_SetLimit(t *testing.T) {
} }
} }
func TestListServersParams_SetOffset(t *testing.T) { func TestNewListServersResult(t *testing.T) {
t.Parallel() t.Parallel()
type args struct { servers := domain.Servers{
offset int domaintest.NewServer(t),
domaintest.NewServer(t),
domaintest.NewServer(t),
} }
next := domaintest.NewServer(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: "ListServersParams",
Field: "offset",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests { res, err := domain.NewListServersResult(servers, next)
tt := tt require.NoError(t, err)
assert.Equal(t, servers, res.Servers())
assert.Equal(t, servers[0].Key(), res.Self().Key())
assert.Equal(t, servers[0].Open(), res.Self().Open())
assert.Equal(t, next.Key(), res.Next().Key())
assert.Equal(t, next.Open(), res.Next().Open())
})
t.Run(tt.name, func(t *testing.T) { t.Run("OK: without next", func(t *testing.T) {
t.Parallel() t.Parallel()
params := domain.NewListServersParams() res, err := domain.NewListServersResult(servers, domain.Server{})
require.NoError(t, err)
assert.Equal(t, servers, res.Servers())
assert.Equal(t, servers[0].Key(), res.Self().Key())
assert.Equal(t, servers[0].Open(), res.Self().Open())
assert.True(t, res.Next().IsZero())
})
require.ErrorIs(t, params.SetOffset(tt.args.offset), tt.expectedErr) t.Run("OK: 0 versions", func(t *testing.T) {
if tt.expectedErr != nil { t.Parallel()
return
} res, err := domain.NewListServersResult(nil, domain.Server{})
assert.Equal(t, tt.args.offset, params.Offset()) require.NoError(t, err)
}) assert.Zero(t, res.Servers())
} assert.True(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
})
} }
type serverKeyValidationTest struct { type serverKeyValidationTest struct {

View File

@ -23,33 +23,42 @@ const (
cursorKeyValueSeparator = "=" cursorKeyValueSeparator = "="
) )
func encodeCursor(m map[string]string) string { func encodeCursor(s [][2]string) string {
if len(m) == 0 { if len(s) == 0 {
return "" return ""
} }
n := len(cursorSeparator) * (len(m) - 1) n := len(cursorSeparator) * (len(s) - 1)
for k, v := range m { for _, el := range s {
n += len(k) + len(v) + len(cursorKeyValueSeparator) n += len(el[0]) + len(el[1]) + len(cursorKeyValueSeparator)
} }
var b bytes.Buffer var b bytes.Buffer
b.Grow(n) b.Grow(n)
for k, v := range m { for _, el := range s {
if b.Len() > 0 { if b.Len() > 0 {
_, _ = b.WriteString(cursorSeparator) _, _ = b.WriteString(cursorSeparator)
} }
_, _ = b.WriteString(k) _, _ = b.WriteString(el[0])
_, _ = b.WriteString(cursorKeyValueSeparator) _, _ = b.WriteString(cursorKeyValueSeparator)
_, _ = b.WriteString(v) _, _ = b.WriteString(el[1])
} }
return base64.StdEncoding.EncodeToString(b.Bytes()) return base64.StdEncoding.EncodeToString(b.Bytes())
} }
const (
encodedCursorMinLength = 1
encodedCursorMaxLength = 1000
)
func decodeCursor(s string) (map[string]string, error) { func decodeCursor(s string) (map[string]string, error) {
if err := validateStringLen(s, encodedCursorMinLength, encodedCursorMaxLength); err != nil {
return nil, err
}
decodedBytes, err := base64.StdEncoding.DecodeString(s) decodedBytes, err := base64.StdEncoding.DecodeString(s)
if err != nil { if err != nil {
return nil, ErrInvalidCursor return nil, ErrInvalidCursor

View File

@ -125,16 +125,7 @@ func NewVersionCursor(code string) (VersionCursor, error) {
}, nil }, nil
} }
const (
encodedVersionCursorMinLength = 1
encodedVersionCursorMaxLength = 1000
)
func decodeVersionCursor(encoded string) (VersionCursor, error) { func decodeVersionCursor(encoded string) (VersionCursor, error) {
if err := validateStringLen(encoded, encodedVersionCursorMinLength, encodedVersionCursorMaxLength); err != nil {
return VersionCursor{}, err
}
m, err := decodeCursor(encoded) m, err := decodeCursor(encoded)
if err != nil { if err != nil {
return VersionCursor{}, err return VersionCursor{}, err
@ -161,8 +152,8 @@ func (vc VersionCursor) Encode() string {
return "" return ""
} }
return encodeCursor(map[string]string{ return encodeCursor([][2]string{
"code": vc.code, {"code", vc.code},
}) })
} }

View File

@ -214,7 +214,6 @@ func TestListVersionsParams_SetEncodedCursor(t *testing.T) {
require.ErrorIs(t, params.SetEncodedCursor(tt.args.cursor), tt.expectedErr) require.ErrorIs(t, params.SetEncodedCursor(tt.args.cursor), tt.expectedErr)
if tt.expectedErr != nil { if tt.expectedErr != nil {
fmt.Println(tt.expectedErr.Error())
return return
} }
assert.Equal(t, tt.expectedCursor.Code(), params.Cursor().Code()) assert.Equal(t, tt.expectedCursor.Code(), params.Cursor().Code())

View File

@ -215,19 +215,16 @@ func TestDataSync(t *testing.T) {
allServers := make(domain.Servers, 0, len(expectedServers)) allServers := make(domain.Servers, 0, len(expectedServers))
for { for {
servers, err := serverRepo.List(ctx, listParams) res, err := serverRepo.List(ctx, listParams)
require.NoError(collect, err) require.NoError(collect, err)
if len(servers) == 0 { allServers = append(allServers, res.Servers()...)
if res.Next().IsZero() {
break break
} }
allServers = append(allServers, servers...) require.NoError(collect, listParams.SetCursor(res.Next()))
require.NoError(collect, listParams.SetKeyGT(domain.NullString{
Value: servers[len(servers)-1].Key(),
Valid: true,
}))
} }
if !assert.Len(collect, allServers, len(expectedServers)) { if !assert.Len(collect, allServers, len(expectedServers)) {

View File

@ -173,21 +173,18 @@ func TestEnnoblementSync(t *testing.T) {
require.NoError(collect, listParams.SetLimit(domain.ServerListMaxLimit)) require.NoError(collect, listParams.SetLimit(domain.ServerListMaxLimit))
for { for {
servers, err := serverRepo.List(ctx, listParams) res, err := serverRepo.List(ctx, listParams)
require.NoError(collect, err) require.NoError(collect, err)
if len(servers) == 0 { for _, s := range res.Servers() {
break
}
for _, s := range servers {
assert.WithinDuration(collect, time.Now(), s.EnnoblementDataSyncedAt(), time.Minute) assert.WithinDuration(collect, time.Now(), s.EnnoblementDataSyncedAt(), time.Minute)
} }
require.NoError(collect, listParams.SetKeyGT(domain.NullString{ if res.Next().IsZero() {
Value: servers[len(servers)-1].Key(), break
Valid: true, }
}))
require.NoError(collect, listParams.SetCursor(res.Next()))
} }
}, 30*time.Second, time.Second, "servers") }, 30*time.Second, time.Second, "servers")

View File

@ -157,22 +157,19 @@ func TestSnapshotCreation(t *testing.T) {
require.NoError(collect, listParams.SetLimit(domain.ServerListMaxLimit)) require.NoError(collect, listParams.SetLimit(domain.ServerListMaxLimit))
for { for {
servers, err := serverRepo.List(ctx, listParams) res, err := serverRepo.List(ctx, listParams)
require.NoError(collect, err) require.NoError(collect, err)
if len(servers) == 0 { for _, s := range res.Servers() {
return
}
for _, s := range servers {
assert.WithinDuration(collect, time.Now(), s.PlayerSnapshotsCreatedAt(), time.Minute, s.Key()) assert.WithinDuration(collect, time.Now(), s.PlayerSnapshotsCreatedAt(), time.Minute, s.Key())
assert.WithinDuration(collect, time.Now(), s.TribeSnapshotsCreatedAt(), time.Minute, s.Key()) assert.WithinDuration(collect, time.Now(), s.TribeSnapshotsCreatedAt(), time.Minute, s.Key())
} }
require.NoError(collect, listParams.SetKeyGT(domain.NullString{ if res.Next().IsZero() {
Value: servers[len(servers)-1].Key(), return
Valid: true, }
}))
require.NoError(collect, listParams.SetCursor(res.Next()))
} }
}, 30*time.Second, 500*time.Millisecond, "servers") }, 30*time.Second, 500*time.Millisecond, "servers")