From 41cb1d042ea1b0e3dc56208ffbe37047ff32590d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Sat, 16 Mar 2024 07:43:46 +0100 Subject: [PATCH] refactor: player snapshot - cursor pagination --- .../adapter/repository_bun_player_snapshot.go | 59 ++++- .../repository_player_snapshot_test.go | 105 ++++++-- internal/adapter/repository_test.go | 2 +- internal/domain/domaintest/player_snapshot.go | 90 +++++++ internal/domain/player_snapshot.go | 211 ++++++++++++++- internal/domain/player_snapshot_test.go | 240 +++++++++++++++--- .../port/consumer_snapshot_creation_test.go | 14 +- 7 files changed, 628 insertions(+), 93 deletions(-) create mode 100644 internal/domain/domaintest/player_snapshot.go diff --git a/internal/adapter/repository_bun_player_snapshot.go b/internal/adapter/repository_bun_player_snapshot.go index 47a3254..c0371ec 100644 --- a/internal/adapter/repository_bun_player_snapshot.go +++ b/internal/adapter/repository_bun_player_snapshot.go @@ -59,17 +59,22 @@ func (repo *PlayerSnapshotBunRepository) Create( func (repo *PlayerSnapshotBunRepository) List( ctx context.Context, params domain.ListPlayerSnapshotsParams, -) (domain.PlayerSnapshots, error) { +) (domain.ListPlayerSnapshotsResult, error) { var playerSnapshots bunmodel.PlayerSnapshots if err := repo.db.NewSelect(). Model(&playerSnapshots). Apply(listPlayerSnapshotsParamsApplier{params: params}.apply). Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) { - return nil, fmt.Errorf("couldn't select player snapshots from the db: %w", err) + return domain.ListPlayerSnapshotsResult{}, fmt.Errorf("couldn't select player snapshots from the db: %w", err) } - return playerSnapshots.ToDomain() + converted, err := playerSnapshots.ToDomain() + if err != nil { + return domain.ListPlayerSnapshotsResult{}, err + } + + return domain.NewListPlayerSnapshotsResult(separateListResultAndNext(converted, params.Limit())) } type listPlayerSnapshotsParamsApplier struct { @@ -81,6 +86,10 @@ func (a listPlayerSnapshotsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQ q = q.Where("ps.server_key IN (?)", bun.In(serverKeys)) } + if playerIDs := a.params.PlayerIDs(); len(playerIDs) > 0 { + q = q.Where("ps.player_id IN (?)", bun.In(playerIDs)) + } + for _, s := range a.params.Sort() { column, dir, err := a.sortToColumnAndDirection(s) if err != nil { @@ -90,7 +99,49 @@ func (a listPlayerSnapshotsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQ q.OrderExpr("? ?", column, dir.Bun()) } - return q.Limit(a.params.Limit()).Offset(a.params.Offset()) + return q.Limit(a.params.Limit() + 1).Apply(a.applyCursor) +} + +func (a listPlayerSnapshotsParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuery { + cursor := a.params.Cursor() + + if cursor.IsZero() { + return q + } + + sort := a.params.Sort() + cursorApplier := cursorPaginationApplier{ + data: make([]cursorPaginationApplierDataElement, 0, len(sort)), + } + + for _, s := range sort { + var err error + var el cursorPaginationApplierDataElement + + el.column, el.direction, err = a.sortToColumnAndDirection(s) + if err != nil { + return q.Err(err) + } + + switch s { + case domain.PlayerSnapshotSortIDASC, + domain.PlayerSnapshotSortIDDESC: + el.value = cursor.ID() + el.unique = true + case domain.PlayerSnapshotSortServerKeyASC, + domain.PlayerSnapshotSortServerKeyDESC: + el.value = cursor.ServerKey() + case domain.PlayerSnapshotSortDateASC, + domain.PlayerSnapshotSortDateDESC: + el.value = cursor.Date() + default: + return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue)) + } + + cursorApplier.data = append(cursorApplier.data, el) + } + + return q.Apply(cursorApplier.apply) } func (a listPlayerSnapshotsParamsApplier) sortToColumnAndDirection( diff --git a/internal/adapter/repository_player_snapshot_test.go b/internal/adapter/repository_player_snapshot_test.go index b03f5fe..3270620 100644 --- a/internal/adapter/repository_player_snapshot_test.go +++ b/internal/adapter/repository_player_snapshot_test.go @@ -30,7 +30,8 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo require.NotEmpty(t, params) - playerSnapshots, err := repos.playerSnapshot.List(ctx, domain.NewListPlayerSnapshotsParams()) + res, err := repos.playerSnapshot.List(ctx, domain.NewListPlayerSnapshotsParams()) + playerSnapshots := res.PlayerSnapshots() require.NoError(t, err) for i, p := range params { date := p.Date().Format(dateFormat) @@ -60,8 +61,9 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo require.NotEmpty(t, params) - playerSnapshots, err := repos.playerSnapshot.List(ctx, domain.NewListPlayerSnapshotsParams()) + res, err := repos.playerSnapshot.List(ctx, domain.NewListPlayerSnapshotsParams()) require.NoError(t, err) + playerSnapshots := res.PlayerSnapshots() m := make(map[string][]int) @@ -119,18 +121,13 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo repos := newRepos(t) - playerSnapshots, listPlayerSnapshotsErr := repos.playerSnapshot.List(ctx, domain.NewListPlayerSnapshotsParams()) - require.NoError(t, listPlayerSnapshotsErr) - require.NotEmpty(t, playerSnapshots) - randPlayerSnapshot := playerSnapshots[0] - tests := []struct { - name string - params func(t *testing.T) domain.ListPlayerSnapshotsParams - assertPlayerSnapshots func( + name string + params func(t *testing.T) domain.ListPlayerSnapshotsParams + assertResult func( t *testing.T, params domain.ListPlayerSnapshotsParams, - playerSnapshots domain.PlayerSnapshots, + res domain.ListPlayerSnapshotsResult, ) assertError func(t *testing.T, err error) }{ @@ -140,12 +137,13 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo t.Helper() return domain.NewListPlayerSnapshotsParams() }, - assertPlayerSnapshots: func( + assertResult: func( t *testing.T, _ domain.ListPlayerSnapshotsParams, - playerSnapshots domain.PlayerSnapshots, + res domain.ListPlayerSnapshotsResult, ) { t.Helper() + playerSnapshots := res.PlayerSnapshots() assert.NotEmpty(t, len(playerSnapshots)) assert.True(t, slices.IsSortedFunc(playerSnapshots, func(a, b domain.PlayerSnapshot) int { return cmp.Or( @@ -154,6 +152,8 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo cmp.Compare(a.ID(), b.ID()), ) })) + assert.False(t, res.Self().IsZero()) + assert.True(t, res.Next().IsZero()) }, }, { @@ -167,12 +167,13 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo })) return params }, - assertPlayerSnapshots: func( + assertResult: func( t *testing.T, _ domain.ListPlayerSnapshotsParams, - playerSnapshots domain.PlayerSnapshots, + res domain.ListPlayerSnapshotsResult, ) { t.Helper() + playerSnapshots := res.PlayerSnapshots() assert.NotEmpty(t, len(playerSnapshots)) assert.True(t, slices.IsSortedFunc(playerSnapshots, func(a, b domain.PlayerSnapshot) int { return cmp.Or( @@ -192,12 +193,13 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo })) return params }, - assertPlayerSnapshots: func( + assertResult: func( t *testing.T, _ domain.ListPlayerSnapshotsParams, - playerSnapshots domain.PlayerSnapshots, + res domain.ListPlayerSnapshotsResult, ) { t.Helper() + playerSnapshots := res.PlayerSnapshots() assert.NotEmpty(t, len(playerSnapshots)) assert.True(t, slices.IsSortedFunc(playerSnapshots, func(a, b domain.PlayerSnapshot) int { return cmp.Compare(a.ID(), b.ID()) @@ -214,12 +216,13 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo })) return params }, - assertPlayerSnapshots: func( + assertResult: func( t *testing.T, _ domain.ListPlayerSnapshotsParams, - playerSnapshots domain.PlayerSnapshots, + res domain.ListPlayerSnapshotsResult, ) { t.Helper() + playerSnapshots := res.PlayerSnapshots() assert.NotEmpty(t, len(playerSnapshots)) assert.True(t, slices.IsSortedFunc(playerSnapshots, func(a, b domain.PlayerSnapshot) int { return cmp.Compare(a.ID(), b.ID()) * -1 @@ -227,43 +230,89 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo }, }, { - name: fmt.Sprintf("OK: serverKeys=[%s]", randPlayerSnapshot.ServerKey()), + name: "OK: serverKeys", params: func(t *testing.T) domain.ListPlayerSnapshotsParams { t.Helper() + params := domain.NewListPlayerSnapshotsParams() + + res, err := repos.playerSnapshot.List(ctx, params) + require.NoError(t, err) + require.NotEmpty(t, res.PlayerSnapshots()) + randPlayerSnapshot := res.PlayerSnapshots()[0] + require.NoError(t, params.SetServerKeys([]string{randPlayerSnapshot.ServerKey()})) + return params }, - assertPlayerSnapshots: func( + assertResult: func( t *testing.T, params domain.ListPlayerSnapshotsParams, - playerSnapshots domain.PlayerSnapshots, + res domain.ListPlayerSnapshotsResult, ) { t.Helper() serverKeys := params.ServerKeys() + playerSnapshots := res.PlayerSnapshots() + assert.NotZero(t, playerSnapshots) for _, ps := range playerSnapshots { assert.True(t, slices.Contains(serverKeys, ps.ServerKey())) } }, }, { - name: "OK: offset=1 limit=2", + name: "OK: playerIDs serverKeys", + params: func(t *testing.T) domain.ListPlayerSnapshotsParams { + t.Helper() + + params := domain.NewListPlayerSnapshotsParams() + + res, err := repos.playerSnapshot.List(ctx, params) + require.NoError(t, err) + require.NotEmpty(t, res.PlayerSnapshots()) + randPlayerSnapshot := res.PlayerSnapshots()[0] + + require.NoError(t, params.SetServerKeys([]string{randPlayerSnapshot.ServerKey()})) + require.NoError(t, params.SetPlayerIDs([]int{randPlayerSnapshot.PlayerID()})) + + return params + }, + assertResult: func( + t *testing.T, + params domain.ListPlayerSnapshotsParams, + res domain.ListPlayerSnapshotsResult, + ) { + t.Helper() + + serverKeys := params.ServerKeys() + playerIDs := params.PlayerIDs() + + playerSnapshots := res.PlayerSnapshots() + assert.NotZero(t, playerSnapshots) + for _, ps := range playerSnapshots { + assert.True(t, slices.Contains(serverKeys, ps.ServerKey())) + assert.True(t, slices.Contains(playerIDs, ps.PlayerID())) + } + }, + }, + { + name: "OK: limit=2", params: func(t *testing.T) domain.ListPlayerSnapshotsParams { t.Helper() params := domain.NewListPlayerSnapshotsParams() - require.NoError(t, params.SetOffset(1)) require.NoError(t, params.SetLimit(2)) return params }, - assertPlayerSnapshots: func( + assertResult: func( t *testing.T, params domain.ListPlayerSnapshotsParams, - playerSnapshots domain.PlayerSnapshots, + res domain.ListPlayerSnapshotsResult, ) { t.Helper() - assert.Len(t, playerSnapshots, params.Limit()) + assert.Len(t, res.PlayerSnapshots(), params.Limit()) + assert.False(t, res.Self().IsZero()) + assert.False(t, res.Next().IsZero()) }, }, } @@ -284,7 +333,7 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo res, err := repos.playerSnapshot.List(ctx, params) assertError(t, err) - tt.assertPlayerSnapshots(t, params, res) + tt.assertResult(t, params, res) }) } }) diff --git a/internal/adapter/repository_test.go b/internal/adapter/repository_test.go index 6de9714..02623ff 100644 --- a/internal/adapter/repository_test.go +++ b/internal/adapter/repository_test.go @@ -70,7 +70,7 @@ type tribeSnapshotRepository interface { type playerSnapshotRepository interface { Create(ctx context.Context, params ...domain.CreatePlayerSnapshotParams) error - List(ctx context.Context, params domain.ListPlayerSnapshotsParams) (domain.PlayerSnapshots, error) + List(ctx context.Context, params domain.ListPlayerSnapshotsParams) (domain.ListPlayerSnapshotsResult, error) } type repositories struct { diff --git a/internal/domain/domaintest/player_snapshot.go b/internal/domain/domaintest/player_snapshot.go new file mode 100644 index 0000000..f261930 --- /dev/null +++ b/internal/domain/domaintest/player_snapshot.go @@ -0,0 +1,90 @@ +package domaintest + +import ( + "time" + + "gitea.dwysokinski.me/twhelp/corev3/internal/domain" + "github.com/brianvoe/gofakeit/v7" + "github.com/stretchr/testify/require" +) + +type PlayerSnapshotCursorConfig struct { + ID int + ServerKey string + Date time.Time +} + +func NewPlayerSnapshotCursor(tb TestingTB, opts ...func(cfg *PlayerSnapshotCursorConfig)) domain.PlayerSnapshotCursor { + tb.Helper() + + cfg := &PlayerSnapshotCursorConfig{ + ID: RandID(), + ServerKey: RandServerKey(), + Date: gofakeit.Date(), + } + + for _, opt := range opts { + opt(cfg) + } + + psc, err := domain.NewPlayerSnapshotCursor( + cfg.ID, + cfg.ServerKey, + cfg.Date, + ) + require.NoError(tb, err) + + return psc +} + +type PlayerSnapshotConfig struct { + ID int + PlayerID int + ServerKey string + NumVillages int + Points int + Rank int + TribeID int + OD domain.OpponentsDefeated + Date time.Time + CreatedAt time.Time +} + +func NewPlayerSnapshot(tb TestingTB, opts ...func(cfg *PlayerSnapshotConfig)) domain.PlayerSnapshot { + tb.Helper() + + now := time.Now() + + cfg := &PlayerSnapshotConfig{ + ID: RandID(), + PlayerID: RandID(), + ServerKey: RandServerKey(), + NumVillages: gofakeit.IntRange(1, 10000), + Points: gofakeit.IntRange(1, 10000), + Rank: gofakeit.IntRange(1, 10000), + TribeID: RandID(), + OD: NewOpponentsDefeated(tb), + Date: now, + CreatedAt: now, + } + + for _, opt := range opts { + opt(cfg) + } + + ps, err := domain.UnmarshalPlayerSnapshotFromDatabase( + cfg.ID, + cfg.PlayerID, + cfg.ServerKey, + cfg.NumVillages, + cfg.Points, + cfg.Rank, + cfg.TribeID, + cfg.OD, + cfg.Date, + cfg.CreatedAt, + ) + require.NoError(tb, err) + + return ps +} diff --git a/internal/domain/player_snapshot.go b/internal/domain/player_snapshot.go index 9dd884f..b1cb633 100644 --- a/internal/domain/player_snapshot.go +++ b/internal/domain/player_snapshot.go @@ -115,6 +115,14 @@ func (ps PlayerSnapshot) CreatedAt() time.Time { return ps.createdAt } +func (ps PlayerSnapshot) ToCursor() (PlayerSnapshotCursor, error) { + return NewPlayerSnapshotCursor(ps.id, ps.serverKey, ps.date) +} + +func (ps PlayerSnapshot) IsZero() bool { + return ps == PlayerSnapshot{} +} + type PlayerSnapshots []PlayerSnapshot type CreatePlayerSnapshotParams struct { @@ -224,16 +232,112 @@ func (s PlayerSnapshotSort) String() string { } } -const PlayerSnapshotListMaxLimit = 500 +type PlayerSnapshotCursor struct { + id int + serverKey string + date time.Time +} + +const playerSnapshotCursorModelName = "PlayerSnapshotCursor" + +func NewPlayerSnapshotCursor(id int, serverKey string, date time.Time) (PlayerSnapshotCursor, error) { + if err := validateIntInRange(id, 1, math.MaxInt); err != nil { + return PlayerSnapshotCursor{}, ValidationError{ + Model: playerSnapshotCursorModelName, + Field: "id", + Err: err, + } + } + + if err := validateServerKey(serverKey); err != nil { + return PlayerSnapshotCursor{}, ValidationError{ + Model: playerSnapshotCursorModelName, + Field: "serverKey", + Err: err, + } + } + + return PlayerSnapshotCursor{ + id: id, + serverKey: serverKey, + date: date, + }, nil +} + +//nolint:gocyclo +func decodePlayerSnapshotCursor(encoded string) (PlayerSnapshotCursor, error) { + m, err := decodeCursor(encoded) + if err != nil { + return PlayerSnapshotCursor{}, err + } + + id, err := m.int("id") + if err != nil { + return PlayerSnapshotCursor{}, ErrInvalidCursor + } + + serverKey, err := m.string("serverKey") + if err != nil { + return PlayerSnapshotCursor{}, ErrInvalidCursor + } + + date, err := m.time("date") + if err != nil { + return PlayerSnapshotCursor{}, ErrInvalidCursor + } + + psc, err := NewPlayerSnapshotCursor( + id, + serverKey, + date, + ) + if err != nil { + return PlayerSnapshotCursor{}, ErrInvalidCursor + } + + return psc, nil +} + +func (psc PlayerSnapshotCursor) ID() int { + return psc.id +} + +func (psc PlayerSnapshotCursor) ServerKey() string { + return psc.serverKey +} + +func (psc PlayerSnapshotCursor) Date() time.Time { + return psc.date +} + +func (psc PlayerSnapshotCursor) IsZero() bool { + return psc == PlayerSnapshotCursor{} +} + +func (psc PlayerSnapshotCursor) Encode() string { + if psc.IsZero() { + return "" + } + + return encodeCursor([]keyValuePair{ + {"id", psc.id}, + {"serverKey", psc.serverKey}, + {"date", psc.date}, + }) +} type ListPlayerSnapshotsParams struct { serverKeys []string + playerIDs []int sort []PlayerSnapshotSort + cursor PlayerSnapshotCursor limit int - offset int } -const listPlayerSnapshotsParamsModelName = "ListPlayerSnapshotsParams" +const ( + PlayerSnapshotListMaxLimit = 500 + listPlayerSnapshotsParamsModelName = "ListPlayerSnapshotsParams" +) func NewListPlayerSnapshotsParams() ListPlayerSnapshotsParams { return ListPlayerSnapshotsParams{ @@ -267,6 +371,27 @@ func (params *ListPlayerSnapshotsParams) SetServerKeys(serverKeys []string) erro return nil } +func (params *ListPlayerSnapshotsParams) PlayerIDs() []int { + return params.playerIDs +} + +func (params *ListPlayerSnapshotsParams) SetPlayerIDs(playerIDs []int) error { + for i, id := range playerIDs { + if err := validateIntInRange(id, 1, math.MaxInt); err != nil { + return SliceElementValidationError{ + Model: listPlayerSnapshotsParamsModelName, + Field: "playerIDs", + Index: i, + Err: err, + } + } + } + + params.playerIDs = playerIDs + + return nil +} + func (params *ListPlayerSnapshotsParams) Sort() []PlayerSnapshotSort { return params.sort } @@ -290,6 +415,30 @@ func (params *ListPlayerSnapshotsParams) SetSort(sort []PlayerSnapshotSort) erro return nil } +func (params *ListPlayerSnapshotsParams) Cursor() PlayerSnapshotCursor { + return params.cursor +} + +func (params *ListPlayerSnapshotsParams) SetCursor(cursor PlayerSnapshotCursor) error { + params.cursor = cursor + return nil +} + +func (params *ListPlayerSnapshotsParams) SetEncodedCursor(encoded string) error { + decoded, err := decodePlayerSnapshotCursor(encoded) + if err != nil { + return ValidationError{ + Model: listPlayerSnapshotsParamsModelName, + Field: "cursor", + Err: err, + } + } + + params.cursor = decoded + + return nil +} + func (params *ListPlayerSnapshotsParams) Limit() int { return params.limit } @@ -308,20 +457,56 @@ func (params *ListPlayerSnapshotsParams) SetLimit(limit int) error { return nil } -func (params *ListPlayerSnapshotsParams) Offset() int { - return params.offset +type ListPlayerSnapshotsResult struct { + snapshots PlayerSnapshots + self PlayerSnapshotCursor + next PlayerSnapshotCursor } -func (params *ListPlayerSnapshotsParams) SetOffset(offset int) error { - if err := validateIntInRange(offset, 0, math.MaxInt); err != nil { - return ValidationError{ - Model: listPlayerSnapshotsParamsModelName, - Field: "offset", - Err: err, +const listPlayerSnapshotsResultModelName = "ListPlayerSnapshotsResult" + +func NewListPlayerSnapshotsResult( + snapshots PlayerSnapshots, + next PlayerSnapshot, +) (ListPlayerSnapshotsResult, error) { + var err error + res := ListPlayerSnapshotsResult{ + snapshots: snapshots, + } + + if len(snapshots) > 0 { + res.self, err = snapshots[0].ToCursor() + if err != nil { + return ListPlayerSnapshotsResult{}, ValidationError{ + Model: listPlayerSnapshotsResultModelName, + Field: "self", + Err: err, + } } } - params.offset = offset + if !next.IsZero() { + res.next, err = next.ToCursor() + if err != nil { + return ListPlayerSnapshotsResult{}, ValidationError{ + Model: listPlayerSnapshotsResultModelName, + Field: "next", + Err: err, + } + } + } - return nil + return res, nil +} + +func (res ListPlayerSnapshotsResult) PlayerSnapshots() PlayerSnapshots { + return res.snapshots +} + +func (res ListPlayerSnapshotsResult) Self() PlayerSnapshotCursor { + return res.self +} + +func (res ListPlayerSnapshotsResult) Next() PlayerSnapshotCursor { + return res.next } diff --git a/internal/domain/player_snapshot_test.go b/internal/domain/player_snapshot_test.go index 31d601c..a229014 100644 --- a/internal/domain/player_snapshot_test.go +++ b/internal/domain/player_snapshot_test.go @@ -8,6 +8,7 @@ import ( "gitea.dwysokinski.me/twhelp/corev3/internal/domain" "gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest" + "github.com/brianvoe/gofakeit/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -109,6 +110,87 @@ func TestPlayerSnapshotSort_IsInConflict(t *testing.T) { } } +func TestNewPlayerSnapshotCursor(t *testing.T) { + t.Parallel() + + validPlayerSnapshotCursor := domaintest.NewPlayerSnapshotCursor(t) + + type args struct { + id int + serverKey string + date time.Time + } + + type test struct { + name string + args args + expectedErr error + } + + tests := []test{ + { + name: "OK", + args: args{ + id: validPlayerSnapshotCursor.ID(), + serverKey: validPlayerSnapshotCursor.ServerKey(), + date: validPlayerSnapshotCursor.Date(), + }, + expectedErr: nil, + }, + { + name: "ERR: id < 1", + args: args{ + id: 0, + serverKey: validPlayerSnapshotCursor.ServerKey(), + date: validPlayerSnapshotCursor.Date(), + }, + expectedErr: domain.ValidationError{ + Model: "PlayerSnapshotCursor", + Field: "id", + Err: domain.MinGreaterEqualError{ + Min: 1, + Current: 0, + }, + }, + }, + } + + for _, serverKeyTest := range newServerKeyValidationTests() { + tests = append(tests, test{ + name: serverKeyTest.name, + args: args{ + id: validPlayerSnapshotCursor.ID(), + serverKey: serverKeyTest.key, + }, + expectedErr: domain.ValidationError{ + Model: "PlayerSnapshotCursor", + Field: "serverKey", + Err: serverKeyTest.expectedErr, + }, + }) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + psc, err := domain.NewPlayerSnapshotCursor( + tt.args.id, + tt.args.serverKey, + tt.args.date, + ) + require.ErrorIs(t, err, tt.expectedErr) + if tt.expectedErr != nil { + return + } + assert.Equal(t, tt.args.id, psc.ID()) + assert.Equal(t, tt.args.serverKey, psc.ServerKey()) + assert.Equal(t, tt.args.date, psc.Date()) + assert.NotEmpty(t, psc.Encode()) + }) + } +} + func TestListPlayerSnapshotsParams_SetServerKeys(t *testing.T) { t.Parallel() @@ -252,6 +334,86 @@ func TestListPlayerSnapshotsParams_SetSort(t *testing.T) { } } +func TestListPlayerSnapshotsParams_SetEncodedCursor(t *testing.T) { + t.Parallel() + + validCursor := domaintest.NewPlayerSnapshotCursor(t) + + type args struct { + cursor string + } + + tests := []struct { + name string + args args + expectedCursor domain.PlayerSnapshotCursor + expectedErr error + }{ + { + name: "OK", + args: args{ + cursor: validCursor.Encode(), + }, + expectedCursor: validCursor, + }, + { + name: "ERR: len(cursor) < 1", + args: args{ + cursor: "", + }, + expectedErr: domain.ValidationError{ + Model: "ListPlayerSnapshotsParams", + 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: "ListPlayerSnapshotsParams", + Field: "cursor", + Err: domain.LenOutOfRangeError{ + Min: 1, + Max: 1000, + Current: 1001, + }, + }, + }, + { + name: "ERR: malformed base64", + args: args{ + cursor: "112345", + }, + expectedErr: domain.ValidationError{ + Model: "ListPlayerSnapshotsParams", + Field: "cursor", + Err: domain.ErrInvalidCursor, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + params := domain.NewListPlayerSnapshotsParams() + + require.ErrorIs(t, params.SetEncodedCursor(tt.args.cursor), tt.expectedErr) + if tt.expectedErr != nil { + return + } + assert.Equal(t, tt.args.cursor, params.Cursor().Encode()) + }) + } +} + func TestListPlayerSnapshotsParams_SetLimit(t *testing.T) { t.Parallel() @@ -315,51 +477,49 @@ func TestListPlayerSnapshotsParams_SetLimit(t *testing.T) { } } -func TestListPlayerSnapshotsParams_SetOffset(t *testing.T) { +func TestNewListPlayerSnapshotsResult(t *testing.T) { t.Parallel() - type args struct { - offset int + snapshots := domain.PlayerSnapshots{ + domaintest.NewPlayerSnapshot(t), + domaintest.NewPlayerSnapshot(t), + domaintest.NewPlayerSnapshot(t), } + next := domaintest.NewPlayerSnapshot(t) - tests := []struct { - name string - args args - expectedErr error - }{ - { - name: "OK", - args: args{ - offset: 100, - }, - }, - { - name: "ERR: offset < 0", - args: args{ - offset: -1, - }, - expectedErr: domain.ValidationError{ - Model: "ListPlayerSnapshotsParams", - Field: "offset", - Err: domain.MinGreaterEqualError{ - Min: 0, - Current: -1, - }, - }, - }, - } + t.Run("OK: with next", func(t *testing.T) { + t.Parallel() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() + res, err := domain.NewListPlayerSnapshotsResult(snapshots, next) + require.NoError(t, err) + assert.Equal(t, snapshots, res.PlayerSnapshots()) + assert.Equal(t, snapshots[0].ID(), res.Self().ID()) + assert.Equal(t, snapshots[0].ServerKey(), res.Self().ServerKey()) + assert.Equal(t, snapshots[0].Date(), res.Self().Date()) + assert.Equal(t, next.ID(), res.Next().ID()) + assert.Equal(t, next.ServerKey(), res.Next().ServerKey()) + assert.Equal(t, next.Date(), res.Next().Date()) + }) - params := domain.NewListPlayerSnapshotsParams() + t.Run("OK: without next", func(t *testing.T) { + t.Parallel() - require.ErrorIs(t, params.SetOffset(tt.args.offset), tt.expectedErr) - if tt.expectedErr != nil { - return - } - assert.Equal(t, tt.args.offset, params.Offset()) - }) - } + res, err := domain.NewListPlayerSnapshotsResult(snapshots, domain.PlayerSnapshot{}) + require.NoError(t, err) + assert.Equal(t, snapshots, res.PlayerSnapshots()) + assert.Equal(t, snapshots[0].ID(), res.Self().ID()) + assert.Equal(t, snapshots[0].ServerKey(), res.Self().ServerKey()) + assert.Equal(t, snapshots[0].Date(), res.Self().Date()) + assert.True(t, res.Next().IsZero()) + }) + + t.Run("OK: 0 snapshots", func(t *testing.T) { + t.Parallel() + + res, err := domain.NewListPlayerSnapshotsResult(nil, domain.PlayerSnapshot{}) + require.NoError(t, err) + assert.Zero(t, res.PlayerSnapshots()) + assert.True(t, res.Self().IsZero()) + assert.True(t, res.Next().IsZero()) + }) } diff --git a/internal/port/consumer_snapshot_creation_test.go b/internal/port/consumer_snapshot_creation_test.go index d068935..38af92c 100644 --- a/internal/port/consumer_snapshot_creation_test.go +++ b/internal/port/consumer_snapshot_creation_test.go @@ -316,14 +316,10 @@ func TestSnapshotCreation(t *testing.T) { cnt := 0 for { - snapshots, err := playerSnapshotRepo.List(ctx, listSnapshotsParams) + res, err := playerSnapshotRepo.List(ctx, listSnapshotsParams) require.NoError(collect, err) - if len(snapshots) == 0 { - break - } - - for _, ps := range snapshots { + for _, ps := range res.PlayerSnapshots() { cnt++ msg := fmt.Sprintf("PlayerID=%d,ServerKey=%s", ps.PlayerID(), ps.ServerKey()) @@ -353,7 +349,11 @@ func TestSnapshotCreation(t *testing.T) { assert.WithinDuration(collect, time.Now(), ps.Date(), 24*time.Hour, msg) } - require.NoError(collect, listSnapshotsParams.SetOffset(listSnapshotsParams.Offset()+listSnapshotsParams.Limit())) + if res.Next().IsZero() { + break + } + + require.NoError(collect, listSnapshotsParams.SetCursor(res.Next())) } //nolint:testifylint