From ef682800e2519cecd0fd803971b878092727c119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Mon, 10 Oct 2022 07:12:24 +0200 Subject: [PATCH] feat: add a new command - group set-server --- internal/bundb/group.go | 52 +++++++++++++++ internal/bundb/group_test.go | 75 ++++++++++++++++++++++ internal/discord/bot.go | 1 + internal/discord/command_group.go | 68 ++++++++++++++++++++ internal/domain/domain.go | 6 ++ internal/domain/error.go | 6 ++ internal/domain/group.go | 30 +++++++++ internal/domain/group_test.go | 101 ++++++++++++++++++++++++++++++ internal/service/group.go | 46 ++++++++++++++ internal/service/group_test.go | 80 +++++++++++++++++++++++ 10 files changed, 465 insertions(+) create mode 100644 internal/domain/domain.go diff --git a/internal/bundb/group.go b/internal/bundb/group.go index efa0a88..b7f7d0b 100644 --- a/internal/bundb/group.go +++ b/internal/bundb/group.go @@ -8,6 +8,7 @@ import ( "gitea.dwysokinski.me/twhelp/dcbot/internal/bundb/internal/model" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" + "github.com/google/uuid" "github.com/uptrace/bun" ) @@ -38,6 +39,33 @@ func (g *Group) Create(ctx context.Context, params domain.CreateGroupParams) (do return group.ToDomain(), nil } +func (g *Group) UpdateByID(ctx context.Context, id string, params domain.UpdateGroupParams) (domain.Group, error) { + if params.IsZero() { + return domain.Group{}, domain.ErrNothingToUpdate + } + + if _, err := uuid.Parse(id); err != nil { + return domain.Group{}, domain.GroupNotFoundError{ID: id} + } + + var group model.Group + + res, err := g.db.NewUpdate(). + Model(&group). + Returning("*"). + Where("id = ?", id). + Apply(updateGroupsParamsApplier{params}.apply). + Exec(ctx) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return domain.Group{}, fmt.Errorf("couldn't update group (id=%s): %w", id, err) + } + if affected, _ := res.RowsAffected(); affected == 0 { + return domain.Group{}, domain.GroupNotFoundError{ID: id} + } + + return group.ToDomain(), nil +} + func (g *Group) List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) { var groups []model.Group @@ -56,6 +84,30 @@ func (g *Group) List(ctx context.Context, params domain.ListGroupsParams) ([]dom return result, nil } +type updateGroupsParamsApplier struct { + params domain.UpdateGroupParams +} + +func (u updateGroupsParamsApplier) apply(q *bun.UpdateQuery) *bun.UpdateQuery { + if u.params.ChannelGainedVillages.Valid { + q = q.Set("channel_gained_villages = ?", u.params.ChannelGainedVillages.String) + } + + if u.params.ChannelLostVillages.Valid { + q = q.Set("channel_lost_villages = ?", u.params.ChannelLostVillages.String) + } + + if u.params.ServerKey.Valid { + q = q.Set("server_key = ?", u.params.ServerKey.String) + } + + if u.params.VersionCode.Valid { + q = q.Set("version_code = ?", u.params.VersionCode.String) + } + + return q +} + type listGroupsParamsApplier struct { params domain.ListGroupsParams } diff --git a/internal/bundb/group_test.go b/internal/bundb/group_test.go index dd16c0d..a4cfaef 100644 --- a/internal/bundb/group_test.go +++ b/internal/bundb/group_test.go @@ -44,6 +44,81 @@ func TestGroup_Create(t *testing.T) { }) } +func TestGroup_UpdateByID(t *testing.T) { + t.Parallel() + + db := newDB(t) + fixture := loadFixtures(t, db) + repo := bundb.NewGroup(db) + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + group := getGroupFromFixture(t, fixture, "group-server-1-1") + params := domain.UpdateGroupParams{ + ChannelGainedVillages: domain.NullString{ + String: group.ChannelGainedVillages + "update", + Valid: true, + }, + ChannelLostVillages: domain.NullString{ + String: group.ChannelLostVillages + "update", + Valid: true, + }, + ServerKey: domain.NullString{ + String: group.ServerKey + "update", + Valid: true, + }, + VersionCode: domain.NullString{ + String: "update", + Valid: true, + }, + } + + updatedGroup, err := repo.UpdateByID(context.Background(), group.ID.String(), params) + assert.NoError(t, err) + assert.Equal(t, params.ChannelGainedVillages.String, updatedGroup.ChannelGainedVillages) + assert.Equal(t, params.ChannelLostVillages.String, updatedGroup.ChannelLostVillages) + assert.Equal(t, params.ServerKey.String, updatedGroup.ServerKey) + assert.Equal(t, params.VersionCode.String, updatedGroup.VersionCode) + }) + + t.Run("ERR: nothing to update", func(t *testing.T) { + t.Parallel() + + updatedGroup, err := repo.UpdateByID(context.Background(), "", domain.UpdateGroupParams{}) + assert.ErrorIs(t, err, domain.ErrNothingToUpdate) + assert.Zero(t, updatedGroup) + }) + + t.Run("ERR: invalid UUID", func(t *testing.T) { + t.Parallel() + + id := "12345" + updatedGroup, err := repo.UpdateByID(context.Background(), id, domain.UpdateGroupParams{ + ChannelGainedVillages: domain.NullString{ + String: "update", + Valid: true, + }, + }) + assert.ErrorIs(t, err, domain.GroupNotFoundError{ID: id}) + assert.Zero(t, updatedGroup) + }) + + t.Run("ERR: group not found", func(t *testing.T) { + t.Parallel() + + id := uuid.NewString() + updatedGroup, err := repo.UpdateByID(context.Background(), id, domain.UpdateGroupParams{ + ChannelGainedVillages: domain.NullString{ + String: "update", + Valid: true, + }, + }) + assert.ErrorIs(t, err, domain.GroupNotFoundError{ID: id}) + assert.Zero(t, updatedGroup) + }) +} + func TestGroup_List(t *testing.T) { t.Parallel() diff --git a/internal/discord/bot.go b/internal/discord/bot.go index 6b5d86b..92d131f 100644 --- a/internal/discord/bot.go +++ b/internal/discord/bot.go @@ -11,6 +11,7 @@ import ( type GroupService interface { Create(ctx context.Context, params domain.CreateGroupParams) (domain.Group, error) + SetTWServer(ctx context.Context, id, versionCode, serverKey string) (domain.Group, error) List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) } diff --git a/internal/discord/command_group.go b/internal/discord/command_group.go index a63fa38..85e4363 100644 --- a/internal/discord/command_group.go +++ b/internal/discord/command_group.go @@ -83,6 +83,32 @@ func (c *groupCommand) create(s *discordgo.Session) error { Description: "Lists all created groups", Type: discordgo.ApplicationCommandOptionSubCommand, }, + { + Name: "set-server", + Description: "Sets a Tribal Wars server (e.g. en115, pl170) for a group", + Type: discordgo.ApplicationCommandOptionSubCommand, + Options: []*discordgo.ApplicationCommandOption{ + { + Name: "group", + Description: "Group ID", + Type: discordgo.ApplicationCommandOptionString, + Required: true, + }, + { + Name: "version", + Description: "e.g. www.tribalwars.net, www.plemiona.pl", + Type: discordgo.ApplicationCommandOptionString, + Choices: versionChoices, + Required: true, + }, + { + Name: "server", + Description: "Tribal Wars server (e.g. en115, pl170)", + Type: discordgo.ApplicationCommandOptionString, + Required: true, + }, + }, + }, }, }) if err != nil { @@ -121,6 +147,9 @@ func (c *groupCommand) handle(s *discordgo.Session, i *discordgo.InteractionCrea case "list": c.handleList(s, i) return + case "set-server": + c.handleSetServer(s, i) + return default: } } @@ -215,6 +244,45 @@ func (c *groupCommand) handleList(s *discordgo.Session, i *discordgo.Interaction }) } +func (c *groupCommand) handleSetServer(s *discordgo.Session, i *discordgo.InteractionCreate) { + ctx := context.Background() + + group := "" + version := "" + server := "" + for _, opt := range i.ApplicationCommandData().Options[0].Options { + if opt == nil { + continue + } + switch opt.Name { + case "group": + group = opt.StringValue() + case "version": + version = opt.StringValue() + case "server": + server = opt.StringValue() + } + } + + _, err := c.svc.SetTWServer(ctx, group, version, server) + 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: "group has been successfully updated", + }, + }) +} + func buildGroupListDescription(groups []domain.Group) string { description := "**ID** - **Version** - **Server**" for i, g := range groups { diff --git a/internal/domain/domain.go b/internal/domain/domain.go new file mode 100644 index 0000000..92264a1 --- /dev/null +++ b/internal/domain/domain.go @@ -0,0 +1,6 @@ +package domain + +type NullString struct { + String string + Valid bool // Valid is true if String is not NULL +} diff --git a/internal/domain/error.go b/internal/domain/error.go index 6254a36..50afa16 100644 --- a/internal/domain/error.go +++ b/internal/domain/error.go @@ -1,5 +1,11 @@ package domain +import "errors" + +var ( + ErrNothingToUpdate = errors.New("nothing to update") +) + type ErrorCode uint8 const ( diff --git a/internal/domain/group.go b/internal/domain/group.go index 73bca1a..650cac6 100644 --- a/internal/domain/group.go +++ b/internal/domain/group.go @@ -71,6 +71,20 @@ func (c CreateGroupParams) ChannelLostVillages() string { return c.channelLostVillages } +type UpdateGroupParams struct { + ChannelGainedVillages NullString + ChannelLostVillages NullString + ServerKey NullString + VersionCode NullString +} + +func (u UpdateGroupParams) IsZero() bool { + return !u.ChannelGainedVillages.Valid && + !u.ChannelLostVillages.Valid && + !u.VersionCode.Valid && + !u.ServerKey.Valid +} + type ListGroupsParams struct { ServerIDs []string } @@ -91,3 +105,19 @@ func (e GroupLimitReachedError) UserError() string { func (e GroupLimitReachedError) Code() ErrorCode { return ErrorCodeValidationError } + +type GroupNotFoundError struct { + ID string +} + +func (e GroupNotFoundError) Error() string { + return fmt.Sprintf("group (ID=%s) not found", e.ID) +} + +func (e GroupNotFoundError) UserError() string { + return e.Error() +} + +func (e GroupNotFoundError) Code() ErrorCode { + return ErrorCodeEntityNotFound +} diff --git a/internal/domain/group_test.go b/internal/domain/group_test.go index 1c15124..7e5891c 100644 --- a/internal/domain/group_test.go +++ b/internal/domain/group_test.go @@ -5,6 +5,7 @@ import ( "testing" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" + "github.com/google/uuid" "github.com/stretchr/testify/assert" ) @@ -78,6 +79,94 @@ func TestNewCreateGroupParams(t *testing.T) { } } +func TestUpdateGroupParams_IsZero(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + params domain.UpdateGroupParams + output bool + }{ + { + name: "OK: all", + params: domain.UpdateGroupParams{ + ChannelGainedVillages: domain.NullString{ + String: "123", + Valid: true, + }, + ChannelLostVillages: domain.NullString{ + String: "123", + Valid: true, + }, + ServerKey: domain.NullString{ + String: "123", + Valid: true, + }, + VersionCode: domain.NullString{ + String: "123", + Valid: true, + }, + }, + output: false, + }, + { + name: "OK: ChannelGainedVillages", + params: domain.UpdateGroupParams{ + ChannelGainedVillages: domain.NullString{ + String: "123", + Valid: true, + }, + }, + output: false, + }, + { + name: "OK: ChannelLostVillages", + params: domain.UpdateGroupParams{ + ChannelLostVillages: domain.NullString{ + String: "123", + Valid: true, + }, + }, + output: false, + }, + { + name: "OK: ServerKey", + params: domain.UpdateGroupParams{ + ServerKey: domain.NullString{ + String: "123", + Valid: true, + }, + }, + output: false, + }, + { + name: "OK: VersionCode", + params: domain.UpdateGroupParams{ + VersionCode: domain.NullString{ + String: "123", + Valid: true, + }, + }, + output: false, + }, + { + name: "OK: empty struct", + params: domain.UpdateGroupParams{}, + output: true, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, tt.output, tt.params.IsZero()) + }) + } +} + func TestGroupLimitReachedError(t *testing.T) { t.Parallel() @@ -90,3 +179,15 @@ func TestGroupLimitReachedError(t *testing.T) { assert.Equal(t, err.Error(), err.UserError()) assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) } + +func TestGroupNotFoundError(t *testing.T) { + t.Parallel() + + err := domain.GroupNotFoundError{ + ID: uuid.NewString(), + } + var _ domain.UserError = err + assert.Equal(t, fmt.Sprintf("group (ID=%s) not found", err.ID), err.Error()) + assert.Equal(t, err.Error(), err.UserError()) + assert.Equal(t, domain.ErrorCodeEntityNotFound, err.Code()) +} diff --git a/internal/service/group.go b/internal/service/group.go index 3aef011..01e7462 100644 --- a/internal/service/group.go +++ b/internal/service/group.go @@ -16,6 +16,7 @@ const ( //counterfeiter:generate -o internal/mock/group_repository.gen.go . GroupRepository type GroupRepository interface { Create(ctx context.Context, params domain.CreateGroupParams) (domain.Group, error) + UpdateByID(ctx context.Context, id string, params domain.UpdateGroupParams) (domain.Group, error) List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) } @@ -70,6 +71,28 @@ func (g *Group) Create(ctx context.Context, params domain.CreateGroupParams) (do return group, nil } +func (g *Group) SetTWServer(ctx context.Context, id, versionCode, serverKey string) (domain.Group, error) { + if err := g.checkTWServer(ctx, versionCode, serverKey); err != nil { + return domain.Group{}, err + } + + group, err := g.repo.UpdateByID(ctx, id, domain.UpdateGroupParams{ + VersionCode: domain.NullString{ + String: versionCode, + Valid: true, + }, + ServerKey: domain.NullString{ + String: serverKey, + Valid: true, + }, + }) + if err != nil { + return domain.Group{}, fmt.Errorf("GroupRepository.UpdateByID: %w", err) + } + + return group, nil +} + func (g *Group) List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) { groups, err := g.repo.List(ctx, params) if err != nil { @@ -77,3 +100,26 @@ func (g *Group) List(ctx context.Context, params domain.ListGroupsParams) ([]dom } return groups, nil } + +func (g *Group) checkTWServer(ctx context.Context, versionCode, serverKey string) error { + server, err := g.client.GetServer(ctx, versionCode, serverKey) + if err != nil { + var apiErr twhelp.APIError + if !errors.As(err, &apiErr) { + return fmt.Errorf("TWHelpClient.GetServer: %w", err) + } + return domain.ServerDoesNotExistError{ + VersionCode: versionCode, + Key: serverKey, + } + } + + if !server.Open { + return domain.ServerIsClosedError{ + VersionCode: versionCode, + Key: serverKey, + } + } + + return nil +} diff --git a/internal/service/group_test.go b/internal/service/group_test.go index 258f0aa..338e3ea 100644 --- a/internal/service/group_test.go +++ b/internal/service/group_test.go @@ -125,3 +125,83 @@ func TestGroup_Create(t *testing.T) { assert.Zero(t, g) }) } + +func TestGroup_SetTWServer(t *testing.T) { + t.Parallel() + + id := uuid.NewString() + versionCode := "pl" + serverKey := "pl181" + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + repo := &mock.FakeGroupRepository{} + repo.UpdateByIDCalls(func(_ context.Context, id string, p domain.UpdateGroupParams) (domain.Group, error) { + return domain.Group{ + ID: id, + ServerID: uuid.NewString(), + ChannelGainedVillages: p.ChannelGainedVillages.String, + ChannelLostVillages: p.ChannelLostVillages.String, + ServerKey: p.ServerKey.String, + VersionCode: p.VersionCode.String, + CreatedAt: time.Now(), + }, nil + }) + repo.ListReturns(nil, nil) + + client := &mock.FakeTWHelpClient{} + client.GetServerCalls(func(_ context.Context, _ string, server string) (twhelp.Server, error) { + return twhelp.Server{ + Key: server, + URL: fmt.Sprintf("https://%s.tribalwars.net", server), + Open: true, + }, nil + }) + + g, err := service.NewGroup(repo, client).SetTWServer(context.Background(), id, versionCode, serverKey) + assert.NoError(t, err) + assert.Equal(t, id, g.ID) + assert.Equal(t, serverKey, g.ServerKey) + assert.Equal(t, versionCode, g.VersionCode) + }) + + t.Run("ERR: server doesn't exist", func(t *testing.T) { + t.Parallel() + + client := &mock.FakeTWHelpClient{} + client.GetServerCalls(func(_ context.Context, _ string, _ string) (twhelp.Server, error) { + return twhelp.Server{}, twhelp.APIError{ + Code: twhelp.ErrorCodeEntityNotFound, + Message: "server not found", + } + }) + + g, err := service.NewGroup(nil, client).SetTWServer(context.Background(), id, versionCode, serverKey) + assert.ErrorIs(t, err, domain.ServerDoesNotExistError{ + VersionCode: versionCode, + Key: serverKey, + }) + assert.Zero(t, g) + }) + + t.Run("ERR: server is closed", func(t *testing.T) { + t.Parallel() + + client := &mock.FakeTWHelpClient{} + client.GetServerCalls(func(ctx context.Context, _ string, server string) (twhelp.Server, error) { + return twhelp.Server{ + Key: server, + URL: fmt.Sprintf("https://%s.tribalwars.net", server), + Open: false, + }, nil + }) + + g, err := service.NewGroup(nil, client).SetTWServer(context.Background(), id, versionCode, serverKey) + assert.ErrorIs(t, err, domain.ServerIsClosedError{ + VersionCode: versionCode, + Key: serverKey, + }) + assert.Zero(t, g) + }) +}