From 48b87eea81c3649f9182b504b45447868a8eed2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Sat, 2 Mar 2024 08:49:42 +0000 Subject: [PATCH] feat: sort validation improvements (#14) Reviewed-on: https://gitea.dwysokinski.me/twhelp/corev3/pulls/14 --- .../adapter/repository_bun_ennoblement.go | 2 +- internal/adapter/repository_bun_player.go | 4 +- .../adapter/repository_bun_player_snapshot.go | 2 +- internal/adapter/repository_bun_server.go | 4 +- internal/adapter/repository_bun_tribe.go | 4 +- .../adapter/repository_bun_tribe_change.go | 2 +- .../adapter/repository_bun_tribe_snapshot.go | 2 +- internal/adapter/repository_bun_version.go | 4 +- internal/adapter/repository_bun_village.go | 2 +- internal/domain/ennoblement.go | 31 ++- internal/domain/ennoblement_test.go | 81 ++++++++ internal/domain/player.go | 99 +++++++--- internal/domain/player_snapshot.go | 32 ++- internal/domain/player_snapshot_test.go | 81 ++++++++ internal/domain/player_test.go | 125 ++++++++++++ internal/domain/server.go | 25 ++- internal/domain/server_test.go | 81 ++++++++ internal/domain/tribe.go | 121 ++++++++---- internal/domain/tribe_change.go | 30 ++- internal/domain/tribe_change_test.go | 81 ++++++++ internal/domain/tribe_snapshot.go | 32 ++- internal/domain/tribe_snapshot_test.go | 81 ++++++++ internal/domain/tribe_test.go | 186 ++++++++++++++++++ internal/domain/validation.go | 46 +++++ internal/domain/version.go | 22 ++- internal/domain/version_test.go | 37 ++++ internal/domain/village.go | 26 ++- internal/domain/village_test.go | 74 +++++++ internal/port/handler_http_api_player_test.go | 38 ++++ internal/port/handler_http_api_tribe_test.go | 38 ++++ 30 files changed, 1311 insertions(+), 82 deletions(-) diff --git a/internal/adapter/repository_bun_ennoblement.go b/internal/adapter/repository_bun_ennoblement.go index f937384..9ff926a 100644 --- a/internal/adapter/repository_bun_ennoblement.go +++ b/internal/adapter/repository_bun_ennoblement.go @@ -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)) } } diff --git a/internal/adapter/repository_bun_player.go b/internal/adapter/repository_bun_player.go index 0bd9702..7677c3c 100644 --- a/internal/adapter/repository_bun_player.go +++ b/internal/adapter/repository_bun_player.go @@ -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) diff --git a/internal/adapter/repository_bun_player_snapshot.go b/internal/adapter/repository_bun_player_snapshot.go index 418327f..90fb131 100644 --- a/internal/adapter/repository_bun_player_snapshot.go +++ b/internal/adapter/repository_bun_player_snapshot.go @@ -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)) } } diff --git a/internal/adapter/repository_bun_server.go b/internal/adapter/repository_bun_server.go index 0f73149..a489b55 100644 --- a/internal/adapter/repository_bun_server.go +++ b/internal/adapter/repository_bun_server.go @@ -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) diff --git a/internal/adapter/repository_bun_tribe.go b/internal/adapter/repository_bun_tribe.go index ab5f403..a6a4e4a 100644 --- a/internal/adapter/repository_bun_tribe.go +++ b/internal/adapter/repository_bun_tribe.go @@ -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) diff --git a/internal/adapter/repository_bun_tribe_change.go b/internal/adapter/repository_bun_tribe_change.go index 703d297..5db2eef 100644 --- a/internal/adapter/repository_bun_tribe_change.go +++ b/internal/adapter/repository_bun_tribe_change.go @@ -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)) } } diff --git a/internal/adapter/repository_bun_tribe_snapshot.go b/internal/adapter/repository_bun_tribe_snapshot.go index 5e646e4..35cd18f 100644 --- a/internal/adapter/repository_bun_tribe_snapshot.go +++ b/internal/adapter/repository_bun_tribe_snapshot.go @@ -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)) } } diff --git a/internal/adapter/repository_bun_version.go b/internal/adapter/repository_bun_version.go index 4451987..4f4de18 100644 --- a/internal/adapter/repository_bun_version.go +++ b/internal/adapter/repository_bun_version.go @@ -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) diff --git a/internal/adapter/repository_bun_village.go b/internal/adapter/repository_bun_village.go index 145d65d..83e692f 100644 --- a/internal/adapter/repository_bun_village.go +++ b/internal/adapter/repository_bun_village.go @@ -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)) } } diff --git a/internal/domain/ennoblement.go b/internal/domain/ennoblement.go index 2f5b85b..e039309 100644 --- a/internal/domain/ennoblement.go +++ b/internal/domain/ennoblement.go @@ -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", diff --git a/internal/domain/ennoblement_test.go b/internal/domain/ennoblement_test.go index cfe8b3c..c79bc68 100644 --- a/internal/domain/ennoblement_test.go +++ b/internal/domain/ennoblement_test.go @@ -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 { diff --git a/internal/domain/player.go b/internal/domain/player.go index 77f223b..db00f1d 100644 --- a/internal/domain/player.go +++ b/internal/domain/player.go @@ -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 { diff --git a/internal/domain/player_snapshot.go b/internal/domain/player_snapshot.go index 3a1319b..14a7df6 100644 --- a/internal/domain/player_snapshot.go +++ b/internal/domain/player_snapshot.go @@ -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", diff --git a/internal/domain/player_snapshot_test.go b/internal/domain/player_snapshot_test.go index e491d61..df0c8c2 100644 --- a/internal/domain/player_snapshot_test.go +++ b/internal/domain/player_snapshot_test.go @@ -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 { diff --git a/internal/domain/player_test.go b/internal/domain/player_test.go index b9b789f..89602c9 100644 --- a/internal/domain/player_test.go +++ b/internal/domain/player_test.go @@ -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 { diff --git a/internal/domain/server.go b/internal/domain/server.go index 457b462..28d714f 100644 --- a/internal/domain/server.go +++ b/internal/domain/server.go @@ -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", diff --git a/internal/domain/server_test.go b/internal/domain/server_test.go index 86a722c..83040ca 100644 --- a/internal/domain/server_test.go +++ b/internal/domain/server_test.go @@ -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 { diff --git a/internal/domain/tribe.go b/internal/domain/tribe.go index 5ee448d..d08e67d 100644 --- a/internal/domain/tribe.go +++ b/internal/domain/tribe.go @@ -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 { diff --git a/internal/domain/tribe_change.go b/internal/domain/tribe_change.go index a26a9b7..4cace66 100644 --- a/internal/domain/tribe_change.go +++ b/internal/domain/tribe_change.go @@ -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", diff --git a/internal/domain/tribe_change_test.go b/internal/domain/tribe_change_test.go index 7955a1d..5796df9 100644 --- a/internal/domain/tribe_change_test.go +++ b/internal/domain/tribe_change_test.go @@ -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 { diff --git a/internal/domain/tribe_snapshot.go b/internal/domain/tribe_snapshot.go index b97ea42..e1a1563 100644 --- a/internal/domain/tribe_snapshot.go +++ b/internal/domain/tribe_snapshot.go @@ -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", diff --git a/internal/domain/tribe_snapshot_test.go b/internal/domain/tribe_snapshot_test.go index 8aa6003..f3dc06c 100644 --- a/internal/domain/tribe_snapshot_test.go +++ b/internal/domain/tribe_snapshot_test.go @@ -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 { diff --git a/internal/domain/tribe_test.go b/internal/domain/tribe_test.go index 73d6d37..67a2ddc 100644 --- a/internal/domain/tribe_test.go +++ b/internal/domain/tribe_test.go @@ -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 { diff --git a/internal/domain/validation.go b/internal/domain/validation.go index 79a61ea..88a386f 100644 --- a/internal/domain/validation.go +++ b/internal/domain/validation.go @@ -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 +} diff --git a/internal/domain/version.go b/internal/domain/version.go index 697d513..4cd537b 100644 --- a/internal/domain/version.go +++ b/internal/domain/version.go @@ -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", diff --git a/internal/domain/version_test.go b/internal/domain/version_test.go index 89fe26b..fe088dc 100644 --- a/internal/domain/version_test.go +++ b/internal/domain/version_test.go @@ -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() diff --git a/internal/domain/village.go b/internal/domain/village.go index 4a66200..2020220 100644 --- a/internal/domain/village.go +++ b/internal/domain/village.go @@ -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", diff --git a/internal/domain/village_test.go b/internal/domain/village_test.go index 4b8ea5f..108fbac 100644 --- a/internal/domain/village_test.go +++ b/internal/domain/village_test.go @@ -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 { diff --git a/internal/port/handler_http_api_player_test.go b/internal/port/handler_http_api_player_test.go index 4195168..7698448 100644 --- a/internal/port/handler_http_api_player_test.go +++ b/internal/port/handler_http_api_player_test.go @@ -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) { diff --git a/internal/port/handler_http_api_tribe_test.go b/internal/port/handler_http_api_tribe_test.go index 633db58..e63da6e 100644 --- a/internal/port/handler_http_api_tribe_test.go +++ b/internal/port/handler_http_api_tribe_test.go @@ -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) {