diff --git a/api/user_test.go b/api/user_test.go new file mode 100644 index 0000000..6160a12 --- /dev/null +++ b/api/user_test.go @@ -0,0 +1,225 @@ +package api + +import ( + "bytes" + "database/sql" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + mockdb "github.com/Delavalom/RBD/db/mock" + db "github.com/Delavalom/RBD/db/sqlc" + "github.com/Delavalom/RBD/util" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +type eqCreateUserParamsMatcher struct { + arg db.CreateUserParams + password string +} + +func (e eqCreateUserParamsMatcher) Matches(x interface{}) bool { + arg, ok := x.(db.CreateUserParams) + if !ok { + return false + } + + err := util.CheckPassword(e.password, arg.HashedPassword) + if err != nil { + return false + } + + e.arg.HashedPassword = arg.HashedPassword + return reflect.DeepEqual(e.arg, arg) +} + +func (e eqCreateUserParamsMatcher) String() string { + return fmt.Sprintf("matches arg %v and password %v", e.arg, e.password) +} + +func EqCreateUserParams(arg db.CreateUserParams, password string) gomock.Matcher { + return eqCreateUserParamsMatcher{arg, password} +} + +func TestCreateUserAPI(t *testing.T) { + user, password := randomUser(t) + + testCases := []struct { + name string + body gin.H + buildStubs func(store *mockdb.MockStore) + checkResponse func(recoder *httptest.ResponseRecorder) + }{ + { + name: "OK", + body: gin.H{ + "username": user.Username, + "password": password, + "full_name": user.FullName, + "email": user.Email, + }, + buildStubs: func(store *mockdb.MockStore) { + arg := db.CreateUserParams{ + Username: user.Username, + FullName: user.FullName, + Email: user.Email, + } + store.EXPECT(). + CreateUser(gomock.Any(), EqCreateUserParams(arg, password)). + Times(1). + Return(user, nil) + }, + checkResponse: func(recorder *httptest.ResponseRecorder) { + require.Equal(t, http.StatusOK, recorder.Code) + requireBodyMatchUser(t, recorder.Body, user) + }, + }, + { + name: "InternalError", + body: gin.H{ + "username": user.Username, + "password": password, + "full_name": user.FullName, + "email": user.Email, + }, + buildStubs: func(store *mockdb.MockStore) { + store.EXPECT(). + CreateUser(gomock.Any(), gomock.Any()). + Times(1). + Return(db.User{}, sql.ErrConnDone) + }, + checkResponse: func(recorder *httptest.ResponseRecorder) { + require.Equal(t, http.StatusInternalServerError, recorder.Code) + }, + }, + { + name: "DuplicateUsername", + body: gin.H{ + "username": user.Username, + "password": password, + "full_name": user.FullName, + "email": user.Email, + }, + buildStubs: func(store *mockdb.MockStore) { + store.EXPECT(). + CreateUser(gomock.Any(), gomock.Any()). + Times(1). + Return(db.User{}, db.ErrUniqueViolation) + }, + checkResponse: func(recorder *httptest.ResponseRecorder) { + require.Equal(t, http.StatusForbidden, recorder.Code) + }, + }, + { + name: "InvalidUsername", + body: gin.H{ + "username": "invalid-user#1", + "password": password, + "full_name": user.FullName, + "email": user.Email, + }, + buildStubs: func(store *mockdb.MockStore) { + store.EXPECT(). + CreateUser(gomock.Any(), gomock.Any()). + Times(0) + }, + checkResponse: func(recorder *httptest.ResponseRecorder) { + require.Equal(t, http.StatusBadRequest, recorder.Code) + }, + }, + { + name: "InvalidEmail", + body: gin.H{ + "username": user.Username, + "password": password, + "full_name": user.FullName, + "email": "invalid-email", + }, + buildStubs: func(store *mockdb.MockStore) { + store.EXPECT(). + CreateUser(gomock.Any(), gomock.Any()). + Times(0) + }, + checkResponse: func(recorder *httptest.ResponseRecorder) { + require.Equal(t, http.StatusBadRequest, recorder.Code) + }, + }, + { + name: "TooShortPassword", + body: gin.H{ + "username": user.Username, + "password": "123", + "full_name": user.FullName, + "email": user.Email, + }, + buildStubs: func(store *mockdb.MockStore) { + store.EXPECT(). + CreateUser(gomock.Any(), gomock.Any()). + Times(0) + }, + checkResponse: func(recorder *httptest.ResponseRecorder) { + require.Equal(t, http.StatusBadRequest, recorder.Code) + }, + }, + } + + for i := range testCases { + tc := testCases[i] + + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mockdb.NewMockStore(ctrl) + tc.buildStubs(store) + + server := NewServer(store) + recorder := httptest.NewRecorder() + + // Marshal body data to JSON + data, err := json.Marshal(tc.body) + require.NoError(t, err) + + url := "/users" + request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data)) + require.NoError(t, err) + + server.router.ServeHTTP(recorder, request) + tc.checkResponse(recorder) + }) + } +} + +func randomUser(t *testing.T) (user db.User, password string) { + password = util.RandomString(6) + hashedPassword, err := util.HashPassword(password) + require.NoError(t, err) + + user = db.User{ + Username: util.RandomOwner(), + HashedPassword: hashedPassword, + FullName: util.RandomOwner(), + Email: util.RandomEmail(), + } + return +} + +func requireBodyMatchUser(t *testing.T, body *bytes.Buffer, user db.User) { + data, err := io.ReadAll(body) + require.NoError(t, err) + + var gotUser db.User + err = json.Unmarshal(data, &gotUser) + + require.NoError(t, err) + require.Equal(t, user.Username, gotUser.Username) + require.Equal(t, user.FullName, gotUser.FullName) + require.Equal(t, user.Email, gotUser.Email) + require.Empty(t, gotUser.HashedPassword) +} diff --git a/db/mock/store.go b/db/mock/store.go index d61bce3..b134611 100644 --- a/db/mock/store.go +++ b/db/mock/store.go @@ -99,6 +99,21 @@ func (mr *MockStoreMockRecorder) CreateTransfer(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTransfer", reflect.TypeOf((*MockStore)(nil).CreateTransfer), arg0, arg1) } +// CreateUser mocks base method. +func (m *MockStore) CreateUser(arg0 context.Context, arg1 db.CreateUserParams) (db.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUser", arg0, arg1) + ret0, _ := ret[0].(db.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateUser indicates an expected call of CreateUser. +func (mr *MockStoreMockRecorder) CreateUser(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockStore)(nil).CreateUser), arg0, arg1) +} + // DeleteAccount mocks base method. func (m *MockStore) DeleteAccount(arg0 context.Context, arg1 int64) error { m.ctrl.T.Helper() @@ -173,6 +188,21 @@ func (mr *MockStoreMockRecorder) GetTransfer(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTransfer", reflect.TypeOf((*MockStore)(nil).GetTransfer), arg0, arg1) } +// GetUser mocks base method. +func (m *MockStore) GetUser(arg0 context.Context, arg1 string) (db.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUser", arg0, arg1) + ret0, _ := ret[0].(db.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUser indicates an expected call of GetUser. +func (mr *MockStoreMockRecorder) GetUser(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockStore)(nil).GetUser), arg0, arg1) +} + // ListAccounts mocks base method. func (m *MockStore) ListAccounts(arg0 context.Context, arg1 db.ListAccountsParams) ([]db.Account, error) { m.ctrl.T.Helper()