From fac01cefffb835e89dcadc8840e2cabbbeaf6621 Mon Sep 17 00:00:00 2001 From: paramita majumdar Date: Tue, 15 Sep 2020 16:29:48 +0530 Subject: [PATCH 1/5] update and getById for user --- db/db.go | 8 +- db/user.go | 125 +++++++++++++++++++++- go.mod | 1 + migrations/1587381324_create_users.up.sql | 17 ++- service/response.go | 39 +++++++ service/router.go | 4 + service/user_http.go | 88 +++++++++++++++ 7 files changed, 270 insertions(+), 12 deletions(-) create mode 100644 service/response.go diff --git a/db/db.go b/db/db.go index 2d1556a..9f839e1 100644 --- a/db/db.go +++ b/db/db.go @@ -4,9 +4,9 @@ import ( "context" ) +//Storer - interface to add methods used for db operations type Storer interface { - ListUsers(context.Context) ([]User, error) - //Create(context.Context, User) error - //GetUser(context.Context) (User, error) - //Delete(context.Context, string) error + ListUsers(ctx context.Context) (userList []User, err error) + GetUser(ctx context.Context, id int) (user User, err error) + UpdateUserByID(ctx context.Context, user User, id int) (err error) } diff --git a/db/user.go b/db/user.go index 17715e9..07fb339 100644 --- a/db/user.go +++ b/db/user.go @@ -2,21 +2,138 @@ package db import ( "context" + "fmt" + + "golang.org/x/crypto/bcrypt" logger "github.com/sirupsen/logrus" ) +const ( + updateUserQuery = `UPDATE users SET ( + first_name, + last_name, + mobile, + address, + password, + country, + state, + city + + ) = + ($1, $2, $3, $4, $5, $6 ,$7,$8) where id = $9 ` + + getUserQuery = `SELECT * from users where id=$1` +) + +//User is a structure of the user type User struct { - Name string `db:"name" json:"full_name"` - Age int `db:"age" json:"age"` + ID int `db:"id" json:"id"` + FirstName string `db:"first_name" json:"first_name"` + LastName string `db:"last_name" json:"last_name"` + Email string `db:"email" json:"email"` + Mobile string `db:"mobile" json:"mobile"` + Address string `db:"address" json:"address"` + Password string `db:"password" json:"password"` + Country string `db:"country" json:"country"` + State string `db:"state" json:"state"` + City string `db:"city" json:"city"` + CreatedAt string `db:"created_at" json:"created_at"` } func (s *pgStore) ListUsers(ctx context.Context) (users []User, err error) { - err = s.db.Select(&users, "SELECT * FROM users ORDER BY name ASC") + err = s.db.Select(&users, "SELECT * FROM users") + if err != nil { + logger.WithField("err", err.Error()).Error("error listing users") + return + } + + return +} + +func (s *pgStore) GetUser(ctx context.Context, id int) (user User, err error) { + err = s.db.Get(&user, getUserQuery, id) + if err != nil { + logger.WithField("err", err.Error()).Error(fmt.Errorf("error selecting user from database by id %d", id)) + return + } + + return +} + +func (s *pgStore) UpdateUserByID(ctx context.Context, user User, userID int) (err error) { + + //check if password is to be updated then convert it to hashcode + if user.Password != "" { + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), 8) + if err != nil { + logger.WithField("err", err.Error()).Error("error while creating hash of the password") + return err + } + user.Password = string(hashedPassword) + + } + + _, err = s.db.Exec( + updateUserQuery, + user.FirstName, + user.LastName, + user.Mobile, + user.Address, + user.Password, + user.Country, + user.State, + user.City, + userID, + ) if err != nil { - logger.WithField("err", err.Error()).Error("Error listing users") + logger.WithField("err", err.Error()).Error("error updating user profile") return } return + } + +//Validate function for user +func (user *User) Validate() (valid bool) { + fieldErrors := make(map[string]string) + + if user.FirstName == "" { + fieldErrors["name"] = "Can't be blank" + } + if user.LastName == "" { + fieldErrors["LastName"] = "Can't be blank" + } + if user.Mobile == "" { + fieldErrors["Mobile"] = "Can't be blank" + } + if user.Password == "" { + fieldErrors["Password"] = "Can't be blank" + } + if user.Address == "" { + fieldErrors["Address"] = "Can't be blank" + } + if user.Country == "" { + fieldErrors["Country"] = "Can't be blank" + } + if user.State == "" { + fieldErrors["State"] = "Can't be blank" + } + if user.City == "" { + fieldErrors["City"] = "Can't be blank" + } + + if len(fieldErrors) == 0 { + valid = true + return + } + + valid = false + //TODO: Ask what other validations are expected + + return +} + +//TODO add function for aunthenticating user diff --git a/go.mod b/go.mod index bd1ae4f..d0a5248 100644 --- a/go.mod +++ b/go.mod @@ -12,4 +12,5 @@ require ( github.com/stretchr/testify v1.6.1 github.com/urfave/cli v1.22.4 github.com/urfave/negroni v1.0.0 + golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 ) diff --git a/migrations/1587381324_create_users.up.sql b/migrations/1587381324_create_users.up.sql index f893282..65f44a2 100644 --- a/migrations/1587381324_create_users.up.sql +++ b/migrations/1587381324_create_users.up.sql @@ -1,4 +1,13 @@ -CREATE TABLE users ( - name text, - age integer -); +CREATE TABLE IF NOT EXISTS users ( + id SERIAL NOT NULL PRIMARY KEY, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + mobile VARCHAR(20) NOT NULL UNIQUE, + country VARCHAR(100), + state VARCHAR(100), + city VARCHAR(100), + address TEXT, + password TEXT, + created_at TIMESTAMP DEFAULT (NOW() AT TIME ZONE 'UTC') +); \ No newline at end of file diff --git a/service/response.go b/service/response.go new file mode 100644 index 0000000..541d80b --- /dev/null +++ b/service/response.go @@ -0,0 +1,39 @@ +package service + +import ( + "encoding/json" + "net/http" + + logger "github.com/sirupsen/logrus" +) + +type successResponse struct { + Data interface{} `json:"data"` +} + +type errorResponse struct { + Error interface{} `json:"error"` +} + +type messageObject struct { + Message string `json:"message"` +} + +type errorObject struct { + Code string `json:"code"` + messageObject + Fields map[string]string `json:"fields"` +} + +func repsonse(rw http.ResponseWriter, status int, responseBody interface{}) { + respBytes, err := json.Marshal(responseBody) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while marshaling core values data") + rw.WriteHeader(http.StatusInternalServerError) + return + } + + rw.Header().Add("Content-Type", "application/json") + rw.WriteHeader(status) + rw.Write(respBytes) +} diff --git a/service/router.go b/service/router.go index 121ad64..e06bc53 100644 --- a/service/router.go +++ b/service/router.go @@ -24,5 +24,9 @@ func InitRouter(deps Dependencies) (router *mux.Router) { v1 := fmt.Sprintf("application/vnd.%s.v1", config.AppName()) router.HandleFunc("/users", listUsersHandler(deps)).Methods(http.MethodGet).Headers(versionHeader, v1) + + //TODO :- use JWT authentication + router.HandleFunc("/user", getUserHandler(deps)).Methods(http.MethodGet).Headers(versionHeader, v1) + router.HandleFunc("/user/update", updateUserHandler(deps)).Methods(http.MethodPatch).Headers(versionHeader, v1) return } diff --git a/service/user_http.go b/service/user_http.go index c544bcd..ee49ee6 100644 --- a/service/user_http.go +++ b/service/user_http.go @@ -2,6 +2,7 @@ package service import ( "encoding/json" + "joshsoftware/go-e-commerce/db" "net/http" logger "github.com/sirupsen/logrus" @@ -33,3 +34,90 @@ func listUsersHandler(deps Dependencies) http.HandlerFunc { rw.Write(respBytes) }) } + +//get user by id +func getUserHandler(deps Dependencies) http.HandlerFunc { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + //TODO :- get the userId from JWT autentication token + userID := 1 + user, err := deps.Store.GetUser(req.Context(), int(userID)) + if err != nil { + logger.WithField("err", err.Error()).Error("error while fetching User") + rw.WriteHeader(http.StatusNotFound) + repsonse(rw, http.StatusNotFound, errorResponse{ + Error: messageObject{ + Message: "id Not Found", + }, + }) + return + } + repsonse(rw, http.StatusOK, successResponse{Data: user}) + }) + +} + +//update user by id +func updateUserHandler(deps Dependencies) http.HandlerFunc { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + + //TODO :- get the userID from JWT autentication token + userID := 1 + var user db.User + + err := json.NewDecoder(req.Body).Decode(&user) + + if err != nil { + rw.WriteHeader(http.StatusBadRequest) + logger.WithField("err", err.Error()).Error("error while decoding user") + repsonse(rw, http.StatusBadRequest, errorResponse{ + Error: messageObject{ + Message: "invalid json body", + }, + }) + return + } + + if user.Email != "" { + + rw.WriteHeader(http.StatusBadRequest) + logger.WithField("err", "cannot update email") + repsonse(rw, http.StatusBadRequest, errorResponse{ + Error: messageObject{ + Message: "cannot update email id !!", + }, + }) + return + } + + user.Validate() + + err = deps.Store.UpdateUserByID(req.Context(), user, int(userID)) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + repsonse(rw, http.StatusInternalServerError, errorResponse{ + Error: messageObject{ + Message: "internal server error", + }, + }) + logger.WithField("err", err.Error()).Error("error while updating user's profile") + return + } + + updatedUser, err := deps.Store.GetUser(req.Context(), int(userID)) + if err != nil { + logger.WithField("err", err.Error()).Error("error while fetching User") + rw.WriteHeader(http.StatusNotFound) + repsonse(rw, http.StatusNotFound, errorResponse{ + Error: messageObject{ + Message: "error while fetching users", + }, + }) + return + } + repsonse(rw, http.StatusOK, successResponse{Data: updatedUser}) + + return + + }) + +} From 28e7bb460707e52600bf99d4c4370f091dc32840 Mon Sep 17 00:00:00 2001 From: paramita majumdar Date: Tue, 15 Sep 2020 17:39:23 +0530 Subject: [PATCH 2/5] changes made according to review commit --- db/db.go | 2 +- db/user.go | 35 ++++++++++------------------------- go.sum | 2 ++ service/user_http.go | 15 ++++++++++++++- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/db/db.go b/db/db.go index 9f839e1..9e59eea 100644 --- a/db/db.go +++ b/db/db.go @@ -6,7 +6,7 @@ import ( //Storer - interface to add methods used for db operations type Storer interface { - ListUsers(ctx context.Context) (userList []User, err error) + ListUsers(ctx context.Context) (user []User, err error) GetUser(ctx context.Context, id int) (user User, err error) UpdateUserByID(ctx context.Context, user User, id int) (err error) } diff --git a/db/user.go b/db/user.go index 07fb339..024d589 100644 --- a/db/user.go +++ b/db/user.go @@ -2,6 +2,7 @@ package db import ( "context" + "errors" "fmt" "golang.org/x/crypto/bcrypt" @@ -19,7 +20,6 @@ const ( country, state, city - ) = ($1, $2, $3, $4, $5, $6 ,$7,$8) where id = $9 ` @@ -62,17 +62,14 @@ func (s *pgStore) GetUser(ctx context.Context, id int) (user User, err error) { } func (s *pgStore) UpdateUserByID(ctx context.Context, user User, userID int) (err error) { - //check if password is to be updated then convert it to hashcode if user.Password != "" { - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), 8) if err != nil { logger.WithField("err", err.Error()).Error("error while creating hash of the password") return err } user.Password = string(hashedPassword) - } _, err = s.db.Exec( @@ -91,48 +88,36 @@ func (s *pgStore) UpdateUserByID(ctx context.Context, user User, userID int) (er logger.WithField("err", err.Error()).Error("error updating user profile") return } - return - } //Validate function for user -func (user *User) Validate() (valid bool) { - fieldErrors := make(map[string]string) +func (user *User) Validate() (err error) { if user.FirstName == "" { - fieldErrors["name"] = "Can't be blank" + return errors.New("first name cannot be blank") } if user.LastName == "" { - fieldErrors["LastName"] = "Can't be blank" + return errors.New("first name cannot be blank") } if user.Mobile == "" { - fieldErrors["Mobile"] = "Can't be blank" + return errors.New("first name cannot be blank") } if user.Password == "" { - fieldErrors["Password"] = "Can't be blank" + return errors.New("first name cannot be blank") } if user.Address == "" { - fieldErrors["Address"] = "Can't be blank" + return errors.New("first name cannot be blank") } if user.Country == "" { - fieldErrors["Country"] = "Can't be blank" + return errors.New("first name cannot be blank") } if user.State == "" { - fieldErrors["State"] = "Can't be blank" + return errors.New("first name cannot be blank") } if user.City == "" { - fieldErrors["City"] = "Can't be blank" + return errors.New("first name cannot be blank") } - - if len(fieldErrors) == 0 { - valid = true - return - } - - valid = false - //TODO: Ask what other validations are expected - return } diff --git a/go.sum b/go.sum index 0b155a0..40190a0 100644 --- a/go.sum +++ b/go.sum @@ -105,6 +105,7 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -203,6 +204,7 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= diff --git a/service/user_http.go b/service/user_http.go index ee49ee6..fad6994 100644 --- a/service/user_http.go +++ b/service/user_http.go @@ -89,7 +89,20 @@ func updateUserHandler(deps Dependencies) http.HandlerFunc { return } - user.Validate() + err = user.Validate() + + if err != nil { + { + rw.WriteHeader(http.StatusBadRequest) + repsonse(rw, http.StatusBadRequest, errorResponse{ + Error: messageObject{ + Message: err.Error(), + }, + }) + logger.WithField("err", err.Error()).Error("error while validating user's profile") + return + } + } err = deps.Store.UpdateUserByID(req.Context(), user, int(userID)) if err != nil { From 6d36484cb47e38b0f9c3b4715040bf19a7a9bcd4 Mon Sep 17 00:00:00 2001 From: paramita majumdar Date: Tue, 15 Sep 2020 18:17:22 +0530 Subject: [PATCH 3/5] added test cases for User getByID and update --- db/mock.go | 13 +++ go.mod | 1 + go.sum | 4 + service/user_http_test.go | 166 +++++++++++++++++++++++++++++++++++--- 4 files changed, 171 insertions(+), 13 deletions(-) diff --git a/db/mock.go b/db/mock.go index a8047e7..a833b9c 100644 --- a/db/mock.go +++ b/db/mock.go @@ -10,7 +10,20 @@ type DBMockStore struct { mock.Mock } +//ListUsers mock method func (m *DBMockStore) ListUsers(ctx context.Context) (users []User, err error) { args := m.Called(ctx) return args.Get(0).([]User), args.Error(1) } + +//GetUser mock method +func (m *DBMockStore) GetUser(ctx context.Context, id int) (user User, err error) { + args := m.Called(ctx) + return args.Get(0).(User), args.Error(1) +} + +//UpdateUser mock method +func (m *DBMockStore) UpdateUserByID(ctx context.Context, user User, id int) (err error) { + args := m.Called(ctx) + return args.Error(0) +} diff --git a/go.mod b/go.mod index d0a5248..62bf44d 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module joshsoftware/go-e-commerce go 1.14 require ( + github.com/bxcodec/faker/v3 v3.5.0 github.com/gorilla/mux v1.8.0 github.com/jmoiron/sqlx v1.2.0 github.com/lib/pq v1.8.0 diff --git a/go.sum b/go.sum index 40190a0..fce3352 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,10 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/bxcodec/faker v1.5.0 h1:RIWOeAcM3ZHye1i8bQtHU2LfNOaLmHuRiCo60mNMOcQ= +github.com/bxcodec/faker v2.0.1+incompatible h1:P0KUpUw5w6WJXwrPfv35oc91i4d8nf40Nwln+M/+faA= +github.com/bxcodec/faker/v3 v3.5.0 h1:Rahy6dwbd6up0wbwbV7dFyQb+jmdC51kpATuUdnzfMg= +github.com/bxcodec/faker/v3 v3.5.0/go.mod h1:gF31YgnMSMKgkvl+fyEo1xuSMbEuieyqfeslGYFjneM= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/service/user_http_test.go b/service/user_http_test.go index 4e2b367..7d8ebed 100644 --- a/service/user_http_test.go +++ b/service/user_http_test.go @@ -1,13 +1,15 @@ package service import ( + "encoding/json" "errors" "joshsoftware/go-e-commerce/db" + "log" "net/http" "net/http/httptest" "strings" - "testing" + "github.com/bxcodec/faker/v3" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -26,27 +28,32 @@ func (suite *UsersHandlerTestSuite) SetupTest() { suite.dbMock = &db.DBMockStore{} } -func TestExampleTestSuite(t *testing.T) { - suite.Run(t, new(UsersHandlerTestSuite)) -} - func (suite *UsersHandlerTestSuite) TestListUsersSuccess() { - suite.dbMock.On("ListUsers", mock.Anything).Return( - []db.User{ - db.User{Name: "test-user", Age: 18}, - }, - nil, - ) + fakeUser := db.User{} + faker.FakeData(&fakeUser) + + // Declare an array of db.User and append the fakeUser onto it for use on the dbMock + fakeUsers := []db.User{} + fakeUsers = append(fakeUsers, fakeUser) + + suite.dbMock.On("ListUsers", mock.Anything).Return(fakeUsers, nil) recorder := makeHTTPCall( http.MethodGet, "/users", + "/users", "", listUsersHandler(Dependencies{Store: suite.dbMock}), ) + var users []db.User + err := json.Unmarshal(recorder.Body.Bytes(), &users) + if err != nil { + log.Fatal("Error converting HTTP body from listUsersHandler into User object in json.Unmarshal") + } + assert.Equal(suite.T(), http.StatusOK, recorder.Code) - assert.Equal(suite.T(), `[{"full_name":"test-user","age":18}]`, recorder.Body.String()) + assert.NotNil(suite.T(), users[0].ID) suite.dbMock.AssertExpectations(suite.T()) } @@ -59,6 +66,7 @@ func (suite *UsersHandlerTestSuite) TestListUsersWhenDBFailure() { recorder := makeHTTPCall( http.MethodGet, "/users", + "/users", "", listUsersHandler(Dependencies{Store: suite.dbMock}), ) @@ -67,7 +75,139 @@ func (suite *UsersHandlerTestSuite) TestListUsersWhenDBFailure() { suite.dbMock.AssertExpectations(suite.T()) } -func makeHTTPCall(method, path, body string, handlerFunc http.HandlerFunc) (recorder *httptest.ResponseRecorder) { +func (suite *UsersHandlerTestSuite) TestGetUserSuccess() { + + userID := 1 + suite.dbMock.On("GetUser", mock.Anything, userID).Return( + db.User{ + ID: 1, + FirstName: "TestUser", + LastName: "TestUser", + Email: "TestEmail", + Mobile: "TestMobile", + Address: "Testaddress", + Password: "TestPass", + Country: "TestCountry", + State: "TestState", + City: "TestCity", + }, nil, + ) + + recorder := makeHTTPCall(http.MethodGet, + "/user", + "/user", + "", + getUserHandler(Dependencies{Store: suite.dbMock}), + ) + + assert.Equal(suite.T(), http.StatusOK, recorder.Code) + assert.Equal(suite.T(), `{ "data": { + "id": 1, + "first_name": "TestUser", + "last_name": "TestUser", + "email": "TestEmail", + "mobile": "TestMobile", + "address": "Testaddress", + "password": "TestPass", + "country": "TestCountry", + "state": "TestState", + "city": "TestCity" + + }}`, recorder.Body.String()) + + suite.dbMock.AssertExpectations(suite.T()) +} + +func (suite *UsersHandlerTestSuite) TestUpdateUserSuccess() { + userID := 1 + user := db.User{ + ID: 1, + FirstName: "UpdatedUserName", + LastName: "TestUser", + Email: "TestEmail", + Mobile: "TestMobile", + Password: "TestPass", + Country: "TestCountry", + State: "TestState", + City: "TestCity", + Address: "Testaddress", + } + + suite.dbMock.On("UpdateUserByID", mock.Anything, user, userID).Return(nil) + + body := ` "id": 1, + "first_name": "UpdatedUserName", + "last_name": "TestUser", + "mobile_number": "TestMobile", + "password": "TestPass", + "country": "TestCountry", + "state": "TestState", + "city": "TestCity", + "address": "Testaddress"` + + recorder := makeHTTPCall(http.MethodPatch, + "/user/update", + "/user/update", + body, + updateUserHandler(Dependencies{Store: suite.dbMock}), + ) + + assert.Equal(suite.T(), http.StatusOK, recorder.Code) + assert.Equal(suite.T(), `{"data": { + "id": 1, + "first_name": "UpdatedUserName", + "last_name": "TestUser", + "email": "TestEmail", + "mobile": "TestMobile", + "address": "Testaddress", + "password": "TestPass", + "country": "TestCountry", + "state": "TestState", + "city": "TestCity" + + }}`, recorder.Body.String()) + suite.dbMock.AssertExpectations(suite.T()) +} + +func (suite *UsersHandlerTestSuite) TestUpdateUserDbFailure() { + userID := 1 + user := db.User{ + ID: 1, + FirstName: "UpdatedUserName", + LastName: "TestUser", + Email: "TestEmail", + Mobile: "TestMobile", + Password: "TestPass", + Country: "TestCountry", + State: "TestState", + City: "TestCity", + Address: "Testaddress", + } + suite.dbMock.On("UpdateUserByID", mock.Anything, user, userID).Return(errors.New("Error while updating user")) + + body := ` "id": 1, + "first_name": "UpdatedUser", + "last_name": "TestUser", + "mobile": "TestMobile", + "address": "Testaddress", + "password": "TestPass", + "country": "TestCountry", + "state": "TestState", + "city": "TestCity" + ` + + recorder := makeHTTPCall(http.MethodPut, + "/user/update", + "/user/update", + body, + updateUserHandler(Dependencies{Store: suite.dbMock}), + ) + + assert.Equal(suite.T(), http.StatusInternalServerError, recorder.Code) + suite.dbMock.AssertExpectations(suite.T()) +} + +func makeHTTPCall(method, path, requestURL, body string, handlerFunc http.HandlerFunc) (recorder *httptest.ResponseRecorder) { // create a http request using the given parameters req, _ := http.NewRequest(method, path, strings.NewReader(body)) From dbb1971586c8e819a485add0e06d5b27b7275e29 Mon Sep 17 00:00:00 2001 From: paramita majumdar Date: Wed, 16 Sep 2020 16:09:32 +0530 Subject: [PATCH 4/5] changed update according to UI requirements --- db/user.go | 51 ++++++++++++++++++++------------------------ service/user_http.go | 38 +++++++++++++++------------------ 2 files changed, 40 insertions(+), 49 deletions(-) diff --git a/db/user.go b/db/user.go index 024d589..7de756b 100644 --- a/db/user.go +++ b/db/user.go @@ -2,7 +2,6 @@ package db import ( "context" - "errors" "fmt" "golang.org/x/crypto/bcrypt" @@ -62,15 +61,6 @@ func (s *pgStore) GetUser(ctx context.Context, id int) (user User, err error) { } func (s *pgStore) UpdateUserByID(ctx context.Context, user User, userID int) (err error) { - //check if password is to be updated then convert it to hashcode - if user.Password != "" { - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), 8) - if err != nil { - logger.WithField("err", err.Error()).Error("error while creating hash of the password") - return err - } - user.Password = string(hashedPassword) - } _, err = s.db.Exec( updateUserQuery, @@ -91,32 +81,37 @@ func (s *pgStore) UpdateUserByID(ctx context.Context, user User, userID int) (er return } -//Validate function for user -func (user *User) Validate() (err error) { +//ValidatePatchParams function for user +func (user *User) ValidatePatchParams(u User) (err error) { - if user.FirstName == "" { - return errors.New("first name cannot be blank") + if u.FirstName != "" { + user.FirstName = u.FirstName } - if user.LastName == "" { - return errors.New("first name cannot be blank") + if u.LastName != "" { + user.LastName = u.LastName } - if user.Mobile == "" { - return errors.New("first name cannot be blank") + if u.Mobile != "" { + user.Mobile = u.Mobile } - if user.Password == "" { - return errors.New("first name cannot be blank") + if u.Password != "" { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), 8) + if err != nil { + logger.WithField("err", err.Error()).Error("error while creating hash of the password") + return err + } + user.Password = string(hashedPassword) } - if user.Address == "" { - return errors.New("first name cannot be blank") + if u.Address != "" { + user.Address = u.Address } - if user.Country == "" { - return errors.New("first name cannot be blank") + if u.Country != "" { + user.Country = u.Country } - if user.State == "" { - return errors.New("first name cannot be blank") + if u.State != "" { + user.State = u.State } - if user.City == "" { - return errors.New("first name cannot be blank") + if u.City != "" { + user.City = u.City } return } diff --git a/service/user_http.go b/service/user_http.go index fad6994..5989826 100644 --- a/service/user_http.go +++ b/service/user_http.go @@ -89,22 +89,18 @@ func updateUserHandler(deps Dependencies) http.HandlerFunc { return } - err = user.Validate() - + dbUser, err := deps.Store.GetUser(req.Context(), int(userID)) if err != nil { - { - rw.WriteHeader(http.StatusBadRequest) - repsonse(rw, http.StatusBadRequest, errorResponse{ - Error: messageObject{ - Message: err.Error(), - }, - }) - logger.WithField("err", err.Error()).Error("error while validating user's profile") - return - } + logger.WithField("err", err.Error()).Error("error while fetching User") + rw.WriteHeader(http.StatusNotFound) + repsonse(rw, http.StatusNotFound, errorResponse{ + Error: messageObject{ + Message: "error while fetching users", + }, + }) + return } - - err = deps.Store.UpdateUserByID(req.Context(), user, int(userID)) + err = dbUser.ValidatePatchParams(user) if err != nil { rw.WriteHeader(http.StatusInternalServerError) repsonse(rw, http.StatusInternalServerError, errorResponse{ @@ -112,23 +108,23 @@ func updateUserHandler(deps Dependencies) http.HandlerFunc { Message: "internal server error", }, }) - logger.WithField("err", err.Error()).Error("error while updating user's profile") + logger.WithField("err", err.Error()) return } - updatedUser, err := deps.Store.GetUser(req.Context(), int(userID)) + err = deps.Store.UpdateUserByID(req.Context(), dbUser, int(userID)) if err != nil { - logger.WithField("err", err.Error()).Error("error while fetching User") - rw.WriteHeader(http.StatusNotFound) - repsonse(rw, http.StatusNotFound, errorResponse{ + rw.WriteHeader(http.StatusInternalServerError) + repsonse(rw, http.StatusInternalServerError, errorResponse{ Error: messageObject{ - Message: "error while fetching users", + Message: "internal server error", }, }) + logger.WithField("err", err.Error()).Error("error while updating user's profile") return } - repsonse(rw, http.StatusOK, successResponse{Data: updatedUser}) + repsonse(rw, http.StatusOK, successResponse{Data: dbUser}) return }) From de5c9f2ae9b4453463b2d6137dad72b753b90d2e Mon Sep 17 00:00:00 2001 From: paramita majumdar Date: Thu, 24 Sep 2020 16:18:49 +0530 Subject: [PATCH 5/5] update user modified to accept object(UserUpdateParams) --- db/db.go | 2 +- db/mock.go | 2 +- db/user.go | 75 ++++++++++++++--------- migrations/1587381324_create_users.up.sql | 8 ++- service/user_http.go | 38 ++++-------- service/user_http_test.go | 5 +- 6 files changed, 68 insertions(+), 62 deletions(-) diff --git a/db/db.go b/db/db.go index 9e59eea..bc2dd5c 100644 --- a/db/db.go +++ b/db/db.go @@ -8,5 +8,5 @@ import ( type Storer interface { ListUsers(ctx context.Context) (user []User, err error) GetUser(ctx context.Context, id int) (user User, err error) - UpdateUserByID(ctx context.Context, user User, id int) (err error) + UpdateUserByID(ctx context.Context, user UserUpdateParams, id int) (err error) } diff --git a/db/mock.go b/db/mock.go index a833b9c..13039d2 100644 --- a/db/mock.go +++ b/db/mock.go @@ -23,7 +23,7 @@ func (m *DBMockStore) GetUser(ctx context.Context, id int) (user User, err error } //UpdateUser mock method -func (m *DBMockStore) UpdateUserByID(ctx context.Context, user User, id int) (err error) { +func (m *DBMockStore) UpdateUserByID(ctx context.Context, user UserUpdateParams, id int) (err error) { args := m.Called(ctx) return args.Error(0) } diff --git a/db/user.go b/db/user.go index 7de756b..2001ab2 100644 --- a/db/user.go +++ b/db/user.go @@ -2,11 +2,12 @@ package db import ( "context" + "errors" "fmt" - - "golang.org/x/crypto/bcrypt" + "time" logger "github.com/sirupsen/logrus" + "golang.org/x/crypto/bcrypt" ) const ( @@ -27,17 +28,31 @@ const ( //User is a structure of the user type User struct { - ID int `db:"id" json:"id"` + ID int `db:"id" json:"id"` + FirstName string `db:"first_name" json:"first_name"` + LastName string `db:"last_name" json:"last_name"` + Email string `db:"email" json:"email"` + Mobile string `db:"mobile" json:"mobile"` + Address string `db:"address" json:"address"` + Password string `db:"password" json:"password"` + Country string `db:"country" json:"country"` + State string `db:"state" json:"state"` + City string `db:"city" json:"city"` + IsAdmin bool `db:"isadmin" json:"isAdmin"` + IsDisabled bool `db:"isdisabled" json:"isDisabled"` + CreatedAt time.Time `db:"created_at" json:"created_at"` +} + +//UserUpdateParams :user fields to be updated +type UserUpdateParams struct { FirstName string `db:"first_name" json:"first_name"` LastName string `db:"last_name" json:"last_name"` - Email string `db:"email" json:"email"` Mobile string `db:"mobile" json:"mobile"` Address string `db:"address" json:"address"` Password string `db:"password" json:"password"` Country string `db:"country" json:"country"` State string `db:"state" json:"state"` City string `db:"city" json:"city"` - CreatedAt string `db:"created_at" json:"created_at"` } func (s *pgStore) ListUsers(ctx context.Context) (users []User, err error) { @@ -60,7 +75,14 @@ func (s *pgStore) GetUser(ctx context.Context, id int) (user User, err error) { return } -func (s *pgStore) UpdateUserByID(ctx context.Context, user User, userID int) (err error) { +func (s *pgStore) UpdateUserByID(ctx context.Context, user UserUpdateParams, userID int) (err error) { + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), 8) + if err != nil { + logger.WithField("err", err.Error()).Error("error while creating hash of the password") + return err + } + user.Password = string(hashedPassword) _, err = s.db.Exec( updateUserQuery, @@ -81,37 +103,32 @@ func (s *pgStore) UpdateUserByID(ctx context.Context, user User, userID int) (er return } -//ValidatePatchParams function for user -func (user *User) ValidatePatchParams(u User) (err error) { +//Validate function to check empty fields +func (user *UserUpdateParams) Validate() (err error) { - if u.FirstName != "" { - user.FirstName = u.FirstName + if user.FirstName == "" { + return errors.New("first name cannot be blank") } - if u.LastName != "" { - user.LastName = u.LastName + if user.LastName == "" { + return errors.New("last name cannot be blank") } - if u.Mobile != "" { - user.Mobile = u.Mobile + if user.Mobile == "" { + return errors.New("mobile cannot be blank") } - if u.Password != "" { - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), 8) - if err != nil { - logger.WithField("err", err.Error()).Error("error while creating hash of the password") - return err - } - user.Password = string(hashedPassword) + if user.Password == "" { + return errors.New("password cannot be blank") } - if u.Address != "" { - user.Address = u.Address + if user.Address == "" { + return errors.New("address cannot be blank") } - if u.Country != "" { - user.Country = u.Country + if user.Country == "" { + return errors.New("country cannot be blank") } - if u.State != "" { - user.State = u.State + if user.State == "" { + return errors.New("state cannot be blank") } - if u.City != "" { - user.City = u.City + if user.City == "" { + return errors.New("city cannot be blank") } return } diff --git a/migrations/1587381324_create_users.up.sql b/migrations/1587381324_create_users.up.sql index 65f44a2..a1a28fb 100644 --- a/migrations/1587381324_create_users.up.sql +++ b/migrations/1587381324_create_users.up.sql @@ -1,13 +1,15 @@ CREATE TABLE IF NOT EXISTS users ( id SERIAL NOT NULL PRIMARY KEY, first_name VARCHAR(255) NOT NULL, - last_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255), email VARCHAR(255) NOT NULL UNIQUE, - mobile VARCHAR(20) NOT NULL UNIQUE, + mobile VARCHAR(20), country VARCHAR(100), state VARCHAR(100), city VARCHAR(100), address TEXT, - password TEXT, + password TEXT, + isAdmin BOOLEAN DEFAULT FALSE, + isDisabled BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT (NOW() AT TIME ZONE 'UTC') ); \ No newline at end of file diff --git a/service/user_http.go b/service/user_http.go index 5989826..a62392f 100644 --- a/service/user_http.go +++ b/service/user_http.go @@ -62,7 +62,7 @@ func updateUserHandler(deps Dependencies) http.HandlerFunc { //TODO :- get the userID from JWT autentication token userID := 1 - var user db.User + var user db.UserUpdateParams err := json.NewDecoder(req.Body).Decode(&user) @@ -76,31 +76,19 @@ func updateUserHandler(deps Dependencies) http.HandlerFunc { }) return } - - if user.Email != "" { - + err = user.Validate() + if err != nil { rw.WriteHeader(http.StatusBadRequest) - logger.WithField("err", "cannot update email") repsonse(rw, http.StatusBadRequest, errorResponse{ Error: messageObject{ - Message: "cannot update email id !!", + Message: err.Error(), }, }) + logger.WithField("err", err.Error()).Error("error while validating user's profile") return } - dbUser, err := deps.Store.GetUser(req.Context(), int(userID)) - if err != nil { - logger.WithField("err", err.Error()).Error("error while fetching User") - rw.WriteHeader(http.StatusNotFound) - repsonse(rw, http.StatusNotFound, errorResponse{ - Error: messageObject{ - Message: "error while fetching users", - }, - }) - return - } - err = dbUser.ValidatePatchParams(user) + err = deps.Store.UpdateUserByID(req.Context(), user, int(userID)) if err != nil { rw.WriteHeader(http.StatusInternalServerError) repsonse(rw, http.StatusInternalServerError, errorResponse{ @@ -108,23 +96,23 @@ func updateUserHandler(deps Dependencies) http.HandlerFunc { Message: "internal server error", }, }) - logger.WithField("err", err.Error()) + logger.WithField("err", err.Error()).Error("error while updating user's profile") return } - err = deps.Store.UpdateUserByID(req.Context(), dbUser, int(userID)) + uUser, err := deps.Store.GetUser(req.Context(), int(userID)) if err != nil { - rw.WriteHeader(http.StatusInternalServerError) - repsonse(rw, http.StatusInternalServerError, errorResponse{ + logger.WithField("err", err.Error()).Error("error while fetching User") + rw.WriteHeader(http.StatusNotFound) + repsonse(rw, http.StatusNotFound, errorResponse{ Error: messageObject{ - Message: "internal server error", + Message: "error in fetching user", }, }) - logger.WithField("err", err.Error()).Error("error while updating user's profile") return } - repsonse(rw, http.StatusOK, successResponse{Data: dbUser}) + repsonse(rw, http.StatusOK, successResponse{Data: uUser}) return }) diff --git a/service/user_http_test.go b/service/user_http_test.go index 7d8ebed..ebe20dc 100644 --- a/service/user_http_test.go +++ b/service/user_http_test.go @@ -185,7 +185,7 @@ func (suite *UsersHandlerTestSuite) TestUpdateUserDbFailure() { } suite.dbMock.On("UpdateUserByID", mock.Anything, user, userID).Return(errors.New("Error while updating user")) - body := ` "id": 1, + body := `"id": 1, "first_name": "UpdatedUser", "last_name": "TestUser", "mobile": "TestMobile", @@ -193,8 +193,7 @@ func (suite *UsersHandlerTestSuite) TestUpdateUserDbFailure() { "password": "TestPass", "country": "TestCountry", "state": "TestState", - "city": "TestCity" - ` + "city": "TestCity"` recorder := makeHTTPCall(http.MethodPut, "/user/update",