/* Copyright (C) 2019, 2020, 2021 Monomax Software Pty Ltd * * This file is part of Dnote. * * Dnote is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Dnote is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Dnote. If not, see . */ package api // import ( // "encoding/json" // "fmt" // "net/http" // "testing" // "time" // // "github.com/dnote/dnote/pkg/assert" // "github.com/dnote/dnote/pkg/clock" // "github.com/dnote/dnote/pkg/server/app" // "github.com/dnote/dnote/pkg/server/database" // "github.com/dnote/dnote/pkg/server/presenters" // "github.com/dnote/dnote/pkg/server/testutils" // "github.com/pkg/errors" // "golang.org/x/crypto/bcrypt" // ) // // func TestUpdatePassword(t *testing.T) { // t.Run("success", func(t *testing.T) { // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // Clock: clock.NewMock(), // }) // defer server.Close() // // user := testutils.SetupUserData() // testutils.SetupAccountData(user, "alice@example.com", "oldpassword") // // // Execute // dat := `{"old_password": "oldpassword", "new_password": "newpassword"}` // req := testutils.MakeReq(server.URL, "PATCH", "/account/password", dat) // res := testutils.HTTPAuthDo(t, req, user) // // // Test // assert.StatusCodeEquals(t, res, http.StatusOK, "Status code mismsatch") // // var account database.Account // testutils.MustExec(t, testutils.DB.Where("user_id = ?", user.ID).First(&account), "finding account") // // passwordErr := bcrypt.CompareHashAndPassword([]byte(account.Password.String), []byte("newpassword")) // assert.Equal(t, passwordErr, nil, "Password mismatch") // }) // // t.Run("old password mismatch", func(t *testing.T) { // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // // Clock: clock.NewMock(), // }) // defer server.Close() // // u := testutils.SetupUserData() // a := testutils.SetupAccountData(u, "alice@example.com", "oldpassword") // // // Execute // dat := `{"old_password": "randompassword", "new_password": "newpassword"}` // req := testutils.MakeReq(server.URL, "PATCH", "/account/password", dat) // res := testutils.HTTPAuthDo(t, req, u) // // // Test // assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "Status code mismsatch") // // var account database.Account // testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&account), "finding account") // assert.Equal(t, a.Password.String, account.Password.String, "password should not have been updated") // }) // // t.Run("password too short", func(t *testing.T) { // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // // Clock: clock.NewMock(), // }) // defer server.Close() // // u := testutils.SetupUserData() // a := testutils.SetupAccountData(u, "alice@example.com", "oldpassword") // // // Execute // dat := `{"old_password": "oldpassword", "new_password": "a"}` // req := testutils.MakeReq(server.URL, "PATCH", "/account/password", dat) // res := testutils.HTTPAuthDo(t, req, u) // // // Test // assert.StatusCodeEquals(t, res, http.StatusBadRequest, "Status code mismsatch") // // var account database.Account // testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&account), "finding account") // assert.Equal(t, a.Password.String, account.Password.String, "password should not have been updated") // }) // } // // func TestCreateVerificationToken(t *testing.T) { // t.Run("success", func(t *testing.T) { // defer testutils.ClearData(testutils.DB) // // // Setup // emailBackend := testutils.MockEmailbackendImplementation{} // server := MustNewServer(t, &app.App{ // Clock: clock.NewMock(), // EmailBackend: &emailBackend, // }) // defer server.Close() // // user := testutils.SetupUserData() // testutils.SetupAccountData(user, "alice@example.com", "pass1234") // // // Execute // req := testutils.MakeReq(server.URL, "POST", "/verification-token", "") // res := testutils.HTTPAuthDo(t, req, user) // // // Test // assert.StatusCodeEquals(t, res, http.StatusCreated, "status code mismatch") // // var account database.Account // var token database.Token // var tokenCount int // testutils.MustExec(t, testutils.DB.Where("user_id = ?", user.ID).First(&account), "finding account") // testutils.MustExec(t, testutils.DB.Where("user_id = ? AND type = ?", user.ID, database.TokenTypeEmailVerification).First(&token), "finding token") // testutils.MustExec(t, testutils.DB.Model(&database.Token{}).Count(&tokenCount), "counting token") // // assert.Equal(t, account.EmailVerified, false, "email_verified should not have been updated") // assert.NotEqual(t, token.Value, "", "token Value mismatch") // assert.Equal(t, tokenCount, 1, "token count mismatch") // assert.Equal(t, token.UsedAt, (*time.Time)(nil), "token UsedAt mismatch") // assert.Equal(t, len(emailBackend.Emails), 1, "email queue count mismatch") // }) // // t.Run("already verified", func(t *testing.T) { // // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // // Clock: clock.NewMock(), // }) // defer server.Close() // // user := testutils.SetupUserData() // a := testutils.SetupAccountData(user, "alice@example.com", "pass1234") // a.EmailVerified = true // testutils.MustExec(t, testutils.DB.Save(&a), "preparing account") // // // Execute // req := testutils.MakeReq(server.URL, "POST", "/verification-token", "") // res := testutils.HTTPAuthDo(t, req, user) // // // Test // assert.StatusCodeEquals(t, res, http.StatusGone, "Status code mismatch") // // var account database.Account // var tokenCount int // testutils.MustExec(t, testutils.DB.Where("user_id = ?", user.ID).First(&account), "finding account") // testutils.MustExec(t, testutils.DB.Model(&database.Token{}).Count(&tokenCount), "counting token") // // assert.Equal(t, account.EmailVerified, true, "email_verified should not have been updated") // assert.Equal(t, tokenCount, 0, "token count mismatch") // }) // } // // func TestVerifyEmail(t *testing.T) { // t.Run("success", func(t *testing.T) { // // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // Clock: clock.NewMock(), // }) // defer server.Close() // // user := testutils.SetupUserData() // testutils.SetupAccountData(user, "alice@example.com", "pass1234") // tok := database.Token{ // UserID: user.ID, // Type: database.TokenTypeEmailVerification, // Value: "someTokenValue", // } // testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") // // dat := `{"token": "someTokenValue"}` // req := testutils.MakeReq(server.URL, "PATCH", "/verify-email", dat) // // // Execute // res := testutils.HTTPAuthDo(t, req, user) // // // Test // assert.StatusCodeEquals(t, res, http.StatusOK, "Status code mismatch") // // var account database.Account // var token database.Token // var tokenCount int // testutils.MustExec(t, testutils.DB.Where("user_id = ?", user.ID).First(&account), "finding account") // testutils.MustExec(t, testutils.DB.Where("user_id = ? AND type = ?", user.ID, database.TokenTypeEmailVerification).First(&token), "finding token") // testutils.MustExec(t, testutils.DB.Model(&database.Token{}).Count(&tokenCount), "counting token") // // assert.Equal(t, account.EmailVerified, true, "email_verified mismatch") // assert.NotEqual(t, token.Value, "", "token value should not have been updated") // assert.Equal(t, tokenCount, 1, "token count mismatch") // assert.NotEqual(t, token.UsedAt, (*time.Time)(nil), "token should have been used") // }) // // t.Run("used token", func(t *testing.T) { // // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // Clock: clock.NewMock(), // }) // defer server.Close() // // user := testutils.SetupUserData() // testutils.SetupAccountData(user, "alice@example.com", "pass1234") // // usedAt := time.Now().Add(time.Hour * -11).UTC() // tok := database.Token{ // UserID: user.ID, // Type: database.TokenTypeEmailVerification, // Value: "someTokenValue", // UsedAt: &usedAt, // } // testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") // // dat := `{"token": "someTokenValue"}` // req := testutils.MakeReq(server.URL, "PATCH", "/verify-email", dat) // // // Execute // res := testutils.HTTPAuthDo(t, req, user) // // // Test // assert.StatusCodeEquals(t, res, http.StatusBadRequest, "") // // var account database.Account // var token database.Token // var tokenCount int // testutils.MustExec(t, testutils.DB.Where("user_id = ?", user.ID).First(&account), "finding account") // testutils.MustExec(t, testutils.DB.Where("user_id = ? AND type = ?", user.ID, database.TokenTypeEmailVerification).First(&token), "finding token") // testutils.MustExec(t, testutils.DB.Model(&database.Token{}).Count(&tokenCount), "counting token") // // assert.Equal(t, account.EmailVerified, false, "email_verified mismatch") // assert.NotEqual(t, token.UsedAt, nil, "token used_at mismatch") // assert.Equal(t, tokenCount, 1, "token count mismatch") // assert.NotEqual(t, token.UsedAt, (*time.Time)(nil), "token should have been used") // }) // // t.Run("expired token", func(t *testing.T) { // // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // Clock: clock.NewMock(), // }) // defer server.Close() // // user := testutils.SetupUserData() // testutils.SetupAccountData(user, "alice@example.com", "pass1234") // // tok := database.Token{ // UserID: user.ID, // Type: database.TokenTypeEmailVerification, // Value: "someTokenValue", // } // testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") // testutils.MustExec(t, testutils.DB.Model(&tok).Update("created_at", time.Now().Add(time.Minute*-31)), "Failed to prepare token created_at") // // dat := `{"token": "someTokenValue"}` // req := testutils.MakeReq(server.URL, "PATCH", "/verify-email", dat) // // // Execute // res := testutils.HTTPAuthDo(t, req, user) // // // Test // assert.StatusCodeEquals(t, res, http.StatusGone, "") // // var account database.Account // var token database.Token // var tokenCount int // testutils.MustExec(t, testutils.DB.Where("user_id = ?", user.ID).First(&account), "finding account") // testutils.MustExec(t, testutils.DB.Where("user_id = ? AND type = ?", user.ID, database.TokenTypeEmailVerification).First(&token), "finding token") // testutils.MustExec(t, testutils.DB.Model(&database.Token{}).Count(&tokenCount), "counting token") // // assert.Equal(t, account.EmailVerified, false, "email_verified mismatch") // assert.Equal(t, tokenCount, 1, "token count mismatch") // assert.Equal(t, token.UsedAt, (*time.Time)(nil), "token should have not been used") // }) // // t.Run("already verified", func(t *testing.T) { // // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // Clock: clock.NewMock(), // }) // defer server.Close() // // user := testutils.SetupUserData() // a := testutils.SetupAccountData(user, "alice@example.com", "oldpass1234") // a.EmailVerified = true // testutils.MustExec(t, testutils.DB.Save(&a), "preparing account") // // tok := database.Token{ // UserID: user.ID, // Type: database.TokenTypeEmailVerification, // Value: "someTokenValue", // } // testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") // // dat := `{"token": "someTokenValue"}` // req := testutils.MakeReq(server.URL, "PATCH", "/verify-email", dat) // // // Execute // res := testutils.HTTPAuthDo(t, req, user) // // // Test // assert.StatusCodeEquals(t, res, http.StatusConflict, "") // // var account database.Account // var token database.Token // var tokenCount int // testutils.MustExec(t, testutils.DB.Where("user_id = ?", user.ID).First(&account), "finding account") // testutils.MustExec(t, testutils.DB.Where("user_id = ? AND type = ?", user.ID, database.TokenTypeEmailVerification).First(&token), "finding token") // testutils.MustExec(t, testutils.DB.Model(&database.Token{}).Count(&tokenCount), "counting token") // // assert.Equal(t, account.EmailVerified, true, "email_verified mismatch") // assert.Equal(t, tokenCount, 1, "token count mismatch") // assert.Equal(t, token.UsedAt, (*time.Time)(nil), "token should have not been used") // }) // } // // func TestUpdateEmail(t *testing.T) { // t.Run("success", func(t *testing.T) { // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // Clock: clock.NewMock(), // }) // defer server.Close() // // u := testutils.SetupUserData() // a := testutils.SetupAccountData(u, "alice@example.com", "pass1234") // a.EmailVerified = true // testutils.MustExec(t, testutils.DB.Save(&a), "updating email_verified") // // // Execute // dat := `{"email": "alice-new@example.com", "password": "pass1234"}` // req := testutils.MakeReq(server.URL, "PATCH", "/account/profile", dat) // res := testutils.HTTPAuthDo(t, req, u) // // // Test // assert.StatusCodeEquals(t, res, http.StatusOK, "") // // var user database.User // var account database.Account // testutils.MustExec(t, testutils.DB.Where("id = ?", u.ID).First(&user), "finding user") // testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&account), "finding account") // // assert.Equal(t, account.Email.String, "alice-new@example.com", "email mismatch") // assert.Equal(t, account.EmailVerified, false, "EmailVerified mismatch") // }) // // t.Run("password mismatch", func(t *testing.T) { // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // Clock: clock.NewMock(), // }) // defer server.Close() // // u := testutils.SetupUserData() // a := testutils.SetupAccountData(u, "alice@example.com", "pass1234") // a.EmailVerified = true // testutils.MustExec(t, testutils.DB.Save(&a), "updating email_verified") // // // Execute // dat := `{"email": "alice-new@example.com", "password": "wrongpassword"}` // req := testutils.MakeReq(server.URL, "PATCH", "/account/profile", dat) // res := testutils.HTTPAuthDo(t, req, u) // // // Test // assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "Status code mismsatch") // // var user database.User // var account database.Account // testutils.MustExec(t, testutils.DB.Where("id = ?", u.ID).First(&user), "finding user") // testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&account), "finding account") // // assert.Equal(t, account.Email.String, "alice@example.com", "email mismatch") // assert.Equal(t, account.EmailVerified, true, "EmailVerified mismatch") // }) // } // // func TestUpdateEmailPreference(t *testing.T) { // t.Run("with login", func(t *testing.T) { // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // Clock: clock.NewMock(), // }) // defer server.Close() // // u := testutils.SetupUserData() // testutils.SetupEmailPreferenceData(u, false) // // // Execute // dat := `{"inactive_reminder": true}` // req := testutils.MakeReq(server.URL, "PATCH", "/account/email-preference", dat) // res := testutils.HTTPAuthDo(t, req, u) // // // Test // assert.StatusCodeEquals(t, res, http.StatusOK, "") // // var preference database.EmailPreference // testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding account") // assert.Equal(t, preference.InactiveReminder, true, "preference mismatch") // }) // // t.Run("with an unused token", func(t *testing.T) { // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // Clock: clock.NewMock(), // }) // defer server.Close() // // u := testutils.SetupUserData() // testutils.SetupEmailPreferenceData(u, false) // tok := database.Token{ // UserID: u.ID, // Type: database.TokenTypeEmailPreference, // Value: "someTokenValue", // } // testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") // // // Execute // dat := `{"inactive_reminder": true}` // url := fmt.Sprintf("/account/email-preference?token=%s", "someTokenValue") // req := testutils.MakeReq(server.URL, "PATCH", url, dat) // res := testutils.HTTPDo(t, req) // // // Test // assert.StatusCodeEquals(t, res, http.StatusOK, "") // // var preference database.EmailPreference // var preferenceCount int // var token database.Token // testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") // testutils.MustExec(t, testutils.DB.Model(database.EmailPreference{}).Count(&preferenceCount), "counting preference") // testutils.MustExec(t, testutils.DB.Where("id = ?", tok.ID).First(&token), "failed to find token") // // assert.Equal(t, preferenceCount, 1, "preference count mismatch") // assert.Equal(t, preference.InactiveReminder, true, "email mismatch") // assert.NotEqual(t, token.UsedAt, (*time.Time)(nil), "token should have been used") // }) // // t.Run("with nonexistent token", func(t *testing.T) { // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // Clock: clock.NewMock(), // }) // defer server.Close() // // u := testutils.SetupUserData() // testutils.SetupEmailPreferenceData(u, true) // tok := database.Token{ // UserID: u.ID, // Type: database.TokenTypeEmailPreference, // Value: "someTokenValue", // } // testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") // // dat := `{"inactive_reminder": false}` // url := fmt.Sprintf("/account/email-preference?token=%s", "someNonexistentToken") // req := testutils.MakeReq(server.URL, "PATCH", url, dat) // // // Execute // res := testutils.HTTPDo(t, req) // // // Test // assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "") // // var preference database.EmailPreference // testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") // assert.Equal(t, preference.InactiveReminder, true, "email mismatch") // }) // // t.Run("with expired token", func(t *testing.T) { // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // // Clock: clock.NewMock(), // }) // defer server.Close() // // u := testutils.SetupUserData() // testutils.SetupEmailPreferenceData(u, true) // // usedAt := time.Now().Add(-11 * time.Minute) // tok := database.Token{ // UserID: u.ID, // Type: database.TokenTypeEmailPreference, // Value: "someTokenValue", // UsedAt: &usedAt, // } // testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") // // // Execute // dat := `{"inactive_reminder": false}` // url := fmt.Sprintf("/account/email-preference?token=%s", "someTokenValue") // req := testutils.MakeReq(server.URL, "PATCH", url, dat) // res := testutils.HTTPDo(t, req) // // // Test // assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "") // // var preference database.EmailPreference // testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") // assert.Equal(t, preference.InactiveReminder, true, "email mismatch") // }) // // t.Run("with a used but unexpired token", func(t *testing.T) { // // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // // Clock: clock.NewMock(), // }) // defer server.Close() // // u := testutils.SetupUserData() // testutils.SetupEmailPreferenceData(u, true) // usedAt := time.Now().Add(-9 * time.Minute) // tok := database.Token{ // UserID: u.ID, // Type: database.TokenTypeEmailPreference, // Value: "someTokenValue", // UsedAt: &usedAt, // } // testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") // // dat := `{"inactive_reminder": false}` // url := fmt.Sprintf("/account/email-preference?token=%s", "someTokenValue") // req := testutils.MakeReq(server.URL, "PATCH", url, dat) // // // Execute // res := testutils.HTTPDo(t, req) // // // Test // assert.StatusCodeEquals(t, res, http.StatusOK, "") // // var preference database.EmailPreference // testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") // assert.Equal(t, preference.InactiveReminder, false, "InactiveReminder mismatch") // }) // // t.Run("no user and no token", func(t *testing.T) { // // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // // Clock: clock.NewMock(), // }) // defer server.Close() // // u := testutils.SetupUserData() // testutils.SetupEmailPreferenceData(u, true) // // // Execute // dat := `{"inactive_reminder": false}` // req := testutils.MakeReq(server.URL, "PATCH", "/account/email-preference", dat) // res := testutils.HTTPDo(t, req) // // // Test // assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "") // // var preference database.EmailPreference // testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") // assert.Equal(t, preference.InactiveReminder, true, "email mismatch") // }) // // t.Run("create a record if not exists", func(t *testing.T) { // // defer testutils.ClearData(testutils.DB) // // // Setup // server := MustNewServer(t, &app.App{ // // Clock: clock.NewMock(), // }) // defer server.Close() // // u := testutils.SetupUserData() // tok := database.Token{ // UserID: u.ID, // Type: database.TokenTypeEmailPreference, // Value: "someTokenValue", // } // testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token") // // // Execute // dat := `{"inactive_reminder": false}` // url := fmt.Sprintf("/account/email-preference?token=%s", "someTokenValue") // req := testutils.MakeReq(server.URL, "PATCH", url, dat) // res := testutils.HTTPDo(t, req) // // // Test // assert.StatusCodeEquals(t, res, http.StatusOK, "") // // var preferenceCount int // testutils.MustExec(t, testutils.DB.Model(database.EmailPreference{}).Count(&preferenceCount), "counting preference") // assert.Equal(t, preferenceCount, 1, "preference count mismatch") // // var preference database.EmailPreference // testutils.MustExec(t, testutils.DB.Where("user_id = ?", u.ID).First(&preference), "finding preference") // assert.Equal(t, preference.InactiveReminder, false, "email mismatch") // }) // } // // func TestGetEmailPreference(t *testing.T) { // defer testutils.ClearData(testutils.DB) // // Setup // server := MustNewServer(t, &app.App{ // // Clock: clock.NewMock(), // }) // defer server.Close() // // u := testutils.SetupUserData() // pref := testutils.SetupEmailPreferenceData(u, true) // // // Execute // req := testutils.MakeReq(server.URL, "GET", "/account/email-preference", "") // res := testutils.HTTPAuthDo(t, req, u) // // // Test // assert.StatusCodeEquals(t, res, http.StatusOK, "") // // var got presenters.EmailPreference // if err := json.NewDecoder(res.Body).Decode(&got); err != nil { // t.Fatal(errors.Wrap(err, "decoding payload")) // } // // expected := presenters.EmailPreference{ // InactiveReminder: pref.InactiveReminder, // ProductUpdate: pref.ProductUpdate, // CreatedAt: presenters.FormatTS(pref.CreatedAt), // UpdatedAt: presenters.FormatTS(pref.UpdatedAt), // } // assert.DeepEqual(t, got, expected, "payload mismatch") // }