mirror of
https://github.com/dnote/dnote
synced 2026-03-16 15:35:52 +01:00
Implement MVC
This commit is contained in:
parent
172f608b66
commit
cd5d094c25
146 changed files with 13596 additions and 2328 deletions
|
|
@ -1,28 +0,0 @@
|
|||
/* Copyright (C) 2019, 2020, 2021 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (a *API) checkHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
/* Copyright (C) 2019, 2020, 2021 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"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/testutils"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
func TestCheckHealth(t *testing.T) {
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
DB: &gorm.DB{},
|
||||
Clock: clock.NewMock(),
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// Execute
|
||||
req := testutils.MakeReq(server.URL, "GET", "/health", "")
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "Status code mismtach")
|
||||
}
|
||||
|
|
@ -18,68 +18,68 @@
|
|||
|
||||
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
|
||||
}
|
||||
// import (
|
||||
// "net/http"
|
||||
// "strings"
|
||||
//
|
||||
// "github.com/dnote/dnote/pkg/server/database"
|
||||
// "github.com/jinzhu/gorm"
|
||||
// "github.com/pkg/errors"
|
||||
// )
|
||||
//
|
||||
// func paginate(conn *gorm.DB, page int) *gorm.DB {
|
||||
// limit := 30
|
||||
//
|
||||
// // Paginate
|
||||
// if page > 0 {
|
||||
// offset := limit * (page - 1)
|
||||
// conn = conn.Offset(offset)
|
||||
// }
|
||||
//
|
||||
// conn = conn.Limit(limit)
|
||||
//
|
||||
// return conn
|
||||
// }
|
||||
//
|
||||
// func getBookIDs(books []database.Book) []int {
|
||||
// ret := []int{}
|
||||
//
|
||||
// for _, book := range books {
|
||||
// ret = append(ret, book.ID)
|
||||
// }
|
||||
//
|
||||
// return ret
|
||||
// }
|
||||
//
|
||||
// func validatePassword(password string) error {
|
||||
// if len(password) < 8 {
|
||||
// return errors.New("Password should be longer than 8 characters")
|
||||
// }
|
||||
//
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// func getClientType(r *http.Request) string {
|
||||
// origin := r.Header.Get("Origin")
|
||||
//
|
||||
// if strings.HasPrefix(origin, "moz-extension://") {
|
||||
// return "firefox-extension"
|
||||
// }
|
||||
//
|
||||
// if strings.HasPrefix(origin, "chrome-extension://") {
|
||||
// return "chrome-extension"
|
||||
// }
|
||||
//
|
||||
// userAgent := r.Header.Get("User-Agent")
|
||||
// if strings.HasPrefix(userAgent, "Go-http-client") {
|
||||
// return "cli"
|
||||
// }
|
||||
//
|
||||
// return "web"
|
||||
// }
|
||||
//
|
||||
// // notSupported is the handler for the route that is no longer supported
|
||||
// func (a *API) notSupported(w http.ResponseWriter, r *http.Request) {
|
||||
// http.Error(w, "API version is not supported. Please upgrade your client.", http.StatusGone)
|
||||
// return
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
/* Copyright (C) 2019, 2020, 2021 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/dnote/pkg/server/testutils"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
testutils.InitTestDB()
|
||||
|
||||
code := m.Run()
|
||||
testutils.ClearData(testutils.DB)
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
|
@ -18,307 +18,307 @@
|
|||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"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/operations"
|
||||
"github.com/dnote/dnote/pkg/server/presenters"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ftsParams struct {
|
||||
HighlightAll bool
|
||||
}
|
||||
|
||||
func getHeadlineOptions(params *ftsParams) string {
|
||||
headlineOptions := []string{
|
||||
"StartSel=<dnotehl>",
|
||||
"StopSel=</dnotehl>",
|
||||
"ShortWord=0",
|
||||
}
|
||||
|
||||
if params != nil && params.HighlightAll {
|
||||
headlineOptions = append(headlineOptions, "HighlightAll=true")
|
||||
} else {
|
||||
headlineOptions = append(headlineOptions, "MaxFragments=3, MaxWords=50, MinWords=10")
|
||||
}
|
||||
|
||||
return strings.Join(headlineOptions, ",")
|
||||
}
|
||||
|
||||
func selectFTSFields(conn *gorm.DB, search string, params *ftsParams) *gorm.DB {
|
||||
headlineOpts := getHeadlineOptions(params)
|
||||
|
||||
return conn.Select(`
|
||||
notes.id,
|
||||
notes.uuid,
|
||||
notes.created_at,
|
||||
notes.updated_at,
|
||||
notes.book_uuid,
|
||||
notes.user_id,
|
||||
notes.added_on,
|
||||
notes.edited_on,
|
||||
notes.usn,
|
||||
notes.deleted,
|
||||
notes.encrypted,
|
||||
ts_headline('english_nostop', notes.body, plainto_tsquery('english_nostop', ?), ?) AS body
|
||||
`, search, headlineOpts)
|
||||
}
|
||||
|
||||
func respondWithNote(w http.ResponseWriter, note database.Note) {
|
||||
presentedNote := presenters.PresentNote(note)
|
||||
|
||||
handlers.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 := handlers.AuthWithSession(a.App.DB, r, nil)
|
||||
if err != nil {
|
||||
handlers.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 {
|
||||
handlers.RespondNotFound(w)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
handlers.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 {
|
||||
handlers.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 {
|
||||
handlers.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 {
|
||||
handlers.DoError(w, "finding notes", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response := GetNotesResponse{
|
||||
Notes: presenters.PresentNotes(notes),
|
||||
Total: total,
|
||||
}
|
||||
handlers.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 {
|
||||
handlers.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 {
|
||||
handlers.DoError(w, "finding notes", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
presented := presenters.PresentNotes(notes)
|
||||
handlers.RespondJSON(w, http.StatusOK, presented)
|
||||
}
|
||||
// import (
|
||||
// "fmt"
|
||||
// "net/http"
|
||||
// "net/url"
|
||||
// "strconv"
|
||||
// "strings"
|
||||
// "time"
|
||||
//
|
||||
// "github.com/dnote/dnote/pkg/server/database"
|
||||
// "github.com/dnote/dnote/pkg/server/helpers"
|
||||
// "github.com/dnote/dnote/pkg/server/middleware"
|
||||
// "github.com/dnote/dnote/pkg/server/operations"
|
||||
// "github.com/dnote/dnote/pkg/server/presenters"
|
||||
// "github.com/gorilla/mux"
|
||||
// "github.com/jinzhu/gorm"
|
||||
// "github.com/pkg/errors"
|
||||
// )
|
||||
//
|
||||
// type ftsParams struct {
|
||||
// HighlightAll bool
|
||||
// }
|
||||
//
|
||||
// func getHeadlineOptions(params *ftsParams) string {
|
||||
// headlineOptions := []string{
|
||||
// "StartSel=<dnotehl>",
|
||||
// "StopSel=</dnotehl>",
|
||||
// "ShortWord=0",
|
||||
// }
|
||||
//
|
||||
// if params != nil && params.HighlightAll {
|
||||
// headlineOptions = append(headlineOptions, "HighlightAll=true")
|
||||
// } else {
|
||||
// headlineOptions = append(headlineOptions, "MaxFragments=3, MaxWords=50, MinWords=10")
|
||||
// }
|
||||
//
|
||||
// return strings.Join(headlineOptions, ",")
|
||||
// }
|
||||
//
|
||||
// func selectFTSFields(conn *gorm.DB, search string, params *ftsParams) *gorm.DB {
|
||||
// headlineOpts := getHeadlineOptions(params)
|
||||
//
|
||||
// return conn.Select(`
|
||||
// notes.id,
|
||||
// notes.uuid,
|
||||
// notes.created_at,
|
||||
// notes.updated_at,
|
||||
// notes.book_uuid,
|
||||
// notes.user_id,
|
||||
// notes.added_on,
|
||||
// notes.edited_on,
|
||||
// notes.usn,
|
||||
// notes.deleted,
|
||||
// notes.encrypted,
|
||||
// ts_headline('english_nostop', notes.body, plainto_tsquery('english_nostop', ?), ?) AS body
|
||||
// `, search, headlineOpts)
|
||||
// }
|
||||
//
|
||||
// func respondWithNote(w http.ResponseWriter, note database.Note) {
|
||||
// presentedNote := presenters.PresentNote(note)
|
||||
//
|
||||
// middleware.RespondJSON(w, http.StatusOK, presentedNote)
|
||||
// }
|
||||
//
|
||||
// func parseSearchQuery(q url.Values) string {
|
||||
// searchStr := q.Get("q")
|
||||
//
|
||||
// return escapeSearchQuery(searchStr)
|
||||
// }
|
||||
//
|
||||
// func getNoteBaseQuery(db *gorm.DB, noteUUID string, search string) *gorm.DB {
|
||||
// var conn *gorm.DB
|
||||
// if search != "" {
|
||||
// conn = selectFTSFields(db, search, &ftsParams{HighlightAll: true})
|
||||
// } else {
|
||||
// conn = db
|
||||
// }
|
||||
//
|
||||
// conn = conn.Where("notes.uuid = ? AND deleted = ?", noteUUID, false)
|
||||
//
|
||||
// return conn
|
||||
// }
|
||||
//
|
||||
// func (a *API) getNote(w http.ResponseWriter, r *http.Request) {
|
||||
// user, _, err := middleware.AuthWithSession(a.App.DB, r)
|
||||
// if err != nil {
|
||||
// middleware.DoError(w, "authenticating", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// vars := mux.Vars(r)
|
||||
// noteUUID := vars["noteUUID"]
|
||||
//
|
||||
// note, ok, err := operations.GetNote(a.App.DB, noteUUID, &user)
|
||||
// if !ok {
|
||||
// middleware.RespondNotFound(w)
|
||||
// return
|
||||
// }
|
||||
// if err != nil {
|
||||
// middleware.DoError(w, "finding note", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// respondWithNote(w, note)
|
||||
// }
|
||||
//
|
||||
// /**** getNotesHandler */
|
||||
//
|
||||
// // GetNotesResponse is a reponse by getNotesHandler
|
||||
// type GetNotesResponse struct {
|
||||
// Notes []presenters.Note `json:"notes"`
|
||||
// Total int `json:"total"`
|
||||
// }
|
||||
//
|
||||
// type dateRange struct {
|
||||
// lower int64
|
||||
// upper int64
|
||||
// }
|
||||
//
|
||||
// func (a *API) getNotes(w http.ResponseWriter, r *http.Request) {
|
||||
// user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
// if !ok {
|
||||
// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
// query := r.URL.Query()
|
||||
//
|
||||
// respondGetNotes(a.App.DB, user.ID, query, w)
|
||||
// }
|
||||
//
|
||||
// func respondGetNotes(db *gorm.DB, userID int, query url.Values, w http.ResponseWriter) {
|
||||
// q, err := parseGetNotesQuery(query)
|
||||
// if err != nil {
|
||||
// http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// conn := getNotesBaseQuery(db, userID, q)
|
||||
//
|
||||
// var total int
|
||||
// if err := conn.Model(database.Note{}).Count(&total).Error; err != nil {
|
||||
// middleware.DoError(w, "counting total", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// notes := []database.Note{}
|
||||
// if total != 0 {
|
||||
// conn = orderGetNotes(conn)
|
||||
// conn = database.PreloadNote(conn)
|
||||
// conn = paginate(conn, q.Page)
|
||||
//
|
||||
// if err := conn.Find(¬es).Error; err != nil {
|
||||
// middleware.DoError(w, "finding notes", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// response := GetNotesResponse{
|
||||
// Notes: presenters.PresentNotes(notes),
|
||||
// Total: total,
|
||||
// }
|
||||
// middleware.RespondJSON(w, http.StatusOK, response)
|
||||
// }
|
||||
//
|
||||
// type getNotesQuery struct {
|
||||
// Year int
|
||||
// Month int
|
||||
// Page int
|
||||
// Books []string
|
||||
// Search string
|
||||
// Encrypted bool
|
||||
// }
|
||||
//
|
||||
// func parseGetNotesQuery(q url.Values) (getNotesQuery, error) {
|
||||
// yearStr := q.Get("year")
|
||||
// monthStr := q.Get("month")
|
||||
// books := q["book"]
|
||||
// pageStr := q.Get("page")
|
||||
// encryptedStr := q.Get("encrypted")
|
||||
//
|
||||
// fmt.Println("books", books)
|
||||
//
|
||||
// var page int
|
||||
// if len(pageStr) > 0 {
|
||||
// p, err := strconv.Atoi(pageStr)
|
||||
// if err != nil {
|
||||
// return getNotesQuery{}, errors.Errorf("invalid page %s", pageStr)
|
||||
// }
|
||||
// if p < 1 {
|
||||
// return getNotesQuery{}, errors.Errorf("invalid page %s", pageStr)
|
||||
// }
|
||||
//
|
||||
// page = p
|
||||
// } else {
|
||||
// page = 1
|
||||
// }
|
||||
//
|
||||
// var year int
|
||||
// if len(yearStr) > 0 {
|
||||
// y, err := strconv.Atoi(yearStr)
|
||||
// if err != nil {
|
||||
// return getNotesQuery{}, errors.Errorf("invalid year %s", yearStr)
|
||||
// }
|
||||
//
|
||||
// year = y
|
||||
// }
|
||||
//
|
||||
// var month int
|
||||
// if len(monthStr) > 0 {
|
||||
// m, err := strconv.Atoi(monthStr)
|
||||
// if err != nil {
|
||||
// return getNotesQuery{}, errors.Errorf("invalid month %s", monthStr)
|
||||
// }
|
||||
// if m < 1 || m > 12 {
|
||||
// return getNotesQuery{}, errors.Errorf("invalid month %s", monthStr)
|
||||
// }
|
||||
//
|
||||
// month = m
|
||||
// }
|
||||
//
|
||||
// var encrypted bool
|
||||
// if strings.ToLower(encryptedStr) == "true" {
|
||||
// encrypted = true
|
||||
// } else {
|
||||
// encrypted = false
|
||||
// }
|
||||
//
|
||||
// ret := getNotesQuery{
|
||||
// Year: year,
|
||||
// Month: month,
|
||||
// Page: page,
|
||||
// Search: parseSearchQuery(q),
|
||||
// Books: books,
|
||||
// Encrypted: encrypted,
|
||||
// }
|
||||
//
|
||||
// return ret, nil
|
||||
// }
|
||||
//
|
||||
// func getDateBounds(year, month int) (int64, int64) {
|
||||
// var yearUpperbound, monthUpperbound int
|
||||
//
|
||||
// if month == 12 {
|
||||
// monthUpperbound = 1
|
||||
// yearUpperbound = year + 1
|
||||
// } else {
|
||||
// monthUpperbound = month + 1
|
||||
// yearUpperbound = year
|
||||
// }
|
||||
//
|
||||
// lower := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC).UnixNano()
|
||||
// upper := time.Date(yearUpperbound, time.Month(monthUpperbound), 1, 0, 0, 0, 0, time.UTC).UnixNano()
|
||||
//
|
||||
// return lower, upper
|
||||
// }
|
||||
//
|
||||
// func getNotesBaseQuery(db *gorm.DB, userID int, q getNotesQuery) *gorm.DB {
|
||||
// conn := db.Where(
|
||||
// "notes.user_id = ? AND notes.deleted = ? AND notes.encrypted = ?",
|
||||
// userID, false, q.Encrypted,
|
||||
// )
|
||||
//
|
||||
// if q.Search != "" {
|
||||
// conn = selectFTSFields(conn, q.Search, nil)
|
||||
// conn = conn.Where("tsv @@ plainto_tsquery('english_nostop', ?)", q.Search)
|
||||
// }
|
||||
//
|
||||
// if len(q.Books) > 0 {
|
||||
// conn = conn.Joins("INNER JOIN books ON books.uuid = notes.book_uuid").
|
||||
// Where("books.label in (?)", q.Books)
|
||||
// }
|
||||
//
|
||||
// if q.Year != 0 || q.Month != 0 {
|
||||
// dateLowerbound, dateUpperbound := getDateBounds(q.Year, q.Month)
|
||||
// conn = conn.Where("notes.added_on >= ? AND notes.added_on < ?", dateLowerbound, dateUpperbound)
|
||||
// }
|
||||
//
|
||||
// return conn
|
||||
// }
|
||||
//
|
||||
// func orderGetNotes(conn *gorm.DB) *gorm.DB {
|
||||
// return conn.Order("notes.updated_at DESC, notes.id DESC")
|
||||
// }
|
||||
//
|
||||
// // escapeSearchQuery escapes the query for full text search
|
||||
// func escapeSearchQuery(searchQuery string) string {
|
||||
// return strings.Join(strings.Fields(searchQuery), "&")
|
||||
// }
|
||||
//
|
||||
// func (a *API) legacyGetNotes(w http.ResponseWriter, r *http.Request) {
|
||||
// user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
// if !ok {
|
||||
// middleware.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// var notes []database.Note
|
||||
// if err := a.App.DB.Where("user_id = ? AND encrypted = true", user.ID).Find(¬es).Error; err != nil {
|
||||
// middleware.DoError(w, "finding notes", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// presented := presenters.PresentNotes(notes)
|
||||
// middleware.RespondJSON(w, http.StatusOK, presented)
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -18,340 +18,340 @@
|
|||
|
||||
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")
|
||||
})
|
||||
}
|
||||
// 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")
|
||||
// })
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -18,99 +18,99 @@
|
|||
|
||||
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/handlers"
|
||||
"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 = handlers.Logging(ret)
|
||||
|
||||
if rateLimit && os.Getenv("GO_ENV") != "TEST" {
|
||||
ret = handlers.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 := handlers.AuthParams{ProOnly: true}
|
||||
app := a.App
|
||||
|
||||
var routes = []handlers.Route{
|
||||
// internal
|
||||
{Method: "GET", Pattern: "/health", HandlerFunc: a.checkHealth, RateLimit: false},
|
||||
{Method: "GET", Pattern: "/me", HandlerFunc: handlers.Auth(app, a.getMe, nil), RateLimit: true},
|
||||
{Method: "POST", Pattern: "/verification-token", HandlerFunc: handlers.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: handlers.Auth(app, a.updateProfile, nil), RateLimit: true},
|
||||
{Method: "PATCH", Pattern: "/account/password", HandlerFunc: handlers.Auth(app, a.updatePassword, nil), RateLimit: true},
|
||||
{Method: "GET", Pattern: "/account/email-preference", HandlerFunc: handlers.TokenAuth(app, a.getEmailPreference, database.TokenTypeEmailPreference, nil), RateLimit: true},
|
||||
{Method: "PATCH", Pattern: "/account/email-preference", HandlerFunc: handlers.TokenAuth(app, a.updateEmailPreference, database.TokenTypeEmailPreference, nil), RateLimit: true},
|
||||
{Method: "GET", Pattern: "/notes", HandlerFunc: handlers.Auth(app, a.getNotes, nil), RateLimit: false},
|
||||
{Method: "GET", Pattern: "/notes/{noteUUID}", HandlerFunc: a.getNote, RateLimit: true},
|
||||
{Method: "GET", Pattern: "/calendar", HandlerFunc: handlers.Auth(app, a.getCalendar, nil), RateLimit: true},
|
||||
|
||||
// v3
|
||||
{Method: "GET", Pattern: "/v3/sync/fragment", HandlerFunc: handlers.Cors(handlers.Auth(app, a.GetSyncFragment, &proOnly)), RateLimit: false},
|
||||
{Method: "GET", Pattern: "/v3/sync/state", HandlerFunc: handlers.Cors(handlers.Auth(app, a.GetSyncState, &proOnly)), RateLimit: false},
|
||||
{Method: "OPTIONS", Pattern: "/v3/books", HandlerFunc: handlers.Cors(a.BooksOptions), RateLimit: true},
|
||||
{Method: "GET", Pattern: "/v3/books", HandlerFunc: handlers.Cors(handlers.Auth(app, a.GetBooks, &proOnly)), RateLimit: true},
|
||||
{Method: "GET", Pattern: "/v3/books/{bookUUID}", HandlerFunc: handlers.Cors(handlers.Auth(app, a.GetBook, &proOnly)), RateLimit: true},
|
||||
{Method: "POST", Pattern: "/v3/books", HandlerFunc: handlers.Cors(handlers.Auth(app, a.CreateBook, &proOnly)), RateLimit: false},
|
||||
{Method: "PATCH", Pattern: "/v3/books/{bookUUID}", HandlerFunc: handlers.Cors(handlers.Auth(app, a.UpdateBook, &proOnly)), RateLimit: false},
|
||||
{Method: "DELETE", Pattern: "/v3/books/{bookUUID}", HandlerFunc: handlers.Cors(handlers.Auth(app, a.DeleteBook, &proOnly)), RateLimit: false},
|
||||
{Method: "OPTIONS", Pattern: "/v3/notes", HandlerFunc: handlers.Cors(a.NotesOptions), RateLimit: true},
|
||||
{Method: "POST", Pattern: "/v3/notes", HandlerFunc: handlers.Cors(handlers.Auth(app, a.CreateNote, &proOnly)), RateLimit: false},
|
||||
{Method: "PATCH", Pattern: "/v3/notes/{noteUUID}", HandlerFunc: handlers.Auth(app, a.UpdateNote, &proOnly), RateLimit: false},
|
||||
{Method: "DELETE", Pattern: "/v3/notes/{noteUUID}", HandlerFunc: handlers.Auth(app, a.DeleteNote, &proOnly), RateLimit: false},
|
||||
{Method: "POST", Pattern: "/v3/signin", HandlerFunc: handlers.Cors(a.signin), RateLimit: true},
|
||||
{Method: "OPTIONS", Pattern: "/v3/signout", HandlerFunc: handlers.Cors(a.signoutOptions), RateLimit: true},
|
||||
{Method: "POST", Pattern: "/v3/signout", HandlerFunc: handlers.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(handlers.NotSupported, true))
|
||||
router.PathPrefix("/v2").Handler(applyMiddleware(handlers.NotSupported, true))
|
||||
|
||||
for _, route := range routes {
|
||||
handler := route.HandlerFunc
|
||||
|
||||
router.
|
||||
Methods(route.Method).
|
||||
Path(route.Pattern).
|
||||
Handler(applyMiddleware(handler, route.RateLimit))
|
||||
}
|
||||
|
||||
return router, nil
|
||||
}
|
||||
// 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
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -18,31 +18,31 @@
|
|||
|
||||
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}
|
||||
}
|
||||
// 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}
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -18,377 +18,377 @@
|
|||
|
||||
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/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 {
|
||||
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, "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()
|
||||
handlers.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()
|
||||
handlers.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 {
|
||||
handlers.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 {
|
||||
handlers.DoError(w, "counting notes", err, http.StatusInternalServerError)
|
||||
}
|
||||
payload[d.Format("2006-1-2")] = count
|
||||
}
|
||||
|
||||
handlers.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 {
|
||||
handlers.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 {
|
||||
handlers.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 {
|
||||
handlers.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 {
|
||||
handlers.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 {
|
||||
handlers.RespondInvalidSMTPConfig(w)
|
||||
} else {
|
||||
handlers.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 {
|
||||
handlers.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 {
|
||||
handlers.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()
|
||||
handlers.DoError(w, "updating email_verified", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := tx.Model(&token).Update("used_at", time.Now()).Error; err != nil {
|
||||
tx.Rollback()
|
||||
handlers.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 {
|
||||
handlers.DoError(w, "finding user", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
s := session.New(user, account)
|
||||
handlers.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 {
|
||||
handlers.DoError(w, "No authenticated user found", nil, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var params emailPreferernceParams
|
||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
handlers.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 {
|
||||
handlers.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()
|
||||
handlers.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()
|
||||
handlers.DoError(w, "updating reset token", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
handlers.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 {
|
||||
handlers.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 {
|
||||
handlers.DoError(w, "finding pref", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
presented := presenters.PresentEmailPreference(pref)
|
||||
handlers.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 {
|
||||
handlers.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 {
|
||||
handlers.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)
|
||||
}
|
||||
// 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)
|
||||
// }
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,548 +0,0 @@
|
|||
/* Copyright (C) 2019, 2020, 2021 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"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 TestGetBooks(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",
|
||||
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
|
||||
req := testutils.MakeReq(server.URL, "GET", "/v3/books", "")
|
||||
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) {
|
||||
|
||||
defer testutils.ClearData(testutils.DB)
|
||||
|
||||
// Setup
|
||||
server := MustNewServer(t, &app.App{
|
||||
|
||||
Clock: clock.NewMock(),
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData()
|
||||
anotherUser := testutils.SetupUserData()
|
||||
req := testutils.MakeReq(server.URL, "GET", "/v3/books?name=js", "")
|
||||
|
||||
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
|
||||
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 TestDeleteBook(t *testing.T) {
|
||||
testCases := []struct {
|
||||
label string
|
||||
deleted bool
|
||||
expectedB2USN int
|
||||
expectedMaxUSN int
|
||||
expectedN2USN int
|
||||
expectedN3USN int
|
||||
}{
|
||||
{
|
||||
label: "n1 content",
|
||||
deleted: false,
|
||||
expectedMaxUSN: 61,
|
||||
expectedB2USN: 61,
|
||||
expectedN2USN: 59,
|
||||
expectedN3USN: 60,
|
||||
},
|
||||
{
|
||||
label: "",
|
||||
deleted: true,
|
||||
expectedMaxUSN: 59,
|
||||
expectedB2USN: 59,
|
||||
expectedN2USN: 5,
|
||||
expectedN3USN: 6,
|
||||
},
|
||||
}
|
||||
|
||||
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", 58), "preparing user max_usn")
|
||||
anotherUser := testutils.SetupUserData()
|
||||
testutils.MustExec(t, testutils.DB.Model(&anotherUser).Update("max_usn", 109), "preparing another user max_usn")
|
||||
|
||||
b1 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: "js",
|
||||
USN: 1,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b1), "preparing a book data")
|
||||
b2 := database.Book{
|
||||
UserID: user.ID,
|
||||
Label: tc.label,
|
||||
USN: 2,
|
||||
Deleted: tc.deleted,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b2), "preparing a book data")
|
||||
b3 := database.Book{
|
||||
UserID: anotherUser.ID,
|
||||
Label: "linux",
|
||||
USN: 3,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&b3), "preparing a book data")
|
||||
|
||||
var n2Body string
|
||||
if !tc.deleted {
|
||||
n2Body = "n2 content"
|
||||
}
|
||||
var n3Body string
|
||||
if !tc.deleted {
|
||||
n3Body = "n3 content"
|
||||
}
|
||||
|
||||
n1 := database.Note{
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Body: "n1 content",
|
||||
USN: 4,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n1), "preparing a note data")
|
||||
n2 := database.Note{
|
||||
UserID: user.ID,
|
||||
BookUUID: b2.UUID,
|
||||
Body: n2Body,
|
||||
USN: 5,
|
||||
Deleted: tc.deleted,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n2), "preparing a note data")
|
||||
n3 := database.Note{
|
||||
UserID: user.ID,
|
||||
BookUUID: b2.UUID,
|
||||
Body: n3Body,
|
||||
USN: 6,
|
||||
Deleted: tc.deleted,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n3), "preparing a note data")
|
||||
n4 := database.Note{
|
||||
UserID: user.ID,
|
||||
BookUUID: b2.UUID,
|
||||
Body: "",
|
||||
USN: 7,
|
||||
Deleted: true,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n4), "preparing a note data")
|
||||
n5 := database.Note{
|
||||
UserID: anotherUser.ID,
|
||||
BookUUID: b3.UUID,
|
||||
Body: "n5 content",
|
||||
USN: 8,
|
||||
}
|
||||
testutils.MustExec(t, testutils.DB.Save(&n5), "preparing a note data")
|
||||
|
||||
endpoint := fmt.Sprintf("/v3/books/%s", b2.UUID)
|
||||
req := testutils.MakeReq(server.URL, "DELETE", endpoint, "")
|
||||
req.Header.Set("Version", "0.1.1")
|
||||
req.Header.Set("Origin", "chrome-extension://iaolnfnipkoinabdbbakcmkkdignedce")
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, "")
|
||||
|
||||
var b1Record, b2Record, b3Record database.Book
|
||||
var n1Record, n2Record, n3Record, n4Record, n5Record 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("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 = ?", b3.ID).First(&b3Record), "finding b3")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", n1.ID).First(&n1Record), "finding n1")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", n2.ID).First(&n2Record), "finding n2")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", n3.ID).First(&n3Record), "finding n3")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", n4.ID).First(&n4Record), "finding n4")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", n5.ID).First(&n5Record), "finding n5")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", user.ID).First(&userRecord), "finding user record")
|
||||
|
||||
assert.Equal(t, bookCount, 3, "book count mismatch")
|
||||
assert.Equal(t, noteCount, 5, "note count mismatch")
|
||||
|
||||
assert.Equal(t, userRecord.MaxUSN, tc.expectedMaxUSN, "user max_usn mismatch")
|
||||
|
||||
assert.Equal(t, b1Record.Deleted, false, "b1 deleted mismatch")
|
||||
assert.Equal(t, b1Record.Label, b1.Label, "b1 content mismatch")
|
||||
assert.Equal(t, b1Record.USN, b1.USN, "b1 usn mismatch")
|
||||
assert.Equal(t, b2Record.Deleted, true, "b2 deleted mismatch")
|
||||
assert.Equal(t, b2Record.Label, "", "b2 content mismatch")
|
||||
assert.Equal(t, b2Record.USN, tc.expectedB2USN, "b2 usn mismatch")
|
||||
assert.Equal(t, b3Record.Deleted, false, "b3 deleted mismatch")
|
||||
assert.Equal(t, b3Record.Label, b3.Label, "b3 content mismatch")
|
||||
assert.Equal(t, b3Record.USN, b3.USN, "b3 usn mismatch")
|
||||
|
||||
assert.Equal(t, n1Record.USN, n1.USN, "n1 usn mismatch")
|
||||
assert.Equal(t, n1Record.Deleted, false, "n1 deleted mismatch")
|
||||
assert.Equal(t, n1Record.Body, n1.Body, "n1 content mismatch")
|
||||
|
||||
assert.Equal(t, n2Record.USN, tc.expectedN2USN, "n2 usn mismatch")
|
||||
assert.Equal(t, n2Record.Deleted, true, "n2 deleted mismatch")
|
||||
assert.Equal(t, n2Record.Body, "", "n2 content mismatch")
|
||||
|
||||
assert.Equal(t, n3Record.USN, tc.expectedN3USN, "n3 usn mismatch")
|
||||
assert.Equal(t, n3Record.Deleted, true, "n3 deleted mismatch")
|
||||
assert.Equal(t, n3Record.Body, "", "n3 content mismatch")
|
||||
|
||||
// if already deleted, usn should remain the same and hence should not contribute to bumping the max_usn
|
||||
assert.Equal(t, n4Record.USN, n4.USN, "n4 usn mismatch")
|
||||
assert.Equal(t, n4Record.Deleted, true, "n4 deleted mismatch")
|
||||
assert.Equal(t, n4Record.Body, "", "n4 content mismatch")
|
||||
|
||||
assert.Equal(t, n5Record.USN, n5.USN, "n5 usn mismatch")
|
||||
assert.Equal(t, n5Record.Deleted, false, "n5 deleted mismatch")
|
||||
assert.Equal(t, n5Record.Body, n5.Body, "n5 content mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateBook(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")
|
||||
|
||||
req := testutils.MakeReq(server.URL, "POST", "/v3/books", `{"name": "js"}`)
|
||||
req.Header.Set("Version", "0.1.1")
|
||||
req.Header.Set("Origin", "chrome-extension://iaolnfnipkoinabdbbakcmkkdignedce")
|
||||
|
||||
// Execute
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusCreated, "")
|
||||
|
||||
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(&bookRecord), "finding book")
|
||||
testutils.MustExec(t, testutils.DB.Where("id = ?", user.ID).First(&userRecord), "finding user record")
|
||||
|
||||
maxUSN := 102
|
||||
|
||||
assert.Equalf(t, bookCount, 1, "book count mismatch")
|
||||
assert.Equalf(t, noteCount, 0, "note count mismatch")
|
||||
|
||||
assert.NotEqual(t, bookRecord.UUID, "", "book uuid should have been generated")
|
||||
assert.Equal(t, bookRecord.Label, "js", "book name mismatch")
|
||||
assert.Equal(t, bookRecord.UserID, user.ID, "book user_id mismatch")
|
||||
assert.Equal(t, bookRecord.USN, maxUSN, "book user_id mismatch")
|
||||
assert.Equal(t, userRecord.MaxUSN, maxUSN, "user max_usn mismatch")
|
||||
|
||||
var got CreateBookResp
|
||||
if err := json.NewDecoder(res.Body).Decode(&got); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding got"))
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
func TestCreateBookDuplicate(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 book data")
|
||||
|
||||
// Execute
|
||||
req := testutils.MakeReq(server.URL, "POST", "/v3/books", `{"name": "js"}`)
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusConflict, "")
|
||||
|
||||
var bookRecord database.Book
|
||||
var bookCount, noteCount int
|
||||
var userRecord database.User
|
||||
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(&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, 0, "note count mismatch")
|
||||
|
||||
assert.Equal(t, bookRecord.Label, "js", "book name mismatch")
|
||||
assert.Equal(t, bookRecord.UserID, user.ID, "book user_id mismatch")
|
||||
assert.Equal(t, bookRecord.USN, b1.USN, "book usn mismatch")
|
||||
assert.Equal(t, userRecord.MaxUSN, 101, "user max_usn mismatch")
|
||||
}
|
||||
|
||||
func TestUpdateBook(t *testing.T) {
|
||||
updatedLabel := "updated-label"
|
||||
|
||||
b1UUID := "ead8790f-aff9-4bdf-8eec-f734ccd29202"
|
||||
b2UUID := "0ecaac96-8d72-4e04-8925-5a21b79a16da"
|
||||
|
||||
testCases := []struct {
|
||||
payload string
|
||||
bookUUID string
|
||||
bookDeleted bool
|
||||
bookLabel string
|
||||
expectedBookLabel string
|
||||
}{
|
||||
{
|
||||
payload: fmt.Sprintf(`{
|
||||
"name": "%s"
|
||||
}`, updatedLabel),
|
||||
bookUUID: b1UUID,
|
||||
bookDeleted: false,
|
||||
bookLabel: "original-label",
|
||||
expectedBookLabel: updatedLabel,
|
||||
},
|
||||
// if a deleted book is updated, it should be un-deleted
|
||||
{
|
||||
payload: fmt.Sprintf(`{
|
||||
"name": "%s"
|
||||
}`, updatedLabel),
|
||||
bookUUID: b1UUID,
|
||||
bookDeleted: true,
|
||||
bookLabel: "",
|
||||
expectedBookLabel: updatedLabel,
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
func() {
|
||||
|
||||
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: tc.bookUUID,
|
||||
UserID: user.ID,
|
||||
Label: tc.bookLabel,
|
||||
Deleted: tc.bookDeleted,
|
||||
}
|
||||
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")
|
||||
|
||||
// Executdb,e
|
||||
endpoint := fmt.Sprintf("/v3/books/%s", tc.bookUUID)
|
||||
req := testutils.MakeReq(server.URL, "PATCH", endpoint, tc.payload)
|
||||
res := testutils.HTTPAuthDo(t, req, user)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusOK, fmt.Sprintf("status code mismatch for test case %d", idx))
|
||||
|
||||
var bookRecord database.Book
|
||||
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("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, 0, "note count mismatch")
|
||||
|
||||
assert.Equalf(t, bookRecord.UUID, tc.bookUUID, "book uuid mismatch")
|
||||
assert.Equalf(t, bookRecord.Label, tc.expectedBookLabel, "book label mismatch")
|
||||
assert.Equalf(t, bookRecord.USN, 102, "book usn mismatch")
|
||||
assert.Equalf(t, bookRecord.Deleted, false, "book Deleted mismatch")
|
||||
|
||||
assert.Equal(t, userRecord.MaxUSN, 102, fmt.Sprintf("user max_usn mismatch for test case %d", idx))
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,304 +0,0 @@
|
|||
/* Copyright (C) 2019, 2020, 2021 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"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/pkg/errors"
|
||||
)
|
||||
|
||||
// fullSyncBefore is the system-wide timestamp that represents the point in time
|
||||
// before which clients must perform a full-sync rather than incremental sync.
|
||||
const fullSyncBefore = 0
|
||||
|
||||
// SyncFragment contains a piece of information about the server's state.
|
||||
// It is used to transfer the server's state to the client gradually without having to
|
||||
// transfer the whole state at once.
|
||||
type SyncFragment struct {
|
||||
FragMaxUSN int `json:"frag_max_usn"`
|
||||
UserMaxUSN int `json:"user_max_usn"`
|
||||
CurrentTime int64 `json:"current_time"`
|
||||
Notes []SyncFragNote `json:"notes"`
|
||||
Books []SyncFragBook `json:"books"`
|
||||
ExpungedNotes []string `json:"expunged_notes"`
|
||||
ExpungedBooks []string `json:"expunged_books"`
|
||||
}
|
||||
|
||||
// SyncFragNote represents a note in a sync fragment and contains only the necessary information
|
||||
// for the client to sync the note locally
|
||||
type SyncFragNote struct {
|
||||
UUID string `json:"uuid"`
|
||||
BookUUID string `json:"book_uuid"`
|
||||
USN int `json:"usn"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
EditedOn int64 `json:"edited_on"`
|
||||
Body string `json:"content"`
|
||||
Public bool `json:"public"`
|
||||
Deleted bool `json:"deleted"`
|
||||
}
|
||||
|
||||
// NewFragNote presents the given note as a SyncFragNote
|
||||
func NewFragNote(note database.Note) SyncFragNote {
|
||||
return SyncFragNote{
|
||||
UUID: note.UUID,
|
||||
USN: note.USN,
|
||||
CreatedAt: note.CreatedAt,
|
||||
UpdatedAt: note.UpdatedAt,
|
||||
AddedOn: note.AddedOn,
|
||||
EditedOn: note.EditedOn,
|
||||
Body: note.Body,
|
||||
Public: note.Public,
|
||||
Deleted: note.Deleted,
|
||||
BookUUID: note.BookUUID,
|
||||
}
|
||||
}
|
||||
|
||||
// SyncFragBook represents a book in a sync fragment and contains only the necessary information
|
||||
// for the client to sync the note locally
|
||||
type SyncFragBook struct {
|
||||
UUID string `json:"uuid"`
|
||||
USN int `json:"usn"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
Label string `json:"label"`
|
||||
Deleted bool `json:"deleted"`
|
||||
}
|
||||
|
||||
// NewFragBook presents the given book as a SyncFragBook
|
||||
func NewFragBook(book database.Book) SyncFragBook {
|
||||
return SyncFragBook{
|
||||
UUID: book.UUID,
|
||||
USN: book.USN,
|
||||
CreatedAt: book.CreatedAt,
|
||||
UpdatedAt: book.UpdatedAt,
|
||||
AddedOn: book.AddedOn,
|
||||
Label: book.Label,
|
||||
Deleted: book.Deleted,
|
||||
}
|
||||
}
|
||||
|
||||
type usnItem struct {
|
||||
usn int
|
||||
val interface{}
|
||||
}
|
||||
|
||||
type queryParamError struct {
|
||||
key string
|
||||
value string
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *queryParamError) Error() string {
|
||||
return fmt.Sprintf("invalid query param %s=%s. %s", e.key, e.value, e.message)
|
||||
}
|
||||
|
||||
func (a *API) newFragment(userID, userMaxUSN, afterUSN, limit int) (SyncFragment, error) {
|
||||
var notes []database.Note
|
||||
if err := a.App.DB.Where("user_id = ? AND usn > ? AND usn <= ?", userID, afterUSN, userMaxUSN).Order("usn ASC").Limit(limit).Find(¬es).Error; err != nil {
|
||||
return SyncFragment{}, nil
|
||||
}
|
||||
var books []database.Book
|
||||
if err := a.App.DB.Where("user_id = ? AND usn > ? AND usn <= ?", userID, afterUSN, userMaxUSN).Order("usn ASC").Limit(limit).Find(&books).Error; err != nil {
|
||||
return SyncFragment{}, nil
|
||||
}
|
||||
|
||||
var items []usnItem
|
||||
for _, note := range notes {
|
||||
i := usnItem{
|
||||
usn: note.USN,
|
||||
val: note,
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
for _, book := range books {
|
||||
i := usnItem{
|
||||
usn: book.USN,
|
||||
val: book,
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
|
||||
// order by usn in ascending order
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return items[i].usn < items[j].usn
|
||||
})
|
||||
|
||||
fragNotes := []SyncFragNote{}
|
||||
fragBooks := []SyncFragBook{}
|
||||
fragExpungedNotes := []string{}
|
||||
fragExpungedBooks := []string{}
|
||||
|
||||
fragMaxUSN := 0
|
||||
for i := 0; i < limit; i++ {
|
||||
if i > len(items)-1 {
|
||||
break
|
||||
}
|
||||
|
||||
item := items[i]
|
||||
|
||||
fragMaxUSN = item.usn
|
||||
|
||||
switch v := item.val.(type) {
|
||||
case database.Note:
|
||||
note := item.val.(database.Note)
|
||||
|
||||
if note.Deleted {
|
||||
fragExpungedNotes = append(fragExpungedNotes, note.UUID)
|
||||
} else {
|
||||
fragNotes = append(fragNotes, NewFragNote(note))
|
||||
}
|
||||
case database.Book:
|
||||
book := item.val.(database.Book)
|
||||
|
||||
if book.Deleted {
|
||||
fragExpungedBooks = append(fragExpungedBooks, book.UUID)
|
||||
} else {
|
||||
fragBooks = append(fragBooks, NewFragBook(book))
|
||||
}
|
||||
default:
|
||||
return SyncFragment{}, errors.Errorf("unknown internal item type %s", v)
|
||||
}
|
||||
}
|
||||
|
||||
ret := SyncFragment{
|
||||
FragMaxUSN: fragMaxUSN,
|
||||
UserMaxUSN: userMaxUSN,
|
||||
CurrentTime: a.App.Clock.Now().Unix(),
|
||||
Notes: fragNotes,
|
||||
Books: fragBooks,
|
||||
ExpungedNotes: fragExpungedNotes,
|
||||
ExpungedBooks: fragExpungedBooks,
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func parseGetSyncFragmentQuery(q url.Values) (afterUSN, limit int, err error) {
|
||||
afterUSNStr := q.Get("after_usn")
|
||||
limitStr := q.Get("limit")
|
||||
|
||||
if len(afterUSNStr) > 0 {
|
||||
afterUSN, err = strconv.Atoi(afterUSNStr)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "invalid after_usn")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
afterUSN = 0
|
||||
}
|
||||
|
||||
if len(limitStr) > 0 {
|
||||
l, e := strconv.Atoi(limitStr)
|
||||
|
||||
if e != nil {
|
||||
err = errors.Wrap(e, "invalid limit")
|
||||
return
|
||||
}
|
||||
|
||||
if l > 100 {
|
||||
err = &queryParamError{
|
||||
key: "limit",
|
||||
value: limitStr,
|
||||
message: "maximum value is 100",
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
limit = l
|
||||
} else {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetSyncFragmentResp represents a response from GetSyncFragment handler
|
||||
type GetSyncFragmentResp struct {
|
||||
Fragment SyncFragment `json:"fragment"`
|
||||
}
|
||||
|
||||
// GetSyncFragment responds with a sync fragment
|
||||
func (a *API) GetSyncFragment(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
|
||||
}
|
||||
|
||||
afterUSN, limit, err := parseGetSyncFragmentQuery(r.URL.Query())
|
||||
if err != nil {
|
||||
handlers.DoError(w, "parsing query params", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fragment, err := a.newFragment(user.ID, user.MaxUSN, afterUSN, limit)
|
||||
if err != nil {
|
||||
handlers.DoError(w, "getting fragment", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := GetSyncFragmentResp{
|
||||
Fragment: fragment,
|
||||
}
|
||||
handlers.RespondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetSyncStateResp represents a response from GetSyncFragment handler
|
||||
type GetSyncStateResp struct {
|
||||
FullSyncBefore int `json:"full_sync_before"`
|
||||
MaxUSN int `json:"max_usn"`
|
||||
CurrentTime int64 `json:"current_time"`
|
||||
}
|
||||
|
||||
// GetSyncState responds with a sync fragment
|
||||
func (a *API) GetSyncState(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
|
||||
}
|
||||
|
||||
response := GetSyncStateResp{
|
||||
FullSyncBefore: fullSyncBefore,
|
||||
MaxUSN: user.MaxUSN,
|
||||
// TODO: exposing server time means we probably shouldn't seed random generator with time?
|
||||
CurrentTime: a.App.Clock.Now().Unix(),
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"user_id": user.ID,
|
||||
"resp": response,
|
||||
}).Info("getting sync state")
|
||||
|
||||
handlers.RespondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
/* Copyright (C) 2019, 2020, 2021 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/dnote/pkg/assert"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestParseGetSyncFragmentQuery(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
afterUSN int
|
||||
limit int
|
||||
err error
|
||||
}{
|
||||
{
|
||||
input: `after_usn=50&limit=50`,
|
||||
afterUSN: 50,
|
||||
limit: 50,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: `limit=50`,
|
||||
afterUSN: 0,
|
||||
limit: 50,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: `after_usn=50`,
|
||||
afterUSN: 50,
|
||||
limit: 100,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: `after_usn=50&limit=100`,
|
||||
afterUSN: 50,
|
||||
limit: 100,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "",
|
||||
afterUSN: 0,
|
||||
limit: 100,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "limit=101",
|
||||
afterUSN: 0,
|
||||
limit: 0,
|
||||
err: &queryParamError{
|
||||
key: "limit",
|
||||
value: "101",
|
||||
message: "maximum value is 100",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
q, err := url.ParseQuery(tc.input)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "parsing test input"))
|
||||
}
|
||||
|
||||
afterUSN, limit, err := parseGetSyncFragmentQuery(q)
|
||||
ok := reflect.DeepEqual(err, tc.err)
|
||||
assert.Equal(t, ok, true, fmt.Sprintf("err mismatch for test case %d. Expected: %+v. Got: %+v", idx, tc.err, err))
|
||||
|
||||
assert.Equal(t, afterUSN, tc.afterUSN, fmt.Sprintf("afterUSN mismatch for test case %d", idx))
|
||||
assert.Equal(t, limit, tc.limit, fmt.Sprintf("limit mismatch for test case %d", idx))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue