parent
437c99510d
commit
422fb7dcb9
|
@ -150,6 +150,42 @@ var cmdConsumer = &cli.Command{
|
||||||
)
|
)
|
||||||
consumer.Register(router)
|
consumer.Register(router)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "village",
|
||||||
|
Usage: "Run the worker responsible for consuming village-related messages",
|
||||||
|
Flags: concatSlices(dbFlags, rmqFlags, twSvcFlags),
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
return runConsumer(
|
||||||
|
c,
|
||||||
|
"VillageConsumer",
|
||||||
|
func(
|
||||||
|
c *cli.Context,
|
||||||
|
router *message.Router,
|
||||||
|
logger watermill.LoggerAdapter,
|
||||||
|
publisher *amqp.Publisher,
|
||||||
|
subscriber *amqp.Subscriber,
|
||||||
|
marshaler watermillmsg.Marshaler,
|
||||||
|
db *bun.DB,
|
||||||
|
) error {
|
||||||
|
twSvc, err := newTWServiceFromFlags(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
consumer := port.NewVillageWatermillConsumer(
|
||||||
|
app.NewVillageService(adapter.NewVillageBunRepository(db), twSvc),
|
||||||
|
subscriber,
|
||||||
|
logger,
|
||||||
|
marshaler,
|
||||||
|
c.String(rmqFlagTopicServerSyncedEvent.Name),
|
||||||
|
)
|
||||||
|
consumer.Register(router)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,6 +21,7 @@ func NewFixture(bunDB *bun.DB) *Fixture {
|
||||||
(*bunmodel.Server)(nil),
|
(*bunmodel.Server)(nil),
|
||||||
(*bunmodel.Tribe)(nil),
|
(*bunmodel.Tribe)(nil),
|
||||||
(*bunmodel.Player)(nil),
|
(*bunmodel.Player)(nil),
|
||||||
|
(*bunmodel.Village)(nil),
|
||||||
)
|
)
|
||||||
return &Fixture{
|
return &Fixture{
|
||||||
f: dbfixture.New(bunDB),
|
f: dbfixture.New(bunDB),
|
||||||
|
|
|
@ -203,12 +203,12 @@ func (t *TWHTTP) convertTribesToDomain(tribes []tw.Tribe) (domain.BaseTribes, er
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TWHTTP) GetPlayers(ctx context.Context, baseURL *url.URL) (domain.BasePlayers, error) {
|
func (t *TWHTTP) GetPlayers(ctx context.Context, baseURL *url.URL) (domain.BasePlayers, error) {
|
||||||
tribes, err := t.client.GetPlayers(ctx, baseURL)
|
players, err := t.client.GetPlayers(ctx, baseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return t.convertPlayersToDomain(tribes)
|
return t.convertPlayersToDomain(players)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TWHTTP) convertPlayersToDomain(players []tw.Player) (domain.BasePlayers, error) {
|
func (t *TWHTTP) convertPlayersToDomain(players []tw.Player) (domain.BasePlayers, error) {
|
||||||
|
@ -248,3 +248,37 @@ func (t *TWHTTP) convertPlayersToDomain(players []tw.Player) (domain.BasePlayers
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TWHTTP) GetVillages(ctx context.Context, baseURL *url.URL) (domain.BaseVillages, error) {
|
||||||
|
villages, err := t.client.GetVillages(ctx, baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.convertVillagesToDomain(villages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TWHTTP) convertVillagesToDomain(villages []tw.Village) (domain.BaseVillages, error) {
|
||||||
|
res := make(domain.BaseVillages, 0, len(villages))
|
||||||
|
|
||||||
|
for _, v := range villages {
|
||||||
|
converted, err := domain.NewBaseVillage(
|
||||||
|
v.ID,
|
||||||
|
v.Name,
|
||||||
|
v.Points,
|
||||||
|
v.X,
|
||||||
|
v.Y,
|
||||||
|
v.Continent,
|
||||||
|
v.Bonus,
|
||||||
|
v.PlayerID,
|
||||||
|
v.ProfileURL,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("couldn't construct domain.BaseVillage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, converted)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
package bunmodel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Village struct {
|
||||||
|
bun.BaseModel `bun:"table:villages,alias:village"`
|
||||||
|
|
||||||
|
ID int `bun:"id,nullzero,pk"`
|
||||||
|
ServerKey string `bun:"server_key,nullzero,pk"`
|
||||||
|
Name string `bun:"name,nullzero"`
|
||||||
|
Points int `bun:"points"`
|
||||||
|
X int `bun:"x"`
|
||||||
|
Y int `bun:"y"`
|
||||||
|
Continent string `bun:"continent"`
|
||||||
|
Bonus int `bun:"bonus"`
|
||||||
|
PlayerID int `bun:"player_id,nullzero"`
|
||||||
|
Player Player `bun:"player,rel:belongs-to,join:player_id=id,join:server_key=server_key"`
|
||||||
|
ProfileURL string `bun:"profile_url,nullzero"`
|
||||||
|
CreatedAt time.Time `bun:"created_at,nullzero"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Village) ToDomain() (domain.Village, error) {
|
||||||
|
converted, err := domain.UnmarshalVillageFromDatabase(
|
||||||
|
v.ID,
|
||||||
|
v.ServerKey,
|
||||||
|
v.Name,
|
||||||
|
v.Points,
|
||||||
|
v.X,
|
||||||
|
v.Y,
|
||||||
|
v.Continent,
|
||||||
|
v.Bonus,
|
||||||
|
v.PlayerID,
|
||||||
|
v.ProfileURL,
|
||||||
|
v.CreatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Village{}, fmt.Errorf(
|
||||||
|
"couldn't construct domain.Village (id=%d,serverKey=%s): %w",
|
||||||
|
v.ID,
|
||||||
|
v.ServerKey,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return converted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Villages []Village
|
||||||
|
|
||||||
|
func (vs Villages) ToDomain() (domain.Villages, error) {
|
||||||
|
res := make(domain.Villages, 0, len(vs))
|
||||||
|
|
||||||
|
for _, v := range vs {
|
||||||
|
converted, err := v.ToDomain()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, converted)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
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 VillageBunRepository struct {
|
||||||
|
db bun.IDB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVillageBunRepository(db bun.IDB) *VillageBunRepository {
|
||||||
|
return &VillageBunRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *VillageBunRepository) CreateOrUpdate(ctx context.Context, params ...domain.CreateVillageParams) error {
|
||||||
|
if len(params) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
villages := make(bunmodel.Villages, 0, len(params))
|
||||||
|
|
||||||
|
for _, p := range params {
|
||||||
|
base := p.Base()
|
||||||
|
villages = append(villages, bunmodel.Village{
|
||||||
|
ID: base.ID(),
|
||||||
|
ServerKey: p.ServerKey(),
|
||||||
|
Name: base.Name(),
|
||||||
|
Points: base.Points(),
|
||||||
|
X: base.X(),
|
||||||
|
Y: base.Y(),
|
||||||
|
Continent: base.Continent(),
|
||||||
|
Bonus: base.Bonus(),
|
||||||
|
PlayerID: base.PlayerID(),
|
||||||
|
ProfileURL: base.ProfileURL().String(),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
q := repo.db.NewInsert().
|
||||||
|
Model(&villages)
|
||||||
|
|
||||||
|
//nolint:exhaustive
|
||||||
|
switch q.Dialect().Name() {
|
||||||
|
case dialect.PG:
|
||||||
|
q = q.On("CONFLICT ON CONSTRAINT villages_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("points = EXCLUDED.points").
|
||||||
|
Set("x = EXCLUDED.x").
|
||||||
|
Set("y = EXCLUDED.y").
|
||||||
|
Set("continent = EXCLUDED.continent").
|
||||||
|
Set("bonus = EXCLUDED.bonus").
|
||||||
|
Set("player_id = EXCLUDED.player_id").
|
||||||
|
Set("profile_url = EXCLUDED.profile_url").
|
||||||
|
Returning("").
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return fmt.Errorf("something went wrong while inserting villages into the db: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *VillageBunRepository) List(ctx context.Context, params domain.ListVillagesParams) (domain.Villages, error) {
|
||||||
|
var villages bunmodel.Villages
|
||||||
|
|
||||||
|
if err := repo.db.NewSelect().
|
||||||
|
Model(&villages).
|
||||||
|
Apply(listVillagesParamsApplier{params: params}.apply).
|
||||||
|
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, fmt.Errorf("couldn't select villages from the db: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return villages.ToDomain()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *VillageBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := repo.db.NewDelete().
|
||||||
|
Model((*bunmodel.Village)(nil)).
|
||||||
|
Where("id IN (?)", bun.In(ids)).
|
||||||
|
Where("server_key = ?", serverKey).
|
||||||
|
Returning("").
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return fmt.Errorf("couldn't delete villages: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type listVillagesParamsApplier struct {
|
||||||
|
params domain.ListVillagesParams
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo
|
||||||
|
func (a listVillagesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
|
||||||
|
if ids := a.params.IDs(); len(ids) > 0 {
|
||||||
|
q = q.Where("village.id IN (?)", bun.In(ids))
|
||||||
|
}
|
||||||
|
|
||||||
|
if idGT := a.params.IDGT(); idGT.Valid {
|
||||||
|
q = q.Where("village.id > ?", idGT.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 {
|
||||||
|
q = q.Where("village.server_key IN (?)", bun.In(serverKeys))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range a.params.Sort() {
|
||||||
|
switch s {
|
||||||
|
case domain.VillageSortIDASC:
|
||||||
|
q = q.Order("village.id ASC")
|
||||||
|
case domain.VillageSortIDDESC:
|
||||||
|
q = q.Order("village.id DESC")
|
||||||
|
case domain.VillageSortServerKeyASC:
|
||||||
|
q = q.Order("village.server_key ASC")
|
||||||
|
case domain.VillageSortServerKeyDESC:
|
||||||
|
q = q.Order("village.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 TestVillageBunRepository_Postgres(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping long-running test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testVillageRepository(t, func(t *testing.T) repositories {
|
||||||
|
t.Helper()
|
||||||
|
return newBunDBRepositories(t, postgres.NewBunDB(t))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVillageBunRepository_SQLite(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testVillageRepository(t, func(t *testing.T) repositories {
|
||||||
|
t.Helper()
|
||||||
|
return newBunDBRepositories(t, adaptertest.NewBunDBSQLite(t))
|
||||||
|
})
|
||||||
|
}
|
|
@ -37,11 +37,18 @@ type playerRepository interface {
|
||||||
Delete(ctx context.Context, serverKey string, ids ...int) error
|
Delete(ctx context.Context, serverKey string, ids ...int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type villageRepository interface {
|
||||||
|
CreateOrUpdate(ctx context.Context, params ...domain.CreateVillageParams) error
|
||||||
|
List(ctx context.Context, params domain.ListVillagesParams) (domain.Villages, 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
|
player playerRepository
|
||||||
|
village villageRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBunDBRepositories(tb testing.TB, bunDB *bun.DB) repositories {
|
func newBunDBRepositories(tb testing.TB, bunDB *bun.DB) repositories {
|
||||||
|
@ -54,5 +61,6 @@ func newBunDBRepositories(tb testing.TB, bunDB *bun.DB) repositories {
|
||||||
server: adapter.NewServerBunRepository(bunDB),
|
server: adapter.NewServerBunRepository(bunDB),
|
||||||
tribe: adapter.NewTribeBunRepository(bunDB),
|
tribe: adapter.NewTribeBunRepository(bunDB),
|
||||||
player: adapter.NewPlayerBunRepository(bunDB),
|
player: adapter.NewPlayerBunRepository(bunDB),
|
||||||
|
village: adapter.NewVillageBunRepository(bunDB),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,317 @@
|
||||||
|
package adapter_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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 testVillageRepository(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.CreateVillageParams) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
require.NotEmpty(t, params)
|
||||||
|
|
||||||
|
ids := make([]int, 0, len(params))
|
||||||
|
for _, p := range params {
|
||||||
|
ids = append(ids, p.Base().ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
listParams := domain.NewListVillagesParams()
|
||||||
|
require.NoError(t, listParams.SetIDs(ids))
|
||||||
|
require.NoError(t, listParams.SetServerKeys([]string{params[0].ServerKey()}))
|
||||||
|
|
||||||
|
villages, err := repos.village.List(ctx, listParams)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, villages, len(params))
|
||||||
|
for i, p := range params {
|
||||||
|
idx := slices.IndexFunc(villages, func(village domain.Village) bool {
|
||||||
|
return village.ID() == p.Base().ID() && village.ServerKey() == p.ServerKey()
|
||||||
|
})
|
||||||
|
require.GreaterOrEqualf(t, idx, 0, "params[%d]", i)
|
||||||
|
village := villages[idx]
|
||||||
|
|
||||||
|
assert.Equalf(t, p.Base(), village.Base(), "params[%d]", i)
|
||||||
|
assert.Equalf(t, p.ServerKey(), village.ServerKey(), "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]
|
||||||
|
|
||||||
|
villagesToCreate := domain.BaseVillages{
|
||||||
|
domaintest.NewBaseVillage(t),
|
||||||
|
domaintest.NewBaseVillage(t),
|
||||||
|
}
|
||||||
|
|
||||||
|
createParams, err := domain.NewCreateVillageParams(server.Key(), villagesToCreate)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, repos.village.CreateOrUpdate(ctx, createParams...))
|
||||||
|
assertCreatedUpdated(t, createParams)
|
||||||
|
|
||||||
|
villagesToUpdate := domain.BaseVillages{
|
||||||
|
domaintest.NewBaseVillage(t, func(cfg *domaintest.BaseVillageConfig) {
|
||||||
|
cfg.ID = villagesToCreate[0].ID()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
updateParams, err := domain.NewCreateVillageParams(server.Key(), villagesToUpdate)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, repos.village.CreateOrUpdate(ctx, updateParams...))
|
||||||
|
assertCreatedUpdated(t, updateParams)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OK: len(params) == 0", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
require.NoError(t, repos.village.CreateOrUpdate(ctx))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("List & ListCount", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
repos := newRepos(t)
|
||||||
|
|
||||||
|
villages, listVillagesErr := repos.village.List(ctx, domain.NewListVillagesParams())
|
||||||
|
require.NoError(t, listVillagesErr)
|
||||||
|
require.NotEmpty(t, villages)
|
||||||
|
randVillage := villages[0]
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
params func(t *testing.T) domain.ListVillagesParams
|
||||||
|
assertVillages func(t *testing.T, params domain.ListVillagesParams, villages domain.Villages)
|
||||||
|
assertError func(t *testing.T, err error)
|
||||||
|
assertTotal func(t *testing.T, params domain.ListVillagesParams, total int)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OK: default params",
|
||||||
|
params: func(t *testing.T) domain.ListVillagesParams {
|
||||||
|
t.Helper()
|
||||||
|
return domain.NewListVillagesParams()
|
||||||
|
},
|
||||||
|
assertVillages: func(t *testing.T, params domain.ListVillagesParams, villages domain.Villages) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, len(villages))
|
||||||
|
assert.True(t, slices.IsSortedFunc(villages, func(a, b domain.Village) 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.ListVillagesParams, total int) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, total)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OK: sort=[serverKey DESC, id DESC]",
|
||||||
|
params: func(t *testing.T) domain.ListVillagesParams {
|
||||||
|
t.Helper()
|
||||||
|
params := domain.NewListVillagesParams()
|
||||||
|
require.NoError(t, params.SetSort([]domain.VillageSort{domain.VillageSortServerKeyDESC, domain.VillageSortIDDESC}))
|
||||||
|
return params
|
||||||
|
},
|
||||||
|
assertVillages: func(t *testing.T, params domain.ListVillagesParams, villages domain.Villages) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, len(villages))
|
||||||
|
assert.True(t, slices.IsSortedFunc(villages, func(a, b domain.Village) 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.ListVillagesParams, total int) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, total)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: fmt.Sprintf("OK: ids=[%d] serverKeys=[%s]", randVillage.ID(), randVillage.ServerKey()),
|
||||||
|
params: func(t *testing.T) domain.ListVillagesParams {
|
||||||
|
t.Helper()
|
||||||
|
params := domain.NewListVillagesParams()
|
||||||
|
require.NoError(t, params.SetIDs([]int{randVillage.ID()}))
|
||||||
|
require.NoError(t, params.SetServerKeys([]string{randVillage.ServerKey()}))
|
||||||
|
return params
|
||||||
|
},
|
||||||
|
assertVillages: func(t *testing.T, params domain.ListVillagesParams, villages domain.Villages) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ids := params.IDs()
|
||||||
|
serverKeys := params.ServerKeys()
|
||||||
|
|
||||||
|
for _, v := range villages {
|
||||||
|
assert.True(t, slices.Contains(ids, v.ID()))
|
||||||
|
assert.True(t, slices.Contains(serverKeys, v.ServerKey()))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
assertError: func(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
assertTotal: func(t *testing.T, params domain.ListVillagesParams, total int) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, total)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: fmt.Sprintf("OK: idGT=%d", randVillage.ID()),
|
||||||
|
params: func(t *testing.T) domain.ListVillagesParams {
|
||||||
|
t.Helper()
|
||||||
|
params := domain.NewListVillagesParams()
|
||||||
|
require.NoError(t, params.SetIDGT(domain.NullInt{
|
||||||
|
Value: randVillage.ID(),
|
||||||
|
Valid: true,
|
||||||
|
}))
|
||||||
|
return params
|
||||||
|
},
|
||||||
|
assertVillages: func(t *testing.T, params domain.ListVillagesParams, villages domain.Villages) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, villages)
|
||||||
|
for _, v := range villages {
|
||||||
|
assert.Greater(t, v.ID(), params.IDGT().Value, v.ID())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
assertError: func(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
assertTotal: func(t *testing.T, params domain.ListVillagesParams, total int) {
|
||||||
|
t.Helper()
|
||||||
|
assert.NotEmpty(t, total)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OK: offset=1 limit=2",
|
||||||
|
params: func(t *testing.T) domain.ListVillagesParams {
|
||||||
|
t.Helper()
|
||||||
|
params := domain.NewListVillagesParams()
|
||||||
|
require.NoError(t, params.SetOffset(1))
|
||||||
|
require.NoError(t, params.SetLimit(2))
|
||||||
|
return params
|
||||||
|
},
|
||||||
|
assertVillages: func(t *testing.T, params domain.ListVillagesParams, villages domain.Villages) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Len(t, villages, params.Limit())
|
||||||
|
},
|
||||||
|
assertError: func(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
assertTotal: func(t *testing.T, params domain.ListVillagesParams, 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.village.List(ctx, params)
|
||||||
|
tt.assertError(t, err)
|
||||||
|
tt.assertVillages(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())
|
||||||
|
}
|
||||||
|
|
||||||
|
listVillagesParams := domain.NewListVillagesParams()
|
||||||
|
require.NoError(t, listVillagesParams.SetServerKeys(serverKeys))
|
||||||
|
|
||||||
|
villagesBeforeDelete, err := repos.village.List(ctx, listVillagesParams)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var serverKey string
|
||||||
|
var ids []int
|
||||||
|
|
||||||
|
for _, v := range villagesBeforeDelete {
|
||||||
|
if serverKey == "" {
|
||||||
|
serverKey = v.ServerKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.ServerKey() == serverKey {
|
||||||
|
ids = append(ids, v.ID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idsToDelete := ids[:int(math.Ceil(float64(len(ids))/2))]
|
||||||
|
|
||||||
|
require.NoError(t, repos.village.Delete(ctx, serverKey, idsToDelete...))
|
||||||
|
|
||||||
|
villagesAfterDelete, err := repos.village.List(ctx, listVillagesParams)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, villagesAfterDelete, len(villagesBeforeDelete)-len(idsToDelete))
|
||||||
|
for _, v := range villagesAfterDelete {
|
||||||
|
if v.ServerKey() == serverKey && slices.Contains(ids, v.ID()) {
|
||||||
|
assert.False(t, slices.Contains(idsToDelete, v.ID()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OK: len(ids) == 0", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
require.NoError(t, repos.village.Delete(ctx, servers[0].Key()))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -7202,3 +7202,89 @@
|
||||||
tribe_id: 2
|
tribe_id: 2
|
||||||
created_at: 2021-09-17T09:01:00Z
|
created_at: 2021-09-17T09:01:00Z
|
||||||
profile_url: http://www.dynamicbleeding-edge.info/synergies/dynamic/productize/relationships
|
profile_url: http://www.dynamicbleeding-edge.info/synergies/dynamic/productize/relationships
|
||||||
|
- model: Village
|
||||||
|
rows:
|
||||||
|
- _id: pl169-village-1
|
||||||
|
id: 1111
|
||||||
|
server_key: pl169
|
||||||
|
name: Village 1
|
||||||
|
points: 12154
|
||||||
|
x: 327
|
||||||
|
y: 428
|
||||||
|
continent: K43
|
||||||
|
bonus: 0
|
||||||
|
player_id: 699783765
|
||||||
|
created_at: 2021-09-17T09:01:00.000Z
|
||||||
|
profile_url: https://pl169.plemiona.pl/game.php?screen=info_village&id=1111
|
||||||
|
- _id: pl169-village-2
|
||||||
|
id: 1112
|
||||||
|
server_key: pl169
|
||||||
|
name: Village 2
|
||||||
|
points: 12154
|
||||||
|
x: 531
|
||||||
|
y: 448
|
||||||
|
continent: K45
|
||||||
|
bonus: 1
|
||||||
|
player_id: 699783765
|
||||||
|
created_at: 2021-09-19T09:01:00.000Z
|
||||||
|
profile_url: https://pl169.plemiona.pl/game.php?screen=info_village&id=1112
|
||||||
|
- _id: pl169-village-3
|
||||||
|
id: 1113
|
||||||
|
server_key: pl169
|
||||||
|
name: Village 3
|
||||||
|
points: 2500
|
||||||
|
x: 532
|
||||||
|
y: 448
|
||||||
|
continent: K45
|
||||||
|
bonus: 0
|
||||||
|
player_id: 0
|
||||||
|
created_at: 2021-09-30T15:11:00.000Z
|
||||||
|
profile_url: https://pl169.plemiona.pl/game.php?screen=info_village&id=1113
|
||||||
|
- _id: pl169-village-4
|
||||||
|
id: 1114
|
||||||
|
server_key: pl169
|
||||||
|
name: Village 4
|
||||||
|
points: 2500
|
||||||
|
x: 533
|
||||||
|
y: 448
|
||||||
|
continent: K45
|
||||||
|
bonus: 4
|
||||||
|
player_id: 0
|
||||||
|
created_at: 2021-09-29T05:11:00.000Z
|
||||||
|
profile_url: https://pl169.plemiona.pl/game.php?screen=info_village&id=1114
|
||||||
|
- _id: it70-village-1
|
||||||
|
id: 10022
|
||||||
|
server_key: it70
|
||||||
|
name: Village 1
|
||||||
|
points: 12154
|
||||||
|
x: 533
|
||||||
|
y: 548
|
||||||
|
continent: K55
|
||||||
|
bonus: 4
|
||||||
|
player_id: 578014
|
||||||
|
created_at: 2022-02-21T18:00:10.000Z
|
||||||
|
profile_url: https://it70.tribals.it/game.php?screen=info_village&id=10022
|
||||||
|
- _id: it70-village-2
|
||||||
|
id: 10023
|
||||||
|
server_key: it70
|
||||||
|
name: Village 2
|
||||||
|
points: 12154
|
||||||
|
x: 633
|
||||||
|
y: 548
|
||||||
|
continent: K56
|
||||||
|
bonus: 0
|
||||||
|
player_id: 578014
|
||||||
|
created_at: 2022-02-22T15:00:10.000Z
|
||||||
|
profile_url: https://it70.tribals.it/game.php?screen=info_village&id=10023
|
||||||
|
- _id: it70-village-3
|
||||||
|
id: 10024
|
||||||
|
server_key: it70
|
||||||
|
name: Village 3
|
||||||
|
points: 12154
|
||||||
|
x: 100
|
||||||
|
y: 100
|
||||||
|
continent: K11
|
||||||
|
bonus: 0
|
||||||
|
player_id: 0
|
||||||
|
created_at: 2022-02-25T15:00:10.000Z
|
||||||
|
profile_url: https://it70.tribals.it/game.php?screen=info_village&id=10024
|
||||||
|
|
|
@ -14,4 +14,5 @@ type TWService interface {
|
||||||
GetBuildingInfo(ctx context.Context, baseURL *url.URL) (domain.BuildingInfo, error)
|
GetBuildingInfo(ctx context.Context, baseURL *url.URL) (domain.BuildingInfo, error)
|
||||||
GetTribes(ctx context.Context, baseURL *url.URL) (domain.BaseTribes, error)
|
GetTribes(ctx context.Context, baseURL *url.URL) (domain.BaseTribes, error)
|
||||||
GetPlayers(ctx context.Context, baseURL *url.URL) (domain.BasePlayers, error)
|
GetPlayers(ctx context.Context, baseURL *url.URL) (domain.BasePlayers, error)
|
||||||
|
GetVillages(ctx context.Context, baseURL *url.URL) (domain.BaseVillages, error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VillageRepository interface {
|
||||||
|
CreateOrUpdate(ctx context.Context, params ...domain.CreateVillageParams) error
|
||||||
|
List(ctx context.Context, params domain.ListVillagesParams) (domain.Villages, error)
|
||||||
|
Delete(ctx context.Context, serverKey string, ids ...int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type VillageService struct {
|
||||||
|
repo VillageRepository
|
||||||
|
twSvc TWService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVillageService(repo VillageRepository, twSvc TWService) *VillageService {
|
||||||
|
return &VillageService{repo: repo, twSvc: twSvc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *VillageService) Sync(ctx context.Context, serverSyncedPayload domain.ServerSyncedEventPayload) error {
|
||||||
|
serverKey := serverSyncedPayload.Key()
|
||||||
|
serverURL := serverSyncedPayload.URL()
|
||||||
|
|
||||||
|
villages, err := svc.twSvc.GetVillages(ctx, serverURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: couldn't get villages: %w", serverKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = svc.createOrUpdate(ctx, serverKey, villages); err != nil {
|
||||||
|
return fmt.Errorf("%s: couldn't create/update villages: %w", serverKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = svc.delete(ctx, serverKey, villages); err != nil {
|
||||||
|
return fmt.Errorf("%s: couldn't delete villages: %w", serverKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const villageCreateOrUpdateChunkSize = domain.VillageListMaxLimit
|
||||||
|
|
||||||
|
func (svc *VillageService) createOrUpdate(ctx context.Context, serverKey string, villages domain.BaseVillages) error {
|
||||||
|
for i := 0; i < len(villages); i += villageCreateOrUpdateChunkSize {
|
||||||
|
end := i + villageCreateOrUpdateChunkSize
|
||||||
|
if end > len(villages) {
|
||||||
|
end = len(villages)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := svc.createOrUpdateChunk(ctx, serverKey, villages[i:end]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *VillageService) createOrUpdateChunk(
|
||||||
|
ctx context.Context,
|
||||||
|
serverKey string,
|
||||||
|
villages domain.BaseVillages,
|
||||||
|
) error {
|
||||||
|
createParams, err := domain.NewCreateVillageParams(serverKey, villages)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc.repo.CreateOrUpdate(ctx, createParams...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *VillageService) delete(ctx context.Context, serverKey string, villages domain.BaseVillages) error {
|
||||||
|
listParams := domain.NewListVillagesParams()
|
||||||
|
if err := listParams.SetServerKeys([]string{serverKey}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := listParams.SetSort([]domain.VillageSort{domain.VillageSortIDASC}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := listParams.SetLimit(domain.VillageListMaxLimit); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var toDelete []int
|
||||||
|
|
||||||
|
for {
|
||||||
|
storedVillages, err := svc.repo.List(ctx, listParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(storedVillages) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
toDelete = append(toDelete, storedVillages.Delete(villages)...)
|
||||||
|
|
||||||
|
if err = listParams.SetIDGT(domain.NullInt{
|
||||||
|
Value: storedVillages[len(storedVillages)-1].ID(),
|
||||||
|
Valid: true,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc.repo.Delete(ctx, serverKey, toDelete...)
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BaseVillage struct {
|
||||||
|
id int
|
||||||
|
name string
|
||||||
|
points int
|
||||||
|
x int
|
||||||
|
y int
|
||||||
|
continent string
|
||||||
|
bonus int
|
||||||
|
playerID int
|
||||||
|
profileURL *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseVillageModelName = "BaseVillage"
|
||||||
|
|
||||||
|
func NewBaseVillage(
|
||||||
|
id int,
|
||||||
|
name string,
|
||||||
|
points int,
|
||||||
|
x, y int,
|
||||||
|
continent string,
|
||||||
|
bonus int,
|
||||||
|
playerID int,
|
||||||
|
profileURL *url.URL,
|
||||||
|
) (BaseVillage, error) {
|
||||||
|
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
|
||||||
|
return BaseVillage{}, ValidationError{
|
||||||
|
Model: baseVillageModelName,
|
||||||
|
Field: "id",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateStringLen(name, villageNameMinLength, villageNameMaxLength); err != nil {
|
||||||
|
return BaseVillage{}, ValidationError{
|
||||||
|
Model: baseVillageModelName,
|
||||||
|
Field: "name",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateIntInRange(points, 1, math.MaxInt); err != nil {
|
||||||
|
return BaseVillage{}, ValidationError{
|
||||||
|
Model: baseVillageModelName,
|
||||||
|
Field: "points",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateIntInRange(x, 0, math.MaxInt); err != nil {
|
||||||
|
return BaseVillage{}, ValidationError{
|
||||||
|
Model: baseVillageModelName,
|
||||||
|
Field: "x",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateIntInRange(y, 0, math.MaxInt); err != nil {
|
||||||
|
return BaseVillage{}, ValidationError{
|
||||||
|
Model: baseVillageModelName,
|
||||||
|
Field: "y",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateStringLen(continent, villageContinentMinLength, villageContinentMaxLength); err != nil {
|
||||||
|
return BaseVillage{}, ValidationError{
|
||||||
|
Model: baseVillageModelName,
|
||||||
|
Field: "continent",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateIntInRange(bonus, 0, math.MaxInt); err != nil {
|
||||||
|
return BaseVillage{}, ValidationError{
|
||||||
|
Model: baseVillageModelName,
|
||||||
|
Field: "bonus",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateIntInRange(playerID, 0, math.MaxInt); err != nil {
|
||||||
|
return BaseVillage{}, ValidationError{
|
||||||
|
Model: baseVillageModelName,
|
||||||
|
Field: "playerID",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if profileURL == nil {
|
||||||
|
return BaseVillage{}, ValidationError{
|
||||||
|
Model: baseVillageModelName,
|
||||||
|
Field: "profileURL",
|
||||||
|
Err: ErrNil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BaseVillage{
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
points: points,
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
continent: continent,
|
||||||
|
bonus: bonus,
|
||||||
|
playerID: playerID,
|
||||||
|
profileURL: profileURL,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v BaseVillage) ID() int {
|
||||||
|
return v.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v BaseVillage) Name() string {
|
||||||
|
return v.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v BaseVillage) Points() int {
|
||||||
|
return v.points
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v BaseVillage) X() int {
|
||||||
|
return v.x
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v BaseVillage) Y() int {
|
||||||
|
return v.y
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v BaseVillage) Continent() string {
|
||||||
|
return v.continent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v BaseVillage) Bonus() int {
|
||||||
|
return v.bonus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v BaseVillage) PlayerID() int {
|
||||||
|
return v.playerID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v BaseVillage) ProfileURL() *url.URL {
|
||||||
|
return v.profileURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v BaseVillage) IsZero() bool {
|
||||||
|
return v == BaseVillage{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseVillages []BaseVillage
|
|
@ -0,0 +1,327 @@
|
||||||
|
package domain_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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 TestNewBaseVillage(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
validBaseVillage := domaintest.NewBaseVillage(t)
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
id int
|
||||||
|
name string
|
||||||
|
points int
|
||||||
|
x int
|
||||||
|
y int
|
||||||
|
continent string
|
||||||
|
bonus int
|
||||||
|
playerID int
|
||||||
|
profileURL *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OK",
|
||||||
|
args: args{
|
||||||
|
id: validBaseVillage.ID(),
|
||||||
|
name: validBaseVillage.Name(),
|
||||||
|
points: validBaseVillage.Points(),
|
||||||
|
x: validBaseVillage.X(),
|
||||||
|
y: validBaseVillage.Y(),
|
||||||
|
continent: validBaseVillage.Continent(),
|
||||||
|
bonus: validBaseVillage.Bonus(),
|
||||||
|
playerID: validBaseVillage.PlayerID(),
|
||||||
|
profileURL: validBaseVillage.ProfileURL(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: id < 1",
|
||||||
|
args: args{
|
||||||
|
id: 0,
|
||||||
|
name: validBaseVillage.Name(),
|
||||||
|
points: validBaseVillage.Points(),
|
||||||
|
x: validBaseVillage.X(),
|
||||||
|
y: validBaseVillage.Y(),
|
||||||
|
continent: validBaseVillage.Continent(),
|
||||||
|
bonus: validBaseVillage.Bonus(),
|
||||||
|
playerID: validBaseVillage.PlayerID(),
|
||||||
|
profileURL: validBaseVillage.ProfileURL(),
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "BaseVillage",
|
||||||
|
Field: "id",
|
||||||
|
Err: domain.MinGreaterEqualError{
|
||||||
|
Min: 1,
|
||||||
|
Current: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: len(name) < 1",
|
||||||
|
args: args{
|
||||||
|
id: validBaseVillage.ID(),
|
||||||
|
name: "",
|
||||||
|
points: validBaseVillage.Points(),
|
||||||
|
x: validBaseVillage.X(),
|
||||||
|
y: validBaseVillage.Y(),
|
||||||
|
continent: validBaseVillage.Continent(),
|
||||||
|
bonus: validBaseVillage.Bonus(),
|
||||||
|
playerID: validBaseVillage.PlayerID(),
|
||||||
|
profileURL: validBaseVillage.ProfileURL(),
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "BaseVillage",
|
||||||
|
Field: "name",
|
||||||
|
Err: domain.LenOutOfRangeError{
|
||||||
|
Min: 1,
|
||||||
|
Max: 150,
|
||||||
|
Current: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: len(name) > 150",
|
||||||
|
args: args{
|
||||||
|
id: validBaseVillage.ID(),
|
||||||
|
name: gofakeit.LetterN(151),
|
||||||
|
points: validBaseVillage.Points(),
|
||||||
|
x: validBaseVillage.X(),
|
||||||
|
y: validBaseVillage.Y(),
|
||||||
|
continent: validBaseVillage.Continent(),
|
||||||
|
bonus: validBaseVillage.Bonus(),
|
||||||
|
playerID: validBaseVillage.PlayerID(),
|
||||||
|
profileURL: validBaseVillage.ProfileURL(),
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "BaseVillage",
|
||||||
|
Field: "name",
|
||||||
|
Err: domain.LenOutOfRangeError{
|
||||||
|
Min: 1,
|
||||||
|
Max: 150,
|
||||||
|
Current: 151,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: points < 1",
|
||||||
|
args: args{
|
||||||
|
id: validBaseVillage.ID(),
|
||||||
|
name: validBaseVillage.Name(),
|
||||||
|
points: 0,
|
||||||
|
x: validBaseVillage.X(),
|
||||||
|
y: validBaseVillage.Y(),
|
||||||
|
continent: validBaseVillage.Continent(),
|
||||||
|
bonus: validBaseVillage.Bonus(),
|
||||||
|
playerID: validBaseVillage.PlayerID(),
|
||||||
|
profileURL: validBaseVillage.ProfileURL(),
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "BaseVillage",
|
||||||
|
Field: "points",
|
||||||
|
Err: domain.MinGreaterEqualError{
|
||||||
|
Min: 1,
|
||||||
|
Current: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: x < 0",
|
||||||
|
args: args{
|
||||||
|
id: validBaseVillage.ID(),
|
||||||
|
name: validBaseVillage.Name(),
|
||||||
|
points: validBaseVillage.Points(),
|
||||||
|
x: -1,
|
||||||
|
y: validBaseVillage.Y(),
|
||||||
|
continent: validBaseVillage.Continent(),
|
||||||
|
bonus: validBaseVillage.Bonus(),
|
||||||
|
playerID: validBaseVillage.PlayerID(),
|
||||||
|
profileURL: validBaseVillage.ProfileURL(),
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "BaseVillage",
|
||||||
|
Field: "x",
|
||||||
|
Err: domain.MinGreaterEqualError{
|
||||||
|
Min: 0,
|
||||||
|
Current: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: y < 0",
|
||||||
|
args: args{
|
||||||
|
id: validBaseVillage.ID(),
|
||||||
|
name: validBaseVillage.Name(),
|
||||||
|
points: validBaseVillage.Points(),
|
||||||
|
x: validBaseVillage.X(),
|
||||||
|
y: -1,
|
||||||
|
continent: validBaseVillage.Continent(),
|
||||||
|
bonus: validBaseVillage.Bonus(),
|
||||||
|
playerID: validBaseVillage.PlayerID(),
|
||||||
|
profileURL: validBaseVillage.ProfileURL(),
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "BaseVillage",
|
||||||
|
Field: "y",
|
||||||
|
Err: domain.MinGreaterEqualError{
|
||||||
|
Min: 0,
|
||||||
|
Current: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: len(continent) < 1",
|
||||||
|
args: args{
|
||||||
|
id: validBaseVillage.ID(),
|
||||||
|
name: validBaseVillage.Name(),
|
||||||
|
points: validBaseVillage.Points(),
|
||||||
|
x: validBaseVillage.X(),
|
||||||
|
y: validBaseVillage.Y(),
|
||||||
|
continent: "",
|
||||||
|
bonus: validBaseVillage.Bonus(),
|
||||||
|
playerID: validBaseVillage.PlayerID(),
|
||||||
|
profileURL: validBaseVillage.ProfileURL(),
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "BaseVillage",
|
||||||
|
Field: "continent",
|
||||||
|
Err: domain.LenOutOfRangeError{
|
||||||
|
Min: 1,
|
||||||
|
Max: 5,
|
||||||
|
Current: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: len(continent) > 5",
|
||||||
|
args: args{
|
||||||
|
id: validBaseVillage.ID(),
|
||||||
|
name: validBaseVillage.Name(),
|
||||||
|
points: validBaseVillage.Points(),
|
||||||
|
x: validBaseVillage.X(),
|
||||||
|
y: validBaseVillage.Y(),
|
||||||
|
continent: gofakeit.LetterN(6),
|
||||||
|
bonus: validBaseVillage.Bonus(),
|
||||||
|
playerID: validBaseVillage.PlayerID(),
|
||||||
|
profileURL: validBaseVillage.ProfileURL(),
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "BaseVillage",
|
||||||
|
Field: "continent",
|
||||||
|
Err: domain.LenOutOfRangeError{
|
||||||
|
Min: 1,
|
||||||
|
Max: 5,
|
||||||
|
Current: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: bonus < 0",
|
||||||
|
args: args{
|
||||||
|
id: validBaseVillage.ID(),
|
||||||
|
name: validBaseVillage.Name(),
|
||||||
|
points: validBaseVillage.Points(),
|
||||||
|
x: validBaseVillage.X(),
|
||||||
|
y: validBaseVillage.Y(),
|
||||||
|
continent: validBaseVillage.Continent(),
|
||||||
|
bonus: -1,
|
||||||
|
playerID: validBaseVillage.PlayerID(),
|
||||||
|
profileURL: validBaseVillage.ProfileURL(),
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "BaseVillage",
|
||||||
|
Field: "bonus",
|
||||||
|
Err: domain.MinGreaterEqualError{
|
||||||
|
Min: 0,
|
||||||
|
Current: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: playerID < 0",
|
||||||
|
args: args{
|
||||||
|
id: validBaseVillage.ID(),
|
||||||
|
name: validBaseVillage.Name(),
|
||||||
|
points: validBaseVillage.Points(),
|
||||||
|
x: validBaseVillage.X(),
|
||||||
|
y: validBaseVillage.Y(),
|
||||||
|
continent: validBaseVillage.Continent(),
|
||||||
|
bonus: validBaseVillage.Bonus(),
|
||||||
|
playerID: -1,
|
||||||
|
profileURL: validBaseVillage.ProfileURL(),
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "BaseVillage",
|
||||||
|
Field: "playerID",
|
||||||
|
Err: domain.MinGreaterEqualError{
|
||||||
|
Min: 0,
|
||||||
|
Current: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: profileURL can't be nil",
|
||||||
|
args: args{
|
||||||
|
id: validBaseVillage.ID(),
|
||||||
|
name: validBaseVillage.Name(),
|
||||||
|
points: validBaseVillage.Points(),
|
||||||
|
x: validBaseVillage.X(),
|
||||||
|
y: validBaseVillage.Y(),
|
||||||
|
continent: validBaseVillage.Continent(),
|
||||||
|
bonus: validBaseVillage.Bonus(),
|
||||||
|
playerID: validBaseVillage.PlayerID(),
|
||||||
|
profileURL: nil,
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "BaseVillage",
|
||||||
|
Field: "profileURL",
|
||||||
|
Err: domain.ErrNil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
res, err := domain.NewBaseVillage(
|
||||||
|
tt.args.id,
|
||||||
|
tt.args.name,
|
||||||
|
tt.args.points,
|
||||||
|
tt.args.x,
|
||||||
|
tt.args.y,
|
||||||
|
tt.args.continent,
|
||||||
|
tt.args.bonus,
|
||||||
|
tt.args.playerID,
|
||||||
|
tt.args.profileURL,
|
||||||
|
)
|
||||||
|
require.ErrorIs(t, err, tt.expectedErr)
|
||||||
|
if tt.expectedErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.args.id, res.ID())
|
||||||
|
assert.Equal(t, tt.args.name, res.Name())
|
||||||
|
assert.Equal(t, tt.args.points, res.Points())
|
||||||
|
assert.Equal(t, tt.args.x, res.X())
|
||||||
|
assert.Equal(t, tt.args.y, res.Y())
|
||||||
|
assert.Equal(t, tt.args.continent, res.Continent())
|
||||||
|
assert.Equal(t, tt.args.bonus, res.Bonus())
|
||||||
|
assert.Equal(t, tt.args.playerID, res.PlayerID())
|
||||||
|
assert.Equal(t, tt.args.profileURL, res.ProfileURL())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package domaintest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||||
|
"github.com/brianvoe/gofakeit/v6"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BaseVillageConfig struct {
|
||||||
|
ID int
|
||||||
|
PlayerID int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBaseVillage(tb TestingTB, opts ...func(cfg *BaseVillageConfig)) domain.BaseVillage {
|
||||||
|
tb.Helper()
|
||||||
|
|
||||||
|
cfg := &BaseVillageConfig{
|
||||||
|
ID: RandID(),
|
||||||
|
PlayerID: gofakeit.IntRange(0, 10000),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.ParseRequestURI(gofakeit.URL())
|
||||||
|
require.NoError(tb, err)
|
||||||
|
|
||||||
|
v, err := domain.NewBaseVillage(
|
||||||
|
cfg.ID,
|
||||||
|
gofakeit.LetterN(50),
|
||||||
|
gofakeit.IntRange(1, 10000),
|
||||||
|
gofakeit.IntRange(1, 1000),
|
||||||
|
gofakeit.IntRange(1, 1000),
|
||||||
|
gofakeit.LetterN(3),
|
||||||
|
0,
|
||||||
|
cfg.PlayerID,
|
||||||
|
u,
|
||||||
|
)
|
||||||
|
require.NoError(tb, err)
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package domaintest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||||
|
"github.com/brianvoe/gofakeit/v6"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VillageConfig struct {
|
||||||
|
ID int
|
||||||
|
ServerKey string
|
||||||
|
PlayerID int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVillage(tb TestingTB, opts ...func(cfg *VillageConfig)) domain.Village {
|
||||||
|
tb.Helper()
|
||||||
|
|
||||||
|
cfg := &VillageConfig{
|
||||||
|
ID: RandID(),
|
||||||
|
ServerKey: RandServerKey(),
|
||||||
|
PlayerID: gofakeit.IntRange(0, 10000),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := domain.UnmarshalVillageFromDatabase(
|
||||||
|
cfg.ID,
|
||||||
|
cfg.ServerKey,
|
||||||
|
gofakeit.LetterN(50),
|
||||||
|
gofakeit.IntRange(1, 10000),
|
||||||
|
gofakeit.IntRange(1, 1000),
|
||||||
|
gofakeit.IntRange(1, 1000),
|
||||||
|
gofakeit.LetterN(3),
|
||||||
|
0,
|
||||||
|
cfg.PlayerID,
|
||||||
|
gofakeit.URL(),
|
||||||
|
time.Now(),
|
||||||
|
)
|
||||||
|
require.NoError(tb, err)
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
|
@ -35,6 +35,8 @@ type Player struct {
|
||||||
deletedAt time.Time
|
deletedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const playerModelName = "Player"
|
||||||
|
|
||||||
// UnmarshalPlayerFromDatabase unmarshals Player from the database.
|
// UnmarshalPlayerFromDatabase unmarshals Player from the database.
|
||||||
//
|
//
|
||||||
// It should be used only for unmarshalling from the database!
|
// It should be used only for unmarshalling from the database!
|
||||||
|
@ -67,6 +69,14 @@ func UnmarshalPlayerFromDatabase(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := validateServerKey(serverKey); err != nil {
|
||||||
|
return Player{}, ValidationError{
|
||||||
|
Model: playerModelName,
|
||||||
|
Field: "serverKey",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
profileURL, err := parseURL(rawProfileURL)
|
profileURL, err := parseURL(rawProfileURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Player{}, ValidationError{
|
return Player{}, ValidationError{
|
||||||
|
@ -98,8 +108,6 @@ func UnmarshalPlayerFromDatabase(
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerModelName = "Player"
|
|
||||||
|
|
||||||
func (p Player) ID() int {
|
func (p Player) ID() int {
|
||||||
return p.id
|
return p.id
|
||||||
}
|
}
|
||||||
|
@ -191,7 +199,7 @@ func (p Player) IsDeleted() bool {
|
||||||
|
|
||||||
type Players []Player
|
type Players []Player
|
||||||
|
|
||||||
// Delete finds all tribes that are not in the given slice with active tribes and returns their ids.
|
// Delete finds all players that are not in the given slice with active players and returns their ids.
|
||||||
// Both slices must be sorted in ascending order by ID.
|
// Both slices must be sorted in ascending order by ID.
|
||||||
func (ps Players) Delete(active BasePlayers) []int {
|
func (ps Players) Delete(active BasePlayers) []int {
|
||||||
//nolint:prealloc
|
//nolint:prealloc
|
||||||
|
@ -227,7 +235,7 @@ const createPlayerParamsModelName = "CreatePlayerParams"
|
||||||
|
|
||||||
// NewCreatePlayerParams constructs a slice of CreatePlayerParams based on the given parameters.
|
// NewCreatePlayerParams constructs a slice of CreatePlayerParams based on the given parameters.
|
||||||
// Both slices must be sorted in ascending order by ID
|
// 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.
|
// + if storedPlayers contains players from different servers. they must be sorted in ascending order by server key.
|
||||||
//
|
//
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
func NewCreatePlayerParams(serverKey string, players BasePlayers, storedPlayers Players) ([]CreatePlayerParams, error) {
|
func NewCreatePlayerParams(serverKey string, players BasePlayers, storedPlayers Players) ([]CreatePlayerParams, error) {
|
||||||
|
@ -299,40 +307,40 @@ func NewCreatePlayerParams(serverKey string, players BasePlayers, storedPlayers
|
||||||
return params, nil
|
return params, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CreatePlayerParams) Base() BasePlayer {
|
func (params CreatePlayerParams) Base() BasePlayer {
|
||||||
return c.base
|
return params.base
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CreatePlayerParams) ServerKey() string {
|
func (params CreatePlayerParams) ServerKey() string {
|
||||||
return c.serverKey
|
return params.serverKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CreatePlayerParams) BestRank() int {
|
func (params CreatePlayerParams) BestRank() int {
|
||||||
return c.bestRank
|
return params.bestRank
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CreatePlayerParams) BestRankAt() time.Time {
|
func (params CreatePlayerParams) BestRankAt() time.Time {
|
||||||
return c.bestRankAt
|
return params.bestRankAt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CreatePlayerParams) MostPoints() int {
|
func (params CreatePlayerParams) MostPoints() int {
|
||||||
return c.mostPoints
|
return params.mostPoints
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CreatePlayerParams) MostPointsAt() time.Time {
|
func (params CreatePlayerParams) MostPointsAt() time.Time {
|
||||||
return c.mostPointsAt
|
return params.mostPointsAt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CreatePlayerParams) MostVillages() int {
|
func (params CreatePlayerParams) MostVillages() int {
|
||||||
return c.mostVillages
|
return params.mostVillages
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CreatePlayerParams) MostVillagesAt() time.Time {
|
func (params CreatePlayerParams) MostVillagesAt() time.Time {
|
||||||
return c.mostVillagesAt
|
return params.mostVillagesAt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CreatePlayerParams) LastActivityAt() time.Time {
|
func (params CreatePlayerParams) LastActivityAt() time.Time {
|
||||||
return c.lastActivityAt
|
return params.lastActivityAt
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlayerSort uint8
|
type PlayerSort uint8
|
||||||
|
|
|
@ -75,6 +75,14 @@ func UnmarshalTribeFromDatabase(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := validateServerKey(serverKey); err != nil {
|
||||||
|
return Tribe{}, ValidationError{
|
||||||
|
Model: tribeModelName,
|
||||||
|
Field: "serverKey",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
profileURL, err := parseURL(rawProfileURL)
|
profileURL, err := parseURL(rawProfileURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Tribe{}, ValidationError{
|
return Tribe{}, ValidationError{
|
||||||
|
|
|
@ -253,28 +253,12 @@ func validateStringLen(s string, min, max int) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateServerKey(key string) error {
|
func validateVersionCode(code string) error {
|
||||||
if l := len(key); l < serverKeyMinLength || l > serverKeyMaxLength {
|
return validateStringLen(code, versionCodeMinLength, versionCodeMaxLength)
|
||||||
return LenOutOfRangeError{
|
|
||||||
Min: serverKeyMinLength,
|
|
||||||
Max: serverKeyMaxLength,
|
|
||||||
Current: l,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateVersionCode(code string) error {
|
func validateServerKey(key string) error {
|
||||||
if l := len(code); l < versionCodeMinLength || l > versionCodeMaxLength {
|
return validateStringLen(key, serverKeyMinLength, serverKeyMaxLength)
|
||||||
return LenOutOfRangeError{
|
|
||||||
Min: versionCodeMinLength,
|
|
||||||
Max: versionCodeMaxLength,
|
|
||||||
Current: l,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateIntInRange(current, min, max int) error {
|
func validateIntInRange(current, min, max int) error {
|
||||||
|
|
|
@ -0,0 +1,351 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net/url"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
villageNameMinLength = 1
|
||||||
|
villageNameMaxLength = 150
|
||||||
|
villageContinentMinLength = 1
|
||||||
|
villageContinentMaxLength = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
type Village struct {
|
||||||
|
id int
|
||||||
|
serverKey string
|
||||||
|
name string
|
||||||
|
points int
|
||||||
|
x int
|
||||||
|
y int
|
||||||
|
continent string
|
||||||
|
bonus int
|
||||||
|
playerID int
|
||||||
|
profileURL *url.URL
|
||||||
|
createdAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
const villageModelName = "Village"
|
||||||
|
|
||||||
|
// UnmarshalVillageFromDatabase unmarshals Village from the database.
|
||||||
|
//
|
||||||
|
// It should be used only for unmarshalling from the database!
|
||||||
|
// You can't use UnmarshalVillageFromDatabase as constructor - It may put domain into the invalid state!
|
||||||
|
func UnmarshalVillageFromDatabase(
|
||||||
|
id int,
|
||||||
|
serverKey string,
|
||||||
|
name string,
|
||||||
|
points int,
|
||||||
|
x int,
|
||||||
|
y int,
|
||||||
|
continent string,
|
||||||
|
bonus int,
|
||||||
|
playerID int,
|
||||||
|
rawProfileURL string,
|
||||||
|
createdAt time.Time,
|
||||||
|
) (Village, error) {
|
||||||
|
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
|
||||||
|
return Village{}, ValidationError{
|
||||||
|
Model: villageModelName,
|
||||||
|
Field: "id",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateServerKey(serverKey); err != nil {
|
||||||
|
return Village{}, ValidationError{
|
||||||
|
Model: villageModelName,
|
||||||
|
Field: "serverKey",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
profileURL, err := parseURL(rawProfileURL)
|
||||||
|
if err != nil {
|
||||||
|
return Village{}, ValidationError{
|
||||||
|
Model: villageModelName,
|
||||||
|
Field: "profileURL",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Village{
|
||||||
|
id: id,
|
||||||
|
serverKey: serverKey,
|
||||||
|
name: name,
|
||||||
|
points: points,
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
continent: continent,
|
||||||
|
bonus: bonus,
|
||||||
|
playerID: playerID,
|
||||||
|
profileURL: profileURL,
|
||||||
|
createdAt: createdAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Village) ID() int {
|
||||||
|
return v.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Village) ServerKey() string {
|
||||||
|
return v.serverKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Village) Name() string {
|
||||||
|
return v.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Village) Points() int {
|
||||||
|
return v.points
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Village) X() int {
|
||||||
|
return v.x
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Village) Y() int {
|
||||||
|
return v.y
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Village) Continent() string {
|
||||||
|
return v.continent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Village) Bonus() int {
|
||||||
|
return v.bonus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Village) PlayerID() int {
|
||||||
|
return v.playerID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Village) ProfileURL() *url.URL {
|
||||||
|
return v.profileURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Village) CreatedAt() time.Time {
|
||||||
|
return v.createdAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Village) Base() BaseVillage {
|
||||||
|
return BaseVillage{
|
||||||
|
id: v.id,
|
||||||
|
name: v.name,
|
||||||
|
points: v.points,
|
||||||
|
x: v.x,
|
||||||
|
y: v.y,
|
||||||
|
continent: v.continent,
|
||||||
|
bonus: v.bonus,
|
||||||
|
playerID: v.playerID,
|
||||||
|
profileURL: v.profileURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Villages []Village
|
||||||
|
|
||||||
|
// Delete finds all villages that are not in the given slice with active villages and returns their ids.
|
||||||
|
// Both slices must be sorted in ascending order by ID.
|
||||||
|
func (vs Villages) Delete(active BaseVillages) []int {
|
||||||
|
//nolint:prealloc
|
||||||
|
var toDelete []int
|
||||||
|
|
||||||
|
for _, v := range vs {
|
||||||
|
_, found := slices.BinarySearchFunc(active, v, func(a BaseVillage, b Village) int {
|
||||||
|
return cmp.Compare(a.ID(), b.ID())
|
||||||
|
})
|
||||||
|
if found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
toDelete = append(toDelete, v.ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
return toDelete
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateVillageParams struct {
|
||||||
|
base BaseVillage
|
||||||
|
serverKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
const createVillageParamsModelName = "CreateVillageParams"
|
||||||
|
|
||||||
|
func NewCreateVillageParams(serverKey string, villages BaseVillages) ([]CreateVillageParams, error) {
|
||||||
|
if err := validateServerKey(serverKey); err != nil {
|
||||||
|
return nil, ValidationError{
|
||||||
|
Model: createVillageParamsModelName,
|
||||||
|
Field: "serverKey",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params := make([]CreateVillageParams, 0, len(villages))
|
||||||
|
|
||||||
|
for i, v := range villages {
|
||||||
|
if v.IsZero() {
|
||||||
|
return nil, fmt.Errorf("villages[%d] is an empty struct", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
params = append(params, CreateVillageParams{
|
||||||
|
base: v,
|
||||||
|
serverKey: serverKey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params CreateVillageParams) Base() BaseVillage {
|
||||||
|
return params.base
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params CreateVillageParams) ServerKey() string {
|
||||||
|
return params.serverKey
|
||||||
|
}
|
||||||
|
|
||||||
|
type VillageSort uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
VillageSortIDASC VillageSort = iota + 1
|
||||||
|
VillageSortIDDESC
|
||||||
|
VillageSortServerKeyASC
|
||||||
|
VillageSortServerKeyDESC
|
||||||
|
)
|
||||||
|
|
||||||
|
const VillageListMaxLimit = 500
|
||||||
|
|
||||||
|
type ListVillagesParams struct {
|
||||||
|
ids []int
|
||||||
|
idGT NullInt
|
||||||
|
serverKeys []string
|
||||||
|
sort []VillageSort
|
||||||
|
limit int
|
||||||
|
offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
const listVillagesParamsModelName = "ListVillagesParams"
|
||||||
|
|
||||||
|
func NewListVillagesParams() ListVillagesParams {
|
||||||
|
return ListVillagesParams{
|
||||||
|
sort: []VillageSort{
|
||||||
|
VillageSortServerKeyASC,
|
||||||
|
VillageSortIDASC,
|
||||||
|
},
|
||||||
|
limit: VillageListMaxLimit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListVillagesParams) IDs() []int {
|
||||||
|
return params.ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListVillagesParams) SetIDs(ids []int) error {
|
||||||
|
for i, id := range ids {
|
||||||
|
if err := validateIntInRange(id, 0, math.MaxInt); err != nil {
|
||||||
|
return SliceElementValidationError{
|
||||||
|
Model: listVillagesParamsModelName,
|
||||||
|
Field: "ids",
|
||||||
|
Index: i,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.ids = ids
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListVillagesParams) IDGT() NullInt {
|
||||||
|
return params.idGT
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListVillagesParams) SetIDGT(idGT NullInt) error {
|
||||||
|
if idGT.Valid {
|
||||||
|
if err := validateIntInRange(idGT.Value, 0, math.MaxInt); err != nil {
|
||||||
|
return ValidationError{
|
||||||
|
Model: listVillagesParamsModelName,
|
||||||
|
Field: "idGT",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.idGT = idGT
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListVillagesParams) ServerKeys() []string {
|
||||||
|
return params.serverKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListVillagesParams) SetServerKeys(serverKeys []string) error {
|
||||||
|
params.serverKeys = serverKeys
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListVillagesParams) Sort() []VillageSort {
|
||||||
|
return params.sort
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
villageSortMinLength = 1
|
||||||
|
villageSortMaxLength = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
func (params *ListVillagesParams) SetSort(sort []VillageSort) error {
|
||||||
|
if err := validateSliceLen(sort, villageSortMinLength, villageSortMaxLength); err != nil {
|
||||||
|
return ValidationError{
|
||||||
|
Model: listVillagesParamsModelName,
|
||||||
|
Field: "sort",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.sort = sort
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListVillagesParams) Limit() int {
|
||||||
|
return params.limit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListVillagesParams) SetLimit(limit int) error {
|
||||||
|
if err := validateIntInRange(limit, 1, VillageListMaxLimit); err != nil {
|
||||||
|
return ValidationError{
|
||||||
|
Model: listVillagesParamsModelName,
|
||||||
|
Field: "limit",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.limit = limit
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListVillagesParams) Offset() int {
|
||||||
|
return params.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
func (params *ListVillagesParams) SetOffset(offset int) error {
|
||||||
|
if err := validateIntInRange(offset, 0, math.MaxInt); err != nil {
|
||||||
|
return ValidationError{
|
||||||
|
Model: listVillagesParamsModelName,
|
||||||
|
Field: "offset",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.offset = offset
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,390 @@
|
||||||
|
package domain_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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 TestVillages_Delete(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
server := domaintest.NewServer(t)
|
||||||
|
|
||||||
|
active := domain.BaseVillages{
|
||||||
|
domaintest.NewBaseVillage(t),
|
||||||
|
domaintest.NewBaseVillage(t),
|
||||||
|
domaintest.NewBaseVillage(t),
|
||||||
|
}
|
||||||
|
slices.SortFunc(active, func(a, b domain.BaseVillage) int {
|
||||||
|
return cmp.Compare(a.ID(), b.ID())
|
||||||
|
})
|
||||||
|
|
||||||
|
villages := domain.Villages{
|
||||||
|
domaintest.NewVillage(t, func(cfg *domaintest.VillageConfig) {
|
||||||
|
cfg.ID = active[0].ID()
|
||||||
|
cfg.ServerKey = server.Key()
|
||||||
|
}),
|
||||||
|
domaintest.NewVillage(t, func(cfg *domaintest.VillageConfig) {
|
||||||
|
cfg.ServerKey = server.Key()
|
||||||
|
}),
|
||||||
|
domaintest.NewVillage(t, func(cfg *domaintest.VillageConfig) {
|
||||||
|
cfg.ID = active[1].ID()
|
||||||
|
cfg.ServerKey = server.Key()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
expectedIDs := []int{villages[1].ID()}
|
||||||
|
slices.SortFunc(villages, func(a, b domain.Village) int {
|
||||||
|
if res := cmp.Compare(a.ServerKey(), b.ServerKey()); res != 0 {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
return cmp.Compare(a.ID(), b.ID())
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, expectedIDs, villages.Delete(active))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewCreateVillageParams(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
server := domaintest.NewServer(t)
|
||||||
|
|
||||||
|
villages := domain.BaseVillages{
|
||||||
|
domaintest.NewBaseVillage(t),
|
||||||
|
domaintest.NewBaseVillage(t),
|
||||||
|
domaintest.NewBaseVillage(t),
|
||||||
|
}
|
||||||
|
slices.SortFunc(villages, func(a, b domain.BaseVillage) int {
|
||||||
|
return cmp.Compare(a.ID(), b.ID())
|
||||||
|
})
|
||||||
|
|
||||||
|
res, err := domain.NewCreateVillageParams(server.Key(), villages)
|
||||||
|
require.NoError(t, err)
|
||||||
|
for i, v := range villages {
|
||||||
|
idx := slices.IndexFunc(res, func(params domain.CreateVillageParams) bool {
|
||||||
|
return params.Base().ID() == v.ID() && params.ServerKey() == server.Key()
|
||||||
|
})
|
||||||
|
require.GreaterOrEqualf(t, idx, 0, "village[%d] not found", i)
|
||||||
|
|
||||||
|
params := res[idx]
|
||||||
|
|
||||||
|
assert.Equalf(t, v, params.Base(), "villages[%d]", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListVillagesParams_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: "ListVillagesParams",
|
||||||
|
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.NewListVillagesParams()
|
||||||
|
|
||||||
|
require.ErrorIs(t, params.SetIDs(tt.args.ids), tt.expectedErr)
|
||||||
|
if tt.expectedErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.args.ids, params.IDs())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListVillagesParams_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: "ListVillagesParams",
|
||||||
|
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.NewListVillagesParams()
|
||||||
|
|
||||||
|
require.ErrorIs(t, params.SetIDGT(tt.args.idGT), tt.expectedErr)
|
||||||
|
if tt.expectedErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.args.idGT, params.IDGT())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListVillagesParams_SetSort(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
sort []domain.VillageSort
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OK",
|
||||||
|
args: args{
|
||||||
|
sort: []domain.VillageSort{
|
||||||
|
domain.VillageSortIDASC,
|
||||||
|
domain.VillageSortServerKeyASC,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: len(sort) < 1",
|
||||||
|
args: args{
|
||||||
|
sort: nil,
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "ListVillagesParams",
|
||||||
|
Field: "sort",
|
||||||
|
Err: domain.LenOutOfRangeError{
|
||||||
|
Min: 1,
|
||||||
|
Max: 2,
|
||||||
|
Current: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: len(sort) > 2",
|
||||||
|
args: args{
|
||||||
|
sort: []domain.VillageSort{
|
||||||
|
domain.VillageSortIDASC,
|
||||||
|
domain.VillageSortServerKeyASC,
|
||||||
|
domain.VillageSortServerKeyDESC,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "ListVillagesParams",
|
||||||
|
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.NewListVillagesParams()
|
||||||
|
|
||||||
|
require.ErrorIs(t, params.SetSort(tt.args.sort), tt.expectedErr)
|
||||||
|
if tt.expectedErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.args.sort, params.Sort())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListVillagesParams_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.VillageListMaxLimit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: limit < 1",
|
||||||
|
args: args{
|
||||||
|
limit: 0,
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "ListVillagesParams",
|
||||||
|
Field: "limit",
|
||||||
|
Err: domain.MinGreaterEqualError{
|
||||||
|
Min: 1,
|
||||||
|
Current: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: fmt.Sprintf("ERR: limit > %d", domain.VillageListMaxLimit),
|
||||||
|
args: args{
|
||||||
|
limit: domain.VillageListMaxLimit + 1,
|
||||||
|
},
|
||||||
|
expectedErr: domain.ValidationError{
|
||||||
|
Model: "ListVillagesParams",
|
||||||
|
Field: "limit",
|
||||||
|
Err: domain.MaxLessEqualError{
|
||||||
|
Max: domain.VillageListMaxLimit,
|
||||||
|
Current: domain.VillageListMaxLimit + 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
params := domain.NewListVillagesParams()
|
||||||
|
|
||||||
|
require.ErrorIs(t, params.SetLimit(tt.args.limit), tt.expectedErr)
|
||||||
|
if tt.expectedErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.args.limit, params.Limit())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListVillagesParams_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: "ListVillagesParams",
|
||||||
|
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.NewListVillagesParams()
|
||||||
|
|
||||||
|
require.ErrorIs(t, params.SetOffset(tt.args.offset), tt.expectedErr)
|
||||||
|
if tt.expectedErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.args.offset, params.Offset())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package port
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/app"
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||||
|
"gitea.dwysokinski.me/twhelp/corev3/internal/watermillmsg"
|
||||||
|
"github.com/ThreeDotsLabs/watermill"
|
||||||
|
"github.com/ThreeDotsLabs/watermill/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VillageWatermillConsumer struct {
|
||||||
|
svc *app.VillageService
|
||||||
|
subscriber message.Subscriber
|
||||||
|
logger watermill.LoggerAdapter
|
||||||
|
marshaler watermillmsg.Marshaler
|
||||||
|
eventServerSyncedTopic string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVillageWatermillConsumer(
|
||||||
|
svc *app.VillageService,
|
||||||
|
subscriber message.Subscriber,
|
||||||
|
logger watermill.LoggerAdapter,
|
||||||
|
marshaler watermillmsg.Marshaler,
|
||||||
|
eventServerSyncedTopic string,
|
||||||
|
) *VillageWatermillConsumer {
|
||||||
|
return &VillageWatermillConsumer{
|
||||||
|
svc: svc,
|
||||||
|
subscriber: subscriber,
|
||||||
|
logger: logger,
|
||||||
|
marshaler: marshaler,
|
||||||
|
eventServerSyncedTopic: eventServerSyncedTopic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VillageWatermillConsumer) Register(router *message.Router) {
|
||||||
|
router.AddNoPublisherHandler(
|
||||||
|
"VillageConsumer.sync",
|
||||||
|
c.eventServerSyncedTopic,
|
||||||
|
c.subscriber,
|
||||||
|
c.sync,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *VillageWatermillConsumer) sync(msg *message.Message) error {
|
||||||
|
var rawPayload watermillmsg.ServerSyncedEventPayload
|
||||||
|
|
||||||
|
if err := c.marshaler.Unmarshal(msg, &rawPayload); err != nil {
|
||||||
|
c.logger.Error("couldn't unmarshal payload", err, watermill.LogFields{
|
||||||
|
"handler": message.HandlerNameFromCtx(msg.Context()),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := domain.NewServerSyncedEventPayload(rawPayload.Key, rawPayload.URL, rawPayload.VersionCode)
|
||||||
|
if err != nil {
|
||||||
|
c.logger.Error("couldn't construct domain.ServerSyncedEventPayload", err, watermill.LogFields{
|
||||||
|
"handler": message.HandlerNameFromCtx(msg.Context()),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.svc.Sync(msg.Context(), payload)
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ resources:
|
||||||
- server-consumer.yml
|
- server-consumer.yml
|
||||||
- tribe-consumer.yml
|
- tribe-consumer.yml
|
||||||
- player-consumer.yml
|
- player-consumer.yml
|
||||||
|
- village-consumer.yml
|
||||||
images:
|
images:
|
||||||
- name: twhelp
|
- name: twhelp
|
||||||
newName: twhelp
|
newName: twhelp
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: twhelp-village-consumer-deployment
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: twhelp-village-consumer
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: twhelp-village-consumer
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: twhelp-village-consumer
|
||||||
|
image: twhelp
|
||||||
|
args: [consumer, village]
|
||||||
|
env:
|
||||||
|
- name: APP_MODE
|
||||||
|
value: development
|
||||||
|
- name: LOG_LEVEL
|
||||||
|
value: debug
|
||||||
|
- name: DB_CONNECTION_STRING
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: twhelp-secret
|
||||||
|
key: db-connection-string
|
||||||
|
- name: RABBITMQ_CONNECTION_STRING
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: twhelp-secret
|
||||||
|
key: rabbitmq-connection-string
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command: [cat, /tmp/live]
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 100Mi
|
||||||
|
limits:
|
||||||
|
cpu: 300m
|
||||||
|
memory: 300Mi
|
Loading…
Reference in New Issue