From 7a03ad604b3cb5e4c2b76e58cadd72892385e219 Mon Sep 17 00:00:00 2001 From: Otieno Calvine Date: Thu, 30 Sep 2021 15:46:37 +0300 Subject: [PATCH] chore: refactor validate email otp and add firebase integration test Signed-off-by: Otieno Calvine --- pkg/clinical/application/utils/helpers.go | 16 ++ .../application/utils/helpers_test.go | 40 +++++ pkg/clinical/domain/models.go | 2 +- .../datastore/firebase/firebase.go | 12 +- .../datastore/firebase/firebase_test.go | 166 ++++++++++++++++++ pkg/clinical/infrastructure/repository.go | 9 +- pkg/clinical/usecases/patient.go | 9 +- 7 files changed, 238 insertions(+), 16 deletions(-) create mode 100644 pkg/clinical/application/utils/helpers.go create mode 100644 pkg/clinical/application/utils/helpers_test.go create mode 100644 pkg/clinical/infrastructure/datastore/firebase/firebase_test.go diff --git a/pkg/clinical/application/utils/helpers.go b/pkg/clinical/application/utils/helpers.go new file mode 100644 index 00000000..e8fd851f --- /dev/null +++ b/pkg/clinical/application/utils/helpers.go @@ -0,0 +1,16 @@ +package utils + +import ( + "fmt" + + "github.com/asaskevich/govalidator" +) + +// ValidateEmail returns an error if the supplied string does not have a +// valid format or resolvable host +func ValidateEmail(email string) error { + if !govalidator.IsEmail(email) { + return fmt.Errorf("invalid email format") + } + return nil +} diff --git a/pkg/clinical/application/utils/helpers_test.go b/pkg/clinical/application/utils/helpers_test.go new file mode 100644 index 00000000..aaaa4f72 --- /dev/null +++ b/pkg/clinical/application/utils/helpers_test.go @@ -0,0 +1,40 @@ +package utils + +import ( + "testing" + + "github.com/savannahghi/firebasetools" +) + +func TestValidateEmail(t *testing.T) { + type args struct { + email string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "valid email address", + args: args{ + email: firebasetools.TestUserEmail, + }, + wantErr: false, + }, + { + name: "invalid email address", + args: args{ + email: "hey@notavalidemail", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ValidateEmail(tt.args.email); (err != nil) != tt.wantErr { + t.Errorf("ValidateEmail() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/clinical/domain/models.go b/pkg/clinical/domain/models.go index 6b08fddb..bd3c4af1 100644 --- a/pkg/clinical/domain/models.go +++ b/pkg/clinical/domain/models.go @@ -433,7 +433,7 @@ type PhoneNumberPayload struct { // EmailOptIn is used to persist and manage email communication whitelists type EmailOptIn struct { - Email string `json:"email" firestore:"optedIn"` + Email string `json:"email" firestore:"email"` OptedIn bool `json:"optedIn" firestore:"optedIn"` } diff --git a/pkg/clinical/infrastructure/datastore/firebase/firebase.go b/pkg/clinical/infrastructure/datastore/firebase/firebase.go index 9a2f2d52..90b3c292 100644 --- a/pkg/clinical/infrastructure/datastore/firebase/firebase.go +++ b/pkg/clinical/infrastructure/datastore/firebase/firebase.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/asaskevich/govalidator" "github.com/savannahghi/clinical/pkg/clinical/domain" "github.com/savannahghi/firebasetools" ) @@ -38,16 +37,13 @@ func (fr Repository) GetEmailOptInCollectionName() string { return suffixed } -// ValidateEmail returns an error if the supplied string does not have a -// valid format or resolvable host -func (fr Repository) ValidateEmail( +// SaveEmailOTP persist the data of the newly created OTP to a datastore +func (fr Repository) SaveEmailOTP( ctx context.Context, email string, optIn bool) error { - - if !govalidator.IsEmail(email) { - return fmt.Errorf("invalid email format") + if email == "" { + return fmt.Errorf("the email cannot be nil") } - if optIn { data := domain.EmailOptIn{ Email: email, diff --git a/pkg/clinical/infrastructure/datastore/firebase/firebase_test.go b/pkg/clinical/infrastructure/datastore/firebase/firebase_test.go new file mode 100644 index 00000000..23e19309 --- /dev/null +++ b/pkg/clinical/infrastructure/datastore/firebase/firebase_test.go @@ -0,0 +1,166 @@ +package firebase_test + +import ( + "context" + "fmt" + "log" + "os" + "testing" + "time" + + "cloud.google.com/go/firestore" + "firebase.google.com/go/auth" + "github.com/savannahghi/clinical/pkg/clinical/infrastructure" + fb "github.com/savannahghi/clinical/pkg/clinical/infrastructure/datastore/firebase" + "github.com/savannahghi/firebasetools" +) + +var testRepo *fb.Repository + +func TestMain(m *testing.M) { + log.Printf("Setting tests up ...") + envOriginalValue := os.Getenv("ENVIRONMENT") + os.Setenv("ENVIRONMENT", "staging") + debugEnvValue := os.Getenv("DEBUG") + os.Setenv("DEBUG", "true") + os.Setenv("REPOSITORY", "firebase") + collectionEnvValue := os.Getenv("ROOT_COLLECTION_SUFFIX") + os.Setenv("CLOUD_HEALTH_PUBSUB_TOPIC", "pubtopic") + os.Setenv("CLOUD_HEALTH_DATASET_ID", "datasetid") + os.Setenv("CLOUD_HEALTH_FHIRSTORE_ID", "fhirid") + + // !NOTE! + // Under no circumstances should you remove this env var when testing + // You risk purging important collections, like our prod collections + os.Setenv("ROOT_COLLECTION_SUFFIX", fmt.Sprintf("clinical_ci_%v", time.Now().Unix())) + ctx := context.Background() + r := fb.Repository{} // They are nil + fsc, fbc := InitializeTestFirebaseClient(ctx) + if fsc == nil { + log.Printf("failed to initialize test FireStore client") + return + } + if fbc == nil { + log.Printf("failed to initialize test FireBase client") + return + } + infra, err := InitializeTestFirebaseRepository() + if err != nil { + log.Printf("unable to initialize test repository: %v", err) + return + } + + testRepo = infra + + purgeRecords := func() { + collections := []string{ + r.GetEmailOptInCollectionName(), + } + for _, collection := range collections { + ref := fsc.Collection(collection) + firebasetools.DeleteCollection(ctx, fsc, ref, 10) + } + } + + // try clean up first + purgeRecords() + + // do clean up + log.Printf("Running tests ...") + code := m.Run() + + log.Printf("Tearing tests down ...") + purgeRecords() + + // restore environment varibles to original values + os.Setenv(envOriginalValue, "ENVIRONMENT") + os.Setenv("DEBUG", debugEnvValue) + os.Setenv("ROOT_COLLECTION_SUFFIX", collectionEnvValue) + + os.Exit(code) +} + +func InitializeTestInfrastructure(ctx context.Context) (infrastructure.Infrastructure, error) { + + return infrastructure.NewInfrastructureInteractor(), nil +} + +func InitializeTestFirebaseRepository() (*fb.Repository, error) { + ctx := context.Background() + fsc, fbc := InitializeTestFirebaseClient(ctx) + if fsc == nil { + log.Printf("failed to initialize test Firestore client") + return nil, fmt.Errorf("failed to initialize test Firestore client") + } + if fbc == nil { + log.Printf("failed to initialize test Firebase client") + return nil, fmt.Errorf("failed to initialize test Firebase client") + } + firestoreExtension := fb.NewFirestoreClientExtension(fsc) + fr := fb.NewFirebaseRepository(firestoreExtension, fbc) + return fr, nil +} +func InitializeTestFirebaseClient(ctx context.Context) (*firestore.Client, *auth.Client) { + fc := firebasetools.FirebaseClient{} + fa, err := fc.InitFirebase() + if err != nil { + log.Panicf("unable to initialize Firebase: %s", err) + } + + fsc, err := fa.Firestore(ctx) + if err != nil { + log.Panicf("unable to initialize Firestore: %s", err) + } + + fbc, err := fa.Auth(ctx) + if err != nil { + log.Panicf("can't initialize Firebase auth when setting up tests: %s", err) + } + return fsc, fbc +} + +func TestRepository_SaveEmailOTP(t *testing.T) { + fr := testRepo + ctx := context.Background() + type args struct { + ctx context.Context + email string + optIn bool + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + name: "valid save email OTP", + args: args{ + ctx: ctx, + email: firebasetools.TestUserEmail, + optIn: true, + }, + want: true, + wantErr: false, + }, + { + name: "invalid save email OTP: nil email address", + args: args{ + ctx: ctx, + optIn: true, + }, + want: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := fr.SaveEmailOTP(tt.args.ctx, tt.args.email, tt.args.optIn) + if (err != nil) != tt.wantErr { + t.Errorf("Repository.SaveEmailOTP() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + + } +} diff --git a/pkg/clinical/infrastructure/repository.go b/pkg/clinical/infrastructure/repository.go index 074a137f..a6adeaab 100644 --- a/pkg/clinical/infrastructure/repository.go +++ b/pkg/clinical/infrastructure/repository.go @@ -93,7 +93,7 @@ func (d FHIRService) GetFHIRPatientEverything(fhirResourceID string) ([]byte, er // Repository ... type Repository interface { - ValidateEmail(ctx context.Context, email string, optIn bool) error + SaveEmailOTP(ctx context.Context, email string, optIn bool) error } // DBService is an implementation of the database repository @@ -127,10 +127,9 @@ func NewDBService() *DBService { } } -// ValidateEmail returns an error if the supplied string does not have a -// valid format or resolvable host -func (db DBService) ValidateEmail( +// SaveEmailOTP persist the data of the newly created OTP to a datastore +func (db DBService) SaveEmailOTP( ctx context.Context, email string, optIn bool) error { - return db.firestore.ValidateEmail(ctx, email, optIn) + return db.firestore.SaveEmailOTP(ctx, email, optIn) } diff --git a/pkg/clinical/usecases/patient.go b/pkg/clinical/usecases/patient.go index 27bd696e..3fd92025 100644 --- a/pkg/clinical/usecases/patient.go +++ b/pkg/clinical/usecases/patient.go @@ -12,6 +12,7 @@ import ( auth "github.com/savannahghi/clinical/pkg/clinical/application/authorization" "github.com/savannahghi/clinical/pkg/clinical/application/common" "github.com/savannahghi/clinical/pkg/clinical/application/common/helpers" + "github.com/savannahghi/clinical/pkg/clinical/application/utils" "github.com/savannahghi/clinical/pkg/clinical/domain" "github.com/savannahghi/clinical/pkg/clinical/infrastructure" "github.com/savannahghi/converterandformatter" @@ -495,9 +496,13 @@ func (c *ClinicalUseCaseImpl) ContactsToContactPointInput(ctx context.Context, p emailSystem := domain.ContactPointSystemEnumEmail for _, email := range emails { - err := c.infrastructure.FirestoreRepo.ValidateEmail(ctx, email.Email, email.CommunicationOptIn) + emailErr := utils.ValidateEmail(email.Email) + if emailErr != nil { + return nil, fmt.Errorf("invalid email: %v", emailErr) + } + err := c.infrastructure.FirestoreRepo.SaveEmailOTP(ctx, email.Email, email.CommunicationOptIn) if err != nil { - return nil, fmt.Errorf("invalid email: %v", err) + return nil, fmt.Errorf("unable to save email otp: %v", err) } emailContact := &domain.FHIRContactPointInput{ System: &emailSystem,