diff --git a/internal/port/handler_http_api_server_test.go b/internal/port/handler_http_api_server_test.go index 81eda94..95f8165 100644 --- a/internal/port/handler_http_api_server_test.go +++ b/internal/port/handler_http_api_server_test.go @@ -80,29 +80,6 @@ func TestListServers(t *testing.T) { assert.Len(t, body.Data, limit) }, }, - { - 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.ListServersResponse](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) { @@ -133,27 +110,6 @@ func TestListServers(t *testing.T) { assert.Len(t, body.Data, limit) }, }, - { - name: "OK: open=true", - reqModifier: func(t *testing.T, req *http.Request) { - t.Helper() - q := req.URL.Query() - q.Set("open", "true") - 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.ListServersResponse](t, resp.Body) - assert.NotZero(t, body.Data) - for _, s := range body.Data { - assert.True(t, s.Open) - } - }, - }, { name: "OK: open=false", reqModifier: func(t *testing.T, req *http.Request) { @@ -175,6 +131,27 @@ func TestListServers(t *testing.T) { } }, }, + { + name: "OK: open=true", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Set("open", "true") + 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.ListServersResponse](t, resp.Body) + assert.NotZero(t, body.Data) + for _, s := range body.Data { + assert.True(t, s.Open) + } + }, + }, { name: "ERR: limit is not a string", reqModifier: func(t *testing.T, req *http.Request) { diff --git a/internal/port/handler_http_api_tribe.go b/internal/port/handler_http_api_tribe.go index 6370ee6..e3ba513 100644 --- a/internal/port/handler_http_api_tribe.go +++ b/internal/port/handler_http_api_tribe.go @@ -143,6 +143,8 @@ func formatListTribesErrorPath(segments []errorPathSegment) []string { return []string{"$query", "limit"} case "deleted": return []string{"$query", "deleted"} + case "tags": + return []string{"$query", "tag"} case "sort": path := []string{"$query", "sort"} if segments[0].index >= 0 { diff --git a/internal/port/handler_http_api_tribe_test.go b/internal/port/handler_http_api_tribe_test.go new file mode 100644 index 0000000..7289c90 --- /dev/null +++ b/internal/port/handler_http_api_tribe_test.go @@ -0,0 +1,906 @@ +package port_test + +import ( + "cmp" + "fmt" + "net/http" + "net/http/httptest" + "slices" + "strconv" + "strings" + "testing" + "time" + + "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/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const endpointListTribes = "/v2/versions/%s/servers/%s/tribes" + +func TestListTribes(t *testing.T) { + t.Parallel() + + handler := newAPIHTTPHandler(t) + tribes := getAllTribes(t, handler) + var server serverWithVersion + + for _, tr := range tribes { + if tr.DeletedAt != nil { + server = tr.Server + 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.ListTribesResponse](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.Tribe) 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.ListTribesResponse](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.ListTribesResponse](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.ListTribesResponse](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=[deletedAt:DESC,points:ASC]", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Add("sort", "deletedAt:DESC") + q.Add("sort", "points:ASC") + 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.ListTribesResponse](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.Tribe) int { + var aDeletedAt time.Time + if a.DeletedAt != nil { + aDeletedAt = *a.DeletedAt + } + + var bDeletedAt time.Time + if b.DeletedAt != nil { + bDeletedAt = *b.DeletedAt + } + + return cmp.Or( + aDeletedAt.Compare(bDeletedAt)*-1, + cmp.Compare(a.Points, b.Points), + cmp.Compare(a.Id, b.Id), + ) + })) + }, + }, + { + 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.ListTribesResponse](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.Tribe) int { + return cmp.Or( + cmp.Compare(a.OpponentsDefeated.ScoreAtt, b.OpponentsDefeated.ScoreAtt)*-1, + cmp.Compare(a.Id, b.Id), + ) + })) + }, + }, + { + name: "OK: tag", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + + q := req.URL.Query() + + for _, tr := range tribes { + if tr.Server.Key == server.Key { + q.Add("tag", tr.Tag) + } + + if len(q["tag"]) == 2 { + break + } + } + + require.NotEmpty(t, q["tag"]) + + 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.ListTribesResponse](t, resp.Body) + assert.NotZero(t, body.Cursor.Self) + assert.NotZero(t, body.Data) + tags := req.URL.Query()["tag"] + assert.Len(t, body.Data, len(tags)) + for _, tr := range body.Data { + assert.True(t, slices.Contains(tags, tr.Tag)) + } + }, + }, + { + name: "OK: deleted=false", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Set("deleted", "false") + 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.ListTribesResponse](t, resp.Body) + assert.NotZero(t, body.Data) + for _, tr := range body.Data { + assert.Nil(t, tr.DeletedAt) + } + }, + }, + { + name: "OK: deleted=true", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Set("deleted", "true") + 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.ListTribesResponse](t, resp.Body) + assert.NotZero(t, body.Data) + for _, tr := range body.Data { + assert.NotNil(t, tr.DeletedAt) + } + }, + }, + { + 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: len(tag) > 100", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + for range 101 { + q.Add("tag", domaintest.RandTribeTag()) + } + 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()["tag"]), + } + 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", "tag"}, + }, + }, + }, body) + }, + }, + { + name: "ERR: deleted is not a valid boolean", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Set("deleted", "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.ParseBool: parsing \"%s\": invalid syntax", + req.URL.Query().Get("deleted"), + ), + Path: []string{"$query", "deleted"}, + }, + }, + }, body) + }, + }, + { + name: "ERR: version not found", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + req.URL.Path = fmt.Sprintf(endpointListTribes, 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(endpointListTribes, 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(endpointListTribes, 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) + }) + } +} + +const endpointGetTribe = "/v2/versions/%s/servers/%s/tribes/%d" + +func TestGetTribe(t *testing.T) { + t.Parallel() + + handler := newAPIHTTPHandler(t) + tribe := randTribe(t, handler) + + 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", + assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) { + t.Helper() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + // body + body := decodeJSON[apimodel.GetTribeResponse](t, resp.Body) + assert.Equal(t, tribe.Tribe, body.Data) + }, + }, + { + name: "ERR: id < 0", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + req.URL.Path = fmt.Sprintf( + endpointGetTribe, + tribe.Server.Version.Code, + tribe.Server.Key, + domaintest.RandID()*-1, + ) + }, + 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) + pathSegments := strings.Split(req.URL.Path, "/") + require.Len(t, pathSegments, 8) + id, err := strconv.Atoi(pathSegments[7]) + require.NoError(t, err) + domainErr := domain.MinGreaterEqualError{ + Min: 0, + Current: id, + } + assert.Equal(t, apimodel.ErrorResponse{ + Errors: []apimodel.Error{ + { + Code: domainErr.Code(), + Message: domainErr.Error(), + Params: map[string]any{ + "min": float64(domainErr.Min), + "current": float64(domainErr.Current), + }, + Path: []string{"$path", "tribeId"}, + }, + }, + }, body) + }, + }, + { + name: "ERR: version not found", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + req.URL.Path = fmt.Sprintf(endpointGetTribe, domaintest.RandVersionCode(), tribe.Server.Key, tribe.Id) + }, + 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, 8) + 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(endpointGetTribe, tribe.Server.Version.Code, domaintest.RandServerKey(), tribe.Id) + }, + 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, 8) + 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( + endpointGetTribe, + tribe.Server.Version.Code, + tribe.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, 8) + 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(endpointGetTribe, tribe.Server.Version.Code, tribe.Server.Key, tribe.Id), + nil, + ) + if tt.reqModifier != nil { + tt.reqModifier(t, req) + } + + resp := doCustomRequest(handler, req) + defer resp.Body.Close() + tt.assertResp(t, req, resp) + }) + } +} + +type tribeWithServer struct { + apimodel.Tribe + Server serverWithVersion +} + +func getAllTribes(tb testing.TB, h http.Handler) []tribeWithServer { + tb.Helper() + + servers := getAllServers(tb, h) + + var tribes []tribeWithServer + + for _, s := range servers { + resp := doRequest(h, http.MethodGet, fmt.Sprintf(endpointListTribes, s.Version.Code, s.Key), nil) + require.Equal(tb, http.StatusOK, resp.StatusCode) + + for _, t := range decodeJSON[apimodel.ListTribesResponse](tb, resp.Body).Data { + tribes = append(tribes, tribeWithServer{ + Tribe: t, + Server: s, + }) + } + + _ = resp.Body.Close() + } + + require.NotZero(tb, tribes) + + return tribes +} + +func randTribe(tb testing.TB, h http.Handler) tribeWithServer { + tb.Helper() + tribes := getAllTribes(tb, h) + return tribes[gofakeit.IntRange(0, len(tribes)-1)] +}