feat: tribe consumer (#11)

Reviewed-on: twhelp/corev3#11
This commit is contained in:
Dawid Wysokiński 2023-12-27 08:07:40 +00:00
parent 206eed966e
commit b5b699ca49
37 changed files with 1671 additions and 263 deletions

View File

@ -64,6 +64,42 @@ var cmdConsumer = &cli.Command{
)
consumer.Register(router)
return nil
},
)
},
},
{
Name: "tribe",
Usage: "Run the worker responsible for consuming tribe-related messages",
Flags: concatSlices(dbFlags, rmqFlags, twSvcFlags),
Action: func(c *cli.Context) error {
return runConsumer(
c,
"TribeConsumer",
func(
c *cli.Context,
router *message.Router,
logger watermill.LoggerAdapter,
publisher *amqp.Publisher,
subscriber *amqp.Subscriber,
marshaler watermillmsg.Marshaler,
db *bun.DB,
) error {
twSvc, err := newTWServiceFromFlags(c)
if err != nil {
return err
}
consumer := port.NewTribeWatermillConsumer(
app.NewTribeService(twSvc),
subscriber,
logger,
marshaler,
c.String(rmqFlagTopicServerSyncedEvent.Name),
)
consumer.Register(router)
return nil
},
)

View File

@ -3,6 +3,7 @@ package adapter
import (
"context"
"fmt"
"net/url"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/tw"
@ -16,7 +17,7 @@ func NewTWHTTP(client *tw.Client) *TWHTTP {
return &TWHTTP{client: client}
}
func (t *TWHTTP) GetOpenServers(ctx context.Context, baseURL string) (domain.BaseServers, error) {
func (t *TWHTTP) GetOpenServers(ctx context.Context, baseURL *url.URL) (domain.BaseServers, error) {
servers, err := t.client.GetOpenServers(ctx, baseURL)
if err != nil {
return nil, err
@ -40,7 +41,7 @@ func (t *TWHTTP) convertServersToDomain(servers []tw.Server) (domain.BaseServers
return res, nil
}
func (t *TWHTTP) GetServerConfig(ctx context.Context, baseURL string) (domain.ServerConfig, error) {
func (t *TWHTTP) GetServerConfig(ctx context.Context, baseURL *url.URL) (domain.ServerConfig, error) {
cfg, err := t.client.GetServerConfig(ctx, baseURL)
if err != nil {
return domain.ServerConfig{}, err
@ -92,7 +93,7 @@ func (t *TWHTTP) GetServerConfig(ctx context.Context, baseURL string) (domain.Se
return res, nil
}
func (t *TWHTTP) GetUnitInfo(ctx context.Context, baseURL string) (domain.UnitInfo, error) {
func (t *TWHTTP) GetUnitInfo(ctx context.Context, baseURL *url.URL) (domain.UnitInfo, error) {
info, err := t.client.GetUnitInfo(ctx, baseURL)
if err != nil {
return domain.UnitInfo{}, err
@ -120,7 +121,7 @@ func (t *TWHTTP) GetUnitInfo(ctx context.Context, baseURL string) (domain.UnitIn
return res, nil
}
func (t *TWHTTP) GetBuildingInfo(ctx context.Context, baseURL string) (domain.BuildingInfo, error) {
func (t *TWHTTP) GetBuildingInfo(ctx context.Context, baseURL *url.URL) (domain.BuildingInfo, error) {
info, err := t.client.GetBuildingInfo(ctx, baseURL)
if err != nil {
return domain.BuildingInfo{}, err
@ -151,3 +152,52 @@ func (t *TWHTTP) GetBuildingInfo(ctx context.Context, baseURL string) (domain.Bu
return res, nil
}
func (t *TWHTTP) GetTribes(ctx context.Context, baseURL *url.URL) (domain.BaseTribes, error) {
tribes, err := t.client.GetTribes(ctx, baseURL)
if err != nil {
return nil, err
}
return t.convertTribesToDomain(tribes)
}
func (t *TWHTTP) convertTribesToDomain(tribes []tw.Tribe) (domain.BaseTribes, error) {
res := make(domain.BaseTribes, 0, len(tribes))
for _, tr := range tribes {
od, err := domain.NewOpponentsDefeated(
tr.OpponentsDefeated.RankAtt,
tr.OpponentsDefeated.ScoreAtt,
tr.OpponentsDefeated.RankDef,
tr.OpponentsDefeated.ScoreDef,
tr.OpponentsDefeated.RankSup,
tr.OpponentsDefeated.ScoreSup,
tr.OpponentsDefeated.RankTotal,
tr.OpponentsDefeated.ScoreTotal,
)
if err != nil {
return nil, fmt.Errorf("couldn't construct domain.OpponentsDefeated: %w", err)
}
converted, err := domain.NewBaseTribe(
tr.ID,
tr.Name,
tr.Tag,
tr.NumMembers,
tr.NumVillages,
tr.Points,
tr.AllPoints,
tr.Rank,
od,
tr.ProfileURL,
)
if err != nil {
return nil, fmt.Errorf("couldn't construct domain.BaseTribe: %w", err)
}
res = append(res, converted)
}
return res, nil
}

View File

@ -36,7 +36,7 @@ func (pub *ServerWatermillPublisher) CmdSync(ctx context.Context, payloads ...do
for _, p := range payloads {
msg, err := pub.marshaler.Marshal(ctx, watermillmsg.SyncServersCmdPayload{
VersionCode: p.VersionCode(),
URL: p.URL().String(),
URL: p.URL(),
})
if err != nil {
return fmt.Errorf("%s: couldn't marshal SyncServersCmdPayload: %w", p.VersionCode(), err)
@ -62,7 +62,7 @@ func (pub *ServerWatermillPublisher) EventSynced(
msg, err := pub.marshaler.Marshal(ctx, watermillmsg.ServerSyncedEventPayload{
Key: p.Key(),
VersionCode: p.VersionCode(),
URL: p.URL().String(),
URL: p.URL(),
})
if err != nil {
return fmt.Errorf("%s: couldn't marshal ServerSyncedEventPayload: %w", p.Key(), err)

View File

@ -26,7 +26,7 @@ func NewServerService(repo ServerRepository, twSvc TWService, publisher ServerPu
func (svc *ServerService) Sync(ctx context.Context, payload domain.SyncServersCmdPayload) error {
versionCode := payload.VersionCode()
openServers, err := svc.twSvc.GetOpenServers(ctx, payload.URL().String())
openServers, err := svc.twSvc.GetOpenServers(ctx, payload.URL())
if err != nil {
return fmt.Errorf("couldn't get open servers for version code %s: %w", versionCode, err)
}
@ -138,19 +138,19 @@ func (svc *ServerService) ListAll(ctx context.Context, params domain.ListServers
}
func (svc *ServerService) SyncConfigAndInfo(ctx context.Context, payload domain.ServerSyncedEventPayload) error {
url := payload.URL().String()
u := payload.URL()
cfg, err := svc.twSvc.GetServerConfig(ctx, url)
cfg, err := svc.twSvc.GetServerConfig(ctx, u)
if err != nil {
return fmt.Errorf("couldn't get server config for server %s: %w", payload.Key(), err)
}
buildingInfo, err := svc.twSvc.GetBuildingInfo(ctx, url)
buildingInfo, err := svc.twSvc.GetBuildingInfo(ctx, u)
if err != nil {
return fmt.Errorf("couldn't get building info for server %s: %w", payload.Key(), err)
}
unitInfo, err := svc.twSvc.GetUnitInfo(ctx, url)
unitInfo, err := svc.twSvc.GetUnitInfo(ctx, u)
if err != nil {
return fmt.Errorf("couldn't get unit info for server %s: %w", payload.Key(), err)
}

View File

@ -0,0 +1,27 @@
package app
import (
"context"
"fmt"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
)
type TribeService struct {
twSvc TWService
}
func NewTribeService(twSvc TWService) *TribeService {
return &TribeService{twSvc: twSvc}
}
func (svc *TribeService) Sync(ctx context.Context, payload domain.ServerSyncedEventPayload) error {
tribes, err := svc.twSvc.GetTribes(ctx, payload.URL())
if err != nil {
return fmt.Errorf("couldn't get tribes for server %s: %w", payload.Key(), err)
}
fmt.Println(payload.URL(), len(tribes))
return nil
}

View File

@ -2,13 +2,15 @@ package app
import (
"context"
"net/url"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
)
type TWService interface {
GetOpenServers(ctx context.Context, baseURL string) (domain.BaseServers, error)
GetServerConfig(ctx context.Context, baseURL string) (domain.ServerConfig, error)
GetUnitInfo(ctx context.Context, baseURL string) (domain.UnitInfo, error)
GetBuildingInfo(ctx context.Context, baseURL string) (domain.BuildingInfo, error)
GetOpenServers(ctx context.Context, baseURL *url.URL) (domain.BaseServers, error)
GetServerConfig(ctx context.Context, baseURL *url.URL) (domain.ServerConfig, error)
GetUnitInfo(ctx context.Context, baseURL *url.URL) (domain.UnitInfo, error)
GetBuildingInfo(ctx context.Context, baseURL *url.URL) (domain.BuildingInfo, error)
GetTribes(ctx context.Context, baseURL *url.URL) (domain.BaseTribes, error)
}

View File

@ -11,19 +11,22 @@ type BaseServer struct {
open bool
}
func NewBaseServer(key, rawURL string, open bool) (BaseServer, error) {
const baseServerModelName = "BaseServer"
func NewBaseServer(key string, u *url.URL, open bool) (BaseServer, error) {
if err := validateServerKey(key); err != nil {
return BaseServer{}, ValidationError{
Err: err,
Model: baseServerModelName,
Field: "key",
Err: err,
}
}
u, err := parseURL(rawURL)
if err != nil {
if u == nil {
return BaseServer{}, ValidationError{
Err: err,
Model: baseServerModelName,
Field: "url",
Err: ErrNotNil,
}
}

View File

@ -1,6 +1,7 @@
package domain_test
import (
"net/url"
"slices"
"testing"
@ -17,7 +18,7 @@ func TestNewBaseServer(t *testing.T) {
type args struct {
key string
url string
url *url.URL
open bool
}
@ -32,20 +33,21 @@ func TestNewBaseServer(t *testing.T) {
name: "OK",
args: args{
key: validBaseServer.Key(),
url: validBaseServer.URL().String(),
url: validBaseServer.URL(),
open: validBaseServer.Open(),
},
},
{
name: "ERR: invalid URL",
name: "ERR: url can't be nil",
args: args{
key: validBaseServer.Key(),
url: " ",
url: nil,
open: validBaseServer.Open(),
},
expectedErr: domain.ValidationError{
Err: domain.InvalidURLError{URL: " "},
Model: "BaseServer",
Field: "url",
Err: domain.ErrNotNil,
},
},
}
@ -55,12 +57,13 @@ func TestNewBaseServer(t *testing.T) {
name: serverKeyTest.name,
args: args{
key: serverKeyTest.key,
url: validBaseServer.URL().String(),
url: validBaseServer.URL(),
open: validBaseServer.Open(),
},
expectedErr: domain.ValidationError{
Err: serverKeyTest.expectedErr,
Model: "BaseServer",
Field: "key",
Err: serverKeyTest.expectedErr,
},
})
}
@ -77,7 +80,7 @@ func TestNewBaseServer(t *testing.T) {
return
}
assert.Equal(t, tt.args.key, res.Key())
assert.Equal(t, tt.args.url, res.URL().String())
assert.Equal(t, tt.args.url, res.URL())
assert.Equal(t, tt.args.open, res.Open())
})
}

View File

@ -0,0 +1,188 @@
package domain
import "net/url"
type BaseTribe struct {
id int
name string
tag string
numMembers int
numVillages int
points int
allPoints int
rank int
od OpponentsDefeated
profileURL *url.URL
}
const baseTribeModelName = "BaseTribe"
//nolint:gocyclo
func NewBaseTribe(
id int,
name string,
tag string,
numMembers int,
numVillages int,
points int,
allPoints int,
rank int,
od OpponentsDefeated,
profileURL *url.URL,
) (BaseTribe, error) {
if id < 1 {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "id",
Err: MinGreaterEqualError{
Min: 1,
Current: id,
},
}
}
if l := len(name); l < tribeNameMinLength || l > tribeNameMaxLength {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "name",
Err: LenOutOfRangeError{
Min: tribeNameMinLength,
Max: tribeNameMaxLength,
Current: l,
},
}
}
// we convert tag to []rune to get the correct length
// e.g. for tags such as Орловы, which takes 12 bytes rather than 6
// explanation: https://golangbyexample.com/number-characters-string-golang/
if l := len([]rune(tag)); l < tribeTagMinLength || l > tribeTagMaxLength {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "tag",
Err: LenOutOfRangeError{
Min: tribeTagMinLength,
Max: tribeTagMaxLength,
Current: l,
},
}
}
if numMembers < 0 {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "numMembers",
Err: MinGreaterEqualError{
Min: 0,
Current: numMembers,
},
}
}
if numVillages < 0 {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "numVillages",
Err: MinGreaterEqualError{
Min: 0,
Current: numVillages,
},
}
}
if points < 0 {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "points",
Err: MinGreaterEqualError{
Min: 0,
Current: points,
},
}
}
if allPoints < 0 {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "allPoints",
Err: MinGreaterEqualError{
Min: 0,
Current: allPoints,
},
}
}
if rank < 0 {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "rank",
Err: MinGreaterEqualError{
Min: 0,
Current: rank,
},
}
}
if profileURL == nil {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "profileURL",
Err: ErrNotNil,
}
}
return BaseTribe{
id: id,
name: name,
tag: tag,
numMembers: numMembers,
numVillages: numVillages,
points: points,
allPoints: allPoints,
rank: rank,
od: od,
profileURL: profileURL,
}, nil
}
func (b BaseTribe) ID() int {
return b.id
}
func (b BaseTribe) Name() string {
return b.name
}
func (b BaseTribe) Tag() string {
return b.tag
}
func (b BaseTribe) NumMembers() int {
return b.numMembers
}
func (b BaseTribe) NumVillages() int {
return b.numVillages
}
func (b BaseTribe) Points() int {
return b.points
}
func (b BaseTribe) AllPoints() int {
return b.allPoints
}
func (b BaseTribe) Rank() int {
return b.rank
}
func (b BaseTribe) OD() OpponentsDefeated {
return b.od
}
func (b BaseTribe) ProfileURL() *url.URL {
return b.profileURL
}
type BaseTribes []BaseTribe

View File

@ -0,0 +1,357 @@
package domain_test
import (
"net/url"
"testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewBaseTribe(t *testing.T) {
t.Parallel()
validBaseTribe := domaintest.NewBaseTribe(t)
type args struct {
id int
name string
tag string
numMembers int
numVillages int
points int
allPoints int
rank int
od domain.OpponentsDefeated
profileURL *url.URL
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
id: validBaseTribe.ID(),
name: validBaseTribe.Name(),
tag: validBaseTribe.Tag(),
numMembers: validBaseTribe.NumMembers(),
numVillages: validBaseTribe.NumVillages(),
points: validBaseTribe.Points(),
allPoints: validBaseTribe.AllPoints(),
rank: validBaseTribe.Rank(),
od: validBaseTribe.OD(),
profileURL: validBaseTribe.ProfileURL(),
},
},
{
name: "OK: tag/name with non-ascii characters",
args: args{
id: validBaseTribe.ID(),
name: "Шубутные",
tag: "Орловы",
numMembers: validBaseTribe.NumMembers(),
numVillages: validBaseTribe.NumVillages(),
points: validBaseTribe.Points(),
allPoints: validBaseTribe.AllPoints(),
rank: validBaseTribe.Rank(),
od: validBaseTribe.OD(),
profileURL: validBaseTribe.ProfileURL(),
},
},
{
name: "ERR: id < 1",
args: args{
id: 0,
name: validBaseTribe.Name(),
tag: validBaseTribe.Tag(),
numMembers: validBaseTribe.NumMembers(),
numVillages: validBaseTribe.NumVillages(),
points: validBaseTribe.Points(),
allPoints: validBaseTribe.AllPoints(),
rank: validBaseTribe.Rank(),
od: validBaseTribe.OD(),
profileURL: validBaseTribe.ProfileURL(),
},
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "id",
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
{
name: "ERR: len(name) < 1",
args: args{
id: validBaseTribe.ID(),
name: "",
tag: validBaseTribe.Tag(),
numMembers: validBaseTribe.NumMembers(),
numVillages: validBaseTribe.NumVillages(),
points: validBaseTribe.Points(),
allPoints: validBaseTribe.AllPoints(),
rank: validBaseTribe.Rank(),
od: validBaseTribe.OD(),
profileURL: validBaseTribe.ProfileURL(),
},
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "name",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 255,
Current: 0,
},
},
},
{
name: "ERR: len(name) > 255",
args: args{
id: validBaseTribe.ID(),
name: gofakeit.LetterN(256),
tag: validBaseTribe.Tag(),
numMembers: validBaseTribe.NumMembers(),
numVillages: validBaseTribe.NumVillages(),
points: validBaseTribe.Points(),
allPoints: validBaseTribe.AllPoints(),
rank: validBaseTribe.Rank(),
od: validBaseTribe.OD(),
profileURL: validBaseTribe.ProfileURL(),
},
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "name",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 255,
Current: 256,
},
},
},
{
name: "ERR: len(tag) < 1",
args: args{
id: validBaseTribe.ID(),
name: validBaseTribe.Tag(),
tag: "",
numMembers: validBaseTribe.NumMembers(),
numVillages: validBaseTribe.NumVillages(),
points: validBaseTribe.Points(),
allPoints: validBaseTribe.AllPoints(),
rank: validBaseTribe.Rank(),
od: validBaseTribe.OD(),
profileURL: validBaseTribe.ProfileURL(),
},
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "tag",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 10,
Current: 0,
},
},
},
{
name: "ERR: len(tag) > 10",
args: args{
id: validBaseTribe.ID(),
name: validBaseTribe.Name(),
tag: gofakeit.LetterN(11),
numMembers: validBaseTribe.NumMembers(),
numVillages: validBaseTribe.NumVillages(),
points: validBaseTribe.Points(),
allPoints: validBaseTribe.AllPoints(),
rank: validBaseTribe.Rank(),
od: validBaseTribe.OD(),
profileURL: validBaseTribe.ProfileURL(),
},
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "tag",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 10,
Current: 11,
},
},
},
{
name: "ERR: numMembers < 0",
args: args{
id: validBaseTribe.ID(),
name: validBaseTribe.Name(),
tag: validBaseTribe.Tag(),
numMembers: -1,
numVillages: validBaseTribe.NumVillages(),
points: validBaseTribe.Points(),
allPoints: validBaseTribe.AllPoints(),
rank: validBaseTribe.Rank(),
od: validBaseTribe.OD(),
profileURL: validBaseTribe.ProfileURL(),
},
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "numMembers",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: numVillages < 0",
args: args{
id: validBaseTribe.ID(),
name: validBaseTribe.Name(),
tag: validBaseTribe.Tag(),
numMembers: validBaseTribe.NumMembers(),
numVillages: -1,
points: validBaseTribe.Points(),
allPoints: validBaseTribe.AllPoints(),
rank: validBaseTribe.Rank(),
od: validBaseTribe.OD(),
profileURL: validBaseTribe.ProfileURL(),
},
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "numVillages",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: points < 0",
args: args{
id: validBaseTribe.ID(),
name: validBaseTribe.Name(),
tag: validBaseTribe.Tag(),
numMembers: validBaseTribe.NumMembers(),
numVillages: validBaseTribe.NumVillages(),
points: -1,
allPoints: validBaseTribe.AllPoints(),
rank: validBaseTribe.Rank(),
od: validBaseTribe.OD(),
profileURL: validBaseTribe.ProfileURL(),
},
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "points",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: allPoints < 0",
args: args{
id: validBaseTribe.ID(),
name: validBaseTribe.Name(),
tag: validBaseTribe.Tag(),
numMembers: validBaseTribe.NumMembers(),
numVillages: validBaseTribe.NumVillages(),
points: validBaseTribe.Points(),
allPoints: -1,
rank: validBaseTribe.Rank(),
od: validBaseTribe.OD(),
profileURL: validBaseTribe.ProfileURL(),
},
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "allPoints",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: rank < 0",
args: args{
id: validBaseTribe.ID(),
name: validBaseTribe.Name(),
tag: validBaseTribe.Tag(),
numMembers: validBaseTribe.NumMembers(),
numVillages: validBaseTribe.NumVillages(),
points: validBaseTribe.Points(),
allPoints: validBaseTribe.AllPoints(),
rank: -1,
od: validBaseTribe.OD(),
profileURL: validBaseTribe.ProfileURL(),
},
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "rank",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: profileURL can't be nil",
args: args{
id: validBaseTribe.ID(),
name: validBaseTribe.Name(),
tag: validBaseTribe.Tag(),
numMembers: validBaseTribe.NumMembers(),
numVillages: validBaseTribe.NumVillages(),
points: validBaseTribe.Points(),
allPoints: validBaseTribe.AllPoints(),
rank: validBaseTribe.Rank(),
od: validBaseTribe.OD(),
profileURL: nil,
},
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "profileURL",
Err: domain.ErrNotNil,
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
res, err := domain.NewBaseTribe(
tt.args.id,
tt.args.name,
tt.args.tag,
tt.args.numMembers,
tt.args.numVillages,
tt.args.points,
tt.args.allPoints,
tt.args.rank,
tt.args.od,
tt.args.profileURL,
)
require.ErrorIs(t, err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.id, res.ID())
assert.Equal(t, tt.args.name, res.Name())
assert.Equal(t, tt.args.tag, res.Tag())
assert.Equal(t, tt.args.numMembers, res.NumMembers())
assert.Equal(t, tt.args.numVillages, res.NumVillages())
assert.Equal(t, tt.args.points, res.Points())
assert.Equal(t, tt.args.allPoints, res.AllPoints())
assert.Equal(t, tt.args.rank, res.Rank())
assert.Equal(t, tt.args.od, res.OD())
assert.Equal(t, tt.args.profileURL, res.ProfileURL())
})
}
}

View File

@ -33,7 +33,7 @@ func NewBaseServer(tb TestingTB, opts ...func(cfg *BaseServerConfig)) domain.Bas
}
}
s, err := domain.NewBaseServer(cfg.Key, cfg.URL.String(), cfg.Open)
s, err := domain.NewBaseServer(cfg.Key, cfg.URL, cfg.Open)
require.NoError(tb, err)
return s

View File

@ -0,0 +1,48 @@
package domaintest
import (
"net/url"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/require"
)
type BaseTribeConfig struct {
ID int
Tag string
OD domain.OpponentsDefeated
}
func NewBaseTribe(tb TestingTB, opts ...func(cfg *BaseTribeConfig)) domain.BaseTribe {
tb.Helper()
cfg := &BaseTribeConfig{
ID: RandID(),
Tag: RandTribeTag(),
OD: NewOpponentsDefeated(tb),
}
for _, opt := range opts {
opt(cfg)
}
u, err := url.ParseRequestURI(gofakeit.URL())
require.NoError(tb, err)
t, err := domain.NewBaseTribe(
cfg.ID,
gofakeit.LetterN(50),
cfg.Tag,
gofakeit.IntRange(1, 10000),
gofakeit.IntRange(1, 10000),
gofakeit.IntRange(1, 10000),
gofakeit.IntRange(1, 10000),
gofakeit.IntRange(1, 10000),
cfg.OD,
u,
)
require.NoError(tb, err)
return t
}

View File

@ -0,0 +1,23 @@
package domaintest
import (
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/require"
)
func NewOpponentsDefeated(tb TestingTB) domain.OpponentsDefeated {
tb.Helper()
cfg, err := domain.NewOpponentsDefeated(
gofakeit.IntRange(1, 100),
gofakeit.IntRange(100000, 1000000),
gofakeit.IntRange(1, 100),
gofakeit.IntRange(100000, 1000000),
gofakeit.IntRange(1, 100),
gofakeit.IntRange(100000, 1000000),
gofakeit.IntRange(1, 100),
gofakeit.IntRange(100000, 1000000),
)
require.NoError(tb, err)
return cfg
}

View File

@ -7,6 +7,7 @@ import (
)
func NewServerConfig(tb TestingTB) domain.ServerConfig {
tb.Helper()
cfg, err := domain.NewServerConfig(
gofakeit.Float64Range(1, 1000),
gofakeit.Float64Range(1, 1000),

View File

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

View File

@ -7,6 +7,7 @@ import (
)
func NewUnitInfo(tb TestingTB) domain.UnitInfo {
tb.Helper()
unitInfo, err := domain.NewUnitInfo(
domain.Unit{Speed: gofakeit.Float64Range(1, 25)},
domain.Unit{Pop: gofakeit.IntRange(1, 3000)},

View File

@ -0,0 +1,7 @@
package domaintest
import "github.com/brianvoe/gofakeit/v6"
func RandID() int {
return gofakeit.IntRange(1, 10000000)
}

View File

@ -4,14 +4,14 @@ type ErrorCode uint8
const (
ErrorCodeUnknown ErrorCode = iota
ErrorCodeEntityNotFound
ErrorCodeNotFound
ErrorCodeIncorrectInput
)
func (e ErrorCode) String() string {
switch e {
case ErrorCodeEntityNotFound:
return "entity-not-found"
case ErrorCodeNotFound:
return "not-found"
case ErrorCodeIncorrectInput:
return "incorrect-input"
case ErrorCodeUnknown:
@ -31,3 +31,23 @@ type ErrorWithParams interface {
Error
Params() map[string]any
}
type simpleError struct {
msg string
code ErrorCode
slug string
}
var _ Error = simpleError{}
func (e simpleError) Error() string {
return e.msg
}
func (e simpleError) Code() ErrorCode {
return e.code
}
func (e simpleError) Slug() string {
return e.slug
}

View File

@ -0,0 +1,156 @@
package domain
type OpponentsDefeated struct {
rankAtt int
scoreAtt int
rankDef int
scoreDef int
rankSup int
scoreSup int
rankTotal int
scoreTotal int
}
const opponentsDefeatedModelName = "OpponentsDefeated"
func NewOpponentsDefeated(
rankAtt int,
scoreAtt int,
rankDef int,
scoreDef int,
rankSup int,
scoreSup int,
rankTotal int,
scoreTotal int,
) (OpponentsDefeated, error) {
if rankAtt < 0 {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "rankAtt",
Err: MinGreaterEqualError{
Min: 0,
Current: rankAtt,
},
}
}
if scoreAtt < 0 {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "scoreAtt",
Err: MinGreaterEqualError{
Min: 0,
Current: scoreAtt,
},
}
}
if rankDef < 0 {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "rankDef",
Err: MinGreaterEqualError{
Min: 0,
Current: rankDef,
},
}
}
if scoreDef < 0 {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "scoreDef",
Err: MinGreaterEqualError{
Min: 0,
Current: scoreDef,
},
}
}
if rankSup < 0 {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "rankSup",
Err: MinGreaterEqualError{
Min: 0,
Current: rankSup,
},
}
}
if scoreSup < 0 {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "scoreSup",
Err: MinGreaterEqualError{
Min: 0,
Current: scoreSup,
},
}
}
if rankTotal < 0 {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "rankTotal",
Err: MinGreaterEqualError{
Min: 0,
Current: rankTotal,
},
}
}
if scoreTotal < 0 {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "scoreTotal",
Err: MinGreaterEqualError{
Min: 0,
Current: scoreTotal,
},
}
}
return OpponentsDefeated{
rankAtt: rankAtt,
scoreAtt: scoreAtt,
rankDef: rankDef,
scoreDef: scoreDef,
rankSup: rankSup,
scoreSup: scoreSup,
rankTotal: rankTotal,
scoreTotal: scoreTotal,
}, nil
}
func (o OpponentsDefeated) RankAtt() int {
return o.rankAtt
}
func (o OpponentsDefeated) ScoreAtt() int {
return o.scoreAtt
}
func (o OpponentsDefeated) RankDef() int {
return o.rankDef
}
func (o OpponentsDefeated) ScoreDef() int {
return o.scoreDef
}
func (o OpponentsDefeated) RankSup() int {
return o.rankSup
}
func (o OpponentsDefeated) ScoreSup() int {
return o.scoreSup
}
func (o OpponentsDefeated) RankTotal() int {
return o.rankTotal
}
func (o OpponentsDefeated) ScoreTotal() int {
return o.scoreTotal
}

View File

@ -0,0 +1,246 @@
package domain_test
import (
"testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewOpponentsDefeated(t *testing.T) {
t.Parallel()
validOD := domaintest.NewOpponentsDefeated(t)
type args struct {
rankAtt int
scoreAtt int
rankDef int
scoreDef int
rankSup int
scoreSup int
rankTotal int
scoreTotal int
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
rankAtt: validOD.RankAtt(),
scoreAtt: validOD.ScoreAtt(),
rankDef: validOD.RankDef(),
scoreDef: validOD.ScoreDef(),
rankSup: validOD.RankSup(),
scoreSup: validOD.ScoreSup(),
rankTotal: validOD.RankTotal(),
scoreTotal: validOD.ScoreTotal(),
},
},
{
name: "ERR: rankAtt < 0",
args: args{
rankAtt: -1,
scoreAtt: validOD.ScoreAtt(),
rankDef: validOD.RankDef(),
scoreDef: validOD.ScoreDef(),
rankSup: validOD.RankSup(),
scoreSup: validOD.ScoreSup(),
rankTotal: validOD.RankTotal(),
scoreTotal: validOD.ScoreTotal(),
},
expectedErr: domain.ValidationError{
Model: "OpponentsDefeated",
Field: "rankAtt",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: scoreAtt < 0",
args: args{
rankAtt: validOD.RankAtt(),
scoreAtt: -1,
rankDef: validOD.RankDef(),
scoreDef: validOD.ScoreDef(),
rankSup: validOD.RankSup(),
scoreSup: validOD.ScoreSup(),
rankTotal: validOD.RankTotal(),
scoreTotal: validOD.ScoreTotal(),
},
expectedErr: domain.ValidationError{
Model: "OpponentsDefeated",
Field: "scoreAtt",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: rankDef < 0",
args: args{
rankAtt: validOD.RankAtt(),
scoreAtt: validOD.ScoreAtt(),
rankDef: -1,
scoreDef: validOD.ScoreDef(),
rankSup: validOD.RankSup(),
scoreSup: validOD.ScoreSup(),
rankTotal: validOD.RankTotal(),
scoreTotal: validOD.ScoreTotal(),
},
expectedErr: domain.ValidationError{
Model: "OpponentsDefeated",
Field: "rankDef",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: scoreDef < 0",
args: args{
rankAtt: validOD.RankAtt(),
scoreAtt: validOD.ScoreAtt(),
rankDef: validOD.RankDef(),
scoreDef: -1,
rankSup: validOD.RankSup(),
scoreSup: validOD.ScoreSup(),
rankTotal: validOD.RankTotal(),
scoreTotal: validOD.ScoreTotal(),
},
expectedErr: domain.ValidationError{
Model: "OpponentsDefeated",
Field: "scoreDef",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: rankSup < 0",
args: args{
rankAtt: validOD.RankAtt(),
scoreAtt: validOD.ScoreAtt(),
rankDef: validOD.RankDef(),
scoreDef: validOD.ScoreDef(),
rankSup: -1,
scoreSup: validOD.ScoreSup(),
rankTotal: validOD.RankTotal(),
scoreTotal: validOD.ScoreTotal(),
},
expectedErr: domain.ValidationError{
Model: "OpponentsDefeated",
Field: "rankSup",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: scoreSup < 0",
args: args{
rankAtt: validOD.RankAtt(),
scoreAtt: validOD.ScoreAtt(),
rankDef: validOD.RankDef(),
scoreDef: validOD.ScoreDef(),
rankSup: validOD.RankSup(),
scoreSup: -1,
rankTotal: validOD.RankTotal(),
scoreTotal: validOD.ScoreTotal(),
},
expectedErr: domain.ValidationError{
Model: "OpponentsDefeated",
Field: "scoreSup",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: rankTotal < 0",
args: args{
rankAtt: validOD.RankAtt(),
scoreAtt: validOD.ScoreAtt(),
rankDef: validOD.RankDef(),
scoreDef: validOD.ScoreDef(),
rankSup: validOD.RankSup(),
scoreSup: validOD.ScoreSup(),
rankTotal: -1,
scoreTotal: validOD.ScoreTotal(),
},
expectedErr: domain.ValidationError{
Model: "OpponentsDefeated",
Field: "rankTotal",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: scoreTotal < 0",
args: args{
rankAtt: validOD.RankAtt(),
scoreAtt: validOD.ScoreAtt(),
rankDef: validOD.RankDef(),
scoreDef: validOD.ScoreDef(),
rankSup: validOD.RankSup(),
scoreSup: validOD.ScoreSup(),
rankTotal: validOD.RankTotal(),
scoreTotal: -1,
},
expectedErr: domain.ValidationError{
Model: "OpponentsDefeated",
Field: "scoreTotal",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
res, err := domain.NewOpponentsDefeated(
tt.args.rankAtt,
tt.args.scoreAtt,
tt.args.rankDef,
tt.args.scoreDef,
tt.args.rankSup,
tt.args.scoreSup,
tt.args.rankTotal,
tt.args.scoreTotal,
)
require.ErrorIs(t, err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.rankAtt, res.RankAtt())
assert.Equal(t, tt.args.scoreAtt, res.ScoreAtt())
assert.Equal(t, tt.args.rankDef, res.RankDef())
assert.Equal(t, tt.args.scoreDef, res.ScoreDef())
assert.Equal(t, tt.args.rankSup, res.RankSup())
assert.Equal(t, tt.args.scoreSup, res.ScoreSup())
assert.Equal(t, tt.args.rankTotal, res.RankTotal())
assert.Equal(t, tt.args.scoreTotal, res.ScoreTotal())
})
}
}

View File

@ -8,6 +8,11 @@ import (
"time"
)
const (
serverKeyMinLength = 1
serverKeyMaxLength = 10
)
type Server struct {
key string
versionCode string
@ -195,7 +200,7 @@ func (ss Servers) Close(open BaseServers) (BaseServers, error) {
continue
}
base, err := NewBaseServer(s.Key(), s.URL().String(), false)
base, err := NewBaseServer(s.Key(), s.URL(), false)
if err != nil {
return nil, fmt.Errorf("couldn't construct BaseServer for server with key '%s': %w", s.Key(), err)
}
@ -211,11 +216,14 @@ type CreateServerParams struct {
versionCode string
}
const createServerParamsModelName = "CreateServerParams"
func NewCreateServerParams(servers BaseServers, versionCode string) ([]CreateServerParams, error) {
if err := validateVersionCode(versionCode); err != nil {
return nil, ValidationError{
Err: err,
Model: createServerParamsModelName,
Field: "versionCode",
Err: err,
}
}
@ -304,6 +312,8 @@ type ListServersParams struct {
offset int
}
const listServersParamsModelName = "ListServersParams"
func NewListServersParams() ListServersParams {
return ListServersParams{
sort: []ServerSort{ServerSortKeyASC},
@ -372,6 +382,7 @@ const (
func (params *ListServersParams) SetSort(sort []ServerSort) error {
if err := validateSliceLen(sort, serverSortMinLength, serverSortMaxLength); err != nil {
return ValidationError{
Model: listServersParamsModelName,
Field: "sort",
Err: err,
}
@ -389,6 +400,7 @@ func (params *ListServersParams) Limit() int {
func (params *ListServersParams) SetLimit(limit int) error {
if limit <= 0 {
return ValidationError{
Model: listServersParamsModelName,
Field: "limit",
Err: MinGreaterEqualError{
Min: 1,
@ -399,6 +411,7 @@ func (params *ListServersParams) SetLimit(limit int) error {
if limit > ServerListMaxLimit {
return ValidationError{
Model: listServersParamsModelName,
Field: "limit",
Err: MaxLessEqualError{
Max: ServerListMaxLimit,
@ -419,6 +432,7 @@ func (params *ListServersParams) Offset() int {
func (params *ListServersParams) SetOffset(offset int) error {
if offset < 0 {
return ValidationError{
Model: listServersParamsModelName,
Field: "offset",
Err: MinGreaterEqualError{
Min: 0,
@ -443,7 +457,7 @@ func (e ServerNotFoundError) Error() string {
}
func (e ServerNotFoundError) Code() ErrorCode {
return ErrorCodeEntityNotFound
return ErrorCodeNotFound
}
func (e ServerNotFoundError) Slug() string {

View File

@ -23,15 +23,6 @@ func NewSyncServersCmdPayload(versionCode string, u *url.URL) (SyncServersCmdPay
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
}
@ -46,7 +37,7 @@ type ServerSyncedEventPayload struct {
versionCode string
}
func NewServerSyncedEventPayload(key string, rawURL string, versionCode string) (ServerSyncedEventPayload, error) {
func NewServerSyncedEventPayload(key string, u *url.URL, versionCode string) (ServerSyncedEventPayload, error) {
if key == "" {
return ServerSyncedEventPayload{}, errors.New("key can't be blank")
}
@ -55,9 +46,8 @@ func NewServerSyncedEventPayload(key string, rawURL string, versionCode string)
return ServerSyncedEventPayload{}, errors.New("version code can't be blank")
}
u, err := parseURL(rawURL)
if err != nil {
return ServerSyncedEventPayload{}, err
if u == nil {
return ServerSyncedEventPayload{}, errors.New("url can't be nil")
}
return ServerSyncedEventPayload{

View File

@ -53,61 +53,12 @@ func TestNewSyncServersCmdPayload(t *testing.T) {
}
}
func TestNewSyncServersCmdPayloadWithStringURL(t *testing.T) {
t.Parallel()
type args struct {
versionCode string
url string
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
versionCode: "pl",
url: "https://plemiona.pl",
},
},
{
name: "ERR: invalid url",
args: args{
versionCode: "pl",
url: "plemiona.pl",
},
expectedErr: domain.InvalidURLError{
URL: "plemiona.pl",
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
payload, err := domain.NewSyncServersCmdPayloadWithStringURL(tt.args.versionCode, tt.args.url)
require.ErrorIs(t, err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.versionCode, payload.VersionCode())
assert.Equal(t, tt.args.url, payload.URL().String())
})
}
}
func TestNewServerSyncedEventPayload(t *testing.T) {
t.Parallel()
server := domaintest.NewServer(t)
payload, err := domain.NewServerSyncedEventPayload(server.Key(), server.URL().String(), server.VersionCode())
payload, err := domain.NewServerSyncedEventPayload(server.Key(), server.URL(), server.VersionCode())
require.NoError(t, err)
assert.Equal(t, server.Key(), payload.Key())
assert.Equal(t, server.URL(), payload.URL())

View File

@ -90,8 +90,9 @@ func TestNewCreateServerParams(t *testing.T) {
},
},
expectedErr: domain.ValidationError{
Err: versionCodeTest.expectedErr,
Model: "CreateServerParams",
Field: "versionCode",
Err: versionCodeTest.expectedErr,
},
})
}
@ -142,6 +143,7 @@ func TestListServersParams_SetSort(t *testing.T) {
sort: nil,
},
expectedErr: domain.ValidationError{
Model: "ListServersParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
@ -160,6 +162,7 @@ func TestListServersParams_SetSort(t *testing.T) {
},
},
expectedErr: domain.ValidationError{
Model: "ListServersParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
@ -211,6 +214,7 @@ func TestListServersParams_SetLimit(t *testing.T) {
limit: 0,
},
expectedErr: domain.ValidationError{
Model: "ListServersParams",
Field: "limit",
Err: domain.MinGreaterEqualError{
Min: 1,
@ -224,6 +228,7 @@ func TestListServersParams_SetLimit(t *testing.T) {
limit: domain.ServerListMaxLimit + 1,
},
expectedErr: domain.ValidationError{
Model: "ListServersParams",
Field: "limit",
Err: domain.MaxLessEqualError{
Max: domain.ServerListMaxLimit,
@ -274,6 +279,7 @@ func TestListServersParams_SetOffset(t *testing.T) {
offset: -1,
},
expectedErr: domain.ValidationError{
Model: "ListServersParams",
Field: "offset",
Err: domain.MinGreaterEqualError{
Min: 0,

116
internal/domain/tribe.go Normal file
View File

@ -0,0 +1,116 @@
package domain
import (
"net/url"
"time"
)
const (
tribeNameMinLength = 1
tribeNameMaxLength = 255
tribeTagMinLength = 1
tribeTagMaxLength = 10
)
type Tribe struct {
id int
serverKey string
name string
tag string
numMembers int
numVillages int
points int
allPoints int
rank int
od OpponentsDefeated
profileURL *url.URL
dominance float64
bestRank int
bestRankAt time.Time
mostPoints int
mostPointsAt time.Time
mostVillages int
mostVillagesAt time.Time
createdAt time.Time
deletedAt time.Time
}
func (t Tribe) ID() int {
return t.id
}
func (t Tribe) ServerKey() string {
return t.serverKey
}
func (t Tribe) Name() string {
return t.name
}
func (t Tribe) Tag() string {
return t.tag
}
func (t Tribe) NumMembers() int {
return t.numMembers
}
func (t Tribe) NumVillages() int {
return t.numVillages
}
func (t Tribe) Points() int {
return t.points
}
func (t Tribe) AllPoints() int {
return t.allPoints
}
func (t Tribe) Rank() int {
return t.rank
}
func (t Tribe) OD() OpponentsDefeated {
return t.od
}
func (t Tribe) ProfileURL() *url.URL {
return t.profileURL
}
func (t Tribe) Dominance() float64 {
return t.dominance
}
func (t Tribe) BestRank() int {
return t.bestRank
}
func (t Tribe) BestRankAt() time.Time {
return t.bestRankAt
}
func (t Tribe) MostPoints() int {
return t.mostPoints
}
func (t Tribe) MostPointsAt() time.Time {
return t.mostPointsAt
}
func (t Tribe) MostVillages() int {
return t.mostVillages
}
func (t Tribe) MostVillagesAt() time.Time {
return t.mostVillagesAt
}
func (t Tribe) CreatedAt() time.Time {
return t.createdAt
}
func (t Tribe) DeletedAt() time.Time {
return t.deletedAt
}

View File

@ -10,7 +10,7 @@ type InvalidURLError struct {
}
func (e InvalidURLError) Error() string {
return fmt.Sprintf("'%s': invalid URL", e.URL)
return fmt.Sprintf("%s: invalid URL", e.URL)
}
func parseURL(rawURL string) (*url.URL, error) {

View File

@ -8,6 +8,7 @@ import (
)
type ValidationError struct {
Model string
Field string
Err error
}
@ -15,7 +16,20 @@ type ValidationError struct {
var _ ErrorWithParams = ValidationError{}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Err)
prefix := e.Model
if e.Field != "" {
if len(prefix) > 0 {
prefix += "."
}
prefix += e.Field
}
if prefix != "" {
prefix += ": "
}
return prefix + e.Err.Error()
}
func (e ValidationError) Code() ErrorCode {
@ -28,22 +42,35 @@ func (e ValidationError) Code() ErrorCode {
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)
if e.Field != "" {
s = slug.Make(e.Field) + "-" + s
}
if e.Model != "" {
s = slug.Make(e.Model) + "-" + s
}
return s
}
func (e ValidationError) Params() map[string]any {
var withParams ErrorWithParams
ok := errors.As(e.Err, &withParams)
if !ok {
if ok := errors.As(e.Err, &withParams); !ok {
return nil
}
return withParams.Params()
}
func (e ValidationError) Unwrap() error {
return e.Err
}
type MinGreaterEqualError struct {
Min int
Current int
@ -124,6 +151,12 @@ func (e LenOutOfRangeError) Params() map[string]any {
}
}
var ErrNotNil error = simpleError{
msg: "must not be nil",
code: ErrorCodeIncorrectInput,
slug: "not-nil",
}
func validateSliceLen[S ~[]E, E any](s S, min, max int) error {
if l := len(s); l > max || l < min {
return LenOutOfRangeError{
@ -136,11 +169,6 @@ func validateSliceLen[S ~[]E, E any](s S, min, max int) error {
return nil
}
const (
serverKeyMinLength = 1
serverKeyMaxLength = 10
)
func validateServerKey(key string) error {
if l := len(key); l < serverKeyMinLength || l > serverKeyMaxLength {
return LenOutOfRangeError{
@ -153,11 +181,6 @@ func validateServerKey(key string) error {
return nil
}
const (
versionCodeMinLength = 2
versionCodeMaxLength = 2
)
func validateVersionCode(code string) error {
if l := len(code); l < versionCodeMinLength || l > versionCodeMaxLength {
return LenOutOfRangeError{

View File

@ -5,6 +5,11 @@ import (
"net/url"
)
const (
versionCodeMinLength = 2
versionCodeMaxLength = 2
)
type Version struct {
code string
name string
@ -82,6 +87,8 @@ type ListVersionsParams struct {
sort []VersionSort
}
const listVersionsParamsModelName = "ListVersionsParams"
func NewListVersionsParams() ListVersionsParams {
return ListVersionsParams{sort: []VersionSort{VersionSortCodeASC}}
}
@ -98,6 +105,7 @@ func (params *ListVersionsParams) Sort() []VersionSort {
func (params *ListVersionsParams) SetSort(sort []VersionSort) error {
if err := validateSliceLen(sort, versionSortMinLength, versionSortMaxLength); err != nil {
return ValidationError{
Model: listVersionsParamsModelName,
Field: "sort",
Err: err,
}

View File

@ -34,6 +34,7 @@ func TestListVersionsParams_SetSort(t *testing.T) {
sort: nil,
},
expectedErr: domain.ValidationError{
Model: "ListVersionsParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
@ -51,6 +52,7 @@ func TestListVersionsParams_SetSort(t *testing.T) {
},
},
expectedErr: domain.ValidationError{
Model: "ListVersionsParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,

View File

@ -55,7 +55,7 @@ func (c *ServerWatermillConsumer) sync(msg *message.Message) error {
return nil
}
payload, err := domain.NewSyncServersCmdPayloadWithStringURL(rawPayload.VersionCode, rawPayload.URL)
payload, err := domain.NewSyncServersCmdPayload(rawPayload.VersionCode, rawPayload.URL)
if err != nil {
c.logger.Error("couldn't construct domain.SyncServersCmdPayload", err, watermill.LogFields{
"handler": message.HandlerNameFromCtx(msg.Context()),

View File

@ -0,0 +1,63 @@
package port
import (
"gitea.dwysokinski.me/twhelp/corev3/internal/app"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/watermillmsg"
"github.com/ThreeDotsLabs/watermill"
"github.com/ThreeDotsLabs/watermill/message"
)
type TribeWatermillConsumer struct {
svc *app.TribeService
subscriber message.Subscriber
logger watermill.LoggerAdapter
marshaler watermillmsg.Marshaler
eventServerSyncedTopic string
}
func NewTribeWatermillConsumer(
svc *app.TribeService,
subscriber message.Subscriber,
logger watermill.LoggerAdapter,
marshaler watermillmsg.Marshaler,
eventServerSyncedTopic string,
) *TribeWatermillConsumer {
return &TribeWatermillConsumer{
svc: svc,
subscriber: subscriber,
logger: logger,
marshaler: marshaler,
eventServerSyncedTopic: eventServerSyncedTopic,
}
}
func (c *TribeWatermillConsumer) Register(router *message.Router) {
router.AddNoPublisherHandler(
"TribeConsumer.sync",
c.eventServerSyncedTopic,
c.subscriber,
c.sync,
)
}
func (c *TribeWatermillConsumer) sync(msg *message.Message) error {
var rawPayload watermillmsg.ServerSyncedEventPayload
if err := c.marshaler.Unmarshal(msg, &rawPayload); err != nil {
c.logger.Error("couldn't unmarshal payload", err, watermill.LogFields{
"handler": message.HandlerNameFromCtx(msg.Context()),
})
return nil
}
payload, err := domain.NewServerSyncedEventPayload(rawPayload.Key, rawPayload.URL, rawPayload.VersionCode)
if err != nil {
c.logger.Error("couldn't construct domain.ServerSyncedEventPayload", err, watermill.LogFields{
"handler": message.HandlerNameFromCtx(msg.Context()),
})
return nil
}
return c.svc.Sync(msg.Context(), payload)
}

View File

@ -59,19 +59,19 @@ func NewClient(opts ...ClientOption) *Client {
}
}
var ErrBaseURLCantBeNil = errors.New("baseURL can't be nil")
var (
ErrInvalidServerKey = errors.New("invalid server key")
ErrInvalidServerURL = errors.New("invalid server URL")
)
func (c *Client) GetOpenServers(ctx context.Context, rawBaseURL string) ([]Server, error) {
baseURL, err := url.ParseRequestURI(rawBaseURL)
if err != nil {
return nil, err
func (c *Client) GetOpenServers(ctx context.Context, baseURL *url.URL) ([]Server, error) {
if baseURL == nil {
return nil, ErrBaseURLCantBeNil
}
u := buildURL(baseURL, endpointGetServers)
resp, err := c.get(ctx, u)
resp, err := c.get(ctx, buildURL(baseURL, endpointGetServers))
if err != nil {
return nil, err
}
@ -79,86 +79,54 @@ func (c *Client) GetOpenServers(ctx context.Context, rawBaseURL string) ([]Serve
_ = resp.Body.Close()
}()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("%s: couldn't read response body: %w", u, err)
}
m, err := phpserialize.UnmarshalAssociativeArray(b)
if err != nil {
return nil, fmt.Errorf("%s: couldn't unmarshal response body: %w", u, err)
}
servers := make([]Server, 0, len(m))
for key, val := range m {
keyStr, ok := key.(string)
if !ok || keyStr == "" {
return nil, fmt.Errorf("%s: parsing '%v': %w", u, key, ErrInvalidServerKey)
}
urlStr, ok := val.(string)
if !ok || urlStr == "" {
return nil, fmt.Errorf("%s: parsing '%v': %w", u, val, ErrInvalidServerURL)
}
servers = append(servers, Server{
Key: keyStr,
URL: urlStr,
})
}
return servers, nil
return serverParser{r: resp.Body}.parse()
}
func (c *Client) GetServerConfig(ctx context.Context, rawBaseURL string) (ServerConfig, error) {
baseURL, err := url.ParseRequestURI(rawBaseURL)
if err != nil {
return ServerConfig{}, err
func (c *Client) GetServerConfig(ctx context.Context, baseURL *url.URL) (ServerConfig, error) {
if baseURL == nil {
return ServerConfig{}, ErrBaseURLCantBeNil
}
var cfg ServerConfig
if err = c.getXML(ctx, buildURLWithQuery(baseURL, endpointInterface, queryConfig), &cfg); err != nil {
if err := c.getXML(ctx, buildURLWithQuery(baseURL, endpointInterface, queryConfig), &cfg); err != nil {
return ServerConfig{}, err
}
return cfg, nil
}
func (c *Client) GetBuildingInfo(ctx context.Context, rawBaseURL string) (BuildingInfo, error) {
baseURL, err := url.ParseRequestURI(rawBaseURL)
if err != nil {
return BuildingInfo{}, err
func (c *Client) GetBuildingInfo(ctx context.Context, baseURL *url.URL) (BuildingInfo, error) {
if baseURL == nil {
return BuildingInfo{}, ErrBaseURLCantBeNil
}
var info BuildingInfo
if err = c.getXML(ctx, buildURLWithQuery(baseURL, endpointInterface, queryBuildingInfo), &info); err != nil {
if err := c.getXML(ctx, buildURLWithQuery(baseURL, endpointInterface, queryBuildingInfo), &info); err != nil {
return BuildingInfo{}, err
}
return info, nil
}
func (c *Client) GetUnitInfo(ctx context.Context, rawBaseURL string) (UnitInfo, error) {
baseURL, err := url.ParseRequestURI(rawBaseURL)
if err != nil {
return UnitInfo{}, err
func (c *Client) GetUnitInfo(ctx context.Context, baseURL *url.URL) (UnitInfo, error) {
if baseURL == nil {
return UnitInfo{}, ErrBaseURLCantBeNil
}
var info UnitInfo
if err = c.getXML(ctx, buildURLWithQuery(baseURL, endpointInterface, queryUnitInfo), &info); err != nil {
if err := c.getXML(ctx, buildURLWithQuery(baseURL, endpointInterface, queryUnitInfo), &info); err != nil {
return UnitInfo{}, err
}
return info, nil
}
func (c *Client) GetTribes(ctx context.Context, rawBaseURL string) ([]Tribe, error) {
baseURL, err := url.ParseRequestURI(rawBaseURL)
if err != nil {
return nil, err
func (c *Client) GetTribes(ctx context.Context, baseURL *url.URL) ([]Tribe, error) {
if baseURL == nil {
return nil, ErrBaseURLCantBeNil
}
od, err := c.getOD(ctx, baseURL, true)
@ -181,10 +149,9 @@ func (c *Client) GetTribes(ctx context.Context, rawBaseURL string) ([]Tribe, err
}.parse()
}
func (c *Client) GetPlayers(ctx context.Context, rawBaseURL string) ([]Player, error) {
baseURL, err := url.ParseRequestURI(rawBaseURL)
if err != nil {
return nil, err
func (c *Client) GetPlayers(ctx context.Context, baseURL *url.URL) ([]Player, error) {
if baseURL == nil {
return nil, ErrBaseURLCantBeNil
}
od, err := c.getOD(ctx, baseURL, false)
@ -260,10 +227,9 @@ func (c *Client) getSingleODFile(ctx context.Context, u *url.URL) ([]odRecord, e
}.parse()
}
func (c *Client) GetVillages(ctx context.Context, rawBaseURL string) ([]Village, error) {
baseURL, err := url.ParseRequestURI(rawBaseURL)
if err != nil {
return nil, err
func (c *Client) GetVillages(ctx context.Context, baseURL *url.URL) ([]Village, error) {
if baseURL == nil {
return nil, ErrBaseURLCantBeNil
}
resp, err := c.get(ctx, buildURL(baseURL, endpointVillages))
@ -280,10 +246,9 @@ func (c *Client) GetVillages(ctx context.Context, rawBaseURL string) ([]Village,
}.parse()
}
func (c *Client) GetEnnoblements(ctx context.Context, rawBaseURL string, since time.Time) ([]Ennoblement, error) {
baseURL, err := url.ParseRequestURI(rawBaseURL)
if err != nil {
return nil, err
func (c *Client) GetEnnoblements(ctx context.Context, baseURL *url.URL, since time.Time) ([]Ennoblement, error) {
if baseURL == nil {
return nil, ErrBaseURLCantBeNil
}
u := buildURL(baseURL, endpointEnnoblements)
@ -311,7 +276,6 @@ func (c *Client) getXML(ctx context.Context, u *url.URL, v any) error {
return err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
@ -327,7 +291,7 @@ func (c *Client) get(ctx context.Context, u *url.URL) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
if err != nil {
return nil, fmt.Errorf("%s: %w", urlStr, err)
return nil, err
}
// headers
@ -347,6 +311,48 @@ func (c *Client) get(ctx context.Context, u *url.URL) (*http.Response, error) {
return resp, nil
}
type serverParser struct {
r io.Reader
}
func (p serverParser) parse() ([]Server, error) {
b, err := io.ReadAll(p.r)
if err != nil {
return nil, fmt.Errorf("couldn't read data: %w", err)
}
m, err := phpserialize.UnmarshalAssociativeArray(b)
if err != nil {
return nil, fmt.Errorf("couldn't unmarshal response body: %w", err)
}
servers := make([]Server, 0, len(m))
for key, val := range m {
keyStr, ok := key.(string)
if !ok || keyStr == "" {
return nil, fmt.Errorf("parsing '%v': %w", key, ErrInvalidServerKey)
}
urlStr, ok := val.(string)
if !ok || urlStr == "" {
return nil, fmt.Errorf("parsing '%v': %w", val, ErrInvalidServerURL)
}
serverURL, parseErr := url.ParseRequestURI(urlStr)
if parseErr != nil {
return nil, fmt.Errorf("parsing '%v': %w", val, parseErr)
}
servers = append(servers, Server{
Key: keyStr,
URL: serverURL,
})
}
return servers, nil
}
type tribeCSVParser struct {
r io.Reader
baseURL *url.URL
@ -355,8 +361,8 @@ type tribeCSVParser struct {
const fieldsPerRecordTribe = 8
func (t tribeCSVParser) parse() ([]Tribe, error) {
csvR := newCSVReader(t.r, fieldsPerRecordTribe)
func (p tribeCSVParser) parse() ([]Tribe, error) {
csvR := newCSVReader(p.r, fieldsPerRecordTribe)
var tribes []Tribe
for {
@ -368,7 +374,7 @@ func (t tribeCSVParser) parse() ([]Tribe, error) {
return nil, err
}
tribe, err := t.parseRecord(rec)
tribe, err := p.parseRecord(rec)
if err != nil {
return nil, err
}
@ -383,7 +389,7 @@ func (t tribeCSVParser) parse() ([]Tribe, error) {
return tribes, nil
}
func (t tribeCSVParser) parseRecord(record []string) (Tribe, error) {
func (p tribeCSVParser) parseRecord(record []string) (Tribe, error) {
var err error
var tribe Tribe
@ -429,9 +435,9 @@ func (t tribeCSVParser) parseRecord(record []string) (Tribe, error) {
return Tribe{}, ParseError{Err: err, Str: record[7], Field: "Tribe.Rank"}
}
tribe.OpponentsDefeated = t.od[tribe.ID]
tribe.OpponentsDefeated = p.od[tribe.ID]
tribe.ProfileURL = buildURLWithQuery(t.baseURL, endpointGame, fmt.Sprintf(queryTribeProfile, tribe.ID)).String()
tribe.ProfileURL = buildURLWithQuery(p.baseURL, endpointGame, fmt.Sprintf(queryTribeProfile, tribe.ID))
return tribe, nil
}
@ -510,7 +516,7 @@ func (p playerCSVParser) parseRecord(record []string) (Player, error) {
player.OpponentsDefeated = p.od[player.ID]
player.ProfileURL = buildURLWithQuery(p.baseURL, endpointGame, fmt.Sprintf(queryPlayerProfile, player.ID)).String()
player.ProfileURL = buildURLWithQuery(p.baseURL, endpointGame, fmt.Sprintf(queryPlayerProfile, player.ID))
return player, nil
}
@ -522,8 +528,8 @@ type villageCSVParser struct {
const fieldsPerRecordVillage = 7
func (v villageCSVParser) parse() ([]Village, error) {
csvR := newCSVReader(v.r, fieldsPerRecordVillage)
func (p villageCSVParser) parse() ([]Village, error) {
csvR := newCSVReader(p.r, fieldsPerRecordVillage)
var villages []Village
for {
@ -534,7 +540,7 @@ func (v villageCSVParser) parse() ([]Village, error) {
if err != nil {
return nil, err
}
village, err := v.parseRecord(rec)
village, err := p.parseRecord(rec)
if err != nil {
return nil, err
}
@ -548,7 +554,7 @@ func (v villageCSVParser) parse() ([]Village, error) {
return villages, nil
}
func (v villageCSVParser) parseRecord(record []string) (Village, error) {
func (p villageCSVParser) parseRecord(record []string) (Village, error) {
var err error
var village Village
@ -589,14 +595,14 @@ func (v villageCSVParser) parseRecord(record []string) (Village, error) {
return Village{}, ParseError{Err: err, Str: record[6], Field: "Village.Bonus"}
}
village.Continent = v.buildContinent(record, village.X, village.Y)
village.Continent = p.buildContinent(record, village.X, village.Y)
village.ProfileURL = buildURLWithQuery(v.baseURL, endpointGame, fmt.Sprintf(queryVillageProfile, village.ID)).String()
village.ProfileURL = buildURLWithQuery(p.baseURL, endpointGame, fmt.Sprintf(queryVillageProfile, village.ID))
return village, nil
}
func (v villageCSVParser) buildContinent(record []string, x, y int) string {
func (p villageCSVParser) buildContinent(record []string, x, y int) string {
continent := "K"
switch {
@ -625,8 +631,8 @@ type odRecord struct {
const fieldsPerRecordOD = 3
func (o odCSVParser) parse() ([]odRecord, error) {
csvR := newCSVReader(o.r, fieldsPerRecordOD)
func (p odCSVParser) parse() ([]odRecord, error) {
csvR := newCSVReader(p.r, fieldsPerRecordOD)
var odRecords []odRecord
for {
@ -638,7 +644,7 @@ func (o odCSVParser) parse() ([]odRecord, error) {
return nil, err
}
od, err := o.parseRecord(rec)
od, err := p.parseRecord(rec)
if err != nil {
return nil, err
}
@ -649,7 +655,7 @@ func (o odCSVParser) parse() ([]odRecord, error) {
return odRecords, nil
}
func (o odCSVParser) parseRecord(record []string) (odRecord, error) {
func (p odCSVParser) parseRecord(record []string) (odRecord, error) {
var err error
var rec odRecord
@ -680,8 +686,8 @@ type ennoblementCSVParser struct {
const fieldsPerRecordEnnoblement = 7
func (e ennoblementCSVParser) parse() ([]Ennoblement, error) {
csvR := newCSVReader(e.r, fieldsPerRecordEnnoblement)
func (p ennoblementCSVParser) parse() ([]Ennoblement, error) {
csvR := newCSVReader(p.r, fieldsPerRecordEnnoblement)
var ennoblements []Ennoblement
for {
@ -692,12 +698,12 @@ func (e ennoblementCSVParser) parse() ([]Ennoblement, error) {
if err != nil {
return nil, err
}
ennoblement, err := e.parseRecord(rec)
ennoblement, err := p.parseRecord(rec)
if err != nil {
return nil, err
}
if ennoblement.CreatedAt.Before(e.since) {
if ennoblement.CreatedAt.Before(p.since) {
continue
}
@ -711,7 +717,7 @@ func (e ennoblementCSVParser) parse() ([]Ennoblement, error) {
return ennoblements, nil
}
func (e ennoblementCSVParser) parseRecord(record []string) (Ennoblement, error) {
func (p ennoblementCSVParser) parseRecord(record []string) (Ennoblement, error) {
var err error
var ennoblement Ennoblement
@ -722,7 +728,7 @@ func (e ennoblementCSVParser) parseRecord(record []string) (Ennoblement, error)
return Ennoblement{}, ParseError{Err: err, Str: record[0], Field: "Ennoblement.VillageID"}
}
ennoblement.CreatedAt, err = e.parseTimestamp(record[1])
ennoblement.CreatedAt, err = p.parseTimestamp(record[1])
if err != nil {
return Ennoblement{}, ParseError{Err: err, Str: record[1], Field: "Ennoblement.CreatedAt"}
}
@ -755,7 +761,7 @@ func (e ennoblementCSVParser) parseRecord(record []string) (Ennoblement, error)
return ennoblement, nil
}
func (e ennoblementCSVParser) parseTimestamp(s string) (time.Time, error) {
func (p ennoblementCSVParser) parseTimestamp(s string) (time.Time, error) {
timestamp, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return time.Time{}, err
@ -795,10 +801,8 @@ func buildURL(base *url.URL, path string) *url.URL {
}
func buildURLWithQuery(base *url.URL, path, query string) *url.URL {
return &url.URL{
Scheme: base.Scheme,
Host: base.Host,
Path: path,
RawQuery: query,
}
newURL := *base
newURL.Path = path
newURL.RawQuery = query
return &newURL
}

View File

@ -7,7 +7,9 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"slices"
"strconv"
"testing"
"time"
@ -52,21 +54,17 @@ func TestClient_GetOpenServers(t *testing.T) {
"pl164",
}
servers, err := newClient(srv.Client()).GetOpenServers(context.Background(), srv.URL)
servers, err := newClient(srv.Client()).GetOpenServers(context.Background(), parseURL(t, srv.URL))
require.NoError(t, err)
assert.Len(t, servers, len(expectedServerKeys))
for _, key := range expectedServerKeys {
found := false
for _, server := range servers {
if server.Key == key && server.URL == fmt.Sprintf("https://%s.plemiona.pl", key) {
found = true
break
assert.True(t, slices.ContainsFunc(servers, func(server tw.Server) bool {
return server.Key == key && *server.URL == url.URL{
Scheme: "https",
Host: key + ".plemiona.pl",
}
}
assert.True(t, found, "key '%s' not found", key)
}), "key '%s' not found", key)
}
}
@ -82,7 +80,7 @@ func TestClient_GetServerConfig(t *testing.T) {
srv := httptest.NewTLSServer(mux)
t.Cleanup(srv.Close)
cfg, err := newClient(srv.Client()).GetServerConfig(context.Background(), srv.URL)
cfg, err := newClient(srv.Client()).GetServerConfig(context.Background(), parseURL(t, srv.URL))
require.NoError(t, err)
assert.InEpsilon(t, 4.5, cfg.Speed, 0.01)
assert.InEpsilon(t, 0.5, cfg.UnitSpeed, 0.01)
@ -206,7 +204,7 @@ func TestClient_GetUnitInfo(t *testing.T) {
srv := httptest.NewTLSServer(mux)
t.Cleanup(srv.Close)
unitInfo, err := newClient(srv.Client()).GetUnitInfo(context.Background(), srv.URL)
unitInfo, err := newClient(srv.Client()).GetUnitInfo(context.Background(), parseURL(t, srv.URL))
require.NoError(t, err)
assert.InEpsilon(t, 226.66666666667, unitInfo.Spear.BuildTime, 0.01)
@ -321,7 +319,7 @@ func TestClient_GetBuildingInfo(t *testing.T) {
srv := httptest.NewTLSServer(mux)
t.Cleanup(srv.Close)
buildingInfo, err := newClient(srv.Client()).GetBuildingInfo(context.Background(), srv.URL)
buildingInfo, err := newClient(srv.Client()).GetBuildingInfo(context.Background(), parseURL(t, srv.URL))
require.NoError(t, err)
assert.Equal(t, 30, buildingInfo.Main.MaxLevel)
@ -556,7 +554,7 @@ func TestClient_GetTribes(t *testing.T) {
respODA string
respODD string
checkErr func(err error) bool
expectedTribes func(url string) []tw.Tribe
expectedTribes func(t *testing.T, url string) []tw.Tribe
}{
{
name: "OK",
@ -564,13 +562,14 @@ func TestClient_GetTribes(t *testing.T) {
respOD: "1,1,1\n2,2,2\n3,3,3",
respODA: "1,1,1\n2,2,2\n3,3,3",
respODD: "1,1,1\n2,2,2\n3,3,3",
expectedTribes: func(url string) []tw.Tribe {
expectedTribes: func(t *testing.T, baseURL string) []tw.Tribe {
t.Helper()
return []tw.Tribe{
{
ID: 1,
Name: "name 1",
Tag: "tag 1",
ProfileURL: url + "/game.php?screen=info_ally&id=1",
ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_ally&id=1"),
NumMembers: 100,
NumVillages: 101,
Points: 102,
@ -591,7 +590,7 @@ func TestClient_GetTribes(t *testing.T) {
ID: 2,
Name: "name2",
Tag: "tag2",
ProfileURL: url + "/game.php?screen=info_ally&id=2",
ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_ally&id=2"),
NumMembers: 200,
NumVillages: 202,
Points: 202,
@ -612,7 +611,7 @@ func TestClient_GetTribes(t *testing.T) {
ID: 3,
Name: "name3",
Tag: "tag3",
ProfileURL: url + "/game.php?screen=info_ally&id=3",
ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_ally&id=3"),
NumMembers: 300,
NumVillages: 303,
Points: 302,
@ -633,7 +632,7 @@ func TestClient_GetTribes(t *testing.T) {
ID: 4,
Name: "name4",
Tag: "tag4",
ProfileURL: url + "/game.php?screen=info_ally&id=4",
ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_ally&id=4"),
NumMembers: 400,
NumVillages: 404,
Points: 402,
@ -795,7 +794,7 @@ func TestClient_GetTribes(t *testing.T) {
srv := httptest.NewTLSServer(mux)
t.Cleanup(srv.Close)
tribes, err := newClient(srv.Client()).GetTribes(context.Background(), srv.URL)
tribes, err := newClient(srv.Client()).GetTribes(context.Background(), parseURL(t, srv.URL))
checkErr := func(err error) bool {
return errors.Is(err, nil)
@ -806,7 +805,7 @@ func TestClient_GetTribes(t *testing.T) {
var expectedTribes []tw.Tribe
if tt.expectedTribes != nil {
expectedTribes = tt.expectedTribes(srv.URL)
expectedTribes = tt.expectedTribes(t, srv.URL)
}
assert.True(t, checkErr(err))
@ -829,7 +828,7 @@ func TestClient_GetPlayers(t *testing.T) {
respODD string
respODS string
checkErr func(err error) bool
expectedPlayers func(url string) []tw.Player
expectedPlayers func(t *testing.T, url string) []tw.Player
}{
{
name: "OK",
@ -838,12 +837,13 @@ func TestClient_GetPlayers(t *testing.T) {
respODA: "1,1,1000\n2,2,253\n3,3,100",
respODD: "1,1,1002\n2,2,251\n3,3,155",
respODS: "1,1,1003\n2,2,250\n3,3,166",
expectedPlayers: func(url string) []tw.Player {
expectedPlayers: func(t *testing.T, baseURL string) []tw.Player {
t.Helper()
return []tw.Player{
{
ID: 1,
Name: "name 1",
ProfileURL: url + "/game.php?screen=info_player&id=1",
ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_player&id=1"),
TribeID: 123,
NumVillages: 124,
Points: 125,
@ -862,7 +862,7 @@ func TestClient_GetPlayers(t *testing.T) {
{
ID: 2,
Name: "name2",
ProfileURL: url + "/game.php?screen=info_player&id=2",
ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_player&id=2"),
TribeID: 256,
NumVillages: 257,
Points: 258,
@ -881,7 +881,7 @@ func TestClient_GetPlayers(t *testing.T) {
{
ID: 3,
Name: "name3",
ProfileURL: url + "/game.php?screen=info_player&id=3",
ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_player&id=3"),
TribeID: 356,
NumVillages: 357,
Points: 358,
@ -900,7 +900,7 @@ func TestClient_GetPlayers(t *testing.T) {
{
ID: 4,
Name: "name4",
ProfileURL: url + "/game.php?screen=info_player&id=4",
ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_player&id=4"),
TribeID: 456,
NumVillages: 457,
Points: 458,
@ -1078,7 +1078,7 @@ func TestClient_GetPlayers(t *testing.T) {
srv := httptest.NewTLSServer(mux)
t.Cleanup(srv.Close)
players, err := newClient(srv.Client()).GetPlayers(context.Background(), srv.URL)
players, err := newClient(srv.Client()).GetPlayers(context.Background(), parseURL(t, srv.URL))
checkErr := func(err error) bool {
return errors.Is(err, nil)
@ -1089,7 +1089,7 @@ func TestClient_GetPlayers(t *testing.T) {
var expectedPlayers []tw.Player
if tt.expectedPlayers != nil {
expectedPlayers = tt.expectedPlayers(srv.URL)
expectedPlayers = tt.expectedPlayers(t, srv.URL)
}
assert.True(t, checkErr(err))
@ -1108,18 +1108,19 @@ func TestClient_GetVillages(t *testing.T) {
name string
resp string
checkErr func(err error) bool
expectedVillages func(url string) []tw.Village
expectedVillages func(t *testing.T, baseURL string) []tw.Village
}{
{
name: "OK",
//nolint:lll
resp: "123,village%201,500,501,502,503,504\n124,village 2,100,201,102,103,104\n125,village%203,1,501,502,503,504\n126,village%204,1,1,502,503,504\n127,village%205,103,1,502,503,504",
expectedVillages: func(url string) []tw.Village {
expectedVillages: func(t *testing.T, baseURL string) []tw.Village {
t.Helper()
return []tw.Village{
{
ID: 123,
Name: "village 1",
ProfileURL: url + "/game.php?screen=info_village&id=123",
ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_village&id=123"),
X: 500,
Y: 501,
Continent: "K55",
@ -1130,7 +1131,7 @@ func TestClient_GetVillages(t *testing.T) {
{
ID: 124,
Name: "village 2",
ProfileURL: url + "/game.php?screen=info_village&id=124",
ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_village&id=124"),
X: 100,
Y: 201,
Continent: "K21",
@ -1141,7 +1142,7 @@ func TestClient_GetVillages(t *testing.T) {
{
ID: 125,
Name: "village 3",
ProfileURL: url + "/game.php?screen=info_village&id=125",
ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_village&id=125"),
X: 1,
Y: 501,
Continent: "K50",
@ -1152,7 +1153,7 @@ func TestClient_GetVillages(t *testing.T) {
{
ID: 126,
Name: "village 4",
ProfileURL: url + "/game.php?screen=info_village&id=126",
ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_village&id=126"),
X: 1,
Y: 1,
Continent: "K0",
@ -1163,7 +1164,7 @@ func TestClient_GetVillages(t *testing.T) {
{
ID: 127,
Name: "village 5",
ProfileURL: url + "/game.php?screen=info_village&id=127",
ProfileURL: parseURL(t, baseURL+"/game.php?screen=info_village&id=127"),
X: 103,
Y: 1,
Continent: "K1",
@ -1250,7 +1251,7 @@ func TestClient_GetVillages(t *testing.T) {
srv := httptest.NewTLSServer(mux)
t.Cleanup(srv.Close)
villages, err := newClient(srv.Client()).GetVillages(context.Background(), srv.URL)
villages, err := newClient(srv.Client()).GetVillages(context.Background(), parseURL(t, srv.URL))
checkErr := func(err error) bool {
return errors.Is(err, nil)
@ -1261,7 +1262,7 @@ func TestClient_GetVillages(t *testing.T) {
var expectedVillages []tw.Village
if tt.expectedVillages != nil {
expectedVillages = tt.expectedVillages(srv.URL)
expectedVillages = tt.expectedVillages(t, srv.URL)
}
assert.True(t, checkErr(err))
@ -1420,7 +1421,7 @@ func TestClient_GetEnnoblements(t *testing.T) {
srv := httptest.NewTLSServer(mux)
t.Cleanup(srv.Close)
ennoblements, err := newClient(srv.Client()).GetEnnoblements(context.Background(), srv.URL, tt.since)
ennoblements, err := newClient(srv.Client()).GetEnnoblements(context.Background(), parseURL(t, srv.URL), tt.since)
checkErr := func(err error) bool {
return errors.Is(err, nil)
@ -1644,7 +1645,7 @@ func TestClient_GetEnnoblements(t *testing.T) {
client := newClient(srv.Client(), tt.clientOpts...)
ennoblements, err := client.GetEnnoblements(context.Background(), srv.URL, tt.since)
ennoblements, err := client.GetEnnoblements(context.Background(), parseURL(t, srv.URL), tt.since)
checkErr := func(err error) bool {
return errors.Is(err, nil)
@ -1663,6 +1664,13 @@ func TestClient_GetEnnoblements(t *testing.T) {
})
}
func parseURL(tb testing.TB, rawURL string) *url.URL {
tb.Helper()
u, err := url.ParseRequestURI(rawURL)
require.NoError(tb, err)
return u
}
// user agent and http client can't be overridden via opts
func newClient(hc *http.Client, opts ...tw.ClientOption) *tw.Client {
return tw.NewClient(append(opts, tw.WithHTTPClient(hc), tw.WithUserAgent(testUserAgent))...)

View File

@ -1,12 +1,13 @@
package tw
import (
"net/url"
"time"
)
type Server struct {
Key string
URL string
URL *url.URL
}
type OpponentsDefeated struct {
@ -31,7 +32,7 @@ type Tribe struct {
Points int
AllPoints int
Rank int
ProfileURL string
ProfileURL *url.URL
}
type Player struct {
@ -43,7 +44,7 @@ type Player struct {
Points int
Rank int
TribeID int
ProfileURL string
ProfileURL *url.URL
}
type Village struct {
@ -55,7 +56,7 @@ type Village struct {
Continent string
Bonus int
PlayerID int
ProfileURL string
ProfileURL *url.URL
}
type Ennoblement struct {

View File

@ -1,12 +1,14 @@
package watermillmsg
import "net/url"
type SyncServersCmdPayload struct {
VersionCode string `json:"versionCode"`
URL string `json:"url"`
VersionCode string `json:"versionCode"`
URL *url.URL `json:"url"`
}
type ServerSyncedEventPayload struct {
Key string `json:"key"`
URL string `json:"url"`
VersionCode string `json:"versionCode"`
Key string `json:"key"`
URL *url.URL `json:"url"`
VersionCode string `json:"versionCode"`
}

View File

@ -3,6 +3,7 @@ kind: Kustomization
resources:
- jobs.yml
- server-consumer.yml
- tribe-consumer.yml
images:
- name: twhelp
newName: twhelp

View File

@ -0,0 +1,44 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: twhelp-tribe-consumer-deployment
spec:
selector:
matchLabels:
app: twhelp-tribe-consumer
template:
metadata:
labels:
app: twhelp-tribe-consumer
spec:
containers:
- name: twhelp-tribe-consumer
image: twhelp
args: [consumer, tribe]
env:
- name: APP_MODE
value: development
- name: LOG_LEVEL
value: debug
- name: DB_CONNECTION_STRING
valueFrom:
secretKeyRef:
name: twhelp-secret
key: db-connection-string
- name: RABBITMQ_CONNECTION_STRING
valueFrom:
secretKeyRef:
name: twhelp-secret
key: rabbitmq-connection-string
livenessProbe:
exec:
command: [cat, /tmp/live]
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 100Mi
limits:
cpu: 300m
memory: 300Mi