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)
|
||||
ennoblementRepo := bundb.NewEnnoblement(cfg.db)
|
||||
tribeChangeRepo := bundb.NewTribeChange(cfg.db)
|
||||
playerSnapshotRepo := bundb.NewPlayerSnapshot(cfg.db)
|
||||
|
||||
// services
|
||||
versionSvc := service.NewVersion(versionRepo)
|
||||
|
@ -110,17 +111,19 @@ func newServer(cfg serverConfig) (*http.Server, error) {
|
|||
playerSvc := service.NewPlayer(playerRepo, tribeChangeSvc, client)
|
||||
villageSvc := service.NewVillage(villageRepo, client)
|
||||
ennoblementSvc := service.NewEnnoblement(ennoblementRepo, client)
|
||||
playerSnapshotSvc := service.NewPlayerSnapshot(playerSnapshotRepo, playerSvc)
|
||||
|
||||
// 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,
|
||||
EnnoblementService: ennoblementSvc,
|
||||
VersionService: versionSvc,
|
||||
ServerService: serverSvc,
|
||||
TribeService: tribeSvc,
|
||||
PlayerService: playerSvc,
|
||||
VillageService: villageSvc,
|
||||
EnnoblementService: ennoblementSvc,
|
||||
PlayerSnapshotService: playerSnapshotSvc,
|
||||
CORS: rest.CORSConfig{
|
||||
Enabled: apiCfg.CORSEnabled,
|
||||
AllowedOrigins: apiCfg.CORSAllowedOrigins,
|
||||
|
|
|
@ -16,6 +16,7 @@ type PlayerSnapshot struct {
|
|||
Points int64 `bun:"points,default:0"`
|
||||
Rank int64 `bun:"rank,default:0"`
|
||||
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"`
|
||||
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"`
|
||||
|
@ -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{
|
||||
OpponentsDefeated: domain.OpponentsDefeated(ps.OpponentsDefeated),
|
||||
ID: ps.ID,
|
||||
PlayerID: ps.PlayerID,
|
||||
NumVillages: ps.NumVillages,
|
||||
Points: ps.Points,
|
||||
Rank: ps.Rank,
|
||||
TribeID: ps.TribeID,
|
||||
ServerKey: ps.ServerKey,
|
||||
Date: ps.Date,
|
||||
CreatedAt: ps.CreatedAt,
|
||||
OpponentsDefeated: domain.OpponentsDefeated(p.OpponentsDefeated),
|
||||
ID: p.ID,
|
||||
PlayerID: p.PlayerID,
|
||||
NumVillages: p.NumVillages,
|
||||
Points: p.Points,
|
||||
Rank: p.Rank,
|
||||
TribeID: p.TribeID,
|
||||
ServerKey: p.ServerKey,
|
||||
Date: p.Date,
|
||||
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.Date, snapshot.Date)
|
||||
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}
|
||||
|
||||
cntQ := p.db.NewSelect().
|
||||
Model(&model.Player{}).
|
||||
Apply(paramsApplier.applyFilters)
|
||||
|
||||
subQ := p.db.NewSelect().
|
||||
Column("id", "server_key").
|
||||
Model(&model.Player{}).
|
||||
|
@ -121,18 +117,24 @@ func (p *Player) ListCountWithRelations(ctx context.Context, params domain.ListP
|
|||
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().
|
||||
Model(&players).
|
||||
ColumnExpr("?TableColumns").
|
||||
ColumnExpr("(?) AS other_servers", 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")).
|
||||
ColumnExpr("(?) AS other_servers", otherServersQ).
|
||||
Order(playerOrders...).
|
||||
Where("(player.id, player.server_key) IN (?)", subQ).
|
||||
Relation("Tribe", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
|
|
|
@ -12,6 +12,10 @@ import (
|
|||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
var (
|
||||
playerSnapshotOrders = []string{"ps.server_key ASC"}
|
||||
)
|
||||
|
||||
type PlayerSnapshot struct {
|
||||
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) {
|
||||
var snapshots []model.PlayerSnapshot
|
||||
|
||||
if err := p.db.NewSelect().
|
||||
q := p.db.NewSelect().
|
||||
Model(&snapshots).
|
||||
Order("server_key ASC", "date ASC", "id ASC").
|
||||
Apply(listPlayerSnapshotsParamsApplier{params}.apply).
|
||||
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
Order(playerSnapshotOrders...)
|
||||
q, err := listPlayerSnapshotsParamsApplier{params}.apply(q)
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -59,6 +67,52 @@ func (p *PlayerSnapshot) List(ctx context.Context, params domain.ListPlayerSnaps
|
|||
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 {
|
||||
if _, err := p.db.NewDelete().
|
||||
Model(&model.PlayerSnapshot{}).
|
||||
|
@ -76,8 +130,12 @@ type listPlayerSnapshotsParamsApplier struct {
|
|||
params domain.ListPlayerSnapshotsParams
|
||||
}
|
||||
|
||||
func (l listPlayerSnapshotsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
return l.applyPagination(l.applyFilters(q))
|
||||
func (l listPlayerSnapshotsParamsApplier) apply(q *bun.SelectQuery) (*bun.SelectQuery, error) {
|
||||
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 {
|
||||
|
@ -85,9 +143,46 @@ func (l listPlayerSnapshotsParamsApplier) applyFilters(q *bun.SelectQuery) *bun.
|
|||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
params domain.ListPlayerSnapshotsParams
|
||||
expectedSnapshots []expectedSnapshots
|
||||
expectedCount int64
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "Empty struct",
|
||||
params: domain.ListPlayerSnapshotsParams{},
|
||||
name: "Sort=[{By=Date,Direction=ASC},{By=ID,Direction=ASC}]",
|
||||
params: domain.ListPlayerSnapshotsParams{
|
||||
Sort: []domain.PlayerSnapshotSort{
|
||||
{
|
||||
By: domain.PlayerSnapshotSortByDate,
|
||||
Direction: domain.SortDirectionASC,
|
||||
},
|
||||
{
|
||||
By: domain.PlayerSnapshotSortByID,
|
||||
Direction: domain.SortDirectionASC,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedSnapshots: allSnapshots,
|
||||
expectedCount: int64(len(allSnapshots)),
|
||||
},
|
||||
{
|
||||
name: "ServerKey=[de188]",
|
||||
name: "ServerKey=[de188],Sort=[{By=Date,Direction=ASC},{By=ID,Direction=ASC}]",
|
||||
params: domain.ListPlayerSnapshotsParams{
|
||||
ServerKeys: []string{"de188"},
|
||||
Sort: []domain.PlayerSnapshotSort{
|
||||
{
|
||||
By: domain.PlayerSnapshotSortByDate,
|
||||
Direction: domain.SortDirectionASC,
|
||||
},
|
||||
{
|
||||
By: domain.PlayerSnapshotSortByID,
|
||||
Direction: domain.SortDirectionASC,
|
||||
},
|
||||
},
|
||||
},
|
||||
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{
|
||||
ServerKeys: []string{"pl169"},
|
||||
Pagination: domain.Pagination{
|
||||
Limit: 2,
|
||||
},
|
||||
Sort: []domain.PlayerSnapshotSort{
|
||||
{
|
||||
By: domain.PlayerSnapshotSortByDate,
|
||||
Direction: domain.SortDirectionASC,
|
||||
},
|
||||
{
|
||||
By: domain.PlayerSnapshotSortByID,
|
||||
Direction: domain.SortDirectionASC,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedSnapshots: []expectedSnapshots{
|
||||
{
|
||||
|
@ -248,14 +283,25 @@ func TestPlayerSnapshot_List(t *testing.T) {
|
|||
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{
|
||||
ServerKeys: []string{"pl169"},
|
||||
Pagination: domain.Pagination{
|
||||
Offset: 1,
|
||||
},
|
||||
Sort: []domain.PlayerSnapshotSort{
|
||||
{
|
||||
By: domain.PlayerSnapshotSortByDate,
|
||||
Direction: domain.SortDirectionASC,
|
||||
},
|
||||
{
|
||||
By: domain.PlayerSnapshotSortByID,
|
||||
Direction: domain.SortDirectionASC,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedSnapshots: []expectedSnapshots{
|
||||
{
|
||||
|
@ -267,14 +313,48 @@ func TestPlayerSnapshot_List(t *testing.T) {
|
|||
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{
|
||||
ServerKeys: []string{"pl169"},
|
||||
Pagination: domain.Pagination{
|
||||
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{
|
||||
|
@ -283,6 +363,29 @@ func TestPlayerSnapshot_List(t *testing.T) {
|
|||
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.Parallel()
|
||||
|
||||
res, err := repo.List(context.Background(), tt.params)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, res, len(tt.expectedSnapshots))
|
||||
resListCountWithRelations, count, err := repo.ListCountWithRelations(context.Background(), tt.params)
|
||||
assert.ErrorIs(t, err, tt.expectedErr)
|
||||
assert.Equal(t, tt.expectedCount, count)
|
||||
assert.Len(t, resListCountWithRelations, len(tt.expectedSnapshots))
|
||||
for _, expSnapshot := range tt.expectedSnapshots {
|
||||
found := false
|
||||
for _, snapshot := range res {
|
||||
if snapshot.ID == expSnapshot.id && snapshot.ServerKey == expSnapshot.serverKey {
|
||||
found = true
|
||||
break
|
||||
for _, snapshot := range resListCountWithRelations {
|
||||
if snapshot.ID != expSnapshot.id {
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
s string
|
||||
output domain.EnnoblementSortBy
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
s: "createdAt",
|
||||
output: domain.EnnoblementSortByCreatedAt,
|
||||
},
|
||||
{
|
||||
s: "unsupported",
|
||||
expectedErr: domain.ErrUnsupportedSortBy,
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
s string
|
||||
output domain.EnnoblementSortBy
|
||||
}{
|
||||
{
|
||||
s: "createdAt",
|
||||
output: domain.EnnoblementSortByCreatedAt,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.s, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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)
|
||||
})
|
||||
res, err := domain.NewEnnoblementSortBy(tt.s)
|
||||
assert.ErrorIs(t, err, tt.expectedErr)
|
||||
assert.Equal(t, tt.output, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,63 +10,56 @@ import (
|
|||
func TestNewPlayerSortBy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
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 {
|
||||
s string
|
||||
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 {
|
||||
tt := tt
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.s, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run(tt.s, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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)
|
||||
})
|
||||
res, err := domain.NewPlayerSortBy(tt.s)
|
||||
assert.ErrorIs(t, err, tt.expectedErr)
|
||||
assert.Equal(t, tt.output, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlayerNotFoundError(t *testing.T) {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package domain
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PlayerSnapshot struct {
|
||||
OpponentsDefeated
|
||||
|
@ -16,6 +19,12 @@ type PlayerSnapshot struct {
|
|||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type PlayerSnapshotWithRelations struct {
|
||||
PlayerSnapshot
|
||||
|
||||
Tribe NullTribeMeta
|
||||
}
|
||||
|
||||
type CreatePlayerSnapshotParams struct {
|
||||
OpponentsDefeated
|
||||
|
||||
|
@ -28,9 +37,33 @@ type CreatePlayerSnapshotParams struct {
|
|||
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 {
|
||||
ServerKeys []string
|
||||
PlayerIDs []int64
|
||||
Pagination Pagination
|
||||
Sort []PlayerSnapshotSort
|
||||
}
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
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 {
|
||||
s string
|
||||
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 {
|
||||
tt := tt
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.s, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run(tt.s, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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)
|
||||
})
|
||||
res, err := domain.NewSortDirection(tt.s)
|
||||
assert.ErrorIs(t, err, tt.expectedErr)
|
||||
assert.Equal(t, tt.output, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,63 +11,56 @@ import (
|
|||
func TestNewTribeSortBy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
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 {
|
||||
s string
|
||||
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 {
|
||||
tt := tt
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.s, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run(tt.s, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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)
|
||||
})
|
||||
res, err := domain.NewTribeSortBy(tt.s)
|
||||
assert.ErrorIs(t, err, tt.expectedErr)
|
||||
assert.Equal(t, tt.output, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTribeNotFoundError(t *testing.T) {
|
||||
|
|
|
@ -63,8 +63,8 @@ func (e *ennoblement) list(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// @ID listPlayerEnnoblements
|
||||
// @Summary List the given user's ennoblements
|
||||
// @Description List the given user's ennoblements
|
||||
// @Summary List the given player's ennoblements
|
||||
// @Description List the given player's ennoblements
|
||||
// @Tags versions,servers,players,ennoblements
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.ListEnnoblementsResp
|
||||
|
|
|
@ -4,6 +4,10 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
dateFormat = "2006-01-02"
|
||||
)
|
||||
|
||||
type NullTime struct {
|
||||
Time time.Time
|
||||
Valid bool
|
||||
|
|
|
@ -9,6 +9,10 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
dateFormat = "2006-01-02"
|
||||
)
|
||||
|
||||
func TestNullTime_MarshalJSON(t *testing.T) {
|
||||
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
|
||||
}
|
||||
|
||||
if _, err := svc.GetByServerKeyAndID(
|
||||
if _, err = svc.GetByServerKeyAndID(
|
||||
ctx,
|
||||
routeCtx.URLParam("serverKey"),
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
if ss == nil {
|
||||
if len(ss) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -118,7 +118,7 @@ func (q queryParams) playerSort() ([]domain.PlayerSort, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ss == nil {
|
||||
if len(ss) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -149,12 +149,48 @@ func (q queryParams) playerSort() ([]domain.PlayerSort, error) {
|
|||
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) {
|
||||
ss, err := q.sort()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ss == nil {
|
||||
if len(ss) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -186,8 +222,8 @@ func (q queryParams) ennoblementSort() ([]domain.EnnoblementSort, error) {
|
|||
}
|
||||
|
||||
func (q queryParams) sort() ([][2]string, error) {
|
||||
ss, ok := q.values["sort"]
|
||||
if !ok || len(ss) == 0 {
|
||||
ss := q.values["sort"]
|
||||
if len(ss) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -34,14 +34,15 @@ type SwaggerConfig struct {
|
|||
}
|
||||
|
||||
type RouterConfig struct {
|
||||
VersionService VersionService
|
||||
ServerService ServerService
|
||||
TribeService TribeService
|
||||
PlayerService PlayerService
|
||||
VillageService VillageService
|
||||
EnnoblementService EnnoblementService
|
||||
CORS CORSConfig
|
||||
Swagger SwaggerConfig
|
||||
VersionService VersionService
|
||||
ServerService ServerService
|
||||
TribeService TribeService
|
||||
PlayerService PlayerService
|
||||
VillageService VillageService
|
||||
EnnoblementService EnnoblementService
|
||||
PlayerSnapshotService PlayerSnapshotService
|
||||
CORS CORSConfig
|
||||
Swagger SwaggerConfig
|
||||
}
|
||||
|
||||
func NewRouter(cfg RouterConfig) *chi.Mux {
|
||||
|
@ -58,6 +59,9 @@ func NewRouter(cfg RouterConfig) *chi.Mux {
|
|||
playerHandler := &player{
|
||||
svc: cfg.PlayerService,
|
||||
}
|
||||
playerSnapshotHandler := &playerSnapshot{
|
||||
svc: cfg.PlayerSnapshotService,
|
||||
}
|
||||
villageHandler := &village{
|
||||
svc: cfg.VillageService,
|
||||
}
|
||||
|
@ -114,6 +118,7 @@ func NewRouter(cfg RouterConfig) *chi.Mux {
|
|||
verifyPlayerIDMiddleware := verifyPlayerID(cfg.PlayerService)
|
||||
|
||||
r.With(verifyPlayerIDMiddleware).Get("/ennoblements", ennoblementHandler.listPlayer)
|
||||
r.With(verifyPlayerIDMiddleware).Get("/history", playerSnapshotHandler.list)
|
||||
})
|
||||
})
|
||||
r.With(verifyServerKeyMiddleware).
|
||||
|
|
|
@ -19,6 +19,10 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
dateFormat = "2006-01-02"
|
||||
)
|
||||
|
||||
func TestRouteNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
|
@ -367,7 +367,7 @@ func (l listPlayersParamsBuilder) build() (domain.ListPlayersParams, error) {
|
|||
}
|
||||
|
||||
if l.Pagination.Limit == 0 {
|
||||
l.Pagination.Limit = tribeMaxLimit
|
||||
l.Pagination.Limit = playerMaxLimit
|
||||
}
|
||||
|
||||
if len(l.Sort) > playerSortMaxLen {
|
||||
|
|
|
@ -8,6 +8,11 @@ import (
|
|||
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
||||
)
|
||||
|
||||
const (
|
||||
playerSnapshotMaxLimit = 100
|
||||
playerSnapshotSortMaxLen = 2
|
||||
)
|
||||
|
||||
//counterfeiter:generate -o internal/mock/player_lister.gen.go . PlayerLister
|
||||
type PlayerLister interface {
|
||||
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
|
||||
type PlayerSnapshotRepository interface {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -80,6 +86,48 @@ func (p *PlayerSnapshot) Create(ctx context.Context, key string, date time.Time)
|
|||
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 {
|
||||
if srv.Special || srv.Open || srv.PlayerSnapshotsCreatedAt.After(time.Now().Add(-30*24*time.Hour) /* 30 days */) {
|
||||
return nil
|
||||
|
|
|
@ -2,12 +2,14 @@ package service_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
||||
"gitea.dwysokinski.me/twhelp/core/internal/service"
|
||||
"gitea.dwysokinski.me/twhelp/core/internal/service/internal/mock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
Reference in New Issue
Block a user