feat: add server repo (#7)

Reviewed-on: twhelp/corev3#7
This commit is contained in:
Dawid Wysokiński 2023-12-24 10:44:20 +00:00
parent d787edde00
commit b4e95f3267
29 changed files with 2054 additions and 166 deletions

1
go.mod
View File

@ -15,6 +15,7 @@ require (
github.com/ory/dockertest/v3 v3.10.0
github.com/stretchr/testify v1.8.4
github.com/uptrace/bun v1.1.16
github.com/uptrace/bun/dbfixture v1.1.16
github.com/uptrace/bun/dialect/pgdialect v1.1.16
github.com/uptrace/bun/dialect/sqlitedialect v1.1.16
github.com/uptrace/bun/driver/pgdriver v1.1.16

2
go.sum
View File

@ -149,6 +149,8 @@ github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYm
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/uptrace/bun v1.1.16 h1:cn9cgEMFwcyYRsQLfxCRMUxyK1WaHwOVrR3TvzEFZ/A=
github.com/uptrace/bun v1.1.16/go.mod h1:7HnsMRRvpLFUcquJxp22JO8PsWKpFQO/gNXqqsuGWg8=
github.com/uptrace/bun/dbfixture v1.1.16 h1:CEdQaeptGRRvwVRhn9PMrawo+yaK+HzAU8/O0h463hA=
github.com/uptrace/bun/dbfixture v1.1.16/go.mod h1:kw11JRwFD3eAHs3Lyq7sHJ6i2KWngUlZQsrvyRy+dqY=
github.com/uptrace/bun/dialect/pgdialect v1.1.16 h1:eUPZ+YCJ69BA+W1X1ZmpOJSkv1oYtinr0zCXf7zCo5g=
github.com/uptrace/bun/dialect/pgdialect v1.1.16/go.mod h1:KQjfx/r6JM0OXfbv0rFrxAbdkPD7idK8VitnjIV9fZI=
github.com/uptrace/bun/dialect/sqlitedialect v1.1.16 h1:gbc9BP/e4sNOB9VBj+Si46dpOz2oktmZPidkda92GYY=

View File

@ -0,0 +1,47 @@
package adaptertest
import (
"context"
"io/fs"
"gitea.dwysokinski.me/twhelp/corev3/internal/adapter/internal/bunmodel"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/stretchr/testify/require"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dbfixture"
)
type Fixture struct {
f *dbfixture.Fixture
}
func NewFixture(bunDB *bun.DB) *Fixture {
bunDB.RegisterModel(
(*bunmodel.Version)(nil),
(*bunmodel.Server)(nil),
)
return &Fixture{
f: dbfixture.New(bunDB),
}
}
//nolint:revive
func (f *Fixture) Load(tb TestingTB, ctx context.Context, fsys fs.FS, names ...string) {
tb.Helper()
require.NoError(tb, f.f.Load(ctx, fsys, names...))
}
func (f *Fixture) Server(tb TestingTB, id string) domain.Server {
tb.Helper()
row, err := f.f.Row("Server." + id)
require.NoError(tb, err)
s, ok := row.(*bunmodel.Server)
require.True(tb, ok)
converted, err := s.ToDomain()
require.NoError(tb, err)
return converted
}

View File

@ -9,14 +9,13 @@ import (
"github.com/uptrace/bun/driver/sqliteshim"
)
const sqliteMaxIdleConns = 1000
// NewBunDBSQLite initializes a new instance of *bun.DB, which is ready for use (all required migrations are applied).
// Data is stored in memory (https://www.sqlite.org/inmemorydb.html).
func NewBunDBSQLite(tb TestingTB) *bun.DB {
sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared")
sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:")
require.NoError(tb, err)
sqldb.SetMaxIdleConns(sqliteMaxIdleConns)
sqldb.SetMaxOpenConns(1)
sqldb.SetMaxIdleConns(1)
sqldb.SetConnMaxLifetime(0)
db := bun.NewDB(sqldb, sqlitedialect.New())

View File

@ -3,7 +3,6 @@ package adaptertest
import (
"context"
"errors"
"fmt"
"net"
"net/url"
"os"
@ -26,8 +25,8 @@ func runMigrations(tb TestingTB, db *bun.DB) {
defer cancel()
require.NoError(tb, migrator.Init(ctx), "couldn't init migrator")
_, err := migrator.Migrate(ctx)
fmt.Println(err)
require.NoError(tb, err, "couldn't apply migrations")
}

View File

@ -0,0 +1,91 @@
package bunmodel
import (
"fmt"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
)
type Building struct {
MaxLevel int
MinLevel int
Wood int
Stone int
Iron int
Pop int
WoodFactor float64
StoneFactor float64
IronFactor float64
PopFactor float64
BuildTime float64
BuildTimeFactor float64
}
type BuildingInfo struct {
Main Building
Barracks Building
Stable Building
Garage Building
Watchtower Building
Snob Building
Smith Building
Place Building
Statue Building
Market Building
Wood Building
Stone Building
Iron Building
Farm Building
Storage Building
Hide Building
Wall Building
}
func NewBuildingInfo(info domain.BuildingInfo) BuildingInfo {
return BuildingInfo{
Main: Building(info.Main()),
Barracks: Building(info.Barracks()),
Stable: Building(info.Stable()),
Garage: Building(info.Garage()),
Watchtower: Building(info.Watchtower()),
Snob: Building(info.Snob()),
Smith: Building(info.Smith()),
Place: Building(info.Place()),
Statue: Building(info.Statue()),
Market: Building(info.Market()),
Wood: Building(info.Wood()),
Stone: Building(info.Stone()),
Iron: Building(info.Iron()),
Farm: Building(info.Farm()),
Storage: Building(info.Storage()),
Hide: Building(info.Hide()),
Wall: Building(info.Wall()),
}
}
func (b BuildingInfo) ToDomain() (domain.BuildingInfo, error) {
info, err := domain.NewBuildingInfo(
domain.Building(b.Main),
domain.Building(b.Barracks),
domain.Building(b.Stable),
domain.Building(b.Garage),
domain.Building(b.Watchtower),
domain.Building(b.Snob),
domain.Building(b.Smith),
domain.Building(b.Place),
domain.Building(b.Statue),
domain.Building(b.Market),
domain.Building(b.Wood),
domain.Building(b.Stone),
domain.Building(b.Iron),
domain.Building(b.Farm),
domain.Building(b.Storage),
domain.Building(b.Hide),
domain.Building(b.Wall),
)
if err != nil {
return domain.BuildingInfo{}, fmt.Errorf("couldn't construct domain.BuildingInfo: %w", err)
}
return info, nil
}

View File

@ -0,0 +1,97 @@
package bunmodel
import (
"fmt"
"time"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/uptrace/bun"
)
type Server struct {
bun.BaseModel `bun:"table:servers,alias:server"`
Key string `bun:"key,nullzero,pk"`
URL string `bun:"url,nullzero"`
Open bool `bun:"open"`
Special bool `bun:"special"`
NumPlayers int `bun:"num_players"`
NumTribes int `bun:"num_tribes"`
NumVillages int `bun:"num_villages"`
NumPlayerVillages int `bun:"num_player_villages"`
NumBarbarianVillages int `bun:"num_barbarian_villages"`
NumBonusVillages int `bun:"num_bonus_villages"`
Config ServerConfig `bun:"config"`
BuildingInfo BuildingInfo `bun:"building_info"`
UnitInfo UnitInfo `bun:"unit_info"`
CreatedAt time.Time `bun:"created_at,nullzero"`
PlayerDataUpdatedAt time.Time `bun:"player_data_updated_at,nullzero"`
PlayerSnapshotsCreatedAt time.Time `bun:"player_snapshots_created_at,nullzero"`
TribeDataUpdatedAt time.Time `bun:"tribe_data_updated_at,nullzero"`
TribeSnapshotsCreatedAt time.Time `bun:"tribe_snapshots_created_at,nullzero"`
VillageDataUpdatedAt time.Time `bun:"village_data_updated_at,nullzero"`
EnnoblementDataUpdatedAt time.Time `bun:"ennoblement_data_updated_at,nullzero"`
VersionCode string `bun:"version_code,nullzero"`
}
func (s Server) ToDomain() (domain.Server, error) {
serverCfg, err := s.Config.ToDomain()
if err != nil {
return domain.Server{}, fmt.Errorf("couldn't construct domain.Server (key=%s): %w", s.Key, err)
}
buildingInfo, err := s.BuildingInfo.ToDomain()
if err != nil {
return domain.Server{}, fmt.Errorf("couldn't construct domain.Server (key=%s): %w", s.Key, err)
}
unitInfo, err := s.UnitInfo.ToDomain()
if err != nil {
return domain.Server{}, fmt.Errorf("couldn't construct domain.Server (key=%s): %w", s.Key, err)
}
converted, err := domain.UnmarshalServerFromDatabase(
s.Key,
s.VersionCode,
s.URL,
s.Open,
s.Special,
s.NumPlayers,
s.NumTribes,
s.NumVillages,
s.NumPlayerVillages,
s.NumBarbarianVillages,
s.NumBonusVillages,
serverCfg,
buildingInfo,
unitInfo,
s.CreatedAt,
s.PlayerDataUpdatedAt,
s.PlayerSnapshotsCreatedAt,
s.TribeDataUpdatedAt,
s.TribeSnapshotsCreatedAt,
s.VillageDataUpdatedAt,
s.EnnoblementDataUpdatedAt,
)
if err != nil {
return domain.Server{}, fmt.Errorf("couldn't construct domain.Server (key=%s): %w", s.Key, err)
}
return converted, nil
}
type Servers []Server
func (ss Servers) ToDomain() (domain.Servers, error) {
res := make(domain.Servers, 0, len(ss))
for _, s := range ss {
converted, err := s.ToDomain()
if err != nil {
return nil, err
}
res = append(res, converted)
}
return res, nil
}

View File

@ -0,0 +1,207 @@
package bunmodel
import (
"fmt"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
)
type ServerConfigBuild struct {
Destroy int
}
type ServerConfigMisc struct {
KillRanking int
Tutorial int
TradeCancelTime int
}
type ServerConfigCommands struct {
MillisArrival int
CommandCancelTime int
}
type ServerConfigNewbie struct {
Days int
RatioDays int
Ratio int
RemoveNewbieVillages int
}
type ServerConfigGame struct {
BuildtimeFormula int
Knight int
KnightNewItems int
Archer int
Tech int
FarmLimit int
Church int
Watchtower int
Stronghold int
FakeLimit float64
BarbarianRise float64
BarbarianShrink int
BarbarianMaxPoints int
Scavenging int
Hauls int
HaulsBase int
HaulsMax int
BaseProduction int
Event int
SuppressEvents int
}
type ServerConfigBuildings struct {
CustomMain int
CustomFarm int
CustomStorage int
CustomPlace int
CustomBarracks int
CustomChurch int
CustomSmith int
CustomWood int
CustomStone int
CustomIron int
CustomMarket int
CustomStable int
CustomWall int
CustomGarage int
CustomHide int
CustomSnob int
CustomStatue int
CustomWatchtower int
}
type ServerConfigSnob struct {
Gold int
CheapRebuild int
Rise int
MaxDist int
Factor float64
CoinWood int
CoinStone int
CoinIron int
NoBarbConquer int
}
type ServerConfigAlly struct {
NoHarm int
NoOtherSupport int
NoOtherSupportType int
AllytimeSupport int
NoLeave int
NoJoin int
Limit int
FixedAllies int
PointsMemberCount int
WarsMemberRequirement int
WarsPointsRequirement int
WarsAutoacceptDays int
Levels int
XpRequirements string
}
type ServerConfigCoord struct {
MapSize int
Func int
EmptyVillages int
BonusVillages int
BonusNew int
Inner int
SelectStart int
VillageMoveWait int
NobleRestart int
StartVillages int
}
type ServerConfigSitter struct {
Allow int
}
type ServerConfigSleep struct {
Active int
Delay int
Min int
Max int
MinAwake int
MaxAwake int
WarnTime int
}
type ServerConfigNight struct {
Active int
StartHour int
EndHour int
DefFactor float64
Duration int
}
type ServerConfigWin struct {
Check int
}
type ServerConfig struct {
Speed float64
UnitSpeed float64
Moral int
Build ServerConfigBuild
Misc ServerConfigMisc
Commands ServerConfigCommands
Newbie ServerConfigNewbie
Game ServerConfigGame
Buildings ServerConfigBuildings
Snob ServerConfigSnob
Ally ServerConfigAlly
Coord ServerConfigCoord
Sitter ServerConfigSitter
Sleep ServerConfigSleep
Night ServerConfigNight
Win ServerConfigWin
}
func NewServerConfig(cfg domain.ServerConfig) ServerConfig {
return ServerConfig{
Speed: cfg.Speed(),
UnitSpeed: cfg.UnitSpeed(),
Moral: cfg.Moral(),
Build: ServerConfigBuild(cfg.Build()),
Misc: ServerConfigMisc(cfg.Misc()),
Commands: ServerConfigCommands(cfg.Commands()),
Newbie: ServerConfigNewbie(cfg.Newbie()),
Game: ServerConfigGame(cfg.Game()),
Buildings: ServerConfigBuildings(cfg.Buildings()),
Snob: ServerConfigSnob(cfg.Snob()),
Ally: ServerConfigAlly(cfg.Ally()),
Coord: ServerConfigCoord(cfg.Coord()),
Sitter: ServerConfigSitter(cfg.Sitter()),
Sleep: ServerConfigSleep(cfg.Sleep()),
Night: ServerConfigNight(cfg.Night()),
Win: ServerConfigWin(cfg.Win()),
}
}
func (s ServerConfig) ToDomain() (domain.ServerConfig, error) {
cfg, err := domain.NewServerConfig(
s.Speed,
s.UnitSpeed,
s.Moral,
domain.ServerConfigBuild(s.Build),
domain.ServerConfigMisc(s.Misc),
domain.ServerConfigCommands(s.Commands),
domain.ServerConfigNewbie(s.Newbie),
domain.ServerConfigGame(s.Game),
domain.ServerConfigBuildings(s.Buildings),
domain.ServerConfigSnob(s.Snob),
domain.ServerConfigAlly(s.Ally),
domain.ServerConfigCoord(s.Coord),
domain.ServerConfigSitter(s.Sitter),
domain.ServerConfigSleep(s.Sleep),
domain.ServerConfigNight(s.Night),
domain.ServerConfigWin(s.Win),
)
if err != nil {
return domain.ServerConfig{}, fmt.Errorf("couldn't construct domain.ServerConfig: %w", err)
}
return cfg, nil
}

View File

@ -0,0 +1,75 @@
package bunmodel
import (
"fmt"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
)
type Unit struct {
BuildTime float64
Pop int
Speed float64
Attack int
Defense int
DefenseCavalry int
DefenseArcher int
Carry int
}
type UnitInfo struct {
Spear Unit
Sword Unit
Axe Unit
Archer Unit
Spy Unit
Light Unit
Marcher Unit
Heavy Unit
Ram Unit
Catapult Unit
Knight Unit
Snob Unit
Militia Unit
}
func NewUnitInfo(info domain.UnitInfo) UnitInfo {
return UnitInfo{
Spear: Unit(info.Spear()),
Sword: Unit(info.Sword()),
Axe: Unit(info.Axe()),
Archer: Unit(info.Archer()),
Spy: Unit(info.Spy()),
Light: Unit(info.Light()),
Marcher: Unit(info.Marcher()),
Heavy: Unit(info.Heavy()),
Ram: Unit(info.Ram()),
Catapult: Unit(info.Catapult()),
Knight: Unit(info.Knight()),
Snob: Unit(info.Snob()),
Militia: Unit(info.Militia()),
}
}
func (u UnitInfo) ToDomain() (domain.UnitInfo, error) {
info, err := domain.NewUnitInfo(
domain.Unit(u.Spear),
domain.Unit(u.Sword),
domain.Unit(u.Axe),
domain.Unit(u.Archer),
domain.Unit(u.Spy),
domain.Unit(u.Light),
domain.Unit(u.Marcher),
domain.Unit(u.Heavy),
domain.Unit(u.Ram),
domain.Unit(u.Catapult),
domain.Unit(u.Knight),
domain.Unit(u.Snob),
domain.Unit(u.Militia),
)
if err != nil {
return domain.UnitInfo{}, fmt.Errorf("couldn't construct domain.UnitInfo: %w", err)
}
return info, nil
}

View File

@ -0,0 +1,119 @@
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 ServerBunRepository struct {
db bun.IDB
}
func NewServerBunRepository(db bun.IDB) *ServerBunRepository {
return &ServerBunRepository{db: db}
}
func (repo *ServerBunRepository) CreateOrUpdate(ctx context.Context, params ...domain.CreateServerParams) error {
if len(params) == 0 {
return nil
}
servers := make(bunmodel.Servers, 0, len(params))
for _, p := range params {
base := p.Base()
servers = append(servers, bunmodel.Server{
Key: base.Key(),
URL: base.URL().String(),
Open: base.Open(),
VersionCode: p.VersionCode(),
CreatedAt: time.Now(),
})
}
q := repo.db.NewInsert().
Model(&servers)
//nolint:exhaustive
switch q.Dialect().Name() {
case dialect.PG:
q = q.On("CONFLICT ON CONSTRAINT servers_pkey DO UPDATE")
case dialect.SQLite:
q = q.On("CONFLICT(key) DO UPDATE")
default:
q = q.Err(errors.New("unsupported dialect"))
}
if _, err := q.
Set("url = EXCLUDED.url").
Set("open = EXCLUDED.open").
Returning("").
Exec(ctx); err != nil {
return fmt.Errorf("something went wrong while inserting servers into the db: %w", err)
}
return nil
}
func (repo *ServerBunRepository) List(ctx context.Context, params domain.ListServersParams) (domain.Servers, error) {
var servers bunmodel.Servers
if err := repo.baseListQuery(params).Model(&servers).Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("couldn't select servers from the db: %w", err)
}
return servers.ToDomain()
}
func (repo *ServerBunRepository) baseListQuery(params domain.ListServersParams) *bun.SelectQuery {
return repo.db.NewSelect().Apply(listServersParamsApplier{params: params}.apply)
}
type listServersParamsApplier struct {
params domain.ListServersParams
}
func (a listServersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
if keys := a.params.Keys(); len(keys) > 0 {
q = q.Where("server.key IN (?)", bun.In(keys))
}
if keyGT := a.params.KeyGT(); keyGT.Valid {
q = q.Where("server.key > ?", keyGT.Value)
}
if open := a.params.Open(); open.Valid {
q = q.Where("server.open = ?", open.Value)
}
if special := a.params.Special(); special.Valid {
q = q.Where("server.special = ?", special.Value)
}
q = q.Limit(a.params.Limit()).Offset(a.params.Offset())
for _, s := range a.params.Sort() {
switch s {
case domain.ServerSortKeyASC:
q = q.Order("server.key ASC")
case domain.ServerSortKeyDESC:
q = q.Order("server.key DESC")
case domain.ServerSortOpenASC:
q = q.Order("server.open ASC")
case domain.ServerSortOpenDESC:
q = q.Order("server.open DESC")
default:
return q.Err(errors.New("unsupported sort value"))
}
}
return q
}

View File

@ -0,0 +1,29 @@
package adapter_test
import (
"testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/adapter/adaptertest"
)
func TestServerBunRepository_Postgres(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("skipping long-running test")
}
testServerRepository(t, func(t *testing.T) repositories {
t.Helper()
return newBunDBRepositories(t, postgres.NewBunDB(t))
})
}
func TestServerBunRepository_SQLite(t *testing.T) {
t.Parallel()
testServerRepository(t, func(t *testing.T) repositories {
t.Helper()
return newBunDBRepositories(t, adaptertest.NewBunDBSQLite(t))
})
}

View File

@ -3,28 +3,27 @@ package adapter_test
import (
"testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/adapter"
"gitea.dwysokinski.me/twhelp/corev3/internal/adapter/adaptertest"
)
func TestVersionBunRepo_Postgres(t *testing.T) {
func TestVersionBunRepository_Postgres(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("skipping long-running test")
}
testVersionRepo(t, func(t *testing.T) versionRepository {
testVersionRepository(t, func(t *testing.T) repositories {
t.Helper()
return adapter.NewVersionBunRepository(postgres.NewBunDB(t))
return newBunDBRepositories(t, postgres.NewBunDB(t))
})
}
func TestVersionBunRepo_SQLite(t *testing.T) {
func TestVersionBunRepository_SQLite(t *testing.T) {
t.Parallel()
testVersionRepo(t, func(t *testing.T) versionRepository {
testVersionRepository(t, func(t *testing.T) repositories {
t.Helper()
return adapter.NewVersionBunRepository(adaptertest.NewBunDBSQLite(t))
return newBunDBRepositories(t, adaptertest.NewBunDBSQLite(t))
})
}

View File

@ -0,0 +1,354 @@
package adapter_test
import (
"cmp"
"context"
"net/url"
"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 testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories) {
t.Helper()
t.Run("CreateOrUpdate", func(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
ctx := context.Background()
repos := newRepos(t)
versions, err := repos.version.List(ctx, domain.NewListVersionsParams())
require.NoError(t, err)
require.NotEmpty(t, versions)
version := versions[0]
serversToCreate := domain.BaseServers{
domaintest.NewBaseServer(t, domaintest.BaseServerConfig{Open: true}),
domaintest.NewBaseServer(t, domaintest.BaseServerConfig{Open: true}),
}
createParams, err := domain.NewCreateServerParams(serversToCreate, version.Code())
require.NoError(t, err)
require.NoError(t, repos.server.CreateOrUpdate(ctx, createParams...))
keys := make([]string, 0, len(serversToCreate))
for _, s := range serversToCreate {
keys = append(keys, s.Key())
}
listCreatedServersParams := domain.NewListServersParams()
require.NoError(t, listCreatedServersParams.SetKeys(keys))
createdServers, err := repos.server.List(ctx, listCreatedServersParams)
require.NoError(t, err)
require.Len(t, createdServers, len(serversToCreate))
for _, base := range serversToCreate {
assert.True(t, slices.ContainsFunc(createdServers, func(server domain.Server) bool {
return server.Key() == base.Key() &&
server.Open() == base.Open() &&
server.URL().String() == base.URL().String() &&
server.VersionCode() == version.Code()
}))
}
serversToUpdate := domain.BaseServers{
domaintest.NewBaseServer(t, domaintest.BaseServerConfig{
Key: serversToCreate[0].Key(),
URL: randURL(t),
Open: !serversToCreate[0].Open(),
}),
}
updateParams, err := domain.NewCreateServerParams(serversToUpdate, version.Code())
require.NoError(t, err)
require.NoError(t, repos.server.CreateOrUpdate(ctx, updateParams...))
keys = make([]string, 0, len(serversToUpdate))
for _, s := range serversToUpdate {
keys = append(keys, s.Key())
}
listUpdatedServersParams := domain.NewListServersParams()
require.NoError(t, listUpdatedServersParams.SetKeys(keys))
updatedServers, err := repos.server.List(ctx, listUpdatedServersParams)
require.NoError(t, err)
require.Len(t, updatedServers, len(serversToUpdate))
for _, base := range serversToUpdate {
assert.True(t, slices.ContainsFunc(updatedServers, func(server domain.Server) bool {
return server.Key() == base.Key() &&
server.Open() == base.Open() &&
server.URL().String() == base.URL().String() &&
server.VersionCode() == version.Code()
}))
}
})
})
t.Run("List & ListCount", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
tests := []struct {
name string
params func(t *testing.T) domain.ListServersParams
assertServers func(t *testing.T, params domain.ListServersParams, servers domain.Servers)
assertError func(t *testing.T, err error)
assertTotal func(t *testing.T, params domain.ListServersParams, total int)
}{
{
name: "OK: default params",
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
return domain.NewListServersParams()
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
t.Helper()
assert.NotEmpty(t, len(servers))
assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int {
return cmp.Compare(a.Key(), b.Key())
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[open ASC, key ASC]",
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
params := domain.NewListServersParams()
require.NoError(t, params.SetSort([]domain.ServerSort{domain.ServerSortOpenASC, domain.ServerSortKeyASC}))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
t.Helper()
assert.NotEmpty(t, len(servers))
assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int {
if a.Open() && !b.Open() {
return 1
}
if !a.Open() && b.Open() {
return -1
}
return cmp.Compare(a.Key(), b.Key())
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[open DESC, key DESC]",
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
params := domain.NewListServersParams()
require.NoError(t, params.SetSort([]domain.ServerSort{domain.ServerSortOpenDESC, domain.ServerSortKeyDESC}))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
t.Helper()
assert.NotEmpty(t, len(servers))
assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int {
if a.Open() && !b.Open() {
return -1
}
if !a.Open() && b.Open() {
return 1
}
return cmp.Compare(a.Key(), b.Key()) * -1
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: keys=[de188, en113]",
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
params := domain.NewListServersParams()
require.NoError(t, params.SetKeys([]string{"de188", "en113"}))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
t.Helper()
keys := params.Keys()
assert.Len(t, servers, len(keys))
for _, k := range keys {
assert.True(t, slices.ContainsFunc(servers, func(server domain.Server) bool {
return server.Key() == k
}), k)
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.Equal(t, len(params.Keys()), total) //nolint:testifylint
},
},
{
name: "OK: keyGT=de188",
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
params := domain.NewListServersParams()
require.NoError(t, params.SetKeyGT(domain.NullString{
Value: "de188",
Valid: true,
}))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
t.Helper()
assert.NotEmpty(t, len(servers))
for _, s := range servers {
assert.Greater(t, s.Key(), params.KeyGT().Value, s.Key())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: special=true",
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
params := domain.NewListServersParams()
require.NoError(t, params.SetSpecial(domain.NullBool{
Value: true,
Valid: true,
}))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
t.Helper()
assert.NotEmpty(t, len(servers))
for _, s := range servers {
assert.True(t, s.Special(), s.Key())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: open=false",
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
params := domain.NewListServersParams()
require.NoError(t, params.SetOpen(domain.NullBool{
Value: false,
Valid: true,
}))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
t.Helper()
assert.NotEmpty(t, len(servers))
for _, s := range servers {
assert.False(t, s.Open(), s.Key())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: offset=1 limit=2",
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
params := domain.NewListServersParams()
require.NoError(t, params.SetOffset(1))
require.NoError(t, params.SetLimit(2))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
t.Helper()
assert.Len(t, servers, params.Limit())
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx := context.Background()
params := tt.params(t)
res, err := repos.server.List(ctx, params)
tt.assertError(t, err)
tt.assertServers(t, params, res)
})
}
})
}
func randURL(tb testing.TB) *url.URL {
tb.Helper()
u, err := url.Parse("https://" + gofakeit.DomainName())
require.NoError(tb, err)
return u
}

View File

@ -0,0 +1,41 @@
package adapter_test
import (
"context"
"os"
"testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/adapter"
"gitea.dwysokinski.me/twhelp/corev3/internal/adapter/adaptertest"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/uptrace/bun"
)
type versionRepository interface {
List(ctx context.Context, params domain.ListVersionsParams) (domain.Versions, error)
ListCount(
ctx context.Context,
params domain.ListVersionsParams,
) (domain.Versions, int, error)
}
type serverRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateServerParams) error
List(ctx context.Context, params domain.ListServersParams) (domain.Servers, error)
}
type repositories struct {
version versionRepository
server serverRepository
}
func newBunDBRepositories(tb testing.TB, bunDB *bun.DB) repositories {
tb.Helper()
adaptertest.NewFixture(bunDB).Load(tb, context.Background(), os.DirFS("testdata"), "fixture.yml")
return repositories{
version: adapter.NewVersionBunRepository(bunDB),
server: adapter.NewServerBunRepository(bunDB),
}
}

View File

@ -11,21 +11,13 @@ import (
"github.com/stretchr/testify/require"
)
type versionRepository interface {
List(ctx context.Context, params domain.ListVersionsParams) (domain.Versions, error)
ListCount(
ctx context.Context,
params domain.ListVersionsParams,
) (domain.Versions, int, error)
}
func testVersionRepo(t *testing.T, newRepo func(t *testing.T) versionRepository) {
func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositories) {
t.Helper()
t.Run("List & ListCount", func(t *testing.T) {
t.Parallel()
repo := newRepo(t)
repos := newRepos(t)
tests := []struct {
name string
@ -53,11 +45,11 @@ func testVersionRepo(t *testing.T, newRepo func(t *testing.T) versionRepository)
},
assertTotal: func(t *testing.T, total int) {
t.Helper()
assert.NotEmpty(t, total, 0)
assert.NotEmpty(t, total)
},
},
{
name: "OK: Sort=[Code DESC]",
name: "OK: sort=[code DESC]",
params: func(t *testing.T) domain.ListVersionsParams {
t.Helper()
params := domain.NewListVersionsParams()
@ -91,11 +83,11 @@ func testVersionRepo(t *testing.T, newRepo func(t *testing.T) versionRepository)
ctx := context.Background()
params := tt.params(t)
res, err := repo.List(ctx, params)
res, err := repos.version.List(ctx, params)
tt.assertError(t, err)
tt.assertVersions(t, res)
res, count, err := repo.ListCount(ctx, params)
res, count, err := repos.version.ListCount(ctx, params)
tt.assertError(t, err)
tt.assertVersions(t, res)
tt.assertTotal(t, count)

78
internal/adapter/testdata/fixture.yml vendored Normal file
View File

@ -0,0 +1,78 @@
- model: Server
rows:
- _id: de188
key: de188
url: https://de188.die-staemme.de
open: true
special: false
num_players: 180
num_tribes: 76
num_villages: 16180
num_player_villages: 15000
num_barbarian_villages: 1180
num_bonus_villages: 512
created_at: 2022-03-19T12:00:54.000Z
player_data_updated_at: 2022-03-19T12:00:54.000Z
player_snapshots_created_at: 2022-03-19T12:00:54.000Z
tribe_data_updated_at: 2022-03-19T12:00:54.000Z
tribe_snapshots_created_at: 2022-03-19T12:00:54.000Z
village_data_updated_at: 2022-03-19T12:00:54.000Z
ennoblement_data_updated_at: 2022-03-19T12:00:54.000Z
version_code: de
- _id: en113
key: en113
url: https://en113.tribalwars.net
open: false
special: false
num_players: 251
num_tribes: 57
num_villages: 41700
num_player_villages: 41000
num_barbarian_villages: 700
num_bonus_villages: 1024
created_at: 2021-04-02T16:01:25.000Z
player_data_updated_at: 2021-04-02T16:01:25.000Z
player_snapshots_created_at: 2021-04-02T16:01:25.000Z
tribe_data_updated_at: 2021-04-02T16:01:25.000Z
tribe_snapshots_created_at: 2021-04-02T16:01:25.000Z
village_data_updated_at: 2021-04-02T16:01:25.000Z
ennoblement_data_updated_at: 2021-04-02T16:01:25.000Z
version_code: en
- _id: it70
key: it70
url: https://it70.tribals.it
open: true
special: false
num_players: 1883
num_tribes: 101
num_villages: 4882
num_player_villages: 3200
num_barbarian_villages: 1682
num_bonus_villages: 256
created_at: 2022-03-19T12:00:04.000Z
player_data_updated_at: 2022-03-19T12:00:04.000Z
player_snapshots_created_at: 2022-03-19T12:00:04.000Z
tribe_data_updated_at: 2022-03-19T12:00:04.000Z
tribe_snapshots_created_at: 2022-03-19T12:00:04.000Z
village_data_updated_at: 2022-03-19T12:00:04.000Z
ennoblement_data_updated_at: 2022-03-19T12:00:04.000Z
version_code: it
- _id: pl169
key: pl169
url: https://pl169.plemiona.pl
open: true
special: false
num_players: 2001
num_tribes: 214
num_villages: 49074
num_player_villages: 48500
num_barbarian_villages: 1574
num_bonus_villages: 2048
created_at: 2022-03-19T12:01:39.000Z
player_data_updated_at: 2022-03-19T12:01:39.000Z
player_snapshots_created_at: 2022-03-19T12:01:39.000Z
tribe_data_updated_at: 2022-03-19T12:01:39.000Z
tribe_snapshots_created_at: 2022-03-19T12:01:39.000Z
village_data_updated_at: 2022-03-19T12:01:39.000Z
ennoblement_data_updated_at: 2022-03-19T12:01:39.000Z
version_code: pl

View File

@ -9,26 +9,27 @@ import (
)
type BaseServerConfig struct {
Key string
URL *url.URL
Key string
URL *url.URL
Open bool
}
func (cfg *BaseServerConfig) init() {
if cfg.Key == "" {
cfg.Key = gofakeit.LetterN(5)
cfg.Key = RandServerKey()
}
if cfg.URL == nil {
cfg.URL = &url.URL{
Scheme: "https",
Host: cfg.Key + ".plemiona.pl",
Host: cfg.Key + "." + gofakeit.DomainName(),
}
}
}
func NewBaseServer(tb TestingTB, cfg BaseServerConfig) domain.BaseServer {
cfg.init()
s, err := domain.NewBaseServer(cfg.Key, cfg.URL.String(), true)
s, err := domain.NewBaseServer(cfg.Key, cfg.URL.String(), cfg.Open)
require.NoError(tb, err)
return s
}

View File

@ -0,0 +1,7 @@
package domaintest
import "github.com/brianvoe/gofakeit/v6"
func RandServerKey() string {
return gofakeit.LetterN(5)
}

View File

@ -0,0 +1,33 @@
package domaintest
import (
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/require"
)
type VersionConfig struct {
Code string
}
func (cfg *VersionConfig) init() {
if cfg.Code == "" {
cfg.Code = RandVersionCode()
}
}
func NewVersion(tb TestingTB, cfg VersionConfig) domain.Version {
cfg.init()
s, err := domain.UnmarshalVersionFromDatabase(
cfg.Code,
gofakeit.LetterN(10),
gofakeit.DomainName(),
gofakeit.TimeZoneRegion(),
)
require.NoError(tb, err)
return s
}
func RandVersionCode() string {
return gofakeit.LetterN(2)
}

View File

@ -1,12 +1,5 @@
package domain
import (
"errors"
"fmt"
"github.com/gosimple/slug"
)
type ErrorCode uint8
const (
@ -38,40 +31,3 @@ type ErrorWithParams interface {
Error
Params() map[string]any
}
type ValidationError struct {
Field string
Err error
}
var _ ErrorWithParams = ValidationError{}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Err)
}
func (e ValidationError) Code() ErrorCode {
var domainErr Error
if errors.As(e.Err, &domainErr) {
return domainErr.Code()
}
return ErrorCodeIncorrectInput
}
func (e ValidationError) Slug() string {
s := "validation"
var domainErr Error
if errors.As(e.Err, &domainErr) {
s = domainErr.Slug()
}
return fmt.Sprintf("%s-%s", slug.Make(e.Field), s)
}
func (e ValidationError) Params() map[string]any {
var withParams ErrorWithParams
ok := errors.As(e.Err, &withParams)
if !ok {
return nil
}
return withParams.Params()
}

12
internal/domain/null.go Normal file
View File

@ -0,0 +1,12 @@
package domain
type NullValue[T any] struct {
Value T
Valid bool // Valid is true if Value is not NULL
}
type NullInt = NullValue[int]
type NullString = NullValue[string]
type NullBool = NullValue[bool]

View File

@ -2,39 +2,359 @@ package domain
import (
"errors"
"fmt"
"net/url"
"time"
)
type SyncServersCmdPayload struct {
versionCode string
url *url.URL
type Server struct {
key string
versionCode string
url *url.URL
open bool
special bool
numPlayers int
numTribes int
numVillages int
numPlayerVillages int
numBarbarianVillages int
numBonusVillages int
config ServerConfig
buildingInfo BuildingInfo
unitInfo UnitInfo
createdAt time.Time
playerDataSyncedAt time.Time
playerSnapshotsCreatedAt time.Time
tribeDataSyncedAt time.Time
tribeSnapshotsCreatedAt time.Time
villageDataSyncedAt time.Time
ennoblementDataSyncedAt time.Time
}
func NewSyncServersCmdPayload(versionCode string, u *url.URL) (SyncServersCmdPayload, error) {
// UnmarshalServerFromDatabase unmarshals Server from the database.
//
// It should be used only for unmarshalling from the database!
// You can't use UnmarshalServerFromDatabase as constructor - It may put domain into the invalid state!
func UnmarshalServerFromDatabase(
key string,
versionCode string,
rawURL string,
open bool,
special bool,
numPlayers int,
numTribes int,
numVillages int,
numPlayerVillages int,
numBarbarianVillages int,
numBonusVillages int,
config ServerConfig,
buildingInfo BuildingInfo,
unitInfo UnitInfo,
createdAt time.Time,
playerDataSyncedAt time.Time,
playerSnapshotsCreatedAt time.Time,
tribeDataSyncedAt time.Time,
tribeSnapshotsCreatedAt time.Time,
villageDataSyncedAt time.Time,
ennoblementDataSyncedAt time.Time,
) (Server, error) {
if key == "" {
return Server{}, errors.New("key can't be blank")
}
if versionCode == "" {
return SyncServersCmdPayload{}, errors.New("version code can't be blank")
return Server{}, errors.New("version code can't be blank")
}
if u == nil {
return SyncServersCmdPayload{}, errors.New("url can't be nil")
}
return SyncServersCmdPayload{versionCode: versionCode, url: u}, nil
}
func NewSyncServersCmdPayloadWithStringURL(versionCode string, rawURL string) (SyncServersCmdPayload, error) {
u, err := parseURL(rawURL)
if err != nil {
return SyncServersCmdPayload{}, err
return Server{}, err
}
return NewSyncServersCmdPayload(versionCode, u)
return Server{
key: key,
url: u,
open: open,
special: special,
numPlayers: numPlayers,
numTribes: numTribes,
numVillages: numVillages,
numPlayerVillages: numPlayerVillages,
numBarbarianVillages: numBarbarianVillages,
numBonusVillages: numBonusVillages,
config: config,
buildingInfo: buildingInfo,
unitInfo: unitInfo,
createdAt: createdAt,
playerDataSyncedAt: playerDataSyncedAt,
playerSnapshotsCreatedAt: playerSnapshotsCreatedAt,
tribeDataSyncedAt: tribeDataSyncedAt,
tribeSnapshotsCreatedAt: tribeSnapshotsCreatedAt,
villageDataSyncedAt: villageDataSyncedAt,
ennoblementDataSyncedAt: ennoblementDataSyncedAt,
versionCode: versionCode,
}, nil
}
func (p SyncServersCmdPayload) VersionCode() string {
return p.versionCode
func (s Server) Key() string {
return s.key
}
func (p SyncServersCmdPayload) URL() *url.URL {
return p.url
func (s Server) VersionCode() string {
return s.versionCode
}
func (s Server) URL() *url.URL {
return s.url
}
func (s Server) Open() bool {
return s.open
}
func (s Server) Special() bool {
return s.special
}
func (s Server) NumPlayers() int {
return s.numPlayers
}
func (s Server) NumTribes() int {
return s.numTribes
}
func (s Server) NumVillages() int {
return s.numVillages
}
func (s Server) NumPlayerVillages() int {
return s.numPlayerVillages
}
func (s Server) NumBarbarianVillages() int {
return s.numBarbarianVillages
}
func (s Server) NumBonusVillages() int {
return s.numBonusVillages
}
func (s Server) Config() ServerConfig {
return s.config
}
func (s Server) BuildingInfo() BuildingInfo {
return s.buildingInfo
}
func (s Server) UnitInfo() UnitInfo {
return s.unitInfo
}
func (s Server) CreatedAt() time.Time {
return s.createdAt
}
func (s Server) PlayerDataSyncedAt() time.Time {
return s.playerDataSyncedAt
}
func (s Server) PlayerSnapshotsCreatedAt() time.Time {
return s.playerSnapshotsCreatedAt
}
func (s Server) TribeDataSyncedAt() time.Time {
return s.tribeDataSyncedAt
}
func (s Server) TribeSnapshotsCreatedAt() time.Time {
return s.tribeSnapshotsCreatedAt
}
func (s Server) VillageDataSyncedAt() time.Time {
return s.villageDataSyncedAt
}
func (s Server) EnnoblementDataSyncedAt() time.Time {
return s.ennoblementDataSyncedAt
}
type Servers []Server
type CreateServerParams struct {
base BaseServer
versionCode string
}
func NewCreateServerParams(servers BaseServers, versionCode string) ([]CreateServerParams, error) {
if err := validateVersionCode(versionCode); err != nil {
return nil, ValidationError{
Err: err,
Field: "versionCode",
}
}
res := make([]CreateServerParams, 0, len(servers))
for i, s := range servers {
if s.IsZero() {
return nil, fmt.Errorf("servers[%d] is an empty struct", i)
}
res = append(res, CreateServerParams{
base: s,
versionCode: versionCode,
})
}
return res, nil
}
func (params CreateServerParams) Base() BaseServer {
return params.base
}
func (params CreateServerParams) VersionCode() string {
return params.versionCode
}
type ServerSort uint8
const (
ServerSortKeyASC ServerSort = iota + 1
ServerSortKeyDESC
ServerSortOpenASC
ServerSortOpenDESC
)
const ServerListMaxLimit = 500
type ListServersParams struct {
keys []string
keyGT NullString
open NullBool
special NullBool
sort []ServerSort
limit int
offset int
}
func NewListServersParams() ListServersParams {
return ListServersParams{
sort: []ServerSort{ServerSortKeyASC},
limit: ServerListMaxLimit,
special: NullBool{
Value: false,
Valid: true,
},
}
}
func (params *ListServersParams) Keys() []string {
return params.keys
}
func (params *ListServersParams) SetKeys(keys []string) error {
params.keys = keys
return nil
}
func (params *ListServersParams) KeyGT() NullString {
return params.keyGT
}
func (params *ListServersParams) SetKeyGT(keyGT NullString) error {
params.keyGT = keyGT
return nil
}
func (params *ListServersParams) Open() NullBool {
return params.open
}
func (params *ListServersParams) SetOpen(open NullBool) error {
params.open = open
return nil
}
func (params *ListServersParams) Special() NullBool {
return params.special
}
func (params *ListServersParams) SetSpecial(special NullBool) error {
params.special = special
return nil
}
func (params *ListServersParams) Sort() []ServerSort {
return params.sort
}
const (
serverSortMinLength = 1
serverSortMaxLength = 2
)
func (params *ListServersParams) SetSort(sort []ServerSort) error {
if err := validateSliceLen(sort, serverSortMinLength, serverSortMaxLength); err != nil {
return ValidationError{
Field: "sort",
Err: err,
}
}
params.sort = sort
return nil
}
func (params *ListServersParams) Limit() int {
return params.limit
}
func (params *ListServersParams) SetLimit(limit int) error {
if limit <= 0 {
return ValidationError{
Field: "limit",
Err: MinGreaterEqualError{
Min: 1,
Current: limit,
},
}
}
if limit > ServerListMaxLimit {
return ValidationError{
Field: "limit",
Err: MaxLessEqualError{
Max: ServerListMaxLimit,
Current: limit,
},
}
}
params.limit = limit
return nil
}
func (params *ListServersParams) Offset() int {
return params.offset
}
func (params *ListServersParams) SetOffset(offset int) error {
if offset < 0 {
return ValidationError{
Field: "offset",
Err: MinGreaterEqualError{
Min: 0,
Current: offset,
},
}
}
params.offset = offset
return nil
}

View File

@ -0,0 +1,40 @@
package domain
import (
"errors"
"net/url"
)
type SyncServersCmdPayload struct {
versionCode string
url *url.URL
}
func NewSyncServersCmdPayload(versionCode string, u *url.URL) (SyncServersCmdPayload, error) {
if versionCode == "" {
return SyncServersCmdPayload{}, errors.New("version code can't be blank")
}
if u == nil {
return SyncServersCmdPayload{}, errors.New("url can't be nil")
}
return SyncServersCmdPayload{versionCode: versionCode, url: u}, nil
}
func NewSyncServersCmdPayloadWithStringURL(versionCode string, rawURL string) (SyncServersCmdPayload, error) {
u, err := parseURL(rawURL)
if err != nil {
return SyncServersCmdPayload{}, err
}
return NewSyncServersCmdPayload(versionCode, u)
}
func (p SyncServersCmdPayload) VersionCode() string {
return p.versionCode
}
func (p SyncServersCmdPayload) URL() *url.URL {
return p.url
}

View File

@ -1,10 +1,12 @@
package domain_test
import (
"fmt"
"net/url"
"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"
)
@ -100,6 +102,255 @@ func TestNewSyncServersCmdPayloadWithStringURL(t *testing.T) {
}
}
func TestNewCreateServerParams(t *testing.T) {
t.Parallel()
validVersion := domaintest.NewVersion(t, domaintest.VersionConfig{})
validBaseServer := domaintest.NewBaseServer(t, domaintest.BaseServerConfig{})
type args struct {
servers domain.BaseServers
versionCode string
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
versionCode: validVersion.Code(),
servers: domain.BaseServers{
validBaseServer,
},
},
},
}
for _, versionCodeTest := range newVersionCodeValidationTests() {
tests = append(tests, test{
name: versionCodeTest.name,
args: args{
versionCode: versionCodeTest.code,
servers: domain.BaseServers{
validBaseServer,
},
},
expectedErr: domain.ValidationError{
Err: versionCodeTest.expectedErr,
Field: "versionCode",
},
})
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
res, err := domain.NewCreateServerParams(tt.args.servers, tt.args.versionCode)
require.ErrorIs(t, err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
require.Len(t, res, len(tt.args.servers))
for i, b := range tt.args.servers {
assert.Equal(t, b, res[i].Base())
assert.Equal(t, tt.args.versionCode, res[i].VersionCode())
}
})
}
}
func TestListServersParams_SetSort(t *testing.T) {
t.Parallel()
type args struct {
sort []domain.ServerSort
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
sort: []domain.ServerSort{
domain.ServerSortKeyASC,
},
},
},
{
name: "ERR: len(sort) < 1",
args: args{
sort: nil,
},
expectedErr: domain.ValidationError{
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 2,
Current: 0,
},
},
},
{
name: "ERR: len(sort) > 2",
args: args{
sort: []domain.ServerSort{
domain.ServerSortKeyASC,
domain.ServerSortKeyDESC,
domain.ServerSortOpenASC,
},
},
expectedErr: domain.ValidationError{
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.NewListServersParams()
require.ErrorIs(t, params.SetSort(tt.args.sort), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.sort, params.Sort())
})
}
}
func TestListServersParams_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.ServerListMaxLimit,
},
},
{
name: "ERR: limit < 1",
args: args{
limit: 0,
},
expectedErr: domain.ValidationError{
Field: "limit",
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
{
name: fmt.Sprintf("ERR: limit > %d", domain.ServerListMaxLimit),
args: args{
limit: domain.ServerListMaxLimit + 1,
},
expectedErr: domain.ValidationError{
Field: "limit",
Err: domain.MaxLessEqualError{
Max: domain.ServerListMaxLimit,
Current: domain.ServerListMaxLimit + 1,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListServersParams()
require.ErrorIs(t, params.SetLimit(tt.args.limit), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.limit, params.Limit())
})
}
}
func TestListServersParams_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{
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.NewListServersParams()
require.ErrorIs(t, params.SetOffset(tt.args.offset), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.offset, params.Offset())
})
}
}
type serverKeyValidationTest struct {
name string
key string
@ -110,7 +361,7 @@ func newServerKeyValidationTests() []serverKeyValidationTest {
return []serverKeyValidationTest{
{
name: "ERR: server key length < 1",
expectedErr: domain.LenError{
expectedErr: domain.LenOutOfRangeError{
Min: 1,
Max: 10,
},
@ -118,7 +369,7 @@ func newServerKeyValidationTests() []serverKeyValidationTest {
{
name: "ERR: server key length > 10",
key: "keykeykeyke",
expectedErr: domain.LenError{
expectedErr: domain.LenOutOfRangeError{
Min: 1,
Max: 10,
Current: len("keykeykeyke"),

View File

@ -0,0 +1,171 @@
package domain
import (
"errors"
"fmt"
"github.com/gosimple/slug"
)
type ValidationError struct {
Field string
Err error
}
var _ ErrorWithParams = ValidationError{}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Err)
}
func (e ValidationError) Code() ErrorCode {
var domainErr Error
if errors.As(e.Err, &domainErr) {
return domainErr.Code()
}
return ErrorCodeIncorrectInput
}
func (e ValidationError) Slug() string {
s := "validation"
var domainErr Error
if errors.As(e.Err, &domainErr) {
s = domainErr.Slug()
}
return fmt.Sprintf("%s-%s", slug.Make(e.Field), s)
}
func (e ValidationError) Params() map[string]any {
var withParams ErrorWithParams
ok := errors.As(e.Err, &withParams)
if !ok {
return nil
}
return withParams.Params()
}
type MinGreaterEqualError struct {
Min int
Current int
}
var _ ErrorWithParams = MinGreaterEqualError{}
func (e MinGreaterEqualError) Error() string {
return fmt.Sprintf("must be no less than %d (current: %d)", e.Min, e.Current)
}
func (e MinGreaterEqualError) Code() ErrorCode {
return ErrorCodeIncorrectInput
}
func (e MinGreaterEqualError) Slug() string {
return "min-greater-equal"
}
func (e MinGreaterEqualError) Params() map[string]any {
return map[string]any{
"Min": e.Min,
"Current": e.Current,
}
}
type MaxLessEqualError struct {
Max int
Current int
}
var _ ErrorWithParams = MaxLessEqualError{}
func (e MaxLessEqualError) Error() string {
return fmt.Sprintf("must be no greater than %d (current: %d)", e.Max, e.Current)
}
func (e MaxLessEqualError) Code() ErrorCode {
return ErrorCodeIncorrectInput
}
func (e MaxLessEqualError) Slug() string {
return "max-less-equal"
}
func (e MaxLessEqualError) Params() map[string]any {
return map[string]any{
"Max": e.Max,
"Current": e.Current,
}
}
type LenOutOfRangeError struct {
Min int
Max int
Current int
}
var _ ErrorWithParams = LenOutOfRangeError{}
func (e LenOutOfRangeError) Error() string {
return fmt.Sprintf("length must be between %d and %d (current length: %d)", e.Min, e.Max, e.Current)
}
func (e LenOutOfRangeError) Code() ErrorCode {
return ErrorCodeIncorrectInput
}
func (e LenOutOfRangeError) Slug() string {
return "length-out-of-range"
}
func (e LenOutOfRangeError) Params() map[string]any {
return map[string]any{
"Min": e.Min,
"Max": e.Max,
"Current": e.Current,
}
}
func validateSliceLen[S ~[]E, E any](s S, min, max int) error {
if l := len(s); l > max || l < min {
return LenOutOfRangeError{
Min: min,
Max: max,
Current: l,
}
}
return nil
}
const (
serverKeyMinLength = 1
serverKeyMaxLength = 10
)
func validateServerKey(key string) error {
if l := len(key); l < serverKeyMinLength || l > serverKeyMaxLength {
return LenOutOfRangeError{
Min: serverKeyMinLength,
Max: serverKeyMaxLength,
Current: l,
}
}
return nil
}
const (
versionCodeMinLength = 2
versionCodeMaxLength = 2
)
func validateVersionCode(code string) error {
if l := len(code); l < versionCodeMinLength || l > versionCodeMaxLength {
return LenOutOfRangeError{
Min: versionCodeMinLength,
Max: versionCodeMaxLength,
Current: l,
}
}
return nil
}

View File

@ -1,62 +0,0 @@
package domain
import (
"fmt"
)
type LenError struct {
Min int
Max int
Current int
}
var _ ErrorWithParams = LenError{}
func (e LenError) Error() string {
return fmt.Sprintf("length must be between %d and %d (current length: %d)", e.Min, e.Max, e.Current)
}
func (e LenError) Code() ErrorCode {
return ErrorCodeIncorrectInput
}
func (e LenError) Slug() string {
return "length"
}
func (e LenError) Params() map[string]any {
return map[string]any{
"Min": e.Min,
"Max": e.Max,
"Current": e.Current,
}
}
func validateSliceLen[S ~[]E, E any](s S, min, max int) error {
if l := len(s); l > max || l < min {
return LenError{
Min: min,
Max: max,
Current: l,
}
}
return nil
}
const (
serverKeyMinLength = 1
serverKeyMaxLength = 10
)
func validateServerKey(key string) error {
if l := len(key); l < serverKeyMinLength || l > serverKeyMaxLength {
return LenError{
Min: serverKeyMinLength,
Max: serverKeyMaxLength,
Current: l,
}
}
return nil
}

View File

@ -91,6 +91,10 @@ const (
versionSortMaxLength = 1
)
func (params *ListVersionsParams) Sort() []VersionSort {
return params.sort
}
func (params *ListVersionsParams) SetSort(sort []VersionSort) error {
if err := validateSliceLen(sort, versionSortMinLength, versionSortMaxLength); err != nil {
return ValidationError{
@ -103,7 +107,3 @@ func (params *ListVersionsParams) SetSort(sort []VersionSort) error {
return nil
}
func (params *ListVersionsParams) Sort() []VersionSort {
return params.sort
}

View File

@ -35,7 +35,7 @@ func TestListVersionsParams_SetSort(t *testing.T) {
},
expectedErr: domain.ValidationError{
Field: "sort",
Err: domain.LenError{
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1,
Current: 0,
@ -52,7 +52,7 @@ func TestListVersionsParams_SetSort(t *testing.T) {
},
expectedErr: domain.ValidationError{
Field: "sort",
Err: domain.LenError{
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1,
Current: 2,
@ -77,3 +77,32 @@ func TestListVersionsParams_SetSort(t *testing.T) {
})
}
}
type versionCodeValidationTest struct {
name string
code string
expectedErr error
}
func newVersionCodeValidationTests() []versionCodeValidationTest {
return []versionCodeValidationTest{
{
name: "ERR: version code length < 2",
code: "p",
expectedErr: domain.LenOutOfRangeError{
Min: 2,
Max: 2,
Current: len("p"),
},
},
{
name: "ERR: version code length > 2",
code: "pll",
expectedErr: domain.LenOutOfRangeError{
Min: 2,
Max: 2,
Current: len("pll"),
},
},
}
}

View File

@ -87,7 +87,7 @@ func testObserver(t *testing.T, newObserver func(t *testing.T) *healthfile.Obser
case <-observerStopped:
// OK
default:
t.Error("Run goroutine not stopped")
assert.Fail(t, "Run goroutine not stopped")
}
})