From 58f73e9ca817cdacaad1da14289fd4f0b1f17b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Sun, 23 Oct 2022 06:20:48 +0000 Subject: [PATCH] feat: add a new command - /monitor create (#21) Reviewed-on: https://gitea.dwysokinski.me/twhelp/dcbot/pulls/21 --- cmd/dcbot/internal/run/run.go | 4 +- internal/bundb/group.go | 27 ++- internal/bundb/group_test.go | 182 ++++++++++------ internal/bundb/internal/model/monitor.go | 27 +++ .../20221022051838_create_monitors_table.go | 31 +++ internal/bundb/monitor.go | 95 +++++++++ internal/bundb/monitor_test.go | 140 +++++++++++++ internal/bundb/testdata/fixture.yml | 27 ++- internal/discord/bot.go | 30 ++- internal/discord/command_group.go | 4 +- internal/discord/command_monitor.go | 118 +++++++++++ internal/domain/error.go | 38 +++- internal/domain/error_test.go | 25 ++- internal/domain/group.go | 25 ++- internal/domain/group_test.go | 27 ++- internal/domain/monitor.go | 84 ++++++++ internal/domain/monitor_test.go | 93 +++++++++ internal/domain/tw.go | 16 ++ internal/domain/tw_test.go | 12 ++ internal/service/group.go | 9 + internal/service/monitor.go | 89 ++++++++ internal/service/monitor_test.go | 195 ++++++++++++++++++ internal/service/service.go | 1 + internal/twhelp/client.go | 13 +- internal/twhelp/{models.go => types.go} | 14 ++ 25 files changed, 1234 insertions(+), 92 deletions(-) create mode 100644 internal/bundb/internal/model/monitor.go create mode 100644 internal/bundb/migrations/20221022051838_create_monitors_table.go create mode 100644 internal/bundb/monitor.go create mode 100644 internal/bundb/monitor_test.go create mode 100644 internal/discord/command_monitor.go create mode 100644 internal/domain/monitor.go create mode 100644 internal/domain/monitor_test.go create mode 100644 internal/service/monitor.go create mode 100644 internal/service/monitor_test.go rename internal/twhelp/{models.go => types.go} (78%) diff --git a/cmd/dcbot/internal/run/run.go b/cmd/dcbot/internal/run/run.go index 1e4d12f..b0d91f1 100644 --- a/cmd/dcbot/internal/run/run.go +++ b/cmd/dcbot/internal/run/run.go @@ -39,15 +39,17 @@ func New() *cli.Command { } groupRepo := bundb.NewGroup(db) + monitorRepo := bundb.NewMonitor(db) groupSvc := service.NewGroup(groupRepo, client) + monitorSvc := service.NewMonitor(monitorRepo, groupRepo, client) cfg, err := newBotConfig() if err != nil { return fmt.Errorf("newBotConfig: %w", err) } - bot, err := discord.NewBot(cfg.Token, groupSvc, client) + bot, err := discord.NewBot(cfg.Token, groupSvc, monitorSvc, client) if err != nil { return fmt.Errorf("discord.NewBot: %w", err) } diff --git a/internal/bundb/group.go b/internal/bundb/group.go index 306ea8a..38312a7 100644 --- a/internal/bundb/group.go +++ b/internal/bundb/group.go @@ -79,12 +79,35 @@ func (g *Group) List(ctx context.Context, params domain.ListGroupsParams) ([]dom } result := make([]domain.Group, 0, len(groups)) - for _, version := range groups { - result = append(result, version.ToDomain()) + for _, group := range groups { + result = append(result, group.ToDomain()) } + return result, nil } +func (g *Group) Get(ctx context.Context, id, serverID string) (domain.Group, error) { + if _, err := uuid.Parse(id); err != nil { + return domain.Group{}, domain.GroupNotFoundError{ID: id} + } + + var group model.Group + + err := g.db.NewSelect(). + Model(&group). + Where("id = ?", id). + Where("server_id = ?", serverID). + Scan(ctx) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return domain.Group{}, domain.GroupNotFoundError{ID: id} + } + return domain.Group{}, fmt.Errorf("couldn't select group (id=%s,serverID=%s) from the db: %w", id, serverID, err) + } + + return group.ToDomain(), nil +} + func (g *Group) Delete(ctx context.Context, id, serverID string) error { if _, err := uuid.Parse(id); err != nil { return domain.GroupNotFoundError{ID: id} diff --git a/internal/bundb/group_test.go b/internal/bundb/group_test.go index 737ff29..4533bcb 100644 --- a/internal/bundb/group_test.go +++ b/internal/bundb/group_test.go @@ -50,7 +50,7 @@ func TestGroup_Update(t *testing.T) { db := newDB(t) fixture := loadFixtures(t, db) repo := bundb.NewGroup(db) - group := getGroupFromFixture(t, fixture, "group-server-1-1") + group := getGroupFromFixture(t, fixture, "group-1-server-1") t.Run("OK", func(t *testing.T) { t.Parallel() @@ -80,45 +80,47 @@ func TestGroup_Update(t *testing.T) { assert.Zero(t, updatedGroup) }) - t.Run("ERR: invalid UUID", func(t *testing.T) { + t.Run("ERR: group not found", func(t *testing.T) { t.Parallel() - id := "12345" - updatedGroup, err := repo.Update(context.Background(), id, "", domain.UpdateGroupParams{ - ChannelGains: domain.NullString{ - String: "update", - Valid: true, + tests := []struct { + name string + id string + serverID string + }{ + { + name: "ID - not UUID", + id: "test", + serverID: group.ServerID, }, - }) - assert.ErrorIs(t, err, domain.GroupNotFoundError{ID: id}) - assert.Zero(t, updatedGroup) - }) - - t.Run("ERR: group not found (unknown ID)", func(t *testing.T) { - t.Parallel() - - id := uuid.NewString() - updatedGroup, err := repo.Update(context.Background(), id, group.ServerID, domain.UpdateGroupParams{ - ChannelGains: domain.NullString{ - String: "update", - Valid: true, + { + name: "ID - random UUID", + id: uuid.NewString(), + serverID: group.ServerID, }, - }) - assert.ErrorIs(t, err, domain.GroupNotFoundError{ID: id}) - assert.Zero(t, updatedGroup) - }) - - t.Run("ERR: group not found (unknown ServerID)", func(t *testing.T) { - t.Parallel() - - updatedGroup, err := repo.Update(context.Background(), group.ID.String(), uuid.NewString(), domain.UpdateGroupParams{ - ChannelGains: domain.NullString{ - String: "update", - Valid: true, + { + name: "ServerID - random UUID", + id: group.ID.String(), + serverID: uuid.NewString(), }, - }) - assert.ErrorIs(t, err, domain.GroupNotFoundError{ID: group.ID.String()}) - assert.Zero(t, updatedGroup) + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + updatedGroup, err := repo.Update(context.Background(), tt.id, tt.serverID, domain.UpdateGroupParams{ + ChannelGains: domain.NullString{ + String: "update", + Valid: true, + }, + }) + assert.ErrorIs(t, err, domain.GroupNotFoundError{ID: tt.id}) + assert.Zero(t, updatedGroup) + }) + } }) } @@ -180,13 +182,65 @@ func TestGroup_List(t *testing.T) { } } +func TestGroup_Get(t *testing.T) { + t.Parallel() + + db := newDB(t) + fixture := loadFixtures(t, db) + repo := bundb.NewGroup(db) + group := getGroupFromFixture(t, fixture, "group-1-server-1") + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("ERR: group not found (unknown ID)", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + id string + serverID string + }{ + { + name: "ID - not UUID", + id: "test", + serverID: group.ServerID, + }, + { + name: "ID - random UUID", + id: uuid.NewString(), + serverID: group.ServerID, + }, + { + name: "ServerID - random UUID", + id: group.ID.String(), + serverID: uuid.NewString(), + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + g, err := repo.Get(context.Background(), tt.id, tt.serverID) + assert.ErrorIs(t, err, domain.GroupNotFoundError{ID: tt.id}) + assert.Zero(t, g) + }) + } + }) +} + func TestGroup_Delete(t *testing.T) { t.Parallel() db := newDB(t) fixture := loadFixtures(t, db) repo := bundb.NewGroup(db) - group := getGroupFromFixture(t, fixture, "group-server-1-1") + group := getGroupFromFixture(t, fixture, "group-1-server-1") t.Run("OK", func(t *testing.T) { t.Parallel() @@ -200,32 +254,44 @@ func TestGroup_Delete(t *testing.T) { assert.Len(t, groups, 0) }) - t.Run("ERR: invalid UUID", func(t *testing.T) { + t.Run("ERR: group not found", func(t *testing.T) { t.Parallel() - id := "12345" - assert.ErrorIs(t, repo.Delete(context.Background(), id, ""), domain.GroupNotFoundError{ID: id}) - }) + tests := []struct { + name string + id string + serverID string + }{ + { + name: "ID - not UUID", + id: "test", + serverID: group.ServerID, + }, + { + name: "ID - random UUID", + id: uuid.NewString(), + serverID: group.ServerID, + }, + { + name: "ServerID - random UUID", + id: group.ID.String(), + serverID: uuid.NewString(), + }, + } - t.Run("ERR: group not found (unknown ID)", func(t *testing.T) { - t.Parallel() + for _, tt := range tests { + tt := tt - id := uuid.NewString() - assert.ErrorIs( - t, - repo.Delete(context.Background(), id, group.ServerID), - domain.GroupNotFoundError{ID: id}, - ) - }) + t.Run(tt.name, func(t *testing.T) { + t.Parallel() - t.Run("ERR: group not found (unknown ServerID)", func(t *testing.T) { - t.Parallel() - - assert.ErrorIs( - t, - repo.Delete(context.Background(), group.ID.String(), uuid.NewString()), - domain.GroupNotFoundError{ID: group.ID.String()}, - ) + assert.ErrorIs( + t, + repo.Delete(context.Background(), tt.id, tt.serverID), + domain.GroupNotFoundError{ID: tt.id}, + ) + }) + } }) } @@ -233,7 +299,7 @@ func getAllGroupsFromFixture(tb testing.TB, fixture *dbfixture.Fixture) []model. tb.Helper() //nolint:lll - ids := []string{"group-server-1-1", "group-server-1-2", "group-server-2-1", "group-server-2-2"} + ids := []string{"group-1-server-1", "group-2-server-1", "group-1-server-2", "group-2-server-2"} groups := make([]model.Group, 0, len(ids)) for _, id := range ids { diff --git a/internal/bundb/internal/model/monitor.go b/internal/bundb/internal/model/monitor.go new file mode 100644 index 0000000..cf2d82e --- /dev/null +++ b/internal/bundb/internal/model/monitor.go @@ -0,0 +1,27 @@ +package model + +import ( + "time" + + "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" + "github.com/google/uuid" + "github.com/uptrace/bun" +) + +type Monitor struct { + bun.BaseModel `bun:"base_model,table:monitors,alias:monitor"` + + ID uuid.UUID `bun:"id,type:uuid,pk,default:gen_random_uuid()"` + GroupID uuid.UUID `bun:"group_id,type:uuid,notnull,unique:monitors_group_id_tribe_id_key"` + TribeID int64 `bun:"tribe_id,notnull,unique:monitors_group_id_tribe_id_key"` + CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"` +} + +func (m Monitor) ToDomain() domain.Monitor { + return domain.Monitor{ + ID: m.ID.String(), + GroupID: m.GroupID.String(), + TribeID: m.TribeID, + CreatedAt: m.CreatedAt, + } +} diff --git a/internal/bundb/migrations/20221022051838_create_monitors_table.go b/internal/bundb/migrations/20221022051838_create_monitors_table.go new file mode 100644 index 0000000..db8680a --- /dev/null +++ b/internal/bundb/migrations/20221022051838_create_monitors_table.go @@ -0,0 +1,31 @@ +package migrations + +import ( + "context" + "fmt" + + "gitea.dwysokinski.me/twhelp/dcbot/internal/bundb/internal/model" + "github.com/uptrace/bun" +) + +func init() { + Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { + if _, err := db.NewCreateTable(). + Model(&model.Monitor{}). + Varchar(defaultVarcharLength). + ForeignKey(`(group_id) REFERENCES groups (id) ON DELETE CASCADE`). + Exec(ctx); err != nil { + return fmt.Errorf("couldn't create the 'monitors' table: %w", err) + } + return nil + }, func(ctx context.Context, db *bun.DB) error { + if _, err := db.NewDropTable(). + Model(&model.Group{}). + IfExists(). + Cascade(). + Exec(ctx); err != nil { + return fmt.Errorf("couldn't drop the 'monitors' table: %w", err) + } + return nil + }) +} diff --git a/internal/bundb/monitor.go b/internal/bundb/monitor.go new file mode 100644 index 0000000..af3c0e4 --- /dev/null +++ b/internal/bundb/monitor.go @@ -0,0 +1,95 @@ +package bundb + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "gitea.dwysokinski.me/twhelp/dcbot/internal/bundb/internal/model" + "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" + "github.com/google/uuid" + "github.com/uptrace/bun" + "github.com/uptrace/bun/driver/pgdriver" +) + +type Monitor struct { + db *bun.DB +} + +func NewMonitor(db *bun.DB) *Monitor { + return &Monitor{db: db} +} + +func (m *Monitor) Create(ctx context.Context, params domain.CreateMonitorParams) (domain.Monitor, error) { + groupID, err := uuid.Parse(params.GroupID()) + if err != nil { + return domain.Monitor{}, domain.GroupDoesNotExistError{ + ID: params.GroupID(), + } + } + + monitor := model.Monitor{ + GroupID: groupID, + TribeID: params.TribeID(), + } + + if _, err = m.db.NewInsert(). + Model(&monitor). + Returning("*"). + Exec(ctx); err != nil { + + return domain.Monitor{}, fmt.Errorf( + "something went wrong while inserting monitor into the db: %w", + mapCreateMonitorError(err, params), + ) + } + + return monitor.ToDomain(), nil +} + +func (m *Monitor) List(ctx context.Context, groupID string) ([]domain.Monitor, error) { + if _, err := uuid.Parse(groupID); err != nil { + return nil, nil + } + + var monitors []model.Monitor + + if err := m.db.NewSelect(). + Model(&monitors). + Order("created_at ASC"). + Where("group_id = ?", groupID). + Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("couldn't select monitors from the db: %w", err) + } + + result := make([]domain.Monitor, 0, len(monitors)) + for _, monitor := range monitors { + result = append(result, monitor.ToDomain()) + } + + return result, nil +} + +func mapCreateMonitorError(err error, params domain.CreateMonitorParams) error { + var pgError pgdriver.Error + if !errors.As(err, &pgError) { + return err + } + + code := pgError.Field('C') + constraint := pgError.Field('n') + switch { + case code == "23503" && constraint == "monitors_group_id_fkey": + return domain.GroupDoesNotExistError{ + ID: params.GroupID(), + } + case code == "23505" && constraint == "monitors_group_id_tribe_id_key": + return domain.MonitorAlreadyExistsError{ + TribeID: params.TribeID(), + GroupID: params.GroupID(), + } + default: + return err + } +} diff --git a/internal/bundb/monitor_test.go b/internal/bundb/monitor_test.go new file mode 100644 index 0000000..945446f --- /dev/null +++ b/internal/bundb/monitor_test.go @@ -0,0 +1,140 @@ +package bundb_test + +import ( + "context" + "testing" + "time" + + "gitea.dwysokinski.me/twhelp/dcbot/internal/bundb" + "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMonitor_Create(t *testing.T) { + t.Parallel() + + db := newDB(t) + fixture := loadFixtures(t, db) + repo := bundb.NewMonitor(db) + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + group := getGroupFromFixture(t, fixture, "group-1-server-1") + + params, err := domain.NewCreateMonitorParams(group.ID.String(), 1234) + require.Zero(t, err) + + monitor, err := repo.Create(context.Background(), params) + assert.NoError(t, err) + _, err = uuid.Parse(monitor.ID) + assert.NoError(t, err) + assert.Equal(t, params.GroupID(), monitor.GroupID) + assert.Equal(t, params.TribeID(), monitor.TribeID) + assert.WithinDuration(t, time.Now(), monitor.CreatedAt, 1*time.Second) + + // check unique index + monitor, err = repo.Create(context.Background(), params) + assert.ErrorIs(t, err, domain.MonitorAlreadyExistsError{ + TribeID: params.TribeID(), + GroupID: params.GroupID(), + }) + assert.Zero(t, monitor) + }) + + t.Run("ERR: group doesn't exist", func(t *testing.T) { + t.Parallel() + + t.Run("foreign key violation", func(t *testing.T) { + t.Parallel() + + groupID := uuid.NewString() + params, err := domain.NewCreateMonitorParams(groupID, 1234) + require.Zero(t, err) + + monitor, err := repo.Create(context.Background(), params) + assert.ErrorIs(t, err, domain.GroupDoesNotExistError{ + ID: groupID, + }) + assert.Zero(t, monitor) + }) + + t.Run("not UUID", func(t *testing.T) { + t.Parallel() + + groupID := "592292203234328587" + params, err := domain.NewCreateMonitorParams(groupID, 1234) + require.Zero(t, err) + + monitor, err := repo.Create(context.Background(), params) + assert.ErrorIs(t, err, domain.GroupDoesNotExistError{ + ID: groupID, + }) + assert.Zero(t, monitor) + }) + }) +} + +func TestMonitor_List(t *testing.T) { + t.Parallel() + + db := newDB(t) + fixture := loadFixtures(t, db) + repo := bundb.NewMonitor(db) + + tests := []struct { + name string + groupID string + expectedMonitors []string + }{ + { + name: "group-1-server-1", + groupID: getGroupFromFixture(t, fixture, "group-1-server-1").ID.String(), + expectedMonitors: []string{ + "e3017ba8-4fba-4bb1-ac8d-e4e9477a04d2", + "89719460-58ab-46b8-8682-46f161546949", + }, + }, + { + name: "group-2-server-2", + groupID: getGroupFromFixture(t, fixture, "group-2-server-2").ID.String(), + expectedMonitors: []string{ + "c7d63c3d-55f6-432e-b9d8-006f8c5ab407", + }, + }, + { + name: "random UUID", + groupID: uuid.NewString(), + expectedMonitors: nil, + }, + { + name: "not UUID", + groupID: "test", + expectedMonitors: nil, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + res, err := repo.List(context.Background(), tt.groupID) + assert.NoError(t, err) + assert.Len(t, res, len(tt.expectedMonitors)) + for _, id := range tt.expectedMonitors { + found := false + for _, m := range res { + if m.ID == id { + found = true + break + } + } + assert.True(t, found, "monitor (id=%s) not found", id) + } + }) + } +} diff --git a/internal/bundb/testdata/fixture.yml b/internal/bundb/testdata/fixture.yml index db9f7fc..79ced71 100644 --- a/internal/bundb/testdata/fixture.yml +++ b/internal/bundb/testdata/fixture.yml @@ -1,26 +1,43 @@ - model: Group rows: - - _id: group-server-1-1 + - _id: group-1-server-1 id: d56ad37f-2637-48ea-98f8-79627f3fcc96 server_id: server-1 server_key: pl181 version_code: pl created_at: 2022-03-15T15:00:10.000Z - - _id: group-server-1-2 + - _id: group-2-server-1 id: 429b790e-7186-4106-b531-4cc4931ce2ba server_id: server-1 server_key: pl181 version_code: pl created_at: 2022-03-15T15:03:10.000Z - - _id: group-server-2-1 + - _id: group-1-server-2 id: abeb6c8e-70b6-445c-989f-890cd2a1f87a server_id: server-2 server_key: pl181 version_code: pl created_at: 2022-03-18T15:03:10.000Z - - _id: group-server-2-2 + - _id: group-2-server-2 id: 0be82203-4ca3-4b4c-a0c8-3a70099d88f7 server_id: server-2 server_key: pl181 version_code: pl - created_at: 2022-03-19T15:03:10.000Z \ No newline at end of file + created_at: 2022-03-19T15:03:10.000Z +- model: Monitor + rows: + - _id: monitor-1-group-1-server-1 + id: e3017ba8-4fba-4bb1-ac8d-e4e9477a04d2 + group_id: d56ad37f-2637-48ea-98f8-79627f3fcc96 + tribe_id: 123 + created_at: 2022-03-15T15:01:10.000Z + - _id: monitor-2-group-1-server-1 + id: 89719460-58ab-46b8-8682-46f161546949 + group_id: d56ad37f-2637-48ea-98f8-79627f3fcc96 + tribe_id: 15 + created_at: 2022-03-15T15:02:10.000Z + - _id: monitor-1-group-2-server-2 + id: c7d63c3d-55f6-432e-b9d8-006f8c5ab407 + group_id: 0be82203-4ca3-4b4c-a0c8-3a70099d88f7 + tribe_id: 121 + created_at: 2022-03-19T15:10:10.000Z \ No newline at end of file diff --git a/internal/discord/bot.go b/internal/discord/bot.go index 64c009b..39573e9 100644 --- a/internal/discord/bot.go +++ b/internal/discord/bot.go @@ -17,36 +17,52 @@ type GroupService interface { Delete(ctx context.Context, id, serverID string) error } +type MonitorService interface { + Create(ctx context.Context, groupID, serverID, tribeTag string) (domain.Monitor, error) +} + type TWHelpClient interface { ListVersions(ctx context.Context) ([]twhelp.Version, error) } type Bot struct { - s *discordgo.Session - groupSvc GroupService - client TWHelpClient + s *discordgo.Session + groupSvc GroupService + monitorSvc MonitorService + client TWHelpClient } -func NewBot(token string, groupSvc GroupService, client TWHelpClient) (*Bot, error) { +func NewBot(token string, groupSvc GroupService, monitorSvc MonitorService, client TWHelpClient) (*Bot, error) { s, err := discordgo.New("Bot " + token) if err != nil { 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, client: client} + + b := &Bot{s: s, groupSvc: groupSvc, monitorSvc: monitorSvc, client: client} if err = b.registerCommands(); err != nil { _ = s.Close() return nil, fmt.Errorf("couldn't register commands: %w", err) } + return b, nil } func (b *Bot) registerCommands() error { - if err := b.registerCommand(&groupCommand{svc: b.groupSvc, client: b.client}); err != nil { - return err + commands := []command{ + &groupCommand{svc: b.groupSvc, client: b.client}, + &monitorCommand{svc: b.monitorSvc}, } + + for _, c := range commands { + if err := b.registerCommand(c); err != nil { + return err + } + } + return nil } diff --git a/internal/discord/command_group.go b/internal/discord/command_group.go index 8be129f..b7e820b 100644 --- a/internal/discord/command_group.go +++ b/internal/discord/command_group.go @@ -35,9 +35,10 @@ func (c *groupCommand) create(s *discordgo.Session) error { } var perm int64 = discordgo.PermissionAdministrator + _, err = s.ApplicationCommandCreate(s.State.User.ID, "", &discordgo.ApplicationCommand{ Name: c.name(), - Description: "Manages monitor groups on this server", + Description: "Manages groups on this server", DefaultMemberPermissions: &perm, Options: []*discordgo.ApplicationCommandOption{ { @@ -185,6 +186,7 @@ func (c *groupCommand) create(s *discordgo.Session) error { if err != nil { return fmt.Errorf("s.ApplicationCommandCreate: %w", err) } + return nil } diff --git a/internal/discord/command_monitor.go b/internal/discord/command_monitor.go new file mode 100644 index 0000000..7df2ca8 --- /dev/null +++ b/internal/discord/command_monitor.go @@ -0,0 +1,118 @@ +package discord + +import ( + "context" + "fmt" + + "github.com/bwmarrin/discordgo" +) + +const ( + tribeTagMaxLength = 10 +) + +type monitorCommand struct { + svc MonitorService +} + +func (c *monitorCommand) name() string { + return "monitor" +} + +func (c *monitorCommand) register(s *discordgo.Session) error { + if err := c.create(s); err != nil { + return err + } + + s.AddHandler(c.handle) + + return nil +} + +func (c *monitorCommand) create(s *discordgo.Session) error { + var perm int64 = discordgo.PermissionAdministrator + + _, err := s.ApplicationCommandCreate(s.State.User.ID, "", &discordgo.ApplicationCommand{ + Name: c.name(), + Description: "Manages monitors", + DefaultMemberPermissions: &perm, + Options: []*discordgo.ApplicationCommandOption{ + { + Name: "create", + Description: "Creates a new monitor", + Type: discordgo.ApplicationCommandOptionSubCommand, + Options: []*discordgo.ApplicationCommandOption{ + { + Name: "group", + Description: "Group ID", + Type: discordgo.ApplicationCommandOptionString, + Required: true, + }, + { + Name: "tag", + Description: "Tribe tag", + Type: discordgo.ApplicationCommandOptionString, + Required: true, + MaxLength: tribeTagMaxLength, + }, + }, + }, + }, + }) + if err != nil { + return fmt.Errorf("s.ApplicationCommandCreate: %w", err) + } + + return nil +} + +func (c *monitorCommand) handle(s *discordgo.Session, i *discordgo.InteractionCreate) { + cmdData := i.ApplicationCommandData() + + if cmdData.Name != c.name() { + return + } + + switch cmdData.Options[0].Name { + case "create": + c.handleCreate(s, i) + return + default: + } +} + +func (c *monitorCommand) handleCreate(s *discordgo.Session, i *discordgo.InteractionCreate) { + ctx := context.Background() + + group := "" + tag := "" + for _, opt := range i.ApplicationCommandData().Options[0].Options { + if opt == nil { + continue + } + switch opt.Name { + case "group": + group = opt.StringValue() + case "tag": + tag = opt.StringValue() + } + } + + monitor, err := c.svc.Create(ctx, group, i.GuildID, tag) + if err != nil { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: messageFromError(err), + }, + }) + return + } + + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "monitor has been successfully created (ID=" + monitor.ID + ")", + }, + }) +} diff --git a/internal/domain/error.go b/internal/domain/error.go index 50afa16..c5f7335 100644 --- a/internal/domain/error.go +++ b/internal/domain/error.go @@ -1,9 +1,13 @@ package domain -import "errors" +import ( + "errors" + "fmt" +) var ( ErrNothingToUpdate = errors.New("nothing to update") + ErrRequired = errors.New("cannot be blank") ) type ErrorCode uint8 @@ -12,6 +16,7 @@ const ( ErrorCodeUnknown ErrorCode = iota ErrorCodeEntityNotFound ErrorCodeValidationError + ErrorCodeAlreadyExists ) type UserError interface { @@ -20,18 +25,39 @@ type UserError interface { Code() ErrorCode } -type RequiredError struct { +type ValidationError struct { Field string + Err error } -func (e RequiredError) Error() string { - return e.Field + ": cannot be blank" +func (e ValidationError) Error() string { + return fmt.Sprintf("%s: %s", e.Field, e.Err) } -func (e RequiredError) UserError() string { +func (e ValidationError) UserError() string { return e.Error() } -func (e RequiredError) Code() ErrorCode { +func (e ValidationError) Code() ErrorCode { + return ErrorCodeValidationError +} + +func (e ValidationError) Unwrap() error { + return e.Err +} + +type MinError struct { + Min int +} + +func (e MinError) Error() string { + return fmt.Sprintf("must be no less than %d", e.Min) +} + +func (e MinError) UserError() string { + return e.Error() +} + +func (e MinError) Code() ErrorCode { return ErrorCodeValidationError } diff --git a/internal/domain/error_test.go b/internal/domain/error_test.go index 183ac98..7bcb5e7 100644 --- a/internal/domain/error_test.go +++ b/internal/domain/error_test.go @@ -1,20 +1,37 @@ package domain_test import ( + "fmt" "testing" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" "github.com/stretchr/testify/assert" ) -func TestRequiredError(t *testing.T) { +func TestValidationError(t *testing.T) { t.Parallel() - err := domain.RequiredError{ - Field: "123", + err := domain.ValidationError{ + Field: "test", + Err: domain.MinError{ + Min: 25, + }, } var _ domain.UserError = err - assert.Equal(t, err.Field+": cannot be blank", err.Error()) + assert.Equal(t, fmt.Sprintf("%s: %s", err.Field, err.Err.Error()), err.Error()) + assert.Equal(t, err.Error(), err.UserError()) + assert.ErrorIs(t, err, err.Err) + assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) +} + +func TestMinError(t *testing.T) { + t.Parallel() + + err := domain.MinError{ + Min: 25, + } + var _ domain.UserError = err + assert.Equal(t, "must be no less than 25", err.Error()) assert.Equal(t, err.Error(), err.UserError()) assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) } diff --git a/internal/domain/group.go b/internal/domain/group.go index 9422d1a..390453d 100644 --- a/internal/domain/group.go +++ b/internal/domain/group.go @@ -25,20 +25,23 @@ type CreateGroupParams struct { func NewCreateGroupParams(serverID, versionCode, serverKey, channelGains, channelLosses string) (CreateGroupParams, error) { if serverID == "" { - return CreateGroupParams{}, RequiredError{ + return CreateGroupParams{}, ValidationError{ Field: "ServerID", + Err: ErrRequired, } } if versionCode == "" { - return CreateGroupParams{}, RequiredError{ + return CreateGroupParams{}, ValidationError{ Field: "VersionCode", + Err: ErrRequired, } } if serverKey == "" { - return CreateGroupParams{}, RequiredError{ + return CreateGroupParams{}, ValidationError{ Field: "ServerKey", + Err: ErrRequired, } } @@ -117,3 +120,19 @@ func (e GroupNotFoundError) UserError() string { func (e GroupNotFoundError) Code() ErrorCode { return ErrorCodeEntityNotFound } + +type GroupDoesNotExistError struct { + ID string +} + +func (e GroupDoesNotExistError) Error() string { + return fmt.Sprintf("group (ID=%s) doesn't exist", e.ID) +} + +func (e GroupDoesNotExistError) UserError() string { + return e.Error() +} + +func (e GroupDoesNotExistError) Code() ErrorCode { + return ErrorCodeValidationError +} diff --git a/internal/domain/group_test.go b/internal/domain/group_test.go index 99884aa..c1b2c4c 100644 --- a/internal/domain/group_test.go +++ b/internal/domain/group_test.go @@ -34,20 +34,29 @@ func TestNewCreateGroupParams(t *testing.T) { name: "ERR: ServerID cannot be blank", versionCode: "", serverID: "", - err: domain.RequiredError{Field: "ServerID"}, + err: domain.ValidationError{ + Field: "ServerID", + Err: domain.ErrRequired, + }, }, { name: "ERR: VersionCode cannot be blank", serverID: "1234", versionCode: "", - err: domain.RequiredError{Field: "VersionCode"}, + err: domain.ValidationError{ + Field: "VersionCode", + Err: domain.ErrRequired, + }, }, { name: "ERR: ServerKey cannot be blank", serverID: "1234", versionCode: "en", serverKey: "", - err: domain.RequiredError{Field: "ServerKey"}, + err: domain.ValidationError{ + Field: "ServerKey", + Err: domain.ErrRequired, + }, }, } @@ -163,3 +172,15 @@ func TestGroupNotFoundError(t *testing.T) { assert.Equal(t, err.Error(), err.UserError()) assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code()) } + +func TestGroupDoesNotExistError(t *testing.T) { + t.Parallel() + + err := domain.GroupDoesNotExistError{ + ID: uuid.NewString(), + } + var _ domain.UserError = err + assert.Equal(t, fmt.Sprintf("group (ID=%s) doesn't exist", err.ID), err.Error()) + assert.Equal(t, err.Error(), err.UserError()) + assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) +} diff --git a/internal/domain/monitor.go b/internal/domain/monitor.go new file mode 100644 index 0000000..c1c6055 --- /dev/null +++ b/internal/domain/monitor.go @@ -0,0 +1,84 @@ +package domain + +import ( + "fmt" + "time" +) + +const ( + tribeIDMin = 1 +) + +type Monitor struct { + ID string + TribeID int64 + GroupID string + CreatedAt time.Time +} + +type CreateMonitorParams struct { + tribeID int64 + groupID string +} + +func NewCreateMonitorParams(groupID string, tribeID int64) (CreateMonitorParams, error) { + if groupID == "" { + return CreateMonitorParams{}, ValidationError{ + Field: "GroupID", + Err: ErrRequired, + } + } + + if tribeID < tribeIDMin { + return CreateMonitorParams{}, ValidationError{ + Field: "TribeID", + Err: MinError{ + Min: tribeIDMin, + }, + } + } + + return CreateMonitorParams{tribeID: tribeID, groupID: groupID}, nil +} + +func (c CreateMonitorParams) TribeID() int64 { + return c.tribeID +} + +func (c CreateMonitorParams) GroupID() string { + return c.groupID +} + +type MonitorAlreadyExistsError struct { + TribeID int64 + GroupID string +} + +func (e MonitorAlreadyExistsError) Error() string { + return fmt.Sprintf("monitor (GroupID=%s,TribeID=%d) already exists", e.GroupID, e.TribeID) +} + +func (e MonitorAlreadyExistsError) UserError() string { + return e.Error() +} + +func (e MonitorAlreadyExistsError) Code() ErrorCode { + return ErrorCodeAlreadyExists +} + +type MonitorLimitReachedError struct { + Current int // current number of groups + Limit int // maximum number of groups +} + +func (e MonitorLimitReachedError) Error() string { + return fmt.Sprintf("monitor limit has been reached (%d/%d)", e.Current, e.Limit) +} + +func (e MonitorLimitReachedError) UserError() string { + return e.Error() +} + +func (e MonitorLimitReachedError) Code() ErrorCode { + return ErrorCodeValidationError +} diff --git a/internal/domain/monitor_test.go b/internal/domain/monitor_test.go new file mode 100644 index 0000000..dbe72c3 --- /dev/null +++ b/internal/domain/monitor_test.go @@ -0,0 +1,93 @@ +package domain_test + +import ( + "fmt" + "testing" + + "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestNewCreateMonitorParams(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + tribeID int64 + groupID string + err error + }{ + { + name: "OK", + tribeID: 15, + groupID: uuid.NewString(), + }, + { + name: "ERR: GroupID cannot be blank", + groupID: "", + err: domain.ValidationError{ + Field: "GroupID", + Err: domain.ErrRequired, + }, + }, + { + name: "ERR: TribeID < 1", + groupID: uuid.NewString(), + tribeID: 0, + err: domain.ValidationError{ + Field: "TribeID", + Err: domain.MinError{ + Min: 1, + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + res, err := domain.NewCreateMonitorParams( + tt.groupID, + tt.tribeID, + ) + if tt.err != nil { + assert.ErrorIs(t, err, tt.err) + assert.Zero(t, res) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.groupID, res.GroupID()) + assert.Equal(t, tt.tribeID, res.TribeID()) + }) + } +} + +func TestMonitorAlreadyExistsError(t *testing.T) { + t.Parallel() + + err := domain.MonitorAlreadyExistsError{ + GroupID: uuid.NewString(), + TribeID: 1234, + } + var _ domain.UserError = err + assert.Equal(t, fmt.Sprintf("monitor (GroupID=%s,TribeID=%d) already exists", err.GroupID, err.TribeID), err.Error()) + assert.Equal(t, err.Error(), err.UserError()) + assert.Equal(t, domain.ErrorCodeAlreadyExists, err.Code()) +} + +func TestMonitorLimitReachedError(t *testing.T) { + t.Parallel() + + err := domain.MonitorLimitReachedError{ + Current: 10, + Limit: 10, + } + var _ domain.UserError = err + assert.Equal(t, fmt.Sprintf("monitor limit has been reached (%d/%d)", err.Current, err.Limit), err.Error()) + assert.Equal(t, err.Error(), err.UserError()) + assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) +} diff --git a/internal/domain/tw.go b/internal/domain/tw.go index 4a67a14..609df19 100644 --- a/internal/domain/tw.go +++ b/internal/domain/tw.go @@ -35,3 +35,19 @@ func (e ServerIsClosedError) UserError() string { func (e ServerIsClosedError) Code() ErrorCode { return ErrorCodeValidationError } + +type TribeDoesNotExistError struct { + Tag string +} + +func (e TribeDoesNotExistError) Error() string { + return fmt.Sprintf("tribe (Tag=%s) doesn't exist", e.Tag) +} + +func (e TribeDoesNotExistError) UserError() string { + return e.Error() +} + +func (e TribeDoesNotExistError) Code() ErrorCode { + return ErrorCodeValidationError +} diff --git a/internal/domain/tw_test.go b/internal/domain/tw_test.go index eade526..d95ba92 100644 --- a/internal/domain/tw_test.go +++ b/internal/domain/tw_test.go @@ -33,3 +33,15 @@ func TestServerIsClosedError(t *testing.T) { assert.Equal(t, err.Error(), err.UserError()) assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) } + +func TestTribeDoesNotExistError(t *testing.T) { + t.Parallel() + + err := domain.TribeDoesNotExistError{ + Tag: "*TAG*", + } + var _ domain.UserError = err + assert.Equal(t, fmt.Sprintf("tribe (Tag=%s) doesn't exist", err.Tag), err.Error()) + assert.Equal(t, err.Error(), err.UserError()) + assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) +} diff --git a/internal/service/group.go b/internal/service/group.go index 89625a5..85cabdd 100644 --- a/internal/service/group.go +++ b/internal/service/group.go @@ -18,6 +18,7 @@ type GroupRepository interface { Create(ctx context.Context, params domain.CreateGroupParams) (domain.Group, error) Update(ctx context.Context, id, serverID string, params domain.UpdateGroupParams) (domain.Group, error) List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) + Get(ctx context.Context, id, serverID string) (domain.Group, error) Delete(ctx context.Context, id, serverID string) error } @@ -91,6 +92,14 @@ func (g *Group) List(ctx context.Context, params domain.ListGroupsParams) ([]dom return groups, nil } +func (g *Group) Get(ctx context.Context, id, serverID string) (domain.Group, error) { + group, err := g.repo.Get(ctx, id, serverID) + if err != nil { + return domain.Group{}, fmt.Errorf("GroupRepository.Get: %w", err) + } + return group, nil +} + func (g *Group) Delete(ctx context.Context, id, serverID string) error { if err := g.repo.Delete(ctx, id, serverID); err != nil { return fmt.Errorf("GroupRepository.Delete: %w", err) diff --git a/internal/service/monitor.go b/internal/service/monitor.go new file mode 100644 index 0000000..0b93813 --- /dev/null +++ b/internal/service/monitor.go @@ -0,0 +1,89 @@ +package service + +import ( + "context" + "errors" + "fmt" + + "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" + "gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp" +) + +const ( + maxMonitorsPerGroup = 10 +) + +//counterfeiter:generate -o internal/mock/monitor_repository.gen.go . MonitorRepository +type MonitorRepository interface { + Create(ctx context.Context, params domain.CreateMonitorParams) (domain.Monitor, error) + List(ctx context.Context, groupID string) ([]domain.Monitor, error) +} + +//counterfeiter:generate -o internal/mock/group_getter.gen.go . GroupGetter +type GroupGetter interface { + Get(ctx context.Context, id, serverID string) (domain.Group, error) +} + +type Monitor struct { + repo MonitorRepository + client TWHelpClient + groupSvc GroupGetter +} + +func NewMonitor(repo MonitorRepository, groupSvc GroupGetter, client TWHelpClient) *Monitor { + return &Monitor{repo: repo, client: client, groupSvc: groupSvc} +} + +func (m *Monitor) Create(ctx context.Context, groupID, serverID, tribeTag string) (domain.Monitor, error) { + // check if group exists + group, err := m.groupSvc.Get(ctx, groupID, serverID) + if err != nil { + if errors.Is(err, domain.GroupNotFoundError{ID: groupID}) { + return domain.Monitor{}, domain.GroupDoesNotExistError{ID: groupID} + } + return domain.Monitor{}, fmt.Errorf("GroupService.Get: %w", err) + } + + // check limit + monitors, err := m.repo.List(ctx, group.ID) + if err != nil { + return domain.Monitor{}, fmt.Errorf("MonitorRepository.List: %w", err) + } + + if len(monitors) >= maxMonitorsPerGroup { + return domain.Monitor{}, domain.MonitorLimitReachedError{ + Current: len(monitors), + Limit: maxMonitorsPerGroup, + } + } + + // check if tribe exists + tribe, err := m.client.GetTribeByTag(ctx, group.VersionCode, group.ServerKey, tribeTag) + if err != nil { + var apiErr twhelp.APIError + if !errors.As(err, &apiErr) { + return domain.Monitor{}, fmt.Errorf("TWHelpClient.GetServer: %w", err) + } + return domain.Monitor{}, domain.TribeDoesNotExistError{ + Tag: tribeTag, + } + } + + if !tribe.DeletedAt.IsZero() { + return domain.Monitor{}, domain.TribeDoesNotExistError{ + Tag: tribeTag, + } + } + + params, err := domain.NewCreateMonitorParams(group.ID, tribe.ID) + if err != nil { + return domain.Monitor{}, fmt.Errorf("domain.NewCreateMonitorParams: %w", err) + } + + monitor, err := m.repo.Create(ctx, params) + if err != nil { + return domain.Monitor{}, fmt.Errorf("MonitorRepository.Create: %w", err) + } + + return monitor, nil +} diff --git a/internal/service/monitor_test.go b/internal/service/monitor_test.go new file mode 100644 index 0000000..7a18247 --- /dev/null +++ b/internal/service/monitor_test.go @@ -0,0 +1,195 @@ +package service_test + +import ( + "context" + "testing" + "time" + + "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" + "gitea.dwysokinski.me/twhelp/dcbot/internal/service" + "gitea.dwysokinski.me/twhelp/dcbot/internal/service/internal/mock" + "gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestMonitor_Create(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + repo := &mock.FakeMonitorRepository{} + repo.ListReturns(nil, nil) + repo.CreateCalls(func(ctx context.Context, params domain.CreateMonitorParams) (domain.Monitor, error) { + return domain.Monitor{ + ID: uuid.NewString(), + TribeID: params.TribeID(), + GroupID: params.GroupID(), + CreatedAt: time.Now(), + }, nil + }) + + groupSvc := &mock.FakeGroupGetter{} + groupSvc.GetCalls(func(ctx context.Context, groupID, serverID string) (domain.Group, error) { + return domain.Group{ + ID: groupID, + ServerID: serverID, + ChannelGains: "", + ChannelLosses: "", + ServerKey: "pl151", + VersionCode: "pl", + CreatedAt: time.Now(), + }, nil + }) + + client := &mock.FakeTWHelpClient{} + tribe := twhelp.Tribe{ + ID: 150, + Tag: "*TAG*", + Name: uuid.NewString(), + ProfileURL: "https://pl151.plemiona.pl/game.php?screen=info_player&id=150", + DeletedAt: time.Time{}, + } + client.GetTribeByTagReturns(tribe, nil) + + groupID := uuid.NewString() + + monitor, err := service.NewMonitor(repo, groupSvc, client). + Create(context.Background(), groupID, uuid.NewString(), tribe.Tag) + assert.NoError(t, err) + assert.NotEmpty(t, monitor.ID) + assert.Equal(t, groupID, monitor.GroupID) + assert.Equal(t, tribe.ID, monitor.TribeID) + assert.NotEmpty(t, monitor.CreatedAt) + }) + + t.Run("ERR: group doesn't exist", func(t *testing.T) { + t.Parallel() + + groupID := uuid.NewString() + groupSvc := &mock.FakeGroupGetter{} + groupSvc.GetReturns(domain.Group{}, domain.GroupNotFoundError{ID: groupID}) + + monitor, err := service.NewMonitor(nil, groupSvc, nil). + Create(context.Background(), groupID, uuid.NewString(), "tag") + assert.ErrorIs(t, err, domain.GroupDoesNotExistError{ + ID: groupID, + }) + assert.Zero(t, monitor) + }) + + t.Run("ERR: monitor limit has been reached", func(t *testing.T) { + t.Parallel() + + const maxMonitorsPerGroup = 10 + + repo := &mock.FakeMonitorRepository{} + repo.ListReturns(make([]domain.Monitor, maxMonitorsPerGroup), nil) + + groupSvc := &mock.FakeGroupGetter{} + groupSvc.GetCalls(func(ctx context.Context, groupID, serverID string) (domain.Group, error) { + return domain.Group{ + ID: groupID, + ServerID: serverID, + ChannelGains: "", + ChannelLosses: "", + ServerKey: "pl151", + VersionCode: "pl", + CreatedAt: time.Now(), + }, nil + }) + + monitor, err := service.NewMonitor(repo, groupSvc, nil). + Create(context.Background(), uuid.NewString(), uuid.NewString(), "TAG") + assert.ErrorIs(t, err, domain.MonitorLimitReachedError{ + Current: maxMonitorsPerGroup, + Limit: maxMonitorsPerGroup, + }) + assert.Zero(t, monitor) + }) + + t.Run("ERR: tribe doesn't exist", func(t *testing.T) { + t.Parallel() + + t.Run("API error", func(t *testing.T) { + t.Parallel() + + repo := &mock.FakeMonitorRepository{} + repo.ListReturns(nil, nil) + repo.CreateCalls(func(ctx context.Context, params domain.CreateMonitorParams) (domain.Monitor, error) { + return domain.Monitor{ + ID: uuid.NewString(), + TribeID: params.TribeID(), + GroupID: params.GroupID(), + CreatedAt: time.Now(), + }, nil + }) + + groupSvc := &mock.FakeGroupGetter{} + groupSvc.GetCalls(func(ctx context.Context, groupID, serverID string) (domain.Group, error) { + return domain.Group{ + ID: groupID, + ServerID: serverID, + ChannelGains: "", + ChannelLosses: "", + ServerKey: "pl151", + VersionCode: "pl", + CreatedAt: time.Now(), + }, nil + }) + + client := &mock.FakeTWHelpClient{} + client.GetTribeByTagReturns(twhelp.Tribe{}, twhelp.APIError{ + Code: twhelp.ErrorCodeEntityNotFound, + Message: "tribe not found", + }) + + tag := "TAG" + + monitor, err := service.NewMonitor(repo, groupSvc, client). + Create(context.Background(), uuid.NewString(), uuid.NewString(), tag) + assert.ErrorIs(t, err, domain.TribeDoesNotExistError{ + Tag: tag, + }) + assert.Zero(t, monitor) + }) + + t.Run("tribe is deleted", func(t *testing.T) { + t.Parallel() + + repo := &mock.FakeMonitorRepository{} + repo.ListReturns(nil, nil) + + groupSvc := &mock.FakeGroupGetter{} + groupSvc.GetCalls(func(ctx context.Context, groupID, serverID string) (domain.Group, error) { + return domain.Group{ + ID: groupID, + ServerID: serverID, + ChannelGains: "", + ChannelLosses: "", + ServerKey: "pl151", + VersionCode: "pl", + CreatedAt: time.Now(), + }, nil + }) + + client := &mock.FakeTWHelpClient{} + tribe := twhelp.Tribe{ + ID: 150, + Tag: "*TAG*", + Name: uuid.NewString(), + ProfileURL: "https://pl151.plemiona.pl/game.php?screen=info_player&id=150", + DeletedAt: time.Now(), + } + client.GetTribeByTagReturns(tribe, nil) + + monitor, err := service.NewMonitor(repo, groupSvc, client). + Create(context.Background(), uuid.NewString(), uuid.NewString(), tribe.Tag) + assert.ErrorIs(t, err, domain.TribeDoesNotExistError{ + Tag: tribe.Tag, + }) + assert.Zero(t, monitor) + }) + }) +} diff --git a/internal/service/service.go b/internal/service/service.go index 3555d28..8abd446 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -11,4 +11,5 @@ import ( //counterfeiter:generate -o internal/mock/twhelp_client.gen.go . TWHelpClient type TWHelpClient interface { GetServer(ctx context.Context, version, server string) (twhelp.Server, error) + GetTribeByTag(ctx context.Context, version, server, tag string) (twhelp.Tribe, error) } diff --git a/internal/twhelp/client.go b/internal/twhelp/client.go index 4d54b79..04b5b43 100644 --- a/internal/twhelp/client.go +++ b/internal/twhelp/client.go @@ -14,8 +14,9 @@ const ( defaultUserAgent = "TWHelpDCBot/development" defaultTimeout = 10 * time.Second - endpointListVersions = "/api/v1/versions" - endpointGetServer = "/api/v1/versions/%s/servers/%s" + endpointListVersions = "/api/v1/versions" + endpointGetServer = "/api/v1/versions/%s/servers/%s" + endpointGetTribeByTag = "/api/v1/versions/%s/servers/%s/tribes/tag/%s" ) type Client struct { @@ -68,6 +69,14 @@ func (c *Client) GetServer(ctx context.Context, version, server string) (Server, return resp.Data, nil } +func (c *Client) GetTribeByTag(ctx context.Context, version, server, tag string) (Tribe, error) { + var resp getTribeResp + if err := c.getJSON(ctx, fmt.Sprintf(endpointGetTribeByTag, version, server, tag), &resp); err != nil { + return Tribe{}, 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 { diff --git a/internal/twhelp/models.go b/internal/twhelp/types.go similarity index 78% rename from internal/twhelp/models.go rename to internal/twhelp/types.go index 6505e19..f9b2615 100644 --- a/internal/twhelp/models.go +++ b/internal/twhelp/types.go @@ -1,5 +1,7 @@ package twhelp +import "time" + type ErrorCode string const ( @@ -47,3 +49,15 @@ type Server struct { 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"` +}