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

Reviewed-on: twhelp/core#62
This commit is contained in:
Dawid Wysokiński 2022-09-02 05:25:57 +00:00
parent 4c82b503e9
commit 262c8ae4c7
23 changed files with 967 additions and 43 deletions

View File

@ -36,6 +36,7 @@ linters:
- grouper
- errname
- exhaustive
- tagliatelle
linters-settings:
tagliatelle:

View File

@ -95,11 +95,13 @@ func newServer(cfg serverConfig) (*http.Server, error) {
versionRepo := bundb.NewVersion(cfg.db)
serverRepo := bundb.NewServer(cfg.db)
tribeRepo := bundb.NewTribe(cfg.db)
playerRepo := bundb.NewPlayer(cfg.db)
// services
versionSvc := service.NewVersion(versionRepo)
serverSvc := service.NewServer(serverRepo, client)
tribeSvc := service.NewTribe(tribeRepo, client)
playerSvc := service.NewPlayer(playerRepo, client)
// router
r := chi.NewRouter()
@ -109,6 +111,7 @@ func newServer(cfg serverConfig) (*http.Server, error) {
VersionService: versionSvc,
ServerService: serverSvc,
TribeService: tribeSvc,
PlayerService: playerSvc,
CORS: rest.CORSConfig{
Enabled: apiCfg.CORSEnabled,
AllowedOrigins: nil,

View File

@ -17,6 +17,7 @@ type Player 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"`
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
DeletedAt time.Time `bun:"deleted_at,nullzero"`
@ -38,6 +39,13 @@ func NewPlayer(p domain.CreatePlayerParams) Player {
}
func (p Player) ToDomain() domain.Player {
var tribe domain.NullTribe
if p.Tribe.ID > 0 {
tribe = domain.NullTribe{
Valid: true,
Tribe: p.Tribe.ToDomain(),
}
}
return domain.Player{
BasePlayer: domain.BasePlayer{
OpponentsDefeated: domain.OpponentsDefeated(p.OpponentsDefeated),
@ -49,6 +57,7 @@ func (p Player) ToDomain() domain.Player {
TribeID: p.TribeID,
},
ServerKey: p.ServerKey,
Tribe: tribe,
CreatedAt: p.CreatedAt,
DeletedAt: p.DeletedAt,
}

View File

@ -13,7 +13,7 @@ func TestNewPlayer(t *testing.T) {
t.Parallel()
deletedAt := time.Now().Add(-100 * time.Hour)
params := domain.CreatePlayerParams{
playerParams := domain.CreatePlayerParams{
BasePlayer: domain.BasePlayer{
OpponentsDefeated: domain.OpponentsDefeated{
RankAtt: 1,
@ -34,12 +34,30 @@ func TestNewPlayer(t *testing.T) {
},
ServerKey: "pl151",
}
tribeParams := domain.CreateTribeParams{
BaseTribe: domain.BaseTribe{
ID: playerParams.TribeID,
},
ServerKey: playerParams.ServerKey,
}
result := model.NewPlayer(playerParams)
result := model.NewPlayer(params)
result.DeletedAt = deletedAt
player := result.ToDomain()
assert.Equal(t, params.BasePlayer, player.BasePlayer)
assert.Equal(t, params.ServerKey, player.ServerKey)
assert.Equal(t, playerParams.BasePlayer, player.BasePlayer)
assert.Equal(t, playerParams.ServerKey, player.ServerKey)
assert.WithinDuration(t, time.Now(), player.CreatedAt, 10*time.Millisecond)
assert.Zero(t, player.Tribe)
assert.Equal(t, deletedAt, player.DeletedAt)
result.Tribe = model.NewTribe(tribeParams)
result.DeletedAt = time.Time{}
player = result.ToDomain()
assert.Equal(t, playerParams.BasePlayer, player.BasePlayer)
assert.Equal(t, playerParams.ServerKey, player.ServerKey)
assert.WithinDuration(t, time.Now(), player.CreatedAt, 10*time.Millisecond)
assert.Equal(t, tribeParams.BaseTribe, player.Tribe.Tribe.BaseTribe)
assert.True(t, player.Tribe.Valid)
assert.Zero(t, player.DeletedAt)
}

View File

@ -27,8 +27,8 @@ func (p *Player) CreateOrUpdate(ctx context.Context, params ...domain.CreatePlay
}
players := make([]model.Player, 0, len(params))
for _, p := range params {
players = append(players, model.NewPlayer(p))
for _, param := range params {
players = append(players, model.NewPlayer(param))
}
if _, err := p.db.NewInsert().
Model(&players).
@ -89,6 +89,7 @@ func (p *Player) List(ctx context.Context, params domain.ListPlayersParams) ([]d
for _, player := range players {
result = append(result, player.ToDomain())
}
return result, int64(count), nil
}
@ -98,20 +99,24 @@ type listPlayersParamsApplier struct {
func (l listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
if l.params.IDs != nil {
q = q.Where("id IN (?)", bun.In(l.params.IDs))
q = q.Where("player.id IN (?)", bun.In(l.params.IDs))
}
if l.params.ServerKeys != nil {
q = q.Where("server_key IN (?)", bun.In(l.params.ServerKeys))
q = q.Where("player.server_key IN (?)", bun.In(l.params.ServerKeys))
}
if l.params.Deleted.Valid {
if l.params.Deleted.Bool {
q = q.Where("deleted_at IS NOT NULL")
q = q.Where("player.deleted_at IS NOT NULL")
} else {
q = q.Where("deleted_at IS NULL")
q = q.Where("player.deleted_at IS NULL")
}
}
return q
if l.params.IncludeTribe {
q = q.Relation("Tribe")
}
return paginationApplier{l.params.Pagination}.apply(q)
}

View File

@ -203,6 +203,7 @@ func TestPlayer_List(t *testing.T) {
type expectedPlayer struct {
id int64
tribeID int64
serverKey string
}
@ -263,34 +264,84 @@ func TestPlayer_List(t *testing.T) {
expectedCount: 1,
},
{
name: "IDs=[8625053,622676],ServerKeys=[en113],Count=true",
params: domain.ListPlayersParams{IDs: []int64{8625053, 622676}, ServerKeys: []string{"en113"}, Count: true},
name: "IDs=[6180190,8419570,699736927],Limit=1,ServerKeys=[pl169],Count=true",
params: domain.ListPlayersParams{
IDs: []int64{6180190, 8419570, 699736927},
ServerKeys: []string{"pl169"},
Pagination: domain.Pagination{
Limit: 1,
},
Count: true,
},
expectedPlayers: []expectedPlayer{
{
id: 6180190,
serverKey: "pl169",
},
},
expectedCount: 3,
},
{
name: "IDs=[6180190,8419570,699736927],Offset=1,ServerKeys=[pl169],Count=true",
params: domain.ListPlayersParams{
IDs: []int64{6180190, 8419570, 699736927},
ServerKeys: []string{"pl169"},
Pagination: domain.Pagination{
Offset: 1,
},
Count: true,
},
expectedPlayers: []expectedPlayer{
{
id: 8419570,
serverKey: "pl169",
},
{
id: 699736927,
serverKey: "pl169",
},
},
expectedCount: 3,
},
{
name: "IDs=[8625053,622676],IncludeTribe=true,ServerKeys=[en113],Count=true",
params: domain.ListPlayersParams{
IDs: []int64{8625053, 622676},
ServerKeys: []string{"en113"},
IncludeTribe: true,
Count: true,
},
expectedPlayers: []expectedPlayer{
{
id: 8625053,
tribeID: 122,
serverKey: "en113",
},
{
id: 622676,
tribeID: 122,
serverKey: "en113",
},
},
expectedCount: 2,
},
{
name: "Deleted=true,ServerKeys=[en113],Count=true",
name: "Deleted=true,IncludeTribe=true,ServerKeys=[en113],Count=true",
params: domain.ListPlayersParams{
Deleted: domain.NullBool{Bool: true, Valid: true},
ServerKeys: []string{"en113"},
Count: true,
Deleted: domain.NullBool{Bool: true, Valid: true},
ServerKeys: []string{"en113"},
IncludeTribe: true,
Count: true,
},
expectedPlayers: []expectedPlayer{
{
id: 1111,
tribeID: 0,
serverKey: "en113",
},
{
id: 1112,
tribeID: 0,
serverKey: "en113",
},
},
@ -311,12 +362,29 @@ func TestPlayer_List(t *testing.T) {
for _, expPlayer := range tt.expectedPlayers {
found := false
for _, player := range res {
if player.ID == expPlayer.id && player.ServerKey == expPlayer.serverKey {
found = true
break
if player.ID != expPlayer.id {
continue
}
if player.ServerKey != expPlayer.serverKey {
continue
}
if player.Tribe.Tribe.ID != expPlayer.tribeID {
continue
}
found = true
break
}
assert.True(t, found, "player (id=%d,serverkey=%s) not found", expPlayer.id, expPlayer.serverKey)
assert.True(
t,
found,
"player (id=%d,tribeID=%d,serverkey=%s) not found",
expPlayer.id,
expPlayer.tribeID,
expPlayer.serverKey,
)
}
})
}

View File

@ -1,6 +1,9 @@
package domain
import "time"
import (
"fmt"
"time"
)
type BasePlayer struct {
OpponentsDefeated
@ -17,6 +20,7 @@ type Player struct {
BasePlayer
ServerKey string
Tribe NullTribe // only available when ListPlayersParams.IncludeTribe == true
CreatedAt time.Time
DeletedAt time.Time
}
@ -28,8 +32,26 @@ type CreatePlayerParams struct {
}
type ListPlayersParams struct {
IDs []int64
ServerKeys []string
Deleted NullBool
Count bool
IDs []int64
ServerKeys []string
Deleted NullBool
IncludeTribe bool
Pagination Pagination
Count bool
}
type PlayerNotFoundError struct {
ID int64
}
func (e PlayerNotFoundError) Error() string {
return fmt.Sprintf("player (id=%d) not found", e.ID)
}
func (e PlayerNotFoundError) UserError() string {
return e.Error()
}
func (e PlayerNotFoundError) Code() ErrorCode {
return ErrorCodeEntityNotFound
}

View File

@ -0,0 +1,20 @@
package domain_test
import (
"testing"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"github.com/stretchr/testify/assert"
)
func TestPlayerNotFoundError(t *testing.T) {
t.Parallel()
err := domain.PlayerNotFoundError{
ID: 1234,
}
var _ domain.UserError = err
assert.Equal(t, "player (id=1234) not found", err.Error())
assert.Equal(t, err.Error(), err.UserError())
assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code())
}

View File

@ -27,6 +27,11 @@ type Tribe struct {
DeletedAt time.Time
}
type NullTribe struct {
Valid bool
Tribe Tribe
}
type CreateTribeParams struct {
BaseTribe

1
internal/rest/.swaggo Normal file
View File

@ -0,0 +1 @@
replace gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model.NullTribe gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model.Tribe

View File

@ -0,0 +1,67 @@
package model
import (
"time"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
)
type Player struct {
ID int64 `json:"id"`
Name string `json:"name"`
NumVillages int64 `json:"numVillages"`
Points int64 `json:"points"`
Rank int64 `json:"rank"`
Tribe NullTribe `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"`
CreatedAt time.Time `json:"createdAt" format:"date-time"`
DeletedAt NullTime `json:"deletedAt" swaggertype:"string" format:"date-time" extensions:"x-nullable"`
} // @name Player
func NewPlayer(p domain.Player) Player {
var t NullTribe
if p.Tribe.Valid {
t = NullTribe{
Valid: true,
Tribe: NewTribe(p.Tribe.Tribe),
}
}
return Player{
ID: p.ID,
Name: p.Name,
NumVillages: p.NumVillages,
Points: p.Points,
Rank: p.Rank,
Tribe: t,
RankAtt: p.RankAtt,
ScoreAtt: p.ScoreAtt,
RankDef: p.RankDef,
ScoreDef: p.ScoreDef,
RankSup: p.RankSup,
ScoreSup: p.ScoreSup,
RankTotal: p.RankTotal,
ScoreTotal: p.ScoreTotal,
CreatedAt: p.CreatedAt,
DeletedAt: NullTime{
Time: p.DeletedAt,
Valid: !p.DeletedAt.IsZero(),
},
}
}
type GetPlayerResp struct {
Data Player `json:"data"`
} // @name GetPlayerResp
func NewGetPlayerResp(v domain.Player) GetPlayerResp {
return GetPlayerResp{
Data: NewPlayer(v),
}
}

View File

@ -0,0 +1,135 @@
package model_test
import (
"testing"
"time"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model"
"github.com/stretchr/testify/assert"
)
func TestNewPlayer(t *testing.T) {
playerWithoutTribe := domain.Player{
BasePlayer: domain.BasePlayer{
OpponentsDefeated: domain.OpponentsDefeated{
RankAtt: 8,
ScoreAtt: 7,
RankDef: 6,
ScoreDef: 5,
RankSup: 4,
ScoreSup: 3,
RankTotal: 2,
ScoreTotal: 1,
},
ID: 997,
Name: "name 997",
NumVillages: 5,
Points: 4,
Rank: 2,
},
ServerKey: "pl151",
CreatedAt: time.Now(),
}
assertPlayer(t, playerWithoutTribe, model.NewPlayer(playerWithoutTribe))
playerWithTribe := domain.Player{
BasePlayer: domain.BasePlayer{
OpponentsDefeated: domain.OpponentsDefeated{
RankAtt: 8,
ScoreAtt: 7,
RankDef: 6,
ScoreDef: 5,
RankSup: 4,
ScoreSup: 3,
RankTotal: 2,
ScoreTotal: 1,
},
ID: 997,
Name: "name 997",
NumVillages: 5,
Points: 4,
Rank: 2,
TribeID: 1234,
},
ServerKey: "pl151",
Tribe: domain.NullTribe{
Valid: true,
Tribe: domain.Tribe{
BaseTribe: domain.BaseTribe{
OpponentsDefeated: domain.OpponentsDefeated{
RankAtt: 8,
ScoreAtt: 7,
RankDef: 6,
ScoreDef: 5,
RankSup: 4,
ScoreSup: 3,
RankTotal: 2,
ScoreTotal: 1,
},
ID: 1234,
Name: "name 997",
Tag: "tag 997",
NumMembers: 6,
NumVillages: 5,
Points: 4,
AllPoints: 3,
Rank: 2,
},
ServerKey: "pl151",
Dominance: 12.5,
CreatedAt: time.Now(),
},
},
CreatedAt: time.Now(),
}
assertPlayer(t, playerWithTribe, model.NewPlayer(playerWithTribe))
}
func TestNewGetPlayerResp(t *testing.T) {
t.Parallel()
player := domain.Player{
BasePlayer: domain.BasePlayer{
OpponentsDefeated: domain.OpponentsDefeated{
RankAtt: 8,
ScoreAtt: 7,
RankDef: 6,
ScoreDef: 5,
RankSup: 4,
ScoreSup: 3,
RankTotal: 2,
ScoreTotal: 1,
},
ID: 997,
Name: "name 997",
NumVillages: 5,
Points: 4,
Rank: 2,
},
ServerKey: "pl151",
CreatedAt: time.Now(),
}
assertPlayer(t, player, model.NewGetPlayerResp(player).Data)
}
func assertPlayer(tb testing.TB, dt domain.Player, rt model.Player) {
tb.Helper()
assert.Equal(tb, dt.ID, rt.ID)
assert.Equal(tb, dt.Name, rt.Name)
assert.Equal(tb, dt.NumVillages, rt.NumVillages)
assert.Equal(tb, dt.Points, rt.Points)
assert.Equal(tb, dt.Rank, rt.Rank)
assert.Equal(tb, dt.RankAtt, rt.RankAtt)
assert.Equal(tb, dt.ScoreAtt, rt.ScoreAtt)
assert.Equal(tb, dt.RankDef, rt.RankDef)
assert.Equal(tb, dt.ScoreDef, rt.ScoreDef)
assert.Equal(tb, dt.RankSup, rt.RankSup)
assert.Equal(tb, dt.ScoreSup, rt.ScoreSup)
assert.Equal(tb, dt.RankTotal, rt.RankTotal)
assert.Equal(tb, dt.ScoreTotal, rt.ScoreTotal)
assert.Equal(tb, dt.CreatedAt, rt.CreatedAt)
assertNullTribe(tb, dt.Tribe, rt.Tribe)
assertNullTime(tb, dt.DeletedAt, rt.DeletedAt)
}

View File

@ -1,6 +1,7 @@
package model
import (
"encoding/json"
"time"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
@ -51,6 +52,34 @@ func NewTribe(t domain.Tribe) Tribe {
}
}
type NullTribe struct {
Tribe Tribe
Valid bool
}
func (t NullTribe) MarshalJSON() ([]byte, error) {
if !t.Valid {
return []byte("null"), nil
}
return json.Marshal(t.Tribe)
}
func (t *NullTribe) UnmarshalJSON(data []byte) error {
// Ignore null, like in the main JSON package.
if string(data) == "null" {
return nil
}
if err := json.Unmarshal(data, &t.Tribe); err != nil {
return err
}
t.Valid = true
return nil
}
type ListTribesResp struct {
Data []Tribe `json:"data"`
} // @name ListTribesResp

View File

@ -1,6 +1,7 @@
package model_test
import (
"encoding/json"
"testing"
"time"
@ -42,6 +43,147 @@ func TestNewTribe(t *testing.T) {
assertTribe(t, tribe, model.NewTribe(tribe))
}
func TestNullTribe_MarshalJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
tribe model.NullTribe
expectedJSON string
}{
{
name: "OK: null 1",
tribe: model.NullTribe{
Tribe: model.Tribe{},
Valid: false,
},
expectedJSON: "null",
},
{
name: "OK: null 2",
tribe: model.NullTribe{
Tribe: model.Tribe{ID: 1234},
Valid: false,
},
expectedJSON: "null",
},
{
name: "OK: valid struct",
tribe: model.NullTribe{
Tribe: model.Tribe{
RankAtt: 8,
ScoreAtt: 7,
RankDef: 6,
ScoreDef: 5,
RankTotal: 2,
ScoreTotal: 1,
ID: 997,
Name: "name 997",
Tag: "tag 997",
NumMembers: 6,
NumVillages: 5,
Points: 4,
AllPoints: 3,
Rank: 2,
Dominance: 12.5,
CreatedAt: time.Date(2022, time.July, 30, 14, 13, 12, 0, time.UTC),
},
Valid: true,
},
//nolint:lll
expectedJSON: `{"id":997,"name":"name 997","tag":"tag 997","numMembers":6,"numVillages":5,"points":4,"allPoints":3,"rank":2,"dominance":12.5,"rankAtt":8,"scoreAtt":7,"rankDef":6,"scoreDef":5,"rankTotal":2,"scoreTotal":1,"createdAt":"2022-07-30T14:13:12Z","deletedAt":null}`,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
b, err := json.Marshal(tt.tribe)
assert.NoError(t, err)
assert.JSONEq(t, tt.expectedJSON, string(b))
})
}
}
func TestNullTribe_UnmarshalJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
json string
expectedTribe model.NullTribe
expectedJSONSyntaxError bool
}{
{
name: "OK: null",
json: "null",
expectedTribe: model.NullTribe{
Tribe: model.Tribe{},
Valid: false,
},
},
{
name: "OK: valid struct",
//nolint:lll
json: `{"id":997,"name":"name 997","tag":"tag 997","numMembers":6,"numVillages":5,"points":4,"allPoints":3,"rank":2,"dominance":12.5,"rankAtt":8,"scoreAtt":7,"rankDef":6,"scoreDef":5,"rankTotal":2,"scoreTotal":1,"createdAt":"2022-07-30T14:13:12Z","deletedAt":null}`,
expectedTribe: model.NullTribe{
Tribe: model.Tribe{
RankAtt: 8,
ScoreAtt: 7,
RankDef: 6,
ScoreDef: 5,
RankTotal: 2,
ScoreTotal: 1,
ID: 997,
Name: "name 997",
Tag: "tag 997",
NumMembers: 6,
NumVillages: 5,
Points: 4,
AllPoints: 3,
Rank: 2,
Dominance: 12.5,
CreatedAt: time.Date(2022, time.July, 30, 14, 13, 12, 0, time.UTC),
},
Valid: true,
},
},
{
name: "ERR: invalid tribe 1",
json: "2022-07-30T14:13:12.0000005Z",
expectedTribe: model.NullTribe{},
expectedJSONSyntaxError: true,
},
{
name: "ERR: invalid tribe 2",
json: "hello world",
expectedTribe: model.NullTribe{},
expectedJSONSyntaxError: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var target model.NullTribe
err := json.Unmarshal([]byte(tt.json), &target)
if tt.expectedJSONSyntaxError {
var syntaxError *json.SyntaxError
assert.ErrorAs(t, err, &syntaxError)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.expectedTribe, target)
})
}
}
func TestNewListTribesResp(t *testing.T) {
t.Parallel()
@ -157,3 +299,16 @@ func assertTribe(tb testing.TB, dt domain.Tribe, rt model.Tribe) {
assert.Equal(tb, dt.CreatedAt, rt.CreatedAt)
assertNullTime(tb, dt.DeletedAt, rt.DeletedAt)
}
func assertNullTribe(tb testing.TB, dt domain.NullTribe, rt model.NullTribe) {
tb.Helper()
if !dt.Valid {
assert.False(tb, rt.Valid)
assert.Zero(tb, rt.Tribe)
return
}
assert.True(tb, rt.Valid)
assertTribe(tb, dt.Tribe, rt.Tribe)
}

52
internal/rest/player.go Normal file
View File

@ -0,0 +1,52 @@
package rest
import (
"context"
"net/http"
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model"
"github.com/go-chi/chi/v5"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
)
//counterfeiter:generate -o internal/mock/player_service.gen.go . PlayerService
type PlayerService interface {
GetByServerKeyAndID(ctx context.Context, serverKey string, id int64, includeTribe bool) (domain.Player, error)
}
type player struct {
svc PlayerService
}
// @ID getPlayer
// @Summary Get a player
// @Description Get a player
// @Tags versions,servers,players
// @Produce json
// @Success 200 {object} model.GetPlayerResp
// @Failure 400 {object} model.ErrorResp
// @Failure 404 {object} model.ErrorResp
// @Failure 500 {object} model.ErrorResp
// @Param versionCode path string true "Version code"
// @Param serverKey path string true "Server key"
// @Param playerId path integer true "Player ID"
// @Router /versions/{versionCode}/servers/{serverKey}/players/{playerId} [get]
func (p *player) getByID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
routeCtx := chi.RouteContext(ctx)
playerID, err := urlParamInt64(routeCtx, "playerId")
if err != nil {
renderErr(w, err)
return
}
pl, err := p.svc.GetByServerKeyAndID(ctx, routeCtx.URLParam("serverKey"), playerID, true)
if err != nil {
renderErr(w, err)
return
}
renderJSON(w, http.StatusOK, model.NewGetPlayerResp(pl))
}

View File

@ -0,0 +1,247 @@
package rest_test
import (
"fmt"
"log"
"net/http"
"testing"
"time"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"gitea.dwysokinski.me/twhelp/core/internal/rest"
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/mock"
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model"
)
func TestPlayer_getByID(t *testing.T) {
t.Parallel()
now := time.Now()
version := domain.Version{
Code: "pl",
Name: "Poland",
Host: "plemiona.pl",
Timezone: "Europe/Warsaw",
}
server := domain.Server{
Key: "pl151",
URL: "https://pl151.plemiona.pl",
Open: true,
Special: false,
VersionCode: "pl",
}
tests := []struct {
name string
setup func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, playerSvc *mock.FakePlayerService)
versionCode string
serverKey string
id string
expectedStatus int
target any
expectedResponse any
}{
{
name: "OK",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, playerSvc *mock.FakePlayerService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
playerSvc.GetByServerKeyAndIDReturns(domain.Player{
BasePlayer: domain.BasePlayer{
OpponentsDefeated: domain.OpponentsDefeated{
RankAtt: 8,
ScoreAtt: 7,
RankDef: 6,
ScoreDef: 5,
RankSup: 4,
ScoreSup: 3,
RankTotal: 2,
ScoreTotal: 1,
},
ID: 997,
Name: "name 997",
NumVillages: 5,
Points: 4,
Rank: 2,
TribeID: 1234,
},
ServerKey: "pl151",
Tribe: domain.NullTribe{
Valid: true,
Tribe: domain.Tribe{
BaseTribe: domain.BaseTribe{
OpponentsDefeated: domain.OpponentsDefeated{
RankAtt: 8,
ScoreAtt: 7,
RankDef: 6,
ScoreDef: 5,
RankSup: 4,
ScoreSup: 3,
RankTotal: 2,
ScoreTotal: 1,
},
ID: 1234,
Name: "name 997",
Tag: "tag 997",
NumMembers: 6,
NumVillages: 5,
Points: 4,
AllPoints: 3,
Rank: 2,
},
ServerKey: "pl151",
Dominance: 12.5,
CreatedAt: now,
},
},
CreatedAt: now,
}, nil)
},
versionCode: version.Code,
serverKey: "pl151",
id: "997",
expectedStatus: http.StatusOK,
target: &model.GetPlayerResp{},
expectedResponse: &model.GetPlayerResp{
Data: model.Player{
ID: 997,
Name: "name 997",
NumVillages: 5,
Points: 4,
Rank: 2,
RankAtt: 8,
ScoreAtt: 7,
RankDef: 6,
ScoreDef: 5,
RankSup: 4,
ScoreSup: 3,
RankTotal: 2,
ScoreTotal: 1,
CreatedAt: now,
Tribe: model.NullTribe{
Tribe: model.Tribe{
ID: 1234,
Name: "name 997",
Tag: "tag 997",
NumMembers: 6,
NumVillages: 5,
Points: 4,
AllPoints: 3,
Rank: 2,
Dominance: 12.5,
RankAtt: 8,
ScoreAtt: 7,
RankDef: 6,
ScoreDef: 5,
RankTotal: 2,
ScoreTotal: 1,
CreatedAt: now,
},
Valid: true,
},
},
},
},
{
name: "ERR: player id is not a valid int64",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, playerSvc *mock.FakePlayerService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
},
versionCode: version.Code,
serverKey: "pl1512",
id: "true",
expectedStatus: http.StatusBadRequest,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeValidationError.String(),
Message: "playerId: strconv.ParseInt: parsing \"true\": invalid syntax",
},
},
},
{
name: "ERR: version not found",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, playerSvc *mock.FakePlayerService) {
versionSvc.GetByCodeReturns(domain.Version{}, domain.VersionNotFoundError{VerCode: version.Code + "2"})
},
versionCode: version.Code + "2",
serverKey: "pl151",
id: "1234",
expectedStatus: http.StatusNotFound,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeEntityNotFound.String(),
Message: fmt.Sprintf("version (code=%s) not found", version.Code+"2"),
},
},
},
{
name: "ERR: server not found",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, playerSvc *mock.FakePlayerService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(domain.Server{}, domain.ServerNotFoundError{Key: "pl1512"})
},
versionCode: version.Code,
serverKey: "pl1512",
id: "1234",
expectedStatus: http.StatusNotFound,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeEntityNotFound.String(),
Message: "server (key=pl1512) not found",
},
},
},
{
name: "ERR: player not found",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, playerSvc *mock.FakePlayerService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
playerSvc.GetByServerKeyAndIDReturns(domain.Player{}, domain.PlayerNotFoundError{ID: 12345551})
},
versionCode: version.Code,
serverKey: server.Key,
id: "12345551",
expectedStatus: http.StatusNotFound,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeEntityNotFound.String(),
Message: "player (id=12345551) not found",
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
versionSvc := &mock.FakeVersionService{}
serverSvc := &mock.FakeServerService{}
playerSvc := &mock.FakePlayerService{}
tt.setup(versionSvc, serverSvc, playerSvc)
router := rest.NewRouter(rest.RouterConfig{
VersionService: versionSvc,
ServerService: serverSvc,
PlayerService: playerSvc,
})
target := fmt.Sprintf(
"/v1/versions/%s/servers/%s/players/%s",
tt.versionCode,
tt.serverKey,
tt.id,
)
log.Println(target)
resp := doRequest(router, http.MethodGet, target, nil)
defer resp.Body.Close()
assertJSONResponse(t, resp, tt.expectedStatus, tt.expectedResponse, tt.target)
})
}
}

View File

@ -32,6 +32,7 @@ type RouterConfig struct {
VersionService VersionService
ServerService ServerService
TribeService TribeService
PlayerService PlayerService
CORS CORSConfig
Swagger bool
}
@ -47,6 +48,9 @@ func NewRouter(cfg RouterConfig) *chi.Mux {
tribeHandler := &tribe{
svc: cfg.TribeService,
}
playerHandler := &player{
svc: cfg.PlayerService,
}
router := chi.NewRouter()
@ -77,12 +81,19 @@ func NewRouter(cfg RouterConfig) *chi.Mux {
r.Get("/", serverHandler.list)
r.Route("/{serverKey}", func(r chi.Router) {
r.Get("/", serverHandler.getByKey)
r.With(verifyServerKey(cfg.ServerService)).
verifyServerKeyMiddleware := verifyServerKey(cfg.ServerService)
r.With(verifyServerKeyMiddleware).
Route("/tribes", func(r chi.Router) {
r.Get("/", tribeHandler.list)
r.Get("/{tribeId}", tribeHandler.getByID)
r.Get("/tag/{tribeTag}", tribeHandler.getByTag)
})
r.With(verifyServerKeyMiddleware).
Route("/players", func(r chi.Router) {
r.Get("/{playerId}", playerHandler.getByID)
})
})
})
})

View File

@ -18,8 +18,8 @@ const (
//counterfeiter:generate -o internal/mock/tribe_service.gen.go . TribeService
type TribeService interface {
List(ctx context.Context, params domain.ListTribesParams) ([]domain.Tribe, int64, error)
GetTribeByServerKeyAndID(ctx context.Context, serverKey string, id int64) (domain.Tribe, error)
GetTribeByServerKeyAndTag(ctx context.Context, serverKey string, tag string) (domain.Tribe, error)
GetByServerKeyAndID(ctx context.Context, serverKey string, id int64) (domain.Tribe, error)
GetByServerKeyAndTag(ctx context.Context, serverKey string, tag string) (domain.Tribe, error)
}
type tribe struct {
@ -103,7 +103,7 @@ func (t *tribe) getByID(w http.ResponseWriter, r *http.Request) {
return
}
trb, err := t.svc.GetTribeByServerKeyAndID(ctx, routeCtx.URLParam("serverKey"), tribeID)
trb, err := t.svc.GetByServerKeyAndID(ctx, routeCtx.URLParam("serverKey"), tribeID)
if err != nil {
renderErr(w, err)
return
@ -128,7 +128,7 @@ func (t *tribe) getByTag(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
routeCtx := chi.RouteContext(ctx)
trb, err := t.svc.GetTribeByServerKeyAndTag(
trb, err := t.svc.GetByServerKeyAndTag(
ctx,
routeCtx.URLParam("serverKey"),
routeCtx.URLParam("tribeTag"),

View File

@ -645,7 +645,7 @@ func TestTribe_getByID(t *testing.T) {
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, tribeSvc *mock.FakeTribeService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
tribeSvc.GetTribeByServerKeyAndIDReturns(domain.Tribe{
tribeSvc.GetByServerKeyAndIDReturns(domain.Tribe{
BaseTribe: domain.BaseTribe{
OpponentsDefeated: domain.OpponentsDefeated{
RankAtt: 8,
@ -755,7 +755,7 @@ func TestTribe_getByID(t *testing.T) {
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, tribeSvc *mock.FakeTribeService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
tribeSvc.GetTribeByServerKeyAndIDReturns(domain.Tribe{}, domain.TribeNotFoundError{ID: 12345551})
tribeSvc.GetByServerKeyAndIDReturns(domain.Tribe{}, domain.TribeNotFoundError{ID: 12345551})
},
versionCode: version.Code,
serverKey: server.Key,
@ -833,7 +833,7 @@ func TestTribe_getByTag(t *testing.T) {
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, tribeSvc *mock.FakeTribeService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
tribeSvc.GetTribeByServerKeyAndTagReturns(domain.Tribe{
tribeSvc.GetByServerKeyAndTagReturns(domain.Tribe{
BaseTribe: domain.BaseTribe{
OpponentsDefeated: domain.OpponentsDefeated{
RankAtt: 8,
@ -925,7 +925,7 @@ func TestTribe_getByTag(t *testing.T) {
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, tribeSvc *mock.FakeTribeService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
tribeSvc.GetTribeByServerKeyAndTagReturns(domain.Tribe{}, domain.TribeNotFoundError{Tag: "*CSA*"})
tribeSvc.GetByServerKeyAndTagReturns(domain.Tribe{}, domain.TribeNotFoundError{Tag: "*CSA*"})
},
versionCode: version.Code,
serverKey: server.Key,

View File

@ -76,3 +76,23 @@ func (p *Player) Refresh(ctx context.Context, key, url string) (int64, error) {
return int64(len(players)), nil
}
func (p *Player) GetByServerKeyAndID(ctx context.Context, serverKey string, id int64, includeTribe bool) (domain.Player, error) {
players, _, err := p.repo.List(ctx, domain.ListPlayersParams{
IDs: []int64{id},
ServerKeys: []string{serverKey},
IncludeTribe: includeTribe,
Pagination: domain.Pagination{
Limit: 1,
},
})
if err != nil {
return domain.Player{}, fmt.Errorf("PlayerRepository.List: %w", err)
}
if len(players) == 0 {
return domain.Player{}, domain.PlayerNotFoundError{
ID: id,
}
}
return players[0], nil
}

View File

@ -113,3 +113,59 @@ func TestPlayer_Refresh(t *testing.T) {
require.Equal(t, 1, repo.ListCallCount())
}
func TestPlayer_GetByServerKeyAndID(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
player := domain.Player{
BasePlayer: domain.BasePlayer{
ID: 123,
TribeID: 123,
},
Tribe: domain.NullTribe{
Valid: true,
Tribe: domain.Tribe{
BaseTribe: domain.BaseTribe{
ID: 123,
},
},
},
ServerKey: "pl151",
}
repo := &mock.FakePlayerRepository{}
repo.ListReturns([]domain.Player{player}, 0, nil)
client := &mock.FakePlayersGetter{}
res, err := service.NewPlayer(repo, client).
GetByServerKeyAndID(context.Background(), player.ServerKey, player.ID, true)
assert.NoError(t, err)
assert.Equal(t, player, res)
require.Equal(t, 1, repo.ListCallCount())
_, params := repo.ListArgsForCall(0)
assert.Equal(t, domain.ListPlayersParams{
IDs: []int64{player.ID},
ServerKeys: []string{player.ServerKey},
IncludeTribe: true,
Pagination: domain.Pagination{
Limit: 1,
},
}, params)
})
t.Run("ERR: player not found", func(t *testing.T) {
t.Parallel()
repo := &mock.FakePlayerRepository{}
client := &mock.FakePlayersGetter{}
var id int64 = 123
res, err := service.NewPlayer(repo, client).
GetByServerKeyAndID(context.Background(), "pl151", id, false)
assert.ErrorIs(t, err, domain.PlayerNotFoundError{ID: id})
assert.Zero(t, res)
})
}

View File

@ -124,7 +124,7 @@ func (t *Tribe) List(ctx context.Context, params domain.ListTribesParams) ([]dom
return servers, count, nil
}
func (t *Tribe) GetTribeByServerKeyAndID(ctx context.Context, serverKey string, id int64) (domain.Tribe, error) {
func (t *Tribe) GetByServerKeyAndID(ctx context.Context, serverKey string, id int64) (domain.Tribe, error) {
tribes, _, err := t.repo.List(ctx, domain.ListTribesParams{
IDs: []int64{id},
ServerKeys: []string{serverKey},
@ -143,7 +143,7 @@ func (t *Tribe) GetTribeByServerKeyAndID(ctx context.Context, serverKey string,
return tribes[0], nil
}
func (t *Tribe) GetTribeByServerKeyAndTag(ctx context.Context, serverKey string, tag string) (domain.Tribe, error) {
func (t *Tribe) GetByServerKeyAndTag(ctx context.Context, serverKey string, tag string) (domain.Tribe, error) {
tribes, _, err := t.repo.List(ctx, domain.ListTribesParams{
Tags: []string{tag},
ServerKeys: []string{serverKey},

View File

@ -305,7 +305,7 @@ func TestTribe_List(t *testing.T) {
})
}
func TestTribe_GetTribeByServerKeyAndID(t *testing.T) {
func TestTribe_GetByServerKeyAndID(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
@ -322,7 +322,7 @@ func TestTribe_GetTribeByServerKeyAndID(t *testing.T) {
client := &mock.FakeTribesGetter{}
res, err := service.NewTribe(repo, client).
GetTribeByServerKeyAndID(context.Background(), tribe.ServerKey, tribe.ID)
GetByServerKeyAndID(context.Background(), tribe.ServerKey, tribe.ID)
assert.NoError(t, err)
assert.Equal(t, tribe, res)
require.Equal(t, 1, repo.ListCallCount())
@ -345,13 +345,13 @@ func TestTribe_GetTribeByServerKeyAndID(t *testing.T) {
var id int64 = 123
res, err := service.NewTribe(repo, client).
GetTribeByServerKeyAndID(context.Background(), "pl151", id)
GetByServerKeyAndID(context.Background(), "pl151", id)
assert.ErrorIs(t, err, domain.TribeNotFoundError{ID: id})
assert.Zero(t, res)
})
}
func TestTribe_GetTribeByServerKeyAndTag(t *testing.T) {
func TestTribe_GetByServerKeyAndTag(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
@ -369,7 +369,7 @@ func TestTribe_GetTribeByServerKeyAndTag(t *testing.T) {
client := &mock.FakeTribesGetter{}
res, err := service.NewTribe(repo, client).
GetTribeByServerKeyAndTag(context.Background(), tribe.ServerKey, tribe.Tag)
GetByServerKeyAndTag(context.Background(), tribe.ServerKey, tribe.Tag)
assert.NoError(t, err)
assert.Equal(t, tribe, res)
require.Equal(t, 1, repo.ListCallCount())
@ -392,7 +392,7 @@ func TestTribe_GetTribeByServerKeyAndTag(t *testing.T) {
tag := "taggg"
res, err := service.NewTribe(repo, client).
GetTribeByServerKeyAndTag(context.Background(), "pl151", tag)
GetByServerKeyAndTag(context.Background(), "pl151", tag)
assert.ErrorIs(t, err, domain.TribeNotFoundError{Tag: tag})
assert.Zero(t, res)
})