diff --git a/db/db.go b/db/db.go index 2d1556a..bc2dd5c 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) (user []User, err error) + GetUser(ctx context.Context, id int) (user User, err error) + UpdateUserByID(ctx context.Context, user UserUpdateParams, id int) (err error) } diff --git a/db/mock.go b/db/mock.go index a8047e7..13039d2 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 UserUpdateParams, id int) (err error) { + args := m.Called(ctx) + return args.Error(0) +} diff --git a/db/user.go b/db/user.go index 17715e9..2001ab2 100644 --- a/db/user.go +++ b/db/user.go @@ -2,21 +2,135 @@ package db import ( "context" + "errors" + "fmt" + "time" logger "github.com/sirupsen/logrus" + "golang.org/x/crypto/bcrypt" ) +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"` + 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"` + 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"` } 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("Error listing users") + 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 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, + 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 updating user profile") + return + } + return +} + +//Validate function to check empty fields +func (user *UserUpdateParams) Validate() (err error) { + + if user.FirstName == "" { + return errors.New("first name cannot be blank") + } + if user.LastName == "" { + return errors.New("last name cannot be blank") + } + if user.Mobile == "" { + return errors.New("mobile cannot be blank") + } + if user.Password == "" { + return errors.New("password cannot be blank") + } + if user.Address == "" { + return errors.New("address cannot be blank") + } + if user.Country == "" { + return errors.New("country cannot be blank") + } + if user.State == "" { + return errors.New("state cannot be blank") + } + if user.City == "" { + return errors.New("city cannot be blank") + } + return +} + +//TODO add function for aunthenticating user diff --git a/go.mod b/go.mod index bd1ae4f..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 @@ -12,4 +13,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/go.sum b/go.sum index 0b155a0..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= @@ -105,6 +109,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 +208,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/migrations/1587381324_create_users.up.sql b/migrations/1587381324_create_users.up.sql index f893282..a1a28fb 100644 --- a/migrations/1587381324_create_users.up.sql +++ b/migrations/1587381324_create_users.up.sql @@ -1,4 +1,15 @@ -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), + email VARCHAR(255) NOT NULL UNIQUE, + mobile VARCHAR(20), + country VARCHAR(100), + state VARCHAR(100), + city VARCHAR(100), + address 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/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..a62392f 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,87 @@ 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.UserUpdateParams + + 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 + } + 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 { + 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 + } + + uUser, 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 in fetching user", + }, + }) + return + } + + repsonse(rw, http.StatusOK, successResponse{Data: uUser}) + return + + }) + +} diff --git a/service/user_http_test.go b/service/user_http_test.go index 4e2b367..ebe20dc 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,138 @@ 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))