Skip to content

Commit

Permalink
feat: implement reset pin (#52)
Browse files Browse the repository at this point in the history
This functionality can be used by admins or healthcare workers to generate and send
a new PIN for a client or other user. The new PIN is generated automatically.
  • Loading branch information
Salaton committed Oct 29, 2021
1 parent a6f7e15 commit 62a1d52
Show file tree
Hide file tree
Showing 14 changed files with 185 additions and 8 deletions.
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 @@ -40,4 +41,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 @@ -340,6 +340,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 @@ -475,6 +476,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 @@ -683,7 +685,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 @@ -1330,6 +1334,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: 6 additions & 0 deletions pkg/onboarding/application/dto/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,9 @@ type SMSPayload struct {
Message string `json:"message"`
Sender enumutils.SenderID `json:"sender"`
}

// ResetPinInput payload to set or change PIN information
type ResetPinInput struct {
UserID string `json:"userID"`
Flavour feedlib.Flavour `json:"flavour"`
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type GormMock struct {
reason enums.TransferReason,
notes string,
) (bool, error)
InvalidatePINFn func(ctx context.Context, userID string) error
}

// NewGormMock initializes a new instance of `GormMock` then mocking the case of success.
Expand Down Expand Up @@ -304,6 +305,10 @@ func NewGormMock() *GormMock {
TransferClientFn: func(ctx context.Context, clientID, originFacilityID, destinationFacilityID string, reason enums.TransferReason, notes string) (bool, error) {
return true, nil
},

InvalidatePINFn: func(ctx context.Context, userID string) error {
return nil
},
}
}

Expand Down Expand Up @@ -420,3 +425,8 @@ func (gm *GormMock) TransferClient(
) (bool, error) {
return gm.TransferClientFn(ctx, clientID, originFacilityID, destinationFacilityID, reason, notes)
}

// InvalidatePIN mocks the invalidate pin implementation
func (gm *GormMock) InvalidatePIN(ctx context.Context, userID string) error {
return gm.InvalidatePINFn(ctx, userID)
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (db *PGInstance) GetUserProfileByUserID(ctx context.Context, userID string,
// GetUserPINByUserID fetches a user profile facility 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}).First(&pin).Error; err != nil {
if err := db.DB.Where(&PINData{UserID: userID, IsValid: true}).First(&pin).Error; err != nil {
return nil, fmt.Errorf("failed to get facility by MFL Code %v: %v", userID, err)
}
return &pin, nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Update interface {
reason enums.TransferReason,
notes string,
) (bool, error)
InvalidatePIN(ctx context.Context, userID string) error
}

// UpdateUserLastSuccessfulLogin updates users last successful login time
Expand Down Expand Up @@ -198,3 +199,8 @@ func (db *PGInstance) TransferClient(

return true, nil
}

// InvalidatePIN toggles the valid field of a pin from true to false
func (db *PGInstance) InvalidatePIN(ctx context.Context, userID string) error {
return db.DB.Model(&PINData{}).Where(&PINData{UserID: userID, IsValid: true}).Select("IsValid").Updates(PINData{IsValid: false}).Error
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (d *OnboardingDb) SavePin(ctx context.Context, pinData *domain.UserPIN) (bo

_, err := d.create.SavePin(ctx, pinObj)
if err != nil {
return false, fmt.Errorf("failed to set user pin: %v", err)
return false, fmt.Errorf("failed to save user pin: %v", err)
}

return true, nil
Expand Down
6 changes: 6 additions & 0 deletions pkg/onboarding/infrastructure/database/postgres/pg_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import (
"github.com/savannahghi/onboarding-service/pkg/onboarding/infrastructure/database/postgres/gorm"
)

// InvalidatePIN invalidates a pin that is linked to the user profile.
// This is done by toggling the IsValid field to false
func (d *OnboardingDb) InvalidatePIN(ctx context.Context, userID string) error {
return d.update.InvalidatePIN(ctx, userID)
}

// UpdateUserLastSuccessfulLogin update the user with the last login time
func (d *OnboardingDb) UpdateUserLastSuccessfulLogin(ctx context.Context, userID string, lastLoginTime time.Time, flavour feedlib.Flavour) error {
return d.update.UpdateUserLastSuccessfulLogin(ctx, userID, lastLoginTime, flavour)
Expand Down
47 changes: 47 additions & 0 deletions pkg/onboarding/infrastructure/database/postgres/pg_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"
"time"

"github.com/google/uuid"
"github.com/savannahghi/enumutils"
"github.com/savannahghi/feedlib"
"github.com/savannahghi/onboarding-service/pkg/onboarding/application/dto"
Expand Down Expand Up @@ -311,3 +312,49 @@ func TestOnboardingDb_UpdateStaffUser(t *testing.T) {
})
}
}

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

type args struct {
ctx context.Context
userID string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Happy Case - Successfully invalidate pin",
args: args{
ctx: ctx,
userID: uuid.New().String(),
},
wantErr: false,
},
{
name: "Sad Case - Fail to invalidate pin",
args: args{
ctx: ctx,
userID: uuid.New().String(),
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var fakeGorm = gormmock.NewGormMock()
d := NewOnboardingDb(fakeGorm, fakeGorm, fakeGorm, fakeGorm)

if tt.name == "Sad Case - Fail to invalidate pin" {
fakeGorm.InvalidatePINFn = func(ctx context.Context, userID string) error {
return fmt.Errorf("failed to invalidate pin")
}
}
if err := d.InvalidatePIN(tt.args.ctx, tt.args.userID); (err != nil) != tt.wantErr {
t.Errorf("OnboardingDb.InvalidatePIN() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
6 changes: 6 additions & 0 deletions pkg/onboarding/infrastructure/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ type Update interface {
reason enums.TransferReason,
notes string,
) (bool, error)
InvalidatePIN(ctx context.Context, userID string) error
}

// ServiceUpdateImpl represents update user implementation object
Expand All @@ -194,6 +195,11 @@ func NewServiceUpdateImpl(on pg.OnboardingDb) Update {
}
}

// InvalidatePIN invalidates pin(s) that are linked to a user
func (u *ServiceUpdateImpl) InvalidatePIN(ctx context.Context, userID string) error {
return u.onboarding.InvalidatePIN(ctx, userID)
}

// UpdateUserLastSuccessfulLogin ...
func (u *ServiceUpdateImpl) UpdateUserLastSuccessfulLogin(ctx context.Context, userID string, lastLoginTime time.Time, flavour feedlib.Flavour) error {
return u.onboarding.UpdateUserLastSuccessfulLogin(ctx, userID, lastLoginTime, flavour)
Expand Down
7 changes: 6 additions & 1 deletion pkg/onboarding/presentation/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func Router(ctx context.Context) (*mux.Router, error) {
r.Use(serverutils.CustomHTTPRequestMetricsMiddleware())

// Shared unauthenticated routes
openSourcePresentation.SharedUnauthenticatedRoutes(h, r)
// openSourcePresentation.SharedUnauthenticatedRoutes(h, r)
// Shared authenticated ISC routes
openSourcePresentation.SharedAuthenticatedISCRoutes(h, r)
// Shared authenticated routes
Expand All @@ -150,6 +150,11 @@ func Router(ctx context.Context) (*mux.Router, error) {
Methods(http.MethodPost, http.MethodOptions).
HandlerFunc(internalHandlers.LoginHandler())

r.Path("/reset_pin").Methods(
http.MethodPost,
http.MethodOptions,
).HandlerFunc(internalHandlers.ResetPin())

// Graphql route
authR := r.Path("/graphql").Subrouter()
authR.Use(firebasetools.AuthenticationMiddleware(firebaseApp))
Expand Down
37 changes: 37 additions & 0 deletions pkg/onboarding/presentation/rest/handlers.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package rest

import (
"fmt"
"net/http"

"github.com/savannahghi/errorcodeutil"
"github.com/savannahghi/onboarding-service/pkg/onboarding/application/dto"
"github.com/savannahghi/onboarding-service/pkg/onboarding/infrastructure"
"github.com/savannahghi/onboarding-service/pkg/onboarding/presentation/interactor"
Expand All @@ -14,6 +16,7 @@ type OnboardingHandlersInterfaces interface {
//Collect metrics handler
CollectMetricsHandler() http.HandlerFunc
LoginHandler() http.HandlerFunc
ResetPin() http.HandlerFunc
}

// OnboardingHandlersInterfacesImpl represents the usecase implementation object
Expand Down Expand Up @@ -62,3 +65,37 @@ func (h *OnboardingHandlersInterfacesImpl) LoginHandler() http.HandlerFunc {
serverutils.WriteJSONResponse(w, response, http.StatusCreated)
}
}

// ResetPin is used to generate and send new pin to a user (client/staff)
func (h *OnboardingHandlersInterfacesImpl) ResetPin() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

resetPinPayload := &dto.ResetPinInput{}
serverutils.DecodeJSONToTargetStruct(w, r, resetPinPayload)

if resetPinPayload.UserID == "" || resetPinPayload.Flavour == "" {
err := fmt.Errorf("expected `userID` and `flavour` to be defines")
serverutils.WriteJSONResponse(
w,
errorcodeutil.CustomError{
Err: err,
Message: err.Error(),
},
http.StatusBadRequest,
)
return
}

response, err := h.interactor.UserUsecase.ResetPIN(
ctx,
resetPinPayload.UserID,
resetPinPayload.Flavour,
)
if err != nil {
serverutils.WriteJSONResponse(w, err, http.StatusBadRequest)
return
}
serverutils.WriteJSONResponse(w, response, http.StatusOK)
}
}
10 changes: 10 additions & 0 deletions pkg/onboarding/usecases/mock/usecase_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ type UpdateMock struct {
reason enums.TransferReason,
notes string,
) (bool, error)
InvalidatePINFn func(ctx context.Context, userID string) error
}

// NewUpdateMock initializes a new instance of `GormMock` then mocking the case of success.
Expand Down Expand Up @@ -322,6 +323,10 @@ func NewUpdateMock() *UpdateMock {
TransferClientFn: func(ctx context.Context, clientID, originFacilityID, destinationFacilityID string, reason enums.TransferReason, notes string) (bool, error) {
return true, nil
},

InvalidatePINFn: func(ctx context.Context, userID string) error {
return nil
},
}
}

Expand Down Expand Up @@ -362,6 +367,11 @@ func (um *UpdateMock) TransferClient(
return um.TransferClientFn(ctx, clientID, originFacilityID, destinationFacilityID, reason, notes)
}

// InvalidatePIN mocks the invalidate pin method
func (um *UpdateMock) InvalidatePIN(ctx context.Context, userID string) error {
return um.InvalidatePINFn(ctx, userID)
}

// DeleteMock ....
type DeleteMock struct{}

Expand Down
Loading

0 comments on commit 62a1d52

Please sign in to comment.