feat: api - /api/v2/versions/{versionCode}/servers/{serverKey}/ennoblements - new query params (#23)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details

Reviewed-on: twhelp/corev3#23
This commit is contained in:
Dawid Wysokiński 2024-03-11 09:05:29 +00:00
parent a86acd760a
commit db528e125e
8 changed files with 981 additions and 30 deletions

View File

@ -317,6 +317,9 @@ paths:
- $ref: "#/components/parameters/ServerKeyPathParam"
- $ref: "#/components/parameters/CursorQueryParam"
- $ref: "#/components/parameters/LimitQueryParam"
- $ref: "#/components/parameters/EnnoblementSortQueryParam"
- $ref: "#/components/parameters/SinceQueryParam"
- $ref: "#/components/parameters/BeforeQueryParam"
responses:
200:
$ref: "#/components/responses/ListEnnoblementsResponse"
@ -1426,6 +1429,34 @@ components:
type: string
example: 500|500
maxItems: 200
EnnoblementSortQueryParam:
name: sort
in: query
description: Order matters!
schema:
type: array
default:
- createdAt:ASC
items:
type: string
enum:
- createdAt:ASC
- createdAt:DESC
maxItems: 1
SinceQueryParam:
name: since
in: query
description: only items created since the provided time are returned, this is a timestamp in RFC 3339 format
schema:
type: string
format: date-time
BeforeQueryParam:
name: before
in: query
description: only items created before the provided time are returned, this is a timestamp in RFC 3339 format
schema:
type: string
format: date-time
VersionCodePathParam:
in: path
name: versionCode

View File

@ -118,6 +118,32 @@ func (a listEnnoblementsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuer
q = q.Where("ennoblement.server_key IN (?)", bun.In(serverKeys))
}
if villageIDs := a.params.PlayerIDs(); len(villageIDs) > 0 {
q = q.Where("ennoblement.village_id IN (?)", bun.In(villageIDs))
}
if playerIDs := a.params.PlayerIDs(); len(playerIDs) > 0 {
q = q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("ennoblement.new_owner_id IN (?)", bun.In(playerIDs)).
WhereOr("ennoblement.old_owner_id IN (?)", bun.In(playerIDs))
})
}
if tribeIDs := a.params.TribeIDs(); len(tribeIDs) > 0 {
q = q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("ennoblement.new_tribe_id IN (?)", bun.In(tribeIDs)).
WhereOr("ennoblement.old_tribe_id IN (?)", bun.In(tribeIDs))
})
}
if since := a.params.Since(); since.Valid {
q = q.Where("ennoblement.created_at >= ?", since.V)
}
if before := a.params.Before(); before.Valid {
q = q.Where("ennoblement.created_at < ?", before.V)
}
for _, s := range a.params.Sort() {
switch s {
case domain.EnnoblementSortCreatedAtASC:

View File

@ -128,7 +128,6 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit
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",
@ -154,10 +153,6 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListEnnoblementsParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[serverKey DESC, createdAt DESC, id ASC]",
@ -187,10 +182,6 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListEnnoblementsParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[id ASC]",
@ -214,10 +205,6 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListEnnoblementsParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[id DESC]",
@ -241,10 +228,6 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListEnnoblementsParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: serverKeys",
@ -277,9 +260,162 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListEnnoblementsParams, total int) {
},
{
name: "OK: villageIDs serverKeys",
params: func(t *testing.T) domain.ListEnnoblementsParams {
t.Helper()
assert.NotEmpty(t, total)
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()}))
require.NoError(t, params.SetVillageIDs([]int{randEnnoblement.VillageID()}))
return params
},
assertResult: func(t *testing.T, params domain.ListEnnoblementsParams, res domain.ListEnnoblementsResult) {
t.Helper()
serverKeys := params.ServerKeys()
villageIDs := params.VillageIDs()
ennoblements := res.Ennoblements()
assert.NotZero(t, ennoblements)
for _, e := range ennoblements {
assert.True(t, slices.Contains(serverKeys, e.ServerKey()))
assert.True(t, slices.Contains(villageIDs, e.VillageID()))
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: playerIDs (new owner) 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()}))
require.NoError(t, params.SetPlayerIDs([]int{randEnnoblement.NewOwnerID()}))
return params
},
assertResult: func(t *testing.T, params domain.ListEnnoblementsParams, res domain.ListEnnoblementsResult) {
t.Helper()
serverKeys := params.ServerKeys()
playerIDs := params.PlayerIDs()
ennoblements := res.Ennoblements()
assert.NotZero(t, ennoblements)
for _, e := range ennoblements {
assert.True(t, slices.Contains(serverKeys, e.ServerKey()))
assert.True(t, slices.Contains(playerIDs, e.NewOwnerID()) || slices.Contains(playerIDs, e.OldOwnerID()))
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: playerIDs (old owner) 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())
var randEnnoblement domain.Ennoblement
for _, e := range res.Ennoblements() {
if e.OldOwnerID() > 0 {
randEnnoblement = e
break
}
}
require.NoError(t, params.SetServerKeys([]string{randEnnoblement.ServerKey()}))
require.NoError(t, params.SetPlayerIDs([]int{randEnnoblement.OldOwnerID()}))
return params
},
assertResult: func(t *testing.T, params domain.ListEnnoblementsParams, res domain.ListEnnoblementsResult) {
t.Helper()
serverKeys := params.ServerKeys()
playerIDs := params.PlayerIDs()
ennoblements := res.Ennoblements()
assert.NotZero(t, ennoblements)
for _, e := range ennoblements {
assert.True(t, slices.Contains(serverKeys, e.ServerKey()))
assert.True(t, slices.Contains(playerIDs, e.NewOwnerID()) || slices.Contains(playerIDs, e.OldOwnerID()))
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: since before",
params: func(t *testing.T) domain.ListEnnoblementsParams {
t.Helper()
params := domain.NewListEnnoblementsParams()
require.NoError(t, params.SetSort([]domain.EnnoblementSort{
domain.EnnoblementSortCreatedAtASC,
domain.EnnoblementSortIDASC,
}))
res, err := repos.ennoblement.List(ctx, params)
require.NoError(t, err)
require.GreaterOrEqual(t, len(res.Ennoblements()), 4)
params = domain.NewListEnnoblementsParams()
require.NoError(t, params.SetSince(domain.NullTime{
V: res.Ennoblements()[1].CreatedAt(),
Valid: true,
}))
require.NoError(t, params.SetBefore(domain.NullTime{
V: res.Ennoblements()[3].CreatedAt(),
Valid: true,
}))
return params
},
assertResult: func(t *testing.T, params domain.ListEnnoblementsParams, res domain.ListEnnoblementsResult) {
t.Helper()
since := params.Since().V
before := params.Before().V
ennoblements := res.Ennoblements()
assert.NotZero(t, ennoblements)
for _, e := range ennoblements {
assert.True(t, e.CreatedAt().After(since) || e.CreatedAt().Equal(since))
assert.True(t, e.CreatedAt().Before(before))
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
@ -432,10 +568,6 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListEnnoblementsParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"math"
"slices"
"strings"
"time"
)
@ -230,6 +231,23 @@ const (
EnnoblementSortServerKeyDESC
)
func newEnnoblementSortFromString(s string) (EnnoblementSort, error) {
allowed := []EnnoblementSort{
EnnoblementSortCreatedAtASC,
EnnoblementSortCreatedAtDESC,
}
for _, a := range allowed {
if strings.EqualFold(a.String(), s) {
return a, nil
}
}
return 0, UnsupportedSortStringError{
Sort: s,
}
}
// IsInConflict returns true if two sorts can't be used together (e.g. EnnoblementSortIDASC and EnnoblementSortIDDESC).
func (s EnnoblementSort) IsInConflict(s2 EnnoblementSort) bool {
ss := []EnnoblementSort{s, s2}
@ -354,6 +372,11 @@ func (ec EnnoblementCursor) Encode() string {
type ListEnnoblementsParams struct {
serverKeys []string
villageIDs []int
playerIDs []int // Ennoblement.NewOwnerID or Ennoblement.OldOwnerID
tribeIDs []int // Ennoblement.NewTribeID or Ennoblement.OldTribeID
since NullTime
before NullTime
sort []EnnoblementSort
cursor EnnoblementCursor
limit int
@ -396,6 +419,87 @@ func (params *ListEnnoblementsParams) SetServerKeys(serverKeys []string) error {
return nil
}
func (params *ListEnnoblementsParams) VillageIDs() []int {
return params.villageIDs
}
func (params *ListEnnoblementsParams) SetVillageIDs(villageIDs []int) error {
for i, id := range villageIDs {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listEnnoblementsParamsModelName,
Field: "villageIDs",
Index: i,
Err: err,
}
}
}
params.villageIDs = villageIDs
return nil
}
func (params *ListEnnoblementsParams) PlayerIDs() []int {
return params.playerIDs
}
func (params *ListEnnoblementsParams) SetPlayerIDs(playerIDs []int) error {
for i, id := range playerIDs {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listEnnoblementsParamsModelName,
Field: "playerIDs",
Index: i,
Err: err,
}
}
}
params.playerIDs = playerIDs
return nil
}
func (params *ListEnnoblementsParams) TribeIDs() []int {
return params.tribeIDs
}
func (params *ListEnnoblementsParams) SetTribeIDs(tribeIDs []int) error {
for i, id := range tribeIDs {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listEnnoblementsParamsModelName,
Field: "tribeIDs",
Index: i,
Err: err,
}
}
}
params.tribeIDs = tribeIDs
return nil
}
func (params *ListEnnoblementsParams) Since() NullTime {
return params.since
}
func (params *ListEnnoblementsParams) SetSince(since NullTime) error {
params.since = since
return nil
}
func (params *ListEnnoblementsParams) Before() NullTime {
return params.before
}
func (params *ListEnnoblementsParams) SetBefore(before NullTime) error {
params.before = before
return nil
}
func (params *ListEnnoblementsParams) Sort() []EnnoblementSort {
return params.sort
}
@ -419,6 +523,37 @@ func (params *ListEnnoblementsParams) SetSort(sort []EnnoblementSort) error {
return nil
}
func (params *ListEnnoblementsParams) PrependSortString(sort []string) error {
if err := validateSliceLen(
sort,
ennoblementSortMinLength,
max(ennoblementSortMaxLength-len(params.sort), 0),
); err != nil {
return ValidationError{
Model: listEnnoblementsParamsModelName,
Field: "sort",
Err: err,
}
}
toPrepend := make([]EnnoblementSort, 0, len(sort))
for i, s := range sort {
converted, err := newEnnoblementSortFromString(s)
if err != nil {
return SliceElementValidationError{
Model: listEnnoblementsParamsModelName,
Field: "sort",
Index: i,
Err: err,
}
}
toPrepend = append(toPrepend, converted)
}
return params.SetSort(append(toPrepend, params.sort...))
}
func (params *ListEnnoblementsParams) Cursor() EnnoblementCursor {
return params.cursor
}

View File

@ -239,6 +239,186 @@ func TestListEnnoblementsParams_SetServerKeys(t *testing.T) {
}
}
func TestListEnnoblementsParams_SetVillageIDs(t *testing.T) {
t.Parallel()
type args struct {
villageIDs []int
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
villageIDs: []int{
domaintest.RandID(),
domaintest.RandID(),
domaintest.RandID(),
},
},
},
{
name: "ERR: value < 1",
args: args{
villageIDs: []int{
domaintest.RandID(),
domaintest.RandID(),
domaintest.RandID(),
0,
domaintest.RandID(),
},
},
expectedErr: domain.SliceElementValidationError{
Model: "ListEnnoblementsParams",
Field: "villageIDs",
Index: 3,
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListEnnoblementsParams()
require.ErrorIs(t, params.SetVillageIDs(tt.args.villageIDs), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.villageIDs, params.VillageIDs())
})
}
}
func TestListEnnoblementsParams_SetPlayerIDs(t *testing.T) {
t.Parallel()
type args struct {
playerIDs []int
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
playerIDs: []int{
domaintest.RandID(),
domaintest.RandID(),
domaintest.RandID(),
},
},
},
{
name: "ERR: value < 1",
args: args{
playerIDs: []int{
domaintest.RandID(),
domaintest.RandID(),
domaintest.RandID(),
0,
domaintest.RandID(),
},
},
expectedErr: domain.SliceElementValidationError{
Model: "ListEnnoblementsParams",
Field: "playerIDs",
Index: 3,
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListEnnoblementsParams()
require.ErrorIs(t, params.SetPlayerIDs(tt.args.playerIDs), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.playerIDs, params.PlayerIDs())
})
}
}
func TestListEnnoblementsParams_SetTribeIDs(t *testing.T) {
t.Parallel()
type args struct {
tribeIDs []int
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
tribeIDs: []int{
domaintest.RandID(),
domaintest.RandID(),
domaintest.RandID(),
},
},
},
{
name: "ERR: value < 1",
args: args{
tribeIDs: []int{
domaintest.RandID(),
domaintest.RandID(),
domaintest.RandID(),
0,
domaintest.RandID(),
},
},
expectedErr: domain.SliceElementValidationError{
Model: "ListEnnoblementsParams",
Field: "tribeIDs",
Index: 3,
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListEnnoblementsParams()
require.ErrorIs(t, params.SetTribeIDs(tt.args.tribeIDs), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.tribeIDs, params.TribeIDs())
})
}
}
func TestListEnnoblementsParams_SetSort(t *testing.T) {
t.Parallel()
@ -329,6 +509,143 @@ func TestListEnnoblementsParams_SetSort(t *testing.T) {
}
}
func TestListEnnoblementsParams_PrependSortString(t *testing.T) {
t.Parallel()
defaultNewParams := func(t *testing.T) domain.ListEnnoblementsParams {
t.Helper()
return domain.ListEnnoblementsParams{}
}
type args struct {
sort []string
}
tests := []struct {
name string
newParams func(t *testing.T) domain.ListEnnoblementsParams
args args
expectedSort []domain.EnnoblementSort
expectedErr error
}{
{
name: "OK: [createdAt:ASC]",
args: args{
sort: []string{
"createdAt:ASC",
},
},
expectedSort: []domain.EnnoblementSort{
domain.EnnoblementSortCreatedAtASC,
},
},
{
name: "OK: [createdAt:DESC]",
args: args{
sort: []string{
"createdAt:DESC",
},
},
expectedSort: []domain.EnnoblementSort{
domain.EnnoblementSortCreatedAtDESC,
},
},
{
name: "ERR: len(sort) < 1",
args: args{
sort: nil,
},
expectedErr: domain.ValidationError{
Model: "ListEnnoblementsParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 3,
Current: 0,
},
},
},
{
name: "ERR: custom params + len(sort) > 1",
newParams: func(t *testing.T) domain.ListEnnoblementsParams {
t.Helper()
params := domain.NewListEnnoblementsParams()
require.NoError(t, params.SetSort([]domain.EnnoblementSort{
domain.EnnoblementSortServerKeyASC,
domain.EnnoblementSortIDASC,
}))
return params
},
args: args{
sort: []string{
"createdAt:ASC",
"createdAt:DESC",
},
},
expectedErr: domain.ValidationError{
Model: "ListEnnoblementsParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1,
Current: 2,
},
},
},
{
name: "ERR: unsupported sort string",
newParams: defaultNewParams,
args: args{
sort: []string{
"createdAt:",
},
},
expectedErr: domain.SliceElementValidationError{
Model: "ListEnnoblementsParams",
Field: "sort",
Index: 0,
Err: domain.UnsupportedSortStringError{
Sort: "createdAt:",
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []string{
"createdAt:ASC",
"createdAt:DESC",
},
},
expectedErr: domain.ValidationError{
Model: "ListEnnoblementsParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.EnnoblementSortCreatedAtASC.String(), domain.EnnoblementSortCreatedAtDESC.String()},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
newParams := defaultNewParams
if tt.newParams != nil {
newParams = tt.newParams
}
params := newParams(t)
require.ErrorIs(t, params.PrependSortString(tt.args.sort), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.expectedSort, params.Sort())
})
}
}
func TestListEnnoblementsParams_SetEncodedCursor(t *testing.T) {
t.Parallel()

View File

@ -48,7 +48,7 @@ func (s ErrorPathSegment) String() string {
path += "."
}
path += s.Field
if s.Index > 0 {
if s.Index >= 0 {
path += "[" + strconv.Itoa(s.Index) + "]"
}
}

View File

@ -1,13 +1,14 @@
package port
import (
"fmt"
"net/http"
"strconv"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
)
//nolint:gocyclo
func (h *apiHTTPHandler) ListEnnoblements(
w http.ResponseWriter,
r *http.Request,
@ -17,19 +18,48 @@ func (h *apiHTTPHandler) ListEnnoblements(
) {
domainParams := domain.NewListEnnoblementsParams()
if err := domainParams.SetSort([]domain.EnnoblementSort{
domain.EnnoblementSortCreatedAtASC,
domain.EnnoblementSortIDASC,
}); err != nil {
if err := domainParams.SetSort([]domain.EnnoblementSort{domain.EnnoblementSortIDASC}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err)
return
}
if params.Sort != nil {
if err := domainParams.PrependSortString(*params.Sort); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err)
return
}
} else {
if err := domainParams.PrependSortString([]string{domain.EnnoblementSortCreatedAtASC.String()}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err)
return
}
}
if err := domainParams.SetServerKeys([]string{serverKey}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err)
return
}
if params.Before != nil {
if err := domainParams.SetBefore(domain.NullTime{
V: *params.Before,
Valid: true,
}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err)
return
}
}
if params.Since != nil {
if err := domainParams.SetSince(domain.NullTime{
V: *params.Since,
Valid: true,
}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err)
return
}
}
if params.Limit != nil {
if err := domainParams.SetLimit(*params.Limit); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err)
@ -46,7 +76,6 @@ func (h *apiHTTPHandler) ListEnnoblements(
res, err := h.ennoblementSvc.ListWithRelations(r.Context(), domainParams)
if err != nil {
fmt.Println(err)
h.errorRenderer.render(w, r, err)
return
}
@ -64,6 +93,12 @@ func formatListEnnoblementsErrorPath(segments []domain.ErrorPathSegment) []strin
return []string{"$query", "cursor"}
case "limit":
return []string{"$query", "limit"}
case "sort":
path := []string{"$query", "sort"}
if segments[0].Index >= 0 {
path = append(path, strconv.Itoa(segments[0].Index))
}
return path
default:
return nil
}

View File

@ -9,6 +9,7 @@ import (
"strconv"
"strings"
"testing"
"time"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
@ -105,6 +106,112 @@ func TestListEnnoblements(t *testing.T) {
assert.Len(t, body.Data, limit)
},
},
{
name: "OK: sort=[createdAt:DESC]",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("sort", "createdAt:DESC")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListPlayersResponse](t, resp.Body)
assert.Zero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.Player) int {
return cmp.Or(
a.CreatedAt.Compare(b.CreatedAt)*-1,
cmp.Compare(a.Id, b.Id),
)
}))
},
},
{
name: "OK: since",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
var filtered []ennoblementWithServer
for _, e := range ennoblements {
if e.Server.Key == server.Key {
filtered = append(filtered, e)
}
}
slices.SortFunc(filtered, func(a, b ennoblementWithServer) int {
return cmp.Or(
a.CreatedAt.Compare(b.CreatedAt),
cmp.Compare(a.Id, b.Id),
)
})
require.GreaterOrEqual(t, len(filtered), 2)
q := req.URL.Query()
q.Set("since", filtered[1].CreatedAt.Format(time.RFC3339))
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
assert.Zero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
since, err := time.Parse(time.RFC3339, req.URL.Query().Get("since"))
require.NoError(t, err)
for _, e := range body.Data {
assert.True(t, e.CreatedAt.After(since) || e.CreatedAt.Equal(since))
}
},
},
{
name: "OK: after",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
var filtered []ennoblementWithServer
for _, e := range ennoblements {
if e.Server.Key == server.Key {
filtered = append(filtered, e)
}
}
slices.SortFunc(filtered, func(a, b ennoblementWithServer) int {
return cmp.Or(
a.CreatedAt.Compare(b.CreatedAt),
cmp.Compare(a.Id, b.Id),
)
})
require.GreaterOrEqual(t, len(filtered), 2)
q := req.URL.Query()
q.Set("before", filtered[1].CreatedAt.Format(time.RFC3339))
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
assert.Zero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
before, err := time.Parse(time.RFC3339, req.URL.Query().Get("before"))
require.NoError(t, err)
for _, e := range body.Data {
assert.True(t, e.CreatedAt.Before(before))
}
},
},
{
name: "ERR: limit is not a string",
reqModifier: func(t *testing.T, req *http.Request) {
@ -306,6 +413,174 @@ func TestListEnnoblements(t *testing.T) {
}, body)
},
},
{
name: "ERR: len(sort) > 2",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("sort", "createdAt:DESC")
q.Add("sort", "createdAt:ASC")
q.Add("sort", "createdAt:ASC")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
domainErr := domain.LenOutOfRangeError{
Min: 1,
Max: 2,
Current: len(req.URL.Query()["sort"]),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: domainErr.Code(),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "sort"},
},
},
}, body)
},
},
{
name: "ERR: invalid sort",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("sort", "createdAt:")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
domainErr := domain.UnsupportedSortStringError{
Sort: req.URL.Query()["sort"][0],
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: domainErr.Code(),
Message: domainErr.Error(),
Params: map[string]any{
"sort": domainErr.Sort,
},
Path: []string{"$query", "sort", "0"},
},
},
}, body)
},
},
{
name: "ERR: sort conflict",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("sort", "createdAt:DESC")
q.Add("sort", "createdAt:ASC")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
q := req.URL.Query()
domainErr := domain.SortConflictError{
Sort: [2]string{q["sort"][0], q["sort"][1]},
}
paramSort := make([]any, len(domainErr.Sort))
for i, s := range domainErr.Sort {
paramSort[i] = s
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: domainErr.Code(),
Message: domainErr.Error(),
Params: map[string]any{
"sort": paramSort,
},
Path: []string{"$query", "sort"},
},
},
}, body)
},
},
{
name: "ERR: invalid since",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("since", gofakeit.LetterN(100))
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
var domainErr domain.Error
require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
since := req.URL.Query().Get("since")
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: "invalid-param-format",
//nolint:lll
Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", since, since, since),
Path: []string{"$query", "since"},
},
},
}, body)
},
},
{
name: "ERR: invalid before",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("before", gofakeit.LetterN(100))
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
var domainErr domain.Error
require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
before := req.URL.Query().Get("before")
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: "invalid-param-format",
//nolint:lll
Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", before, before, before),
Path: []string{"$query", "before"},
},
},
}, body)
},
},
{
name: "ERR: version not found",
reqModifier: func(t *testing.T, req *http.Request) {