diff --git a/internal/adapter/repository_bun_ennoblement.go b/internal/adapter/repository_bun_ennoblement.go index 9ff926a..21818b5 100644 --- a/internal/adapter/repository_bun_ennoblement.go +++ b/internal/adapter/repository_bun_ennoblement.go @@ -54,17 +54,22 @@ func (repo *EnnoblementBunRepository) Create(ctx context.Context, params ...doma func (repo *EnnoblementBunRepository) List( ctx context.Context, params domain.ListEnnoblementsParams, -) (domain.Ennoblements, error) { +) (domain.ListEnnoblementsResult, error) { var ennoblements bunmodel.Ennoblements if err := repo.db.NewSelect(). Model(&ennoblements). Apply(listEnnoblementsParamsApplier{params: params}.apply). Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) { - return nil, fmt.Errorf("couldn't select ennoblements from the db: %w", err) + return domain.ListEnnoblementsResult{}, fmt.Errorf("couldn't select ennoblements from the db: %w", err) } - return ennoblements.ToDomain() + converted, err := ennoblements.ToDomain() + if err != nil { + return domain.ListEnnoblementsResult{}, err + } + + return domain.NewListEnnoblementsResult(separateListResultAndNext(converted, params.Limit())) } type listEnnoblementsParamsApplier struct { @@ -96,5 +101,57 @@ func (a listEnnoblementsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuer } } - return q.Limit(a.params.Limit()).Offset(a.params.Offset()) + return q.Limit(a.params.Limit() + 1).Apply(a.applyCursor) +} + +func (a listEnnoblementsParamsApplier) 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.EnnoblementSortIDASC: + el.value = cursor.ID() + el.unique = true + el.column = "ennoblement.id" + el.direction = sortDirectionASC + case domain.EnnoblementSortIDDESC: + el.value = cursor.ID() + el.unique = true + el.column = "ennoblement.id" + el.direction = sortDirectionDESC + case domain.EnnoblementSortServerKeyASC: + el.value = cursor.ServerKey() + el.column = "ennoblement.server_key" + el.direction = sortDirectionASC + case domain.EnnoblementSortServerKeyDESC: + el.value = cursor.ServerKey() + el.column = "ennoblement.server_key" + el.direction = sortDirectionDESC + case domain.EnnoblementSortCreatedAtASC: + el.value = cursor.CreatedAt() + el.column = "ennoblement.created_at" + el.direction = sortDirectionASC + case domain.EnnoblementSortCreatedAtDESC: + el.value = cursor.CreatedAt() + el.column = "ennoblement.created_at" + 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) } diff --git a/internal/adapter/repository_ennoblement_test.go b/internal/adapter/repository_ennoblement_test.go index df34139..0454a3a 100644 --- a/internal/adapter/repository_ennoblement_test.go +++ b/internal/adapter/repository_ennoblement_test.go @@ -34,7 +34,8 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit listParams := domain.NewListEnnoblementsParams() require.NoError(t, listParams.SetServerKeys([]string{params[0].ServerKey()})) - ennoblements, err := repos.ennoblement.List(ctx, listParams) + res, err := repos.ennoblement.List(ctx, listParams) + ennoblements := res.Ennoblements() require.NoError(t, err) for i, p := range params { idx := slices.IndexFunc(ennoblements, func(ennoblement domain.Ennoblement) bool { @@ -61,8 +62,9 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit listParams := domain.NewListEnnoblementsParams() require.NoError(t, listParams.SetServerKeys([]string{params[0].ServerKey()})) - ennoblements, err := repos.ennoblement.List(ctx, listParams) + res, err := repos.ennoblement.List(ctx, listParams) require.NoError(t, err) + ennoblements := res.Ennoblements() m := make(map[string][]int) @@ -121,17 +123,12 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit repos := newRepos(t) - ennoblements, listEnnoblementsErr := repos.ennoblement.List(ctx, domain.NewListEnnoblementsParams()) - require.NoError(t, listEnnoblementsErr) - require.NotEmpty(t, ennoblements) - randEnnoblement := ennoblements[0] - tests := []struct { - name string - params func(t *testing.T) domain.ListEnnoblementsParams - assertEnnoblements func(t *testing.T, params domain.ListEnnoblementsParams, ennoblements domain.Ennoblements) - assertError func(t *testing.T, err error) - assertTotal func(t *testing.T, params domain.ListEnnoblementsParams, total int) + name string + params func(t *testing.T) domain.ListEnnoblementsParams + assertResult func(t *testing.T, params domain.ListEnnoblementsParams, res domain.ListEnnoblementsResult) + assertError func(t *testing.T, err error) + assertTotal func(t *testing.T, params domain.ListEnnoblementsParams, total int) }{ { name: "OK: default params", @@ -139,8 +136,9 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit t.Helper() return domain.NewListEnnoblementsParams() }, - assertEnnoblements: func(t *testing.T, _ domain.ListEnnoblementsParams, ennoblements domain.Ennoblements) { + assertResult: func(t *testing.T, _ domain.ListEnnoblementsParams, res domain.ListEnnoblementsResult) { t.Helper() + ennoblements := res.Ennoblements() assert.NotEmpty(t, len(ennoblements)) assert.True(t, slices.IsSortedFunc(ennoblements, func(a, b domain.Ennoblement) int { return cmp.Or( @@ -149,6 +147,8 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit 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) { t.Helper() @@ -160,24 +160,27 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit }, }, { - name: "OK: sort=[serverKey DESC, createdAt DESC]", + name: "OK: sort=[serverKey DESC, createdAt DESC, id ASC]", params: func(t *testing.T) domain.ListEnnoblementsParams { t.Helper() params := domain.NewListEnnoblementsParams() require.NoError(t, params.SetSort([]domain.EnnoblementSort{ domain.EnnoblementSortServerKeyDESC, domain.EnnoblementSortCreatedAtDESC, + domain.EnnoblementSortIDASC, })) return params }, - assertEnnoblements: func(t *testing.T, _ domain.ListEnnoblementsParams, ennoblements domain.Ennoblements) { + assertResult: func(t *testing.T, _ domain.ListEnnoblementsParams, res domain.ListEnnoblementsResult) { t.Helper() + ennoblements := res.Ennoblements() assert.NotEmpty(t, len(ennoblements)) assert.True(t, slices.IsSortedFunc(ennoblements, func(a, b domain.Ennoblement) int { return cmp.Or( - cmp.Compare(a.ServerKey(), b.ServerKey()), - a.CreatedAt().Compare(b.CreatedAt()), - ) * -1 + cmp.Compare(a.ServerKey(), b.ServerKey())*-1, + a.CreatedAt().Compare(b.CreatedAt())*-1, + cmp.Compare(a.ID(), b.ID()), + ) })) }, assertError: func(t *testing.T, err error) { @@ -199,8 +202,9 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit })) return params }, - assertEnnoblements: func(t *testing.T, _ domain.ListEnnoblementsParams, ennoblements domain.Ennoblements) { + assertResult: func(t *testing.T, _ domain.ListEnnoblementsParams, res domain.ListEnnoblementsResult) { t.Helper() + ennoblements := res.Ennoblements() assert.NotEmpty(t, len(ennoblements)) assert.True(t, slices.IsSortedFunc(ennoblements, func(a, b domain.Ennoblement) int { return cmp.Compare(a.ID(), b.ID()) @@ -225,8 +229,9 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit })) return params }, - assertEnnoblements: func(t *testing.T, _ domain.ListEnnoblementsParams, ennoblements domain.Ennoblements) { + assertResult: func(t *testing.T, _ domain.ListEnnoblementsParams, res domain.ListEnnoblementsResult) { t.Helper() + ennoblements := res.Ennoblements() assert.NotEmpty(t, len(ennoblements)) assert.True(t, slices.IsSortedFunc(ennoblements, func(a, b domain.Ennoblement) int { return cmp.Compare(a.ID(), b.ID()) * -1 @@ -242,18 +247,28 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit }, }, { - name: fmt.Sprintf("OK: serverKeys=[%s]", randEnnoblement.ServerKey()), + name: "OK: serverKeys", params: func(t *testing.T) domain.ListEnnoblementsParams { t.Helper() + params := domain.NewListEnnoblementsParams() + + res, err := repos.ennoblement.List(ctx, params) + require.NoError(t, err) + require.NotEmpty(t, res.Ennoblements()) + randEnnoblement := res.Ennoblements()[0] + require.NoError(t, params.SetServerKeys([]string{randEnnoblement.ServerKey()})) + return params }, - assertEnnoblements: func(t *testing.T, params domain.ListEnnoblementsParams, ennoblements domain.Ennoblements) { + assertResult: func(t *testing.T, params domain.ListEnnoblementsParams, res domain.ListEnnoblementsResult) { t.Helper() serverKeys := params.ServerKeys() + ennoblements := res.Ennoblements() + assert.NotZero(t, ennoblements) for _, e := range ennoblements { assert.True(t, slices.Contains(serverKeys, e.ServerKey())) } @@ -268,17 +283,150 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit }, }, { - name: "OK: offset=1 limit=2", + name: "OK: cursor serverKeys sort=[id ASC]", + params: func(t *testing.T) domain.ListEnnoblementsParams { + t.Helper() + + params := domain.NewListEnnoblementsParams() + + res, err := repos.ennoblement.List(ctx, params) + require.NoError(t, err) + require.Greater(t, len(res.Ennoblements()), 2) + + require.NoError(t, params.SetSort([]domain.EnnoblementSort{domain.EnnoblementSortIDASC})) + require.NoError(t, params.SetServerKeys([]string{res.Ennoblements()[1].ServerKey()})) + require.NoError( + t, + params.SetCursor(domaintest.NewEnnoblementCursor(t, func(cfg *domaintest.EnnoblementCursorConfig) { + cfg.ID = res.Ennoblements()[1].ID() + })), + ) + + return params + }, + assertResult: func(t *testing.T, params domain.ListEnnoblementsParams, res domain.ListEnnoblementsResult) { + t.Helper() + + serverKeys := params.ServerKeys() + + ennoblements := res.Ennoblements() + assert.NotEmpty(t, len(ennoblements)) + for _, e := range ennoblements { + assert.GreaterOrEqual(t, e.ID(), params.Cursor().ID()) + assert.True(t, slices.Contains(serverKeys, e.ServerKey())) + } + assert.True(t, slices.IsSortedFunc(ennoblements, func(a, b domain.Ennoblement) int { + return cmp.Compare(a.ID(), b.ID()) + })) + }, + assertError: func(t *testing.T, err error) { + t.Helper() + require.NoError(t, err) + }, + }, + { + name: "OK: cursor sort=[serverKey ASC, id ASC]", + params: func(t *testing.T) domain.ListEnnoblementsParams { + t.Helper() + + params := domain.NewListEnnoblementsParams() + require.NoError(t, params.SetSort([]domain.EnnoblementSort{ + domain.EnnoblementSortServerKeyASC, + domain.EnnoblementSortIDASC, + })) + + res, err := repos.ennoblement.List(ctx, params) + require.NoError(t, err) + require.Greater(t, len(res.Ennoblements()), 2) + + require.NoError( + t, + params.SetCursor(domaintest.NewEnnoblementCursor(t, func(cfg *domaintest.EnnoblementCursorConfig) { + cfg.ID = res.Ennoblements()[1].ID() + cfg.ServerKey = res.Ennoblements()[1].ServerKey() + })), + ) + + return params + }, + assertResult: func(t *testing.T, params domain.ListEnnoblementsParams, res domain.ListEnnoblementsResult) { + t.Helper() + ennoblements := res.Ennoblements() + assert.NotEmpty(t, len(ennoblements)) + assert.True(t, slices.IsSortedFunc(ennoblements, func(a, b domain.Ennoblement) int { + return cmp.Or( + cmp.Compare(a.ServerKey(), b.ServerKey()), + cmp.Compare(a.ID(), b.ID()), + ) + })) + assert.GreaterOrEqual(t, ennoblements[0].ID(), params.Cursor().ID()) + for _, e := range ennoblements { + assert.GreaterOrEqual(t, e.ServerKey(), params.Cursor().ServerKey()) + } + }, + assertError: func(t *testing.T, err error) { + t.Helper() + require.NoError(t, err) + }, + }, + { + name: "OK: cursor sort=[serverKey DESC, id DESC]", + params: func(t *testing.T) domain.ListEnnoblementsParams { + t.Helper() + + params := domain.NewListEnnoblementsParams() + require.NoError(t, params.SetSort([]domain.EnnoblementSort{ + domain.EnnoblementSortServerKeyDESC, + domain.EnnoblementSortIDDESC, + })) + + res, err := repos.ennoblement.List(ctx, params) + require.NoError(t, err) + require.Greater(t, len(res.Ennoblements()), 2) + + require.NoError( + t, + params.SetCursor(domaintest.NewEnnoblementCursor(t, func(cfg *domaintest.EnnoblementCursorConfig) { + cfg.ID = res.Ennoblements()[1].ID() + cfg.ServerKey = res.Ennoblements()[1].ServerKey() + })), + ) + + return params + }, + assertResult: func(t *testing.T, params domain.ListEnnoblementsParams, res domain.ListEnnoblementsResult) { + t.Helper() + ennoblements := res.Ennoblements() + assert.NotEmpty(t, len(ennoblements)) + assert.True(t, slices.IsSortedFunc(ennoblements, func(a, b domain.Ennoblement) int { + return cmp.Or( + cmp.Compare(a.ServerKey(), b.ServerKey()), + cmp.Compare(a.ID(), b.ID()), + ) * -1 + })) + assert.LessOrEqual(t, ennoblements[0].ID(), params.Cursor().ID()) + for _, e := range ennoblements { + assert.LessOrEqual(t, e.ServerKey(), params.Cursor().ServerKey()) + } + }, + assertError: func(t *testing.T, err error) { + t.Helper() + require.NoError(t, err) + }, + }, + { + name: "OK: limit=2", params: func(t *testing.T) domain.ListEnnoblementsParams { t.Helper() params := domain.NewListEnnoblementsParams() - require.NoError(t, params.SetOffset(1)) require.NoError(t, params.SetLimit(2)) return params }, - assertEnnoblements: func(t *testing.T, params domain.ListEnnoblementsParams, ennoblements domain.Ennoblements) { + assertResult: func(t *testing.T, params domain.ListEnnoblementsParams, res domain.ListEnnoblementsResult) { t.Helper() - assert.Len(t, ennoblements, params.Limit()) + assert.Len(t, res.Ennoblements(), params.Limit()) + assert.False(t, res.Self().IsZero()) + assert.False(t, res.Next().IsZero()) }, assertError: func(t *testing.T, err error) { t.Helper() @@ -299,7 +447,7 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit res, err := repos.ennoblement.List(ctx, params) tt.assertError(t, err) - tt.assertEnnoblements(t, params, res) + tt.assertResult(t, params, res) }) } }) diff --git a/internal/adapter/repository_test.go b/internal/adapter/repository_test.go index a23ddf5..23954e5 100644 --- a/internal/adapter/repository_test.go +++ b/internal/adapter/repository_test.go @@ -47,7 +47,7 @@ type villageRepository interface { type ennoblementRepository interface { Create(ctx context.Context, params ...domain.CreateEnnoblementParams) error - List(ctx context.Context, params domain.ListEnnoblementsParams) (domain.Ennoblements, error) + List(ctx context.Context, params domain.ListEnnoblementsParams) (domain.ListEnnoblementsResult, error) } type tribeChangeRepository interface { diff --git a/internal/app/service_ennoblement.go b/internal/app/service_ennoblement.go index 4d2e39a..0b613df 100644 --- a/internal/app/service_ennoblement.go +++ b/internal/app/service_ennoblement.go @@ -11,7 +11,7 @@ import ( type EnnoblementRepository interface { Create(ctx context.Context, params ...domain.CreateEnnoblementParams) error - List(ctx context.Context, params domain.ListEnnoblementsParams) (domain.Ennoblements, error) + List(ctx context.Context, params domain.ListEnnoblementsParams) (domain.ListEnnoblementsResult, error) } type EnnoblementService struct { @@ -80,10 +80,12 @@ func (svc *EnnoblementService) getLatest(ctx context.Context, serverKey string) return domain.Ennoblement{}, err } - ennoblements, err := svc.repo.List(ctx, params) + res, err := svc.repo.List(ctx, params) if err != nil { return domain.Ennoblement{}, err } + ennoblements := res.Ennoblements() + if len(ennoblements) == 0 { return domain.Ennoblement{}, errEnnoblementNotFound } @@ -116,3 +118,10 @@ func (svc *EnnoblementService) create( return nil } + +func (svc *EnnoblementService) List( + ctx context.Context, + params domain.ListEnnoblementsParams, +) (domain.ListEnnoblementsResult, error) { + return svc.repo.List(ctx, params) +} diff --git a/internal/domain/domaintest/ennoblement.go b/internal/domain/domaintest/ennoblement.go index 141252d..086a82c 100644 --- a/internal/domain/domaintest/ennoblement.go +++ b/internal/domain/domaintest/ennoblement.go @@ -8,6 +8,35 @@ import ( "github.com/stretchr/testify/require" ) +type EnnoblementCursorConfig struct { + ID int + ServerKey string + CreatedAt time.Time +} + +func NewEnnoblementCursor(tb TestingTB, opts ...func(cfg *EnnoblementCursorConfig)) domain.EnnoblementCursor { + tb.Helper() + + cfg := &EnnoblementCursorConfig{ + ID: RandID(), + ServerKey: RandServerKey(), + CreatedAt: gofakeit.Date(), + } + + for _, opt := range opts { + opt(cfg) + } + + ec, err := domain.NewEnnoblementCursor( + cfg.ID, + cfg.ServerKey, + cfg.CreatedAt, + ) + require.NoError(tb, err) + + return ec +} + type EnnoblementConfig struct { ID int ServerKey string diff --git a/internal/domain/domaintest/village.go b/internal/domain/domaintest/village.go index 52a61be..0836344 100644 --- a/internal/domain/domaintest/village.go +++ b/internal/domain/domaintest/village.go @@ -25,13 +25,13 @@ func NewVillageCursor(tb TestingTB, opts ...func(cfg *VillageCursorConfig)) doma opt(cfg) } - pc, err := domain.NewVillageCursor( + vc, err := domain.NewVillageCursor( cfg.ID, cfg.ServerKey, ) require.NoError(tb, err) - return pc + return vc } type VillageConfig struct { diff --git a/internal/domain/ennoblement.go b/internal/domain/ennoblement.go index 1ee2a6d..ceaebee 100644 --- a/internal/domain/ennoblement.go +++ b/internal/domain/ennoblement.go @@ -101,6 +101,10 @@ func (e Ennoblement) CreatedAt() time.Time { return e.createdAt } +func (e Ennoblement) ToCursor() (EnnoblementCursor, error) { + return NewEnnoblementCursor(e.id, e.serverKey, e.createdAt) +} + func (e Ennoblement) Base() BaseEnnoblement { return BaseEnnoblement{ villageID: e.villageID, @@ -113,6 +117,10 @@ func (e Ennoblement) Base() BaseEnnoblement { } } +func (e Ennoblement) IsZero() bool { + return e == Ennoblement{} +} + type Ennoblements []Ennoblement type CreateEnnoblementParams struct { @@ -194,11 +202,105 @@ func (s EnnoblementSort) String() string { } } +type EnnoblementCursor struct { + id int + serverKey string + createdAt time.Time +} + +const ennoblementCursorModelName = "EnnoblementCursor" + +func NewEnnoblementCursor(id int, serverKey string, createdAt time.Time) (EnnoblementCursor, error) { + if err := validateIntInRange(id, 1, math.MaxInt); err != nil { + return EnnoblementCursor{}, ValidationError{ + Model: ennoblementCursorModelName, + Field: "id", + Err: err, + } + } + + if err := validateServerKey(serverKey); err != nil { + return EnnoblementCursor{}, ValidationError{ + Model: ennoblementCursorModelName, + Field: "serverKey", + Err: err, + } + } + + return EnnoblementCursor{ + id: id, + serverKey: serverKey, + createdAt: createdAt, + }, nil +} + +//nolint:gocyclo +func decodeEnnoblementCursor(encoded string) (EnnoblementCursor, error) { + m, err := decodeCursor(encoded) + if err != nil { + return EnnoblementCursor{}, err + } + + id, err := m.int("id") + if err != nil { + return EnnoblementCursor{}, ErrInvalidCursor + } + + serverKey, err := m.string("serverKey") + if err != nil { + return EnnoblementCursor{}, ErrInvalidCursor + } + + createdAt, err := m.time("createdAt") + if err != nil { + return EnnoblementCursor{}, ErrInvalidCursor + } + + ec, err := NewEnnoblementCursor( + id, + serverKey, + createdAt, + ) + if err != nil { + return EnnoblementCursor{}, ErrInvalidCursor + } + + return ec, nil +} + +func (ec EnnoblementCursor) ID() int { + return ec.id +} + +func (ec EnnoblementCursor) ServerKey() string { + return ec.serverKey +} + +func (ec EnnoblementCursor) CreatedAt() time.Time { + return ec.createdAt +} + +func (ec EnnoblementCursor) IsZero() bool { + return ec == EnnoblementCursor{} +} + +func (ec EnnoblementCursor) Encode() string { + if ec.IsZero() { + return "" + } + + return encodeCursor([]keyValuePair{ + {"id", ec.id}, + {"serverKey", ec.serverKey}, + {"createdAt", ec.createdAt}, + }) +} + type ListEnnoblementsParams struct { serverKeys []string sort []EnnoblementSort + cursor EnnoblementCursor limit int - offset int } const ( @@ -261,6 +363,30 @@ func (params *ListEnnoblementsParams) SetSort(sort []EnnoblementSort) error { return nil } +func (params *ListEnnoblementsParams) Cursor() EnnoblementCursor { + return params.cursor +} + +func (params *ListEnnoblementsParams) SetCursor(cursor EnnoblementCursor) error { + params.cursor = cursor + return nil +} + +func (params *ListEnnoblementsParams) SetEncodedCursor(encoded string) error { + decoded, err := decodeEnnoblementCursor(encoded) + if err != nil { + return ValidationError{ + Model: listEnnoblementsParamsModelName, + Field: "cursor", + Err: err, + } + } + + params.cursor = decoded + + return nil +} + func (params *ListEnnoblementsParams) Limit() int { return params.limit } @@ -279,20 +405,53 @@ func (params *ListEnnoblementsParams) SetLimit(limit int) error { return nil } -func (params *ListEnnoblementsParams) Offset() int { - return params.offset +type ListEnnoblementsResult struct { + ennoblements Ennoblements + self EnnoblementCursor + next EnnoblementCursor } -func (params *ListEnnoblementsParams) SetOffset(offset int) error { - if err := validateIntInRange(offset, 0, math.MaxInt); err != nil { - return ValidationError{ - Model: listEnnoblementsParamsModelName, - Field: "offset", - Err: err, +const listEnnoblementsResultModelName = "ListEnnoblementsResult" + +func NewListEnnoblementsResult(ennoblements Ennoblements, next Ennoblement) (ListEnnoblementsResult, error) { + var err error + res := ListEnnoblementsResult{ + ennoblements: ennoblements, + } + + if len(ennoblements) > 0 { + res.self, err = ennoblements[0].ToCursor() + if err != nil { + return ListEnnoblementsResult{}, ValidationError{ + Model: listEnnoblementsResultModelName, + Field: "self", + Err: err, + } } } - params.offset = offset + if !next.IsZero() { + res.next, err = next.ToCursor() + if err != nil { + return ListEnnoblementsResult{}, ValidationError{ + Model: listEnnoblementsResultModelName, + Field: "next", + Err: err, + } + } + } - return nil + return res, nil +} + +func (res ListEnnoblementsResult) Ennoblements() Ennoblements { + return res.ennoblements +} + +func (res ListEnnoblementsResult) Self() EnnoblementCursor { + return res.self +} + +func (res ListEnnoblementsResult) Next() EnnoblementCursor { + return res.next } diff --git a/internal/domain/ennoblement_test.go b/internal/domain/ennoblement_test.go index 6e0329a..3b96abf 100644 --- a/internal/domain/ennoblement_test.go +++ b/internal/domain/ennoblement_test.go @@ -4,9 +4,11 @@ import ( "fmt" "slices" "testing" + "time" "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" ) @@ -102,6 +104,87 @@ func TestEnnoblementSort_IsInConflict(t *testing.T) { } } +func TestNewEnnoblementCursor(t *testing.T) { + t.Parallel() + + validEnnoblementCursor := domaintest.NewEnnoblementCursor(t) + + type args struct { + id int + serverKey string + createdAt time.Time + } + + type test struct { + name string + args args + expectedErr error + } + + tests := []test{ + { + name: "OK", + args: args{ + id: validEnnoblementCursor.ID(), + serverKey: validEnnoblementCursor.ServerKey(), + createdAt: validEnnoblementCursor.CreatedAt(), + }, + expectedErr: nil, + }, + { + name: "ERR: id < 1", + args: args{ + id: 0, + serverKey: validEnnoblementCursor.ServerKey(), + createdAt: validEnnoblementCursor.CreatedAt(), + }, + expectedErr: domain.ValidationError{ + Model: "EnnoblementCursor", + Field: "id", + Err: domain.MinGreaterEqualError{ + Min: 1, + Current: 0, + }, + }, + }, + } + + for _, serverKeyTest := range newServerKeyValidationTests() { + tests = append(tests, test{ + name: serverKeyTest.name, + args: args{ + id: validEnnoblementCursor.ID(), + serverKey: serverKeyTest.key, + }, + expectedErr: domain.ValidationError{ + Model: "EnnoblementCursor", + Field: "serverKey", + Err: serverKeyTest.expectedErr, + }, + }) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ec, err := domain.NewEnnoblementCursor( + tt.args.id, + tt.args.serverKey, + tt.args.createdAt, + ) + require.ErrorIs(t, err, tt.expectedErr) + if tt.expectedErr != nil { + return + } + assert.Equal(t, tt.args.id, ec.ID()) + assert.Equal(t, tt.args.serverKey, ec.ServerKey()) + assert.Equal(t, tt.args.createdAt, ec.CreatedAt()) + assert.NotEmpty(t, ec.Encode()) + }) + } +} + func TestListEnnoblementsParams_SetServerKeys(t *testing.T) { t.Parallel() @@ -246,6 +329,86 @@ func TestListEnnoblementsParams_SetSort(t *testing.T) { } } +func TestListEnnoblementsParams_SetEncodedCursor(t *testing.T) { + t.Parallel() + + validCursor := domaintest.NewEnnoblementCursor(t) + + type args struct { + cursor string + } + + tests := []struct { + name string + args args + expectedCursor domain.EnnoblementCursor + expectedErr error + }{ + { + name: "OK", + args: args{ + cursor: validCursor.Encode(), + }, + expectedCursor: validCursor, + }, + { + name: "ERR: len(cursor) < 1", + args: args{ + cursor: "", + }, + expectedErr: domain.ValidationError{ + Model: "ListEnnoblementsParams", + 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: "ListEnnoblementsParams", + Field: "cursor", + Err: domain.LenOutOfRangeError{ + Min: 1, + Max: 1000, + Current: 1001, + }, + }, + }, + { + name: "ERR: malformed base64", + args: args{ + cursor: "112345", + }, + expectedErr: domain.ValidationError{ + Model: "ListEnnoblementsParams", + Field: "cursor", + Err: domain.ErrInvalidCursor, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + params := domain.NewListEnnoblementsParams() + + 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 TestListEnnoblementsParams_SetLimit(t *testing.T) { t.Parallel() @@ -309,51 +472,46 @@ func TestListEnnoblementsParams_SetLimit(t *testing.T) { } } -func TestListEnnoblementsParams_SetOffset(t *testing.T) { +func TestNewListEnnoblementsResult(t *testing.T) { t.Parallel() - type args struct { - offset int + ennoblements := domain.Ennoblements{ + domaintest.NewEnnoblement(t), + domaintest.NewEnnoblement(t), + domaintest.NewEnnoblement(t), } + next := domaintest.NewEnnoblement(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: "ListEnnoblementsParams", - 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.NewListEnnoblementsResult(ennoblements, next) + require.NoError(t, err) + assert.Equal(t, ennoblements, res.Ennoblements()) + assert.Equal(t, ennoblements[0].ID(), res.Self().ID()) + assert.Equal(t, ennoblements[0].ServerKey(), res.Self().ServerKey()) + assert.Equal(t, next.ID(), res.Next().ID()) + assert.Equal(t, next.ServerKey(), res.Next().ServerKey()) + }) - params := domain.NewListEnnoblementsParams() + 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.NewListEnnoblementsResult(ennoblements, domain.Ennoblement{}) + require.NoError(t, err) + assert.Equal(t, ennoblements, res.Ennoblements()) + assert.Equal(t, ennoblements[0].ID(), res.Self().ID()) + assert.Equal(t, ennoblements[0].ServerKey(), res.Self().ServerKey()) + assert.True(t, res.Next().IsZero()) + }) + + t.Run("OK: 0 ennoblements", func(t *testing.T) { + t.Parallel() + + res, err := domain.NewListEnnoblementsResult(nil, domain.Ennoblement{}) + require.NoError(t, err) + assert.Zero(t, res.Ennoblements()) + assert.True(t, res.Self().IsZero()) + assert.True(t, res.Next().IsZero()) + }) } diff --git a/internal/port/consumer_ennoblement_sync_test.go b/internal/port/consumer_ennoblement_sync_test.go index ec2adc7..4ba14f1 100644 --- a/internal/port/consumer_ennoblement_sync_test.go +++ b/internal/port/consumer_ennoblement_sync_test.go @@ -204,16 +204,16 @@ func TestEnnoblementSync(t *testing.T) { allEnnoblements := make(domain.Ennoblements, 0, len(expectedEnnoblements)) for { - ennoblements, err := ennoblementRepo.List(ctx, listParams) + res, err := ennoblementRepo.List(ctx, listParams) require.NoError(collect, err) - if len(ennoblements) == 0 { + allEnnoblements = append(allEnnoblements, res.Ennoblements()...) + + if res.Next().IsZero() { break } - allEnnoblements = append(allEnnoblements, ennoblements...) - - require.NoError(collect, listParams.SetOffset(listParams.Offset()+domain.EnnoblementListMaxLimit)) + require.NoError(collect, listParams.SetCursor(res.Next())) } if !assert.Len(collect, allEnnoblements, len(expectedEnnoblements)) {