diff --git a/api/openapi3.yml b/api/openapi3.yml index e946a84..219401a 100644 --- a/api/openapi3.yml +++ b/api/openapi3.yml @@ -78,6 +78,7 @@ paths: - $ref: "#/components/parameters/PlayerDeletedQueryParam" - $ref: "#/components/parameters/PlayerSortQueryParam" - $ref: "#/components/parameters/PlayerNameQueryParam" + - $ref: "#/components/parameters/PlayerIdQueryParam" responses: 200: $ref: "#/components/responses/ListPlayersWithServersResponse" @@ -214,6 +215,7 @@ paths: - $ref: "#/components/parameters/PlayerDeletedQueryParam" - $ref: "#/components/parameters/PlayerSortQueryParam" - $ref: "#/components/parameters/PlayerNameQueryParam" + - $ref: "#/components/parameters/PlayerIdQueryParam" responses: 200: $ref: "#/components/responses/ListPlayersResponse" @@ -1766,6 +1768,13 @@ components: minLength: 1 maxLength: 150 maxItems: 100 + PlayerIdQueryParam: + in: query + name: id + schema: + type: array + items: + $ref: "#/components/schemas/IntId" VillageCoordsQueryParam: name: coords in: query diff --git a/internal/port/handler_http_api_ennoblement_test.go b/internal/port/handler_http_api_ennoblement_test.go index 68f8499..0bb1dc2 100644 --- a/internal/port/handler_http_api_ennoblement_test.go +++ b/internal/port/handler_http_api_ennoblement_test.go @@ -213,7 +213,7 @@ func TestListEnnoblements(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() @@ -821,7 +821,7 @@ func TestListPlayerEnnoblements(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() @@ -1487,7 +1487,7 @@ func TestListTribeEnnoblements(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() @@ -2140,7 +2140,7 @@ func TestListVillageEnnoblements(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() diff --git a/internal/port/handler_http_api_player.go b/internal/port/handler_http_api_player.go index 458b020..1743321 100644 --- a/internal/port/handler_http_api_player.go +++ b/internal/port/handler_http_api_player.go @@ -63,6 +63,13 @@ func (h *apiHTTPHandler) ListVersionPlayers( return } + if params.Id != nil { + if err := domainParams.SetIDs(*params.Id); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err) + return + } + } + if params.Name != nil { if err := domainParams.SetNames(*params.Name); err != nil { h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err) @@ -134,6 +141,13 @@ func (h *apiHTTPHandler) ListServerPlayers( return } + if params.Id != nil { + if err := domainParams.SetIDs(*params.Id); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err) + return + } + } + if params.Name != nil { if err := domainParams.SetNames(*params.Name); err != nil { h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err) @@ -287,6 +301,7 @@ func playerFromContext(ctx context.Context) (domain.PlayerWithRelations, bool) { return p, ok } +//nolint:gocyclo func formatListPlayersErrorPath(segments []domain.ErrorPathSegment) []string { if segments[0].Model != "ListPlayersParams" { return nil @@ -305,6 +320,12 @@ func formatListPlayersErrorPath(segments []domain.ErrorPathSegment) []string { path = append(path, strconv.Itoa(segments[0].Index)) } return path + case "ids": + path := []string{"$query", "id"} + 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 { diff --git a/internal/port/handler_http_api_player_snapshot_test.go b/internal/port/handler_http_api_player_snapshot_test.go index c192019..c9d14f5 100644 --- a/internal/port/handler_http_api_player_snapshot_test.go +++ b/internal/port/handler_http_api_player_snapshot_test.go @@ -147,7 +147,7 @@ func TestListPlayerPlayerSnapshots(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() diff --git a/internal/port/handler_http_api_player_test.go b/internal/port/handler_http_api_player_test.go index 7a559be..a722133 100644 --- a/internal/port/handler_http_api_player_test.go +++ b/internal/port/handler_http_api_player_test.go @@ -218,6 +218,43 @@ func TestListVersionPlayers(t *testing.T) { } }, }, + { + name: "OK: id", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + + q := req.URL.Query() + + for _, p := range players { + if p.Server.Version.Code == version.Code { + q.Add("id", strconv.Itoa(p.Id)) + } + + if len(q["id"]) == 2 { + break + } + } + + require.NotEmpty(t, q["id"]) + + 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.ListPlayersWithServersResponse](t, resp.Body) + assert.NotZero(t, body.Cursor.Self) + assert.NotZero(t, body.Data) + ids := req.URL.Query()["id"] + assert.Len(t, body.Data, len(ids)) + for _, p := range body.Data { + assert.True(t, slices.Contains(ids, strconv.Itoa(p.Id))) + } + }, + }, { name: "OK: deleted=false", reqModifier: func(t *testing.T, req *http.Request) { @@ -261,7 +298,7 @@ func TestListVersionPlayers(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() @@ -570,6 +607,71 @@ func TestListVersionPlayers(t *testing.T) { }, body) }, }, + { + name: "ERR: id is not an integer", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Set("id", "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 setting array element: error binding string parameter: strconv.ParseInt: parsing \"%s\": invalid syntax", + req.URL.Query().Get("id"), + ), + Path: []string{"$query", "id"}, + }, + }, + }, body) + }, + }, + { + name: "ERR: id < 1", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Set("id", "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) + id, err := strconv.Atoi(req.URL.Query().Get("id")) + require.NoError(t, err) + domainErr := domain.MinGreaterEqualError{ + Min: 1, + Current: id, + } + assert.Equal(t, apimodel.ErrorResponse{ + Errors: []apimodel.Error{ + { + Code: apimodel.ErrorCode(domainErr.Code()), + Message: domainErr.Error(), + Params: map[string]any{ + "min": float64(domainErr.Min), + "current": float64(domainErr.Current), + }, + Path: []string{"$query", "id", "0"}, + }, + }, + }, body) + }, + }, { name: "ERR: len(name) > 100", reqModifier: func(t *testing.T, req *http.Request) { @@ -958,6 +1060,43 @@ func TestListServerPlayers(t *testing.T) { } }, }, + { + name: "OK: id", + 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("id", strconv.Itoa(p.Id)) + } + + if len(q["id"]) == 2 { + break + } + } + + require.NotEmpty(t, q["id"]) + + 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) + ids := req.URL.Query()["id"] + assert.Len(t, body.Data, len(ids)) + for _, p := range body.Data { + assert.True(t, slices.Contains(ids, strconv.Itoa(p.Id))) + } + }, + }, { name: "OK: deleted=false", reqModifier: func(t *testing.T, req *http.Request) { @@ -1001,7 +1140,7 @@ func TestListServerPlayers(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() @@ -1310,6 +1449,71 @@ func TestListServerPlayers(t *testing.T) { }, body) }, }, + { + name: "ERR: id is not an integer", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Set("id", "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 setting array element: error binding string parameter: strconv.ParseInt: parsing \"%s\": invalid syntax", + req.URL.Query().Get("id"), + ), + Path: []string{"$query", "id"}, + }, + }, + }, body) + }, + }, + { + name: "ERR: id < 1", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Set("id", "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) + id, err := strconv.Atoi(req.URL.Query().Get("id")) + require.NoError(t, err) + domainErr := domain.MinGreaterEqualError{ + Min: 1, + Current: id, + } + assert.Equal(t, apimodel.ErrorResponse{ + Errors: []apimodel.Error{ + { + Code: apimodel.ErrorCode(domainErr.Code()), + Message: domainErr.Error(), + Params: map[string]any{ + "min": float64(domainErr.Min), + "current": float64(domainErr.Current), + }, + Path: []string{"$query", "id", "0"}, + }, + }, + }, body) + }, + }, { name: "ERR: len(name) > 100", reqModifier: func(t *testing.T, req *http.Request) { @@ -1660,7 +1864,7 @@ func TestListTribeMembers(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() diff --git a/internal/port/handler_http_api_server_test.go b/internal/port/handler_http_api_server_test.go index 412de8c..74f9f62 100644 --- a/internal/port/handler_http_api_server_test.go +++ b/internal/port/handler_http_api_server_test.go @@ -153,7 +153,7 @@ func TestListServers(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() diff --git a/internal/port/handler_http_api_tribe_change_test.go b/internal/port/handler_http_api_tribe_change_test.go index a0e9381..4d9046b 100644 --- a/internal/port/handler_http_api_tribe_change_test.go +++ b/internal/port/handler_http_api_tribe_change_test.go @@ -228,7 +228,7 @@ func TestListPlayerTribeChanges(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() @@ -920,7 +920,7 @@ func TestListTribeMemberChanges(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() diff --git a/internal/port/handler_http_api_tribe_snapshot_test.go b/internal/port/handler_http_api_tribe_snapshot_test.go index e9a9617..baa23c5 100644 --- a/internal/port/handler_http_api_tribe_snapshot_test.go +++ b/internal/port/handler_http_api_tribe_snapshot_test.go @@ -147,7 +147,7 @@ func TestListTribeTribeSnapshots(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() diff --git a/internal/port/handler_http_api_tribe_test.go b/internal/port/handler_http_api_tribe_test.go index 88b3cd7..db39a78 100644 --- a/internal/port/handler_http_api_tribe_test.go +++ b/internal/port/handler_http_api_tribe_test.go @@ -256,7 +256,7 @@ func TestListTribes(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() diff --git a/internal/port/handler_http_api_version_test.go b/internal/port/handler_http_api_version_test.go index db422c6..65ceaa9 100644 --- a/internal/port/handler_http_api_version_test.go +++ b/internal/port/handler_http_api_version_test.go @@ -101,7 +101,7 @@ func TestListVersions(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() diff --git a/internal/port/handler_http_api_village_test.go b/internal/port/handler_http_api_village_test.go index 9685f98..dd68e09 100644 --- a/internal/port/handler_http_api_village_test.go +++ b/internal/port/handler_http_api_village_test.go @@ -140,7 +140,7 @@ func TestListVillages(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() @@ -584,7 +584,7 @@ func TestListPlayerVillages(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() @@ -997,7 +997,7 @@ func TestListTribeVillages(t *testing.T) { }, }, { - name: "ERR: limit is not a string", + name: "ERR: limit is not an integer", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query()