feat: new endpoint /api/v2/versions/{versionCode}/servers/{serverKey}/players/{playerId}/snapshots (#31)
Reviewed-on: twhelp/corev3#31
This commit is contained in:
parent
387fe30b82
commit
7ba172f7a4
|
@ -463,6 +463,27 @@ paths:
|
|||
$ref: "#/components/responses/ListTribeSnapshotsResponse"
|
||||
default:
|
||||
$ref: "#/components/responses/ErrorResponse"
|
||||
/v2/versions/{versionCode}/servers/{serverKey}/players/{playerId}/snapshots:
|
||||
get:
|
||||
operationId: listPlayerPlayerSnapshots
|
||||
tags:
|
||||
- versions
|
||||
- servers
|
||||
- players
|
||||
- snapshots
|
||||
description: List the given player's snapshots
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/VersionCodePathParam"
|
||||
- $ref: "#/components/parameters/ServerKeyPathParam"
|
||||
- $ref: "#/components/parameters/PlayerIdPathParam"
|
||||
- $ref: "#/components/parameters/CursorQueryParam"
|
||||
- $ref: "#/components/parameters/LimitQueryParam"
|
||||
- $ref: "#/components/parameters/PlayerSnapshotSortQueryParam"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/ListPlayerSnapshotsResponse"
|
||||
default:
|
||||
$ref: "#/components/responses/ErrorResponse"
|
||||
components:
|
||||
schemas:
|
||||
DomainErrorCode:
|
||||
|
@ -1542,6 +1563,32 @@ components:
|
|||
type: integer
|
||||
opponentsDefeated:
|
||||
$ref: "#/components/schemas/TribeOpponentsDefeated"
|
||||
PlayerSnapshot:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- numVillages
|
||||
- points
|
||||
- rank
|
||||
- date
|
||||
- opponentsDefeated
|
||||
- player
|
||||
properties:
|
||||
id:
|
||||
$ref: "#/components/schemas/IntId"
|
||||
numVillages:
|
||||
type: integer
|
||||
points:
|
||||
type: integer
|
||||
rank:
|
||||
type: integer
|
||||
date:
|
||||
type: string
|
||||
format: date
|
||||
opponentsDefeated:
|
||||
$ref: "#/components/schemas/PlayerOpponentsDefeated"
|
||||
player:
|
||||
$ref: "#/components/schemas/PlayerMeta"
|
||||
CursorString:
|
||||
type: string
|
||||
minLength: 1
|
||||
|
@ -1719,6 +1766,20 @@ components:
|
|||
- date:ASC
|
||||
- date:DESC
|
||||
maxItems: 2
|
||||
PlayerSnapshotSortQueryParam:
|
||||
name: sort
|
||||
in: query
|
||||
description: Order matters!
|
||||
schema:
|
||||
type: array
|
||||
default:
|
||||
- date:ASC
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- date:ASC
|
||||
- date:DESC
|
||||
maxItems: 2
|
||||
SinceQueryParam:
|
||||
name: since
|
||||
in: query
|
||||
|
@ -1972,6 +2033,21 @@ components:
|
|||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/TribeSnapshot"
|
||||
ListPlayerSnapshotsResponse:
|
||||
description: ""
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/PaginationResponse"
|
||||
- type: object
|
||||
required:
|
||||
- data
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/PlayerSnapshot"
|
||||
ErrorResponse:
|
||||
description: Default error response.
|
||||
content:
|
||||
|
|
|
@ -114,6 +114,7 @@ var cmdServe = &cli.Command{
|
|||
ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB)
|
||||
tribeChangeRepo := adapter.NewTribeChangeBunRepository(bunDB)
|
||||
tribeSnapshotRepo := adapter.NewTribeSnapshotBunRepository(bunDB)
|
||||
playerSnapshotRepo := adapter.NewPlayerSnapshotBunRepository(bunDB)
|
||||
|
||||
// services
|
||||
versionSvc := app.NewVersionService(versionRepo)
|
||||
|
@ -124,6 +125,7 @@ var cmdServe = &cli.Command{
|
|||
villageSvc := app.NewVillageService(villageRepo, nil, nil)
|
||||
ennoblementSvc := app.NewEnnoblementService(ennoblementRepo, nil, nil)
|
||||
tribeSnapshotSvc := app.NewTribeSnapshotService(tribeSnapshotRepo, tribeSvc, nil)
|
||||
playerSnapshotSvc := app.NewPlayerSnapshotService(playerSnapshotRepo, playerSvc, nil)
|
||||
|
||||
// health
|
||||
h := health.New()
|
||||
|
@ -159,6 +161,7 @@ var cmdServe = &cli.Command{
|
|||
ennoblementSvc,
|
||||
tribeChangeSvc,
|
||||
tribeSnapshotSvc,
|
||||
playerSnapshotSvc,
|
||||
port.WithOpenAPIConfig(oapiCfg),
|
||||
))
|
||||
})
|
||||
|
|
|
@ -11,6 +11,10 @@ type PlayerSnapshotRepository interface {
|
|||
// Create persists player snapshots in a store (e.g. Postgres).
|
||||
// Duplicates are ignored.
|
||||
Create(ctx context.Context, params ...domain.CreatePlayerSnapshotParams) error
|
||||
ListWithRelations(
|
||||
ctx context.Context,
|
||||
params domain.ListPlayerSnapshotsParams,
|
||||
) (domain.ListPlayerSnapshotsWithRelationsResult, error)
|
||||
}
|
||||
|
||||
type PlayerSnapshotService struct {
|
||||
|
@ -92,3 +96,10 @@ func (svc *PlayerSnapshotService) Create(
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *PlayerSnapshotService) ListWithRelations(
|
||||
ctx context.Context,
|
||||
params domain.ListPlayerSnapshotsParams,
|
||||
) (domain.ListPlayerSnapshotsWithRelationsResult, error) {
|
||||
return svc.repo.ListWithRelations(ctx, params)
|
||||
}
|
||||
|
|
|
@ -13,16 +13,17 @@ import (
|
|||
)
|
||||
|
||||
type apiHTTPHandler struct {
|
||||
versionSvc *app.VersionService
|
||||
serverSvc *app.ServerService
|
||||
tribeSvc *app.TribeService
|
||||
playerSvc *app.PlayerService
|
||||
villageSvc *app.VillageService
|
||||
ennoblementSvc *app.EnnoblementService
|
||||
tribeChangeSvc *app.TribeChangeService
|
||||
tribeSnapshotSvc *app.TribeSnapshotService
|
||||
errorRenderer apiErrorRenderer
|
||||
openAPISchema func() (*openapi3.T, error)
|
||||
versionSvc *app.VersionService
|
||||
serverSvc *app.ServerService
|
||||
tribeSvc *app.TribeService
|
||||
playerSvc *app.PlayerService
|
||||
villageSvc *app.VillageService
|
||||
ennoblementSvc *app.EnnoblementService
|
||||
tribeChangeSvc *app.TribeChangeService
|
||||
tribeSnapshotSvc *app.TribeSnapshotService
|
||||
playerSnapshotSvc *app.PlayerSnapshotService
|
||||
errorRenderer apiErrorRenderer
|
||||
openAPISchema func() (*openapi3.T, error)
|
||||
}
|
||||
|
||||
func WithOpenAPIConfig(oapiCfg OpenAPIConfig) APIHTTPHandlerOption {
|
||||
|
@ -40,19 +41,21 @@ func NewAPIHTTPHandler(
|
|||
ennoblementSvc *app.EnnoblementService,
|
||||
tribeChangeSvc *app.TribeChangeService,
|
||||
tribeSnapshotSvc *app.TribeSnapshotService,
|
||||
playerSnapshotSvc *app.PlayerSnapshotService,
|
||||
opts ...APIHTTPHandlerOption,
|
||||
) http.Handler {
|
||||
cfg := newAPIHTTPHandlerConfig(opts...)
|
||||
|
||||
h := &apiHTTPHandler{
|
||||
versionSvc: versionSvc,
|
||||
serverSvc: serverSvc,
|
||||
tribeSvc: tribeSvc,
|
||||
playerSvc: playerSvc,
|
||||
villageSvc: villageSvc,
|
||||
ennoblementSvc: ennoblementSvc,
|
||||
tribeChangeSvc: tribeChangeSvc,
|
||||
tribeSnapshotSvc: tribeSnapshotSvc,
|
||||
versionSvc: versionSvc,
|
||||
serverSvc: serverSvc,
|
||||
tribeSvc: tribeSvc,
|
||||
playerSvc: playerSvc,
|
||||
villageSvc: villageSvc,
|
||||
ennoblementSvc: ennoblementSvc,
|
||||
tribeChangeSvc: tribeChangeSvc,
|
||||
tribeSnapshotSvc: tribeSnapshotSvc,
|
||||
playerSnapshotSvc: playerSnapshotSvc,
|
||||
openAPISchema: sync.OnceValues(func() (*openapi3.T, error) {
|
||||
return getOpenAPISchema(cfg.openAPI)
|
||||
}),
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
package port
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
|
||||
)
|
||||
|
||||
//nolint:gocyclo
|
||||
func (h *apiHTTPHandler) ListPlayerPlayerSnapshots(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
_ apimodel.VersionCodePathParam,
|
||||
serverKey apimodel.ServerKeyPathParam,
|
||||
playerID apimodel.PlayerIdPathParam,
|
||||
params apimodel.ListPlayerPlayerSnapshotsParams,
|
||||
) {
|
||||
domainParams := domain.NewListPlayerSnapshotsParams()
|
||||
|
||||
if err := domainParams.SetSort([]domain.PlayerSnapshotSort{domain.PlayerSnapshotSortIDASC}); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayerSnapshotsErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := domainParams.SetPlayerIDs([]int{playerID}); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayerSnapshotsErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if params.Sort != nil {
|
||||
if err := domainParams.PrependSortString(*params.Sort); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayerSnapshotsErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := domainParams.PrependSortString([]string{domain.PlayerSnapshotSortDateASC.String()}); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayerSnapshotsErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := domainParams.SetServerKeys([]string{serverKey}); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayerSnapshotsErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if params.Limit != nil {
|
||||
if err := domainParams.SetLimit(*params.Limit); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayerSnapshotsErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if params.Cursor != nil {
|
||||
if err := domainParams.SetEncodedCursor(*params.Cursor); err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatListPlayerSnapshotsErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
res, err := h.playerSnapshotSvc.ListWithRelations(r.Context(), domainParams)
|
||||
if err != nil {
|
||||
h.errorRenderer.render(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
renderJSON(w, r, http.StatusOK, apimodel.NewListPlayerSnapshotsResponse(res))
|
||||
}
|
||||
|
||||
func formatListPlayerSnapshotsErrorPath(segments []domain.ErrorPathSegment) []string {
|
||||
if segments[0].Model != "ListPlayerSnapshotsParams" {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch segments[0].Field {
|
||||
case "cursor":
|
||||
return []string{"$query", "cursor"}
|
||||
case "limit":
|
||||
return []string{"$query", "limit"}
|
||||
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,626 @@
|
|||
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/v7"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const endpointListPlayerPlayerSnapshots = "/v2/versions/%s/servers/%s/players/%d/snapshots"
|
||||
|
||||
func TestListPlayerPlayerSnapshots(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := newAPIHTTPHandler(t)
|
||||
pss := getAllPlayerSnapshots(t, handler)
|
||||
var server serverWithVersion
|
||||
var player apimodel.PlayerMeta
|
||||
|
||||
pssGroupedByPlayerIDAndServerKey := make(map[string][]playerSnapshotWithServer)
|
||||
for _, ps := range pss {
|
||||
key := fmt.Sprintf("%d-%s", ps.Player.Id, ps.Server.Key)
|
||||
pssGroupedByPlayerIDAndServerKey[key] = append(pssGroupedByPlayerIDAndServerKey[key], ps)
|
||||
}
|
||||
currentMax := -1
|
||||
for _, grouped := range pssGroupedByPlayerIDAndServerKey {
|
||||
if l := len(grouped); l > currentMax && l > 0 {
|
||||
currentMax = l
|
||||
server = grouped[0].Server
|
||||
player = grouped[0].Player
|
||||
}
|
||||
}
|
||||
|
||||
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.ListPlayerSnapshotsResponse](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.PlayerSnapshot) int {
|
||||
return cmp.Or(
|
||||
a.Date.Compare(b.Date.Time),
|
||||
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.ListPlayerSnapshotsResponse](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.ListPlayerSnapshotsResponse](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.ListPlayerSnapshotsResponse](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=[date:DESC]",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Set("sort", "date: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.ListPlayerSnapshotsResponse](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.PlayerSnapshot) int {
|
||||
return cmp.Or(
|
||||
a.Date.Compare(b.Date.Time)*-1,
|
||||
cmp.Compare(a.Id, b.Id),
|
||||
)
|
||||
}))
|
||||
},
|
||||
},
|
||||
{
|
||||
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.PlayerSnapshotListMaxLimit),
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Set("limit", strconv.Itoa(domain.PlayerSnapshotListMaxLimit+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.PlayerSnapshotListMaxLimit,
|
||||
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", "date:DESC")
|
||||
q.Add("sort", "date:ASC")
|
||||
q.Add("sort", "date: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: 1,
|
||||
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", "date:")
|
||||
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"][0],
|
||||
}
|
||||
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", "0"},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: sort conflict",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
q := req.URL.Query()
|
||||
q.Add("sort", "date:DESC")
|
||||
q.Add("sort", "date: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)
|
||||
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: version not found",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
req.URL.Path = fmt.Sprintf(
|
||||
endpointListPlayerPlayerSnapshots,
|
||||
randInvalidVersionCode(t, handler),
|
||||
server.Key,
|
||||
player.Id,
|
||||
)
|
||||
},
|
||||
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, 9)
|
||||
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)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: server not found",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
req.URL.Path = fmt.Sprintf(
|
||||
endpointListPlayerPlayerSnapshots,
|
||||
server.Version.Code,
|
||||
domaintest.RandServerKey(),
|
||||
player.Id,
|
||||
)
|
||||
},
|
||||
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, 9)
|
||||
domainErr := domain.ServerNotFoundError{
|
||||
Key: pathSegments[5],
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: apimodel.ErrorCode(domainErr.Code()),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"key": domainErr.Key,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: player not found",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
req.URL.Path = fmt.Sprintf(
|
||||
endpointListPlayerPlayerSnapshots,
|
||||
server.Version.Code,
|
||||
server.Key,
|
||||
domaintest.RandID(),
|
||||
)
|
||||
},
|
||||
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, 9)
|
||||
id, err := strconv.Atoi(pathSegments[7])
|
||||
require.NoError(t, err)
|
||||
domainErr := domain.PlayerNotFoundError{
|
||||
ID: id,
|
||||
ServerKey: pathSegments[5],
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: apimodel.ErrorCode(domainErr.Code()),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"id": float64(domainErr.ID),
|
||||
"serverKey": domainErr.ServerKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := httptest.NewRequest(
|
||||
http.MethodGet,
|
||||
fmt.Sprintf(endpointListPlayerPlayerSnapshots, server.Version.Code, server.Key, player.Id),
|
||||
nil,
|
||||
)
|
||||
if tt.reqModifier != nil {
|
||||
tt.reqModifier(t, req)
|
||||
}
|
||||
|
||||
resp := doCustomRequest(handler, req)
|
||||
defer resp.Body.Close()
|
||||
tt.assertResp(t, req, resp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type playerSnapshotWithServer struct {
|
||||
apimodel.PlayerSnapshot
|
||||
Server serverWithVersion
|
||||
}
|
||||
|
||||
func getAllPlayerSnapshots(tb testing.TB, h http.Handler) []playerSnapshotWithServer {
|
||||
tb.Helper()
|
||||
|
||||
players := getAllPlayers(tb, h)
|
||||
|
||||
var pss []playerSnapshotWithServer
|
||||
|
||||
for _, p := range players {
|
||||
resp := doRequest(h, http.MethodGet, fmt.Sprintf(
|
||||
endpointListPlayerPlayerSnapshots,
|
||||
p.Server.Version.Code,
|
||||
p.Server.Key,
|
||||
p.Id,
|
||||
), nil)
|
||||
require.Equal(tb, http.StatusOK, resp.StatusCode)
|
||||
|
||||
for _, ps := range decodeJSON[apimodel.ListPlayerSnapshotsResponse](tb, resp.Body).Data {
|
||||
pss = append(pss, playerSnapshotWithServer{
|
||||
PlayerSnapshot: ps,
|
||||
Server: p.Server,
|
||||
})
|
||||
}
|
||||
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
|
||||
require.NotZero(tb, pss)
|
||||
|
||||
return pss
|
||||
}
|
|
@ -41,6 +41,7 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h
|
|||
ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB)
|
||||
tribeChangeRepo := adapter.NewTribeChangeBunRepository(bunDB)
|
||||
tribeSnapshotRepo := adapter.NewTribeSnapshotBunRepository(bunDB)
|
||||
playerSnapshotRepo := adapter.NewPlayerSnapshotBunRepository(bunDB)
|
||||
|
||||
return port.NewAPIHTTPHandler(
|
||||
app.NewVersionService(versionRepo),
|
||||
|
@ -51,6 +52,7 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h
|
|||
app.NewEnnoblementService(ennoblementRepo, nil, nil),
|
||||
app.NewTribeChangeService(tribeChangeRepo),
|
||||
app.NewTribeSnapshotService(tribeSnapshotRepo, nil, nil),
|
||||
app.NewPlayerSnapshotService(playerSnapshotRepo, nil, nil),
|
||||
cfg.options...,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package apimodel
|
||||
|
||||
import (
|
||||
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||
oapitypes "github.com/oapi-codegen/runtime/types"
|
||||
)
|
||||
|
||||
func NewPlayerSnapshot(withRelations domain.PlayerSnapshotWithRelations) PlayerSnapshot {
|
||||
ps := withRelations.PlayerSnapshot()
|
||||
return PlayerSnapshot{
|
||||
Date: oapitypes.Date{Time: ps.Date()},
|
||||
Id: ps.ID(),
|
||||
NumVillages: ps.NumVillages(),
|
||||
OpponentsDefeated: NewPlayerOpponentsDefeated(ps.OD()),
|
||||
Player: NewPlayerMeta(withRelations.Player().WithRelations(withRelations.Tribe())),
|
||||
Points: ps.Points(),
|
||||
Rank: ps.Rank(),
|
||||
}
|
||||
}
|
||||
|
||||
func NewListPlayerSnapshotsResponse(res domain.ListPlayerSnapshotsWithRelationsResult) ListPlayerSnapshotsResponse {
|
||||
pss := res.PlayerSnapshots()
|
||||
|
||||
resp := ListPlayerSnapshotsResponse{
|
||||
Data: make([]PlayerSnapshot, 0, len(pss)),
|
||||
Cursor: Cursor{
|
||||
Next: res.Next().Encode(),
|
||||
Self: res.Self().Encode(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, ps := range pss {
|
||||
resp.Data = append(resp.Data, NewPlayerSnapshot(ps))
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
|
@ -6,34 +6,34 @@ import (
|
|||
)
|
||||
|
||||
func NewTribeSnapshot(withRelations domain.TribeSnapshotWithRelations) TribeSnapshot {
|
||||
tc := withRelations.TribeSnapshot()
|
||||
ts := withRelations.TribeSnapshot()
|
||||
return TribeSnapshot{
|
||||
AllPoints: tc.AllPoints(),
|
||||
Date: oapitypes.Date{Time: tc.Date()},
|
||||
Dominance: tc.Dominance(),
|
||||
Id: tc.ID(),
|
||||
NumMembers: tc.NumMembers(),
|
||||
NumVillages: tc.NumVillages(),
|
||||
OpponentsDefeated: NewTribeOpponentsDefeated(tc.OD()),
|
||||
Points: tc.Points(),
|
||||
Rank: tc.Rank(),
|
||||
AllPoints: ts.AllPoints(),
|
||||
Date: oapitypes.Date{Time: ts.Date()},
|
||||
Dominance: ts.Dominance(),
|
||||
Id: ts.ID(),
|
||||
NumMembers: ts.NumMembers(),
|
||||
NumVillages: ts.NumVillages(),
|
||||
OpponentsDefeated: NewTribeOpponentsDefeated(ts.OD()),
|
||||
Points: ts.Points(),
|
||||
Rank: ts.Rank(),
|
||||
Tribe: NewTribeMeta(withRelations.Tribe()),
|
||||
}
|
||||
}
|
||||
|
||||
func NewListTribeSnapshotsResponse(res domain.ListTribeSnapshotsWithRelationsResult) ListTribeSnapshotsResponse {
|
||||
tcs := res.TribeSnapshots()
|
||||
tss := res.TribeSnapshots()
|
||||
|
||||
resp := ListTribeSnapshotsResponse{
|
||||
Data: make([]TribeSnapshot, 0, len(tcs)),
|
||||
Data: make([]TribeSnapshot, 0, len(tss)),
|
||||
Cursor: Cursor{
|
||||
Next: res.Next().Encode(),
|
||||
Self: res.Self().Encode(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
resp.Data = append(resp.Data, NewTribeSnapshot(tc))
|
||||
for _, ts := range tss {
|
||||
resp.Data = append(resp.Data, NewTribeSnapshot(ts))
|
||||
}
|
||||
|
||||
return resp
|
||||
|
|
Loading…
Reference in New Issue