feat: add a new endpoint - GET /api/v1/versions/:code/servers
Some checks failed
continuous-integration/drone/pr Build is failing

This commit is contained in:
Dawid Wysokiński 2022-08-26 16:24:10 +02:00
parent 8f7322013a
commit 3cdbbde5ee
Signed by: Kichiyaki
GPG Key ID: B5445E357FB8B892
6 changed files with 549 additions and 34 deletions

View File

@ -7,33 +7,27 @@ import (
)
type Server struct {
Key string `json:"key" example:"pl151"`
URL string `json:"url" example:"https://pl151.plemiona.pl"`
Open bool `json:"open"`
NumPlayers int64 `json:"numPlayers"`
NumTribes int64 `json:"numTribes"`
NumVillages int64 `json:"numVillages"`
Config ServerConfig `json:"config"`
BuildingInfo BuildingInfo `json:"buildingInfo"`
UnitInfo UnitInfo `json:"unitInfo"`
CreatedAt time.Time `json:"createdAt" format:"date-time"`
PlayerDataUpdatedAt NullTime `json:"playerDataUpdatedAt" swaggertype:"string" format:"date-time" extensions:"x-nullable"`
TribeDataUpdatedAt NullTime `json:"tribeDataUpdatedAt" swaggertype:"string" format:"date-time" extensions:"x-nullable"`
VillageDataUpdatedAt NullTime `json:"villageDataUpdatedAt" swaggertype:"string" format:"date-time" extensions:"x-nullable"`
Key string `json:"key" example:"pl151"`
URL string `json:"url" example:"https://pl151.plemiona.pl"`
Open bool `json:"open"`
NumPlayers int64 `json:"numPlayers"`
NumTribes int64 `json:"numTribes"`
NumVillages int64 `json:"numVillages"`
CreatedAt time.Time `json:"createdAt" format:"date-time"`
PlayerDataUpdatedAt NullTime `json:"playerDataUpdatedAt" swaggertype:"string" format:"date-time" extensions:"x-nullable"`
TribeDataUpdatedAt NullTime `json:"tribeDataUpdatedAt" swaggertype:"string" format:"date-time" extensions:"x-nullable"`
VillageDataUpdatedAt NullTime `json:"villageDataUpdatedAt" swaggertype:"string" format:"date-time" extensions:"x-nullable"`
} // @name Server
func NewServer(s domain.Server) Server {
return Server{
Key: s.Key,
URL: s.URL,
Open: s.Open,
NumPlayers: s.NumPlayers,
NumTribes: s.NumTribes,
NumVillages: s.NumVillages,
Config: NewServerConfig(s.Config),
BuildingInfo: NewBuildingInfo(s.BuildingInfo),
UnitInfo: NewUnitInfo(s.UnitInfo),
CreatedAt: s.CreatedAt,
Key: s.Key,
URL: s.URL,
Open: s.Open,
NumPlayers: s.NumPlayers,
NumTribes: s.NumTribes,
NumVillages: s.NumVillages,
CreatedAt: s.CreatedAt,
PlayerDataUpdatedAt: NullTime{
Time: s.PlayerDataUpdatedAt,
Valid: !s.PlayerDataUpdatedAt.IsZero(),
@ -49,6 +43,22 @@ func NewServer(s domain.Server) Server {
}
}
type ExtendedServer struct {
Server
Config ServerConfig `json:"config"`
BuildingInfo BuildingInfo `json:"buildingInfo"`
UnitInfo UnitInfo `json:"unitInfo"`
} // @name ExtendedServer
func NewExtendedServer(s domain.Server) ExtendedServer {
return ExtendedServer{
Server: NewServer(s),
Config: NewServerConfig(s.Config),
BuildingInfo: NewBuildingInfo(s.BuildingInfo),
UnitInfo: NewUnitInfo(s.UnitInfo),
}
}
type ListServersResp struct {
Data []Server `json:"data"`
} // @name ListServersResp

View File

@ -33,6 +33,30 @@ func TestNewServer(t *testing.T) {
assertServer(t, srv, model.NewServer(srv))
}
func TestNewExtendedServer(t *testing.T) {
t.Parallel()
srv := domain.Server{
Key: "pl151",
URL: "https://pl151.plemiona.pl",
Open: true,
Special: false,
NumPlayers: 1234,
NumTribes: 1235,
NumVillages: 1236,
Config: domain.ServerConfig{
Speed: 0.25,
},
BuildingInfo: domain.BuildingInfo{},
UnitInfo: domain.UnitInfo{},
CreatedAt: time.Now().Add(-20 * time.Hour),
PlayerDataUpdatedAt: time.Time{},
TribeDataUpdatedAt: time.Now(),
VillageDataUpdatedAt: time.Now().Add(-5 * time.Hour),
VersionCode: "pl",
}
assertExtendedServer(t, srv, model.NewExtendedServer(srv))
}
func TestNewListServersResp(t *testing.T) {
t.Parallel()
@ -79,6 +103,15 @@ func TestNewListServersResp(t *testing.T) {
}
}
func assertExtendedServer(tb testing.TB, dsrv domain.Server, rsrv model.ExtendedServer) {
tb.Helper()
assertServer(tb, dsrv, rsrv.Server)
assertServerConfig(tb, dsrv.Config, rsrv.Config)
assertBuildingInfo(tb, dsrv.BuildingInfo, rsrv.BuildingInfo)
assertUnitInfo(tb, dsrv.UnitInfo, rsrv.UnitInfo)
}
func assertServer(tb testing.TB, dsrv domain.Server, rsrv model.Server) {
tb.Helper()
@ -88,9 +121,6 @@ func assertServer(tb testing.TB, dsrv domain.Server, rsrv model.Server) {
assert.Equal(tb, dsrv.NumPlayers, rsrv.NumPlayers)
assert.Equal(tb, dsrv.NumTribes, rsrv.NumTribes)
assert.Equal(tb, dsrv.NumVillages, rsrv.NumVillages)
assertServerConfig(tb, dsrv.Config, rsrv.Config)
assertBuildingInfo(tb, dsrv.BuildingInfo, rsrv.BuildingInfo)
assertUnitInfo(tb, dsrv.UnitInfo, rsrv.UnitInfo)
assert.Equal(tb, dsrv.CreatedAt, rsrv.CreatedAt)
assertNullTime(tb, dsrv.PlayerDataUpdatedAt, rsrv.PlayerDataUpdatedAt)
assertNullTime(tb, dsrv.TribeDataUpdatedAt, rsrv.TribeDataUpdatedAt)

View File

@ -8,12 +8,46 @@ import (
"testing"
"time"
"gitea.dwysokinski.me/twhelp/core/internal/rest"
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model"
"github.com/google/uuid"
"github.com/go-chi/chi/v5"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert"
)
func TestRouteNotFound(t *testing.T) {
t.Parallel()
expectedResp := &model.ErrorResp{
Error: model.APIError{
Code: "route-not-found",
Message: "route not found",
},
}
resp := doRequest(rest.NewRouter(rest.RouterConfig{}), http.MethodGet, "/v1/"+uuid.NewString(), nil)
defer resp.Body.Close()
assertJSONResponse(t, resp, http.StatusNotFound, expectedResp, &model.ErrorResp{})
}
func TestMethodNotAllowed(t *testing.T) {
t.Parallel()
expectedResp := &model.ErrorResp{
Error: model.APIError{
Code: "method-not-allowed",
Message: "method not allowed",
},
}
resp := doRequest(rest.NewRouter(rest.RouterConfig{}), http.MethodPost, "/v1/versions", nil)
defer resp.Body.Close()
assertJSONResponse(t, resp, http.StatusMethodNotAllowed, expectedResp, &model.ErrorResp{})
}
func doRequest(mux chi.Router, method, target string, body io.Reader) *http.Response {
rr := httptest.NewRecorder()
req := httptest.NewRequest(method, target, body)

View File

@ -13,7 +13,7 @@ import (
const (
serverDefaultOffset = 0
serverDefaultLimit = 1000
serverDefaultLimit = 2000
)
//counterfeiter:generate -o internal/mock/server_service.gen.go . ServerService
@ -31,13 +31,14 @@ type server struct {
// @Tags versions,servers
// @Produce json
// @Success 200 {object} model.ListServersResp
// @Success 400 {object} model.ErrorResp
// @Success 404 {object} model.ErrorResp
// @Failure 500 {object} model.ErrorResp
// @Header 200 {integer} X-Total-Count "Total number of records"
// @Param versionCode path string true "Version code"
// @Param open query boolean false "true=only open servers, false=only closed servers, by default both open and closed servers are returned"
// @Param offset query int false "specifies where to start a page" minimum(0) default(0)
// @Param limit query int false "page size" minimum(1) maximum(1000) default(1000)
// @Param limit query int false "page size" minimum(1) maximum(2000) default(2000)
// @Router /versions/{versionCode}/servers [get]
func (s *server) list(w http.ResponseWriter, r *http.Request) {
var err error

View File

@ -0,0 +1,444 @@
package rest_test
import (
"context"
"errors"
"net/http"
"net/url"
"testing"
"time"
"gitea.dwysokinski.me/twhelp/core/internal/rest"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/mock"
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model"
)
func TestServer_list(t *testing.T) {
t.Parallel()
version := domain.Version{
Code: "pl",
Name: "Poland",
Host: "plemiona.pl",
Timezone: "Europe/Warsaw",
}
now := time.Now()
tests := []struct {
name string
setup func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService)
versionCode string
queryParams url.Values
expectedStatus int
target any
expectedResponse any
expectedTotalCount string
}{
{
name: "OK: without params",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.ListCalls(func(_ context.Context, params domain.ListServersParams) ([]domain.Server, int64, error) {
if len(params.VersionCodes) == 0 || params.VersionCodes[0] != version.Code {
return nil, 0, errors.New("invalid version code")
}
if params.Open.Valid {
return nil, 0, errors.New("params.Open.Valid should be false")
}
if params.Pagination.Limit != 2000 {
return nil, 0, errors.New("params.Pagination.Limit should be 2000")
}
if params.Pagination.Offset != 0 {
return nil, 0, errors.New("params.Pagination.Offset should be 0")
}
servers := []domain.Server{
{
Key: "pl151",
URL: "https://pl151.plemiona.pl",
Open: true,
Special: false,
NumPlayers: 1234,
NumTribes: 1235,
NumVillages: 1236,
Config: domain.ServerConfig{
Speed: 0.25,
},
BuildingInfo: domain.BuildingInfo{},
UnitInfo: domain.UnitInfo{},
CreatedAt: now.Add(-20 * time.Hour),
PlayerDataUpdatedAt: time.Time{},
TribeDataUpdatedAt: now,
VillageDataUpdatedAt: now.Add(-5 * time.Hour),
VersionCode: "pl",
},
{
Key: "pl152",
URL: "https://pl152.plemiona.pl",
Open: true,
Special: false,
NumPlayers: 2234,
NumTribes: 2235,
NumVillages: 2236,
Config: domain.ServerConfig{},
BuildingInfo: domain.BuildingInfo{},
UnitInfo: domain.UnitInfo{},
CreatedAt: now.Add(-20 * time.Hour),
PlayerDataUpdatedAt: now,
TribeDataUpdatedAt: now,
VillageDataUpdatedAt: now.Add(-5 * time.Hour),
VersionCode: "pl",
},
}
return servers, int64(len(servers)), nil
})
},
versionCode: version.Code,
expectedStatus: 200,
target: &model.ListServersResp{},
expectedResponse: &model.ListServersResp{
Data: []model.Server{
{
Key: "pl151",
URL: "https://pl151.plemiona.pl",
Open: true,
NumPlayers: 1234,
NumTribes: 1235,
NumVillages: 1236,
CreatedAt: now.Add(-20 * time.Hour),
PlayerDataUpdatedAt: model.NullTime{},
TribeDataUpdatedAt: model.NullTime{
Valid: true,
Time: now,
},
VillageDataUpdatedAt: model.NullTime{
Valid: true,
Time: now.Add(-5 * time.Hour),
},
},
{
Key: "pl152",
URL: "https://pl152.plemiona.pl",
Open: true,
NumPlayers: 2234,
NumTribes: 2235,
NumVillages: 2236,
CreatedAt: now.Add(-20 * time.Hour),
PlayerDataUpdatedAt: model.NullTime{
Valid: true,
Time: now,
},
TribeDataUpdatedAt: model.NullTime{
Valid: true,
Time: now,
},
VillageDataUpdatedAt: model.NullTime{
Valid: true,
Time: now.Add(-5 * time.Hour),
},
},
},
},
expectedTotalCount: "2",
},
{
name: "OK: open=true,limit=500,offset=25",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.ListCalls(func(_ context.Context, params domain.ListServersParams) ([]domain.Server, int64, error) {
if len(params.VersionCodes) == 0 || params.VersionCodes[0] != version.Code {
return nil, 0, errors.New("invalid version code")
}
if !params.Open.Valid || !params.Open.Bool {
return nil, 0, errors.New("params.Open.Bool should be true")
}
if params.Pagination.Limit != 500 {
return nil, 0, errors.New("params.Pagination.Limit should be 500")
}
if params.Pagination.Offset != 25 {
return nil, 0, errors.New("params.Pagination.Offset should be 25")
}
servers := []domain.Server{
{
Key: "pl151",
URL: "https://pl151.plemiona.pl",
Open: true,
Special: false,
NumPlayers: 1234,
NumTribes: 1235,
NumVillages: 1236,
Config: domain.ServerConfig{
Speed: 0.25,
},
BuildingInfo: domain.BuildingInfo{},
UnitInfo: domain.UnitInfo{},
CreatedAt: now.Add(-20 * time.Hour),
PlayerDataUpdatedAt: time.Time{},
TribeDataUpdatedAt: now,
VillageDataUpdatedAt: now.Add(-5 * time.Hour),
VersionCode: "pl",
},
{
Key: "pl152",
URL: "https://pl152.plemiona.pl",
Open: true,
Special: false,
NumPlayers: 2234,
NumTribes: 2235,
NumVillages: 2236,
Config: domain.ServerConfig{},
BuildingInfo: domain.BuildingInfo{},
UnitInfo: domain.UnitInfo{},
CreatedAt: now.Add(-20 * time.Hour),
PlayerDataUpdatedAt: now,
TribeDataUpdatedAt: now,
VillageDataUpdatedAt: now.Add(-5 * time.Hour),
VersionCode: "pl",
},
}
return servers, int64(len(servers)) + 25, nil
})
},
versionCode: version.Code,
queryParams: url.Values{
"offset": []string{"25"},
"open": []string{"true"},
"limit": []string{"500"},
},
expectedStatus: 200,
target: &model.ListServersResp{},
expectedResponse: &model.ListServersResp{
Data: []model.Server{
{
Key: "pl151",
URL: "https://pl151.plemiona.pl",
Open: true,
NumPlayers: 1234,
NumTribes: 1235,
NumVillages: 1236,
CreatedAt: now.Add(-20 * time.Hour),
PlayerDataUpdatedAt: model.NullTime{},
TribeDataUpdatedAt: model.NullTime{
Valid: true,
Time: now,
},
VillageDataUpdatedAt: model.NullTime{
Valid: true,
Time: now.Add(-5 * time.Hour),
},
},
{
Key: "pl152",
URL: "https://pl152.plemiona.pl",
Open: true,
NumPlayers: 2234,
NumTribes: 2235,
NumVillages: 2236,
CreatedAt: now.Add(-20 * time.Hour),
PlayerDataUpdatedAt: model.NullTime{
Valid: true,
Time: now,
},
TribeDataUpdatedAt: model.NullTime{
Valid: true,
Time: now,
},
VillageDataUpdatedAt: model.NullTime{
Valid: true,
Time: now.Add(-5 * time.Hour),
},
},
},
},
expectedTotalCount: "27",
},
{
name: "OK: open=false,limit=1",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.ListCalls(func(_ context.Context, params domain.ListServersParams) ([]domain.Server, int64, error) {
if len(params.VersionCodes) == 0 || params.VersionCodes[0] != version.Code {
return nil, 0, errors.New("invalid version code")
}
if !params.Open.Valid || params.Open.Bool {
return nil, 0, errors.New("params.Open.Bool should be false")
}
if params.Pagination.Limit != 1 {
return nil, 0, errors.New("params.Pagination.Limit should be 1")
}
if params.Pagination.Offset != 0 {
return nil, 0, errors.New("params.Pagination.Offset should be 0")
}
servers := []domain.Server{
{
Key: "pl151",
URL: "https://pl151.plemiona.pl",
Open: false,
Special: false,
NumPlayers: 1234,
NumTribes: 1235,
NumVillages: 1236,
Config: domain.ServerConfig{
Speed: 0.25,
},
BuildingInfo: domain.BuildingInfo{},
UnitInfo: domain.UnitInfo{},
CreatedAt: now.Add(-20 * time.Hour),
PlayerDataUpdatedAt: time.Time{},
TribeDataUpdatedAt: now,
VillageDataUpdatedAt: now.Add(-5 * time.Hour),
VersionCode: "pl",
},
}
return servers, int64(len(servers)), nil
})
},
versionCode: version.Code,
queryParams: url.Values{
"open": []string{"false"},
"limit": []string{"1"},
},
expectedStatus: 200,
target: &model.ListServersResp{},
expectedResponse: &model.ListServersResp{
Data: []model.Server{
{
Key: "pl151",
URL: "https://pl151.plemiona.pl",
Open: false,
NumPlayers: 1234,
NumTribes: 1235,
NumVillages: 1236,
CreatedAt: now.Add(-20 * time.Hour),
PlayerDataUpdatedAt: model.NullTime{},
TribeDataUpdatedAt: model.NullTime{
Valid: true,
Time: now,
},
VillageDataUpdatedAt: model.NullTime{
Valid: true,
Time: now.Add(-5 * time.Hour),
},
},
},
},
expectedTotalCount: "1",
},
{
name: "ERR: limit is not a valid int32",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService) {
versionSvc.GetByCodeReturns(version, nil)
},
versionCode: version.Code,
queryParams: url.Values{
"limit": []string{"asd"},
},
expectedStatus: 400,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeValidationError.String(),
Message: "limit: the string 'asd' is not a valid value for int32",
},
},
expectedTotalCount: "",
},
{
name: "ERR: offset is not a valid int32",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService) {
versionSvc.GetByCodeReturns(version, nil)
},
versionCode: version.Code,
queryParams: url.Values{
"offset": []string{"asd"},
},
expectedStatus: 400,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeValidationError.String(),
Message: "offset: the string 'asd' is not a valid value for int32",
},
},
expectedTotalCount: "",
},
{
name: "ERR: open is not a valid boolean",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService) {
versionSvc.GetByCodeReturns(version, nil)
},
versionCode: version.Code,
queryParams: url.Values{
"open": []string{"asd"},
},
expectedStatus: 400,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeValidationError.String(),
Message: "open: the string 'asd' is not a valid boolean value",
},
},
expectedTotalCount: "",
},
{
name: "ERR: version not found",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService) {
versionSvc.GetByCodeReturns(domain.Version{}, domain.ErrVersionNotFound)
},
versionCode: "pl2",
expectedStatus: 404,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrVersionNotFound.Code().String(),
Message: domain.ErrVersionNotFound.Message(),
},
},
expectedTotalCount: "",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
versionSvc := &mock.FakeVersionService{}
serverSvc := &mock.FakeServerService{}
tt.setup(versionSvc, serverSvc)
router := rest.NewRouter(rest.RouterConfig{
VersionService: versionSvc,
ServerService: serverSvc,
})
resp := doRequest(
router,
http.MethodGet,
"/v1/versions/"+tt.versionCode+"/servers?"+tt.queryParams.Encode(),
nil,
)
defer resp.Body.Close()
assertTotalCount(t, resp.Header, tt.expectedTotalCount)
assertJSONResponse(t, resp, tt.expectedStatus, tt.expectedResponse, tt.target)
})
}
}

View File

@ -82,9 +82,7 @@ func TestVersionHandler_list(t *testing.T) {
t.Parallel()
svc := &mock.FakeVersionService{}
if tt.setup != nil {
tt.setup(svc)
}
tt.setup(svc)
router := rest.NewRouter(rest.RouterConfig{
VersionService: svc,
@ -155,9 +153,7 @@ func TestVersion_getByCode(t *testing.T) {
t.Parallel()
svc := &mock.FakeVersionService{}
if tt.setup != nil {
tt.setup(svc)
}
tt.setup(svc)
router := rest.NewRouter(rest.RouterConfig{
VersionService: svc,