feat: add a new endpoint - GET /api/v1/versions/:code/servers/:key/tribes/:id (#59)
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: twhelp/core#59
This commit is contained in:
Dawid Wysokiński 2022-08-31 04:53:28 +00:00
parent aed56ea34f
commit d4125c5f08
24 changed files with 437 additions and 78 deletions

View File

@ -38,38 +38,6 @@ type UserError interface {
Code() ErrorCode
}
type VersionNotFoundError struct {
VerCode string
}
func (e VersionNotFoundError) Error() string {
return fmt.Sprintf("version (code=%s) not found", e.VerCode)
}
func (e VersionNotFoundError) UserError() string {
return e.Error()
}
func (e VersionNotFoundError) Code() ErrorCode {
return ErrorCodeEntityNotFound
}
type ServerNotFoundError struct {
Key string
}
func (e ServerNotFoundError) Error() string {
return fmt.Sprintf("server (key=%s) not found", e.Key)
}
func (e ServerNotFoundError) UserError() string {
return e.Error()
}
func (e ServerNotFoundError) Code() ErrorCode {
return ErrorCodeEntityNotFound
}
type ValidationError struct {
Field string
Err error

View File

@ -17,30 +17,6 @@ func TestErrorCode_String(t *testing.T) {
assert.Equal(t, domain.ErrorCodeValidationError.String(), "validation-error")
}
func TestVersionNotFoundError(t *testing.T) {
t.Parallel()
err := domain.VersionNotFoundError{
VerCode: "pl",
}
var _ domain.UserError = err
assert.Equal(t, "version (code=pl) not found", err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code())
}
func TestServerNotFoundError(t *testing.T) {
t.Parallel()
err := domain.ServerNotFoundError{
Key: "pl151",
}
var _ domain.UserError = err
assert.Equal(t, "server (key=pl151) not found", err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code())
}
func TestValidationError(t *testing.T) {
t.Parallel()

View File

@ -1,6 +1,9 @@
package domain
import "time"
import (
"fmt"
"time"
)
type OpenServer struct {
Key string
@ -69,3 +72,19 @@ type RefreshServersCmdPayload struct {
Host string
VersionCode string
}
type ServerNotFoundError struct {
Key string
}
func (e ServerNotFoundError) Error() string {
return fmt.Sprintf("server (key=%s) not found", e.Key)
}
func (e ServerNotFoundError) UserError() string {
return e.Error()
}
func (e ServerNotFoundError) Code() ErrorCode {
return ErrorCodeEntityNotFound
}

View File

@ -97,3 +97,15 @@ func TestUpdateServerParams_IsZero(t *testing.T) {
})
}
}
func TestServerNotFoundError(t *testing.T) {
t.Parallel()
err := domain.ServerNotFoundError{
Key: "pl151",
}
var _ domain.UserError = err
assert.Equal(t, "server (key=pl151) not found", err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code())
}

View File

@ -78,3 +78,19 @@ type ListTribesParams struct {
Sort []TribeSort
Count bool
}
type TribeNotFoundError struct {
ID int64
}
func (e TribeNotFoundError) Error() string {
return fmt.Sprintf("tribe (id=%d) not found", e.ID)
}
func (e TribeNotFoundError) UserError() string {
return e.Error()
}
func (e TribeNotFoundError) Code() ErrorCode {
return ErrorCodeEntityNotFound
}

View File

@ -69,3 +69,15 @@ func TestNewTribeSortBy(t *testing.T) {
assert.Zero(t, res)
})
}
func TestTribeNotFoundError(t *testing.T) {
t.Parallel()
err := domain.TribeNotFoundError{
ID: 123,
}
var _ domain.UserError = err
assert.Equal(t, "tribe (id=123) not found", err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code())
}

View File

@ -1,5 +1,7 @@
package domain
import "fmt"
type Version struct {
Code string
Name string
@ -10,3 +12,19 @@ type Version struct {
type ListVersionsParams struct {
Count bool
}
type VersionNotFoundError struct {
VerCode string
}
func (e VersionNotFoundError) Error() string {
return fmt.Sprintf("version (code=%s) not found", e.VerCode)
}
func (e VersionNotFoundError) UserError() string {
return e.Error()
}
func (e VersionNotFoundError) Code() ErrorCode {
return ErrorCodeEntityNotFound
}

View File

@ -0,0 +1,20 @@
package domain_test
import (
"testing"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"github.com/stretchr/testify/assert"
)
func TestVersionNotFoundError(t *testing.T) {
t.Parallel()
err := domain.VersionNotFoundError{
VerCode: "pl",
}
var _ domain.UserError = err
assert.Equal(t, "version (code=pl) not found", err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code())
}

View File

@ -73,12 +73,12 @@ func NewListServersResp(servers []domain.Server) ListServersResp {
return resp
}
type GetServerByKeyResp struct {
type GetServerResp struct {
Data ExtendedServer `json:"data"`
} // @name GetServerByKeyResp
} // @name GetServerResp
func NewGetServerByKeyResp(v domain.Server) GetServerByKeyResp {
return GetServerByKeyResp{
func NewGetServerResp(v domain.Server) GetServerResp {
return GetServerResp{
Data: NewExtendedServer(v),
}
}

View File

@ -104,7 +104,7 @@ func TestNewListServersResp(t *testing.T) {
}
}
func TestNewGetServerByKeyResp(t *testing.T) {
func TestNewGetServerResp(t *testing.T) {
t.Parallel()
srv := domain.Server{
@ -126,7 +126,7 @@ func TestNewGetServerByKeyResp(t *testing.T) {
VillageDataUpdatedAt: time.Now().Add(-5 * time.Hour),
VersionCode: "pl",
}
assertExtendedServer(t, srv, model.NewGetServerByKeyResp(srv).Data)
assertExtendedServer(t, srv, model.NewGetServerResp(srv).Data)
}
func assertExtendedServer(tb testing.TB, dsrv domain.Server, rsrv model.ExtendedServer) {

View File

@ -64,3 +64,13 @@ func NewListTribesResp(tribes []domain.Tribe) ListTribesResp {
}
return resp
}
type GetTribeResp struct {
Data Tribe `json:"data"`
} // @name GetTribeResp
func NewGetTribeResp(v domain.Tribe) GetTribeResp {
return GetTribeResp{
Data: NewTribe(v),
}
}

View File

@ -105,6 +105,37 @@ func TestNewListTribesResp(t *testing.T) {
}
}
func TestNewGetTribeResp(t *testing.T) {
t.Parallel()
tribe := domain.Tribe{
BaseTribe: domain.BaseTribe{
OpponentsDefeated: domain.OpponentsDefeated{
RankAtt: 8,
ScoreAtt: 7,
RankDef: 6,
ScoreDef: 5,
RankSup: 4,
ScoreSup: 3,
RankTotal: 2,
ScoreTotal: 1,
},
ID: 997,
Name: "name 997",
Tag: "tag 997",
NumMembers: 6,
NumVillages: 5,
Points: 4,
AllPoints: 3,
Rank: 2,
},
ServerKey: "pl151",
Dominance: 12.5,
CreatedAt: time.Now(),
}
assertTribe(t, tribe, model.NewGetTribeResp(tribe).Data)
}
func assertTribe(tb testing.TB, dt domain.Tribe, rt model.Tribe) {
tb.Helper()

View File

@ -32,12 +32,12 @@ func NewListVersionsResp(versions []domain.Version) ListVersionsResp {
return resp
}
type GetVersionByCodeResp struct {
type GetVersionResp struct {
Data Version `json:"data"`
} // @name GetVersionByCodeResp
} // @name GetVersionResp
func NewGetVersionByCodeResp(v domain.Version) GetVersionByCodeResp {
return GetVersionByCodeResp{
func NewGetVersionResp(v domain.Version) GetVersionResp {
return GetVersionResp{
Data: NewVersion(v),
}
}

View File

@ -52,7 +52,7 @@ func TestNewListVersionsResp(t *testing.T) {
}
}
func TestNewGetVersionByCodeResp(t *testing.T) {
func TestNewGetVersionResp(t *testing.T) {
t.Parallel()
version := domain.Version{
@ -61,7 +61,7 @@ func TestNewGetVersionByCodeResp(t *testing.T) {
Host: "plemiona.pl",
Timezone: "Europe/Warsaw",
}
assertVersion(t, version, model.NewGetVersionByCodeResp(version).Data)
assertVersion(t, version, model.NewGetVersionResp(version).Data)
}
func assertVersion(tb testing.TB, vd domain.Version, vr model.Version) {

View File

@ -80,6 +80,7 @@ func NewRouter(cfg RouterConfig) *chi.Mux {
r.With(verifyServerKey(cfg.ServerService)).
Route("/tribes", func(r chi.Router) {
r.Get("/", tribeHandler.list)
r.Get("/{tribeId}", tribeHandler.getByID)
})
})
})
@ -194,3 +195,14 @@ func renderJSON(w http.ResponseWriter, status int, data any) {
func setTotalCountHeader(header http.Header, total int64) {
header.Set("X-Total-Count", strconv.FormatInt(total, 10))
}
func urlParamInt64(ctx *chi.Context, key string) (int64, error) {
v, err := strconv.ParseInt(ctx.URLParam(key), 10, 64)
if err != nil {
return 0, domain.ValidationError{
Field: key,
Err: err,
}
}
return v, nil
}

View File

@ -80,7 +80,7 @@ func (s *server) list(w http.ResponseWriter, r *http.Request) {
// @Description Get a server
// @Tags versions,servers
// @Produce json
// @Success 200 {object} model.GetServerByKeyResp
// @Success 200 {object} model.GetServerResp
// @Failure 404 {object} model.ErrorResp
// @Failure 500 {object} model.ErrorResp
// @Param versionCode path string true "Version code"
@ -98,5 +98,5 @@ func (s *server) getByKey(w http.ResponseWriter, r *http.Request) {
renderErr(w, err)
return
}
renderJSON(w, http.StatusOK, model.NewGetServerByKeyResp(srv))
renderJSON(w, http.StatusOK, model.NewGetServerResp(srv))
}

View File

@ -507,8 +507,8 @@ func TestServer_getByKey(t *testing.T) {
versionCode: version.Code,
key: "pl151",
expectedStatus: http.StatusOK,
target: &model.GetServerByKeyResp{},
expectedResponse: &model.GetServerByKeyResp{
target: &model.GetServerResp{},
expectedResponse: &model.GetServerResp{
Data: model.ExtendedServer{
Server: model.Server{
Key: "pl151",

View File

@ -18,6 +18,7 @@ const (
//counterfeiter:generate -o internal/mock/tribe_service.gen.go . TribeService
type TribeService interface {
List(ctx context.Context, params domain.ListTribesParams) ([]domain.Tribe, int64, error)
GetTribeByServerKeyAndID(ctx context.Context, serverKey string, id int64) (domain.Tribe, error)
}
type tribe struct {
@ -76,3 +77,34 @@ func (t *tribe) list(w http.ResponseWriter, r *http.Request) {
setTotalCountHeader(w.Header(), count)
renderJSON(w, http.StatusOK, model.NewListTribesResp(tribes))
}
// @ID getTribeByID
// @Summary Get a tribe
// @Description Get a tribe
// @Tags versions,servers,tribes
// @Produce json
// @Success 200 {object} model.GetTribeResp
// @Failure 400 {object} model.ErrorResp
// @Failure 404 {object} model.ErrorResp
// @Failure 500 {object} model.ErrorResp
// @Param versionCode path string true "Version code"
// @Param serverKey path string true "Server key"
// @Param tribeId path integer true "Tribe ID"
// @Router /versions/{versionCode}/servers/{serverKey}/tribes/{tribeId} [get]
func (t *tribe) getByID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
routeCtx := chi.RouteContext(ctx)
tribeID, err := urlParamInt64(routeCtx, "tribeId")
if err != nil {
renderErr(w, err)
return
}
trb, err := t.svc.GetTribeByServerKeyAndID(ctx, routeCtx.URLParam("serverKey"), tribeID)
if err != nil {
renderErr(w, err)
return
}
renderJSON(w, http.StatusOK, model.NewGetTribeResp(trb))
}

View File

@ -612,3 +612,172 @@ func TestTribe_list(t *testing.T) {
})
}
}
func TestTribe_getByID(t *testing.T) {
t.Parallel()
now := time.Now()
version := domain.Version{
Code: "pl",
Name: "Poland",
Host: "plemiona.pl",
Timezone: "Europe/Warsaw",
}
server := domain.Server{
Key: "pl151",
URL: "https://pl151.plemiona.pl",
Open: true,
Special: false,
VersionCode: "pl",
}
tests := []struct {
name string
setup func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, tribeSvc *mock.FakeTribeService)
versionCode string
serverKey string
id string
expectedStatus int
target any
expectedResponse any
}{
{
name: "OK",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, tribeSvc *mock.FakeTribeService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
tribeSvc.GetTribeByServerKeyAndIDReturns(domain.Tribe{
BaseTribe: domain.BaseTribe{
OpponentsDefeated: domain.OpponentsDefeated{
RankAtt: 8,
ScoreAtt: 7,
RankDef: 6,
ScoreDef: 5,
RankSup: 4,
ScoreSup: 3,
RankTotal: 2,
ScoreTotal: 1,
},
ID: 997,
Name: "name 997",
Tag: "tag 997",
NumMembers: 6,
NumVillages: 5,
Points: 4,
AllPoints: 3,
Rank: 2,
},
ServerKey: server.Key,
Dominance: 12.5,
CreatedAt: now,
}, nil)
},
versionCode: version.Code,
serverKey: "pl151",
id: "997",
expectedStatus: http.StatusOK,
target: &model.GetTribeResp{},
expectedResponse: &model.GetTribeResp{
Data: model.Tribe{
RankAtt: 8,
ScoreAtt: 7,
RankDef: 6,
ScoreDef: 5,
RankTotal: 2,
ScoreTotal: 1,
ID: 997,
Name: "name 997",
Tag: "tag 997",
NumMembers: 6,
NumVillages: 5,
Points: 4,
AllPoints: 3,
Rank: 2,
Dominance: 12.5,
CreatedAt: now,
},
},
},
{
name: "ERR: tribe id is not a valid int64",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, tribeSvc *mock.FakeTribeService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
},
versionCode: version.Code,
serverKey: "pl1512",
id: "true",
expectedStatus: http.StatusBadRequest,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeValidationError.String(),
Message: "tribeId: strconv.ParseInt: parsing \"true\": invalid syntax",
},
},
},
{
name: "ERR: version not found",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, tribeSvc *mock.FakeTribeService) {
versionSvc.GetByCodeReturns(domain.Version{}, domain.VersionNotFoundError{VerCode: version.Code + "2"})
},
versionCode: version.Code + "2",
serverKey: "pl151",
id: "1234",
expectedStatus: http.StatusNotFound,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeEntityNotFound.String(),
Message: fmt.Sprintf("version (code=%s) not found", version.Code+"2"),
},
},
},
{
name: "ERR: server not found",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, tribeSvc *mock.FakeTribeService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(domain.Server{}, domain.ServerNotFoundError{Key: "pl1512"})
},
versionCode: version.Code,
serverKey: "pl1512",
id: "1234",
expectedStatus: http.StatusNotFound,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeEntityNotFound.String(),
Message: "server (key=pl1512) not found",
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
versionSvc := &mock.FakeVersionService{}
serverSvc := &mock.FakeServerService{}
tribeSvc := &mock.FakeTribeService{}
tt.setup(versionSvc, serverSvc, tribeSvc)
router := rest.NewRouter(rest.RouterConfig{
VersionService: versionSvc,
ServerService: serverSvc,
TribeService: tribeSvc,
})
target := fmt.Sprintf(
"/v1/versions/%s/servers/%s/tribes/%s",
tt.versionCode,
tt.serverKey,
tt.id,
)
resp := doRequest(router, http.MethodGet, target, nil)
defer resp.Body.Close()
assertJSONResponse(t, resp, tt.expectedStatus, tt.expectedResponse, tt.target)
})
}
}

View File

@ -45,7 +45,7 @@ func (v *version) list(w http.ResponseWriter, r *http.Request) {
// @Description Get a version
// @Tags versions
// @Produce json
// @Success 200 {object} model.GetVersionByCodeResp
// @Success 200 {object} model.GetVersionResp
// @Failure 404 {object} model.ErrorResp
// @Failure 500 {object} model.ErrorResp
// @Param versionCode path string true "Version code"
@ -57,5 +57,5 @@ func (v *version) getByCode(w http.ResponseWriter, r *http.Request) {
renderErr(w, err)
return
}
renderJSON(w, http.StatusOK, model.NewGetVersionByCodeResp(ver))
renderJSON(w, http.StatusOK, model.NewGetVersionResp(ver))
}

View File

@ -119,8 +119,8 @@ func TestVersion_getByCode(t *testing.T) {
},
code: "pl",
expectedStatus: http.StatusOK,
target: &model.GetVersionByCodeResp{},
expectedResponse: &model.GetVersionByCodeResp{
target: &model.GetVersionResp{},
expectedResponse: &model.GetVersionResp{
Data: model.Version{
Code: "pl",
Name: "Poland",

View File

@ -189,7 +189,6 @@ func (s *Server) List(ctx context.Context, params domain.ListServersParams) ([]d
}
func (s *Server) GetNormalByVersionCodeAndKey(ctx context.Context, versionCode, key string) (domain.Server, error) {
// TODO: this functions should also take versionCode as an argument
servers, _, err := s.repo.List(ctx, domain.ListServersParams{
Special: domain.NullBool{
Bool: false,

View File

@ -123,3 +123,22 @@ func (t *Tribe) List(ctx context.Context, params domain.ListTribesParams) ([]dom
}
return servers, count, nil
}
func (t *Tribe) GetTribeByServerKeyAndID(ctx context.Context, serverKey string, id int64) (domain.Tribe, error) {
tribes, _, err := t.repo.List(ctx, domain.ListTribesParams{
IDs: []int64{id},
ServerKeys: []string{serverKey},
Pagination: domain.Pagination{
Limit: 1,
},
})
if err != nil {
return domain.Tribe{}, fmt.Errorf("TribeRepository.List: %w", err)
}
if len(tribes) == 0 {
return domain.Tribe{}, domain.TribeNotFoundError{
ID: id,
}
}
return tribes[0], nil
}

View File

@ -304,3 +304,49 @@ func TestTribe_List(t *testing.T) {
}
})
}
func TestTribe_GetTribeByServerKeyAndID(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
tribe := domain.Tribe{
BaseTribe: domain.BaseTribe{
ID: 123,
},
ServerKey: "pl151",
}
repo := &mock.FakeTribeRepository{}
repo.ListReturns([]domain.Tribe{tribe}, 0, nil)
client := &mock.FakeTribesGetter{}
res, err := service.NewTribe(repo, client).
GetTribeByServerKeyAndID(context.Background(), tribe.ServerKey, tribe.ID)
assert.NoError(t, err)
assert.Equal(t, tribe, res)
require.Equal(t, 1, repo.ListCallCount())
_, params := repo.ListArgsForCall(0)
assert.Equal(t, domain.ListTribesParams{
IDs: []int64{tribe.ID},
ServerKeys: []string{tribe.ServerKey},
Pagination: domain.Pagination{
Limit: 1,
},
}, params)
})
t.Run("ERR: tribe not found", func(t *testing.T) {
t.Parallel()
repo := &mock.FakeTribeRepository{}
client := &mock.FakeTribesGetter{}
var id int64 = 123
res, err := service.NewTribe(repo, client).
GetTribeByServerKeyAndID(context.Background(), "pl151", id)
assert.ErrorIs(t, err, domain.TribeNotFoundError{ID: id})
assert.Zero(t, res)
})
}