parent
1b699d225f
commit
751805d6b1
|
@ -135,7 +135,7 @@ var cmdConsumer = &cli.Command{
|
||||||
}
|
}
|
||||||
|
|
||||||
consumer := port.NewPlayerWatermillConsumer(
|
consumer := port.NewPlayerWatermillConsumer(
|
||||||
app.NewPlayerService(twSvc),
|
app.NewPlayerService(adapter.NewPlayerBunRepository(db), twSvc),
|
||||||
subscriber,
|
subscriber,
|
||||||
logger,
|
logger,
|
||||||
marshaler,
|
marshaler,
|
||||||
|
|
|
@ -20,6 +20,7 @@ func NewFixture(bunDB *bun.DB) *Fixture {
|
||||||
(*bunmodel.Version)(nil),
|
(*bunmodel.Version)(nil),
|
||||||
(*bunmodel.Server)(nil),
|
(*bunmodel.Server)(nil),
|
||||||
(*bunmodel.Tribe)(nil),
|
(*bunmodel.Tribe)(nil),
|
||||||
|
(*bunmodel.Player)(nil),
|
||||||
)
|
)
|
||||||
return &Fixture{
|
return &Fixture{
|
||||||
f: dbfixture.New(bunDB),
|
f: dbfixture.New(bunDB),
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
package bunmodel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
bun.BaseModel `bun:"table:players,alias:player"`
|
||||||
|
|
||||||
|
ID int `bun:"id,nullzero,pk"`
|
||||||
|
ServerKey string `bun:"server_key,nullzero,pk"`
|
||||||
|
Server Server `bun:"server,rel:belongs-to,join:server_key=key"`
|
||||||
|
Name string `bun:"name,nullzero"`
|
||||||
|
NumVillages int `bun:"num_villages"`
|
||||||
|
Points int `bun:"points"`
|
||||||
|
Rank int `bun:"rank"`
|
||||||
|
TribeID int `bun:"tribe_id,nullzero"`
|
||||||
|
Tribe Tribe `bun:"tribe,rel:belongs-to,join:tribe_id=id,join:server_key=server_key"`
|
||||||
|
ProfileURL string `bun:"profile_url,nullzero"`
|
||||||
|
BestRank int `bun:"best_rank"`
|
||||||
|
BestRankAt time.Time `bun:"best_rank_at,nullzero,default:CURRENT_TIMESTAMP"`
|
||||||
|
MostPoints int `bun:"most_points"`
|
||||||
|
MostPointsAt time.Time `bun:"most_points_at,nullzero,default:CURRENT_TIMESTAMP"`
|
||||||
|
MostVillages int `bun:"most_villages"`
|
||||||
|
MostVillagesAt time.Time `bun:"most_villages_at,nullzero,default:CURRENT_TIMESTAMP"`
|
||||||
|
LastActivityAt time.Time `bun:"last_activity_at,nullzero,default:CURRENT_TIMESTAMP"`
|
||||||
|
CreatedAt time.Time `bun:"created_at,nullzero,default:CURRENT_TIMESTAMP"`
|
||||||
|
DeletedAt time.Time `bun:"deleted_at,nullzero"`
|
||||||
|
|
||||||
|
OpponentsDefeated
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) ToDomain() (domain.Player, error) {
|
||||||
|
od, err := p.OpponentsDefeated.ToDomain()
|
||||||
|
if err != nil {
|
||||||
|
return domain.Player{}, fmt.Errorf(
|
||||||
|
"couldn't construct domain.Player (id=%d,serverKey=%s): %w",
|
||||||
|
p.ID,
|
||||||
|
p.ServerKey,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
converted, err := domain.UnmarshalPlayerFromDatabase(
|
||||||
|
p.ID,
|
||||||
|
p.ServerKey,
|
||||||
|
p.Name,
|
||||||
|
p.NumVillages,
|
||||||
|
p.Points,
|
||||||
|
p.Rank,
|
||||||
|
p.TribeID,
|
||||||
|
od,
|
||||||
|
p.ProfileURL,
|
||||||
|
p.BestRank,
|
||||||
|
p.BestRankAt,
|
||||||
|
p.MostPoints,
|
||||||
|
p.MostPointsAt,
|
||||||
|
p.MostVillages,
|
||||||
|
p.MostVillagesAt,
|
||||||
|
p.LastActivityAt,
|
||||||
|
p.CreatedAt,
|
||||||
|
p.DeletedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Player{}, fmt.Errorf(
|
||||||
|
"couldn't construct domain.Player (id=%d,serverKey=%s): %w",
|
||||||
|
p.ID,
|
||||||
|
p.ServerKey,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return converted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Players []Player
|
||||||
|
|
||||||
|
func (ps Players) ToDomain() (domain.Players, error) {
|
||||||
|
res := make(domain.Players, 0, len(ps))
|
||||||
|
|
||||||
|
for _, p := range ps {
|
||||||
|
converted, err := p.ToDomain()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, converted)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
|
@ -0,0 +1,166 @@
|
||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/adapter/internal/bunmodel"
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
"github.com/uptrace/bun/dialect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlayerBunRepository struct {
|
||||||
|
db bun.IDB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlayerBunRepository(db bun.IDB) *PlayerBunRepository {
|
||||||
|
return &PlayerBunRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *PlayerBunRepository) CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error {
|
||||||
|
if len(params) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
players := make(bunmodel.Players, 0, len(params))
|
||||||
|
|
||||||
|
for _, p := range params {
|
||||||
|
base := p.Base()
|
||||||
|
players = append(players, bunmodel.Player{
|
||||||
|
ID: base.ID(),
|
||||||
|
ServerKey: p.ServerKey(),
|
||||||
|
Name: base.Name(),
|
||||||
|
NumVillages: base.NumVillages(),
|
||||||
|
Points: base.Points(),
|
||||||
|
Rank: base.Rank(),
|
||||||
|
TribeID: base.TribeID(),
|
||||||
|
ProfileURL: base.ProfileURL().String(),
|
||||||
|
BestRank: p.BestRank(),
|
||||||
|
BestRankAt: p.BestRankAt(),
|
||||||
|
MostPoints: p.MostPoints(),
|
||||||
|
MostPointsAt: p.MostPointsAt(),
|
||||||
|
MostVillages: p.MostVillages(),
|
||||||
|
MostVillagesAt: p.MostVillagesAt(),
|
||||||
|
LastActivityAt: p.LastActivityAt(),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
OpponentsDefeated: bunmodel.NewOpponentsDefeated(base.OD()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
q := repo.db.NewInsert().
|
||||||
|
Model(&players)
|
||||||
|
|
||||||
|
//nolint:exhaustive
|
||||||
|
switch q.Dialect().Name() {
|
||||||
|
case dialect.PG:
|
||||||
|
q = q.On("CONFLICT ON CONSTRAINT players_pkey DO UPDATE")
|
||||||
|
case dialect.SQLite:
|
||||||
|
q = q.On("CONFLICT(id, server_key) DO UPDATE")
|
||||||
|
default:
|
||||||
|
q = q.Err(errors.New("unsupported dialect"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := q.
|
||||||
|
Set("name = EXCLUDED.name").
|
||||||
|
Set("num_villages = EXCLUDED.num_villages").
|
||||||
|
Set("points = EXCLUDED.points").
|
||||||
|
Set("rank = EXCLUDED.rank").
|
||||||
|
Set("tribe_id = EXCLUDED.tribe_id").
|
||||||
|
Set("profile_url = EXCLUDED.profile_url").
|
||||||
|
Set("best_rank = EXCLUDED.best_rank").
|
||||||
|
Set("best_rank_at = EXCLUDED.best_rank_at").
|
||||||
|
Set("most_villages = EXCLUDED.most_villages").
|
||||||
|
Set("most_villages_at = EXCLUDED.most_villages_at").
|
||||||
|
Set("most_points = EXCLUDED.most_points").
|
||||||
|
Set("most_points_at = EXCLUDED.most_points_at").
|
||||||
|
Set("last_activity_at = EXCLUDED.last_activity_at").
|
||||||
|
Set("deleted_at = EXCLUDED.deleted_at").
|
||||||
|
Apply(appendODSetClauses).
|
||||||
|
Returning("").
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return fmt.Errorf("something went wrong while inserting players into the db: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *PlayerBunRepository) List(ctx context.Context, params domain.ListPlayersParams) (domain.Players, error) {
|
||||||
|
var players bunmodel.Players
|
||||||
|
|
||||||
|
if err := repo.db.NewSelect().
|
||||||
|
Model(&players).
|
||||||
|
Apply(listPlayersParamsApplier{params: params}.apply).
|
||||||
|
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, fmt.Errorf("couldn't select players from the db: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return players.ToDomain()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *PlayerBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := repo.db.NewUpdate().
|
||||||
|
Model((*bunmodel.Player)(nil)).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
Where("id IN (?)", bun.In(ids)).
|
||||||
|
Where("server_key = ?", serverKey).
|
||||||
|
Set("deleted_at = ?", time.Now()).
|
||||||
|
Set("tribe_id = NULL").
|
||||||
|
Returning("").
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return fmt.Errorf("couldn't delete players: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type listPlayersParamsApplier struct {
|
||||||
|
params domain.ListPlayersParams
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo
|
||||||
|
func (a listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
|
||||||
|
if ids := a.params.IDs(); len(ids) > 0 {
|
||||||
|
q = q.Where("player.id IN (?)", bun.In(ids))
|
||||||
|
}
|
||||||
|
|
||||||
|
if idGT := a.params.IDGT(); idGT.Valid {
|
||||||
|
q = q.Where("player.id > ?", idGT.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 {
|
||||||
|
q = q.Where("player.server_key IN (?)", bun.In(serverKeys))
|
||||||
|
}
|
||||||
|
|
||||||
|
if deleted := a.params.Deleted(); deleted.Valid {
|
||||||
|
if deleted.Value {
|
||||||
|
q = q.Where("player.deleted_at IS NOT NULL")
|
||||||
|
} else {
|
||||||
|
q = q.Where("player.deleted_at IS NULL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range a.params.Sort() {
|
||||||
|
switch s {
|
||||||
|
case domain.PlayerSortIDASC:
|
||||||
|
q = q.Order("player.id ASC")
|
||||||
|
case domain.PlayerSortIDDESC:
|
||||||
|
q = q.Order("player.id DESC")
|
||||||
|
case domain.PlayerSortServerKeyASC:
|
||||||
|
q = q.Order("player.server_key ASC")
|
||||||
|
case domain.PlayerSortServerKeyDESC:
|
||||||
|
q = q.Order("player.server_key DESC")
|
||||||
|
default:
|
||||||
|
return q.Err(errors.New("unsupported sort value"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.Limit(a.params.Limit()).Offset(a.params.Offset())
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package adapter_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/adapter/adaptertest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPlayerBunRepository_Postgres(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping long-running test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testPlayerRepository(t, func(t *testing.T) repositories {
|
||||||
|
t.Helper()
|
||||||
|
return newBunDBRepositories(t, postgres.NewBunDB(t))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlayerBunRepository_SQLite(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testPlayerRepository(t, func(t *testing.T) repositories {
|
||||||
|
t.Helper()
|
||||||
|
return newBunDBRepositories(t, adaptertest.NewBunDBSQLite(t))
|
||||||
|
})
|
||||||
|
}
|
|
@ -91,16 +91,16 @@ func (repo *TribeBunRepository) CreateOrUpdate(ctx context.Context, params ...do
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *TribeBunRepository) List(ctx context.Context, params domain.ListTribesParams) (domain.Tribes, error) {
|
func (repo *TribeBunRepository) List(ctx context.Context, params domain.ListTribesParams) (domain.Tribes, error) {
|
||||||
var servers bunmodel.Tribes
|
var tribes bunmodel.Tribes
|
||||||
|
|
||||||
if err := repo.db.NewSelect().
|
if err := repo.db.NewSelect().
|
||||||
Model(&servers).
|
Model(&tribes).
|
||||||
Apply(listTribesParamsApplier{params: params}.apply).
|
Apply(listTribesParamsApplier{params: params}.apply).
|
||||||
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, fmt.Errorf("couldn't select tribes from the db: %w", err)
|
return nil, fmt.Errorf("couldn't select tribes from the db: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return servers.ToDomain()
|
return tribes.ToDomain()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *TribeBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error {
|
func (repo *TribeBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error {
|
||||||
|
|
|
@ -0,0 +1,392 @@
|
||||||
|
package adapter_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("CreateOrUpdate", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
repos := newRepos(t)
|
||||||
|
|
||||||
|
assertCreatedUpdated := func(t *testing.T, params []domain.CreatePlayerParams) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
require.NotEmpty(t, params)
|
||||||
|
|
||||||
|
ids := make([]int, 0, len(params))
|
||||||
|
for _, p := range params {
|
||||||
|
ids = append(ids, p.Base().ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
listParams := domain.NewListPlayersParams()
|
||||||
|
require.NoError(t, listParams.SetIDs(ids))
|
||||||
|
require.NoError(t, listParams.SetServerKeys([]string{params[0].ServerKey()}))
|
||||||
|
|
||||||
|
players, err := repos.player.List(ctx, listParams)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, players, len(params))
|
||||||
|
for i, p := range params {
|
||||||
|
idx := slices.IndexFunc(players, func(player domain.Player) bool {
|
||||||
|
return player.ID() == p.Base().ID() && player.ServerKey() == p.ServerKey()
|
||||||
|
})
|
||||||
|
require.GreaterOrEqualf(t, idx, 0, "params[%d]", i)
|
||||||
|
player := players[idx]
|
||||||
|
|
||||||
|
assert.Equalf(t, p.Base(), player.Base(), "params[%d]", i)
|
||||||
|
assert.Equalf(t, p.ServerKey(), player.ServerKey(), "params[%d]", i)
|
||||||
|
assert.Equalf(t, p.BestRank(), player.BestRank(), "params[%d]", i)
|
||||||
|
assert.WithinDurationf(t, p.BestRankAt(), player.BestRankAt(), time.Minute, "params[%d]", i)
|
||||||
|
assert.Equalf(t, p.MostVillages(), player.MostVillages(), "params[%d]", i)
|
||||||
|
assert.WithinDurationf(t, p.MostVillagesAt(), player.MostVillagesAt(), time.Minute, "params[%d]", i)
|
||||||
|
assert.Equalf(t, p.MostPoints(), player.MostPoints(), "params[%d]", i)
|
||||||
|
assert.WithinDurationf(t, p.MostPointsAt(), player.MostPointsAt(), time.Minute, "params[%d]", i)
|
||||||
|
assert.WithinDurationf(t, p.LastActivityAt(), player.LastActivityAt(), time.Minute, "params[%d]", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
servers, err := repos.server.List(ctx, domain.NewListServersParams())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, servers)
|
||||||
|
server := servers[0]
|
||||||
|
|
||||||
|
playersToCreate := domain.BasePlayers{
|
||||||
|
domaintest.NewBasePlayer(t),
|
||||||
|
domaintest.NewBasePlayer(t),
|
||||||
|
}
|
||||||
|
|
||||||
|
createParams, err := domain.NewCreatePlayerParams(server.Key(), playersToCreate, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, repos.player.CreateOrUpdate(ctx, createParams...))
|
||||||
|
assertCreatedUpdated(t, createParams)
|
||||||
|
|
||||||
|
playersToUpdate := domain.BasePlayers{
|
||||||
|
domaintest.NewBasePlayer(t, func(cfg *domaintest.BasePlayerConfig) {
|
||||||
|
cfg.ID = playersToCreate[0].ID()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
updateParams, err := domain.NewCreatePlayerParams(server.Key(), playersToUpdate, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, repos.player.CreateOrUpdate(ctx, updateParams...))
|
||||||
|
assertCreatedUpdated(t, updateParams)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OK: len(params) == 0", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
require.NoError(t, repos.player.CreateOrUpdate(ctx))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("List & ListCount", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
repos := newRepos(t)
|
||||||
|
|
||||||
|
players, listPlayersErr := repos.player.List(ctx, domain.NewListPlayersParams())
|
||||||
|
require.NoError(t, listPlayersErr)
|
||||||
|
require.NotEmpty(t, players)
|
||||||
|
randPlayer := players[0]
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
params func(t *testing.T) domain.ListPlayersParams
|
||||||
|
assertPlayers func(t *testing.T, params domain.ListPlayersParams, players domain.Players)
|
||||||
|
assertError func(t *testing.T, err error)
|
||||||
|
assertTotal func(t *testing.T, params domain.ListPlayersParams, total int)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OK: default params",
|
||||||
|
params: func(t *testing.T) domain.ListPlayersParams {
|
||||||
|
t.Helper()
|
||||||
|
return domain.NewListPlayersParams()
|
||||||
|
},
|
||||||
|
assertPlayers: func(t *testing.T, params domain.ListPlayersParams, players domain.Players) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, len(players))
|
||||||
|
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
|
||||||
|
if x := cmp.Compare(a.ServerKey(), b.ServerKey()); x != 0 {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return cmp.Compare(a.ID(), b.ID())
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
assertError: func(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
assertTotal: func(t *testing.T, params domain.ListPlayersParams, total int) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, total)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OK: sort=[serverKey DESC, id DESC]",
|
||||||
|
params: func(t *testing.T) domain.ListPlayersParams {
|
||||||
|
t.Helper()
|
||||||
|
params := domain.NewListPlayersParams()
|
||||||
|
require.NoError(t, params.SetSort([]domain.PlayerSort{domain.PlayerSortServerKeyDESC, domain.PlayerSortIDDESC}))
|
||||||
|
return params
|
||||||
|
},
|
||||||
|
assertPlayers: func(t *testing.T, params domain.ListPlayersParams, players domain.Players) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, len(players))
|
||||||
|
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
|
||||||
|
if x := cmp.Compare(a.ServerKey(), b.ServerKey()) * -1; x != 0 {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return cmp.Compare(a.ID(), b.ID()) * -1
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
assertError: func(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
assertTotal: func(t *testing.T, params domain.ListPlayersParams, total int) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, total)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: fmt.Sprintf("OK: ids=[%d] serverKeys=[%s]", randPlayer.ID(), randPlayer.ServerKey()),
|
||||||
|
params: func(t *testing.T) domain.ListPlayersParams {
|
||||||
|
t.Helper()
|
||||||
|
params := domain.NewListPlayersParams()
|
||||||
|
require.NoError(t, params.SetIDs([]int{randPlayer.ID()}))
|
||||||
|
require.NoError(t, params.SetServerKeys([]string{randPlayer.ServerKey()}))
|
||||||
|
return params
|
||||||
|
},
|
||||||
|
assertPlayers: func(t *testing.T, params domain.ListPlayersParams, players domain.Players) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ids := params.IDs()
|
||||||
|
serverKeys := params.ServerKeys()
|
||||||
|
|
||||||
|
for _, p := range players {
|
||||||
|
assert.True(t, slices.Contains(ids, p.ID()))
|
||||||
|
assert.True(t, slices.Contains(serverKeys, p.ServerKey()))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
assertError: func(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
assertTotal: func(t *testing.T, params domain.ListPlayersParams, total int) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, total)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: fmt.Sprintf("OK: idGT=%d", randPlayer.ID()),
|
||||||
|
params: func(t *testing.T) domain.ListPlayersParams {
|
||||||
|
t.Helper()
|
||||||
|
params := domain.NewListPlayersParams()
|
||||||
|
require.NoError(t, params.SetIDGT(domain.NullInt{
|
||||||
|
Value: randPlayer.ID(),
|
||||||
|
Valid: true,
|
||||||
|
}))
|
||||||
|
return params
|
||||||
|
},
|
||||||
|
assertPlayers: func(t *testing.T, params domain.ListPlayersParams, players domain.Players) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, players)
|
||||||
|
for _, p := range players {
|
||||||
|
assert.Greater(t, p.ID(), params.IDGT().Value, p.ID())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
assertError: func(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
assertTotal: func(t *testing.T, params domain.ListPlayersParams, total int) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, total)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OK: deleted=true",
|
||||||
|
params: func(t *testing.T) domain.ListPlayersParams {
|
||||||
|
t.Helper()
|
||||||
|
params := domain.NewListPlayersParams()
|
||||||
|
require.NoError(t, params.SetDeleted(domain.NullBool{
|
||||||
|
Value: true,
|
||||||
|
Valid: true,
|
||||||
|
}))
|
||||||
|
return params
|
||||||
|
},
|
||||||
|
assertPlayers: func(t *testing.T, params domain.ListPlayersParams, players domain.Players) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, players)
|
||||||
|
for _, s := range players {
|
||||||
|
assert.True(t, s.IsDeleted())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
assertError: func(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
assertTotal: func(t *testing.T, params domain.ListPlayersParams, total int) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, total)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OK: deleted=false",
|
||||||
|
params: func(t *testing.T) domain.ListPlayersParams {
|
||||||
|
t.Helper()
|
||||||
|
params := domain.NewListPlayersParams()
|
||||||
|
require.NoError(t, params.SetDeleted(domain.NullBool{
|
||||||
|
Value: false,
|
||||||
|
Valid: true,
|
||||||
|
}))
|
||||||
|
return params
|
||||||
|
},
|
||||||
|
assertPlayers: func(t *testing.T, params domain.ListPlayersParams, players domain.Players) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, players)
|
||||||
|
for _, s := range players {
|
||||||
|
assert.False(t, s.IsDeleted())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
assertError: func(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
assertTotal: func(t *testing.T, params domain.ListPlayersParams, total int) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, total)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OK: offset=1 limit=2",
|
||||||
|
params: func(t *testing.T) domain.ListPlayersParams {
|
||||||
|
t.Helper()
|
||||||
|
params := domain.NewListPlayersParams()
|
||||||
|
require.NoError(t, params.SetOffset(1))
|
||||||
|
require.NoError(t, params.SetLimit(2))
|
||||||
|
return params
|
||||||
|
},
|
||||||
|
assertPlayers: func(t *testing.T, params domain.ListPlayersParams, players domain.Players) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Len(t, players, params.Limit())
|
||||||
|
},
|
||||||
|
assertError: func(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
assertTotal: func(t *testing.T, params domain.ListPlayersParams, total int) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, total)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
params := tt.params(t)
|
||||||
|
|
||||||
|
res, err := repos.player.List(ctx, params)
|
||||||
|
tt.assertError(t, err)
|
||||||
|
tt.assertPlayers(t, params, res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Delete", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
repos := newRepos(t)
|
||||||
|
|
||||||
|
listServersParams := domain.NewListServersParams()
|
||||||
|
require.NoError(t, listServersParams.SetSpecial(domain.NullBool{Value: false, Valid: true}))
|
||||||
|
servers, listServersErr := repos.server.List(ctx, listServersParams)
|
||||||
|
require.NoError(t, listServersErr)
|
||||||
|
require.NotEmpty(t, servers)
|
||||||
|
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
serverKeys := make([]string, 0, len(servers))
|
||||||
|
for _, s := range servers {
|
||||||
|
serverKeys = append(serverKeys, s.Key())
|
||||||
|
}
|
||||||
|
|
||||||
|
listPlayersParams := domain.NewListPlayersParams()
|
||||||
|
require.NoError(t, listPlayersParams.SetDeleted(domain.NullBool{Value: false, Valid: true}))
|
||||||
|
require.NoError(t, listPlayersParams.SetServerKeys(serverKeys))
|
||||||
|
|
||||||
|
players, err := repos.player.List(ctx, listPlayersParams)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var serverKey string
|
||||||
|
var ids []int
|
||||||
|
|
||||||
|
for _, p := range players {
|
||||||
|
if serverKey == "" {
|
||||||
|
serverKey = p.ServerKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.ServerKey() == serverKey {
|
||||||
|
ids = append(ids, p.ID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idsToDelete := ids[:int(math.Ceil(float64(len(ids))/2))]
|
||||||
|
|
||||||
|
require.NoError(t, repos.player.Delete(ctx, serverKey, idsToDelete...))
|
||||||
|
|
||||||
|
listPlayersParams = domain.NewListPlayersParams()
|
||||||
|
require.NoError(t, listPlayersParams.SetDeleted(domain.NullBool{Valid: false}))
|
||||||
|
require.NoError(t, listPlayersParams.SetServerKeys(serverKeys))
|
||||||
|
|
||||||
|
players, err = repos.player.List(ctx, listPlayersParams)
|
||||||
|
require.NoError(t, err)
|
||||||
|
for _, p := range players {
|
||||||
|
if p.ServerKey() == serverKey && slices.Contains(ids, p.ID()) {
|
||||||
|
if slices.Contains(idsToDelete, p.ID()) {
|
||||||
|
assert.WithinDuration(t, time.Now(), p.DeletedAt(), time.Minute)
|
||||||
|
assert.Zero(t, p.TribeID())
|
||||||
|
} else {
|
||||||
|
assert.Zero(t, p.DeletedAt())
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure that no other player is removed
|
||||||
|
assert.WithinRange(t, p.DeletedAt(), time.Time{}, time.Now().Add(-time.Minute))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OK: len(ids) == 0", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
require.NoError(t, repos.player.Delete(ctx, servers[0].Key()))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -31,10 +31,17 @@ type tribeRepository interface {
|
||||||
Delete(ctx context.Context, serverKey string, ids ...int) error
|
Delete(ctx context.Context, serverKey string, ids ...int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type playerRepository interface {
|
||||||
|
CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error
|
||||||
|
List(ctx context.Context, params domain.ListPlayersParams) (domain.Players, error)
|
||||||
|
Delete(ctx context.Context, serverKey string, ids ...int) error
|
||||||
|
}
|
||||||
|
|
||||||
type repositories struct {
|
type repositories struct {
|
||||||
version versionRepository
|
version versionRepository
|
||||||
server serverRepository
|
server serverRepository
|
||||||
tribe tribeRepository
|
tribe tribeRepository
|
||||||
|
player playerRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBunDBRepositories(tb testing.TB, bunDB *bun.DB) repositories {
|
func newBunDBRepositories(tb testing.TB, bunDB *bun.DB) repositories {
|
||||||
|
@ -46,5 +53,6 @@ func newBunDBRepositories(tb testing.TB, bunDB *bun.DB) repositories {
|
||||||
version: adapter.NewVersionBunRepository(bunDB),
|
version: adapter.NewVersionBunRepository(bunDB),
|
||||||
server: adapter.NewServerBunRepository(bunDB),
|
server: adapter.NewServerBunRepository(bunDB),
|
||||||
tribe: adapter.NewTribeBunRepository(bunDB),
|
tribe: adapter.NewTribeBunRepository(bunDB),
|
||||||
|
player: adapter.NewPlayerBunRepository(bunDB),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,21 +7,134 @@ import (
|
||||||
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PlayerRepository interface {
|
||||||
|
CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error
|
||||||
|
List(ctx context.Context, params domain.ListPlayersParams) (domain.Players, error)
|
||||||
|
// Delete marks players with the given serverKey and ids as deleted (sets deleted at to now).
|
||||||
|
// In addition, Delete sets TribeID to null.
|
||||||
|
//
|
||||||
|
// https://en.wiktionary.org/wiki/soft_deletion
|
||||||
|
Delete(ctx context.Context, serverKey string, ids ...int) error
|
||||||
|
}
|
||||||
|
|
||||||
type PlayerService struct {
|
type PlayerService struct {
|
||||||
|
repo PlayerRepository
|
||||||
twSvc TWService
|
twSvc TWService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPlayerService(twSvc TWService) *PlayerService {
|
func NewPlayerService(repo PlayerRepository, twSvc TWService) *PlayerService {
|
||||||
return &PlayerService{twSvc: twSvc}
|
return &PlayerService{repo: repo, twSvc: twSvc}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *PlayerService) Sync(ctx context.Context, payload domain.ServerSyncedEventPayload) error {
|
func (svc *PlayerService) Sync(ctx context.Context, serverSyncedPayload domain.ServerSyncedEventPayload) error {
|
||||||
tribes, err := svc.twSvc.GetPlayers(ctx, payload.URL())
|
serverKey := serverSyncedPayload.Key()
|
||||||
|
serverURL := serverSyncedPayload.URL()
|
||||||
|
|
||||||
|
players, err := svc.twSvc.GetPlayers(ctx, serverURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: couldn't get players: %w", payload.Key(), err)
|
return fmt.Errorf("%s: couldn't get players: %w", serverKey, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(payload.URL(), len(tribes))
|
if err = svc.createOrUpdate(ctx, serverKey, players); err != nil {
|
||||||
|
return fmt.Errorf("%s: couldn't create/update players: %w", serverKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = svc.delete(ctx, serverKey, players); err != nil {
|
||||||
|
return fmt.Errorf("%s: couldn't delete players: %w", serverKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const playerCreateOrUpdateChunkSize = domain.PlayerListMaxLimit
|
||||||
|
|
||||||
|
func (svc *PlayerService) createOrUpdate(ctx context.Context, serverKey string, players domain.BasePlayers) error {
|
||||||
|
for i := 0; i < len(players); i += playerCreateOrUpdateChunkSize {
|
||||||
|
end := i + playerCreateOrUpdateChunkSize
|
||||||
|
if end > len(players) {
|
||||||
|
end = len(players)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := svc.createOrUpdateChunk(ctx, serverKey, players[i:end]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *PlayerService) createOrUpdateChunk(ctx context.Context, serverKey string, players domain.BasePlayers) error {
|
||||||
|
ids := make([]int, 0, len(players))
|
||||||
|
for _, p := range players {
|
||||||
|
ids = append(ids, p.ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
listParams := domain.NewListPlayersParams()
|
||||||
|
if err := listParams.SetServerKeys([]string{serverKey}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := listParams.SetIDs(ids); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := listParams.SetSort([]domain.PlayerSort{domain.PlayerSortIDASC}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := listParams.SetLimit(domain.PlayerListMaxLimit); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
storedPlayers, err := svc.repo.List(ctx, listParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
createParams, err := domain.NewCreatePlayerParams(serverKey, players, storedPlayers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc.repo.CreateOrUpdate(ctx, createParams...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *PlayerService) delete(ctx context.Context, serverKey string, players domain.BasePlayers) error {
|
||||||
|
listParams := domain.NewListPlayersParams()
|
||||||
|
if err := listParams.SetServerKeys([]string{serverKey}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := listParams.SetDeleted(domain.NullBool{
|
||||||
|
Value: false,
|
||||||
|
Valid: true,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := listParams.SetSort([]domain.PlayerSort{domain.PlayerSortIDASC}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := listParams.SetLimit(domain.PlayerListMaxLimit); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var toDelete []int
|
||||||
|
|
||||||
|
for {
|
||||||
|
storedPlayers, err := svc.repo.List(ctx, listParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(storedPlayers) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
toDelete = append(toDelete, storedPlayers.Delete(players)...)
|
||||||
|
|
||||||
|
if err = listParams.SetIDGT(domain.NullInt{
|
||||||
|
Value: storedPlayers[len(storedPlayers)-1].ID(),
|
||||||
|
Valid: true,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc.repo.Delete(ctx, serverKey, toDelete...)
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,9 @@ import (
|
||||||
type TribeRepository interface {
|
type TribeRepository interface {
|
||||||
CreateOrUpdate(ctx context.Context, params ...domain.CreateTribeParams) error
|
CreateOrUpdate(ctx context.Context, params ...domain.CreateTribeParams) error
|
||||||
List(ctx context.Context, params domain.ListTribesParams) (domain.Tribes, error)
|
List(ctx context.Context, params domain.ListTribesParams) (domain.Tribes, error)
|
||||||
|
// Delete marks players with the given serverKey and ids as deleted (sets deleted at to now).
|
||||||
|
//
|
||||||
|
// https://en.wiktionary.org/wiki/soft_deletion
|
||||||
Delete(ctx context.Context, serverKey string, ids ...int) error
|
Delete(ctx context.Context, serverKey string, ids ...int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -128,4 +128,8 @@ func (p BasePlayer) ProfileURL() *url.URL {
|
||||||
return p.profileURL
|
return p.profileURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p BasePlayer) IsZero() bool {
|
||||||
|
return p == BasePlayer{}
|
||||||
|
}
|
||||||
|
|
||||||
type BasePlayers []BasePlayer
|
type BasePlayers []BasePlayer
|
||||||
|
|
|
@ -6,11 +6,24 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewOpponentsDefeated(tb TestingTB) domain.OpponentsDefeated {
|
type OpponentsDefeatedConfig struct {
|
||||||
|
ScoreAtt int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOpponentsDefeated(tb TestingTB, opts ...func(cfg *OpponentsDefeatedConfig)) domain.OpponentsDefeated {
|
||||||
tb.Helper()
|
tb.Helper()
|
||||||
cfg, err := domain.NewOpponentsDefeated(
|
|
||||||
|
cfg := &OpponentsDefeatedConfig{
|
||||||
|
ScoreAtt: gofakeit.IntRange(100000, 1000000),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
od, err := domain.NewOpponentsDefeated(
|
||||||
gofakeit.IntRange(1, 100),
|
gofakeit.IntRange(1, 100),
|
||||||
gofakeit.IntRange(100000, 1000000),
|
cfg.ScoreAtt,
|
||||||
gofakeit.IntRange(1, 100),
|
gofakeit.IntRange(1, 100),
|
||||||
gofakeit.IntRange(100000, 1000000),
|
gofakeit.IntRange(100000, 1000000),
|
||||||
gofakeit.IntRange(1, 100),
|
gofakeit.IntRange(1, 100),
|
||||||
|
@ -19,5 +32,6 @@ func NewOpponentsDefeated(tb TestingTB) domain.OpponentsDefeated {
|
||||||
gofakeit.IntRange(100000, 1000000),
|
gofakeit.IntRange(100000, 1000000),
|
||||||
)
|
)
|
||||||
require.NoError(tb, err)
|
require.NoError(tb, err)
|
||||||
return cfg
|
|
||||||
|
return od
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
package domaintest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||||
|
"github.com/brianvoe/gofakeit/v6"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlayerConfig struct {
|
||||||
|
ID int
|
||||||
|
Points int
|
||||||
|
NumVillages int
|
||||||
|
ServerKey string
|
||||||
|
OD domain.OpponentsDefeated
|
||||||
|
BestRank int
|
||||||
|
BestRankAt time.Time
|
||||||
|
MostPoints int
|
||||||
|
MostPointsAt time.Time
|
||||||
|
MostVillages int
|
||||||
|
MostVillagesAt time.Time
|
||||||
|
LastActivityAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlayer(tb TestingTB, opts ...func(cfg *PlayerConfig)) domain.Player {
|
||||||
|
tb.Helper()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
cfg := &PlayerConfig{
|
||||||
|
ID: RandID(),
|
||||||
|
ServerKey: RandServerKey(),
|
||||||
|
Points: gofakeit.IntRange(1, 10000),
|
||||||
|
NumVillages: gofakeit.IntRange(1, 10000),
|
||||||
|
OD: NewOpponentsDefeated(tb),
|
||||||
|
BestRank: gofakeit.IntRange(1, 10000),
|
||||||
|
BestRankAt: now,
|
||||||
|
MostPoints: gofakeit.IntRange(1, 10000),
|
||||||
|
MostPointsAt: now,
|
||||||
|
MostVillages: gofakeit.IntRange(1, 10000),
|
||||||
|
MostVillagesAt: now,
|
||||||
|
LastActivityAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := domain.UnmarshalPlayerFromDatabase(
|
||||||
|
cfg.ID,
|
||||||
|
cfg.ServerKey,
|
||||||
|
gofakeit.LetterN(50),
|
||||||
|
cfg.NumVillages,
|
||||||
|
cfg.Points,
|
||||||
|
gofakeit.IntRange(1, 10000),
|
||||||
|
gofakeit.IntRange(1, 10000),
|
||||||
|
cfg.OD,
|
||||||
|
gofakeit.URL(),
|
||||||
|
cfg.BestRank,
|
||||||
|
cfg.BestRankAt,
|
||||||
|
cfg.MostPoints,
|
||||||
|
cfg.MostPointsAt,
|
||||||
|
cfg.MostVillages,
|
||||||
|
cfg.MostVillagesAt,
|
||||||
|
cfg.LastActivityAt,
|
||||||
|
now,
|
||||||
|
time.Time{},
|
||||||
|
)
|
||||||
|
require.NoError(tb, err)
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
|
@ -1,6 +1,487 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net/url"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
playerNameMinLength = 1
|
playerNameMinLength = 1
|
||||||
playerNameMaxLength = 150
|
playerNameMaxLength = 150
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
id int
|
||||||
|
serverKey string
|
||||||
|
name string
|
||||||
|
numVillages int
|
||||||
|
points int
|
||||||
|
rank int
|
||||||
|
tribeID int
|
||||||
|
od OpponentsDefeated
|
||||||
|
profileURL *url.URL
|
||||||
|
bestRank int
|
||||||
|
bestRankAt time.Time
|
||||||
|
mostPoints int
|
||||||
|
mostPointsAt time.Time
|
||||||
|
mostVillages int
|
||||||
|
mostVillagesAt time.Time
|
||||||
|
lastActivityAt time.Time
|
||||||
|
createdAt time.Time
|
||||||
|
deletedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPlayerFromDatabase unmarshals Player from the database.
|
||||||
|
//
|
||||||
|
// It should be used only for unmarshalling from the database!
|
||||||
|
// You can't use UnmarshalPlayerFromDatabase as constructor - It may put domain into the invalid state!
|
||||||
|
func UnmarshalPlayerFromDatabase(
|
||||||
|
id int,
|
||||||
|
serverKey string,
|
||||||
|
name string,
|
||||||
|
numVillages int,
|
||||||
|
points int,
|
||||||
|
rank int,
|
||||||
|
tribeID int,
|
||||||
|
od OpponentsDefeated,
|
||||||
|
rawProfileURL string,
|
||||||
|
bestRank int,
|
||||||
|
bestRankAt time.Time,
|
||||||
|
mostPoints int,
|
||||||
|
mostPointsAt time.Time,
|
||||||
|
mostVillages int,
|
||||||
|
mostVillagesAt time.Time,
|
||||||
|
lastActivityAt time.Time,
|
||||||
|
createdAt time.Time,
|
||||||
|
deletedAt time.Time,
|
||||||
|
) (Player, error) {
|
||||||
|
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
|
||||||
|
return Player{}, ValidationError{
|
||||||
|
Model: playerModelName,
|
||||||
|
Field: "id",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
profileURL, err := parseURL(rawProfileURL)
|
||||||
|
if err != nil {
|
||||||
|
return Player{}, ValidationError{
|
||||||
|
Model: playerModelName,
|
||||||
|
Field: "profileURL",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Player{
|
||||||
|
id: id,
|
||||||
|
serverKey: serverKey,
|
||||||
|
name: name,
|
||||||
|
numVillages: numVillages,
|
||||||
|
points: points,
|
||||||
|
rank: rank,
|
||||||
|
tribeID: tribeID,
|
||||||
|
od: od,
|
||||||
|
profileURL: profileURL,
|
||||||
|
bestRank: bestRank,
|
||||||
|
bestRankAt: bestRankAt,
|
||||||
|
mostPoints: mostPoints,
|
||||||
|
mostPointsAt: mostPointsAt,
|
||||||
|
mostVillages: mostVillages,
|
||||||
|
mostVillagesAt: mostVillagesAt,
|
||||||
|
lastActivityAt: lastActivityAt,
|
||||||
|
createdAt: createdAt,
|
||||||
|
deletedAt: deletedAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerModelName = "Player"
|
||||||
|
|
||||||
|
func (p Player) ID() int {
|
||||||
|
return p.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) ServerKey() string {
|
||||||
|
return p.serverKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) Name() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) NumVillages() int {
|
||||||
|
return p.numVillages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) Points() int {
|
||||||
|
return p.points
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) Rank() int {
|
||||||
|
return p.rank
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) TribeID() int {
|
||||||
|
return p.tribeID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) OD() OpponentsDefeated {
|
||||||
|
return p.od
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) ProfileURL() *url.URL {
|
||||||
|
return p.profileURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) BestRank() int {
|
||||||
|
return p.bestRank
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) BestRankAt() time.Time {
|
||||||
|
return p.bestRankAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) MostPoints() int {
|
||||||
|
return p.mostPoints
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) MostPointsAt() time.Time {
|
||||||
|
return p.mostPointsAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) MostVillages() int {
|
||||||
|
return p.mostVillages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) MostVillagesAt() time.Time {
|
||||||
|
return p.mostVillagesAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) LastActivityAt() time.Time {
|
||||||
|
return p.lastActivityAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) CreatedAt() time.Time {
|
||||||
|
return p.createdAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) DeletedAt() time.Time {
|
||||||
|
return p.deletedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) Base() BasePlayer {
|
||||||
|
return BasePlayer{
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
numVillages: p.numVillages,
|
||||||
|
points: p.points,
|
||||||
|
rank: p.rank,
|
||||||
|
tribeID: p.tribeID,
|
||||||
|
od: p.od,
|
||||||
|
profileURL: p.profileURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Player) IsDeleted() bool {
|
||||||
|
return !p.deletedAt.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Players []Player
|
||||||
|
|
||||||
|
// Delete finds all tribes that are not in the given slice with active tribes and returns their ids.
|
||||||
|
// Both slices must be sorted in ascending order by ID.
|
||||||
|
func (ps Players) Delete(active BasePlayers) []int {
|
||||||
|
//nolint:prealloc
|
||||||
|
var toDelete []int
|
||||||
|
|
||||||
|
for _, p := range ps {
|
||||||
|
_, found := slices.BinarySearchFunc(active, p, func(a BasePlayer, b Player) int {
|
||||||
|
return cmp.Compare(a.ID(), b.ID())
|
||||||
|
})
|
||||||
|
if found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
toDelete = append(toDelete, p.ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
return toDelete
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePlayerParams struct {
|
||||||
|
base BasePlayer
|
||||||
|
serverKey string
|
||||||
|
bestRank int
|
||||||
|
bestRankAt time.Time
|
||||||
|
mostPoints int
|
||||||
|
mostPointsAt time.Time
|
||||||
|
mostVillages int
|
||||||
|
mostVillagesAt time.Time
|
||||||
|
lastActivityAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
const createPlayerParamsModelName = "CreatePlayerParams"
|
||||||
|
|
||||||
|
// NewCreatePlayerParams constructs a slice of CreatePlayerParams based on the given parameters.
|
||||||
|
// Both slices must be sorted in ascending order by ID
|
||||||
|
// + if storedPlayers contains tribes from different servers. they must be sorted in ascending order by server key.
|
||||||
|
//
|
||||||
|
//nolint:gocyclo
|
||||||
|
func NewCreatePlayerParams(serverKey string, players BasePlayers, storedPlayers Players) ([]CreatePlayerParams, error) {
|
||||||
|
if err := validateServerKey(serverKey); err != nil {
|
||||||
|
return nil, ValidationError{
|
||||||
|
Model: createPlayerParamsModelName,
|
||||||
|
Field: "serverKey",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params := make([]CreatePlayerParams, 0, len(players))
|
||||||
|
|
||||||
|
for i, player := range players {
|
||||||
|
if player.IsZero() {
|
||||||
|
return nil, fmt.Errorf("players[%d] is an empty struct", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
var old Player
|
||||||
|
idx, found := slices.BinarySearchFunc(storedPlayers, player, func(a Player, b BasePlayer) int {
|
||||||
|
if res := cmp.Compare(a.ServerKey(), serverKey); res != 0 {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
return cmp.Compare(a.ID(), b.ID())
|
||||||
|
})
|
||||||
|
if found {
|
||||||
|
old = storedPlayers[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
p := CreatePlayerParams{
|
||||||
|
base: player,
|
||||||
|
serverKey: serverKey,
|
||||||
|
bestRank: player.Rank(),
|
||||||
|
bestRankAt: now,
|
||||||
|
mostPoints: player.Points(),
|
||||||
|
mostPointsAt: now,
|
||||||
|
mostVillages: player.NumVillages(),
|
||||||
|
mostVillagesAt: now,
|
||||||
|
lastActivityAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if old.ID() > 0 {
|
||||||
|
if old.MostPoints() >= p.MostPoints() {
|
||||||
|
p.mostPoints = old.MostPoints()
|
||||||
|
p.mostPointsAt = old.MostPointsAt()
|
||||||
|
}
|
||||||
|
|
||||||
|
if old.MostVillages() >= p.MostVillages() {
|
||||||
|
p.mostVillages = old.MostVillages()
|
||||||
|
p.mostVillagesAt = old.MostVillagesAt()
|
||||||
|
}
|
||||||
|
|
||||||
|
if old.BestRank() <= p.BestRank() {
|
||||||
|
p.bestRank = old.BestRank()
|
||||||
|
p.bestRankAt = old.BestRankAt()
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:lll
|
||||||
|
if player.Points() <= old.Points() && player.NumVillages() <= old.NumVillages() && player.OD().ScoreAtt() <= old.OD().ScoreAtt() {
|
||||||
|
p.lastActivityAt = old.LastActivityAt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params = append(params, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CreatePlayerParams) Base() BasePlayer {
|
||||||
|
return c.base
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CreatePlayerParams) ServerKey() string {
|
||||||
|
return c.serverKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CreatePlayerParams) BestRank() int {
|
||||||
|
return c.bestRank
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CreatePlayerParams) BestRankAt() time.Time {
|
||||||
|
return c.bestRankAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CreatePlayerParams) MostPoints() int {
|
||||||
|
return c.mostPoints
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CreatePlayerParams) MostPointsAt() time.Time {
|
||||||
|
return c.mostPointsAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CreatePlayerParams) MostVillages() int {
|
||||||
|
return c.mostVillages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CreatePlayerParams) MostVillagesAt() time.Time {
|
||||||
|
return c.mostVillagesAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CreatePlayerParams) LastActivityAt() time.Time {
|
||||||
|
return c.lastActivityAt
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlayerSort uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlayerSortIDASC PlayerSort = iota + 1
|
||||||
|
PlayerSortIDDESC
|
||||||
|
PlayerSortServerKeyASC
|
||||||
|
PlayerSortServerKeyDESC
|
||||||
|
)
|
||||||
|
|
||||||
|
const PlayerListMaxLimit = 200
|
||||||
|
|
||||||
|
type ListPlayersParams struct {
|
||||||
|
ids []int
|
||||||
|
idGT NullInt
|
||||||
|
serverKeys []string
|
||||||
|
deleted NullBool
|
||||||
|
sort []PlayerSort
|
||||||
|
limit int
|
||||||
|
offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
const listPlayersParamsModelName = "ListPlayersParams"
|
||||||
|
|
||||||
|
func NewListPlayersParams() ListPlayersParams {
|
||||||
|
return ListPlayersParams{
|
||||||
|
sort: []PlayerSort{
|
||||||
|
PlayerSortServerKeyASC,
|
||||||
|
PlayerSortIDASC,
|
||||||
|
},
|
||||||
|
limit: PlayerListMaxLimit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListPlayersParams) IDs() []int {
|
||||||
|
return params.ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListPlayersParams) SetIDs(ids []int) error {
|
||||||
|
for i, id := range ids {
|
||||||
|
if err := validateIntInRange(id, 0, math.MaxInt); err != nil {
|
||||||
|
return SliceElementValidationError{
|
||||||
|
Model: listPlayersParamsModelName,
|
||||||
|
Field: "ids",
|
||||||
|
Index: i,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.ids = ids
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListPlayersParams) IDGT() NullInt {
|
||||||
|
return params.idGT
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListPlayersParams) SetIDGT(idGT NullInt) error {
|
||||||
|
if idGT.Valid {
|
||||||
|
if err := validateIntInRange(idGT.Value, 0, math.MaxInt); err != nil {
|
||||||
|
return ValidationError{
|
||||||
|
Model: listPlayersParamsModelName,
|
||||||
|
Field: "idGT",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.idGT = idGT
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListPlayersParams) ServerKeys() []string {
|
||||||
|
return params.serverKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListPlayersParams) SetServerKeys(serverKeys []string) error {
|
||||||
|
params.serverKeys = serverKeys
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListPlayersParams) Deleted() NullBool {
|
||||||
|
return params.deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListPlayersParams) SetDeleted(deleted NullBool) error {
|
||||||
|
params.deleted = deleted
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListPlayersParams) Sort() []PlayerSort {
|
||||||
|
return params.sort
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
playerSortMinLength = 1
|
||||||
|
playerSortMaxLength = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
func (params *ListPlayersParams) SetSort(sort []PlayerSort) error {
|
||||||
|
if err := validateSliceLen(sort, playerSortMinLength, playerSortMaxLength); err != nil {
|
||||||
|
return ValidationError{
|
||||||
|
Model: listPlayersParamsModelName,
|
||||||
|
Field: "sort",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.sort = sort
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListPlayersParams) Limit() int {
|
||||||
|
return params.limit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListPlayersParams) SetLimit(limit int) error {
|
||||||
|
if err := validateIntInRange(limit, 1, PlayerListMaxLimit); err != nil {
|
||||||
|
return ValidationError{
|
||||||
|
Model: listPlayersParamsModelName,
|
||||||
|
Field: "limit",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.limit = limit
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListPlayersParams) Offset() int {
|
||||||
|
return params.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListPlayersParams) SetOffset(offset int) error {
|
||||||
|
if err := validateIntInRange(offset, 0, math.MaxInt); err != nil {
|
||||||
|
return ValidationError{
|
||||||
|
Model: listPlayersParamsModelName,
|
||||||
|
Field: "offset",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.offset = offset
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,489 @@
|
||||||
|
package domain_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
|
||||||
|
"github.com/brianvoe/gofakeit/v6"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPlayers_Delete(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
server := domaintest.NewServer(t)
|
||||||
|
|
||||||
|
active := domain.BasePlayers{
|
||||||
|
domaintest.NewBasePlayer(t),
|
||||||
|
domaintest.NewBasePlayer(t),
|
||||||
|
domaintest.NewBasePlayer(t),
|
||||||
|
}
|
||||||
|
slices.SortFunc(active, func(a, b domain.BasePlayer) int {
|
||||||
|
return cmp.Compare(a.ID(), b.ID())
|
||||||
|
})
|
||||||
|
|
||||||
|
players := domain.Players{
|
||||||
|
domaintest.NewPlayer(t, func(cfg *domaintest.PlayerConfig) {
|
||||||
|
cfg.ID = active[0].ID()
|
||||||
|
cfg.ServerKey = server.Key()
|
||||||
|
}),
|
||||||
|
domaintest.NewPlayer(t, func(cfg *domaintest.PlayerConfig) {
|
||||||
|
cfg.ServerKey = server.Key()
|
||||||
|
}),
|
||||||
|
domaintest.NewPlayer(t, func(cfg *domaintest.PlayerConfig) {
|
||||||
|
cfg.ID = active[1].ID()
|
||||||
|
cfg.ServerKey = server.Key()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
expectedIDs := []int{players[1].ID()}
|
||||||
|
slices.SortFunc(players, func(a, b domain.Player) int {
|
||||||
|
if res := cmp.Compare(a.ServerKey(), b.ServerKey()); res != 0 {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
return cmp.Compare(a.ID(), b.ID())
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, expectedIDs, players.Delete(active))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewCreatePlayerParams(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
server := domaintest.NewServer(t)
|
||||||
|
|
||||||
|
players := domain.BasePlayers{
|
||||||
|
domaintest.NewBasePlayer(t),
|
||||||
|
domaintest.NewBasePlayer(t),
|
||||||
|
domaintest.NewBasePlayer(t),
|
||||||
|
}
|
||||||
|
slices.SortFunc(players, func(a, b domain.BasePlayer) int {
|
||||||
|
return cmp.Compare(a.ID(), b.ID())
|
||||||
|
})
|
||||||
|
|
||||||
|
storedPlayers := domain.Players{
|
||||||
|
// server with random server key to verify that slice order matters
|
||||||
|
domaintest.NewPlayer(t, func(cfg *domaintest.PlayerConfig) {
|
||||||
|
cfg.ID = players[0].ID()
|
||||||
|
cfg.ServerKey = domaintest.RandServerKey()
|
||||||
|
}),
|
||||||
|
// should update BestRank/MostPoints/MostVillages/LastActivityAt
|
||||||
|
domaintest.NewPlayer(t, func(cfg *domaintest.PlayerConfig) {
|
||||||
|
cfg.ID = players[0].ID()
|
||||||
|
cfg.Points = players[0].Points() - 1
|
||||||
|
cfg.NumVillages = players[0].NumVillages() - 1
|
||||||
|
cfg.OD = players[0].OD()
|
||||||
|
cfg.ServerKey = server.Key()
|
||||||
|
cfg.BestRank = players[0].Rank() + 1
|
||||||
|
cfg.BestRankAt = now.Add(-time.Hour)
|
||||||
|
cfg.MostPoints = players[0].Points() - 1
|
||||||
|
cfg.MostPointsAt = now.Add(-time.Hour)
|
||||||
|
cfg.MostVillages = players[0].NumVillages() - 1
|
||||||
|
cfg.MostVillagesAt = now.Add(-time.Hour)
|
||||||
|
cfg.LastActivityAt = now.Add(-time.Hour)
|
||||||
|
}),
|
||||||
|
// shouldn't update BestRank/MostPoints/MostVillages/LastActivityAt
|
||||||
|
domaintest.NewPlayer(t, func(cfg *domaintest.PlayerConfig) {
|
||||||
|
cfg.ID = players[1].ID()
|
||||||
|
cfg.Points = players[1].Points() + 1
|
||||||
|
cfg.NumVillages = players[1].NumVillages() + 1
|
||||||
|
cfg.OD = players[1].OD()
|
||||||
|
cfg.ServerKey = server.Key()
|
||||||
|
cfg.BestRank = players[1].Rank() - 1
|
||||||
|
cfg.BestRankAt = now.Add(-time.Hour)
|
||||||
|
cfg.MostPoints = players[1].Points() + 1
|
||||||
|
cfg.MostPointsAt = now.Add(-time.Hour)
|
||||||
|
cfg.MostVillages = players[1].NumVillages() + 1
|
||||||
|
cfg.MostVillagesAt = now.Add(-time.Hour)
|
||||||
|
cfg.LastActivityAt = now.Add(-time.Hour)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
storedPlayersSorted := slices.Clone(storedPlayers)
|
||||||
|
slices.SortFunc(storedPlayersSorted, func(a, b domain.Player) int {
|
||||||
|
if res := cmp.Compare(a.ServerKey(), b.ServerKey()); res != 0 {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
return cmp.Compare(a.ID(), b.ID())
|
||||||
|
})
|
||||||
|
|
||||||
|
expectedParams := []struct {
|
||||||
|
base domain.BasePlayer
|
||||||
|
serverKey string
|
||||||
|
bestRank int
|
||||||
|
bestRankAt time.Time
|
||||||
|
mostPoints int
|
||||||
|
mostPointsAt time.Time
|
||||||
|
mostVillages int
|
||||||
|
mostVillagesAt time.Time
|
||||||
|
lastActivityAt time.Time
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
base: players[0],
|
||||||
|
bestRank: players[0].Rank(),
|
||||||
|
bestRankAt: now,
|
||||||
|
mostPoints: players[0].Points(),
|
||||||
|
mostPointsAt: now,
|
||||||
|
mostVillages: players[0].NumVillages(),
|
||||||
|
mostVillagesAt: now,
|
||||||
|
lastActivityAt: now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: players[1],
|
||||||
|
bestRank: storedPlayers[2].BestRank(),
|
||||||
|
bestRankAt: storedPlayers[2].BestRankAt(),
|
||||||
|
mostPoints: storedPlayers[2].MostPoints(),
|
||||||
|
mostPointsAt: storedPlayers[2].MostPointsAt(),
|
||||||
|
mostVillages: storedPlayers[2].MostVillages(),
|
||||||
|
mostVillagesAt: storedPlayers[2].MostVillagesAt(),
|
||||||
|
lastActivityAt: storedPlayers[2].LastActivityAt(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
base: players[2],
|
||||||
|
bestRank: players[2].Rank(),
|
||||||
|
bestRankAt: now,
|
||||||
|
mostPoints: players[2].Points(),
|
||||||
|
mostPointsAt: now,
|
||||||
|
mostVillages: players[2].NumVillages(),
|
||||||
|
mostVillagesAt: now,
|
||||||
|
lastActivityAt: now,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := domain.NewCreatePlayerParams(server.Key(), players, storedPlayersSorted)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, res, len(expectedParams))
|
||||||
|
for i, expected := range expectedParams {
|
||||||
|
idx := slices.IndexFunc(res, func(params domain.CreatePlayerParams) bool {
|
||||||
|
return params.Base().ID() == expected.base.ID() && params.ServerKey() == server.Key()
|
||||||
|
})
|
||||||
|
require.GreaterOrEqualf(t, idx, 0, "expectedParams[%d] not found", i)
|
||||||
|
|
||||||
|
params := res[idx]
|
||||||
|
|
||||||
|
assert.Equalf(t, expected.base, params.Base(), "expectedParams[%d]", i)
|
||||||
|
assert.Equalf(t, expected.bestRank, params.BestRank(), "expectedParams[%d]", i)
|
||||||
|
assert.WithinDurationf(t, expected.bestRankAt, params.BestRankAt(), time.Minute, "expectedParams[%d]", i)
|
||||||
|
assert.Equalf(t, expected.mostPoints, params.MostPoints(), "expectedParams[%d]", i)
|
||||||
|
assert.WithinDurationf(t, expected.mostPointsAt, params.MostPointsAt(), time.Minute, "expectedParams[%d]", i)
|
||||||
|
assert.Equalf(t, expected.mostVillages, params.MostVillages(), "expectedParams[%d]", i)
|
||||||
|
assert.WithinDurationf(t, expected.mostVillagesAt, params.MostVillagesAt(), time.Minute, "expectedParams[%d]", i)
|
||||||
|
assert.WithinDurationf(t, expected.lastActivityAt, params.LastActivityAt(), time.Minute, "expectedParams[%d]", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListPlayersParams_SetIDs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
ids []int
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OK",
|
||||||
|
args: args{
|
||||||
|
ids: []int{
|
||||||
|
gofakeit.IntRange(0, math.MaxInt),
|
||||||
|
gofakeit.IntRange(0, math.MaxInt),
|
||||||
|
gofakeit.IntRange(0, math.MaxInt),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: value < 0",
|
||||||
|
args: args{
|
||||||
|
ids: []int{
|
||||||
|
gofakeit.IntRange(0, math.MaxInt),
|
||||||
|
gofakeit.IntRange(0, math.MaxInt),
|
||||||
|
gofakeit.IntRange(0, math.MaxInt),
|
||||||
|
-1,
|
||||||
|
gofakeit.IntRange(0, math.MaxInt),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErr: domain.SliceElementValidationError{
|
||||||
|
Model: "ListPlayersParams",
|
||||||
|
Field: "ids",
|
||||||
|
Index: 3,
|
||||||
|
Err: domain.MinGreaterEqualError{
|
||||||
|
Min: 0,
|
||||||
|
Current: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
params := domain.NewListPlayersParams()
|
||||||
|
|
||||||
|
require.ErrorIs(t, params.SetIDs(tt.args.ids), tt.expectedErr)
|
||||||
|
if tt.expectedErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.args.ids, params.IDs())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListPlayersParams_SetIDGT(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
idGT domain.NullInt
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OK",
|
||||||
|
args: args{
|
||||||
|
idGT: domain.NullInt{
|
||||||
|
Value: gofakeit.IntRange(0, math.MaxInt),
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: value < 0",
|
||||||
|
args: args{
|
||||||
|
idGT: domain.NullInt{
|
||||||
|
Value: -1,
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "ListPlayersParams",
|
||||||
|
Field: "idGT",
|
||||||
|
Err: domain.MinGreaterEqualError{
|
||||||
|
Min: 0,
|
||||||
|
Current: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
params := domain.NewListPlayersParams()
|
||||||
|
|
||||||
|
require.ErrorIs(t, params.SetIDGT(tt.args.idGT), tt.expectedErr)
|
||||||
|
if tt.expectedErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.args.idGT, params.IDGT())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListPlayersParams_SetSort(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
sort []domain.PlayerSort
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OK",
|
||||||
|
args: args{
|
||||||
|
sort: []domain.PlayerSort{
|
||||||
|
domain.PlayerSortIDASC,
|
||||||
|
domain.PlayerSortServerKeyASC,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: len(sort) < 1",
|
||||||
|
args: args{
|
||||||
|
sort: nil,
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "ListPlayersParams",
|
||||||
|
Field: "sort",
|
||||||
|
Err: domain.LenOutOfRangeError{
|
||||||
|
Min: 1,
|
||||||
|
Max: 2,
|
||||||
|
Current: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: len(sort) > 2",
|
||||||
|
args: args{
|
||||||
|
sort: []domain.PlayerSort{
|
||||||
|
domain.PlayerSortIDASC,
|
||||||
|
domain.PlayerSortServerKeyASC,
|
||||||
|
domain.PlayerSortServerKeyDESC,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "ListPlayersParams",
|
||||||
|
Field: "sort",
|
||||||
|
Err: domain.LenOutOfRangeError{
|
||||||
|
Min: 1,
|
||||||
|
Max: 2,
|
||||||
|
Current: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
params := domain.NewListPlayersParams()
|
||||||
|
|
||||||
|
require.ErrorIs(t, params.SetSort(tt.args.sort), tt.expectedErr)
|
||||||
|
if tt.expectedErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.args.sort, params.Sort())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListPlayersParams_SetLimit(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
limit int
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OK",
|
||||||
|
args: args{
|
||||||
|
limit: domain.PlayerListMaxLimit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: limit < 1",
|
||||||
|
args: args{
|
||||||
|
limit: 0,
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "ListPlayersParams",
|
||||||
|
Field: "limit",
|
||||||
|
Err: domain.MinGreaterEqualError{
|
||||||
|
Min: 1,
|
||||||
|
Current: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: fmt.Sprintf("ERR: limit > %d", domain.PlayerListMaxLimit),
|
||||||
|
args: args{
|
||||||
|
limit: domain.PlayerListMaxLimit + 1,
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "ListPlayersParams",
|
||||||
|
Field: "limit",
|
||||||
|
Err: domain.MaxLessEqualError{
|
||||||
|
Max: domain.PlayerListMaxLimit,
|
||||||
|
Current: domain.PlayerListMaxLimit + 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
params := domain.NewListPlayersParams()
|
||||||
|
|
||||||
|
require.ErrorIs(t, params.SetLimit(tt.args.limit), tt.expectedErr)
|
||||||
|
if tt.expectedErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.args.limit, params.Limit())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListPlayersParams_SetOffset(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OK",
|
||||||
|
args: args{
|
||||||
|
offset: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: offset < 0",
|
||||||
|
args: args{
|
||||||
|
offset: -1,
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "ListPlayersParams",
|
||||||
|
Field: "offset",
|
||||||
|
Err: domain.MinGreaterEqualError{
|
||||||
|
Min: 0,
|
||||||
|
Current: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
params := domain.NewListPlayersParams()
|
||||||
|
|
||||||
|
require.ErrorIs(t, params.SetOffset(tt.args.offset), tt.expectedErr)
|
||||||
|
if tt.expectedErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.args.offset, params.Offset())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -274,30 +274,34 @@ func NewCreateTribeParams(serverKey string, tribes BaseTribes, storedTribes Trib
|
||||||
old = storedTribes[idx]
|
old = storedTribes[idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
p := CreateTribeParams{
|
p := CreateTribeParams{
|
||||||
base: t,
|
base: t,
|
||||||
serverKey: serverKey,
|
serverKey: serverKey,
|
||||||
bestRank: old.bestRank,
|
bestRank: t.Rank(),
|
||||||
bestRankAt: old.bestRankAt,
|
bestRankAt: now,
|
||||||
mostPoints: old.mostPoints,
|
mostPoints: t.AllPoints(),
|
||||||
mostPointsAt: old.mostPointsAt,
|
mostPointsAt: now,
|
||||||
mostVillages: old.mostVillages,
|
mostVillages: t.NumVillages(),
|
||||||
mostVillagesAt: old.mostVillagesAt,
|
mostVillagesAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.AllPoints() > p.MostPoints() || old.ID() <= 0 {
|
if old.ID() > 0 {
|
||||||
p.mostPoints = t.AllPoints()
|
if old.MostPoints() >= p.MostPoints() {
|
||||||
p.mostPointsAt = time.Now()
|
p.mostPoints = old.MostPoints()
|
||||||
}
|
p.mostPointsAt = old.MostPointsAt()
|
||||||
|
}
|
||||||
|
|
||||||
if t.NumVillages() > p.MostVillages() || old.ID() <= 0 {
|
if old.MostVillages() >= p.MostVillages() {
|
||||||
p.mostVillages = t.NumVillages()
|
p.mostVillages = old.MostVillages()
|
||||||
p.mostVillagesAt = time.Now()
|
p.mostVillagesAt = old.MostVillagesAt()
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.Rank() < p.BestRank() || old.ID() <= 0 {
|
if old.BestRank() <= p.BestRank() {
|
||||||
p.bestRank = t.Rank()
|
p.bestRank = old.BestRank()
|
||||||
p.bestRankAt = time.Now()
|
p.bestRankAt = old.BestRankAt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
params = append(params, p)
|
params = append(params, p)
|
||||||
|
|
|
@ -72,13 +72,7 @@ func TestNewCreateTribeParams(t *testing.T) {
|
||||||
storedTribes := domain.Tribes{
|
storedTribes := domain.Tribes{
|
||||||
domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) {
|
domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) {
|
||||||
cfg.ID = tribes[0].ID()
|
cfg.ID = tribes[0].ID()
|
||||||
cfg.ServerKey = server.Key()[:len(server.Key())-1]
|
cfg.ServerKey = domaintest.RandServerKey()
|
||||||
cfg.BestRank = tribes[0].Rank() + 1
|
|
||||||
cfg.BestRankAt = now.Add(-time.Hour)
|
|
||||||
cfg.MostPoints = tribes[0].AllPoints() - 1
|
|
||||||
cfg.MostPointsAt = now.Add(-time.Hour)
|
|
||||||
cfg.MostVillages = tribes[0].NumVillages() - 1
|
|
||||||
cfg.MostVillagesAt = now.Add(-time.Hour)
|
|
||||||
}),
|
}),
|
||||||
domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) {
|
domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) {
|
||||||
cfg.ID = tribes[0].ID()
|
cfg.ID = tribes[0].ID()
|
||||||
|
@ -101,7 +95,8 @@ func TestNewCreateTribeParams(t *testing.T) {
|
||||||
cfg.MostVillagesAt = now.Add(-time.Hour)
|
cfg.MostVillagesAt = now.Add(-time.Hour)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
slices.SortFunc(storedTribes, func(a, b domain.Tribe) int {
|
storedTribesSorted := slices.Clone(storedTribes)
|
||||||
|
slices.SortFunc(storedTribesSorted, func(a, b domain.Tribe) int {
|
||||||
if res := cmp.Compare(a.ServerKey(), b.ServerKey()); res != 0 {
|
if res := cmp.Compare(a.ServerKey(), b.ServerKey()); res != 0 {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
@ -146,12 +141,12 @@ func TestNewCreateTribeParams(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := domain.NewCreateTribeParams(server.Key(), tribes, storedTribes)
|
res, err := domain.NewCreateTribeParams(server.Key(), tribes, storedTribesSorted)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, res, len(expectedParams))
|
assert.Len(t, res, len(expectedParams))
|
||||||
for i, expected := range expectedParams {
|
for i, expected := range expectedParams {
|
||||||
idx := slices.IndexFunc(res, func(params domain.CreateTribeParams) bool {
|
idx := slices.IndexFunc(res, func(params domain.CreateTribeParams) bool {
|
||||||
return params.Base().ID() == expected.base.ID()
|
return params.Base().ID() == expected.base.ID() && params.ServerKey() == server.Key()
|
||||||
})
|
})
|
||||||
require.GreaterOrEqualf(t, idx, 0, "expectedParams[%d] not found", i)
|
require.GreaterOrEqualf(t, idx, 0, "expectedParams[%d] not found", i)
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,9 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InvalidURLError struct {
|
|
||||||
URL string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e InvalidURLError) Error() string {
|
|
||||||
return fmt.Sprintf("%s: invalid URL", e.URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseURL(rawURL string) (*url.URL, error) {
|
func parseURL(rawURL string) (*url.URL, error) {
|
||||||
u, err := url.ParseRequestURI(rawURL)
|
u, err := url.ParseRequestURI(rawURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -193,6 +193,30 @@ func (e LenOutOfRangeError) Params() map[string]any {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InvalidURLError struct {
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ErrorWithParams = InvalidURLError{}
|
||||||
|
|
||||||
|
func (e InvalidURLError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: invalid URL", e.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e InvalidURLError) Code() ErrorCode {
|
||||||
|
return ErrorCodeIncorrectInput
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e InvalidURLError) Slug() string {
|
||||||
|
return "invalid-url"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e InvalidURLError) Params() map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"URL": e.URL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var ErrRequired error = simpleError{
|
var ErrRequired error = simpleError{
|
||||||
msg: "can't be blank",
|
msg: "can't be blank",
|
||||||
code: ErrorCodeIncorrectInput,
|
code: ErrorCodeIncorrectInput,
|
||||||
|
|
Loading…
Reference in New Issue