diff --git a/db/db.go b/db/db.go index 2d1556a..0647eb5 100644 --- a/db/db.go +++ b/db/db.go @@ -6,7 +6,6 @@ import ( type Storer interface { ListUsers(context.Context) ([]User, error) - //Create(context.Context, User) error - //GetUser(context.Context) (User, error) - //Delete(context.Context, string) error + CreateUser(context.Context, User) (User, error) + GetUserByEmail(context.Context, string) (User, error) } diff --git a/db/mock.go b/db/mock.go index a8047e7..0b34d39 100644 --- a/db/mock.go +++ b/db/mock.go @@ -14,3 +14,15 @@ func (m *DBMockStore) ListUsers(ctx context.Context) (users []User, err error) { args := m.Called(ctx) return args.Get(0).([]User), args.Error(1) } + +// CreateUser - test mock +func (m *DBMockStore) CreateUser(ctx context.Context, u User) (user User, err error) { + args := m.Called(ctx, u) + return args.Get(0).(User), args.Error(1) +} + +// GetUserByEmail - test mock +func (m *DBMockStore) GetUserByEmail(ctx context.Context, email string) (user User, err error) { + args := m.Called(ctx, email) + return args.Get(0).(User), args.Error(1) +} diff --git a/db/user.go b/db/user.go index 17715e9..e8f13ad 100644 --- a/db/user.go +++ b/db/user.go @@ -2,13 +2,32 @@ package db import ( "context" + "database/sql" logger "github.com/sirupsen/logrus" + "golang.org/x/crypto/bcrypt" ) +const ( + insertUserQuery = `INSERT INTO users (first_name, last_name, email, mobile, country, state, city, address, password) + VALUES (:first_name, :last_name, :email, :mobile, :country, :state, :city, :address, :password)` + + getUserByEmailQuery = `SELECT * FROM users WHERE email=$1 LIMIT 1` +) + +// User - struct representing a 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"` + Country string `db:"country" json:"country"` + State string `db:"state" json:"state"` + City string `db:"city" json:"city"` + Address string `db:"address" json:"address"` + Password string `db:"password" json:"password"` + CreatedAt string `db:"created_at" json:"created_at"` } func (s *pgStore) ListUsers(ctx context.Context) (users []User, err error) { @@ -20,3 +39,44 @@ func (s *pgStore) ListUsers(ctx context.Context) (users []User, err error) { return } + +// CreateNewUser = creates a new user in database +func (s *pgStore) CreateUser(ctx context.Context, u User) (newUser User, err error) { + // creating hash of the 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 + } + u.Password = string(hashedPassword) + + stmt, err := s.db.PrepareNamed(insertUserQuery) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while preparing user insert query") + return + } + _, err = stmt.Exec(u) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while inserting user into database") + return + } + newUser, err = s.GetUserByEmail(ctx, u.Email) + if err != nil { + logger.WithField("err", err.Error()).Error("Error selecting user from database with email: " + u.Email) + return + } + return +} + +// GetUserByEmail - Checks if user is present in DB and if then return user +func (s *pgStore) GetUserByEmail(ctx context.Context, email string) (user User, err error) { + err = s.db.Get(&user, getUserByEmailQuery, email) + if err != nil { + if err == sql.ErrNoRows { + return + } + logger.WithField("err", err.Error()).Error("Error while selecting user from database by email " + email) + return + } + return +} diff --git a/go.mod b/go.mod index bd1ae4f..bb9b135 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,16 @@ 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 github.com/mattes/migrate v3.0.1+incompatible + github.com/rs/cors v1.7.0 github.com/sirupsen/logrus v1.6.0 github.com/spf13/viper v1.7.1 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..a3ed01a 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= @@ -154,6 +158,8 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -203,7 +209,9 @@ 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/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= 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= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/main.go b/main.go index e0ad81d..5d7618d 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "os" "strconv" + "github.com/rs/cors" logger "github.com/sirupsen/logrus" "github.com/urfave/cli" "github.com/urfave/negroni" @@ -70,6 +71,13 @@ func startApp() (err error) { return } + c := cors.New(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"POST", "GET", "DELETE", "PUT", "PATCH", "OPTIONS"}, + AllowCredentials: true, + Debug: true, + }) + deps := service.Dependencies{ Store: store, } @@ -79,6 +87,7 @@ func startApp() (err error) { // init web server server := negroni.Classic() + server.Use(c) server.UseHandler(router) port := config.AppPort() // This can be changed to the service port number via environment variable. diff --git a/migrations/1587381324_create_users.down.sql b/migrations/1587381324_create_users.down.sql deleted file mode 100644 index cc1f647..0000000 --- a/migrations/1587381324_create_users.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE users; diff --git a/migrations/1587381324_create_users.up.sql b/migrations/1587381324_create_users.up.sql deleted file mode 100644 index f893282..0000000 --- a/migrations/1587381324_create_users.up.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TABLE users ( - name text, - age integer -); diff --git a/migrations/1599540715_create_users.down.sql b/migrations/1599540715_create_users.down.sql new file mode 100644 index 0000000..441087a --- /dev/null +++ b/migrations/1599540715_create_users.down.sql @@ -0,0 +1 @@ +DROP TABLE users; \ No newline at end of file diff --git a/migrations/1599540715_create_users.up.sql b/migrations/1599540715_create_users.up.sql new file mode 100644 index 0000000..75c0b9a --- /dev/null +++ b/migrations/1599540715_create_users.up.sql @@ -0,0 +1,13 @@ +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, + created_at TIMESTAMP DEFAULT (NOW() AT TIME ZONE 'UTC') +); \ No newline at end of file diff --git a/service/router.go b/service/router.go index 121ad64..15b257c 100644 --- a/service/router.go +++ b/service/router.go @@ -24,5 +24,6 @@ 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) + router.HandleFunc("/register", registerUserHandler(deps)).Methods(http.MethodPost).Headers(versionHeader, v1) return } diff --git a/service/user_http.go b/service/user_http.go index c544bcd..04befdc 100644 --- a/service/user_http.go +++ b/service/user_http.go @@ -1,12 +1,22 @@ package service import ( + "database/sql" "encoding/json" + "io/ioutil" + "joshsoftware/go-e-commerce/db" "net/http" logger "github.com/sirupsen/logrus" ) +type errorResponse struct { + Error string `json:"error"` +} +type successResponse struct { + Message string `json:"message"` +} + // @Title listUsers // @Description list all User // @Router /users [get] @@ -33,3 +43,79 @@ func listUsersHandler(deps Dependencies) http.HandlerFunc { rw.Write(respBytes) }) } + +// @Title registerUser +// @Description registers new user +// @Router /register [post] +// @Accept json +// @Success 201 {object} +// @Failure 400 {object} +func registerUserHandler(deps Dependencies) http.HandlerFunc { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + + // reading data from body + reqBody, err := ioutil.ReadAll(req.Body) + if err != nil { + logger.WithField("err", err.Error()).Error("Error in reading request body") + rw.WriteHeader(http.StatusInternalServerError) + return + } + user := db.User{} + err = json.Unmarshal(reqBody, &user) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while Unmarshalling request json") + rw.WriteHeader(http.StatusInternalServerError) + return + } + + // Getting user by email to check if user is already present in db + _, err = deps.Store.GetUserByEmail(req.Context(), user.Email) + + // If error is nil then user is already registered + if err == nil { + e := errorResponse{ + Error: "user already registered", + } + respBytes, err := json.Marshal(e) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while marshalling error msg ") + rw.WriteHeader(http.StatusInternalServerError) + return + } + rw.Header().Add("Content-Type", "application/json") + rw.WriteHeader(http.StatusBadRequest) + rw.Write(respBytes) + return + } + + // For checking error occured while looking already registered user + if err != nil && err != sql.ErrNoRows { + logger.WithField("err", err.Error()).Error("Error while looking existing user") + rw.WriteHeader(http.StatusInternalServerError) + return + } + + // Storing new user's data in database + _, err = deps.Store.CreateUser(req.Context(), user) + if err != nil { + logger.WithField("err", err.Error()).Error("Error in inserting user in database") + rw.WriteHeader(http.StatusInternalServerError) + return + } + + msg := successResponse{ + Message: "user successfully registered", + } + respBytes, err := json.Marshal(msg) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while marshalling success msg ") + rw.WriteHeader(http.StatusInternalServerError) + return + } + rw.Header().Add("Content-Type", "application/json") + rw.WriteHeader(http.StatusCreated) + rw.Write(respBytes) + return + + }) +} diff --git a/service/user_http_test.go b/service/user_http_test.go index 4e2b367..66de7b8 100644 --- a/service/user_http_test.go +++ b/service/user_http_test.go @@ -1,16 +1,17 @@ package service import ( - "errors" + "database/sql" "joshsoftware/go-e-commerce/db" "net/http" "net/http/httptest" "strings" "testing" + "github.com/stretchr/testify/mock" + "github.com/gorilla/mux" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) @@ -30,43 +31,6 @@ 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, - ) - - recorder := makeHTTPCall( - http.MethodGet, - "/users", - "", - listUsersHandler(Dependencies{Store: suite.dbMock}), - ) - - assert.Equal(suite.T(), http.StatusOK, recorder.Code) - assert.Equal(suite.T(), `[{"full_name":"test-user","age":18}]`, recorder.Body.String()) - suite.dbMock.AssertExpectations(suite.T()) -} - -func (suite *UsersHandlerTestSuite) TestListUsersWhenDBFailure() { - suite.dbMock.On("ListUsers", mock.Anything).Return( - []db.User{}, - errors.New("error fetching user records"), - ) - - recorder := makeHTTPCall( - http.MethodGet, - "/users", - "", - listUsersHandler(Dependencies{Store: suite.dbMock}), - ) - - assert.Equal(suite.T(), http.StatusInternalServerError, recorder.Code) - suite.dbMock.AssertExpectations(suite.T()) -} - func makeHTTPCall(method, path, body string, handlerFunc http.HandlerFunc) (recorder *httptest.ResponseRecorder) { // create a http request using the given parameters req, _ := http.NewRequest(method, path, strings.NewReader(body)) @@ -82,3 +46,79 @@ func makeHTTPCall(method, path, body string, handlerFunc http.HandlerFunc) (reco router.ServeHTTP(recorder, req) return } + +func (suite *UsersHandlerTestSuite) TestRegisterUserSuccess() { + user := db.User{} + user.Email = "test@gmail.com" + user.FirstName = "test1" + user.LastName = "test2" + user.Mobile = "8421987856" + user.Address = "abc" + user.State = "Maharashtra" + user.City = "Nashik" + user.Password = "password" + user.Country = "India" + + suite.dbMock.On("CreateUser", mock.Anything, user).Return(user, nil) + suite.dbMock.On("GetUserByEmail", mock.Anything, mock.Anything).Return(user, sql.ErrNoRows) + body := + `{ + "first_name" : "test1", + "last_name" : "test2", + "email" : "test@gmail.com", + "mobile": "8421987856", + "country": "India", + "state": "Maharashtra", + "city": "Nashik", + "address": "abc", + "password": "password" + }` + recorder := makeHTTPCall(http.MethodPost, + "/register", + body, + registerUserHandler(Dependencies{Store: suite.dbMock}), + ) + + assert.Equal(suite.T(), http.StatusCreated, recorder.Code) + suite.dbMock.AssertExpectations(suite.T()) + +} + +func (suite *UsersHandlerTestSuite) TestRegisterUserFailure() { + user := db.User{} + user.Email = "test@gmail.com" + user.FirstName = "test1" + user.LastName = "test2" + user.Mobile = "8421987856" + user.Address = "abc" + user.State = "Maharashtra" + user.City = "Nashik" + user.Password = "password" + user.Country = "India" + + suite.dbMock.On("CreateUser", mock.Anything, user).Return(user, nil) + suite.dbMock.On("GetUserByEmail", mock.Anything, user.Email).Return(user, nil) + body := + `{ + "first_name" : "test1", + "last_name" : "test2", + "email" : "test@gmail.com", + "mobile": "8421987856", + "country": "India", + "state": "Maharashtra", + "city": "Nashik", + "address": "abc", + "password": "password" + }` + recorder := makeHTTPCall(http.MethodPost, + "/register", + body, + registerUserHandler(Dependencies{Store: suite.dbMock}), + ) + + assert.Equal(suite.T(), `{"error":"user already registered"}`, recorder.Body.String()) + assert.Equal(suite.T(), http.StatusBadRequest, recorder.Code) + suite.dbMock.AssertNotCalled(suite.T(), "CreateUser", mock.Anything, user) + suite.dbMock.AssertCalled(suite.T(), "GetUserByEmail", mock.Anything, user.Email) + +}