Skip to content

Commit

Permalink
feat: implement login by phone (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
Salaton committed Nov 2, 2021
1 parent 438246d commit bb4d482
Show file tree
Hide file tree
Showing 19 changed files with 379 additions and 14 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# MyCareHub service

![Linting and Tests](https://github.com/savannahghi/mycarehub/actions/workflows/ci.yml/badge.svg)
[![Coverage Status](https://coveralls.io/repos/github/savannahghi/mycarehub/badge.svg?branch=main)](https://coveralls.io/github/savannahghi/mycarehub?branch=main)
[![Coverage Status](https://coveralls.io/repos/github/savannahghi/mycarehub/badge.svg)](https://coveralls.io/github/savannahghi/mycarehub)

This service contains the implementation of the mycarehub project.

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/lib/pq v1.10.3
github.com/savannahghi/converterandformatter v0.0.9
github.com/savannahghi/enumutils v0.0.3
github.com/savannahghi/errorcodeutil v0.0.1
github.com/savannahghi/feedlib v0.0.4
github.com/savannahghi/firebasetools v0.0.15
github.com/savannahghi/interserviceclient v0.0.13
Expand All @@ -39,4 +40,5 @@ require (
gorm.io/datatypes v1.0.2
gorm.io/driver/postgres v1.1.2
gorm.io/gorm v1.21.16
moul.io/http2curl v1.0.0 // indirect
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
Expand Down Expand Up @@ -473,6 +474,7 @@ github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
Expand Down Expand Up @@ -681,7 +683,9 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
Expand Down Expand Up @@ -1328,6 +1332,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8=
moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
Expand Down
6 changes: 3 additions & 3 deletions pkg/mycarehub/application/dto/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ type PinInput struct {

// LoginInput represents the Login input data structure
type LoginInput struct {
UserID string `json:"userID"`
PIN string `json:"pin"`
Flavour feedlib.Flavour `json:"flavour"`
PhoneNumber *string `json:"phoneNumber"`
PIN *string `json:"pin"`
Flavour feedlib.Flavour `json:"flavour"`
}

// StaffProfileInput contains input required to register a staff
Expand Down
30 changes: 30 additions & 0 deletions pkg/mycarehub/application/exceptions/custom_errors.go
Original file line number Diff line number Diff line change
@@ -1 +1,31 @@
package exceptions

import "github.com/savannahghi/errorcodeutil"

// NormalizeMSISDNError returns an error when normalizing the msisdn fails
func NormalizeMSISDNError(err error) error {
return &errorcodeutil.CustomError{
Err: err,
Message: NormalizeMSISDNErrMsg,
Code: int(errorcodeutil.Internal),
}
}

// PinMismatchError displays an error when the supplied PIN
// does not match the PIN stored
func PinMismatchError(err error) error {
return &errorcodeutil.CustomError{
Err: err,
Message: PINMismatchErrMsg,
Code: int(errorcodeutil.PINMismatch),
}
}

// ExpiredPinError displays an error when the pin
// is expired
func ExpiredPinError() error {
return &errorcodeutil.CustomError{
Message: ExpiredPinErrMsg,
Code: 10,
}
}
15 changes: 15 additions & 0 deletions pkg/mycarehub/application/exceptions/error_messages.go
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
package exceptions

const (
// NormalizeMSISDNErrMsg is the error message displayed when
// normalize the msisdn(phone number) fails
NormalizeMSISDNErrMsg = "unable to normalize the phonenumber"

// PINMismatchErrMsg is the error message displayed when
// the user supplied PIN number does not match the PIN
// record we have stored
PINMismatchErrMsg = "wrong PIN credentials supplied"

// ExpiredPinErrMsg is the error message displayed when the user supplied pin
// has expired
ExpiredPinErrMsg = "the user pin has expired"
)
23 changes: 23 additions & 0 deletions pkg/mycarehub/infrastructure/database/postgres/gorm/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package gorm_test

import (
"log"
"os"
"testing"

"github.com/savannahghi/mycarehub/pkg/mycarehub/infrastructure/database/postgres/gorm"
)

var testingDB *gorm.PGInstance

func TestMain(m *testing.M) {
log.Println("setting up test database")
var err error
testingDB, err = gorm.NewPGInstance()
if err != nil {
os.Exit(1)
}

log.Printf("Running tests ...")
os.Exit(m.Run())
}
5 changes: 3 additions & 2 deletions pkg/mycarehub/infrastructure/database/postgres/gorm/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,12 @@ func (db *PGInstance) RegisterClient(

// SavePin saves the pin to the database
func (db *PGInstance) SavePin(ctx context.Context, pinData *PINData) (bool, error) {
if pinData == nil {
return false, fmt.Errorf("nil pin data provided")
}
err := db.DB.Create(pinData).Error

if err != nil {
return false, fmt.Errorf("failed to save pin data: %v", err)
}

return true, nil
}
69 changes: 69 additions & 0 deletions pkg/mycarehub/infrastructure/database/postgres/gorm/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package gorm_test

import (
"context"
"testing"
"time"

"github.com/google/uuid"
"github.com/savannahghi/feedlib"
"github.com/savannahghi/mycarehub/pkg/mycarehub/infrastructure/database/postgres/gorm"
)

func TestPGInstance_SavePin(t *testing.T) {
ctx := context.Background()

type args struct {
ctx context.Context
pinData *gorm.PINData
}

id := uuid.New().String()
pinPayload := &gorm.PINData{
PINDataID: &id,
UserID: id,
HashedPIN: "1234",
ValidFrom: time.Now(),
ValidTo: time.Now(),
IsValid: true,
Flavour: feedlib.FlavourConsumer,
Salt: "1234",
}
tests := []struct {
name string
args args
want bool
wantErr bool
}{
{
name: "Happy Case - Successfully save pin",
args: args{
ctx: ctx,
pinData: pinPayload,
},
want: true,
wantErr: false,
},
{
name: "Sad Case - Fail to save pin",
args: args{
ctx: ctx,
pinData: nil,
},
want: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := testingDB.SavePin(tt.args.ctx, tt.args.pinData)
if (err != nil) != tt.wantErr {
t.Errorf("PGInstance.SavePin() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("PGInstance.SavePin() = %v, want %v", got, tt.want)
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package mock
import (
"context"
"strconv"
"time"

"github.com/brianvoe/gofakeit"
"github.com/google/uuid"
"github.com/savannahghi/enumutils"
"github.com/savannahghi/feedlib"
"github.com/savannahghi/mycarehub/pkg/mycarehub/application/enums"
"github.com/savannahghi/mycarehub/pkg/mycarehub/domain"
"github.com/savannahghi/mycarehub/pkg/mycarehub/infrastructure/database/postgres/gorm"
Expand All @@ -23,6 +25,7 @@ type GormMock struct {
MockGetUserProfileByPhoneNumberFn func(ctx context.Context, phoneNumber string) (*gorm.User, error)
MockSavePinFn func(ctx context.Context, pinData *gorm.PINData) (bool, error)
MockListFacilitiesFn func(ctx context.Context, searchTerm *string, filter []*domain.FiltersParam, pagination domain.FacilityPage) (*domain.FacilityPage, error)
MockGetUserPINByUserIDFn func(ctx context.Context, userID string) (*gorm.PINData, error)
}

// NewGormMock initializes a new instance of `GormMock` then mocking the case of success.
Expand All @@ -34,14 +37,14 @@ func NewGormMock() *GormMock {
In this section, you find commonly shared success case structs for mock tests
*/

facilityID := uuid.New().String()
ID := uuid.New().String()
name := gofakeit.Name()
code := "KN001"
county := enums.CountyTypeNairobi
description := gofakeit.HipsterSentence(15)

facility := &gorm.Facility{
FacilityID: &facilityID,
FacilityID: &ID,
Name: name,
Code: code,
Active: strconv.FormatBool(true),
Expand Down Expand Up @@ -79,7 +82,7 @@ func NewGormMock() *GormMock {
},
Facilities: []domain.Facility{
{
ID: &facilityID,
ID: &ID,
Name: name,
Code: code,
Active: true,
Expand All @@ -89,6 +92,16 @@ func NewGormMock() *GormMock {
},
}

pinData := &gorm.PINData{
PINDataID: &ID,
UserID: gofakeit.UUID(),
HashedPIN: uuid.New().String(),
ValidFrom: time.Now(),
ValidTo: time.Now(),
IsValid: true,
Flavour: feedlib.FlavourConsumer,
}

return &GormMock{
MockSavePinFn: func(ctx context.Context, pinData *gorm.PINData) (bool, error) {
return true, nil
Expand Down Expand Up @@ -126,6 +139,9 @@ func NewGormMock() *GormMock {
MockListFacilitiesFn: func(ctx context.Context, searchTerm *string, filter []*domain.FiltersParam, pagination domain.FacilityPage) (*domain.FacilityPage, error) {
return facilitiesPage, nil
},
MockGetUserPINByUserIDFn: func(ctx context.Context, userID string) (*gorm.PINData, error) {
return pinData, nil
},
}
}

Expand Down Expand Up @@ -177,3 +193,8 @@ func (gm *GormMock) SavePin(ctx context.Context, pinData *gorm.PINData) (bool, e
func (gm *GormMock) ListFacilities(ctx context.Context, searchTerm *string, filter []*domain.FiltersParam, pagination domain.FacilityPage) (*domain.FacilityPage, error) {
return gm.MockListFacilitiesFn(ctx, searchTerm, filter, pagination)
}

// GetUserPINByUserID mocks the implementation of retrieving a user pin by user ID
func (gm *GormMock) GetUserPINByUserID(ctx context.Context, userID string) (*gorm.PINData, error) {
return gm.MockGetUserPINByUserIDFn(ctx, userID)
}
10 changes: 10 additions & 0 deletions pkg/mycarehub/infrastructure/database/postgres/gorm/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Query interface {
GetFacilities(ctx context.Context) ([]Facility, error)
GetUserProfileByPhoneNumber(ctx context.Context, phoneNumber string) (*User, error)
ListFacilities(ctx context.Context, searchTerm *string, filter []*domain.FiltersParam, pagination domain.FacilityPage) (*domain.FacilityPage, error)
GetUserPINByUserID(ctx context.Context, userID string) (*PINData, error)
}

// RetrieveFacility fetches a single facility
Expand Down Expand Up @@ -105,3 +106,12 @@ func (db *PGInstance) ListFacilities(

return &pagination, nil
}

// GetUserPINByUserID fetches a user's pin using the user ID
func (db *PGInstance) GetUserPINByUserID(ctx context.Context, userID string) (*PINData, error) {
var pin PINData
if err := db.DB.Where(&PINData{UserID: userID, IsValid: true}).First(&pin).Error; err != nil {
return nil, fmt.Errorf("failed to get pin: %v", err)
}
return &pin, nil
}
17 changes: 17 additions & 0 deletions pkg/mycarehub/infrastructure/database/postgres/mappers.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,20 @@ func (d *MyCareHubDb) mapProfileObjectToDomain(profileObject *gorm.User) *domain

return user
}

// mapPINObjectToDomain maps the db pin data to a domain model.
func (d *MyCareHubDb) mapPINObjectToDomain(pinObj *gorm.PINData) *domain.UserPIN {
if pinObj == nil {
return nil
}

return &domain.UserPIN{
UserID: pinObj.UserID,
HashedPIN: pinObj.HashedPIN,
ValidFrom: pinObj.ValidFrom,
ValidTo: pinObj.ValidTo,
Flavour: pinObj.Flavour,
IsValid: pinObj.IsValid,
Salt: pinObj.Salt,
}
}
10 changes: 10 additions & 0 deletions pkg/mycarehub/infrastructure/database/postgres/pg_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,13 @@ func (d *MyCareHubDb) ListFacilities(
}
return facilities, nil
}

// GetUserPINByUserID fetches a user pin by the user ID
func (d *MyCareHubDb) GetUserPINByUserID(ctx context.Context, userID string) (*domain.UserPIN, error) {
pinData, err := d.query.GetUserPINByUserID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed query and retrieve user PIN data by user ID: %s", err)
}

return d.mapPINObjectToDomain(pinData), nil
}
Loading

0 comments on commit bb4d482

Please sign in to comment.