feat: new API endpoint - GET /api/v2/versions/{versionCode}/servers/{serverKey}/ennoblements (#22)
Reviewed-on: twhelp/corev3#22
This commit is contained in:
parent
01ee81ab6e
commit
b2fa484902
|
@ -15,6 +15,7 @@ tags:
|
||||||
- name: tribes
|
- name: tribes
|
||||||
- name: players
|
- name: players
|
||||||
- name: villages
|
- name: villages
|
||||||
|
- name: ennoblements
|
||||||
servers:
|
servers:
|
||||||
- url: "{scheme}://{hostname}/api"
|
- url: "{scheme}://{hostname}/api"
|
||||||
variables:
|
variables:
|
||||||
|
@ -303,6 +304,24 @@ paths:
|
||||||
$ref: "#/components/responses/ListVillagesResponse"
|
$ref: "#/components/responses/ListVillagesResponse"
|
||||||
default:
|
default:
|
||||||
$ref: "#/components/responses/ErrorResponse"
|
$ref: "#/components/responses/ErrorResponse"
|
||||||
|
/v2/versions/{versionCode}/servers/{serverKey}/ennoblements:
|
||||||
|
get:
|
||||||
|
operationId: listEnnoblements
|
||||||
|
tags:
|
||||||
|
- versions
|
||||||
|
- servers
|
||||||
|
- ennoblements
|
||||||
|
description: List ennoblements
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/VersionCodePathParam"
|
||||||
|
- $ref: "#/components/parameters/ServerKeyPathParam"
|
||||||
|
- $ref: "#/components/parameters/CursorQueryParam"
|
||||||
|
- $ref: "#/components/parameters/LimitQueryParam"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
$ref: "#/components/responses/ListEnnoblementsResponse"
|
||||||
|
default:
|
||||||
|
$ref: "#/components/responses/ErrorResponse"
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
Error:
|
Error:
|
||||||
|
@ -1225,6 +1244,54 @@ components:
|
||||||
format: date-time
|
format: date-time
|
||||||
player:
|
player:
|
||||||
$ref: "#/components/schemas/PlayerMeta"
|
$ref: "#/components/schemas/PlayerMeta"
|
||||||
|
VillageMeta:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- fullName
|
||||||
|
- x
|
||||||
|
- y
|
||||||
|
- profileUrl
|
||||||
|
- continent
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
$ref: "#/components/schemas/IntId"
|
||||||
|
fullName:
|
||||||
|
type: string
|
||||||
|
example: Village (450|450) K44
|
||||||
|
x:
|
||||||
|
type: integer
|
||||||
|
example: 450
|
||||||
|
y:
|
||||||
|
type: integer
|
||||||
|
example: 450
|
||||||
|
profileUrl:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
continent:
|
||||||
|
type: string
|
||||||
|
example: K44
|
||||||
|
player:
|
||||||
|
$ref: "#/components/schemas/PlayerMeta"
|
||||||
|
Ennoblement:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- points
|
||||||
|
- village
|
||||||
|
- createdAt
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
$ref: "#/components/schemas/IntId"
|
||||||
|
points:
|
||||||
|
type: integer
|
||||||
|
newOwner:
|
||||||
|
$ref: "#/components/schemas/PlayerMeta"
|
||||||
|
village:
|
||||||
|
$ref: "#/components/schemas/VillageMeta"
|
||||||
|
createdAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
Cursor:
|
Cursor:
|
||||||
type: object
|
type: object
|
||||||
x-go-type-skip-optional-pointer: true
|
x-go-type-skip-optional-pointer: true
|
||||||
|
@ -1551,6 +1618,21 @@ components:
|
||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
$ref: "#/components/schemas/Village"
|
$ref: "#/components/schemas/Village"
|
||||||
|
ListEnnoblementsResponse:
|
||||||
|
description: ""
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/PaginationResponse"
|
||||||
|
- type: object
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Ennoblement"
|
||||||
ErrorResponse:
|
ErrorResponse:
|
||||||
description: Default error response.
|
description: Default error response.
|
||||||
content:
|
content:
|
||||||
|
|
|
@ -109,6 +109,7 @@ var cmdServe = &cli.Command{
|
||||||
tribeRepo := adapter.NewTribeBunRepository(bunDB)
|
tribeRepo := adapter.NewTribeBunRepository(bunDB)
|
||||||
playerRepo := adapter.NewPlayerBunRepository(bunDB)
|
playerRepo := adapter.NewPlayerBunRepository(bunDB)
|
||||||
villageRepo := adapter.NewVillageBunRepository(bunDB)
|
villageRepo := adapter.NewVillageBunRepository(bunDB)
|
||||||
|
ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB)
|
||||||
|
|
||||||
// services
|
// services
|
||||||
versionSvc := app.NewVersionService(versionRepo)
|
versionSvc := app.NewVersionService(versionRepo)
|
||||||
|
@ -116,6 +117,7 @@ var cmdServe = &cli.Command{
|
||||||
tribeSvc := app.NewTribeService(tribeRepo, nil, nil)
|
tribeSvc := app.NewTribeService(tribeRepo, nil, nil)
|
||||||
playerSvc := app.NewPlayerService(playerRepo, nil, nil, nil)
|
playerSvc := app.NewPlayerService(playerRepo, nil, nil, nil)
|
||||||
villageSvc := app.NewVillageService(villageRepo, nil, nil)
|
villageSvc := app.NewVillageService(villageRepo, nil, nil)
|
||||||
|
ennoblementSvc := app.NewEnnoblementService(ennoblementRepo, nil, nil)
|
||||||
|
|
||||||
// health
|
// health
|
||||||
h := health.New()
|
h := health.New()
|
||||||
|
@ -144,6 +146,7 @@ var cmdServe = &cli.Command{
|
||||||
tribeSvc,
|
tribeSvc,
|
||||||
playerSvc,
|
playerSvc,
|
||||||
villageSvc,
|
villageSvc,
|
||||||
|
ennoblementSvc,
|
||||||
port.WithOpenAPIConfig(oapiCfg),
|
port.WithOpenAPIConfig(oapiCfg),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
|
@ -353,6 +353,16 @@ func (p PlayerMeta) WithRelations(tribe NullTribeMeta) PlayerMetaWithRelations {
|
||||||
|
|
||||||
type NullPlayerMeta NullValue[PlayerMeta]
|
type NullPlayerMeta NullValue[PlayerMeta]
|
||||||
|
|
||||||
|
func (p NullPlayerMeta) WithRelations(tribe NullTribeMeta) NullPlayerMetaWithRelations {
|
||||||
|
return NullPlayerMetaWithRelations{
|
||||||
|
V: PlayerMetaWithRelations{
|
||||||
|
player: p.V,
|
||||||
|
tribe: tribe,
|
||||||
|
},
|
||||||
|
Valid: !p.IsZero(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p NullPlayerMeta) IsZero() bool {
|
func (p NullPlayerMeta) IsZero() bool {
|
||||||
return !p.Valid
|
return !p.Valid
|
||||||
}
|
}
|
||||||
|
|
|
@ -313,6 +313,26 @@ func (v VillageMeta) IsZero() bool {
|
||||||
return v == VillageMeta{}
|
return v == VillageMeta{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v VillageMeta) WithRelations(player NullPlayerMetaWithRelations) VillageMetaWithRelations {
|
||||||
|
return VillageMetaWithRelations{
|
||||||
|
village: v,
|
||||||
|
player: player,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type VillageMetaWithRelations struct {
|
||||||
|
village VillageMeta
|
||||||
|
player NullPlayerMetaWithRelations
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v VillageMetaWithRelations) Village() VillageMeta {
|
||||||
|
return v.village
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v VillageMetaWithRelations) Player() NullPlayerMetaWithRelations {
|
||||||
|
return v.player
|
||||||
|
}
|
||||||
|
|
||||||
type CreateVillageParams struct {
|
type CreateVillageParams struct {
|
||||||
base BaseVillage
|
base BaseVillage
|
||||||
serverKey string
|
serverKey string
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package health
|
package health
|
||||||
|
|
||||||
|
import "runtime"
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
liveChecks []Checker
|
liveChecks []Checker
|
||||||
readyChecks []Checker
|
readyChecks []Checker
|
||||||
|
@ -12,7 +14,7 @@ const maxConcurrentDefault = 5
|
||||||
|
|
||||||
func newConfig(opts ...Option) *config {
|
func newConfig(opts ...Option) *config {
|
||||||
cfg := &config{
|
cfg := &config{
|
||||||
maxConcurrent: maxConcurrentDefault,
|
maxConcurrent: min(runtime.NumCPU(), maxConcurrentDefault),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
|
|
@ -39,7 +39,15 @@ type response struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
result := h.check(r.Context())
|
ctx := r.Context()
|
||||||
|
|
||||||
|
result := h.check(ctx)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
respChecks := make(map[string][]singleCheckResult, len(result.Checks))
|
respChecks := make(map[string][]singleCheckResult, len(result.Checks))
|
||||||
for name, checks := range result.Checks {
|
for name, checks := range result.Checks {
|
||||||
|
|
|
@ -13,13 +13,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type apiHTTPHandler struct {
|
type apiHTTPHandler struct {
|
||||||
versionSvc *app.VersionService
|
versionSvc *app.VersionService
|
||||||
serverSvc *app.ServerService
|
serverSvc *app.ServerService
|
||||||
tribeSvc *app.TribeService
|
tribeSvc *app.TribeService
|
||||||
playerSvc *app.PlayerService
|
playerSvc *app.PlayerService
|
||||||
villageSvc *app.VillageService
|
villageSvc *app.VillageService
|
||||||
errorRenderer apiErrorRenderer
|
ennoblementSvc *app.EnnoblementService
|
||||||
openAPISchema func() (*openapi3.T, error)
|
errorRenderer apiErrorRenderer
|
||||||
|
openAPISchema func() (*openapi3.T, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithOpenAPIConfig(oapiCfg OpenAPIConfig) APIHTTPHandlerOption {
|
func WithOpenAPIConfig(oapiCfg OpenAPIConfig) APIHTTPHandlerOption {
|
||||||
|
@ -34,16 +35,18 @@ func NewAPIHTTPHandler(
|
||||||
tribeSvc *app.TribeService,
|
tribeSvc *app.TribeService,
|
||||||
playerSvc *app.PlayerService,
|
playerSvc *app.PlayerService,
|
||||||
villageSvc *app.VillageService,
|
villageSvc *app.VillageService,
|
||||||
|
ennoblementSvc *app.EnnoblementService,
|
||||||
opts ...APIHTTPHandlerOption,
|
opts ...APIHTTPHandlerOption,
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
cfg := newAPIHTTPHandlerConfig(opts...)
|
cfg := newAPIHTTPHandlerConfig(opts...)
|
||||||
|
|
||||||
h := &apiHTTPHandler{
|
h := &apiHTTPHandler{
|
||||||
versionSvc: versionSvc,
|
versionSvc: versionSvc,
|
||||||
serverSvc: serverSvc,
|
serverSvc: serverSvc,
|
||||||
tribeSvc: tribeSvc,
|
tribeSvc: tribeSvc,
|
||||||
playerSvc: playerSvc,
|
playerSvc: playerSvc,
|
||||||
villageSvc: villageSvc,
|
villageSvc: villageSvc,
|
||||||
|
ennoblementSvc: ennoblementSvc,
|
||||||
openAPISchema: sync.OnceValues(func() (*openapi3.T, error) {
|
openAPISchema: sync.OnceValues(func() (*openapi3.T, error) {
|
||||||
return getOpenAPISchema(cfg.openAPI)
|
return getOpenAPISchema(cfg.openAPI)
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
package port
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *apiHTTPHandler) ListEnnoblements(
|
||||||
|
w http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
_ apimodel.VersionCodePathParam,
|
||||||
|
serverKey apimodel.ServerKeyPathParam,
|
||||||
|
params apimodel.ListEnnoblementsParams,
|
||||||
|
) {
|
||||||
|
domainParams := domain.NewListEnnoblementsParams()
|
||||||
|
|
||||||
|
if err := domainParams.SetSort([]domain.EnnoblementSort{domain.EnnoblementSortIDASC}); err != nil {
|
||||||
|
h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := domainParams.SetServerKeys([]string{serverKey}); err != nil {
|
||||||
|
h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Limit != nil {
|
||||||
|
if err := domainParams.SetLimit(*params.Limit); err != nil {
|
||||||
|
h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Cursor != nil {
|
||||||
|
if err := domainParams.SetEncodedCursor(*params.Cursor); err != nil {
|
||||||
|
h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.ennoblementSvc.ListWithRelations(r.Context(), domainParams)
|
||||||
|
if err != nil {
|
||||||
|
h.errorRenderer.render(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderJSON(w, r, http.StatusOK, apimodel.NewListEnnoblementsResponse(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatListEnnoblementsErrorPath(segments []domain.ErrorPathSegment) []string {
|
||||||
|
if segments[0].Model != "ListEnnoblementsParams" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch segments[0].Field {
|
||||||
|
case "cursor":
|
||||||
|
return []string{"$query", "cursor"}
|
||||||
|
case "limit":
|
||||||
|
return []string{"$query", "limit"}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,419 @@
|
||||||
|
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 endpointListEnnoblements = "/v2/versions/%s/servers/%s/ennoblements"
|
||||||
|
|
||||||
|
func TestListEnnoblements(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
handler := newAPIHTTPHandler(t)
|
||||||
|
ennoblements := getAllEnnoblements(t, handler)
|
||||||
|
server := ennoblements[0].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.ListEnnoblementsResponse](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.Ennoblement) 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.ListEnnoblementsResponse](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.ListEnnoblementsResponse](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.ListEnnoblementsResponse](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: "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: version not found",
|
||||||
|
reqModifier: func(t *testing.T, req *http.Request) {
|
||||||
|
t.Helper()
|
||||||
|
req.URL.Path = fmt.Sprintf(endpointListEnnoblements, 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(endpointListEnnoblements, 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(endpointListEnnoblements, 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 ennoblementWithServer struct {
|
||||||
|
apimodel.Ennoblement
|
||||||
|
Server serverWithVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllEnnoblements(tb testing.TB, h http.Handler) []ennoblementWithServer {
|
||||||
|
tb.Helper()
|
||||||
|
|
||||||
|
servers := getAllServers(tb, h)
|
||||||
|
|
||||||
|
var ennoblements []ennoblementWithServer
|
||||||
|
|
||||||
|
for _, s := range servers {
|
||||||
|
resp := doRequest(h, http.MethodGet, fmt.Sprintf(endpointListEnnoblements, s.Version.Code, s.Key), nil)
|
||||||
|
require.Equal(tb, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
for _, e := range decodeJSON[apimodel.ListEnnoblementsResponse](tb, resp.Body).Data {
|
||||||
|
ennoblements = append(ennoblements, ennoblementWithServer{
|
||||||
|
Ennoblement: e,
|
||||||
|
Server: s,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotZero(tb, ennoblements)
|
||||||
|
|
||||||
|
return ennoblements
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h
|
||||||
tribeRepo := adapter.NewTribeBunRepository(bunDB)
|
tribeRepo := adapter.NewTribeBunRepository(bunDB)
|
||||||
playerRepo := adapter.NewPlayerBunRepository(bunDB)
|
playerRepo := adapter.NewPlayerBunRepository(bunDB)
|
||||||
villageRepo := adapter.NewVillageBunRepository(bunDB)
|
villageRepo := adapter.NewVillageBunRepository(bunDB)
|
||||||
|
ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB)
|
||||||
|
|
||||||
return port.NewAPIHTTPHandler(
|
return port.NewAPIHTTPHandler(
|
||||||
app.NewVersionService(versionRepo),
|
app.NewVersionService(versionRepo),
|
||||||
|
@ -45,6 +46,7 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h
|
||||||
app.NewTribeService(tribeRepo, nil, nil),
|
app.NewTribeService(tribeRepo, nil, nil),
|
||||||
app.NewPlayerService(playerRepo, nil, nil, nil),
|
app.NewPlayerService(playerRepo, nil, nil, nil),
|
||||||
app.NewVillageService(villageRepo, nil, nil),
|
app.NewVillageService(villageRepo, nil, nil),
|
||||||
|
app.NewEnnoblementService(ennoblementRepo, nil, nil),
|
||||||
cfg.options...,
|
cfg.options...,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,22 @@ package port
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.dwysokinski.me/twhelp/corev3/internal/health"
|
"gitea.dwysokinski.me/twhelp/corev3/internal/health"
|
||||||
"gitea.dwysokinski.me/twhelp/corev3/internal/health/healthhttp"
|
"gitea.dwysokinski.me/twhelp/corev3/internal/health/healthhttp"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const metaHandlerTimeout = 5 * time.Second
|
||||||
|
|
||||||
func NewMetaHTTPHandler(h *health.Health) http.Handler {
|
func NewMetaHTTPHandler(h *health.Health) http.Handler {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
r.Use(func(next http.Handler) http.Handler {
|
||||||
|
return http.TimeoutHandler(next, metaHandlerTimeout, "Timeout")
|
||||||
|
})
|
||||||
|
|
||||||
r.Get("/livez", healthhttp.LiveHandler(h).ServeHTTP)
|
r.Get("/livez", healthhttp.LiveHandler(h).ServeHTTP)
|
||||||
r.Get("/readyz", healthhttp.ReadyHandler(h).ServeHTTP)
|
r.Get("/readyz", healthhttp.ReadyHandler(h).ServeHTTP)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
package apimodel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewEnnoblement(withRelations domain.EnnoblementWithRelations) Ennoblement {
|
||||||
|
e := withRelations.Ennoblement()
|
||||||
|
return Ennoblement{
|
||||||
|
CreatedAt: e.CreatedAt(),
|
||||||
|
Id: e.ID(),
|
||||||
|
Points: e.Points(),
|
||||||
|
Village: NewVillageMeta(
|
||||||
|
withRelations.Village().WithRelations(withRelations.OldOwner().WithRelations(withRelations.OldTribe())),
|
||||||
|
),
|
||||||
|
NewOwner: NewNullPlayerMeta(withRelations.NewOwner().WithRelations(withRelations.NewTribe())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListEnnoblementsResponse(res domain.ListEnnoblementsWithRelationsResult) ListEnnoblementsResponse {
|
||||||
|
ennoblements := res.Ennoblements()
|
||||||
|
|
||||||
|
resp := ListEnnoblementsResponse{
|
||||||
|
Data: make([]Ennoblement, 0, len(ennoblements)),
|
||||||
|
Cursor: Cursor{
|
||||||
|
Next: res.Next().Encode(),
|
||||||
|
Self: res.Self().Encode(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range ennoblements {
|
||||||
|
resp.Data = append(resp.Data, NewEnnoblement(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
|
@ -21,18 +21,31 @@ func NewVillage(withRelations domain.VillageWithRelations) Village {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewVillageMeta(withRelations domain.VillageMetaWithRelations) VillageMeta {
|
||||||
|
v := withRelations.Village()
|
||||||
|
return VillageMeta{
|
||||||
|
Continent: v.Continent(),
|
||||||
|
FullName: v.FullName(),
|
||||||
|
Id: v.ID(),
|
||||||
|
Player: NewNullPlayerMeta(withRelations.Player()),
|
||||||
|
ProfileUrl: v.ProfileURL().String(),
|
||||||
|
X: v.X(),
|
||||||
|
Y: v.Y(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewListVillagesResponse(res domain.ListVillagesWithRelationsResult) ListVillagesResponse {
|
func NewListVillagesResponse(res domain.ListVillagesWithRelationsResult) ListVillagesResponse {
|
||||||
versions := res.Villages()
|
villages := res.Villages()
|
||||||
|
|
||||||
resp := ListVillagesResponse{
|
resp := ListVillagesResponse{
|
||||||
Data: make([]Village, 0, len(versions)),
|
Data: make([]Village, 0, len(villages)),
|
||||||
Cursor: Cursor{
|
Cursor: Cursor{
|
||||||
Next: res.Next().Encode(),
|
Next: res.Next().Encode(),
|
||||||
Self: res.Self().Encode(),
|
Self: res.Self().Encode(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range versions {
|
for _, v := range villages {
|
||||||
resp.Data = append(resp.Data, NewVillage(v))
|
resp.Data = append(resp.Data, NewVillage(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue