From b6130b9fc90c0aaf6e12a5df13616480f495229a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Sun, 9 Oct 2022 08:19:31 +0000 Subject: [PATCH 1/2] feat: group limit (#9) Reviewed-on: https://gitea.dwysokinski.me/twhelp/dcbot/pulls/9 --- go.mod | 1 + go.sum | 2 + internal/bundb/bundb_test.go | 10 ++++ internal/bundb/group.go | 34 ++++++++++++ internal/bundb/group_test.go | 84 +++++++++++++++++++++++++++++ internal/bundb/testdata/fixture.yml | 26 +++++++++ internal/domain/error.go | 36 ------------- internal/domain/error_test.go | 27 ---------- internal/domain/group.go | 26 ++++++++- internal/domain/group_test.go | 14 +++++ internal/domain/tw.go | 37 +++++++++++++ internal/domain/tw_test.go | 35 ++++++++++++ internal/service/group.go | 19 +++++++ internal/service/group_test.go | 29 ++++++++-- 14 files changed, 313 insertions(+), 67 deletions(-) create mode 100644 internal/bundb/testdata/fixture.yml create mode 100644 internal/domain/tw.go create mode 100644 internal/domain/tw_test.go diff --git a/go.mod b/go.mod index f8990fe..3db864d 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/ory/dockertest/v3 v3.9.1 github.com/stretchr/testify v1.8.0 github.com/uptrace/bun v1.1.8 + github.com/uptrace/bun/dbfixture v1.1.8 github.com/uptrace/bun/dialect/pgdialect v1.1.8 github.com/uptrace/bun/driver/pgdriver v1.1.8 github.com/uptrace/bun/extra/bunotel v1.1.8 diff --git a/go.sum b/go.sum index fb3524c..53247cd 100644 --- a/go.sum +++ b/go.sum @@ -111,6 +111,8 @@ github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYm github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= github.com/uptrace/bun v1.1.8 h1:slxuaP4LYWFbPRUmTtQhfJN+6eX/6ar2HDKYTcI50SA= github.com/uptrace/bun v1.1.8/go.mod h1:iT89ESdV3uMupD9ixt6Khidht+BK0STabK/LeZE+B84= +github.com/uptrace/bun/dbfixture v1.1.8 h1:cDRqII+emIgmmhWSebdAZWu5vKmuIw+PZ/arf0oYJdo= +github.com/uptrace/bun/dbfixture v1.1.8/go.mod h1:GrRDt9lIfDpMyAGIJjWNsYeRHKjr8Gr2wDl2Rsf1sR4= github.com/uptrace/bun/dialect/pgdialect v1.1.8 h1:wayJhjYDPGv8tgOBLolbBtSFQ0TihFoo8E1T129UdA8= github.com/uptrace/bun/dialect/pgdialect v1.1.8/go.mod h1:nNbU8PHTjTUM+CRtGmqyBb9zcuRAB8I680/qoFSmBUk= github.com/uptrace/bun/driver/pgdriver v1.1.8 h1:gyL22axRQfjJS2Umq0erzJnp0bLOdUE8/USKZHPQB8o= diff --git a/internal/bundb/bundb_test.go b/internal/bundb/bundb_test.go index 62f0b79..aa49b16 100644 --- a/internal/bundb/bundb_test.go +++ b/internal/bundb/bundb_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/uptrace/bun" + "github.com/uptrace/bun/dbfixture" "github.com/uptrace/bun/dialect/pgdialect" "github.com/uptrace/bun/driver/pgdriver" ) @@ -126,6 +127,15 @@ func runMigrations(tb testing.TB, db *bun.DB) { require.NoError(tb, err, "couldn't migrate (2)") } +func loadFixtures(tb testing.TB, bunDB *bun.DB) *dbfixture.Fixture { + tb.Helper() + + fixture := dbfixture.New(bunDB) + err := fixture.Load(context.Background(), os.DirFS("testdata"), "fixture.yml") + require.NoError(tb, err, "couldn't load fixtures") + return fixture +} + func generateSchema() string { return strings.TrimFunc(strings.ReplaceAll(uuid.NewString(), "-", "_"), unicode.IsNumber) } diff --git a/internal/bundb/group.go b/internal/bundb/group.go index bd4d695..efa0a88 100644 --- a/internal/bundb/group.go +++ b/internal/bundb/group.go @@ -2,6 +2,8 @@ package bundb import ( "context" + "database/sql" + "errors" "fmt" "gitea.dwysokinski.me/twhelp/dcbot/internal/bundb/internal/model" @@ -25,11 +27,43 @@ func (g *Group) Create(ctx context.Context, params domain.CreateGroupParams) (do ChannelLostVillages: params.ChannelLostVillages(), ServerKey: params.ServerKey(), } + if _, err := g.db.NewInsert(). Model(&group). Returning("*"). Exec(ctx); err != nil { return domain.Group{}, fmt.Errorf("something went wrong while inserting group into the db: %w", err) } + return group.ToDomain(), nil } + +func (g *Group) List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) { + var groups []model.Group + + if err := g.db.NewSelect(). + Model(&groups). + Order("created_at ASC"). + Apply(listGroupsParamsApplier{params}.apply). + Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("couldn't select groups from the db: %w", err) + } + + result := make([]domain.Group, 0, len(groups)) + for _, version := range groups { + result = append(result, version.ToDomain()) + } + return result, nil +} + +type listGroupsParamsApplier struct { + params domain.ListGroupsParams +} + +func (l listGroupsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery { + if l.params.ServerIDs != nil { + q = q.Where("server_id IN (?)", bun.In(l.params.ServerIDs)) + } + + return q +} diff --git a/internal/bundb/group_test.go b/internal/bundb/group_test.go index 4a3c5a7..dd16c0d 100644 --- a/internal/bundb/group_test.go +++ b/internal/bundb/group_test.go @@ -6,10 +6,12 @@ import ( "time" "gitea.dwysokinski.me/twhelp/dcbot/internal/bundb" + "gitea.dwysokinski.me/twhelp/dcbot/internal/bundb/internal/model" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/uptrace/bun/dbfixture" ) func TestGroup_Create(t *testing.T) { @@ -41,3 +43,85 @@ func TestGroup_Create(t *testing.T) { assert.WithinDuration(t, time.Now(), group.CreatedAt, 1*time.Second) }) } + +func TestGroup_List(t *testing.T) { + t.Parallel() + + db := newDB(t) + fixture := loadFixtures(t, db) + repo := bundb.NewGroup(db) + groups := getAllGroupsFromFixture(t, fixture) + + allGroups := make([]string, 0, len(groups)) + for _, g := range groups { + allGroups = append(allGroups, g.ID.String()) + } + + tests := []struct { + name string + params domain.ListGroupsParams + expectedGroups []string + }{ + { + name: "no params", + params: domain.ListGroupsParams{}, + expectedGroups: allGroups, + }, + { + name: "ServerIDs=[server-1]", + params: domain.ListGroupsParams{ + ServerIDs: []string{"server-1"}, + }, + expectedGroups: []string{ + "d56ad37f-2637-48ea-98f8-79627f3fcc96", + "429b790e-7186-4106-b531-4cc4931ce2ba", + }, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + res, err := repo.List(context.Background(), tt.params) + assert.NoError(t, err) + assert.Len(t, res, len(tt.expectedGroups)) + for _, id := range tt.expectedGroups { + found := false + for _, grp := range res { + if grp.ID == id { + found = true + break + } + } + assert.True(t, found, "group (id=%s) not found", id) + } + }) + } +} + +func getAllGroupsFromFixture(tb testing.TB, fixture *dbfixture.Fixture) []model.Group { + tb.Helper() + + //nolint:lll + ids := []string{"group-server-1-1", "group-server-1-2", "group-server-2-1", "group-server-2-2"} + + groups := make([]model.Group, 0, len(ids)) + for _, id := range ids { + groups = append(groups, getGroupFromFixture(tb, fixture, id)) + } + + return groups +} + +func getGroupFromFixture(tb testing.TB, fixture *dbfixture.Fixture, id string) model.Group { + tb.Helper() + + row, err := fixture.Row("Group." + id) + require.NoError(tb, err) + g, ok := row.(*model.Group) + require.True(tb, ok) + return *g +} diff --git a/internal/bundb/testdata/fixture.yml b/internal/bundb/testdata/fixture.yml new file mode 100644 index 0000000..db9f7fc --- /dev/null +++ b/internal/bundb/testdata/fixture.yml @@ -0,0 +1,26 @@ +- model: Group + rows: + - _id: group-server-1-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: 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: 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: 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 diff --git a/internal/domain/error.go b/internal/domain/error.go index 9e45ac9..6254a36 100644 --- a/internal/domain/error.go +++ b/internal/domain/error.go @@ -1,7 +1,5 @@ package domain -import "fmt" - type ErrorCode uint8 const ( @@ -31,37 +29,3 @@ func (e RequiredError) UserError() string { func (e RequiredError) Code() ErrorCode { return ErrorCodeValidationError } - -type ServerDoesNotExistError struct { - VersionCode string - Key string -} - -func (e ServerDoesNotExistError) Error() string { - return fmt.Sprintf("server (VersionCode=%s,Key=%s) doesn't exist", e.VersionCode, e.Key) -} - -func (e ServerDoesNotExistError) UserError() string { - return e.Error() -} - -func (e ServerDoesNotExistError) Code() ErrorCode { - return ErrorCodeValidationError -} - -type ServerIsClosedError struct { - VersionCode string - Key string -} - -func (e ServerIsClosedError) Error() string { - return fmt.Sprintf("server (VersionCode=%s,Key=%s) is closed", e.VersionCode, e.Key) -} - -func (e ServerIsClosedError) UserError() string { - return e.Error() -} - -func (e ServerIsClosedError) Code() ErrorCode { - return ErrorCodeValidationError -} diff --git a/internal/domain/error_test.go b/internal/domain/error_test.go index 071bae5..183ac98 100644 --- a/internal/domain/error_test.go +++ b/internal/domain/error_test.go @@ -1,7 +1,6 @@ package domain_test import ( - "fmt" "testing" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" @@ -19,29 +18,3 @@ func TestRequiredError(t *testing.T) { assert.Equal(t, err.Error(), err.UserError()) assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) } - -func TestServerDoesNotExistError(t *testing.T) { - t.Parallel() - - err := domain.ServerDoesNotExistError{ - VersionCode: "pl", - Key: "pl151", - } - var _ domain.UserError = err - assert.Equal(t, fmt.Sprintf("server (VersionCode=%s,Key=%s) doesn't exist", err.VersionCode, err.Key), err.Error()) - assert.Equal(t, err.Error(), err.UserError()) - assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) -} - -func TestServerIsClosedError(t *testing.T) { - t.Parallel() - - err := domain.ServerIsClosedError{ - VersionCode: "pl", - Key: "pl151", - } - var _ domain.UserError = err - assert.Equal(t, fmt.Sprintf("server (VersionCode=%s,Key=%s) is closed", err.VersionCode, err.Key), 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 be50d20..73bca1a 100644 --- a/internal/domain/group.go +++ b/internal/domain/group.go @@ -1,6 +1,9 @@ package domain -import "time" +import ( + "fmt" + "time" +) type Group struct { ID string @@ -67,3 +70,24 @@ func (c CreateGroupParams) ChannelGainedVillages() string { func (c CreateGroupParams) ChannelLostVillages() string { return c.channelLostVillages } + +type ListGroupsParams struct { + ServerIDs []string +} + +type GroupLimitReachedError struct { + Current int // current number of groups + Limit int // maximum number of groups +} + +func (e GroupLimitReachedError) Error() string { + return fmt.Sprintf("group limit has been reached (%d/%d)", e.Current, e.Limit) +} + +func (e GroupLimitReachedError) UserError() string { + return e.Error() +} + +func (e GroupLimitReachedError) Code() ErrorCode { + return ErrorCodeValidationError +} diff --git a/internal/domain/group_test.go b/internal/domain/group_test.go index e9011bc..153d58c 100644 --- a/internal/domain/group_test.go +++ b/internal/domain/group_test.go @@ -1,6 +1,7 @@ package domain_test import ( + "fmt" "testing" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" @@ -75,3 +76,16 @@ func TestNewCreateGroupParams(t *testing.T) { }) } } + +func TestGroupLimitReachedError(t *testing.T) { + t.Parallel() + + err := domain.GroupLimitReachedError{ + Current: 10, + Limit: 10, + } + var _ domain.UserError = err + assert.Equal(t, fmt.Sprintf("group 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 new file mode 100644 index 0000000..4a67a14 --- /dev/null +++ b/internal/domain/tw.go @@ -0,0 +1,37 @@ +package domain + +import "fmt" + +type ServerDoesNotExistError struct { + VersionCode string + Key string +} + +func (e ServerDoesNotExistError) Error() string { + return fmt.Sprintf("server (VersionCode=%s,Key=%s) doesn't exist", e.VersionCode, e.Key) +} + +func (e ServerDoesNotExistError) UserError() string { + return e.Error() +} + +func (e ServerDoesNotExistError) Code() ErrorCode { + return ErrorCodeValidationError +} + +type ServerIsClosedError struct { + VersionCode string + Key string +} + +func (e ServerIsClosedError) Error() string { + return fmt.Sprintf("server (VersionCode=%s,Key=%s) is closed", e.VersionCode, e.Key) +} + +func (e ServerIsClosedError) UserError() string { + return e.Error() +} + +func (e ServerIsClosedError) Code() ErrorCode { + return ErrorCodeValidationError +} diff --git a/internal/domain/tw_test.go b/internal/domain/tw_test.go new file mode 100644 index 0000000..eade526 --- /dev/null +++ b/internal/domain/tw_test.go @@ -0,0 +1,35 @@ +package domain_test + +import ( + "fmt" + "testing" + + "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" + "github.com/stretchr/testify/assert" +) + +func TestServerDoesNotExistError(t *testing.T) { + t.Parallel() + + err := domain.ServerDoesNotExistError{ + VersionCode: "pl", + Key: "pl151", + } + var _ domain.UserError = err + assert.Equal(t, fmt.Sprintf("server (VersionCode=%s,Key=%s) doesn't exist", err.VersionCode, err.Key), err.Error()) + assert.Equal(t, err.Error(), err.UserError()) + assert.Equal(t, domain.ErrorCodeValidationError, err.Code()) +} + +func TestServerIsClosedError(t *testing.T) { + t.Parallel() + + err := domain.ServerIsClosedError{ + VersionCode: "pl", + Key: "pl151", + } + var _ domain.UserError = err + assert.Equal(t, fmt.Sprintf("server (VersionCode=%s,Key=%s) is closed", err.VersionCode, err.Key), 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 fe159b2..b097ebb 100644 --- a/internal/service/group.go +++ b/internal/service/group.go @@ -9,9 +9,14 @@ import ( "gitea.dwysokinski.me/twhelp/dcbot/internal/twhelp" ) +const ( + maxGroupsPerServer = 10 +) + //counterfeiter:generate -o internal/mock/group_repository.gen.go . GroupRepository type GroupRepository interface { Create(ctx context.Context, params domain.CreateGroupParams) (domain.Group, error) + List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) } type Group struct { @@ -24,6 +29,20 @@ func NewGroup(repo GroupRepository, client TWHelpClient) *Group { } func (g *Group) Create(ctx context.Context, params domain.CreateGroupParams) (domain.Group, error) { + groups, err := g.repo.List(ctx, domain.ListGroupsParams{ + ServerIDs: []string{params.ServerID()}, + }) + if err != nil { + return domain.Group{}, fmt.Errorf("GroupRepository.List: %w", err) + } + + if len(groups) >= maxGroupsPerServer { + return domain.Group{}, domain.GroupLimitReachedError{ + Current: len(groups), + Limit: maxGroupsPerServer, + } + } + server, err := g.client.GetServer(ctx, params.VersionCode(), params.ServerKey()) if err != nil { var apiErr twhelp.APIError diff --git a/internal/service/group_test.go b/internal/service/group_test.go index 51f0be8..258f0aa 100644 --- a/internal/service/group_test.go +++ b/internal/service/group_test.go @@ -42,6 +42,7 @@ func TestGroup_Create(t *testing.T) { CreatedAt: time.Now(), }, nil }) + repo.ListReturns(nil, nil) client := &mock.FakeTWHelpClient{} client.GetServerCalls(func(_ context.Context, _ string, server string) (twhelp.Server, error) { @@ -63,9 +64,28 @@ func TestGroup_Create(t *testing.T) { assert.NotEmpty(t, g.CreatedAt) }) - t.Run("ERR: server doesnt exist", func(t *testing.T) { + t.Run("ERR: group limit has been reached", func(t *testing.T) { t.Parallel() + const maxGroupsPerServer = 10 + + repo := &mock.FakeGroupRepository{} + repo.ListReturns(make([]domain.Group, maxGroupsPerServer), nil) + + g, err := service.NewGroup(repo, nil).Create(context.Background(), params) + assert.ErrorIs(t, err, domain.GroupLimitReachedError{ + Current: maxGroupsPerServer, + Limit: maxGroupsPerServer, + }) + assert.Zero(t, g) + }) + + t.Run("ERR: server doesn't exist", func(t *testing.T) { + t.Parallel() + + repo := &mock.FakeGroupRepository{} + repo.ListReturns(nil, nil) + client := &mock.FakeTWHelpClient{} client.GetServerCalls(func(_ context.Context, _ string, _ string) (twhelp.Server, error) { return twhelp.Server{}, twhelp.APIError{ @@ -74,7 +94,7 @@ func TestGroup_Create(t *testing.T) { } }) - g, err := service.NewGroup(nil, client).Create(context.Background(), params) + g, err := service.NewGroup(repo, client).Create(context.Background(), params) assert.ErrorIs(t, err, domain.ServerDoesNotExistError{ VersionCode: params.VersionCode(), Key: params.ServerKey(), @@ -85,6 +105,9 @@ func TestGroup_Create(t *testing.T) { t.Run("ERR: server is closed", func(t *testing.T) { t.Parallel() + repo := &mock.FakeGroupRepository{} + repo.ListReturns(nil, nil) + client := &mock.FakeTWHelpClient{} client.GetServerCalls(func(ctx context.Context, _ string, server string) (twhelp.Server, error) { return twhelp.Server{ @@ -94,7 +117,7 @@ func TestGroup_Create(t *testing.T) { }, nil }) - g, err := service.NewGroup(nil, client).Create(context.Background(), params) + g, err := service.NewGroup(repo, client).Create(context.Background(), params) assert.ErrorIs(t, err, domain.ServerIsClosedError{ VersionCode: params.VersionCode(), Key: params.ServerKey(), From 7650c03d3af383922d2fb4d5fdca9eca8fb39245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Sun, 9 Oct 2022 10:22:42 +0200 Subject: [PATCH 2/2] fix: add missing assert --- internal/domain/group_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/domain/group_test.go b/internal/domain/group_test.go index 153d58c..1c15124 100644 --- a/internal/domain/group_test.go +++ b/internal/domain/group_test.go @@ -71,6 +71,7 @@ func TestNewCreateGroupParams(t *testing.T) { assert.NoError(t, err) assert.Equal(t, tt.serverID, res.ServerID()) assert.Equal(t, tt.serverKey, res.ServerKey()) + assert.Equal(t, tt.versionCode, res.VersionCode()) assert.Equal(t, tt.channelGainedVillages, res.ChannelGainedVillages()) assert.Equal(t, tt.channelLostVillages, res.ChannelLostVillages()) })