diff --git a/.mockery.yaml b/.mockery.yaml new file mode 100644 index 0000000..2340717 --- /dev/null +++ b/.mockery.yaml @@ -0,0 +1,3 @@ +inpackage: True +with-expecter: True +testonly: True \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index e69de29..2f87a42 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,17 @@ +## Mocking interfaces in unit tests +1- add a //go:generate directive above the interface: +```go +//go:generate mockery --name Repository +type Repository interface { + AdminExistByPhoneNumber(ctx context.Context, phoneNumber string) (bool, error) + AdminExistByEmail(ctx context.Context, email string) (bool, error) +} +``` +2- run go generate to create the mock files: +```bash +go generate ./... +``` +3- use the generated mock types in your tests. + +for more information visit: +https://vektra.github.io/mockery/latest/ \ No newline at end of file diff --git a/go.mod b/go.mod index e455427..123543c 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/labstack/echo/v4 v4.12.0 github.com/redis/go-redis/v9 v9.4.0 github.com/rubenv/sql-migrate v1.6.0 + github.com/stretchr/testify v1.9.0 github.com/swaggo/echo-swagger v1.4.1 github.com/swaggo/swag v1.16.3 golang.org/x/crypto v0.23.0 @@ -23,6 +24,7 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -42,6 +44,8 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect diff --git a/go.sum b/go.sum index 587aeb7..1736058 100644 --- a/go.sum +++ b/go.sum @@ -292,6 +292,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= diff --git a/validator/admin/admin/login_test.go b/validator/admin/admin/login_test.go new file mode 100644 index 0000000..b488023 --- /dev/null +++ b/validator/admin/admin/login_test.go @@ -0,0 +1,115 @@ +package adminvalidator + +import ( + "context" + "errors" + adminserviceparam "git.gocasts.ir/ebhomengo/niki/param/admin/admin" + errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidateLoginWithPhoneNumberRequest(t *testing.T) { + mockRepo := NewMockRepository(t) + validator := New(mockRepo) + ctx := context.Background() + validPhoneNumber := "09123456789" + validPassword := "validpassword" + + t.Run("Valid request", func(t *testing.T) { + + req := adminserviceparam.LoginWithPhoneNumberRequest{ + PhoneNumber: validPhoneNumber, + Password: validPassword, + } + + mockRepo.EXPECT().AdminExistByPhoneNumber(ctx, validPhoneNumber).Return(true, nil).Once() + + fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req) + assert.NoError(t, err) + assert.Nil(t, fieldErrors) + }) + + t.Run("Empty phone number", func(t *testing.T) { + + req := adminserviceparam.LoginWithPhoneNumberRequest{ + PhoneNumber: "", + Password: validPassword, + } + + fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req) + assert.Error(t, err) + assert.NotNil(t, fieldErrors) + assert.Contains(t, fieldErrors, "phone_number") + }) + + t.Run("Empty password", func(t *testing.T) { + + req := adminserviceparam.LoginWithPhoneNumberRequest{ + PhoneNumber: validPhoneNumber, + Password: "", + } + + mockRepo.EXPECT().AdminExistByPhoneNumber(ctx, validPhoneNumber).Return(true, nil).Once() + + fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req) + assert.Error(t, err) + assert.NotNil(t, fieldErrors) + assert.Contains(t, fieldErrors, "password") + }) + + t.Run("Invalid phone number format", func(t *testing.T) { + req := adminserviceparam.LoginWithPhoneNumberRequest{ + PhoneNumber: "12345", + Password: validPassword, + } + + fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req) + assert.Error(t, err) + assert.NotNil(t, fieldErrors) + assert.Equal(t, errmsg.ErrorMsgPhoneNumberIsNotValid, fieldErrors["phone_number"]) + }) + + t.Run("Invalid password length", func(t *testing.T) { + req := adminserviceparam.LoginWithPhoneNumberRequest{ + PhoneNumber: validPhoneNumber, + Password: "short", + } + + mockRepo.EXPECT().AdminExistByPhoneNumber(ctx, validPhoneNumber).Return(true, nil).Once() + + fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req) + assert.Error(t, err) + assert.NotNil(t, fieldErrors) + assert.Contains(t, fieldErrors, "password") + }) + + t.Run("Phone number does not exist", func(t *testing.T) { + req := adminserviceparam.LoginWithPhoneNumberRequest{ + PhoneNumber: validPhoneNumber, + Password: validPassword, + } + + mockRepo.EXPECT().AdminExistByPhoneNumber(ctx, validPhoneNumber).Return(false, nil).Once() + + fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req) + assert.Error(t, err) + assert.NotNil(t, fieldErrors) + assert.Equal(t, errmsg.ErrorMsgPhoneNumberOrPassIsIncorrect, fieldErrors["phone_number"]) + }) + + t.Run("Repository error", func(t *testing.T) { + req := adminserviceparam.LoginWithPhoneNumberRequest{ + PhoneNumber: validPhoneNumber, + Password: validPassword, + } + + mockRepo.EXPECT().AdminExistByPhoneNumber(ctx, validPhoneNumber).Return(false, errors.New("repo error")).Once() + + fieldErrors, err := validator.ValidateLoginWithPhoneNumberRequest(ctx, req) + assert.Error(t, err) + assert.NotNil(t, fieldErrors) + assert.Equal(t, errmsg.ErrorMsgSomethingWentWrong, fieldErrors["phone_number"]) + }) +} diff --git a/validator/admin/admin/mock_Repository_test.go b/validator/admin/admin/mock_Repository_test.go new file mode 100644 index 0000000..d400c45 --- /dev/null +++ b/validator/admin/admin/mock_Repository_test.go @@ -0,0 +1,150 @@ +// Code generated by mockery v2.41.0. DO NOT EDIT. + +package adminvalidator + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// MockRepository is an autogenerated mock type for the Repository type +type MockRepository struct { + mock.Mock +} + +type MockRepository_Expecter struct { + mock *mock.Mock +} + +func (_m *MockRepository) EXPECT() *MockRepository_Expecter { + return &MockRepository_Expecter{mock: &_m.Mock} +} + +// AdminExistByEmail provides a mock function with given fields: ctx, email +func (_m *MockRepository) AdminExistByEmail(ctx context.Context, email string) (bool, error) { + ret := _m.Called(ctx, email) + + if len(ret) == 0 { + panic("no return value specified for AdminExistByEmail") + } + + 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 +} + +// MockRepository_AdminExistByEmail_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AdminExistByEmail' +type MockRepository_AdminExistByEmail_Call struct { + *mock.Call +} + +// AdminExistByEmail is a helper method to define mock.On call +// - ctx context.Context +// - email string +func (_e *MockRepository_Expecter) AdminExistByEmail(ctx interface{}, email interface{}) *MockRepository_AdminExistByEmail_Call { + return &MockRepository_AdminExistByEmail_Call{Call: _e.mock.On("AdminExistByEmail", ctx, email)} +} + +func (_c *MockRepository_AdminExistByEmail_Call) Run(run func(ctx context.Context, email string)) *MockRepository_AdminExistByEmail_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockRepository_AdminExistByEmail_Call) Return(_a0 bool, _a1 error) *MockRepository_AdminExistByEmail_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRepository_AdminExistByEmail_Call) RunAndReturn(run func(context.Context, string) (bool, error)) *MockRepository_AdminExistByEmail_Call { + _c.Call.Return(run) + return _c +} + +// AdminExistByPhoneNumber provides a mock function with given fields: ctx, phoneNumber +func (_m *MockRepository) AdminExistByPhoneNumber(ctx context.Context, phoneNumber string) (bool, error) { + ret := _m.Called(ctx, phoneNumber) + + if len(ret) == 0 { + panic("no return value specified for AdminExistByPhoneNumber") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok { + return rf(ctx, phoneNumber) + } + if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { + r0 = rf(ctx, phoneNumber) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, phoneNumber) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRepository_AdminExistByPhoneNumber_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AdminExistByPhoneNumber' +type MockRepository_AdminExistByPhoneNumber_Call struct { + *mock.Call +} + +// AdminExistByPhoneNumber is a helper method to define mock.On call +// - ctx context.Context +// - phoneNumber string +func (_e *MockRepository_Expecter) AdminExistByPhoneNumber(ctx interface{}, phoneNumber interface{}) *MockRepository_AdminExistByPhoneNumber_Call { + return &MockRepository_AdminExistByPhoneNumber_Call{Call: _e.mock.On("AdminExistByPhoneNumber", ctx, phoneNumber)} +} + +func (_c *MockRepository_AdminExistByPhoneNumber_Call) Run(run func(ctx context.Context, phoneNumber string)) *MockRepository_AdminExistByPhoneNumber_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockRepository_AdminExistByPhoneNumber_Call) Return(_a0 bool, _a1 error) *MockRepository_AdminExistByPhoneNumber_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRepository_AdminExistByPhoneNumber_Call) RunAndReturn(run func(context.Context, string) (bool, error)) *MockRepository_AdminExistByPhoneNumber_Call { + _c.Call.Return(run) + return _c +} + +// NewMockRepository creates a new instance of MockRepository. 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 NewMockRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *MockRepository { + mock := &MockRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/validator/admin/admin/validator.go b/validator/admin/admin/validator.go index eafd076..e583bc0 100644 --- a/validator/admin/admin/validator.go +++ b/validator/admin/admin/validator.go @@ -19,10 +19,12 @@ const ( maxLengthPassword = 32 ) +//go:generate mockery --name Repository type Repository interface { AdminExistByPhoneNumber(ctx context.Context, phoneNumber string) (bool, error) AdminExistByEmail(ctx context.Context, email string) (bool, error) } + type Validator struct { repo Repository }