diff --git a/api/api.go b/api/api.go index 25ab3e3..b757a5a 100644 --- a/api/api.go +++ b/api/api.go @@ -149,6 +149,12 @@ func (a *API) initRouter() http.Handler { // verify user log.Infow("new route", "method", "POST", "path", verifyUserEndpoint) r.Post(verifyUserEndpoint, a.verifyUserAccountHandler) + // get user verification code information + log.Infow("new route", "method", "GET", "path", verifyUserCodeEndpoint) + r.Get(verifyUserCodeEndpoint, a.userVerificationCodeInfoHandler) + // resend user verification code + log.Infow("new route", "method", "POST", "path", verifyUserCodeEndpoint) + r.Post(verifyUserCodeEndpoint, a.resendUserVerificationCodeHandler) // request user password recovery log.Infow("new route", "method", "POST", "path", usersRecoveryPasswordEndpoint) r.Post(usersRecoveryPasswordEndpoint, a.recoverUserPasswordHandler) diff --git a/api/const.go b/api/const.go index f85ad2a..aaf82c8 100644 --- a/api/const.go +++ b/api/const.go @@ -1,5 +1,11 @@ package api +import "time" + +// VerificationCodeExpiration is the duration of the verification code +// before it is invalidated +var VerificationCodeExpiration = 2 * time.Minute + const ( // VerificationCodeLength is the length of the verification code in bytes VerificationCodeLength = 3 diff --git a/api/docs.md b/api/docs.md index a55391e..3ada530 100644 --- a/api/docs.md +++ b/api/docs.md @@ -14,6 +14,8 @@ - [👥 Users](#-users) - [🙋 Register](#-register) - [✅ Verify user](#-verify-user) + - [🪪 User verification code info](#-user-verification-code-info) + - [📤 Resend user verification code](#-resend-user-verification-code) - [🧑‍💻 Get current user info](#-get-current-user-info) - [💇 Update current user info](#-update-current-user-info) - [🔏 Update current user password](#-update-current-user-password) @@ -225,6 +227,57 @@ This endpoint only returns the addresses of the organizations where the current |:---:|:---:|:---| | `401` | `40001` | `user not authorized` | | `400` | `40004` | `malformed JSON body` | +| `400` | `40005` | `invalid user data` | +| `400` | `40015` | `user account already verified` | +| `401` | `40016` | `verification code expired` | +| `500` | `50002` | `internal server error` | + +### 🪪 User verification code info + +* **Path** `/users/verify/code` +* **Method** `GET` +* **Query params** + * `email` + +* **Response** +```json +{ + "email": "user@email.com", + "expiration": "2024-09-20T09:02:26.849Z", + "valid": true +} +``` + +* **Errors** + +| HTTP Status | Error code | Message | +|:---:|:---:|:---| +| `401` | `40001` | `user not authorized` | +| `400` | `40005` | `invalid user data` | +| `400` | `40015` | `user account already verified` | +| `404` | `40018` | `user not found` | +| `500` | `50002` | `internal server error` | + +### 📤 Resend user verification code + +* **Path** `/users/verify/code` +* **Method** `POST` +* **Request Body** +```json +{ + "email": "user@email.com", +} +``` + +* **Errors** + +| HTTP Status | Error code | Message | +|:---:|:---:|:---| +| `401` | `40001` | `user not authorized` | +| `400` | `40004` | `malformed JSON body` | +| `400` | `40005` | `invalid user data` | +| `400` | `40015` | `user account already verified` | +| `400` | `40017` | `last verification code still valid` | | `500` | `50002` | `internal server error` | ### 🧑‍💻 Get current user info diff --git a/api/errors_definition.go b/api/errors_definition.go index 22dd41b..59f3a06 100644 --- a/api/errors_definition.go +++ b/api/errors_definition.go @@ -40,6 +40,10 @@ var ( ErrNoOrganizations = Error{Code: 40012, HTTPstatus: http.StatusNotFound, Err: fmt.Errorf("this user has not been assigned to any organization")} ErrInvalidOrganizationData = Error{Code: 40013, HTTPstatus: http.StatusBadRequest, Err: fmt.Errorf("invalid organization data")} ErrUserNoVerified = Error{Code: 40014, HTTPstatus: http.StatusUnauthorized, Err: fmt.Errorf("user account not verified")} + ErrUserAlreadyVerified = Error{Code: 40015, HTTPstatus: http.StatusBadRequest, Err: fmt.Errorf("user account already verified")} + ErrVerificationCodeExpired = Error{Code: 40016, HTTPstatus: http.StatusUnauthorized, Err: fmt.Errorf("verification code expired")} + ErrVerificationCodeValid = Error{Code: 40017, HTTPstatus: http.StatusBadRequest, Err: fmt.Errorf("last verification code still valid")} + ErrUserNotFound = Error{Code: 40018, HTTPstatus: http.StatusNotFound, Err: fmt.Errorf("user not found")} ErrMarshalingServerJSONFailed = Error{Code: 50001, HTTPstatus: http.StatusInternalServerError, Err: fmt.Errorf("marshaling (server-side) JSON failed")} ErrGenericInternalServerError = Error{Code: 50002, HTTPstatus: http.StatusInternalServerError, Err: fmt.Errorf("internal server error")} diff --git a/api/routes.go b/api/routes.go index 107f6ee..aa87180 100644 --- a/api/routes.go +++ b/api/routes.go @@ -20,6 +20,9 @@ const ( usersEndpoint = "/users" // POST /users/verify to verify the user verifyUserEndpoint = "/users/verify" + // GET /users/verify/code to get the user verification code information + // POST /users/verify/code to try to resend the user verification code + verifyUserCodeEndpoint = "/users/verify/code" // GET /users/me to get the current user information // PUT /users/me to update the current user information usersMeEndpoint = "/users/me" diff --git a/api/types.go b/api/types.go index 50a7a7b..19506cf 100644 --- a/api/types.go +++ b/api/types.go @@ -66,9 +66,11 @@ type UserPasswordUpdate struct { // UserVerificationRequest is the request to verify a user. type UserVerification struct { - Email string `json:"email"` - Code string `json:"code"` - Phone string `json:"phone"` + Email string `json:"email,omitempty"` + Code string `json:"code,omitempty"` + Phone string `json:"phone,omitempty"` + Expiration time.Time `json:"expiration,omitempty"` + Valid bool `json:"valid"` } type UserPasswordReset struct { diff --git a/api/users.go b/api/users.go index a3361bd..ae25bad 100644 --- a/api/users.go +++ b/api/users.go @@ -24,7 +24,7 @@ import ( // of codes can be added in the future. If neither the mail service nor the SMS // service are available, the verification code will be empty but stored in the // database to mock the verification process in any case. -func (a *API) sendUserCode(ctx context.Context, user *db.User, codeType db.CodeType) error { +func (a *API) sendUserCode(ctx context.Context, user *db.User, t db.CodeType) error { // generate verification code if the mail service is available, if not // the verification code will not be sent but stored in the database // generated with just the user email to mock the verification process @@ -32,9 +32,10 @@ func (a *API) sendUserCode(ctx context.Context, user *db.User, codeType db.CodeT if a.mail != nil || a.sms != nil { code = util.RandomHex(VerificationCodeLength) } - hashCode := internal.HashVerificationCode(user.Email, code) // store the verification code in the database - if err := a.db.SetVerificationCode(&db.User{ID: user.ID}, hashCode, codeType); err != nil { + hashCode := internal.HashVerificationCode(user.Email, code) + exp := time.Now().Add(VerificationCodeExpiration) + if err := a.db.SetVerificationCode(&db.User{ID: user.ID}, hashCode, t, exp); err != nil { return err } ctx, cancel := context.WithTimeout(ctx, time.Second*10) @@ -128,18 +129,27 @@ func (a *API) registerHandler(w http.ResponseWriter, r *http.Request) { } // verifyUserAccountHandler handles the request to verify the user account. It -// requires the user email and the verification code to be provided. If the -// verification code is correct, the user account is verified and a new token is -// generated and sent back to the user. If the verification code is incorrect, -// an error is returned. +// requires the user email and the verification code to be provided. It checks +// if the user has not been verified yet, if the verification code is not +// expired and if the verification code is correct. If all the checks are +// correct, the user account is verified and a new token is generated and sent +// back to the user. If the user is already verified, an error is returned. If +// the verification code is expired, an error is returned. If the verification +// code is incorrect, an error is returned and the number of attempts to verify +// it is increased. If any other error occurs, a generic error is returned. func (a *API) verifyUserAccountHandler(w http.ResponseWriter, r *http.Request) { verification := &UserVerification{} if err := json.NewDecoder(r.Body).Decode(verification); err != nil { ErrMalformedBody.Write(w) return } - hashCode := internal.HashVerificationCode(verification.Email, verification.Code) - user, err := a.db.UserByVerificationCode(hashCode, db.CodeTypeAccountVerification) + // check the email and verification code are not empty + if verification.Email == "" || verification.Code == "" { + ErrInvalidUserData.With("no verification code or email provided").Write(w) + return + } + // get the user information from the database by email + user, err := a.db.UserByEmail(verification.Email) if err != nil { if err == db.ErrNotFound { ErrUnauthorized.Write(w) @@ -148,6 +158,33 @@ func (a *API) verifyUserAccountHandler(w http.ResponseWriter, r *http.Request) { ErrGenericInternalServerError.Write(w) return } + // check the user is not already verified + if user.Verified { + ErrUserAlreadyVerified.Write(w) + return + } + // get the verification code from the database + code, err := a.db.UserVerificationCode(user, db.CodeTypeAccountVerification) + if err != nil { + if err != db.ErrNotFound { + log.Warnw("could not get verification code", "error", err) + } + ErrUnauthorized.Write(w) + return + } + // check the verification code is not expired + if code.Expiration.Before(time.Now()) { + ErrVerificationCodeExpired.Write(w) + return + } + // check the verification code is correct + hashCode := internal.HashVerificationCode(verification.Email, verification.Code) + if code.Code != hashCode { + ErrUnauthorized.Write(w) + return + } + // verify the user account if the current verification code is valid and + // matches with the provided one if err := a.db.VerifyUserAccount(user); err != nil { ErrGenericInternalServerError.Write(w) return @@ -162,6 +199,125 @@ func (a *API) verifyUserAccountHandler(w http.ResponseWriter, r *http.Request) { httpWriteJSON(w, res) } +// userVerificationCodeInfoHandler handles the request to get the verification +// code information of a user. It requires the user email to be provided. It +// returns the user email, the verification code, the phone number, the code +// expiration and if the code is valid (not expired and has not reached the +// maximum number of attempts). If the user is already verified, an error is +// returned. If the user is not found, an error is returned. If the +// verification code is not found, an error is returned. If any other error +// occurs, a generic error is returned. +func (a *API) userVerificationCodeInfoHandler(w http.ResponseWriter, r *http.Request) { + // get the user email or the phone number of the user from the request query + userEmail := r.URL.Query().Get("email") + userPhone := r.URL.Query().Get("phone") + // check the email or the phone number is not empty + if userEmail == "" && userPhone == "" { + ErrInvalidUserData.With("no email or phone number provided").Write(w) + return + } + var err error + var user *db.User + // get the user information from the database by email or phone + if userEmail != "" { + user, err = a.db.UserByEmail(userEmail) + } else { + user, err = a.db.UserByPhone(userPhone) + } + // check the error getting the user information + if err != nil { + if err == db.ErrNotFound { + ErrUserNotFound.Write(w) + return + } + ErrGenericInternalServerError.Write(w) + return + } + // check if the user is already verified + if user.Verified { + ErrUserAlreadyVerified.Write(w) + return + } + // get the verification code from the database + code, err := a.db.UserVerificationCode(user, db.CodeTypeAccountVerification) + if err != nil { + if err != db.ErrNotFound { + log.Warnw("could not get verification code", "error", err) + } + ErrUnauthorized.Write(w) + return + } + // return the verification code information + httpWriteJSON(w, UserVerification{ + Email: user.Email, + Phone: user.Phone, + Expiration: code.Expiration, + Valid: code.Expiration.After(time.Now()), + }) +} + +// resendUserVerificationCodeHandler handles the request to resend the user +// verification code. It requires the user email to be provided. If the user is +// not found, an error is returned. If the user is already verified, an error is +// returned. If the verification code is not expired, an error is returned. If +// the verification code is found and expired, a new verification code is sent +// to the user email. If any other error occurs, a generic error is returned. +func (a *API) resendUserVerificationCodeHandler(w http.ResponseWriter, r *http.Request) { + verification := &UserVerification{} + if err := json.NewDecoder(r.Body).Decode(verification); err != nil { + ErrMalformedBody.Write(w) + return + } + // check the email or the phone number is not empty + if verification.Email == "" && verification.Phone == "" { + ErrInvalidUserData.With("no email or phone number provided").Write(w) + return + } + var err error + var user *db.User + // get the user information from the database by email or phone + if verification.Email != "" { + user, err = a.db.UserByEmail(verification.Email) + } else { + user, err = a.db.UserByPhone(verification.Phone) + } + // check the error getting the user information + if err != nil { + if err == db.ErrNotFound { + ErrUnauthorized.Write(w) + return + } + ErrGenericInternalServerError.Write(w) + return + } + // check the user is not already verified + if user.Verified { + ErrUserAlreadyVerified.Write(w) + return + } + // get the verification code from the database + code, err := a.db.UserVerificationCode(user, db.CodeTypeAccountVerification) + if err != nil { + if err != db.ErrNotFound { + log.Warnw("could not get verification code", "error", err) + } + ErrUnauthorized.Write(w) + return + } + // if the verification code is not expired, return an error + if code.Expiration.After(time.Now()) { + ErrVerificationCodeValid.Write(w) + return + } + // set a new code and send it + if err := a.sendUserCode(r.Context(), user, db.CodeTypeAccountVerification); err != nil { + log.Warnw("could not send verification code", "error", err) + ErrGenericInternalServerError.Write(w) + return + } + httpWriteOK(w) +} + // userInfoHandler handles the request to get the information of the current // authenticated user. func (a *API) userInfoHandler(w http.ResponseWriter, r *http.Request) { diff --git a/api/users_test.go b/api/users_test.go index ef824b3..f98beb6 100644 --- a/api/users_test.go +++ b/api/users_test.go @@ -9,6 +9,7 @@ import ( "regexp" "strings" "testing" + "time" qt "github.com/frankban/quicktest" ) @@ -154,7 +155,8 @@ func TestVerifyAccountHandler(t *testing.T) { c.Logf("error resetting test database: %v", err) } }() - // register a user + // register a user with short expiration time + VerificationCodeExpiration = 5 * time.Second jsonUser := mustMarshal(&UserInfo{ Email: testEmail, Password: testPass, @@ -178,6 +180,7 @@ func TestVerifyAccountHandler(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(resp.StatusCode, qt.Equals, http.StatusUnauthorized) c.Assert(resp.Body.Close(), qt.IsNil) + // try to verify the user (should fail) // get the verification code from the email mailBody, err := testMailService.FindEmail(context.Background(), testEmail) c.Assert(err, qt.IsNil) @@ -191,6 +194,30 @@ func TestVerifyAccountHandler(t *testing.T) { }) req, err = http.NewRequest(http.MethodPost, testURL(verifyUserEndpoint), bytes.NewBuffer(verification)) c.Assert(err, qt.IsNil) + // wait to expire the verification code + time.Sleep(VerificationCodeExpiration) + resp, err = http.DefaultClient.Do(req) + c.Assert(err, qt.IsNil) + c.Assert(resp.StatusCode, qt.Equals, http.StatusUnauthorized) + c.Assert(resp.Body.Close(), qt.IsNil) + // resend the verification code and verify the user + req, err = http.NewRequest(http.MethodPost, testURL(verifyUserCodeEndpoint), bytes.NewBuffer(verification)) + c.Assert(err, qt.IsNil) + resp, err = http.DefaultClient.Do(req) + c.Assert(err, qt.IsNil) + c.Assert(resp.StatusCode, qt.Equals, http.StatusOK) + c.Assert(resp.Body.Close(), qt.IsNil) + // get the verification code from the email + mailBody, err = testMailService.FindEmail(context.Background(), testEmail) + c.Assert(err, qt.IsNil) + mailCode = mailCodeRgx.FindStringSubmatch(mailBody) + // verify the user + verification = mustMarshal(&UserVerification{ + Email: testEmail, + Code: mailCode[1], + }) + req, err = http.NewRequest(http.MethodPost, testURL(verifyUserEndpoint), bytes.NewBuffer(verification)) + c.Assert(err, qt.IsNil) resp, err = http.DefaultClient.Do(req) c.Assert(err, qt.IsNil) c.Assert(resp.StatusCode, qt.Equals, http.StatusOK) @@ -200,7 +227,7 @@ func TestVerifyAccountHandler(t *testing.T) { c.Assert(err, qt.IsNil) resp, err = http.DefaultClient.Do(req) c.Assert(err, qt.IsNil) - c.Assert(resp.StatusCode, qt.Equals, http.StatusUnauthorized) + c.Assert(resp.StatusCode, qt.Equals, http.StatusBadRequest) c.Assert(resp.Body.Close(), qt.IsNil) // try to login again req, err = http.NewRequest(http.MethodPost, testURL(authLoginEndpoint), bytes.NewBuffer(jsonLogin)) diff --git a/db/helpers.go b/db/helpers.go index 3708b2d..031030c 100644 --- a/db/helpers.go +++ b/db/helpers.go @@ -113,6 +113,14 @@ func (ms *MongoStorage) createIndexes() error { if _, err := ms.users.Indexes().CreateOne(ctx, userEmailIndex); err != nil { return fmt.Errorf("failed to create index on addresses for users: %w", err) } + // create an index for the 'phone' field on users + userPhoneIndex := mongo.IndexModel{ + Keys: bson.D{{Key: "phone", Value: 1}}, // 1 for ascending order + Options: options.Index().SetUnique(true), + } + if _, err := ms.users.Indexes().CreateOne(ctx, userPhoneIndex); err != nil { + return fmt.Errorf("failed to create index on phone for users: %w", err) + } // create an index for the 'name' field on organizations (must be unique) organizationNameIndex := mongo.IndexModel{ Keys: bson.D{{Key: "name", Value: 1}}, // 1 for ascending order @@ -121,9 +129,12 @@ func (ms *MongoStorage) createIndexes() error { if _, err := ms.organizations.Indexes().CreateOne(ctx, organizationNameIndex); err != nil { return fmt.Errorf("failed to create index on name for organizations: %w", err) } - // create an index for the 'code' field on user verifications (must be unique) + // create an index for the ('code', 'type') tuple on user verifications (must be unique) verificationCodeIndex := mongo.IndexModel{ - Keys: bson.D{{Key: "code", Value: 1}}, // 1 for ascending order + Keys: bson.D{ + {Key: "code", Value: 1}, // 1 for ascending order + {Key: "type", Value: 1}, // 1 for ascending order + }, Options: options.Index().SetUnique(true), } if _, err := ms.verifications.Indexes().CreateOne(ctx, verificationCodeIndex); err != nil { diff --git a/db/types.go b/db/types.go index 48a152b..3ab1df2 100644 --- a/db/types.go +++ b/db/types.go @@ -16,9 +16,10 @@ type User struct { type CodeType string type UserVerification struct { - ID uint64 `json:"id" bson:"_id"` - Code string `json:"code" bson:"code"` - Type CodeType `json:"type" bson:"type"` + ID uint64 `json:"id" bson:"_id"` + Code string `json:"code" bson:"code"` + Type CodeType `json:"type" bson:"type"` + Expiration time.Time `json:"expiration" bson:"expiration"` } func (u *User) HasRoleFor(address string, role UserRole) bool { diff --git a/db/users.go b/db/users.go index eb8d7d4..5b5c5c3 100644 --- a/db/users.go +++ b/db/users.go @@ -103,6 +103,27 @@ func (ms *MongoStorage) UserByEmail(email string) (*User, error) { return user, nil } +// UserByPhone method returns the user with the given phone number. If the user +// doesn't exist, it returns a specific error. If other errors occur, it returns +// the error. +func (ms *MongoStorage) UserByPhone(phone string) (*User, error) { + ms.keysLock.RLock() + defer ms.keysLock.RUnlock() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + result := ms.users.FindOne(ctx, bson.M{"phone": phone}) + user := &User{} + if err := result.Decode(user); err != nil { + if err == mongo.ErrNoDocuments { + return nil, ErrNotFound + } + return nil, err + } + return user, nil +} + // SetUser method creates or updates the user in the database. If the user // already exists, it updates the fields that have changed. If the user doesn't // exist, it creates it. If an error occurs, it returns the error. diff --git a/db/users_test.go b/db/users_test.go index 9c6832a..ac5d7b3 100644 --- a/db/users_test.go +++ b/db/users_test.go @@ -198,3 +198,32 @@ func TestIsMemberOf(t *testing.T) { c.Assert(err, qt.Equals, ErrNotFound) c.Assert(success, qt.IsFalse) } + +func TestVerifyUser(t *testing.T) { + defer func() { + if err := db.Reset(); err != nil { + t.Error(err) + } + }() + c := qt.New(t) + + nonExistingUserID := uint64(100) + c.Assert(db.VerifyUserAccount(&User{ID: nonExistingUserID}), qt.Equals, ErrNotFound) + + userID, err := db.SetUser(&User{ + Email: testUserEmail, + Password: testUserPass, + FirstName: testUserFirstName, + LastName: testUserLastName, + }) + c.Assert(err, qt.IsNil) + + user, err := db.User(userID) + c.Assert(err, qt.IsNil) + c.Assert(user.Verified, qt.IsFalse) + + c.Assert(db.VerifyUserAccount(&User{ID: userID}), qt.IsNil) + user, err = db.User(userID) + c.Assert(err, qt.IsNil) + c.Assert(user.Verified, qt.IsTrue) +} diff --git a/db/verifications.go b/db/verifications.go index df02d01..1947f51 100644 --- a/db/verifications.go +++ b/db/verifications.go @@ -9,6 +9,14 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" ) +// delVerificationCode private method deletes the verification code for the +// user and type provided. This method must be called with the keysLock held. +func (ms *MongoStorage) delVerificationCode(ctx context.Context, id uint64, t CodeType) error { + // delete the verification code for the user provided + _, err := ms.verifications.DeleteOne(ctx, bson.M{"_id": id, "type": t}) + return err +} + // UserByVerificationCode method returns the user with the given verification // code. If the user or the verification code doesn't exist, it returns a // specific error. If other errors occur, it returns the error. It checks the @@ -35,7 +43,7 @@ func (ms *MongoStorage) UserByVerificationCode(code string, t CodeType) (*User, // UserVerificationCode returns the verification code for the user provided. If // the user has not a verification code, it returns an specific error, if other // error occurs, it returns the error. -func (ms *MongoStorage) UserVerificationCode(user *User, t CodeType) (string, error) { +func (ms *MongoStorage) UserVerificationCode(user *User, t CodeType) (*UserVerification, error) { ms.keysLock.RLock() defer ms.keysLock.RUnlock() @@ -46,17 +54,17 @@ func (ms *MongoStorage) UserVerificationCode(user *User, t CodeType) (string, er verification := &UserVerification{} if err := result.Decode(verification); err != nil { if err == mongo.ErrNoDocuments { - return "", ErrNotFound + return nil, ErrNotFound } - return "", err + return nil, err } - return verification.Code, nil + return verification, nil } // SetVerificationCode method sets the verification code for the user provided. // If the user already has a verification code, it updates it. If an error // occurs, it returns the error. -func (ms *MongoStorage) SetVerificationCode(user *User, code string, t CodeType) error { +func (ms *MongoStorage) SetVerificationCode(user *User, code string, t CodeType, exp time.Time) error { ms.keysLock.Lock() defer ms.keysLock.Unlock() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -68,17 +76,12 @@ func (ms *MongoStorage) SetVerificationCode(user *User, code string, t CodeType) // insert the verification code for the user provided filter := bson.M{"_id": user.ID} verification := &UserVerification{ - ID: user.ID, - Code: code, - Type: t, + ID: user.ID, + Code: code, + Type: t, + Expiration: exp, } opts := options.Replace().SetUpsert(true) _, err := ms.verifications.ReplaceOne(ctx, filter, verification, opts) return err } - -func (ms *MongoStorage) delVerificationCode(ctx context.Context, id uint64, t CodeType) error { - // delete the verification code for the user provided - _, err := ms.verifications.DeleteOne(ctx, bson.M{"_id": id, "type": t}) - return err -} diff --git a/db/verifications_test.go b/db/verifications_test.go index 8796cf8..cd1f1ca 100644 --- a/db/verifications_test.go +++ b/db/verifications_test.go @@ -2,6 +2,7 @@ package db import ( "testing" + "time" qt "github.com/frankban/quicktest" ) @@ -26,11 +27,11 @@ func TestUserVerificationCode(t *testing.T) { c.Assert(err, qt.Equals, ErrNotFound) testCode := "testCode" - c.Assert(db.SetVerificationCode(&User{ID: userID}, testCode, CodeTypeAccountVerification), qt.IsNil) + c.Assert(db.SetVerificationCode(&User{ID: userID}, testCode, CodeTypeAccountVerification, time.Now()), qt.IsNil) code, err := db.UserVerificationCode(&User{ID: userID}, CodeTypeAccountVerification) c.Assert(err, qt.IsNil) - c.Assert(code, qt.Equals, testCode) + c.Assert(code.Code, qt.Equals, testCode) c.Assert(db.VerifyUserAccount(&User{ID: userID}), qt.IsNil) _, err = db.UserVerificationCode(&User{ID: userID}, CodeTypeAccountVerification) @@ -46,7 +47,8 @@ func TestSetVerificationCode(t *testing.T) { c := qt.New(t) nonExistingUserID := uint64(100) - c.Assert(db.SetVerificationCode(&User{ID: nonExistingUserID}, "testCode", CodeTypeAccountVerification), qt.Equals, ErrNotFound) + err := db.SetVerificationCode(&User{ID: nonExistingUserID}, "testCode", CodeTypeAccountVerification, time.Now()) + c.Assert(err, qt.Equals, ErrNotFound) userID, err := db.SetUser(&User{ Email: testUserEmail, @@ -57,45 +59,16 @@ func TestSetVerificationCode(t *testing.T) { c.Assert(err, qt.IsNil) testCode := "testCode" - c.Assert(db.SetVerificationCode(&User{ID: userID}, testCode, CodeTypeAccountVerification), qt.IsNil) + c.Assert(db.SetVerificationCode(&User{ID: userID}, testCode, CodeTypeAccountVerification, time.Now()), qt.IsNil) code, err := db.UserVerificationCode(&User{ID: userID}, CodeTypeAccountVerification) c.Assert(err, qt.IsNil) - c.Assert(code, qt.Equals, testCode) + c.Assert(code.Code, qt.Equals, testCode) testCode = "testCode2" - c.Assert(db.SetVerificationCode(&User{ID: userID}, testCode, CodeTypeAccountVerification), qt.IsNil) + c.Assert(db.SetVerificationCode(&User{ID: userID}, testCode, CodeTypeAccountVerification, time.Now()), qt.IsNil) code, err = db.UserVerificationCode(&User{ID: userID}, CodeTypeAccountVerification) c.Assert(err, qt.IsNil) - c.Assert(code, qt.Equals, testCode) -} - -func TestVerifyUser(t *testing.T) { - defer func() { - if err := db.Reset(); err != nil { - t.Error(err) - } - }() - c := qt.New(t) - - nonExistingUserID := uint64(100) - c.Assert(db.VerifyUserAccount(&User{ID: nonExistingUserID}), qt.Equals, ErrNotFound) - - userID, err := db.SetUser(&User{ - Email: testUserEmail, - Password: testUserPass, - FirstName: testUserFirstName, - LastName: testUserLastName, - }) - c.Assert(err, qt.IsNil) - - user, err := db.User(userID) - c.Assert(err, qt.IsNil) - c.Assert(user.Verified, qt.IsFalse) - - c.Assert(db.VerifyUserAccount(&User{ID: userID}), qt.IsNil) - user, err = db.User(userID) - c.Assert(err, qt.IsNil) - c.Assert(user.Verified, qt.IsTrue) + c.Assert(code.Code, qt.Equals, testCode) }