Compare commits
3 Commits
523b80a774
...
084a771bc8
Author | SHA1 | Date |
---|---|---|
Renovate | 084a771bc8 | |
Dawid Wysokiński | 70deae8696 | |
Dawid Wysokiński | 084bb5aa85 |
112
api/openapi3.yml
112
api/openapi3.yml
|
@ -166,6 +166,25 @@ paths:
|
|||
$ref: "#/components/responses/GetTribeResponse"
|
||||
default:
|
||||
$ref: "#/components/responses/ErrorResponse"
|
||||
/v2/versions/{versionCode}/servers/{serverKey}/players:
|
||||
get:
|
||||
operationId: listPlayers
|
||||
tags:
|
||||
- versions
|
||||
- servers
|
||||
- players
|
||||
description: List players
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/VersionCodePathParam"
|
||||
- $ref: "#/components/parameters/ServerKeyPathParam"
|
||||
- $ref: "#/components/parameters/CursorQueryParam"
|
||||
- $ref: "#/components/parameters/LimitQueryParam"
|
||||
- $ref: "#/components/parameters/PlayerDeletedQueryParam"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/ListPlayersResponse"
|
||||
default:
|
||||
$ref: "#/components/responses/ErrorResponse"
|
||||
components:
|
||||
schemas:
|
||||
Error:
|
||||
|
@ -920,6 +939,76 @@ components:
|
|||
deletedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
PlayerOpponentsDefeated:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TribeOpponentsDefeated"
|
||||
- type: object
|
||||
required:
|
||||
- rankSup
|
||||
- scoreSup
|
||||
properties:
|
||||
rankSup:
|
||||
type: integer
|
||||
scoreSup:
|
||||
type: integer
|
||||
Player:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- rank
|
||||
- points
|
||||
- numVillages
|
||||
- profileUrl
|
||||
- lastActivityAt
|
||||
- bestRank
|
||||
- bestRankAt
|
||||
- mostPoints
|
||||
- mostPointsAt
|
||||
- mostVillages
|
||||
- mostVillagesAt
|
||||
- opponentsDefeated
|
||||
- createdAt
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
rank:
|
||||
type: integer
|
||||
points:
|
||||
type: integer
|
||||
numVillages:
|
||||
type: integer
|
||||
profileUrl:
|
||||
type: string
|
||||
format: uri
|
||||
lastActivityAt:
|
||||
type: string
|
||||
format: date-time
|
||||
bestRank:
|
||||
type: integer
|
||||
bestRankAt:
|
||||
type: string
|
||||
format: date-time
|
||||
mostPoints:
|
||||
type: integer
|
||||
mostPointsAt:
|
||||
type: string
|
||||
format: date-time
|
||||
mostVillages:
|
||||
type: integer
|
||||
mostVillagesAt:
|
||||
type: string
|
||||
format: date-time
|
||||
opponentsDefeated:
|
||||
$ref: "#/components/schemas/PlayerOpponentsDefeated"
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
deletedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
Cursor:
|
||||
type: object
|
||||
x-go-type-skip-optional-pointer: true
|
||||
|
@ -1003,6 +1092,14 @@ components:
|
|||
items:
|
||||
type: string
|
||||
maxItems: 100
|
||||
PlayerDeletedQueryParam:
|
||||
name: deleted
|
||||
in: query
|
||||
description: true=only deleted players, false=only existing players,
|
||||
by default both existing and deleted players are returned
|
||||
schema:
|
||||
type: boolean
|
||||
required: false
|
||||
VersionCodePathParam:
|
||||
in: path
|
||||
name: versionCode
|
||||
|
@ -1134,6 +1231,21 @@ components:
|
|||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/Tribe"
|
||||
ListPlayersResponse:
|
||||
description: ""
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/PaginationResponse"
|
||||
- type: object
|
||||
required:
|
||||
- data
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Player"
|
||||
ErrorResponse:
|
||||
description: Default error response.
|
||||
content:
|
||||
|
|
|
@ -107,11 +107,13 @@ var cmdServe = &cli.Command{
|
|||
versionRepo := adapter.NewVersionBunRepository(bunDB)
|
||||
serverRepo := adapter.NewServerBunRepository(bunDB)
|
||||
tribeRepo := adapter.NewTribeBunRepository(bunDB)
|
||||
playerRepo := adapter.NewPlayerBunRepository(bunDB)
|
||||
|
||||
// services
|
||||
versionSvc := app.NewVersionService(versionRepo)
|
||||
serverSvc := app.NewServerService(serverRepo, nil, nil)
|
||||
tribeSvc := app.NewTribeService(tribeRepo, nil, nil)
|
||||
playerSvc := app.NewPlayerService(playerRepo, nil, nil, nil)
|
||||
|
||||
// health
|
||||
h := health.New()
|
||||
|
@ -138,6 +140,7 @@ var cmdServe = &cli.Command{
|
|||
versionSvc,
|
||||
serverSvc,
|
||||
tribeSvc,
|
||||
playerSvc,
|
||||
port.WithOpenAPIConfig(oapiCfg),
|
||||
))
|
||||
|
||||
|
|
|
@ -89,17 +89,25 @@ func (repo *PlayerBunRepository) CreateOrUpdate(ctx context.Context, params ...d
|
|||
return nil
|
||||
}
|
||||
|
||||
func (repo *PlayerBunRepository) List(ctx context.Context, params domain.ListPlayersParams) (domain.Players, error) {
|
||||
func (repo *PlayerBunRepository) List(
|
||||
ctx context.Context,
|
||||
params domain.ListPlayersParams,
|
||||
) (domain.ListPlayersResult, error) {
|
||||
var players bunmodel.Players
|
||||
|
||||
if err := repo.db.NewSelect().
|
||||
Model(&players).
|
||||
Apply(listPlayersParamsApplier{params: params}.apply).
|
||||
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, fmt.Errorf("couldn't select players from the db: %w", err)
|
||||
return domain.ListPlayersResult{}, fmt.Errorf("couldn't select players from the db: %w", err)
|
||||
}
|
||||
|
||||
return players.ToDomain()
|
||||
converted, err := players.ToDomain()
|
||||
if err != nil {
|
||||
return domain.ListPlayersResult{}, err
|
||||
}
|
||||
|
||||
return domain.NewListPlayersResult(separateListResultAndNext(converted, params.Limit()))
|
||||
}
|
||||
|
||||
func (repo *PlayerBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error {
|
||||
|
@ -132,10 +140,6 @@ func (a listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
|
|||
q = q.Where("player.id IN (?)", bun.In(ids))
|
||||
}
|
||||
|
||||
if idGT := a.params.IDGT(); idGT.Valid {
|
||||
q = q.Where("player.id > ?", idGT.Value)
|
||||
}
|
||||
|
||||
if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 {
|
||||
q = q.Where("player.server_key IN (?)", bun.In(serverKeys))
|
||||
}
|
||||
|
@ -163,5 +167,90 @@ func (a listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
|
|||
}
|
||||
}
|
||||
|
||||
return q.Limit(a.params.Limit()).Offset(a.params.Offset())
|
||||
return q.Limit(a.params.Limit() + 1).Apply(a.applyCursor)
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (a listPlayersParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
if a.params.Cursor().IsZero() {
|
||||
return q
|
||||
}
|
||||
|
||||
q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
cursor := a.params.Cursor()
|
||||
cursorID := cursor.ID()
|
||||
cursorServerKey := cursor.ServerKey()
|
||||
|
||||
sort := a.params.Sort()
|
||||
sortLen := len(sort)
|
||||
|
||||
// based on https://github.com/prisma/prisma/issues/19159#issuecomment-1713389245
|
||||
|
||||
switch {
|
||||
case sortLen == 1:
|
||||
switch sort[0] {
|
||||
case domain.PlayerSortIDASC:
|
||||
q = q.Where("player.id >= ?", cursorID)
|
||||
case domain.PlayerSortIDDESC:
|
||||
q = q.Where("player.id <= ?", cursorID)
|
||||
case domain.PlayerSortServerKeyASC,
|
||||
domain.PlayerSortServerKeyDESC:
|
||||
return q.Err(errSortNoUniqueField)
|
||||
default:
|
||||
return q.Err(errUnsupportedSortValue)
|
||||
}
|
||||
case sortLen > 1:
|
||||
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
for i := 0; i < sortLen; i++ {
|
||||
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
current := sort[i]
|
||||
|
||||
for j := 0; j < i; j++ {
|
||||
s := sort[j]
|
||||
|
||||
switch s {
|
||||
case domain.PlayerSortIDASC,
|
||||
domain.PlayerSortIDDESC:
|
||||
q = q.Where("player.id = ?", cursorID)
|
||||
case domain.PlayerSortServerKeyASC,
|
||||
domain.PlayerSortServerKeyDESC:
|
||||
q = q.Where("player.server_key = ?", cursorServerKey)
|
||||
default:
|
||||
return q.Err(errUnsupportedSortValue)
|
||||
}
|
||||
}
|
||||
|
||||
greaterSymbol := bun.Safe(">")
|
||||
lessSymbol := bun.Safe("<")
|
||||
|
||||
if i == sortLen-1 {
|
||||
greaterSymbol = ">="
|
||||
lessSymbol = "<="
|
||||
}
|
||||
|
||||
switch current {
|
||||
case domain.PlayerSortIDASC:
|
||||
q = q.Where("player.id ? ?", greaterSymbol, cursorID)
|
||||
case domain.PlayerSortIDDESC:
|
||||
q = q.Where("player.id ? ?", lessSymbol, cursorID)
|
||||
case domain.PlayerSortServerKeyASC:
|
||||
q = q.Where("player.server_key ? ?", greaterSymbol, cursorServerKey)
|
||||
case domain.PlayerSortServerKeyDESC:
|
||||
q = q.Where("player.server_key ? ?", lessSymbol, cursorServerKey)
|
||||
default:
|
||||
return q.Err(errUnsupportedSortValue)
|
||||
}
|
||||
|
||||
return q
|
||||
})
|
||||
}
|
||||
|
||||
return q
|
||||
})
|
||||
}
|
||||
|
||||
return q
|
||||
})
|
||||
|
||||
return q
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit
|
|||
})
|
||||
})
|
||||
|
||||
t.Run("List & ListCount", func(t *testing.T) {
|
||||
t.Run("List", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
repos := newRepos(t)
|
||||
|
|
|
@ -90,8 +90,9 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo
|
|||
}))
|
||||
require.NoError(t, listPlayersParams.SetLimit(domain.PlayerSnapshotListMaxLimit/2))
|
||||
|
||||
players, err := repos.player.List(ctx, listPlayersParams)
|
||||
res, err := repos.player.List(ctx, listPlayersParams)
|
||||
require.NoError(t, err)
|
||||
players := res.Players()
|
||||
require.NotEmpty(t, players)
|
||||
|
||||
date := time.Now()
|
||||
|
@ -113,7 +114,7 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo
|
|||
})
|
||||
})
|
||||
|
||||
t.Run("List & ListCount", func(t *testing.T) {
|
||||
t.Run("List", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
repos := newRepos(t)
|
||||
|
|
|
@ -3,7 +3,6 @@ package adapter_test
|
|||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"testing"
|
||||
|
@ -39,8 +38,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
|||
require.NoError(t, listParams.SetIDs(ids))
|
||||
require.NoError(t, listParams.SetServerKeys([]string{params[0].ServerKey()}))
|
||||
|
||||
players, err := repos.player.List(ctx, listParams)
|
||||
res, err := repos.player.List(ctx, listParams)
|
||||
require.NoError(t, err)
|
||||
players := res.Players()
|
||||
assert.Len(t, players, len(params))
|
||||
for i, p := range params {
|
||||
idx := slices.IndexFunc(players, func(player domain.Player) bool {
|
||||
|
@ -100,22 +100,16 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
|||
})
|
||||
})
|
||||
|
||||
t.Run("List & ListCount", func(t *testing.T) {
|
||||
t.Run("List", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
repos := newRepos(t)
|
||||
|
||||
players, listPlayersErr := repos.player.List(ctx, domain.NewListPlayersParams())
|
||||
require.NoError(t, listPlayersErr)
|
||||
require.NotEmpty(t, players)
|
||||
randPlayer := players[0]
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
params func(t *testing.T) domain.ListPlayersParams
|
||||
assertPlayers func(t *testing.T, params domain.ListPlayersParams, players domain.Players)
|
||||
assertError func(t *testing.T, err error)
|
||||
assertTotal func(t *testing.T, params domain.ListPlayersParams, total int)
|
||||
name string
|
||||
params func(t *testing.T) domain.ListPlayersParams
|
||||
assertResult func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult)
|
||||
assertError func(t *testing.T, err error)
|
||||
}{
|
||||
{
|
||||
name: "OK: default params",
|
||||
|
@ -123,8 +117,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
|||
t.Helper()
|
||||
return domain.NewListPlayersParams()
|
||||
},
|
||||
assertPlayers: func(t *testing.T, _ domain.ListPlayersParams, players domain.Players) {
|
||||
assertResult: func(t *testing.T, _ domain.ListPlayersParams, res domain.ListPlayersResult) {
|
||||
t.Helper()
|
||||
players := res.Players()
|
||||
assert.NotEmpty(t, len(players))
|
||||
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
|
||||
return cmp.Or(
|
||||
|
@ -132,15 +127,13 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
|||
cmp.Compare(a.ID(), b.ID()),
|
||||
)
|
||||
}))
|
||||
assert.False(t, res.Self().IsZero())
|
||||
assert.False(t, res.Next().IsZero())
|
||||
},
|
||||
assertError: func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
require.NoError(t, err)
|
||||
},
|
||||
assertTotal: func(t *testing.T, _ domain.ListPlayersParams, total int) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, total)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: sort=[serverKey DESC, id DESC]",
|
||||
|
@ -150,8 +143,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
|||
require.NoError(t, params.SetSort([]domain.PlayerSort{domain.PlayerSortServerKeyDESC, domain.PlayerSortIDDESC}))
|
||||
return params
|
||||
},
|
||||
assertPlayers: func(t *testing.T, _ domain.ListPlayersParams, players domain.Players) {
|
||||
assertResult: func(t *testing.T, _ domain.ListPlayersParams, res domain.ListPlayersResult) {
|
||||
t.Helper()
|
||||
players := res.Players()
|
||||
assert.NotEmpty(t, len(players))
|
||||
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
|
||||
return cmp.Or(
|
||||
|
@ -164,26 +158,32 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
|||
t.Helper()
|
||||
require.NoError(t, err)
|
||||
},
|
||||
assertTotal: func(t *testing.T, _ domain.ListPlayersParams, total int) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, total)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("OK: ids=[%d] serverKeys=[%s]", randPlayer.ID(), randPlayer.ServerKey()),
|
||||
name: "OK: ids serverKeys",
|
||||
params: func(t *testing.T) domain.ListPlayersParams {
|
||||
t.Helper()
|
||||
|
||||
params := domain.NewListPlayersParams()
|
||||
|
||||
res, err := repos.player.List(ctx, params)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, len(res.Players()))
|
||||
randPlayer := res.Players()[0]
|
||||
|
||||
require.NoError(t, params.SetIDs([]int{randPlayer.ID()}))
|
||||
require.NoError(t, params.SetServerKeys([]string{randPlayer.ServerKey()}))
|
||||
|
||||
return params
|
||||
},
|
||||
assertPlayers: func(t *testing.T, params domain.ListPlayersParams, players domain.Players) {
|
||||
assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) {
|
||||
t.Helper()
|
||||
|
||||
ids := params.IDs()
|
||||
serverKeys := params.ServerKeys()
|
||||
|
||||
players := res.Players()
|
||||
assert.NotEmpty(t, players)
|
||||
for _, p := range players {
|
||||
assert.True(t, slices.Contains(ids, p.ID()))
|
||||
assert.True(t, slices.Contains(serverKeys, p.ServerKey()))
|
||||
|
@ -193,37 +193,6 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
|||
t.Helper()
|
||||
require.NoError(t, err)
|
||||
},
|
||||
assertTotal: func(t *testing.T, _ domain.ListPlayersParams, total int) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, total)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("OK: idGT=%d", randPlayer.ID()),
|
||||
params: func(t *testing.T) domain.ListPlayersParams {
|
||||
t.Helper()
|
||||
params := domain.NewListPlayersParams()
|
||||
require.NoError(t, params.SetIDGT(domain.NullInt{
|
||||
Value: randPlayer.ID(),
|
||||
Valid: true,
|
||||
}))
|
||||
return params
|
||||
},
|
||||
assertPlayers: func(t *testing.T, params domain.ListPlayersParams, players domain.Players) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, players)
|
||||
for _, p := range players {
|
||||
assert.Greater(t, p.ID(), params.IDGT().Value, p.ID())
|
||||
}
|
||||
},
|
||||
assertError: func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
require.NoError(t, err)
|
||||
},
|
||||
assertTotal: func(t *testing.T, _ domain.ListPlayersParams, total int) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, total)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: deleted=true",
|
||||
|
@ -236,8 +205,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
|||
}))
|
||||
return params
|
||||
},
|
||||
assertPlayers: func(t *testing.T, _ domain.ListPlayersParams, players domain.Players) {
|
||||
assertResult: func(t *testing.T, _ domain.ListPlayersParams, res domain.ListPlayersResult) {
|
||||
t.Helper()
|
||||
players := res.Players()
|
||||
assert.NotEmpty(t, players)
|
||||
for _, s := range players {
|
||||
assert.True(t, s.IsDeleted())
|
||||
|
@ -247,10 +217,6 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
|||
t.Helper()
|
||||
require.NoError(t, err)
|
||||
},
|
||||
assertTotal: func(t *testing.T, _ domain.ListPlayersParams, total int) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, total)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: deleted=false",
|
||||
|
@ -263,8 +229,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
|||
}))
|
||||
return params
|
||||
},
|
||||
assertPlayers: func(t *testing.T, _ domain.ListPlayersParams, players domain.Players) {
|
||||
assertResult: func(t *testing.T, _ domain.ListPlayersParams, res domain.ListPlayersResult) {
|
||||
t.Helper()
|
||||
players := res.Players()
|
||||
assert.NotEmpty(t, players)
|
||||
for _, s := range players {
|
||||
assert.False(t, s.IsDeleted())
|
||||
|
@ -274,31 +241,145 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
|||
t.Helper()
|
||||
require.NoError(t, err)
|
||||
},
|
||||
assertTotal: func(t *testing.T, _ domain.ListPlayersParams, total int) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, total)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: offset=1 limit=2",
|
||||
name: "OK: cursor serverKeys sort=[id ASC]",
|
||||
params: func(t *testing.T) domain.ListPlayersParams {
|
||||
t.Helper()
|
||||
|
||||
params := domain.NewListPlayersParams()
|
||||
require.NoError(t, params.SetOffset(1))
|
||||
require.NoError(t, params.SetLimit(2))
|
||||
|
||||
res, err := repos.player.List(ctx, params)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(res.Players()), 2)
|
||||
|
||||
require.NoError(t, params.SetSort([]domain.PlayerSort{domain.PlayerSortIDASC}))
|
||||
require.NoError(t, params.SetServerKeys([]string{res.Players()[1].ServerKey()}))
|
||||
require.NoError(t, params.SetCursor(domaintest.NewPlayerCursor(t, func(cfg *domaintest.PlayerCursorConfig) {
|
||||
cfg.ID = res.Players()[1].ID()
|
||||
})))
|
||||
|
||||
return params
|
||||
},
|
||||
assertPlayers: func(t *testing.T, params domain.ListPlayersParams, players domain.Players) {
|
||||
assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) {
|
||||
t.Helper()
|
||||
assert.Len(t, players, params.Limit())
|
||||
|
||||
serverKeys := params.ServerKeys()
|
||||
|
||||
players := res.Players()
|
||||
assert.NotEmpty(t, len(players))
|
||||
for _, p := range res.Players() {
|
||||
assert.GreaterOrEqual(t, p.ID(), params.Cursor().ID())
|
||||
assert.True(t, slices.Contains(serverKeys, p.ServerKey()))
|
||||
}
|
||||
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
|
||||
return cmp.Compare(a.ID(), b.ID())
|
||||
}))
|
||||
},
|
||||
assertError: func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
require.NoError(t, err)
|
||||
},
|
||||
assertTotal: func(t *testing.T, _ domain.ListPlayersParams, total int) {
|
||||
},
|
||||
{
|
||||
name: "OK: cursor sort=[serverKey ASC, id ASC]",
|
||||
params: func(t *testing.T) domain.ListPlayersParams {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, total)
|
||||
|
||||
params := domain.NewListPlayersParams()
|
||||
require.NoError(t, params.SetSort([]domain.PlayerSort{
|
||||
domain.PlayerSortServerKeyASC,
|
||||
domain.PlayerSortIDASC,
|
||||
}))
|
||||
|
||||
res, err := repos.player.List(ctx, params)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(res.Players()), 2)
|
||||
|
||||
require.NoError(t, params.SetCursor(domaintest.NewPlayerCursor(t, func(cfg *domaintest.PlayerCursorConfig) {
|
||||
cfg.ID = res.Players()[1].ID()
|
||||
cfg.ServerKey = res.Players()[1].ServerKey()
|
||||
})))
|
||||
|
||||
return params
|
||||
},
|
||||
assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) {
|
||||
t.Helper()
|
||||
players := res.Players()
|
||||
assert.NotEmpty(t, len(players))
|
||||
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
|
||||
return cmp.Or(
|
||||
cmp.Compare(a.ServerKey(), b.ServerKey()),
|
||||
cmp.Compare(a.ID(), b.ID()),
|
||||
)
|
||||
}))
|
||||
assert.GreaterOrEqual(t, players[0].ID(), params.Cursor().ID())
|
||||
for _, p := range res.Players() {
|
||||
assert.GreaterOrEqual(t, p.ServerKey(), params.Cursor().ServerKey())
|
||||
}
|
||||
},
|
||||
assertError: func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
require.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: cursor sort=[serverKey DESC, id DESC]",
|
||||
params: func(t *testing.T) domain.ListPlayersParams {
|
||||
t.Helper()
|
||||
|
||||
params := domain.NewListPlayersParams()
|
||||
require.NoError(t, params.SetSort([]domain.PlayerSort{
|
||||
domain.PlayerSortServerKeyDESC,
|
||||
domain.PlayerSortIDDESC,
|
||||
}))
|
||||
|
||||
res, err := repos.player.List(ctx, params)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(res.Players()), 2)
|
||||
|
||||
require.NoError(t, params.SetCursor(domaintest.NewPlayerCursor(t, func(cfg *domaintest.PlayerCursorConfig) {
|
||||
cfg.ID = res.Players()[1].ID()
|
||||
cfg.ServerKey = res.Players()[1].ServerKey()
|
||||
})))
|
||||
|
||||
return params
|
||||
},
|
||||
assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) {
|
||||
t.Helper()
|
||||
players := res.Players()
|
||||
assert.NotEmpty(t, len(players))
|
||||
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
|
||||
return cmp.Or(
|
||||
cmp.Compare(a.ServerKey(), b.ServerKey()),
|
||||
cmp.Compare(a.ID(), b.ID()),
|
||||
) * -1
|
||||
}))
|
||||
assert.LessOrEqual(t, players[0].ID(), params.Cursor().ID())
|
||||
for _, p := range res.Players() {
|
||||
assert.LessOrEqual(t, p.ServerKey(), params.Cursor().ServerKey())
|
||||
}
|
||||
},
|
||||
assertError: func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
require.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK: limit=2",
|
||||
params: func(t *testing.T) domain.ListPlayersParams {
|
||||
t.Helper()
|
||||
params := domain.NewListPlayersParams()
|
||||
require.NoError(t, params.SetLimit(2))
|
||||
return params
|
||||
},
|
||||
assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) {
|
||||
t.Helper()
|
||||
assert.Len(t, res.Players(), params.Limit())
|
||||
},
|
||||
assertError: func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
require.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -311,7 +392,7 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
|||
|
||||
res, err := repos.player.List(ctx, params)
|
||||
tt.assertError(t, err)
|
||||
tt.assertPlayers(t, params, res)
|
||||
tt.assertResult(t, params, res)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -339,8 +420,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
|||
require.NoError(t, listPlayersParams.SetDeleted(domain.NullBool{Value: false, Valid: true}))
|
||||
require.NoError(t, listPlayersParams.SetServerKeys(serverKeys))
|
||||
|
||||
players, err := repos.player.List(ctx, listPlayersParams)
|
||||
res, err := repos.player.List(ctx, listPlayersParams)
|
||||
require.NoError(t, err)
|
||||
players := res.Players()
|
||||
|
||||
var serverKey string
|
||||
var ids []int
|
||||
|
@ -363,9 +445,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
|||
require.NoError(t, listPlayersParams.SetDeleted(domain.NullBool{Valid: false}))
|
||||
require.NoError(t, listPlayersParams.SetServerKeys(serverKeys))
|
||||
|
||||
players, err = repos.player.List(ctx, listPlayersParams)
|
||||
res, err = repos.player.List(ctx, listPlayersParams)
|
||||
require.NoError(t, err)
|
||||
for _, p := range players {
|
||||
for _, p := range res.Players() {
|
||||
if p.ServerKey() == serverKey && slices.Contains(ids, p.ID()) {
|
||||
if slices.Contains(idsToDelete, p.ID()) {
|
||||
assert.WithinDuration(t, time.Now(), p.DeletedAt(), time.Minute)
|
||||
|
|
|
@ -30,7 +30,7 @@ type tribeRepository interface {
|
|||
|
||||
type playerRepository interface {
|
||||
CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error
|
||||
List(ctx context.Context, params domain.ListPlayersParams) (domain.Players, error)
|
||||
List(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersResult, error)
|
||||
Delete(ctx context.Context, serverKey string, ids ...int) error
|
||||
}
|
||||
|
||||
|
|
|
@ -84,8 +84,9 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
|
|||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
players, err := repos.player.List(ctx, domain.NewListPlayersParams())
|
||||
res, err := repos.player.List(ctx, domain.NewListPlayersParams())
|
||||
require.NoError(t, err)
|
||||
players := res.Players()
|
||||
require.NotEmpty(t, players)
|
||||
player := players[0]
|
||||
|
||||
|
@ -133,7 +134,7 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
|
|||
})
|
||||
})
|
||||
|
||||
t.Run("List & ListCount", func(t *testing.T) {
|
||||
t.Run("List", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
repos := newRepos(t)
|
||||
|
|
|
@ -115,7 +115,7 @@ func testTribeSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repos
|
|||
})
|
||||
})
|
||||
|
||||
t.Run("List & ListCount", func(t *testing.T) {
|
||||
t.Run("List", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
repos := newRepos(t)
|
||||
|
|
|
@ -163,7 +163,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("List & ListCount", func(t *testing.T) {
|
||||
t.Run("List", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
repos := newRepos(t)
|
||||
|
|
|
@ -92,7 +92,7 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
|
|||
})
|
||||
})
|
||||
|
||||
t.Run("List & ListCount", func(t *testing.T) {
|
||||
t.Run("List", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
repos := newRepos(t)
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
type PlayerRepository interface {
|
||||
CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error
|
||||
List(ctx context.Context, params domain.ListPlayersParams) (domain.Players, error)
|
||||
List(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersResult, error)
|
||||
// Delete marks players with the given serverKey and ids as deleted (sets deleted at to now).
|
||||
// In addition, Delete sets TribeID to null.
|
||||
//
|
||||
|
@ -104,10 +104,11 @@ func (svc *PlayerService) createOrUpdateChunk(ctx context.Context, serverKey str
|
|||
return err
|
||||
}
|
||||
|
||||
storedPlayers, err := svc.repo.List(ctx, listParams)
|
||||
res, err := svc.repo.List(ctx, listParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
storedPlayers := res.Players()
|
||||
|
||||
createParams, err := domain.NewCreatePlayerParams(serverKey, players, storedPlayers)
|
||||
if err != nil {
|
||||
|
@ -149,16 +150,12 @@ func (svc *PlayerService) delete(ctx context.Context, serverKey string, players
|
|||
var tribeChangesParams []domain.CreateTribeChangeParams
|
||||
|
||||
for {
|
||||
storedPlayers, err := svc.repo.List(ctx, listParams)
|
||||
res, err := svc.repo.List(ctx, listParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(storedPlayers) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
ids, params, err := storedPlayers.Delete(serverKey, players)
|
||||
ids, params, err := res.Players().Delete(serverKey, players)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -166,10 +163,11 @@ func (svc *PlayerService) delete(ctx context.Context, serverKey string, players
|
|||
toDelete = append(toDelete, ids...)
|
||||
tribeChangesParams = append(tribeChangesParams, params...)
|
||||
|
||||
if err = listParams.SetIDGT(domain.NullInt{
|
||||
Value: storedPlayers[len(storedPlayers)-1].ID(),
|
||||
Valid: true,
|
||||
}); err != nil {
|
||||
if res.Next().IsZero() {
|
||||
break
|
||||
}
|
||||
|
||||
if err = listParams.SetCursor(res.Next()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -181,6 +179,6 @@ func (svc *PlayerService) delete(ctx context.Context, serverKey string, players
|
|||
return svc.tribeChangeSvc.Create(ctx, tribeChangesParams...)
|
||||
}
|
||||
|
||||
func (svc *PlayerService) List(ctx context.Context, params domain.ListPlayersParams) (domain.Players, error) {
|
||||
func (svc *PlayerService) List(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersResult, error) {
|
||||
return svc.repo.List(ctx, params)
|
||||
}
|
||||
|
|
|
@ -53,10 +53,11 @@ func (svc *PlayerSnapshotService) Create(
|
|||
}
|
||||
|
||||
for {
|
||||
players, err := svc.playerSvc.List(ctx, listPlayersParams)
|
||||
res, err := svc.playerSvc.List(ctx, listPlayersParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", serverKey, err)
|
||||
}
|
||||
players := res.Players()
|
||||
|
||||
if len(players) == 0 {
|
||||
break
|
||||
|
@ -71,10 +72,11 @@ func (svc *PlayerSnapshotService) Create(
|
|||
return fmt.Errorf("%s: %w", serverKey, err)
|
||||
}
|
||||
|
||||
if err = listPlayersParams.SetIDGT(domain.NullInt{
|
||||
Value: players[len(players)-1].ID(),
|
||||
Valid: true,
|
||||
}); err != nil {
|
||||
if res.Next().IsZero() {
|
||||
break
|
||||
}
|
||||
|
||||
if err = listPlayersParams.SetCursor(res.Next()); err != nil {
|
||||
return fmt.Errorf("%s: %w", serverKey, err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package domaintest
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||
|
@ -8,6 +9,47 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type PlayerCursorConfig struct {
|
||||
ID int
|
||||
ServerKey string
|
||||
ODScoreAtt int
|
||||
ODScoreDef int
|
||||
ODScoreTotal int
|
||||
Points int
|
||||
DeletedAt time.Time
|
||||
}
|
||||
|
||||
func NewPlayerCursor(tb TestingTB, opts ...func(cfg *PlayerCursorConfig)) domain.PlayerCursor {
|
||||
tb.Helper()
|
||||
|
||||
cfg := &PlayerCursorConfig{
|
||||
ID: RandID(),
|
||||
ServerKey: RandServerKey(),
|
||||
ODScoreAtt: gofakeit.IntRange(0, math.MaxInt),
|
||||
ODScoreDef: gofakeit.IntRange(0, math.MaxInt),
|
||||
ODScoreTotal: gofakeit.IntRange(0, math.MaxInt),
|
||||
Points: gofakeit.IntRange(0, math.MaxInt),
|
||||
DeletedAt: time.Time{},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(cfg)
|
||||
}
|
||||
|
||||
pc, err := domain.NewPlayerCursor(
|
||||
cfg.ID,
|
||||
cfg.ServerKey,
|
||||
cfg.ODScoreAtt,
|
||||
cfg.ODScoreDef,
|
||||
cfg.ODScoreTotal,
|
||||
cfg.Points,
|
||||
cfg.DeletedAt,
|
||||
)
|
||||
require.NoError(tb, err)
|
||||
|
||||
return pc
|
||||
}
|
||||
|
||||
type PlayerConfig struct {
|
||||
ID int
|
||||
Points int
|
||||
|
|
|
@ -373,14 +373,199 @@ const (
|
|||
PlayerSortServerKeyDESC
|
||||
)
|
||||
|
||||
type PlayerCursor struct {
|
||||
id int
|
||||
serverKey string
|
||||
odScoreAtt int
|
||||
odScoreDef int
|
||||
odScoreTotal int
|
||||
points int
|
||||
deletedAt time.Time
|
||||
}
|
||||
|
||||
const playerCursorModelName = "PlayerCursor"
|
||||
|
||||
func NewPlayerCursor(
|
||||
id int,
|
||||
serverKey string,
|
||||
odScoreAtt int,
|
||||
odScoreDef int,
|
||||
odScoreTotal int,
|
||||
points int,
|
||||
deletedAt time.Time,
|
||||
) (PlayerCursor, error) {
|
||||
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
|
||||
return PlayerCursor{}, ValidationError{
|
||||
Model: playerCursorModelName,
|
||||
Field: "id",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateServerKey(serverKey); err != nil {
|
||||
return PlayerCursor{}, ValidationError{
|
||||
Model: playerCursorModelName,
|
||||
Field: "serverKey",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateIntInRange(odScoreAtt, 0, math.MaxInt); err != nil {
|
||||
return PlayerCursor{}, ValidationError{
|
||||
Model: playerCursorModelName,
|
||||
Field: "odScoreAtt",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateIntInRange(odScoreDef, 0, math.MaxInt); err != nil {
|
||||
return PlayerCursor{}, ValidationError{
|
||||
Model: playerCursorModelName,
|
||||
Field: "odScoreDef",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateIntInRange(odScoreTotal, 0, math.MaxInt); err != nil {
|
||||
return PlayerCursor{}, ValidationError{
|
||||
Model: playerCursorModelName,
|
||||
Field: "odScoreTotal",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateIntInRange(points, 0, math.MaxInt); err != nil {
|
||||
return PlayerCursor{}, ValidationError{
|
||||
Model: playerCursorModelName,
|
||||
Field: "points",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return PlayerCursor{
|
||||
id: id,
|
||||
serverKey: serverKey,
|
||||
odScoreAtt: odScoreAtt,
|
||||
odScoreDef: odScoreDef,
|
||||
odScoreTotal: odScoreTotal,
|
||||
points: points,
|
||||
deletedAt: deletedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func decodePlayerCursor(encoded string) (PlayerCursor, error) {
|
||||
m, err := decodeCursor(encoded)
|
||||
if err != nil {
|
||||
return PlayerCursor{}, err
|
||||
}
|
||||
|
||||
id, err := m.int("id")
|
||||
if err != nil {
|
||||
return PlayerCursor{}, ErrInvalidCursor
|
||||
}
|
||||
|
||||
serverKey, err := m.string("serverKey")
|
||||
if err != nil {
|
||||
return PlayerCursor{}, ErrInvalidCursor
|
||||
}
|
||||
|
||||
odScoreAtt, err := m.int("odScoreAtt")
|
||||
if err != nil {
|
||||
return PlayerCursor{}, ErrInvalidCursor
|
||||
}
|
||||
|
||||
odScoreDef, err := m.int("odScoreDef")
|
||||
if err != nil {
|
||||
return PlayerCursor{}, ErrInvalidCursor
|
||||
}
|
||||
|
||||
odScoreTotal, err := m.int("odScoreTotal")
|
||||
if err != nil {
|
||||
return PlayerCursor{}, ErrInvalidCursor
|
||||
}
|
||||
|
||||
points, err := m.int("points")
|
||||
if err != nil {
|
||||
return PlayerCursor{}, ErrInvalidCursor
|
||||
}
|
||||
|
||||
deletedAt, err := m.time("deletedAt")
|
||||
if err != nil {
|
||||
return PlayerCursor{}, ErrInvalidCursor
|
||||
}
|
||||
|
||||
pc, err := NewPlayerCursor(
|
||||
id,
|
||||
serverKey,
|
||||
odScoreAtt,
|
||||
odScoreDef,
|
||||
odScoreTotal,
|
||||
points,
|
||||
deletedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return PlayerCursor{}, ErrInvalidCursor
|
||||
}
|
||||
|
||||
return pc, nil
|
||||
}
|
||||
|
||||
func (pc PlayerCursor) ID() int {
|
||||
return pc.id
|
||||
}
|
||||
|
||||
func (pc PlayerCursor) ServerKey() string {
|
||||
return pc.serverKey
|
||||
}
|
||||
|
||||
func (pc PlayerCursor) ODScoreAtt() int {
|
||||
return pc.odScoreAtt
|
||||
}
|
||||
|
||||
func (pc PlayerCursor) ODScoreDef() int {
|
||||
return pc.odScoreDef
|
||||
}
|
||||
|
||||
func (pc PlayerCursor) ODScoreTotal() int {
|
||||
return pc.odScoreTotal
|
||||
}
|
||||
|
||||
func (pc PlayerCursor) Points() int {
|
||||
return pc.points
|
||||
}
|
||||
|
||||
func (pc PlayerCursor) DeletedAt() time.Time {
|
||||
return pc.deletedAt
|
||||
}
|
||||
|
||||
func (pc PlayerCursor) IsZero() bool {
|
||||
return pc == PlayerCursor{}
|
||||
}
|
||||
|
||||
func (pc PlayerCursor) Encode() string {
|
||||
if pc.IsZero() {
|
||||
return ""
|
||||
}
|
||||
|
||||
return encodeCursor([]keyValuePair{
|
||||
{"id", pc.id},
|
||||
{"serverKey", pc.serverKey},
|
||||
{"odScoreAtt", pc.odScoreAtt},
|
||||
{"odScoreDef", pc.odScoreDef},
|
||||
{"odScoreTotal", pc.odScoreTotal},
|
||||
{"points", pc.points},
|
||||
{"deletedAt", pc.deletedAt},
|
||||
})
|
||||
}
|
||||
|
||||
type ListPlayersParams struct {
|
||||
ids []int
|
||||
idGT NullInt
|
||||
serverKeys []string
|
||||
deleted NullBool
|
||||
sort []PlayerSort
|
||||
cursor PlayerCursor
|
||||
limit int
|
||||
offset int
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -419,26 +604,6 @@ func (params *ListPlayersParams) SetIDs(ids []int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (params *ListPlayersParams) IDGT() NullInt {
|
||||
return params.idGT
|
||||
}
|
||||
|
||||
func (params *ListPlayersParams) SetIDGT(idGT NullInt) error {
|
||||
if idGT.Valid {
|
||||
if err := validateIntInRange(idGT.Value, 0, math.MaxInt); err != nil {
|
||||
return ValidationError{
|
||||
Model: listPlayersParamsModelName,
|
||||
Field: "idGT",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
params.idGT = idGT
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (params *ListPlayersParams) ServerKeys() []string {
|
||||
return params.serverKeys
|
||||
}
|
||||
|
@ -480,6 +645,30 @@ func (params *ListPlayersParams) SetSort(sort []PlayerSort) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (params *ListPlayersParams) Cursor() PlayerCursor {
|
||||
return params.cursor
|
||||
}
|
||||
|
||||
func (params *ListPlayersParams) SetCursor(cursor PlayerCursor) error {
|
||||
params.cursor = cursor
|
||||
return nil
|
||||
}
|
||||
|
||||
func (params *ListPlayersParams) SetEncodedCursor(encoded string) error {
|
||||
decoded, err := decodePlayerCursor(encoded)
|
||||
if err != nil {
|
||||
return ValidationError{
|
||||
Model: listPlayersParamsModelName,
|
||||
Field: "cursor",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
params.cursor = decoded
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (params *ListPlayersParams) Limit() int {
|
||||
return params.limit
|
||||
}
|
||||
|
@ -498,20 +687,71 @@ func (params *ListPlayersParams) SetLimit(limit int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (params *ListPlayersParams) Offset() int {
|
||||
return params.offset
|
||||
type ListPlayersResult struct {
|
||||
players Players
|
||||
self PlayerCursor
|
||||
next PlayerCursor
|
||||
}
|
||||
|
||||
func (params *ListPlayersParams) SetOffset(offset int) error {
|
||||
if err := validateIntInRange(offset, 0, math.MaxInt); err != nil {
|
||||
return ValidationError{
|
||||
Model: listPlayersParamsModelName,
|
||||
Field: "offset",
|
||||
Err: err,
|
||||
const listPlayersResultModelName = "ListPlayersResult"
|
||||
|
||||
func NewListPlayersResult(players Players, next Player) (ListPlayersResult, error) {
|
||||
var err error
|
||||
res := ListPlayersResult{
|
||||
players: players,
|
||||
}
|
||||
|
||||
if len(players) > 0 {
|
||||
od := players[0].OD()
|
||||
res.self, err = NewPlayerCursor(
|
||||
players[0].ID(),
|
||||
players[0].ServerKey(),
|
||||
od.ScoreAtt(),
|
||||
od.ScoreDef(),
|
||||
od.ScoreTotal(),
|
||||
players[0].Points(),
|
||||
players[0].DeletedAt(),
|
||||
)
|
||||
if err != nil {
|
||||
return ListPlayersResult{}, ValidationError{
|
||||
Model: listPlayersResultModelName,
|
||||
Field: "self",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
params.offset = offset
|
||||
if !next.IsZero() {
|
||||
od := next.OD()
|
||||
res.next, err = NewPlayerCursor(
|
||||
next.ID(),
|
||||
next.ServerKey(),
|
||||
od.ScoreAtt(),
|
||||
od.ScoreDef(),
|
||||
od.ScoreTotal(),
|
||||
next.Points(),
|
||||
next.DeletedAt(),
|
||||
)
|
||||
if err != nil {
|
||||
return ListPlayersResult{}, ValidationError{
|
||||
Model: listPlayersResultModelName,
|
||||
Field: "next",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (res ListPlayersResult) Players() Players {
|
||||
return res.players
|
||||
}
|
||||
|
||||
func (res ListPlayersResult) Self() PlayerCursor {
|
||||
return res.self
|
||||
}
|
||||
|
||||
func (res ListPlayersResult) Next() PlayerCursor {
|
||||
return res.next
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -227,6 +228,192 @@ func TestNewCreatePlayerParams(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNewPlayerCursor(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
validPlayerCursor := domaintest.NewPlayerCursor(t)
|
||||
|
||||
type args struct {
|
||||
id int
|
||||
serverKey string
|
||||
odScoreAtt int
|
||||
odScoreDef int
|
||||
odScoreTotal int
|
||||
points int
|
||||
deletedAt time.Time
|
||||
}
|
||||
|
||||
type test struct {
|
||||
name string
|
||||
args args
|
||||
expectedErr error
|
||||
}
|
||||
|
||||
tests := []test{
|
||||
{
|
||||
name: "OK",
|
||||
args: args{
|
||||
id: validPlayerCursor.ID(),
|
||||
serverKey: validPlayerCursor.ServerKey(),
|
||||
odScoreAtt: validPlayerCursor.ODScoreAtt(),
|
||||
odScoreDef: validPlayerCursor.ODScoreDef(),
|
||||
odScoreTotal: validPlayerCursor.ODScoreTotal(),
|
||||
points: validPlayerCursor.Points(),
|
||||
deletedAt: validPlayerCursor.DeletedAt(),
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "ERR: id < 1",
|
||||
args: args{
|
||||
id: 0,
|
||||
serverKey: validPlayerCursor.ServerKey(),
|
||||
odScoreAtt: validPlayerCursor.ODScoreAtt(),
|
||||
odScoreDef: validPlayerCursor.ODScoreDef(),
|
||||
odScoreTotal: validPlayerCursor.ODScoreTotal(),
|
||||
points: validPlayerCursor.Points(),
|
||||
deletedAt: validPlayerCursor.DeletedAt(),
|
||||
},
|
||||
expectedErr: domain.ValidationError{
|
||||
Model: "PlayerCursor",
|
||||
Field: "id",
|
||||
Err: domain.MinGreaterEqualError{
|
||||
Min: 1,
|
||||
Current: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: odScoreAtt < 0",
|
||||
args: args{
|
||||
id: validPlayerCursor.ID(),
|
||||
serverKey: validPlayerCursor.ServerKey(),
|
||||
odScoreAtt: -1,
|
||||
odScoreDef: validPlayerCursor.ODScoreDef(),
|
||||
odScoreTotal: validPlayerCursor.ODScoreTotal(),
|
||||
points: validPlayerCursor.Points(),
|
||||
deletedAt: validPlayerCursor.DeletedAt(),
|
||||
},
|
||||
expectedErr: domain.ValidationError{
|
||||
Model: "PlayerCursor",
|
||||
Field: "odScoreAtt",
|
||||
Err: domain.MinGreaterEqualError{
|
||||
Min: 0,
|
||||
Current: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: odScoreDef < 0",
|
||||
args: args{
|
||||
id: validPlayerCursor.ID(),
|
||||
serverKey: validPlayerCursor.ServerKey(),
|
||||
odScoreAtt: validPlayerCursor.ODScoreAtt(),
|
||||
odScoreDef: -1,
|
||||
odScoreTotal: validPlayerCursor.ODScoreTotal(),
|
||||
points: validPlayerCursor.Points(),
|
||||
deletedAt: validPlayerCursor.DeletedAt(),
|
||||
},
|
||||
expectedErr: domain.ValidationError{
|
||||
Model: "PlayerCursor",
|
||||
Field: "odScoreDef",
|
||||
Err: domain.MinGreaterEqualError{
|
||||
Min: 0,
|
||||
Current: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: odScoreTotal < 0",
|
||||
args: args{
|
||||
id: validPlayerCursor.ID(),
|
||||
serverKey: validPlayerCursor.ServerKey(),
|
||||
odScoreAtt: validPlayerCursor.ODScoreAtt(),
|
||||
odScoreDef: validPlayerCursor.ODScoreDef(),
|
||||
odScoreTotal: -1,
|
||||
points: validPlayerCursor.Points(),
|
||||
deletedAt: validPlayerCursor.DeletedAt(),
|
||||
},
|
||||
expectedErr: domain.ValidationError{
|
||||
Model: "PlayerCursor",
|
||||
Field: "odScoreTotal",
|
||||
Err: domain.MinGreaterEqualError{
|
||||
Min: 0,
|
||||
Current: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: points < 0",
|
||||
args: args{
|
||||
id: validPlayerCursor.ID(),
|
||||
serverKey: validPlayerCursor.ServerKey(),
|
||||
odScoreAtt: validPlayerCursor.ODScoreAtt(),
|
||||
odScoreDef: validPlayerCursor.ODScoreDef(),
|
||||
odScoreTotal: validPlayerCursor.ODScoreTotal(),
|
||||
points: -1,
|
||||
deletedAt: validPlayerCursor.DeletedAt(),
|
||||
},
|
||||
expectedErr: domain.ValidationError{
|
||||
Model: "PlayerCursor",
|
||||
Field: "points",
|
||||
Err: domain.MinGreaterEqualError{
|
||||
Min: 0,
|
||||
Current: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, serverKeyTest := range newServerKeyValidationTests() {
|
||||
tests = append(tests, test{
|
||||
name: serverKeyTest.name,
|
||||
args: args{
|
||||
id: validPlayerCursor.ID(),
|
||||
serverKey: serverKeyTest.key,
|
||||
odScoreAtt: validPlayerCursor.ODScoreAtt(),
|
||||
odScoreDef: validPlayerCursor.ODScoreDef(),
|
||||
odScoreTotal: validPlayerCursor.ODScoreTotal(),
|
||||
points: validPlayerCursor.Points(),
|
||||
deletedAt: validPlayerCursor.DeletedAt(),
|
||||
},
|
||||
expectedErr: domain.ValidationError{
|
||||
Model: "PlayerCursor",
|
||||
Field: "serverKey",
|
||||
Err: serverKeyTest.expectedErr,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tc, err := domain.NewPlayerCursor(
|
||||
tt.args.id,
|
||||
tt.args.serverKey,
|
||||
tt.args.odScoreAtt,
|
||||
tt.args.odScoreDef,
|
||||
tt.args.odScoreTotal,
|
||||
tt.args.points,
|
||||
tt.args.deletedAt,
|
||||
)
|
||||
require.ErrorIs(t, err, tt.expectedErr)
|
||||
if tt.expectedErr != nil {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.args.id, tc.ID())
|
||||
assert.Equal(t, tt.args.serverKey, tc.ServerKey())
|
||||
assert.Equal(t, tt.args.odScoreAtt, tc.ODScoreAtt())
|
||||
assert.Equal(t, tt.args.odScoreDef, tc.ODScoreDef())
|
||||
assert.Equal(t, tt.args.odScoreTotal, tc.ODScoreTotal())
|
||||
assert.Equal(t, tt.args.points, tc.Points())
|
||||
assert.Equal(t, tt.args.deletedAt, tc.DeletedAt())
|
||||
assert.NotEmpty(t, tc.Encode())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPlayersParams_SetIDs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
@ -287,61 +474,6 @@ func TestListPlayersParams_SetIDs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestListPlayersParams_SetIDGT(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type args struct {
|
||||
idGT domain.NullInt
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "OK",
|
||||
args: args{
|
||||
idGT: domain.NullInt{
|
||||
Value: domaintest.RandID(),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: value < 0",
|
||||
args: args{
|
||||
idGT: domain.NullInt{
|
||||
Value: -1,
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
expectedErr: domain.ValidationError{
|
||||
Model: "ListPlayersParams",
|
||||
Field: "idGT",
|
||||
Err: domain.MinGreaterEqualError{
|
||||
Min: 0,
|
||||
Current: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params := domain.NewListPlayersParams()
|
||||
|
||||
require.ErrorIs(t, params.SetIDGT(tt.args.idGT), tt.expectedErr)
|
||||
if tt.expectedErr != nil {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.args.idGT, params.IDGT())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPlayersParams_SetSort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
@ -414,6 +546,87 @@ func TestListPlayersParams_SetSort(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestListPlayersParams_SetEncodedCursor(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
validCursor := domaintest.NewPlayerCursor(t)
|
||||
|
||||
type args struct {
|
||||
cursor string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedCursor domain.PlayerCursor
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "OK",
|
||||
args: args{
|
||||
cursor: validCursor.Encode(),
|
||||
},
|
||||
expectedCursor: validCursor,
|
||||
},
|
||||
{
|
||||
name: "ERR: len(cursor) < 1",
|
||||
args: args{
|
||||
cursor: "",
|
||||
},
|
||||
expectedErr: domain.ValidationError{
|
||||
Model: "ListPlayersParams",
|
||||
Field: "cursor",
|
||||
Err: domain.LenOutOfRangeError{
|
||||
Min: 1,
|
||||
Max: 1000,
|
||||
Current: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: len(cursor) > 1000",
|
||||
args: args{
|
||||
cursor: gofakeit.LetterN(1001),
|
||||
},
|
||||
expectedErr: domain.ValidationError{
|
||||
Model: "ListPlayersParams",
|
||||
Field: "cursor",
|
||||
Err: domain.LenOutOfRangeError{
|
||||
Min: 1,
|
||||
Max: 1000,
|
||||
Current: 1001,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: malformed base64",
|
||||
args: args{
|
||||
cursor: "112345",
|
||||
},
|
||||
expectedErr: domain.ValidationError{
|
||||
Model: "ListPlayersParams",
|
||||
Field: "cursor",
|
||||
Err: domain.ErrInvalidCursor,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params := domain.NewListPlayersParams()
|
||||
|
||||
require.ErrorIs(t, params.SetEncodedCursor(tt.args.cursor), tt.expectedErr)
|
||||
if tt.expectedErr != nil {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.expectedCursor, params.Cursor())
|
||||
assert.Equal(t, tt.args.cursor, params.Cursor().Encode())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPlayersParams_SetLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
@ -477,51 +690,46 @@ func TestListPlayersParams_SetLimit(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestListPlayersParams_SetOffset(t *testing.T) {
|
||||
func TestNewListPlayersResult(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type args struct {
|
||||
offset int
|
||||
players := domain.Players{
|
||||
domaintest.NewPlayer(t),
|
||||
domaintest.NewPlayer(t),
|
||||
domaintest.NewPlayer(t),
|
||||
}
|
||||
next := domaintest.NewPlayer(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "OK",
|
||||
args: args{
|
||||
offset: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: offset < 0",
|
||||
args: args{
|
||||
offset: -1,
|
||||
},
|
||||
expectedErr: domain.ValidationError{
|
||||
Model: "ListPlayersParams",
|
||||
Field: "offset",
|
||||
Err: domain.MinGreaterEqualError{
|
||||
Min: 0,
|
||||
Current: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
t.Run("OK: with next", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
res, err := domain.NewListPlayersResult(players, next)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, players, res.Players())
|
||||
assert.Equal(t, players[0].ID(), res.Self().ID())
|
||||
assert.Equal(t, players[0].ServerKey(), res.Self().ServerKey())
|
||||
assert.Equal(t, next.ID(), res.Next().ID())
|
||||
assert.Equal(t, next.ServerKey(), res.Next().ServerKey())
|
||||
})
|
||||
|
||||
params := domain.NewListPlayersParams()
|
||||
t.Run("OK: without next", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.ErrorIs(t, params.SetOffset(tt.args.offset), tt.expectedErr)
|
||||
if tt.expectedErr != nil {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.args.offset, params.Offset())
|
||||
})
|
||||
}
|
||||
res, err := domain.NewListPlayersResult(players, domain.Player{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, players, res.Players())
|
||||
assert.Equal(t, players[0].ID(), res.Self().ID())
|
||||
assert.Equal(t, players[0].ServerKey(), res.Self().ServerKey())
|
||||
assert.True(t, res.Next().IsZero())
|
||||
})
|
||||
|
||||
t.Run("OK: 0 players", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
res, err := domain.NewListPlayersResult(nil, domain.Player{})
|
||||
require.NoError(t, err)
|
||||
assert.Zero(t, res.Players())
|
||||
assert.True(t, res.Self().IsZero())
|
||||
assert.True(t, res.Next().IsZero())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -489,7 +489,7 @@ func TestNewListServersResult(t *testing.T) {
|
|||
assert.True(t, res.Next().IsZero())
|
||||
})
|
||||
|
||||
t.Run("OK: 0 versions", func(t *testing.T) {
|
||||
t.Run("OK: 0 servers", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
res, err := domain.NewListServersResult(nil, domain.Server{})
|
||||
|
|
|
@ -365,6 +365,12 @@ func TestNewTribeCursor(t *testing.T) {
|
|||
}
|
||||
assert.Equal(t, tt.args.id, tc.ID())
|
||||
assert.Equal(t, tt.args.serverKey, tc.ServerKey())
|
||||
assert.Equal(t, tt.args.odScoreAtt, tc.ODScoreAtt())
|
||||
assert.Equal(t, tt.args.odScoreDef, tc.ODScoreDef())
|
||||
assert.Equal(t, tt.args.odScoreTotal, tc.ODScoreTotal())
|
||||
assert.Equal(t, tt.args.points, tc.Points())
|
||||
assert.InDelta(t, tt.args.dominance, tc.Dominance(), 0.001)
|
||||
assert.Equal(t, tt.args.deletedAt, tc.DeletedAt())
|
||||
assert.NotEmpty(t, tc.Encode())
|
||||
})
|
||||
}
|
||||
|
@ -835,8 +841,7 @@ func TestListTribesParams_SetEncodedCursor(t *testing.T) {
|
|||
if tt.expectedErr != nil {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.expectedCursor.ID(), params.Cursor().ID())
|
||||
assert.Equal(t, tt.expectedCursor.ServerKey(), params.Cursor().ServerKey())
|
||||
assert.Equal(t, tt.expectedCursor, params.Cursor())
|
||||
assert.Equal(t, tt.args.cursor, params.Cursor().Encode())
|
||||
})
|
||||
}
|
||||
|
@ -908,7 +913,7 @@ func TestListTribesParams_SetLimit(t *testing.T) {
|
|||
func TestNewListTribesResult(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
servers := domain.Tribes{
|
||||
tribes := domain.Tribes{
|
||||
domaintest.NewTribe(t),
|
||||
domaintest.NewTribe(t),
|
||||
domaintest.NewTribe(t),
|
||||
|
@ -918,11 +923,11 @@ func TestNewListTribesResult(t *testing.T) {
|
|||
t.Run("OK: with next", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
res, err := domain.NewListTribesResult(servers, next)
|
||||
res, err := domain.NewListTribesResult(tribes, next)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, servers, res.Tribes())
|
||||
assert.Equal(t, servers[0].ID(), res.Self().ID())
|
||||
assert.Equal(t, servers[0].ServerKey(), res.Self().ServerKey())
|
||||
assert.Equal(t, tribes, res.Tribes())
|
||||
assert.Equal(t, tribes[0].ID(), res.Self().ID())
|
||||
assert.Equal(t, tribes[0].ServerKey(), res.Self().ServerKey())
|
||||
assert.Equal(t, next.ID(), res.Next().ID())
|
||||
assert.Equal(t, next.ServerKey(), res.Next().ServerKey())
|
||||
})
|
||||
|
@ -930,15 +935,15 @@ func TestNewListTribesResult(t *testing.T) {
|
|||
t.Run("OK: without next", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
res, err := domain.NewListTribesResult(servers, domain.Tribe{})
|
||||
res, err := domain.NewListTribesResult(tribes, domain.Tribe{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, servers, res.Tribes())
|
||||
assert.Equal(t, servers[0].ID(), res.Self().ID())
|
||||
assert.Equal(t, servers[0].ServerKey(), res.Self().ServerKey())
|
||||
assert.Equal(t, tribes, res.Tribes())
|
||||
assert.Equal(t, tribes[0].ID(), res.Self().ID())
|
||||
assert.Equal(t, tribes[0].ServerKey(), res.Self().ServerKey())
|
||||
assert.True(t, res.Next().IsZero())
|
||||
})
|
||||
|
||||
t.Run("OK: 0 versions", func(t *testing.T) {
|
||||
t.Run("OK: 0 tribes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
res, err := domain.NewListTribesResult(nil, domain.Tribe{})
|
||||
|
|
|
@ -357,19 +357,16 @@ func TestDataSync(t *testing.T) {
|
|||
allPlayers := make(domain.Players, 0, len(expectedPlayers))
|
||||
|
||||
for {
|
||||
players, err := playerRepo.List(ctx, listParams)
|
||||
res, err := playerRepo.List(ctx, listParams)
|
||||
require.NoError(collect, err)
|
||||
|
||||
if len(players) == 0 {
|
||||
allPlayers = append(allPlayers, res.Players()...)
|
||||
|
||||
if res.Next().IsZero() {
|
||||
break
|
||||
}
|
||||
|
||||
allPlayers = append(allPlayers, players...)
|
||||
|
||||
require.NoError(collect, listParams.SetIDGT(domain.NullInt{
|
||||
Value: players[len(players)-1].ID(),
|
||||
Valid: true,
|
||||
}))
|
||||
require.NoError(collect, listParams.SetCursor(res.Next()))
|
||||
}
|
||||
|
||||
if !assert.Len(collect, allPlayers, len(expectedPlayers)) {
|
||||
|
|
|
@ -276,19 +276,16 @@ func TestSnapshotCreation(t *testing.T) {
|
|||
var allPlayers domain.Players
|
||||
|
||||
for {
|
||||
players, err := playerRepo.List(ctx, listPlayersParams)
|
||||
res, err := playerRepo.List(ctx, listPlayersParams)
|
||||
require.NoError(collect, err)
|
||||
|
||||
if len(players) == 0 {
|
||||
allPlayers = append(allPlayers, res.Players()...)
|
||||
|
||||
if res.Next().IsZero() {
|
||||
break
|
||||
}
|
||||
|
||||
allPlayers = append(allPlayers, players...)
|
||||
|
||||
require.NoError(collect, listPlayersParams.SetIDGT(domain.NullInt{
|
||||
Value: players[len(players)-1].ID(),
|
||||
Valid: true,
|
||||
}))
|
||||
require.NoError(collect, listPlayersParams.SetCursor(res.Next()))
|
||||
}
|
||||
|
||||
listSnapshotsParams := domain.NewListPlayerSnapshotsParams()
|
||||
|
|
|
@ -16,6 +16,7 @@ type apiHTTPHandler struct {
|
|||
versionSvc *app.VersionService
|
||||
serverSvc *app.ServerService
|
||||
tribeSvc *app.TribeService
|
||||
playerSvc *app.PlayerService
|
||||
errorRenderer apiErrorRenderer
|
||||
openAPISchema func() (*openapi3.T, error)
|
||||
}
|
||||
|
@ -30,6 +31,7 @@ func NewAPIHTTPHandler(
|
|||
versionSvc *app.VersionService,
|
||||
serverSvc *app.ServerService,
|
||||
tribeSvc *app.TribeService,
|
||||
playerSvc *app.PlayerService,
|
||||
opts ...APIHTTPHandlerOption,
|
||||
) http.Handler {
|
||||
cfg := newAPIHTTPHandlerConfig(opts...)
|
||||
|
@ -38,6 +40,7 @@ func NewAPIHTTPHandler(
|
|||
versionSvc: versionSvc,
|
||||
serverSvc: serverSvc,
|
||||
tribeSvc: tribeSvc,
|
||||
playerSvc: playerSvc,
|
||||
openAPISchema: sync.OnceValues(func() (*openapi3.T, error) {
|
||||
return getOpenAPISchema(cfg.openAPI)
|
||||
}),
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
package port
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
|
||||
)
|
||||
|
||||
func (h *apiHTTPHandler) ListPlayers(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
_ apimodel.VersionCodePathParam,
|
||||
serverKey apimodel.ServerKeyPathParam,
|
||||
params apimodel.ListPlayersParams,
|
||||
) {
|
||||
domainParams := domain.NewListPlayersParams()
|
||||
|
||||
if err := domainParams.SetSort([]domain.PlayerSort{domain.PlayerSortIDASC}); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := domainParams.SetServerKeys([]string{serverKey}); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if params.Deleted != nil {
|
||||
if err := domainParams.SetDeleted(domain.NullBool{
|
||||
Value: *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.List(r.Context(), domainParams)
|
||||
if err != nil {
|
||||
h.errorRenderer.render(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
renderJSON(w, r, http.StatusOK, apimodel.NewListPlayersResponse(res))
|
||||
}
|
||||
|
||||
func formatListPlayersErrorPath(segments []errorPathSegment) []string {
|
||||
if segments[0].model != "ListPlayersParams" {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch segments[0].field {
|
||||
case "cursor":
|
||||
return []string{"$query", "cursor"}
|
||||
case "limit":
|
||||
return []string{"$query", "limit"}
|
||||
case "deleted":
|
||||
return []string{"$query", "deleted"}
|
||||
case "sort":
|
||||
path := []string{"$query", "sort"}
|
||||
if segments[0].index >= 0 {
|
||||
path = append(path, strconv.Itoa(segments[0].index))
|
||||
}
|
||||
return path
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,499 @@
|
|||
package port_test
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
|
||||
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const endpointListPlayers = "/v2/versions/%s/servers/%s/players"
|
||||
|
||||
func TestListPlayers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := newAPIHTTPHandler(t)
|
||||
players := getAllPlayers(t, handler)
|
||||
var server serverWithVersion
|
||||
|
||||
for _, p := range players {
|
||||
if p.DeletedAt != nil {
|
||||
server = p.Server
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.NotZero(t, server)
|
||||
|
||||
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.ListPlayersResponse](t, resp.Body)
|
||||
assert.Zero(t, body.Cursor.Next)
|
||||
assert.NotZero(t, body.Cursor.Self)
|
||||
assert.NotZero(t, body.Data)
|
||||
assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.Player) int {
|
||||
return cmp.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.ListPlayersResponse](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.ListPlayersResponse](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.ListPlayersResponse](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: 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.ListPlayersResponse](t, resp.Body)
|
||||
assert.NotZero(t, body.Data)
|
||||
for _, tr := range body.Data {
|
||||
assert.Nil(t, tr.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.ListPlayersResponse](t, resp.Body)
|
||||
assert.NotZero(t, body.Data)
|
||||
for _, tr := range body.Data {
|
||||
assert.NotNil(t, tr.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: domainErr.Code(),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"current": float64(domainErr.Current),
|
||||
"min": float64(domainErr.Min),
|
||||
},
|
||||
Path: []string{"$query", "limit"},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: limit > 200",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Set("limit", "201")
|
||||
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: 200,
|
||||
Current: limit,
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: domainErr.Code(),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"current": float64(domainErr.Current),
|
||||
"max": float64(domainErr.Max),
|
||||
},
|
||||
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: 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: 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: domainErr.Code(),
|
||||
Message: domainErr.Error(),
|
||||
Path: []string{"$query", "cursor"},
|
||||
},
|
||||
},
|
||||
}, 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(endpointListPlayers, domaintest.RandVersionCode(), server.Key)
|
||||
},
|
||||
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, 7)
|
||||
domainErr := domain.VersionNotFoundError{
|
||||
VersionCode: pathSegments[3],
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: domainErr.Code(),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"code": domainErr.VersionCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
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())
|
||||
},
|
||||
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, 7)
|
||||
domainErr := domain.ServerNotFoundError{
|
||||
Key: pathSegments[5],
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: domainErr.Code(),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"key": domainErr.Key,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := httptest.NewRequest(
|
||||
http.MethodGet,
|
||||
fmt.Sprintf(endpointListPlayers, server.Version.Code, server.Key),
|
||||
nil,
|
||||
)
|
||||
if tt.reqModifier != nil {
|
||||
tt.reqModifier(t, req)
|
||||
}
|
||||
|
||||
resp := doCustomRequest(handler, req)
|
||||
defer resp.Body.Close()
|
||||
tt.assertResp(t, req, resp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type playerWithServer struct {
|
||||
apimodel.Player
|
||||
Server serverWithVersion
|
||||
}
|
||||
|
||||
func getAllPlayers(tb testing.TB, h http.Handler) []playerWithServer {
|
||||
tb.Helper()
|
||||
|
||||
servers := getAllServers(tb, h)
|
||||
|
||||
var players []playerWithServer
|
||||
|
||||
for _, s := range servers {
|
||||
resp := doRequest(h, http.MethodGet, fmt.Sprintf(endpointListPlayers, s.Version.Code, s.Key), nil)
|
||||
require.Equal(tb, http.StatusOK, resp.StatusCode)
|
||||
|
||||
for _, p := range decodeJSON[apimodel.ListPlayersResponse](tb, resp.Body).Data {
|
||||
players = append(players, playerWithServer{
|
||||
Player: p,
|
||||
Server: s,
|
||||
})
|
||||
}
|
||||
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
|
||||
require.NotZero(tb, players)
|
||||
|
||||
return players
|
||||
}
|
|
@ -35,11 +35,13 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h
|
|||
versionRepo := adapter.NewVersionBunRepository(bunDB)
|
||||
serverRepo := adapter.NewServerBunRepository(bunDB)
|
||||
tribeRepo := adapter.NewTribeBunRepository(bunDB)
|
||||
playerRepo := adapter.NewPlayerBunRepository(bunDB)
|
||||
|
||||
return port.NewAPIHTTPHandler(
|
||||
app.NewVersionService(versionRepo),
|
||||
app.NewServerService(serverRepo, nil, nil),
|
||||
app.NewTribeService(tribeRepo, nil, nil),
|
||||
app.NewPlayerService(playerRepo, nil, nil, nil),
|
||||
cfg.options...,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package apimodel
|
||||
|
||||
import (
|
||||
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||
)
|
||||
|
||||
func NewPlayerOpponentsDefeated(od domain.OpponentsDefeated) PlayerOpponentsDefeated {
|
||||
return PlayerOpponentsDefeated{
|
||||
RankAtt: od.RankAtt(),
|
||||
RankDef: od.RankDef(),
|
||||
RankSup: od.RankSup(),
|
||||
RankTotal: od.RankTotal(),
|
||||
ScoreAtt: od.ScoreAtt(),
|
||||
ScoreDef: od.ScoreDef(),
|
||||
ScoreSup: od.ScoreSup(),
|
||||
ScoreTotal: od.ScoreTotal(),
|
||||
}
|
||||
}
|
||||
|
||||
func NewPlayer(p domain.Player) Player {
|
||||
converted := Player{
|
||||
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(),
|
||||
}
|
||||
|
||||
if deletedAt := p.DeletedAt(); !deletedAt.IsZero() {
|
||||
converted.DeletedAt = &deletedAt
|
||||
}
|
||||
|
||||
return converted
|
||||
}
|
||||
|
||||
func NewListPlayersResponse(res domain.ListPlayersResult) ListPlayersResponse {
|
||||
players := res.Players()
|
||||
|
||||
resp := ListPlayersResponse{
|
||||
Data: make([]Player, 0, len(players)),
|
||||
Cursor: Cursor{
|
||||
Next: res.Next().Encode(),
|
||||
Self: res.Self().Encode(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, p := range players {
|
||||
resp.Data = append(resp.Data, NewPlayer(p))
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
Loading…
Reference in New Issue