feat: api - GET /api/v2/versions/{versionCode}/servers/{serverKey}/players - new query param 'name' (#9)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details

Reviewed-on: twhelp/corev3#9
This commit is contained in:
Dawid Wysokiński 2024-02-29 06:03:36 +00:00
parent 0be010ab50
commit bf2b3b178c
8 changed files with 368 additions and 3 deletions

View File

@ -182,6 +182,7 @@ paths:
- $ref: "#/components/parameters/LimitQueryParam"
- $ref: "#/components/parameters/PlayerDeletedQueryParam"
- $ref: "#/components/parameters/PlayerSortQueryParam"
- $ref: "#/components/parameters/PlayerNameQueryParam"
responses:
200:
$ref: "#/components/responses/ListPlayersResponse"
@ -1141,6 +1142,16 @@ components:
- deletedAt:ASC
- deletedAt:DESC
maxItems: 2
PlayerNameQueryParam:
name: name
in: query
schema:
type: array
items:
type: string
minLength: 1
maxLength: 150
maxItems: 100
VersionCodePathParam:
in: path
name: versionCode

View File

@ -168,6 +168,10 @@ func (a listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
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.V {
q = q.Where("player.deleted_at IS NOT NULL")

View File

@ -357,7 +357,7 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
serverKeys := params.ServerKeys()
players := res.Players()
assert.NotEmpty(t, players)
assert.Len(t, players, len(ids))
for _, p := range players {
assert.True(t, slices.Contains(ids, p.ID()))
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)
},
},
{
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",
params: func(t *testing.T) domain.ListPlayersParams {

View File

@ -449,7 +449,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
serverKeys := params.ServerKeys()
tribes := res.Tribes()
assert.NotEmpty(t, tribes)
assert.Len(t, tribes, len(ids))
for _, tr := range tribes {
assert.True(t, slices.Contains(ids, tr.ID()))
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()
tribes := res.Tribes()
assert.NotEmpty(t, tribes)
assert.Len(t, tribes, len(tags))
for _, tr := range tribes {
assert.True(t, slices.Contains(tags, tr.Tag()))
assert.True(t, slices.Contains(serverKeys, tr.ServerKey()))

View File

@ -629,6 +629,7 @@ func (pc PlayerCursor) Encode() string {
type ListPlayersParams struct {
ids []int
serverKeys []string
names []string
deleted NullBool
sort []PlayerSort
cursor PlayerCursor
@ -680,6 +681,40 @@ func (params *ListPlayersParams) SetServerKeys(serverKeys []string) error {
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 {
return params.deleted
}

View File

@ -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) {
t.Parallel()

View File

@ -35,6 +35,13 @@ func (h *apiHTTPHandler) ListPlayers(
return
}
if params.Name != nil {
if err := domainParams.SetNames(*params.Name); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
return
}
}
if params.Deleted != nil {
if err := domainParams.SetDeleted(domain.NullBool{
V: *params.Deleted,
@ -80,6 +87,12 @@ func formatListPlayersErrorPath(segments []errorPathSegment) []string {
return []string{"$query", "limit"}
case "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":
path := []string{"$query", "sort"}
if segments[0].index >= 0 {

View File

@ -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",
reqModifier: func(t *testing.T, req *http.Request) {
@ -490,6 +527,118 @@ func TestListPlayers(t *testing.T) {
}, 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",
reqModifier: func(t *testing.T, req *http.Request) {