diff --git a/api/openapi3.yml b/api/openapi3.yml index b8b59a0..3bcaa34 100644 --- a/api/openapi3.yml +++ b/api/openapi3.yml @@ -205,6 +205,27 @@ paths: $ref: "#/components/responses/GetPlayerResponse" default: $ref: "#/components/responses/ErrorResponse" + /v2/versions/{versionCode}/servers/{serverKey}/tribes/{tribeId}/members: + get: + operationId: listTribeMembers + tags: + - versions + - servers + - tribes + - players + description: List tribe members + parameters: + - $ref: "#/components/parameters/VersionCodePathParam" + - $ref: "#/components/parameters/ServerKeyPathParam" + - $ref: "#/components/parameters/TribeIdPathParam" + - $ref: "#/components/parameters/CursorQueryParam" + - $ref: "#/components/parameters/LimitQueryParam" + - $ref: "#/components/parameters/PlayerSortQueryParam" + responses: + 200: + $ref: "#/components/responses/ListPlayersResponse" + default: + $ref: "#/components/responses/ErrorResponse" components: schemas: diff --git a/internal/adapter/repository_bun_player.go b/internal/adapter/repository_bun_player.go index fc06289..8d79604 100644 --- a/internal/adapter/repository_bun_player.go +++ b/internal/adapter/repository_bun_player.go @@ -172,6 +172,10 @@ func (a listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery { q = q.Where("player.name IN (?)", bun.In(names)) } + if tribeIDs := a.params.TribeIDs(); len(tribeIDs) > 0 { + q = q.Where("player.tribe_id IN (?)", bun.In(tribeIDs)) + } + if deleted := a.params.Deleted(); deleted.Valid { if deleted.V { q = q.Where("player.deleted_at IS NOT NULL") diff --git a/internal/adapter/repository_player_test.go b/internal/adapter/repository_player_test.go index 23e2ae3..a5c9490 100644 --- a/internal/adapter/repository_player_test.go +++ b/internal/adapter/repository_player_test.go @@ -432,6 +432,44 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories require.NoError(t, err) }, }, + { + name: "OK: tribeIDs serverKeys", + params: func(t *testing.T) domain.ListPlayersParams { + t.Helper() + + params := domain.NewListPlayersParams() + + res, err := repos.player.List(ctx, params) + require.NoError(t, err) + idx := slices.IndexFunc(res.Players(), func(player domain.Player) bool { + return player.TribeID() > 0 + }) + require.GreaterOrEqual(t, idx, 0) + randPlayer := res.Players()[idx] + + require.NoError(t, params.SetTribeIDs([]int{randPlayer.TribeID()})) + require.NoError(t, params.SetServerKeys([]string{randPlayer.ServerKey()})) + + return params + }, + assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) { + t.Helper() + + tribeIDs := params.TribeIDs() + serverKeys := params.ServerKeys() + + players := res.Players() + assert.NotZero(t, players) + for _, p := range players { + assert.True(t, slices.Contains(tribeIDs, p.TribeID())) + 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 { diff --git a/internal/domain/player.go b/internal/domain/player.go index 4957134..1a1ea95 100644 --- a/internal/domain/player.go +++ b/internal/domain/player.go @@ -699,6 +699,7 @@ type ListPlayersParams struct { ids []int serverKeys []string names []string + tribeIDs []int deleted NullBool sort []PlayerSort cursor PlayerCursor @@ -796,6 +797,27 @@ func (params *ListPlayersParams) SetNames(names []string) error { return nil } +func (params *ListPlayersParams) TribeIDs() []int { + return params.tribeIDs +} + +func (params *ListPlayersParams) SetTribeIDs(tribeIDs []int) error { + for i, id := range tribeIDs { + if err := validateIntInRange(id, 1, math.MaxInt); err != nil { + return SliceElementValidationError{ + Model: listPlayersParamsModelName, + Field: "tribeIDs", + Index: i, + Err: err, + } + } + } + + params.tribeIDs = tribeIDs + + return nil +} + func (params *ListPlayersParams) Deleted() NullBool { return params.deleted } @@ -1008,7 +1030,6 @@ func NewListPlayersWithRelationsResult( } if !next.IsZero() { - fmt.Println(next.IsZero()) player := next.Player() od := player.OD() res.next, err = NewPlayerCursor( diff --git a/internal/domain/player_test.go b/internal/domain/player_test.go index ebe073a..5a7b914 100644 --- a/internal/domain/player_test.go +++ b/internal/domain/player_test.go @@ -769,6 +769,66 @@ func TestListPlayersParams_SetNames(t *testing.T) { } } +func TestListPlayersParams_SetTribeIDs(t *testing.T) { + t.Parallel() + + type args struct { + tribeIDs []int + } + + tests := []struct { + name string + args args + expectedErr error + }{ + { + name: "OK", + args: args{ + tribeIDs: []int{ + domaintest.RandID(), + domaintest.RandID(), + domaintest.RandID(), + }, + }, + }, + { + name: "ERR: value < 1", + args: args{ + tribeIDs: []int{ + domaintest.RandID(), + domaintest.RandID(), + domaintest.RandID(), + 0, + domaintest.RandID(), + }, + }, + expectedErr: domain.SliceElementValidationError{ + Model: "ListPlayersParams", + Field: "tribeIDs", + Index: 3, + Err: domain.MinGreaterEqualError{ + Min: 1, + Current: 0, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + params := domain.NewListPlayersParams() + + require.ErrorIs(t, params.SetTribeIDs(tt.args.tribeIDs), tt.expectedErr) + if tt.expectedErr != nil { + return + } + assert.Equal(t, tt.args.tribeIDs, params.TribeIDs()) + }) + } +} + func TestListPlayersParams_SetSort(t *testing.T) { t.Parallel() diff --git a/internal/port/handler_http_api_player.go b/internal/port/handler_http_api_player.go index c09b092..cc01369 100644 --- a/internal/port/handler_http_api_player.go +++ b/internal/port/handler_http_api_player.go @@ -78,6 +78,62 @@ func (h *apiHTTPHandler) ListPlayers( renderJSON(w, r, http.StatusOK, apimodel.NewListPlayersResponse(res)) } +//nolint:gocyclo +func (h *apiHTTPHandler) ListTribeMembers( + w http.ResponseWriter, + r *http.Request, + _ apimodel.VersionCodePathParam, + serverKey apimodel.ServerKeyPathParam, + tribeID apimodel.TribeIdPathParam, + params apimodel.ListTribeMembersParams, +) { + domainParams := domain.NewListPlayersParams() + + if err := domainParams.SetSort([]domain.PlayerSort{domain.PlayerSortIDASC}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err) + return + } + + if params.Sort != nil { + if err := domainParams.PrependSortString(*params.Sort); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err) + return + } + } + + if err := domainParams.SetServerKeys([]string{serverKey}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err) + return + } + + if err := domainParams.SetTribeIDs([]int{tribeID}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err) + return + } + + if params.Limit != nil { + if err := domainParams.SetLimit(*params.Limit); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err) + return + } + } + + if params.Cursor != nil { + if err := domainParams.SetEncodedCursor(*params.Cursor); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err) + return + } + } + + res, err := h.playerSvc.ListWithRelations(r.Context(), domainParams) + if err != nil { + h.errorRenderer.render(w, r, err) + return + } + + renderJSON(w, r, http.StatusOK, apimodel.NewListPlayersResponse(res)) +} + func (h *apiHTTPHandler) GetPlayer( w http.ResponseWriter, r *http.Request, diff --git a/internal/port/handler_http_api_player_test.go b/internal/port/handler_http_api_player_test.go index 7698448..7311f52 100644 --- a/internal/port/handler_http_api_player_test.go +++ b/internal/port/handler_http_api_player_test.go @@ -177,7 +177,7 @@ func TestListPlayers(t *testing.T) { }, }, { - name: "OK: tag", + name: "OK: name", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() @@ -790,6 +790,559 @@ func TestListPlayers(t *testing.T) { } } +const endpointListTribeMembers = "/v2/versions/%s/servers/%s/tribes/%d/members" + +func TestListTribeMembers(t *testing.T) { + t.Parallel() + + handler := newAPIHTTPHandler(t) + players := getAllPlayers(t, handler) + var server serverWithVersion + var tribeID int + + for _, p := range players { + if p.Tribe != nil && p.Tribe.Id > 0 { + server = p.Server + tribeID = p.Tribe.Id + break + } + } + + require.NotZero(t, 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.ListPlayersResponse](t, resp.Body) + assert.Zero(t, body.Cursor.Next) + assert.NotZero(t, body.Cursor.Self) + assert.NotZero(t, body.Data) + assert.False(t, slices.ContainsFunc(body.Data, func(player apimodel.Player) bool { + return player.Tribe == nil || player.Tribe.Id != tribeID + })) + assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.Player) 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.ListPlayersResponse](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.ListPlayersResponse](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.ListPlayersResponse](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=[odScoreAtt:DESC]", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Set("sort", "odScoreAtt: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.ListPlayersResponse](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.Player) int { + return cmp.Or( + cmp.Compare(a.OpponentsDefeated.ScoreAtt, b.OpponentsDefeated.ScoreAtt)*-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: 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: len(sort) > 2", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Add("sort", "odScoreAtt:DESC") + q.Add("sort", "odScoreAtt:ASC") + q.Add("sort", "points: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: 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", "odScoreAtt:ASC") + q.Add("sort", "test:DESC") + 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"][1], + } + assert.Equal(t, apimodel.ErrorResponse{ + Errors: []apimodel.Error{ + { + Code: domainErr.Code(), + Message: domainErr.Error(), + Params: map[string]any{ + "sort": domainErr.Sort, + }, + Path: []string{"$query", "sort", "1"}, + }, + }, + }, body) + }, + }, + { + name: "ERR: sort conflict", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Add("sort", "odScoreAtt:ASC") + q.Add("sort", "odScoreAtt:DESC") + 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: 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(endpointListTribeMembers, domaintest.RandVersionCode(), server.Key, tribeID) + }, + 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: 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(endpointListTribeMembers, server.Version.Code, domaintest.RandServerKey(), tribeID) + }, + 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: domainErr.Code(), + Message: domainErr.Error(), + Params: map[string]any{ + "key": domainErr.Key, + }, + }, + }, + }, body) + }, + }, + { + name: "ERR: tribe not found", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + req.URL.Path = fmt.Sprintf(endpointListTribeMembers, 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.TribeNotFoundError{ + ID: id, + ServerKey: pathSegments[5], + } + assert.Equal(t, apimodel.ErrorResponse{ + Errors: []apimodel.Error{ + { + Code: 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(endpointListTribeMembers, server.Version.Code, server.Key, tribeID), + nil, + ) + if tt.reqModifier != nil { + tt.reqModifier(t, req) + } + + resp := doCustomRequest(handler, req) + defer resp.Body.Close() + tt.assertResp(t, req, resp) + }) + } +} + const endpointGetPlayer = "/v2/versions/%s/servers/%s/players/%d" func TestGetPlayer(t *testing.T) {