Fix test and remove api package

This commit is contained in:
Sung Won Cho 2022-04-17 11:30:23 +10:00
commit 348bf8398c
21 changed files with 394 additions and 4844 deletions

View file

@ -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(&params); 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(&params); 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")
}
}

View file

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

View file

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

View file

@ -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(&notes).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(&notes).Error; err != nil {
// middleware.DoError(w, "finding notes", err, http.StatusInternalServerError)
// return
// }
//
// presented := presenters.PresentNotes(notes)
// middleware.RespondJSON(w, http.StatusOK, presented)
// }

View file

@ -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")
// })
// }

View file

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

View file

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

View file

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

View file

@ -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(&params)
// 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(&params); 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(&params); 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(&params); 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)
// }

View file

@ -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")
// }

View file

@ -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(&params)
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
}
}

View file

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

View file

@ -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(&params)
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(&params)
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(&notes).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)
}

View file

@ -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(&params)
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(&note).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(&note).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(&params)
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")
}

View file

@ -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(&noteCount), "counting notes")
testutils.MustExec(t, testutils.DB.First(&noteRecord), "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(&note), "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(&noteCount), "counting notes")
testutils.MustExec(t, testutils.DB.Where("uuid = ?", note.UUID).First(&noteRecord), "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(&note), "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(&noteCount), "counting notes")
testutils.MustExec(t, testutils.DB.Where("uuid = ?", note.UUID).First(&noteRecord), "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")
})
}
}

View file

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

View file

@ -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(&noteCount), "counting notes")
testutils.MustExec(t, testutils.DB.First(&noteRecord), "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(&noteCount), "counting notes")
testutils.MustExec(t, testutils.DB.First(&noteRecord), "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(&note), "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)

View file

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

View file

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

View file

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

View file

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