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/v7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const endpointListEnnoblements = "/v2/versions/%s/servers/%s/ennoblements" func TestListEnnoblements(t *testing.T) { t.Parallel() handler := newAPIHTTPHandler(t) ennoblements := getAllEnnoblements(t, handler) server := ennoblements[0].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.ListEnnoblementsResponse](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.Ennoblement) int { return cmp.Or( a.CreatedAt.Compare(b.CreatedAt), 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.ListEnnoblementsResponse](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.ListEnnoblementsResponse](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.ListEnnoblementsResponse](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=[createdAt:DESC]", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Set("sort", "createdAt: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.ListEnnoblementsResponse](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.Ennoblement) int { return cmp.Or( a.CreatedAt.Compare(b.CreatedAt)*-1, cmp.Compare(a.Id, b.Id), ) })) }, }, { name: "OK: since", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() var filtered []ennoblementWithServer for _, e := range ennoblements { if e.Server.Key == server.Key { filtered = append(filtered, e) } } slices.SortFunc(filtered, func(a, b ennoblementWithServer) int { return cmp.Or( a.CreatedAt.Compare(b.CreatedAt), cmp.Compare(a.Id, b.Id), ) }) require.GreaterOrEqual(t, len(filtered), 2) q := req.URL.Query() q.Set("since", filtered[1].CreatedAt.Format(time.RFC3339)) 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.ListEnnoblementsResponse](t, resp.Body) assert.Zero(t, body.Cursor.Next) assert.NotZero(t, body.Cursor.Self) assert.NotZero(t, body.Data) since, err := time.Parse(time.RFC3339, req.URL.Query().Get("since")) require.NoError(t, err) for _, e := range body.Data { assert.True(t, e.CreatedAt.After(since) || e.CreatedAt.Equal(since)) } }, }, { name: "OK: before", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() var filtered []ennoblementWithServer for _, e := range ennoblements { if e.Server.Key == server.Key { filtered = append(filtered, e) } } slices.SortFunc(filtered, func(a, b ennoblementWithServer) int { return cmp.Or( a.CreatedAt.Compare(b.CreatedAt), cmp.Compare(a.Id, b.Id), ) }) require.GreaterOrEqual(t, len(filtered), 2) q := req.URL.Query() q.Set("before", filtered[1].CreatedAt.Format(time.RFC3339)) 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.ListEnnoblementsResponse](t, resp.Body) assert.Zero(t, body.Cursor.Next) assert.NotZero(t, body.Cursor.Self) assert.NotZero(t, body.Data) before, err := time.Parse(time.RFC3339, req.URL.Query().Get("before")) require.NoError(t, err) for _, e := range body.Data { assert.True(t, e.CreatedAt.Before(before)) } }, }, { 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: apimodel.ErrorCode(domainErr.Code()), Message: domainErr.Error(), Params: map[string]any{ "current": float64(domainErr.Current), "min": float64(domainErr.Min), }, Path: []string{"$query", "limit"}, }, }, }, body) }, }, { name: fmt.Sprintf("ERR: limit > %d", domain.EnnoblementListMaxLimit), reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Set("limit", strconv.Itoa(domain.EnnoblementListMaxLimit+1)) 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: domain.EnnoblementListMaxLimit, Current: limit, } assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: apimodel.ErrorCode(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: apimodel.ErrorCode(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: apimodel.ErrorCode(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: apimodel.ErrorCode(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", "createdAt:DESC") q.Add("sort", "createdAt:ASC") q.Add("sort", "createdAt: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: apimodel.ErrorCode(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", "createdAt:") 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"][0], } assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: apimodel.ErrorCode(domainErr.Code()), Message: domainErr.Error(), Params: map[string]any{ "sort": domainErr.Sort, }, Path: []string{"$query", "sort", "0"}, }, }, }, body) }, }, { name: "ERR: sort conflict", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Add("sort", "createdAt:DESC") q.Add("sort", "createdAt: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) 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: apimodel.ErrorCode(domainErr.Code()), Message: domainErr.Error(), Params: map[string]any{ "sort": paramSort, }, Path: []string{"$query", "sort"}, }, }, }, body) }, }, { name: "ERR: invalid since", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Set("since", gofakeit.LetterN(100)) 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) var domainErr domain.Error require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr) since := req.URL.Query().Get("since") assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: "invalid-param-format", //nolint:lll Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", since, since, since), Path: []string{"$query", "since"}, }, }, }, body) }, }, { name: "ERR: invalid before", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Set("before", gofakeit.LetterN(100)) 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) var domainErr domain.Error require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr) before := req.URL.Query().Get("before") assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: "invalid-param-format", //nolint:lll Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", before, before, before), Path: []string{"$query", "before"}, }, }, }, body) }, }, { name: "ERR: version not found", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() req.URL.Path = fmt.Sprintf(endpointListEnnoblements, randInvalidVersionCode(t, handler), 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: apimodel.ErrorCode(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(endpointListEnnoblements, 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: apimodel.ErrorCode(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(endpointListEnnoblements, 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 endpointListPlayerEnnoblements = "/v2/versions/%s/servers/%s/players/%d/ennoblements" func TestListPlayerEnnoblements(t *testing.T) { t.Parallel() handler := newAPIHTTPHandler(t) ennoblements := getAllEnnoblements(t, handler) server := ennoblements[0].Server player := ennoblements[0].NewOwner 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.ListEnnoblementsResponse](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.Ennoblement) int { return cmp.Or( a.CreatedAt.Compare(b.CreatedAt), 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.ListEnnoblementsResponse](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.ListEnnoblementsResponse](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.ListEnnoblementsResponse](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=[createdAt:DESC]", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Set("sort", "createdAt: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.ListEnnoblementsResponse](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.Ennoblement) int { return cmp.Or( a.CreatedAt.Compare(b.CreatedAt)*-1, cmp.Compare(a.Id, b.Id), ) })) }, }, { name: "OK: since", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() var filtered []ennoblementWithServer for _, e := range ennoblements { if e.Server.Key == server.Key { filtered = append(filtered, e) } } slices.SortFunc(filtered, func(a, b ennoblementWithServer) int { return cmp.Or( a.CreatedAt.Compare(b.CreatedAt), cmp.Compare(a.Id, b.Id), ) }) require.GreaterOrEqual(t, len(filtered), 2) q := req.URL.Query() q.Set("since", filtered[1].CreatedAt.Format(time.RFC3339)) 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.ListEnnoblementsResponse](t, resp.Body) assert.Zero(t, body.Cursor.Next) assert.NotZero(t, body.Cursor.Self) assert.NotZero(t, body.Data) since, err := time.Parse(time.RFC3339, req.URL.Query().Get("since")) require.NoError(t, err) for _, e := range body.Data { assert.True(t, e.CreatedAt.After(since) || e.CreatedAt.Equal(since)) } }, }, { name: "OK: before", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() var filtered []ennoblementWithServer for _, e := range ennoblements { if e.Server.Key == server.Key { filtered = append(filtered, e) } } slices.SortFunc(filtered, func(a, b ennoblementWithServer) int { return cmp.Or( a.CreatedAt.Compare(b.CreatedAt), cmp.Compare(a.Id, b.Id), ) }) require.GreaterOrEqual(t, len(filtered), 2) q := req.URL.Query() q.Set("before", filtered[1].CreatedAt.Format(time.RFC3339)) 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.ListEnnoblementsResponse](t, resp.Body) assert.Zero(t, body.Cursor.Next) assert.NotZero(t, body.Cursor.Self) assert.NotZero(t, body.Data) before, err := time.Parse(time.RFC3339, req.URL.Query().Get("before")) require.NoError(t, err) for _, e := range body.Data { assert.True(t, e.CreatedAt.Before(before)) } }, }, { 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: apimodel.ErrorCode(domainErr.Code()), Message: domainErr.Error(), Params: map[string]any{ "current": float64(domainErr.Current), "min": float64(domainErr.Min), }, Path: []string{"$query", "limit"}, }, }, }, body) }, }, { name: fmt.Sprintf("ERR: limit > %d", domain.EnnoblementListMaxLimit), reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Set("limit", strconv.Itoa(domain.EnnoblementListMaxLimit+1)) 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: domain.EnnoblementListMaxLimit, Current: limit, } assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: apimodel.ErrorCode(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: apimodel.ErrorCode(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: apimodel.ErrorCode(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: apimodel.ErrorCode(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", "createdAt:DESC") q.Add("sort", "createdAt:ASC") q.Add("sort", "createdAt: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: apimodel.ErrorCode(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", "createdAt:") 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"][0], } assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: apimodel.ErrorCode(domainErr.Code()), Message: domainErr.Error(), Params: map[string]any{ "sort": domainErr.Sort, }, Path: []string{"$query", "sort", "0"}, }, }, }, body) }, }, { name: "ERR: sort conflict", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Add("sort", "createdAt:DESC") q.Add("sort", "createdAt: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) 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: apimodel.ErrorCode(domainErr.Code()), Message: domainErr.Error(), Params: map[string]any{ "sort": paramSort, }, Path: []string{"$query", "sort"}, }, }, }, body) }, }, { name: "ERR: invalid since", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Set("since", gofakeit.LetterN(100)) 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) var domainErr domain.Error require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr) since := req.URL.Query().Get("since") assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: "invalid-param-format", //nolint:lll Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", since, since, since), Path: []string{"$query", "since"}, }, }, }, body) }, }, { name: "ERR: invalid before", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Set("before", gofakeit.LetterN(100)) 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) var domainErr domain.Error require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr) before := req.URL.Query().Get("before") assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: "invalid-param-format", //nolint:lll Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", before, before, before), Path: []string{"$query", "before"}, }, }, }, body) }, }, { name: "ERR: version not found", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() req.URL.Path = fmt.Sprintf( endpointListPlayerEnnoblements, randInvalidVersionCode(t, handler), server.Key, player.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, 9) domainErr := domain.VersionNotFoundError{ VersionCode: pathSegments[3], } assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: apimodel.ErrorCode(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( endpointListPlayerEnnoblements, server.Version.Code, domaintest.RandServerKey(), player.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, 9) domainErr := domain.ServerNotFoundError{ Key: pathSegments[5], } assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: apimodel.ErrorCode(domainErr.Code()), Message: domainErr.Error(), Params: map[string]any{ "key": domainErr.Key, }, }, }, }, body) }, }, { name: "ERR: player not found", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() req.URL.Path = fmt.Sprintf( endpointListPlayerEnnoblements, 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.PlayerNotFoundError{ ID: id, ServerKey: pathSegments[5], } assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: apimodel.ErrorCode(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(endpointListPlayerEnnoblements, server.Version.Code, server.Key, player.Id), nil, ) if tt.reqModifier != nil { tt.reqModifier(t, req) } resp := doCustomRequest(handler, req) defer resp.Body.Close() tt.assertResp(t, req, resp) }) } } const endpointListTribeEnnoblements = "/v2/versions/%s/servers/%s/tribes/%d/ennoblements" func TestListTribeEnnoblements(t *testing.T) { t.Parallel() handler := newAPIHTTPHandler(t) ennoblements := getAllEnnoblements(t, handler) var server serverWithVersion var tribe apimodel.TribeMeta for _, e := range ennoblements { if e.NewOwner.Tribe != nil { server = e.Server tribe = *e.NewOwner.Tribe break } } 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.ListEnnoblementsResponse](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.Ennoblement) int { return cmp.Or( a.CreatedAt.Compare(b.CreatedAt), 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.ListEnnoblementsResponse](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.ListEnnoblementsResponse](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.ListEnnoblementsResponse](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=[createdAt:DESC]", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Set("sort", "createdAt: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.ListEnnoblementsResponse](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.Ennoblement) int { return cmp.Or( a.CreatedAt.Compare(b.CreatedAt)*-1, cmp.Compare(a.Id, b.Id), ) })) }, }, { name: "OK: since", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() var filtered []ennoblementWithServer for _, e := range ennoblements { if e.Server.Key == server.Key { filtered = append(filtered, e) } } slices.SortFunc(filtered, func(a, b ennoblementWithServer) int { return cmp.Or( a.CreatedAt.Compare(b.CreatedAt), cmp.Compare(a.Id, b.Id), ) }) require.GreaterOrEqual(t, len(filtered), 2) q := req.URL.Query() q.Set("since", filtered[1].CreatedAt.Format(time.RFC3339)) 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.ListEnnoblementsResponse](t, resp.Body) assert.Zero(t, body.Cursor.Next) assert.NotZero(t, body.Cursor.Self) assert.NotZero(t, body.Data) since, err := time.Parse(time.RFC3339, req.URL.Query().Get("since")) require.NoError(t, err) for _, e := range body.Data { assert.True(t, e.CreatedAt.After(since) || e.CreatedAt.Equal(since)) } }, }, { name: "OK: before", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() var filtered []ennoblementWithServer for _, e := range ennoblements { if e.Server.Key == server.Key { filtered = append(filtered, e) } } slices.SortFunc(filtered, func(a, b ennoblementWithServer) int { return cmp.Or( a.CreatedAt.Compare(b.CreatedAt), cmp.Compare(a.Id, b.Id), ) }) require.GreaterOrEqual(t, len(filtered), 2) q := req.URL.Query() q.Set("before", filtered[1].CreatedAt.Format(time.RFC3339)) 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.ListEnnoblementsResponse](t, resp.Body) assert.Zero(t, body.Cursor.Next) assert.NotZero(t, body.Cursor.Self) assert.NotZero(t, body.Data) before, err := time.Parse(time.RFC3339, req.URL.Query().Get("before")) require.NoError(t, err) for _, e := range body.Data { assert.True(t, e.CreatedAt.Before(before)) } }, }, { 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: apimodel.ErrorCode(domainErr.Code()), Message: domainErr.Error(), Params: map[string]any{ "current": float64(domainErr.Current), "min": float64(domainErr.Min), }, Path: []string{"$query", "limit"}, }, }, }, body) }, }, { name: fmt.Sprintf("ERR: limit > %d", domain.EnnoblementListMaxLimit), reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Set("limit", strconv.Itoa(domain.EnnoblementListMaxLimit+1)) 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: domain.EnnoblementListMaxLimit, Current: limit, } assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: apimodel.ErrorCode(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: apimodel.ErrorCode(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: apimodel.ErrorCode(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: apimodel.ErrorCode(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", "createdAt:DESC") q.Add("sort", "createdAt:ASC") q.Add("sort", "createdAt: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: apimodel.ErrorCode(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", "createdAt:") 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"][0], } assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: apimodel.ErrorCode(domainErr.Code()), Message: domainErr.Error(), Params: map[string]any{ "sort": domainErr.Sort, }, Path: []string{"$query", "sort", "0"}, }, }, }, body) }, }, { name: "ERR: sort conflict", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Add("sort", "createdAt:DESC") q.Add("sort", "createdAt: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) 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: apimodel.ErrorCode(domainErr.Code()), Message: domainErr.Error(), Params: map[string]any{ "sort": paramSort, }, Path: []string{"$query", "sort"}, }, }, }, body) }, }, { name: "ERR: invalid since", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Set("since", gofakeit.LetterN(100)) 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) var domainErr domain.Error require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr) since := req.URL.Query().Get("since") assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: "invalid-param-format", //nolint:lll Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", since, since, since), Path: []string{"$query", "since"}, }, }, }, body) }, }, { name: "ERR: invalid before", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Set("before", gofakeit.LetterN(100)) 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) var domainErr domain.Error require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr) before := req.URL.Query().Get("before") assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: "invalid-param-format", //nolint:lll Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", before, before, before), Path: []string{"$query", "before"}, }, }, }, body) }, }, { name: "ERR: version not found", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() req.URL.Path = fmt.Sprintf(endpointListTribeEnnoblements, randInvalidVersionCode(t, handler), 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, 9) domainErr := domain.VersionNotFoundError{ VersionCode: pathSegments[3], } assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: apimodel.ErrorCode(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( endpointListTribeEnnoblements, 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, 9) domainErr := domain.ServerNotFoundError{ Key: pathSegments[5], } assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: apimodel.ErrorCode(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( endpointListTribeEnnoblements, 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: apimodel.ErrorCode(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(endpointListTribeEnnoblements, server.Version.Code, 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) }) } } const endpointListVillageEnnoblements = "/v2/versions/%s/servers/%s/villages/%d/ennoblements" func TestListVillageEnnoblements(t *testing.T) { t.Parallel() handler := newAPIHTTPHandler(t) ennoblements := getAllEnnoblements(t, handler) server := ennoblements[0].Server village := ennoblements[0].Village 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.ListEnnoblementsResponse](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.Ennoblement) int { return cmp.Or( a.CreatedAt.Compare(b.CreatedAt), 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.ListEnnoblementsResponse](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.ListEnnoblementsResponse](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.ListEnnoblementsResponse](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=[createdAt:DESC]", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Set("sort", "createdAt: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.ListEnnoblementsResponse](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.Ennoblement) int { return cmp.Or( a.CreatedAt.Compare(b.CreatedAt)*-1, cmp.Compare(a.Id, b.Id), ) })) }, }, { name: "OK: since", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() var filtered []ennoblementWithServer for _, e := range ennoblements { if e.Server.Key == server.Key { filtered = append(filtered, e) } } slices.SortFunc(filtered, func(a, b ennoblementWithServer) int { return cmp.Or( a.CreatedAt.Compare(b.CreatedAt), cmp.Compare(a.Id, b.Id), ) }) require.GreaterOrEqual(t, len(filtered), 2) q := req.URL.Query() q.Set("since", filtered[1].CreatedAt.Format(time.RFC3339)) 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.ListEnnoblementsResponse](t, resp.Body) assert.Zero(t, body.Cursor.Next) assert.NotZero(t, body.Cursor.Self) assert.NotZero(t, body.Data) since, err := time.Parse(time.RFC3339, req.URL.Query().Get("since")) require.NoError(t, err) for _, e := range body.Data { assert.True(t, e.CreatedAt.After(since) || e.CreatedAt.Equal(since)) } }, }, { name: "OK: before", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() var filtered []ennoblementWithServer for _, e := range ennoblements { if e.Server.Key == server.Key { filtered = append(filtered, e) } } slices.SortFunc(filtered, func(a, b ennoblementWithServer) int { return cmp.Or( a.CreatedAt.Compare(b.CreatedAt), cmp.Compare(a.Id, b.Id), ) }) require.GreaterOrEqual(t, len(filtered), 2) q := req.URL.Query() q.Set("before", filtered[1].CreatedAt.Format(time.RFC3339)) 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.ListEnnoblementsResponse](t, resp.Body) assert.Zero(t, body.Cursor.Next) assert.NotZero(t, body.Cursor.Self) assert.NotZero(t, body.Data) before, err := time.Parse(time.RFC3339, req.URL.Query().Get("before")) require.NoError(t, err) for _, e := range body.Data { assert.True(t, e.CreatedAt.Before(before)) } }, }, { 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: apimodel.ErrorCode(domainErr.Code()), Message: domainErr.Error(), Params: map[string]any{ "current": float64(domainErr.Current), "min": float64(domainErr.Min), }, Path: []string{"$query", "limit"}, }, }, }, body) }, }, { name: fmt.Sprintf("ERR: limit > %d", domain.EnnoblementListMaxLimit), reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Set("limit", strconv.Itoa(domain.EnnoblementListMaxLimit+1)) 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: domain.EnnoblementListMaxLimit, Current: limit, } assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: apimodel.ErrorCode(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: apimodel.ErrorCode(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: apimodel.ErrorCode(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: apimodel.ErrorCode(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", "createdAt:DESC") q.Add("sort", "createdAt:ASC") q.Add("sort", "createdAt: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: apimodel.ErrorCode(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", "createdAt:") 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"][0], } assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: apimodel.ErrorCode(domainErr.Code()), Message: domainErr.Error(), Params: map[string]any{ "sort": domainErr.Sort, }, Path: []string{"$query", "sort", "0"}, }, }, }, body) }, }, { name: "ERR: sort conflict", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Add("sort", "createdAt:DESC") q.Add("sort", "createdAt: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) 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: apimodel.ErrorCode(domainErr.Code()), Message: domainErr.Error(), Params: map[string]any{ "sort": paramSort, }, Path: []string{"$query", "sort"}, }, }, }, body) }, }, { name: "ERR: invalid since", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Set("since", gofakeit.LetterN(100)) 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) var domainErr domain.Error require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr) since := req.URL.Query().Get("since") assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: "invalid-param-format", //nolint:lll Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", since, since, since), Path: []string{"$query", "since"}, }, }, }, body) }, }, { name: "ERR: invalid before", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() q := req.URL.Query() q.Set("before", gofakeit.LetterN(100)) 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) var domainErr domain.Error require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr) before := req.URL.Query().Get("before") assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: "invalid-param-format", //nolint:lll Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", before, before, before), Path: []string{"$query", "before"}, }, }, }, body) }, }, { name: "ERR: version not found", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() req.URL.Path = fmt.Sprintf( endpointListVillageEnnoblements, randInvalidVersionCode(t, handler), server.Key, village.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, 9) domainErr := domain.VersionNotFoundError{ VersionCode: pathSegments[3], } assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: apimodel.ErrorCode(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( endpointListVillageEnnoblements, server.Version.Code, domaintest.RandServerKey(), village.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, 9) domainErr := domain.ServerNotFoundError{ Key: pathSegments[5], } assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: apimodel.ErrorCode(domainErr.Code()), Message: domainErr.Error(), Params: map[string]any{ "key": domainErr.Key, }, }, }, }, body) }, }, { name: "ERR: village not found", reqModifier: func(t *testing.T, req *http.Request) { t.Helper() req.URL.Path = fmt.Sprintf( endpointListVillageEnnoblements, 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.VillageNotFoundError{ ID: id, ServerKey: pathSegments[5], } assert.Equal(t, apimodel.ErrorResponse{ Errors: []apimodel.Error{ { Code: apimodel.ErrorCode(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(endpointListVillageEnnoblements, server.Version.Code, server.Key, village.Id), nil, ) if tt.reqModifier != nil { tt.reqModifier(t, req) } resp := doCustomRequest(handler, req) defer resp.Body.Close() tt.assertResp(t, req, resp) }) } } type ennoblementWithServer struct { apimodel.Ennoblement Server serverWithVersion } func getAllEnnoblements(tb testing.TB, h http.Handler) []ennoblementWithServer { tb.Helper() servers := getAllServers(tb, h) var ennoblements []ennoblementWithServer for _, s := range servers { resp := doRequest(h, http.MethodGet, fmt.Sprintf(endpointListEnnoblements, s.Version.Code, s.Key), nil) require.Equal(tb, http.StatusOK, resp.StatusCode) for _, e := range decodeJSON[apimodel.ListEnnoblementsResponse](tb, resp.Body).Data { ennoblements = append(ennoblements, ennoblementWithServer{ Ennoblement: e, Server: s, }) } _ = resp.Body.Close() } require.NotZero(tb, ennoblements) return ennoblements }