feat: sort validation improvements (#14)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details

Reviewed-on: twhelp/corev3#14
This commit is contained in:
Dawid Wysokiński 2024-03-02 08:49:42 +00:00
parent 646dfedde4
commit 48b87eea81
30 changed files with 1311 additions and 82 deletions

View File

@ -92,7 +92,7 @@ func (a listEnnoblementsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuer
case domain.EnnoblementSortServerKeyDESC:
q = q.Order("ennoblement.server_key DESC")
default:
return q.Err(errInvalidSortValue)
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
}

View File

@ -211,7 +211,7 @@ func (a listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
case domain.PlayerSortDeletedAtDESC:
q = q.OrderExpr("COALESCE(player.deleted_at, ?) DESC", time.Time{})
default:
return q.Err(errInvalidSortValue)
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
}
@ -306,7 +306,7 @@ func (a listPlayersParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQue
el.column = "COALESCE(player.deleted_at, '0001-01-01 00:00:00+00:00')"
el.direction = sortDirectionDESC
default:
return q.Err(errInvalidSortValue)
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
cursorApplier.data = append(cursorApplier.data, el)

View File

@ -97,7 +97,7 @@ func (a listPlayerSnapshotsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQ
case domain.PlayerSnapshotSortServerKeyDESC:
q = q.Order("ps.server_key DESC")
default:
return q.Err(errInvalidSortValue)
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
}

View File

@ -228,7 +228,7 @@ func (a listServersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
case domain.ServerSortOpenDESC:
q = q.Order("server.open DESC")
default:
return q.Err(errInvalidSortValue)
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
}
@ -272,7 +272,7 @@ func (a listServersParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQue
el.column = "server.open"
el.direction = sortDirectionDESC
default:
return q.Err(errInvalidSortValue)
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
cursorApplier.data = append(cursorApplier.data, el)

View File

@ -212,7 +212,7 @@ func (a listTribesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
case domain.TribeSortDeletedAtDESC:
q = q.OrderExpr("COALESCE(tribe.deleted_at, ?) DESC", time.Time{})
default:
return q.Err(errInvalidSortValue)
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
}
@ -317,7 +317,7 @@ func (a listTribesParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuer
el.column = "COALESCE(tribe.deleted_at, '0001-01-01 00:00:00+00:00')"
el.direction = sortDirectionDESC
default:
return q.Err(errInvalidSortValue)
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
cursorApplier.data = append(cursorApplier.data, el)

View File

@ -90,7 +90,7 @@ func (a listTribeChangesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuer
case domain.TribeChangeSortServerKeyDESC:
q = q.Order("tc.server_key DESC")
default:
return q.Err(errInvalidSortValue)
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
}

View File

@ -96,7 +96,7 @@ func (a listTribeSnapshotsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQu
case domain.TribeSnapshotSortServerKeyDESC:
q = q.Order("ts.server_key DESC")
default:
return q.Err(errInvalidSortValue)
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
}

View File

@ -56,7 +56,7 @@ func (a listVersionsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
case domain.VersionSortCodeDESC:
q = q.Order("version.code DESC")
default:
return q.Err(errInvalidSortValue)
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
}
@ -90,7 +90,7 @@ func (a listVersionsParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQu
el.column = "version.code"
el.direction = sortDirectionDESC
default:
return q.Err(errInvalidSortValue)
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
cursorApplier.data = append(cursorApplier.data, el)

View File

@ -135,7 +135,7 @@ func (a listVillagesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
case domain.VillageSortServerKeyDESC:
q = q.Order("village.server_key DESC")
default:
return q.Err(errInvalidSortValue)
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
}

View File

@ -3,6 +3,7 @@ package domain
import (
"fmt"
"math"
"slices"
"time"
)
@ -165,6 +166,34 @@ const (
EnnoblementSortServerKeyDESC
)
// 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}
slices.Sort(ss)
// ASC is always an odd number, DESC is always an even number
return (ss[0]%2 == 1 && ss[0] == ss[1]-1) || ss[0] == ss[1]
}
//nolint:gocyclo
func (s EnnoblementSort) String() string {
switch s {
case EnnoblementSortCreatedAtASC:
return "createdAt:ASC"
case EnnoblementSortCreatedAtDESC:
return "createdAt:DESC"
case EnnoblementSortIDASC:
return "id:ASC"
case EnnoblementSortIDDESC:
return "id:DESC"
case EnnoblementSortServerKeyASC:
return "serverKey:ASC"
case EnnoblementSortServerKeyDESC:
return "serverKey:DESC"
default:
return "unknown ennoblement sort"
}
}
type ListEnnoblementsParams struct {
serverKeys []string
sort []EnnoblementSort
@ -207,7 +236,7 @@ const (
)
func (params *ListEnnoblementsParams) SetSort(sort []EnnoblementSort) error {
if err := validateSliceLen(sort, ennoblementSortMinLength, ennoblementSortMaxLength); err != nil {
if err := validateSort(sort, ennoblementSortMinLength, ennoblementSortMaxLength); err != nil {
return ValidationError{
Model: listEnnoblementsParamsModelName,
Field: "sort",

View File

@ -37,6 +37,71 @@ func TestNewCreateEnnoblementParams(t *testing.T) {
}
}
func TestEnnoblementSort_IsInConflict(t *testing.T) {
t.Parallel()
type args struct {
sorts [2]domain.EnnoblementSort
}
tests := []struct {
name string
args args
expectedRes bool
}{
{
name: "OK: id:ASC serverKey:ASC",
args: args{
sorts: [2]domain.EnnoblementSort{domain.EnnoblementSortIDASC, domain.EnnoblementSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:DESC serverKey:ASC",
args: args{
sorts: [2]domain.EnnoblementSort{domain.EnnoblementSortIDDESC, domain.EnnoblementSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:ASC id:ASC",
args: args{
sorts: [2]domain.EnnoblementSort{domain.EnnoblementSortIDASC, domain.EnnoblementSortIDASC},
},
expectedRes: true,
},
{
name: "OK: id:ASC id:DESC",
args: args{
sorts: [2]domain.EnnoblementSort{domain.EnnoblementSortIDDESC, domain.EnnoblementSortIDDESC},
},
expectedRes: true,
},
{
name: "OK: createdAt:ASC createdAt:DESC",
args: args{
sorts: [2]domain.EnnoblementSort{domain.EnnoblementSortCreatedAtDESC, domain.EnnoblementSortCreatedAtDESC},
},
expectedRes: true,
},
{
name: "OK: serverKey:DESC serverKey:ASC",
args: args{
sorts: [2]domain.EnnoblementSort{domain.EnnoblementSortServerKeyDESC, domain.EnnoblementSortServerKeyASC},
},
expectedRes: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expectedRes, tt.args.sorts[0].IsInConflict(tt.args.sorts[1]))
})
}
}
func TestListEnnoblementsParams_SetSort(t *testing.T) {
t.Parallel()
@ -94,6 +159,22 @@ func TestListEnnoblementsParams_SetSort(t *testing.T) {
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []domain.EnnoblementSort{
domain.EnnoblementSortIDASC,
domain.EnnoblementSortIDDESC,
},
},
expectedErr: domain.ValidationError{
Model: "ListEnnoblementsParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.EnnoblementSortIDASC.String(), domain.EnnoblementSortIDDESC.String()},
},
},
},
}
for _, tt := range tests {

View File

@ -410,34 +410,73 @@ const (
PlayerSortDeletedAtDESC
)
// IsInConflict returns true if two sorts can't be used together (e.g. PlayerSortIDASC and PlayerSortIDDESC).
func (s PlayerSort) IsInConflict(s2 PlayerSort) bool {
ss := []PlayerSort{s, s2}
slices.Sort(ss)
// ASC is always an odd number, DESC is always an even number
return (ss[0]%2 == 1 && ss[0] == ss[1]-1) || ss[0] == ss[1]
}
//nolint:gocyclo
func newPlayerSortFromString(s string) (PlayerSort, error) {
switch strings.ToLower(s) {
case "odscoreatt:asc":
return PlayerSortODScoreAttASC, nil
case "odscoreatt:desc":
return PlayerSortODScoreAttDESC, nil
case "odscoredef:asc":
return PlayerSortODScoreDefASC, nil
case "odscoredef:desc":
return PlayerSortODScoreDefDESC, nil
case "odscoretotal:asc":
return PlayerSortODScoreTotalASC, nil
case "odscoretotal:desc":
return PlayerSortODScoreTotalDESC, nil
case "points:asc":
return PlayerSortPointsASC, nil
case "points:desc":
return PlayerSortPointsDESC, nil
case "deletedat:asc":
return PlayerSortDeletedAtASC, nil
case "deletedat:desc":
return PlayerSortDeletedAtDESC, nil
func (s PlayerSort) String() string {
switch s {
case PlayerSortIDASC:
return "id:ASC"
case PlayerSortIDDESC:
return "id:DESC"
case PlayerSortServerKeyASC:
return "serverKey:ASC"
case PlayerSortServerKeyDESC:
return "serverKey:DESC"
case PlayerSortODScoreAttASC:
return "odScoreAtt:ASC"
case PlayerSortODScoreAttDESC:
return "odScoreAtt:DESC"
case PlayerSortODScoreDefASC:
return "odScoreDef:ASC"
case PlayerSortODScoreDefDESC:
return "odScoreDef:DESC"
case PlayerSortODScoreTotalASC:
return "odScoreTotal:ASC"
case PlayerSortODScoreTotalDESC:
return "odScoreTotal:DESC"
case PlayerSortPointsASC:
return "points:ASC"
case PlayerSortPointsDESC:
return "points:DESC"
case PlayerSortDeletedAtASC:
return "deletedAt:ASC"
case PlayerSortDeletedAtDESC:
return "deletedAt:DESC"
default:
return 0, UnsupportedSortStringError{
Sort: s,
return "unknown player sort"
}
}
func newPlayerSortFromString(s string) (PlayerSort, error) {
allowed := []PlayerSort{
PlayerSortODScoreAttASC,
PlayerSortODScoreAttDESC,
PlayerSortODScoreDefASC,
PlayerSortODScoreDefDESC,
PlayerSortODScoreTotalASC,
PlayerSortODScoreTotalDESC,
PlayerSortPointsASC,
PlayerSortPointsDESC,
PlayerSortDeletedAtASC,
PlayerSortDeletedAtDESC,
}
for _, a := range allowed {
if strings.EqualFold(a.String(), s) {
return a, nil
}
}
return 0, UnsupportedSortStringError{
Sort: s,
}
}
type PlayerCursor struct {
@ -734,7 +773,7 @@ const (
)
func (params *ListPlayersParams) SetSort(sort []PlayerSort) error {
if err := validateSliceLen(sort, playerSortMinLength, playerSortMaxLength); err != nil {
if err := validateSort(sort, playerSortMinLength, playerSortMaxLength); err != nil {
return ValidationError{
Model: listPlayersParamsModelName,
Field: "sort",
@ -756,8 +795,10 @@ func (params *ListPlayersParams) PrependSortString(sort []string) error {
}
}
for i := len(sort) - 1; i >= 0; i-- {
converted, err := newPlayerSortFromString(sort[i])
toPrepend := make([]PlayerSort, 0, len(sort))
for i, s := range sort {
converted, err := newPlayerSortFromString(s)
if err != nil {
return SliceElementValidationError{
Model: listPlayersParamsModelName,
@ -766,10 +807,10 @@ func (params *ListPlayersParams) PrependSortString(sort []string) error {
Err: err,
}
}
params.sort = append([]PlayerSort{converted}, params.sort...)
toPrepend = append(toPrepend, converted)
}
return nil
return params.SetSort(append(toPrepend, params.sort...))
}
func (params *ListPlayersParams) Cursor() PlayerCursor {

View File

@ -3,6 +3,7 @@ package domain
import (
"fmt"
"math"
"slices"
"time"
)
@ -198,6 +199,35 @@ const (
PlayerSnapshotSortServerKeyDESC
)
// IsInConflict returns true if two sorts can't be used together
// (e.g. PlayerSnapshotSortIDASC and PlayerSnapshotSortIDDESC).
func (s PlayerSnapshotSort) IsInConflict(s2 PlayerSnapshotSort) bool {
ss := []PlayerSnapshotSort{s, s2}
slices.Sort(ss)
// ASC is always an odd number, DESC is always an even number
return (ss[0]%2 == 1 && ss[0] == ss[1]-1) || ss[0] == ss[1]
}
//nolint:gocyclo
func (s PlayerSnapshotSort) String() string {
switch s {
case PlayerSnapshotSortDateASC:
return "date:ASC"
case PlayerSnapshotSortDateDESC:
return "date:DESC"
case PlayerSnapshotSortIDASC:
return "id:ASC"
case PlayerSnapshotSortIDDESC:
return "id:DESC"
case PlayerSnapshotSortServerKeyASC:
return "serverKey:ASC"
case PlayerSnapshotSortServerKeyDESC:
return "serverKey:DESC"
default:
return "unknown player snapshot sort"
}
}
const PlayerSnapshotListMaxLimit = 200
type ListPlayerSnapshotsParams struct {
@ -239,7 +269,7 @@ const (
)
func (params *ListPlayerSnapshotsParams) SetSort(sort []PlayerSnapshotSort) error {
if err := validateSliceLen(sort, playerSnapshotSortMinLength, playerSnapshotSortMaxLength); err != nil {
if err := validateSort(sort, playerSnapshotSortMinLength, playerSnapshotSortMaxLength); err != nil {
return ValidationError{
Model: listPlayerSnapshotsParamsModelName,
Field: "sort",

View File

@ -44,6 +44,71 @@ func TestNewCreatePlayerSnapshotParams(t *testing.T) {
}
}
func TestPlayerSnapshotSort_IsInConflict(t *testing.T) {
t.Parallel()
type args struct {
sorts [2]domain.PlayerSnapshotSort
}
tests := []struct {
name string
args args
expectedRes bool
}{
{
name: "OK: id:ASC serverKey:ASC",
args: args{
sorts: [2]domain.PlayerSnapshotSort{domain.PlayerSnapshotSortIDASC, domain.PlayerSnapshotSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:DESC serverKey:ASC",
args: args{
sorts: [2]domain.PlayerSnapshotSort{domain.PlayerSnapshotSortIDDESC, domain.PlayerSnapshotSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:ASC id:ASC",
args: args{
sorts: [2]domain.PlayerSnapshotSort{domain.PlayerSnapshotSortIDASC, domain.PlayerSnapshotSortIDASC},
},
expectedRes: true,
},
{
name: "OK: id:ASC id:DESC",
args: args{
sorts: [2]domain.PlayerSnapshotSort{domain.PlayerSnapshotSortIDASC, domain.PlayerSnapshotSortIDDESC},
},
expectedRes: true,
},
{
name: "OK: date:ASC date:DESC",
args: args{
sorts: [2]domain.PlayerSnapshotSort{domain.PlayerSnapshotSortDateASC, domain.PlayerSnapshotSortDateDESC},
},
expectedRes: true,
},
{
name: "OK: serverKey:DESC serverKey:ASC",
args: args{
sorts: [2]domain.PlayerSnapshotSort{domain.PlayerSnapshotSortServerKeyDESC, domain.PlayerSnapshotSortServerKeyASC},
},
expectedRes: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expectedRes, tt.args.sorts[0].IsInConflict(tt.args.sorts[1]))
})
}
}
func TestListPlayerSnapshotsParams_SetSort(t *testing.T) {
t.Parallel()
@ -100,6 +165,22 @@ func TestListPlayerSnapshotsParams_SetSort(t *testing.T) {
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []domain.PlayerSnapshotSort{
domain.PlayerSnapshotSortIDASC,
domain.PlayerSnapshotSortIDDESC,
},
},
expectedErr: domain.ValidationError{
Model: "ListPlayerSnapshotsParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.PlayerSnapshotSortIDASC.String(), domain.PlayerSnapshotSortIDDESC.String()},
},
},
},
}
for _, tt := range tests {

View File

@ -228,6 +228,99 @@ func TestNewCreatePlayerParams(t *testing.T) {
}
}
func TestPlayerSort_IsInConflict(t *testing.T) {
t.Parallel()
type args struct {
sorts [2]domain.PlayerSort
}
tests := []struct {
name string
args args
expectedRes bool
}{
{
name: "OK: id:ASC serverKey:ASC",
args: args{
sorts: [2]domain.PlayerSort{domain.PlayerSortIDASC, domain.PlayerSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:DESC serverKey:ASC",
args: args{
sorts: [2]domain.PlayerSort{domain.PlayerSortIDDESC, domain.PlayerSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:ASC id:ASC",
args: args{
sorts: [2]domain.PlayerSort{domain.PlayerSortIDASC, domain.PlayerSortIDASC},
},
expectedRes: true,
},
{
name: "OK: id:ASC id:DESC",
args: args{
sorts: [2]domain.PlayerSort{domain.PlayerSortIDASC, domain.PlayerSortIDDESC},
},
expectedRes: true,
},
{
name: "OK: serverKey:DESC serverKey:ASC",
args: args{
sorts: [2]domain.PlayerSort{domain.PlayerSortServerKeyDESC, domain.PlayerSortServerKeyASC},
},
expectedRes: true,
},
{
name: "OK: odScoreAtt:ASC odScoreAtt:DESC",
args: args{
sorts: [2]domain.PlayerSort{domain.PlayerSortODScoreAttASC, domain.PlayerSortODScoreAttDESC},
},
expectedRes: true,
},
{
name: "OK: odScoreDef:ASC odScoreDef:DESC",
args: args{
sorts: [2]domain.PlayerSort{domain.PlayerSortODScoreDefASC, domain.PlayerSortODScoreDefDESC},
},
expectedRes: true,
},
{
name: "OK: odScoreTotal:ASC odScoreTotal:DESC",
args: args{
sorts: [2]domain.PlayerSort{domain.PlayerSortODScoreTotalASC, domain.PlayerSortODScoreTotalDESC},
},
expectedRes: true,
},
{
name: "OK: points:ASC points:DESC",
args: args{
sorts: [2]domain.PlayerSort{domain.PlayerSortPointsASC, domain.PlayerSortPointsDESC},
},
expectedRes: true,
},
{
name: "OK: deletedAt:ASC deletedAt:DESC",
args: args{
sorts: [2]domain.PlayerSort{domain.PlayerSortDeletedAtASC, domain.PlayerSortDeletedAtDESC},
},
expectedRes: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expectedRes, tt.args.sorts[0].IsInConflict(tt.args.sorts[1]))
})
}
}
func TestNewPlayerCursor(t *testing.T) {
t.Parallel()
@ -649,6 +742,22 @@ func TestListPlayersParams_SetSort(t *testing.T) {
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []domain.PlayerSort{
domain.PlayerSortIDASC,
domain.PlayerSortIDDESC,
},
},
expectedErr: domain.ValidationError{
Model: "ListPlayersParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.PlayerSortIDASC.String(), domain.PlayerSortIDDESC.String()},
},
},
},
}
for _, tt := range tests {
@ -820,6 +929,22 @@ func TestListPlayersParams_PrependSortString(t *testing.T) {
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []string{
"odScoreAtt:ASC",
"odScoreAtt:DESC",
},
},
expectedErr: domain.ValidationError{
Model: "ListPlayersParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.PlayerSortODScoreAttASC.String(), domain.PlayerSortODScoreAttDESC.String()},
},
},
},
}
for _, tt := range tests {

View File

@ -521,6 +521,29 @@ const (
ServerSortOpenDESC
)
// IsInConflict returns true if two sorts can't be used together (e.g. ServerSortKeyASC and ServerSortKeyDESC).
func (s ServerSort) IsInConflict(s2 ServerSort) bool {
ss := []ServerSort{s, s2}
slices.Sort(ss)
// ASC is always an odd number, DESC is always an even number
return (ss[0]%2 == 1 && ss[0] == ss[1]-1) || ss[0] == ss[1]
}
func (s ServerSort) String() string {
switch s {
case ServerSortKeyASC:
return "key:ASC"
case ServerSortKeyDESC:
return "key:DESC"
case ServerSortOpenASC:
return "open:ASC"
case ServerSortOpenDESC:
return "open:DESC"
default:
return "unknown server sort"
}
}
type ServerCursor struct {
key string
open bool
@ -703,7 +726,7 @@ const (
)
func (params *ListServersParams) SetSort(sort []ServerSort) error {
if err := validateSliceLen(sort, serverSortMinLength, serverSortMaxLength); err != nil {
if err := validateSort(sort, serverSortMinLength, serverSortMaxLength); err != nil {
return ValidationError{
Model: listServersParamsModelName,
Field: "sort",

View File

@ -498,6 +498,71 @@ func TestUpdateServerParams_SetNumBonusVillages(t *testing.T) {
}
}
func TestServerSort_IsInConflict(t *testing.T) {
t.Parallel()
type args struct {
sorts [2]domain.ServerSort
}
tests := []struct {
name string
args args
expectedRes bool
}{
{
name: "OK: key:ASC open:ASC",
args: args{
sorts: [2]domain.ServerSort{domain.ServerSortKeyASC, domain.ServerSortOpenASC},
},
expectedRes: false,
},
{
name: "OK: key:ASC open:DESC",
args: args{
sorts: [2]domain.ServerSort{domain.ServerSortKeyASC, domain.ServerSortOpenDESC},
},
expectedRes: false,
},
{
name: "OK: key:DESC open:ASC",
args: args{
sorts: [2]domain.ServerSort{domain.ServerSortKeyDESC, domain.ServerSortOpenASC},
},
expectedRes: false,
},
{
name: "OK: key:ASC key:DESC",
args: args{
sorts: [2]domain.ServerSort{domain.ServerSortKeyASC, domain.ServerSortKeyDESC},
},
expectedRes: true,
},
{
name: "OK: open:DESC open:ASC",
args: args{
sorts: [2]domain.ServerSort{domain.ServerSortOpenDESC, domain.ServerSortOpenASC},
},
expectedRes: true,
},
{
name: "OK: open:DESC open:DESC",
args: args{
sorts: [2]domain.ServerSort{domain.ServerSortOpenDESC, domain.ServerSortOpenDESC},
},
expectedRes: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expectedRes, tt.args.sorts[0].IsInConflict(tt.args.sorts[1]))
})
}
}
func TestNewServerCursor(t *testing.T) {
t.Parallel()
@ -609,6 +674,22 @@ func TestListServersParams_SetSort(t *testing.T) {
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []domain.ServerSort{
domain.ServerSortKeyASC,
domain.ServerSortKeyDESC,
},
},
expectedErr: domain.ValidationError{
Model: "ListServersParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.ServerSortKeyASC.String(), domain.ServerSortKeyDESC.String()},
},
},
},
}
for _, tt := range tests {

View File

@ -450,38 +450,79 @@ const (
TribeSortDeletedAtDESC
)
// IsInConflict returns true if two sorts can't be used together (e.g. TribeSortIDASC and TribeSortIDDESC).
func (s TribeSort) IsInConflict(s2 TribeSort) bool {
ss := []TribeSort{s, s2}
slices.Sort(ss)
// ASC is always an odd number, DESC is always an even number
return (ss[0]%2 == 1 && ss[0] == ss[1]-1) || ss[0] == ss[1]
}
//nolint:gocyclo
func newTribeSortFromString(s string) (TribeSort, error) {
switch strings.ToLower(s) {
case "odscoreatt:asc":
return TribeSortODScoreAttASC, nil
case "odscoreatt:desc":
return TribeSortODScoreAttDESC, nil
case "odscoredef:asc":
return TribeSortODScoreDefASC, nil
case "odscoredef:desc":
return TribeSortODScoreDefDESC, nil
case "odscoretotal:asc":
return TribeSortODScoreTotalASC, nil
case "odscoretotal:desc":
return TribeSortODScoreTotalDESC, nil
case "points:asc":
return TribeSortPointsASC, nil
case "points:desc":
return TribeSortPointsDESC, nil
case "dominance:asc":
return TribeSortDominanceASC, nil
case "dominance:desc":
return TribeSortDominanceDESC, nil
case "deletedat:asc":
return TribeSortDeletedAtASC, nil
case "deletedat:desc":
return TribeSortDeletedAtDESC, nil
func (s TribeSort) String() string {
switch s {
case TribeSortIDASC:
return "id:ASC"
case TribeSortIDDESC:
return "id:DESC"
case TribeSortServerKeyASC:
return "serverKey:ASC"
case TribeSortServerKeyDESC:
return "serverKey:DESC"
case TribeSortODScoreAttASC:
return "odScoreAtt:ASC"
case TribeSortODScoreAttDESC:
return "odScoreAtt:DESC"
case TribeSortODScoreDefASC:
return "odScoreDef:ASC"
case TribeSortODScoreDefDESC:
return "odScoreDef:DESC"
case TribeSortODScoreTotalASC:
return "odScoreTotal:ASC"
case TribeSortODScoreTotalDESC:
return "odScoreTotal:DESC"
case TribeSortPointsASC:
return "points:ASC"
case TribeSortPointsDESC:
return "points:DESC"
case TribeSortDominanceASC:
return "dominance:ASC"
case TribeSortDominanceDESC:
return "dominance:DESC"
case TribeSortDeletedAtASC:
return "deletedAt:ASC"
case TribeSortDeletedAtDESC:
return "deletedAt:DESC"
default:
return 0, UnsupportedSortStringError{
Sort: s,
return "unknown tribe sort"
}
}
func newTribeSortFromString(s string) (TribeSort, error) {
allowed := []TribeSort{
TribeSortODScoreAttASC,
TribeSortODScoreAttDESC,
TribeSortODScoreDefASC,
TribeSortODScoreDefDESC,
TribeSortODScoreTotalASC,
TribeSortODScoreTotalDESC,
TribeSortPointsASC,
TribeSortPointsDESC,
TribeSortDominanceASC,
TribeSortDominanceDESC,
TribeSortDeletedAtASC,
TribeSortDeletedAtDESC,
}
for _, a := range allowed {
if strings.EqualFold(a.String(), s) {
return a, nil
}
}
return 0, UnsupportedSortStringError{
Sort: s,
}
}
type TribeCursor struct {
@ -735,7 +776,19 @@ func (params *ListTribesParams) ServerKeys() []string {
}
func (params *ListTribesParams) SetServerKeys(serverKeys []string) error {
for i, sk := range serverKeys {
if err := validateServerKey(sk); err != nil {
return SliceElementValidationError{
Model: listTribesParamsModelName,
Field: "serverKeys",
Index: i,
Err: err,
}
}
}
params.serverKeys = serverKeys
return nil
}
@ -781,7 +834,7 @@ const (
)
func (params *ListTribesParams) SetSort(sort []TribeSort) error {
if err := validateSliceLen(sort, tribeSortMinLength, tribeSortMaxLength); err != nil {
if err := validateSort(sort, tribeSortMinLength, tribeSortMaxLength); err != nil {
return ValidationError{
Model: listTribesParamsModelName,
Field: "sort",
@ -803,8 +856,10 @@ func (params *ListTribesParams) PrependSortString(sort []string) error {
}
}
for i := len(sort) - 1; i >= 0; i-- {
converted, err := newTribeSortFromString(sort[i])
toPrepend := make([]TribeSort, 0, len(sort))
for i, s := range sort {
converted, err := newTribeSortFromString(s)
if err != nil {
return SliceElementValidationError{
Model: listTribesParamsModelName,
@ -813,10 +868,10 @@ func (params *ListTribesParams) PrependSortString(sort []string) error {
Err: err,
}
}
params.sort = append([]TribeSort{converted}, params.sort...)
toPrepend = append(toPrepend, converted)
}
return nil
return params.SetSort(append(toPrepend, params.sort...))
}
func (params *ListTribesParams) Cursor() TribeCursor {

View File

@ -213,6 +213,34 @@ const (
TribeChangeSortServerKeyDESC
)
// IsInConflict returns true if two sorts can't be used together (e.g. TribeChangeSortIDASC and TribeChangeSortIDDESC).
func (s TribeChangeSort) IsInConflict(s2 TribeChangeSort) bool {
ss := []TribeChangeSort{s, s2}
slices.Sort(ss)
// ASC is always an odd number, DESC is always an even number
return (ss[0]%2 == 1 && ss[0] == ss[1]-1) || ss[0] == ss[1]
}
//nolint:gocyclo
func (s TribeChangeSort) String() string {
switch s {
case TribeChangeSortCreatedAtASC:
return "createdAt:ASC"
case TribeChangeSortCreatedAtDESC:
return "createdAt:DESC"
case TribeChangeSortIDASC:
return "id:ASC"
case TribeChangeSortIDDESC:
return "id:DESC"
case TribeChangeSortServerKeyASC:
return "serverKey:ASC"
case TribeChangeSortServerKeyDESC:
return "serverKey:DESC"
default:
return "unknown tribe change sort"
}
}
type ListTribeChangesParams struct {
serverKeys []string
sort []TribeChangeSort
@ -255,7 +283,7 @@ const (
)
func (params *ListTribeChangesParams) SetSort(sort []TribeChangeSort) error {
if err := validateSliceLen(sort, tribeChangeSortMinLength, tribeChangeSortMaxLength); err != nil {
if err := validateSort(sort, tribeChangeSortMinLength, tribeChangeSortMaxLength); err != nil {
return ValidationError{
Model: listTribeChangesParamsModelName,
Field: "sort",

View File

@ -214,6 +214,71 @@ func TestNewCreateTribeChangeParamsFromPlayers(t *testing.T) {
}
}
func TestTribeChangeSort_IsInConflict(t *testing.T) {
t.Parallel()
type args struct {
sorts [2]domain.TribeChangeSort
}
tests := []struct {
name string
args args
expectedRes bool
}{
{
name: "OK: id:ASC serverKey:ASC",
args: args{
sorts: [2]domain.TribeChangeSort{domain.TribeChangeSortIDASC, domain.TribeChangeSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:DESC serverKey:ASC",
args: args{
sorts: [2]domain.TribeChangeSort{domain.TribeChangeSortIDDESC, domain.TribeChangeSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:ASC id:ASC",
args: args{
sorts: [2]domain.TribeChangeSort{domain.TribeChangeSortIDASC, domain.TribeChangeSortIDASC},
},
expectedRes: true,
},
{
name: "OK: id:ASC id:DESC",
args: args{
sorts: [2]domain.TribeChangeSort{domain.TribeChangeSortIDDESC, domain.TribeChangeSortIDDESC},
},
expectedRes: true,
},
{
name: "OK: createdAt:ASC createdAt:DESC",
args: args{
sorts: [2]domain.TribeChangeSort{domain.TribeChangeSortCreatedAtDESC, domain.TribeChangeSortCreatedAtDESC},
},
expectedRes: true,
},
{
name: "OK: serverKey:DESC serverKey:ASC",
args: args{
sorts: [2]domain.TribeChangeSort{domain.TribeChangeSortServerKeyDESC, domain.TribeChangeSortServerKeyASC},
},
expectedRes: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expectedRes, tt.args.sorts[0].IsInConflict(tt.args.sorts[1]))
})
}
}
func TestListTribeChangesParams_SetSort(t *testing.T) {
t.Parallel()
@ -270,6 +335,22 @@ func TestListTribeChangesParams_SetSort(t *testing.T) {
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []domain.TribeChangeSort{
domain.TribeChangeSortIDASC,
domain.TribeChangeSortIDDESC,
},
},
expectedErr: domain.ValidationError{
Model: "ListTribeChangesParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.TribeChangeSortIDASC.String(), domain.TribeChangeSortIDDESC.String()},
},
},
},
}
for _, tt := range tests {

View File

@ -3,6 +3,7 @@ package domain
import (
"fmt"
"math"
"slices"
"time"
)
@ -224,6 +225,35 @@ const (
TribeSnapshotSortServerKeyDESC
)
// IsInConflict returns true if two sorts can't be used together
// (e.g. TribeSnapshotSortIDASC and TribeSnapshotSortIDDESC).
func (s TribeSnapshotSort) IsInConflict(s2 TribeSnapshotSort) bool {
ss := []TribeSnapshotSort{s, s2}
slices.Sort(ss)
// ASC is always an odd number, DESC is always an even number
return (ss[0]%2 == 1 && ss[0] == ss[1]-1) || ss[0] == ss[1]
}
//nolint:gocyclo
func (s TribeSnapshotSort) String() string {
switch s {
case TribeSnapshotSortDateASC:
return "date:ASC"
case TribeSnapshotSortDateDESC:
return "date:DESC"
case TribeSnapshotSortIDASC:
return "id:ASC"
case TribeSnapshotSortIDDESC:
return "id:DESC"
case TribeSnapshotSortServerKeyASC:
return "serverKey:ASC"
case TribeSnapshotSortServerKeyDESC:
return "serverKey:DESC"
default:
return "unknown tribe snapshot sort"
}
}
type ListTribeSnapshotsParams struct {
serverKeys []string
sort []TribeSnapshotSort
@ -266,7 +296,7 @@ const (
)
func (params *ListTribeSnapshotsParams) SetSort(sort []TribeSnapshotSort) error {
if err := validateSliceLen(sort, tribeSnapshotSortMinLength, tribeSnapshotSortMaxLength); err != nil {
if err := validateSort(sort, tribeSnapshotSortMinLength, tribeSnapshotSortMaxLength); err != nil {
return ValidationError{
Model: listTribeSnapshotsParamsModelName,
Field: "sort",

View File

@ -46,6 +46,71 @@ func TestNewCreateTribeSnapshotParams(t *testing.T) {
}
}
func TestTribeSnapshotSort_IsInConflict(t *testing.T) {
t.Parallel()
type args struct {
sorts [2]domain.TribeSnapshotSort
}
tests := []struct {
name string
args args
expectedRes bool
}{
{
name: "OK: id:ASC serverKey:ASC",
args: args{
sorts: [2]domain.TribeSnapshotSort{domain.TribeSnapshotSortIDASC, domain.TribeSnapshotSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:DESC serverKey:ASC",
args: args{
sorts: [2]domain.TribeSnapshotSort{domain.TribeSnapshotSortIDDESC, domain.TribeSnapshotSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:ASC id:ASC",
args: args{
sorts: [2]domain.TribeSnapshotSort{domain.TribeSnapshotSortIDASC, domain.TribeSnapshotSortIDASC},
},
expectedRes: true,
},
{
name: "OK: id:ASC id:DESC",
args: args{
sorts: [2]domain.TribeSnapshotSort{domain.TribeSnapshotSortIDASC, domain.TribeSnapshotSortIDDESC},
},
expectedRes: true,
},
{
name: "OK: date:ASC date:DESC",
args: args{
sorts: [2]domain.TribeSnapshotSort{domain.TribeSnapshotSortDateASC, domain.TribeSnapshotSortDateDESC},
},
expectedRes: true,
},
{
name: "OK: serverKey:DESC serverKey:ASC",
args: args{
sorts: [2]domain.TribeSnapshotSort{domain.TribeSnapshotSortServerKeyDESC, domain.TribeSnapshotSortServerKeyASC},
},
expectedRes: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expectedRes, tt.args.sorts[0].IsInConflict(tt.args.sorts[1]))
})
}
}
func TestListTribeSnapshotsParams_SetSort(t *testing.T) {
t.Parallel()
@ -102,6 +167,22 @@ func TestListTribeSnapshotsParams_SetSort(t *testing.T) {
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []domain.TribeSnapshotSort{
domain.TribeSnapshotSortIDASC,
domain.TribeSnapshotSortIDDESC,
},
},
expectedErr: domain.ValidationError{
Model: "ListTribeSnapshotsParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.TribeSnapshotSortIDASC.String(), domain.TribeSnapshotSortIDDESC.String()},
},
},
},
}
for _, tt := range tests {

View File

@ -180,6 +180,106 @@ func TestNewCreateTribeParams(t *testing.T) {
}
}
func TestTribeSort_IsInConflict(t *testing.T) {
t.Parallel()
type args struct {
sorts [2]domain.TribeSort
}
tests := []struct {
name string
args args
expectedRes bool
}{
{
name: "OK: id:ASC serverKey:ASC",
args: args{
sorts: [2]domain.TribeSort{domain.TribeSortIDASC, domain.TribeSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:DESC serverKey:ASC",
args: args{
sorts: [2]domain.TribeSort{domain.TribeSortIDDESC, domain.TribeSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:ASC id:ASC",
args: args{
sorts: [2]domain.TribeSort{domain.TribeSortIDASC, domain.TribeSortIDASC},
},
expectedRes: true,
},
{
name: "OK: id:ASC id:DESC",
args: args{
sorts: [2]domain.TribeSort{domain.TribeSortIDASC, domain.TribeSortIDDESC},
},
expectedRes: true,
},
{
name: "OK: serverKey:DESC serverKey:ASC",
args: args{
sorts: [2]domain.TribeSort{domain.TribeSortServerKeyDESC, domain.TribeSortServerKeyASC},
},
expectedRes: true,
},
{
name: "OK: odScoreAtt:ASC odScoreAtt:DESC",
args: args{
sorts: [2]domain.TribeSort{domain.TribeSortODScoreAttASC, domain.TribeSortODScoreAttDESC},
},
expectedRes: true,
},
{
name: "OK: odScoreDef:ASC odScoreDef:DESC",
args: args{
sorts: [2]domain.TribeSort{domain.TribeSortODScoreDefASC, domain.TribeSortODScoreDefDESC},
},
expectedRes: true,
},
{
name: "OK: odScoreTotal:ASC odScoreTotal:DESC",
args: args{
sorts: [2]domain.TribeSort{domain.TribeSortODScoreTotalASC, domain.TribeSortODScoreTotalDESC},
},
expectedRes: true,
},
{
name: "OK: points:ASC points:DESC",
args: args{
sorts: [2]domain.TribeSort{domain.TribeSortPointsASC, domain.TribeSortPointsDESC},
},
expectedRes: true,
},
{
name: "OK: dominance:ASC dominance:DESC",
args: args{
sorts: [2]domain.TribeSort{domain.TribeSortDominanceASC, domain.TribeSortDominanceDESC},
},
expectedRes: true,
},
{
name: "OK: deletedAt:ASC deletedAt:DESC",
args: args{
sorts: [2]domain.TribeSort{domain.TribeSortDeletedAtASC, domain.TribeSortDeletedAtDESC},
},
expectedRes: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expectedRes, tt.args.sorts[0].IsInConflict(tt.args.sorts[1]))
})
}
}
func TestNewTribeCursor(t *testing.T) {
t.Parallel()
@ -436,6 +536,60 @@ func TestListTribesParams_SetIDs(t *testing.T) {
}
}
func TestListTribesParams_SetServerKeys(t *testing.T) {
t.Parallel()
type args struct {
serverKeys []string
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
serverKeys: []string{
domaintest.RandServerKey(),
},
},
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
serverKeys: []string{serverKeyTest.key},
},
expectedErr: domain.SliceElementValidationError{
Model: "ListTribesParams",
Field: "serverKeys",
Index: 0,
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListTribesParams()
require.ErrorIs(t, params.SetServerKeys(tt.args.serverKeys), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.serverKeys, params.ServerKeys())
})
}
}
func TestListTribesParams_SetTags(t *testing.T) {
t.Parallel()
@ -570,6 +724,22 @@ func TestListTribesParams_SetSort(t *testing.T) {
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []domain.TribeSort{
domain.TribeSortIDASC,
domain.TribeSortIDDESC,
},
},
expectedErr: domain.ValidationError{
Model: "ListTribesParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.TribeSortIDASC.String(), domain.TribeSortIDDESC.String()},
},
},
},
}
for _, tt := range tests {
@ -745,6 +915,22 @@ func TestListTribesParams_PrependSortString(t *testing.T) {
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []string{
"odScoreAtt:ASC",
"odScoreAtt:DESC",
},
},
expectedErr: domain.ValidationError{
Model: "ListTribesParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.TribeSortODScoreAttASC.String(), domain.TribeSortODScoreAttDESC.String()},
},
},
},
}
for _, tt := range tests {

View File

@ -244,6 +244,30 @@ func (e UnsupportedSortStringError) Params() map[string]any {
}
}
type SortConflictError struct {
Sort [2]string
}
var _ ErrorWithParams = SortConflictError{}
func (e SortConflictError) Error() string {
return fmt.Sprintf("sort values %s are in conflict and can't be used together", e.Sort[0]+" and "+e.Sort[1])
}
func (e SortConflictError) Type() ErrorType {
return ErrorTypeIncorrectInput
}
func (e SortConflictError) Code() string {
return "sort-conflict"
}
func (e SortConflictError) Params() map[string]any {
return map[string]any{
"Sort": e.Sort,
}
}
var ErrRequired error = simpleError{
msg: "can't be blank",
typ: ErrorTypeIncorrectInput,
@ -322,3 +346,25 @@ func validateIntInRange(current, min, max int) error {
return nil
}
func validateSort[S ~[]E, E interface {
IsInConflict(s E) bool
String() string
}](sort S, min, max int) error {
if err := validateSliceLen(sort, min, max); err != nil {
return err
}
for i, s1 := range sort {
for j := i + 1; j < len(sort); j++ {
s2 := sort[j]
if s1.IsInConflict(s2) {
return SortConflictError{
Sort: [2]string{s1.String(), s2.String()},
}
}
}
}
return nil
}

View File

@ -3,6 +3,7 @@ package domain
import (
"fmt"
"net/url"
"slices"
)
type Version struct {
@ -100,6 +101,25 @@ const (
VersionSortCodeDESC
)
// IsInConflict returns true if two sorts can't be used together (e.g. VersionSortCodeASC and VersionSortCodeDESC).
func (s VersionSort) IsInConflict(s2 VersionSort) bool {
ss := []VersionSort{s, s2}
slices.Sort(ss)
// ASC is always an odd number, DESC is always an even number
return (ss[0]%2 == 1 && ss[0] == ss[1]-1) || ss[0] == ss[1]
}
func (s VersionSort) String() string {
switch s {
case VersionSortCodeASC:
return "code:ASC"
case VersionSortCodeDESC:
return "code:DESC"
default:
return "unknown version sort"
}
}
type VersionCursor struct {
code string
}
@ -207,7 +227,7 @@ func (params *ListVersionsParams) Sort() []VersionSort {
}
func (params *ListVersionsParams) SetSort(sort []VersionSort) error {
if err := validateSliceLen(sort, versionSortMinLength, versionSortMaxLength); err != nil {
if err := validateSort(sort, versionSortMinLength, versionSortMaxLength); err != nil {
return ValidationError{
Model: listVersionsParamsModelName,
Field: "sort",

View File

@ -11,6 +11,43 @@ import (
"github.com/stretchr/testify/require"
)
func TestVersionSort_IsInConflict(t *testing.T) {
t.Parallel()
type args struct {
sorts [2]domain.VersionSort
}
tests := []struct {
name string
args args
expectedRes bool
}{
{
name: "OK: code:ASC code:ASC",
args: args{
sorts: [2]domain.VersionSort{domain.VersionSortCodeASC, domain.VersionSortCodeASC},
},
expectedRes: true,
},
{
name: "OK: code:ASC code:DESC",
args: args{
sorts: [2]domain.VersionSort{domain.VersionSortCodeASC, domain.VersionSortCodeDESC},
},
expectedRes: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expectedRes, tt.args.sorts[0].IsInConflict(tt.args.sorts[1]))
})
}
}
func TestNewVersionCursor(t *testing.T) {
t.Parallel()

View File

@ -224,6 +224,30 @@ const (
VillageSortServerKeyDESC
)
// IsInConflict returns true if two sorts can't be used together (e.g. VillageSortIDASC and VillageSortIDDESC).
func (s VillageSort) IsInConflict(s2 VillageSort) bool {
ss := []VillageSort{s, s2}
slices.Sort(ss)
// ASC is always an odd number, DESC is always an even number
return (ss[0]%2 == 1 && ss[0] == ss[1]-1) || ss[0] == ss[1]
}
//nolint:gocyclo
func (s VillageSort) String() string {
switch s {
case VillageSortIDASC:
return "id:ASC"
case VillageSortIDDESC:
return "id:DESC"
case VillageSortServerKeyASC:
return "serverKey:ASC"
case VillageSortServerKeyDESC:
return "serverKey:DESC"
default:
return "unknown village sort"
}
}
type ListVillagesParams struct {
ids []int
idGT NullInt
@ -308,7 +332,7 @@ const (
)
func (params *ListVillagesParams) SetSort(sort []VillageSort) error {
if err := validateSliceLen(sort, villageSortMinLength, villageSortMaxLength); err != nil {
if err := validateSort(sort, villageSortMinLength, villageSortMaxLength); err != nil {
return ValidationError{
Model: listVillagesParamsModelName,
Field: "sort",

View File

@ -88,6 +88,64 @@ func TestNewCreateVillageParams(t *testing.T) {
}
}
func TestVillageSort_IsInConflict(t *testing.T) {
t.Parallel()
type args struct {
sorts [2]domain.VillageSort
}
tests := []struct {
name string
args args
expectedRes bool
}{
{
name: "OK: id:ASC serverKey:ASC",
args: args{
sorts: [2]domain.VillageSort{domain.VillageSortIDASC, domain.VillageSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:DESC serverKey:ASC",
args: args{
sorts: [2]domain.VillageSort{domain.VillageSortIDDESC, domain.VillageSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:ASC id:ASC",
args: args{
sorts: [2]domain.VillageSort{domain.VillageSortIDASC, domain.VillageSortIDASC},
},
expectedRes: true,
},
{
name: "OK: id:ASC id:DESC",
args: args{
sorts: [2]domain.VillageSort{domain.VillageSortIDASC, domain.VillageSortIDDESC},
},
expectedRes: true,
},
{
name: "OK: serverKey:DESC serverKey:ASC",
args: args{
sorts: [2]domain.VillageSort{domain.VillageSortServerKeyDESC, domain.VillageSortServerKeyASC},
},
expectedRes: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expectedRes, tt.args.sorts[0].IsInConflict(tt.args.sorts[1]))
})
}
}
func TestListVillagesParams_SetIDs(t *testing.T) {
t.Parallel()
@ -258,6 +316,22 @@ func TestListVillagesParams_SetSort(t *testing.T) {
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []domain.VillageSort{
domain.VillageSortIDASC,
domain.VillageSortIDDESC,
},
},
expectedErr: domain.ValidationError{
Model: "ListVillagesParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.VillageSortIDASC.String(), domain.VillageSortIDDESC.String()},
},
},
},
}
for _, tt := range tests {

View File

@ -527,6 +527,44 @@ func TestListPlayers(t *testing.T) {
}, body)
},
},
{
name: "ERR: sort conflict",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("sort", "odScoreAtt:ASC")
q.Add("sort", "odScoreAtt:DESC")
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: len(name) > 100",
reqModifier: func(t *testing.T, req *http.Request) {

View File

@ -527,6 +527,44 @@ func TestListTribes(t *testing.T) {
}, body)
},
},
{
name: "ERR: sort conflict",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("sort", "odScoreAtt:ASC")
q.Add("sort", "odScoreAtt:DESC")
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: len(tag) > 100",
reqModifier: func(t *testing.T, req *http.Request) {