feat: add a new endpoint - GET /api/v1/versions/:code/servers/:key/players/:id/history (#152)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: twhelp/core#152
This commit is contained in:
parent
298f3fed02
commit
c2a35d4c98
|
@ -101,6 +101,7 @@ func newServer(cfg serverConfig) (*http.Server, error) {
|
||||||
villageRepo := bundb.NewVillage(cfg.db)
|
villageRepo := bundb.NewVillage(cfg.db)
|
||||||
ennoblementRepo := bundb.NewEnnoblement(cfg.db)
|
ennoblementRepo := bundb.NewEnnoblement(cfg.db)
|
||||||
tribeChangeRepo := bundb.NewTribeChange(cfg.db)
|
tribeChangeRepo := bundb.NewTribeChange(cfg.db)
|
||||||
|
playerSnapshotRepo := bundb.NewPlayerSnapshot(cfg.db)
|
||||||
|
|
||||||
// services
|
// services
|
||||||
versionSvc := service.NewVersion(versionRepo)
|
versionSvc := service.NewVersion(versionRepo)
|
||||||
|
@ -110,17 +111,19 @@ func newServer(cfg serverConfig) (*http.Server, error) {
|
||||||
playerSvc := service.NewPlayer(playerRepo, tribeChangeSvc, client)
|
playerSvc := service.NewPlayer(playerRepo, tribeChangeSvc, client)
|
||||||
villageSvc := service.NewVillage(villageRepo, client)
|
villageSvc := service.NewVillage(villageRepo, client)
|
||||||
ennoblementSvc := service.NewEnnoblement(ennoblementRepo, client)
|
ennoblementSvc := service.NewEnnoblement(ennoblementRepo, client)
|
||||||
|
playerSnapshotSvc := service.NewPlayerSnapshot(playerSnapshotRepo, playerSvc)
|
||||||
|
|
||||||
// router
|
// router
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Use(getChiMiddlewares(cfg.logger)...)
|
r.Use(getChiMiddlewares(cfg.logger)...)
|
||||||
r.Mount("/api", rest.NewRouter(rest.RouterConfig{
|
r.Mount("/api", rest.NewRouter(rest.RouterConfig{
|
||||||
VersionService: versionSvc,
|
VersionService: versionSvc,
|
||||||
ServerService: serverSvc,
|
ServerService: serverSvc,
|
||||||
TribeService: tribeSvc,
|
TribeService: tribeSvc,
|
||||||
PlayerService: playerSvc,
|
PlayerService: playerSvc,
|
||||||
VillageService: villageSvc,
|
VillageService: villageSvc,
|
||||||
EnnoblementService: ennoblementSvc,
|
EnnoblementService: ennoblementSvc,
|
||||||
|
PlayerSnapshotService: playerSnapshotSvc,
|
||||||
CORS: rest.CORSConfig{
|
CORS: rest.CORSConfig{
|
||||||
Enabled: apiCfg.CORSEnabled,
|
Enabled: apiCfg.CORSEnabled,
|
||||||
AllowedOrigins: apiCfg.CORSAllowedOrigins,
|
AllowedOrigins: apiCfg.CORSAllowedOrigins,
|
||||||
|
|
|
@ -16,6 +16,7 @@ type PlayerSnapshot struct {
|
||||||
Points int64 `bun:"points,default:0"`
|
Points int64 `bun:"points,default:0"`
|
||||||
Rank int64 `bun:"rank,default:0"`
|
Rank int64 `bun:"rank,default:0"`
|
||||||
TribeID int64 `bun:"tribe_id,nullzero"`
|
TribeID int64 `bun:"tribe_id,nullzero"`
|
||||||
|
Tribe Tribe `bun:"tribe,rel:belongs-to,join:tribe_id=id,join:server_key=server_key"`
|
||||||
ServerKey string `bun:"server_key,notnull,nullzero,type:varchar(100),unique:player_snapshots_player_id_server_key_date_key"`
|
ServerKey string `bun:"server_key,notnull,nullzero,type:varchar(100),unique:player_snapshots_player_id_server_key_date_key"`
|
||||||
Date time.Time `bun:"date,notnull,nullzero,type:date,unique:player_snapshots_player_id_server_key_date_key"`
|
Date time.Time `bun:"date,notnull,nullzero,type:date,unique:player_snapshots_player_id_server_key_date_key"`
|
||||||
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
|
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
|
||||||
|
@ -37,17 +38,32 @@ func NewPlayerSnapshot(p domain.CreatePlayerSnapshotParams) PlayerSnapshot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps PlayerSnapshot) ToDomain() domain.PlayerSnapshot {
|
func (p PlayerSnapshot) ToDomain() domain.PlayerSnapshot {
|
||||||
return domain.PlayerSnapshot{
|
return domain.PlayerSnapshot{
|
||||||
OpponentsDefeated: domain.OpponentsDefeated(ps.OpponentsDefeated),
|
OpponentsDefeated: domain.OpponentsDefeated(p.OpponentsDefeated),
|
||||||
ID: ps.ID,
|
ID: p.ID,
|
||||||
PlayerID: ps.PlayerID,
|
PlayerID: p.PlayerID,
|
||||||
NumVillages: ps.NumVillages,
|
NumVillages: p.NumVillages,
|
||||||
Points: ps.Points,
|
Points: p.Points,
|
||||||
Rank: ps.Rank,
|
Rank: p.Rank,
|
||||||
TribeID: ps.TribeID,
|
TribeID: p.TribeID,
|
||||||
ServerKey: ps.ServerKey,
|
ServerKey: p.ServerKey,
|
||||||
Date: ps.Date,
|
Date: p.Date,
|
||||||
CreatedAt: ps.CreatedAt,
|
CreatedAt: p.CreatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p PlayerSnapshot) ToDomainWithRelations() domain.PlayerSnapshotWithRelations {
|
||||||
|
res := domain.PlayerSnapshotWithRelations{
|
||||||
|
PlayerSnapshot: p.ToDomain(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Tribe.ID > 0 {
|
||||||
|
res.Tribe = domain.NullTribeMeta{
|
||||||
|
Valid: true,
|
||||||
|
Tribe: p.Tribe.ToMeta(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
|
@ -48,4 +48,14 @@ func TestPlayerSnapshot(t *testing.T) {
|
||||||
assert.Equal(t, params.ServerKey, snapshot.ServerKey)
|
assert.Equal(t, params.ServerKey, snapshot.ServerKey)
|
||||||
assert.Equal(t, params.Date, snapshot.Date)
|
assert.Equal(t, params.Date, snapshot.Date)
|
||||||
assert.WithinDuration(t, time.Now(), snapshot.CreatedAt, 10*time.Millisecond)
|
assert.WithinDuration(t, time.Now(), snapshot.CreatedAt, 10*time.Millisecond)
|
||||||
|
|
||||||
|
result.Tribe = model.Tribe{
|
||||||
|
ID: params.TribeID,
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshotWithRelations := result.ToDomainWithRelations()
|
||||||
|
assert.Equal(t, snapshot, snapshotWithRelations.PlayerSnapshot)
|
||||||
|
assert.WithinDuration(t, time.Now(), snapshotWithRelations.CreatedAt, 10*time.Millisecond)
|
||||||
|
assert.True(t, snapshotWithRelations.Tribe.Valid)
|
||||||
|
assert.Equal(t, params.TribeID, snapshotWithRelations.Tribe.Tribe.ID)
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,10 +108,6 @@ func (p *Player) ListCountWithRelations(ctx context.Context, params domain.ListP
|
||||||
|
|
||||||
paramsApplier := listPlayersParamsApplier{params}
|
paramsApplier := listPlayersParamsApplier{params}
|
||||||
|
|
||||||
cntQ := p.db.NewSelect().
|
|
||||||
Model(&model.Player{}).
|
|
||||||
Apply(paramsApplier.applyFilters)
|
|
||||||
|
|
||||||
subQ := p.db.NewSelect().
|
subQ := p.db.NewSelect().
|
||||||
Column("id", "server_key").
|
Column("id", "server_key").
|
||||||
Model(&model.Player{}).
|
Model(&model.Player{}).
|
||||||
|
@ -121,18 +117,24 @@ func (p *Player) ListCountWithRelations(ctx context.Context, params domain.ListP
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cntQ := p.db.NewSelect().
|
||||||
|
Model(&model.Player{}).
|
||||||
|
Apply(paramsApplier.applyFilters)
|
||||||
|
|
||||||
|
otherServersQ := p.db.NewSelect().
|
||||||
|
Model(&model.Player{}).
|
||||||
|
ExcludeColumn("*").
|
||||||
|
ColumnExpr("jsonb_agg(jsonb_build_object('key', player2.server_key, 'profileUrl', player2.profile_url, 'deletedAt', player2.deleted_at))").
|
||||||
|
ModelTableExpr("?TableName as player2").
|
||||||
|
Join("INNER JOIN servers AS server2 ON server2.key = player2.server_key").
|
||||||
|
Where("player2.id = player.id").
|
||||||
|
Where("server2.version_code = server.version_code").
|
||||||
|
Where("server2.key != server.key")
|
||||||
|
|
||||||
q := p.db.NewSelect().
|
q := p.db.NewSelect().
|
||||||
Model(&players).
|
Model(&players).
|
||||||
ColumnExpr("?TableColumns").
|
ColumnExpr("?TableColumns").
|
||||||
ColumnExpr("(?) AS other_servers", p.db.NewSelect().
|
ColumnExpr("(?) AS other_servers", otherServersQ).
|
||||||
Model(&model.Player{}).
|
|
||||||
ExcludeColumn("*").
|
|
||||||
ColumnExpr("jsonb_agg(jsonb_build_object('key', player2.server_key, 'profileUrl', player2.profile_url, 'deletedAt', player2.deleted_at))").
|
|
||||||
ModelTableExpr("?TableName as player2").
|
|
||||||
Join("INNER JOIN servers AS server2 ON server2.key = player2.server_key").
|
|
||||||
Where("player2.id = player.id").
|
|
||||||
Where("server2.version_code = server.version_code").
|
|
||||||
Where("server2.key != server.key")).
|
|
||||||
Order(playerOrders...).
|
Order(playerOrders...).
|
||||||
Where("(player.id, player.server_key) IN (?)", subQ).
|
Where("(player.id, player.server_key) IN (?)", subQ).
|
||||||
Relation("Tribe", func(q *bun.SelectQuery) *bun.SelectQuery {
|
Relation("Tribe", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||||
|
|
|
@ -12,6 +12,10 @@ import (
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
playerSnapshotOrders = []string{"ps.server_key ASC"}
|
||||||
|
)
|
||||||
|
|
||||||
type PlayerSnapshot struct {
|
type PlayerSnapshot struct {
|
||||||
db *bun.DB
|
db *bun.DB
|
||||||
}
|
}
|
||||||
|
@ -43,11 +47,15 @@ func (p *PlayerSnapshot) Create(ctx context.Context, params ...domain.CreatePlay
|
||||||
func (p *PlayerSnapshot) List(ctx context.Context, params domain.ListPlayerSnapshotsParams) ([]domain.PlayerSnapshot, error) {
|
func (p *PlayerSnapshot) List(ctx context.Context, params domain.ListPlayerSnapshotsParams) ([]domain.PlayerSnapshot, error) {
|
||||||
var snapshots []model.PlayerSnapshot
|
var snapshots []model.PlayerSnapshot
|
||||||
|
|
||||||
if err := p.db.NewSelect().
|
q := p.db.NewSelect().
|
||||||
Model(&snapshots).
|
Model(&snapshots).
|
||||||
Order("server_key ASC", "date ASC", "id ASC").
|
Order(playerSnapshotOrders...)
|
||||||
Apply(listPlayerSnapshotsParamsApplier{params}.apply).
|
q, err := listPlayerSnapshotsParamsApplier{params}.apply(q)
|
||||||
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("listPlayerSnapshotsParamsApplier.apply: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = q.Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, fmt.Errorf("couldn't select player snapshots from the db: %w", err)
|
return nil, fmt.Errorf("couldn't select player snapshots from the db: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,6 +67,52 @@ func (p *PlayerSnapshot) List(ctx context.Context, params domain.ListPlayerSnaps
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PlayerSnapshot) ListCountWithRelations(
|
||||||
|
ctx context.Context,
|
||||||
|
params domain.ListPlayerSnapshotsParams,
|
||||||
|
) ([]domain.PlayerSnapshotWithRelations, int64, error) {
|
||||||
|
var snapshots []model.PlayerSnapshot
|
||||||
|
|
||||||
|
paramsApplier := listPlayerSnapshotsParamsApplier{params}
|
||||||
|
|
||||||
|
subQ := p.db.NewSelect().
|
||||||
|
Column("id").
|
||||||
|
Model(&model.PlayerSnapshot{}).
|
||||||
|
Order(playerSnapshotOrders...)
|
||||||
|
subQ, err := paramsApplier.apply(subQ)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cntQ := p.db.NewSelect().
|
||||||
|
Model(&model.PlayerSnapshot{}).
|
||||||
|
Apply(paramsApplier.applyFilters)
|
||||||
|
|
||||||
|
q := p.db.NewSelect().
|
||||||
|
Model(&snapshots).
|
||||||
|
Order(playerSnapshotOrders...).
|
||||||
|
Where("ps.id IN (?)", subQ).
|
||||||
|
Relation("Tribe", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||||
|
return q.Column(tribeMetaColumns...)
|
||||||
|
})
|
||||||
|
q, err = paramsApplier.applySort(q)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := scanAndCount(ctx, cntQ, q)
|
||||||
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, 0, fmt.Errorf("couldn't select player snapshots from the db: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]domain.PlayerSnapshotWithRelations, 0, len(snapshots))
|
||||||
|
for _, snapshot := range snapshots {
|
||||||
|
result = append(result, snapshot.ToDomainWithRelations())
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, int64(count), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *PlayerSnapshot) Delete(ctx context.Context, serverKey string, createdAtLTE time.Time) error {
|
func (p *PlayerSnapshot) Delete(ctx context.Context, serverKey string, createdAtLTE time.Time) error {
|
||||||
if _, err := p.db.NewDelete().
|
if _, err := p.db.NewDelete().
|
||||||
Model(&model.PlayerSnapshot{}).
|
Model(&model.PlayerSnapshot{}).
|
||||||
|
@ -76,8 +130,12 @@ type listPlayerSnapshotsParamsApplier struct {
|
||||||
params domain.ListPlayerSnapshotsParams
|
params domain.ListPlayerSnapshotsParams
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l listPlayerSnapshotsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
|
func (l listPlayerSnapshotsParamsApplier) apply(q *bun.SelectQuery) (*bun.SelectQuery, error) {
|
||||||
return l.applyPagination(l.applyFilters(q))
|
q, err := l.applySort(q)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return l.applyPagination(l.applyFilters(q)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l listPlayerSnapshotsParamsApplier) applyFilters(q *bun.SelectQuery) *bun.SelectQuery {
|
func (l listPlayerSnapshotsParamsApplier) applyFilters(q *bun.SelectQuery) *bun.SelectQuery {
|
||||||
|
@ -85,9 +143,46 @@ func (l listPlayerSnapshotsParamsApplier) applyFilters(q *bun.SelectQuery) *bun.
|
||||||
q = q.Where("ps.server_key IN (?)", bun.In(l.params.ServerKeys))
|
q = q.Where("ps.server_key IN (?)", bun.In(l.params.ServerKeys))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if l.params.PlayerIDs != nil {
|
||||||
|
q = q.Where("ps.player_id IN (?)", bun.In(l.params.PlayerIDs))
|
||||||
|
}
|
||||||
|
|
||||||
return q
|
return q
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l listPlayerSnapshotsParamsApplier) applySort(q *bun.SelectQuery) (*bun.SelectQuery, error) {
|
||||||
|
if len(l.params.Sort) == 0 {
|
||||||
|
return q, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
orders := make([]string, 0, len(l.params.Sort))
|
||||||
|
for i, s := range l.params.Sort {
|
||||||
|
column, err := playerSnapshotSortByToColumn(s.By)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("playerSnapshotSortByToColumn (index=%d): %w", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
direction, err := sortDirectionToString(s.Direction)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("sortDirectionToString (index=%d): %w", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
orders = append(orders, column+" "+direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.Order(orders...), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (l listPlayerSnapshotsParamsApplier) applyPagination(q *bun.SelectQuery) *bun.SelectQuery {
|
func (l listPlayerSnapshotsParamsApplier) applyPagination(q *bun.SelectQuery) *bun.SelectQuery {
|
||||||
return (paginationApplier{pagination: l.params.Pagination}).apply(q)
|
return (paginationApplier{pagination: l.params.Pagination}).apply(q)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func playerSnapshotSortByToColumn(sortBy domain.PlayerSnapshotSortBy) (string, error) {
|
||||||
|
switch sortBy {
|
||||||
|
case domain.PlayerSnapshotSortByID:
|
||||||
|
return "ps.id", nil
|
||||||
|
case domain.PlayerSnapshotSortByDate:
|
||||||
|
return "ps.date", nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("%w: %d", domain.ErrUnsupportedSortBy, sortBy)
|
||||||
|
}
|
||||||
|
|
|
@ -217,26 +217,61 @@ func TestPlayerSnapshot_List(t *testing.T) {
|
||||||
name string
|
name string
|
||||||
params domain.ListPlayerSnapshotsParams
|
params domain.ListPlayerSnapshotsParams
|
||||||
expectedSnapshots []expectedSnapshots
|
expectedSnapshots []expectedSnapshots
|
||||||
|
expectedCount int64
|
||||||
|
expectedErr error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Empty struct",
|
name: "Sort=[{By=Date,Direction=ASC},{By=ID,Direction=ASC}]",
|
||||||
params: domain.ListPlayerSnapshotsParams{},
|
params: domain.ListPlayerSnapshotsParams{
|
||||||
|
Sort: []domain.PlayerSnapshotSort{
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByDate,
|
||||||
|
Direction: domain.SortDirectionASC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByID,
|
||||||
|
Direction: domain.SortDirectionASC,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
expectedSnapshots: allSnapshots,
|
expectedSnapshots: allSnapshots,
|
||||||
|
expectedCount: int64(len(allSnapshots)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ServerKey=[de188]",
|
name: "ServerKey=[de188],Sort=[{By=Date,Direction=ASC},{By=ID,Direction=ASC}]",
|
||||||
params: domain.ListPlayerSnapshotsParams{
|
params: domain.ListPlayerSnapshotsParams{
|
||||||
ServerKeys: []string{"de188"},
|
ServerKeys: []string{"de188"},
|
||||||
|
Sort: []domain.PlayerSnapshotSort{
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByDate,
|
||||||
|
Direction: domain.SortDirectionASC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByID,
|
||||||
|
Direction: domain.SortDirectionASC,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
expectedSnapshots: snapshotsDE188,
|
expectedSnapshots: snapshotsDE188,
|
||||||
|
expectedCount: int64(len(snapshotsDE188)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ServerKey=[pl169],Limit=2",
|
name: "ServerKey=[pl169],Sort=[{By=Date,Direction=ASC},{By=ID,Direction=ASC}],Limit=2",
|
||||||
params: domain.ListPlayerSnapshotsParams{
|
params: domain.ListPlayerSnapshotsParams{
|
||||||
ServerKeys: []string{"pl169"},
|
ServerKeys: []string{"pl169"},
|
||||||
Pagination: domain.Pagination{
|
Pagination: domain.Pagination{
|
||||||
Limit: 2,
|
Limit: 2,
|
||||||
},
|
},
|
||||||
|
Sort: []domain.PlayerSnapshotSort{
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByDate,
|
||||||
|
Direction: domain.SortDirectionASC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByID,
|
||||||
|
Direction: domain.SortDirectionASC,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
expectedSnapshots: []expectedSnapshots{
|
expectedSnapshots: []expectedSnapshots{
|
||||||
{
|
{
|
||||||
|
@ -248,14 +283,25 @@ func TestPlayerSnapshot_List(t *testing.T) {
|
||||||
serverKey: "pl169",
|
serverKey: "pl169",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
expectedCount: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ServerKey=[pl169],Offset=1",
|
name: "ServerKey=[pl169],Sort=[{By=Date,Direction=ASC},{By=ID,Direction=ASC}],Offset=1",
|
||||||
params: domain.ListPlayerSnapshotsParams{
|
params: domain.ListPlayerSnapshotsParams{
|
||||||
ServerKeys: []string{"pl169"},
|
ServerKeys: []string{"pl169"},
|
||||||
Pagination: domain.Pagination{
|
Pagination: domain.Pagination{
|
||||||
Offset: 1,
|
Offset: 1,
|
||||||
},
|
},
|
||||||
|
Sort: []domain.PlayerSnapshotSort{
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByDate,
|
||||||
|
Direction: domain.SortDirectionASC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByID,
|
||||||
|
Direction: domain.SortDirectionASC,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
expectedSnapshots: []expectedSnapshots{
|
expectedSnapshots: []expectedSnapshots{
|
||||||
{
|
{
|
||||||
|
@ -267,14 +313,48 @@ func TestPlayerSnapshot_List(t *testing.T) {
|
||||||
serverKey: "pl169",
|
serverKey: "pl169",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
expectedCount: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ServerKey=[pl169],Offset=1,Limit=1",
|
name: "ServerKey=[pl169],Sort=[{By=Date,Direction=DESC},{By=ID,Direction=ASC}],Offset=2,Limit=1",
|
||||||
params: domain.ListPlayerSnapshotsParams{
|
params: domain.ListPlayerSnapshotsParams{
|
||||||
ServerKeys: []string{"pl169"},
|
ServerKeys: []string{"pl169"},
|
||||||
Pagination: domain.Pagination{
|
Pagination: domain.Pagination{
|
||||||
Limit: 1,
|
Limit: 1,
|
||||||
Offset: 1,
|
Offset: 2,
|
||||||
|
},
|
||||||
|
Sort: []domain.PlayerSnapshotSort{
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByDate,
|
||||||
|
Direction: domain.SortDirectionDESC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByID,
|
||||||
|
Direction: domain.SortDirectionASC,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSnapshots: []expectedSnapshots{
|
||||||
|
{
|
||||||
|
id: 100000,
|
||||||
|
serverKey: "pl169",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedCount: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ServerKey=[pl169],PlayerIDs=[6180190],Sort=[{By=ID,Direction=DESC}],Limit=1",
|
||||||
|
params: domain.ListPlayerSnapshotsParams{
|
||||||
|
ServerKeys: []string{"pl169"},
|
||||||
|
PlayerIDs: []int64{6180190},
|
||||||
|
Pagination: domain.Pagination{
|
||||||
|
Limit: 1,
|
||||||
|
},
|
||||||
|
Sort: []domain.PlayerSnapshotSort{
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByID,
|
||||||
|
Direction: domain.SortDirectionDESC,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedSnapshots: []expectedSnapshots{
|
expectedSnapshots: []expectedSnapshots{
|
||||||
|
@ -283,6 +363,29 @@ func TestPlayerSnapshot_List(t *testing.T) {
|
||||||
serverKey: "pl169",
|
serverKey: "pl169",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
expectedCount: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: unsupported sort by",
|
||||||
|
params: domain.ListPlayerSnapshotsParams{
|
||||||
|
Sort: []domain.PlayerSnapshotSort{
|
||||||
|
{By: 100, Direction: domain.SortDirectionDESC},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSnapshots: nil,
|
||||||
|
expectedCount: 0,
|
||||||
|
expectedErr: domain.ErrUnsupportedSortBy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: unsupported sort direction",
|
||||||
|
params: domain.ListPlayerSnapshotsParams{
|
||||||
|
Sort: []domain.PlayerSnapshotSort{
|
||||||
|
{By: domain.PlayerSnapshotSortByDate, Direction: 100},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSnapshots: nil,
|
||||||
|
expectedCount: 0,
|
||||||
|
expectedErr: domain.ErrUnsupportedSortDirection,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,19 +395,41 @@ func TestPlayerSnapshot_List(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
res, err := repo.List(context.Background(), tt.params)
|
resListCountWithRelations, count, err := repo.ListCountWithRelations(context.Background(), tt.params)
|
||||||
assert.NoError(t, err)
|
assert.ErrorIs(t, err, tt.expectedErr)
|
||||||
assert.Len(t, res, len(tt.expectedSnapshots))
|
assert.Equal(t, tt.expectedCount, count)
|
||||||
|
assert.Len(t, resListCountWithRelations, len(tt.expectedSnapshots))
|
||||||
for _, expSnapshot := range tt.expectedSnapshots {
|
for _, expSnapshot := range tt.expectedSnapshots {
|
||||||
found := false
|
found := false
|
||||||
for _, snapshot := range res {
|
for _, snapshot := range resListCountWithRelations {
|
||||||
if snapshot.ID == expSnapshot.id && snapshot.ServerKey == expSnapshot.serverKey {
|
if snapshot.ID != expSnapshot.id {
|
||||||
found = true
|
continue
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if snapshot.ServerKey != expSnapshot.serverKey {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if snapshot.TribeID != 0 && (!snapshot.Tribe.Valid || snapshot.Tribe.Tribe.ID != snapshot.TribeID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if snapshot.TribeID == 0 && snapshot.Tribe.Valid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
found = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
assert.True(t, found, "snapshot (id=%d,serverkey=%s) not found", expSnapshot.id, expSnapshot.serverKey)
|
assert.True(t, found, "snapshot (id=%d,serverkey=%s) not found", expSnapshot.id, expSnapshot.serverKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resList, err := repo.List(context.Background(), tt.params)
|
||||||
|
assert.ErrorIs(t, err, tt.expectedErr)
|
||||||
|
assert.Len(t, resList, len(resListCountWithRelations))
|
||||||
|
for i, snapshot := range resList {
|
||||||
|
assert.Equal(t, resListCountWithRelations[i].PlayerSnapshot, snapshot)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,37 +10,30 @@ import (
|
||||||
func TestNewEnnoblementSortBy(t *testing.T) {
|
func TestNewEnnoblementSortBy(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("OK", func(t *testing.T) {
|
tests := []struct {
|
||||||
t.Parallel()
|
s string
|
||||||
|
output domain.EnnoblementSortBy
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
s: "createdAt",
|
||||||
|
output: domain.EnnoblementSortByCreatedAt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "unsupported",
|
||||||
|
expectedErr: domain.ErrUnsupportedSortBy,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
tests := []struct {
|
for _, tt := range tests {
|
||||||
s string
|
tt := tt
|
||||||
output domain.EnnoblementSortBy
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
s: "createdAt",
|
|
||||||
output: domain.EnnoblementSortByCreatedAt,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
t.Run(tt.s, func(t *testing.T) {
|
||||||
tt := tt
|
t.Parallel()
|
||||||
|
|
||||||
t.Run(tt.s, func(t *testing.T) {
|
res, err := domain.NewEnnoblementSortBy(tt.s)
|
||||||
t.Parallel()
|
assert.ErrorIs(t, err, tt.expectedErr)
|
||||||
|
assert.Equal(t, tt.output, res)
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,63 +10,56 @@ import (
|
||||||
func TestNewPlayerSortBy(t *testing.T) {
|
func TestNewPlayerSortBy(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("OK", func(t *testing.T) {
|
tests := []struct {
|
||||||
t.Parallel()
|
s string
|
||||||
|
output domain.PlayerSortBy
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
s: "id",
|
||||||
|
output: domain.PlayerSortByID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "scoreAtt",
|
||||||
|
output: domain.PlayerSortByScoreAtt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "scoreDef",
|
||||||
|
output: domain.PlayerSortByScoreDef,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "scoreSup",
|
||||||
|
output: domain.PlayerSortByScoreSup,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "scoreTotal",
|
||||||
|
output: domain.PlayerSortByScoreTotal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "points",
|
||||||
|
output: domain.PlayerSortByPoints,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "deletedAt",
|
||||||
|
output: domain.PlayerSortByDeletedAt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "unsupported",
|
||||||
|
expectedErr: domain.ErrUnsupportedSortBy,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
tests := []struct {
|
for _, tt := range tests {
|
||||||
s string
|
tt := tt
|
||||||
output domain.PlayerSortBy
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
s: "id",
|
|
||||||
output: domain.PlayerSortByID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "scoreAtt",
|
|
||||||
output: domain.PlayerSortByScoreAtt,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "scoreDef",
|
|
||||||
output: domain.PlayerSortByScoreDef,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "scoreSup",
|
|
||||||
output: domain.PlayerSortByScoreSup,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "scoreTotal",
|
|
||||||
output: domain.PlayerSortByScoreTotal,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "points",
|
|
||||||
output: domain.PlayerSortByPoints,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "deletedAt",
|
|
||||||
output: domain.PlayerSortByDeletedAt,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
t.Run(tt.s, func(t *testing.T) {
|
||||||
tt := tt
|
t.Parallel()
|
||||||
|
|
||||||
t.Run(tt.s, func(t *testing.T) {
|
res, err := domain.NewPlayerSortBy(tt.s)
|
||||||
t.Parallel()
|
assert.ErrorIs(t, err, tt.expectedErr)
|
||||||
|
assert.Equal(t, tt.output, res)
|
||||||
res, err := domain.NewPlayerSortBy(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.NewPlayerSortBy("unsupported")
|
|
||||||
assert.ErrorIs(t, err, domain.ErrUnsupportedSortBy)
|
|
||||||
assert.Zero(t, res)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPlayerNotFoundError(t *testing.T) {
|
func TestPlayerNotFoundError(t *testing.T) {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type PlayerSnapshot struct {
|
type PlayerSnapshot struct {
|
||||||
OpponentsDefeated
|
OpponentsDefeated
|
||||||
|
@ -16,6 +19,12 @@ type PlayerSnapshot struct {
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PlayerSnapshotWithRelations struct {
|
||||||
|
PlayerSnapshot
|
||||||
|
|
||||||
|
Tribe NullTribeMeta
|
||||||
|
}
|
||||||
|
|
||||||
type CreatePlayerSnapshotParams struct {
|
type CreatePlayerSnapshotParams struct {
|
||||||
OpponentsDefeated
|
OpponentsDefeated
|
||||||
|
|
||||||
|
@ -28,9 +37,33 @@ type CreatePlayerSnapshotParams struct {
|
||||||
Date time.Time
|
Date time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PlayerSnapshotSortBy uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlayerSnapshotSortByID PlayerSnapshotSortBy = iota
|
||||||
|
PlayerSnapshotSortByDate
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewPlayerSnapshotSortBy(s string) (PlayerSnapshotSortBy, error) {
|
||||||
|
switch s {
|
||||||
|
case "id":
|
||||||
|
return PlayerSnapshotSortByID, nil
|
||||||
|
case "date":
|
||||||
|
return PlayerSnapshotSortByDate, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%w: \"%s\"", ErrUnsupportedSortBy, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlayerSnapshotSort struct {
|
||||||
|
By PlayerSnapshotSortBy
|
||||||
|
Direction SortDirection
|
||||||
|
}
|
||||||
|
|
||||||
type ListPlayerSnapshotsParams struct {
|
type ListPlayerSnapshotsParams struct {
|
||||||
ServerKeys []string
|
ServerKeys []string
|
||||||
|
PlayerIDs []int64
|
||||||
Pagination Pagination
|
Pagination Pagination
|
||||||
|
Sort []PlayerSnapshotSort
|
||||||
}
|
}
|
||||||
|
|
||||||
type TribeSnapshot struct {
|
type TribeSnapshot struct {
|
||||||
|
|
43
internal/domain/snapshot_test.go
Normal file
43
internal/domain/snapshot_test.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package domain_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewPlayerSnapshotSortBy(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
s string
|
||||||
|
output domain.PlayerSnapshotSortBy
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
s: "id",
|
||||||
|
output: domain.PlayerSnapshotSortByID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "date",
|
||||||
|
output: domain.PlayerSnapshotSortByDate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "unsupported",
|
||||||
|
expectedErr: domain.ErrUnsupportedSortBy,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
|
||||||
|
t.Run(tt.s, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
res, err := domain.NewPlayerSnapshotSortBy(tt.s)
|
||||||
|
assert.ErrorIs(t, err, tt.expectedErr)
|
||||||
|
assert.Equal(t, tt.output, res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,49 +10,42 @@ import (
|
||||||
func TestNewSortDirection(t *testing.T) {
|
func TestNewSortDirection(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("OK", func(t *testing.T) {
|
tests := []struct {
|
||||||
t.Parallel()
|
s string
|
||||||
|
output domain.SortDirection
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
s: "asc",
|
||||||
|
output: domain.SortDirectionASC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "ASC",
|
||||||
|
output: domain.SortDirectionASC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "desc",
|
||||||
|
output: domain.SortDirectionDESC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "DESC",
|
||||||
|
output: domain.SortDirectionDESC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "unsupported",
|
||||||
|
expectedErr: domain.ErrUnsupportedSortDirection,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
tests := []struct {
|
for _, tt := range tests {
|
||||||
s string
|
tt := tt
|
||||||
output domain.SortDirection
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
s: "asc",
|
|
||||||
output: domain.SortDirectionASC,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "ASC",
|
|
||||||
output: domain.SortDirectionASC,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "desc",
|
|
||||||
output: domain.SortDirectionDESC,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "DESC",
|
|
||||||
output: domain.SortDirectionDESC,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
t.Run(tt.s, func(t *testing.T) {
|
||||||
tt := tt
|
t.Parallel()
|
||||||
|
|
||||||
t.Run(tt.s, func(t *testing.T) {
|
res, err := domain.NewSortDirection(tt.s)
|
||||||
t.Parallel()
|
assert.ErrorIs(t, err, tt.expectedErr)
|
||||||
|
assert.Equal(t, tt.output, res)
|
||||||
res, err := domain.NewSortDirection(tt.s)
|
})
|
||||||
assert.NoError(t, err)
|
}
|
||||||
assert.Equal(t, tt.output, res)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("ERR: unsupported sort direction", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
res, err := domain.NewSortDirection("unsupported")
|
|
||||||
assert.ErrorIs(t, err, domain.ErrUnsupportedSortDirection)
|
|
||||||
assert.Zero(t, res)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,63 +11,56 @@ import (
|
||||||
func TestNewTribeSortBy(t *testing.T) {
|
func TestNewTribeSortBy(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("OK", func(t *testing.T) {
|
tests := []struct {
|
||||||
t.Parallel()
|
s string
|
||||||
|
output domain.TribeSortBy
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
s: "id",
|
||||||
|
output: domain.TribeSortByID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "scoreAtt",
|
||||||
|
output: domain.TribeSortByScoreAtt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "scoreDef",
|
||||||
|
output: domain.TribeSortByScoreDef,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "scoreTotal",
|
||||||
|
output: domain.TribeSortByScoreTotal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "points",
|
||||||
|
output: domain.TribeSortByPoints,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "dominance",
|
||||||
|
output: domain.TribeSortByDominance,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "deletedAt",
|
||||||
|
output: domain.TribeSortByDeletedAt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
s: "unsupported",
|
||||||
|
expectedErr: domain.ErrUnsupportedSortBy,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
tests := []struct {
|
for _, tt := range tests {
|
||||||
s string
|
tt := tt
|
||||||
output domain.TribeSortBy
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
s: "id",
|
|
||||||
output: domain.TribeSortByID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "scoreAtt",
|
|
||||||
output: domain.TribeSortByScoreAtt,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "scoreDef",
|
|
||||||
output: domain.TribeSortByScoreDef,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "scoreTotal",
|
|
||||||
output: domain.TribeSortByScoreTotal,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "points",
|
|
||||||
output: domain.TribeSortByPoints,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "dominance",
|
|
||||||
output: domain.TribeSortByDominance,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "deletedAt",
|
|
||||||
output: domain.TribeSortByDeletedAt,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
t.Run(tt.s, func(t *testing.T) {
|
||||||
tt := tt
|
t.Parallel()
|
||||||
|
|
||||||
t.Run(tt.s, func(t *testing.T) {
|
res, err := domain.NewTribeSortBy(tt.s)
|
||||||
t.Parallel()
|
assert.ErrorIs(t, err, tt.expectedErr)
|
||||||
|
assert.Equal(t, tt.output, res)
|
||||||
res, err := domain.NewTribeSortBy(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.NewTribeSortBy("unsupported")
|
|
||||||
assert.ErrorIs(t, err, domain.ErrUnsupportedSortBy)
|
|
||||||
assert.Zero(t, res)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTribeNotFoundError(t *testing.T) {
|
func TestTribeNotFoundError(t *testing.T) {
|
||||||
|
|
|
@ -63,8 +63,8 @@ func (e *ennoblement) list(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ID listPlayerEnnoblements
|
// @ID listPlayerEnnoblements
|
||||||
// @Summary List the given user's ennoblements
|
// @Summary List the given player's ennoblements
|
||||||
// @Description List the given user's ennoblements
|
// @Description List the given player's ennoblements
|
||||||
// @Tags versions,servers,players,ennoblements
|
// @Tags versions,servers,players,ennoblements
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} model.ListEnnoblementsResp
|
// @Success 200 {object} model.ListEnnoblementsResp
|
||||||
|
|
|
@ -4,6 +4,10 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dateFormat = "2006-01-02"
|
||||||
|
)
|
||||||
|
|
||||||
type NullTime struct {
|
type NullTime struct {
|
||||||
Time time.Time
|
Time time.Time
|
||||||
Valid bool
|
Valid bool
|
||||||
|
|
|
@ -9,6 +9,10 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dateFormat = "2006-01-02"
|
||||||
|
)
|
||||||
|
|
||||||
func TestNullTime_MarshalJSON(t *testing.T) {
|
func TestNullTime_MarshalJSON(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|
63
internal/router/rest/internal/model/player_snapshot.go
Normal file
63
internal/router/rest/internal/model/player_snapshot.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlayerSnapshot struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
NumVillages int64 `json:"numVillages"`
|
||||||
|
Points int64 `json:"points"`
|
||||||
|
Rank int64 `json:"rank"`
|
||||||
|
Tribe NullTribeMeta `json:"tribe" extensions:"x-nullable"`
|
||||||
|
RankAtt int64 `json:"rankAtt"`
|
||||||
|
ScoreAtt int64 `json:"scoreAtt"`
|
||||||
|
RankDef int64 `json:"rankDef"`
|
||||||
|
ScoreDef int64 `json:"scoreDef"`
|
||||||
|
RankSup int64 `json:"rankSup"`
|
||||||
|
ScoreSup int64 `json:"scoreSup"`
|
||||||
|
RankTotal int64 `json:"rankTotal"`
|
||||||
|
ScoreTotal int64 `json:"scoreTotal"`
|
||||||
|
Date string `json:"date" swaggertype:"string" format:"date"`
|
||||||
|
} // @name PlayerSnapshot
|
||||||
|
|
||||||
|
func NewPlayerSnapshot(s domain.PlayerSnapshotWithRelations) PlayerSnapshot {
|
||||||
|
var t NullTribeMeta
|
||||||
|
if s.Tribe.Valid {
|
||||||
|
t = NullTribeMeta{
|
||||||
|
Valid: true,
|
||||||
|
Tribe: NewTribeMeta(s.Tribe.Tribe),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PlayerSnapshot{
|
||||||
|
ID: s.ID,
|
||||||
|
NumVillages: s.NumVillages,
|
||||||
|
Points: s.Points,
|
||||||
|
Rank: s.Rank,
|
||||||
|
Tribe: t,
|
||||||
|
RankAtt: s.RankAtt,
|
||||||
|
ScoreAtt: s.ScoreAtt,
|
||||||
|
RankDef: s.RankDef,
|
||||||
|
ScoreDef: s.ScoreDef,
|
||||||
|
RankSup: s.RankSup,
|
||||||
|
ScoreSup: s.ScoreSup,
|
||||||
|
RankTotal: s.RankTotal,
|
||||||
|
ScoreTotal: s.ScoreTotal,
|
||||||
|
Date: s.Date.Format(dateFormat),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPlayerSnapshotsResp struct {
|
||||||
|
Data []PlayerSnapshot `json:"data"`
|
||||||
|
} // @name ListPlayerSnapshotsResp
|
||||||
|
|
||||||
|
func NewListPlayerSnapshotsResp(snapshots []domain.PlayerSnapshotWithRelations) ListPlayerSnapshotsResp {
|
||||||
|
resp := ListPlayerSnapshotsResp{
|
||||||
|
Data: make([]PlayerSnapshot, 0, len(snapshots)),
|
||||||
|
}
|
||||||
|
for _, p := range snapshots {
|
||||||
|
resp.Data = append(resp.Data, NewPlayerSnapshot(p))
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
137
internal/router/rest/internal/model/player_snapshot_test.go
Normal file
137
internal/router/rest/internal/model/player_snapshot_test.go
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
package model_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
||||||
|
"gitea.dwysokinski.me/twhelp/core/internal/router/rest/internal/model"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewPlayerSnapshot(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
snapshot := domain.PlayerSnapshotWithRelations{
|
||||||
|
PlayerSnapshot: domain.PlayerSnapshot{
|
||||||
|
OpponentsDefeated: domain.OpponentsDefeated{
|
||||||
|
RankAtt: 99,
|
||||||
|
ScoreAtt: 100,
|
||||||
|
RankDef: 101,
|
||||||
|
ScoreDef: 102,
|
||||||
|
RankSup: 103,
|
||||||
|
ScoreSup: 104,
|
||||||
|
RankTotal: 105,
|
||||||
|
ScoreTotal: 106,
|
||||||
|
},
|
||||||
|
ID: 999,
|
||||||
|
PlayerID: 150,
|
||||||
|
ServerKey: "pl151",
|
||||||
|
NumVillages: 100,
|
||||||
|
Points: 101,
|
||||||
|
Rank: 102,
|
||||||
|
TribeID: 15,
|
||||||
|
Date: time.Now(),
|
||||||
|
CreatedAt: time.Now().Add(-5 * time.Minute),
|
||||||
|
},
|
||||||
|
Tribe: domain.NullTribeMeta{
|
||||||
|
Valid: true,
|
||||||
|
Tribe: domain.TribeMeta{
|
||||||
|
ID: 15,
|
||||||
|
Name: "Name",
|
||||||
|
Tag: "Tag",
|
||||||
|
ProfileURL: "profile-15",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assertPlayerSnapshot(t, snapshot, model.NewPlayerSnapshot(snapshot))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewListPlayerSnapshotsResp(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
snapshots := []domain.PlayerSnapshotWithRelations{
|
||||||
|
{
|
||||||
|
PlayerSnapshot: domain.PlayerSnapshot{
|
||||||
|
OpponentsDefeated: domain.OpponentsDefeated{
|
||||||
|
RankAtt: 99,
|
||||||
|
ScoreAtt: 100,
|
||||||
|
RankDef: 101,
|
||||||
|
ScoreDef: 102,
|
||||||
|
RankSup: 103,
|
||||||
|
ScoreSup: 104,
|
||||||
|
RankTotal: 105,
|
||||||
|
ScoreTotal: 106,
|
||||||
|
},
|
||||||
|
ID: 999,
|
||||||
|
PlayerID: 150,
|
||||||
|
ServerKey: "pl151",
|
||||||
|
NumVillages: 100,
|
||||||
|
Points: 101,
|
||||||
|
Rank: 102,
|
||||||
|
TribeID: 15,
|
||||||
|
Date: time.Now(),
|
||||||
|
CreatedAt: time.Now().Add(-5 * time.Minute),
|
||||||
|
},
|
||||||
|
Tribe: domain.NullTribeMeta{
|
||||||
|
Valid: true,
|
||||||
|
Tribe: domain.TribeMeta{
|
||||||
|
ID: 15,
|
||||||
|
Name: "Name",
|
||||||
|
Tag: "Tag",
|
||||||
|
ProfileURL: "profile-15",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
PlayerSnapshot: domain.PlayerSnapshot{
|
||||||
|
OpponentsDefeated: domain.OpponentsDefeated{
|
||||||
|
RankAtt: 999,
|
||||||
|
ScoreAtt: 1000,
|
||||||
|
RankDef: 1010,
|
||||||
|
ScoreDef: 1020,
|
||||||
|
RankSup: 1030,
|
||||||
|
ScoreSup: 1040,
|
||||||
|
RankTotal: 1050,
|
||||||
|
ScoreTotal: 1060,
|
||||||
|
},
|
||||||
|
ID: 9990,
|
||||||
|
PlayerID: 150,
|
||||||
|
ServerKey: "pl151",
|
||||||
|
NumVillages: 1000,
|
||||||
|
Points: 1010,
|
||||||
|
Rank: 1020,
|
||||||
|
TribeID: 0,
|
||||||
|
Date: time.Now().Add(-24 * time.Hour),
|
||||||
|
CreatedAt: time.Now().Add(-36 * time.Hour),
|
||||||
|
},
|
||||||
|
Tribe: domain.NullTribeMeta{
|
||||||
|
Valid: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp := model.NewListPlayerSnapshotsResp(snapshots)
|
||||||
|
assert.Len(t, resp.Data, len(snapshots))
|
||||||
|
for i, snapshot := range resp.Data {
|
||||||
|
assertPlayerSnapshot(t, snapshots[i], snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertPlayerSnapshot(tb testing.TB, dp domain.PlayerSnapshotWithRelations, rp model.PlayerSnapshot) {
|
||||||
|
tb.Helper()
|
||||||
|
|
||||||
|
assert.Equal(tb, dp.ID, rp.ID)
|
||||||
|
assert.Equal(tb, dp.NumVillages, rp.NumVillages)
|
||||||
|
assert.Equal(tb, dp.Points, rp.Points)
|
||||||
|
assert.Equal(tb, dp.Rank, rp.Rank)
|
||||||
|
assert.Equal(tb, dp.RankAtt, rp.RankAtt)
|
||||||
|
assert.Equal(tb, dp.ScoreAtt, rp.ScoreAtt)
|
||||||
|
assert.Equal(tb, dp.RankDef, rp.RankDef)
|
||||||
|
assert.Equal(tb, dp.ScoreDef, rp.ScoreDef)
|
||||||
|
assert.Equal(tb, dp.RankSup, rp.RankSup)
|
||||||
|
assert.Equal(tb, dp.ScoreSup, rp.ScoreSup)
|
||||||
|
assert.Equal(tb, dp.RankTotal, rp.RankTotal)
|
||||||
|
assert.Equal(tb, dp.ScoreTotal, rp.ScoreTotal)
|
||||||
|
assert.Equal(tb, dp.Date.Format(dateFormat), rp.Date)
|
||||||
|
assertNullTribeMeta(tb, dp.Tribe, rp.Tribe)
|
||||||
|
}
|
|
@ -123,7 +123,7 @@ func verifyPlayerID(svc PlayerService) func(http.Handler) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := svc.GetByServerKeyAndID(
|
if _, err = svc.GetByServerKeyAndID(
|
||||||
ctx,
|
ctx,
|
||||||
routeCtx.URLParam("serverKey"),
|
routeCtx.URLParam("serverKey"),
|
||||||
playerID,
|
playerID,
|
||||||
|
|
80
internal/router/rest/player_snapshot.go
Normal file
80
internal/router/rest/player_snapshot.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
||||||
|
"gitea.dwysokinski.me/twhelp/core/internal/router/rest/internal/model"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
playerSnapshotDefaultOffset = 0
|
||||||
|
playerSnapshotDefaultLimit = 100
|
||||||
|
)
|
||||||
|
|
||||||
|
//counterfeiter:generate -o internal/mock/player_snapshot_service.gen.go . PlayerSnapshotService
|
||||||
|
type PlayerSnapshotService interface {
|
||||||
|
ListCountWithRelations(ctx context.Context, params domain.ListPlayerSnapshotsParams) ([]domain.PlayerSnapshotWithRelations, int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type playerSnapshot struct {
|
||||||
|
svc PlayerSnapshotService
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ID listPlayerSnapshots
|
||||||
|
// @Summary List the given player's snapshots
|
||||||
|
// @Description List the given player's snapshots
|
||||||
|
// @Tags versions,servers,players,snapshots
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} model.ListPlayerSnapshotsResp
|
||||||
|
// @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 playerId path integer true "Player ID"
|
||||||
|
// @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(100) default(100)
|
||||||
|
// @Param sort query []string false "format: field:direction, default: [date:asc,id:asc]" Enums(id:asc,id:desc,date:asc,date:desc)
|
||||||
|
// @Router /versions/{versionCode}/servers/{serverKey}/players/{playerId}/history [get]
|
||||||
|
func (p *playerSnapshot) list(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var err error
|
||||||
|
ctx := r.Context()
|
||||||
|
routeCtx := chi.RouteContext(ctx)
|
||||||
|
|
||||||
|
params := domain.ListPlayerSnapshotsParams{
|
||||||
|
ServerKeys: []string{routeCtx.URLParam("serverKey")},
|
||||||
|
}
|
||||||
|
query := queryParams{r.URL.Query()}
|
||||||
|
|
||||||
|
playerID, err := urlParamInt64(routeCtx, "playerId")
|
||||||
|
if err != nil {
|
||||||
|
renderErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params.PlayerIDs = []int64{playerID}
|
||||||
|
|
||||||
|
params.Pagination, err = query.pagination(playerSnapshotDefaultOffset, playerSnapshotDefaultLimit)
|
||||||
|
if err != nil {
|
||||||
|
renderErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params.Sort, err = query.playerSnapshotSort()
|
||||||
|
if err != nil {
|
||||||
|
renderErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots, count, err := p.svc.ListCountWithRelations(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
renderErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setTotalCountHeader(w.Header(), count)
|
||||||
|
renderJSON(w, http.StatusOK, model.NewListPlayerSnapshotsResp(snapshots))
|
||||||
|
}
|
567
internal/router/rest/player_snapshot_test.go
Normal file
567
internal/router/rest/player_snapshot_test.go
Normal file
|
@ -0,0 +1,567 @@
|
||||||
|
package rest_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
||||||
|
"gitea.dwysokinski.me/twhelp/core/internal/router/rest"
|
||||||
|
"gitea.dwysokinski.me/twhelp/core/internal/router/rest/internal/mock"
|
||||||
|
"gitea.dwysokinski.me/twhelp/core/internal/router/rest/internal/model"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPlayerSnapshot_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",
|
||||||
|
}
|
||||||
|
player := domain.Player{
|
||||||
|
ID: 124,
|
||||||
|
Name: "player 124",
|
||||||
|
ProfileURL: "player-124",
|
||||||
|
}
|
||||||
|
playerID := strconv.FormatInt(player.ID, 10)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setup func(
|
||||||
|
versionSvc *mock.FakeVersionService,
|
||||||
|
serverSvc *mock.FakeServerService,
|
||||||
|
playerSvc *mock.FakePlayerService,
|
||||||
|
playerSnapshotSvc *mock.FakePlayerSnapshotService,
|
||||||
|
)
|
||||||
|
versionCode string
|
||||||
|
key string
|
||||||
|
playerID string
|
||||||
|
queryParams url.Values
|
||||||
|
expectedStatus int
|
||||||
|
target any
|
||||||
|
expectedResponse any
|
||||||
|
expectedTotalCount string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OK: without params",
|
||||||
|
setup: func(
|
||||||
|
versionSvc *mock.FakeVersionService,
|
||||||
|
serverSvc *mock.FakeServerService,
|
||||||
|
playerSvc *mock.FakePlayerService,
|
||||||
|
playerSnapshotSvc *mock.FakePlayerSnapshotService,
|
||||||
|
) {
|
||||||
|
versionSvc.GetByCodeReturns(version, nil)
|
||||||
|
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
|
||||||
|
playerSvc.GetByServerKeyAndIDReturns(player, nil)
|
||||||
|
playerSnapshotSvc.ListCountWithRelationsCalls(func(
|
||||||
|
_ context.Context,
|
||||||
|
params domain.ListPlayerSnapshotsParams,
|
||||||
|
) ([]domain.PlayerSnapshotWithRelations, int64, error) {
|
||||||
|
expectedParams := domain.ListPlayerSnapshotsParams{
|
||||||
|
ServerKeys: []string{server.Key},
|
||||||
|
PlayerIDs: []int64{player.ID},
|
||||||
|
Pagination: domain.Pagination{
|
||||||
|
Limit: 100,
|
||||||
|
Offset: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(params, expectedParams); diff != "" {
|
||||||
|
return nil, 0, fmt.Errorf("validation failed: %s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots := []domain.PlayerSnapshotWithRelations{
|
||||||
|
{
|
||||||
|
PlayerSnapshot: domain.PlayerSnapshot{
|
||||||
|
OpponentsDefeated: domain.OpponentsDefeated{
|
||||||
|
RankAtt: 99,
|
||||||
|
ScoreAtt: 100,
|
||||||
|
RankDef: 101,
|
||||||
|
ScoreDef: 102,
|
||||||
|
RankSup: 103,
|
||||||
|
ScoreSup: 104,
|
||||||
|
RankTotal: 105,
|
||||||
|
ScoreTotal: 106,
|
||||||
|
},
|
||||||
|
ID: 999,
|
||||||
|
PlayerID: player.ID,
|
||||||
|
ServerKey: server.Key,
|
||||||
|
NumVillages: 100,
|
||||||
|
Points: 101,
|
||||||
|
Rank: 102,
|
||||||
|
TribeID: 15,
|
||||||
|
Date: now,
|
||||||
|
CreatedAt: now.Add(-5 * time.Minute),
|
||||||
|
},
|
||||||
|
Tribe: domain.NullTribeMeta{
|
||||||
|
Valid: true,
|
||||||
|
Tribe: domain.TribeMeta{
|
||||||
|
ID: 15,
|
||||||
|
Name: "Name",
|
||||||
|
Tag: "Tag",
|
||||||
|
ProfileURL: "profile-15",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
PlayerSnapshot: domain.PlayerSnapshot{
|
||||||
|
OpponentsDefeated: domain.OpponentsDefeated{
|
||||||
|
RankAtt: 999,
|
||||||
|
ScoreAtt: 1000,
|
||||||
|
RankDef: 1010,
|
||||||
|
ScoreDef: 1020,
|
||||||
|
RankSup: 1030,
|
||||||
|
ScoreSup: 1040,
|
||||||
|
RankTotal: 1050,
|
||||||
|
ScoreTotal: 1060,
|
||||||
|
},
|
||||||
|
ID: 9990,
|
||||||
|
PlayerID: player.ID,
|
||||||
|
ServerKey: server.Key,
|
||||||
|
NumVillages: 1000,
|
||||||
|
Points: 1010,
|
||||||
|
Rank: 1020,
|
||||||
|
TribeID: 0,
|
||||||
|
Date: now.Add(-24 * time.Hour),
|
||||||
|
CreatedAt: now.Add(-36 * time.Hour),
|
||||||
|
},
|
||||||
|
Tribe: domain.NullTribeMeta{
|
||||||
|
Valid: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return snapshots, int64(len(snapshots)), nil
|
||||||
|
})
|
||||||
|
},
|
||||||
|
versionCode: version.Code,
|
||||||
|
key: server.Key,
|
||||||
|
playerID: playerID,
|
||||||
|
queryParams: nil,
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
target: &model.ListPlayerSnapshotsResp{},
|
||||||
|
expectedResponse: &model.ListPlayerSnapshotsResp{
|
||||||
|
Data: []model.PlayerSnapshot{
|
||||||
|
{
|
||||||
|
ID: 999,
|
||||||
|
NumVillages: 100,
|
||||||
|
Points: 101,
|
||||||
|
Rank: 102,
|
||||||
|
RankAtt: 99,
|
||||||
|
ScoreAtt: 100,
|
||||||
|
RankDef: 101,
|
||||||
|
ScoreDef: 102,
|
||||||
|
RankSup: 103,
|
||||||
|
ScoreSup: 104,
|
||||||
|
RankTotal: 105,
|
||||||
|
ScoreTotal: 106,
|
||||||
|
Tribe: model.NullTribeMeta{
|
||||||
|
Tribe: model.TribeMeta{
|
||||||
|
ID: 15,
|
||||||
|
Name: "Name",
|
||||||
|
Tag: "Tag",
|
||||||
|
ProfileURL: "profile-15",
|
||||||
|
},
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
Date: now.Format(dateFormat),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 9990,
|
||||||
|
NumVillages: 1000,
|
||||||
|
Points: 1010,
|
||||||
|
Rank: 1020,
|
||||||
|
RankAtt: 999,
|
||||||
|
ScoreAtt: 1000,
|
||||||
|
RankDef: 1010,
|
||||||
|
ScoreDef: 1020,
|
||||||
|
RankSup: 1030,
|
||||||
|
ScoreSup: 1040,
|
||||||
|
RankTotal: 1050,
|
||||||
|
ScoreTotal: 1060,
|
||||||
|
Date: now.Add(-24 * time.Hour).Format(dateFormat),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedTotalCount: "2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OK: limit=1,offset=100,sort=[date:desc,id:desc]",
|
||||||
|
setup: func(
|
||||||
|
versionSvc *mock.FakeVersionService,
|
||||||
|
serverSvc *mock.FakeServerService,
|
||||||
|
playerSvc *mock.FakePlayerService,
|
||||||
|
playerSnapshotSvc *mock.FakePlayerSnapshotService,
|
||||||
|
) {
|
||||||
|
versionSvc.GetByCodeReturns(version, nil)
|
||||||
|
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
|
||||||
|
playerSvc.GetByServerKeyAndIDReturns(player, nil)
|
||||||
|
playerSnapshotSvc.ListCountWithRelationsCalls(func(
|
||||||
|
_ context.Context,
|
||||||
|
params domain.ListPlayerSnapshotsParams,
|
||||||
|
) ([]domain.PlayerSnapshotWithRelations, int64, error) {
|
||||||
|
expectedParams := domain.ListPlayerSnapshotsParams{
|
||||||
|
ServerKeys: []string{server.Key},
|
||||||
|
PlayerIDs: []int64{player.ID},
|
||||||
|
Pagination: domain.Pagination{
|
||||||
|
Limit: 1,
|
||||||
|
Offset: 100,
|
||||||
|
},
|
||||||
|
Sort: []domain.PlayerSnapshotSort{
|
||||||
|
{By: domain.PlayerSnapshotSortByDate, Direction: domain.SortDirectionDESC},
|
||||||
|
{By: domain.PlayerSnapshotSortByID, Direction: domain.SortDirectionDESC},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(params, expectedParams); diff != "" {
|
||||||
|
fmt.Println(diff)
|
||||||
|
return nil, 0, fmt.Errorf("validation failed: %s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots := []domain.PlayerSnapshotWithRelations{
|
||||||
|
{
|
||||||
|
PlayerSnapshot: domain.PlayerSnapshot{
|
||||||
|
OpponentsDefeated: domain.OpponentsDefeated{
|
||||||
|
RankAtt: 999,
|
||||||
|
ScoreAtt: 1000,
|
||||||
|
RankDef: 1010,
|
||||||
|
ScoreDef: 1020,
|
||||||
|
RankSup: 1030,
|
||||||
|
ScoreSup: 1040,
|
||||||
|
RankTotal: 1050,
|
||||||
|
ScoreTotal: 1060,
|
||||||
|
},
|
||||||
|
ID: 9990,
|
||||||
|
PlayerID: player.ID,
|
||||||
|
ServerKey: server.Key,
|
||||||
|
NumVillages: 1000,
|
||||||
|
Points: 1010,
|
||||||
|
Rank: 1020,
|
||||||
|
TribeID: 0,
|
||||||
|
Date: now.Add(-24 * time.Hour),
|
||||||
|
CreatedAt: now.Add(-36 * time.Hour),
|
||||||
|
},
|
||||||
|
Tribe: domain.NullTribeMeta{
|
||||||
|
Valid: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return snapshots, int64(len(snapshots)) + int64(params.Pagination.Offset), nil
|
||||||
|
})
|
||||||
|
},
|
||||||
|
versionCode: version.Code,
|
||||||
|
key: server.Key,
|
||||||
|
playerID: playerID,
|
||||||
|
queryParams: url.Values{
|
||||||
|
"limit": []string{"1"},
|
||||||
|
"offset": []string{"100"},
|
||||||
|
"sort": []string{"date:desc", "id:desc"},
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
target: &model.ListPlayerSnapshotsResp{},
|
||||||
|
expectedResponse: &model.ListPlayerSnapshotsResp{
|
||||||
|
Data: []model.PlayerSnapshot{
|
||||||
|
{
|
||||||
|
ID: 9990,
|
||||||
|
NumVillages: 1000,
|
||||||
|
Points: 1010,
|
||||||
|
Rank: 1020,
|
||||||
|
RankAtt: 999,
|
||||||
|
ScoreAtt: 1000,
|
||||||
|
RankDef: 1010,
|
||||||
|
ScoreDef: 1020,
|
||||||
|
RankSup: 1030,
|
||||||
|
ScoreSup: 1040,
|
||||||
|
RankTotal: 1050,
|
||||||
|
ScoreTotal: 1060,
|
||||||
|
Date: now.Add(-24 * time.Hour).Format(dateFormat),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedTotalCount: "101",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: limit is not a valid int32",
|
||||||
|
setup: func(
|
||||||
|
versionSvc *mock.FakeVersionService,
|
||||||
|
serverSvc *mock.FakeServerService,
|
||||||
|
playerSvc *mock.FakePlayerService,
|
||||||
|
playerSnapshotSvc *mock.FakePlayerSnapshotService,
|
||||||
|
) {
|
||||||
|
versionSvc.GetByCodeReturns(version, nil)
|
||||||
|
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
|
||||||
|
playerSvc.GetByServerKeyAndIDReturns(player, nil)
|
||||||
|
},
|
||||||
|
versionCode: version.Code,
|
||||||
|
key: server.Key,
|
||||||
|
playerID: playerID,
|
||||||
|
queryParams: url.Values{
|
||||||
|
"limit": []string{"asd"},
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
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,
|
||||||
|
playerSvc *mock.FakePlayerService,
|
||||||
|
playerSnapshotSvc *mock.FakePlayerSnapshotService,
|
||||||
|
) {
|
||||||
|
versionSvc.GetByCodeReturns(version, nil)
|
||||||
|
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
|
||||||
|
playerSvc.GetByServerKeyAndIDReturns(player, nil)
|
||||||
|
},
|
||||||
|
versionCode: version.Code,
|
||||||
|
key: server.Key,
|
||||||
|
playerID: playerID,
|
||||||
|
queryParams: url.Values{
|
||||||
|
"offset": []string{"asd"},
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
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,
|
||||||
|
playerSvc *mock.FakePlayerService,
|
||||||
|
playerSnapshotSvc *mock.FakePlayerSnapshotService,
|
||||||
|
) {
|
||||||
|
versionSvc.GetByCodeReturns(version, nil)
|
||||||
|
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
|
||||||
|
playerSvc.GetByServerKeyAndIDReturns(player, nil)
|
||||||
|
},
|
||||||
|
versionCode: version.Code,
|
||||||
|
key: server.Key,
|
||||||
|
playerID: playerID,
|
||||||
|
queryParams: url.Values{
|
||||||
|
"sort": []string{"createdAt:asc", "test"},
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
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,
|
||||||
|
playerSvc *mock.FakePlayerService,
|
||||||
|
playerSnapshotSvc *mock.FakePlayerSnapshotService,
|
||||||
|
) {
|
||||||
|
versionSvc.GetByCodeReturns(version, nil)
|
||||||
|
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
|
||||||
|
playerSvc.GetByServerKeyAndIDReturns(player, nil)
|
||||||
|
},
|
||||||
|
versionCode: version.Code,
|
||||||
|
key: server.Key,
|
||||||
|
playerID: playerID,
|
||||||
|
queryParams: url.Values{
|
||||||
|
"sort": []string{"date:asc", "test:asc"},
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
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,
|
||||||
|
playerSvc *mock.FakePlayerService,
|
||||||
|
playerSnapshotSvc *mock.FakePlayerSnapshotService,
|
||||||
|
) {
|
||||||
|
versionSvc.GetByCodeReturns(version, nil)
|
||||||
|
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
|
||||||
|
playerSvc.GetByServerKeyAndIDReturns(player, nil)
|
||||||
|
},
|
||||||
|
versionCode: version.Code,
|
||||||
|
key: server.Key,
|
||||||
|
playerID: playerID,
|
||||||
|
queryParams: url.Values{
|
||||||
|
"sort": []string{"date:asc2"},
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
target: &model.ErrorResp{},
|
||||||
|
expectedResponse: &model.ErrorResp{
|
||||||
|
Error: model.APIError{
|
||||||
|
Code: domain.ErrorCodeValidationError.String(),
|
||||||
|
Message: "sort[0]: unsupported sort direction: \"asc2\"",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedTotalCount: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: player id is not a valid int64",
|
||||||
|
setup: func(
|
||||||
|
versionSvc *mock.FakeVersionService,
|
||||||
|
serverSvc *mock.FakeServerService,
|
||||||
|
playerSvc *mock.FakePlayerService,
|
||||||
|
playerSnapshotSvc *mock.FakePlayerSnapshotService,
|
||||||
|
) {
|
||||||
|
versionSvc.GetByCodeReturns(version, nil)
|
||||||
|
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
|
||||||
|
},
|
||||||
|
versionCode: version.Code,
|
||||||
|
key: server.Key,
|
||||||
|
playerID: "asdf",
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
target: &model.ErrorResp{},
|
||||||
|
expectedResponse: &model.ErrorResp{
|
||||||
|
Error: model.APIError{
|
||||||
|
Code: domain.ErrorCodeValidationError.String(),
|
||||||
|
Message: "playerId: strconv.ParseInt: parsing \"asdf\": invalid syntax",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedTotalCount: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: version not found",
|
||||||
|
setup: func(
|
||||||
|
versionSvc *mock.FakeVersionService,
|
||||||
|
serverSvc *mock.FakeServerService,
|
||||||
|
playerSvc *mock.FakePlayerService,
|
||||||
|
playerSnapshotSvc *mock.FakePlayerSnapshotService,
|
||||||
|
) {
|
||||||
|
versionSvc.GetByCodeReturns(domain.Version{}, domain.VersionNotFoundError{VerCode: version.Code + "2"})
|
||||||
|
},
|
||||||
|
versionCode: version.Code + "2",
|
||||||
|
key: server.Key,
|
||||||
|
playerID: playerID,
|
||||||
|
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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedTotalCount: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: server not found",
|
||||||
|
setup: func(
|
||||||
|
versionSvc *mock.FakeVersionService,
|
||||||
|
serverSvc *mock.FakeServerService,
|
||||||
|
playerSvc *mock.FakePlayerService,
|
||||||
|
playerSnapshotSvc *mock.FakePlayerSnapshotService,
|
||||||
|
) {
|
||||||
|
versionSvc.GetByCodeReturns(version, nil)
|
||||||
|
serverSvc.GetNormalByVersionCodeAndKeyReturns(domain.Server{}, domain.ServerNotFoundError{Key: server.Key + "2"})
|
||||||
|
},
|
||||||
|
versionCode: version.Code,
|
||||||
|
key: server.Key + "2",
|
||||||
|
playerID: playerID,
|
||||||
|
expectedStatus: http.StatusNotFound,
|
||||||
|
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: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: player not found",
|
||||||
|
setup: func(
|
||||||
|
versionSvc *mock.FakeVersionService,
|
||||||
|
serverSvc *mock.FakeServerService,
|
||||||
|
playerSvc *mock.FakePlayerService,
|
||||||
|
playerSnapshotSvc *mock.FakePlayerSnapshotService,
|
||||||
|
) {
|
||||||
|
versionSvc.GetByCodeReturns(version, nil)
|
||||||
|
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
|
||||||
|
playerSvc.GetByServerKeyAndIDReturns(domain.Player{}, domain.PlayerNotFoundError{ID: 1241})
|
||||||
|
},
|
||||||
|
versionCode: version.Code,
|
||||||
|
key: server.Key,
|
||||||
|
playerID: playerID + "1",
|
||||||
|
expectedStatus: http.StatusNotFound,
|
||||||
|
target: &model.ErrorResp{},
|
||||||
|
expectedResponse: &model.ErrorResp{
|
||||||
|
Error: model.APIError{
|
||||||
|
Code: domain.ErrorCodeEntityNotFound.String(),
|
||||||
|
Message: fmt.Sprintf("player (id=%d) not found", 1241),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedTotalCount: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
versionSvc := &mock.FakeVersionService{}
|
||||||
|
serverSvc := &mock.FakeServerService{}
|
||||||
|
playerSnapshotSvc := &mock.FakePlayerSnapshotService{}
|
||||||
|
playerSvc := &mock.FakePlayerService{}
|
||||||
|
tt.setup(versionSvc, serverSvc, playerSvc, playerSnapshotSvc)
|
||||||
|
|
||||||
|
router := rest.NewRouter(rest.RouterConfig{
|
||||||
|
VersionService: versionSvc,
|
||||||
|
ServerService: serverSvc,
|
||||||
|
PlayerSnapshotService: playerSnapshotSvc,
|
||||||
|
PlayerService: playerSvc,
|
||||||
|
})
|
||||||
|
|
||||||
|
target := fmt.Sprintf(
|
||||||
|
"/v1/versions/%s/servers/%s/players/%s/history?%s",
|
||||||
|
tt.versionCode,
|
||||||
|
tt.key,
|
||||||
|
tt.playerID,
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -82,7 +82,7 @@ func (q queryParams) tribeSort() ([]domain.TribeSort, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if ss == nil {
|
if len(ss) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ func (q queryParams) playerSort() ([]domain.PlayerSort, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if ss == nil {
|
if len(ss) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,12 +149,48 @@ func (q queryParams) playerSort() ([]domain.PlayerSort, error) {
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q queryParams) playerSnapshotSort() ([]domain.PlayerSnapshotSort, error) {
|
||||||
|
ss, err := q.sort()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(ss) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]domain.PlayerSnapshotSort, 0, len(ss))
|
||||||
|
for i, s := range ss {
|
||||||
|
by, err := domain.NewPlayerSnapshotSortBy(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.PlayerSnapshotSort{
|
||||||
|
By: by,
|
||||||
|
Direction: direction,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (q queryParams) ennoblementSort() ([]domain.EnnoblementSort, error) {
|
func (q queryParams) ennoblementSort() ([]domain.EnnoblementSort, error) {
|
||||||
ss, err := q.sort()
|
ss, err := q.sort()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if ss == nil {
|
if len(ss) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,8 +222,8 @@ func (q queryParams) ennoblementSort() ([]domain.EnnoblementSort, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q queryParams) sort() ([][2]string, error) {
|
func (q queryParams) sort() ([][2]string, error) {
|
||||||
ss, ok := q.values["sort"]
|
ss := q.values["sort"]
|
||||||
if !ok || len(ss) == 0 {
|
if len(ss) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,14 +34,15 @@ type SwaggerConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type RouterConfig struct {
|
type RouterConfig struct {
|
||||||
VersionService VersionService
|
VersionService VersionService
|
||||||
ServerService ServerService
|
ServerService ServerService
|
||||||
TribeService TribeService
|
TribeService TribeService
|
||||||
PlayerService PlayerService
|
PlayerService PlayerService
|
||||||
VillageService VillageService
|
VillageService VillageService
|
||||||
EnnoblementService EnnoblementService
|
EnnoblementService EnnoblementService
|
||||||
CORS CORSConfig
|
PlayerSnapshotService PlayerSnapshotService
|
||||||
Swagger SwaggerConfig
|
CORS CORSConfig
|
||||||
|
Swagger SwaggerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter(cfg RouterConfig) *chi.Mux {
|
func NewRouter(cfg RouterConfig) *chi.Mux {
|
||||||
|
@ -58,6 +59,9 @@ func NewRouter(cfg RouterConfig) *chi.Mux {
|
||||||
playerHandler := &player{
|
playerHandler := &player{
|
||||||
svc: cfg.PlayerService,
|
svc: cfg.PlayerService,
|
||||||
}
|
}
|
||||||
|
playerSnapshotHandler := &playerSnapshot{
|
||||||
|
svc: cfg.PlayerSnapshotService,
|
||||||
|
}
|
||||||
villageHandler := &village{
|
villageHandler := &village{
|
||||||
svc: cfg.VillageService,
|
svc: cfg.VillageService,
|
||||||
}
|
}
|
||||||
|
@ -114,6 +118,7 @@ func NewRouter(cfg RouterConfig) *chi.Mux {
|
||||||
verifyPlayerIDMiddleware := verifyPlayerID(cfg.PlayerService)
|
verifyPlayerIDMiddleware := verifyPlayerID(cfg.PlayerService)
|
||||||
|
|
||||||
r.With(verifyPlayerIDMiddleware).Get("/ennoblements", ennoblementHandler.listPlayer)
|
r.With(verifyPlayerIDMiddleware).Get("/ennoblements", ennoblementHandler.listPlayer)
|
||||||
|
r.With(verifyPlayerIDMiddleware).Get("/history", playerSnapshotHandler.list)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
r.With(verifyServerKeyMiddleware).
|
r.With(verifyServerKeyMiddleware).
|
||||||
|
|
|
@ -19,6 +19,10 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dateFormat = "2006-01-02"
|
||||||
|
)
|
||||||
|
|
||||||
func TestRouteNotFound(t *testing.T) {
|
func TestRouteNotFound(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|
|
@ -367,7 +367,7 @@ func (l listPlayersParamsBuilder) build() (domain.ListPlayersParams, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.Pagination.Limit == 0 {
|
if l.Pagination.Limit == 0 {
|
||||||
l.Pagination.Limit = tribeMaxLimit
|
l.Pagination.Limit = playerMaxLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(l.Sort) > playerSortMaxLen {
|
if len(l.Sort) > playerSortMaxLen {
|
||||||
|
|
|
@ -8,6 +8,11 @@ import (
|
||||||
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
playerSnapshotMaxLimit = 100
|
||||||
|
playerSnapshotSortMaxLen = 2
|
||||||
|
)
|
||||||
|
|
||||||
//counterfeiter:generate -o internal/mock/player_lister.gen.go . PlayerLister
|
//counterfeiter:generate -o internal/mock/player_lister.gen.go . PlayerLister
|
||||||
type PlayerLister interface {
|
type PlayerLister interface {
|
||||||
List(ctx context.Context, params domain.ListPlayersParams) ([]domain.Player, error)
|
List(ctx context.Context, params domain.ListPlayersParams) ([]domain.Player, error)
|
||||||
|
@ -16,6 +21,7 @@ type PlayerLister interface {
|
||||||
//counterfeiter:generate -o internal/mock/player_snapshot_repository.gen.go . PlayerSnapshotRepository
|
//counterfeiter:generate -o internal/mock/player_snapshot_repository.gen.go . PlayerSnapshotRepository
|
||||||
type PlayerSnapshotRepository interface {
|
type PlayerSnapshotRepository interface {
|
||||||
Create(ctx context.Context, params ...domain.CreatePlayerSnapshotParams) error
|
Create(ctx context.Context, params ...domain.CreatePlayerSnapshotParams) error
|
||||||
|
ListCountWithRelations(ctx context.Context, params domain.ListPlayerSnapshotsParams) ([]domain.PlayerSnapshotWithRelations, int64, error)
|
||||||
Delete(ctx context.Context, serverKey string, createdAtLTE time.Time) error
|
Delete(ctx context.Context, serverKey string, createdAtLTE time.Time) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,6 +86,48 @@ func (p *PlayerSnapshot) Create(ctx context.Context, key string, date time.Time)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PlayerSnapshot) ListCountWithRelations(
|
||||||
|
ctx context.Context,
|
||||||
|
params domain.ListPlayerSnapshotsParams,
|
||||||
|
) ([]domain.PlayerSnapshotWithRelations, int64, error) {
|
||||||
|
if len(params.Sort) == 0 {
|
||||||
|
params.Sort = []domain.PlayerSnapshotSort{
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByDate,
|
||||||
|
Direction: domain.SortDirectionASC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByID,
|
||||||
|
Direction: domain.SortDirectionASC,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Pagination.Limit == 0 {
|
||||||
|
params.Pagination.Limit = playerSnapshotMaxLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params.Sort) > playerSnapshotSortMaxLen {
|
||||||
|
return nil, 0, domain.ValidationError{
|
||||||
|
Field: "sort",
|
||||||
|
Err: domain.MaxLengthError{
|
||||||
|
Max: playerSnapshotSortMaxLen,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validatePagination(params.Pagination, playerSnapshotMaxLimit); err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("validatePagination: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots, count, err := p.repo.ListCountWithRelations(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("PlayerSnapshotRepository.ListCountWithRelations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshots, count, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *PlayerSnapshot) CleanUp(ctx context.Context, srv domain.Server) error {
|
func (p *PlayerSnapshot) CleanUp(ctx context.Context, srv domain.Server) error {
|
||||||
if srv.Special || srv.Open || srv.PlayerSnapshotsCreatedAt.After(time.Now().Add(-30*24*time.Hour) /* 30 days */) {
|
if srv.Special || srv.Open || srv.PlayerSnapshotsCreatedAt.After(time.Now().Add(-30*24*time.Hour) /* 30 days */) {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -2,12 +2,14 @@ package service_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
||||||
"gitea.dwysokinski.me/twhelp/core/internal/service"
|
"gitea.dwysokinski.me/twhelp/core/internal/service"
|
||||||
"gitea.dwysokinski.me/twhelp/core/internal/service/internal/mock"
|
"gitea.dwysokinski.me/twhelp/core/internal/service/internal/mock"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -83,6 +85,193 @@ func TestPlayerSnapshot_Create(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPlayerSnapshot_ListCountWithRelations(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
limit int32
|
||||||
|
sort []domain.PlayerSnapshotSort
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default limit, default sort",
|
||||||
|
limit: 0,
|
||||||
|
sort: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom limit",
|
||||||
|
limit: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom sort",
|
||||||
|
limit: 0,
|
||||||
|
sort: []domain.PlayerSnapshotSort{
|
||||||
|
{By: domain.PlayerSnapshotSortByDate, 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 = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := &mock.FakePlayerSnapshotRepository{}
|
||||||
|
repo.ListCountWithRelationsCalls(func(
|
||||||
|
_ context.Context,
|
||||||
|
params domain.ListPlayerSnapshotsParams,
|
||||||
|
) ([]domain.PlayerSnapshotWithRelations, int64, error) {
|
||||||
|
expectedParams := domain.ListPlayerSnapshotsParams{
|
||||||
|
Pagination: domain.Pagination{
|
||||||
|
Limit: limit,
|
||||||
|
},
|
||||||
|
Sort: func(sort []domain.PlayerSnapshotSort) []domain.PlayerSnapshotSort {
|
||||||
|
if len(sort) == 0 {
|
||||||
|
return []domain.PlayerSnapshotSort{
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByDate,
|
||||||
|
Direction: domain.SortDirectionASC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByID,
|
||||||
|
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.PlayerSnapshotWithRelations, params.Pagination.Limit), int64(params.Pagination.Limit), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
svc := service.NewPlayerSnapshot(repo, &mock.FakePlayerLister{})
|
||||||
|
|
||||||
|
params := domain.ListPlayerSnapshotsParams{
|
||||||
|
Pagination: domain.Pagination{
|
||||||
|
Limit: tt.limit,
|
||||||
|
},
|
||||||
|
Sort: tt.sort,
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots, count, err := svc.ListCountWithRelations(context.Background(), params)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, limit, count)
|
||||||
|
assert.Len(t, snapshots, int(limit))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ERR: validation failed", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
params domain.ListPlayerSnapshotsParams
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "params.Pagination.Limit < 0",
|
||||||
|
params: domain.ListPlayerSnapshotsParams{
|
||||||
|
Pagination: domain.Pagination{
|
||||||
|
Limit: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Field: "limit",
|
||||||
|
Err: domain.MinError{
|
||||||
|
Min: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "params.Pagination.Limit > 100",
|
||||||
|
params: domain.ListPlayerSnapshotsParams{
|
||||||
|
Pagination: domain.Pagination{
|
||||||
|
Limit: 101,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Field: "limit",
|
||||||
|
Err: domain.MaxError{
|
||||||
|
Max: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "params.Pagination.Offset < 0",
|
||||||
|
params: domain.ListPlayerSnapshotsParams{
|
||||||
|
Pagination: domain.Pagination{
|
||||||
|
Offset: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Field: "offset",
|
||||||
|
Err: domain.MinError{
|
||||||
|
Min: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "len(params.Sort) > 2",
|
||||||
|
params: domain.ListPlayerSnapshotsParams{
|
||||||
|
Sort: []domain.PlayerSnapshotSort{
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByID,
|
||||||
|
Direction: domain.SortDirectionASC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByDate,
|
||||||
|
Direction: domain.SortDirectionASC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
By: domain.PlayerSnapshotSortByID,
|
||||||
|
Direction: domain.SortDirectionDESC,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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.FakePlayerSnapshotRepository{}
|
||||||
|
|
||||||
|
svc := service.NewPlayerSnapshot(repo, &mock.FakePlayerLister{})
|
||||||
|
|
||||||
|
snapshots, count, err := svc.ListCountWithRelations(context.Background(), tt.params)
|
||||||
|
assert.ErrorIs(t, err, tt.expectedErr)
|
||||||
|
assert.Zero(t, snapshots)
|
||||||
|
assert.Zero(t, count)
|
||||||
|
assert.Equal(t, 0, repo.ListCountWithRelationsCallCount())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestPlayerSnapshot_CleanUp(t *testing.T) {
|
func TestPlayerSnapshot_CleanUp(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|
Reference in New Issue
Block a user