feat: add a new API endpoint - /api/v1/versions/{versionCode}/players (#37)
Reviewed-on: twhelp/corev3#37
This commit is contained in:
parent
99b187d930
commit
c1dfb0fa9b
|
@ -11,13 +11,21 @@ info:
|
|||
name: MIT
|
||||
tags:
|
||||
- name: versions
|
||||
description: Version related endpoints
|
||||
- name: servers
|
||||
description: Server related endpoints
|
||||
- name: tribes
|
||||
description: Tribe related endpoints
|
||||
- name: players
|
||||
description: Player related endpoints
|
||||
- name: villages
|
||||
description: Village related endpoints
|
||||
- name: ennoblements
|
||||
description: Ennoblement (conquer) related endpoints
|
||||
- name: tribe-changes
|
||||
description: Tribe change related endpoints
|
||||
- name: snapshots
|
||||
description: Snapshot (historical records) related endpoints
|
||||
servers:
|
||||
- url: "{scheme}://{hostname}/api"
|
||||
variables:
|
||||
|
@ -56,13 +64,32 @@ paths:
|
|||
$ref: "#/components/responses/GetVersionResponse"
|
||||
default:
|
||||
$ref: "#/components/responses/ErrorResponse"
|
||||
/v2/versions/{versionCode}/players:
|
||||
get:
|
||||
operationId: listVersionPlayers
|
||||
tags:
|
||||
- versions
|
||||
- players
|
||||
description: List players associated with the given version
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/VersionCodePathParam"
|
||||
- $ref: "#/components/parameters/CursorQueryParam"
|
||||
- $ref: "#/components/parameters/LimitQueryParam"
|
||||
- $ref: "#/components/parameters/PlayerDeletedQueryParam"
|
||||
- $ref: "#/components/parameters/PlayerSortQueryParam"
|
||||
- $ref: "#/components/parameters/PlayerNameQueryParam"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/ListPlayersWithServersResponse"
|
||||
default:
|
||||
$ref: "#/components/responses/ErrorResponse"
|
||||
/v2/versions/{versionCode}/servers:
|
||||
get:
|
||||
operationId: listServers
|
||||
tags:
|
||||
- versions
|
||||
- servers
|
||||
description: List servers
|
||||
description: List servers associated with the given version
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/VersionCodePathParam"
|
||||
- $ref: "#/components/parameters/CursorQueryParam"
|
||||
|
@ -140,7 +167,7 @@ paths:
|
|||
- versions
|
||||
- servers
|
||||
- tribes
|
||||
description: List tribes
|
||||
description: List tribes associated with the given server
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/VersionCodePathParam"
|
||||
- $ref: "#/components/parameters/ServerKeyPathParam"
|
||||
|
@ -173,12 +200,12 @@ paths:
|
|||
$ref: "#/components/responses/ErrorResponse"
|
||||
/v2/versions/{versionCode}/servers/{serverKey}/players:
|
||||
get:
|
||||
operationId: listPlayers
|
||||
operationId: listServerPlayers
|
||||
tags:
|
||||
- versions
|
||||
- servers
|
||||
- players
|
||||
description: List players
|
||||
description: List players associated with the given server
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/VersionCodePathParam"
|
||||
- $ref: "#/components/parameters/ServerKeyPathParam"
|
||||
|
@ -237,7 +264,7 @@ paths:
|
|||
- versions
|
||||
- servers
|
||||
- villages
|
||||
description: List villages
|
||||
description: List villages associated with the given server
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/VersionCodePathParam"
|
||||
- $ref: "#/components/parameters/ServerKeyPathParam"
|
||||
|
@ -313,7 +340,7 @@ paths:
|
|||
- versions
|
||||
- servers
|
||||
- ennoblements
|
||||
description: List ennoblements
|
||||
description: List ennoblements associated with the given server
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/VersionCodePathParam"
|
||||
- $ref: "#/components/parameters/ServerKeyPathParam"
|
||||
|
@ -597,6 +624,21 @@ components:
|
|||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
ServerMeta:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- open
|
||||
- url
|
||||
properties:
|
||||
key:
|
||||
$ref: "#/components/schemas/ServerKey"
|
||||
open:
|
||||
type: boolean
|
||||
url:
|
||||
type: string
|
||||
format: uri
|
||||
example: https://en138.tribalwars.net
|
||||
ServerConfig:
|
||||
type: object
|
||||
required:
|
||||
|
@ -1371,6 +1413,15 @@ components:
|
|||
deletedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
PlayerWithServer:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/Player"
|
||||
- type: object
|
||||
required:
|
||||
- server
|
||||
properties:
|
||||
server:
|
||||
$ref: "#/components/schemas/ServerMeta"
|
||||
PlayerMeta:
|
||||
type: object
|
||||
required:
|
||||
|
@ -1936,6 +1987,21 @@ components:
|
|||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/Tribe"
|
||||
ListPlayersWithServersResponse:
|
||||
description: ""
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/PaginationResponse"
|
||||
- type: object
|
||||
required:
|
||||
- data
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/PlayerWithServer"
|
||||
ListPlayersResponse:
|
||||
description: ""
|
||||
content:
|
||||
|
|
|
@ -118,10 +118,7 @@ func (repo *PlayerBunRepository) ListWithRelations(
|
|||
|
||||
if err := repo.db.NewSelect().
|
||||
Model(&players).
|
||||
Apply(listPlayersParamsApplier{params: params}.apply).
|
||||
Relation("Tribe", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
return q.Column(bunmodel.TribeMetaColumns...)
|
||||
}).
|
||||
Apply(listPlayersParamsApplier{params: params, includeRelations: true}.apply).
|
||||
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return domain.ListPlayersWithRelationsResult{}, fmt.Errorf("couldn't select players from the db: %w", err)
|
||||
}
|
||||
|
@ -155,7 +152,8 @@ func (repo *PlayerBunRepository) Delete(ctx context.Context, serverKey string, i
|
|||
}
|
||||
|
||||
type listPlayersParamsApplier struct {
|
||||
params domain.ListPlayersParams
|
||||
params domain.ListPlayersParams
|
||||
includeRelations bool
|
||||
}
|
||||
|
||||
func (a listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
|
@ -192,7 +190,38 @@ func (a listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
|
|||
q.OrderExpr("? ?", column, dir.Bun())
|
||||
}
|
||||
|
||||
return q.Limit(a.params.Limit() + 1).Apply(a.applyCursor)
|
||||
return q.Limit(a.params.Limit() + 1).
|
||||
Apply(a.applyCursor).
|
||||
Apply(a.applyRelations)
|
||||
}
|
||||
|
||||
func (a listPlayersParamsApplier) applyRelations(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
if a.includeRelations {
|
||||
q = q.Relation("Tribe", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
return q.Column(bunmodel.TribeMetaColumns...)
|
||||
})
|
||||
}
|
||||
|
||||
if versionCodes := a.params.VersionCodes(); len(versionCodes) > 0 || a.includeRelations {
|
||||
q = q.Join("INNER JOIN servers as server").JoinOn("server.key = player.server_key")
|
||||
|
||||
// according to EXPLAIN ANALYZE (https://www.postgresql.org/docs/current/sql-explain.html)
|
||||
// this way of filtering servers is much more efficient compared to
|
||||
// using the WHERE clause
|
||||
if len(versionCodes) > 0 {
|
||||
q = q.JoinOn("server.version_code IN (?)", bun.In(versionCodes))
|
||||
}
|
||||
|
||||
if a.includeRelations {
|
||||
q = q.ColumnExpr("?TableColumns")
|
||||
for _, col := range bunmodel.ServerMetaColumns {
|
||||
safeCol := bun.Safe(col)
|
||||
q = q.ColumnExpr("server.? as server__?", safeCol, safeCol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
|
|
|
@ -326,6 +326,51 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
|||
}))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: versionCodes",
|
||||
params: func(t *testing.T) domain.ListPlayersParams {
|
||||
t.Helper()
|
||||
|
||||
playerParams := domain.NewListPlayersParams()
|
||||
|
||||
resPlayers, err := repos.player.List(ctx, playerParams)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, resPlayers.Players())
|
||||
randPlayer := resPlayers.Players()[0]
|
||||
|
||||
serverParams := domain.NewListServersParams()
|
||||
require.NoError(t, serverParams.SetKeys([]string{randPlayer.ServerKey()}))
|
||||
|
||||
resServers, err := repos.server.List(ctx, serverParams)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, resServers.Servers())
|
||||
|
||||
require.NoError(t, playerParams.SetVersionCodes([]string{resServers.Servers()[0].VersionCode()}))
|
||||
|
||||
return playerParams
|
||||
},
|
||||
assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) {
|
||||
t.Helper()
|
||||
|
||||
versionCodes := params.VersionCodes()
|
||||
|
||||
serverParams := domain.NewListServersParams()
|
||||
require.NoError(t, serverParams.SetVersionCodes(versionCodes))
|
||||
|
||||
resServers, err := repos.server.List(ctx, serverParams)
|
||||
require.NoError(t, err)
|
||||
servers := resServers.Servers()
|
||||
require.NotEmpty(t, servers)
|
||||
|
||||
players := res.Players()
|
||||
assert.NotEmpty(t, res.Players())
|
||||
for _, p := range players {
|
||||
assert.True(t, slices.ContainsFunc(servers, func(server domain.Server) bool {
|
||||
return server.Key() == p.ServerKey() && slices.Contains(versionCodes, server.VersionCode())
|
||||
}))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: ids serverKeys",
|
||||
params: func(t *testing.T) domain.ListPlayersParams {
|
||||
|
@ -611,6 +656,7 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
|||
require.Len(t, resWithRelations.Players(), len(res.Players()))
|
||||
for i, p := range resWithRelations.Players() {
|
||||
assert.Equal(t, res.Players()[i], p.Player())
|
||||
assert.Equal(t, p.Player().ServerKey(), p.Server().Key())
|
||||
assert.Equal(t, p.Player().TribeID(), p.Tribe().V.ID())
|
||||
assert.Equal(t, p.Player().TribeID() != 0, p.Tribe().Valid)
|
||||
}
|
||||
|
|
|
@ -98,10 +98,9 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
|
|||
repos := newRepos(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
params func(t *testing.T) domain.ListVillagesParams
|
||||
assertResult func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult)
|
||||
// assertResultWithRelations is optional
|
||||
name string
|
||||
params func(t *testing.T) domain.ListVillagesParams
|
||||
assertResult func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult)
|
||||
assertResultWithRelations func(
|
||||
t *testing.T,
|
||||
params domain.ListVillagesParams,
|
||||
|
|
|
@ -94,7 +94,12 @@ func (p Player) ToDomainWithRelations() (domain.PlayerWithRelations, error) {
|
|||
}
|
||||
}
|
||||
|
||||
return converted.WithRelations(tribe), nil
|
||||
server, err := p.Server.ToMeta()
|
||||
if err != nil {
|
||||
return domain.PlayerWithRelations{}, err
|
||||
}
|
||||
|
||||
return converted.WithRelations(server, tribe), nil
|
||||
}
|
||||
|
||||
func (p Player) ToMeta() (domain.PlayerMeta, error) {
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
var ServerMetaColumns = []string{"key", "url", "open"}
|
||||
|
||||
type Server struct {
|
||||
bun.BaseModel `bun:"table:servers,alias:server"`
|
||||
Key string `bun:"key,nullzero,pk"`
|
||||
|
@ -83,6 +85,14 @@ func (s Server) ToDomain() (domain.Server, error) {
|
|||
return converted, nil
|
||||
}
|
||||
|
||||
func (s Server) ToMeta() (domain.ServerMeta, error) {
|
||||
converted, err := domain.UnmarshalServerMetaFromDatabase(s.Key, s.URL, s.Open)
|
||||
if err != nil {
|
||||
return domain.ServerMeta{}, fmt.Errorf("couldn't construct domain.Server (key=%s): %w", s.Key, err)
|
||||
}
|
||||
return converted, nil
|
||||
}
|
||||
|
||||
type Servers []Server
|
||||
|
||||
func (ss Servers) ToDomain() (domain.Servers, error) {
|
||||
|
|
|
@ -122,6 +122,7 @@ func NewPlayer(tb TestingTB, opts ...func(cfg *PlayerConfig)) domain.Player {
|
|||
}
|
||||
|
||||
type PlayerWithRelationsConfig struct {
|
||||
ServerOptions []func(cfg *ServerConfig)
|
||||
PlayerOptions []func(cfg *PlayerConfig)
|
||||
TribeOptions []func(cfg *TribeConfig)
|
||||
}
|
||||
|
@ -137,6 +138,12 @@ func NewPlayerWithRelations(tb TestingTB, opts ...func(cfg *PlayerWithRelationsC
|
|||
|
||||
p := NewPlayer(tb, cfg.PlayerOptions...)
|
||||
|
||||
cfg.ServerOptions = append([]func(cfg *ServerConfig){
|
||||
func(cfg *ServerConfig) {
|
||||
cfg.Key = p.ServerKey()
|
||||
},
|
||||
}, cfg.ServerOptions...)
|
||||
|
||||
if p.TribeID() > 0 {
|
||||
cfg.TribeOptions = append([]func(cfg *TribeConfig){
|
||||
func(cfg *TribeConfig) {
|
||||
|
@ -150,8 +157,11 @@ func NewPlayerWithRelations(tb TestingTB, opts ...func(cfg *PlayerWithRelationsC
|
|||
tribe = NewTribe(tb, cfg.TribeOptions...).Meta()
|
||||
}
|
||||
|
||||
return p.WithRelations(domain.NullTribeMeta{
|
||||
V: tribe,
|
||||
Valid: !tribe.IsZero(),
|
||||
})
|
||||
return p.WithRelations(
|
||||
NewServer(tb, cfg.ServerOptions...).Meta(),
|
||||
domain.NullTribeMeta{
|
||||
V: tribe,
|
||||
Valid: !tribe.IsZero(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -183,9 +183,10 @@ func (p Player) DeletedAt() time.Time {
|
|||
return p.deletedAt
|
||||
}
|
||||
|
||||
func (p Player) WithRelations(tribe NullTribeMeta) PlayerWithRelations {
|
||||
func (p Player) WithRelations(server ServerMeta, tribe NullTribeMeta) PlayerWithRelations {
|
||||
return PlayerWithRelations{
|
||||
player: p,
|
||||
server: server,
|
||||
tribe: tribe,
|
||||
}
|
||||
}
|
||||
|
@ -283,6 +284,7 @@ func (ps Players) Delete(serverKey string, active BasePlayers) ([]int, []CreateT
|
|||
|
||||
type PlayerWithRelations struct {
|
||||
player Player
|
||||
server ServerMeta
|
||||
tribe NullTribeMeta
|
||||
}
|
||||
|
||||
|
@ -290,6 +292,10 @@ func (p PlayerWithRelations) Player() Player {
|
|||
return p.player
|
||||
}
|
||||
|
||||
func (p PlayerWithRelations) Server() ServerMeta {
|
||||
return p.server
|
||||
}
|
||||
|
||||
func (p PlayerWithRelations) Tribe() NullTribeMeta {
|
||||
return p.tribe
|
||||
}
|
||||
|
@ -804,14 +810,15 @@ func (pc PlayerCursor) Encode() string {
|
|||
}
|
||||
|
||||
type ListPlayersParams struct {
|
||||
ids []int
|
||||
serverKeys []string
|
||||
names []string
|
||||
tribeIDs []int
|
||||
deleted NullBool
|
||||
sort []PlayerSort
|
||||
cursor PlayerCursor
|
||||
limit int
|
||||
ids []int
|
||||
serverKeys []string
|
||||
versionCodes []string
|
||||
names []string
|
||||
tribeIDs []int
|
||||
deleted NullBool
|
||||
sort []PlayerSort
|
||||
cursor PlayerCursor
|
||||
limit int
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -871,6 +878,27 @@ func (params *ListPlayersParams) SetServerKeys(serverKeys []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (params *ListPlayersParams) VersionCodes() []string {
|
||||
return params.versionCodes
|
||||
}
|
||||
|
||||
func (params *ListPlayersParams) SetVersionCodes(versionCodes []string) error {
|
||||
for i, vc := range versionCodes {
|
||||
if err := validateVersionCode(vc); err != nil {
|
||||
return SliceElementValidationError{
|
||||
Model: listPlayersParamsModelName,
|
||||
Field: "versionCodes",
|
||||
Index: i,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
params.versionCodes = versionCodes
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (params *ListPlayersParams) Names() []string {
|
||||
return params.names
|
||||
}
|
||||
|
@ -958,6 +986,22 @@ func (params *ListPlayersParams) SetSort(sort []PlayerSort) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (params *ListPlayersParams) PrependSort(sort []PlayerSort) error {
|
||||
if len(sort) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := validateSliceLen(sort, 0, max(playerSortMaxLength-len(params.sort), 0)); err != nil {
|
||||
return ValidationError{
|
||||
Model: listPlayersParamsModelName,
|
||||
Field: "sort",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return params.SetSort(append(sort, params.sort...))
|
||||
}
|
||||
|
||||
func (params *ListPlayersParams) PrependSortString(sort []string, allowed []PlayerSort, maxLength int) error {
|
||||
if len(sort) == 0 {
|
||||
return nil
|
||||
|
|
|
@ -651,6 +651,60 @@ func TestListPlayersParams_SetServerKeys(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestListPlayersParams_SetVersionCodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type args struct {
|
||||
versionCodes []string
|
||||
}
|
||||
|
||||
type test struct {
|
||||
name string
|
||||
args args
|
||||
expectedErr error
|
||||
}
|
||||
|
||||
tests := []test{
|
||||
{
|
||||
name: "OK",
|
||||
args: args{
|
||||
versionCodes: []string{
|
||||
domaintest.RandVersionCode(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, versionCodeTest := range newVersionCodeValidationTests() {
|
||||
tests = append(tests, test{
|
||||
name: versionCodeTest.name,
|
||||
args: args{
|
||||
versionCodes: []string{versionCodeTest.code},
|
||||
},
|
||||
expectedErr: domain.SliceElementValidationError{
|
||||
Model: "ListPlayersParams",
|
||||
Field: "versionCodes",
|
||||
Index: 0,
|
||||
Err: versionCodeTest.expectedErr,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params := domain.NewListPlayersParams()
|
||||
|
||||
require.ErrorIs(t, params.SetVersionCodes(tt.args.versionCodes), tt.expectedErr)
|
||||
if tt.expectedErr != nil {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.args.versionCodes, params.VersionCodes())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPlayersParams_SetNames(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
@ -896,6 +950,157 @@ func TestListPlayersParams_SetSort(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestListPlayersParams_PrependSort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
defaultNewParams := func(t *testing.T) domain.ListPlayersParams {
|
||||
t.Helper()
|
||||
return domain.ListPlayersParams{}
|
||||
}
|
||||
|
||||
type args struct {
|
||||
sort []domain.PlayerSort
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
newParams func(t *testing.T) domain.ListPlayersParams
|
||||
args args
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "OK",
|
||||
args: args{
|
||||
sort: []domain.PlayerSort{
|
||||
domain.PlayerSortIDASC,
|
||||
domain.PlayerSortODScoreAttASC,
|
||||
domain.PlayerSortODScoreDefASC,
|
||||
domain.PlayerSortODScoreTotalASC,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: custom params",
|
||||
newParams: func(t *testing.T) domain.ListPlayersParams {
|
||||
t.Helper()
|
||||
params := domain.NewListPlayersParams()
|
||||
require.NoError(t, params.SetSort([]domain.PlayerSort{
|
||||
domain.PlayerSortIDASC,
|
||||
domain.PlayerSortServerKeyASC,
|
||||
}))
|
||||
return params
|
||||
},
|
||||
args: args{
|
||||
sort: []domain.PlayerSort{
|
||||
domain.PlayerSortODScoreDefASC,
|
||||
domain.PlayerSortODScoreTotalASC,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: empty slice",
|
||||
newParams: func(t *testing.T) domain.ListPlayersParams {
|
||||
t.Helper()
|
||||
params := domain.NewListPlayersParams()
|
||||
require.NoError(t, params.SetSort([]domain.PlayerSort{
|
||||
domain.PlayerSortIDASC,
|
||||
domain.PlayerSortServerKeyASC,
|
||||
}))
|
||||
return params
|
||||
},
|
||||
args: args{
|
||||
sort: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: custom params + len(sort) > sortMaxLength - len(sort)",
|
||||
newParams: func(t *testing.T) domain.ListPlayersParams {
|
||||
t.Helper()
|
||||
params := domain.NewListPlayersParams()
|
||||
require.NoError(t, params.SetSort([]domain.PlayerSort{
|
||||
domain.PlayerSortIDASC,
|
||||
domain.PlayerSortServerKeyASC,
|
||||
}))
|
||||
return params
|
||||
},
|
||||
args: args{
|
||||
sort: []domain.PlayerSort{
|
||||
domain.PlayerSortODScoreAttASC,
|
||||
domain.PlayerSortODScoreDefASC,
|
||||
domain.PlayerSortODScoreTotalASC,
|
||||
},
|
||||
},
|
||||
expectedErr: domain.ValidationError{
|
||||
Model: "ListPlayersParams",
|
||||
Field: "sort",
|
||||
Err: domain.LenOutOfRangeError{
|
||||
Min: 0,
|
||||
Max: 2,
|
||||
Current: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: len(sort) > 4",
|
||||
newParams: defaultNewParams,
|
||||
args: args{
|
||||
sort: []domain.PlayerSort{
|
||||
domain.PlayerSortODScoreAttASC,
|
||||
domain.PlayerSortODScoreDefASC,
|
||||
domain.PlayerSortODScoreTotalASC,
|
||||
domain.PlayerSortODScoreTotalASC,
|
||||
domain.PlayerSortODScoreTotalASC,
|
||||
},
|
||||
},
|
||||
expectedErr: domain.ValidationError{
|
||||
Model: "ListPlayersParams",
|
||||
Field: "sort",
|
||||
Err: domain.LenOutOfRangeError{
|
||||
Min: 0,
|
||||
Max: 4,
|
||||
Current: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: conflict",
|
||||
args: args{
|
||||
sort: []domain.PlayerSort{
|
||||
domain.PlayerSortODScoreTotalASC,
|
||||
domain.PlayerSortODScoreTotalASC,
|
||||
},
|
||||
},
|
||||
expectedErr: domain.ValidationError{
|
||||
Model: "ListPlayersParams",
|
||||
Field: "sort",
|
||||
Err: domain.SortConflictError{
|
||||
Sort: [2]string{domain.PlayerSortODScoreTotalASC.String(), domain.PlayerSortODScoreTotalASC.String()},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
newParams := defaultNewParams
|
||||
if tt.newParams != nil {
|
||||
newParams = tt.newParams
|
||||
}
|
||||
params := newParams(t)
|
||||
|
||||
expectedSort := params.Sort()
|
||||
|
||||
require.ErrorIs(t, params.PrependSort(tt.args.sort), tt.expectedErr)
|
||||
if tt.expectedErr != nil {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, append(tt.args.sort, expectedSort...), params.Sort())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPlayersParams_PrependSortString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
|
@ -199,6 +199,14 @@ func (s Server) ToCursor() (ServerCursor, error) {
|
|||
return NewServerCursor(s.key, s.open)
|
||||
}
|
||||
|
||||
func (s Server) Meta() ServerMeta {
|
||||
return ServerMeta{
|
||||
key: s.key,
|
||||
url: s.url,
|
||||
open: s.open,
|
||||
}
|
||||
}
|
||||
|
||||
func (s Server) Base() BaseServer {
|
||||
return BaseServer{
|
||||
key: s.key,
|
||||
|
@ -280,6 +288,51 @@ func (ss Servers) CleanUpData() ([]CleanUpDataCmdPayload, error) {
|
|||
return res, nil
|
||||
}
|
||||
|
||||
type ServerMeta struct {
|
||||
key string
|
||||
url *url.URL
|
||||
open bool
|
||||
}
|
||||
|
||||
const serverMetaModelName = "ServerMeta"
|
||||
|
||||
// UnmarshalServerMetaFromDatabase unmarshals ServerMeta from the database.
|
||||
//
|
||||
// It should be used only for unmarshalling from the database!
|
||||
// You can't use UnmarshalServerMetaFromDatabase as constructor - It may put domain into the invalid state!
|
||||
func UnmarshalServerMetaFromDatabase(key string, rawURL string, open bool) (ServerMeta, error) {
|
||||
if key == "" {
|
||||
return ServerMeta{}, ValidationError{
|
||||
Model: serverModelName,
|
||||
Field: "key",
|
||||
Err: ErrRequired,
|
||||
}
|
||||
}
|
||||
|
||||
u, err := parseURL(rawURL)
|
||||
if err != nil {
|
||||
return ServerMeta{}, ValidationError{
|
||||
Model: serverMetaModelName,
|
||||
Field: "url",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return ServerMeta{key: key, url: u, open: open}, nil
|
||||
}
|
||||
|
||||
func (s ServerMeta) Key() string {
|
||||
return s.key
|
||||
}
|
||||
|
||||
func (s ServerMeta) URL() *url.URL {
|
||||
return s.url
|
||||
}
|
||||
|
||||
func (s ServerMeta) Open() bool {
|
||||
return s.open
|
||||
}
|
||||
|
||||
type CreateServerParams struct {
|
||||
base BaseServer
|
||||
versionCode string
|
||||
|
|
|
@ -29,12 +29,87 @@ var apiPlayerSortAllowedValues = []domain.PlayerSort{
|
|||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (h *apiHTTPHandler) ListPlayers(
|
||||
func (h *apiHTTPHandler) ListVersionPlayers(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
versionCode apimodel.VersionCodePathParam,
|
||||
params apimodel.ListVersionPlayersParams,
|
||||
) {
|
||||
domainParams := domain.NewListPlayersParams()
|
||||
|
||||
if err := domainParams.SetSort([]domain.PlayerSort{domain.PlayerSortIDASC}); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if params.Sort != nil {
|
||||
if err := domainParams.PrependSortString(
|
||||
*params.Sort,
|
||||
apiPlayerSortAllowedValues,
|
||||
apiPlayerSortMaxLength,
|
||||
); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := domainParams.PrependSort([]domain.PlayerSort{domain.PlayerSortServerKeyASC}); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := domainParams.SetVersionCodes([]string{versionCode}); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if params.Name != nil {
|
||||
if err := domainParams.SetNames(*params.Name); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if params.Deleted != nil {
|
||||
if err := domainParams.SetDeleted(domain.NullBool{
|
||||
V: *params.Deleted,
|
||||
Valid: true,
|
||||
}); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if params.Limit != nil {
|
||||
if err := domainParams.SetLimit(*params.Limit); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if params.Cursor != nil {
|
||||
if err := domainParams.SetEncodedCursor(*params.Cursor); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
res, err := h.playerSvc.ListWithRelations(r.Context(), domainParams)
|
||||
if err != nil {
|
||||
h.errorRenderer.render(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
renderJSON(w, r, http.StatusOK, apimodel.NewListPlayersWithServersResponse(res))
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (h *apiHTTPHandler) ListServerPlayers(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
_ apimodel.VersionCodePathParam,
|
||||
serverKey apimodel.ServerKeyPathParam,
|
||||
params apimodel.ListPlayersParams,
|
||||
params apimodel.ListServerPlayersParams,
|
||||
) {
|
||||
domainParams := domain.NewListPlayersParams()
|
||||
|
||||
|
|
|
@ -19,9 +19,754 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const endpointListPlayers = "/v2/versions/%s/servers/%s/players"
|
||||
const endpointListVersionPlayers = "/v2/versions/%s/players"
|
||||
|
||||
func TestListPlayers(t *testing.T) {
|
||||
func TestListVersionPlayers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := newAPIHTTPHandler(t)
|
||||
players := getAllPlayers(t, handler)
|
||||
var version apimodel.Version
|
||||
|
||||
for _, p := range players {
|
||||
if p.DeletedAt != nil {
|
||||
version = p.Server.Version
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.NotZero(t, version)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
reqModifier func(t *testing.T, req *http.Request)
|
||||
assertResp func(t *testing.T, req *http.Request, resp *http.Response)
|
||||
}{
|
||||
{
|
||||
name: "OK: without params",
|
||||
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
|
||||
assert.Zero(t, body.Cursor.Next)
|
||||
assert.NotZero(t, body.Cursor.Self)
|
||||
assert.NotZero(t, body.Data)
|
||||
assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.PlayerWithServer) int {
|
||||
return cmp.Or(
|
||||
cmp.Compare(a.Server.Key, b.Server.Key),
|
||||
cmp.Compare(a.Id, b.Id),
|
||||
)
|
||||
}))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: limit=1",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Set("limit", "1")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
},
|
||||
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
|
||||
assert.NotZero(t, body.Cursor.Next)
|
||||
assert.NotZero(t, body.Cursor.Self)
|
||||
assert.NotZero(t, body.Data)
|
||||
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, body.Data, limit)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: limit=1 cursor",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Set("limit", "1")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
resp := doCustomRequest(handler, req.Clone(req.Context()))
|
||||
defer resp.Body.Close()
|
||||
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
|
||||
require.NotEmpty(t, body.Cursor.Next)
|
||||
|
||||
q.Set("cursor", body.Cursor.Next)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
},
|
||||
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
|
||||
assert.Equal(t, req.URL.Query().Get("cursor"), body.Cursor.Self)
|
||||
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, body.Data, limit)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: sort=[deletedAt:DESC,points:ASC]",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Add("sort", "deletedAt:DESC")
|
||||
q.Add("sort", "points:ASC")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
},
|
||||
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
|
||||
assert.Zero(t, body.Cursor.Next)
|
||||
assert.NotZero(t, body.Cursor.Self)
|
||||
assert.NotZero(t, body.Data)
|
||||
assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.PlayerWithServer) int {
|
||||
var aDeletedAt time.Time
|
||||
if a.DeletedAt != nil {
|
||||
aDeletedAt = *a.DeletedAt
|
||||
}
|
||||
|
||||
var bDeletedAt time.Time
|
||||
if b.DeletedAt != nil {
|
||||
bDeletedAt = *b.DeletedAt
|
||||
}
|
||||
|
||||
return cmp.Or(
|
||||
cmp.Compare(a.Server.Key, b.Server.Key),
|
||||
aDeletedAt.Compare(bDeletedAt)*-1,
|
||||
cmp.Compare(a.Points, b.Points),
|
||||
cmp.Compare(a.Id, b.Id),
|
||||
)
|
||||
}))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: sort=[odScoreAtt:DESC]",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Set("sort", "odScoreAtt:DESC")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
},
|
||||
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
|
||||
assert.Zero(t, body.Cursor.Next)
|
||||
assert.NotZero(t, body.Cursor.Self)
|
||||
assert.NotZero(t, body.Data)
|
||||
assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.PlayerWithServer) int {
|
||||
return cmp.Or(
|
||||
cmp.Compare(a.Server.Key, b.Server.Key),
|
||||
cmp.Compare(a.OpponentsDefeated.ScoreAtt, b.OpponentsDefeated.ScoreAtt)*-1,
|
||||
cmp.Compare(a.Id, b.Id),
|
||||
)
|
||||
}))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: name",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
|
||||
q := req.URL.Query()
|
||||
|
||||
for _, p := range players {
|
||||
if p.Server.Version.Code == version.Code {
|
||||
q.Add("name", p.Name)
|
||||
}
|
||||
|
||||
if len(q["name"]) == 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.NotEmpty(t, q["name"])
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
},
|
||||
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
|
||||
assert.NotZero(t, body.Cursor.Self)
|
||||
assert.NotZero(t, body.Data)
|
||||
names := req.URL.Query()["name"]
|
||||
assert.Len(t, body.Data, len(names))
|
||||
for _, p := range body.Data {
|
||||
assert.True(t, slices.Contains(names, p.Name))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: deleted=false",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Set("deleted", "false")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
},
|
||||
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
|
||||
assert.NotZero(t, body.Data)
|
||||
for _, p := range body.Data {
|
||||
assert.Nil(t, p.DeletedAt)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: deleted=true",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Set("deleted", "true")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
},
|
||||
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
|
||||
assert.NotZero(t, body.Data)
|
||||
for _, p := range body.Data {
|
||||
assert.NotNil(t, p.DeletedAt)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: limit is not a string",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Set("limit", "asd")
|
||||
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)
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: "invalid-param-format",
|
||||
Message: fmt.Sprintf(
|
||||
"error binding string parameter: strconv.ParseInt: parsing \"%s\": invalid syntax",
|
||||
req.URL.Query().Get("limit"),
|
||||
),
|
||||
Path: []string{"$query", "limit"},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: limit < 1",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Set("limit", "0")
|
||||
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)
|
||||
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
|
||||
require.NoError(t, err)
|
||||
domainErr := domain.MinGreaterEqualError{
|
||||
Min: 1,
|
||||
Current: limit,
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: apimodel.ErrorCode(domainErr.Code()),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"current": float64(domainErr.Current),
|
||||
"min": float64(domainErr.Min),
|
||||
},
|
||||
Path: []string{"$query", "limit"},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("ERR: limit > %d", domain.PlayerListMaxLimit),
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Set("limit", strconv.Itoa(domain.PlayerListMaxLimit+1))
|
||||
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)
|
||||
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
|
||||
require.NoError(t, err)
|
||||
domainErr := domain.MaxLessEqualError{
|
||||
Max: domain.PlayerListMaxLimit,
|
||||
Current: limit,
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: apimodel.ErrorCode(domainErr.Code()),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"current": float64(domainErr.Current),
|
||||
"max": float64(domainErr.Max),
|
||||
},
|
||||
Path: []string{"$query", "limit"},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: len(cursor) < 1",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Set("cursor", "")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
},
|
||||
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
|
||||
domainErr := domain.LenOutOfRangeError{
|
||||
Min: 1,
|
||||
Max: 1000,
|
||||
Current: len(req.URL.Query().Get("cursor")),
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: apimodel.ErrorCode(domainErr.Code()),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"current": float64(domainErr.Current),
|
||||
"max": float64(domainErr.Max),
|
||||
"min": float64(domainErr.Min),
|
||||
},
|
||||
Path: []string{"$query", "cursor"},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: len(cursor) > 1000",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Set("cursor", gofakeit.LetterN(1001))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
},
|
||||
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
|
||||
domainErr := domain.LenOutOfRangeError{
|
||||
Min: 1,
|
||||
Max: 1000,
|
||||
Current: len(req.URL.Query().Get("cursor")),
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: apimodel.ErrorCode(domainErr.Code()),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"current": float64(domainErr.Current),
|
||||
"max": float64(domainErr.Max),
|
||||
"min": float64(domainErr.Min),
|
||||
},
|
||||
Path: []string{"$query", "cursor"},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: invalid cursor",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Set("cursor", gofakeit.LetterN(100))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
},
|
||||
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
|
||||
var domainErr domain.Error
|
||||
require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: apimodel.ErrorCode(domainErr.Code()),
|
||||
Message: domainErr.Error(),
|
||||
Path: []string{"$query", "cursor"},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: len(sort) > 2",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Add("sort", "odScoreAtt:DESC")
|
||||
q.Add("sort", "odScoreAtt:ASC")
|
||||
q.Add("sort", "points:ASC")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
},
|
||||
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
|
||||
domainErr := domain.LenOutOfRangeError{
|
||||
Min: 0,
|
||||
Max: 2,
|
||||
Current: len(req.URL.Query()["sort"]),
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: apimodel.ErrorCode(domainErr.Code()),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"current": float64(domainErr.Current),
|
||||
"max": float64(domainErr.Max),
|
||||
"min": float64(domainErr.Min),
|
||||
},
|
||||
Path: []string{"$query", "sort"},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: invalid sort",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Add("sort", "odScoreAtt:ASC")
|
||||
q.Add("sort", "test: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)
|
||||
domainErr := domain.UnsupportedSortStringError{
|
||||
Sort: req.URL.Query()["sort"][1],
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: apimodel.ErrorCode(domainErr.Code()),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"sort": domainErr.Sort,
|
||||
},
|
||||
Path: []string{"$query", "sort", "1"},
|
||||
},
|
||||
},
|
||||
}, 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: apimodel.ErrorCode(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) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
for range 101 {
|
||||
q.Add("name", gofakeit.LetterN(50))
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
},
|
||||
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
|
||||
domainErr := domain.LenOutOfRangeError{
|
||||
Min: 0,
|
||||
Max: 100,
|
||||
Current: len(req.URL.Query()["name"]),
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: apimodel.ErrorCode(domainErr.Code()),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"current": float64(domainErr.Current),
|
||||
"max": float64(domainErr.Max),
|
||||
"min": float64(domainErr.Min),
|
||||
},
|
||||
Path: []string{"$query", "name"},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: len(name[1]) < 1",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Add("name", gofakeit.LetterN(50))
|
||||
q.Add("name", "")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
},
|
||||
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
|
||||
domainErr := domain.LenOutOfRangeError{
|
||||
Min: 1,
|
||||
Max: 150,
|
||||
Current: len(req.URL.Query()["name"][1]),
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: apimodel.ErrorCode(domainErr.Code()),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"current": float64(domainErr.Current),
|
||||
"max": float64(domainErr.Max),
|
||||
"min": float64(domainErr.Min),
|
||||
},
|
||||
Path: []string{"$query", "name", "1"},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: len(name[1]) > 150",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Add("name", gofakeit.LetterN(50))
|
||||
q.Add("name", gofakeit.LetterN(151))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
},
|
||||
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
|
||||
domainErr := domain.LenOutOfRangeError{
|
||||
Min: 1,
|
||||
Max: 150,
|
||||
Current: len(req.URL.Query()["name"][1]),
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: apimodel.ErrorCode(domainErr.Code()),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"current": float64(domainErr.Current),
|
||||
"max": float64(domainErr.Max),
|
||||
"min": float64(domainErr.Min),
|
||||
},
|
||||
Path: []string{"$query", "name", "1"},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: deleted is not a valid boolean",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Set("deleted", "asd")
|
||||
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)
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: "invalid-param-format",
|
||||
Message: fmt.Sprintf(
|
||||
"error binding string parameter: strconv.ParseBool: parsing \"%s\": invalid syntax",
|
||||
req.URL.Query().Get("deleted"),
|
||||
),
|
||||
Path: []string{"$query", "deleted"},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: version not found",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
req.URL.Path = fmt.Sprintf(endpointListVersionPlayers, randInvalidVersionCode(t, handler))
|
||||
},
|
||||
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
|
||||
pathSegments := strings.Split(req.URL.Path, "/")
|
||||
require.Len(t, pathSegments, 5)
|
||||
domainErr := domain.VersionNotFoundError{
|
||||
VersionCode: pathSegments[3],
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: apimodel.ErrorCode(domainErr.Code()),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"code": domainErr.VersionCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := httptest.NewRequest(
|
||||
http.MethodGet,
|
||||
fmt.Sprintf(endpointListVersionPlayers, version.Code),
|
||||
nil,
|
||||
)
|
||||
if tt.reqModifier != nil {
|
||||
tt.reqModifier(t, req)
|
||||
}
|
||||
|
||||
resp := doCustomRequest(handler, req)
|
||||
defer resp.Body.Close()
|
||||
tt.assertResp(t, req, resp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const endpointListServerPlayers = "/v2/versions/%s/servers/%s/players"
|
||||
|
||||
func TestListServerPlayers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := newAPIHTTPHandler(t)
|
||||
|
@ -710,7 +1455,7 @@ func TestListPlayers(t *testing.T) {
|
|||
name: "ERR: version not found",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
req.URL.Path = fmt.Sprintf(endpointListPlayers, randInvalidVersionCode(t, handler), server.Key)
|
||||
req.URL.Path = fmt.Sprintf(endpointListServerPlayers, randInvalidVersionCode(t, handler), server.Key)
|
||||
},
|
||||
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
@ -741,7 +1486,7 @@ func TestListPlayers(t *testing.T) {
|
|||
name: "ERR: server not found",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
req.URL.Path = fmt.Sprintf(endpointListPlayers, server.Version.Code, domaintest.RandServerKey())
|
||||
req.URL.Path = fmt.Sprintf(endpointListServerPlayers, server.Version.Code, domaintest.RandServerKey())
|
||||
},
|
||||
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
@ -776,7 +1521,7 @@ func TestListPlayers(t *testing.T) {
|
|||
|
||||
req := httptest.NewRequest(
|
||||
http.MethodGet,
|
||||
fmt.Sprintf(endpointListPlayers, server.Version.Code, server.Key),
|
||||
fmt.Sprintf(endpointListServerPlayers, server.Version.Code, server.Key),
|
||||
nil,
|
||||
)
|
||||
if tt.reqModifier != nil {
|
||||
|
@ -1546,7 +2291,7 @@ func getAllPlayers(tb testing.TB, h http.Handler) []playerWithServer {
|
|||
var players []playerWithServer
|
||||
|
||||
for _, s := range servers {
|
||||
resp := doRequest(h, http.MethodGet, fmt.Sprintf(endpointListPlayers, s.Version.Code, s.Key), nil)
|
||||
resp := doRequest(h, http.MethodGet, fmt.Sprintf(endpointListServerPlayers, s.Version.Code, s.Key), nil)
|
||||
require.Equal(tb, http.StatusOK, resp.StatusCode)
|
||||
|
||||
for _, p := range decodeJSON[apimodel.ListPlayersResponse](tb, resp.Body).Data {
|
||||
|
|
|
@ -46,6 +46,36 @@ func NewPlayer(withRelations domain.PlayerWithRelations) Player {
|
|||
return converted
|
||||
}
|
||||
|
||||
func NewPlayerWithServer(withRelations domain.PlayerWithRelations) PlayerWithServer {
|
||||
p := withRelations.Player()
|
||||
|
||||
converted := PlayerWithServer{
|
||||
BestRank: p.BestRank(),
|
||||
BestRankAt: p.BestRankAt(),
|
||||
CreatedAt: p.CreatedAt(),
|
||||
Id: p.ID(),
|
||||
LastActivityAt: p.LastActivityAt(),
|
||||
MostPoints: p.MostPoints(),
|
||||
MostPointsAt: p.MostPointsAt(),
|
||||
MostVillages: p.MostVillages(),
|
||||
MostVillagesAt: p.MostVillagesAt(),
|
||||
Name: p.Name(),
|
||||
NumVillages: p.NumVillages(),
|
||||
OpponentsDefeated: NewPlayerOpponentsDefeated(p.OD()),
|
||||
Points: p.Points(),
|
||||
ProfileUrl: p.ProfileURL().String(),
|
||||
Rank: p.Rank(),
|
||||
Tribe: NewNullTribeMeta(withRelations.Tribe()),
|
||||
Server: NewServerMeta(withRelations.Server()),
|
||||
}
|
||||
|
||||
if deletedAt := p.DeletedAt(); !deletedAt.IsZero() {
|
||||
converted.DeletedAt = &deletedAt
|
||||
}
|
||||
|
||||
return converted
|
||||
}
|
||||
|
||||
func NewPlayerMeta(withRelations domain.PlayerMetaWithRelations) PlayerMeta {
|
||||
p := withRelations.Player()
|
||||
return PlayerMeta{
|
||||
|
@ -89,3 +119,21 @@ func NewGetPlayerResponse(p domain.PlayerWithRelations) GetPlayerResponse {
|
|||
Data: NewPlayer(p),
|
||||
}
|
||||
}
|
||||
|
||||
func NewListPlayersWithServersResponse(res domain.ListPlayersWithRelationsResult) ListPlayersWithServersResponse {
|
||||
players := res.Players()
|
||||
|
||||
resp := ListPlayersWithServersResponse{
|
||||
Data: make([]PlayerWithServer, 0, len(players)),
|
||||
Cursor: Cursor{
|
||||
Next: res.Next().Encode(),
|
||||
Self: res.Self().Encode(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, p := range players {
|
||||
resp.Data = append(resp.Data, NewPlayerWithServer(p))
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
|
|
@ -37,6 +37,14 @@ func NewServer(s domain.Server) Server {
|
|||
return converted
|
||||
}
|
||||
|
||||
func NewServerMeta(s domain.ServerMeta) ServerMeta {
|
||||
return ServerMeta{
|
||||
Key: s.Key(),
|
||||
Open: s.Open(),
|
||||
Url: s.URL().String(),
|
||||
}
|
||||
}
|
||||
|
||||
func NewListServersResponse(res domain.ListServersResult) ListServersResponse {
|
||||
servers := res.Servers()
|
||||
|
||||
|
|
Loading…
Reference in New Issue