From b5b699ca49a61c73ef94b170f486e6f39b10a91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Wed, 27 Dec 2023 08:07:40 +0000 Subject: [PATCH] feat: tribe consumer (#11) Reviewed-on: https://gitea.dwysokinski.me/twhelp/corev3/pulls/11 --- cmd/twhelp/cmd_consumer.go | 36 ++ internal/adapter/http_tw.go | 58 ++- .../adapter/publisher_watermill_server.go | 4 +- internal/app/service_server.go | 10 +- internal/app/service_tribe.go | 27 ++ internal/app/service_tw.go | 10 +- internal/domain/base_server.go | 13 +- internal/domain/base_server_test.go | 19 +- internal/domain/base_tribe.go | 188 +++++++++ internal/domain/base_tribe_test.go | 357 ++++++++++++++++++ internal/domain/domaintest/base_server.go | 2 +- internal/domain/domaintest/base_tribe.go | 48 +++ .../domain/domaintest/opponents_defeated.go | 23 ++ internal/domain/domaintest/server_config.go | 1 + internal/domain/domaintest/tribe.go | 7 + internal/domain/domaintest/unit_info.go | 1 + internal/domain/domaintest/utils.go | 7 + internal/domain/error.go | 26 +- internal/domain/opponents_defeated.go | 156 ++++++++ internal/domain/opponents_defeated_test.go | 246 ++++++++++++ internal/domain/server.go | 20 +- internal/domain/server_message_payloads.go | 16 +- .../domain/server_message_payloads_test.go | 51 +-- internal/domain/server_test.go | 8 +- internal/domain/tribe.go | 116 ++++++ internal/domain/utils.go | 2 +- internal/domain/validation.go | 51 ++- internal/domain/version.go | 8 + internal/domain/version_test.go | 2 + internal/port/consumer_watermill_server.go | 2 +- internal/port/consumer_watermill_tribe.go | 63 ++++ internal/tw/client.go | 202 +++++----- internal/tw/client_test.go | 88 +++-- internal/tw/tw.go | 9 +- internal/watermillmsg/server.go | 12 +- k8s/base/kustomization.yml | 1 + k8s/base/tribe-consumer.yml | 44 +++ 37 files changed, 1671 insertions(+), 263 deletions(-) create mode 100644 internal/app/service_tribe.go create mode 100644 internal/domain/base_tribe.go create mode 100644 internal/domain/base_tribe_test.go create mode 100644 internal/domain/domaintest/base_tribe.go create mode 100644 internal/domain/domaintest/opponents_defeated.go create mode 100644 internal/domain/domaintest/tribe.go create mode 100644 internal/domain/domaintest/utils.go create mode 100644 internal/domain/opponents_defeated.go create mode 100644 internal/domain/opponents_defeated_test.go create mode 100644 internal/domain/tribe.go create mode 100644 internal/port/consumer_watermill_tribe.go create mode 100644 k8s/base/tribe-consumer.yml diff --git a/cmd/twhelp/cmd_consumer.go b/cmd/twhelp/cmd_consumer.go index 7e63fd8..2044b11 100644 --- a/cmd/twhelp/cmd_consumer.go +++ b/cmd/twhelp/cmd_consumer.go @@ -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 }, ) diff --git a/internal/adapter/http_tw.go b/internal/adapter/http_tw.go index c0db887..eb4c351 100644 --- a/internal/adapter/http_tw.go +++ b/internal/adapter/http_tw.go @@ -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 +} diff --git a/internal/adapter/publisher_watermill_server.go b/internal/adapter/publisher_watermill_server.go index 7ac6d5c..86f84b3 100644 --- a/internal/adapter/publisher_watermill_server.go +++ b/internal/adapter/publisher_watermill_server.go @@ -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) diff --git a/internal/app/service_server.go b/internal/app/service_server.go index a1fa504..04abaf5 100644 --- a/internal/app/service_server.go +++ b/internal/app/service_server.go @@ -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) } diff --git a/internal/app/service_tribe.go b/internal/app/service_tribe.go new file mode 100644 index 0000000..ac100a8 --- /dev/null +++ b/internal/app/service_tribe.go @@ -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 +} diff --git a/internal/app/service_tw.go b/internal/app/service_tw.go index 2a8b248..b0e8d54 100644 --- a/internal/app/service_tw.go +++ b/internal/app/service_tw.go @@ -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) } diff --git a/internal/domain/base_server.go b/internal/domain/base_server.go index 941d8da..4f9532a 100644 --- a/internal/domain/base_server.go +++ b/internal/domain/base_server.go @@ -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, } } diff --git a/internal/domain/base_server_test.go b/internal/domain/base_server_test.go index 2cc0b71..b3692ac 100644 --- a/internal/domain/base_server_test.go +++ b/internal/domain/base_server_test.go @@ -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()) }) } diff --git a/internal/domain/base_tribe.go b/internal/domain/base_tribe.go new file mode 100644 index 0000000..0c183b2 --- /dev/null +++ b/internal/domain/base_tribe.go @@ -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 diff --git a/internal/domain/base_tribe_test.go b/internal/domain/base_tribe_test.go new file mode 100644 index 0000000..6e76ae2 --- /dev/null +++ b/internal/domain/base_tribe_test.go @@ -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()) + }) + } +} diff --git a/internal/domain/domaintest/base_server.go b/internal/domain/domaintest/base_server.go index d752607..835b350 100644 --- a/internal/domain/domaintest/base_server.go +++ b/internal/domain/domaintest/base_server.go @@ -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 diff --git a/internal/domain/domaintest/base_tribe.go b/internal/domain/domaintest/base_tribe.go new file mode 100644 index 0000000..2d2ab73 --- /dev/null +++ b/internal/domain/domaintest/base_tribe.go @@ -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 +} diff --git a/internal/domain/domaintest/opponents_defeated.go b/internal/domain/domaintest/opponents_defeated.go new file mode 100644 index 0000000..34f8e1f --- /dev/null +++ b/internal/domain/domaintest/opponents_defeated.go @@ -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 +} diff --git a/internal/domain/domaintest/server_config.go b/internal/domain/domaintest/server_config.go index b1277dd..c13a542 100644 --- a/internal/domain/domaintest/server_config.go +++ b/internal/domain/domaintest/server_config.go @@ -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), diff --git a/internal/domain/domaintest/tribe.go b/internal/domain/domaintest/tribe.go new file mode 100644 index 0000000..c4028b6 --- /dev/null +++ b/internal/domain/domaintest/tribe.go @@ -0,0 +1,7 @@ +package domaintest + +import "github.com/brianvoe/gofakeit/v6" + +func RandTribeTag() string { + return gofakeit.LetterN(5) +} diff --git a/internal/domain/domaintest/unit_info.go b/internal/domain/domaintest/unit_info.go index 721adf3..ec70dea 100644 --- a/internal/domain/domaintest/unit_info.go +++ b/internal/domain/domaintest/unit_info.go @@ -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)}, diff --git a/internal/domain/domaintest/utils.go b/internal/domain/domaintest/utils.go new file mode 100644 index 0000000..74c42d7 --- /dev/null +++ b/internal/domain/domaintest/utils.go @@ -0,0 +1,7 @@ +package domaintest + +import "github.com/brianvoe/gofakeit/v6" + +func RandID() int { + return gofakeit.IntRange(1, 10000000) +} diff --git a/internal/domain/error.go b/internal/domain/error.go index 1bde5e3..038621c 100644 --- a/internal/domain/error.go +++ b/internal/domain/error.go @@ -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 +} diff --git a/internal/domain/opponents_defeated.go b/internal/domain/opponents_defeated.go new file mode 100644 index 0000000..96b8a77 --- /dev/null +++ b/internal/domain/opponents_defeated.go @@ -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 +} diff --git a/internal/domain/opponents_defeated_test.go b/internal/domain/opponents_defeated_test.go new file mode 100644 index 0000000..a4b504e --- /dev/null +++ b/internal/domain/opponents_defeated_test.go @@ -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()) + }) + } +} diff --git a/internal/domain/server.go b/internal/domain/server.go index 500ae1e..6cf4522 100644 --- a/internal/domain/server.go +++ b/internal/domain/server.go @@ -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 { diff --git a/internal/domain/server_message_payloads.go b/internal/domain/server_message_payloads.go index 1d3c720..8a77428 100644 --- a/internal/domain/server_message_payloads.go +++ b/internal/domain/server_message_payloads.go @@ -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{ diff --git a/internal/domain/server_message_payloads_test.go b/internal/domain/server_message_payloads_test.go index 2c127a1..620e472 100644 --- a/internal/domain/server_message_payloads_test.go +++ b/internal/domain/server_message_payloads_test.go @@ -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()) diff --git a/internal/domain/server_test.go b/internal/domain/server_test.go index 927ce67..cfeebdb 100644 --- a/internal/domain/server_test.go +++ b/internal/domain/server_test.go @@ -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, diff --git a/internal/domain/tribe.go b/internal/domain/tribe.go new file mode 100644 index 0000000..937de6c --- /dev/null +++ b/internal/domain/tribe.go @@ -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 +} diff --git a/internal/domain/utils.go b/internal/domain/utils.go index ec3f9a3..d398ba7 100644 --- a/internal/domain/utils.go +++ b/internal/domain/utils.go @@ -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) { diff --git a/internal/domain/validation.go b/internal/domain/validation.go index fbbf931..f3bd39a 100644 --- a/internal/domain/validation.go +++ b/internal/domain/validation.go @@ -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{ diff --git a/internal/domain/version.go b/internal/domain/version.go index ce3cca3..358c3e6 100644 --- a/internal/domain/version.go +++ b/internal/domain/version.go @@ -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, } diff --git a/internal/domain/version_test.go b/internal/domain/version_test.go index 654a207..d544d8f 100644 --- a/internal/domain/version_test.go +++ b/internal/domain/version_test.go @@ -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, diff --git a/internal/port/consumer_watermill_server.go b/internal/port/consumer_watermill_server.go index 972872f..4e36116 100644 --- a/internal/port/consumer_watermill_server.go +++ b/internal/port/consumer_watermill_server.go @@ -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()), diff --git a/internal/port/consumer_watermill_tribe.go b/internal/port/consumer_watermill_tribe.go new file mode 100644 index 0000000..0da87ac --- /dev/null +++ b/internal/port/consumer_watermill_tribe.go @@ -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) +} diff --git a/internal/tw/client.go b/internal/tw/client.go index a146528..3be90bf 100644 --- a/internal/tw/client.go +++ b/internal/tw/client.go @@ -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 } diff --git a/internal/tw/client_test.go b/internal/tw/client_test.go index 7b9ceab..9f9148d 100644 --- a/internal/tw/client_test.go +++ b/internal/tw/client_test.go @@ -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))...) diff --git a/internal/tw/tw.go b/internal/tw/tw.go index db7a6dd..0b66877 100644 --- a/internal/tw/tw.go +++ b/internal/tw/tw.go @@ -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 { diff --git a/internal/watermillmsg/server.go b/internal/watermillmsg/server.go index cb99082..08ee800 100644 --- a/internal/watermillmsg/server.go +++ b/internal/watermillmsg/server.go @@ -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"` } diff --git a/k8s/base/kustomization.yml b/k8s/base/kustomization.yml index 07244cb..e8d20b9 100644 --- a/k8s/base/kustomization.yml +++ b/k8s/base/kustomization.yml @@ -3,6 +3,7 @@ kind: Kustomization resources: - jobs.yml - server-consumer.yml + - tribe-consumer.yml images: - name: twhelp newName: twhelp diff --git a/k8s/base/tribe-consumer.yml b/k8s/base/tribe-consumer.yml new file mode 100644 index 0000000..3fa10f1 --- /dev/null +++ b/k8s/base/tribe-consumer.yml @@ -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