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
|
name: MIT
|
||||||
tags:
|
tags:
|
||||||
- name: versions
|
- name: versions
|
||||||
|
description: Version related endpoints
|
||||||
- name: servers
|
- name: servers
|
||||||
|
description: Server related endpoints
|
||||||
- name: tribes
|
- name: tribes
|
||||||
|
description: Tribe related endpoints
|
||||||
- name: players
|
- name: players
|
||||||
|
description: Player related endpoints
|
||||||
- name: villages
|
- name: villages
|
||||||
|
description: Village related endpoints
|
||||||
- name: ennoblements
|
- name: ennoblements
|
||||||
|
description: Ennoblement (conquer) related endpoints
|
||||||
- name: tribe-changes
|
- name: tribe-changes
|
||||||
|
description: Tribe change related endpoints
|
||||||
- name: snapshots
|
- name: snapshots
|
||||||
|
description: Snapshot (historical records) related endpoints
|
||||||
servers:
|
servers:
|
||||||
- url: "{scheme}://{hostname}/api"
|
- url: "{scheme}://{hostname}/api"
|
||||||
variables:
|
variables:
|
||||||
|
@ -56,13 +64,32 @@ paths:
|
||||||
$ref: "#/components/responses/GetVersionResponse"
|
$ref: "#/components/responses/GetVersionResponse"
|
||||||
default:
|
default:
|
||||||
$ref: "#/components/responses/ErrorResponse"
|
$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:
|
/v2/versions/{versionCode}/servers:
|
||||||
get:
|
get:
|
||||||
operationId: listServers
|
operationId: listServers
|
||||||
tags:
|
tags:
|
||||||
- versions
|
- versions
|
||||||
- servers
|
- servers
|
||||||
description: List servers
|
description: List servers associated with the given version
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/VersionCodePathParam"
|
- $ref: "#/components/parameters/VersionCodePathParam"
|
||||||
- $ref: "#/components/parameters/CursorQueryParam"
|
- $ref: "#/components/parameters/CursorQueryParam"
|
||||||
|
@ -140,7 +167,7 @@ paths:
|
||||||
- versions
|
- versions
|
||||||
- servers
|
- servers
|
||||||
- tribes
|
- tribes
|
||||||
description: List tribes
|
description: List tribes associated with the given server
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/VersionCodePathParam"
|
- $ref: "#/components/parameters/VersionCodePathParam"
|
||||||
- $ref: "#/components/parameters/ServerKeyPathParam"
|
- $ref: "#/components/parameters/ServerKeyPathParam"
|
||||||
|
@ -173,12 +200,12 @@ paths:
|
||||||
$ref: "#/components/responses/ErrorResponse"
|
$ref: "#/components/responses/ErrorResponse"
|
||||||
/v2/versions/{versionCode}/servers/{serverKey}/players:
|
/v2/versions/{versionCode}/servers/{serverKey}/players:
|
||||||
get:
|
get:
|
||||||
operationId: listPlayers
|
operationId: listServerPlayers
|
||||||
tags:
|
tags:
|
||||||
- versions
|
- versions
|
||||||
- servers
|
- servers
|
||||||
- players
|
- players
|
||||||
description: List players
|
description: List players associated with the given server
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/VersionCodePathParam"
|
- $ref: "#/components/parameters/VersionCodePathParam"
|
||||||
- $ref: "#/components/parameters/ServerKeyPathParam"
|
- $ref: "#/components/parameters/ServerKeyPathParam"
|
||||||
|
@ -237,7 +264,7 @@ paths:
|
||||||
- versions
|
- versions
|
||||||
- servers
|
- servers
|
||||||
- villages
|
- villages
|
||||||
description: List villages
|
description: List villages associated with the given server
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/VersionCodePathParam"
|
- $ref: "#/components/parameters/VersionCodePathParam"
|
||||||
- $ref: "#/components/parameters/ServerKeyPathParam"
|
- $ref: "#/components/parameters/ServerKeyPathParam"
|
||||||
|
@ -313,7 +340,7 @@ paths:
|
||||||
- versions
|
- versions
|
||||||
- servers
|
- servers
|
||||||
- ennoblements
|
- ennoblements
|
||||||
description: List ennoblements
|
description: List ennoblements associated with the given server
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/VersionCodePathParam"
|
- $ref: "#/components/parameters/VersionCodePathParam"
|
||||||
- $ref: "#/components/parameters/ServerKeyPathParam"
|
- $ref: "#/components/parameters/ServerKeyPathParam"
|
||||||
|
@ -597,6 +624,21 @@ components:
|
||||||
createdAt:
|
createdAt:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
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:
|
ServerConfig:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
@ -1371,6 +1413,15 @@ components:
|
||||||
deletedAt:
|
deletedAt:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
PlayerWithServer:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/Player"
|
||||||
|
- type: object
|
||||||
|
required:
|
||||||
|
- server
|
||||||
|
properties:
|
||||||
|
server:
|
||||||
|
$ref: "#/components/schemas/ServerMeta"
|
||||||
PlayerMeta:
|
PlayerMeta:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
@ -1936,6 +1987,21 @@ components:
|
||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
$ref: "#/components/schemas/Tribe"
|
$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:
|
ListPlayersResponse:
|
||||||
description: ""
|
description: ""
|
||||||
content:
|
content:
|
||||||
|
|
|
@ -118,10 +118,7 @@ func (repo *PlayerBunRepository) ListWithRelations(
|
||||||
|
|
||||||
if err := repo.db.NewSelect().
|
if err := repo.db.NewSelect().
|
||||||
Model(&players).
|
Model(&players).
|
||||||
Apply(listPlayersParamsApplier{params: params}.apply).
|
Apply(listPlayersParamsApplier{params: params, includeRelations: true}.apply).
|
||||||
Relation("Tribe", func(q *bun.SelectQuery) *bun.SelectQuery {
|
|
||||||
return q.Column(bunmodel.TribeMetaColumns...)
|
|
||||||
}).
|
|
||||||
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
return domain.ListPlayersWithRelationsResult{}, fmt.Errorf("couldn't select players from the db: %w", err)
|
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 {
|
type listPlayersParamsApplier struct {
|
||||||
params domain.ListPlayersParams
|
params domain.ListPlayersParams
|
||||||
|
includeRelations bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
|
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())
|
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
|
//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",
|
name: "OK: ids serverKeys",
|
||||||
params: func(t *testing.T) domain.ListPlayersParams {
|
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()))
|
require.Len(t, resWithRelations.Players(), len(res.Players()))
|
||||||
for i, p := range resWithRelations.Players() {
|
for i, p := range resWithRelations.Players() {
|
||||||
assert.Equal(t, res.Players()[i], p.Player())
|
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(), p.Tribe().V.ID())
|
||||||
assert.Equal(t, p.Player().TribeID() != 0, p.Tribe().Valid)
|
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)
|
repos := newRepos(t)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
params func(t *testing.T) domain.ListVillagesParams
|
params func(t *testing.T) domain.ListVillagesParams
|
||||||
assertResult func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult)
|
assertResult func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult)
|
||||||
// assertResultWithRelations is optional
|
|
||||||
assertResultWithRelations func(
|
assertResultWithRelations func(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
params domain.ListVillagesParams,
|
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) {
|
func (p Player) ToMeta() (domain.PlayerMeta, error) {
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ServerMetaColumns = []string{"key", "url", "open"}
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
bun.BaseModel `bun:"table:servers,alias:server"`
|
bun.BaseModel `bun:"table:servers,alias:server"`
|
||||||
Key string `bun:"key,nullzero,pk"`
|
Key string `bun:"key,nullzero,pk"`
|
||||||
|
@ -83,6 +85,14 @@ func (s Server) ToDomain() (domain.Server, error) {
|
||||||
return converted, nil
|
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
|
type Servers []Server
|
||||||
|
|
||||||
func (ss Servers) ToDomain() (domain.Servers, error) {
|
func (ss Servers) ToDomain() (domain.Servers, error) {
|
||||||
|
|
|
@ -122,6 +122,7 @@ func NewPlayer(tb TestingTB, opts ...func(cfg *PlayerConfig)) domain.Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlayerWithRelationsConfig struct {
|
type PlayerWithRelationsConfig struct {
|
||||||
|
ServerOptions []func(cfg *ServerConfig)
|
||||||
PlayerOptions []func(cfg *PlayerConfig)
|
PlayerOptions []func(cfg *PlayerConfig)
|
||||||
TribeOptions []func(cfg *TribeConfig)
|
TribeOptions []func(cfg *TribeConfig)
|
||||||
}
|
}
|
||||||
|
@ -137,6 +138,12 @@ func NewPlayerWithRelations(tb TestingTB, opts ...func(cfg *PlayerWithRelationsC
|
||||||
|
|
||||||
p := NewPlayer(tb, cfg.PlayerOptions...)
|
p := NewPlayer(tb, cfg.PlayerOptions...)
|
||||||
|
|
||||||
|
cfg.ServerOptions = append([]func(cfg *ServerConfig){
|
||||||
|
func(cfg *ServerConfig) {
|
||||||
|
cfg.Key = p.ServerKey()
|
||||||
|
},
|
||||||
|
}, cfg.ServerOptions...)
|
||||||
|
|
||||||
if p.TribeID() > 0 {
|
if p.TribeID() > 0 {
|
||||||
cfg.TribeOptions = append([]func(cfg *TribeConfig){
|
cfg.TribeOptions = append([]func(cfg *TribeConfig){
|
||||||
func(cfg *TribeConfig) {
|
func(cfg *TribeConfig) {
|
||||||
|
@ -150,8 +157,11 @@ func NewPlayerWithRelations(tb TestingTB, opts ...func(cfg *PlayerWithRelationsC
|
||||||
tribe = NewTribe(tb, cfg.TribeOptions...).Meta()
|
tribe = NewTribe(tb, cfg.TribeOptions...).Meta()
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.WithRelations(domain.NullTribeMeta{
|
return p.WithRelations(
|
||||||
V: tribe,
|
NewServer(tb, cfg.ServerOptions...).Meta(),
|
||||||
Valid: !tribe.IsZero(),
|
domain.NullTribeMeta{
|
||||||
})
|
V: tribe,
|
||||||
|
Valid: !tribe.IsZero(),
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -183,9 +183,10 @@ func (p Player) DeletedAt() time.Time {
|
||||||
return p.deletedAt
|
return p.deletedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Player) WithRelations(tribe NullTribeMeta) PlayerWithRelations {
|
func (p Player) WithRelations(server ServerMeta, tribe NullTribeMeta) PlayerWithRelations {
|
||||||
return PlayerWithRelations{
|
return PlayerWithRelations{
|
||||||
player: p,
|
player: p,
|
||||||
|
server: server,
|
||||||
tribe: tribe,
|
tribe: tribe,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -283,6 +284,7 @@ func (ps Players) Delete(serverKey string, active BasePlayers) ([]int, []CreateT
|
||||||
|
|
||||||
type PlayerWithRelations struct {
|
type PlayerWithRelations struct {
|
||||||
player Player
|
player Player
|
||||||
|
server ServerMeta
|
||||||
tribe NullTribeMeta
|
tribe NullTribeMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,6 +292,10 @@ func (p PlayerWithRelations) Player() Player {
|
||||||
return p.player
|
return p.player
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p PlayerWithRelations) Server() ServerMeta {
|
||||||
|
return p.server
|
||||||
|
}
|
||||||
|
|
||||||
func (p PlayerWithRelations) Tribe() NullTribeMeta {
|
func (p PlayerWithRelations) Tribe() NullTribeMeta {
|
||||||
return p.tribe
|
return p.tribe
|
||||||
}
|
}
|
||||||
|
@ -804,14 +810,15 @@ func (pc PlayerCursor) Encode() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListPlayersParams struct {
|
type ListPlayersParams struct {
|
||||||
ids []int
|
ids []int
|
||||||
serverKeys []string
|
serverKeys []string
|
||||||
names []string
|
versionCodes []string
|
||||||
tribeIDs []int
|
names []string
|
||||||
deleted NullBool
|
tribeIDs []int
|
||||||
sort []PlayerSort
|
deleted NullBool
|
||||||
cursor PlayerCursor
|
sort []PlayerSort
|
||||||
limit int
|
cursor PlayerCursor
|
||||||
|
limit int
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -871,6 +878,27 @@ func (params *ListPlayersParams) SetServerKeys(serverKeys []string) error {
|
||||||
return nil
|
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 {
|
func (params *ListPlayersParams) Names() []string {
|
||||||
return params.names
|
return params.names
|
||||||
}
|
}
|
||||||
|
@ -958,6 +986,22 @@ func (params *ListPlayersParams) SetSort(sort []PlayerSort) error {
|
||||||
return nil
|
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 {
|
func (params *ListPlayersParams) PrependSortString(sort []string, allowed []PlayerSort, maxLength int) error {
|
||||||
if len(sort) == 0 {
|
if len(sort) == 0 {
|
||||||
return nil
|
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) {
|
func TestListPlayersParams_SetNames(t *testing.T) {
|
||||||
t.Parallel()
|
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) {
|
func TestListPlayersParams_PrependSortString(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|
|
@ -199,6 +199,14 @@ func (s Server) ToCursor() (ServerCursor, error) {
|
||||||
return NewServerCursor(s.key, s.open)
|
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 {
|
func (s Server) Base() BaseServer {
|
||||||
return BaseServer{
|
return BaseServer{
|
||||||
key: s.key,
|
key: s.key,
|
||||||
|
@ -280,6 +288,51 @@ func (ss Servers) CleanUpData() ([]CleanUpDataCmdPayload, error) {
|
||||||
return res, nil
|
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 {
|
type CreateServerParams struct {
|
||||||
base BaseServer
|
base BaseServer
|
||||||
versionCode string
|
versionCode string
|
||||||
|
|
|
@ -29,12 +29,87 @@ var apiPlayerSortAllowedValues = []domain.PlayerSort{
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo
|
//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,
|
w http.ResponseWriter,
|
||||||
r *http.Request,
|
r *http.Request,
|
||||||
_ apimodel.VersionCodePathParam,
|
_ apimodel.VersionCodePathParam,
|
||||||
serverKey apimodel.ServerKeyPathParam,
|
serverKey apimodel.ServerKeyPathParam,
|
||||||
params apimodel.ListPlayersParams,
|
params apimodel.ListServerPlayersParams,
|
||||||
) {
|
) {
|
||||||
domainParams := domain.NewListPlayersParams()
|
domainParams := domain.NewListPlayersParams()
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,754 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"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()
|
t.Parallel()
|
||||||
|
|
||||||
handler := newAPIHTTPHandler(t)
|
handler := newAPIHTTPHandler(t)
|
||||||
|
@ -710,7 +1455,7 @@ func TestListPlayers(t *testing.T) {
|
||||||
name: "ERR: version not found",
|
name: "ERR: version not found",
|
||||||
reqModifier: func(t *testing.T, req *http.Request) {
|
reqModifier: func(t *testing.T, req *http.Request) {
|
||||||
t.Helper()
|
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) {
|
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
@ -741,7 +1486,7 @@ func TestListPlayers(t *testing.T) {
|
||||||
name: "ERR: server not found",
|
name: "ERR: server not found",
|
||||||
reqModifier: func(t *testing.T, req *http.Request) {
|
reqModifier: func(t *testing.T, req *http.Request) {
|
||||||
t.Helper()
|
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) {
|
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
@ -776,7 +1521,7 @@ func TestListPlayers(t *testing.T) {
|
||||||
|
|
||||||
req := httptest.NewRequest(
|
req := httptest.NewRequest(
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
fmt.Sprintf(endpointListPlayers, server.Version.Code, server.Key),
|
fmt.Sprintf(endpointListServerPlayers, server.Version.Code, server.Key),
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
if tt.reqModifier != nil {
|
if tt.reqModifier != nil {
|
||||||
|
@ -1546,7 +2291,7 @@ func getAllPlayers(tb testing.TB, h http.Handler) []playerWithServer {
|
||||||
var players []playerWithServer
|
var players []playerWithServer
|
||||||
|
|
||||||
for _, s := range servers {
|
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)
|
require.Equal(tb, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
for _, p := range decodeJSON[apimodel.ListPlayersResponse](tb, resp.Body).Data {
|
for _, p := range decodeJSON[apimodel.ListPlayersResponse](tb, resp.Body).Data {
|
||||||
|
|
|
@ -46,6 +46,36 @@ func NewPlayer(withRelations domain.PlayerWithRelations) Player {
|
||||||
return converted
|
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 {
|
func NewPlayerMeta(withRelations domain.PlayerMetaWithRelations) PlayerMeta {
|
||||||
p := withRelations.Player()
|
p := withRelations.Player()
|
||||||
return PlayerMeta{
|
return PlayerMeta{
|
||||||
|
@ -89,3 +119,21 @@ func NewGetPlayerResponse(p domain.PlayerWithRelations) GetPlayerResponse {
|
||||||
Data: NewPlayer(p),
|
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
|
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 {
|
func NewListServersResponse(res domain.ListServersResult) ListServersResponse {
|
||||||
servers := res.Servers()
|
servers := res.Servers()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue