diff --git a/Makefile b/Makefile index 12030a3..9990be8 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ install-deps: GOBIN=$(LOCAL_BIN) go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.31.0 GOBIN=$(LOCAL_BIN) go install -mod=mod google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0 GOBIN=$(LOCAL_BIN) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2 + GOBIN=$(LOCAL_BIN) go install github.com/vektra/mockery/v2@v2.36.1 .PHONY: tidy tidy: diff --git a/auth-server/.gitignore b/auth-server/.gitignore index ae3c172..65776c3 100644 --- a/auth-server/.gitignore +++ b/auth-server/.gitignore @@ -1 +1 @@ -/bin/ +/bin/ \ No newline at end of file diff --git a/auth-server/.mockery.yaml b/auth-server/.mockery.yaml new file mode 100644 index 0000000..e3cce26 --- /dev/null +++ b/auth-server/.mockery.yaml @@ -0,0 +1,15 @@ +quiet: False +with-expecter: True +mockname: "Mock{{.InterfaceName}}" +filename: "{{.InterfaceName}}.mock.gen.go" +dir: "{{.InterfaceDir}}/mocks" +outpkg: "mocks" +packages: + github.com/aywan/balun_miserv_s2/auth-server/internal/repository: + interfaces: + User: + Audit: + github.com/aywan/balun_miserv_s2/auth-server/internal/service: + interfaces: + User: + diff --git a/auth-server/Makefile b/auth-server/Makefile index e5a26e0..efe1a22 100644 --- a/auth-server/Makefile +++ b/auth-server/Makefile @@ -1,5 +1,6 @@ LOCAL_BIN?=$(CURDIR)/../bin GO_EXE?=go +export PATH:=$(PATH):$(LOCAL_BIN) .PHONY: tidy tidy: @@ -16,6 +17,11 @@ tests: .PHONY: generate generate: make generate-api-user-v1 + make mocks + +.PHONY: mocks +mocks: + $(LOCAL_BIN)/mockery .PHONY: generate-api-user-v1 generate-api-user-v1: diff --git a/auth-server/go.mod b/auth-server/go.mod index 627ebc9..612cad6 100644 --- a/auth-server/go.mod +++ b/auth-server/go.mod @@ -7,6 +7,7 @@ require ( github.com/aywan/balun_miserv_s2/shared/lib/db v0.0.0 github.com/aywan/balun_miserv_s2/shared/lib/logger v0.0.0 github.com/aywan/balun_miserv_s2/shared/lib/runutil v0.0.0 + github.com/brianvoe/gofakeit/v6 v6.24.0 github.com/fatih/color v1.15.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/stretchr/testify v1.8.4 @@ -38,6 +39,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/stretchr/objx v0.5.0 // indirect go.uber.org/dig v1.17.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/net v0.15.0 // indirect diff --git a/auth-server/go.sum b/auth-server/go.sum index 1c95eda..b5d6fa4 100644 --- a/auth-server/go.sum +++ b/auth-server/go.sum @@ -2,6 +2,8 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8 github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/brianvoe/gofakeit/v6 v6.24.0 h1:74yq7RRz/noddscZHRS2T84oHZisW9muwbb8sRnU52A= +github.com/brianvoe/gofakeit/v6 v6.24.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs= github.com/cockroachdb/cockroach-go/v2 v2.2.0/go.mod h1:u3MiKYGupPPjkn3ozknpMUpxPaNLTFWAya419/zv6eI= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -57,11 +59,14 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= diff --git a/auth-server/internal/api/user/impl_test.go b/auth-server/internal/api/user/impl_test.go new file mode 100644 index 0000000..12773a0 --- /dev/null +++ b/auth-server/internal/api/user/impl_test.go @@ -0,0 +1,129 @@ +package user + +import ( + "context" + "database/sql" + "testing" + + "github.com/aywan/balun_miserv_s2/auth-server/internal/model" + "github.com/aywan/balun_miserv_s2/auth-server/internal/repository/user/dto" + "github.com/aywan/balun_miserv_s2/auth-server/internal/service/mocks" + desc "github.com/aywan/balun_miserv_s2/auth-server/pkg/grpc/v1/user_v1" + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +func TestImplementation_Create(t *testing.T) { + t.Parallel() + log := zaptest.NewLogger(t) + ctx := context.Background() + var err error + + userID := gofakeit.Int64() + pwd := gofakeit.HexUint16() + + req := &desc.CreateRequest{ + User: &desc.UserData{ + Name: gofakeit.Name(), + Email: gofakeit.Email(), + Role: desc.UserRole_ADMIN, + }, + Credentials: &desc.UserCredentials{ + Password: pwd, + PasswordConfirm: pwd, + }, + } + + userService := mocks.NewMockUser(t) + userService.EXPECT(). + Create(ctx, model.UserData{ + Name: req.User.Name, + Email: req.User.Email, + PasswordHash: req.Credentials.Password, + Role: int32(req.User.Role), + }). + Return(userID, nil) + + srv := New(log, userService) + + rsp, err := srv.Create(ctx, req) + require.NoError(t, err) + + require.Equal(t, rsp.Id, userID) +} + +func TestImplementation_Update(t *testing.T) { + t.Parallel() + log := zaptest.NewLogger(t) + ctx := context.Background() + var err error + + req := desc.UpdateRequest{ + Id: gofakeit.Int64(), + Name: wrapperspb.String(gofakeit.Name()), + Email: wrapperspb.String(gofakeit.Email()), + } + + userService := mocks.NewMockUser(t) + userService.EXPECT(). + Update(ctx, req.Id, dto.UpdateDTO{ + Name: sql.NullString{req.Name.Value, true}, + Email: sql.NullString{req.Email.Value, true}, + PasswordHash: sql.NullString{}, + Role: sql.NullInt32{}, + }). + Return(nil) + + srv := New(log, userService) + + _, err = srv.Update(ctx, &req) + require.NoError(t, err) +} + +func TestImplementation_Delete(t *testing.T) { + t.Parallel() + log := zaptest.NewLogger(t) + ctx := context.Background() + var err error + + userID := gofakeit.Int64() + + userService := mocks.NewMockUser(t) + userService.EXPECT(). + Delete(ctx, userID). + Return(nil) + + srv := New(log, userService) + + _, err = srv.Delete(ctx, &desc.UserIdRequest{Id: userID}) + require.NoError(t, err) +} + +func TestImplementation_Get(t *testing.T) { + t.Parallel() + log := zaptest.NewLogger(t) + ctx := context.Background() + var err error + + userModel := model.FixtureUserWithFaker(t) + + userService := mocks.NewMockUser(t) + userService.EXPECT(). + Get(ctx, userModel.ID). + Return(userModel, nil) + + srv := New(log, userService) + + rsp, err := srv.Get(ctx, &desc.UserIdRequest{Id: userModel.ID}) + require.NoError(t, err) + + require.Equal(t, rsp.Id, userModel.ID) + require.Equal(t, rsp.CreatedAt, timestamppb.New(userModel.CreatedAt)) + require.Equal(t, rsp.UpdatedAt, timestamppb.New(userModel.UpdatedAt.Time)) + require.Equal(t, rsp.User.Name, userModel.Data.Name) + require.Equal(t, rsp.User.Email, userModel.Data.Email) + require.Equal(t, rsp.User.Role, desc.UserRole(userModel.Data.Role)) +} diff --git a/auth-server/internal/model/audit.go b/auth-server/internal/model/audit.go new file mode 100644 index 0000000..2b708d0 --- /dev/null +++ b/auth-server/internal/model/audit.go @@ -0,0 +1,5 @@ +package model + +type AuditReference string + +type AuditAction string diff --git a/auth-server/internal/model/user_fixture.go b/auth-server/internal/model/user_fixture.go new file mode 100644 index 0000000..5bd20f1 --- /dev/null +++ b/auth-server/internal/model/user_fixture.go @@ -0,0 +1,27 @@ +package model + +import ( + "database/sql" + "testing" + + "github.com/brianvoe/gofakeit/v6" +) + +func FixtureUserWithFaker(t *testing.T) User { + t.Helper() + + userModel := User{ + ID: gofakeit.Int64(), + CreatedAt: gofakeit.Date(), + UpdatedAt: sql.NullTime{gofakeit.Date(), true}, + DeletedAt: sql.NullTime{}, + Data: UserData{ + Name: gofakeit.Name(), + Email: gofakeit.Email(), + PasswordHash: gofakeit.HexUint128(), + Role: gofakeit.Int32(), + }, + } + + return userModel +} diff --git a/auth-server/internal/repository/audit/dto/dto.go b/auth-server/internal/repository/audit/dto/dto.go index b3f0c7e..5224352 100644 --- a/auth-server/internal/repository/audit/dto/dto.go +++ b/auth-server/internal/repository/audit/dto/dto.go @@ -2,11 +2,13 @@ package dto import ( "database/sql" + + "github.com/aywan/balun_miserv_s2/auth-server/internal/model" ) type InsertDTO struct { CreatorId sql.NullInt64 - Reference string + Reference model.AuditReference ReferenceID int64 - Action string + Action model.AuditAction } diff --git a/auth-server/internal/repository/mocks/Audit.mock.gen.go b/auth-server/internal/repository/mocks/Audit.mock.gen.go new file mode 100644 index 0000000..4545acc --- /dev/null +++ b/auth-server/internal/repository/mocks/Audit.mock.gen.go @@ -0,0 +1,90 @@ +// Code generated by mockery v2.36.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + dto "github.com/aywan/balun_miserv_s2/auth-server/internal/repository/audit/dto" + mock "github.com/stretchr/testify/mock" +) + +// MockAudit is an autogenerated mock type for the Audit type +type MockAudit struct { + mock.Mock +} + +type MockAudit_Expecter struct { + mock *mock.Mock +} + +func (_m *MockAudit) EXPECT() *MockAudit_Expecter { + return &MockAudit_Expecter{mock: &_m.Mock} +} + +// Insert provides a mock function with given fields: ctx, data +func (_m *MockAudit) Insert(ctx context.Context, data dto.InsertDTO) (int64, error) { + ret := _m.Called(ctx, data) + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, dto.InsertDTO) (int64, error)); ok { + return rf(ctx, data) + } + if rf, ok := ret.Get(0).(func(context.Context, dto.InsertDTO) int64); ok { + r0 = rf(ctx, data) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, dto.InsertDTO) error); ok { + r1 = rf(ctx, data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockAudit_Insert_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Insert' +type MockAudit_Insert_Call struct { + *mock.Call +} + +// Insert is a helper method to define mock.On call +// - ctx context.Context +// - data dto.InsertDTO +func (_e *MockAudit_Expecter) Insert(ctx interface{}, data interface{}) *MockAudit_Insert_Call { + return &MockAudit_Insert_Call{Call: _e.mock.On("Insert", ctx, data)} +} + +func (_c *MockAudit_Insert_Call) Run(run func(ctx context.Context, data dto.InsertDTO)) *MockAudit_Insert_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(dto.InsertDTO)) + }) + return _c +} + +func (_c *MockAudit_Insert_Call) Return(_a0 int64, _a1 error) *MockAudit_Insert_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockAudit_Insert_Call) RunAndReturn(run func(context.Context, dto.InsertDTO) (int64, error)) *MockAudit_Insert_Call { + _c.Call.Return(run) + return _c +} + +// NewMockAudit creates a new instance of MockAudit. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockAudit(t interface { + mock.TestingT + Cleanup(func()) +}) *MockAudit { + mock := &MockAudit{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/auth-server/internal/repository/mocks/User.mock.gen.go b/auth-server/internal/repository/mocks/User.mock.gen.go new file mode 100644 index 0000000..229e339 --- /dev/null +++ b/auth-server/internal/repository/mocks/User.mock.gen.go @@ -0,0 +1,338 @@ +// Code generated by mockery v2.36.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + dto "github.com/aywan/balun_miserv_s2/auth-server/internal/repository/user/dto" + mock "github.com/stretchr/testify/mock" + + model "github.com/aywan/balun_miserv_s2/auth-server/internal/model" +) + +// MockUser is an autogenerated mock type for the User type +type MockUser struct { + mock.Mock +} + +type MockUser_Expecter struct { + mock *mock.Mock +} + +func (_m *MockUser) EXPECT() *MockUser_Expecter { + return &MockUser_Expecter{mock: &_m.Mock} +} + +// Create provides a mock function with given fields: ctx, data +func (_m *MockUser) Create(ctx context.Context, data model.UserData) (int64, error) { + ret := _m.Called(ctx, data) + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, model.UserData) (int64, error)); ok { + return rf(ctx, data) + } + if rf, ok := ret.Get(0).(func(context.Context, model.UserData) int64); ok { + r0 = rf(ctx, data) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, model.UserData) error); ok { + r1 = rf(ctx, data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockUser_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type MockUser_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - ctx context.Context +// - data model.UserData +func (_e *MockUser_Expecter) Create(ctx interface{}, data interface{}) *MockUser_Create_Call { + return &MockUser_Create_Call{Call: _e.mock.On("Create", ctx, data)} +} + +func (_c *MockUser_Create_Call) Run(run func(ctx context.Context, data model.UserData)) *MockUser_Create_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(model.UserData)) + }) + return _c +} + +func (_c *MockUser_Create_Call) Return(_a0 int64, _a1 error) *MockUser_Create_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockUser_Create_Call) RunAndReturn(run func(context.Context, model.UserData) (int64, error)) *MockUser_Create_Call { + _c.Call.Return(run) + return _c +} + +// Delete provides a mock function with given fields: ctx, userId +func (_m *MockUser) Delete(ctx context.Context, userId int64) error { + ret := _m.Called(ctx, userId) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, userId) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockUser_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type MockUser_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - ctx context.Context +// - userId int64 +func (_e *MockUser_Expecter) Delete(ctx interface{}, userId interface{}) *MockUser_Delete_Call { + return &MockUser_Delete_Call{Call: _e.mock.On("Delete", ctx, userId)} +} + +func (_c *MockUser_Delete_Call) Run(run func(ctx context.Context, userId int64)) *MockUser_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *MockUser_Delete_Call) Return(_a0 error) *MockUser_Delete_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockUser_Delete_Call) RunAndReturn(run func(context.Context, int64) error) *MockUser_Delete_Call { + _c.Call.Return(run) + return _c +} + +// ExistsByEmail provides a mock function with given fields: ctx, email +func (_m *MockUser) ExistsByEmail(ctx context.Context, email string) (bool, error) { + ret := _m.Called(ctx, email) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok { + return rf(ctx, email) + } + if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { + r0 = rf(ctx, email) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, email) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockUser_ExistsByEmail_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExistsByEmail' +type MockUser_ExistsByEmail_Call struct { + *mock.Call +} + +// ExistsByEmail is a helper method to define mock.On call +// - ctx context.Context +// - email string +func (_e *MockUser_Expecter) ExistsByEmail(ctx interface{}, email interface{}) *MockUser_ExistsByEmail_Call { + return &MockUser_ExistsByEmail_Call{Call: _e.mock.On("ExistsByEmail", ctx, email)} +} + +func (_c *MockUser_ExistsByEmail_Call) Run(run func(ctx context.Context, email string)) *MockUser_ExistsByEmail_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockUser_ExistsByEmail_Call) Return(_a0 bool, _a1 error) *MockUser_ExistsByEmail_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockUser_ExistsByEmail_Call) RunAndReturn(run func(context.Context, string) (bool, error)) *MockUser_ExistsByEmail_Call { + _c.Call.Return(run) + return _c +} + +// ExistsById provides a mock function with given fields: ctx, userId +func (_m *MockUser) ExistsById(ctx context.Context, userId int64) (bool, error) { + ret := _m.Called(ctx, userId) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (bool, error)); ok { + return rf(ctx, userId) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) bool); ok { + r0 = rf(ctx, userId) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, userId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockUser_ExistsById_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExistsById' +type MockUser_ExistsById_Call struct { + *mock.Call +} + +// ExistsById is a helper method to define mock.On call +// - ctx context.Context +// - userId int64 +func (_e *MockUser_Expecter) ExistsById(ctx interface{}, userId interface{}) *MockUser_ExistsById_Call { + return &MockUser_ExistsById_Call{Call: _e.mock.On("ExistsById", ctx, userId)} +} + +func (_c *MockUser_ExistsById_Call) Run(run func(ctx context.Context, userId int64)) *MockUser_ExistsById_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *MockUser_ExistsById_Call) Return(_a0 bool, _a1 error) *MockUser_ExistsById_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockUser_ExistsById_Call) RunAndReturn(run func(context.Context, int64) (bool, error)) *MockUser_ExistsById_Call { + _c.Call.Return(run) + return _c +} + +// GetNotDeleted provides a mock function with given fields: ctx, userId +func (_m *MockUser) GetNotDeleted(ctx context.Context, userId int64) (model.User, error) { + ret := _m.Called(ctx, userId) + + var r0 model.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (model.User, error)); ok { + return rf(ctx, userId) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) model.User); ok { + r0 = rf(ctx, userId) + } else { + r0 = ret.Get(0).(model.User) + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, userId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockUser_GetNotDeleted_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetNotDeleted' +type MockUser_GetNotDeleted_Call struct { + *mock.Call +} + +// GetNotDeleted is a helper method to define mock.On call +// - ctx context.Context +// - userId int64 +func (_e *MockUser_Expecter) GetNotDeleted(ctx interface{}, userId interface{}) *MockUser_GetNotDeleted_Call { + return &MockUser_GetNotDeleted_Call{Call: _e.mock.On("GetNotDeleted", ctx, userId)} +} + +func (_c *MockUser_GetNotDeleted_Call) Run(run func(ctx context.Context, userId int64)) *MockUser_GetNotDeleted_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *MockUser_GetNotDeleted_Call) Return(_a0 model.User, _a1 error) *MockUser_GetNotDeleted_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockUser_GetNotDeleted_Call) RunAndReturn(run func(context.Context, int64) (model.User, error)) *MockUser_GetNotDeleted_Call { + _c.Call.Return(run) + return _c +} + +// Update provides a mock function with given fields: ctx, userId, data +func (_m *MockUser) Update(ctx context.Context, userId int64, data dto.UpdateDTO) error { + ret := _m.Called(ctx, userId, data) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, dto.UpdateDTO) error); ok { + r0 = rf(ctx, userId, data) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockUser_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' +type MockUser_Update_Call struct { + *mock.Call +} + +// Update is a helper method to define mock.On call +// - ctx context.Context +// - userId int64 +// - data dto.UpdateDTO +func (_e *MockUser_Expecter) Update(ctx interface{}, userId interface{}, data interface{}) *MockUser_Update_Call { + return &MockUser_Update_Call{Call: _e.mock.On("Update", ctx, userId, data)} +} + +func (_c *MockUser_Update_Call) Run(run func(ctx context.Context, userId int64, data dto.UpdateDTO)) *MockUser_Update_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64), args[2].(dto.UpdateDTO)) + }) + return _c +} + +func (_c *MockUser_Update_Call) Return(_a0 error) *MockUser_Update_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockUser_Update_Call) RunAndReturn(run func(context.Context, int64, dto.UpdateDTO) error) *MockUser_Update_Call { + _c.Call.Return(run) + return _c +} + +// NewMockUser creates a new instance of MockUser. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockUser(t interface { + mock.TestingT + Cleanup(func()) +}) *MockUser { + mock := &MockUser{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/auth-server/internal/repository/repository.go b/auth-server/internal/repository/repository.go index 03049bd..119efcc 100644 --- a/auth-server/internal/repository/repository.go +++ b/auth-server/internal/repository/repository.go @@ -4,19 +4,21 @@ import ( "context" "github.com/aywan/balun_miserv_s2/auth-server/internal/model" - dto2 "github.com/aywan/balun_miserv_s2/auth-server/internal/repository/audit/dto" - "github.com/aywan/balun_miserv_s2/auth-server/internal/repository/user/dto" + auditDto "github.com/aywan/balun_miserv_s2/auth-server/internal/repository/audit/dto" + userDto "github.com/aywan/balun_miserv_s2/auth-server/internal/repository/user/dto" ) +//go:generate mockery + type User interface { GetNotDeleted(ctx context.Context, userId int64) (model.User, error) Create(ctx context.Context, data model.UserData) (int64, error) - Update(ctx context.Context, userId int64, data dto.UpdateDTO) error + Update(ctx context.Context, userId int64, data userDto.UpdateDTO) error Delete(ctx context.Context, userId int64) error ExistsById(ctx context.Context, userId int64) (bool, error) ExistsByEmail(ctx context.Context, email string) (bool, error) } type Audit interface { - Insert(ctx context.Context, data dto2.InsertDTO) (int64, error) + Insert(ctx context.Context, data auditDto.InsertDTO) (int64, error) } diff --git a/auth-server/internal/service/mocks/User.mock.gen.go b/auth-server/internal/service/mocks/User.mock.gen.go new file mode 100644 index 0000000..35e83dd --- /dev/null +++ b/auth-server/internal/service/mocks/User.mock.gen.go @@ -0,0 +1,232 @@ +// Code generated by mockery v2.36.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + dto "github.com/aywan/balun_miserv_s2/auth-server/internal/repository/user/dto" + mock "github.com/stretchr/testify/mock" + + model "github.com/aywan/balun_miserv_s2/auth-server/internal/model" +) + +// MockUser is an autogenerated mock type for the User type +type MockUser struct { + mock.Mock +} + +type MockUser_Expecter struct { + mock *mock.Mock +} + +func (_m *MockUser) EXPECT() *MockUser_Expecter { + return &MockUser_Expecter{mock: &_m.Mock} +} + +// Create provides a mock function with given fields: ctx, data +func (_m *MockUser) Create(ctx context.Context, data model.UserData) (int64, error) { + ret := _m.Called(ctx, data) + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, model.UserData) (int64, error)); ok { + return rf(ctx, data) + } + if rf, ok := ret.Get(0).(func(context.Context, model.UserData) int64); ok { + r0 = rf(ctx, data) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, model.UserData) error); ok { + r1 = rf(ctx, data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockUser_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type MockUser_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - ctx context.Context +// - data model.UserData +func (_e *MockUser_Expecter) Create(ctx interface{}, data interface{}) *MockUser_Create_Call { + return &MockUser_Create_Call{Call: _e.mock.On("Create", ctx, data)} +} + +func (_c *MockUser_Create_Call) Run(run func(ctx context.Context, data model.UserData)) *MockUser_Create_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(model.UserData)) + }) + return _c +} + +func (_c *MockUser_Create_Call) Return(_a0 int64, _a1 error) *MockUser_Create_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockUser_Create_Call) RunAndReturn(run func(context.Context, model.UserData) (int64, error)) *MockUser_Create_Call { + _c.Call.Return(run) + return _c +} + +// Delete provides a mock function with given fields: ctx, userId +func (_m *MockUser) Delete(ctx context.Context, userId int64) error { + ret := _m.Called(ctx, userId) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, userId) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockUser_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type MockUser_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - ctx context.Context +// - userId int64 +func (_e *MockUser_Expecter) Delete(ctx interface{}, userId interface{}) *MockUser_Delete_Call { + return &MockUser_Delete_Call{Call: _e.mock.On("Delete", ctx, userId)} +} + +func (_c *MockUser_Delete_Call) Run(run func(ctx context.Context, userId int64)) *MockUser_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *MockUser_Delete_Call) Return(_a0 error) *MockUser_Delete_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockUser_Delete_Call) RunAndReturn(run func(context.Context, int64) error) *MockUser_Delete_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function with given fields: ctx, userId +func (_m *MockUser) Get(ctx context.Context, userId int64) (model.User, error) { + ret := _m.Called(ctx, userId) + + var r0 model.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (model.User, error)); ok { + return rf(ctx, userId) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) model.User); ok { + r0 = rf(ctx, userId) + } else { + r0 = ret.Get(0).(model.User) + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, userId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockUser_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type MockUser_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - userId int64 +func (_e *MockUser_Expecter) Get(ctx interface{}, userId interface{}) *MockUser_Get_Call { + return &MockUser_Get_Call{Call: _e.mock.On("Get", ctx, userId)} +} + +func (_c *MockUser_Get_Call) Run(run func(ctx context.Context, userId int64)) *MockUser_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *MockUser_Get_Call) Return(_a0 model.User, _a1 error) *MockUser_Get_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockUser_Get_Call) RunAndReturn(run func(context.Context, int64) (model.User, error)) *MockUser_Get_Call { + _c.Call.Return(run) + return _c +} + +// Update provides a mock function with given fields: ctx, userId, data +func (_m *MockUser) Update(ctx context.Context, userId int64, data dto.UpdateDTO) error { + ret := _m.Called(ctx, userId, data) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, dto.UpdateDTO) error); ok { + r0 = rf(ctx, userId, data) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockUser_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' +type MockUser_Update_Call struct { + *mock.Call +} + +// Update is a helper method to define mock.On call +// - ctx context.Context +// - userId int64 +// - data dto.UpdateDTO +func (_e *MockUser_Expecter) Update(ctx interface{}, userId interface{}, data interface{}) *MockUser_Update_Call { + return &MockUser_Update_Call{Call: _e.mock.On("Update", ctx, userId, data)} +} + +func (_c *MockUser_Update_Call) Run(run func(ctx context.Context, userId int64, data dto.UpdateDTO)) *MockUser_Update_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64), args[2].(dto.UpdateDTO)) + }) + return _c +} + +func (_c *MockUser_Update_Call) Return(_a0 error) *MockUser_Update_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockUser_Update_Call) RunAndReturn(run func(context.Context, int64, dto.UpdateDTO) error) *MockUser_Update_Call { + _c.Call.Return(run) + return _c +} + +// NewMockUser creates a new instance of MockUser. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockUser(t interface { + mock.TestingT + Cleanup(func()) +}) *MockUser { + mock := &MockUser{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/auth-server/internal/service/user/service.go b/auth-server/internal/service/user/service.go index d507456..1983c74 100644 --- a/auth-server/internal/service/user/service.go +++ b/auth-server/internal/service/user/service.go @@ -7,7 +7,7 @@ import ( "github.com/aywan/balun_miserv_s2/auth-server/internal/model" "github.com/aywan/balun_miserv_s2/auth-server/internal/repository" - dto2 "github.com/aywan/balun_miserv_s2/auth-server/internal/repository/audit/dto" + auditDto "github.com/aywan/balun_miserv_s2/auth-server/internal/repository/audit/dto" "github.com/aywan/balun_miserv_s2/auth-server/internal/repository/user/dto" "github.com/aywan/balun_miserv_s2/auth-server/internal/security" "github.com/aywan/balun_miserv_s2/auth-server/internal/service" @@ -15,7 +15,10 @@ import ( "go.uber.org/zap" ) -const auditUserRef = "user" +const auditUserRef model.AuditReference = "user" +const auditNewUser model.AuditAction = "new user" +const auditUpdateUser model.AuditAction = "update user" +const auditDeleteUser model.AuditAction = "delete user" type Service struct { log *zap.Logger @@ -67,11 +70,11 @@ func (s *Service) Create(ctx context.Context, data model.UserData) (int64, error return err } - _, err = s.auditRepo.Insert(ctx, dto2.InsertDTO{ + _, err = s.auditRepo.Insert(ctx, auditDto.InsertDTO{ CreatorId: sql.NullInt64{}, Reference: auditUserRef, ReferenceID: userId, - Action: "new user", + Action: auditNewUser, }) if err != nil { @@ -100,11 +103,11 @@ func (s *Service) Update(ctx context.Context, userId int64, data dto.UpdateDTO) return err } - _, err = s.auditRepo.Insert(ctx, dto2.InsertDTO{ + _, err = s.auditRepo.Insert(ctx, auditDto.InsertDTO{ CreatorId: sql.NullInt64{}, Reference: auditUserRef, ReferenceID: userId, - Action: "update user", + Action: auditUpdateUser, }) return err @@ -126,11 +129,11 @@ func (s *Service) Delete(ctx context.Context, userId int64) error { return err } - _, err = s.auditRepo.Insert(ctx, dto2.InsertDTO{ + _, err = s.auditRepo.Insert(ctx, auditDto.InsertDTO{ CreatorId: sql.NullInt64{}, Reference: auditUserRef, ReferenceID: userId, - Action: "delete user", + Action: auditDeleteUser, }) return err diff --git a/auth-server/internal/service/user/service_test.go b/auth-server/internal/service/user/service_test.go new file mode 100644 index 0000000..abc535b --- /dev/null +++ b/auth-server/internal/service/user/service_test.go @@ -0,0 +1,153 @@ +package user + +import ( + "context" + "database/sql" + "testing" + + "github.com/aywan/balun_miserv_s2/auth-server/internal/model" + "github.com/aywan/balun_miserv_s2/auth-server/internal/repository/audit/dto" + repoMocks "github.com/aywan/balun_miserv_s2/auth-server/internal/repository/mocks" + userDTO "github.com/aywan/balun_miserv_s2/auth-server/internal/repository/user/dto" + "github.com/aywan/balun_miserv_s2/shared/lib/db" + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +func TestService_Get(t *testing.T) { + t.Parallel() + log := zaptest.NewLogger(t) + ctx := context.Background() + + userModel := model.FixtureUserWithFaker(t) + + userRepoMock := repoMocks.NewMockUser(t) + userRepoMock.EXPECT(). + GetNotDeleted(ctx, userModel.ID). + Return(userModel, nil) + + auditRepoMock := repoMocks.NewMockAudit(t) + + testTxManager := db.NewTestTxManager(t) + service := New(log, userRepoMock, auditRepoMock, testTxManager) + + user, err := service.Get(ctx, userModel.ID) + require.NoError(t, err) + + require.Equal(t, userModel, user) +} + +func TestService_Create(t *testing.T) { + t.Parallel() + log := zaptest.NewLogger(t) + ctx := context.Background() + var err error + + userModel := model.FixtureUserWithFaker(t) + + userRepoMock := repoMocks.NewMockUser(t) + userRepoMock.EXPECT(). + ExistsByEmail(ctx, userModel.Data.Email). + Return(false, nil) + + userRepoMock.EXPECT(). + Create(ctx, mock.Anything). + Return(userModel.ID, nil) + + auditRepoMock := repoMocks.NewMockAudit(t) + auditRepoMock.EXPECT(). + Insert(ctx, mock.Anything). + RunAndReturn(func(ctx context.Context, data dto.InsertDTO) (int64, error) { + require.Equal(t, auditUserRef, data.Reference) + require.Equal(t, userModel.ID, data.ReferenceID) + require.Equal(t, auditNewUser, data.Action) + + return 1, nil + }) + + testTxManager := db.NewTestTxManager(t) + service := New(log, userRepoMock, auditRepoMock, testTxManager) + + actualUserId, err := service.Create(ctx, userModel.Data) + require.NoError(t, err) + require.Equal(t, userModel.ID, actualUserId) +} + +func TestService_Update(t *testing.T) { + t.Parallel() + log := zaptest.NewLogger(t) + ctx := context.Background() + var err error + + userModel := model.FixtureUserWithFaker(t) + + updateDTO := userDTO.UpdateDTO{ + Name: sql.NullString{gofakeit.Name(), true}, + Email: sql.NullString{gofakeit.Email(), true}, + PasswordHash: sql.NullString{gofakeit.HexUint16(), true}, + Role: sql.NullInt32{gofakeit.Int32(), true}, + } + + userRepoMock := repoMocks.NewMockUser(t) + userRepoMock.EXPECT(). + ExistsById(ctx, userModel.ID). + Return(true, nil) + + userRepoMock.EXPECT(). + Update(ctx, userModel.ID, mock.Anything). + Return(nil) + + auditRepoMock := repoMocks.NewMockAudit(t) + auditRepoMock.EXPECT(). + Insert(ctx, mock.Anything). + RunAndReturn(func(ctx context.Context, data dto.InsertDTO) (int64, error) { + require.Equal(t, auditUserRef, data.Reference) + require.Equal(t, userModel.ID, data.ReferenceID) + require.Equal(t, auditUpdateUser, data.Action) + + return 1, nil + }) + + testTxManager := db.NewTestTxManager(t) + service := New(log, userRepoMock, auditRepoMock, testTxManager) + + err = service.Update(ctx, userModel.ID, updateDTO) + require.NoError(t, err) +} + +func TestService_Delete(t *testing.T) { + t.Parallel() + log := zaptest.NewLogger(t) + ctx := context.Background() + var err error + + userModel := model.FixtureUserWithFaker(t) + + userRepoMock := repoMocks.NewMockUser(t) + userRepoMock.EXPECT(). + ExistsById(ctx, userModel.ID). + Return(true, nil) + + userRepoMock.EXPECT(). + Delete(ctx, userModel.ID). + Return(nil) + + auditRepoMock := repoMocks.NewMockAudit(t) + auditRepoMock.EXPECT(). + Insert(ctx, mock.Anything). + RunAndReturn(func(ctx context.Context, data dto.InsertDTO) (int64, error) { + require.Equal(t, auditUserRef, data.Reference) + require.Equal(t, userModel.ID, data.ReferenceID) + require.Equal(t, auditDeleteUser, data.Action) + + return 1, nil + }) + + testTxManager := db.NewTestTxManager(t) + service := New(log, userRepoMock, auditRepoMock, testTxManager) + + err = service.Delete(ctx, userModel.ID) + require.NoError(t, err) +} diff --git a/chat-server/.mockery.yaml b/chat-server/.mockery.yaml new file mode 100644 index 0000000..ad300a1 --- /dev/null +++ b/chat-server/.mockery.yaml @@ -0,0 +1,14 @@ +quiet: False +with-expecter: True +mockname: "Mock{{.InterfaceName}}" +filename: "{{.InterfaceName}}.mock.gen.go" +dir: "{{.InterfaceDir}}/mocks" +outpkg: "mocks" +packages: + github.com/aywan/balun_miserv_s2/chat-server/internal/repository: + interfaces: + Chat: + github.com/aywan/balun_miserv_s2/chat-server/internal/service: + interfaces: + Chat: + diff --git a/chat-server/Makefile b/chat-server/Makefile index 89e1e4b..2690d28 100644 --- a/chat-server/Makefile +++ b/chat-server/Makefile @@ -16,6 +16,11 @@ tests: .PHONY: generate generate: make generate-api-chat-v1 + make mocks + +.PHONY: mocks +mocks: + $(LOCAL_BIN)/mockery .PHONY: generate-api-chat-v1 generate-api-chat-v1: diff --git a/chat-server/go.mod b/chat-server/go.mod index 06ae2a7..5c1e260 100644 --- a/chat-server/go.mod +++ b/chat-server/go.mod @@ -7,6 +7,7 @@ require ( github.com/aywan/balun_miserv_s2/shared/lib/db v0.0.0 github.com/aywan/balun_miserv_s2/shared/lib/logger v0.0.0 github.com/aywan/balun_miserv_s2/shared/lib/runutil v0.0.0 + github.com/brianvoe/gofakeit/v6 v6.24.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/stretchr/testify v1.8.4 go.uber.org/fx v1.20.1 @@ -34,6 +35,7 @@ require ( github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect go.uber.org/dig v1.17.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/crypto v0.11.0 // indirect diff --git a/chat-server/go.sum b/chat-server/go.sum index a71eadf..d8ecbe5 100644 --- a/chat-server/go.sum +++ b/chat-server/go.sum @@ -2,6 +2,8 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8 github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/brianvoe/gofakeit/v6 v6.24.0 h1:74yq7RRz/noddscZHRS2T84oHZisW9muwbb8sRnU52A= +github.com/brianvoe/gofakeit/v6 v6.24.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs= github.com/cockroachdb/cockroach-go/v2 v2.2.0/go.mod h1:u3MiKYGupPPjkn3ozknpMUpxPaNLTFWAya419/zv6eI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -46,11 +48,14 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= diff --git a/chat-server/internal/api/chat/api_test.go b/chat-server/internal/api/chat/api_test.go new file mode 100644 index 0000000..3f2fe80 --- /dev/null +++ b/chat-server/internal/api/chat/api_test.go @@ -0,0 +1,158 @@ +package chat + +import ( + "context" + "database/sql" + "testing" + + "github.com/aywan/balun_miserv_s2/chat-server/internal/model" + "github.com/aywan/balun_miserv_s2/chat-server/internal/service/chat/dto" + "github.com/aywan/balun_miserv_s2/chat-server/internal/service/mocks" + desc "github.com/aywan/balun_miserv_s2/chat-server/pkg/grpc/v1/chat_v1" + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +func TestApi_Create(t *testing.T) { + t.Parallel() + log := zaptest.NewLogger(t) + ctx := context.Background() + var err error + + req := &desc.CreateRequest{ + Users: []int64{1, 2, 3}, + OwnerId: 1, + Name: gofakeit.BeerStyle(), + } + chatId := gofakeit.Int64() + + chatService := mocks.NewMockChat(t) + chatService.EXPECT(). + Create(ctx, dto.NewChatDTO{ + OwnerID: req.OwnerId, + Name: req.Name, + Users: req.Users, + }). + Return(chatId, nil) + + api := New(log, chatService) + + rsp, err := api.Create(ctx, req) + require.NoError(t, err) + require.Equal(t, chatId, rsp.Id) +} + +func TestApi_Delete(t *testing.T) { + t.Parallel() + log := zaptest.NewLogger(t) + ctx := context.Background() + var err error + + req := &desc.ChatIdRequest{ + Id: gofakeit.Int64(), + } + + chatService := mocks.NewMockChat(t) + chatService.EXPECT(). + Delete(ctx, req.Id). + Return(nil) + + api := New(log, chatService) + + _, err = api.Delete(ctx, req) + require.NoError(t, err) +} + +func TestApi_GetMessages(t *testing.T) { + t.Parallel() + log := zaptest.NewLogger(t) + ctx := context.Background() + var err error + + req := &desc.GetMessagesRequest{ + ChatId: gofakeit.Int64(), + Limit: gofakeit.Int64(), + AfterMessageId: gofakeit.Int64(), + BeforeMessageId: gofakeit.Int64(), + } + + messages := dto.MessagesResultDTO{ + Items: model.MessageList{ + { + ID: gofakeit.Int64(), + CreatedAt: gofakeit.Date(), + UserID: sql.NullInt64{}, + MsgType: model.MsgTypeSystem, + Text: gofakeit.BeerStyle(), + }, + { + ID: gofakeit.Int64(), + CreatedAt: gofakeit.Date(), + UserID: sql.NullInt64{gofakeit.Int64(), true}, + MsgType: model.MsgTypeUser, + Text: gofakeit.BeerAlcohol() + " " + gofakeit.BeerIbu(), + }, + }, + HasNext: gofakeit.Bool(), + NextId: gofakeit.Int64(), + } + + chatService := mocks.NewMockChat(t) + chatService.EXPECT(). + GetMessages(ctx, dto.GetMessagesDTO{ + ChatID: req.ChatId, + Limit: req.Limit, + AfterMessageId: req.AfterMessageId, + BeforeMessageId: req.BeforeMessageId, + }). + Return(messages, nil) + + api := New(log, chatService) + + rsp, err := api.GetMessages(ctx, req) + require.NoError(t, err) + + require.Equal(t, messages.HasNext, rsp.HasNext) + require.Equal(t, messages.NextId, rsp.NextId) + + require.Equal(t, messages.Items[0].ID, rsp.Items[0].Id) + require.Equal(t, messages.Items[0].CreatedAt, rsp.Items[0].CreatedAt.AsTime()) + require.Nil(t, rsp.Items[0].UserId) + require.Equal(t, desc.MessageType_SYSTEM, rsp.Items[0].Type) + require.Equal(t, messages.Items[0].Text, rsp.Items[0].Text) + + require.Equal(t, messages.Items[1].ID, rsp.Items[1].Id) + require.Equal(t, messages.Items[1].CreatedAt, rsp.Items[1].CreatedAt.AsTime()) + require.Equal(t, messages.Items[1].UserID.Int64, *rsp.Items[1].UserId) + require.Equal(t, desc.MessageType_USER, rsp.Items[1].Type) + require.Equal(t, messages.Items[1].Text, rsp.Items[1].Text) +} + +func TestApi_SendMessage(t *testing.T) { + t.Parallel() + log := zaptest.NewLogger(t) + ctx := context.Background() + var err error + + req := &desc.SendMessageRequest{ + ChatId: gofakeit.Int64(), + Type: desc.MessageType_USER, + UserId: gofakeit.Int64(), + Text: gofakeit.BeerStyle(), + } + + chatService := mocks.NewMockChat(t) + chatService.EXPECT(). + SendMessage(ctx, dto.SendMessageDTO{ + ChatID: req.ChatId, + UserID: req.UserId, + Text: req.Text, + }). + Return(nil) + + api := New(log, chatService) + + _, err = api.SendMessage(ctx, req) + require.NoError(t, err) +} diff --git a/chat-server/internal/repository/mocks/Chat.mock.gen.go b/chat-server/internal/repository/mocks/Chat.mock.gen.go new file mode 100644 index 0000000..f24b856 --- /dev/null +++ b/chat-server/internal/repository/mocks/Chat.mock.gen.go @@ -0,0 +1,391 @@ +// Code generated by mockery v2.36.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + dto "github.com/aywan/balun_miserv_s2/chat-server/internal/repository/chat/dto" + mock "github.com/stretchr/testify/mock" + + model "github.com/aywan/balun_miserv_s2/chat-server/internal/model" +) + +// MockChat is an autogenerated mock type for the Chat type +type MockChat struct { + mock.Mock +} + +type MockChat_Expecter struct { + mock *mock.Mock +} + +func (_m *MockChat) EXPECT() *MockChat_Expecter { + return &MockChat_Expecter{mock: &_m.Mock} +} + +// AddUsersToChat provides a mock function with given fields: ctx, chatID, users, lastMessageID +func (_m *MockChat) AddUsersToChat(ctx context.Context, chatID int64, users []int64, lastMessageID int64) error { + ret := _m.Called(ctx, chatID, users, lastMessageID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, []int64, int64) error); ok { + r0 = rf(ctx, chatID, users, lastMessageID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockChat_AddUsersToChat_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUsersToChat' +type MockChat_AddUsersToChat_Call struct { + *mock.Call +} + +// AddUsersToChat is a helper method to define mock.On call +// - ctx context.Context +// - chatID int64 +// - users []int64 +// - lastMessageID int64 +func (_e *MockChat_Expecter) AddUsersToChat(ctx interface{}, chatID interface{}, users interface{}, lastMessageID interface{}) *MockChat_AddUsersToChat_Call { + return &MockChat_AddUsersToChat_Call{Call: _e.mock.On("AddUsersToChat", ctx, chatID, users, lastMessageID)} +} + +func (_c *MockChat_AddUsersToChat_Call) Run(run func(ctx context.Context, chatID int64, users []int64, lastMessageID int64)) *MockChat_AddUsersToChat_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64), args[2].([]int64), args[3].(int64)) + }) + return _c +} + +func (_c *MockChat_AddUsersToChat_Call) Return(_a0 error) *MockChat_AddUsersToChat_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockChat_AddUsersToChat_Call) RunAndReturn(run func(context.Context, int64, []int64, int64) error) *MockChat_AddUsersToChat_Call { + _c.Call.Return(run) + return _c +} + +// CreateChat provides a mock function with given fields: ctx, data +func (_m *MockChat) CreateChat(ctx context.Context, data dto.CreateChatDTO) (int64, error) { + ret := _m.Called(ctx, data) + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, dto.CreateChatDTO) (int64, error)); ok { + return rf(ctx, data) + } + if rf, ok := ret.Get(0).(func(context.Context, dto.CreateChatDTO) int64); ok { + r0 = rf(ctx, data) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, dto.CreateChatDTO) error); ok { + r1 = rf(ctx, data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockChat_CreateChat_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateChat' +type MockChat_CreateChat_Call struct { + *mock.Call +} + +// CreateChat is a helper method to define mock.On call +// - ctx context.Context +// - data dto.CreateChatDTO +func (_e *MockChat_Expecter) CreateChat(ctx interface{}, data interface{}) *MockChat_CreateChat_Call { + return &MockChat_CreateChat_Call{Call: _e.mock.On("CreateChat", ctx, data)} +} + +func (_c *MockChat_CreateChat_Call) Run(run func(ctx context.Context, data dto.CreateChatDTO)) *MockChat_CreateChat_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(dto.CreateChatDTO)) + }) + return _c +} + +func (_c *MockChat_CreateChat_Call) Return(_a0 int64, _a1 error) *MockChat_CreateChat_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockChat_CreateChat_Call) RunAndReturn(run func(context.Context, dto.CreateChatDTO) (int64, error)) *MockChat_CreateChat_Call { + _c.Call.Return(run) + return _c +} + +// CreateMessage provides a mock function with given fields: ctx, data +func (_m *MockChat) CreateMessage(ctx context.Context, data dto.CreateMessageDTO) (int64, error) { + ret := _m.Called(ctx, data) + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, dto.CreateMessageDTO) (int64, error)); ok { + return rf(ctx, data) + } + if rf, ok := ret.Get(0).(func(context.Context, dto.CreateMessageDTO) int64); ok { + r0 = rf(ctx, data) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, dto.CreateMessageDTO) error); ok { + r1 = rf(ctx, data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockChat_CreateMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateMessage' +type MockChat_CreateMessage_Call struct { + *mock.Call +} + +// CreateMessage is a helper method to define mock.On call +// - ctx context.Context +// - data dto.CreateMessageDTO +func (_e *MockChat_Expecter) CreateMessage(ctx interface{}, data interface{}) *MockChat_CreateMessage_Call { + return &MockChat_CreateMessage_Call{Call: _e.mock.On("CreateMessage", ctx, data)} +} + +func (_c *MockChat_CreateMessage_Call) Run(run func(ctx context.Context, data dto.CreateMessageDTO)) *MockChat_CreateMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(dto.CreateMessageDTO)) + }) + return _c +} + +func (_c *MockChat_CreateMessage_Call) Return(_a0 int64, _a1 error) *MockChat_CreateMessage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockChat_CreateMessage_Call) RunAndReturn(run func(context.Context, dto.CreateMessageDTO) (int64, error)) *MockChat_CreateMessage_Call { + _c.Call.Return(run) + return _c +} + +// DeleteChat provides a mock function with given fields: ctx, chatID +func (_m *MockChat) DeleteChat(ctx context.Context, chatID int64) error { + ret := _m.Called(ctx, chatID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, chatID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockChat_DeleteChat_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteChat' +type MockChat_DeleteChat_Call struct { + *mock.Call +} + +// DeleteChat is a helper method to define mock.On call +// - ctx context.Context +// - chatID int64 +func (_e *MockChat_Expecter) DeleteChat(ctx interface{}, chatID interface{}) *MockChat_DeleteChat_Call { + return &MockChat_DeleteChat_Call{Call: _e.mock.On("DeleteChat", ctx, chatID)} +} + +func (_c *MockChat_DeleteChat_Call) Run(run func(ctx context.Context, chatID int64)) *MockChat_DeleteChat_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *MockChat_DeleteChat_Call) Return(_a0 error) *MockChat_DeleteChat_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockChat_DeleteChat_Call) RunAndReturn(run func(context.Context, int64) error) *MockChat_DeleteChat_Call { + _c.Call.Return(run) + return _c +} + +// GetMessageAfter provides a mock function with given fields: ctx, chatId, messageId, limit +func (_m *MockChat) GetMessageAfter(ctx context.Context, chatId int64, messageId int64, limit uint64) (model.MessageList, error) { + ret := _m.Called(ctx, chatId, messageId, limit) + + var r0 model.MessageList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, int64, uint64) (model.MessageList, error)); ok { + return rf(ctx, chatId, messageId, limit) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, int64, uint64) model.MessageList); ok { + r0 = rf(ctx, chatId, messageId, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(model.MessageList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, int64, uint64) error); ok { + r1 = rf(ctx, chatId, messageId, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockChat_GetMessageAfter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetMessageAfter' +type MockChat_GetMessageAfter_Call struct { + *mock.Call +} + +// GetMessageAfter is a helper method to define mock.On call +// - ctx context.Context +// - chatId int64 +// - messageId int64 +// - limit uint64 +func (_e *MockChat_Expecter) GetMessageAfter(ctx interface{}, chatId interface{}, messageId interface{}, limit interface{}) *MockChat_GetMessageAfter_Call { + return &MockChat_GetMessageAfter_Call{Call: _e.mock.On("GetMessageAfter", ctx, chatId, messageId, limit)} +} + +func (_c *MockChat_GetMessageAfter_Call) Run(run func(ctx context.Context, chatId int64, messageId int64, limit uint64)) *MockChat_GetMessageAfter_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64), args[2].(int64), args[3].(uint64)) + }) + return _c +} + +func (_c *MockChat_GetMessageAfter_Call) Return(_a0 model.MessageList, _a1 error) *MockChat_GetMessageAfter_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockChat_GetMessageAfter_Call) RunAndReturn(run func(context.Context, int64, int64, uint64) (model.MessageList, error)) *MockChat_GetMessageAfter_Call { + _c.Call.Return(run) + return _c +} + +// GetMessageBefore provides a mock function with given fields: ctx, chatId, messageId, limit +func (_m *MockChat) GetMessageBefore(ctx context.Context, chatId int64, messageId int64, limit uint64) (model.MessageList, error) { + ret := _m.Called(ctx, chatId, messageId, limit) + + var r0 model.MessageList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, int64, uint64) (model.MessageList, error)); ok { + return rf(ctx, chatId, messageId, limit) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, int64, uint64) model.MessageList); ok { + r0 = rf(ctx, chatId, messageId, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(model.MessageList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, int64, uint64) error); ok { + r1 = rf(ctx, chatId, messageId, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockChat_GetMessageBefore_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetMessageBefore' +type MockChat_GetMessageBefore_Call struct { + *mock.Call +} + +// GetMessageBefore is a helper method to define mock.On call +// - ctx context.Context +// - chatId int64 +// - messageId int64 +// - limit uint64 +func (_e *MockChat_Expecter) GetMessageBefore(ctx interface{}, chatId interface{}, messageId interface{}, limit interface{}) *MockChat_GetMessageBefore_Call { + return &MockChat_GetMessageBefore_Call{Call: _e.mock.On("GetMessageBefore", ctx, chatId, messageId, limit)} +} + +func (_c *MockChat_GetMessageBefore_Call) Run(run func(ctx context.Context, chatId int64, messageId int64, limit uint64)) *MockChat_GetMessageBefore_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64), args[2].(int64), args[3].(uint64)) + }) + return _c +} + +func (_c *MockChat_GetMessageBefore_Call) Return(_a0 model.MessageList, _a1 error) *MockChat_GetMessageBefore_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockChat_GetMessageBefore_Call) RunAndReturn(run func(context.Context, int64, int64, uint64) (model.MessageList, error)) *MockChat_GetMessageBefore_Call { + _c.Call.Return(run) + return _c +} + +// UpdateChatLastMessage provides a mock function with given fields: ctx, chatID, lastMessageID +func (_m *MockChat) UpdateChatLastMessage(ctx context.Context, chatID int64, lastMessageID int64) error { + ret := _m.Called(ctx, chatID, lastMessageID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok { + r0 = rf(ctx, chatID, lastMessageID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockChat_UpdateChatLastMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateChatLastMessage' +type MockChat_UpdateChatLastMessage_Call struct { + *mock.Call +} + +// UpdateChatLastMessage is a helper method to define mock.On call +// - ctx context.Context +// - chatID int64 +// - lastMessageID int64 +func (_e *MockChat_Expecter) UpdateChatLastMessage(ctx interface{}, chatID interface{}, lastMessageID interface{}) *MockChat_UpdateChatLastMessage_Call { + return &MockChat_UpdateChatLastMessage_Call{Call: _e.mock.On("UpdateChatLastMessage", ctx, chatID, lastMessageID)} +} + +func (_c *MockChat_UpdateChatLastMessage_Call) Run(run func(ctx context.Context, chatID int64, lastMessageID int64)) *MockChat_UpdateChatLastMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64), args[2].(int64)) + }) + return _c +} + +func (_c *MockChat_UpdateChatLastMessage_Call) Return(_a0 error) *MockChat_UpdateChatLastMessage_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockChat_UpdateChatLastMessage_Call) RunAndReturn(run func(context.Context, int64, int64) error) *MockChat_UpdateChatLastMessage_Call { + _c.Call.Return(run) + return _c +} + +// NewMockChat creates a new instance of MockChat. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockChat(t interface { + mock.TestingT + Cleanup(func()) +}) *MockChat { + mock := &MockChat{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/chat-server/internal/service/chat/chat_test.go b/chat-server/internal/service/chat/chat_test.go new file mode 100644 index 0000000..54aceb3 --- /dev/null +++ b/chat-server/internal/service/chat/chat_test.go @@ -0,0 +1,234 @@ +package chat + +import ( + "context" + "database/sql" + "testing" + "time" + + "github.com/aywan/balun_miserv_s2/chat-server/internal/model" + repoDto "github.com/aywan/balun_miserv_s2/chat-server/internal/repository/chat/dto" + "github.com/aywan/balun_miserv_s2/chat-server/internal/repository/mocks" + "github.com/aywan/balun_miserv_s2/chat-server/internal/service/chat/dto" + "github.com/aywan/balun_miserv_s2/shared/lib/db" + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +func TestService_Create(t *testing.T) { + t.Parallel() + log := zaptest.NewLogger(t) + ctx := context.Background() + var err error + + newChatDTO := dto.NewChatDTO{ + OwnerID: gofakeit.Int64(), + Name: gofakeit.BeerStyle(), + Users: []int64{gofakeit.Int64(), gofakeit.Int64()}, + } + chatId := gofakeit.Int64() + msgId := gofakeit.Int64() + + chatRepoMock := mocks.NewMockChat(t) + chatRepoMock.EXPECT(). + CreateChat(ctx, repoDto.CreateChatDTO{ + OwnerID: newChatDTO.OwnerID, + Name: newChatDTO.Name, + }). + Return(chatId, nil) + + chatRepoMock.EXPECT(). + CreateMessage(ctx, repoDto.CreateMessageDTO{ + ChatId: chatId, + UserId: sql.NullInt64{}, + Text: "start new chat", + Type: model.MsgTypeSystem, + }). + Return(msgId, nil) + + chatRepoMock.EXPECT(). + UpdateChatLastMessage(ctx, chatId, msgId). + Return(nil) + + chatRepoMock.EXPECT(). + AddUsersToChat(ctx, chatId, newChatDTO.Users, msgId). + Return(nil) + + txManager := db.NewTestTxManager(t) + + service := New(log, txManager, chatRepoMock) + + actualChatId, err := service.Create(ctx, newChatDTO) + require.NoError(t, err) + require.Equal(t, chatId, actualChatId) +} + +func TestService_Delete(t *testing.T) { + t.Parallel() + log := zaptest.NewLogger(t) + ctx := context.Background() + var err error + + chatId := gofakeit.Int64() + + chatRepoMock := mocks.NewMockChat(t) + chatRepoMock.EXPECT(). + DeleteChat(ctx, chatId). + Return(nil) + + txManager := db.NewTestTxManager(t) + + service := New(log, txManager, chatRepoMock) + + err = service.Delete(ctx, chatId) + require.NoError(t, err) +} + +func TestService_SendMessage(t *testing.T) { + t.Parallel() + log := zaptest.NewLogger(t) + ctx := context.Background() + var err error + + msgDto := dto.SendMessageDTO{ + ChatID: gofakeit.Int64(), + UserID: gofakeit.Int64(), + Text: gofakeit.BeerName(), + } + + msgId := gofakeit.Int64() + + chatRepoMock := mocks.NewMockChat(t) + chatRepoMock.EXPECT(). + CreateMessage(ctx, repoDto.CreateMessageDTO{ + ChatId: msgDto.ChatID, + UserId: sql.NullInt64{msgDto.UserID, true}, + Text: msgDto.Text, + Type: model.MsgTypeUser, + }). + Return(msgId, nil) + + chatRepoMock.EXPECT(). + UpdateChatLastMessage(ctx, msgDto.ChatID, msgId). + Return(nil) + + txManager := db.NewTestTxManager(t) + + service := New(log, txManager, chatRepoMock) + + err = service.SendMessage(ctx, msgDto) + require.NoError(t, err) +} + +func TestService_GetMessages(t *testing.T) { + t.Parallel() + log := zaptest.NewLogger(t) + ctx := context.Background() + + messages := model.MessageList{ + { + ID: 1, + CreatedAt: time.Time{}, + UserID: sql.NullInt64{}, + MsgType: model.MsgTypeSystem, + Text: "System", + }, + { + ID: 2, + CreatedAt: time.Time{}, + UserID: sql.NullInt64{22, true}, + MsgType: model.MsgTypeUser, + Text: "User", + }, + } + + chatRepoMock := mocks.NewMockChat(t) + chatRepoMock.EXPECT(). + GetMessageBefore(ctx, mock.Anything, mock.Anything, mock.Anything). + Return(messages, nil). + Maybe() + chatRepoMock.EXPECT(). + GetMessageAfter(ctx, mock.Anything, mock.Anything, mock.Anything). + Return(messages, nil). + Maybe() + txManager := db.NewTestTxManager(t) + service := New(log, txManager, chatRepoMock) + + cases := []struct { + Name string + Req dto.GetMessagesDTO + ExpectIds []int64 + ExpectHasNext bool + ExpectNextId int64 + ExpectedErr error + }{ + { + Name: "after limit 2", + Req: dto.GetMessagesDTO{ + Limit: 2, + AfterMessageId: 1, + BeforeMessageId: 0, + }, + ExpectIds: []int64{1, 2}, + ExpectHasNext: false, + ExpectNextId: 0, + }, + { + Name: "after limit 1", + Req: dto.GetMessagesDTO{ + Limit: 1, + AfterMessageId: 1, + BeforeMessageId: 0, + }, + ExpectIds: []int64{1}, + ExpectHasNext: true, + ExpectNextId: 2, + }, + { + Name: "before limit 2", + Req: dto.GetMessagesDTO{ + Limit: 2, + AfterMessageId: 0, + BeforeMessageId: 2, + }, + ExpectIds: []int64{1, 2}, + ExpectHasNext: false, + ExpectNextId: 0, + }, + { + Name: "before limit 1", + Req: dto.GetMessagesDTO{ + Limit: 1, + AfterMessageId: 0, + BeforeMessageId: 2, + }, + ExpectIds: []int64{2}, + ExpectHasNext: true, + ExpectNextId: 1, + }, + } + + for _, c := range cases { + cc := c + t.Run(cc.Name, func(t *testing.T) { + t.Parallel() + + rsp, errActual := service.GetMessages(ctx, cc.Req) + if cc.ExpectedErr != nil { + require.Error(t, errActual) + require.ErrorIs(t, errActual, cc.ExpectedErr) + return + } + + actualIds := make([]int64, 0, len(rsp.Items)) + for _, item := range rsp.Items { + actualIds = append(actualIds, item.ID) + } + require.Equal(t, cc.ExpectIds, actualIds) + require.Equal(t, cc.ExpectHasNext, rsp.HasNext) + require.Equal(t, cc.ExpectNextId, rsp.NextId) + }) + } +} diff --git a/chat-server/internal/service/mocks/Chat.mock.gen.go b/chat-server/internal/service/mocks/Chat.mock.gen.go new file mode 100644 index 0000000..c9f14b9 --- /dev/null +++ b/chat-server/internal/service/mocks/Chat.mock.gen.go @@ -0,0 +1,229 @@ +// Code generated by mockery v2.36.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + dto "github.com/aywan/balun_miserv_s2/chat-server/internal/service/chat/dto" + mock "github.com/stretchr/testify/mock" +) + +// MockChat is an autogenerated mock type for the Chat type +type MockChat struct { + mock.Mock +} + +type MockChat_Expecter struct { + mock *mock.Mock +} + +func (_m *MockChat) EXPECT() *MockChat_Expecter { + return &MockChat_Expecter{mock: &_m.Mock} +} + +// Create provides a mock function with given fields: ctx, _a1 +func (_m *MockChat) Create(ctx context.Context, _a1 dto.NewChatDTO) (int64, error) { + ret := _m.Called(ctx, _a1) + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, dto.NewChatDTO) (int64, error)); ok { + return rf(ctx, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, dto.NewChatDTO) int64); ok { + r0 = rf(ctx, _a1) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, dto.NewChatDTO) error); ok { + r1 = rf(ctx, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockChat_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type MockChat_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - ctx context.Context +// - _a1 dto.NewChatDTO +func (_e *MockChat_Expecter) Create(ctx interface{}, _a1 interface{}) *MockChat_Create_Call { + return &MockChat_Create_Call{Call: _e.mock.On("Create", ctx, _a1)} +} + +func (_c *MockChat_Create_Call) Run(run func(ctx context.Context, _a1 dto.NewChatDTO)) *MockChat_Create_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(dto.NewChatDTO)) + }) + return _c +} + +func (_c *MockChat_Create_Call) Return(_a0 int64, _a1 error) *MockChat_Create_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockChat_Create_Call) RunAndReturn(run func(context.Context, dto.NewChatDTO) (int64, error)) *MockChat_Create_Call { + _c.Call.Return(run) + return _c +} + +// Delete provides a mock function with given fields: ctx, id +func (_m *MockChat) Delete(ctx context.Context, id int64) error { + ret := _m.Called(ctx, id) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockChat_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type MockChat_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - ctx context.Context +// - id int64 +func (_e *MockChat_Expecter) Delete(ctx interface{}, id interface{}) *MockChat_Delete_Call { + return &MockChat_Delete_Call{Call: _e.mock.On("Delete", ctx, id)} +} + +func (_c *MockChat_Delete_Call) Run(run func(ctx context.Context, id int64)) *MockChat_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *MockChat_Delete_Call) Return(_a0 error) *MockChat_Delete_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockChat_Delete_Call) RunAndReturn(run func(context.Context, int64) error) *MockChat_Delete_Call { + _c.Call.Return(run) + return _c +} + +// GetMessages provides a mock function with given fields: ctx, req +func (_m *MockChat) GetMessages(ctx context.Context, req dto.GetMessagesDTO) (dto.MessagesResultDTO, error) { + ret := _m.Called(ctx, req) + + var r0 dto.MessagesResultDTO + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, dto.GetMessagesDTO) (dto.MessagesResultDTO, error)); ok { + return rf(ctx, req) + } + if rf, ok := ret.Get(0).(func(context.Context, dto.GetMessagesDTO) dto.MessagesResultDTO); ok { + r0 = rf(ctx, req) + } else { + r0 = ret.Get(0).(dto.MessagesResultDTO) + } + + if rf, ok := ret.Get(1).(func(context.Context, dto.GetMessagesDTO) error); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockChat_GetMessages_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetMessages' +type MockChat_GetMessages_Call struct { + *mock.Call +} + +// GetMessages is a helper method to define mock.On call +// - ctx context.Context +// - req dto.GetMessagesDTO +func (_e *MockChat_Expecter) GetMessages(ctx interface{}, req interface{}) *MockChat_GetMessages_Call { + return &MockChat_GetMessages_Call{Call: _e.mock.On("GetMessages", ctx, req)} +} + +func (_c *MockChat_GetMessages_Call) Run(run func(ctx context.Context, req dto.GetMessagesDTO)) *MockChat_GetMessages_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(dto.GetMessagesDTO)) + }) + return _c +} + +func (_c *MockChat_GetMessages_Call) Return(_a0 dto.MessagesResultDTO, _a1 error) *MockChat_GetMessages_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockChat_GetMessages_Call) RunAndReturn(run func(context.Context, dto.GetMessagesDTO) (dto.MessagesResultDTO, error)) *MockChat_GetMessages_Call { + _c.Call.Return(run) + return _c +} + +// SendMessage provides a mock function with given fields: ctx, data +func (_m *MockChat) SendMessage(ctx context.Context, data dto.SendMessageDTO) error { + ret := _m.Called(ctx, data) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, dto.SendMessageDTO) error); ok { + r0 = rf(ctx, data) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockChat_SendMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMessage' +type MockChat_SendMessage_Call struct { + *mock.Call +} + +// SendMessage is a helper method to define mock.On call +// - ctx context.Context +// - data dto.SendMessageDTO +func (_e *MockChat_Expecter) SendMessage(ctx interface{}, data interface{}) *MockChat_SendMessage_Call { + return &MockChat_SendMessage_Call{Call: _e.mock.On("SendMessage", ctx, data)} +} + +func (_c *MockChat_SendMessage_Call) Run(run func(ctx context.Context, data dto.SendMessageDTO)) *MockChat_SendMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(dto.SendMessageDTO)) + }) + return _c +} + +func (_c *MockChat_SendMessage_Call) Return(_a0 error) *MockChat_SendMessage_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockChat_SendMessage_Call) RunAndReturn(run func(context.Context, dto.SendMessageDTO) error) *MockChat_SendMessage_Call { + _c.Call.Return(run) + return _c +} + +// NewMockChat creates a new instance of MockChat. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockChat(t interface { + mock.TestingT + Cleanup(func()) +}) *MockChat { + mock := &MockChat{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/shared/lib/db/tx_manager.go b/shared/lib/db/tx_manager.go new file mode 100644 index 0000000..2e5c441 --- /dev/null +++ b/shared/lib/db/tx_manager.go @@ -0,0 +1,34 @@ +package db + +import ( + "context" + "testing" +) + +const ( + testingTxKey key = "testing_tx" +) + +type TestTxManager struct { + txLevel int +} + +func (t TestTxManager) ReadCommitted(ctx context.Context, f TxHandler) error { + t.txLevel++ + err := f(ctx) + t.txLevel-- + + return err +} + +func NewTestTxManager(t *testing.T) *TestTxManager { + m := &TestTxManager{} + + t.Cleanup(func() { + if m.txLevel != 0 { + t.Errorf("not closed transaction") + } + }) + + return m +}