diff --git a/Makefile b/Makefile index f9cc1ac..55f5bf1 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,7 @@ server: mock: mockgen -package mockdb -destination db/mock/store.go github.com/raphaeldiscky/simple-bank/db/sqlc Store + mockgen -package mockwk -destination worker/mock/distributor.go github.com/raphaeldiscky/simple-bank/worker TaskDistributor proto: rm -f pb/*.go diff --git a/gapi/main_test.go b/gapi/main_test.go new file mode 100644 index 0000000..8908aed --- /dev/null +++ b/gapi/main_test.go @@ -0,0 +1,23 @@ +package gapi + +import ( + "testing" + "time" + + db "github.com/raphaeldiscky/simple-bank/db/sqlc" + "github.com/raphaeldiscky/simple-bank/utils" + "github.com/raphaeldiscky/simple-bank/worker" + "github.com/stretchr/testify/require" +) + +func NewTestServer(t *testing.T, store db.Store, taskDistributor worker.TaskDistributor) *Server { + config := utils.Config{ + TokenSymmetricKey: utils.RandomString(32), + AccessTokenDuration: time.Minute, + } + + server, err := NewServer(config, store, taskDistributor) + require.NoError(t, err) + + return server +} diff --git a/gapi/rpc_create_user_test.go b/gapi/rpc_create_user_test.go new file mode 100644 index 0000000..36ae736 --- /dev/null +++ b/gapi/rpc_create_user_test.go @@ -0,0 +1,164 @@ +package gapi + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "testing" + + mockdb "github.com/raphaeldiscky/simple-bank/db/mock" + db "github.com/raphaeldiscky/simple-bank/db/sqlc" + "github.com/raphaeldiscky/simple-bank/pb" + "github.com/raphaeldiscky/simple-bank/utils" + "github.com/raphaeldiscky/simple-bank/worker" + mockwk "github.com/raphaeldiscky/simple-bank/worker/mock" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type eqCreateUserTxParamsMatcher struct { + arg db.CreateUserTxParams + password string + user db.User +} + +func (expected eqCreateUserTxParamsMatcher) Matches(x interface{}) bool { + actualArg, ok := x.(db.CreateUserTxParams) + if !ok { + return false + } + + err := utils.CheckPassword(expected.password, actualArg.HashedPassword) + if err != nil { + return false + } + + expected.arg.HashedPassword = actualArg.HashedPassword + if !reflect.DeepEqual(expected.arg.CreateUserParams, actualArg.CreateUserParams) { + return false + } + + err = actualArg.AfterCreate(expected.user) + return err == nil +} + +func (e eqCreateUserTxParamsMatcher) String() string { + return fmt.Sprintf("matches arg %v and password %v", e.arg, e.password) +} + +func EqCreateUserTxParams(arg db.CreateUserTxParams, password string, user db.User) gomock.Matcher { + return eqCreateUserTxParamsMatcher{arg, password, user} +} + +func randomUser(t *testing.T) (user db.User, password string) { + password = utils.RandomString(6) + hashedPassword, err := utils.HashPassword(password) + require.NoError(t, err) + + user = db.User{ + Username: utils.RandomOwner(), + HashedPassword: hashedPassword, + FullName: utils.RandomOwner(), + Email: utils.RandomEmail(), + } + return +} + +func TestCreateUserAPI(t *testing.T) { + user, password := randomUser(t) + + testCases := []struct { + name string + req *pb.CreateUserRequest + buildStubs func(store *mockdb.MockStore, taskDistributor *mockwk.MockTaskDistributor) + checkResponse func(t *testing.T, res *pb.CreateUserResponse, err error) + }{ + { + name: "OK", + req: &pb.CreateUserRequest{ + Username: user.Username, + Password: password, + FullName: user.FullName, + Email: user.Email, + }, + buildStubs: func(store *mockdb.MockStore, taskDistributor *mockwk.MockTaskDistributor) { + arg := db.CreateUserTxParams{ + CreateUserParams: db.CreateUserParams{ + Username: user.Username, + FullName: user.FullName, + Email: user.Email, + }, + } + store.EXPECT(). + CreateUserTx(gomock.Any(), EqCreateUserTxParams(arg, password, user)). + Times(1). + Return(db.CreateUserTxResult{User: user}, nil) + + taskPayload := &worker.PayloadSendVerifyEmail{ + Username: user.Username, + } + + taskDistributor.EXPECT(). + DistributeTaskSendVerifyEmail(gomock.Any(), taskPayload, gomock.Any()). + Times(1). + Return(nil) + }, + checkResponse: func(t *testing.T, res *pb.CreateUserResponse, err error) { + require.NoError(t, err) + require.NotNil(t, res) + createdUser := res.GetUser() + require.Equal(t, user.Username, createdUser.Username) + require.Equal(t, user.FullName, createdUser.FullName) + require.Equal(t, user.Email, createdUser.Email) + }, + }, + { + name: "InternalError", + req: &pb.CreateUserRequest{ + Username: user.Username, + Password: password, + FullName: user.FullName, + Email: user.Email, + }, + buildStubs: func(store *mockdb.MockStore, taskDistributor *mockwk.MockTaskDistributor) { + store.EXPECT(). + CreateUserTx(gomock.Any(), gomock.Any()). + Times(1). + Return(db.CreateUserTxResult{}, sql.ErrConnDone) + + taskDistributor.EXPECT(). + DistributeTaskSendVerifyEmail(gomock.Any(), gomock.Any(), gomock.Any()). + Times(0) + }, + checkResponse: func(t *testing.T, res *pb.CreateUserResponse, err error) { + require.Error(t, err) + st, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, st.Code(), codes.Internal) + }, + }, + } + + for i := range testCases { + tc := testCases[i] + + t.Run(tc.name, func(t *testing.T) { + storeCtrl := gomock.NewController(t) + defer storeCtrl.Finish() + + store := mockdb.NewMockStore(storeCtrl) + + taskCtrl := gomock.NewController(t) + taskDistributor := mockwk.NewMockTaskDistributor(taskCtrl) + + tc.buildStubs(store, taskDistributor) + + server := NewTestServer(t, store, taskDistributor) + res, err := server.CreateUser(context.Background(), tc.req) + tc.checkResponse(t, res, err) + }) + } +} diff --git a/worker/mock/distributor.go b/worker/mock/distributor.go new file mode 100644 index 0000000..0ac617b --- /dev/null +++ b/worker/mock/distributor.go @@ -0,0 +1,61 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/raphaeldiscky/simple-bank/worker (interfaces: TaskDistributor) +// +// Generated by this command: +// +// mockgen -package mockwk -destination worker/mock/distributor.go github.com/raphaeldiscky/simple-bank/worker TaskDistributor +// + +// Package mockwk is a generated GoMock package. +package mockwk + +import ( + context "context" + reflect "reflect" + + asynq "github.com/hibiken/asynq" + worker "github.com/raphaeldiscky/simple-bank/worker" + gomock "go.uber.org/mock/gomock" +) + +// MockTaskDistributor is a mock of TaskDistributor interface. +type MockTaskDistributor struct { + ctrl *gomock.Controller + recorder *MockTaskDistributorMockRecorder +} + +// MockTaskDistributorMockRecorder is the mock recorder for MockTaskDistributor. +type MockTaskDistributorMockRecorder struct { + mock *MockTaskDistributor +} + +// NewMockTaskDistributor creates a new mock instance. +func NewMockTaskDistributor(ctrl *gomock.Controller) *MockTaskDistributor { + mock := &MockTaskDistributor{ctrl: ctrl} + mock.recorder = &MockTaskDistributorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTaskDistributor) EXPECT() *MockTaskDistributorMockRecorder { + return m.recorder +} + +// DistributeTaskSendVerifyEmail mocks base method. +func (m *MockTaskDistributor) DistributeTaskSendVerifyEmail(arg0 context.Context, arg1 *worker.PayloadSendVerifyEmail, arg2 ...asynq.Option) error { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DistributeTaskSendVerifyEmail", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// DistributeTaskSendVerifyEmail indicates an expected call of DistributeTaskSendVerifyEmail. +func (mr *MockTaskDistributorMockRecorder) DistributeTaskSendVerifyEmail(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DistributeTaskSendVerifyEmail", reflect.TypeOf((*MockTaskDistributor)(nil).DistributeTaskSendVerifyEmail), varargs...) +}