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
}
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

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) {
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")
}
})

View file

@ -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"

View file

@ -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

View file

@ -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
}