Skip to content

Commit

Permalink
chore: unit testing wip
Browse files Browse the repository at this point in the history
chore: refactoring & update readme
  • Loading branch information
aryanA101a committed Dec 15, 2023
1 parent b01351a commit 38dcc44
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 29 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`



8 changes: 6 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
41 changes: 34 additions & 7 deletions handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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, "")
}
Expand All @@ -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, "")
Expand Down Expand Up @@ -75,15 +88,29 @@ 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, "")
}
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)),
Expand Down
230 changes: 230 additions & 0 deletions handler/handler_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 38dcc44

Please sign in to comment.