diff --git a/pkg/server/api/auth.go b/pkg/server/api/auth.go deleted file mode 100644 index 56425c87..00000000 --- a/pkg/server/api/auth.go +++ /dev/null @@ -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 . - */ - -package api - -import ( - "encoding/json" - "net/http" - "time" - - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/handlers" - "github.com/dnote/dnote/pkg/server/helpers" - "github.com/dnote/dnote/pkg/server/log" - "github.com/dnote/dnote/pkg/server/mailer" - "github.com/dnote/dnote/pkg/server/session" - "github.com/dnote/dnote/pkg/server/token" - "github.com/pkg/errors" - "golang.org/x/crypto/bcrypt" -) - -// GetMeResponse is the response for getMe endpoint -type GetMeResponse struct { - User session.Session `json:"user"` -} - -func (a *API) getMe(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - handlers.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError) - return - } - - var account database.Account - if err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil { - handlers.DoError(w, "finding account", err, http.StatusInternalServerError) - return - } - - tx := a.App.DB.Begin() - if err := a.App.TouchLastLoginAt(user, tx); err != nil { - tx.Rollback() - // In case of an error, gracefully continue to avoid disturbing the service - log.ErrorWrap(err, "error touching last_login_at") - } - tx.Commit() - - response := GetMeResponse{ - User: session.New(user, account), - } - handlers.RespondJSON(w, http.StatusOK, response) -} - -type createResetTokenPayload struct { - Email string `json:"email"` -} - -func (a *API) createResetToken(w http.ResponseWriter, r *http.Request) { - var params createResetTokenPayload - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - http.Error(w, "invalid payload", http.StatusBadRequest) - return - } - - var account database.Account - conn := a.App.DB.Where("email = ?", params.Email).First(&account) - if conn.RecordNotFound() { - return - } - if err := conn.Error; err != nil { - handlers.DoError(w, errors.Wrap(err, "finding account").Error(), nil, http.StatusInternalServerError) - return - } - - resetToken, err := token.Create(a.App.DB, account.UserID, database.TokenTypeResetPassword) - if err != nil { - handlers.DoError(w, errors.Wrap(err, "generating token").Error(), nil, http.StatusInternalServerError) - return - } - - if err := a.App.SendPasswordResetEmail(account.Email.String, resetToken.Value); err != nil { - if errors.Cause(err) == mailer.ErrSMTPNotConfigured { - handlers.RespondInvalidSMTPConfig(w) - } else { - handlers.DoError(w, errors.Wrap(err, "sending password reset email").Error(), nil, http.StatusInternalServerError) - } - - return - } -} - -type resetPasswordPayload struct { - Password string `json:"password"` - Token string `json:"token"` -} - -func (a *API) resetPassword(w http.ResponseWriter, r *http.Request) { - var params resetPasswordPayload - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - http.Error(w, "invalid payload", http.StatusBadRequest) - return - } - - var token database.Token - conn := a.App.DB.Where("value = ? AND type =? AND used_at IS NULL", params.Token, database.TokenTypeResetPassword).First(&token) - if conn.RecordNotFound() { - http.Error(w, "invalid token", http.StatusBadRequest) - return - } - if err := conn.Error; err != nil { - handlers.DoError(w, errors.Wrap(err, "finding token").Error(), nil, http.StatusInternalServerError) - return - } - - if token.UsedAt != nil { - http.Error(w, "invalid token", http.StatusBadRequest) - return - } - - // Expire after 10 minutes - if time.Since(token.CreatedAt).Minutes() > 10 { - http.Error(w, "This link has been expired. Please request a new password reset link.", http.StatusGone) - return - } - - tx := a.App.DB.Begin() - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(params.Password), bcrypt.DefaultCost) - if err != nil { - tx.Rollback() - handlers.DoError(w, errors.Wrap(err, "hashing password").Error(), nil, http.StatusInternalServerError) - return - } - - var account database.Account - if err := a.App.DB.Where("user_id = ?", token.UserID).First(&account).Error; err != nil { - tx.Rollback() - handlers.DoError(w, errors.Wrap(err, "finding user").Error(), nil, http.StatusInternalServerError) - return - } - - if err := tx.Model(&account).Update("password", string(hashedPassword)).Error; err != nil { - tx.Rollback() - handlers.DoError(w, errors.Wrap(err, "updating password").Error(), nil, http.StatusInternalServerError) - return - } - if err := tx.Model(&token).Update("used_at", time.Now()).Error; err != nil { - tx.Rollback() - handlers.DoError(w, errors.Wrap(err, "updating password reset token").Error(), nil, http.StatusInternalServerError) - return - } - - if err := a.App.DeleteUserSessions(tx, account.UserID); err != nil { - tx.Rollback() - handlers.DoError(w, errors.Wrap(err, "deleting user sessions").Error(), nil, http.StatusInternalServerError) - return - } - - tx.Commit() - - var user database.User - if err := a.App.DB.Where("id = ?", account.UserID).First(&user).Error; err != nil { - handlers.DoError(w, errors.Wrap(err, "finding user").Error(), nil, http.StatusInternalServerError) - return - } - - a.respondWithSession(a.App.DB, w, user.ID, http.StatusOK) - - if err := a.App.SendPasswordResetAlertEmail(account.Email.String); err != nil { - log.ErrorWrap(err, "sending password reset email") - } -} diff --git a/pkg/server/api/auth_test.go b/pkg/server/api/auth_test.go deleted file mode 100644 index 1e75dd92..00000000 --- a/pkg/server/api/auth_test.go +++ /dev/null @@ -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 . - */ - -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") - }) -} diff --git a/pkg/server/api/helpers.go b/pkg/server/api/helpers.go deleted file mode 100644 index 69c65d25..00000000 --- a/pkg/server/api/helpers.go +++ /dev/null @@ -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 . - */ - -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 -// } diff --git a/pkg/server/api/notes.go b/pkg/server/api/notes.go deleted file mode 100644 index f3570a42..00000000 --- a/pkg/server/api/notes.go +++ /dev/null @@ -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 . - */ - -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=", -// "StopSel=", -// "ShortWord=0", -// } -// -// if params != nil && params.HighlightAll { -// headlineOptions = append(headlineOptions, "HighlightAll=true") -// } else { -// headlineOptions = append(headlineOptions, "MaxFragments=3, MaxWords=50, MinWords=10") -// } -// -// return strings.Join(headlineOptions, ",") -// } -// -// func selectFTSFields(conn *gorm.DB, search string, params *ftsParams) *gorm.DB { -// headlineOpts := getHeadlineOptions(params) -// -// return conn.Select(` -// notes.id, -// notes.uuid, -// notes.created_at, -// notes.updated_at, -// notes.book_uuid, -// notes.user_id, -// notes.added_on, -// notes.edited_on, -// notes.usn, -// notes.deleted, -// notes.encrypted, -// ts_headline('english_nostop', notes.body, plainto_tsquery('english_nostop', ?), ?) AS body -// `, search, headlineOpts) -// } -// -// func respondWithNote(w http.ResponseWriter, note database.Note) { -// presentedNote := presenters.PresentNote(note) -// -// middleware.RespondJSON(w, http.StatusOK, presentedNote) -// } -// -// func parseSearchQuery(q url.Values) string { -// searchStr := q.Get("q") -// -// return escapeSearchQuery(searchStr) -// } -// -// func getNoteBaseQuery(db *gorm.DB, noteUUID string, search string) *gorm.DB { -// var conn *gorm.DB -// if search != "" { -// conn = selectFTSFields(db, search, &ftsParams{HighlightAll: true}) -// } else { -// conn = db -// } -// -// conn = conn.Where("notes.uuid = ? AND deleted = ?", noteUUID, false) -// -// return conn -// } -// -// func (a *API) getNote(w http.ResponseWriter, r *http.Request) { -// user, _, err := middleware.AuthWithSession(a.App.DB, r) -// if err != nil { -// middleware.DoError(w, "authenticating", err, http.StatusInternalServerError) -// return -// } -// -// vars := mux.Vars(r) -// noteUUID := vars["noteUUID"] -// -// note, ok, err := operations.GetNote(a.App.DB, noteUUID, &user) -// if !ok { -// middleware.RespondNotFound(w) -// return -// } -// if err != nil { -// middleware.DoError(w, "finding note", err, http.StatusInternalServerError) -// return -// } -// -// respondWithNote(w, note) -// } -// -// /**** getNotesHandler */ -// -// // GetNotesResponse is a reponse by getNotesHandler -// type GetNotesResponse struct { -// Notes []presenters.Note `json:"notes"` -// Total int `json:"total"` -// } -// -// type dateRange struct { -// lower int64 -// upper int64 -// } -// -// func (a *API) getNotes(w http.ResponseWriter, r *http.Request) { -// user, ok := r.Context().Value(helpers.KeyUser).(database.User) -// if !ok { -// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError) -// return -// } -// query := r.URL.Query() -// -// respondGetNotes(a.App.DB, user.ID, query, w) -// } -// -// func respondGetNotes(db *gorm.DB, userID int, query url.Values, w http.ResponseWriter) { -// q, err := parseGetNotesQuery(query) -// if err != nil { -// http.Error(w, err.Error(), http.StatusBadRequest) -// return -// } -// -// conn := getNotesBaseQuery(db, userID, q) -// -// var total int -// if err := conn.Model(database.Note{}).Count(&total).Error; err != nil { -// middleware.DoError(w, "counting total", err, http.StatusInternalServerError) -// return -// } -// -// notes := []database.Note{} -// if total != 0 { -// conn = orderGetNotes(conn) -// conn = database.PreloadNote(conn) -// conn = paginate(conn, q.Page) -// -// if err := conn.Find(¬es).Error; err != nil { -// middleware.DoError(w, "finding notes", err, http.StatusInternalServerError) -// return -// } -// } -// -// response := GetNotesResponse{ -// Notes: presenters.PresentNotes(notes), -// Total: total, -// } -// middleware.RespondJSON(w, http.StatusOK, response) -// } -// -// type getNotesQuery struct { -// Year int -// Month int -// Page int -// Books []string -// Search string -// Encrypted bool -// } -// -// func parseGetNotesQuery(q url.Values) (getNotesQuery, error) { -// yearStr := q.Get("year") -// monthStr := q.Get("month") -// books := q["book"] -// pageStr := q.Get("page") -// encryptedStr := q.Get("encrypted") -// -// fmt.Println("books", books) -// -// var page int -// if len(pageStr) > 0 { -// p, err := strconv.Atoi(pageStr) -// if err != nil { -// return getNotesQuery{}, errors.Errorf("invalid page %s", pageStr) -// } -// if p < 1 { -// return getNotesQuery{}, errors.Errorf("invalid page %s", pageStr) -// } -// -// page = p -// } else { -// page = 1 -// } -// -// var year int -// if len(yearStr) > 0 { -// y, err := strconv.Atoi(yearStr) -// if err != nil { -// return getNotesQuery{}, errors.Errorf("invalid year %s", yearStr) -// } -// -// year = y -// } -// -// var month int -// if len(monthStr) > 0 { -// m, err := strconv.Atoi(monthStr) -// if err != nil { -// return getNotesQuery{}, errors.Errorf("invalid month %s", monthStr) -// } -// if m < 1 || m > 12 { -// return getNotesQuery{}, errors.Errorf("invalid month %s", monthStr) -// } -// -// month = m -// } -// -// var encrypted bool -// if strings.ToLower(encryptedStr) == "true" { -// encrypted = true -// } else { -// encrypted = false -// } -// -// ret := getNotesQuery{ -// Year: year, -// Month: month, -// Page: page, -// Search: parseSearchQuery(q), -// Books: books, -// Encrypted: encrypted, -// } -// -// return ret, nil -// } -// -// func getDateBounds(year, month int) (int64, int64) { -// var yearUpperbound, monthUpperbound int -// -// if month == 12 { -// monthUpperbound = 1 -// yearUpperbound = year + 1 -// } else { -// monthUpperbound = month + 1 -// yearUpperbound = year -// } -// -// lower := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC).UnixNano() -// upper := time.Date(yearUpperbound, time.Month(monthUpperbound), 1, 0, 0, 0, 0, time.UTC).UnixNano() -// -// return lower, upper -// } -// -// func getNotesBaseQuery(db *gorm.DB, userID int, q getNotesQuery) *gorm.DB { -// conn := db.Where( -// "notes.user_id = ? AND notes.deleted = ? AND notes.encrypted = ?", -// userID, false, q.Encrypted, -// ) -// -// if q.Search != "" { -// conn = selectFTSFields(conn, q.Search, nil) -// conn = conn.Where("tsv @@ plainto_tsquery('english_nostop', ?)", q.Search) -// } -// -// if len(q.Books) > 0 { -// conn = conn.Joins("INNER JOIN books ON books.uuid = notes.book_uuid"). -// Where("books.label in (?)", q.Books) -// } -// -// if q.Year != 0 || q.Month != 0 { -// dateLowerbound, dateUpperbound := getDateBounds(q.Year, q.Month) -// conn = conn.Where("notes.added_on >= ? AND notes.added_on < ?", dateLowerbound, dateUpperbound) -// } -// -// return conn -// } -// -// func orderGetNotes(conn *gorm.DB) *gorm.DB { -// return conn.Order("notes.updated_at DESC, notes.id DESC") -// } -// -// // escapeSearchQuery escapes the query for full text search -// func escapeSearchQuery(searchQuery string) string { -// return strings.Join(strings.Fields(searchQuery), "&") -// } -// -// func (a *API) legacyGetNotes(w http.ResponseWriter, r *http.Request) { -// user, ok := r.Context().Value(helpers.KeyUser).(database.User) -// if !ok { -// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError) -// return -// } -// -// var notes []database.Note -// if err := a.App.DB.Where("user_id = ? AND encrypted = true", user.ID).Find(¬es).Error; err != nil { -// middleware.DoError(w, "finding notes", err, http.StatusInternalServerError) -// return -// } -// -// presented := presenters.PresentNotes(notes) -// middleware.RespondJSON(w, http.StatusOK, presented) -// } diff --git a/pkg/server/api/notes_test.go b/pkg/server/api/notes_test.go deleted file mode 100644 index 93282607..00000000 --- a/pkg/server/api/notes_test.go +++ /dev/null @@ -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 . - */ - -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") -// }) -// } diff --git a/pkg/server/api/routes.go b/pkg/server/api/routes.go deleted file mode 100644 index 71e913db..00000000 --- a/pkg/server/api/routes.go +++ /dev/null @@ -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 . - */ - -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 -// } diff --git a/pkg/server/api/routes_test.go b/pkg/server/api/routes_test.go deleted file mode 100644 index 7b8f5ec2..00000000 --- a/pkg/server/api/routes_test.go +++ /dev/null @@ -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 . - */ - -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") - }) - } -} diff --git a/pkg/server/api/testutils.go b/pkg/server/api/testutils.go deleted file mode 100644 index e2541ab1..00000000 --- a/pkg/server/api/testutils.go +++ /dev/null @@ -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 . - */ - -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} -// } diff --git a/pkg/server/api/user.go b/pkg/server/api/user.go deleted file mode 100644 index 35b0c55c..00000000 --- a/pkg/server/api/user.go +++ /dev/null @@ -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 . - */ - -package api - -// import ( -// "encoding/json" -// "net/http" -// "time" -// -// "github.com/dnote/dnote/pkg/server/database" -// "github.com/dnote/dnote/pkg/server/middleware" -// "github.com/dnote/dnote/pkg/server/helpers" -// "github.com/dnote/dnote/pkg/server/log" -// "github.com/dnote/dnote/pkg/server/mailer" -// "github.com/dnote/dnote/pkg/server/presenters" -// "github.com/dnote/dnote/pkg/server/session" -// "github.com/dnote/dnote/pkg/server/token" -// "github.com/jinzhu/gorm" -// "github.com/pkg/errors" -// "golang.org/x/crypto/bcrypt" -// ) -// -// type updateProfilePayload struct { -// Email string `json:"email"` -// Password string `json:"password"` -// } -// -// // updateProfile updates user -// func (a *API) updateProfile(w http.ResponseWriter, r *http.Request) { -// user, ok := r.Context().Value(helpers.KeyUser).(database.User) -// if !ok { -// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError) -// return -// } -// -// var account database.Account -// if err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil { -// middleware.DoError(w, "getting account", nil, http.StatusInternalServerError) -// return -// } -// -// var params updateProfilePayload -// err := json.NewDecoder(r.Body).Decode(¶ms) -// if err != nil { -// http.Error(w, errors.Wrap(err, "invalid params").Error(), http.StatusBadRequest) -// return -// } -// -// password := []byte(params.Password) -// if err := bcrypt.CompareHashAndPassword([]byte(account.Password.String), password); err != nil { -// log.WithFields(log.Fields{ -// "user_id": user.ID, -// }).Warn("invalid email update attempt") -// http.Error(w, "Wrong password", http.StatusUnauthorized) -// return -// } -// -// // Validate -// if len(params.Email) > 60 { -// http.Error(w, "Email is too long", http.StatusBadRequest) -// return -// } -// -// tx := a.App.DB.Begin() -// if err := tx.Save(&user).Error; err != nil { -// tx.Rollback() -// middleware.DoError(w, "saving user", err, http.StatusInternalServerError) -// return -// } -// -// // check if email was changed -// if params.Email != account.Email.String { -// account.EmailVerified = false -// } -// account.Email.String = params.Email -// -// if err := tx.Save(&account).Error; err != nil { -// tx.Rollback() -// middleware.DoError(w, "saving account", err, http.StatusInternalServerError) -// return -// } -// -// tx.Commit() -// -// a.respondWithSession(a.App.DB, w, user.ID, http.StatusOK) -// } -// -// type updateEmailPayload struct { -// NewEmail string `json:"new_email"` -// NewCipherKeyEnc string `json:"new_cipher_key_enc"` -// OldAuthKey string `json:"old_auth_key"` -// NewAuthKey string `json:"new_auth_key"` -// } -// -// func respondWithCalendar(db *gorm.DB, w http.ResponseWriter, userID int) { -// rows, err := db.Table("notes").Select("COUNT(id), date(to_timestamp(added_on/1000000000)) AS added_date"). -// Where("user_id = ?", userID). -// Group("added_date"). -// Order("added_date DESC").Rows() -// -// if err != nil { -// middleware.DoError(w, "Failed to count lessons", err, http.StatusInternalServerError) -// return -// } -// -// payload := map[string]int{} -// -// for rows.Next() { -// var count int -// var d time.Time -// -// if err := rows.Scan(&count, &d); err != nil { -// middleware.DoError(w, "counting notes", err, http.StatusInternalServerError) -// } -// payload[d.Format("2006-1-2")] = count -// } -// -// middleware.RespondJSON(w, http.StatusOK, payload) -// } -// -// func (a *API) getCalendar(w http.ResponseWriter, r *http.Request) { -// user, ok := r.Context().Value(helpers.KeyUser).(database.User) -// if !ok { -// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError) -// return -// } -// -// respondWithCalendar(a.App.DB, w, user.ID) -// } -// -// func (a *API) createVerificationToken(w http.ResponseWriter, r *http.Request) { -// user, ok := r.Context().Value(helpers.KeyUser).(database.User) -// if !ok { -// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError) -// return -// } -// -// var account database.Account -// err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error -// if err != nil { -// middleware.DoError(w, "finding account", err, http.StatusInternalServerError) -// return -// } -// -// if account.EmailVerified { -// http.Error(w, "Email already verified", http.StatusGone) -// return -// } -// if account.Email.String == "" { -// http.Error(w, "Email not set", http.StatusUnprocessableEntity) -// return -// } -// -// tok, err := token.Create(a.App.DB, account.UserID, database.TokenTypeEmailVerification) -// if err != nil { -// middleware.DoError(w, "saving token", err, http.StatusInternalServerError) -// return -// } -// -// if err := a.App.SendVerificationEmail(account.Email.String, tok.Value); err != nil { -// if errors.Cause(err) == mailer.ErrSMTPNotConfigured { -// middleware.RespondInvalidSMTPConfig(w) -// } else { -// middleware.DoError(w, errors.Wrap(err, "sending verification email").Error(), nil, http.StatusInternalServerError) -// } -// -// return -// } -// -// w.WriteHeader(http.StatusCreated) -// } -// -// type verifyEmailPayload struct { -// Token string `json:"token"` -// } -// -// func (a *API) verifyEmail(w http.ResponseWriter, r *http.Request) { -// var params verifyEmailPayload -// if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { -// middleware.DoError(w, "decoding payload", err, http.StatusInternalServerError) -// return -// } -// -// var token database.Token -// if err := a.App.DB. -// Where("value = ? AND type = ?", params.Token, database.TokenTypeEmailVerification). -// First(&token).Error; err != nil { -// http.Error(w, "invalid token", http.StatusBadRequest) -// return -// } -// -// if token.UsedAt != nil { -// http.Error(w, "invalid token", http.StatusBadRequest) -// return -// } -// -// // Expire after ttl -// if time.Since(token.CreatedAt).Minutes() > 30 { -// http.Error(w, "This link has been expired. Please request a new link.", http.StatusGone) -// return -// } -// -// var account database.Account -// if err := a.App.DB.Where("user_id = ?", token.UserID).First(&account).Error; err != nil { -// middleware.DoError(w, "finding account", err, http.StatusInternalServerError) -// return -// } -// if account.EmailVerified { -// http.Error(w, "Already verified", http.StatusConflict) -// return -// } -// -// tx := a.App.DB.Begin() -// account.EmailVerified = true -// if err := tx.Save(&account).Error; err != nil { -// tx.Rollback() -// middleware.DoError(w, "updating email_verified", err, http.StatusInternalServerError) -// return -// } -// if err := tx.Model(&token).Update("used_at", time.Now()).Error; err != nil { -// tx.Rollback() -// middleware.DoError(w, "updating reset token", err, http.StatusInternalServerError) -// return -// } -// tx.Commit() -// -// var user database.User -// if err := a.App.DB.Where("id = ?", token.UserID).First(&user).Error; err != nil { -// middleware.DoError(w, "finding user", err, http.StatusInternalServerError) -// return -// } -// -// s := session.New(user, account) -// middleware.RespondJSON(w, http.StatusOK, s) -// } -// -// type emailPreferernceParams struct { -// InactiveReminder *bool `json:"inactive_reminder"` -// ProductUpdate *bool `json:"product_update"` -// } -// -// func (p emailPreferernceParams) getInactiveReminder() bool { -// if p.InactiveReminder == nil { -// return false -// } -// -// return *p.InactiveReminder -// } -// -// func (p emailPreferernceParams) getProductUpdate() bool { -// if p.ProductUpdate == nil { -// return false -// } -// -// return *p.ProductUpdate -// } -// -// func (a *API) updateEmailPreference(w http.ResponseWriter, r *http.Request) { -// user, ok := r.Context().Value(helpers.KeyUser).(database.User) -// if !ok { -// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError) -// return -// } -// -// var params emailPreferernceParams -// if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { -// middleware.DoError(w, "decoding payload", err, http.StatusInternalServerError) -// return -// } -// -// var pref database.EmailPreference -// if err := a.App.DB.Where(database.EmailPreference{UserID: user.ID}).FirstOrCreate(&pref).Error; err != nil { -// middleware.DoError(w, "finding pref", err, http.StatusInternalServerError) -// return -// } -// -// tx := a.App.DB.Begin() -// -// if params.InactiveReminder != nil { -// pref.InactiveReminder = params.getInactiveReminder() -// } -// if params.ProductUpdate != nil { -// pref.ProductUpdate = params.getProductUpdate() -// } -// -// if err := tx.Save(&pref).Error; err != nil { -// tx.Rollback() -// middleware.DoError(w, "saving pref", err, http.StatusInternalServerError) -// return -// } -// -// token, ok := r.Context().Value(helpers.KeyToken).(database.Token) -// if ok { -// // Mark token as used if the user was authenticated by token -// if err := tx.Model(&token).Update("used_at", time.Now()).Error; err != nil { -// tx.Rollback() -// middleware.DoError(w, "updating reset token", err, http.StatusInternalServerError) -// return -// } -// } -// -// tx.Commit() -// -// middleware.RespondJSON(w, http.StatusOK, pref) -// } -// -// func (a *API) getEmailPreference(w http.ResponseWriter, r *http.Request) { -// user, ok := r.Context().Value(helpers.KeyUser).(database.User) -// if !ok { -// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError) -// return -// } -// -// var pref database.EmailPreference -// if err := a.App.DB.Where(database.EmailPreference{UserID: user.ID}).First(&pref).Error; err != nil { -// middleware.DoError(w, "finding pref", err, http.StatusInternalServerError) -// return -// } -// -// presented := presenters.PresentEmailPreference(pref) -// middleware.RespondJSON(w, http.StatusOK, presented) -// } -// -// type updatePasswordPayload struct { -// OldPassword string `json:"old_password"` -// NewPassword string `json:"new_password"` -// } -// -// func (a *API) updatePassword(w http.ResponseWriter, r *http.Request) { -// user, ok := r.Context().Value(helpers.KeyUser).(database.User) -// if !ok { -// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError) -// return -// } -// -// var params updatePasswordPayload -// if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { -// http.Error(w, err.Error(), http.StatusBadRequest) -// return -// } -// if params.OldPassword == "" || params.NewPassword == "" { -// http.Error(w, "invalid params", http.StatusBadRequest) -// return -// } -// -// var account database.Account -// if err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil { -// middleware.DoError(w, "getting account", nil, http.StatusInternalServerError) -// return -// } -// -// password := []byte(params.OldPassword) -// if err := bcrypt.CompareHashAndPassword([]byte(account.Password.String), password); err != nil { -// log.WithFields(log.Fields{ -// "user_id": user.ID, -// }).Warn("invalid password update attempt") -// http.Error(w, "Wrong password", http.StatusUnauthorized) -// return -// } -// -// if err := validatePassword(params.NewPassword); err != nil { -// http.Error(w, err.Error(), http.StatusBadRequest) -// return -// } -// -// hashedNewPassword, err := bcrypt.GenerateFromPassword([]byte(params.NewPassword), bcrypt.DefaultCost) -// if err != nil { -// http.Error(w, errors.Wrap(err, "hashing password").Error(), http.StatusInternalServerError) -// return -// } -// -// if err := a.App.DB.Model(&account).Update("password", string(hashedNewPassword)).Error; err != nil { -// http.Error(w, errors.Wrap(err, "updating password").Error(), http.StatusInternalServerError) -// return -// } -// -// w.WriteHeader(http.StatusOK) -// } diff --git a/pkg/server/api/user_test.go b/pkg/server/api/user_test.go deleted file mode 100644 index c2135688..00000000 --- a/pkg/server/api/user_test.go +++ /dev/null @@ -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 . - */ - -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") -// } diff --git a/pkg/server/api/v3_auth.go b/pkg/server/api/v3_auth.go deleted file mode 100644 index 580f82e1..00000000 --- a/pkg/server/api/v3_auth.go +++ /dev/null @@ -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 . - */ - -package api - -import ( - "encoding/json" - "net/http" - "time" - - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/handlers" - "github.com/dnote/dnote/pkg/server/log" - "github.com/jinzhu/gorm" - "github.com/pkg/errors" - "golang.org/x/crypto/bcrypt" -) - -// ErrLoginFailure is an error for failed login -var ErrLoginFailure = errors.New("Wrong email and password combination") - -// SessionResponse is a response containing a session information -type SessionResponse struct { - Key string `json:"key"` - ExpiresAt int64 `json:"expires_at"` -} - -func setSessionCookie(w http.ResponseWriter, key string, expires time.Time) { - cookie := http.Cookie{ - Name: "id", - Value: key, - Expires: expires, - Path: "/", - HttpOnly: true, - } - http.SetCookie(w, &cookie) -} - -func touchLastLoginAt(db *gorm.DB, user database.User) error { - t := time.Now() - if err := db.Model(&user).Update(database.User{LastLoginAt: &t}).Error; err != nil { - return errors.Wrap(err, "updating last_login_at") - } - - return nil -} - -type signinPayload struct { - Email string `json:"email"` - Password string `json:"password"` -} - -func (a *API) signin(w http.ResponseWriter, r *http.Request) { - var params signinPayload - err := json.NewDecoder(r.Body).Decode(¶ms) - if err != nil { - handlers.DoError(w, "decoding payload", err, http.StatusInternalServerError) - return - } - if params.Email == "" || params.Password == "" { - http.Error(w, ErrLoginFailure.Error(), http.StatusUnauthorized) - return - } - - var account database.Account - conn := a.App.DB.Where("email = ?", params.Email).First(&account) - if conn.RecordNotFound() { - http.Error(w, ErrLoginFailure.Error(), http.StatusUnauthorized) - return - } else if conn.Error != nil { - handlers.DoError(w, "getting user", err, http.StatusInternalServerError) - return - } - - password := []byte(params.Password) - err = bcrypt.CompareHashAndPassword([]byte(account.Password.String), password) - if err != nil { - http.Error(w, ErrLoginFailure.Error(), http.StatusUnauthorized) - return - } - - var user database.User - err = a.App.DB.Where("id = ?", account.UserID).First(&user).Error - if err != nil { - handlers.DoError(w, "finding user", err, http.StatusInternalServerError) - return - } - - err = a.App.TouchLastLoginAt(user, a.App.DB) - if err != nil { - http.Error(w, errors.Wrap(err, "touching login timestamp").Error(), http.StatusInternalServerError) - return - } - - a.respondWithSession(a.App.DB, w, account.UserID, http.StatusOK) -} - -func (a *API) signoutOptions(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Methods", "POST") - w.Header().Set("Access-Control-Allow-Headers", "Authorization, Version") -} - -func (a *API) signout(w http.ResponseWriter, r *http.Request) { - key, err := handlers.GetCredential(r) - if err != nil { - handlers.DoError(w, "getting credential", nil, http.StatusInternalServerError) - return - } - - if key == "" { - w.WriteHeader(http.StatusNoContent) - return - } - - err = a.App.DeleteSession(key) - if err != nil { - handlers.DoError(w, "deleting session", nil, http.StatusInternalServerError) - return - } - - handlers.UnsetSessionCookie(w) - w.WriteHeader(http.StatusNoContent) -} - -type registerPayload struct { - Email string `json:"email"` - Password string `json:"password"` -} - -func validateRegisterPayload(p registerPayload) error { - if p.Email == "" { - return errors.New("email is required") - } - if len(p.Password) < 8 { - return errors.New("Password should be longer than 8 characters") - } - - return nil -} - -func parseRegisterPaylaod(r *http.Request) (registerPayload, error) { - var ret registerPayload - if err := json.NewDecoder(r.Body).Decode(&ret); err != nil { - return ret, errors.Wrap(err, "decoding json") - } - - return ret, nil -} - -func (a *API) register(w http.ResponseWriter, r *http.Request) { - if a.App.Config.DisableRegistration { - handlers.RespondForbidden(w) - return - } - - params, err := parseRegisterPaylaod(r) - if err != nil { - http.Error(w, "invalid payload", http.StatusBadRequest) - return - } - if err := validateRegisterPayload(params); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - var count int - if err := a.App.DB.Model(database.Account{}).Where("email = ?", params.Email).Count(&count).Error; err != nil { - handlers.DoError(w, "checking duplicate user", err, http.StatusInternalServerError) - return - } - if count > 0 { - http.Error(w, "Duplicate email", http.StatusBadRequest) - return - } - - user, err := a.App.CreateUser(params.Email, params.Password) - if err != nil { - handlers.DoError(w, "creating user", err, http.StatusInternalServerError) - return - } - - a.respondWithSession(a.App.DB, w, user.ID, http.StatusCreated) - - if err := a.App.SendWelcomeEmail(params.Email); err != nil { - log.ErrorWrap(err, "sending welcome email") - } -} - -// respondWithSession makes a HTTP response with the session from the user with the given userID. -// It sets the HTTP-Only cookie for browser clients and also sends a JSON response for non-browser clients. -func (a *API) respondWithSession(db *gorm.DB, w http.ResponseWriter, userID int, statusCode int) { - session, err := a.App.CreateSession(userID) - if err != nil { - handlers.DoError(w, "creating session", nil, http.StatusBadRequest) - return - } - - setSessionCookie(w, session.Key, session.ExpiresAt) - - response := SessionResponse{ - Key: session.Key, - ExpiresAt: session.ExpiresAt.Unix(), - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - if err := json.NewEncoder(w).Encode(response); err != nil { - handlers.DoError(w, "encoding response", err, http.StatusInternalServerError) - return - } -} diff --git a/pkg/server/api/v3_auth_test.go b/pkg/server/api/v3_auth_test.go deleted file mode 100644 index 2aa4761e..00000000 --- a/pkg/server/api/v3_auth_test.go +++ /dev/null @@ -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 . - */ - -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") - }) -} diff --git a/pkg/server/api/v3_books.go b/pkg/server/api/v3_books.go deleted file mode 100644 index 34985bb5..00000000 --- a/pkg/server/api/v3_books.go +++ /dev/null @@ -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 . - */ - -package api - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/handlers" - "github.com/dnote/dnote/pkg/server/helpers" - "github.com/dnote/dnote/pkg/server/presenters" - "github.com/gorilla/mux" - "github.com/jinzhu/gorm" - "github.com/pkg/errors" -) - -type createBookPayload struct { - Name string `json:"name"` -} - -// CreateBookResp is the response from create book api -type CreateBookResp struct { - Book presenters.Book `json:"book"` -} - -func validateCreateBookPayload(p createBookPayload) error { - if p.Name == "" { - return errors.New("name is required") - } - - return nil -} - -// CreateBook creates a new book -func (a *API) CreateBook(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - return - } - - var params createBookPayload - err := json.NewDecoder(r.Body).Decode(¶ms) - if err != nil { - handlers.DoError(w, "decoding payload", err, http.StatusInternalServerError) - return - } - - err = validateCreateBookPayload(params) - if err != nil { - handlers.DoError(w, "validating payload", err, http.StatusBadRequest) - return - } - - var bookCount int - err = a.App.DB.Model(database.Book{}). - Where("user_id = ? AND label = ?", user.ID, params.Name). - Count(&bookCount).Error - if err != nil { - handlers.DoError(w, "checking duplicate", err, http.StatusInternalServerError) - return - } - if bookCount > 0 { - http.Error(w, "duplicate book exists", http.StatusConflict) - return - } - - book, err := a.App.CreateBook(user, params.Name) - if err != nil { - handlers.DoError(w, "inserting book", err, http.StatusInternalServerError) - } - resp := CreateBookResp{ - Book: presenters.PresentBook(book), - } - handlers.RespondJSON(w, http.StatusCreated, resp) -} - -// BooksOptions is a handler for OPTIONS endpoint for notes -func (a *API) BooksOptions(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Methods", "GET, POST") - w.Header().Set("Access-Control-Allow-Headers", "Authorization, Version") -} - -func respondWithBooks(db *gorm.DB, userID int, query url.Values, w http.ResponseWriter) { - var books []database.Book - conn := db.Where("user_id = ? AND NOT deleted", userID).Order("label ASC") - name := query.Get("name") - encryptedStr := query.Get("encrypted") - - if name != "" { - part := fmt.Sprintf("%%%s%%", name) - conn = conn.Where("LOWER(label) LIKE ?", part) - } - if encryptedStr != "" { - var encrypted bool - if encryptedStr == "true" { - encrypted = true - } else { - encrypted = false - } - - conn = conn.Where("encrypted = ?", encrypted) - } - - if err := conn.Find(&books).Error; err != nil { - handlers.DoError(w, "finding books", err, http.StatusInternalServerError) - return - } - - presentedBooks := presenters.PresentBooks(books) - handlers.RespondJSON(w, http.StatusOK, presentedBooks) -} - -// GetBooks returns books for the user -func (a *API) GetBooks(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - return - } - - query := r.URL.Query() - - respondWithBooks(a.App.DB, user.ID, query, w) -} - -// GetBook returns a book for the user -func (a *API) GetBook(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - return - } - - vars := mux.Vars(r) - bookUUID := vars["bookUUID"] - - var book database.Book - conn := a.App.DB.Where("uuid = ? AND user_id = ?", bookUUID, user.ID).First(&book) - - if conn.RecordNotFound() { - w.WriteHeader(http.StatusNotFound) - return - } - if err := conn.Error; err != nil { - handlers.DoError(w, "finding book", err, http.StatusInternalServerError) - return - } - - p := presenters.PresentBook(book) - handlers.RespondJSON(w, http.StatusOK, p) -} - -type updateBookPayload struct { - Name *string `json:"name"` -} - -// UpdateBookResp is the response from create book api -type UpdateBookResp struct { - Book presenters.Book `json:"book"` -} - -// UpdateBook updates a book -func (a *API) UpdateBook(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - return - } - - vars := mux.Vars(r) - uuid := vars["bookUUID"] - - tx := a.App.DB.Begin() - - var book database.Book - if err := tx.Where("user_id = ? AND uuid = ?", user.ID, uuid).First(&book).Error; err != nil { - handlers.DoError(w, "finding book", err, http.StatusInternalServerError) - return - } - - var params updateBookPayload - err := json.NewDecoder(r.Body).Decode(¶ms) - if err != nil { - handlers.DoError(w, "decoding payload", err, http.StatusInternalServerError) - return - } - - book, err = a.App.UpdateBook(tx, user, book, params.Name) - if err != nil { - tx.Rollback() - handlers.DoError(w, "updating a book", err, http.StatusInternalServerError) - } - - tx.Commit() - - resp := UpdateBookResp{ - Book: presenters.PresentBook(book), - } - handlers.RespondJSON(w, http.StatusOK, resp) -} - -// DeleteBookResp is the response from create book api -type DeleteBookResp struct { - Status int `json:"status"` - Book presenters.Book `json:"book"` -} - -// DeleteBook removes a book -func (a *API) DeleteBook(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - return - } - - vars := mux.Vars(r) - uuid := vars["bookUUID"] - - tx := a.App.DB.Begin() - - var book database.Book - if err := tx.Where("user_id = ? AND uuid = ?", user.ID, uuid).First(&book).Error; err != nil { - handlers.DoError(w, "finding book", err, http.StatusInternalServerError) - return - } - - var notes []database.Note - if err := tx.Where("book_uuid = ? AND NOT deleted", uuid).Order("usn ASC").Find(¬es).Error; err != nil { - handlers.DoError(w, "finding notes", err, http.StatusInternalServerError) - return - } - - for _, note := range notes { - if _, err := a.App.DeleteNote(tx, user, note); err != nil { - handlers.DoError(w, "deleting a note", err, http.StatusInternalServerError) - return - } - } - b, err := a.App.DeleteBook(tx, user, book) - if err != nil { - handlers.DoError(w, "deleting book", err, http.StatusInternalServerError) - return - } - - tx.Commit() - - resp := DeleteBookResp{ - Status: http.StatusOK, - Book: presenters.PresentBook(b), - } - handlers.RespondJSON(w, http.StatusOK, resp) -} diff --git a/pkg/server/api/v3_notes.go b/pkg/server/api/v3_notes.go deleted file mode 100644 index c38ced5a..00000000 --- a/pkg/server/api/v3_notes.go +++ /dev/null @@ -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 . - */ - -package api - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/dnote/dnote/pkg/server/app" - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/handlers" - "github.com/dnote/dnote/pkg/server/helpers" - "github.com/dnote/dnote/pkg/server/presenters" - "github.com/gorilla/mux" - "github.com/pkg/errors" -) - -type updateNotePayload struct { - BookUUID *string `json:"book_uuid"` - Content *string `json:"content"` - Public *bool `json:"public"` -} - -type updateNoteResp struct { - Status int `json:"status"` - Result presenters.Note `json:"result"` -} - -func validateUpdateNotePayload(p updateNotePayload) bool { - return p.BookUUID != nil || p.Content != nil || p.Public != nil -} - -// UpdateNote updates note -func (a *API) UpdateNote(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - noteUUID := vars["noteUUID"] - - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - handlers.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError) - return - } - - var params updateNotePayload - err := json.NewDecoder(r.Body).Decode(¶ms) - if err != nil { - handlers.DoError(w, "decoding params", err, http.StatusInternalServerError) - return - } - - if ok := validateUpdateNotePayload(params); !ok { - handlers.DoError(w, "Invalid payload", nil, http.StatusBadRequest) - return - } - - var note database.Note - if err := a.App.DB.Where("uuid = ? AND user_id = ?", noteUUID, user.ID).First(¬e).Error; err != nil { - handlers.DoError(w, "finding note", err, http.StatusInternalServerError) - return - } - - tx := a.App.DB.Begin() - - note, err = a.App.UpdateNote(tx, user, note, &app.UpdateNoteParams{ - BookUUID: params.BookUUID, - Content: params.Content, - Public: params.Public, - }) - if err != nil { - tx.Rollback() - handlers.DoError(w, "updating note", err, http.StatusInternalServerError) - return - } - - var book database.Book - if err := tx.Where("uuid = ? AND user_id = ?", note.BookUUID, user.ID).First(&book).Error; err != nil { - tx.Rollback() - handlers.DoError(w, fmt.Sprintf("finding book %s to preload", note.BookUUID), err, http.StatusInternalServerError) - return - } - - tx.Commit() - - // preload associations - note.User = user - note.Book = book - - resp := updateNoteResp{ - Status: http.StatusOK, - Result: presenters.PresentNote(note), - } - handlers.RespondJSON(w, http.StatusOK, resp) -} - -type deleteNoteResp struct { - Status int `json:"status"` - Result presenters.Note `json:"result"` -} - -// DeleteNote removes note -func (a *API) DeleteNote(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - noteUUID := vars["noteUUID"] - - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - handlers.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError) - return - } - - var note database.Note - if err := a.App.DB.Where("uuid = ? AND user_id = ?", noteUUID, user.ID).Preload("Book").First(¬e).Error; err != nil { - handlers.DoError(w, "finding note", err, http.StatusInternalServerError) - return - } - - tx := a.App.DB.Begin() - - n, err := a.App.DeleteNote(tx, user, note) - if err != nil { - tx.Rollback() - handlers.DoError(w, "deleting note", err, http.StatusInternalServerError) - return - } - - tx.Commit() - - resp := deleteNoteResp{ - Status: http.StatusNoContent, - Result: presenters.PresentNote(n), - } - handlers.RespondJSON(w, http.StatusOK, resp) -} - -type createNotePayload struct { - BookUUID string `json:"book_uuid"` - Content string `json:"content"` - AddedOn *int64 `json:"added_on"` - EditedOn *int64 `json:"edited_on"` -} - -func validateCreateNotePayload(p createNotePayload) error { - if p.BookUUID == "" { - return errors.New("bookUUID is required") - } - - return nil -} - -// CreateNoteResp is a response for creating a note -type CreateNoteResp struct { - Result presenters.Note `json:"result"` -} - -// CreateNote creates a note -func (a *API) CreateNote(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(helpers.KeyUser).(database.User) - if !ok { - handlers.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError) - return - } - - var params createNotePayload - err := json.NewDecoder(r.Body).Decode(¶ms) - if err != nil { - handlers.DoError(w, "decoding payload", err, http.StatusInternalServerError) - return - } - - err = validateCreateNotePayload(params) - if err != nil { - handlers.DoError(w, "validating payload", err, http.StatusBadRequest) - return - } - - var book database.Book - if err := a.App.DB.Where("uuid = ? AND user_id = ?", params.BookUUID, user.ID).First(&book).Error; err != nil { - handlers.DoError(w, "finding book", err, http.StatusInternalServerError) - return - } - - client := getClientType(r) - note, err := a.App.CreateNote(user, params.BookUUID, params.Content, params.AddedOn, params.EditedOn, false, client) - if err != nil { - handlers.DoError(w, "creating note", err, http.StatusInternalServerError) - return - } - - // preload associations - note.User = user - note.Book = book - - resp := CreateNoteResp{ - Result: presenters.PresentNote(note), - } - handlers.RespondJSON(w, http.StatusCreated, resp) -} - -// NotesOptions is a handler for OPTIONS endpoint for notes -func (a *API) NotesOptions(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Methods", "POST") - w.Header().Set("Access-Control-Allow-Headers", "Authorization, Version") -} diff --git a/pkg/server/api/v3_notes_test.go b/pkg/server/api/v3_notes_test.go deleted file mode 100644 index df30beed..00000000 --- a/pkg/server/api/v3_notes_test.go +++ /dev/null @@ -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 . - */ - -package api - -import ( - "fmt" - "net/http" - "testing" - - "github.com/dnote/dnote/pkg/assert" - "github.com/dnote/dnote/pkg/clock" - "github.com/dnote/dnote/pkg/server/app" - "github.com/dnote/dnote/pkg/server/database" - "github.com/dnote/dnote/pkg/server/testutils" -) - -func TestCreateNote(t *testing.T) { - - defer testutils.ClearData(testutils.DB) - - // Setup - server := MustNewServer(t, &app.App{ - - Clock: clock.NewMock(), - }) - defer server.Close() - - user := testutils.SetupUserData() - testutils.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 101), "preparing user max_usn") - - b1 := database.Book{ - UserID: user.ID, - Label: "js", - USN: 58, - } - testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1") - - // Execute - dat := fmt.Sprintf(`{"book_uuid": "%s", "content": "note content"}`, b1.UUID) - req := testutils.MakeReq(server.URL, "POST", "/v3/notes", dat) - res := testutils.HTTPAuthDo(t, req, user) - - // Test - assert.StatusCodeEquals(t, res, http.StatusCreated, "") - - var noteRecord database.Note - var bookRecord database.Book - var userRecord database.User - var bookCount, noteCount int - testutils.MustExec(t, testutils.DB.Model(&database.Book{}).Count(&bookCount), "counting books") - testutils.MustExec(t, testutils.DB.Model(&database.Note{}).Count(¬eCount), "counting notes") - testutils.MustExec(t, testutils.DB.First(¬eRecord), "finding note") - testutils.MustExec(t, testutils.DB.Where("id = ?", b1.ID).First(&bookRecord), "finding book") - testutils.MustExec(t, testutils.DB.Where("id = ?", user.ID).First(&userRecord), "finding user record") - - assert.Equalf(t, bookCount, 1, "book count mismatch") - assert.Equalf(t, noteCount, 1, "note count mismatch") - - assert.Equal(t, bookRecord.Label, b1.Label, "book name mismatch") - assert.Equal(t, bookRecord.UUID, b1.UUID, "book uuid mismatch") - assert.Equal(t, bookRecord.UserID, b1.UserID, "book user_id mismatch") - assert.Equal(t, bookRecord.USN, 58, "book usn mismatch") - - assert.NotEqual(t, noteRecord.UUID, "", "note uuid should have been generated") - assert.Equal(t, noteRecord.BookUUID, b1.UUID, "note book_uuid mismatch") - assert.Equal(t, noteRecord.Body, "note content", "note content mismatch") - assert.Equal(t, noteRecord.USN, 102, "note usn mismatch") -} - -func TestUpdateNote(t *testing.T) { - updatedBody := "some updated content" - - b1UUID := "37868a8e-a844-4265-9a4f-0be598084733" - b2UUID := "8f3bd424-6aa5-4ed5-910d-e5b38ab09f8c" - - testCases := []struct { - payload string - noteUUID string - noteBookUUID string - noteBody string - notePublic bool - noteDeleted bool - expectedNoteBody string - expectedNoteBookName string - expectedNoteBookUUID string - expectedNotePublic bool - }{ - { - payload: fmt.Sprintf(`{ - "content": "%s" - }`, updatedBody), - noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053", - noteBookUUID: b1UUID, - notePublic: false, - noteBody: "original content", - noteDeleted: false, - expectedNoteBookUUID: b1UUID, - expectedNoteBody: "some updated content", - expectedNoteBookName: "css", - expectedNotePublic: false, - }, - { - payload: fmt.Sprintf(`{ - "book_uuid": "%s" - }`, b1UUID), - noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053", - noteBookUUID: b1UUID, - notePublic: false, - noteBody: "original content", - noteDeleted: false, - expectedNoteBookUUID: b1UUID, - expectedNoteBody: "original content", - expectedNoteBookName: "css", - expectedNotePublic: false, - }, - { - payload: fmt.Sprintf(`{ - "book_uuid": "%s" - }`, b2UUID), - noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053", - noteBookUUID: b1UUID, - notePublic: false, - noteBody: "original content", - noteDeleted: false, - expectedNoteBookUUID: b2UUID, - expectedNoteBody: "original content", - expectedNoteBookName: "js", - expectedNotePublic: false, - }, - { - payload: fmt.Sprintf(`{ - "book_uuid": "%s", - "content": "%s" - }`, b2UUID, updatedBody), - noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053", - noteBookUUID: b1UUID, - notePublic: false, - noteBody: "original content", - noteDeleted: false, - expectedNoteBookUUID: b2UUID, - expectedNoteBody: "some updated content", - expectedNoteBookName: "js", - expectedNotePublic: false, - }, - { - payload: fmt.Sprintf(`{ - "book_uuid": "%s", - "content": "%s" - }`, b1UUID, updatedBody), - noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053", - noteBookUUID: b1UUID, - notePublic: false, - noteBody: "", - noteDeleted: true, - expectedNoteBookUUID: b1UUID, - expectedNoteBody: updatedBody, - expectedNoteBookName: "js", - expectedNotePublic: false, - }, - { - payload: fmt.Sprintf(`{ - "public": %t - }`, true), - noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053", - noteBookUUID: b1UUID, - notePublic: false, - noteBody: "original content", - noteDeleted: false, - expectedNoteBookUUID: b1UUID, - expectedNoteBody: "original content", - expectedNoteBookName: "css", - expectedNotePublic: true, - }, - { - payload: fmt.Sprintf(`{ - "public": %t - }`, false), - noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053", - noteBookUUID: b1UUID, - notePublic: true, - noteBody: "original content", - noteDeleted: false, - expectedNoteBookUUID: b1UUID, - expectedNoteBody: "original content", - expectedNoteBookName: "css", - expectedNotePublic: false, - }, - { - payload: fmt.Sprintf(`{ - "content": "%s", - "public": %t - }`, updatedBody, false), - noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053", - noteBookUUID: b1UUID, - notePublic: true, - noteBody: "original content", - noteDeleted: false, - expectedNoteBookUUID: b1UUID, - expectedNoteBody: updatedBody, - expectedNoteBookName: "css", - expectedNotePublic: false, - }, - { - payload: fmt.Sprintf(`{ - "book_uuid": "%s", - "content": "%s", - "public": %t - }`, b2UUID, updatedBody, true), - noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053", - noteBookUUID: b1UUID, - notePublic: false, - noteBody: "original content", - noteDeleted: false, - expectedNoteBookUUID: b2UUID, - expectedNoteBody: updatedBody, - expectedNoteBookName: "js", - expectedNotePublic: true, - }, - } - - for idx, tc := range testCases { - t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) { - - defer testutils.ClearData(testutils.DB) - - // Setup - server := MustNewServer(t, &app.App{ - - Clock: clock.NewMock(), - }) - defer server.Close() - - user := testutils.SetupUserData() - testutils.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 101), "preparing user max_usn") - - b1 := database.Book{ - UUID: b1UUID, - UserID: user.ID, - Label: "css", - } - testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1") - b2 := database.Book{ - UUID: b2UUID, - UserID: user.ID, - Label: "js", - } - testutils.MustExec(t, testutils.DB.Save(&b2), "preparing b2") - - note := database.Note{ - UserID: user.ID, - UUID: tc.noteUUID, - BookUUID: tc.noteBookUUID, - Body: tc.noteBody, - Deleted: tc.noteDeleted, - Public: tc.notePublic, - } - testutils.MustExec(t, testutils.DB.Save(¬e), "preparing note") - - // Execute - endpoint := fmt.Sprintf("/v3/notes/%s", note.UUID) - req := testutils.MakeReq(server.URL, "PATCH", endpoint, tc.payload) - res := testutils.HTTPAuthDo(t, req, user) - - // Test - assert.StatusCodeEquals(t, res, http.StatusOK, "status code mismatch for test case") - - var bookRecord database.Book - var noteRecord database.Note - var userRecord database.User - var noteCount, bookCount int - testutils.MustExec(t, testutils.DB.Model(&database.Book{}).Count(&bookCount), "counting books") - testutils.MustExec(t, testutils.DB.Model(&database.Note{}).Count(¬eCount), "counting notes") - testutils.MustExec(t, testutils.DB.Where("uuid = ?", note.UUID).First(¬eRecord), "finding note") - testutils.MustExec(t, testutils.DB.Where("id = ?", b1.ID).First(&bookRecord), "finding book") - testutils.MustExec(t, testutils.DB.Where("id = ?", user.ID).First(&userRecord), "finding user record") - - assert.Equalf(t, bookCount, 2, "book count mismatch") - assert.Equalf(t, noteCount, 1, "note count mismatch") - - assert.Equal(t, noteRecord.UUID, tc.noteUUID, "note uuid mismatch for test case") - assert.Equal(t, noteRecord.Body, tc.expectedNoteBody, "note content mismatch for test case") - assert.Equal(t, noteRecord.BookUUID, tc.expectedNoteBookUUID, "note book_uuid mismatch for test case") - assert.Equal(t, noteRecord.Public, tc.expectedNotePublic, "note public mismatch for test case") - assert.Equal(t, noteRecord.USN, 102, "note usn mismatch for test case") - - assert.Equal(t, userRecord.MaxUSN, 102, "user max_usn mismatch for test case") - }) - } -} - -func TestDeleteNote(t *testing.T) { - b1UUID := "37868a8e-a844-4265-9a4f-0be598084733" - - testCases := []struct { - content string - deleted bool - originalUSN int - expectedUSN int - expectedMaxUSN int - }{ - { - content: "n1 content", - deleted: false, - originalUSN: 12, - expectedUSN: 982, - expectedMaxUSN: 982, - }, - { - content: "", - deleted: true, - originalUSN: 12, - expectedUSN: 982, - expectedMaxUSN: 982, - }, - } - - for _, tc := range testCases { - t.Run(fmt.Sprintf("originally deleted %t", tc.deleted), func(t *testing.T) { - - defer testutils.ClearData(testutils.DB) - - // Setup - server := MustNewServer(t, &app.App{ - - Clock: clock.NewMock(), - }) - defer server.Close() - - user := testutils.SetupUserData() - testutils.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 981), "preparing user max_usn") - - b1 := database.Book{ - UUID: b1UUID, - UserID: user.ID, - Label: "js", - } - testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1") - note := database.Note{ - UserID: user.ID, - BookUUID: b1.UUID, - Body: tc.content, - Deleted: tc.deleted, - USN: tc.originalUSN, - } - testutils.MustExec(t, testutils.DB.Save(¬e), "preparing note") - - // Execute - endpoint := fmt.Sprintf("/v3/notes/%s", note.UUID) - req := testutils.MakeReq(server.URL, "DELETE", endpoint, "") - res := testutils.HTTPAuthDo(t, req, user) - - // Test - assert.StatusCodeEquals(t, res, http.StatusOK, "") - - var bookRecord database.Book - var noteRecord database.Note - var userRecord database.User - var bookCount, noteCount int - testutils.MustExec(t, testutils.DB.Model(&database.Book{}).Count(&bookCount), "counting books") - testutils.MustExec(t, testutils.DB.Model(&database.Note{}).Count(¬eCount), "counting notes") - testutils.MustExec(t, testutils.DB.Where("uuid = ?", note.UUID).First(¬eRecord), "finding note") - testutils.MustExec(t, testutils.DB.Where("id = ?", b1.ID).First(&bookRecord), "finding book") - testutils.MustExec(t, testutils.DB.Where("id = ?", user.ID).First(&userRecord), "finding user record") - - assert.Equalf(t, bookCount, 1, "book count mismatch") - assert.Equalf(t, noteCount, 1, "note count mismatch") - - assert.Equal(t, noteRecord.UUID, note.UUID, "note uuid mismatch for test case") - assert.Equal(t, noteRecord.Body, "", "note content mismatch for test case") - assert.Equal(t, noteRecord.Deleted, true, "note deleted mismatch for test case") - assert.Equal(t, noteRecord.BookUUID, note.BookUUID, "note book_uuid mismatch for test case") - assert.Equal(t, noteRecord.UserID, note.UserID, "note user_id mismatch for test case") - assert.Equal(t, noteRecord.USN, tc.expectedUSN, "note usn mismatch for test case") - - assert.Equal(t, userRecord.MaxUSN, tc.expectedMaxUSN, "user max_usn mismatch for test case") - }) - } -} diff --git a/pkg/server/controllers/books_test.go b/pkg/server/controllers/books_test.go index 6c8525ef..d60db1e5 100644 --- a/pkg/server/controllers/books_test.go +++ b/pkg/server/controllers/books_test.go @@ -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) diff --git a/pkg/server/controllers/notes_test.go b/pkg/server/controllers/notes_test.go index 73411f5c..c6fa1ffe 100644 --- a/pkg/server/controllers/notes_test.go +++ b/pkg/server/controllers/notes_test.go @@ -23,7 +23,6 @@ import ( "fmt" "io/ioutil" "net/http" - "net/url" "testing" "time" @@ -57,127 +56,120 @@ func getExpectedNotePayload(n database.Note, b database.Book, u database.User) p } func TestGetNotes(t *testing.T) { - testutils.RunForWebAndAPI(t, "get notes", func(t *testing.T, target testutils.EndpointType) { - defer testutils.ClearData(testutils.DB) + defer testutils.ClearData(testutils.DB) - // Setup - server := MustNewServer(t, &app.App{ - Clock: clock.NewMock(), - Config: config.Config{ - PageTemplateDir: "../views", - }, - }) - defer server.Close() - - user := testutils.SetupUserData() - anotherUser := testutils.SetupUserData() - - b1 := database.Book{ - UserID: user.ID, - Label: "js", - } - testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1") - b2 := database.Book{ - UserID: user.ID, - Label: "css", - } - testutils.MustExec(t, testutils.DB.Save(&b2), "preparing b2") - b3 := database.Book{ - UserID: anotherUser.ID, - Label: "css", - } - testutils.MustExec(t, testutils.DB.Save(&b3), "preparing b3") - - n1 := database.Note{ - UserID: user.ID, - BookUUID: b1.UUID, - Body: "n1 content", - USN: 11, - Deleted: false, - AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(), - } - testutils.MustExec(t, testutils.DB.Save(&n1), "preparing n1") - n2 := database.Note{ - UserID: user.ID, - BookUUID: b1.UUID, - Body: "n2 content", - USN: 14, - Deleted: false, - AddedOn: time.Date(2018, time.August, 11, 22, 0, 0, 0, time.UTC).UnixNano(), - } - testutils.MustExec(t, testutils.DB.Save(&n2), "preparing n2") - n3 := database.Note{ - UserID: user.ID, - BookUUID: b1.UUID, - Body: "n3 content", - USN: 17, - Deleted: false, - AddedOn: time.Date(2017, time.January, 10, 23, 0, 0, 0, time.UTC).UnixNano(), - } - testutils.MustExec(t, testutils.DB.Save(&n3), "preparing n3") - n4 := database.Note{ - UserID: user.ID, - BookUUID: b2.UUID, - Body: "n4 content", - USN: 18, - Deleted: false, - AddedOn: time.Date(2018, time.September, 10, 23, 0, 0, 0, time.UTC).UnixNano(), - } - testutils.MustExec(t, testutils.DB.Save(&n4), "preparing n4") - n5 := database.Note{ - UserID: anotherUser.ID, - BookUUID: b3.UUID, - Body: "n5 content", - USN: 19, - Deleted: false, - AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(), - } - testutils.MustExec(t, testutils.DB.Save(&n5), "preparing n5") - n6 := database.Note{ - UserID: user.ID, - BookUUID: b1.UUID, - Body: "", - USN: 11, - Deleted: true, - AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(), - } - testutils.MustExec(t, testutils.DB.Save(&n6), "preparing n6") - - // Execute - var endpoint string - if target == testutils.EndpointWeb { - endpoint = "/" - } else { - endpoint = "/api/v3/notes" - } - - req := testutils.MakeReq(server.URL, "GET", fmt.Sprintf("%s?year=2018&month=8", endpoint), "") - res := testutils.HTTPAuthDo(t, req, user) - - // Test - assert.StatusCodeEquals(t, res, http.StatusOK, "") - - if target == testutils.EndpointAPI { - var payload GetNotesResponse - if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { - t.Fatal(errors.Wrap(err, "decoding payload")) - } - - var n2Record, n1Record database.Note - testutils.MustExec(t, testutils.DB.Where("uuid = ?", n2.UUID).First(&n2Record), "finding n2Record") - testutils.MustExec(t, testutils.DB.Where("uuid = ?", n1.UUID).First(&n1Record), "finding n1Record") - - expected := GetNotesResponse{ - Notes: []presenters.Note{ - getExpectedNotePayload(n2Record, b1, user), - getExpectedNotePayload(n1Record, b1, user), - }, - Total: 2, - } - - assert.DeepEqual(t, payload, expected, "payload mismatch") - } + // Setup + server := MustNewServer(t, &app.App{ + Clock: clock.NewMock(), + Config: config.Config{ + PageTemplateDir: "../views", + }, }) + defer server.Close() + + user := testutils.SetupUserData() + testutils.SetupAccountData(user, "alice@test.com", "pass1234") + anotherUser := testutils.SetupUserData() + testutils.SetupAccountData(anotherUser, "bob@test.com", "pass1234") + + b1 := database.Book{ + UserID: user.ID, + Label: "js", + } + testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1") + b2 := database.Book{ + UserID: user.ID, + Label: "css", + } + testutils.MustExec(t, testutils.DB.Save(&b2), "preparing b2") + b3 := database.Book{ + UserID: anotherUser.ID, + Label: "css", + } + testutils.MustExec(t, testutils.DB.Save(&b3), "preparing b3") + + n1 := database.Note{ + UserID: user.ID, + BookUUID: b1.UUID, + Body: "n1 content", + USN: 11, + Deleted: false, + AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(), + } + testutils.MustExec(t, testutils.DB.Save(&n1), "preparing n1") + n2 := database.Note{ + UserID: user.ID, + BookUUID: b1.UUID, + Body: "n2 content", + USN: 14, + Deleted: false, + AddedOn: time.Date(2018, time.August, 11, 22, 0, 0, 0, time.UTC).UnixNano(), + } + testutils.MustExec(t, testutils.DB.Save(&n2), "preparing n2") + n3 := database.Note{ + UserID: user.ID, + BookUUID: b1.UUID, + Body: "n3 content", + USN: 17, + Deleted: false, + AddedOn: time.Date(2017, time.January, 10, 23, 0, 0, 0, time.UTC).UnixNano(), + } + testutils.MustExec(t, testutils.DB.Save(&n3), "preparing n3") + n4 := database.Note{ + UserID: user.ID, + BookUUID: b2.UUID, + Body: "n4 content", + USN: 18, + Deleted: false, + AddedOn: time.Date(2018, time.September, 10, 23, 0, 0, 0, time.UTC).UnixNano(), + } + testutils.MustExec(t, testutils.DB.Save(&n4), "preparing n4") + n5 := database.Note{ + UserID: anotherUser.ID, + BookUUID: b3.UUID, + Body: "n5 content", + USN: 19, + Deleted: false, + AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(), + } + testutils.MustExec(t, testutils.DB.Save(&n5), "preparing n5") + n6 := database.Note{ + UserID: user.ID, + BookUUID: b1.UUID, + Body: "", + USN: 11, + Deleted: true, + AddedOn: time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC).UnixNano(), + } + testutils.MustExec(t, testutils.DB.Save(&n6), "preparing n6") + + // Execute + endpoint := "/api/v3/notes" + + req := testutils.MakeReq(server.URL, "GET", fmt.Sprintf("%s?year=2018&month=8", endpoint), "") + res := testutils.HTTPAuthDo(t, req, user) + + // Test + assert.StatusCodeEquals(t, res, http.StatusOK, "") + + var payload GetNotesResponse + if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { + t.Fatal(errors.Wrap(err, "decoding payload")) + } + + var n2Record, n1Record database.Note + testutils.MustExec(t, testutils.DB.Where("uuid = ?", n2.UUID).First(&n2Record), "finding n2Record") + testutils.MustExec(t, testutils.DB.Where("uuid = ?", n1.UUID).First(&n1Record), "finding n1Record") + + expected := GetNotesResponse{ + Notes: []presenters.Note{ + getExpectedNotePayload(n2Record, b1, user), + getExpectedNotePayload(n1Record, b1, user), + }, + Total: 2, + } + + assert.DeepEqual(t, payload, expected, "payload mismatch") } func TestGetNote(t *testing.T) { @@ -222,246 +214,217 @@ func TestGetNote(t *testing.T) { } testutils.MustExec(t, testutils.DB.Save(&deletedNote), "preparing deletedNote") - getURL := func(noteUUID string, target testutils.EndpointType) string { - if target == testutils.EndpointWeb { - return fmt.Sprintf("/notes/%s", noteUUID) - } - + getURL := func(noteUUID string) string { return fmt.Sprintf("/api/v3/notes/%s", noteUUID) } - testutils.RunForWebAndAPI(t, "owner accessing private note", func(t *testing.T, target testutils.EndpointType) { + t.Run("owner accessing private note", func(t *testing.T) { // Execute - url := getURL(publicNote.UUID, target) + url := getURL(publicNote.UUID) req := testutils.MakeReq(server.URL, "GET", url, "") res := testutils.HTTPAuthDo(t, req, user) // Test assert.StatusCodeEquals(t, res, http.StatusOK, "") - if target == testutils.EndpointAPI { - var payload presenters.Note - if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { - t.Fatal(errors.Wrap(err, "decoding payload")) - } - - var n2Record database.Note - testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record") - - expected := getExpectedNotePayload(n2Record, b1, user) - assert.DeepEqual(t, payload, expected, "payload mismatch") + var payload presenters.Note + if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { + t.Fatal(errors.Wrap(err, "decoding payload")) } + + var n2Record database.Note + testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record") + + expected := getExpectedNotePayload(n2Record, b1, user) + assert.DeepEqual(t, payload, expected, "payload mismatch") }) - testutils.RunForWebAndAPI(t, "owner accessing public note", func(t *testing.T, target testutils.EndpointType) { + t.Run("owner accessing public note", func(t *testing.T) { // Execute - url := getURL(publicNote.UUID, target) + url := getURL(publicNote.UUID) req := testutils.MakeReq(server.URL, "GET", url, "") res := testutils.HTTPAuthDo(t, req, user) // Test assert.StatusCodeEquals(t, res, http.StatusOK, "") - if target == testutils.EndpointAPI { - var payload presenters.Note - if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { - t.Fatal(errors.Wrap(err, "decoding payload")) - } - - var n2Record database.Note - testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record") - - expected := getExpectedNotePayload(n2Record, b1, user) - assert.DeepEqual(t, payload, expected, "payload mismatch") + var payload presenters.Note + if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { + t.Fatal(errors.Wrap(err, "decoding payload")) } + + var n2Record database.Note + testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record") + + expected := getExpectedNotePayload(n2Record, b1, user) + assert.DeepEqual(t, payload, expected, "payload mismatch") }) - testutils.RunForWebAndAPI(t, "non-owner accessing public note", func(t *testing.T, target testutils.EndpointType) { + t.Run("non-owner accessing public note", func(t *testing.T) { // Execute - url := getURL(publicNote.UUID, target) + url := getURL(publicNote.UUID) req := testutils.MakeReq(server.URL, "GET", url, "") res := testutils.HTTPAuthDo(t, req, anotherUser) // Test assert.StatusCodeEquals(t, res, http.StatusOK, "") - if target == testutils.EndpointAPI { - var payload presenters.Note - if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { - t.Fatal(errors.Wrap(err, "decoding payload")) - } - - var n2Record database.Note - testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record") - - expected := getExpectedNotePayload(n2Record, b1, user) - assert.DeepEqual(t, payload, expected, "payload mismatch") + var payload presenters.Note + if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { + t.Fatal(errors.Wrap(err, "decoding payload")) } + + var n2Record database.Note + testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record") + + expected := getExpectedNotePayload(n2Record, b1, user) + assert.DeepEqual(t, payload, expected, "payload mismatch") }) - testutils.RunForWebAndAPI(t, "non-owner accessing private note", func(t *testing.T, target testutils.EndpointType) { + t.Run("non-owner accessing private note", func(t *testing.T) { // Execute - url := getURL(privateNote.UUID, target) + url := getURL(privateNote.UUID) req := testutils.MakeReq(server.URL, "GET", url, "") res := testutils.HTTPAuthDo(t, req, anotherUser) // Test assert.StatusCodeEquals(t, res, http.StatusNotFound, "") - if target == testutils.EndpointAPI { - body, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Fatal(errors.Wrap(err, "reading body")) - } - - assert.DeepEqual(t, string(body), "not found\n", "payload mismatch") + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatal(errors.Wrap(err, "reading body")) } + + assert.DeepEqual(t, string(body), "not found\n", "payload mismatch") }) - testutils.RunForWebAndAPI(t, "guest accessing public note", func(t *testing.T, target testutils.EndpointType) { + t.Run("guest accessing public note", func(t *testing.T) { // Execute - url := getURL(publicNote.UUID, target) + url := getURL(publicNote.UUID) req := testutils.MakeReq(server.URL, "GET", url, "") res := testutils.HTTPDo(t, req) // Test assert.StatusCodeEquals(t, res, http.StatusOK, "") - if target == testutils.EndpointAPI { - var payload presenters.Note - if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { - t.Fatal(errors.Wrap(err, "decoding payload")) - } - - var n2Record database.Note - testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record") - - expected := getExpectedNotePayload(n2Record, b1, user) - assert.DeepEqual(t, payload, expected, "payload mismatch") + var payload presenters.Note + if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { + t.Fatal(errors.Wrap(err, "decoding payload")) } + + var n2Record database.Note + testutils.MustExec(t, testutils.DB.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record") + + expected := getExpectedNotePayload(n2Record, b1, user) + assert.DeepEqual(t, payload, expected, "payload mismatch") }) - testutils.RunForWebAndAPI(t, "guest accessing private note", func(t *testing.T, target testutils.EndpointType) { + t.Run("guest accessing private note", func(t *testing.T) { // Execute - url := getURL(privateNote.UUID, target) + url := getURL(privateNote.UUID) req := testutils.MakeReq(server.URL, "GET", url, "") res := testutils.HTTPDo(t, req) // Test assert.StatusCodeEquals(t, res, http.StatusNotFound, "") - if target == testutils.EndpointAPI { - body, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Fatal(errors.Wrap(err, "reading body")) - } - - assert.DeepEqual(t, string(body), "not found\n", "payload mismatch") + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatal(errors.Wrap(err, "reading body")) } + + assert.DeepEqual(t, string(body), "not found\n", "payload mismatch") }) - testutils.RunForWebAndAPI(t, "nonexistent", func(t *testing.T, target testutils.EndpointType) { + t.Run("nonexistent", func(t *testing.T) { // Execute - url := getURL("somerandomstring", target) + url := getURL("somerandomstring") req := testutils.MakeReq(server.URL, "GET", url, "") res := testutils.HTTPAuthDo(t, req, user) // Test assert.StatusCodeEquals(t, res, http.StatusNotFound, "") - if target == testutils.EndpointAPI { - body, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Fatal(errors.Wrap(err, "reading body")) - } - - assert.DeepEqual(t, string(body), "not found\n", "payload mismatch") + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatal(errors.Wrap(err, "reading body")) } + + assert.DeepEqual(t, string(body), "not found\n", "payload mismatch") }) - testutils.RunForWebAndAPI(t, "deleted", func(t *testing.T, target testutils.EndpointType) { + t.Run("deleted", func(t *testing.T) { // Execute - url := getURL(deletedNote.UUID, target) + url := getURL(deletedNote.UUID) req := testutils.MakeReq(server.URL, "GET", url, "") res := testutils.HTTPAuthDo(t, req, user) // Test assert.StatusCodeEquals(t, res, http.StatusNotFound, "") - if target == testutils.EndpointAPI { - body, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Fatal(errors.Wrap(err, "reading body")) - } - - assert.DeepEqual(t, string(body), "not found\n", "payload mismatch") + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatal(errors.Wrap(err, "reading body")) } + + assert.DeepEqual(t, string(body), "not found\n", "payload mismatch") }) } func TestCreateNote(t *testing.T) { - testutils.RunForWebAndAPI(t, "success", func(t *testing.T, target testutils.EndpointType) { - defer testutils.ClearData(testutils.DB) + defer testutils.ClearData(testutils.DB) - // Setup - server := MustNewServer(t, &app.App{ - Clock: clock.NewMock(), - Config: config.Config{ - PageTemplateDir: "../views", - }, - }) - defer server.Close() - - user := testutils.SetupUserData() - testutils.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 101), "preparing user max_usn") - - b1 := database.Book{ - UserID: user.ID, - Label: "js", - USN: 58, - } - testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1") - - // Execute - - var req *http.Request - if target == testutils.EndpointAPI { - dat := fmt.Sprintf(`{"book_uuid": "%s", "content": "note content"}`, b1.UUID) - req = testutils.MakeReq(server.URL, "POST", "/api/v3/notes", dat) - } else { - dat := url.Values{} - dat.Set("book_uuid", b1.UUID) - dat.Set("content", "note content") - req = testutils.MakeFormReq(server.URL, "POST", "/notes", dat) - } - res := testutils.HTTPAuthDo(t, req, user) - - // Test - assert.StatusCodeEquals(t, res, http.StatusCreated, "") - - var noteRecord database.Note - var bookRecord database.Book - var userRecord database.User - var bookCount, noteCount int - testutils.MustExec(t, testutils.DB.Model(&database.Book{}).Count(&bookCount), "counting books") - testutils.MustExec(t, testutils.DB.Model(&database.Note{}).Count(¬eCount), "counting notes") - testutils.MustExec(t, testutils.DB.First(¬eRecord), "finding note") - testutils.MustExec(t, testutils.DB.Where("id = ?", b1.ID).First(&bookRecord), "finding book") - testutils.MustExec(t, testutils.DB.Where("id = ?", user.ID).First(&userRecord), "finding user record") - - assert.Equalf(t, bookCount, 1, "book count mismatch") - assert.Equalf(t, noteCount, 1, "note count mismatch") - - assert.Equal(t, bookRecord.Label, b1.Label, "book name mismatch") - assert.Equal(t, bookRecord.UUID, b1.UUID, "book uuid mismatch") - assert.Equal(t, bookRecord.UserID, b1.UserID, "book user_id mismatch") - assert.Equal(t, bookRecord.USN, 58, "book usn mismatch") - - assert.NotEqual(t, noteRecord.UUID, "", "note uuid should have been generated") - assert.Equal(t, noteRecord.BookUUID, b1.UUID, "note book_uuid mismatch") - assert.Equal(t, noteRecord.Body, "note content", "note content mismatch") - assert.Equal(t, noteRecord.USN, 102, "note usn mismatch") + // Setup + server := MustNewServer(t, &app.App{ + Clock: clock.NewMock(), + Config: config.Config{ + PageTemplateDir: "../views", + }, }) + defer server.Close() + + user := testutils.SetupUserData() + testutils.SetupAccountData(user, "alice@test.com", "pass1234") + testutils.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 101), "preparing user max_usn") + + b1 := database.Book{ + UserID: user.ID, + Label: "js", + USN: 58, + } + testutils.MustExec(t, testutils.DB.Save(&b1), "preparing b1") + + // Execute + + dat := fmt.Sprintf(`{"book_uuid": "%s", "content": "note content"}`, b1.UUID) + req := testutils.MakeReq(server.URL, "POST", "/api/v3/notes", dat) + res := testutils.HTTPAuthDo(t, req, user) + + // Test + assert.StatusCodeEquals(t, res, http.StatusCreated, "") + + var noteRecord database.Note + var bookRecord database.Book + var userRecord database.User + var bookCount, noteCount int + testutils.MustExec(t, testutils.DB.Model(&database.Book{}).Count(&bookCount), "counting books") + testutils.MustExec(t, testutils.DB.Model(&database.Note{}).Count(¬eCount), "counting notes") + testutils.MustExec(t, testutils.DB.First(¬eRecord), "finding note") + testutils.MustExec(t, testutils.DB.Where("id = ?", b1.ID).First(&bookRecord), "finding book") + testutils.MustExec(t, testutils.DB.Where("id = ?", user.ID).First(&userRecord), "finding user record") + + assert.Equalf(t, bookCount, 1, "book count mismatch") + assert.Equalf(t, noteCount, 1, "note count mismatch") + + assert.Equal(t, bookRecord.Label, b1.Label, "book name mismatch") + assert.Equal(t, bookRecord.UUID, b1.UUID, "book uuid mismatch") + assert.Equal(t, bookRecord.UserID, b1.UserID, "book user_id mismatch") + assert.Equal(t, bookRecord.USN, 58, "book usn mismatch") + + assert.NotEqual(t, noteRecord.UUID, "", "note uuid should have been generated") + assert.Equal(t, noteRecord.BookUUID, b1.UUID, "note book_uuid mismatch") + assert.Equal(t, noteRecord.Body, "note content", "note content mismatch") + assert.Equal(t, noteRecord.USN, 102, "note usn mismatch") } func TestDeleteNote(t *testing.T) { @@ -490,8 +453,8 @@ func TestDeleteNote(t *testing.T) { }, } - for _, tc := range testCases { - testutils.RunForWebAndAPI(t, fmt.Sprintf("originally deleted %t", tc.deleted), func(t *testing.T, target testutils.EndpointType) { + for idx, tc := range testCases { + t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) { defer testutils.ClearData(testutils.DB) // Setup @@ -504,6 +467,7 @@ func TestDeleteNote(t *testing.T) { defer server.Close() user := testutils.SetupUserData() + testutils.SetupAccountData(user, "alice@test.com", "pass1234") testutils.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 981), "preparing user max_usn") b1 := database.Book{ @@ -522,14 +486,8 @@ func TestDeleteNote(t *testing.T) { testutils.MustExec(t, testutils.DB.Save(¬e), "preparing note") // Execute - var req *http.Request - if target == testutils.EndpointAPI { - endpoint := fmt.Sprintf("/api/v3/notes/%s", note.UUID) - req = testutils.MakeReq(server.URL, "DELETE", endpoint, "") - } else { - endpoint := fmt.Sprintf("/notes/%s", note.UUID) - req = testutils.MakeFormReq(server.URL, "DELETE", endpoint, nil) - } + endpoint := fmt.Sprintf("/api/v3/notes/%s", note.UUID) + req := testutils.MakeReq(server.URL, "DELETE", endpoint, "") res := testutils.HTTPAuthDo(t, req, user) // Test @@ -736,7 +694,7 @@ func TestUpdateNote(t *testing.T) { } for idx, tc := range testCases { - testutils.RunForWebAndAPI(t, fmt.Sprintf("test case %d", idx), func(t *testing.T, target testutils.EndpointType) { + t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) { defer testutils.ClearData(testutils.DB) // Setup @@ -749,6 +707,8 @@ func TestUpdateNote(t *testing.T) { defer server.Close() user := testutils.SetupUserData() + testutils.SetupAccountData(user, "alice@test.com", "pass1234") + testutils.MustExec(t, testutils.DB.Model(&user).Update("max_usn", 101), "preparing user max_usn") b1 := database.Book{ @@ -777,13 +737,8 @@ func TestUpdateNote(t *testing.T) { // Execute var req *http.Request - if target == testutils.EndpointWeb { - endpoint := fmt.Sprintf("/notes/%s", note.UUID) - req = testutils.MakeFormReq(server.URL, "PATCH", endpoint, tc.payload.ToURLValues()) - } else { - endpoint := fmt.Sprintf("/api/v3/notes/%s", note.UUID) - req = testutils.MakeReq(server.URL, "PATCH", endpoint, tc.payload.ToJSON(t)) - } + endpoint := fmt.Sprintf("/api/v3/notes/%s", note.UUID) + req = testutils.MakeReq(server.URL, "PATCH", endpoint, tc.payload.ToJSON(t)) res := testutils.HTTPAuthDo(t, req, user) diff --git a/pkg/server/controllers/routes.go b/pkg/server/controllers/routes.go index 2bb36572..785c9ad4 100644 --- a/pkg/server/controllers/routes.go +++ b/pkg/server/controllers/routes.go @@ -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}, diff --git a/pkg/server/middleware/helpers_test.go b/pkg/server/middleware/helpers_test.go index dd6fc447..1eb9fe7a 100644 --- a/pkg/server/middleware/helpers_test.go +++ b/pkg/server/middleware/helpers_test.go @@ -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=", diff --git a/pkg/server/views/layouts/navbar.gohtml b/pkg/server/views/layouts/navbar.gohtml index 00487757..d7d0f78b 100644 --- a/pkg/server/views/layouts/navbar.gohtml +++ b/pkg/server/views/layouts/navbar.gohtml @@ -12,9 +12,6 @@ {{if .User}}