diff --git a/api/openapi3.yml b/api/openapi3.yml index e2776dd..af5af47 100644 --- a/api/openapi3.yml +++ b/api/openapi3.yml @@ -325,6 +325,75 @@ paths: $ref: "#/components/responses/ListEnnoblementsResponse" default: $ref: "#/components/responses/ErrorResponse" + /v2/versions/{versionCode}/servers/{serverKey}/players/{playerId}/ennoblements: + get: + operationId: listPlayerEnnoblements + tags: + - versions + - servers + - players + - ennoblements + description: List player ennoblements + parameters: + - $ref: "#/components/parameters/VersionCodePathParam" + - $ref: "#/components/parameters/ServerKeyPathParam" + - $ref: "#/components/parameters/PlayerIdPathParam" + - $ref: "#/components/parameters/CursorQueryParam" + - $ref: "#/components/parameters/LimitQueryParam" + - $ref: "#/components/parameters/EnnoblementSortQueryParam" + - $ref: "#/components/parameters/SinceQueryParam" + - $ref: "#/components/parameters/BeforeQueryParam" + responses: + 200: + $ref: "#/components/responses/ListEnnoblementsResponse" + default: + $ref: "#/components/responses/ErrorResponse" + /v2/versions/{versionCode}/servers/{serverKey}/tribes/{tribeId}/ennoblements: + get: + operationId: listTribeEnnoblements + tags: + - versions + - servers + - tribes + - ennoblements + description: List tribe ennoblements + parameters: + - $ref: "#/components/parameters/VersionCodePathParam" + - $ref: "#/components/parameters/ServerKeyPathParam" + - $ref: "#/components/parameters/TribeIdPathParam" + - $ref: "#/components/parameters/CursorQueryParam" + - $ref: "#/components/parameters/LimitQueryParam" + - $ref: "#/components/parameters/EnnoblementSortQueryParam" + - $ref: "#/components/parameters/SinceQueryParam" + - $ref: "#/components/parameters/BeforeQueryParam" + responses: + 200: + $ref: "#/components/responses/ListEnnoblementsResponse" + default: + $ref: "#/components/responses/ErrorResponse" + /v2/versions/{versionCode}/servers/{serverKey}/villages/{villageId}/ennoblements: + get: + operationId: listVillageEnnoblements + tags: + - versions + - servers + - villages + - ennoblements + description: List village ennoblements + parameters: + - $ref: "#/components/parameters/VersionCodePathParam" + - $ref: "#/components/parameters/ServerKeyPathParam" + - $ref: "#/components/parameters/VillageIdPathParam" + - $ref: "#/components/parameters/CursorQueryParam" + - $ref: "#/components/parameters/LimitQueryParam" + - $ref: "#/components/parameters/EnnoblementSortQueryParam" + - $ref: "#/components/parameters/SinceQueryParam" + - $ref: "#/components/parameters/BeforeQueryParam" + responses: + 200: + $ref: "#/components/responses/ListEnnoblementsResponse" + default: + $ref: "#/components/responses/ErrorResponse" components: schemas: Error: diff --git a/internal/adapter/repository_bun_ennoblement.go b/internal/adapter/repository_bun_ennoblement.go index 60c3331..4d9c537 100644 --- a/internal/adapter/repository_bun_ennoblement.go +++ b/internal/adapter/repository_bun_ennoblement.go @@ -118,7 +118,7 @@ func (a listEnnoblementsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuer q = q.Where("ennoblement.server_key IN (?)", bun.In(serverKeys)) } - if villageIDs := a.params.PlayerIDs(); len(villageIDs) > 0 { + if villageIDs := a.params.VillageIDs(); len(villageIDs) > 0 { q = q.Where("ennoblement.village_id IN (?)", bun.In(villageIDs)) } diff --git a/internal/port/handler_http_api_ennoblement.go b/internal/port/handler_http_api_ennoblement.go index fdebe78..86cad88 100644 --- a/internal/port/handler_http_api_ennoblement.go +++ b/internal/port/handler_http_api_ennoblement.go @@ -83,6 +83,249 @@ func (h *apiHTTPHandler) ListEnnoblements( renderJSON(w, r, http.StatusOK, apimodel.NewListEnnoblementsResponse(res)) } +//nolint:gocyclo +func (h *apiHTTPHandler) ListPlayerEnnoblements( + w http.ResponseWriter, + r *http.Request, + _ apimodel.VersionCodePathParam, + serverKey apimodel.ServerKeyPathParam, + playerID apimodel.PlayerIdPathParam, + params apimodel.ListPlayerEnnoblementsParams, +) { + domainParams := domain.NewListEnnoblementsParams() + + if err := domainParams.SetSort([]domain.EnnoblementSort{domain.EnnoblementSortIDASC}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + + if err := domainParams.SetPlayerIDs([]int{playerID}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + + if params.Sort != nil { + if err := domainParams.PrependSortString(*params.Sort); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } else { + if err := domainParams.PrependSortString([]string{domain.EnnoblementSortCreatedAtASC.String()}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + if err := domainParams.SetServerKeys([]string{serverKey}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + + if params.Before != nil { + if err := domainParams.SetBefore(domain.NullTime{ + V: *params.Before, + Valid: true, + }); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + if params.Since != nil { + if err := domainParams.SetSince(domain.NullTime{ + V: *params.Since, + Valid: true, + }); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + if params.Limit != nil { + if err := domainParams.SetLimit(*params.Limit); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + if params.Cursor != nil { + if err := domainParams.SetEncodedCursor(*params.Cursor); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + res, err := h.ennoblementSvc.ListWithRelations(r.Context(), domainParams) + if err != nil { + h.errorRenderer.render(w, r, err) + return + } + + renderJSON(w, r, http.StatusOK, apimodel.NewListEnnoblementsResponse(res)) +} + +//nolint:gocyclo +func (h *apiHTTPHandler) ListTribeEnnoblements( + w http.ResponseWriter, + r *http.Request, + _ apimodel.VersionCodePathParam, + serverKey apimodel.ServerKeyPathParam, + tribeID apimodel.TribeIdPathParam, + params apimodel.ListTribeEnnoblementsParams, +) { + domainParams := domain.NewListEnnoblementsParams() + + if err := domainParams.SetSort([]domain.EnnoblementSort{domain.EnnoblementSortIDASC}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + + if err := domainParams.SetTribeIDs([]int{tribeID}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + + if params.Sort != nil { + if err := domainParams.PrependSortString(*params.Sort); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } else { + if err := domainParams.PrependSortString([]string{domain.EnnoblementSortCreatedAtASC.String()}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + if err := domainParams.SetServerKeys([]string{serverKey}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + + if params.Before != nil { + if err := domainParams.SetBefore(domain.NullTime{ + V: *params.Before, + Valid: true, + }); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + if params.Since != nil { + if err := domainParams.SetSince(domain.NullTime{ + V: *params.Since, + Valid: true, + }); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + if params.Limit != nil { + if err := domainParams.SetLimit(*params.Limit); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + if params.Cursor != nil { + if err := domainParams.SetEncodedCursor(*params.Cursor); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + res, err := h.ennoblementSvc.ListWithRelations(r.Context(), domainParams) + if err != nil { + h.errorRenderer.render(w, r, err) + return + } + + renderJSON(w, r, http.StatusOK, apimodel.NewListEnnoblementsResponse(res)) +} + +//nolint:gocyclo +func (h *apiHTTPHandler) ListVillageEnnoblements( + w http.ResponseWriter, + r *http.Request, + _ apimodel.VersionCodePathParam, + serverKey apimodel.ServerKeyPathParam, + villageID apimodel.VillageIdPathParam, + params apimodel.ListVillageEnnoblementsParams, +) { + domainParams := domain.NewListEnnoblementsParams() + + if err := domainParams.SetSort([]domain.EnnoblementSort{domain.EnnoblementSortIDASC}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + + if err := domainParams.SetVillageIDs([]int{villageID}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + + if params.Sort != nil { + if err := domainParams.PrependSortString(*params.Sort); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } else { + if err := domainParams.PrependSortString([]string{domain.EnnoblementSortCreatedAtASC.String()}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + if err := domainParams.SetServerKeys([]string{serverKey}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + + if params.Before != nil { + if err := domainParams.SetBefore(domain.NullTime{ + V: *params.Before, + Valid: true, + }); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + if params.Since != nil { + if err := domainParams.SetSince(domain.NullTime{ + V: *params.Since, + Valid: true, + }); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + if params.Limit != nil { + if err := domainParams.SetLimit(*params.Limit); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + if params.Cursor != nil { + if err := domainParams.SetEncodedCursor(*params.Cursor); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + res, err := h.ennoblementSvc.ListWithRelations(r.Context(), domainParams) + if err != nil { + h.errorRenderer.render(w, r, err) + return + } + + renderJSON(w, r, http.StatusOK, apimodel.NewListEnnoblementsResponse(res)) +} + func formatListEnnoblementsErrorPath(segments []domain.ErrorPathSegment) []string { if segments[0].Model != "ListEnnoblementsParams" { return nil diff --git a/internal/port/handler_http_api_ennoblement_test.go b/internal/port/handler_http_api_ennoblement_test.go index dc21095..e0139a3 100644 --- a/internal/port/handler_http_api_ennoblement_test.go +++ b/internal/port/handler_http_api_ennoblement_test.go @@ -120,11 +120,11 @@ func TestListEnnoblements(t *testing.T) { assert.Equal(t, http.StatusOK, resp.StatusCode) // body - body := decodeJSON[apimodel.ListPlayersResponse](t, resp.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.Player) int { + 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), @@ -665,6 +665,2090 @@ func TestListEnnoblements(t *testing.T) { } } +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: after", + 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: 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: 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", "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: 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: 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: 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, domaintest.RandVersionCode(), 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: 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: 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: 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: after", + 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: 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: 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", "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: 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: 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: 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, domaintest.RandVersionCode(), 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: 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: 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: 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: after", + 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: 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: 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", "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: 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: 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: 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, domaintest.RandVersionCode(), 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: 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: 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: 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