From 5ffcddf8f727e319d6955d1bc57d2ce1140762b4 Mon Sep 17 00:00:00 2001 From: Daniel Krol Date: Fri, 29 Jul 2022 20:49:00 -0400 Subject: [PATCH] Endpoint to re-send verify token string --- server/account.go | 62 +++++++++++++++++ server/account_test.go | 146 ++++++++++++++++++++++++++++++++++++++++- server/server.go | 1 + server/server_test.go | 7 ++ store/store.go | 5 ++ 5 files changed, 220 insertions(+), 1 deletion(-) diff --git a/server/account.go b/server/account.go index c28aea6..22f2ee4 100644 --- a/server/account.go +++ b/server/account.go @@ -138,6 +138,68 @@ func getVerifyParams(req *http.Request) (token auth.VerifyTokenString, err error return } +type ResendVerifyEmailRequest struct { + Email auth.Email `json:"email"` +} + +func (r *ResendVerifyEmailRequest) validate() error { + if !r.Email.Validate() { + return fmt.Errorf("Invalid or missing 'email'") + } + return nil +} + +func (s *Server) resendVerifyEmail(w http.ResponseWriter, req *http.Request) { + verificationMode, err := env.GetAccountVerificationMode(s.env) + if err != nil { + internalServiceErrorJson(w, err, "Error getting account verification mode") + return + } + if verificationMode != env.AccountVerificationModeEmailVerify { + errorJson(w, http.StatusForbidden, "Account verification mode is not set to EmailVerify") + return + } + + var resendVerifyEmailRequest ResendVerifyEmailRequest + if !getPostData(w, req, &resendVerifyEmailRequest) { + return + } + + token, err := s.auth.NewVerifyTokenString() + + if err != nil { + internalServiceErrorJson(w, err, "Error generating verify token string") + return + } + + err = s.store.UpdateVerifyTokenString(resendVerifyEmailRequest.Email, token) + if err == store.ErrWrongCredentials { + errorJson(w, http.StatusUnauthorized, "No match for email") + return + } + if err != nil { + internalServiceErrorJson(w, err, "Error updating verify token string") + return + } + + err = s.mail.SendVerificationEmail(resendVerifyEmailRequest.Email, token) + + if err != nil { + internalServiceErrorJson(w, err, "Error re-sending verification email") + return + } + + var verifyResponse struct{} + response, err := json.Marshal(verifyResponse) + + if err != nil { + internalServiceErrorJson(w, err, "Error generating verify response") + return + } + + fmt.Fprintf(w, string(response)) +} + func (s *Server) verify(w http.ResponseWriter, req *http.Request) { if !getGetData(w, req) { return diff --git a/server/account_test.go b/server/account_test.go index 4cf9d05..1be1ff6 100644 --- a/server/account_test.go +++ b/server/account_test.go @@ -324,6 +324,147 @@ func TestServerValidateRegisterRequest(t *testing.T) { } } +func TestServerResendVerifyEmailSuccess(t *testing.T) { + testStore := TestStore{} + testMail := TestMail{} + + env := map[string]string{ + "ACCOUNT_VERIFICATION_MODE": "EmailVerify", + } + s := Server{&TestAuth{}, &testStore, &TestEnv{env}, &testMail} + + requestBody := []byte(`{"email": "abc@example.com"}`) + req := httptest.NewRequest(http.MethodPost, PathVerify, bytes.NewBuffer(requestBody)) + w := httptest.NewRecorder() + + s.resendVerifyEmail(w, req) + body, _ := ioutil.ReadAll(w.Body) + + expectStatusCode(t, w, http.StatusOK) + + if string(body) != "{}" { + t.Errorf("Expected register response to be \"{}\": result: %+v", string(body)) + } + + if !testStore.Called.UpdateVerifyTokenString { + t.Errorf("Expected Store.UpdateVerifyTokenString to be called") + } + + if testMail.SendVerificationEmailCall == nil { + // We're doing EmailVerify for this test. + t.Fatalf("Expected Store.SendVerificationEmail to be called") + } +} + +func TestServerResendVerifyEmailErrors(t *testing.T) { + tt := []struct { + name string + omitEmailAddress bool + accountVerificationMode string + + expectedStatusCode int + expectedErrorString string + expectedCallSendVerificationEmail bool + expectedCallUpdateVerifyTokenString bool + + storeErrors TestStoreFunctionsErrors + mailError error + }{ + + { + name: "wrong account verification mode", + accountVerificationMode: "Whitelist", + expectedStatusCode: http.StatusForbidden, + expectedErrorString: http.StatusText(http.StatusForbidden) + ": Account verification mode is not set to EmailVerify", + expectedCallSendVerificationEmail: false, + expectedCallUpdateVerifyTokenString: false, + }, + { + name: "validation error", + accountVerificationMode: "EmailVerify", + omitEmailAddress: true, + expectedStatusCode: http.StatusBadRequest, + expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Request failed validation: Invalid or missing 'email'", + expectedCallSendVerificationEmail: false, + expectedCallUpdateVerifyTokenString: false, + }, + + { + name: "not found email", + accountVerificationMode: "EmailVerify", + expectedStatusCode: http.StatusUnauthorized, + expectedErrorString: http.StatusText(http.StatusUnauthorized) + ": No match for email", + storeErrors: TestStoreFunctionsErrors{UpdateVerifyTokenString: store.ErrWrongCredentials}, + expectedCallSendVerificationEmail: false, + expectedCallUpdateVerifyTokenString: true, + }, + { + name: "assorted db error", + accountVerificationMode: "EmailVerify", + expectedStatusCode: http.StatusInternalServerError, + expectedErrorString: http.StatusText(http.StatusInternalServerError), + storeErrors: TestStoreFunctionsErrors{UpdateVerifyTokenString: fmt.Errorf("TestStore.UpdateVerifyTokenString fail")}, + expectedCallSendVerificationEmail: false, + expectedCallUpdateVerifyTokenString: true, + }, + { + name: "fail to generate verification email", + accountVerificationMode: "EmailVerify", + expectedStatusCode: http.StatusInternalServerError, + expectedErrorString: http.StatusText(http.StatusInternalServerError), + expectedCallSendVerificationEmail: true, + expectedCallUpdateVerifyTokenString: true, + + mailError: fmt.Errorf("TestEmail.SendVerificationEmail fail"), + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + + env := map[string]string{ + "ACCOUNT_VERIFICATION_MODE": tc.accountVerificationMode, + } + + // Set this up to fail according to specification + testStore := TestStore{Errors: tc.storeErrors} + testMail := TestMail{SendVerificationEmailError: tc.mailError} + s := Server{&TestAuth{}, &testStore, &TestEnv{env}, &testMail} + + // Make request + var requestBody []byte + if tc.omitEmailAddress { + requestBody = []byte(`{}`) + } else { + requestBody = []byte(`{"email": "abc@example.com"}`) + } + req := httptest.NewRequest(http.MethodPost, PathVerify, bytes.NewBuffer(requestBody)) + w := httptest.NewRecorder() + + s.resendVerifyEmail(w, req) + body, _ := ioutil.ReadAll(w.Body) + + expectStatusCode(t, w, tc.expectedStatusCode) + expectErrorString(t, body, tc.expectedErrorString) + + if tc.expectedCallUpdateVerifyTokenString && !testStore.Called.UpdateVerifyTokenString { + t.Errorf("Expected Store.UpdateVerifyTokenString to be called") + } + if !tc.expectedCallUpdateVerifyTokenString && testStore.Called.UpdateVerifyTokenString { + t.Errorf("Expected Store.UpdateVerifyTokenString not to be called") + } + + if tc.expectedCallSendVerificationEmail && testMail.SendVerificationEmailCall == nil { + // We're doing EmailVerify for this test. + t.Fatalf("Expected Store.SendVerificationEmail to be called") + } + if !tc.expectedCallSendVerificationEmail && testMail.SendVerificationEmailCall != nil { + // We're doing EmailVerify for this test. + t.Fatalf("Expected Store.SendVerificationEmail not to be called") + } + }) + } +} + func TestServerVerifyAccountSuccess(t *testing.T) { testStore := TestStore{} s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}} @@ -402,7 +543,10 @@ func TestServerVerifyAccountErrors(t *testing.T) { expectStatusCode(t, w, tc.expectedStatusCode) expectErrorString(t, body, tc.expectedErrorString) - if tc.expectedCallVerifyAccount != testStore.Called.VerifyAccount { + if tc.expectedCallVerifyAccount && !testStore.Called.VerifyAccount { + t.Errorf("Expected Store.VerifyAccount to be called") + } + if !tc.expectedCallVerifyAccount && testStore.Called.VerifyAccount { t.Errorf("Expected Store.VerifyAccount not to be called") } }) diff --git a/server/server.go b/server/server.go index df45dd1..368ed71 100644 --- a/server/server.go +++ b/server/server.go @@ -23,6 +23,7 @@ const PathPrometheus = "/metrics" const PathAuthToken = PathPrefix + "/auth/full" const PathRegister = PathPrefix + "/signup" const PathVerify = PathPrefix + "/verify" +const PathResendVerify = PathPrefix + "/verify/resend" const PathPassword = PathPrefix + "/password" const PathWallet = PathPrefix + "/wallet" const PathClientSaltSeed = PathPrefix + "/client-salt-seed" diff --git a/server/server_test.go b/server/server_test.go index 49c6099..e597bd1 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -96,6 +96,7 @@ type TestStoreFunctionsCalled struct { GetToken auth.AuthTokenString GetUserId bool CreateAccount *CreateAccountCall + UpdateVerifyTokenString bool VerifyAccount bool SetWallet SetWalletCall GetWallet bool @@ -109,6 +110,7 @@ type TestStoreFunctionsErrors struct { GetToken error GetUserId error CreateAccount error + UpdateVerifyTokenString error VerifyAccount error SetWallet error GetWallet error @@ -159,6 +161,11 @@ func (s *TestStore) CreateAccount(email auth.Email, password auth.Password, seed return s.Errors.CreateAccount } +func (s *TestStore) UpdateVerifyTokenString(auth.Email, auth.VerifyTokenString) (err error) { + s.Called.UpdateVerifyTokenString = true + return s.Errors.UpdateVerifyTokenString +} + func (s *TestStore) VerifyAccount(auth.VerifyTokenString) (err error) { s.Called.VerifyAccount = true return s.Errors.VerifyAccount diff --git a/store/store.go b/store/store.go index 5b4bf64..6778e9c 100644 --- a/store/store.go +++ b/store/store.go @@ -42,6 +42,7 @@ type StoreInterface interface { GetWallet(auth.UserId) (wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac, error) GetUserId(auth.Email, auth.Password) (auth.UserId, error) CreateAccount(auth.Email, auth.Password, auth.ClientSaltSeed, auth.VerifyTokenString) error + UpdateVerifyTokenString(auth.Email, auth.VerifyTokenString) error VerifyAccount(auth.VerifyTokenString) error ChangePasswordWithWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed, wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac) error ChangePasswordNoWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed) error @@ -383,6 +384,10 @@ func (s *Store) CreateAccount(email auth.Email, password auth.Password, seed aut return } +func (s *Store) UpdateVerifyTokenString(auth.Email, auth.VerifyTokenString) (err error) { + return +} + func (s *Store) VerifyAccount(auth.VerifyTokenString) (err error) { return }