feat: add a new command - /monitor create (#21)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #21
This commit is contained in:
parent
13a8da3325
commit
58f73e9ca8
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
27
internal/bundb/internal/model/monitor.go
Normal file
27
internal/bundb/internal/model/monitor.go
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
95
internal/bundb/monitor.go
Normal file
95
internal/bundb/monitor.go
Normal file
|
@ -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
|
||||
}
|
||||
}
|
140
internal/bundb/monitor_test.go
Normal file
140
internal/bundb/monitor_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
25
internal/bundb/testdata/fixture.yml
vendored
25
internal/bundb/testdata/fixture.yml
vendored
|
@ -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
|
||||
- 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
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
118
internal/discord/command_monitor.go
Normal file
118
internal/discord/command_monitor.go
Normal file
|
@ -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 + ")",
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
84
internal/domain/monitor.go
Normal file
84
internal/domain/monitor.go
Normal file
|
@ -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
|
||||
}
|
93
internal/domain/monitor_test.go
Normal file
93
internal/domain/monitor_test.go
Normal file
|
@ -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())
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
89
internal/service/monitor.go
Normal file
89
internal/service/monitor.go
Normal file
|
@ -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
|
||||
}
|
195
internal/service/monitor_test.go
Normal file
195
internal/service/monitor_test.go
Normal file
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"`
|
||||
}
|
Loading…
Reference in New Issue
Block a user