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

Reviewed-on: twhelp/core#71
This commit is contained in:
Dawid Wysokiński 2022-09-06 07:50:00 +00:00
parent 71700a589b
commit e468a1f391
34 changed files with 1834 additions and 124 deletions

View File

@ -97,6 +97,7 @@ func newServer(cfg serverConfig) (*http.Server, error) {
tribeRepo := bundb.NewTribe(cfg.db)
playerRepo := bundb.NewPlayer(cfg.db)
villageRepo := bundb.NewVillage(cfg.db)
ennoblementRepo := bundb.NewEnnoblement(cfg.db)
// services
versionSvc := service.NewVersion(versionRepo)
@ -104,17 +105,19 @@ func newServer(cfg serverConfig) (*http.Server, error) {
tribeSvc := service.NewTribe(tribeRepo, client)
playerSvc := service.NewPlayer(playerRepo, client)
villageSvc := service.NewVillage(villageRepo, client)
ennoblementSvc := service.NewEnnoblement(ennoblementRepo, client)
// router
r := chi.NewRouter()
r.Use(getChiMiddlewares(cfg.logger)...)
r.Mount("/api", rest.NewRouter(rest.RouterConfig{
VersionService: versionSvc,
ServerService: serverSvc,
TribeService: tribeSvc,
PlayerService: playerSvc,
VillageService: villageSvc,
VersionService: versionSvc,
ServerService: serverSvc,
TribeService: tribeSvc,
PlayerService: playerSvc,
VillageService: villageSvc,
EnnoblementService: ennoblementSvc,
CORS: rest.CORSConfig{
Enabled: apiCfg.CORSEnabled,
AllowedOrigins: nil,

View File

@ -76,7 +76,35 @@ func (l listEnnoblementsParamsApplier) apply(q *bun.SelectQuery) (*bun.SelectQue
var err error
if l.params.ServerKeys != nil {
q = q.Where("server_key IN (?)", bun.In(l.params.ServerKeys))
q = q.Where("ennoblement.server_key IN (?)", bun.In(l.params.ServerKeys))
}
if l.params.CreatedAtGTE.Valid {
q = q.Where("ennoblement.created_at >= ?", l.params.CreatedAtGTE.Time)
}
if l.params.CreatedAtLTE.Valid {
q = q.Where("ennoblement.created_at <= ?", l.params.CreatedAtLTE.Time)
}
if l.params.IncludeVillage {
q = q.Relation("Village")
}
if l.params.IncludeNewOwner {
q = q.Relation("NewOwner")
}
if l.params.IncludeNewTribe {
q = q.Relation("NewTribe")
}
if l.params.IncludeOldOwner {
q = q.Relation("OldOwner")
}
if l.params.IncludeOldTribe {
q = q.Relation("OldTribe")
}
q, err = l.applySort(q)

View File

@ -123,38 +123,21 @@ func TestEnnoblement_List(t *testing.T) {
ennoblements := getAllEnnoblementsFromFixture(t, fixture)
type expectedEnnoblement struct {
id int64
serverKey string
}
newExpectedEnnoblement := func(e model.Ennoblement) expectedEnnoblement {
return expectedEnnoblement{
id: e.ID,
serverKey: e.ServerKey,
}
id int64
serverKey string
villageID int64
newOwnerID int64
newTribeID int64
oldOwnerID int64
oldTribeID int64
}
allEnnoblements := make([]expectedEnnoblement, 0, len(ennoblements))
for _, ennoblement := range ennoblements {
allEnnoblements = append(allEnnoblements, newExpectedEnnoblement(ennoblement))
}
//nolint:prealloc
var ennoblementsPL169 []expectedEnnoblement
for _, ennoblement := range ennoblements {
if ennoblement.ServerKey != "pl169" {
continue
}
ennoblementsPL169 = append(ennoblementsPL169, newExpectedEnnoblement(ennoblement))
}
//nolint:prealloc
var ennoblementsIT70 []expectedEnnoblement
for _, ennoblement := range ennoblements {
if ennoblement.ServerKey != "it70" {
continue
}
ennoblementsIT70 = append(ennoblementsIT70, newExpectedEnnoblement(ennoblement))
allEnnoblements = append(allEnnoblements, expectedEnnoblement{
id: ennoblement.ID,
serverKey: ennoblement.ServerKey,
})
}
tests := []struct {
@ -182,8 +165,17 @@ func TestEnnoblement_List(t *testing.T) {
Count: true,
ServerKeys: []string{"pl169"},
},
expectedEnnoblements: ennoblementsPL169,
expectedCount: int64(len(ennoblementsPL169)),
expectedEnnoblements: []expectedEnnoblement{
{
id: 1,
serverKey: "pl169",
},
{
id: 2,
serverKey: "pl169",
},
},
expectedCount: 2,
},
{
name: "ServerKeys=[pl169],Sort=[{By=CreatedAt,Direction=DESC}],Limit=1,Count=true",
@ -198,9 +190,12 @@ func TestEnnoblement_List(t *testing.T) {
},
},
expectedEnnoblements: []expectedEnnoblement{
newExpectedEnnoblement(getEnnoblementFromFixture(t, fixture, "pl169-village-2-2")),
{
id: 2,
serverKey: "pl169",
},
},
expectedCount: int64(len(ennoblementsPL169)),
expectedCount: 2,
},
{
name: "ServerKeys=[it70],Sort=[{By=ID,Direction=DESC}],Limit=1,Offset=1,Count=true",
@ -216,9 +211,70 @@ func TestEnnoblement_List(t *testing.T) {
},
},
expectedEnnoblements: []expectedEnnoblement{
newExpectedEnnoblement(getEnnoblementFromFixture(t, fixture, "it70-village-2-1")),
{
id: 3,
serverKey: "it70",
},
},
expectedCount: int64(len(ennoblementsIT70)),
expectedCount: 2,
},
{
name: "ServerKeys=[it70],IncludeVillage=true,IncludeNewOwner=true,IncludeNewTribe=true,IncludeOldOwner=true,IncludeOldTribe=true,Count=true",
params: domain.ListEnnoblementsParams{
Count: true,
ServerKeys: []string{"it70"},
IncludeVillage: true,
IncludeNewOwner: true,
IncludeNewTribe: true,
IncludeOldOwner: true,
IncludeOldTribe: true,
},
expectedEnnoblements: []expectedEnnoblement{
{
id: 3,
serverKey: "it70",
villageID: 10023,
newOwnerID: 848881282,
newTribeID: 31,
oldOwnerID: 0,
oldTribeID: 0,
},
{
id: 4,
serverKey: "it70",
villageID: 10023,
newOwnerID: 578014,
newTribeID: 1,
oldOwnerID: 848881282,
oldTribeID: 31,
},
},
expectedCount: 2,
},
{
name: "CreatedAtLTE=2021-12-31T23:59:59Z,CreatedAtGTE=2021-01-01T00:00:00Z,Count=true",
params: domain.ListEnnoblementsParams{
Count: true,
CreatedAtLTE: domain.NullTime{
Time: time.Date(2021, time.December, 31, 23, 59, 59, 0, time.UTC),
Valid: true,
},
CreatedAtGTE: domain.NullTime{
Time: time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC),
Valid: true,
},
},
expectedEnnoblements: []expectedEnnoblement{
{
id: 1,
serverKey: "pl169",
},
{
id: 2,
serverKey: "pl169",
},
},
expectedCount: 2,
},
{
name: "ERR: unsupported sort by",
@ -256,17 +312,50 @@ func TestEnnoblement_List(t *testing.T) {
assert.Len(t, res, len(tt.expectedEnnoblements))
for _, expEnnoblement := range tt.expectedEnnoblements {
found := false
for _, ennoblement := range res {
if ennoblement.ID == expEnnoblement.id && ennoblement.ServerKey == expEnnoblement.serverKey {
found = true
break
if ennoblement.ID != expEnnoblement.id {
continue
}
if ennoblement.ServerKey != expEnnoblement.serverKey {
continue
}
if ennoblement.Village.Village.ID != expEnnoblement.villageID {
continue
}
if ennoblement.NewOwner.Player.ID != expEnnoblement.newOwnerID {
continue
}
if ennoblement.NewTribe.Tribe.ID != expEnnoblement.newTribeID {
continue
}
if ennoblement.OldOwner.Player.ID != expEnnoblement.oldOwnerID {
continue
}
if ennoblement.OldTribe.Tribe.ID != expEnnoblement.oldTribeID {
continue
}
found = true
break
}
assert.True(
t,
found,
"ennoblement (id=%d,serverkey=%s) not found",
"ennoblement (id=%d,villageID=%d,newOwnerID=%d,newTribeID=%d,oldOwnerID=%d,oldTribeID=%d,serverkey=%s) not found",
expEnnoblement.id,
expEnnoblement.villageID,
expEnnoblement.newOwnerID,
expEnnoblement.newTribeID,
expEnnoblement.oldOwnerID,
expEnnoblement.oldTribeID,
expEnnoblement.serverKey,
)
}

View File

@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestNewBuildingInfo(t *testing.T) {
func TestBuildingInfo(t *testing.T) {
t.Parallel()
info := domain.BuildingInfo{

View File

@ -14,10 +14,15 @@ type Ennoblement struct {
ID int64 `bun:"id,pk,autoincrement,identity"`
ServerKey string `bun:"server_key,nullzero,pk,type:varchar(100)"`
VillageID int64 `bun:"village_id,nullzero,notnull"`
Village Village `bun:"village,rel:belongs-to,join:village_id=id,join:server_key=server_key"`
NewOwnerID int64 `bun:"new_owner_id,nullzero"`
NewOwner Player `bun:"new_owner,rel:belongs-to,join:new_owner_id=id,join:server_key=server_key"`
NewTribeID int64 `bun:"new_tribe_id,nullzero"`
NewTribe Tribe `bun:"new_tribe,rel:belongs-to,join:new_tribe_id=id,join:server_key=server_key"`
OldOwnerID int64 `bun:"old_owner_id,nullzero"`
OldOwner Player `bun:"old_owner,rel:belongs-to,join:old_owner_id=id,join:server_key=server_key"`
OldTribeID int64 `bun:"old_tribe_id,nullzero"`
OldTribe Tribe `bun:"old_tribe,rel:belongs-to,join:old_tribe_id=id,join:server_key=server_key"`
Points int64 `bun:"points,default:0"`
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
}
@ -36,6 +41,46 @@ func NewEnnoblement(p domain.CreateEnnoblementParams) Ennoblement {
}
func (e Ennoblement) ToDomain() domain.Ennoblement {
var newOwner domain.NullPlayer
if e.NewOwner.ID > 0 {
newOwner = domain.NullPlayer{
Valid: true,
Player: e.NewOwner.ToDomain(),
}
}
var newTribe domain.NullTribe
if e.NewTribe.ID > 0 {
newTribe = domain.NullTribe{
Valid: true,
Tribe: e.NewTribe.ToDomain(),
}
}
var oldOwner domain.NullPlayer
if e.OldOwner.ID > 0 {
oldOwner = domain.NullPlayer{
Valid: true,
Player: e.OldOwner.ToDomain(),
}
}
var oldTribe domain.NullTribe
if e.OldTribe.ID > 0 {
oldTribe = domain.NullTribe{
Valid: true,
Tribe: e.OldTribe.ToDomain(),
}
}
var village domain.NullVillage
if e.Village.ID > 0 {
village = domain.NullVillage{
Valid: true,
Village: e.Village.ToDomain(),
}
}
return domain.Ennoblement{
BaseEnnoblement: domain.BaseEnnoblement{
VillageID: e.VillageID,
@ -47,6 +92,11 @@ func (e Ennoblement) ToDomain() domain.Ennoblement {
CreatedAt: e.CreatedAt,
},
ID: e.ID,
Village: village,
NewOwner: newOwner,
NewTribe: newTribe,
OldOwner: oldOwner,
OldTribe: oldTribe,
ServerKey: e.ServerKey,
}
}

View File

@ -10,7 +10,7 @@ import (
"gitea.dwysokinski.me/twhelp/core/internal/domain"
)
func TestNewEnnoblement(t *testing.T) {
func TestEnnoblement(t *testing.T) {
t.Parallel()
var id int64 = 1234
@ -29,8 +29,39 @@ func TestNewEnnoblement(t *testing.T) {
result := model.NewEnnoblement(params)
result.ID = id
ennoblement := result.ToDomain()
assert.Equal(t, params.BaseEnnoblement, ennoblement.BaseEnnoblement)
assert.Equal(t, params.ServerKey, ennoblement.ServerKey)
assert.Equal(t, id, ennoblement.ID)
ennoblementWithoutRelations := result.ToDomain()
assert.Equal(t, params.BaseEnnoblement, ennoblementWithoutRelations.BaseEnnoblement)
assert.Equal(t, params.ServerKey, ennoblementWithoutRelations.ServerKey)
assert.Equal(t, id, ennoblementWithoutRelations.ID)
result.Village = model.Village{
ID: 12345,
}
result.NewOwner = model.Player{
ID: 11231,
}
result.NewTribe = model.Tribe{
ID: 11232,
}
result.OldOwner = model.Player{
ID: 11233,
}
result.OldTribe = model.Tribe{
ID: 11234,
}
ennoblementWithRelations := result.ToDomain()
assert.Equal(t, params.BaseEnnoblement, ennoblementWithRelations.BaseEnnoblement)
assert.Equal(t, params.ServerKey, ennoblementWithRelations.ServerKey)
assert.Equal(t, id, ennoblementWithRelations.ID)
assert.True(t, ennoblementWithRelations.Village.Valid)
assert.Equal(t, result.Village.ID, ennoblementWithRelations.Village.Village.ID)
assert.True(t, ennoblementWithRelations.NewOwner.Valid)
assert.Equal(t, result.NewOwner.ID, ennoblementWithRelations.NewOwner.Player.ID)
assert.True(t, ennoblementWithRelations.NewTribe.Valid)
assert.Equal(t, result.NewTribe.ID, ennoblementWithRelations.NewTribe.Tribe.ID)
assert.True(t, ennoblementWithRelations.OldOwner.Valid)
assert.Equal(t, result.OldOwner.ID, ennoblementWithRelations.OldOwner.Player.ID)
assert.True(t, ennoblementWithRelations.OldTribe.Valid)
assert.Equal(t, result.OldTribe.ID, ennoblementWithRelations.OldTribe.Tribe.ID)
}

View File

@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestNewPlayer(t *testing.T) {
func TestPlayer(t *testing.T) {
t.Parallel()
deletedAt := time.Now().Add(-100 * time.Hour)
@ -44,20 +44,20 @@ func TestNewPlayer(t *testing.T) {
result := model.NewPlayer(playerParams)
result.DeletedAt = deletedAt
player := result.ToDomain()
assert.Equal(t, playerParams.BasePlayer, player.BasePlayer)
assert.Equal(t, playerParams.ServerKey, player.ServerKey)
assert.WithinDuration(t, time.Now(), player.CreatedAt, 10*time.Millisecond)
assert.Zero(t, player.Tribe)
assert.Equal(t, deletedAt, player.DeletedAt)
playerWithoutTribe := result.ToDomain()
assert.Equal(t, playerParams.BasePlayer, playerWithoutTribe.BasePlayer)
assert.Equal(t, playerParams.ServerKey, playerWithoutTribe.ServerKey)
assert.WithinDuration(t, time.Now(), playerWithoutTribe.CreatedAt, 10*time.Millisecond)
assert.Zero(t, playerWithoutTribe.Tribe)
assert.Equal(t, deletedAt, playerWithoutTribe.DeletedAt)
result.Tribe = model.NewTribe(tribeParams)
result.DeletedAt = time.Time{}
player = result.ToDomain()
assert.Equal(t, playerParams.BasePlayer, player.BasePlayer)
assert.Equal(t, playerParams.ServerKey, player.ServerKey)
assert.WithinDuration(t, time.Now(), player.CreatedAt, 10*time.Millisecond)
assert.Equal(t, tribeParams.BaseTribe, player.Tribe.Tribe.BaseTribe)
assert.True(t, player.Tribe.Valid)
assert.Zero(t, player.DeletedAt)
playerWithTribe := result.ToDomain()
assert.Equal(t, playerParams.BasePlayer, playerWithTribe.BasePlayer)
assert.Equal(t, playerParams.ServerKey, playerWithTribe.ServerKey)
assert.WithinDuration(t, time.Now(), playerWithTribe.CreatedAt, 10*time.Millisecond)
assert.Zero(t, playerWithTribe.DeletedAt)
assert.True(t, playerWithTribe.Tribe.Valid)
assert.Equal(t, tribeParams.ID, playerWithTribe.Tribe.Tribe.ID)
}

View File

@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestNewServerConfig(t *testing.T) {
func TestServerConfig(t *testing.T) {
t.Parallel()
cfg := domain.ServerConfig{

View File

@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestNewTribe(t *testing.T) {
func TestTribe(t *testing.T) {
t.Parallel()
deletedAt := time.Now().Add(-100 * time.Hour)

View File

@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestNewUnitInfo(t *testing.T) {
func TestUnitInfo(t *testing.T) {
t.Parallel()
info := domain.UnitInfo{

View File

@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestNewVillage(t *testing.T) {
func TestVillage(t *testing.T) {
t.Parallel()
params := domain.CreateVillageParams{
@ -27,8 +27,24 @@ func TestNewVillage(t *testing.T) {
}
result := model.NewVillage(params)
village := result.ToDomain()
assert.Equal(t, params.BaseVillage, village.BaseVillage)
assert.Equal(t, params.ServerKey, village.ServerKey)
assert.WithinDuration(t, time.Now(), village.CreatedAt, 10*time.Millisecond)
villageWithoutPlayer := result.ToDomain()
assert.Equal(t, params.BaseVillage, villageWithoutPlayer.BaseVillage)
assert.Equal(t, params.ServerKey, villageWithoutPlayer.ServerKey)
assert.WithinDuration(t, time.Now(), villageWithoutPlayer.CreatedAt, 10*time.Millisecond)
result.Player = model.Player{
ID: 12321,
Tribe: model.Tribe{
ID: 1234112,
},
}
villageWithPlayer := result.ToDomain()
assert.Equal(t, params.BaseVillage, villageWithPlayer.BaseVillage)
assert.Equal(t, params.ServerKey, villageWithPlayer.ServerKey)
assert.WithinDuration(t, time.Now(), villageWithPlayer.CreatedAt, 10*time.Millisecond)
assert.True(t, villageWithPlayer.Player.Valid)
assert.Equal(t, result.Player.ID, villageWithPlayer.Player.Player.ID)
assert.True(t, villageWithPlayer.Player.Player.Tribe.Valid)
assert.Equal(t, result.Player.Tribe.ID, villageWithPlayer.Player.Player.Tribe.Tribe.ID)
}

View File

@ -7239,7 +7239,7 @@
created_at: 2022-02-25T15:00:10.000Z
- model: Ennoblement
rows:
- _id: pl169-village-2-1
- _id: pl169-village-2-1 # id: 1
server_key: pl169
village_id: 1112
new_owner_id: 699513260
@ -7248,7 +7248,7 @@
old_tribe_id: 0
points: 2500
created_at: 2021-09-30T09:01:00.000Z
- _id: pl169-village-2-2
- _id: pl169-village-2-2 # id: 2
server_key: pl169
village_id: 1112
new_owner_id: 699783765
@ -7257,7 +7257,7 @@
old_tribe_id: 27
points: 5000
created_at: 2021-10-30T09:01:00.000Z
- _id: it70-village-2-1
- _id: it70-village-2-1 # id: 3
server_key: it70
village_id: 10023
new_owner_id: 848881282
@ -7266,7 +7266,7 @@
old_tribe_id: 0
points: 2500
created_at: 2022-03-15T15:00:10.000Z
- _id: it70-village-2-2
- _id: it70-village-2-2 # id: 4
server_key: it70
village_id: 10023
new_owner_id: 578014

View File

@ -1,5 +1,7 @@
package domain
import "time"
type NullBool struct {
Bool bool
Valid bool // Valid is true if Bool is not NULL
@ -15,6 +17,11 @@ type NullInt32 struct {
Valid bool // Valid is true if Int32 is not NULL
}
type NullTime struct {
Time time.Time
Valid bool // Valid is true if Time is not NULL
}
type Pagination struct {
Offset int32
Limit int32

View File

@ -1,6 +1,9 @@
package domain
import "time"
import (
"fmt"
"time"
)
type BaseEnnoblement struct {
VillageID int64
@ -16,6 +19,11 @@ type Ennoblement struct {
BaseEnnoblement
ID int64
Village NullVillage
NewOwner NullPlayer
NewTribe NullTribe
OldOwner NullPlayer
OldTribe NullTribe
ServerKey string
}
@ -32,16 +40,33 @@ const (
EnnoblementSortByCreatedAt
)
func NewEnnoblementSortBy(s string) (EnnoblementSortBy, error) {
switch s {
case "id":
return EnnoblementSortByID, nil
case "createdAt":
return EnnoblementSortByCreatedAt, nil
}
return 0, fmt.Errorf("%w: \"%s\"", ErrUnsupportedSortBy, s)
}
type EnnoblementSort struct {
By EnnoblementSortBy
Direction SortDirection
}
type ListEnnoblementsParams struct {
ServerKeys []string
Pagination Pagination
Sort []EnnoblementSort
Count bool
ServerKeys []string
CreatedAtGTE NullTime
CreatedAtLTE NullTime
Pagination Pagination
Sort []EnnoblementSort
IncludeVillage bool
IncludeNewOwner bool
IncludeNewTribe bool
IncludeOldOwner bool
IncludeOldTribe bool
Count bool
}
type RefreshEnnoblementsCmdPayload struct {

View File

@ -0,0 +1,50 @@
package domain_test
import (
"testing"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"github.com/stretchr/testify/assert"
)
func TestNewEnnoblementSortBy(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
tests := []struct {
s string
output domain.EnnoblementSortBy
}{
{
s: "id",
output: domain.EnnoblementSortByID,
},
{
s: "createdAt",
output: domain.EnnoblementSortByCreatedAt,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.s, func(t *testing.T) {
t.Parallel()
res, err := domain.NewEnnoblementSortBy(tt.s)
assert.NoError(t, err)
assert.Equal(t, tt.output, res)
})
}
})
t.Run("ERR: unsupported sort by", func(t *testing.T) {
t.Parallel()
res, err := domain.NewEnnoblementSortBy("unsupported")
assert.ErrorIs(t, err, domain.ErrUnsupportedSortBy)
assert.Zero(t, res)
})
}

View File

@ -24,6 +24,11 @@ type Village struct {
CreatedAt time.Time
}
type NullVillage struct {
Village Village
Valid bool
}
type CreateVillageParams struct {
BaseVillage

View File

@ -1,2 +1,3 @@
replace gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model.NullTribeMeta gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model.TribeMeta
replace gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model.NullPlayerMeta gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model.PlayerMeta
replace gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model.NullVillageMeta gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model.VillageMeta

View File

@ -0,0 +1,91 @@
package rest
import (
"context"
"net/http"
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model"
"github.com/go-chi/chi/v5"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
)
const (
ennoblementDefaultOffset = 0
ennoblementDefaultLimit = 3000
)
//counterfeiter:generate -o internal/mock/ennoblement_service.gen.go . EnnoblementService
type EnnoblementService interface {
List(ctx context.Context, params domain.ListEnnoblementsParams) ([]domain.Ennoblement, int64, error)
}
type ennoblement struct {
svc EnnoblementService
}
// @ID listEnnoblements
// @Summary List ennoblements
// @Description List all ennoblements
// @Tags versions,servers,ennoblements
// @Produce json
// @Success 200 {object} model.ListEnnoblementsResp
// @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 serverKey path string true "Server key"
// @Param since query string false "only show items created after the given time, this is a timestamp in RFC 3339 format" format(date-time)
// @Param before query string false "only show items created before the given time, this is a timestamp in RFC 3339 format" format(date-time)
// @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(3000) default(3000)
// @Param sort query []string false "format: field:direction, default: [id:asc]" Enums(id:asc,id:desc,createdAt:asc,createdAt:desc)
// @Router /versions/{versionCode}/servers/{serverKey}/ennoblements [get]
func (e *ennoblement) list(w http.ResponseWriter, r *http.Request) {
var err error
ctx := r.Context()
params := domain.ListEnnoblementsParams{
ServerKeys: []string{chi.URLParamFromCtx(ctx, "serverKey")},
Count: true,
IncludeVillage: true,
IncludeNewOwner: true,
IncludeNewTribe: true,
IncludeOldOwner: true,
IncludeOldTribe: true,
}
query := queryParams{r.URL.Query()}
params.CreatedAtGTE, err = query.nullTime("since")
if err != nil {
renderErr(w, err)
return
}
params.CreatedAtLTE, err = query.nullTime("before")
if err != nil {
renderErr(w, err)
return
}
params.Pagination, err = query.pagination(ennoblementDefaultOffset, ennoblementDefaultLimit)
if err != nil {
renderErr(w, err)
return
}
params.Sort, err = query.ennoblementSort()
if err != nil {
renderErr(w, err)
return
}
ennoblements, count, err := e.svc.List(ctx, params)
if err != nil {
renderErr(w, err)
return
}
setTotalCountHeader(w.Header(), count)
renderJSON(w, http.StatusOK, model.NewListEnnoblementsResp(ennoblements))
}

View File

@ -0,0 +1,591 @@
package rest_test
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"testing"
"time"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/go-cmp/cmp"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"gitea.dwysokinski.me/twhelp/core/internal/rest"
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/mock"
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model"
)
func TestEnnoblement_list(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, ennoblementSvc *mock.FakeEnnoblementService)
versionCode string
key string
queryParams url.Values
expectedStatus int
target any
expectedResponse any
expectedTotalCount string
}{
{
name: "OK: without params",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
ennoblementSvc.ListCalls(func(_ context.Context, params domain.ListEnnoblementsParams) ([]domain.Ennoblement, int64, error) {
expectedParams := domain.ListEnnoblementsParams{
ServerKeys: []string{server.Key},
IncludeVillage: true,
IncludeNewOwner: true,
IncludeNewTribe: true,
IncludeOldOwner: true,
IncludeOldTribe: true,
Pagination: domain.Pagination{
Limit: 3000,
Offset: 0,
},
Count: true,
}
if diff := cmp.Diff(params, expectedParams); diff != "" {
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
ennoblements := []domain.Ennoblement{
{
BaseEnnoblement: domain.BaseEnnoblement{
VillageID: 123,
NewOwnerID: 124,
NewTribeID: 125,
OldOwnerID: 126,
OldTribeID: 127,
Points: 128,
CreatedAt: now,
},
ID: 1234,
Village: domain.NullVillage{
Valid: true,
Village: domain.Village{
BaseVillage: domain.BaseVillage{
ID: 123,
Name: "village name",
},
},
},
NewOwner: domain.NullPlayer{
Valid: true,
Player: domain.Player{
BasePlayer: domain.BasePlayer{
ID: 124,
Name: "new owner name",
},
},
},
NewTribe: domain.NullTribe{
Valid: true,
Tribe: domain.Tribe{
BaseTribe: domain.BaseTribe{
ID: 125,
Name: "new tribe name",
Tag: "new tribe tag",
},
},
},
OldOwner: domain.NullPlayer{
Valid: true,
Player: domain.Player{
BasePlayer: domain.BasePlayer{
ID: 126,
Name: "old owner name",
},
},
},
OldTribe: domain.NullTribe{
Valid: true,
Tribe: domain.Tribe{
BaseTribe: domain.BaseTribe{
ID: 127,
Name: "old tribe name",
Tag: "old tribe tag",
},
},
},
ServerKey: server.Key,
},
{
BaseEnnoblement: domain.BaseEnnoblement{
VillageID: 311,
NewOwnerID: 312,
NewTribeID: 313,
OldOwnerID: 0,
OldTribeID: 0,
Points: 129,
CreatedAt: now,
},
ID: 12345,
Village: domain.NullVillage{
Valid: true,
Village: domain.Village{
BaseVillage: domain.BaseVillage{
ID: 311,
Name: "village name",
},
},
},
NewOwner: domain.NullPlayer{
Valid: true,
Player: domain.Player{
BasePlayer: domain.BasePlayer{
ID: 312,
Name: "new owner name",
},
},
},
NewTribe: domain.NullTribe{
Valid: true,
Tribe: domain.Tribe{
BaseTribe: domain.BaseTribe{
ID: 313,
Name: "new tribe name",
Tag: "new tribe tag",
},
},
},
ServerKey: server.Key,
},
}
return ennoblements, int64(len(ennoblements)), nil
})
},
versionCode: version.Code,
key: server.Key,
expectedStatus: 200,
target: &model.ListEnnoblementsResp{},
expectedResponse: &model.ListEnnoblementsResp{
Data: []model.Ennoblement{
{
ID: 1234,
Points: 128,
Village: model.VillageMeta{
ID: 123,
Name: "village name",
},
NewOwner: model.NullPlayerMeta{
Valid: true,
Player: model.PlayerMeta{
ID: 124,
Name: "new owner name",
Tribe: model.NullTribeMeta{
Valid: true,
Tribe: model.TribeMeta{
ID: 125,
Name: "new tribe name",
Tag: "new tribe tag",
},
},
},
},
OldOwner: model.NullPlayerMeta{
Valid: true,
Player: model.PlayerMeta{
ID: 126,
Name: "old owner name",
Tribe: model.NullTribeMeta{
Valid: true,
Tribe: model.TribeMeta{
ID: 127,
Name: "old tribe name",
Tag: "old tribe tag",
},
},
},
},
CreatedAt: now,
},
{
ID: 12345,
Points: 129,
Village: model.VillageMeta{
ID: 311,
Name: "village name",
},
NewOwner: model.NullPlayerMeta{
Valid: true,
Player: model.PlayerMeta{
ID: 312,
Name: "new owner name",
Tribe: model.NullTribeMeta{
Valid: true,
Tribe: model.TribeMeta{
ID: 313,
Name: "new tribe name",
Tag: "new tribe tag",
},
},
},
},
CreatedAt: now,
},
},
},
expectedTotalCount: "2",
},
{
name: "OK: limit=1,offset=100,since=now-30h,before=now,sort=[createdAt:desc]",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
ennoblementSvc.ListCalls(func(_ context.Context, params domain.ListEnnoblementsParams) ([]domain.Ennoblement, int64, error) {
expectedParams := domain.ListEnnoblementsParams{
ServerKeys: []string{server.Key},
IncludeVillage: true,
IncludeNewOwner: true,
IncludeNewTribe: true,
IncludeOldOwner: true,
IncludeOldTribe: true,
CreatedAtGTE: domain.NullTime{
Valid: true,
Time: now.Add(-30 * time.Hour).Truncate(time.Second),
},
CreatedAtLTE: domain.NullTime{
Valid: true,
Time: now.Truncate(time.Second),
},
Sort: []domain.EnnoblementSort{
{By: domain.EnnoblementSortByCreatedAt, Direction: domain.SortDirectionDESC},
},
Pagination: domain.Pagination{
Limit: 1,
Offset: 100,
},
Count: true,
}
if diff := cmp.Diff(params, expectedParams, cmpopts.IgnoreUnexported(time.Time{})); diff != "" {
return nil, 0, errors.New("invalid params: " + diff)
}
ennoblements := []domain.Ennoblement{
{
BaseEnnoblement: domain.BaseEnnoblement{
VillageID: 311,
NewOwnerID: 312,
NewTribeID: 313,
OldOwnerID: 0,
OldTribeID: 0,
Points: 129,
CreatedAt: now,
},
ID: 12345,
Village: domain.NullVillage{
Valid: true,
Village: domain.Village{
BaseVillage: domain.BaseVillage{
ID: 311,
Name: "village name",
},
},
},
NewOwner: domain.NullPlayer{
Valid: true,
Player: domain.Player{
BasePlayer: domain.BasePlayer{
ID: 312,
Name: "new owner name",
},
},
},
NewTribe: domain.NullTribe{
Valid: true,
Tribe: domain.Tribe{
BaseTribe: domain.BaseTribe{
ID: 313,
Name: "new tribe name",
Tag: "new tribe tag",
},
},
},
ServerKey: server.Key,
},
}
return ennoblements, int64(len(ennoblements)), nil
})
},
versionCode: version.Code,
key: server.Key,
expectedStatus: 200,
queryParams: url.Values{
"limit": []string{"1"},
"offset": []string{"100"},
"sort": []string{"createdAt:DESC"},
"since": []string{now.Add(-30 * time.Hour).Format(time.RFC3339)},
"before": []string{now.Format(time.RFC3339)},
},
target: &model.ListEnnoblementsResp{},
expectedResponse: &model.ListEnnoblementsResp{
Data: []model.Ennoblement{
{
ID: 12345,
Points: 129,
Village: model.VillageMeta{
ID: 311,
Name: "village name",
},
NewOwner: model.NullPlayerMeta{
Valid: true,
Player: model.PlayerMeta{
ID: 312,
Name: "new owner name",
Tribe: model.NullTribeMeta{
Valid: true,
Tribe: model.TribeMeta{
ID: 313,
Name: "new tribe name",
Tag: "new tribe tag",
},
},
},
},
CreatedAt: now,
},
},
},
expectedTotalCount: "1",
},
{
name: "ERR: since is not a valid timestamp",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, playerSvc *mock.FakeEnnoblementService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
},
versionCode: version.Code,
key: server.Key,
queryParams: url.Values{
"since": []string{"???"},
},
expectedStatus: 400,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeValidationError.String(),
Message: "since: parsing time \"???\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"???\" as \"2006\"",
},
},
expectedTotalCount: "",
},
{
name: "ERR: before is not a valid timestamp",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, playerSvc *mock.FakeEnnoblementService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
},
versionCode: version.Code,
key: server.Key,
queryParams: url.Values{
"before": []string{"???"},
},
expectedStatus: 400,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeValidationError.String(),
Message: "before: parsing time \"???\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"???\" as \"2006\"",
},
},
expectedTotalCount: "",
},
{
name: "ERR: limit is not a valid int32",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, playerSvc *mock.FakeEnnoblementService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
},
versionCode: version.Code,
key: server.Key,
queryParams: url.Values{
"limit": []string{"asd"},
},
expectedStatus: 400,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeValidationError.String(),
Message: "limit: strconv.ParseInt: parsing \"asd\": invalid syntax",
},
},
expectedTotalCount: "",
},
{
name: "ERR: offset is not a valid int32",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
},
versionCode: version.Code,
key: server.Key,
queryParams: url.Values{
"offset": []string{"asd"},
},
expectedStatus: 400,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeValidationError.String(),
Message: "offset: strconv.ParseInt: parsing \"asd\": invalid syntax",
},
},
expectedTotalCount: "",
},
{
name: "ERR: sort - invalid format",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
},
versionCode: version.Code,
key: server.Key,
queryParams: url.Values{
"sort": []string{"id:asc", "test"},
},
expectedStatus: 400,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeValidationError.String(),
Message: "sort[1]: parsing \"test\": invalid syntax, expected field:direction",
},
},
expectedTotalCount: "",
},
{
name: "ERR: sort - unsupported field",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
},
versionCode: version.Code,
key: server.Key,
queryParams: url.Values{
"sort": []string{"id:asc", "test:asc"},
},
expectedStatus: 400,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeValidationError.String(),
Message: "sort[1]: unsupported sort by: \"test\"",
},
},
expectedTotalCount: "",
},
{
name: "ERR: sort - unsupported direction",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
},
versionCode: version.Code,
key: server.Key,
queryParams: url.Values{
"sort": []string{"id:asc2"},
},
expectedStatus: 400,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeValidationError.String(),
Message: "sort[0]: unsupported sort direction: \"asc2\"",
},
},
expectedTotalCount: "",
},
{
name: "ERR: version not found",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) {
versionSvc.GetByCodeReturns(domain.Version{}, domain.VersionNotFoundError{VerCode: version.Code + "2"})
},
versionCode: version.Code + "2",
key: server.Key,
expectedStatus: 404,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeEntityNotFound.String(),
Message: fmt.Sprintf("version (code=%s) not found", version.Code+"2"),
},
},
expectedTotalCount: "",
},
{
name: "ERR: server not found",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, ennoblementSvc *mock.FakeEnnoblementService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(domain.Server{}, domain.ServerNotFoundError{Key: server.Key + "2"})
},
versionCode: version.Code,
key: server.Key + "2",
expectedStatus: 404,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeEntityNotFound.String(),
Message: fmt.Sprintf("server (key=%s) not found", server.Key+"2"),
},
},
expectedTotalCount: "",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
versionSvc := &mock.FakeVersionService{}
serverSvc := &mock.FakeServerService{}
ennoblementSvc := &mock.FakeEnnoblementService{}
tt.setup(versionSvc, serverSvc, ennoblementSvc)
router := rest.NewRouter(rest.RouterConfig{
VersionService: versionSvc,
ServerService: serverSvc,
EnnoblementService: ennoblementSvc,
})
target := fmt.Sprintf(
"/v1/versions/%s/servers/%s/ennoblements?%s",
tt.versionCode,
tt.key,
tt.queryParams.Encode(),
)
resp := doRequest(router, http.MethodGet, target, nil)
defer resp.Body.Close()
assertTotalCount(t, resp.Header, tt.expectedTotalCount)
assertJSONResponse(t, resp, tt.expectedStatus, tt.expectedResponse, tt.target)
})
}
}

View File

@ -0,0 +1,59 @@
package model
import (
"time"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
)
type Ennoblement struct {
ID int64 `json:"id"`
Village VillageMeta `json:"village"`
NewOwner NullPlayerMeta `json:"newOwner" extensions:"x-nullable"`
OldOwner NullPlayerMeta `json:"oldOwner" extensions:"x-nullable"`
Points int64 `json:"points"`
CreatedAt time.Time `json:"createdAt" format:"date-time"`
} // @name Ennoblement
func NewEnnoblement(e domain.Ennoblement) Ennoblement {
var newOwner NullPlayerMeta
if e.NewOwner.Valid {
e.NewOwner.Player.Tribe = e.NewTribe
newOwner = NullPlayerMeta{
Valid: true,
Player: NewPlayerMeta(e.NewOwner.Player),
}
}
var oldOwner NullPlayerMeta
if e.OldOwner.Valid {
e.OldOwner.Player.Tribe = e.OldTribe
oldOwner = NullPlayerMeta{
Valid: true,
Player: NewPlayerMeta(e.OldOwner.Player),
}
}
return Ennoblement{
ID: e.ID,
Village: NewVillageMeta(e.Village.Village),
NewOwner: newOwner,
OldOwner: oldOwner,
Points: e.Points,
CreatedAt: e.CreatedAt,
}
}
type ListEnnoblementsResp struct {
Data []Ennoblement `json:"data"`
} // @name ListEnnoblementsResp
func NewListEnnoblementsResp(ennoblements []domain.Ennoblement) ListEnnoblementsResp {
resp := ListEnnoblementsResp{
Data: make([]Ennoblement, 0, len(ennoblements)),
}
for _, e := range ennoblements {
resp.Data = append(resp.Data, NewEnnoblement(e))
}
return resp
}

View File

@ -0,0 +1,205 @@
package model_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
)
func TestNewEnnoblement(t *testing.T) {
t.Parallel()
ennoblement := domain.Ennoblement{
BaseEnnoblement: domain.BaseEnnoblement{
VillageID: 123,
NewOwnerID: 124,
NewTribeID: 125,
OldOwnerID: 126,
OldTribeID: 127,
Points: 128,
CreatedAt: time.Now(),
},
ID: 1234,
Village: domain.NullVillage{
Valid: true,
Village: domain.Village{
BaseVillage: domain.BaseVillage{
ID: 123,
Name: "village name",
},
},
},
NewOwner: domain.NullPlayer{
Valid: true,
Player: domain.Player{
BasePlayer: domain.BasePlayer{
ID: 124,
Name: "new owner name",
},
},
},
NewTribe: domain.NullTribe{
Valid: true,
Tribe: domain.Tribe{
BaseTribe: domain.BaseTribe{
ID: 125,
Name: "new tribe name",
Tag: "new tribe tag",
},
},
},
OldOwner: domain.NullPlayer{
Valid: true,
Player: domain.Player{
BasePlayer: domain.BasePlayer{
ID: 126,
Name: "old owner name",
},
},
},
OldTribe: domain.NullTribe{
Valid: true,
Tribe: domain.Tribe{
BaseTribe: domain.BaseTribe{
ID: 125,
Name: "old tribe name",
Tag: "old tribe tag",
},
},
},
ServerKey: "pl151",
}
assertEnnoblement(t, ennoblement, model.NewEnnoblement(ennoblement))
}
func TestNewListEnnoblementsResp(t *testing.T) {
t.Parallel()
ennoblements := []domain.Ennoblement{
{
BaseEnnoblement: domain.BaseEnnoblement{
VillageID: 123,
NewOwnerID: 124,
NewTribeID: 125,
OldOwnerID: 126,
OldTribeID: 127,
Points: 128,
CreatedAt: time.Now(),
},
ID: 1234,
Village: domain.NullVillage{
Valid: true,
Village: domain.Village{
BaseVillage: domain.BaseVillage{
ID: 123,
Name: "village name",
},
},
},
NewOwner: domain.NullPlayer{
Valid: true,
Player: domain.Player{
BasePlayer: domain.BasePlayer{
ID: 124,
Name: "new owner name",
},
},
},
NewTribe: domain.NullTribe{
Valid: true,
Tribe: domain.Tribe{
BaseTribe: domain.BaseTribe{
ID: 125,
Name: "new tribe name",
Tag: "new tribe tag",
},
},
},
OldOwner: domain.NullPlayer{
Valid: true,
Player: domain.Player{
BasePlayer: domain.BasePlayer{
ID: 126,
Name: "old owner name",
},
},
},
OldTribe: domain.NullTribe{
Valid: true,
Tribe: domain.Tribe{
BaseTribe: domain.BaseTribe{
ID: 125,
Name: "old tribe name",
Tag: "old tribe tag",
},
},
},
ServerKey: "pl151",
},
{
BaseEnnoblement: domain.BaseEnnoblement{
VillageID: 311,
NewOwnerID: 312,
NewTribeID: 313,
OldOwnerID: 0,
OldTribeID: 0,
Points: 129,
CreatedAt: time.Now(),
},
ID: 1234,
Village: domain.NullVillage{
Valid: true,
Village: domain.Village{
BaseVillage: domain.BaseVillage{
ID: 311,
Name: "village name",
},
},
},
NewOwner: domain.NullPlayer{
Valid: true,
Player: domain.Player{
BasePlayer: domain.BasePlayer{
ID: 312,
Name: "new owner name",
},
},
},
NewTribe: domain.NullTribe{
Valid: true,
Tribe: domain.Tribe{
BaseTribe: domain.BaseTribe{
ID: 313,
Name: "new tribe name",
Tag: "new tribe tag",
},
},
},
ServerKey: "pl151",
},
}
resp := model.NewListEnnoblementsResp(ennoblements)
assert.Len(t, resp.Data, len(ennoblements))
for i, e := range resp.Data {
assertEnnoblement(t, ennoblements[i], e)
}
}
func assertEnnoblement(tb testing.TB, de domain.Ennoblement, re model.Ennoblement) {
tb.Helper()
assert.Equal(tb, de.ID, re.ID)
assert.Equal(tb, de.Points, re.Points)
assertVillageMeta(tb, de.Village.Village, re.Village)
de.NewOwner.Player.Tribe = de.NewTribe
assertNullPlayerMeta(tb, de.NewOwner, re.NewOwner)
de.OldOwner.Player.Tribe = de.OldTribe
assertNullPlayerMeta(tb, de.OldOwner, re.OldOwner)
assert.Equal(tb, de.CreatedAt, re.CreatedAt)
}

View File

@ -1,6 +1,7 @@
package model
import (
"encoding/json"
"time"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
@ -39,6 +40,48 @@ func NewVillage(v domain.Village) Village {
}
}
// VillageMeta represents basic village information
// @Description Basic village information
type VillageMeta struct {
ID int64 `json:"id"`
Name string `json:"name"`
} // @name VillageMeta
func NewVillageMeta(v domain.Village) VillageMeta {
return VillageMeta{
ID: v.ID,
Name: v.Name,
}
}
type NullVillageMeta struct {
Village VillageMeta
Valid bool
}
func (v NullVillageMeta) MarshalJSON() ([]byte, error) {
if !v.Valid {
return []byte("null"), nil
}
return json.Marshal(v.Village)
}
func (v *NullVillageMeta) UnmarshalJSON(data []byte) error {
// Ignore null, like in the main JSON package.
if string(data) == "null" {
return nil
}
if err := json.Unmarshal(data, &v.Village); err != nil {
return err
}
v.Valid = true
return nil
}
type ListVillagesResp struct {
Data []Village `json:"data"`
} // @name ListVillagesResp

View File

@ -1,6 +1,7 @@
package model_test
import (
"encoding/json"
"testing"
"time"
@ -99,6 +100,139 @@ func TestNewVillage(t *testing.T) {
assertVillage(t, playerVillage, model.NewVillage(playerVillage))
}
func TestNewVillageMeta(t *testing.T) {
t.Parallel()
village := domain.Village{
BaseVillage: domain.BaseVillage{
ID: 1234,
Name: "name",
Points: 1,
X: 2,
Y: 3,
Continent: "K11",
Bonus: 1,
PlayerID: 0,
},
ServerKey: "pl151",
Player: domain.NullPlayer{},
CreatedAt: time.Time{},
}
assertVillageMeta(t, village, model.NewVillageMeta(village))
}
func TestNullVillageMeta_MarshalJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
village model.NullVillageMeta
expectedJSON string
}{
{
name: "OK: null 1",
village: model.NullVillageMeta{
Village: model.VillageMeta{},
Valid: false,
},
expectedJSON: "null",
},
{
name: "OK: null 2",
village: model.NullVillageMeta{
Village: model.VillageMeta{ID: 1234},
Valid: false,
},
expectedJSON: "null",
},
{
name: "OK: valid struct",
village: model.NullVillageMeta{
Village: model.VillageMeta{
ID: 997,
Name: "name 997",
},
Valid: true,
},
expectedJSON: `{"id":997,"name":"name 997"}`,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
b, err := json.Marshal(tt.village)
assert.NoError(t, err)
assert.JSONEq(t, tt.expectedJSON, string(b))
})
}
}
func TestNullVillageMeta_UnmarshalJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
json string
expectedVillage model.NullVillageMeta
expectedJSONSyntaxError bool
}{
{
name: "OK: null",
json: "null",
expectedVillage: model.NullVillageMeta{
Village: model.VillageMeta{},
Valid: false,
},
},
{
name: "OK: valid struct",
//nolint:lll
json: `{"id":997,"name":"name 997"}`,
expectedVillage: model.NullVillageMeta{
Village: model.VillageMeta{
ID: 997,
Name: "name 997",
},
Valid: true,
},
},
{
name: "ERR: invalid tribe 1",
json: "2022-07-30T14:13:12.0000005Z",
expectedVillage: model.NullVillageMeta{},
expectedJSONSyntaxError: true,
},
{
name: "ERR: invalid tribe 2",
json: "hello world",
expectedVillage: model.NullVillageMeta{},
expectedJSONSyntaxError: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var target model.NullVillageMeta
err := json.Unmarshal([]byte(tt.json), &target)
if tt.expectedJSONSyntaxError {
var syntaxError *json.SyntaxError
assert.ErrorAs(t, err, &syntaxError)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.expectedVillage, target)
})
}
}
func TestNewListVillagesResp(t *testing.T) {
t.Parallel()
@ -227,3 +361,10 @@ func assertVillage(tb testing.TB, dv domain.Village, rv model.Village) {
assert.Equal(tb, dv.CreatedAt, rv.CreatedAt)
assertNullPlayerMeta(tb, dv.Player, rv.Player)
}
func assertVillageMeta(tb testing.TB, dv domain.Village, rv model.VillageMeta) {
tb.Helper()
assert.Equal(tb, dv.ID, rv.ID)
assert.Equal(tb, dv.Name, rv.Name)
}

View File

@ -2,7 +2,6 @@ package rest_test
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
@ -53,10 +52,6 @@ func TestPlayer_list(t *testing.T) {
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
playerSvc.ListCalls(func(_ context.Context, params domain.ListPlayersParams) ([]domain.Player, int64, error) {
expectedParams := domain.ListPlayersParams{
Deleted: domain.NullBool{
Valid: false,
Bool: false,
},
ServerKeys: []string{server.Key},
IncludeTribe: true,
Pagination: domain.Pagination{
@ -66,8 +61,8 @@ func TestPlayer_list(t *testing.T) {
Count: true,
}
if !cmp.Equal(params, expectedParams) {
return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams))
if diff := cmp.Diff(params, expectedParams); diff != "" {
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
players := []domain.Player{
@ -226,8 +221,8 @@ func TestPlayer_list(t *testing.T) {
},
}
if !cmp.Equal(params, expectedParams) {
return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams))
if diff := cmp.Diff(params, expectedParams); diff != "" {
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
players := []domain.Player{
@ -325,8 +320,8 @@ func TestPlayer_list(t *testing.T) {
},
}
if !cmp.Equal(params, expectedParams) {
return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams))
if diff := cmp.Diff(params, expectedParams); diff != "" {
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
players := []domain.Player{

View File

@ -5,6 +5,7 @@ import (
"net/url"
"strconv"
"strings"
"time"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
)
@ -140,6 +141,42 @@ func (q queryParams) playerSort() ([]domain.PlayerSort, error) {
return res, nil
}
func (q queryParams) ennoblementSort() ([]domain.EnnoblementSort, error) {
ss, err := q.sort()
if err != nil {
return nil, err
}
if ss == nil {
return nil, nil
}
res := make([]domain.EnnoblementSort, 0, len(ss))
for i, s := range ss {
by, err := domain.NewEnnoblementSortBy(s[0])
if err != nil {
return nil, domain.ValidationError{
Field: fmt.Sprintf("sort[%d]", i),
Err: err,
}
}
direction, err := domain.NewSortDirection(s[1])
if err != nil {
return nil, domain.ValidationError{
Field: fmt.Sprintf("sort[%d]", i),
Err: err,
}
}
res = append(res, domain.EnnoblementSort{
By: by,
Direction: direction,
})
}
return res, nil
}
func (q queryParams) sort() ([][2]string, error) {
ss, ok := q.values["sort"]
if !ok || len(ss) == 0 {
@ -161,3 +198,22 @@ func (q queryParams) sort() ([][2]string, error) {
return res, nil
}
func (q queryParams) nullTime(key string) (domain.NullTime, error) {
if !q.values.Has(key) {
return domain.NullTime{}, nil
}
t, err := time.Parse(time.RFC3339, q.values.Get(key))
if err != nil {
return domain.NullTime{}, domain.ValidationError{
Field: key,
Err: err,
}
}
return domain.NullTime{
Valid: true,
Time: t,
}, nil
}

View File

@ -29,13 +29,14 @@ type CORSConfig struct {
}
type RouterConfig struct {
VersionService VersionService
ServerService ServerService
TribeService TribeService
PlayerService PlayerService
VillageService VillageService
CORS CORSConfig
Swagger bool
VersionService VersionService
ServerService ServerService
TribeService TribeService
PlayerService PlayerService
VillageService VillageService
EnnoblementService EnnoblementService
CORS CORSConfig
Swagger bool
}
func NewRouter(cfg RouterConfig) *chi.Mux {
@ -55,6 +56,9 @@ func NewRouter(cfg RouterConfig) *chi.Mux {
villageHandler := &village{
svc: cfg.VillageService,
}
ennoblementHandler := &ennoblement{
svc: cfg.EnnoblementService,
}
router := chi.NewRouter()
@ -104,6 +108,8 @@ func NewRouter(cfg RouterConfig) *chi.Mux {
r.Get("/", villageHandler.list)
r.Get("/{villageId}", villageHandler.getByID)
})
r.With(verifyServerKeyMiddleware).
Get("/ennoblements", ennoblementHandler.list)
})
})
})

View File

@ -2,7 +2,6 @@ package rest_test
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
@ -61,8 +60,8 @@ func TestServer_list(t *testing.T) {
Count: true,
}
if !cmp.Equal(params, expectedParams) {
return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams))
if diff := cmp.Diff(params, expectedParams); diff != "" {
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
servers := []domain.Server{
@ -177,8 +176,8 @@ func TestServer_list(t *testing.T) {
Count: true,
}
if !cmp.Equal(params, expectedParams) {
return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams))
if diff := cmp.Diff(params, expectedParams); diff != "" {
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
servers := []domain.Server{
@ -298,8 +297,8 @@ func TestServer_list(t *testing.T) {
Count: true,
}
if !cmp.Equal(params, expectedParams) {
return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams))
if diff := cmp.Diff(params, expectedParams); diff != "" {
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
servers := []domain.Server{

View File

@ -2,7 +2,6 @@ package rest_test
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
@ -77,8 +76,8 @@ func TestTribe_list(t *testing.T) {
Count: true,
}
if !cmp.Equal(params, expectedParams) {
return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams))
if diff := cmp.Diff(params, expectedParams); diff != "" {
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
tribes := []domain.Tribe{
@ -220,8 +219,8 @@ func TestTribe_list(t *testing.T) {
Count: true,
}
if !cmp.Equal(params, expectedParams) {
return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams))
if diff := cmp.Diff(params, expectedParams); diff != "" {
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
tribes := []domain.Tribe{
@ -352,8 +351,8 @@ func TestTribe_list(t *testing.T) {
Count: true,
}
if !cmp.Equal(params, expectedParams) {
return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams))
if diff := cmp.Diff(params, expectedParams); diff != "" {
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
tribes := []domain.Tribe{

View File

@ -2,7 +2,6 @@ package rest_test
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
@ -63,8 +62,8 @@ func TestVillage_list(t *testing.T) {
Count: true,
}
if !cmp.Equal(params, expectedParams) {
return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams))
if diff := cmp.Diff(params, expectedParams); diff != "" {
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
villages := []domain.Village{
@ -220,8 +219,8 @@ func TestVillage_list(t *testing.T) {
Count: true,
}
if !cmp.Equal(params, expectedParams) {
return nil, 0, errors.New("invalid params: " + cmp.Diff(params, expectedParams))
if diff := cmp.Diff(params, expectedParams); diff != "" {
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
villages := []domain.Village{

View File

@ -9,6 +9,11 @@ import (
"gitea.dwysokinski.me/twhelp/core/internal/domain"
)
const (
ennoblementMaxLimit = 3000
ennoblementSortMaxLen = 2
)
var (
errEnnoblementNotFound = errors.New("ennoblement not found")
)
@ -64,6 +69,41 @@ func (e *Ennoblement) Refresh(ctx context.Context, key, url string) error {
return nil
}
func (e *Ennoblement) List(ctx context.Context, params domain.ListEnnoblementsParams) ([]domain.Ennoblement, int64, error) {
if len(params.Sort) == 0 {
params.Sort = []domain.EnnoblementSort{
{
By: domain.EnnoblementSortByID,
Direction: domain.SortDirectionASC,
},
}
}
if len(params.Sort) > ennoblementSortMaxLen {
return nil, 0, domain.ValidationError{
Field: "sort",
Err: domain.MaxLengthError{
Max: ennoblementSortMaxLen,
},
}
}
if params.Pagination.Limit == 0 {
params.Pagination.Limit = ennoblementMaxLimit
}
if err := validatePagination(params.Pagination, ennoblementMaxLimit); err != nil {
return nil, 0, fmt.Errorf("validatePagination: %w", err)
}
ennoblements, count, err := e.repo.List(ctx, params)
if err != nil {
return nil, 0, fmt.Errorf("EnnoblementRepository.List: %w", err)
}
return ennoblements, count, nil
}
func (e *Ennoblement) getLatestEnnoblement(ctx context.Context, key string) (domain.Ennoblement, error) {
ennoblements, _, err := e.repo.List(ctx, domain.ListEnnoblementsParams{
ServerKeys: []string{key},

View File

@ -2,9 +2,12 @@ package service_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"gitea.dwysokinski.me/twhelp/core/internal/service"
@ -123,3 +126,181 @@ func TestEnnoblement_Refresh(t *testing.T) {
})
}
}
func TestEnnoblement_List(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
limit int32
sort []domain.EnnoblementSort
}{
{
name: "default limit, default sort",
limit: 0,
sort: nil,
},
{
name: "custom limit",
limit: 2999,
},
{
name: "custom sort",
limit: 0,
sort: []domain.EnnoblementSort{
{By: domain.EnnoblementSortByCreatedAt, Direction: domain.SortDirectionDESC},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
limit := tt.limit
if limit == 0 {
limit = 3000
}
repo := &mock.FakeEnnoblementRepository{}
repo.ListCalls(func(_ context.Context, params domain.ListEnnoblementsParams) ([]domain.Ennoblement, int64, error) {
expectedParams := domain.ListEnnoblementsParams{
Pagination: domain.Pagination{
Limit: limit,
},
Sort: func(sort []domain.EnnoblementSort) []domain.EnnoblementSort {
if len(sort) == 0 {
return []domain.EnnoblementSort{
{
By: domain.EnnoblementSortByID,
Direction: domain.SortDirectionASC,
},
}
}
return sort
}(tt.sort),
}
if diff := cmp.Diff(params, expectedParams); diff != "" {
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
return make([]domain.Ennoblement, params.Pagination.Limit), int64(params.Pagination.Limit), nil
})
client := &mock.FakeEnnoblementsGetter{}
players, count, err := service.NewEnnoblement(repo, client).
List(context.Background(), domain.ListEnnoblementsParams{
Pagination: domain.Pagination{
Limit: tt.limit,
},
Sort: tt.sort,
})
assert.NoError(t, err)
assert.EqualValues(t, limit, count)
assert.Len(t, players, int(limit))
})
}
})
t.Run("ERR: validation failed", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
params domain.ListEnnoblementsParams
expectedErr error
}{
{
name: "params.Pagination.Limit < 0",
params: domain.ListEnnoblementsParams{
Pagination: domain.Pagination{
Limit: -1,
},
},
expectedErr: domain.ValidationError{
Field: "limit",
Err: domain.MinError{
Min: 1,
},
},
},
{
name: "params.Pagination.Limit > 3000",
params: domain.ListEnnoblementsParams{
Pagination: domain.Pagination{
Limit: 3001,
},
},
expectedErr: domain.ValidationError{
Field: "limit",
Err: domain.MaxError{
Max: 3000,
},
},
},
{
name: "params.Pagination.Offset < 0",
params: domain.ListEnnoblementsParams{
Pagination: domain.Pagination{
Offset: -1,
},
},
expectedErr: domain.ValidationError{
Field: "offset",
Err: domain.MinError{
Min: 0,
},
},
},
{
name: "len(params.Sort) > 2",
params: domain.ListEnnoblementsParams{
Sort: []domain.EnnoblementSort{
{
By: domain.EnnoblementSortByID,
Direction: domain.SortDirectionASC,
},
{
By: domain.EnnoblementSortByCreatedAt,
Direction: domain.SortDirectionASC,
},
{
By: domain.EnnoblementSortByCreatedAt,
Direction: domain.SortDirectionASC,
},
},
},
expectedErr: domain.ValidationError{
Field: "sort",
Err: domain.MaxLengthError{
Max: 2,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
repo := &mock.FakeEnnoblementRepository{}
client := &mock.FakeEnnoblementsGetter{}
players, count, err := service.NewEnnoblement(repo, client).
List(context.Background(), tt.params)
assert.ErrorIs(t, err, tt.expectedErr)
assert.Zero(t, players)
assert.Zero(t, count)
assert.Equal(t, 0, repo.ListCallCount())
})
}
})
}

View File

@ -176,8 +176,8 @@ func TestPlayer_List(t *testing.T) {
}(tt.sort),
}
if !cmp.Equal(params, expectedParams) {
return nil, 0, fmt.Errorf("validation failed: %s", cmp.Diff(params, expectedParams))
if diff := cmp.Diff(params, expectedParams); diff != "" {
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
return make([]domain.Player, params.Pagination.Limit), int64(params.Pagination.Limit), nil

View File

@ -189,8 +189,8 @@ func TestTribe_List(t *testing.T) {
}(tt.sort),
}
if !cmp.Equal(params, expectedParams) {
return nil, 0, fmt.Errorf("validation failed: %s", cmp.Diff(params, expectedParams))
if diff := cmp.Diff(params, expectedParams); diff != "" {
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
return make([]domain.Tribe, params.Pagination.Limit), int64(params.Pagination.Limit), nil

View File

@ -131,8 +131,8 @@ func TestVillage_List(t *testing.T) {
},
}
if !cmp.Equal(params, expectedParams) {
return nil, 0, fmt.Errorf("validation failed: %s", cmp.Diff(params, expectedParams))
if diff := cmp.Diff(params, expectedParams); diff != "" {
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
return make([]domain.Village, params.Pagination.Limit), int64(params.Pagination.Limit), nil