feat: api - GET /api/v2/versions/{versionCode}/servers/{serverKey}/players - new query param 'name' (#9)
Reviewed-on: twhelp/corev3#9
This commit is contained in:
parent
0be010ab50
commit
bf2b3b178c
|
@ -182,6 +182,7 @@ paths:
|
||||||
- $ref: "#/components/parameters/LimitQueryParam"
|
- $ref: "#/components/parameters/LimitQueryParam"
|
||||||
- $ref: "#/components/parameters/PlayerDeletedQueryParam"
|
- $ref: "#/components/parameters/PlayerDeletedQueryParam"
|
||||||
- $ref: "#/components/parameters/PlayerSortQueryParam"
|
- $ref: "#/components/parameters/PlayerSortQueryParam"
|
||||||
|
- $ref: "#/components/parameters/PlayerNameQueryParam"
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
$ref: "#/components/responses/ListPlayersResponse"
|
$ref: "#/components/responses/ListPlayersResponse"
|
||||||
|
@ -1141,6 +1142,16 @@ components:
|
||||||
- deletedAt:ASC
|
- deletedAt:ASC
|
||||||
- deletedAt:DESC
|
- deletedAt:DESC
|
||||||
maxItems: 2
|
maxItems: 2
|
||||||
|
PlayerNameQueryParam:
|
||||||
|
name: name
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
maxLength: 150
|
||||||
|
maxItems: 100
|
||||||
VersionCodePathParam:
|
VersionCodePathParam:
|
||||||
in: path
|
in: path
|
||||||
name: versionCode
|
name: versionCode
|
||||||
|
|
|
@ -168,6 +168,10 @@ func (a listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
|
||||||
q = q.Where("player.server_key IN (?)", bun.In(serverKeys))
|
q = q.Where("player.server_key IN (?)", bun.In(serverKeys))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if names := a.params.Names(); len(names) > 0 {
|
||||||
|
q = q.Where("player.name IN (?)", bun.In(names))
|
||||||
|
}
|
||||||
|
|
||||||
if deleted := a.params.Deleted(); deleted.Valid {
|
if deleted := a.params.Deleted(); deleted.Valid {
|
||||||
if deleted.V {
|
if deleted.V {
|
||||||
q = q.Where("player.deleted_at IS NOT NULL")
|
q = q.Where("player.deleted_at IS NOT NULL")
|
||||||
|
|
|
@ -357,7 +357,7 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
||||||
serverKeys := params.ServerKeys()
|
serverKeys := params.ServerKeys()
|
||||||
|
|
||||||
players := res.Players()
|
players := res.Players()
|
||||||
assert.NotEmpty(t, players)
|
assert.Len(t, players, len(ids))
|
||||||
for _, p := range players {
|
for _, p := range players {
|
||||||
assert.True(t, slices.Contains(ids, p.ID()))
|
assert.True(t, slices.Contains(ids, p.ID()))
|
||||||
assert.True(t, slices.Contains(serverKeys, p.ServerKey()))
|
assert.True(t, slices.Contains(serverKeys, p.ServerKey()))
|
||||||
|
@ -368,6 +368,41 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "OK: names 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.SetNames([]string{randPlayer.Name()}))
|
||||||
|
require.NoError(t, params.SetServerKeys([]string{randPlayer.ServerKey()}))
|
||||||
|
|
||||||
|
return params
|
||||||
|
},
|
||||||
|
assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
names := params.Names()
|
||||||
|
serverKeys := params.ServerKeys()
|
||||||
|
|
||||||
|
players := res.Players()
|
||||||
|
assert.Len(t, players, len(names))
|
||||||
|
for _, p := range players {
|
||||||
|
assert.True(t, slices.Contains(names, p.Name()))
|
||||||
|
assert.True(t, slices.Contains(serverKeys, p.ServerKey()))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
assertError: func(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "OK: deleted=true",
|
name: "OK: deleted=true",
|
||||||
params: func(t *testing.T) domain.ListPlayersParams {
|
params: func(t *testing.T) domain.ListPlayersParams {
|
||||||
|
|
|
@ -449,7 +449,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
|
||||||
serverKeys := params.ServerKeys()
|
serverKeys := params.ServerKeys()
|
||||||
|
|
||||||
tribes := res.Tribes()
|
tribes := res.Tribes()
|
||||||
assert.NotEmpty(t, tribes)
|
assert.Len(t, tribes, len(ids))
|
||||||
for _, tr := range tribes {
|
for _, tr := range tribes {
|
||||||
assert.True(t, slices.Contains(ids, tr.ID()))
|
assert.True(t, slices.Contains(ids, tr.ID()))
|
||||||
assert.True(t, slices.Contains(serverKeys, tr.ServerKey()))
|
assert.True(t, slices.Contains(serverKeys, tr.ServerKey()))
|
||||||
|
@ -484,7 +484,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
|
||||||
serverKeys := params.ServerKeys()
|
serverKeys := params.ServerKeys()
|
||||||
|
|
||||||
tribes := res.Tribes()
|
tribes := res.Tribes()
|
||||||
assert.NotEmpty(t, tribes)
|
assert.Len(t, tribes, len(tags))
|
||||||
for _, tr := range tribes {
|
for _, tr := range tribes {
|
||||||
assert.True(t, slices.Contains(tags, tr.Tag()))
|
assert.True(t, slices.Contains(tags, tr.Tag()))
|
||||||
assert.True(t, slices.Contains(serverKeys, tr.ServerKey()))
|
assert.True(t, slices.Contains(serverKeys, tr.ServerKey()))
|
||||||
|
|
|
@ -629,6 +629,7 @@ func (pc PlayerCursor) Encode() string {
|
||||||
type ListPlayersParams struct {
|
type ListPlayersParams struct {
|
||||||
ids []int
|
ids []int
|
||||||
serverKeys []string
|
serverKeys []string
|
||||||
|
names []string
|
||||||
deleted NullBool
|
deleted NullBool
|
||||||
sort []PlayerSort
|
sort []PlayerSort
|
||||||
cursor PlayerCursor
|
cursor PlayerCursor
|
||||||
|
@ -680,6 +681,40 @@ func (params *ListPlayersParams) SetServerKeys(serverKeys []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (params *ListPlayersParams) Names() []string {
|
||||||
|
return params.names
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
playerNamesMinLength = 1
|
||||||
|
playerNamesMaxLength = 100
|
||||||
|
)
|
||||||
|
|
||||||
|
func (params *ListPlayersParams) SetNames(names []string) error {
|
||||||
|
if err := validateSliceLen(names, playerNamesMinLength, playerNamesMaxLength); err != nil {
|
||||||
|
return ValidationError{
|
||||||
|
Model: listPlayersParamsModelName,
|
||||||
|
Field: "names",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, n := range names {
|
||||||
|
if err := validateStringLen(n, playerNameMinLength, playerNameMaxLength); err != nil {
|
||||||
|
return SliceElementValidationError{
|
||||||
|
Model: listPlayersParamsModelName,
|
||||||
|
Field: "names",
|
||||||
|
Index: i,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.names = names
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (params *ListPlayersParams) Deleted() NullBool {
|
func (params *ListPlayersParams) Deleted() NullBool {
|
||||||
return params.deleted
|
return params.deleted
|
||||||
}
|
}
|
||||||
|
|
|
@ -474,6 +474,124 @@ func TestListPlayersParams_SetIDs(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListPlayersParams_SetNames(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
names []string
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OK",
|
||||||
|
args: args{
|
||||||
|
names: []string{
|
||||||
|
gofakeit.LetterN(50),
|
||||||
|
gofakeit.LetterN(50),
|
||||||
|
gofakeit.LetterN(50),
|
||||||
|
gofakeit.LetterN(50),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: len(names) < 1",
|
||||||
|
args: args{
|
||||||
|
names: nil,
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "ListPlayersParams",
|
||||||
|
Field: "names",
|
||||||
|
Err: domain.LenOutOfRangeError{
|
||||||
|
Min: 1,
|
||||||
|
Max: 100,
|
||||||
|
Current: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: len(names) > 100",
|
||||||
|
args: args{
|
||||||
|
names: func() []string {
|
||||||
|
names := make([]string, 101)
|
||||||
|
for i := range names {
|
||||||
|
names[i] = gofakeit.LetterN(50)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "ListPlayersParams",
|
||||||
|
Field: "names",
|
||||||
|
Err: domain.LenOutOfRangeError{
|
||||||
|
Min: 1,
|
||||||
|
Max: 100,
|
||||||
|
Current: 101,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: len(name[1]) < 1",
|
||||||
|
args: args{
|
||||||
|
names: []string{
|
||||||
|
gofakeit.LetterN(50),
|
||||||
|
"",
|
||||||
|
gofakeit.LetterN(50),
|
||||||
|
gofakeit.LetterN(50),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErr: domain.SliceElementValidationError{
|
||||||
|
Model: "ListPlayersParams",
|
||||||
|
Field: "names",
|
||||||
|
Index: 1,
|
||||||
|
Err: domain.LenOutOfRangeError{
|
||||||
|
Min: 1,
|
||||||
|
Max: 150,
|
||||||
|
Current: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: len(name[2]) > 150",
|
||||||
|
args: args{
|
||||||
|
names: []string{
|
||||||
|
gofakeit.LetterN(50),
|
||||||
|
gofakeit.LetterN(50),
|
||||||
|
gofakeit.LetterN(151),
|
||||||
|
gofakeit.LetterN(50),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErr: domain.SliceElementValidationError{
|
||||||
|
Model: "ListPlayersParams",
|
||||||
|
Field: "names",
|
||||||
|
Index: 2,
|
||||||
|
Err: domain.LenOutOfRangeError{
|
||||||
|
Min: 1,
|
||||||
|
Max: 150,
|
||||||
|
Current: 151,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
params := domain.NewListPlayersParams()
|
||||||
|
|
||||||
|
require.ErrorIs(t, params.SetNames(tt.args.names), tt.expectedErr)
|
||||||
|
if tt.expectedErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.args.names, params.Names())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestListPlayersParams_SetSort(t *testing.T) {
|
func TestListPlayersParams_SetSort(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,13 @@ func (h *apiHTTPHandler) ListPlayers(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if params.Name != nil {
|
||||||
|
if err := domainParams.SetNames(*params.Name); err != nil {
|
||||||
|
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if params.Deleted != nil {
|
if params.Deleted != nil {
|
||||||
if err := domainParams.SetDeleted(domain.NullBool{
|
if err := domainParams.SetDeleted(domain.NullBool{
|
||||||
V: *params.Deleted,
|
V: *params.Deleted,
|
||||||
|
@ -80,6 +87,12 @@ func formatListPlayersErrorPath(segments []errorPathSegment) []string {
|
||||||
return []string{"$query", "limit"}
|
return []string{"$query", "limit"}
|
||||||
case "deleted":
|
case "deleted":
|
||||||
return []string{"$query", "deleted"}
|
return []string{"$query", "deleted"}
|
||||||
|
case "names":
|
||||||
|
path := []string{"$query", "name"}
|
||||||
|
if segments[0].index >= 0 {
|
||||||
|
path = append(path, strconv.Itoa(segments[0].index))
|
||||||
|
}
|
||||||
|
return path
|
||||||
case "sort":
|
case "sort":
|
||||||
path := []string{"$query", "sort"}
|
path := []string{"$query", "sort"}
|
||||||
if segments[0].index >= 0 {
|
if segments[0].index >= 0 {
|
||||||
|
|
|
@ -176,6 +176,43 @@ func TestListPlayers(t *testing.T) {
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "OK: tag",
|
||||||
|
reqModifier: func(t *testing.T, req *http.Request) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
|
||||||
|
for _, p := range players {
|
||||||
|
if p.Server.Key == server.Key {
|
||||||
|
q.Add("name", p.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(q["name"]) == 2 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotEmpty(t, q["name"])
|
||||||
|
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
},
|
||||||
|
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// body
|
||||||
|
body := decodeJSON[apimodel.ListPlayersResponse](t, resp.Body)
|
||||||
|
assert.NotZero(t, body.Cursor.Self)
|
||||||
|
assert.NotZero(t, body.Data)
|
||||||
|
names := req.URL.Query()["name"]
|
||||||
|
assert.Len(t, body.Data, len(names))
|
||||||
|
for _, p := range body.Data {
|
||||||
|
assert.True(t, slices.Contains(names, p.Name))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "OK: deleted=false",
|
name: "OK: deleted=false",
|
||||||
reqModifier: func(t *testing.T, req *http.Request) {
|
reqModifier: func(t *testing.T, req *http.Request) {
|
||||||
|
@ -490,6 +527,118 @@ func TestListPlayers(t *testing.T) {
|
||||||
}, body)
|
}, body)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: len(name) > 100",
|
||||||
|
reqModifier: func(t *testing.T, req *http.Request) {
|
||||||
|
t.Helper()
|
||||||
|
q := req.URL.Query()
|
||||||
|
for range 101 {
|
||||||
|
q.Add("name", gofakeit.LetterN(50))
|
||||||
|
}
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
},
|
||||||
|
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
|
||||||
|
// body
|
||||||
|
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
|
||||||
|
domainErr := domain.LenOutOfRangeError{
|
||||||
|
Min: 1,
|
||||||
|
Max: 100,
|
||||||
|
Current: len(req.URL.Query()["name"]),
|
||||||
|
}
|
||||||
|
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", "name"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, body)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: len(name[1]) < 1",
|
||||||
|
reqModifier: func(t *testing.T, req *http.Request) {
|
||||||
|
t.Helper()
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Add("name", gofakeit.LetterN(50))
|
||||||
|
q.Add("name", "")
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
},
|
||||||
|
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
|
||||||
|
// body
|
||||||
|
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
|
||||||
|
domainErr := domain.LenOutOfRangeError{
|
||||||
|
Min: 1,
|
||||||
|
Max: 150,
|
||||||
|
Current: len(req.URL.Query()["name"][1]),
|
||||||
|
}
|
||||||
|
assert.Equal(t, apimodel.ErrorResponse{
|
||||||
|
Errors: []apimodel.Error{
|
||||||
|
{
|
||||||
|
Code: domainErr.Code(),
|
||||||
|
Message: domainErr.Error(),
|
||||||
|
Params: map[string]any{
|
||||||
|
"current": float64(domainErr.Current),
|
||||||
|
"max": float64(domainErr.Max),
|
||||||
|
"min": float64(domainErr.Min),
|
||||||
|
},
|
||||||
|
Path: []string{"$query", "name", "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, body)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: len(name[1]) > 150",
|
||||||
|
reqModifier: func(t *testing.T, req *http.Request) {
|
||||||
|
t.Helper()
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Add("name", gofakeit.LetterN(50))
|
||||||
|
q.Add("name", gofakeit.LetterN(151))
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
},
|
||||||
|
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
|
||||||
|
// body
|
||||||
|
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
|
||||||
|
domainErr := domain.LenOutOfRangeError{
|
||||||
|
Min: 1,
|
||||||
|
Max: 150,
|
||||||
|
Current: len(req.URL.Query()["name"][1]),
|
||||||
|
}
|
||||||
|
assert.Equal(t, apimodel.ErrorResponse{
|
||||||
|
Errors: []apimodel.Error{
|
||||||
|
{
|
||||||
|
Code: domainErr.Code(),
|
||||||
|
Message: domainErr.Error(),
|
||||||
|
Params: map[string]any{
|
||||||
|
"current": float64(domainErr.Current),
|
||||||
|
"max": float64(domainErr.Max),
|
||||||
|
"min": float64(domainErr.Min),
|
||||||
|
},
|
||||||
|
Path: []string{"$query", "name", "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, body)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "ERR: deleted is not a valid boolean",
|
name: "ERR: deleted is not a valid boolean",
|
||||||
reqModifier: func(t *testing.T, req *http.Request) {
|
reqModifier: func(t *testing.T, req *http.Request) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user