mirror of
https://github.com/dnote/dnote
synced 2026-03-14 22:45:50 +01:00
Fix test and remove api package
This commit is contained in:
parent
cd5d094c25
commit
348bf8398c
21 changed files with 394 additions and 4844 deletions
|
|
@ -1,187 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/dnote/pkg/server/database"
|
||||
"github.com/dnote/dnote/pkg/server/handlers"
|
||||
"github.com/dnote/dnote/pkg/server/helpers"
|
||||
"github.com/dnote/dnote/pkg/server/log"
|
||||
"github.com/dnote/dnote/pkg/server/mailer"
|
||||
"github.com/dnote/dnote/pkg/server/session"
|
||||
"github.com/dnote/dnote/pkg/server/token"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// GetMeResponse is the response for getMe endpoint
|
||||
type GetMeResponse struct {
|
||||
User session.Session `json:"user"`
|
||||
}
|
||||
|
||||
func (a *API) getMe(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
if !ok {
|
||||
handlers.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var account database.Account
|
||||
if err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
|
||||
handlers.DoError(w, "finding account", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tx := a.App.DB.Begin()
|
||||
if err := a.App.TouchLastLoginAt(user, tx); err != nil {
|
||||
tx.Rollback()
|
||||
// In case of an error, gracefully continue to avoid disturbing the service
|
||||
log.ErrorWrap(err, "error touching last_login_at")
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
response := GetMeResponse{
|
||||
User: session.New(user, account),
|
||||
}
|
||||
handlers.RespondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
type createResetTokenPayload struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func (a *API) createResetToken(w http.ResponseWriter, r *http.Request) {
|
||||
var params createResetTokenPayload
|
||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
http.Error(w, "invalid payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var account database.Account
|
||||
conn := a.App.DB.Where("email = ?", params.Email).First(&account)
|
||||
if conn.RecordNotFound() {
|
||||
return
|
||||
}
|
||||
if err := conn.Error; err != nil {
|
||||
handlers.DoError(w, errors.Wrap(err, "finding account").Error(), nil, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resetToken, err := token.Create(a.App.DB, account.UserID, database.TokenTypeResetPassword)
|
||||
if err != nil {
|
||||
handlers.DoError(w, errors.Wrap(err, "generating token").Error(), nil, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.App.SendPasswordResetEmail(account.Email.String, resetToken.Value); err != nil {
|
||||
if errors.Cause(err) == mailer.ErrSMTPNotConfigured {
|
||||
handlers.RespondInvalidSMTPConfig(w)
|
||||
} else {
|
||||
handlers.DoError(w, errors.Wrap(err, "sending password reset email").Error(), nil, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type resetPasswordPayload struct {
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (a *API) resetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
var params resetPasswordPayload
|
||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
http.Error(w, "invalid payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var token database.Token
|
||||
conn := a.App.DB.Where("value = ? AND type =? AND used_at IS NULL", params.Token, database.TokenTypeResetPassword).First(&token)
|
||||
if conn.RecordNotFound() {
|
||||
http.Error(w, "invalid token", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := conn.Error; err != nil {
|
||||
handlers.DoError(w, errors.Wrap(err, "finding token").Error(), nil, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if token.UsedAt != nil {
|
||||
http.Error(w, "invalid token", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Expire after 10 minutes
|
||||
if time.Since(token.CreatedAt).Minutes() > 10 {
|
||||
http.Error(w, "This link has been expired. Please request a new password reset link.", http.StatusGone)
|
||||
return
|
||||
}
|
||||
|
||||
tx := a.App.DB.Begin()
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(params.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
handlers.DoError(w, errors.Wrap(err, "hashing password").Error(), nil, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var account database.Account
|
||||
if err := a.App.DB.Where("user_id = ?", token.UserID).First(&account).Error; err != nil {
|
||||
tx.Rollback()
|
||||
handlers.DoError(w, errors.Wrap(err, "finding user").Error(), nil, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.Model(&account).Update("password", string(hashedPassword)).Error; err != nil {
|
||||
tx.Rollback()
|
||||
handlers.DoError(w, errors.Wrap(err, "updating password").Error(), nil, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := tx.Model(&token).Update("used_at", time.Now()).Error; err != nil {
|
||||
tx.Rollback()
|
||||
handlers.DoError(w, errors.Wrap(err, "updating password reset token").Error(), nil, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.App.DeleteUserSessions(tx, account.UserID); err != nil {
|
||||
tx.Rollback()
|
||||
handlers.DoError(w, errors.Wrap(err, "deleting user sessions").Error(), nil, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
var user database.User
|
||||
if err := a.App.DB.Where("id = ?", account.UserID).First(&user).Error; err != nil {
|
||||
handlers.DoError(w, errors.Wrap(err, "finding user").Error(), nil, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
a.respondWithSession(a.App.DB, w, user.ID, http.StatusOK)
|
||||
|
||||
if err := a.App.SendPasswordResetAlertEmail(account.Email.String); err != nil {
|
||||
log.ErrorWrap(err, "sending password reset email")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,408 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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/session"
|
||||
"github.com/dnote/dnote/pkg/server/testutils"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestGetMe(t *testing.T) {
|
||||
testutils.InitTestDB()
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
Clock: clock.NewMock(),
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
u1 := testutils.SetupUserData()
|
||||
a1 := testutils.SetupAccountData(u1, "alice@example.com", "somepassword")
|
||||
|
||||
u2 := testutils.SetupUserData()
|
||||
testutils.MustExec(t, testutils.DB.Model(&u2).Update("cloud", false), "preparing u2 cloud")
|
||||
a2 := testutils.SetupAccountData(u2, "bob@example.com", "somepassword")
|
||||
|
||||
testCases := []struct {
|
||||
user database.User
|
||||
account database.Account
|
||||
expectedPro bool
|
||||
}{
|
||||
{
|
||||
user: u1,
|
||||
account: a1,
|
||||
expectedPro: true,
|
||||
},
|
||||
{
|
||||
user: u2,
|
||||
account: a2,
|
||||
expectedPro: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("user pro %t", tc.expectedPro), func(t *testing.T) {
|
||||
// Execute
|
||||
req := testutils.MakeReq(server.URL, "GET", "/me", "")
|
||||
res := testutils.HTTPAuthDo(t, req, tc.user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
|
||||
var payload GetMeResponse
|
||||
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
expectedPayload := GetMeResponse{
|
||||
User: session.Session{
|
||||
UUID: tc.user.UUID,
|
||||
Pro: tc.expectedPro,
|
||||
Email: tc.account.Email.String,
|
||||
EmailVerified: tc.account.EmailVerified,
|
||||
},
|
||||
}
|
||||
assert.DeepEqual(t, payload, expectedPayload, "payload mismatch")
|
||||
|
||||
var user database.User
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", tc.user.ID).First(&user), "finding user")
|
||||
assert.NotEqual(t, user.LastLoginAt, nil, "LastLoginAt mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateResetToken(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()
|
||||
testutils.SetupAccountData(u, "alice@example.com", "somepassword")
|
||||
|
||||
dat := `{"email": "alice@example.com"}`
|
||||
req := testutils.MakeReq(server.URL, "POST", "/reset-token", dat)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "Status code mismtach")
|
||||
|
||||
var tokenCount int
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Token{}).Count(&tokenCount), "counting tokens")
|
||||
|
||||
var resetToken database.Token
|
||||
testutils.MustExec(t, testutils.DB.Where("user_id = ? AND type = ?", u.ID, database.TokenTypeResetPassword).First(&resetToken), "finding reset token")
|
||||
|
||||
assert.Equal(t, tokenCount, 1, "reset_token count mismatch")
|
||||
assert.NotEqual(t, resetToken.Value, nil, "reset_token value mismatch")
|
||||
assert.Equal(t, resetToken.UsedAt, (*time.Time)(nil), "reset_token UsedAt mismatch")
|
||||
})
|
||||
|
||||
t.Run("nonexistent email", 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.SetupAccountData(u, "alice@example.com", "somepassword")
|
||||
|
||||
dat := `{"email": "bob@example.com"}`
|
||||
req := testutils.MakeReq(server.URL, "POST", "/reset-token", dat)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "Status code mismtach")
|
||||
|
||||
var tokenCount int
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Token{}).Count(&tokenCount), "counting tokens")
|
||||
assert.Equal(t, tokenCount, 0, "reset_token count mismatch")
|
||||
})
|
||||
}
|
||||
|
||||
func TestResetPassword(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", "oldpassword")
|
||||
tok := database.Token{
|
||||
UserID: u.ID,
|
||||
Value: "MivFxYiSMMA4An9dP24DNQ==",
|
||||
Type: database.TokenTypeResetPassword,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token")
|
||||
otherTok := database.Token{
|
||||
UserID: u.ID,
|
||||
Value: "somerandomvalue",
|
||||
Type: database.TokenTypeEmailVerification,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&otherTok), "preparing another token")
|
||||
|
||||
dat := `{"token": "MivFxYiSMMA4An9dP24DNQ==", "password": "newpassword"}`
|
||||
req := testutils.MakeReq(server.URL, "PATCH", "/reset-password", dat)
|
||||
|
||||
s1 := database.Session{
|
||||
Key: "some-session-key-1",
|
||||
UserID: u.ID,
|
||||
ExpiresAt: time.Now().Add(time.Hour * 10 * 24),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&s1), "preparing user session 1")
|
||||
|
||||
s2 := &database.Session{
|
||||
Key: "some-session-key-2",
|
||||
UserID: u.ID,
|
||||
ExpiresAt: time.Now().Add(time.Hour * 10 * 24),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&s2), "preparing user session 2")
|
||||
|
||||
anotherUser := testutils.SetupUserData()
|
||||
testutils.MustExec(t, testutils.DB.Save(&database.Session{
|
||||
Key: "some-session-key-3",
|
||||
UserID: anotherUser.ID,
|
||||
ExpiresAt: time.Now().Add(time.Hour * 10 * 24),
|
||||
}), "preparing anotherUser session 1")
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "Status code mismatch")
|
||||
|
||||
var resetToken, verificationToken database.Token
|
||||
var account database.Account
|
||||
testutils.MustExec(t, testutils.DB.Where("value = ?", "MivFxYiSMMA4An9dP24DNQ==").First(&resetToken), "finding reset token")
|
||||
testutils.MustExec(t, testutils.DB.Where("value = ?", "somerandomvalue").First(&verificationToken), "finding reset token")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", a.ID).First(&account), "finding account")
|
||||
|
||||
assert.NotEqual(t, resetToken.UsedAt, nil, "reset_token UsedAt mismatch")
|
||||
passwordErr := bcrypt.CompareHashAndPassword([]byte(account.Password.String), []byte("newpassword"))
|
||||
assert.Equal(t, passwordErr, nil, "Password mismatch")
|
||||
assert.Equal(t, verificationToken.UsedAt, (*time.Time)(nil), "verificationToken UsedAt mismatch")
|
||||
|
||||
var s1Count, s2Count int
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Session{}).Where("id = ?", s1.ID).Count(&s1Count), "counting s1")
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Session{}).Where("id = ?", s2.ID).Count(&s2Count), "counting s2")
|
||||
|
||||
assert.Equal(t, s1Count, 0, "s1 should have been deleted")
|
||||
assert.Equal(t, s2Count, 0, "s2 should have been deleted")
|
||||
|
||||
var userSessionCount, anotherUserSessionCount int
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Session{}).Where("user_id = ?", u.ID).Count(&userSessionCount), "counting user session")
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Session{}).Where("user_id = ?", anotherUser.ID).Count(&anotherUserSessionCount), "counting anotherUser session")
|
||||
|
||||
assert.Equal(t, userSessionCount, 1, "should have created a new user session")
|
||||
assert.Equal(t, anotherUserSessionCount, 1, "anotherUser session count mismatch")
|
||||
})
|
||||
|
||||
t.Run("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()
|
||||
a := testutils.SetupAccountData(u, "alice@example.com", "somepassword")
|
||||
tok := database.Token{
|
||||
UserID: u.ID,
|
||||
Value: "MivFxYiSMMA4An9dP24DNQ==",
|
||||
Type: database.TokenTypeResetPassword,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token")
|
||||
|
||||
dat := `{"token": "-ApMnyvpg59uOU5b-Kf5uQ==", "password": "oldpassword"}`
|
||||
req := testutils.MakeReq(server.URL, "PATCH", "/reset-password", dat)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusBadRequest, "Status code mismatch")
|
||||
|
||||
var resetToken database.Token
|
||||
var account database.Account
|
||||
testutils.MustExec(t, testutils.DB.Where("value = ?", "MivFxYiSMMA4An9dP24DNQ==").First(&resetToken), "finding reset token")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", a.ID).First(&account), "finding account")
|
||||
|
||||
assert.Equal(t, a.Password, account.Password, "password should not have been updated")
|
||||
assert.Equal(t, a.Password, account.Password, "password should not have been updated")
|
||||
assert.Equal(t, resetToken.UsedAt, (*time.Time)(nil), "used_at should be nil")
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
u := testutils.SetupUserData()
|
||||
a := testutils.SetupAccountData(u, "alice@example.com", "somepassword")
|
||||
tok := database.Token{
|
||||
UserID: u.ID,
|
||||
Value: "MivFxYiSMMA4An9dP24DNQ==",
|
||||
Type: database.TokenTypeResetPassword,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token")
|
||||
testutils.MustExec(t, testutils.DB.Model(&tok).Update("created_at", time.Now().Add(time.Minute*-11)), "Failed to prepare reset_token created_at")
|
||||
|
||||
dat := `{"token": "MivFxYiSMMA4An9dP24DNQ==", "password": "oldpassword"}`
|
||||
req := testutils.MakeReq(server.URL, "PATCH", "/reset-password", dat)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusGone, "Status code mismatch")
|
||||
|
||||
var resetToken database.Token
|
||||
var account database.Account
|
||||
testutils.MustExec(t, testutils.DB.Where("value = ?", "MivFxYiSMMA4An9dP24DNQ==").First(&resetToken), "failed to find reset_token")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", a.ID).First(&account), "failed to find account")
|
||||
assert.Equal(t, a.Password, account.Password, "password should not have been updated")
|
||||
assert.Equal(t, resetToken.UsedAt, (*time.Time)(nil), "used_at should be nil")
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
u := testutils.SetupUserData()
|
||||
a := testutils.SetupAccountData(u, "alice@example.com", "somepassword")
|
||||
|
||||
usedAt := time.Now().Add(time.Hour * -11).UTC()
|
||||
tok := database.Token{
|
||||
UserID: u.ID,
|
||||
Value: "MivFxYiSMMA4An9dP24DNQ==",
|
||||
Type: database.TokenTypeResetPassword,
|
||||
UsedAt: &usedAt,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&tok), "preparing token")
|
||||
testutils.MustExec(t, testutils.DB.Model(&tok).Update("created_at", time.Now().Add(time.Minute*-11)), "Failed to prepare reset_token created_at")
|
||||
|
||||
dat := `{"token": "MivFxYiSMMA4An9dP24DNQ==", "password": "oldpassword"}`
|
||||
req := testutils.MakeReq(server.URL, "PATCH", "/reset-password", dat)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusBadRequest, "Status code mismatch")
|
||||
|
||||
var resetToken database.Token
|
||||
var account database.Account
|
||||
testutils.MustExec(t, testutils.DB.Where("value = ?", "MivFxYiSMMA4An9dP24DNQ==").First(&resetToken), "failed to find reset_token")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", a.ID).First(&account), "failed to find account")
|
||||
assert.Equal(t, a.Password, account.Password, "password should not have been updated")
|
||||
|
||||
if resetToken.UsedAt.Year() != usedAt.Year() ||
|
||||
resetToken.UsedAt.Month() != usedAt.Month() ||
|
||||
resetToken.UsedAt.Day() != usedAt.Day() ||
|
||||
resetToken.UsedAt.Hour() != usedAt.Hour() ||
|
||||
resetToken.UsedAt.Minute() != usedAt.Minute() ||
|
||||
resetToken.UsedAt.Second() != usedAt.Second() {
|
||||
t.Errorf("used_at should be %+v but got: %+v", usedAt, resetToken.UsedAt)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("using wrong type token: email_verification", 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", "somepassword")
|
||||
tok := database.Token{
|
||||
UserID: u.ID,
|
||||
Value: "MivFxYiSMMA4An9dP24DNQ==",
|
||||
Type: database.TokenTypeEmailVerification,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&tok), "Failed to prepare reset_token")
|
||||
testutils.MustExec(t, testutils.DB.Model(&tok).Update("created_at", time.Now().Add(time.Minute*-11)), "Failed to prepare reset_token created_at")
|
||||
|
||||
dat := `{"token": "MivFxYiSMMA4An9dP24DNQ==", "password": "oldpassword"}`
|
||||
req := testutils.MakeReq(server.URL, "PATCH", "/reset-password", dat)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusBadRequest, "Status code mismatch")
|
||||
|
||||
var resetToken database.Token
|
||||
var account database.Account
|
||||
testutils.MustExec(t, testutils.DB.Where("value = ?", "MivFxYiSMMA4An9dP24DNQ==").First(&resetToken), "failed to find reset_token")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", a.ID).First(&account), "failed to find account")
|
||||
|
||||
assert.Equal(t, a.Password, account.Password, "password should not have been updated")
|
||||
assert.Equal(t, resetToken.UsedAt, (*time.Time)(nil), "used_at should be nil")
|
||||
})
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
// import (
|
||||
// "net/http"
|
||||
// "strings"
|
||||
//
|
||||
// "github.com/dnote/dnote/pkg/server/database"
|
||||
// "github.com/jinzhu/gorm"
|
||||
// "github.com/pkg/errors"
|
||||
// )
|
||||
//
|
||||
// func paginate(conn *gorm.DB, page int) *gorm.DB {
|
||||
// limit := 30
|
||||
//
|
||||
// // Paginate
|
||||
// if page > 0 {
|
||||
// offset := limit * (page - 1)
|
||||
// conn = conn.Offset(offset)
|
||||
// }
|
||||
//
|
||||
// conn = conn.Limit(limit)
|
||||
//
|
||||
// return conn
|
||||
// }
|
||||
//
|
||||
// func getBookIDs(books []database.Book) []int {
|
||||
// ret := []int{}
|
||||
//
|
||||
// for _, book := range books {
|
||||
// ret = append(ret, book.ID)
|
||||
// }
|
||||
//
|
||||
// return ret
|
||||
// }
|
||||
//
|
||||
// func validatePassword(password string) error {
|
||||
// if len(password) < 8 {
|
||||
// return errors.New("Password should be longer than 8 characters")
|
||||
// }
|
||||
//
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// func getClientType(r *http.Request) string {
|
||||
// origin := r.Header.Get("Origin")
|
||||
//
|
||||
// if strings.HasPrefix(origin, "moz-extension://") {
|
||||
// return "firefox-extension"
|
||||
// }
|
||||
//
|
||||
// if strings.HasPrefix(origin, "chrome-extension://") {
|
||||
// return "chrome-extension"
|
||||
// }
|
||||
//
|
||||
// userAgent := r.Header.Get("User-Agent")
|
||||
// if strings.HasPrefix(userAgent, "Go-http-client") {
|
||||
// return "cli"
|
||||
// }
|
||||
//
|
||||
// return "web"
|
||||
// }
|
||||
//
|
||||
// // notSupported is the handler for the route that is no longer supported
|
||||
// func (a *API) notSupported(w http.ResponseWriter, r *http.Request) {
|
||||
// http.Error(w, "API version is not supported. Please upgrade your client.", http.StatusGone)
|
||||
// return
|
||||
// }
|
||||
|
|
@ -1,324 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
// import (
|
||||
// "fmt"
|
||||
// "net/http"
|
||||
// "net/url"
|
||||
// "strconv"
|
||||
// "strings"
|
||||
// "time"
|
||||
//
|
||||
// "github.com/dnote/dnote/pkg/server/database"
|
||||
// "github.com/dnote/dnote/pkg/server/helpers"
|
||||
// "github.com/dnote/dnote/pkg/server/middleware"
|
||||
// "github.com/dnote/dnote/pkg/server/operations"
|
||||
// "github.com/dnote/dnote/pkg/server/presenters"
|
||||
// "github.com/gorilla/mux"
|
||||
// "github.com/jinzhu/gorm"
|
||||
// "github.com/pkg/errors"
|
||||
// )
|
||||
//
|
||||
// type ftsParams struct {
|
||||
// HighlightAll bool
|
||||
// }
|
||||
//
|
||||
// func getHeadlineOptions(params *ftsParams) string {
|
||||
// headlineOptions := []string{
|
||||
// "StartSel=<dnotehl>",
|
||||
// "StopSel=</dnotehl>",
|
||||
// "ShortWord=0",
|
||||
// }
|
||||
//
|
||||
// if params != nil && params.HighlightAll {
|
||||
// headlineOptions = append(headlineOptions, "HighlightAll=true")
|
||||
// } else {
|
||||
// headlineOptions = append(headlineOptions, "MaxFragments=3, MaxWords=50, MinWords=10")
|
||||
// }
|
||||
//
|
||||
// return strings.Join(headlineOptions, ",")
|
||||
// }
|
||||
//
|
||||
// func selectFTSFields(conn *gorm.DB, search string, params *ftsParams) *gorm.DB {
|
||||
// headlineOpts := getHeadlineOptions(params)
|
||||
//
|
||||
// return conn.Select(`
|
||||
// notes.id,
|
||||
// notes.uuid,
|
||||
// notes.created_at,
|
||||
// notes.updated_at,
|
||||
// notes.book_uuid,
|
||||
// notes.user_id,
|
||||
// notes.added_on,
|
||||
// notes.edited_on,
|
||||
// notes.usn,
|
||||
// notes.deleted,
|
||||
// notes.encrypted,
|
||||
// ts_headline('english_nostop', notes.body, plainto_tsquery('english_nostop', ?), ?) AS body
|
||||
// `, search, headlineOpts)
|
||||
// }
|
||||
//
|
||||
// func respondWithNote(w http.ResponseWriter, note database.Note) {
|
||||
// presentedNote := presenters.PresentNote(note)
|
||||
//
|
||||
// middleware.RespondJSON(w, http.StatusOK, presentedNote)
|
||||
// }
|
||||
//
|
||||
// func parseSearchQuery(q url.Values) string {
|
||||
// searchStr := q.Get("q")
|
||||
//
|
||||
// return escapeSearchQuery(searchStr)
|
||||
// }
|
||||
//
|
||||
// func getNoteBaseQuery(db *gorm.DB, noteUUID string, search string) *gorm.DB {
|
||||
// var conn *gorm.DB
|
||||
// if search != "" {
|
||||
// conn = selectFTSFields(db, search, &ftsParams{HighlightAll: true})
|
||||
// } else {
|
||||
// conn = db
|
||||
// }
|
||||
//
|
||||
// conn = conn.Where("notes.uuid = ? AND deleted = ?", noteUUID, false)
|
||||
//
|
||||
// return conn
|
||||
// }
|
||||
//
|
||||
// func (a *API) getNote(w http.ResponseWriter, r *http.Request) {
|
||||
// user, _, err := middleware.AuthWithSession(a.App.DB, r)
|
||||
// if err != nil {
|
||||
// middleware.DoError(w, "authenticating", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// vars := mux.Vars(r)
|
||||
// noteUUID := vars["noteUUID"]
|
||||
//
|
||||
// note, ok, err := operations.GetNote(a.App.DB, noteUUID, &user)
|
||||
// if !ok {
|
||||
// middleware.RespondNotFound(w)
|
||||
// return
|
||||
// }
|
||||
// if err != nil {
|
||||
// middleware.DoError(w, "finding note", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// respondWithNote(w, note)
|
||||
// }
|
||||
//
|
||||
// /**** getNotesHandler */
|
||||
//
|
||||
// // GetNotesResponse is a reponse by getNotesHandler
|
||||
// type GetNotesResponse struct {
|
||||
// Notes []presenters.Note `json:"notes"`
|
||||
// Total int `json:"total"`
|
||||
// }
|
||||
//
|
||||
// type dateRange struct {
|
||||
// lower int64
|
||||
// upper int64
|
||||
// }
|
||||
//
|
||||
// func (a *API) getNotes(w http.ResponseWriter, r *http.Request) {
|
||||
// user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
// if !ok {
|
||||
// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
// query := r.URL.Query()
|
||||
//
|
||||
// respondGetNotes(a.App.DB, user.ID, query, w)
|
||||
// }
|
||||
//
|
||||
// func respondGetNotes(db *gorm.DB, userID int, query url.Values, w http.ResponseWriter) {
|
||||
// q, err := parseGetNotesQuery(query)
|
||||
// if err != nil {
|
||||
// http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// conn := getNotesBaseQuery(db, userID, q)
|
||||
//
|
||||
// var total int
|
||||
// if err := conn.Model(database.Note{}).Count(&total).Error; err != nil {
|
||||
// middleware.DoError(w, "counting total", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// notes := []database.Note{}
|
||||
// if total != 0 {
|
||||
// conn = orderGetNotes(conn)
|
||||
// conn = database.PreloadNote(conn)
|
||||
// conn = paginate(conn, q.Page)
|
||||
//
|
||||
// if err := conn.Find(¬es).Error; err != nil {
|
||||
// middleware.DoError(w, "finding notes", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// response := GetNotesResponse{
|
||||
// Notes: presenters.PresentNotes(notes),
|
||||
// Total: total,
|
||||
// }
|
||||
// middleware.RespondJSON(w, http.StatusOK, response)
|
||||
// }
|
||||
//
|
||||
// type getNotesQuery struct {
|
||||
// Year int
|
||||
// Month int
|
||||
// Page int
|
||||
// Books []string
|
||||
// Search string
|
||||
// Encrypted bool
|
||||
// }
|
||||
//
|
||||
// func parseGetNotesQuery(q url.Values) (getNotesQuery, error) {
|
||||
// yearStr := q.Get("year")
|
||||
// monthStr := q.Get("month")
|
||||
// books := q["book"]
|
||||
// pageStr := q.Get("page")
|
||||
// encryptedStr := q.Get("encrypted")
|
||||
//
|
||||
// fmt.Println("books", books)
|
||||
//
|
||||
// var page int
|
||||
// if len(pageStr) > 0 {
|
||||
// p, err := strconv.Atoi(pageStr)
|
||||
// if err != nil {
|
||||
// return getNotesQuery{}, errors.Errorf("invalid page %s", pageStr)
|
||||
// }
|
||||
// if p < 1 {
|
||||
// return getNotesQuery{}, errors.Errorf("invalid page %s", pageStr)
|
||||
// }
|
||||
//
|
||||
// page = p
|
||||
// } else {
|
||||
// page = 1
|
||||
// }
|
||||
//
|
||||
// var year int
|
||||
// if len(yearStr) > 0 {
|
||||
// y, err := strconv.Atoi(yearStr)
|
||||
// if err != nil {
|
||||
// return getNotesQuery{}, errors.Errorf("invalid year %s", yearStr)
|
||||
// }
|
||||
//
|
||||
// year = y
|
||||
// }
|
||||
//
|
||||
// var month int
|
||||
// if len(monthStr) > 0 {
|
||||
// m, err := strconv.Atoi(monthStr)
|
||||
// if err != nil {
|
||||
// return getNotesQuery{}, errors.Errorf("invalid month %s", monthStr)
|
||||
// }
|
||||
// if m < 1 || m > 12 {
|
||||
// return getNotesQuery{}, errors.Errorf("invalid month %s", monthStr)
|
||||
// }
|
||||
//
|
||||
// month = m
|
||||
// }
|
||||
//
|
||||
// var encrypted bool
|
||||
// if strings.ToLower(encryptedStr) == "true" {
|
||||
// encrypted = true
|
||||
// } else {
|
||||
// encrypted = false
|
||||
// }
|
||||
//
|
||||
// ret := getNotesQuery{
|
||||
// Year: year,
|
||||
// Month: month,
|
||||
// Page: page,
|
||||
// Search: parseSearchQuery(q),
|
||||
// Books: books,
|
||||
// Encrypted: encrypted,
|
||||
// }
|
||||
//
|
||||
// return ret, nil
|
||||
// }
|
||||
//
|
||||
// func getDateBounds(year, month int) (int64, int64) {
|
||||
// var yearUpperbound, monthUpperbound int
|
||||
//
|
||||
// if month == 12 {
|
||||
// monthUpperbound = 1
|
||||
// yearUpperbound = year + 1
|
||||
// } else {
|
||||
// monthUpperbound = month + 1
|
||||
// yearUpperbound = year
|
||||
// }
|
||||
//
|
||||
// lower := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC).UnixNano()
|
||||
// upper := time.Date(yearUpperbound, time.Month(monthUpperbound), 1, 0, 0, 0, 0, time.UTC).UnixNano()
|
||||
//
|
||||
// return lower, upper
|
||||
// }
|
||||
//
|
||||
// func getNotesBaseQuery(db *gorm.DB, userID int, q getNotesQuery) *gorm.DB {
|
||||
// conn := db.Where(
|
||||
// "notes.user_id = ? AND notes.deleted = ? AND notes.encrypted = ?",
|
||||
// userID, false, q.Encrypted,
|
||||
// )
|
||||
//
|
||||
// if q.Search != "" {
|
||||
// conn = selectFTSFields(conn, q.Search, nil)
|
||||
// conn = conn.Where("tsv @@ plainto_tsquery('english_nostop', ?)", q.Search)
|
||||
// }
|
||||
//
|
||||
// if len(q.Books) > 0 {
|
||||
// conn = conn.Joins("INNER JOIN books ON books.uuid = notes.book_uuid").
|
||||
// Where("books.label in (?)", q.Books)
|
||||
// }
|
||||
//
|
||||
// if q.Year != 0 || q.Month != 0 {
|
||||
// dateLowerbound, dateUpperbound := getDateBounds(q.Year, q.Month)
|
||||
// conn = conn.Where("notes.added_on >= ? AND notes.added_on < ?", dateLowerbound, dateUpperbound)
|
||||
// }
|
||||
//
|
||||
// return conn
|
||||
// }
|
||||
//
|
||||
// func orderGetNotes(conn *gorm.DB) *gorm.DB {
|
||||
// return conn.Order("notes.updated_at DESC, notes.id DESC")
|
||||
// }
|
||||
//
|
||||
// // escapeSearchQuery escapes the query for full text search
|
||||
// func escapeSearchQuery(searchQuery string) string {
|
||||
// return strings.Join(strings.Fields(searchQuery), "&")
|
||||
// }
|
||||
//
|
||||
// func (a *API) legacyGetNotes(w http.ResponseWriter, r *http.Request) {
|
||||
// user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
// if !ok {
|
||||
// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// var notes []database.Note
|
||||
// if err := a.App.DB.Where("user_id = ? AND encrypted = true", user.ID).Find(¬es).Error; err != nil {
|
||||
// middleware.DoError(w, "finding notes", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// presented := presenters.PresentNotes(notes)
|
||||
// middleware.RespondJSON(w, http.StatusOK, presented)
|
||||
// }
|
||||
|
|
@ -1,357 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
// import (
|
||||
// "encoding/json"
|
||||
// "fmt"
|
||||
// "io/ioutil"
|
||||
// "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"
|
||||
// )
|
||||
//
|
||||
// func getExpectedNotePayload(n database.Note, b database.Book, u database.User) presenters.Note {
|
||||
// return presenters.Note{
|
||||
// UUID: n.UUID,
|
||||
// CreatedAt: n.CreatedAt,
|
||||
// UpdatedAt: n.UpdatedAt,
|
||||
// Body: n.Body,
|
||||
// AddedOn: n.AddedOn,
|
||||
// Public: n.Public,
|
||||
// USN: n.USN,
|
||||
// Book: presenters.NoteBook{
|
||||
// UUID: b.UUID,
|
||||
// Label: b.Label,
|
||||
// },
|
||||
// User: presenters.NoteUser{
|
||||
// UUID: u.UUID,
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func TestGetNotes(t *testing.T) {
|
||||
// defer testutils.ClearData(testutils.DB)
|
||||
//
|
||||
// // Setup
|
||||
// server := MustNewServer(t, &app.App{
|
||||
// Clock: clock.NewMock(),
|
||||
// })
|
||||
// defer server.Close()
|
||||
//
|
||||
// user := testutils.SetupUserData()
|
||||
// anotherUser := testutils.SetupUserData()
|
||||
//
|
||||
// b1 := database.Book{
|
||||
// UserID: user.ID,
|
||||
// Label: "js",
|
||||
// }
|
||||
// testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1")
|
||||
// b2 := database.Book{
|
||||
// UserID: user.ID,
|
||||
// Label: "css",
|
||||
// }
|
||||
// testutils.MustExec(t, testutils.DB.Save(&b2), "preparing b2")
|
||||
// b3 := database.Book{
|
||||
// UserID: anotherUser.ID,
|
||||
// Label: "css",
|
||||
// }
|
||||
// testutils.MustExec(t, testutils.DB.Save(&b3), "preparing b3")
|
||||
//
|
||||
// n1 := database.Note{
|
||||
// UserID: user.ID,
|
||||
// BookUUID: b1.UUID,
|
||||
// Body: "n1 content",
|
||||
// USN: 11,
|
||||
// Deleted: false,
|
||||
// AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
|
||||
// }
|
||||
// testutils.MustExec(t, testutils.DB.Save(&n1), "preparing n1")
|
||||
// n2 := database.Note{
|
||||
// UserID: user.ID,
|
||||
// BookUUID: b1.UUID,
|
||||
// Body: "n2 content",
|
||||
// USN: 14,
|
||||
// Deleted: false,
|
||||
// AddedOn: time.Date(2018, time.August, 11, 22, 0, 0, 0, time.UTC).UnixNano(),
|
||||
// }
|
||||
// testutils.MustExec(t, testutils.DB.Save(&n2), "preparing n2")
|
||||
// n3 := database.Note{
|
||||
// UserID: user.ID,
|
||||
// BookUUID: b1.UUID,
|
||||
// Body: "n3 content",
|
||||
// USN: 17,
|
||||
// Deleted: false,
|
||||
// AddedOn: time.Date(2017, time.January, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
|
||||
// }
|
||||
// testutils.MustExec(t, testutils.DB.Save(&n3), "preparing n3")
|
||||
// n4 := database.Note{
|
||||
// UserID: user.ID,
|
||||
// BookUUID: b2.UUID,
|
||||
// Body: "n4 content",
|
||||
// USN: 18,
|
||||
// Deleted: false,
|
||||
// AddedOn: time.Date(2018, time.September, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
|
||||
// }
|
||||
// testutils.MustExec(t, testutils.DB.Save(&n4), "preparing n4")
|
||||
// n5 := database.Note{
|
||||
// UserID: anotherUser.ID,
|
||||
// BookUUID: b3.UUID,
|
||||
// Body: "n5 content",
|
||||
// USN: 19,
|
||||
// Deleted: false,
|
||||
// AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
|
||||
// }
|
||||
// testutils.MustExec(t, testutils.DB.Save(&n5), "preparing n5")
|
||||
// n6 := database.Note{
|
||||
// UserID: user.ID,
|
||||
// BookUUID: b1.UUID,
|
||||
// Body: "",
|
||||
// USN: 11,
|
||||
// Deleted: true,
|
||||
// AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
|
||||
// }
|
||||
// testutils.MustExec(t, testutils.DB.Save(&n6), "preparing n6")
|
||||
//
|
||||
// // Execute
|
||||
// req := testutils.MakeReq(server.URL, "GET", "/notes?year=2018&month=8", "")
|
||||
// res := testutils.HTTPAuthDo(t, req, user)
|
||||
//
|
||||
// // Test
|
||||
// assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
//
|
||||
// var payload GetNotesResponse
|
||||
// if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
// t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
// }
|
||||
//
|
||||
// var n2Record, n1Record database.Note
|
||||
// testutils.MustExec(t, testutils.DB.Where("uuid = ?", n2.UUID).First(&n2Record), "finding n2Record")
|
||||
// testutils.MustExec(t, testutils.DB.Where("uuid = ?", n1.UUID).First(&n1Record), "finding n1Record")
|
||||
//
|
||||
// expected := GetNotesResponse{
|
||||
// Notes: []presenters.Note{
|
||||
// getExpectedNotePayload(n2Record, b1, user),
|
||||
// getExpectedNotePayload(n1Record, b1, user),
|
||||
// },
|
||||
// Total: 2,
|
||||
// }
|
||||
//
|
||||
// assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
// }
|
||||
//
|
||||
// func TestGetNote(t *testing.T) {
|
||||
// defer testutils.ClearData(testutils.DB)
|
||||
//
|
||||
// // Setup
|
||||
// server := MustNewServer(t, &app.App{
|
||||
// Clock: clock.NewMock(),
|
||||
// })
|
||||
// defer server.Close()
|
||||
//
|
||||
// user := testutils.SetupUserData()
|
||||
// anotherUser := testutils.SetupUserData()
|
||||
//
|
||||
// b1 := database.Book{
|
||||
// UserID: user.ID,
|
||||
// Label: "js",
|
||||
// }
|
||||
// testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1")
|
||||
//
|
||||
// privateNote := database.Note{
|
||||
// UserID: user.ID,
|
||||
// BookUUID: b1.UUID,
|
||||
// Body: "privateNote content",
|
||||
// Public: false,
|
||||
// }
|
||||
// testutils.MustExec(t, testutils.DB.Save(&privateNote), "preparing privateNote")
|
||||
// publicNote := database.Note{
|
||||
// UserID: user.ID,
|
||||
// BookUUID: b1.UUID,
|
||||
// Body: "publicNote content",
|
||||
// Public: true,
|
||||
// }
|
||||
// testutils.MustExec(t, testutils.DB.Save(&publicNote), "preparing publicNote")
|
||||
// deletedNote := database.Note{
|
||||
// UserID: user.ID,
|
||||
// BookUUID: b1.UUID,
|
||||
// Deleted: true,
|
||||
// }
|
||||
// testutils.MustExec(t, testutils.DB.Save(&deletedNote), "preparing publicNote")
|
||||
//
|
||||
// t.Run("owner accessing private note", func(t *testing.T) {
|
||||
// // Execute
|
||||
// url := fmt.Sprintf("/notes/%s", privateNote.UUID)
|
||||
// req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
// res := testutils.HTTPAuthDo(t, req, user)
|
||||
//
|
||||
// // Test
|
||||
// assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
//
|
||||
// var payload presenters.Note
|
||||
// if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
// t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
// }
|
||||
//
|
||||
// var n1Record database.Note
|
||||
// testutils.MustExec(t, testutils.DB.Where("uuid = ?", privateNote.UUID).First(&n1Record), "finding n1Record")
|
||||
//
|
||||
// expected := getExpectedNotePayload(n1Record, b1, user)
|
||||
// assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
// })
|
||||
//
|
||||
// t.Run("owner accessing public note", func(t *testing.T) {
|
||||
// // Execute
|
||||
// url := fmt.Sprintf("/notes/%s", publicNote.UUID)
|
||||
// req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
// res := testutils.HTTPAuthDo(t, req, user)
|
||||
//
|
||||
// // Test
|
||||
// assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
//
|
||||
// var payload presenters.Note
|
||||
// if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
// t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
// }
|
||||
//
|
||||
// var n2Record database.Note
|
||||
// testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record")
|
||||
//
|
||||
// expected := getExpectedNotePayload(n2Record, b1, user)
|
||||
// assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
// })
|
||||
//
|
||||
// t.Run("non-owner accessing public note", func(t *testing.T) {
|
||||
// // Execute
|
||||
// url := fmt.Sprintf("/notes/%s", publicNote.UUID)
|
||||
// req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
// res := testutils.HTTPAuthDo(t, req, anotherUser)
|
||||
//
|
||||
// // Test
|
||||
// assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
//
|
||||
// var payload presenters.Note
|
||||
// if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
// t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
// }
|
||||
//
|
||||
// var n2Record database.Note
|
||||
// testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record")
|
||||
//
|
||||
// expected := getExpectedNotePayload(n2Record, b1, user)
|
||||
// assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
// })
|
||||
//
|
||||
// t.Run("non-owner accessing private note", func(t *testing.T) {
|
||||
// // Execute
|
||||
// url := fmt.Sprintf("/notes/%s", privateNote.UUID)
|
||||
// req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
// res := testutils.HTTPAuthDo(t, req, anotherUser)
|
||||
//
|
||||
// // Test
|
||||
// assert.StatusCodeEquals(t, res, http.StatusNotFound, "")
|
||||
//
|
||||
// body, err := ioutil.ReadAll(res.Body)
|
||||
// if err != nil {
|
||||
// t.Fatal(errors.Wrap(err, "reading body"))
|
||||
// }
|
||||
//
|
||||
// assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
|
||||
// })
|
||||
//
|
||||
// t.Run("guest accessing public note", func(t *testing.T) {
|
||||
// // Execute
|
||||
// url := fmt.Sprintf("/notes/%s", publicNote.UUID)
|
||||
// req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
// res := testutils.HTTPDo(t, req)
|
||||
//
|
||||
// // Test
|
||||
// assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
//
|
||||
// var payload presenters.Note
|
||||
// if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
// t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
// }
|
||||
//
|
||||
// var n2Record database.Note
|
||||
// testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record")
|
||||
//
|
||||
// expected := getExpectedNotePayload(n2Record, b1, user)
|
||||
// assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
// })
|
||||
//
|
||||
// t.Run("guest accessing private note", func(t *testing.T) {
|
||||
// // Execute
|
||||
// url := fmt.Sprintf("/notes/%s", privateNote.UUID)
|
||||
// req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
// res := testutils.HTTPDo(t, req)
|
||||
//
|
||||
// // Test
|
||||
// assert.StatusCodeEquals(t, res, http.StatusNotFound, "")
|
||||
//
|
||||
// body, err := ioutil.ReadAll(res.Body)
|
||||
// if err != nil {
|
||||
// t.Fatal(errors.Wrap(err, "reading body"))
|
||||
// }
|
||||
//
|
||||
// assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
|
||||
// })
|
||||
//
|
||||
// t.Run("nonexistent", func(t *testing.T) {
|
||||
// // Execute
|
||||
// url := fmt.Sprintf("/notes/%s", "someRandomString")
|
||||
// req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
// res := testutils.HTTPAuthDo(t, req, user)
|
||||
//
|
||||
// // Test
|
||||
// assert.StatusCodeEquals(t, res, http.StatusNotFound, "")
|
||||
//
|
||||
// body, err := ioutil.ReadAll(res.Body)
|
||||
// if err != nil {
|
||||
// t.Fatal(errors.Wrap(err, "reading body"))
|
||||
// }
|
||||
//
|
||||
// assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
|
||||
// })
|
||||
//
|
||||
// t.Run("deleted", func(t *testing.T) {
|
||||
// // Execute
|
||||
// url := fmt.Sprintf("/notes/%s", deletedNote.UUID)
|
||||
// req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
// res := testutils.HTTPAuthDo(t, req, user)
|
||||
//
|
||||
// // Test
|
||||
// assert.StatusCodeEquals(t, res, http.StatusNotFound, "")
|
||||
//
|
||||
// body, err := ioutil.ReadAll(res.Body)
|
||||
// if err != nil {
|
||||
// t.Fatal(errors.Wrap(err, "reading body"))
|
||||
// }
|
||||
//
|
||||
// assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
|
||||
// })
|
||||
// }
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
// import (
|
||||
// "net/http"
|
||||
// "os"
|
||||
//
|
||||
// "github.com/dnote/dnote/pkg/server/app"
|
||||
// "github.com/dnote/dnote/pkg/server/database"
|
||||
// "github.com/dnote/dnote/pkg/server/middleware"
|
||||
// "github.com/gorilla/mux"
|
||||
// "github.com/pkg/errors"
|
||||
// )
|
||||
//
|
||||
// // API is a web API configuration
|
||||
// type API struct {
|
||||
// App *app.App
|
||||
// }
|
||||
//
|
||||
// // init sets up the application based on the configuration
|
||||
// func (a *API) init() error {
|
||||
// if err := a.App.Validate(); err != nil {
|
||||
// return errors.Wrap(err, "validating the app parameters")
|
||||
// }
|
||||
//
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// func applyMiddleware(h http.HandlerFunc, rateLimit bool) http.Handler {
|
||||
// ret := h
|
||||
// ret = middleware.Logging(ret)
|
||||
//
|
||||
// if rateLimit && os.Getenv("GO_ENV") != "TEST" {
|
||||
// ret = middleware.Limit(ret)
|
||||
// }
|
||||
//
|
||||
// return ret
|
||||
// }
|
||||
//
|
||||
// // NewRouter creates and returns a new router
|
||||
// func NewRouter(a *API) (*mux.Router, error) {
|
||||
// if err := a.init(); err != nil {
|
||||
// return nil, errors.Wrap(err, "initializing app")
|
||||
// }
|
||||
//
|
||||
// proOnly := middleware.AuthParams{ProOnly: true}
|
||||
// app := a.App
|
||||
//
|
||||
// var routes = []middleware.Route{
|
||||
// // internal
|
||||
// {Method: "GET", Pattern: "/health", HandlerFunc: a.checkHealth, RateLimit: false},
|
||||
// {Method: "GET", Pattern: "/me", HandlerFunc: middleware.Auth(app, a.getMe, nil), RateLimit: true},
|
||||
// {Method: "POST", Pattern: "/verification-token", HandlerFunc: middleware.Auth(app, a.createVerificationToken, nil), RateLimit: true},
|
||||
// {Method: "PATCH", Pattern: "/verify-email", HandlerFunc: a.verifyEmail, RateLimit: true},
|
||||
// {Method: "POST", Pattern: "/reset-token", HandlerFunc: a.createResetToken, RateLimit: true},
|
||||
// {Method: "PATCH", Pattern: "/reset-password", HandlerFunc: a.resetPassword, RateLimit: true},
|
||||
// {Method: "PATCH", Pattern: "/account/profile", HandlerFunc: middleware.Auth(app, a.updateProfile, nil), RateLimit: true},
|
||||
// {Method: "PATCH", Pattern: "/account/password", HandlerFunc: middleware.Auth(app, a.updatePassword, nil), RateLimit: true},
|
||||
// {Method: "GET", Pattern: "/account/email-preference", HandlerFunc: middleware.TokenAuth(app, a.getEmailPreference, database.TokenTypeEmailPreference, nil), RateLimit: true},
|
||||
// {Method: "PATCH", Pattern: "/account/email-preference", HandlerFunc: middleware.TokenAuth(app, a.updateEmailPreference, database.TokenTypeEmailPreference, nil), RateLimit: true},
|
||||
// {Method: "GET", Pattern: "/notes", HandlerFunc: middleware.Auth(app, a.getNotes, nil), RateLimit: false},
|
||||
// {Method: "GET", Pattern: "/notes/{noteUUID}", HandlerFunc: a.getNote, RateLimit: true},
|
||||
// {Method: "GET", Pattern: "/calendar", HandlerFunc: middleware.Auth(app, a.getCalendar, nil), RateLimit: true},
|
||||
//
|
||||
// // v3
|
||||
// {Method: "GET", Pattern: "/v3/sync/fragment", HandlerFunc: middleware.Cors(middleware.Auth(app, a.GetSyncFragment, &proOnly)), RateLimit: false},
|
||||
// {Method: "GET", Pattern: "/v3/sync/state", HandlerFunc: middleware.Cors(middleware.Auth(app, a.GetSyncState, &proOnly)), RateLimit: false},
|
||||
// {Method: "OPTIONS", Pattern: "/v3/books", HandlerFunc: middleware.Cors(a.BooksOptions), RateLimit: true},
|
||||
// {Method: "GET", Pattern: "/v3/books", HandlerFunc: middleware.Cors(middleware.Auth(app, a.GetBooks, &proOnly)), RateLimit: true},
|
||||
// {Method: "GET", Pattern: "/v3/books/{bookUUID}", HandlerFunc: middleware.Cors(middleware.Auth(app, a.GetBook, &proOnly)), RateLimit: true},
|
||||
// {Method: "POST", Pattern: "/v3/books", HandlerFunc: middleware.Cors(middleware.Auth(app, a.CreateBook, &proOnly)), RateLimit: false},
|
||||
// {Method: "PATCH", Pattern: "/v3/books/{bookUUID}", HandlerFunc: middleware.Cors(middleware.Auth(app, a.UpdateBook, &proOnly)), RateLimit: false},
|
||||
// {Method: "DELETE", Pattern: "/v3/books/{bookUUID}", HandlerFunc: middleware.Cors(middleware.Auth(app, a.DeleteBook, &proOnly)), RateLimit: false},
|
||||
// {Method: "OPTIONS", Pattern: "/v3/notes", HandlerFunc: middleware.Cors(a.NotesOptions), RateLimit: true},
|
||||
// {Method: "POST", Pattern: "/v3/notes", HandlerFunc: middleware.Cors(middleware.Auth(app, a.CreateNote, &proOnly)), RateLimit: false},
|
||||
// {Method: "PATCH", Pattern: "/v3/notes/{noteUUID}", HandlerFunc: middleware.Auth(app, a.UpdateNote, &proOnly), RateLimit: false},
|
||||
// {Method: "DELETE", Pattern: "/v3/notes/{noteUUID}", HandlerFunc: middleware.Auth(app, a.DeleteNote, &proOnly), RateLimit: false},
|
||||
// {Method: "POST", Pattern: "/v3/signin", HandlerFunc: middleware.Cors(a.signin), RateLimit: true},
|
||||
// {Method: "OPTIONS", Pattern: "/v3/signout", HandlerFunc: middleware.Cors(a.signoutOptions), RateLimit: true},
|
||||
// {Method: "POST", Pattern: "/v3/signout", HandlerFunc: middleware.Cors(a.signout), RateLimit: true},
|
||||
// {Method: "POST", Pattern: "/v3/register", HandlerFunc: a.register, RateLimit: true},
|
||||
// }
|
||||
//
|
||||
// router := mux.NewRouter().StrictSlash(true)
|
||||
//
|
||||
// router.PathPrefix("/v1").Handler(applyMiddleware(middleware.NotSupported, true))
|
||||
// router.PathPrefix("/v2").Handler(applyMiddleware(middleware.NotSupported, true))
|
||||
//
|
||||
// for _, route := range routes {
|
||||
// handler := route.HandlerFunc
|
||||
//
|
||||
// router.
|
||||
// Methods(route.Method).
|
||||
// Path(route.Pattern).
|
||||
// Handler(applyMiddleware(handler, route.RateLimit))
|
||||
// }
|
||||
//
|
||||
// return router, nil
|
||||
// }
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"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/config"
|
||||
"github.com/dnote/dnote/pkg/server/mailer"
|
||||
"github.com/dnote/dnote/pkg/server/testutils"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestNotSupportedVersions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
path string
|
||||
}{
|
||||
// v1
|
||||
{
|
||||
path: "/v1",
|
||||
},
|
||||
{
|
||||
path: "/v1/foo",
|
||||
},
|
||||
{
|
||||
path: "/v1/bar/baz",
|
||||
},
|
||||
// v2
|
||||
{
|
||||
path: "/v2",
|
||||
},
|
||||
{
|
||||
path: "/v2/foo",
|
||||
},
|
||||
{
|
||||
path: "/v2/bar/baz",
|
||||
},
|
||||
}
|
||||
|
||||
// setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
DB: &gorm.DB{},
|
||||
Clock: clock.NewMock(),
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.path, func(t *testing.T) {
|
||||
// execute
|
||||
req := testutils.MakeReq(server.URL, "GET", tc.path, "")
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// test
|
||||
assert.Equal(t, res.StatusCode, http.StatusGone, "status code mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRouter_AppValidate(t *testing.T) {
|
||||
c := config.Load()
|
||||
|
||||
configWithoutWebURL := config.Load()
|
||||
configWithoutWebURL.WebURL = ""
|
||||
|
||||
testCases := []struct {
|
||||
app app.App
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
app: app.App{
|
||||
DB: &gorm.DB{},
|
||||
Clock: clock.NewMock(),
|
||||
EmailTemplates: mailer.Templates{},
|
||||
EmailBackend: &testutils.MockEmailbackendImplementation{},
|
||||
Config: c,
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
app: app.App{
|
||||
DB: nil,
|
||||
Clock: clock.NewMock(),
|
||||
EmailTemplates: mailer.Templates{},
|
||||
EmailBackend: &testutils.MockEmailbackendImplementation{},
|
||||
Config: c,
|
||||
},
|
||||
expectedErr: app.ErrEmptyDB,
|
||||
},
|
||||
{
|
||||
app: app.App{
|
||||
DB: &gorm.DB{},
|
||||
Clock: nil,
|
||||
EmailTemplates: mailer.Templates{},
|
||||
EmailBackend: &testutils.MockEmailbackendImplementation{},
|
||||
Config: c,
|
||||
},
|
||||
expectedErr: app.ErrEmptyClock,
|
||||
},
|
||||
{
|
||||
app: app.App{
|
||||
DB: &gorm.DB{},
|
||||
Clock: clock.NewMock(),
|
||||
EmailTemplates: nil,
|
||||
EmailBackend: &testutils.MockEmailbackendImplementation{},
|
||||
Config: c,
|
||||
},
|
||||
expectedErr: app.ErrEmptyEmailTemplates,
|
||||
},
|
||||
{
|
||||
app: app.App{
|
||||
DB: &gorm.DB{},
|
||||
Clock: clock.NewMock(),
|
||||
EmailTemplates: mailer.Templates{},
|
||||
EmailBackend: nil,
|
||||
Config: c,
|
||||
},
|
||||
expectedErr: app.ErrEmptyEmailBackend,
|
||||
},
|
||||
{
|
||||
app: app.App{
|
||||
DB: &gorm.DB{},
|
||||
Clock: clock.NewMock(),
|
||||
EmailTemplates: mailer.Templates{},
|
||||
EmailBackend: &testutils.MockEmailbackendImplementation{},
|
||||
Config: configWithoutWebURL,
|
||||
},
|
||||
expectedErr: app.ErrEmptyWebURL,
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) {
|
||||
api := API{App: &tc.app}
|
||||
_, err := NewRouter(&api)
|
||||
|
||||
assert.Equal(t, errors.Cause(err), tc.expectedErr, "error mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
// import (
|
||||
// "net/http/httptest"
|
||||
// "testing"
|
||||
//
|
||||
// "github.com/dnote/dnote/pkg/server/app"
|
||||
// "github.com/pkg/errors"
|
||||
// )
|
||||
//
|
||||
// // MustNewServer is a test utility function to initialize a new server
|
||||
// // with the given app paratmers
|
||||
// func MustNewServer(t *testing.T, appParams *app.App) *httptest.Server {
|
||||
// api := NewTestAPI(appParams)
|
||||
// r, err := NewRouter(&api)
|
||||
// if err != nil {
|
||||
// t.Fatal(errors.Wrap(err, "initializing server"))
|
||||
// }
|
||||
//
|
||||
// server := httptest.NewServer(r)
|
||||
//
|
||||
// return server
|
||||
// }
|
||||
//
|
||||
// // NewTestAPI returns a new API for test
|
||||
// func NewTestAPI(appParams *app.App) API {
|
||||
// a := app.NewTest(appParams)
|
||||
//
|
||||
// return API{App: &a}
|
||||
// }
|
||||
|
|
@ -1,394 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
// import (
|
||||
// "encoding/json"
|
||||
// "net/http"
|
||||
// "time"
|
||||
//
|
||||
// "github.com/dnote/dnote/pkg/server/database"
|
||||
// "github.com/dnote/dnote/pkg/server/middleware"
|
||||
// "github.com/dnote/dnote/pkg/server/helpers"
|
||||
// "github.com/dnote/dnote/pkg/server/log"
|
||||
// "github.com/dnote/dnote/pkg/server/mailer"
|
||||
// "github.com/dnote/dnote/pkg/server/presenters"
|
||||
// "github.com/dnote/dnote/pkg/server/session"
|
||||
// "github.com/dnote/dnote/pkg/server/token"
|
||||
// "github.com/jinzhu/gorm"
|
||||
// "github.com/pkg/errors"
|
||||
// "golang.org/x/crypto/bcrypt"
|
||||
// )
|
||||
//
|
||||
// type updateProfilePayload struct {
|
||||
// Email string `json:"email"`
|
||||
// Password string `json:"password"`
|
||||
// }
|
||||
//
|
||||
// // updateProfile updates user
|
||||
// func (a *API) updateProfile(w http.ResponseWriter, r *http.Request) {
|
||||
// user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
// if !ok {
|
||||
// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// var account database.Account
|
||||
// if err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
|
||||
// middleware.DoError(w, "getting account", nil, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// var params updateProfilePayload
|
||||
// err := json.NewDecoder(r.Body).Decode(¶ms)
|
||||
// if err != nil {
|
||||
// http.Error(w, errors.Wrap(err, "invalid params").Error(), http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// password := []byte(params.Password)
|
||||
// if err := bcrypt.CompareHashAndPassword([]byte(account.Password.String), password); err != nil {
|
||||
// log.WithFields(log.Fields{
|
||||
// "user_id": user.ID,
|
||||
// }).Warn("invalid email update attempt")
|
||||
// http.Error(w, "Wrong password", http.StatusUnauthorized)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Validate
|
||||
// if len(params.Email) > 60 {
|
||||
// http.Error(w, "Email is too long", http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// tx := a.App.DB.Begin()
|
||||
// if err := tx.Save(&user).Error; err != nil {
|
||||
// tx.Rollback()
|
||||
// middleware.DoError(w, "saving user", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // check if email was changed
|
||||
// if params.Email != account.Email.String {
|
||||
// account.EmailVerified = false
|
||||
// }
|
||||
// account.Email.String = params.Email
|
||||
//
|
||||
// if err := tx.Save(&account).Error; err != nil {
|
||||
// tx.Rollback()
|
||||
// middleware.DoError(w, "saving account", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// tx.Commit()
|
||||
//
|
||||
// a.respondWithSession(a.App.DB, w, user.ID, http.StatusOK)
|
||||
// }
|
||||
//
|
||||
// type updateEmailPayload struct {
|
||||
// NewEmail string `json:"new_email"`
|
||||
// NewCipherKeyEnc string `json:"new_cipher_key_enc"`
|
||||
// OldAuthKey string `json:"old_auth_key"`
|
||||
// NewAuthKey string `json:"new_auth_key"`
|
||||
// }
|
||||
//
|
||||
// func respondWithCalendar(db *gorm.DB, w http.ResponseWriter, userID int) {
|
||||
// rows, err := db.Table("notes").Select("COUNT(id), date(to_timestamp(added_on/1000000000)) AS added_date").
|
||||
// Where("user_id = ?", userID).
|
||||
// Group("added_date").
|
||||
// Order("added_date DESC").Rows()
|
||||
//
|
||||
// if err != nil {
|
||||
// middleware.DoError(w, "Failed to count lessons", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// payload := map[string]int{}
|
||||
//
|
||||
// for rows.Next() {
|
||||
// var count int
|
||||
// var d time.Time
|
||||
//
|
||||
// if err := rows.Scan(&count, &d); err != nil {
|
||||
// middleware.DoError(w, "counting notes", err, http.StatusInternalServerError)
|
||||
// }
|
||||
// payload[d.Format("2006-1-2")] = count
|
||||
// }
|
||||
//
|
||||
// middleware.RespondJSON(w, http.StatusOK, payload)
|
||||
// }
|
||||
//
|
||||
// func (a *API) getCalendar(w http.ResponseWriter, r *http.Request) {
|
||||
// user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
// if !ok {
|
||||
// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// respondWithCalendar(a.App.DB, w, user.ID)
|
||||
// }
|
||||
//
|
||||
// func (a *API) createVerificationToken(w http.ResponseWriter, r *http.Request) {
|
||||
// user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
// if !ok {
|
||||
// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// var account database.Account
|
||||
// err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error
|
||||
// if err != nil {
|
||||
// middleware.DoError(w, "finding account", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if account.EmailVerified {
|
||||
// http.Error(w, "Email already verified", http.StatusGone)
|
||||
// return
|
||||
// }
|
||||
// if account.Email.String == "" {
|
||||
// http.Error(w, "Email not set", http.StatusUnprocessableEntity)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// tok, err := token.Create(a.App.DB, account.UserID, database.TokenTypeEmailVerification)
|
||||
// if err != nil {
|
||||
// middleware.DoError(w, "saving token", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if err := a.App.SendVerificationEmail(account.Email.String, tok.Value); err != nil {
|
||||
// if errors.Cause(err) == mailer.ErrSMTPNotConfigured {
|
||||
// middleware.RespondInvalidSMTPConfig(w)
|
||||
// } else {
|
||||
// middleware.DoError(w, errors.Wrap(err, "sending verification email").Error(), nil, http.StatusInternalServerError)
|
||||
// }
|
||||
//
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// w.WriteHeader(http.StatusCreated)
|
||||
// }
|
||||
//
|
||||
// type verifyEmailPayload struct {
|
||||
// Token string `json:"token"`
|
||||
// }
|
||||
//
|
||||
// func (a *API) verifyEmail(w http.ResponseWriter, r *http.Request) {
|
||||
// var params verifyEmailPayload
|
||||
// if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
// middleware.DoError(w, "decoding payload", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// var token database.Token
|
||||
// if err := a.App.DB.
|
||||
// Where("value = ? AND type = ?", params.Token, database.TokenTypeEmailVerification).
|
||||
// First(&token).Error; err != nil {
|
||||
// http.Error(w, "invalid token", http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if token.UsedAt != nil {
|
||||
// http.Error(w, "invalid token", http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Expire after ttl
|
||||
// if time.Since(token.CreatedAt).Minutes() > 30 {
|
||||
// http.Error(w, "This link has been expired. Please request a new link.", http.StatusGone)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// var account database.Account
|
||||
// if err := a.App.DB.Where("user_id = ?", token.UserID).First(&account).Error; err != nil {
|
||||
// middleware.DoError(w, "finding account", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
// if account.EmailVerified {
|
||||
// http.Error(w, "Already verified", http.StatusConflict)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// tx := a.App.DB.Begin()
|
||||
// account.EmailVerified = true
|
||||
// if err := tx.Save(&account).Error; err != nil {
|
||||
// tx.Rollback()
|
||||
// middleware.DoError(w, "updating email_verified", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
// if err := tx.Model(&token).Update("used_at", time.Now()).Error; err != nil {
|
||||
// tx.Rollback()
|
||||
// middleware.DoError(w, "updating reset token", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
// tx.Commit()
|
||||
//
|
||||
// var user database.User
|
||||
// if err := a.App.DB.Where("id = ?", token.UserID).First(&user).Error; err != nil {
|
||||
// middleware.DoError(w, "finding user", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// s := session.New(user, account)
|
||||
// middleware.RespondJSON(w, http.StatusOK, s)
|
||||
// }
|
||||
//
|
||||
// type emailPreferernceParams struct {
|
||||
// InactiveReminder *bool `json:"inactive_reminder"`
|
||||
// ProductUpdate *bool `json:"product_update"`
|
||||
// }
|
||||
//
|
||||
// func (p emailPreferernceParams) getInactiveReminder() bool {
|
||||
// if p.InactiveReminder == nil {
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// return *p.InactiveReminder
|
||||
// }
|
||||
//
|
||||
// func (p emailPreferernceParams) getProductUpdate() bool {
|
||||
// if p.ProductUpdate == nil {
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// return *p.ProductUpdate
|
||||
// }
|
||||
//
|
||||
// func (a *API) updateEmailPreference(w http.ResponseWriter, r *http.Request) {
|
||||
// user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
// if !ok {
|
||||
// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// var params emailPreferernceParams
|
||||
// if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
// middleware.DoError(w, "decoding payload", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// var pref database.EmailPreference
|
||||
// if err := a.App.DB.Where(database.EmailPreference{UserID: user.ID}).FirstOrCreate(&pref).Error; err != nil {
|
||||
// middleware.DoError(w, "finding pref", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// tx := a.App.DB.Begin()
|
||||
//
|
||||
// if params.InactiveReminder != nil {
|
||||
// pref.InactiveReminder = params.getInactiveReminder()
|
||||
// }
|
||||
// if params.ProductUpdate != nil {
|
||||
// pref.ProductUpdate = params.getProductUpdate()
|
||||
// }
|
||||
//
|
||||
// if err := tx.Save(&pref).Error; err != nil {
|
||||
// tx.Rollback()
|
||||
// middleware.DoError(w, "saving pref", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// token, ok := r.Context().Value(helpers.KeyToken).(database.Token)
|
||||
// if ok {
|
||||
// // Mark token as used if the user was authenticated by token
|
||||
// if err := tx.Model(&token).Update("used_at", time.Now()).Error; err != nil {
|
||||
// tx.Rollback()
|
||||
// middleware.DoError(w, "updating reset token", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// tx.Commit()
|
||||
//
|
||||
// middleware.RespondJSON(w, http.StatusOK, pref)
|
||||
// }
|
||||
//
|
||||
// func (a *API) getEmailPreference(w http.ResponseWriter, r *http.Request) {
|
||||
// user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
// if !ok {
|
||||
// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// var pref database.EmailPreference
|
||||
// if err := a.App.DB.Where(database.EmailPreference{UserID: user.ID}).First(&pref).Error; err != nil {
|
||||
// middleware.DoError(w, "finding pref", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// presented := presenters.PresentEmailPreference(pref)
|
||||
// middleware.RespondJSON(w, http.StatusOK, presented)
|
||||
// }
|
||||
//
|
||||
// type updatePasswordPayload struct {
|
||||
// OldPassword string `json:"old_password"`
|
||||
// NewPassword string `json:"new_password"`
|
||||
// }
|
||||
//
|
||||
// func (a *API) updatePassword(w http.ResponseWriter, r *http.Request) {
|
||||
// user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
// if !ok {
|
||||
// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// var params updatePasswordPayload
|
||||
// if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
// http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
// if params.OldPassword == "" || params.NewPassword == "" {
|
||||
// http.Error(w, "invalid params", http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// var account database.Account
|
||||
// if err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
|
||||
// middleware.DoError(w, "getting account", nil, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// password := []byte(params.OldPassword)
|
||||
// if err := bcrypt.CompareHashAndPassword([]byte(account.Password.String), password); err != nil {
|
||||
// log.WithFields(log.Fields{
|
||||
// "user_id": user.ID,
|
||||
// }).Warn("invalid password update attempt")
|
||||
// http.Error(w, "Wrong password", http.StatusUnauthorized)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if err := validatePassword(params.NewPassword); err != nil {
|
||||
// http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// hashedNewPassword, err := bcrypt.GenerateFromPassword([]byte(params.NewPassword), bcrypt.DefaultCost)
|
||||
// if err != nil {
|
||||
// http.Error(w, errors.Wrap(err, "hashing password").Error(), http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if err := a.App.DB.Model(&account).Update("password", string(hashedNewPassword)).Error; err != nil {
|
||||
// http.Error(w, errors.Wrap(err, "updating password").Error(), http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// w.WriteHeader(http.StatusOK)
|
||||
// }
|
||||
|
|
@ -1,691 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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")
|
||||
// }
|
||||
|
|
@ -1,226 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/dnote/pkg/server/database"
|
||||
"github.com/dnote/dnote/pkg/server/handlers"
|
||||
"github.com/dnote/dnote/pkg/server/log"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// ErrLoginFailure is an error for failed login
|
||||
var ErrLoginFailure = errors.New("Wrong email and password combination")
|
||||
|
||||
// SessionResponse is a response containing a session information
|
||||
type SessionResponse struct {
|
||||
Key string `json:"key"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
}
|
||||
|
||||
func setSessionCookie(w http.ResponseWriter, key string, expires time.Time) {
|
||||
cookie := http.Cookie{
|
||||
Name: "id",
|
||||
Value: key,
|
||||
Expires: expires,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
}
|
||||
http.SetCookie(w, &cookie)
|
||||
}
|
||||
|
||||
func touchLastLoginAt(db *gorm.DB, user database.User) error {
|
||||
t := time.Now()
|
||||
if err := db.Model(&user).Update(database.User{LastLoginAt: &t}).Error; err != nil {
|
||||
return errors.Wrap(err, "updating last_login_at")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type signinPayload struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (a *API) signin(w http.ResponseWriter, r *http.Request) {
|
||||
var params signinPayload
|
||||
err := json.NewDecoder(r.Body).Decode(¶ms)
|
||||
if err != nil {
|
||||
handlers.DoError(w, "decoding payload", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if params.Email == "" || params.Password == "" {
|
||||
http.Error(w, ErrLoginFailure.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var account database.Account
|
||||
conn := a.App.DB.Where("email = ?", params.Email).First(&account)
|
||||
if conn.RecordNotFound() {
|
||||
http.Error(w, ErrLoginFailure.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
} else if conn.Error != nil {
|
||||
handlers.DoError(w, "getting user", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
password := []byte(params.Password)
|
||||
err = bcrypt.CompareHashAndPassword([]byte(account.Password.String), password)
|
||||
if err != nil {
|
||||
http.Error(w, ErrLoginFailure.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var user database.User
|
||||
err = a.App.DB.Where("id = ?", account.UserID).First(&user).Error
|
||||
if err != nil {
|
||||
handlers.DoError(w, "finding user", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = a.App.TouchLastLoginAt(user, a.App.DB)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "touching login timestamp").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
a.respondWithSession(a.App.DB, w, account.UserID, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *API) signoutOptions(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Version")
|
||||
}
|
||||
|
||||
func (a *API) signout(w http.ResponseWriter, r *http.Request) {
|
||||
key, err := handlers.GetCredential(r)
|
||||
if err != nil {
|
||||
handlers.DoError(w, "getting credential", nil, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if key == "" {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
err = a.App.DeleteSession(key)
|
||||
if err != nil {
|
||||
handlers.DoError(w, "deleting session", nil, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
handlers.UnsetSessionCookie(w)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type registerPayload struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func validateRegisterPayload(p registerPayload) error {
|
||||
if p.Email == "" {
|
||||
return errors.New("email is required")
|
||||
}
|
||||
if len(p.Password) < 8 {
|
||||
return errors.New("Password should be longer than 8 characters")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseRegisterPaylaod(r *http.Request) (registerPayload, error) {
|
||||
var ret registerPayload
|
||||
if err := json.NewDecoder(r.Body).Decode(&ret); err != nil {
|
||||
return ret, errors.Wrap(err, "decoding json")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (a *API) register(w http.ResponseWriter, r *http.Request) {
|
||||
if a.App.Config.DisableRegistration {
|
||||
handlers.RespondForbidden(w)
|
||||
return
|
||||
}
|
||||
|
||||
params, err := parseRegisterPaylaod(r)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validateRegisterPayload(params); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var count int
|
||||
if err := a.App.DB.Model(database.Account{}).Where("email = ?", params.Email).Count(&count).Error; err != nil {
|
||||
handlers.DoError(w, "checking duplicate user", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
http.Error(w, "Duplicate email", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := a.App.CreateUser(params.Email, params.Password)
|
||||
if err != nil {
|
||||
handlers.DoError(w, "creating user", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
a.respondWithSession(a.App.DB, w, user.ID, http.StatusCreated)
|
||||
|
||||
if err := a.App.SendWelcomeEmail(params.Email); err != nil {
|
||||
log.ErrorWrap(err, "sending welcome email")
|
||||
}
|
||||
}
|
||||
|
||||
// respondWithSession makes a HTTP response with the session from the user with the given userID.
|
||||
// It sets the HTTP-Only cookie for browser clients and also sends a JSON response for non-browser clients.
|
||||
func (a *API) respondWithSession(db *gorm.DB, w http.ResponseWriter, userID int, statusCode int) {
|
||||
session, err := a.App.CreateSession(userID)
|
||||
if err != nil {
|
||||
handlers.DoError(w, "creating session", nil, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
setSessionCookie(w, session.Key, session.ExpiresAt)
|
||||
|
||||
response := SessionResponse{
|
||||
Key: session.Key,
|
||||
ExpiresAt: session.ExpiresAt.Unix(),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
handlers.DoError(w, "encoding response", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -1,482 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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/config"
|
||||
"github.com/dnote/dnote/pkg/server/database"
|
||||
"github.com/dnote/dnote/pkg/server/testutils"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func assertSessionResp(t *testing.T, res *http.Response) {
|
||||
// after register, should sign in user
|
||||
var got SessionResponse
|
||||
if err := json.NewDecoder(res.Body).Decode(&got); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
var sessionCount int
|
||||
var session database.Session
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Session{}).Count(&sessionCount), "counting session")
|
||||
testutils.MustExec(t, testutils.DB.First(&session), "getting session")
|
||||
|
||||
assert.Equal(t, sessionCount, 1, "sessionCount mismatch")
|
||||
assert.Equal(t, got.Key, session.Key, "session Key mismatch")
|
||||
assert.Equal(t, got.ExpiresAt, session.ExpiresAt.Unix(), "session ExpiresAt mismatch")
|
||||
|
||||
c := testutils.GetCookieByName(res.Cookies(), "id")
|
||||
assert.Equal(t, c.Value, session.Key, "session key mismatch")
|
||||
assert.Equal(t, c.Path, "/", "session path mismatch")
|
||||
assert.Equal(t, c.HttpOnly, true, "session HTTPOnly mismatch")
|
||||
assert.Equal(t, c.Expires.Unix(), session.ExpiresAt.Unix(), "session Expires mismatch")
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
testCases := []struct {
|
||||
email string
|
||||
password string
|
||||
onPremise bool
|
||||
expectedPro bool
|
||||
}{
|
||||
{
|
||||
email: "alice@example.com",
|
||||
password: "pass1234",
|
||||
onPremise: false,
|
||||
expectedPro: false,
|
||||
},
|
||||
{
|
||||
email: "bob@example.com",
|
||||
password: "Y9EwmjH@Jq6y5a64MSACUoM4w7SAhzvY",
|
||||
onPremise: false,
|
||||
expectedPro: false,
|
||||
},
|
||||
{
|
||||
email: "chuck@example.com",
|
||||
password: "e*H@kJi^vXbWEcD9T5^Am!Y@7#Po2@PC",
|
||||
onPremise: false,
|
||||
expectedPro: false,
|
||||
},
|
||||
// on premise
|
||||
{
|
||||
email: "dan@example.com",
|
||||
password: "e*H@kJi^vXbWEcD9T5^Am!Y@7#Po2@PC",
|
||||
onPremise: true,
|
||||
expectedPro: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("register %s %s", tc.email, tc.password), func(t *testing.T) {
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
c := config.Load()
|
||||
c.SetOnPremise(tc.onPremise)
|
||||
|
||||
// Setup
|
||||
emailBackend := testutils.MockEmailbackendImplementation{}
|
||||
server := MustNewServer(t, &app.App{
|
||||
Clock: clock.NewMock(),
|
||||
EmailBackend: &emailBackend,
|
||||
Config: c,
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
dat := fmt.Sprintf(`{"email": "%s", "password": "%s"}`, tc.email, tc.password)
|
||||
req := testutils.MakeReq(server.URL, "POST", "/v3/register", dat)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusCreated, "")
|
||||
|
||||
var account database.Account
|
||||
testutils.MustExec(t, testutils.DB.Where("email = ?", tc.email).First(&account), "finding account")
|
||||
assert.Equal(t, account.Email.String, tc.email, "Email mismatch")
|
||||
assert.NotEqual(t, account.UserID, 0, "UserID mismatch")
|
||||
passwordErr := bcrypt.CompareHashAndPassword([]byte(account.Password.String), []byte(tc.password))
|
||||
assert.Equal(t, passwordErr, nil, "Password mismatch")
|
||||
|
||||
var user database.User
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", account.UserID).First(&user), "finding user")
|
||||
assert.Equal(t, user.Cloud, tc.expectedPro, "Cloud mismatch")
|
||||
assert.Equal(t, user.MaxUSN, 0, "MaxUSN mismatch")
|
||||
|
||||
// welcome email
|
||||
assert.Equalf(t, len(emailBackend.Emails), 1, "email queue count mismatch")
|
||||
assert.DeepEqual(t, emailBackend.Emails[0].To, []string{tc.email}, "email to mismatch")
|
||||
|
||||
// after register, should sign in user
|
||||
assertSessionResp(t, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterMissingParams(t *testing.T) {
|
||||
t.Run("missing email", func(t *testing.T) {
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
Clock: clock.NewMock(),
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
dat := fmt.Sprintf(`{"password": %s}`, "SLMZFM5RmSjA5vfXnG5lPOnrpZSbtmV76cnAcrlr2yU")
|
||||
req := testutils.MakeReq(server.URL, "POST", "/v3/register", dat)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusBadRequest, "Status mismatch")
|
||||
|
||||
var accountCount, userCount int
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Account{}).Count(&accountCount), "counting account")
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.User{}).Count(&userCount), "counting user")
|
||||
|
||||
assert.Equal(t, accountCount, 0, "accountCount mismatch")
|
||||
assert.Equal(t, userCount, 0, "userCount mismatch")
|
||||
})
|
||||
|
||||
t.Run("missing password", func(t *testing.T) {
|
||||
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
Clock: clock.NewMock(),
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
dat := fmt.Sprintf(`{"email": "%s"}`, "alice@example.com")
|
||||
req := testutils.MakeReq(server.URL, "POST", "/v3/register", dat)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusBadRequest, "Status mismatch")
|
||||
|
||||
var accountCount, userCount int
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Account{}).Count(&accountCount), "counting account")
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.User{}).Count(&userCount), "counting user")
|
||||
|
||||
assert.Equal(t, accountCount, 0, "accountCount mismatch")
|
||||
assert.Equal(t, userCount, 0, "userCount mismatch")
|
||||
})
|
||||
}
|
||||
|
||||
func TestRegisterDuplicateEmail(t *testing.T) {
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
Clock: clock.NewMock(),
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
u := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(u, "alice@example.com", "somepassword")
|
||||
|
||||
dat := `{"email": "alice@example.com", "password": "foobarbaz"}`
|
||||
req := testutils.MakeReq(server.URL, "POST", "/v3/register", dat)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusBadRequest, "status code mismatch")
|
||||
|
||||
var accountCount, userCount, verificationTokenCount int
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Account{}).Count(&accountCount), "counting account")
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.User{}).Count(&userCount), "counting user")
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Token{}).Count(&verificationTokenCount), "counting verification token")
|
||||
|
||||
var user database.User
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", u.ID).First(&user), "finding user")
|
||||
|
||||
assert.Equal(t, accountCount, 1, "account count mismatch")
|
||||
assert.Equal(t, userCount, 1, "user count mismatch")
|
||||
assert.Equal(t, verificationTokenCount, 0, "verification_token should not have been created")
|
||||
assert.Equal(t, user.LastLoginAt, (*time.Time)(nil), "LastLoginAt mismatch")
|
||||
}
|
||||
|
||||
func TestRegisterDisabled(t *testing.T) {
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
c := config.Load()
|
||||
c.DisableRegistration = true
|
||||
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
Clock: clock.NewMock(),
|
||||
Config: c,
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
dat := `{"email": "alice@example.com", "password": "foobarbaz"}`
|
||||
req := testutils.MakeReq(server.URL, "POST", "/v3/register", dat)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusForbidden, "status code mismatch")
|
||||
|
||||
var accountCount, userCount int
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Account{}).Count(&accountCount), "counting account")
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.User{}).Count(&userCount), "counting user")
|
||||
|
||||
assert.Equal(t, accountCount, 0, "account count mismatch")
|
||||
assert.Equal(t, userCount, 0, "user count mismatch")
|
||||
}
|
||||
|
||||
func TestSignIn(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()
|
||||
testutils.SetupAccountData(u, "alice@example.com", "pass1234")
|
||||
|
||||
dat := `{"email": "alice@example.com", "password": "pass1234"}`
|
||||
req := testutils.MakeReq(server.URL, "POST", "/v3/signin", dat)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
|
||||
var user database.User
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.User{}).First(&user), "finding user")
|
||||
assert.NotEqual(t, user.LastLoginAt, nil, "LastLoginAt mismatch")
|
||||
|
||||
// after register, should sign in user
|
||||
assertSessionResp(t, res)
|
||||
})
|
||||
|
||||
t.Run("wrong password", 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.SetupAccountData(u, "alice@example.com", "pass1234")
|
||||
|
||||
dat := `{"email": "alice@example.com", "password": "wrongpassword1234"}`
|
||||
req := testutils.MakeReq(server.URL, "POST", "/v3/signin", dat)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "")
|
||||
|
||||
var user database.User
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.User{}).First(&user), "finding user")
|
||||
assert.Equal(t, user.LastLoginAt, (*time.Time)(nil), "LastLoginAt mismatch")
|
||||
|
||||
var sessionCount int
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Session{}).Count(&sessionCount), "counting session")
|
||||
assert.Equal(t, sessionCount, 0, "sessionCount mismatch")
|
||||
})
|
||||
|
||||
t.Run("wrong email", 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.SetupAccountData(u, "alice@example.com", "pass1234")
|
||||
|
||||
dat := `{"email": "bob@example.com", "password": "pass1234"}`
|
||||
req := testutils.MakeReq(server.URL, "POST", "/v3/signin", dat)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "")
|
||||
|
||||
var user database.User
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.User{}).First(&user), "finding user")
|
||||
assert.DeepEqual(t, user.LastLoginAt, (*time.Time)(nil), "LastLoginAt mismatch")
|
||||
|
||||
var sessionCount int
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Session{}).Count(&sessionCount), "counting session")
|
||||
assert.Equal(t, sessionCount, 0, "sessionCount mismatch")
|
||||
})
|
||||
|
||||
t.Run("nonexistent email", func(t *testing.T) {
|
||||
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
|
||||
Clock: clock.NewMock(),
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
dat := `{"email": "nonexistent@example.com", "password": "pass1234"}`
|
||||
req := testutils.MakeReq(server.URL, "POST", "/v3/signin", dat)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "")
|
||||
|
||||
var sessionCount int
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Session{}).Count(&sessionCount), "counting session")
|
||||
assert.Equal(t, sessionCount, 0, "sessionCount mismatch")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSignout(t *testing.T) {
|
||||
t.Run("authenticated", func(t *testing.T) {
|
||||
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
aliceUser := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(aliceUser, "alice@example.com", "pass1234")
|
||||
anotherUser := testutils.SetupUserData()
|
||||
|
||||
session1 := database.Session{
|
||||
Key: "A9xgggqzTHETy++GDi1NpDNe0iyqosPm9bitdeNGkJU=",
|
||||
UserID: aliceUser.ID,
|
||||
ExpiresAt: time.Now().Add(time.Hour * 24),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&session1), "preparing session1")
|
||||
session2 := database.Session{
|
||||
Key: "MDCpbvCRg7W2sH6S870wqLqZDZTObYeVd0PzOekfo/A=",
|
||||
UserID: anotherUser.ID,
|
||||
ExpiresAt: time.Now().Add(time.Hour * 24),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&session2), "preparing session2")
|
||||
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
|
||||
Clock: clock.NewMock(),
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// Execute
|
||||
req := testutils.MakeReq(server.URL, "POST", "/v3/signout", "")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", "A9xgggqzTHETy++GDi1NpDNe0iyqosPm9bitdeNGkJU="))
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusNoContent, "Status mismatch")
|
||||
|
||||
var sessionCount int
|
||||
var s2 database.Session
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Session{}).Count(&sessionCount), "counting session")
|
||||
testutils.MustExec(t, testutils.DB.Where("key = ?", "MDCpbvCRg7W2sH6S870wqLqZDZTObYeVd0PzOekfo/A=").First(&s2), "getting s2")
|
||||
|
||||
assert.Equal(t, sessionCount, 1, "sessionCount mismatch")
|
||||
|
||||
c := testutils.GetCookieByName(res.Cookies(), "id")
|
||||
assert.Equal(t, c.Value, "", "session key mismatch")
|
||||
assert.Equal(t, c.Path, "/", "session path mismatch")
|
||||
assert.Equal(t, c.HttpOnly, true, "session HTTPOnly mismatch")
|
||||
if c.Expires.After(time.Now()) {
|
||||
t.Error("session cookie is not expired")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unauthenticated", func(t *testing.T) {
|
||||
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
aliceUser := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(aliceUser, "alice@example.com", "pass1234")
|
||||
anotherUser := testutils.SetupUserData()
|
||||
|
||||
session1 := database.Session{
|
||||
Key: "A9xgggqzTHETy++GDi1NpDNe0iyqosPm9bitdeNGkJU=",
|
||||
UserID: aliceUser.ID,
|
||||
ExpiresAt: time.Now().Add(time.Hour * 24),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&session1), "preparing session1")
|
||||
session2 := database.Session{
|
||||
Key: "MDCpbvCRg7W2sH6S870wqLqZDZTObYeVd0PzOekfo/A=",
|
||||
UserID: anotherUser.ID,
|
||||
ExpiresAt: time.Now().Add(time.Hour * 24),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&session2), "preparing session2")
|
||||
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
|
||||
Clock: clock.NewMock(),
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// Execute
|
||||
req := testutils.MakeReq(server.URL, "POST", "/v3/signout", "")
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusNoContent, "Status mismatch")
|
||||
|
||||
var sessionCount int
|
||||
var postSession1, postSession2 database.Session
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Session{}).Count(&sessionCount), "counting session")
|
||||
testutils.MustExec(t, testutils.DB.Where("key = ?", "A9xgggqzTHETy++GDi1NpDNe0iyqosPm9bitdeNGkJU=").First(&postSession1), "getting postSession1")
|
||||
testutils.MustExec(t, testutils.DB.Where("key = ?", "MDCpbvCRg7W2sH6S870wqLqZDZTObYeVd0PzOekfo/A=").First(&postSession2), "getting postSession2")
|
||||
|
||||
// two existing sessions should remain
|
||||
assert.Equal(t, sessionCount, 2, "sessionCount mismatch")
|
||||
|
||||
c := testutils.GetCookieByName(res.Cookies(), "id")
|
||||
assert.Equal(t, c, (*http.Cookie)(nil), "id cookie should have not been set")
|
||||
})
|
||||
}
|
||||
|
|
@ -1,267 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/dnote/dnote/pkg/server/database"
|
||||
"github.com/dnote/dnote/pkg/server/handlers"
|
||||
"github.com/dnote/dnote/pkg/server/helpers"
|
||||
"github.com/dnote/dnote/pkg/server/presenters"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type createBookPayload struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// CreateBookResp is the response from create book api
|
||||
type CreateBookResp struct {
|
||||
Book presenters.Book `json:"book"`
|
||||
}
|
||||
|
||||
func validateCreateBookPayload(p createBookPayload) error {
|
||||
if p.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateBook creates a new book
|
||||
func (a *API) CreateBook(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var params createBookPayload
|
||||
err := json.NewDecoder(r.Body).Decode(¶ms)
|
||||
if err != nil {
|
||||
handlers.DoError(w, "decoding payload", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = validateCreateBookPayload(params)
|
||||
if err != nil {
|
||||
handlers.DoError(w, "validating payload", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var bookCount int
|
||||
err = a.App.DB.Model(database.Book{}).
|
||||
Where("user_id = ? AND label = ?", user.ID, params.Name).
|
||||
Count(&bookCount).Error
|
||||
if err != nil {
|
||||
handlers.DoError(w, "checking duplicate", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if bookCount > 0 {
|
||||
http.Error(w, "duplicate book exists", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
book, err := a.App.CreateBook(user, params.Name)
|
||||
if err != nil {
|
||||
handlers.DoError(w, "inserting book", err, http.StatusInternalServerError)
|
||||
}
|
||||
resp := CreateBookResp{
|
||||
Book: presenters.PresentBook(book),
|
||||
}
|
||||
handlers.RespondJSON(w, http.StatusCreated, resp)
|
||||
}
|
||||
|
||||
// BooksOptions is a handler for OPTIONS endpoint for notes
|
||||
func (a *API) BooksOptions(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Version")
|
||||
}
|
||||
|
||||
func respondWithBooks(db *gorm.DB, userID int, query url.Values, w http.ResponseWriter) {
|
||||
var books []database.Book
|
||||
conn := db.Where("user_id = ? AND NOT deleted", userID).Order("label ASC")
|
||||
name := query.Get("name")
|
||||
encryptedStr := query.Get("encrypted")
|
||||
|
||||
if name != "" {
|
||||
part := fmt.Sprintf("%%%s%%", name)
|
||||
conn = conn.Where("LOWER(label) LIKE ?", part)
|
||||
}
|
||||
if encryptedStr != "" {
|
||||
var encrypted bool
|
||||
if encryptedStr == "true" {
|
||||
encrypted = true
|
||||
} else {
|
||||
encrypted = false
|
||||
}
|
||||
|
||||
conn = conn.Where("encrypted = ?", encrypted)
|
||||
}
|
||||
|
||||
if err := conn.Find(&books).Error; err != nil {
|
||||
handlers.DoError(w, "finding books", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
presentedBooks := presenters.PresentBooks(books)
|
||||
handlers.RespondJSON(w, http.StatusOK, presentedBooks)
|
||||
}
|
||||
|
||||
// GetBooks returns books for the user
|
||||
func (a *API) GetBooks(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
|
||||
respondWithBooks(a.App.DB, user.ID, query, w)
|
||||
}
|
||||
|
||||
// GetBook returns a book for the user
|
||||
func (a *API) GetBook(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bookUUID := vars["bookUUID"]
|
||||
|
||||
var book database.Book
|
||||
conn := a.App.DB.Where("uuid = ? AND user_id = ?", bookUUID, user.ID).First(&book)
|
||||
|
||||
if conn.RecordNotFound() {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err := conn.Error; err != nil {
|
||||
handlers.DoError(w, "finding book", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
p := presenters.PresentBook(book)
|
||||
handlers.RespondJSON(w, http.StatusOK, p)
|
||||
}
|
||||
|
||||
type updateBookPayload struct {
|
||||
Name *string `json:"name"`
|
||||
}
|
||||
|
||||
// UpdateBookResp is the response from create book api
|
||||
type UpdateBookResp struct {
|
||||
Book presenters.Book `json:"book"`
|
||||
}
|
||||
|
||||
// UpdateBook updates a book
|
||||
func (a *API) UpdateBook(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
uuid := vars["bookUUID"]
|
||||
|
||||
tx := a.App.DB.Begin()
|
||||
|
||||
var book database.Book
|
||||
if err := tx.Where("user_id = ? AND uuid = ?", user.ID, uuid).First(&book).Error; err != nil {
|
||||
handlers.DoError(w, "finding book", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var params updateBookPayload
|
||||
err := json.NewDecoder(r.Body).Decode(¶ms)
|
||||
if err != nil {
|
||||
handlers.DoError(w, "decoding payload", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
book, err = a.App.UpdateBook(tx, user, book, params.Name)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
handlers.DoError(w, "updating a book", err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
resp := UpdateBookResp{
|
||||
Book: presenters.PresentBook(book),
|
||||
}
|
||||
handlers.RespondJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// DeleteBookResp is the response from create book api
|
||||
type DeleteBookResp struct {
|
||||
Status int `json:"status"`
|
||||
Book presenters.Book `json:"book"`
|
||||
}
|
||||
|
||||
// DeleteBook removes a book
|
||||
func (a *API) DeleteBook(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
uuid := vars["bookUUID"]
|
||||
|
||||
tx := a.App.DB.Begin()
|
||||
|
||||
var book database.Book
|
||||
if err := tx.Where("user_id = ? AND uuid = ?", user.ID, uuid).First(&book).Error; err != nil {
|
||||
handlers.DoError(w, "finding book", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var notes []database.Note
|
||||
if err := tx.Where("book_uuid = ? AND NOT deleted", uuid).Order("usn ASC").Find(¬es).Error; err != nil {
|
||||
handlers.DoError(w, "finding notes", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for _, note := range notes {
|
||||
if _, err := a.App.DeleteNote(tx, user, note); err != nil {
|
||||
handlers.DoError(w, "deleting a note", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
b, err := a.App.DeleteBook(tx, user, book)
|
||||
if err != nil {
|
||||
handlers.DoError(w, "deleting book", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
resp := DeleteBookResp{
|
||||
Status: http.StatusOK,
|
||||
Book: presenters.PresentBook(b),
|
||||
}
|
||||
handlers.RespondJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/dnote/dnote/pkg/server/app"
|
||||
"github.com/dnote/dnote/pkg/server/database"
|
||||
"github.com/dnote/dnote/pkg/server/handlers"
|
||||
"github.com/dnote/dnote/pkg/server/helpers"
|
||||
"github.com/dnote/dnote/pkg/server/presenters"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type updateNotePayload struct {
|
||||
BookUUID *string `json:"book_uuid"`
|
||||
Content *string `json:"content"`
|
||||
Public *bool `json:"public"`
|
||||
}
|
||||
|
||||
type updateNoteResp struct {
|
||||
Status int `json:"status"`
|
||||
Result presenters.Note `json:"result"`
|
||||
}
|
||||
|
||||
func validateUpdateNotePayload(p updateNotePayload) bool {
|
||||
return p.BookUUID != nil || p.Content != nil || p.Public != nil
|
||||
}
|
||||
|
||||
// UpdateNote updates note
|
||||
func (a *API) UpdateNote(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
noteUUID := vars["noteUUID"]
|
||||
|
||||
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
if !ok {
|
||||
handlers.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var params updateNotePayload
|
||||
err := json.NewDecoder(r.Body).Decode(¶ms)
|
||||
if err != nil {
|
||||
handlers.DoError(w, "decoding params", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if ok := validateUpdateNotePayload(params); !ok {
|
||||
handlers.DoError(w, "Invalid payload", nil, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var note database.Note
|
||||
if err := a.App.DB.Where("uuid = ? AND user_id = ?", noteUUID, user.ID).First(¬e).Error; err != nil {
|
||||
handlers.DoError(w, "finding note", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tx := a.App.DB.Begin()
|
||||
|
||||
note, err = a.App.UpdateNote(tx, user, note, &app.UpdateNoteParams{
|
||||
BookUUID: params.BookUUID,
|
||||
Content: params.Content,
|
||||
Public: params.Public,
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
handlers.DoError(w, "updating note", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var book database.Book
|
||||
if err := tx.Where("uuid = ? AND user_id = ?", note.BookUUID, user.ID).First(&book).Error; err != nil {
|
||||
tx.Rollback()
|
||||
handlers.DoError(w, fmt.Sprintf("finding book %s to preload", note.BookUUID), err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// preload associations
|
||||
note.User = user
|
||||
note.Book = book
|
||||
|
||||
resp := updateNoteResp{
|
||||
Status: http.StatusOK,
|
||||
Result: presenters.PresentNote(note),
|
||||
}
|
||||
handlers.RespondJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
type deleteNoteResp struct {
|
||||
Status int `json:"status"`
|
||||
Result presenters.Note `json:"result"`
|
||||
}
|
||||
|
||||
// DeleteNote removes note
|
||||
func (a *API) DeleteNote(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
noteUUID := vars["noteUUID"]
|
||||
|
||||
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
if !ok {
|
||||
handlers.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var note database.Note
|
||||
if err := a.App.DB.Where("uuid = ? AND user_id = ?", noteUUID, user.ID).Preload("Book").First(¬e).Error; err != nil {
|
||||
handlers.DoError(w, "finding note", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tx := a.App.DB.Begin()
|
||||
|
||||
n, err := a.App.DeleteNote(tx, user, note)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
handlers.DoError(w, "deleting note", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
resp := deleteNoteResp{
|
||||
Status: http.StatusNoContent,
|
||||
Result: presenters.PresentNote(n),
|
||||
}
|
||||
handlers.RespondJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
type createNotePayload struct {
|
||||
BookUUID string `json:"book_uuid"`
|
||||
Content string `json:"content"`
|
||||
AddedOn *int64 `json:"added_on"`
|
||||
EditedOn *int64 `json:"edited_on"`
|
||||
}
|
||||
|
||||
func validateCreateNotePayload(p createNotePayload) error {
|
||||
if p.BookUUID == "" {
|
||||
return errors.New("bookUUID is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateNoteResp is a response for creating a note
|
||||
type CreateNoteResp struct {
|
||||
Result presenters.Note `json:"result"`
|
||||
}
|
||||
|
||||
// CreateNote creates a note
|
||||
func (a *API) CreateNote(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
if !ok {
|
||||
handlers.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var params createNotePayload
|
||||
err := json.NewDecoder(r.Body).Decode(¶ms)
|
||||
if err != nil {
|
||||
handlers.DoError(w, "decoding payload", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = validateCreateNotePayload(params)
|
||||
if err != nil {
|
||||
handlers.DoError(w, "validating payload", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var book database.Book
|
||||
if err := a.App.DB.Where("uuid = ? AND user_id = ?", params.BookUUID, user.ID).First(&book).Error; err != nil {
|
||||
handlers.DoError(w, "finding book", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
client := getClientType(r)
|
||||
note, err := a.App.CreateNote(user, params.BookUUID, params.Content, params.AddedOn, params.EditedOn, false, client)
|
||||
if err != nil {
|
||||
handlers.DoError(w, "creating note", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// preload associations
|
||||
note.User = user
|
||||
note.Book = book
|
||||
|
||||
resp := CreateNoteResp{
|
||||
Result: presenters.PresentNote(note),
|
||||
}
|
||||
handlers.RespondJSON(w, http.StatusCreated, resp)
|
||||
}
|
||||
|
||||
// NotesOptions is a handler for OPTIONS endpoint for notes
|
||||
func (a *API) NotesOptions(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Version")
|
||||
}
|
||||
|
|
@ -1,394 +0,0 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"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/testutils"
|
||||
)
|
||||
|
||||
func TestCreateNote(t *testing.T) {
|
||||
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
|
||||
Clock: clock.NewMock(),
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
testutils.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 101), "preparing user max_usn")
|
||||
|
||||
b1 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "js",
|
||||
USN: 58,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1")
|
||||
|
||||
// Execute
|
||||
dat := fmt.Sprintf(`{"book_uuid": "%s", "content": "note content"}`, b1.UUID)
|
||||
req := testutils.MakeReq(server.URL, "POST", "/v3/notes", dat)
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusCreated, "")
|
||||
|
||||
var noteRecord database.Note
|
||||
var bookRecord database.Book
|
||||
var userRecord database.User
|
||||
var bookCount, noteCount int
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Book{}).Count(&bookCount), "counting books")
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Note{}).Count(¬eCount), "counting notes")
|
||||
testutils.MustExec(t, testutils.DB.First(¬eRecord), "finding note")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", b1.ID).First(&bookRecord), "finding book")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", user.ID).First(&userRecord), "finding user record")
|
||||
|
||||
assert.Equalf(t, bookCount, 1, "book count mismatch")
|
||||
assert.Equalf(t, noteCount, 1, "note count mismatch")
|
||||
|
||||
assert.Equal(t, bookRecord.Label, b1.Label, "book name mismatch")
|
||||
assert.Equal(t, bookRecord.UUID, b1.UUID, "book uuid mismatch")
|
||||
assert.Equal(t, bookRecord.UserID, b1.UserID, "book user_id mismatch")
|
||||
assert.Equal(t, bookRecord.USN, 58, "book usn mismatch")
|
||||
|
||||
assert.NotEqual(t, noteRecord.UUID, "", "note uuid should have been generated")
|
||||
assert.Equal(t, noteRecord.BookUUID, b1.UUID, "note book_uuid mismatch")
|
||||
assert.Equal(t, noteRecord.Body, "note content", "note content mismatch")
|
||||
assert.Equal(t, noteRecord.USN, 102, "note usn mismatch")
|
||||
}
|
||||
|
||||
func TestUpdateNote(t *testing.T) {
|
||||
updatedBody := "some updated content"
|
||||
|
||||
b1UUID := "37868a8e-a844-4265-9a4f-0be598084733"
|
||||
b2UUID := "8f3bd424-6aa5-4ed5-910d-e5b38ab09f8c"
|
||||
|
||||
testCases := []struct {
|
||||
payload string
|
||||
noteUUID string
|
||||
noteBookUUID string
|
||||
noteBody string
|
||||
notePublic bool
|
||||
noteDeleted bool
|
||||
expectedNoteBody string
|
||||
expectedNoteBookName string
|
||||
expectedNoteBookUUID string
|
||||
expectedNotePublic bool
|
||||
}{
|
||||
{
|
||||
payload: fmt.Sprintf(`{
|
||||
"content": "%s"
|
||||
}`, updatedBody),
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: false,
|
||||
noteBody: "original content",
|
||||
noteDeleted: false,
|
||||
expectedNoteBookUUID: b1UUID,
|
||||
expectedNoteBody: "some updated content",
|
||||
expectedNoteBookName: "css",
|
||||
expectedNotePublic: false,
|
||||
},
|
||||
{
|
||||
payload: fmt.Sprintf(`{
|
||||
"book_uuid": "%s"
|
||||
}`, b1UUID),
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: false,
|
||||
noteBody: "original content",
|
||||
noteDeleted: false,
|
||||
expectedNoteBookUUID: b1UUID,
|
||||
expectedNoteBody: "original content",
|
||||
expectedNoteBookName: "css",
|
||||
expectedNotePublic: false,
|
||||
},
|
||||
{
|
||||
payload: fmt.Sprintf(`{
|
||||
"book_uuid": "%s"
|
||||
}`, b2UUID),
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: false,
|
||||
noteBody: "original content",
|
||||
noteDeleted: false,
|
||||
expectedNoteBookUUID: b2UUID,
|
||||
expectedNoteBody: "original content",
|
||||
expectedNoteBookName: "js",
|
||||
expectedNotePublic: false,
|
||||
},
|
||||
{
|
||||
payload: fmt.Sprintf(`{
|
||||
"book_uuid": "%s",
|
||||
"content": "%s"
|
||||
}`, b2UUID, updatedBody),
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: false,
|
||||
noteBody: "original content",
|
||||
noteDeleted: false,
|
||||
expectedNoteBookUUID: b2UUID,
|
||||
expectedNoteBody: "some updated content",
|
||||
expectedNoteBookName: "js",
|
||||
expectedNotePublic: false,
|
||||
},
|
||||
{
|
||||
payload: fmt.Sprintf(`{
|
||||
"book_uuid": "%s",
|
||||
"content": "%s"
|
||||
}`, b1UUID, updatedBody),
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: false,
|
||||
noteBody: "",
|
||||
noteDeleted: true,
|
||||
expectedNoteBookUUID: b1UUID,
|
||||
expectedNoteBody: updatedBody,
|
||||
expectedNoteBookName: "js",
|
||||
expectedNotePublic: false,
|
||||
},
|
||||
{
|
||||
payload: fmt.Sprintf(`{
|
||||
"public": %t
|
||||
}`, true),
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: false,
|
||||
noteBody: "original content",
|
||||
noteDeleted: false,
|
||||
expectedNoteBookUUID: b1UUID,
|
||||
expectedNoteBody: "original content",
|
||||
expectedNoteBookName: "css",
|
||||
expectedNotePublic: true,
|
||||
},
|
||||
{
|
||||
payload: fmt.Sprintf(`{
|
||||
"public": %t
|
||||
}`, false),
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: true,
|
||||
noteBody: "original content",
|
||||
noteDeleted: false,
|
||||
expectedNoteBookUUID: b1UUID,
|
||||
expectedNoteBody: "original content",
|
||||
expectedNoteBookName: "css",
|
||||
expectedNotePublic: false,
|
||||
},
|
||||
{
|
||||
payload: fmt.Sprintf(`{
|
||||
"content": "%s",
|
||||
"public": %t
|
||||
}`, updatedBody, false),
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: true,
|
||||
noteBody: "original content",
|
||||
noteDeleted: false,
|
||||
expectedNoteBookUUID: b1UUID,
|
||||
expectedNoteBody: updatedBody,
|
||||
expectedNoteBookName: "css",
|
||||
expectedNotePublic: false,
|
||||
},
|
||||
{
|
||||
payload: fmt.Sprintf(`{
|
||||
"book_uuid": "%s",
|
||||
"content": "%s",
|
||||
"public": %t
|
||||
}`, b2UUID, updatedBody, true),
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: false,
|
||||
noteBody: "original content",
|
||||
noteDeleted: false,
|
||||
expectedNoteBookUUID: b2UUID,
|
||||
expectedNoteBody: updatedBody,
|
||||
expectedNoteBookName: "js",
|
||||
expectedNotePublic: true,
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("test case %d", idx), 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.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 101), "preparing user max_usn")
|
||||
|
||||
b1 := database.Book{
|
||||
UUID: b1UUID,
|
||||
UserID: user.ID,
|
||||
Label: "css",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1")
|
||||
b2 := database.Book{
|
||||
UUID: b2UUID,
|
||||
UserID: user.ID,
|
||||
Label: "js",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b2), "preparing b2")
|
||||
|
||||
note := database.Note{
|
||||
UserID: user.ID,
|
||||
UUID: tc.noteUUID,
|
||||
BookUUID: tc.noteBookUUID,
|
||||
Body: tc.noteBody,
|
||||
Deleted: tc.noteDeleted,
|
||||
Public: tc.notePublic,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(¬e), "preparing note")
|
||||
|
||||
// Execute
|
||||
endpoint := fmt.Sprintf("/v3/notes/%s", note.UUID)
|
||||
req := testutils.MakeReq(server.URL, "PATCH", endpoint, tc.payload)
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "status code mismatch for test case")
|
||||
|
||||
var bookRecord database.Book
|
||||
var noteRecord database.Note
|
||||
var userRecord database.User
|
||||
var noteCount, bookCount int
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Book{}).Count(&bookCount), "counting books")
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Note{}).Count(¬eCount), "counting notes")
|
||||
testutils.MustExec(t, testutils.DB.Where("uuid = ?", note.UUID).First(¬eRecord), "finding note")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", b1.ID).First(&bookRecord), "finding book")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", user.ID).First(&userRecord), "finding user record")
|
||||
|
||||
assert.Equalf(t, bookCount, 2, "book count mismatch")
|
||||
assert.Equalf(t, noteCount, 1, "note count mismatch")
|
||||
|
||||
assert.Equal(t, noteRecord.UUID, tc.noteUUID, "note uuid mismatch for test case")
|
||||
assert.Equal(t, noteRecord.Body, tc.expectedNoteBody, "note content mismatch for test case")
|
||||
assert.Equal(t, noteRecord.BookUUID, tc.expectedNoteBookUUID, "note book_uuid mismatch for test case")
|
||||
assert.Equal(t, noteRecord.Public, tc.expectedNotePublic, "note public mismatch for test case")
|
||||
assert.Equal(t, noteRecord.USN, 102, "note usn mismatch for test case")
|
||||
|
||||
assert.Equal(t, userRecord.MaxUSN, 102, "user max_usn mismatch for test case")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteNote(t *testing.T) {
|
||||
b1UUID := "37868a8e-a844-4265-9a4f-0be598084733"
|
||||
|
||||
testCases := []struct {
|
||||
content string
|
||||
deleted bool
|
||||
originalUSN int
|
||||
expectedUSN int
|
||||
expectedMaxUSN int
|
||||
}{
|
||||
{
|
||||
content: "n1 content",
|
||||
deleted: false,
|
||||
originalUSN: 12,
|
||||
expectedUSN: 982,
|
||||
expectedMaxUSN: 982,
|
||||
},
|
||||
{
|
||||
content: "",
|
||||
deleted: true,
|
||||
originalUSN: 12,
|
||||
expectedUSN: 982,
|
||||
expectedMaxUSN: 982,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("originally deleted %t", tc.deleted), 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.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 981), "preparing user max_usn")
|
||||
|
||||
b1 := database.Book{
|
||||
UUID: b1UUID,
|
||||
UserID: user.ID,
|
||||
Label: "js",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1")
|
||||
note := database.Note{
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Body: tc.content,
|
||||
Deleted: tc.deleted,
|
||||
USN: tc.originalUSN,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(¬e), "preparing note")
|
||||
|
||||
// Execute
|
||||
endpoint := fmt.Sprintf("/v3/notes/%s", note.UUID)
|
||||
req := testutils.MakeReq(server.URL, "DELETE", endpoint, "")
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
|
||||
var bookRecord database.Book
|
||||
var noteRecord database.Note
|
||||
var userRecord database.User
|
||||
var bookCount, noteCount int
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Book{}).Count(&bookCount), "counting books")
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Note{}).Count(¬eCount), "counting notes")
|
||||
testutils.MustExec(t, testutils.DB.Where("uuid = ?", note.UUID).First(¬eRecord), "finding note")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", b1.ID).First(&bookRecord), "finding book")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", user.ID).First(&userRecord), "finding user record")
|
||||
|
||||
assert.Equalf(t, bookCount, 1, "book count mismatch")
|
||||
assert.Equalf(t, noteCount, 1, "note count mismatch")
|
||||
|
||||
assert.Equal(t, noteRecord.UUID, note.UUID, "note uuid mismatch for test case")
|
||||
assert.Equal(t, noteRecord.Body, "", "note content mismatch for test case")
|
||||
assert.Equal(t, noteRecord.Deleted, true, "note deleted mismatch for test case")
|
||||
assert.Equal(t, noteRecord.BookUUID, note.BookUUID, "note book_uuid mismatch for test case")
|
||||
assert.Equal(t, noteRecord.UserID, note.UserID, "note user_id mismatch for test case")
|
||||
assert.Equal(t, noteRecord.USN, tc.expectedUSN, "note usn mismatch for test case")
|
||||
|
||||
assert.Equal(t, userRecord.MaxUSN, tc.expectedMaxUSN, "user max_usn mismatch for test case")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -23,9 +23,7 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
// "time"
|
||||
|
||||
"github.com/dnote/dnote/pkg/assert"
|
||||
"github.com/dnote/dnote/pkg/clock"
|
||||
|
|
@ -38,165 +36,151 @@ import (
|
|||
)
|
||||
|
||||
func TestGetBooks(t *testing.T) {
|
||||
testutils.RunForWebAndAPI(t, "get notes", func(t *testing.T, target testutils.EndpointType) {
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
Clock: clock.NewMock(),
|
||||
Config: config.Config{
|
||||
PageTemplateDir: "../views",
|
||||
},
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
anotherUser := testutils.SetupUserData()
|
||||
|
||||
b1 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "js",
|
||||
USN: 1123,
|
||||
Deleted: false,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1")
|
||||
b2 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "css",
|
||||
USN: 1125,
|
||||
Deleted: false,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b2), "preparing b2")
|
||||
b3 := database.Book{
|
||||
UserID: anotherUser.ID,
|
||||
Label: "css",
|
||||
USN: 1128,
|
||||
Deleted: false,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b3), "preparing b3")
|
||||
b4 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "",
|
||||
USN: 1129,
|
||||
Deleted: true,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b4), "preparing b4")
|
||||
|
||||
// Execute
|
||||
var endpoint string
|
||||
if target == testutils.EndpointWeb {
|
||||
endpoint = "/books"
|
||||
} else {
|
||||
endpoint = "/api/v3/books"
|
||||
}
|
||||
|
||||
req := testutils.MakeReq(server.URL, "GET", endpoint, "")
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
|
||||
if target == testutils.EndpointAPI {
|
||||
var payload []presenters.Book
|
||||
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
var b1Record, b2Record database.Book
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", b1.ID).First(&b1Record), "finding b1")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", b2.ID).First(&b2Record), "finding b2")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", b2.ID).First(&b2Record), "finding b2")
|
||||
|
||||
expected := []presenters.Book{
|
||||
{
|
||||
UUID: b2Record.UUID,
|
||||
CreatedAt: b2Record.CreatedAt,
|
||||
UpdatedAt: b2Record.UpdatedAt,
|
||||
Label: b2Record.Label,
|
||||
USN: b2Record.USN,
|
||||
},
|
||||
{
|
||||
UUID: b1Record.UUID,
|
||||
CreatedAt: b1Record.CreatedAt,
|
||||
UpdatedAt: b1Record.UpdatedAt,
|
||||
Label: b1Record.Label,
|
||||
USN: b1Record.USN,
|
||||
},
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
}
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
Clock: clock.NewMock(),
|
||||
Config: config.Config{
|
||||
PageTemplateDir: "../views",
|
||||
},
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(user, "alice@test.com", "pass1234")
|
||||
anotherUser := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(anotherUser, "bob@test.com", "pass1234")
|
||||
|
||||
b1 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "js",
|
||||
USN: 1123,
|
||||
Deleted: false,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1")
|
||||
b2 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "css",
|
||||
USN: 1125,
|
||||
Deleted: false,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b2), "preparing b2")
|
||||
b3 := database.Book{
|
||||
UserID: anotherUser.ID,
|
||||
Label: "css",
|
||||
USN: 1128,
|
||||
Deleted: false,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b3), "preparing b3")
|
||||
b4 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "",
|
||||
USN: 1129,
|
||||
Deleted: true,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b4), "preparing b4")
|
||||
|
||||
// Execute
|
||||
endpoint := "/api/v3/books"
|
||||
|
||||
req := testutils.MakeReq(server.URL, "GET", endpoint, "")
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
|
||||
var payload []presenters.Book
|
||||
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
var b1Record, b2Record database.Book
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", b1.ID).First(&b1Record), "finding b1")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", b2.ID).First(&b2Record), "finding b2")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", b2.ID).First(&b2Record), "finding b2")
|
||||
|
||||
expected := []presenters.Book{
|
||||
{
|
||||
UUID: b2Record.UUID,
|
||||
CreatedAt: b2Record.CreatedAt,
|
||||
UpdatedAt: b2Record.UpdatedAt,
|
||||
Label: b2Record.Label,
|
||||
USN: b2Record.USN,
|
||||
},
|
||||
{
|
||||
UUID: b1Record.UUID,
|
||||
CreatedAt: b1Record.CreatedAt,
|
||||
UpdatedAt: b1Record.UpdatedAt,
|
||||
Label: b1Record.Label,
|
||||
USN: b1Record.USN,
|
||||
},
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
}
|
||||
|
||||
func TestGetBooksByName(t *testing.T) {
|
||||
testutils.RunForWebAndAPI(t, "get notes", func(t *testing.T, target testutils.EndpointType) {
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
Clock: clock.NewMock(),
|
||||
Config: config.Config{
|
||||
PageTemplateDir: "../views",
|
||||
},
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
anotherUser := testutils.SetupUserData()
|
||||
|
||||
b1 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "js",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1")
|
||||
b2 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "css",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b2), "preparing b2")
|
||||
b3 := database.Book{
|
||||
UserID: anotherUser.ID,
|
||||
Label: "js",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b3), "preparing b3")
|
||||
|
||||
// Execute
|
||||
var endpoint string
|
||||
if target == testutils.EndpointWeb {
|
||||
endpoint = "/books?name=js"
|
||||
} else {
|
||||
endpoint = "/api/v3/books?name=js"
|
||||
}
|
||||
|
||||
req := testutils.MakeReq(server.URL, "GET", endpoint, "")
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
|
||||
if target == testutils.EndpointAPI {
|
||||
var payload []presenters.Book
|
||||
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
var b1Record database.Book
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", b1.ID).First(&b1Record), "finding b1")
|
||||
|
||||
expected := []presenters.Book{
|
||||
{
|
||||
UUID: b1Record.UUID,
|
||||
CreatedAt: b1Record.CreatedAt,
|
||||
UpdatedAt: b1Record.UpdatedAt,
|
||||
Label: b1Record.Label,
|
||||
USN: b1Record.USN,
|
||||
},
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
}
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
Clock: clock.NewMock(),
|
||||
Config: config.Config{
|
||||
PageTemplateDir: "../views",
|
||||
},
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(user, "alice@test.com", "pass1234")
|
||||
anotherUser := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(anotherUser, "bob@test.com", "pass1234")
|
||||
|
||||
b1 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "js",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1")
|
||||
b2 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "css",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b2), "preparing b2")
|
||||
b3 := database.Book{
|
||||
UserID: anotherUser.ID,
|
||||
Label: "js",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b3), "preparing b3")
|
||||
|
||||
// Execute
|
||||
endpoint := "/api/v3/books?name=js"
|
||||
|
||||
req := testutils.MakeReq(server.URL, "GET", endpoint, "")
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
|
||||
var payload []presenters.Book
|
||||
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
var b1Record database.Book
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", b1.ID).First(&b1Record), "finding b1")
|
||||
|
||||
expected := []presenters.Book{
|
||||
{
|
||||
UUID: b1Record.UUID,
|
||||
CreatedAt: b1Record.CreatedAt,
|
||||
UpdatedAt: b1Record.UpdatedAt,
|
||||
Label: b1Record.Label,
|
||||
USN: b1Record.USN,
|
||||
},
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
}
|
||||
|
||||
func TestGetBook(t *testing.T) {
|
||||
|
|
@ -212,7 +196,9 @@ func TestGetBook(t *testing.T) {
|
|||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(user, "alice@test.com", "pass1234")
|
||||
anotherUser := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(anotherUser, "bob@test.com", "pass1234")
|
||||
|
||||
b1 := database.Book{
|
||||
UserID: user.ID,
|
||||
|
|
@ -270,7 +256,9 @@ func TestGetBookNonOwner(t *testing.T) {
|
|||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(user, "alice@test.com", "pass1234")
|
||||
nonOwner := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(nonOwner, "bob@test.com", "pass1234")
|
||||
|
||||
b1 := database.Book{
|
||||
UserID: user.ID,
|
||||
|
|
@ -294,7 +282,7 @@ func TestGetBookNonOwner(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCreateBook(t *testing.T) {
|
||||
testutils.RunForWebAndAPI(t, "success", func(t *testing.T, target testutils.EndpointType) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
|
|
@ -307,16 +295,10 @@ func TestCreateBook(t *testing.T) {
|
|||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(user, "alice@test.com", "pass1234")
|
||||
testutils.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 101), "preparing user max_usn")
|
||||
|
||||
var req *http.Request
|
||||
if target == testutils.EndpointWeb {
|
||||
dat := url.Values{}
|
||||
dat.Set("name", "js")
|
||||
req = testutils.MakeFormReq(server.URL, "POST", "/books", dat)
|
||||
} else {
|
||||
req = testutils.MakeReq(server.URL, "POST", "/api/v3/books", `{"name": "js"}`)
|
||||
}
|
||||
req := testutils.MakeReq(server.URL, "POST", "/api/v3/books", `{"name": "js"}`)
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
|
@ -343,26 +325,24 @@ func TestCreateBook(t *testing.T) {
|
|||
assert.Equal(t, bookRecord.USN, maxUSN, "book user_id mismatch")
|
||||
assert.Equal(t, userRecord.MaxUSN, maxUSN, "user max_usn mismatch")
|
||||
|
||||
if target == testutils.EndpointAPI {
|
||||
var got createBookResp
|
||||
if err := json.NewDecoder(res.Body).Decode(&got); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding"))
|
||||
}
|
||||
expected := createBookResp{
|
||||
Book: presenters.Book{
|
||||
UUID: bookRecord.UUID,
|
||||
USN: bookRecord.USN,
|
||||
CreatedAt: bookRecord.CreatedAt,
|
||||
UpdatedAt: bookRecord.UpdatedAt,
|
||||
Label: "js",
|
||||
},
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, got, expected, "payload mismatch")
|
||||
var got createBookResp
|
||||
if err := json.NewDecoder(res.Body).Decode(&got); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding"))
|
||||
}
|
||||
expected := createBookResp{
|
||||
Book: presenters.Book{
|
||||
UUID: bookRecord.UUID,
|
||||
USN: bookRecord.USN,
|
||||
CreatedAt: bookRecord.CreatedAt,
|
||||
UpdatedAt: bookRecord.UpdatedAt,
|
||||
Label: "js",
|
||||
},
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, got, expected, "payload mismatch")
|
||||
})
|
||||
|
||||
testutils.RunForWebAndAPI(t, "duplicate", func(t *testing.T, target testutils.EndpointType) {
|
||||
t.Run("duplicate", func(t *testing.T) {
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
|
|
@ -375,6 +355,7 @@ func TestCreateBook(t *testing.T) {
|
|||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(user, "alice@test.com", "pass1234")
|
||||
testutils.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 101), "preparing user max_usn")
|
||||
|
||||
b1 := database.Book{
|
||||
|
|
@ -385,14 +366,7 @@ func TestCreateBook(t *testing.T) {
|
|||
testutils.MustExec(t, testutils.DB.Save(&b1), "preparing book data")
|
||||
|
||||
// Execute
|
||||
var req *http.Request
|
||||
if target == testutils.EndpointWeb {
|
||||
dat := url.Values{}
|
||||
dat.Set("name", "js")
|
||||
req = testutils.MakeFormReq(server.URL, "POST", "/books", dat)
|
||||
} else {
|
||||
req = testutils.MakeReq(server.URL, "POST", "/api/v3/books", `{"name": "js"}`)
|
||||
}
|
||||
req := testutils.MakeReq(server.URL, "POST", "/api/v3/books", `{"name": "js"}`)
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
|
|
@ -459,7 +433,7 @@ func TestUpdateBook(t *testing.T) {
|
|||
}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
testutils.RunForWebAndAPI(t, fmt.Sprintf("test case %d", idx), func(t *testing.T, target testutils.EndpointType) {
|
||||
t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) {
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
|
|
@ -472,6 +446,7 @@ func TestUpdateBook(t *testing.T) {
|
|||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(user, "alice@test.com", "pass1234")
|
||||
testutils.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 101), "preparing user max_usn")
|
||||
|
||||
b1 := database.Book{
|
||||
|
|
@ -489,14 +464,8 @@ func TestUpdateBook(t *testing.T) {
|
|||
testutils.MustExec(t, testutils.DB.Save(&b2), "preparing b2")
|
||||
|
||||
// Execute
|
||||
var req *http.Request
|
||||
if target == testutils.EndpointWeb {
|
||||
endpoint := fmt.Sprintf("/books/%s", tc.bookUUID)
|
||||
req = testutils.MakeFormReq(server.URL, "PATCH", endpoint, tc.payload.ToURLValues())
|
||||
} else {
|
||||
endpoint := fmt.Sprintf("/api/v3/books/%s", tc.bookUUID)
|
||||
req = testutils.MakeReq(server.URL, "PATCH", endpoint, tc.payload.ToJSON(t))
|
||||
}
|
||||
endpoint := fmt.Sprintf("/api/v3/books/%s", tc.bookUUID)
|
||||
req := testutils.MakeReq(server.URL, "PATCH", endpoint, tc.payload.ToJSON(t))
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
|
|
@ -551,7 +520,7 @@ func TestDeleteBook(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
testutils.RunForWebAndAPI(t, fmt.Sprintf("originally deleted %t", tc.deleted), func(t *testing.T, target testutils.EndpointType) {
|
||||
t.Run(fmt.Sprintf("originally deleted %t", tc.deleted), func(t *testing.T) {
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
|
|
@ -564,8 +533,10 @@ func TestDeleteBook(t *testing.T) {
|
|||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(user, "alice@test.com", "pass1234")
|
||||
testutils.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 58), "preparing user max_usn")
|
||||
anotherUser := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(anotherUser, "bob@test.com", "pass1234")
|
||||
testutils.MustExec(t, testutils.DB.Model(&anotherUser).Update("max_usn", 109), "preparing another user max_usn")
|
||||
|
||||
b1 := database.Book{
|
||||
|
|
@ -637,12 +608,7 @@ func TestDeleteBook(t *testing.T) {
|
|||
testutils.MustExec(t, testutils.DB.Save(&n5), "preparing a note data")
|
||||
|
||||
// Execute
|
||||
var endpoint string
|
||||
if target == testutils.EndpointWeb {
|
||||
endpoint = fmt.Sprintf("/books/%s", b2.UUID)
|
||||
} else {
|
||||
endpoint = fmt.Sprintf("/api/v3/books/%s", b2.UUID)
|
||||
}
|
||||
endpoint := fmt.Sprintf("/api/v3/books/%s", b2.UUID)
|
||||
|
||||
req := testutils.MakeReq(server.URL, "DELETE", endpoint, "")
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -57,127 +56,120 @@ func getExpectedNotePayload(n database.Note, b database.Book, u database.User) p
|
|||
}
|
||||
|
||||
func TestGetNotes(t *testing.T) {
|
||||
testutils.RunForWebAndAPI(t, "get notes", func(t *testing.T, target testutils.EndpointType) {
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
Clock: clock.NewMock(),
|
||||
Config: config.Config{
|
||||
PageTemplateDir: "../views",
|
||||
},
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
anotherUser := testutils.SetupUserData()
|
||||
|
||||
b1 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "js",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1")
|
||||
b2 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "css",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b2), "preparing b2")
|
||||
b3 := database.Book{
|
||||
UserID: anotherUser.ID,
|
||||
Label: "css",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b3), "preparing b3")
|
||||
|
||||
n1 := database.Note{
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Body: "n1 content",
|
||||
USN: 11,
|
||||
Deleted: false,
|
||||
AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n1), "preparing n1")
|
||||
n2 := database.Note{
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Body: "n2 content",
|
||||
USN: 14,
|
||||
Deleted: false,
|
||||
AddedOn: time.Date(2018, time.August, 11, 22, 0, 0, 0, time.UTC).UnixNano(),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n2), "preparing n2")
|
||||
n3 := database.Note{
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Body: "n3 content",
|
||||
USN: 17,
|
||||
Deleted: false,
|
||||
AddedOn: time.Date(2017, time.January, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n3), "preparing n3")
|
||||
n4 := database.Note{
|
||||
UserID: user.ID,
|
||||
BookUUID: b2.UUID,
|
||||
Body: "n4 content",
|
||||
USN: 18,
|
||||
Deleted: false,
|
||||
AddedOn: time.Date(2018, time.September, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n4), "preparing n4")
|
||||
n5 := database.Note{
|
||||
UserID: anotherUser.ID,
|
||||
BookUUID: b3.UUID,
|
||||
Body: "n5 content",
|
||||
USN: 19,
|
||||
Deleted: false,
|
||||
AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n5), "preparing n5")
|
||||
n6 := database.Note{
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Body: "",
|
||||
USN: 11,
|
||||
Deleted: true,
|
||||
AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n6), "preparing n6")
|
||||
|
||||
// Execute
|
||||
var endpoint string
|
||||
if target == testutils.EndpointWeb {
|
||||
endpoint = "/"
|
||||
} else {
|
||||
endpoint = "/api/v3/notes"
|
||||
}
|
||||
|
||||
req := testutils.MakeReq(server.URL, "GET", fmt.Sprintf("%s?year=2018&month=8", endpoint), "")
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
|
||||
if target == testutils.EndpointAPI {
|
||||
var payload GetNotesResponse
|
||||
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
var n2Record, n1Record database.Note
|
||||
testutils.MustExec(t, testutils.DB.Where("uuid = ?", n2.UUID).First(&n2Record), "finding n2Record")
|
||||
testutils.MustExec(t, testutils.DB.Where("uuid = ?", n1.UUID).First(&n1Record), "finding n1Record")
|
||||
|
||||
expected := GetNotesResponse{
|
||||
Notes: []presenters.Note{
|
||||
getExpectedNotePayload(n2Record, b1, user),
|
||||
getExpectedNotePayload(n1Record, b1, user),
|
||||
},
|
||||
Total: 2,
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
}
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
Clock: clock.NewMock(),
|
||||
Config: config.Config{
|
||||
PageTemplateDir: "../views",
|
||||
},
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(user, "alice@test.com", "pass1234")
|
||||
anotherUser := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(anotherUser, "bob@test.com", "pass1234")
|
||||
|
||||
b1 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "js",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1")
|
||||
b2 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "css",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b2), "preparing b2")
|
||||
b3 := database.Book{
|
||||
UserID: anotherUser.ID,
|
||||
Label: "css",
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b3), "preparing b3")
|
||||
|
||||
n1 := database.Note{
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Body: "n1 content",
|
||||
USN: 11,
|
||||
Deleted: false,
|
||||
AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n1), "preparing n1")
|
||||
n2 := database.Note{
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Body: "n2 content",
|
||||
USN: 14,
|
||||
Deleted: false,
|
||||
AddedOn: time.Date(2018, time.August, 11, 22, 0, 0, 0, time.UTC).UnixNano(),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n2), "preparing n2")
|
||||
n3 := database.Note{
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Body: "n3 content",
|
||||
USN: 17,
|
||||
Deleted: false,
|
||||
AddedOn: time.Date(2017, time.January, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n3), "preparing n3")
|
||||
n4 := database.Note{
|
||||
UserID: user.ID,
|
||||
BookUUID: b2.UUID,
|
||||
Body: "n4 content",
|
||||
USN: 18,
|
||||
Deleted: false,
|
||||
AddedOn: time.Date(2018, time.September, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n4), "preparing n4")
|
||||
n5 := database.Note{
|
||||
UserID: anotherUser.ID,
|
||||
BookUUID: b3.UUID,
|
||||
Body: "n5 content",
|
||||
USN: 19,
|
||||
Deleted: false,
|
||||
AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n5), "preparing n5")
|
||||
n6 := database.Note{
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Body: "",
|
||||
USN: 11,
|
||||
Deleted: true,
|
||||
AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(),
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n6), "preparing n6")
|
||||
|
||||
// Execute
|
||||
endpoint := "/api/v3/notes"
|
||||
|
||||
req := testutils.MakeReq(server.URL, "GET", fmt.Sprintf("%s?year=2018&month=8", endpoint), "")
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
|
||||
var payload GetNotesResponse
|
||||
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
var n2Record, n1Record database.Note
|
||||
testutils.MustExec(t, testutils.DB.Where("uuid = ?", n2.UUID).First(&n2Record), "finding n2Record")
|
||||
testutils.MustExec(t, testutils.DB.Where("uuid = ?", n1.UUID).First(&n1Record), "finding n1Record")
|
||||
|
||||
expected := GetNotesResponse{
|
||||
Notes: []presenters.Note{
|
||||
getExpectedNotePayload(n2Record, b1, user),
|
||||
getExpectedNotePayload(n1Record, b1, user),
|
||||
},
|
||||
Total: 2,
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
}
|
||||
|
||||
func TestGetNote(t *testing.T) {
|
||||
|
|
@ -222,246 +214,217 @@ func TestGetNote(t *testing.T) {
|
|||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&deletedNote), "preparing deletedNote")
|
||||
|
||||
getURL := func(noteUUID string, target testutils.EndpointType) string {
|
||||
if target == testutils.EndpointWeb {
|
||||
return fmt.Sprintf("/notes/%s", noteUUID)
|
||||
}
|
||||
|
||||
getURL := func(noteUUID string) string {
|
||||
return fmt.Sprintf("/api/v3/notes/%s", noteUUID)
|
||||
}
|
||||
|
||||
testutils.RunForWebAndAPI(t, "owner accessing private note", func(t *testing.T, target testutils.EndpointType) {
|
||||
t.Run("owner accessing private note", func(t *testing.T) {
|
||||
// Execute
|
||||
url := getURL(publicNote.UUID, target)
|
||||
url := getURL(publicNote.UUID)
|
||||
req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
|
||||
if target == testutils.EndpointAPI {
|
||||
var payload presenters.Note
|
||||
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
var n2Record database.Note
|
||||
testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record")
|
||||
|
||||
expected := getExpectedNotePayload(n2Record, b1, user)
|
||||
assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
var payload presenters.Note
|
||||
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
var n2Record database.Note
|
||||
testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record")
|
||||
|
||||
expected := getExpectedNotePayload(n2Record, b1, user)
|
||||
assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
})
|
||||
|
||||
testutils.RunForWebAndAPI(t, "owner accessing public note", func(t *testing.T, target testutils.EndpointType) {
|
||||
t.Run("owner accessing public note", func(t *testing.T) {
|
||||
// Execute
|
||||
url := getURL(publicNote.UUID, target)
|
||||
url := getURL(publicNote.UUID)
|
||||
req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
|
||||
if target == testutils.EndpointAPI {
|
||||
var payload presenters.Note
|
||||
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
var n2Record database.Note
|
||||
testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record")
|
||||
|
||||
expected := getExpectedNotePayload(n2Record, b1, user)
|
||||
assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
var payload presenters.Note
|
||||
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
var n2Record database.Note
|
||||
testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record")
|
||||
|
||||
expected := getExpectedNotePayload(n2Record, b1, user)
|
||||
assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
})
|
||||
|
||||
testutils.RunForWebAndAPI(t, "non-owner accessing public note", func(t *testing.T, target testutils.EndpointType) {
|
||||
t.Run("non-owner accessing public note", func(t *testing.T) {
|
||||
// Execute
|
||||
url := getURL(publicNote.UUID, target)
|
||||
url := getURL(publicNote.UUID)
|
||||
req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
res := testutils.HTTPAuthDo(t, req, anotherUser)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
|
||||
if target == testutils.EndpointAPI {
|
||||
var payload presenters.Note
|
||||
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
var n2Record database.Note
|
||||
testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record")
|
||||
|
||||
expected := getExpectedNotePayload(n2Record, b1, user)
|
||||
assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
var payload presenters.Note
|
||||
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
var n2Record database.Note
|
||||
testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record")
|
||||
|
||||
expected := getExpectedNotePayload(n2Record, b1, user)
|
||||
assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
})
|
||||
|
||||
testutils.RunForWebAndAPI(t, "non-owner accessing private note", func(t *testing.T, target testutils.EndpointType) {
|
||||
t.Run("non-owner accessing private note", func(t *testing.T) {
|
||||
// Execute
|
||||
url := getURL(privateNote.UUID, target)
|
||||
url := getURL(privateNote.UUID)
|
||||
req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
res := testutils.HTTPAuthDo(t, req, anotherUser)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusNotFound, "")
|
||||
|
||||
if target == testutils.EndpointAPI {
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "reading body"))
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "reading body"))
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
|
||||
})
|
||||
|
||||
testutils.RunForWebAndAPI(t, "guest accessing public note", func(t *testing.T, target testutils.EndpointType) {
|
||||
t.Run("guest accessing public note", func(t *testing.T) {
|
||||
// Execute
|
||||
url := getURL(publicNote.UUID, target)
|
||||
url := getURL(publicNote.UUID)
|
||||
req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
|
||||
if target == testutils.EndpointAPI {
|
||||
var payload presenters.Note
|
||||
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
var n2Record database.Note
|
||||
testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record")
|
||||
|
||||
expected := getExpectedNotePayload(n2Record, b1, user)
|
||||
assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
var payload presenters.Note
|
||||
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
var n2Record database.Note
|
||||
testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record")
|
||||
|
||||
expected := getExpectedNotePayload(n2Record, b1, user)
|
||||
assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
})
|
||||
|
||||
testutils.RunForWebAndAPI(t, "guest accessing private note", func(t *testing.T, target testutils.EndpointType) {
|
||||
t.Run("guest accessing private note", func(t *testing.T) {
|
||||
// Execute
|
||||
url := getURL(privateNote.UUID, target)
|
||||
url := getURL(privateNote.UUID)
|
||||
req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusNotFound, "")
|
||||
|
||||
if target == testutils.EndpointAPI {
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "reading body"))
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "reading body"))
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
|
||||
})
|
||||
|
||||
testutils.RunForWebAndAPI(t, "nonexistent", func(t *testing.T, target testutils.EndpointType) {
|
||||
t.Run("nonexistent", func(t *testing.T) {
|
||||
// Execute
|
||||
url := getURL("somerandomstring", target)
|
||||
url := getURL("somerandomstring")
|
||||
req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusNotFound, "")
|
||||
|
||||
if target == testutils.EndpointAPI {
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "reading body"))
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "reading body"))
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
|
||||
})
|
||||
|
||||
testutils.RunForWebAndAPI(t, "deleted", func(t *testing.T, target testutils.EndpointType) {
|
||||
t.Run("deleted", func(t *testing.T) {
|
||||
// Execute
|
||||
url := getURL(deletedNote.UUID, target)
|
||||
url := getURL(deletedNote.UUID)
|
||||
req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusNotFound, "")
|
||||
|
||||
if target == testutils.EndpointAPI {
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "reading body"))
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "reading body"))
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateNote(t *testing.T) {
|
||||
testutils.RunForWebAndAPI(t, "success", func(t *testing.T, target testutils.EndpointType) {
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
Clock: clock.NewMock(),
|
||||
Config: config.Config{
|
||||
PageTemplateDir: "../views",
|
||||
},
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
testutils.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 101), "preparing user max_usn")
|
||||
|
||||
b1 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "js",
|
||||
USN: 58,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1")
|
||||
|
||||
// Execute
|
||||
|
||||
var req *http.Request
|
||||
if target == testutils.EndpointAPI {
|
||||
dat := fmt.Sprintf(`{"book_uuid": "%s", "content": "note content"}`, b1.UUID)
|
||||
req = testutils.MakeReq(server.URL, "POST", "/api/v3/notes", dat)
|
||||
} else {
|
||||
dat := url.Values{}
|
||||
dat.Set("book_uuid", b1.UUID)
|
||||
dat.Set("content", "note content")
|
||||
req = testutils.MakeFormReq(server.URL, "POST", "/notes", dat)
|
||||
}
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusCreated, "")
|
||||
|
||||
var noteRecord database.Note
|
||||
var bookRecord database.Book
|
||||
var userRecord database.User
|
||||
var bookCount, noteCount int
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Book{}).Count(&bookCount), "counting books")
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Note{}).Count(¬eCount), "counting notes")
|
||||
testutils.MustExec(t, testutils.DB.First(¬eRecord), "finding note")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", b1.ID).First(&bookRecord), "finding book")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", user.ID).First(&userRecord), "finding user record")
|
||||
|
||||
assert.Equalf(t, bookCount, 1, "book count mismatch")
|
||||
assert.Equalf(t, noteCount, 1, "note count mismatch")
|
||||
|
||||
assert.Equal(t, bookRecord.Label, b1.Label, "book name mismatch")
|
||||
assert.Equal(t, bookRecord.UUID, b1.UUID, "book uuid mismatch")
|
||||
assert.Equal(t, bookRecord.UserID, b1.UserID, "book user_id mismatch")
|
||||
assert.Equal(t, bookRecord.USN, 58, "book usn mismatch")
|
||||
|
||||
assert.NotEqual(t, noteRecord.UUID, "", "note uuid should have been generated")
|
||||
assert.Equal(t, noteRecord.BookUUID, b1.UUID, "note book_uuid mismatch")
|
||||
assert.Equal(t, noteRecord.Body, "note content", "note content mismatch")
|
||||
assert.Equal(t, noteRecord.USN, 102, "note usn mismatch")
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
Clock: clock.NewMock(),
|
||||
Config: config.Config{
|
||||
PageTemplateDir: "../views",
|
||||
},
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(user, "alice@test.com", "pass1234")
|
||||
testutils.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 101), "preparing user max_usn")
|
||||
|
||||
b1 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "js",
|
||||
USN: 58,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1")
|
||||
|
||||
// Execute
|
||||
|
||||
dat := fmt.Sprintf(`{"book_uuid": "%s", "content": "note content"}`, b1.UUID)
|
||||
req := testutils.MakeReq(server.URL, "POST", "/api/v3/notes", dat)
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusCreated, "")
|
||||
|
||||
var noteRecord database.Note
|
||||
var bookRecord database.Book
|
||||
var userRecord database.User
|
||||
var bookCount, noteCount int
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Book{}).Count(&bookCount), "counting books")
|
||||
testutils.MustExec(t, testutils.DB.Model(&database.Note{}).Count(¬eCount), "counting notes")
|
||||
testutils.MustExec(t, testutils.DB.First(¬eRecord), "finding note")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", b1.ID).First(&bookRecord), "finding book")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", user.ID).First(&userRecord), "finding user record")
|
||||
|
||||
assert.Equalf(t, bookCount, 1, "book count mismatch")
|
||||
assert.Equalf(t, noteCount, 1, "note count mismatch")
|
||||
|
||||
assert.Equal(t, bookRecord.Label, b1.Label, "book name mismatch")
|
||||
assert.Equal(t, bookRecord.UUID, b1.UUID, "book uuid mismatch")
|
||||
assert.Equal(t, bookRecord.UserID, b1.UserID, "book user_id mismatch")
|
||||
assert.Equal(t, bookRecord.USN, 58, "book usn mismatch")
|
||||
|
||||
assert.NotEqual(t, noteRecord.UUID, "", "note uuid should have been generated")
|
||||
assert.Equal(t, noteRecord.BookUUID, b1.UUID, "note book_uuid mismatch")
|
||||
assert.Equal(t, noteRecord.Body, "note content", "note content mismatch")
|
||||
assert.Equal(t, noteRecord.USN, 102, "note usn mismatch")
|
||||
}
|
||||
|
||||
func TestDeleteNote(t *testing.T) {
|
||||
|
|
@ -490,8 +453,8 @@ func TestDeleteNote(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
testutils.RunForWebAndAPI(t, fmt.Sprintf("originally deleted %t", tc.deleted), func(t *testing.T, target testutils.EndpointType) {
|
||||
for idx, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) {
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
|
|
@ -504,6 +467,7 @@ func TestDeleteNote(t *testing.T) {
|
|||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(user, "alice@test.com", "pass1234")
|
||||
testutils.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 981), "preparing user max_usn")
|
||||
|
||||
b1 := database.Book{
|
||||
|
|
@ -522,14 +486,8 @@ func TestDeleteNote(t *testing.T) {
|
|||
testutils.MustExec(t, testutils.DB.Save(¬e), "preparing note")
|
||||
|
||||
// Execute
|
||||
var req *http.Request
|
||||
if target == testutils.EndpointAPI {
|
||||
endpoint := fmt.Sprintf("/api/v3/notes/%s", note.UUID)
|
||||
req = testutils.MakeReq(server.URL, "DELETE", endpoint, "")
|
||||
} else {
|
||||
endpoint := fmt.Sprintf("/notes/%s", note.UUID)
|
||||
req = testutils.MakeFormReq(server.URL, "DELETE", endpoint, nil)
|
||||
}
|
||||
endpoint := fmt.Sprintf("/api/v3/notes/%s", note.UUID)
|
||||
req := testutils.MakeReq(server.URL, "DELETE", endpoint, "")
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
|
|
@ -736,7 +694,7 @@ func TestUpdateNote(t *testing.T) {
|
|||
}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
testutils.RunForWebAndAPI(t, fmt.Sprintf("test case %d", idx), func(t *testing.T, target testutils.EndpointType) {
|
||||
t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) {
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
|
|
@ -749,6 +707,8 @@ func TestUpdateNote(t *testing.T) {
|
|||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(user, "alice@test.com", "pass1234")
|
||||
|
||||
testutils.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 101), "preparing user max_usn")
|
||||
|
||||
b1 := database.Book{
|
||||
|
|
@ -777,13 +737,8 @@ func TestUpdateNote(t *testing.T) {
|
|||
// Execute
|
||||
var req *http.Request
|
||||
|
||||
if target == testutils.EndpointWeb {
|
||||
endpoint := fmt.Sprintf("/notes/%s", note.UUID)
|
||||
req = testutils.MakeFormReq(server.URL, "PATCH", endpoint, tc.payload.ToURLValues())
|
||||
} else {
|
||||
endpoint := fmt.Sprintf("/api/v3/notes/%s", note.UUID)
|
||||
req = testutils.MakeReq(server.URL, "PATCH", endpoint, tc.payload.ToJSON(t))
|
||||
}
|
||||
endpoint := fmt.Sprintf("/api/v3/notes/%s", note.UUID)
|
||||
req = testutils.MakeReq(server.URL, "PATCH", endpoint, tc.payload.ToJSON(t))
|
||||
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
|
|
|
|||
|
|
@ -29,20 +29,11 @@ func NewWebRoutes(a *app.App, c *Controllers) []Route {
|
|||
redirectGuest := &mw.AuthParams{RedirectGuestsToLogin: true}
|
||||
|
||||
ret := []Route{
|
||||
{"GET", "/", mw.Auth(a, c.Notes.Index, redirectGuest), true},
|
||||
{"GET", "/", mw.Auth(a, c.Users.Settings, redirectGuest), true},
|
||||
{"GET", "/login", mw.GuestOnly(a, c.Users.NewLogin), true},
|
||||
{"POST", "/login", mw.GuestOnly(a, c.Users.Login), true},
|
||||
{"POST", "/logout", c.Users.Logout, true},
|
||||
{"GET", "/notes/{noteUUID}", mw.Auth(a, c.Notes.Show, nil), true},
|
||||
{"POST", "/notes", mw.Auth(a, c.Notes.Create, nil), true},
|
||||
{"DELETE", "/notes/{noteUUID}", mw.Auth(a, c.Notes.Delete, nil), true},
|
||||
{"PATCH", "/notes/{noteUUID}", mw.Auth(a, c.Notes.Update, nil), true},
|
||||
{"GET", "/books", mw.Auth(a, c.Books.Index, redirectGuest), true},
|
||||
{"POST", "/books", mw.Auth(a, c.Books.Create, nil), true},
|
||||
{"PATCH", "/books/{bookUUID}", mw.Auth(a, c.Books.Update, nil), true},
|
||||
{"DELETE", "/books/{bookUUID}", mw.Auth(a, c.Books.Delete, nil), true},
|
||||
|
||||
{"GET", "/settings", mw.Auth(a, c.Users.Settings, nil), true},
|
||||
{"GET", "/password-reset", c.Users.PasswordResetView.ServeHTTP, true},
|
||||
{"PATCH", "/password-reset", c.Users.PasswordReset, true},
|
||||
{"GET", "/password-reset/{token}", c.Users.PasswordResetConfirm, true},
|
||||
|
|
|
|||
|
|
@ -184,6 +184,8 @@ func TestAuthMiddleware(t *testing.T) {
|
|||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(user, "alice@test.com", "pass1234")
|
||||
|
||||
session := database.Session{
|
||||
Key: "A9xgggqzTHETy++GDi1NpDNe0iyqosPm9bitdeNGkJU=",
|
||||
UserID: user.ID,
|
||||
|
|
@ -412,6 +414,8 @@ func TestAuthMiddleware_RedirectGuestsToLogin(t *testing.T) {
|
|||
req := testutils.MakeReq(server.URL, "GET", "/", "")
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
testutils.SetupAccountData(user, "alice@test.com", "pass1234")
|
||||
|
||||
testutils.MustExec(t, testutils.DB.Model(&user).Update("cloud", false), "preparing session")
|
||||
session := database.Session{
|
||||
Key: "A9xgggqzTHETy++GDi1NpDNe0iyqosPm9bitdeNGkJU=",
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@
|
|||
{{if .User}}
|
||||
<nav class="main-nav">
|
||||
<ul class="list-unstyled list">
|
||||
<li class="item">
|
||||
<a class="nav-link nav-item" href="/books">Books</a>
|
||||
</li>
|
||||
<li class="item">
|
||||
{{template "accountDropdown" .}}
|
||||
</li>
|
||||
|
|
@ -52,9 +49,6 @@
|
|||
</header>
|
||||
|
||||
<ul class="list-unstyled" role="menu">
|
||||
<li role="none">
|
||||
<a class="dropdown-link" href="/" role="menuitem">Home</a>
|
||||
</li>
|
||||
<li role="none">
|
||||
<a class="dropdown-link" href="/settings" role="menuitem">Settings</a>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ function run_test {
|
|||
if [ -z "$1" ]; then
|
||||
go test ./... -cover -p 1
|
||||
else
|
||||
go test "$1" -cover -p 1
|
||||
go test -run "$1" -cover -p 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue