diff --git a/internal/bundb/group_test.go b/internal/bundb/group_test.go index 4533bcb..be9a99a 100644 --- a/internal/bundb/group_test.go +++ b/internal/bundb/group_test.go @@ -193,6 +193,9 @@ func TestGroup_Get(t *testing.T) { t.Run("OK", func(t *testing.T) { t.Parallel() + g, err := repo.Get(context.Background(), group.ID.String(), group.ServerID) + assert.NoError(t, err) + assert.Equal(t, group.ToDomain(), g) }) t.Run("ERR: group not found (unknown ID)", func(t *testing.T) { @@ -239,19 +242,30 @@ func TestGroup_Delete(t *testing.T) { db := newDB(t) fixture := loadFixtures(t, db) - repo := bundb.NewGroup(db) + groupRepo := bundb.NewGroup(db) + monitorRepo := bundb.NewMonitor(db) group := getGroupFromFixture(t, fixture, "group-1-server-1") t.Run("OK", func(t *testing.T) { t.Parallel() - assert.NoError(t, repo.Delete(context.Background(), group.ID.String(), group.ServerID)) - - groups, err := repo.List(context.Background(), domain.ListGroupsParams{ - ServerIDs: []string{group.ID.String()}, + beforeDelete, err := groupRepo.List(context.Background(), domain.ListGroupsParams{ + ServerIDs: []string{group.ServerID}, }) assert.NoError(t, err) - assert.Len(t, groups, 0) + + assert.NoError(t, groupRepo.Delete(context.Background(), group.ID.String(), group.ServerID)) + + afterDelete, err := groupRepo.List(context.Background(), domain.ListGroupsParams{ + ServerIDs: []string{group.ServerID}, + }) + assert.NoError(t, err) + assert.Len(t, afterDelete, len(beforeDelete)-1) + + // monitors should also be deleted + monitors, err := monitorRepo.List(context.Background(), group.ID.String()) + assert.NoError(t, err) + assert.Len(t, monitors, 0) }) t.Run("ERR: group not found", func(t *testing.T) { @@ -287,7 +301,7 @@ func TestGroup_Delete(t *testing.T) { assert.ErrorIs( t, - repo.Delete(context.Background(), tt.id, tt.serverID), + groupRepo.Delete(context.Background(), tt.id, tt.serverID), domain.GroupNotFoundError{ID: tt.id}, ) }) diff --git a/internal/bundb/monitor.go b/internal/bundb/monitor.go index af3c0e4..1cda68a 100644 --- a/internal/bundb/monitor.go +++ b/internal/bundb/monitor.go @@ -71,6 +71,47 @@ func (m *Monitor) List(ctx context.Context, groupID string) ([]domain.Monitor, e return result, nil } +func (m *Monitor) Get(ctx context.Context, id string) (domain.Monitor, error) { + if _, err := uuid.Parse(id); err != nil { + return domain.Monitor{}, domain.MonitorNotFoundError{ID: id} + } + + var monitor model.Monitor + + err := m.db.NewSelect(). + Model(&monitor). + Where("id = ?", id). + Scan(ctx) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return domain.Monitor{}, domain.MonitorNotFoundError{ID: id} + } + return domain.Monitor{}, fmt.Errorf("couldn't select monitor from the db (id=%s): %w", id, err) + } + + return monitor.ToDomain(), nil +} + +func (m *Monitor) Delete(ctx context.Context, id string) error { + if _, err := uuid.Parse(id); err != nil { + return domain.MonitorNotFoundError{ID: id} + } + + res, err := m.db.NewDelete(). + Model(&model.Monitor{}). + Returning("NULL"). + Where("id = ?", id). + Exec(ctx) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("couldn't delete monitor (id=%s): %w", id, err) + } + if affected, _ := res.RowsAffected(); affected == 0 { + return domain.MonitorNotFoundError{ID: id} + } + + return nil +} + func mapCreateMonitorError(err error, params domain.CreateMonitorParams) error { var pgError pgdriver.Error if !errors.As(err, &pgError) { diff --git a/internal/bundb/monitor_test.go b/internal/bundb/monitor_test.go index 945446f..5b5ff5a 100644 --- a/internal/bundb/monitor_test.go +++ b/internal/bundb/monitor_test.go @@ -138,3 +138,111 @@ func TestMonitor_List(t *testing.T) { }) } } + +func TestMonitor_Get(t *testing.T) { + t.Parallel() + + db := newDB(t) + fixture := loadFixtures(t, db) + repo := bundb.NewMonitor(db) + group := getGroupFromFixture(t, fixture, "group-1-server-1") + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + monitors, err := repo.List(context.Background(), group.ID.String()) + require.NoError(t, err) + require.Greater(t, len(monitors), 0) + + m, err := repo.Get(context.Background(), monitors[0].ID) + assert.NoError(t, err) + assert.Equal(t, monitors[0], m) + }) + + t.Run("ERR: monitor not found", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + id string + }{ + { + name: "ID - not UUID", + id: "test", + }, + { + name: "ID - random UUID", + id: uuid.NewString(), + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + assert.ErrorIs( + t, + repo.Delete(context.Background(), tt.id), + domain.MonitorNotFoundError{ID: tt.id}, + ) + }) + } + }) +} + +func TestMonitor_Delete(t *testing.T) { + t.Parallel() + + db := newDB(t) + fixture := loadFixtures(t, db) + repo := bundb.NewMonitor(db) + group := getGroupFromFixture(t, fixture, "group-1-server-1") + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + beforeDelete, err := repo.List(context.Background(), group.ID.String()) + assert.NoError(t, err) + assert.Greater(t, len(beforeDelete), 0) + + assert.NoError(t, repo.Delete(context.Background(), beforeDelete[0].ID)) + + afterDelete, err := repo.List(context.Background(), group.ID.String()) + assert.NoError(t, err) + assert.Len(t, afterDelete, len(beforeDelete)-1) + }) + + t.Run("ERR: monitor not found", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + id string + }{ + { + name: "ID - not UUID", + id: "test", + }, + { + name: "ID - random UUID", + id: uuid.NewString(), + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + assert.ErrorIs( + t, + repo.Delete(context.Background(), tt.id), + domain.MonitorNotFoundError{ID: tt.id}, + ) + }) + } + }) +} diff --git a/internal/discord/bot.go b/internal/discord/bot.go index f99309e..c17a13c 100644 --- a/internal/discord/bot.go +++ b/internal/discord/bot.go @@ -18,6 +18,7 @@ type GroupService interface { type MonitorService interface { Create(ctx context.Context, groupID, serverID, tribeTag string) (domain.Monitor, error) + Delete(ctx context.Context, id, serverID string) error } type ChoiceService interface { diff --git a/internal/discord/command_monitor.go b/internal/discord/command_monitor.go index 7df2ca8..b7bc3aa 100644 --- a/internal/discord/command_monitor.go +++ b/internal/discord/command_monitor.go @@ -57,6 +57,19 @@ func (c *monitorCommand) create(s *discordgo.Session) error { }, }, }, + { + Name: "delete", + Description: "Deletes a monitor", + Type: discordgo.ApplicationCommandOptionSubCommand, + Options: []*discordgo.ApplicationCommandOption{ + { + Name: "monitor", + Description: "Monitor ID", + Type: discordgo.ApplicationCommandOptionString, + Required: true, + }, + }, + }, }, }) if err != nil { @@ -77,6 +90,9 @@ func (c *monitorCommand) handle(s *discordgo.Session, i *discordgo.InteractionCr case "create": c.handleCreate(s, i) return + case "delete": + c.handleDelete(s, i) + return default: } } @@ -116,3 +132,25 @@ func (c *monitorCommand) handleCreate(s *discordgo.Session, i *discordgo.Interac }, }) } + +func (c *monitorCommand) handleDelete(s *discordgo.Session, i *discordgo.InteractionCreate) { + ctx := context.Background() + + monitor := i.ApplicationCommandData().Options[0].Options[0].StringValue() + if err := c.svc.Delete(ctx, monitor, i.GuildID); 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 deleted", + }, + }) +} diff --git a/internal/domain/monitor.go b/internal/domain/monitor.go index c1c6055..d02ec58 100644 --- a/internal/domain/monitor.go +++ b/internal/domain/monitor.go @@ -82,3 +82,19 @@ func (e MonitorLimitReachedError) UserError() string { func (e MonitorLimitReachedError) Code() ErrorCode { return ErrorCodeValidationError } + +type MonitorNotFoundError struct { + ID string +} + +func (e MonitorNotFoundError) Error() string { + return fmt.Sprintf("monitor (ID=%s) not found", e.ID) +} + +func (e MonitorNotFoundError) UserError() string { + return e.Error() +} + +func (e MonitorNotFoundError) Code() ErrorCode { + return ErrorCodeEntityNotFound +} diff --git a/internal/domain/monitor_test.go b/internal/domain/monitor_test.go index dbe72c3..a311031 100644 --- a/internal/domain/monitor_test.go +++ b/internal/domain/monitor_test.go @@ -91,3 +91,15 @@ func TestMonitorLimitReachedError(t *testing.T) { assert.Equal(t, err.Error(), err.UserError()) assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) } + +func TestMonitorNotFoundError(t *testing.T) { + t.Parallel() + + err := domain.MonitorNotFoundError{ + ID: uuid.NewString(), + } + var _ domain.UserError = err + assert.Equal(t, fmt.Sprintf("monitor (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/monitor.go b/internal/service/monitor.go index 0b93813..d94c717 100644 --- a/internal/service/monitor.go +++ b/internal/service/monitor.go @@ -17,6 +17,8 @@ const ( type MonitorRepository interface { Create(ctx context.Context, params domain.CreateMonitorParams) (domain.Monitor, error) List(ctx context.Context, groupID string) ([]domain.Monitor, error) + Get(ctx context.Context, id string) (domain.Monitor, error) + Delete(ctx context.Context, id string) error } //counterfeiter:generate -o internal/mock/group_getter.gen.go . GroupGetter @@ -87,3 +89,21 @@ func (m *Monitor) Create(ctx context.Context, groupID, serverID, tribeTag string return monitor, nil } + +func (m *Monitor) Delete(ctx context.Context, id, serverID string) error { + monitor, err := m.repo.Get(ctx, id) + if err != nil { + return fmt.Errorf("MonitorRepository.Get: %w", err) + } + + _, err = m.groupSvc.Get(ctx, monitor.GroupID, serverID) + if err != nil { + return fmt.Errorf("GroupService.Get: %w", err) + } + + if err = m.repo.Delete(ctx, id); err != nil { + return fmt.Errorf("MonitorRepository.Delete: %w", err) + } + + return nil +}