diff --git a/README.md b/README.md index cc7a90e..60074f5 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,14 @@ Check out documentation [here](https://aryana101a.github.io/legoshichat-backend/ - [ ] CI - [ ] CD -## Steps To Run(WIP) +## Steps To Run +1. `docker run --name gofr-pgsql -e POSTGRES_DB=legoshichat -e POSTGRES_USER=legoshiuser -e POSTGRES_PASSWORD=legoshipass -p 2006:5432 -d postgres:latest` + +2. `go run .` + +**To monitor database** +`docker exec -it gofr-pgsql psql --username=legoshiuser --dbname=legoshichat` diff --git a/go.mod b/go.mod index 2bcf2ba..74e976f 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,10 @@ go 1.21 toolchain go1.21.5 require ( + github.com/go-playground/validator v9.31.0+incompatible + github.com/golang-jwt/jwt/v5 v5.2.0 github.com/google/uuid v1.5.0 + github.com/stretchr/testify v1.8.4 gofr.dev v1.0.2 golang.org/x/crypto v0.16.0 ) @@ -51,14 +54,12 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator v9.31.0+incompatible // indirect github.com/go-redis/redis/extra/rediscmd v0.2.0 // indirect github.com/go-redis/redis/extra/redisotel v0.3.0 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/gocql/gocql v1.6.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect - github.com/golang-jwt/jwt/v5 v5.2.0 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -106,6 +107,7 @@ require ( github.com/openzipkin/zipkin-go v0.4.2 // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.17.0 // indirect github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/prometheus/common v0.44.0 // indirect @@ -145,7 +147,9 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect google.golang.org/grpc v1.59.0 // indirect google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/mysql v1.4.5 // indirect gorm.io/driver/postgres v1.4.8 // indirect gorm.io/driver/sqlite v1.4.4 // indirect diff --git a/go.sum b/go.sum index 2a11a1a..b5333f1 100644 --- a/go.sum +++ b/go.sum @@ -176,7 +176,6 @@ github.com/gocql/gocql v1.6.0 h1:IdFdOTbnpbd0pDhl4REKQDM+Q0SzKXQ1Yh+YZZ8T/qU= github.com/gocql/gocql v1.6.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= @@ -640,9 +639,12 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= diff --git a/handler/handler.go b/handler/handler.go index 709a457..b00887b 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -10,17 +10,30 @@ import ( "github.com/aryanA101a/legoshichat-backend/store" "github.com/go-playground/validator" "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" "gofr.dev/pkg/gofr" "gofr.dev/pkg/gofr/types" "golang.org/x/crypto/bcrypt" ) +type Creator interface { + NewAccount(name string, phoneNumber uint64, password string) (*model.Account, error) + NewJWT(ctx *gofr.Context, userId string) (string, error) +} + +type authCreator struct{} + type handler struct { - auth store.AuthStore + auth store.AuthStore + authCreator Creator +} + +func New(a store.AuthStore,c Creator) handler { + return handler{auth: a, authCreator: c} } -func New(a store.AuthStore) handler { - return handler{auth: a} +func NewCreator()authCreator{ + return authCreator{} } func (h handler) HandleCreateAccount(ctx *gofr.Context) (interface{}, error) { @@ -32,7 +45,7 @@ func (h handler) HandleCreateAccount(ctx *gofr.Context) (interface{}, error) { return nil, e.HttpStatusError(400, "Invalid inputs or missing required fields"+err.Error()) } - newAccount, err := model.NewAccount(accountRequest.Name, accountRequest.PhoneNumber, accountRequest.Password) + newAccount, err := h.authCreator.NewAccount(accountRequest.Name, accountRequest.PhoneNumber, accountRequest.Password) if err != nil { return nil, e.HttpStatusError(500, "") } @@ -43,7 +56,7 @@ func (h handler) HandleCreateAccount(ctx *gofr.Context) (interface{}, error) { return nil, e.HttpStatusError(500, err.Error()) } - token, err := createJWT(ctx, user.ID) + token, err := h.authCreator.NewJWT(ctx, user.ID) if err != nil { ctx.Logger.Error(err) return nil, e.HttpStatusError(500, "") @@ -75,7 +88,7 @@ func (h handler) HandleLogin(ctx *gofr.Context) (interface{}, error) { return nil, e.HttpStatusError(401, "Invalid password") } - token, err := createJWT(ctx, account.ID) + token, err := h.authCreator.NewJWT(ctx, account.ID) if err != nil { ctx.Logger.Error(err) return nil, e.HttpStatusError(500, "") @@ -83,7 +96,21 @@ func (h handler) HandleLogin(ctx *gofr.Context) (interface{}, error) { return types.Raw{Data: model.AuthResponse{User: model.User{ID: account.ID, Name: account.Name, PhoneNumber: account.PhoneNumber}, Token: token}}, nil } -func createJWT(ctx *gofr.Context, userId string) (string, error) { +func (authCreator) NewAccount(name string, phoneNumber uint64, password string) (*model.Account, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + + return &model.Account{ + ID: uuid.New().String(), + Name: name, + PhoneNumber: phoneNumber, + Password: string(hashedPassword), + }, nil +} + +func (authCreator) NewJWT(ctx *gofr.Context, userId string) (string, error) { claims := &jwt.MapClaims{ "id": userId, "expiresAt": time.Now().Add(time.Duration(time.Duration.Hours(24) * 5)), diff --git a/handler/handler_test.go b/handler/handler_test.go new file mode 100644 index 0000000..0d647f5 --- /dev/null +++ b/handler/handler_test.go @@ -0,0 +1,230 @@ +package handler + +import ( + "bytes" + "database/sql" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/request" + "gofr.dev/pkg/gofr/responder" + "gofr.dev/pkg/gofr/types" + + e "github.com/aryanA101a/legoshichat-backend/error" + "github.com/aryanA101a/legoshichat-backend/model" + "github.com/aryanA101a/legoshichat-backend/store" + "github.com/stretchr/testify/assert" +) + +type mockAuthStore struct { +} + +type createAccountTCmockAuthCreator struct { +} +type jwtErrorTCmockAuthCreator struct { +} + +func newMockAuthStore() store.AuthStore { + return mockAuthStore{} +} + + +func (mockAuthStore) CreateAccount(ctx *gofr.Context, account model.Account) (*model.User, error) { + + return &model.User{ID: account.ID, Name: account.Name, PhoneNumber: account.PhoneNumber}, nil + +} + +func (mockAuthStore) FetchAccount(ctx *gofr.Context, loginRequest model.LoginRequest) (*model.Account, error) { + + return &model.Account{Password: "$2a$10$hlaojbP2Ix5ZLFa64fPmgO3tyvH.jdBDVtq8veZyQMOWohcAndlpS"}, nil +} + +func (createAccountTCmockAuthCreator) NewAccount(name string, phoneNumber uint64, password string) (*model.Account, error) { +fmt.Println("t3mockAuthCreator called") + return nil, e.NewError("") +} + +func (createAccountTCmockAuthCreator) NewJWT(ctx *gofr.Context, userId string) (string, error) { + return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHBpcmVzQXQiOiIyMDIzLTEyLTE0VDE1OjA5OjA1LjY2MzE5NjA1NiswNTozMCIsImlkIjoiY2E0MGZkMmItZjlhMi00NWQ5LWE2ZjctNDFjZGFiNWVjMDFlIn0.2Wp_KL-rM7RMQmWCCLvtHrG5M-zVuGFO-7PBLzDAbJ4",nil +} + +func (jwtErrorTCmockAuthCreator) NewAccount(name string, phoneNumber uint64, password string) (*model.Account, error) { + + return &model.Account{}, nil +} + +func (jwtErrorTCmockAuthCreator) NewJWT(ctx *gofr.Context, userId string) (string, error) { + return "", e.NewError("") +} + +type testCase struct { + desc string + body []byte + expected interface{} + err error +} + +func TestHandleCreateAccount(t *testing.T) { + + app := gofr.New() + + successfulTC := testCase{ + desc: "create account success", + body: []byte(`{"name":"TestUser4","phoneNumber":4234324890,"password":"securepassword"}`), + expected: types.Raw{Data: model.AuthResponse{}}, + err: nil, + } + + invalidRequestBodyTC := testCase{ + desc: "invalid request body", + body: []byte(`invalidjson`), + err: e.NewError(""), + } + + createAccountErrorTC := testCase{ + desc: "create account error", + body: []byte(`{"name":"TestUser4","phoneNumber":4234324890,"password":"securepassword"}`), + err: e.NewError(""), + } + + jwtErrorTC := testCase{ + desc: "create account JWT error", + body: []byte(`{"name":"TestUser","phoneNumber":"1234567890","password":"securepassword"}`), + err: e.NewError(""), + } + + runTest(t, successfulTC, app, New(newMockAuthStore(), NewCreator())) + runTest(t, invalidRequestBodyTC, app, New(newMockAuthStore(), NewCreator())) + runTest(t, createAccountErrorTC, app, New(newMockAuthStore(), createAccountTCmockAuthCreator{})) + runTest(t, jwtErrorTC, app, New(newMockAuthStore(), jwtErrorTCmockAuthCreator{})) + + +} + +func runTest(t *testing.T, tc testCase, app *gofr.Gofr, h handler) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "http://dummy", bytes.NewReader(tc.body)) + + req := request.NewHTTPRequest(r) + res := responder.NewContextualResponder(w, r) + ctx := gofr.NewContext(res, req, app) + + result, err := h.HandleCreateAccount(ctx) + + fmt.Println("errr", tc.expected,err) + + if tc.err != nil { + assert.Error(t, err, "TEST: %s: unexpected error", tc.desc) + } else { + assert.NoError(t, err, "TEST: Unexpected Error: %s", tc.desc) + } + + assert.IsType(t, tc.expected, result, "TEST: %s: unexpected result type", tc.desc) +} + + +type fetchAccountErrorTcmockAuthStore struct{} + +func (fetchAccountErrorTcmockAuthStore) CreateAccount(ctx *gofr.Context, account model.Account) (*model.User, error) { + + return &model.User{ID: account.ID, Name: account.Name, PhoneNumber: account.PhoneNumber}, nil + +} + +func (fetchAccountErrorTcmockAuthStore) FetchAccount(ctx *gofr.Context, loginRequest model.LoginRequest) (*model.Account, error) { + + return nil, e.NewError("") +} + +type userNotExistsTCmockAuthStore struct{} + +func (userNotExistsTCmockAuthStore) CreateAccount(ctx *gofr.Context, account model.Account) (*model.User, error) { + + return &model.User{ID: account.ID, Name: account.Name, PhoneNumber: account.PhoneNumber}, nil + +} + +func (userNotExistsTCmockAuthStore) FetchAccount(ctx *gofr.Context, loginRequest model.LoginRequest) (*model.Account, error) { + + return nil, sql.ErrNoRows +} +func TestHandleLogin(t *testing.T) { + app := gofr.New() + + successfulLoginTC := testCase{ + desc: "login success", + body: []byte(`{"phoneNumber":1234567890,"password":"unittestpass"}`), + expected: types.Raw{ + Data: model.AuthResponse{ + // User: model.User{ID: "testUserID", Name: "TestUser", PhoneNumber: 4234324890}, + // Token: "testToken", + }, + }, + err: nil, + } + + invalidRequestBodyTC := testCase{ + desc: "invalid request body", + body: []byte(`invalidjson`), + err: e.NewError(""), + } + + fetchAccountErrorTC := testCase{ + desc: "fetch account error", + body: []byte(`{"phoneNumber":1234567890,"password":"securepassword"}`), + err: e.HttpStatusError(500, ""), + } + + userNotExistsTC := testCase{ + desc: "user does not exist", + body: []byte(`{"phoneNumber":1234567892,"password":"securepassword"}`), + err: e.HttpStatusError(401, "User does not exist"), + } + + invalidPasswordTC := testCase{ + desc: "invalid password", + body: []byte(`{"phoneNumber":1234567890,"password":"wrongpassword"}`), + err: e.HttpStatusError(401, "Invalid password"), + } + + jwtErrorTC := testCase{ + desc: "login JWT error", + body: []byte(`{"phoneNumber":1234567890,"password":"unittestpass"}`), + err: e.NewError(""), + } + + // Add more test cases as needed + + runLoginTest(t, successfulLoginTC, app, New(newMockAuthStore(), NewCreator(), )) + runLoginTest(t, invalidRequestBodyTC, app, New(newMockAuthStore(), NewCreator())) + runLoginTest(t, fetchAccountErrorTC, app, New(fetchAccountErrorTcmockAuthStore{}, NewCreator(), )) + runLoginTest(t, userNotExistsTC, app, New(userNotExistsTCmockAuthStore{}, NewCreator(),)) + runLoginTest(t, invalidPasswordTC, app, New(newMockAuthStore(), NewCreator(),)) + runLoginTest(t, jwtErrorTC, app, New(newMockAuthStore(), jwtErrorTCmockAuthCreator{},)) +} + + +func runLoginTest(t *testing.T, tc testCase, app *gofr.Gofr, h handler) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "http://dummy", bytes.NewReader(tc.body)) + + req := request.NewHTTPRequest(r) + res := responder.NewContextualResponder(w, r) + ctx := gofr.NewContext(res, req, app) + + result, err := h.HandleLogin(ctx) + + fmt.Println("errr", tc.expected, err) + + if tc.err != nil { + assert.Error(t, err, "TEST: %s: unexpected error", tc.desc) + } else { + assert.NoError(t, err, "TEST: Unexpected Error: %s", tc.desc) + } + + assert.IsType(t, tc.expected, result, "TEST: %s: unexpected result type", tc.desc) +} \ No newline at end of file diff --git a/main.go b/main.go index f9a56b9..568f8d1 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,8 @@ func main() { app := gofr.New() authStore := store.NewAuthStore(app.DB()) - handler := handler.New(authStore) + authCreator:=handler.NewCreator() + handler := handler.New(authStore,authCreator) app.POST("/create-account", handler.HandleCreateAccount) diff --git a/model/auth.go b/model/auth.go index 909b4f9..1e26941 100644 --- a/model/auth.go +++ b/model/auth.go @@ -1,9 +1,6 @@ package model -import ( - "github.com/google/uuid" - "golang.org/x/crypto/bcrypt" -) + type AuthResponse struct { User User `json:"user"` @@ -30,18 +27,11 @@ type User struct { PhoneNumber uint64 `json:"phoneNumber"` } -func NewAccount(name string, phoneNumber uint64, password string) (*Account, error) { - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return nil, err - } - - return &Account{ - ID: uuid.New().String(), - Name: name, - PhoneNumber: phoneNumber, - Password: string(hashedPassword), - }, nil -} + + + + + +