From b4e95f326722d7aec7e4203137de211d71605ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Sun, 24 Dec 2023 10:44:20 +0000 Subject: [PATCH] feat: add server repo (#7) Reviewed-on: https://gitea.dwysokinski.me/twhelp/corev3/pulls/7 --- go.mod | 1 + go.sum | 2 + internal/adapter/adaptertest/fixture.go | 47 +++ internal/adapter/adaptertest/sqlite.go | 7 +- internal/adapter/adaptertest/utils.go | 3 +- .../internal/bunmodel/building_info.go | 91 +++++ internal/adapter/internal/bunmodel/server.go | 97 +++++ .../internal/bunmodel/server_config.go | 207 ++++++++++ .../adapter/internal/bunmodel/unit_info.go | 75 ++++ internal/adapter/repository_bun_server.go | 119 ++++++ .../adapter/repository_bun_server_test.go | 29 ++ .../adapter/repository_bun_version_test.go | 13 +- internal/adapter/repository_server_test.go | 354 +++++++++++++++++ internal/adapter/repository_test.go | 41 ++ internal/adapter/repository_version_test.go | 20 +- internal/adapter/testdata/fixture.yml | 78 ++++ internal/domain/domaintest/base_server.go | 11 +- internal/domain/domaintest/server.go | 7 + internal/domain/domaintest/version.go | 33 ++ internal/domain/error.go | 44 --- internal/domain/null.go | 12 + internal/domain/server.go | 358 +++++++++++++++++- internal/domain/server_message_payloads.go | 40 ++ internal/domain/server_test.go | 255 ++++++++++++- internal/domain/validation.go | 171 +++++++++ internal/domain/validators.go | 62 --- internal/domain/version.go | 8 +- internal/domain/version_test.go | 33 +- internal/health/healthfile/observer_test.go | 2 +- 29 files changed, 2054 insertions(+), 166 deletions(-) create mode 100644 internal/adapter/adaptertest/fixture.go create mode 100644 internal/adapter/internal/bunmodel/building_info.go create mode 100644 internal/adapter/internal/bunmodel/server.go create mode 100644 internal/adapter/internal/bunmodel/server_config.go create mode 100644 internal/adapter/internal/bunmodel/unit_info.go create mode 100644 internal/adapter/repository_bun_server.go create mode 100644 internal/adapter/repository_bun_server_test.go create mode 100644 internal/adapter/repository_server_test.go create mode 100644 internal/adapter/repository_test.go create mode 100644 internal/adapter/testdata/fixture.yml create mode 100644 internal/domain/domaintest/server.go create mode 100644 internal/domain/domaintest/version.go create mode 100644 internal/domain/null.go create mode 100644 internal/domain/server_message_payloads.go create mode 100644 internal/domain/validation.go delete mode 100644 internal/domain/validators.go diff --git a/go.mod b/go.mod index 5d8a300..64ae77b 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 31c1af3..a9f9b1b 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/adapter/adaptertest/fixture.go b/internal/adapter/adaptertest/fixture.go new file mode 100644 index 0000000..ec2210d --- /dev/null +++ b/internal/adapter/adaptertest/fixture.go @@ -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 +} diff --git a/internal/adapter/adaptertest/sqlite.go b/internal/adapter/adaptertest/sqlite.go index 754f255..0a1311a 100644 --- a/internal/adapter/adaptertest/sqlite.go +++ b/internal/adapter/adaptertest/sqlite.go @@ -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()) diff --git a/internal/adapter/adaptertest/utils.go b/internal/adapter/adaptertest/utils.go index d9b2fdc..e6482d1 100644 --- a/internal/adapter/adaptertest/utils.go +++ b/internal/adapter/adaptertest/utils.go @@ -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") } diff --git a/internal/adapter/internal/bunmodel/building_info.go b/internal/adapter/internal/bunmodel/building_info.go new file mode 100644 index 0000000..cd116e5 --- /dev/null +++ b/internal/adapter/internal/bunmodel/building_info.go @@ -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 +} diff --git a/internal/adapter/internal/bunmodel/server.go b/internal/adapter/internal/bunmodel/server.go new file mode 100644 index 0000000..95fcfa5 --- /dev/null +++ b/internal/adapter/internal/bunmodel/server.go @@ -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 +} diff --git a/internal/adapter/internal/bunmodel/server_config.go b/internal/adapter/internal/bunmodel/server_config.go new file mode 100644 index 0000000..19b7760 --- /dev/null +++ b/internal/adapter/internal/bunmodel/server_config.go @@ -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 +} diff --git a/internal/adapter/internal/bunmodel/unit_info.go b/internal/adapter/internal/bunmodel/unit_info.go new file mode 100644 index 0000000..8708758 --- /dev/null +++ b/internal/adapter/internal/bunmodel/unit_info.go @@ -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 +} diff --git a/internal/adapter/repository_bun_server.go b/internal/adapter/repository_bun_server.go new file mode 100644 index 0000000..7a94c09 --- /dev/null +++ b/internal/adapter/repository_bun_server.go @@ -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 +} diff --git a/internal/adapter/repository_bun_server_test.go b/internal/adapter/repository_bun_server_test.go new file mode 100644 index 0000000..9d367bb --- /dev/null +++ b/internal/adapter/repository_bun_server_test.go @@ -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)) + }) +} diff --git a/internal/adapter/repository_bun_version_test.go b/internal/adapter/repository_bun_version_test.go index 053bca6..99d7561 100644 --- a/internal/adapter/repository_bun_version_test.go +++ b/internal/adapter/repository_bun_version_test.go @@ -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)) }) } diff --git a/internal/adapter/repository_server_test.go b/internal/adapter/repository_server_test.go new file mode 100644 index 0000000..db764c1 --- /dev/null +++ b/internal/adapter/repository_server_test.go @@ -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 +} diff --git a/internal/adapter/repository_test.go b/internal/adapter/repository_test.go new file mode 100644 index 0000000..4874558 --- /dev/null +++ b/internal/adapter/repository_test.go @@ -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), + } +} diff --git a/internal/adapter/repository_version_test.go b/internal/adapter/repository_version_test.go index a4cf60c..a3d1362 100644 --- a/internal/adapter/repository_version_test.go +++ b/internal/adapter/repository_version_test.go @@ -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) diff --git a/internal/adapter/testdata/fixture.yml b/internal/adapter/testdata/fixture.yml new file mode 100644 index 0000000..7a2523f --- /dev/null +++ b/internal/adapter/testdata/fixture.yml @@ -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 diff --git a/internal/domain/domaintest/base_server.go b/internal/domain/domaintest/base_server.go index 13d5552..91dbdfa 100644 --- a/internal/domain/domaintest/base_server.go +++ b/internal/domain/domaintest/base_server.go @@ -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 } diff --git a/internal/domain/domaintest/server.go b/internal/domain/domaintest/server.go new file mode 100644 index 0000000..90dc7bd --- /dev/null +++ b/internal/domain/domaintest/server.go @@ -0,0 +1,7 @@ +package domaintest + +import "github.com/brianvoe/gofakeit/v6" + +func RandServerKey() string { + return gofakeit.LetterN(5) +} diff --git a/internal/domain/domaintest/version.go b/internal/domain/domaintest/version.go new file mode 100644 index 0000000..81a4206 --- /dev/null +++ b/internal/domain/domaintest/version.go @@ -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) +} diff --git a/internal/domain/error.go b/internal/domain/error.go index 3cc8830..1bde5e3 100644 --- a/internal/domain/error.go +++ b/internal/domain/error.go @@ -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() -} diff --git a/internal/domain/null.go b/internal/domain/null.go new file mode 100644 index 0000000..d585c1f --- /dev/null +++ b/internal/domain/null.go @@ -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] diff --git a/internal/domain/server.go b/internal/domain/server.go index 4a01076..3da74eb 100644 --- a/internal/domain/server.go +++ b/internal/domain/server.go @@ -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 } diff --git a/internal/domain/server_message_payloads.go b/internal/domain/server_message_payloads.go new file mode 100644 index 0000000..4a01076 --- /dev/null +++ b/internal/domain/server_message_payloads.go @@ -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 +} diff --git a/internal/domain/server_test.go b/internal/domain/server_test.go index 20f85ad..807979c 100644 --- a/internal/domain/server_test.go +++ b/internal/domain/server_test.go @@ -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"), diff --git a/internal/domain/validation.go b/internal/domain/validation.go new file mode 100644 index 0000000..fbbf931 --- /dev/null +++ b/internal/domain/validation.go @@ -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 +} diff --git a/internal/domain/validators.go b/internal/domain/validators.go deleted file mode 100644 index 0f349db..0000000 --- a/internal/domain/validators.go +++ /dev/null @@ -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 -} diff --git a/internal/domain/version.go b/internal/domain/version.go index 308dfef..ce3cca3 100644 --- a/internal/domain/version.go +++ b/internal/domain/version.go @@ -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 -} diff --git a/internal/domain/version_test.go b/internal/domain/version_test.go index 6a19a58..654a207 100644 --- a/internal/domain/version_test.go +++ b/internal/domain/version_test.go @@ -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"), + }, + }, + } +} diff --git a/internal/health/healthfile/observer_test.go b/internal/health/healthfile/observer_test.go index c47c200..a2d5a1d 100644 --- a/internal/health/healthfile/observer_test.go +++ b/internal/health/healthfile/observer_test.go @@ -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") } })