Endpoint to re-send verify token string

This commit is contained in:
Daniel Krol 2022-07-29 20:49:00 -04:00
parent c8620f7c8c
commit 5ffcddf8f7
5 changed files with 220 additions and 1 deletions

View file

@ -138,6 +138,68 @@ func getVerifyParams(req *http.Request) (token auth.VerifyTokenString, err error
return 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) { func (s *Server) verify(w http.ResponseWriter, req *http.Request) {
if !getGetData(w, req) { if !getGetData(w, req) {
return return

View file

@ -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) { func TestServerVerifyAccountSuccess(t *testing.T) {
testStore := TestStore{} testStore := TestStore{}
s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}} s := Server{&TestAuth{}, &testStore, &TestEnv{}, &TestMail{}}
@ -402,7 +543,10 @@ func TestServerVerifyAccountErrors(t *testing.T) {
expectStatusCode(t, w, tc.expectedStatusCode) expectStatusCode(t, w, tc.expectedStatusCode)
expectErrorString(t, body, tc.expectedErrorString) 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") t.Errorf("Expected Store.VerifyAccount not to be called")
} }
}) })

View file

@ -23,6 +23,7 @@ const PathPrometheus = "/metrics"
const PathAuthToken = PathPrefix + "/auth/full" const PathAuthToken = PathPrefix + "/auth/full"
const PathRegister = PathPrefix + "/signup" const PathRegister = PathPrefix + "/signup"
const PathVerify = PathPrefix + "/verify" const PathVerify = PathPrefix + "/verify"
const PathResendVerify = PathPrefix + "/verify/resend"
const PathPassword = PathPrefix + "/password" const PathPassword = PathPrefix + "/password"
const PathWallet = PathPrefix + "/wallet" const PathWallet = PathPrefix + "/wallet"
const PathClientSaltSeed = PathPrefix + "/client-salt-seed" const PathClientSaltSeed = PathPrefix + "/client-salt-seed"

View file

@ -96,6 +96,7 @@ type TestStoreFunctionsCalled struct {
GetToken auth.AuthTokenString GetToken auth.AuthTokenString
GetUserId bool GetUserId bool
CreateAccount *CreateAccountCall CreateAccount *CreateAccountCall
UpdateVerifyTokenString bool
VerifyAccount bool VerifyAccount bool
SetWallet SetWalletCall SetWallet SetWalletCall
GetWallet bool GetWallet bool
@ -109,6 +110,7 @@ type TestStoreFunctionsErrors struct {
GetToken error GetToken error
GetUserId error GetUserId error
CreateAccount error CreateAccount error
UpdateVerifyTokenString error
VerifyAccount error VerifyAccount error
SetWallet error SetWallet error
GetWallet error GetWallet error
@ -159,6 +161,11 @@ func (s *TestStore) CreateAccount(email auth.Email, password auth.Password, seed
return s.Errors.CreateAccount 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) { func (s *TestStore) VerifyAccount(auth.VerifyTokenString) (err error) {
s.Called.VerifyAccount = true s.Called.VerifyAccount = true
return s.Errors.VerifyAccount return s.Errors.VerifyAccount

View file

@ -42,6 +42,7 @@ type StoreInterface interface {
GetWallet(auth.UserId) (wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac, error) GetWallet(auth.UserId) (wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac, error)
GetUserId(auth.Email, auth.Password) (auth.UserId, error) GetUserId(auth.Email, auth.Password) (auth.UserId, error)
CreateAccount(auth.Email, auth.Password, auth.ClientSaltSeed, auth.VerifyTokenString) error CreateAccount(auth.Email, auth.Password, auth.ClientSaltSeed, auth.VerifyTokenString) error
UpdateVerifyTokenString(auth.Email, auth.VerifyTokenString) error
VerifyAccount(auth.VerifyTokenString) error VerifyAccount(auth.VerifyTokenString) error
ChangePasswordWithWallet(auth.Email, auth.Password, auth.Password, auth.ClientSaltSeed, wallet.EncryptedWallet, wallet.Sequence, wallet.WalletHmac) 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 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 return
} }
func (s *Store) UpdateVerifyTokenString(auth.Email, auth.VerifyTokenString) (err error) {
return
}
func (s *Store) VerifyAccount(auth.VerifyTokenString) (err error) { func (s *Store) VerifyAccount(auth.VerifyTokenString) (err error) {
return return
} }