feat: notifications

This commit is contained in:
Dawid Wysokiński 2022-10-25 07:25:14 +02:00
parent db2026c6e4
commit e76cb17caf
Signed by: Kichiyaki
GPG Key ID: B5445E357FB8B892
13 changed files with 687 additions and 106 deletions

View File

@ -28,6 +28,11 @@ func New() *cli.Command {
Action: func(c *cli.Context) error {
logger := zap.L()
cfg, err := newBotConfig()
if err != nil {
return fmt.Errorf("newBotConfig: %w", err)
}
db, err := internal.NewBunDB()
if err != nil {
return fmt.Errorf("internal.NewBunDB: %w", err)
@ -43,22 +48,18 @@ func New() *cli.Command {
choiceSvc := service.NewChoice(client)
groupSvc := service.NewGroup(groupRepo, client)
monitorSvc := service.NewMonitor(monitorRepo, groupRepo, client)
cfg, err := newBotConfig()
if err != nil {
return fmt.Errorf("newBotConfig: %w", err)
}
monitorSvc := service.NewMonitor(monitorRepo, groupRepo, client, cfg.EnnoblementsReqLimit)
bot, err := discord.NewBot(cfg.Token, groupSvc, monitorSvc, choiceSvc)
if err != nil {
return fmt.Errorf("discord.NewBot: %w", err)
}
defer func() {
_ = bot.Close()
go func() {
_ = bot.Run()
}()
if err := os.WriteFile(healthyFilePath, []byte("healthy"), healthyFilePerm); err != nil {
if err = os.WriteFile(healthyFilePath, []byte("healthy"), healthyFilePerm); err != nil {
return fmt.Errorf("couldn't create file (path=%s): %w", healthyFilePath, err)
}
@ -66,13 +67,18 @@ func New() *cli.Command {
waitForSignal(c.Context)
if err = bot.Close(); err != nil {
return fmt.Errorf("bot.Close: %w", err)
}
return nil
},
}
}
type botConfig struct {
Token string `envconfig:"TOKEN" required:"true"`
Token string `envconfig:"TOKEN" required:"true"`
EnnoblementsReqLimit int `envconfig:"ENNOBLEMENTS_REQ_LIMIT" default:"4"`
}
func newBotConfig() (botConfig, error) {

1
go.mod
View File

@ -43,6 +43,7 @@ require (
github.com/opencontainers/runc v1.1.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect

2
go.sum
View File

@ -89,6 +89,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=

View File

@ -3,9 +3,11 @@ package discord
import (
"context"
"fmt"
"time"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"github.com/bwmarrin/discordgo"
"github.com/robfig/cron/v3"
)
type GroupService interface {
@ -18,6 +20,7 @@ type GroupService interface {
type MonitorService interface {
Create(ctx context.Context, groupID, serverID, tribeTag string) (domain.Monitor, error)
Execute(ctx context.Context) error
Delete(ctx context.Context, id, serverID string) error
}
@ -27,6 +30,7 @@ type ChoiceService interface {
type Bot struct {
s *discordgo.Session
c *cron.Cron
groupSvc GroupService
monitorSvc MonitorService
choiceSvc ChoiceService
@ -38,19 +42,46 @@ func NewBot(token string, groupSvc GroupService, monitorSvc MonitorService, clie
return nil, fmt.Errorf("discordgo.New: %w", err)
}
if err = s.Open(); err != nil {
return nil, fmt.Errorf("s.Open: %w", err)
}
b := &Bot{s: s, groupSvc: groupSvc, monitorSvc: monitorSvc, choiceSvc: client}
if err = b.registerCommands(); err != nil {
_ = s.Close()
return nil, fmt.Errorf("couldn't register commands: %w", err)
b := &Bot{
s: s,
c: cron.New(
cron.WithLocation(time.UTC),
cron.WithChain(
cron.SkipIfStillRunning(cron.DiscardLogger),
),
),
groupSvc: groupSvc,
monitorSvc: monitorSvc,
choiceSvc: client,
}
return b, nil
}
func (b *Bot) Run() error {
if err := b.initCron(); err != nil {
return fmt.Errorf("initCron: %w", err)
}
if err := b.s.Open(); err != nil {
return fmt.Errorf("s.Open: %w", err)
}
if err := b.registerCommands(); err != nil {
_ = b.s.Close()
return fmt.Errorf("couldn't register commands: %w", err)
}
b.c.Run()
return nil
}
type command interface {
name() string
register(s *discordgo.Session) error
}
func (b *Bot) registerCommands() error {
commands := []command{
&groupCommand{groupSvc: b.groupSvc, choiceSvc: b.choiceSvc},
@ -66,11 +97,6 @@ func (b *Bot) registerCommands() error {
return nil
}
type command interface {
name() string
register(s *discordgo.Session) error
}
func (b *Bot) registerCommand(cmd command) error {
if err := cmd.register(b.s); err != nil {
return fmt.Errorf("couldn't register command '%s': %w", cmd.name(), err)
@ -78,6 +104,15 @@ func (b *Bot) registerCommand(cmd command) error {
return nil
}
func (b *Bot) initCron() error {
_, err := b.c.AddJob("@every 1m", &executeMonitorsJob{svc: b.monitorSvc})
if err != nil {
return err
}
return nil
}
func (b *Bot) Close() error {
<-b.c.Stop().Done()
return b.s.Close()
}

View File

@ -0,0 +1,22 @@
package discord
import (
"context"
"fmt"
"time"
)
const (
executeMonitorsJobTimeout = 2 * time.Minute
)
type executeMonitorsJob struct {
svc MonitorService
}
func (e *executeMonitorsJob) Run() {
ctx, cancel := context.WithTimeout(context.Background(), executeMonitorsJobTimeout)
defer cancel()
fmt.Println("execute")
_ = e.svc.Execute(ctx)
}

View File

@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"sync"
"time"
"gitea.dwysokinski.me/twhelp/dcbot/internal/domain"
"gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp"
@ -21,19 +23,33 @@ type MonitorRepository interface {
Delete(ctx context.Context, id string) error
}
//counterfeiter:generate -o internal/mock/group_getter.gen.go . GroupGetter
type GroupGetter interface {
//counterfeiter:generate -o internal/mock/group_reader.gen.go . GroupReader
type GroupReader interface {
Get(ctx context.Context, id, serverID string) (domain.Group, error)
List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error)
}
type Monitor struct {
repo MonitorRepository
client TWHelpClient
groupSvc GroupGetter
repo MonitorRepository
client TWHelpClient
groupSvc GroupReader
lastExecutionMu sync.RWMutex
lastExecution time.Time
ennoblementsReqLimit int
}
func NewMonitor(repo MonitorRepository, groupSvc GroupGetter, client TWHelpClient) *Monitor {
return &Monitor{repo: repo, client: client, groupSvc: groupSvc}
func NewMonitor(
repo MonitorRepository,
groupSvc GroupReader,
client TWHelpClient,
ennoblementsReqLimit int,
) *Monitor {
return &Monitor{
repo: repo,
client: client,
groupSvc: groupSvc,
ennoblementsReqLimit: ennoblementsReqLimit,
}
}
func (m *Monitor) Create(ctx context.Context, groupID, serverID, tribeTag string) (domain.Monitor, error) {
@ -107,3 +123,110 @@ func (m *Monitor) Delete(ctx context.Context, id, serverID string) error {
return nil
}
func (m *Monitor) Execute(ctx context.Context) error {
groups, err := m.groupSvc.List(ctx, domain.ListGroupsParams{})
if err != nil {
return fmt.Errorf("GroupService.List: %w", err)
}
ennoblements, err := m.preloadEnnoblements(ctx, groups)
if err != nil {
return err
}
for _, e := range ennoblements {
fmt.Println(e)
}
return nil
}
type listEnnoblementsResult struct {
versionCode string
serverKey string
ennoblements []twhelp.Ennoblement
err error
}
type preloadEnnoblementsResult struct {
VersionCode string
ServerKey string
Ennoblements []twhelp.Ennoblement
}
func (m *Monitor) preloadEnnoblements(ctx context.Context, groups []domain.Group) ([]preloadEnnoblementsResult, error) {
var wg sync.WaitGroup
ch := make(chan listEnnoblementsResult)
reqLimitCh := make(chan struct{}, m.ennoblementsReqLimit)
skip := make(map[string]struct{})
m.lastExecutionMu.RLock()
since := m.lastExecution
m.lastExecutionMu.RUnlock()
if since.IsZero() {
since = time.Now().Add(-1 * time.Minute)
}
since = since.Add(1 * time.Millisecond)
for _, g := range groups {
if g.ChannelGains == "" && g.ChannelLosses == "" {
continue
}
key := g.VersionCode + ":" + g.ServerKey
if _, ok := skip[key]; ok {
continue
}
skip[key] = struct{}{}
wg.Add(1)
go func(g domain.Group, since time.Time) {
reqLimitCh <- struct{}{}
defer func() {
<-reqLimitCh
}()
res := listEnnoblementsResult{
versionCode: g.VersionCode,
serverKey: g.ServerKey,
}
res.ennoblements, res.err = m.client.ListEnnoblements(
ctx,
g.VersionCode,
g.ServerKey,
twhelp.ListEnnoblementsQueryParams{
Since: since,
},
)
ch <- res
}(g, since)
}
go func() {
wg.Wait()
close(ch)
}()
//nolint:prealloc
var results []preloadEnnoblementsResult
for res := range ch {
wg.Done()
if res.err != nil {
continue
}
results = append(results, preloadEnnoblementsResult{
VersionCode: res.versionCode,
ServerKey: res.serverKey,
Ennoblements: res.ennoblements,
})
}
m.lastExecutionMu.Lock()
m.lastExecution = time.Now()
m.lastExecutionMu.Unlock()
return results, nil
}

View File

@ -30,7 +30,7 @@ func TestMonitor_Create(t *testing.T) {
}, nil
})
groupSvc := &mock.FakeGroupGetter{}
groupSvc := &mock.FakeGroupReader{}
groupSvc.GetCalls(func(ctx context.Context, groupID, serverID string) (domain.Group, error) {
return domain.Group{
ID: groupID,
@ -55,7 +55,7 @@ func TestMonitor_Create(t *testing.T) {
groupID := uuid.NewString()
monitor, err := service.NewMonitor(repo, groupSvc, client).
monitor, err := service.NewMonitor(repo, groupSvc, client, 10).
Create(context.Background(), groupID, uuid.NewString(), tribe.Tag)
assert.NoError(t, err)
assert.NotEmpty(t, monitor.ID)
@ -68,10 +68,10 @@ func TestMonitor_Create(t *testing.T) {
t.Parallel()
groupID := uuid.NewString()
groupSvc := &mock.FakeGroupGetter{}
groupSvc := &mock.FakeGroupReader{}
groupSvc.GetReturns(domain.Group{}, domain.GroupNotFoundError{ID: groupID})
monitor, err := service.NewMonitor(nil, groupSvc, nil).
monitor, err := service.NewMonitor(nil, groupSvc, nil, 10).
Create(context.Background(), groupID, uuid.NewString(), "tag")
assert.ErrorIs(t, err, domain.GroupDoesNotExistError{
ID: groupID,
@ -87,7 +87,7 @@ func TestMonitor_Create(t *testing.T) {
repo := &mock.FakeMonitorRepository{}
repo.ListReturns(make([]domain.Monitor, maxMonitorsPerGroup), nil)
groupSvc := &mock.FakeGroupGetter{}
groupSvc := &mock.FakeGroupReader{}
groupSvc.GetCalls(func(ctx context.Context, groupID, serverID string) (domain.Group, error) {
return domain.Group{
ID: groupID,
@ -100,7 +100,7 @@ func TestMonitor_Create(t *testing.T) {
}, nil
})
monitor, err := service.NewMonitor(repo, groupSvc, nil).
monitor, err := service.NewMonitor(repo, groupSvc, nil, 10).
Create(context.Background(), uuid.NewString(), uuid.NewString(), "TAG")
assert.ErrorIs(t, err, domain.MonitorLimitReachedError{
Current: maxMonitorsPerGroup,
@ -126,7 +126,7 @@ func TestMonitor_Create(t *testing.T) {
}, nil
})
groupSvc := &mock.FakeGroupGetter{}
groupSvc := &mock.FakeGroupReader{}
groupSvc.GetCalls(func(ctx context.Context, groupID, serverID string) (domain.Group, error) {
return domain.Group{
ID: groupID,
@ -147,7 +147,7 @@ func TestMonitor_Create(t *testing.T) {
tag := "TAG"
monitor, err := service.NewMonitor(repo, groupSvc, client).
monitor, err := service.NewMonitor(repo, groupSvc, client, 10).
Create(context.Background(), uuid.NewString(), uuid.NewString(), tag)
assert.ErrorIs(t, err, domain.TribeDoesNotExistError{
Tag: tag,
@ -161,7 +161,7 @@ func TestMonitor_Create(t *testing.T) {
repo := &mock.FakeMonitorRepository{}
repo.ListReturns(nil, nil)
groupSvc := &mock.FakeGroupGetter{}
groupSvc := &mock.FakeGroupReader{}
groupSvc.GetCalls(func(ctx context.Context, groupID, serverID string) (domain.Group, error) {
return domain.Group{
ID: groupID,
@ -184,7 +184,7 @@ func TestMonitor_Create(t *testing.T) {
}
client.GetTribeByTagReturns(tribe, nil)
monitor, err := service.NewMonitor(repo, groupSvc, client).
monitor, err := service.NewMonitor(repo, groupSvc, client, 10).
Create(context.Background(), uuid.NewString(), uuid.NewString(), tribe.Tag)
assert.ErrorIs(t, err, domain.TribeDoesNotExistError{
Tag: tribe.Tag,

View File

@ -13,4 +13,9 @@ type TWHelpClient interface {
ListVersions(ctx context.Context) ([]twhelp.Version, error)
GetServer(ctx context.Context, version, server string) (twhelp.Server, error)
GetTribeByTag(ctx context.Context, version, server, tag string) (twhelp.Tribe, error)
ListEnnoblements(
ctx context.Context,
version, server string,
queryParams twhelp.ListEnnoblementsQueryParams,
) ([]twhelp.Ennoblement, error)
}

View File

@ -7,6 +7,7 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"time"
)
@ -14,9 +15,10 @@ const (
defaultUserAgent = "TWHelpDCBot/development"
defaultTimeout = 10 * time.Second
endpointListVersions = "/api/v1/versions"
endpointGetServer = "/api/v1/versions/%s/servers/%s"
endpointGetTribeByTag = "/api/v1/versions/%s/servers/%s/tribes/tag/%s"
endpointListVersions = "/api/v1/versions"
endpointGetServer = "/api/v1/versions/%s/servers/%s"
endpointGetTribeByTag = "/api/v1/versions/%s/servers/%s/tribes/tag/%s"
endpointListEnnoblements = "/api/v1/versions/%s/servers/%s/ennoblements"
)
type Client struct {
@ -77,6 +79,40 @@ func (c *Client) GetTribeByTag(ctx context.Context, version, server, tag string)
return resp.Data, nil
}
type ListEnnoblementsQueryParams struct {
Limit int32
Offset int32
Since time.Time
}
func (c *Client) ListEnnoblements(
ctx context.Context,
version, server string,
queryParams ListEnnoblementsQueryParams,
) ([]Ennoblement, error) {
q := url.Values{}
if queryParams.Limit >= 0 {
q.Set("limit", strconv.Itoa(int(queryParams.Limit)))
}
if queryParams.Offset >= 0 {
q.Set("offset", strconv.Itoa(int(queryParams.Offset)))
}
if !queryParams.Since.IsZero() {
q.Set("since", queryParams.Since.Format(time.RFC3339))
}
var resp listEnnoblementsResp
if err := c.getJSON(ctx, fmt.Sprintf(endpointListEnnoblements, version, server)+"?"+q.Encode(), &resp); err != nil {
return nil, err
}
return resp.Data, nil
}
func (c *Client) getJSON(ctx context.Context, urlStr string, v any) error {
u, err := c.baseURL.Parse(urlStr)
if err != nil {

154
internal/twhelp/twhelp.go Normal file
View File

@ -0,0 +1,154 @@
package twhelp
import (
"encoding/json"
"time"
)
type ErrorCode string
const (
ErrorCodeInternalServerError ErrorCode = "internal-server-error"
ErrorCodeEntityNotFound ErrorCode = "entity-not-found"
ErrorCodeValidationError ErrorCode = "validation-error"
ErrorCodeRouteNotFound ErrorCode = "route-not-found"
ErrorCodeMethodNotAllowed ErrorCode = "method-not-allowed"
)
func (c ErrorCode) String() string {
return string(c)
}
type APIError struct {
Code ErrorCode `json:"code"`
Message string `json:"message"`
}
func (e APIError) Error() string {
return e.Message + " (code=" + e.Code.String() + ")"
}
type errorResp struct {
Error APIError `json:"error"`
}
type Version struct {
Code string `json:"code"`
Host string `json:"host"`
Name string `json:"name"`
Timezone string `json:"timezone"`
}
type listVersionsResp struct {
Data []Version `json:"data"`
}
type Server struct {
Key string `json:"key"`
URL string `json:"url"`
Open bool `json:"open"`
}
type getServerResp struct {
Data Server `json:"data"`
}
type Tribe struct {
ID int64 `json:"id"`
Tag string `json:"tag"`
Name string `json:"name"`
ProfileURL string `json:"profileUrl"`
DeletedAt time.Time `json:"deletedAt"`
}
type getTribeResp struct {
Data Tribe `json:"data"`
}
type TribeMeta struct {
ID int64 `json:"id"`
Name string `json:"name"`
Tag string `json:"tag"`
ProfileURL string `json:"profileUrl"`
}
type NullTribeMeta struct {
Tribe TribeMeta
Valid bool
}
func (t NullTribeMeta) MarshalJSON() ([]byte, error) {
if !t.Valid {
return []byte("null"), nil
}
return json.Marshal(t.Tribe)
}
func (t *NullTribeMeta) UnmarshalJSON(data []byte) error {
// Ignore null, like in the main JSON package.
if string(data) == "null" {
return nil
}
if err := json.Unmarshal(data, &t.Tribe); err != nil {
return err
}
t.Valid = true
return nil
}
type PlayerMeta struct {
ID int64 `json:"id"`
Name string `json:"name"`
ProfileURL string `json:"profileUrl"`
Tribe NullTribeMeta `json:"tribe"`
}
type NullPlayerMeta struct {
Player PlayerMeta
Valid bool
}
func (p NullPlayerMeta) MarshalJSON() ([]byte, error) {
if !p.Valid {
return []byte("null"), nil
}
return json.Marshal(p.Player)
}
func (p *NullPlayerMeta) UnmarshalJSON(data []byte) error {
// Ignore null, like in the main JSON package.
if string(data) == "null" {
return nil
}
if err := json.Unmarshal(data, &p.Player); err != nil {
return err
}
p.Valid = true
return nil
}
type VillageMeta struct {
ID int64 `json:"id"`
FullName string `json:"fullName"`
ProfileURL string `json:"profileUrl"`
}
type Ennoblement struct {
ID int64 `json:"id"`
Village VillageMeta `json:"village"`
NewOwner NullPlayerMeta `json:"newOwner"`
OldOwner NullPlayerMeta `json:"oldOwner"`
CreatedAt time.Time `json:"createdAt"`
}
type listEnnoblementsResp struct {
Data []Ennoblement `json:"data"`
}

View File

@ -0,0 +1,258 @@
package twhelp_test
import (
"encoding/json"
"testing"
"gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp"
"github.com/stretchr/testify/assert"
)
func TestNullTribeMeta_MarshalJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
tribe twhelp.NullTribeMeta
expectedJSON string
}{
{
name: "OK: null 1",
tribe: twhelp.NullTribeMeta{
Tribe: twhelp.TribeMeta{},
Valid: false,
},
expectedJSON: "null",
},
{
name: "OK: null 2",
tribe: twhelp.NullTribeMeta{
Tribe: twhelp.TribeMeta{ID: 1234},
Valid: false,
},
expectedJSON: "null",
},
{
name: "OK: valid struct",
tribe: twhelp.NullTribeMeta{
Tribe: twhelp.TribeMeta{
ID: 997,
Name: "name 997",
Tag: "tag 997",
ProfileURL: "profile-997",
},
Valid: true,
},
expectedJSON: `{"id":997,"name":"name 997","tag":"tag 997","profileUrl":"profile-997"}`,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
b, err := json.Marshal(tt.tribe)
assert.NoError(t, err)
assert.JSONEq(t, tt.expectedJSON, string(b))
})
}
}
func TestNullTribeMeta_UnmarshalJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
json string
expectedTribe twhelp.NullTribeMeta
expectedJSONSyntaxError bool
}{
{
name: "OK: null",
json: "null",
expectedTribe: twhelp.NullTribeMeta{
Tribe: twhelp.TribeMeta{},
Valid: false,
},
},
{
name: "OK: valid struct",
//nolint:lll
json: `{"id":997,"name":"name 997","tag":"tag 997","profileUrl":"profile-997"}`,
expectedTribe: twhelp.NullTribeMeta{
Tribe: twhelp.TribeMeta{
ID: 997,
Name: "name 997",
Tag: "tag 997",
ProfileURL: "profile-997",
},
Valid: true,
},
},
{
name: "ERR: invalid tribe 1",
json: "2022-07-30T14:13:12.0000005Z",
expectedTribe: twhelp.NullTribeMeta{},
expectedJSONSyntaxError: true,
},
{
name: "ERR: invalid tribe 2",
json: "hello world",
expectedTribe: twhelp.NullTribeMeta{},
expectedJSONSyntaxError: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var target twhelp.NullTribeMeta
err := json.Unmarshal([]byte(tt.json), &target)
if tt.expectedJSONSyntaxError {
var syntaxError *json.SyntaxError
assert.ErrorAs(t, err, &syntaxError)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.expectedTribe, target)
})
}
}
func TestNullPlayerMeta_MarshalJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
player twhelp.NullPlayerMeta
expectedJSON string
}{
{
name: "OK: null 1",
player: twhelp.NullPlayerMeta{
Player: twhelp.PlayerMeta{},
Valid: false,
},
expectedJSON: "null",
},
{
name: "OK: null 2",
player: twhelp.NullPlayerMeta{
Player: twhelp.PlayerMeta{ID: 1234},
Valid: false,
},
expectedJSON: "null",
},
{
name: "OK: valid struct",
player: twhelp.NullPlayerMeta{
Player: twhelp.PlayerMeta{
ID: 997,
Name: "name 997",
ProfileURL: "profile-997",
Tribe: twhelp.NullTribeMeta{
Valid: true,
Tribe: twhelp.TribeMeta{
ID: 1234,
Name: "name 997",
ProfileURL: "profile-997",
Tag: "tag 997",
},
},
},
Valid: true,
},
//nolint:lll
expectedJSON: `{"id":997,"name":"name 997","profileUrl":"profile-997","tribe":{"id":1234,"name":"name 997","profileUrl":"profile-997","tag":"tag 997"}}`,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
b, err := json.Marshal(tt.player)
assert.NoError(t, err)
assert.JSONEq(t, tt.expectedJSON, string(b))
})
}
}
func TestNullPlayerMeta_UnmarshalJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
json string
expectedPlayer twhelp.NullPlayerMeta
expectedJSONSyntaxError bool
}{
{
name: "OK: null",
json: "null",
expectedPlayer: twhelp.NullPlayerMeta{
Player: twhelp.PlayerMeta{},
Valid: false,
},
},
{
name: "OK: valid struct",
//nolint:lll
json: `{"id":997,"name":"name 997","profileUrl":"profile-997","tribe":{"id":1234,"name":"name 997","profileUrl":"profile-997","tag":"tag 997"}}`,
expectedPlayer: twhelp.NullPlayerMeta{
Player: twhelp.PlayerMeta{
ID: 997,
Name: "name 997",
ProfileURL: "profile-997",
Tribe: twhelp.NullTribeMeta{
Valid: true,
Tribe: twhelp.TribeMeta{
ID: 1234,
Name: "name 997",
ProfileURL: "profile-997",
Tag: "tag 997",
},
},
},
Valid: true,
},
},
{
name: "ERR: invalid tribe 1",
json: "2022-07-30T14:13:12.0000005Z",
expectedPlayer: twhelp.NullPlayerMeta{},
expectedJSONSyntaxError: true,
},
{
name: "ERR: invalid tribe 2",
json: "hello world",
expectedPlayer: twhelp.NullPlayerMeta{},
expectedJSONSyntaxError: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var target twhelp.NullPlayerMeta
err := json.Unmarshal([]byte(tt.json), &target)
if tt.expectedJSONSyntaxError {
var syntaxError *json.SyntaxError
assert.ErrorAs(t, err, &syntaxError)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.expectedPlayer, target)
})
}
}

View File

@ -1,63 +0,0 @@
package twhelp
import "time"
type ErrorCode string
const (
ErrorCodeInternalServerError ErrorCode = "internal-server-error"
ErrorCodeEntityNotFound ErrorCode = "entity-not-found"
ErrorCodeValidationError ErrorCode = "validation-error"
ErrorCodeRouteNotFound ErrorCode = "route-not-found"
ErrorCodeMethodNotAllowed ErrorCode = "method-not-allowed"
)
func (c ErrorCode) String() string {
return string(c)
}
type APIError struct {
Code ErrorCode `json:"code"`
Message string `json:"message"`
}
func (e APIError) Error() string {
return e.Message + " (code=" + e.Code.String() + ")"
}
type errorResp struct {
Error APIError `json:"error"`
}
type Version struct {
Code string `json:"code"`
Host string `json:"host"`
Name string `json:"name"`
Timezone string `json:"timezone"`
}
type listVersionsResp struct {
Data []Version `json:"data"`
}
type Server struct {
Key string `json:"key"`
URL string `json:"url"`
Open bool `json:"open"`
}
type getServerResp struct {
Data Server `json:"data"`
}
type Tribe struct {
ID int64 `json:"id"`
Tag string `json:"tag"`
Name string `json:"name"`
ProfileURL string `json:"profileUrl"`
DeletedAt time.Time `json:"deletedAt"`
}
type getTribeResp struct {
Data Tribe `json:"data"`
}

View File

@ -34,6 +34,8 @@ spec:
key: token
- name: TWHELP_URL
value: "https://tribalwarshelp.com"
- name: BOT_ENNOBLEMENTS_REQ_LIMIT
value: "4"
livenessProbe:
exec:
command: [ "cat", "/tmp/healthy" ]