mirror of
https://github.com/dnote/dnote
synced 2026-03-14 14:35:50 +01:00
Remove the unused encrypted and public fields (#700)
* Remove encrypted fields from notes and books * Remove public from notes * Use consistent flags
This commit is contained in:
parent
83ac43b737
commit
b03ca999a5
22 changed files with 175 additions and 653 deletions
|
|
@ -97,8 +97,7 @@ func (l syncList) getLength() int {
|
|||
return len(l.Notes) + len(l.Books) + len(l.ExpungedNotes) + len(l.ExpungedBooks)
|
||||
}
|
||||
|
||||
// processFragments categorizes items in sync fragments into a sync list. It also decrypts any
|
||||
// encrypted data in sync fragments.
|
||||
// processFragments categorizes items in sync fragments into a sync list.
|
||||
func processFragments(fragments []client.SyncFragment) (syncList, error) {
|
||||
notes := map[string]client.SyncFragNote{}
|
||||
books := map[string]client.SyncFragBook{}
|
||||
|
|
|
|||
|
|
@ -1,123 +0,0 @@
|
|||
/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Package crypt provides cryptographic funcitonalities
|
||||
package crypt
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
var aesGcmNonceSize = 12
|
||||
|
||||
func runHkdf(secret, salt, info []byte) ([]byte, error) {
|
||||
r := hkdf.New(sha256.New, secret, salt, info)
|
||||
|
||||
ret := make([]byte, 32)
|
||||
_, err := io.ReadFull(r, ret)
|
||||
if err != nil {
|
||||
return []byte{}, errors.Wrap(err, "reading key bytes")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// MakeKeys derives, from the given credential, a key set comprising of an encryption key
|
||||
// and an authentication key
|
||||
func MakeKeys(password, email []byte, iteration int) ([]byte, []byte, error) {
|
||||
masterKey := pbkdf2.Key([]byte(password), []byte(email), iteration, 32, sha256.New)
|
||||
|
||||
authKey, err := runHkdf(masterKey, email, []byte("auth"))
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "deriving auth key")
|
||||
}
|
||||
|
||||
return masterKey, authKey, nil
|
||||
}
|
||||
|
||||
// AesGcmEncrypt encrypts the plaintext using AES in a GCM mode. It returns
|
||||
// a ciphertext prepended by a 12 byte pseudo-random nonce, encoded in base64.
|
||||
func AesGcmEncrypt(key, plaintext []byte) (string, error) {
|
||||
if key == nil {
|
||||
return "", errors.New("no key provided")
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "initializing aes")
|
||||
}
|
||||
|
||||
aesgcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "initializing gcm")
|
||||
}
|
||||
|
||||
nonce := make([]byte, aesGcmNonceSize)
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", errors.Wrap(err, "generating nonce")
|
||||
}
|
||||
|
||||
ciphertext := aesgcm.Seal(nonce, nonce, []byte(plaintext), nil)
|
||||
cipherKeyB64 := base64.StdEncoding.EncodeToString(ciphertext)
|
||||
|
||||
return cipherKeyB64, nil
|
||||
}
|
||||
|
||||
// AesGcmDecrypt decrypts the encrypted data using AES in a GCM mode. The data should be
|
||||
// a base64 encoded string in the format of 12 byte nonce followed by a ciphertext.
|
||||
func AesGcmDecrypt(key []byte, dataB64 string) ([]byte, error) {
|
||||
if key == nil {
|
||||
return nil, errors.New("no key provided")
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(dataB64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "decoding base64 data")
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "initializing aes")
|
||||
}
|
||||
|
||||
aesgcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "initializing gcm")
|
||||
}
|
||||
|
||||
if len(data) < aesGcmNonceSize {
|
||||
return nil, errors.Wrap(err, "malformed data")
|
||||
}
|
||||
|
||||
nonce, ciphertext := data[:aesGcmNonceSize], data[aesGcmNonceSize:]
|
||||
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "decrypting")
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package crypt
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/dnote/pkg/assert"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestAesGcmEncrypt(t *testing.T) {
|
||||
testCases := []struct {
|
||||
key []byte
|
||||
plaintext []byte
|
||||
}{
|
||||
{
|
||||
key: []byte("AES256Key-32Characters1234567890"),
|
||||
plaintext: []byte("foo bar baz quz"),
|
||||
},
|
||||
{
|
||||
key: []byte("AES256Key-32Charactersabcdefghij"),
|
||||
plaintext: []byte("1234 foo 5678 bar 7890 baz"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("key %s plaintext %s", tc.key, tc.plaintext), func(t *testing.T) {
|
||||
// encrypt
|
||||
dataB64, err := AesGcmEncrypt(tc.key, tc.plaintext)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "performing encryption"))
|
||||
}
|
||||
|
||||
// test that data can be decrypted
|
||||
data, err := base64.StdEncoding.DecodeString(dataB64)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decoding data from base64"))
|
||||
}
|
||||
|
||||
nonce, ciphertext := data[:12], data[12:]
|
||||
|
||||
fmt.Println(string(data))
|
||||
|
||||
block, err := aes.NewCipher([]byte(tc.key))
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "initializing aes"))
|
||||
}
|
||||
|
||||
aesgcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "initializing gcm"))
|
||||
}
|
||||
|
||||
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "decode"))
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, plaintext, tc.plaintext, "plaintext mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAesGcmDecrypt(t *testing.T) {
|
||||
testCases := []struct {
|
||||
key []byte
|
||||
ciphertextB64 string
|
||||
expectedPlaintext string
|
||||
}{
|
||||
{
|
||||
key: []byte("AES256Key-32Characters1234567890"),
|
||||
ciphertextB64: "M2ov9hWMQ52v1S/zigwX3bJt4cVCV02uiRm/grKqN/rZxNkJrD7vK4Ii0g==",
|
||||
expectedPlaintext: "foo bar baz quz",
|
||||
},
|
||||
{
|
||||
key: []byte("AES256Key-32Characters1234567890"),
|
||||
ciphertextB64: "M4csFKUIUbD1FBEzLgHjscoKgN0lhMGJ0n2nKWiCkE/qSKlRP7kS",
|
||||
expectedPlaintext: "foo\n1\nbar\n2",
|
||||
},
|
||||
{
|
||||
key: []byte("AES256Key-32Characters1234567890"),
|
||||
ciphertextB64: "pe/fnw73MR1clmVIlRSJ5gDwBdnPly/DF7DsR5dJVz4dHZlv0b10WzvJEGOCHZEr+Q==",
|
||||
expectedPlaintext: "föo\nbār\nbåz & qūz",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("key %s ciphertext %s", tc.key, tc.ciphertextB64), func(t *testing.T) {
|
||||
plaintext, err := AesGcmDecrypt(tc.key, tc.ciphertextB64)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "performing decryption"))
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, plaintext, []byte(tc.expectedPlaintext), "plaintext mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ func TestServerStart(t *testing.T) {
|
|||
port := "13456" // Use different port to avoid conflicts with main test server
|
||||
|
||||
// Start server in background
|
||||
cmd := exec.Command(testServerBinary, "start", "-port", port)
|
||||
cmd := exec.Command(testServerBinary, "start", "--port", port)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"DBPath="+tmpDB,
|
||||
"WebURL=http://localhost:"+port,
|
||||
|
|
@ -143,11 +143,11 @@ func TestServerStartHelp(t *testing.T) {
|
|||
|
||||
outputStr := string(output)
|
||||
assert.Equal(t, strings.Contains(outputStr, "dnote-server start [flags]"), true, "output should contain usage")
|
||||
assert.Equal(t, strings.Contains(outputStr, "-appEnv"), true, "output should contain appEnv flag")
|
||||
assert.Equal(t, strings.Contains(outputStr, "-port"), true, "output should contain port flag")
|
||||
assert.Equal(t, strings.Contains(outputStr, "-webUrl"), true, "output should contain webUrl flag")
|
||||
assert.Equal(t, strings.Contains(outputStr, "-dbPath"), true, "output should contain dbPath flag")
|
||||
assert.Equal(t, strings.Contains(outputStr, "-disableRegistration"), true, "output should contain disableRegistration flag")
|
||||
assert.Equal(t, strings.Contains(outputStr, "--appEnv"), true, "output should contain appEnv flag")
|
||||
assert.Equal(t, strings.Contains(outputStr, "--port"), true, "output should contain port flag")
|
||||
assert.Equal(t, strings.Contains(outputStr, "--webUrl"), true, "output should contain webUrl flag")
|
||||
assert.Equal(t, strings.Contains(outputStr, "--dbPath"), true, "output should contain dbPath flag")
|
||||
assert.Equal(t, strings.Contains(outputStr, "--disableRegistration"), true, "output should contain disableRegistration flag")
|
||||
}
|
||||
|
||||
func TestServerStartInvalidConfig(t *testing.T) {
|
||||
|
|
@ -166,7 +166,7 @@ func TestServerStartInvalidConfig(t *testing.T) {
|
|||
assert.Equal(t, strings.Contains(outputStr, "Error:"), true, "output should contain error message")
|
||||
assert.Equal(t, strings.Contains(outputStr, "Invalid WebURL"), true, "output should mention invalid WebURL")
|
||||
assert.Equal(t, strings.Contains(outputStr, "dnote-server start [flags]"), true, "output should show usage")
|
||||
assert.Equal(t, strings.Contains(outputStr, "-webUrl"), true, "output should show flags")
|
||||
assert.Equal(t, strings.Contains(outputStr, "--webUrl"), true, "output should show flags")
|
||||
}
|
||||
|
||||
func TestServerUnknownCommand(t *testing.T) {
|
||||
|
|
@ -321,3 +321,19 @@ func TestServerUserRemove(t *testing.T) {
|
|||
db.Table("users").Count(&count)
|
||||
assert.Equal(t, count, int64(0), "should have 0 users after removal")
|
||||
}
|
||||
|
||||
func TestServerUserCreateHelp(t *testing.T) {
|
||||
cmd := exec.Command(testServerBinary, "user", "create", "--help")
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("help command failed: %v\nOutput: %s", err, output)
|
||||
}
|
||||
|
||||
outputStr := string(output)
|
||||
|
||||
// Verify help shows double-dash flags for consistency with CLI
|
||||
assert.Equal(t, strings.Contains(outputStr, "--email"), true, "help should show --email (double dash)")
|
||||
assert.Equal(t, strings.Contains(outputStr, "--password"), true, "help should show --password (double dash)")
|
||||
assert.Equal(t, strings.Contains(outputStr, "--dbPath"), true, "help should show --dbPath (double dash)")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,12 +41,11 @@ func (a *App) CreateBook(user database.User, name string) (database.Book, error)
|
|||
}
|
||||
|
||||
book := database.Book{
|
||||
UUID: uuid,
|
||||
UserID: user.ID,
|
||||
Label: name,
|
||||
AddedOn: a.Clock.Now().UnixNano(),
|
||||
USN: nextUSN,
|
||||
Encrypted: false,
|
||||
UUID: uuid,
|
||||
UserID: user.ID,
|
||||
Label: name,
|
||||
AddedOn: a.Clock.Now().UnixNano(),
|
||||
USN: nextUSN,
|
||||
}
|
||||
if err := tx.Create(&book).Error; err != nil {
|
||||
tx.Rollback()
|
||||
|
|
@ -99,8 +98,6 @@ func (a *App) UpdateBook(tx *gorm.DB, user database.User, book database.Book, la
|
|||
book.USN = nextUSN
|
||||
book.EditedOn = a.Clock.Now().UnixNano()
|
||||
book.Deleted = false
|
||||
// TODO: remove after all users have been migrated
|
||||
book.Encrypted = false
|
||||
|
||||
if err := tx.Save(&book).Error; err != nil {
|
||||
return book, errors.Wrap(err, "updating the book")
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import (
|
|||
|
||||
// CreateNote creates a note with the next usn and updates the user's max_usn.
|
||||
// It returns the created note.
|
||||
func (a *App) CreateNote(user database.User, bookUUID, content string, addedOn *int64, editedOn *int64, public bool, client string) (database.Note, error) {
|
||||
func (a *App) CreateNote(user database.User, bookUUID, content string, addedOn *int64, editedOn *int64, client string) (database.Note, error) {
|
||||
tx := a.DB.Begin()
|
||||
|
||||
nextUSN, err := incrementUserUSN(tx, user.ID)
|
||||
|
|
@ -59,16 +59,14 @@ func (a *App) CreateNote(user database.User, bookUUID, content string, addedOn *
|
|||
}
|
||||
|
||||
note := database.Note{
|
||||
UUID: uuid,
|
||||
BookUUID: bookUUID,
|
||||
UserID: user.ID,
|
||||
AddedOn: noteAddedOn,
|
||||
EditedOn: noteEditedOn,
|
||||
USN: nextUSN,
|
||||
Body: content,
|
||||
Public: public,
|
||||
Encrypted: false,
|
||||
Client: client,
|
||||
UUID: uuid,
|
||||
BookUUID: bookUUID,
|
||||
UserID: user.ID,
|
||||
AddedOn: noteAddedOn,
|
||||
EditedOn: noteEditedOn,
|
||||
USN: nextUSN,
|
||||
Body: content,
|
||||
Client: client,
|
||||
}
|
||||
if err := tx.Create(¬e).Error; err != nil {
|
||||
tx.Rollback()
|
||||
|
|
@ -84,7 +82,6 @@ func (a *App) CreateNote(user database.User, bookUUID, content string, addedOn *
|
|||
type UpdateNoteParams struct {
|
||||
BookUUID *string
|
||||
Content *string
|
||||
Public *bool
|
||||
}
|
||||
|
||||
// GetBookUUID gets the bookUUID from the UpdateNoteParams
|
||||
|
|
@ -105,15 +102,6 @@ func (r UpdateNoteParams) GetContent() string {
|
|||
return *r.Content
|
||||
}
|
||||
|
||||
// GetPublic gets the public field from the UpdateNoteParams
|
||||
func (r UpdateNoteParams) GetPublic() bool {
|
||||
if r.Public == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return *r.Public
|
||||
}
|
||||
|
||||
// UpdateNote creates a note with the next usn and updates the user's max_usn
|
||||
func (a *App) UpdateNote(tx *gorm.DB, user database.User, note database.Note, p *UpdateNoteParams) (database.Note, error) {
|
||||
nextUSN, err := incrementUserUSN(tx, user.ID)
|
||||
|
|
@ -127,15 +115,10 @@ func (a *App) UpdateNote(tx *gorm.DB, user database.User, note database.Note, p
|
|||
if p.Content != nil {
|
||||
note.Body = p.GetContent()
|
||||
}
|
||||
if p.Public != nil {
|
||||
note.Public = p.GetPublic()
|
||||
}
|
||||
|
||||
note.USN = nextUSN
|
||||
note.EditedOn = a.Clock.Now().UnixNano()
|
||||
note.Deleted = false
|
||||
// TODO: remove after all users are migrated
|
||||
note.Encrypted = false
|
||||
|
||||
if err := tx.Save(¬e).Error; err != nil {
|
||||
return note, pkgErrors.Wrap(err, "editing note")
|
||||
|
|
@ -180,13 +163,12 @@ func (a *App) GetUserNoteByUUID(userID int, uuid string) (*database.Note, error)
|
|||
|
||||
// GetNotesParams is params for finding notes
|
||||
type GetNotesParams struct {
|
||||
Year int
|
||||
Month int
|
||||
Page int
|
||||
Books []string
|
||||
Search string
|
||||
Encrypted bool
|
||||
PerPage int
|
||||
Year int
|
||||
Month int
|
||||
Page int
|
||||
Books []string
|
||||
Search string
|
||||
PerPage int
|
||||
}
|
||||
|
||||
type ftsParams struct {
|
||||
|
|
@ -215,14 +197,13 @@ notes.added_on,
|
|||
notes.edited_on,
|
||||
notes.usn,
|
||||
notes.deleted,
|
||||
notes.encrypted,
|
||||
` + bodyExpr)
|
||||
}
|
||||
|
||||
func getNotesBaseQuery(db *gorm.DB, userID int, q GetNotesParams) *gorm.DB {
|
||||
conn := db.Where(
|
||||
"notes.user_id = ? AND notes.deleted = ? AND notes.encrypted = ?",
|
||||
userID, false, q.Encrypted,
|
||||
"notes.user_id = ? AND notes.deleted = ?",
|
||||
userID, false,
|
||||
)
|
||||
|
||||
if q.Search != "" {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ func TestCreateNote(t *testing.T) {
|
|||
a.DB = db
|
||||
a.Clock = mockClock
|
||||
|
||||
if _, err := a.CreateNote(user, b1.UUID, "note content", tc.addedOn, tc.editedOn, false, ""); err != nil {
|
||||
if _, err := a.CreateNote(user, b1.UUID, "note content", tc.addedOn, tc.editedOn, ""); err != nil {
|
||||
t.Fatal(errors.Wrapf(err, "creating note for test case %d", idx))
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +139,7 @@ func TestCreateNote_EmptyBody(t *testing.T) {
|
|||
a.Clock = clock.NewMock()
|
||||
|
||||
// Create note with empty body
|
||||
note, err := a.CreateNote(user, b1.UUID, "", nil, nil, false, "")
|
||||
note, err := a.CreateNote(user, b1.UUID, "", nil, nil, "")
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "creating note with empty body"))
|
||||
}
|
||||
|
|
@ -188,7 +188,6 @@ func TestUpdateNote(t *testing.T) {
|
|||
|
||||
c := clock.NewMock()
|
||||
content := "updated test content"
|
||||
public := true
|
||||
|
||||
a := NewTest()
|
||||
a.DB = db
|
||||
|
|
@ -197,7 +196,6 @@ func TestUpdateNote(t *testing.T) {
|
|||
tx := db.Begin()
|
||||
if _, err := a.UpdateNote(tx, user, note, &UpdateNoteParams{
|
||||
Content: &content,
|
||||
Public: &public,
|
||||
}); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "updating note"))
|
||||
|
|
@ -218,7 +216,6 @@ func TestUpdateNote(t *testing.T) {
|
|||
assert.Equal(t, noteCount, int64(1), "note count mismatch")
|
||||
assert.Equal(t, noteRecord.UserID, user.ID, "note UserID mismatch")
|
||||
assert.Equal(t, noteRecord.Body, content, "note Body mismatch")
|
||||
assert.Equal(t, noteRecord.Public, public, "note Public mismatch")
|
||||
assert.Equal(t, noteRecord.Deleted, false, "note Deleted mismatch")
|
||||
assert.Equal(t, noteRecord.USN, expectedUSN, "note USN mismatch")
|
||||
assert.Equal(t, userRecord.MaxUSN, expectedUSN, "user MaxUSN mismatch")
|
||||
|
|
@ -374,10 +371,9 @@ func TestGetNotes_FTSSearch(t *testing.T) {
|
|||
|
||||
// Search "baz"
|
||||
result, err := a.GetNotes(user.ID, GetNotesParams{
|
||||
Search: "baz",
|
||||
Encrypted: false,
|
||||
Page: 1,
|
||||
PerPage: 30,
|
||||
Search: "baz",
|
||||
Page: 1,
|
||||
PerPage: 30,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "getting notes with FTS search"))
|
||||
|
|
@ -390,10 +386,9 @@ func TestGetNotes_FTSSearch(t *testing.T) {
|
|||
|
||||
// Search for "running" - should return 1 note
|
||||
result, err = a.GetNotes(user.ID, GetNotesParams{
|
||||
Search: "running",
|
||||
Encrypted: false,
|
||||
Page: 1,
|
||||
PerPage: 30,
|
||||
Search: "running",
|
||||
Page: 1,
|
||||
PerPage: 30,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "getting notes with FTS search for review"))
|
||||
|
|
@ -405,10 +400,9 @@ func TestGetNotes_FTSSearch(t *testing.T) {
|
|||
|
||||
// Search for non-existent term - should return 0 notes
|
||||
result, err = a.GetNotes(user.ID, GetNotesParams{
|
||||
Search: "nonexistent",
|
||||
Encrypted: false,
|
||||
Page: 1,
|
||||
PerPage: 30,
|
||||
Search: "nonexistent",
|
||||
Page: 1,
|
||||
PerPage: 30,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "getting notes with FTS search for nonexistent"))
|
||||
|
|
@ -437,10 +431,9 @@ func TestGetNotes_FTSSearch_Snippet(t *testing.T) {
|
|||
|
||||
// Search for "keyword" in long note - should return snippet with "..."
|
||||
result, err := a.GetNotes(user.ID, GetNotesParams{
|
||||
Search: "keyword",
|
||||
Encrypted: false,
|
||||
Page: 1,
|
||||
PerPage: 30,
|
||||
Search: "keyword",
|
||||
Page: 1,
|
||||
PerPage: 30,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "getting notes with FTS search for keyword"))
|
||||
|
|
@ -472,10 +465,9 @@ func TestGetNotes_FTSSearch_ShortWord(t *testing.T) {
|
|||
a.Clock = clock.NewMock()
|
||||
|
||||
result, err := a.GetNotes(user.ID, GetNotesParams{
|
||||
Search: "a",
|
||||
Encrypted: false,
|
||||
Page: 1,
|
||||
PerPage: 30,
|
||||
Search: "a",
|
||||
Page: 1,
|
||||
PerPage: 30,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "getting notes with FTS search for 'a'"))
|
||||
|
|
@ -504,10 +496,9 @@ func TestGetNotes_All(t *testing.T) {
|
|||
a.Clock = clock.NewMock()
|
||||
|
||||
result, err := a.GetNotes(user.ID, GetNotesParams{
|
||||
Search: "",
|
||||
Encrypted: false,
|
||||
Page: 1,
|
||||
PerPage: 30,
|
||||
Search: "",
|
||||
Page: 1,
|
||||
PerPage: 30,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "getting notes with FTS search for 'a'"))
|
||||
|
|
|
|||
|
|
@ -65,6 +65,29 @@ func initApp(cfg config.Config) app.App {
|
|||
}
|
||||
}
|
||||
|
||||
// printFlags prints flags with -- prefix for consistency with CLI
|
||||
func printFlags(fs *flag.FlagSet) {
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
fmt.Printf(" --%s", f.Name)
|
||||
|
||||
// Print type hint for non-boolean flags
|
||||
name, usage := flag.UnquoteUsage(f)
|
||||
if name != "" {
|
||||
fmt.Printf(" %s", name)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// Print usage description with indentation
|
||||
if usage != "" {
|
||||
fmt.Printf(" \t%s", usage)
|
||||
if f.DefValue != "" && f.DefValue != "false" {
|
||||
fmt.Printf(" (default: %s)", f.DefValue)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// setupFlagSet creates a FlagSet with standard usage format
|
||||
func setupFlagSet(name, usageCmd string) *flag.FlagSet {
|
||||
fs := flag.NewFlagSet(name, flag.ExitOnError)
|
||||
|
|
@ -74,7 +97,7 @@ func setupFlagSet(name, usageCmd string) *flag.FlagSet {
|
|||
|
||||
Flags:
|
||||
`, usageCmd)
|
||||
fs.PrintDefaults()
|
||||
printFlags(fs)
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,22 +56,11 @@ func (b *Books) getBooks(r *http.Request) ([]database.Book, error) {
|
|||
|
||||
query := r.URL.Query()
|
||||
name := query.Get("name")
|
||||
encryptedStr := query.Get("encrypted")
|
||||
|
||||
if name != "" {
|
||||
part := fmt.Sprintf("%%%s%%", name)
|
||||
conn = conn.Where("LOWER(label) LIKE ?", part)
|
||||
}
|
||||
if encryptedStr != "" {
|
||||
var encrypted bool
|
||||
if encryptedStr == "true" {
|
||||
encrypted = true
|
||||
} else {
|
||||
encrypted = false
|
||||
}
|
||||
|
||||
conn = conn.Where("encrypted = ?", encrypted)
|
||||
}
|
||||
|
||||
var books []database.Book
|
||||
if err := conn.Find(&books).Error; err != nil {
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ func parseGetNotesQuery(q url.Values) (app.GetNotesParams, error) {
|
|||
yearStr := q.Get("year")
|
||||
monthStr := q.Get("month")
|
||||
books := q["book"]
|
||||
encryptedStr := q.Get("encrypted")
|
||||
pageStr := q.Get("page")
|
||||
|
||||
page, err := parsePageQuery(q)
|
||||
|
|
@ -107,21 +106,13 @@ func parseGetNotesQuery(q url.Values) (app.GetNotesParams, error) {
|
|||
month = m
|
||||
}
|
||||
|
||||
var encrypted bool
|
||||
if strings.ToLower(encryptedStr) == "true" {
|
||||
encrypted = true
|
||||
} else {
|
||||
encrypted = false
|
||||
}
|
||||
|
||||
ret := app.GetNotesParams{
|
||||
Year: year,
|
||||
Month: month,
|
||||
Page: page,
|
||||
Search: parseSearchQuery(q),
|
||||
Books: books,
|
||||
Encrypted: encrypted,
|
||||
PerPage: notesPerPage,
|
||||
Year: year,
|
||||
Month: month,
|
||||
Page: page,
|
||||
Search: parseSearchQuery(q),
|
||||
Books: books,
|
||||
PerPage: notesPerPage,
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
|
|
@ -231,7 +222,7 @@ func (n *Notes) create(r *http.Request) (database.Note, error) {
|
|||
}
|
||||
|
||||
client := getClientType(r)
|
||||
note, err := n.app.CreateNote(*user, params.BookUUID, params.Content, params.AddedOn, params.EditedOn, false, client)
|
||||
note, err := n.app.CreateNote(*user, params.BookUUID, params.Content, params.AddedOn, params.EditedOn, client)
|
||||
if err != nil {
|
||||
return database.Note{}, errors.Wrap(err, "creating note")
|
||||
}
|
||||
|
|
@ -310,11 +301,10 @@ func (n *Notes) V3Delete(w http.ResponseWriter, r *http.Request) {
|
|||
type updateNotePayload struct {
|
||||
BookUUID *string `schema:"book_uuid" json:"book_uuid"`
|
||||
Content *string `schema:"content" json:"content"`
|
||||
Public *bool `schema:"public" json:"public"`
|
||||
}
|
||||
|
||||
func validateUpdateNotePayload(p updateNotePayload) error {
|
||||
if p.BookUUID == nil && p.Content == nil && p.Public == nil {
|
||||
if p.BookUUID == nil && p.Content == nil {
|
||||
return app.ErrEmptyUpdate
|
||||
}
|
||||
|
||||
|
|
@ -350,7 +340,6 @@ func (n *Notes) update(r *http.Request) (database.Note, error) {
|
|||
note, err = n.app.UpdateNote(tx, *user, note, &app.UpdateNoteParams{
|
||||
BookUUID: params.BookUUID,
|
||||
Content: params.Content,
|
||||
Public: params.Public,
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ func getExpectedNotePayload(n database.Note, b database.Book, u database.User) p
|
|||
UpdatedAt: truncateMicro(n.UpdatedAt),
|
||||
Body: n.Body,
|
||||
AddedOn: n.AddedOn,
|
||||
Public: n.Public,
|
||||
USN: n.USN,
|
||||
Book: presenters.NoteBook{
|
||||
UUID: b.UUID,
|
||||
|
|
@ -189,7 +188,9 @@ func TestGetNote(t *testing.T) {
|
|||
defer server.Close()
|
||||
|
||||
user := testutils.SetupUserData(db)
|
||||
testutils.SetupAccountData(db, user, "user@test.com", "pass1234")
|
||||
anotherUser := testutils.SetupUserData(db)
|
||||
testutils.SetupAccountData(db, anotherUser, "another@test.com", "pass1234")
|
||||
|
||||
b1 := database.Book{
|
||||
UUID: testutils.MustUUID(t),
|
||||
|
|
@ -198,22 +199,13 @@ func TestGetNote(t *testing.T) {
|
|||
}
|
||||
testutils.MustExec(t, db.Save(&b1), "preparing b1")
|
||||
|
||||
privateNote := database.Note{
|
||||
note := database.Note{
|
||||
UUID: testutils.MustUUID(t),
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Body: "privateNote content",
|
||||
Public: false,
|
||||
Body: "note content",
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&privateNote), "preparing privateNote")
|
||||
publicNote := database.Note{
|
||||
UUID: testutils.MustUUID(t),
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Body: "publicNote content",
|
||||
Public: true,
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&publicNote), "preparing publicNote")
|
||||
testutils.MustExec(t, db.Save(¬e), "preparing note")
|
||||
deletedNote := database.Note{
|
||||
UUID: testutils.MustUUID(t),
|
||||
UserID: user.ID,
|
||||
|
|
@ -226,9 +218,9 @@ func TestGetNote(t *testing.T) {
|
|||
return fmt.Sprintf("/api/v3/notes/%s", noteUUID)
|
||||
}
|
||||
|
||||
t.Run("owner accessing private note", func(t *testing.T) {
|
||||
t.Run("owner accessing note", func(t *testing.T) {
|
||||
// Execute
|
||||
url := getURL(publicNote.UUID)
|
||||
url := getURL(note.UUID)
|
||||
req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
res := testutils.HTTPAuthDo(t, db, req, user)
|
||||
|
||||
|
|
@ -240,58 +232,16 @@ func TestGetNote(t *testing.T) {
|
|||
t.Fatal(errors.Wrap(err, "decoding payload"))
|
||||
}
|
||||
|
||||
var n2Record database.Note
|
||||
testutils.MustExec(t, db.Where("uuid = ?", publicNote.UUID).First(&n2Record), "finding n2Record")
|
||||
var noteRecord database.Note
|
||||
testutils.MustExec(t, db.Where("uuid = ?", note.UUID).First(¬eRecord), "finding noteRecord")
|
||||
|
||||
expected := getExpectedNotePayload(n2Record, b1, user)
|
||||
expected := getExpectedNotePayload(noteRecord, b1, user)
|
||||
assert.DeepEqual(t, payload, expected, "payload mismatch")
|
||||
})
|
||||
|
||||
t.Run("owner accessing public note", func(t *testing.T) {
|
||||
t.Run("non-owner accessing note", func(t *testing.T) {
|
||||
// Execute
|
||||
url := getURL(publicNote.UUID)
|
||||
req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
res := testutils.HTTPAuthDo(t, db, 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, 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 := getURL(publicNote.UUID)
|
||||
req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
res := testutils.HTTPAuthDo(t, db, 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, 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 := getURL(privateNote.UUID)
|
||||
url := getURL(note.UUID)
|
||||
req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
res := testutils.HTTPAuthDo(t, db, req, anotherUser)
|
||||
|
||||
|
|
@ -306,42 +256,21 @@ func TestGetNote(t *testing.T) {
|
|||
assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
|
||||
})
|
||||
|
||||
t.Run("guest accessing public note", func(t *testing.T) {
|
||||
t.Run("guest accessing note", func(t *testing.T) {
|
||||
// Execute
|
||||
url := getURL(publicNote.UUID)
|
||||
url := getURL(note.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, 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 := getURL(privateNote.UUID)
|
||||
req := testutils.MakeReq(server.URL, "GET", url, "")
|
||||
res := testutils.HTTPDo(t, req)
|
||||
|
||||
// Test
|
||||
assert.StatusCodeEquals(t, res, http.StatusNotFound, "")
|
||||
assert.StatusCodeEquals(t, res, http.StatusUnauthorized, "")
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "reading body"))
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, string(body), "not found\n", "payload mismatch")
|
||||
assert.DeepEqual(t, string(body), "unauthorized\n", "payload mismatch")
|
||||
})
|
||||
|
||||
t.Run("nonexistent", func(t *testing.T) {
|
||||
|
|
@ -533,7 +462,6 @@ func TestUpdateNote(t *testing.T) {
|
|||
type payloadData struct {
|
||||
Content *string `schema:"content" json:"content,omitempty"`
|
||||
BookUUID *string `schema:"book_uuid" json:"book_uuid,omitempty"`
|
||||
Public *bool `schema:"public" json:"public,omitempty"`
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
|
|
@ -541,12 +469,10 @@ func TestUpdateNote(t *testing.T) {
|
|||
noteUUID string
|
||||
noteBookUUID string
|
||||
noteBody string
|
||||
notePublic bool
|
||||
noteDeleted bool
|
||||
expectedNoteBody string
|
||||
expectedNoteBookName string
|
||||
expectedNoteBookUUID string
|
||||
expectedNotePublic bool
|
||||
}{
|
||||
{
|
||||
payload: testutils.PayloadWrapper{
|
||||
|
|
@ -556,13 +482,11 @@ func TestUpdateNote(t *testing.T) {
|
|||
},
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: false,
|
||||
noteBody: "original content",
|
||||
noteDeleted: false,
|
||||
expectedNoteBookUUID: b1UUID,
|
||||
expectedNoteBody: "some updated content",
|
||||
expectedNoteBookName: "css",
|
||||
expectedNotePublic: false,
|
||||
},
|
||||
{
|
||||
payload: testutils.PayloadWrapper{
|
||||
|
|
@ -572,13 +496,11 @@ func TestUpdateNote(t *testing.T) {
|
|||
},
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: false,
|
||||
noteBody: "original content",
|
||||
noteDeleted: false,
|
||||
expectedNoteBookUUID: b1UUID,
|
||||
expectedNoteBody: "original content",
|
||||
expectedNoteBookName: "css",
|
||||
expectedNotePublic: false,
|
||||
},
|
||||
{
|
||||
payload: testutils.PayloadWrapper{
|
||||
|
|
@ -588,13 +510,11 @@ func TestUpdateNote(t *testing.T) {
|
|||
},
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: false,
|
||||
noteBody: "original content",
|
||||
noteDeleted: false,
|
||||
expectedNoteBookUUID: b2UUID,
|
||||
expectedNoteBody: "original content",
|
||||
expectedNoteBookName: "js",
|
||||
expectedNotePublic: false,
|
||||
},
|
||||
{
|
||||
payload: testutils.PayloadWrapper{
|
||||
|
|
@ -605,13 +525,11 @@ func TestUpdateNote(t *testing.T) {
|
|||
},
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: false,
|
||||
noteBody: "original content",
|
||||
noteDeleted: false,
|
||||
expectedNoteBookUUID: b2UUID,
|
||||
expectedNoteBody: "some updated content",
|
||||
expectedNoteBookName: "js",
|
||||
expectedNotePublic: false,
|
||||
},
|
||||
{
|
||||
payload: testutils.PayloadWrapper{
|
||||
|
|
@ -622,80 +540,11 @@ func TestUpdateNote(t *testing.T) {
|
|||
},
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: false,
|
||||
noteBody: "",
|
||||
noteDeleted: true,
|
||||
expectedNoteBookUUID: b1UUID,
|
||||
expectedNoteBody: updatedBody,
|
||||
expectedNoteBookName: "js",
|
||||
expectedNotePublic: false,
|
||||
},
|
||||
{
|
||||
payload: testutils.PayloadWrapper{
|
||||
Data: payloadData{
|
||||
Public: &testutils.TrueVal,
|
||||
},
|
||||
},
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: false,
|
||||
noteBody: "original content",
|
||||
noteDeleted: false,
|
||||
expectedNoteBookUUID: b1UUID,
|
||||
expectedNoteBody: "original content",
|
||||
expectedNoteBookName: "css",
|
||||
expectedNotePublic: true,
|
||||
},
|
||||
{
|
||||
payload: testutils.PayloadWrapper{
|
||||
Data: payloadData{
|
||||
Public: &testutils.FalseVal,
|
||||
},
|
||||
},
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: true,
|
||||
noteBody: "original content",
|
||||
noteDeleted: false,
|
||||
expectedNoteBookUUID: b1UUID,
|
||||
expectedNoteBody: "original content",
|
||||
expectedNoteBookName: "css",
|
||||
expectedNotePublic: false,
|
||||
},
|
||||
{
|
||||
payload: testutils.PayloadWrapper{
|
||||
Data: payloadData{
|
||||
Content: &updatedBody,
|
||||
Public: &testutils.FalseVal,
|
||||
},
|
||||
},
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: true,
|
||||
noteBody: "original content",
|
||||
noteDeleted: false,
|
||||
expectedNoteBookUUID: b1UUID,
|
||||
expectedNoteBody: updatedBody,
|
||||
expectedNoteBookName: "css",
|
||||
expectedNotePublic: false,
|
||||
},
|
||||
{
|
||||
payload: testutils.PayloadWrapper{
|
||||
Data: payloadData{
|
||||
BookUUID: &b2UUID,
|
||||
Content: &updatedBody,
|
||||
Public: &testutils.TrueVal,
|
||||
},
|
||||
},
|
||||
noteUUID: "ab50aa32-b232-40d8-b10f-10a7f9134053",
|
||||
noteBookUUID: b1UUID,
|
||||
notePublic: false,
|
||||
noteBody: "original content",
|
||||
noteDeleted: false,
|
||||
expectedNoteBookUUID: b2UUID,
|
||||
expectedNoteBody: updatedBody,
|
||||
expectedNoteBookName: "js",
|
||||
expectedNotePublic: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -734,7 +583,6 @@ func TestUpdateNote(t *testing.T) {
|
|||
BookUUID: tc.noteBookUUID,
|
||||
Body: tc.noteBody,
|
||||
Deleted: tc.noteDeleted,
|
||||
Public: tc.notePublic,
|
||||
}
|
||||
testutils.MustExec(t, db.Save(¬e), "preparing note")
|
||||
|
||||
|
|
@ -765,7 +613,6 @@ func TestUpdateNote(t *testing.T) {
|
|||
assert.Equal(t, noteRecord.UUID, tc.noteUUID, "note uuid mismatch for test case")
|
||||
assert.Equal(t, noteRecord.Body, tc.expectedNoteBody, "note content mismatch for test case")
|
||||
assert.Equal(t, noteRecord.BookUUID, tc.expectedNoteBookUUID, "note book_uuid mismatch for test case")
|
||||
assert.Equal(t, noteRecord.Public, tc.expectedNotePublic, "note public mismatch for test case")
|
||||
assert.Equal(t, noteRecord.USN, 102, "note usn mismatch for test case")
|
||||
|
||||
assert.Equal(t, userRecord.MaxUSN, 102, "user max_usn mismatch for test case")
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ func NewAPIRoutes(a *app.App, c *Controllers) []Route {
|
|||
{"POST", "/v3/signout", c.Users.V3Logout, true},
|
||||
{"OPTIONS", "/v3/signout", c.Users.logoutOptions, true},
|
||||
{"GET", "/v3/notes", mw.Auth(a.DB, c.Notes.V3Index, nil), true},
|
||||
{"GET", "/v3/notes/{noteUUID}", c.Notes.V3Show, true},
|
||||
{"GET", "/v3/notes/{noteUUID}", mw.Auth(a.DB, c.Notes.V3Show, nil), true},
|
||||
{"POST", "/v3/notes", mw.Auth(a.DB, c.Notes.V3Create, nil), true},
|
||||
{"DELETE", "/v3/notes/{noteUUID}", mw.Auth(a.DB, c.Notes.V3Delete, nil), true},
|
||||
{"PATCH", "/v3/notes/{noteUUID}", mw.Auth(a.DB, c.Notes.V3Update, nil), true},
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ type SyncFragNote struct {
|
|||
AddedOn int64 `json:"added_on"`
|
||||
EditedOn int64 `json:"edited_on"`
|
||||
Body string `json:"content"`
|
||||
Public bool `json:"public"`
|
||||
Deleted bool `json:"deleted"`
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +88,6 @@ func NewFragNote(note database.Note) SyncFragNote {
|
|||
AddedOn: note.AddedOn,
|
||||
EditedOn: note.EditedOn,
|
||||
Body: note.Body,
|
||||
Public: note.Public,
|
||||
Deleted: note.Deleted,
|
||||
BookUUID: note.BookUUID,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ type Book struct {
|
|||
EditedOn int64 `json:"edited_on"`
|
||||
USN int `json:"-" gorm:"index"`
|
||||
Deleted bool `json:"-" gorm:"default:false"`
|
||||
Encrypted bool `json:"-" gorm:"default:false"`
|
||||
}
|
||||
|
||||
// Note is a model for a note
|
||||
|
|
@ -54,10 +53,8 @@ type Note struct {
|
|||
Body string `json:"content"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
EditedOn int64 `json:"edited_on"`
|
||||
Public bool `json:"public" gorm:"default:false"`
|
||||
USN int `json:"-" gorm:"index"`
|
||||
Deleted bool `json:"-" gorm:"default:false"`
|
||||
Encrypted bool `json:"-" gorm:"default:false"`
|
||||
Client string `gorm:"index"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,11 @@ func WithAccount(db *gorm.DB, next http.HandlerFunc) http.HandlerFunc {
|
|||
user := context.User(r.Context())
|
||||
|
||||
var account database.Account
|
||||
if err := db.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
|
||||
err := db.Where("user_id = ?", user.ID).First(&account).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
DoError(w, "account not found", err, http.StatusForbidden)
|
||||
return
|
||||
} else if err != nil {
|
||||
DoError(w, "finding account", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,3 +233,37 @@ func TestTokenAuth(t *testing.T) {
|
|||
assert.Equal(t, res.StatusCode, http.StatusUnauthorized, "status code mismatch")
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithAccount(t *testing.T) {
|
||||
db := testutils.InitMemoryDB(t)
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
t.Run("user with account", func(t *testing.T) {
|
||||
user := testutils.SetupUserData(db)
|
||||
testutils.SetupAccountData(db, user, "alice@test.com", "pass1234")
|
||||
|
||||
server := httptest.NewServer(Auth(db, handler, nil))
|
||||
defer server.Close()
|
||||
|
||||
req := testutils.MakeReq(server.URL, "GET", "/", "")
|
||||
res := testutils.HTTPAuthDo(t, db, req, user)
|
||||
|
||||
assert.Equal(t, res.StatusCode, http.StatusOK, "status code mismatch")
|
||||
})
|
||||
|
||||
t.Run("user without account", func(t *testing.T) {
|
||||
user := testutils.SetupUserData(db)
|
||||
// Note: not creating account for this user
|
||||
|
||||
server := httptest.NewServer(Auth(db, handler, nil))
|
||||
defer server.Close()
|
||||
|
||||
req := testutils.MakeReq(server.URL, "GET", "/", "")
|
||||
res := testutils.HTTPAuthDo(t, db, req, user)
|
||||
|
||||
assert.Equal(t, res.StatusCode, http.StatusForbidden, "status code mismatch")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,29 +40,17 @@ func TestGetNote(t *testing.T) {
|
|||
}
|
||||
testutils.MustExec(t, db.Save(&b1), "preparing b1")
|
||||
|
||||
privateNote := database.Note{
|
||||
note := database.Note{
|
||||
UUID: testutils.MustUUID(t),
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Body: "privateNote content",
|
||||
Body: "note content",
|
||||
Deleted: false,
|
||||
Public: false,
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&privateNote), "preparing privateNote")
|
||||
testutils.MustExec(t, db.Save(¬e), "preparing note")
|
||||
|
||||
publicNote := database.Note{
|
||||
UUID: testutils.MustUUID(t),
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Body: "privateNote content",
|
||||
Deleted: false,
|
||||
Public: true,
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&publicNote), "preparing privateNote")
|
||||
|
||||
var privateNoteRecord, publicNoteRecord database.Note
|
||||
testutils.MustExec(t, db.Where("uuid = ?", privateNote.UUID).Preload("Book").Preload("User").First(&privateNoteRecord), "finding privateNote")
|
||||
testutils.MustExec(t, db.Where("uuid = ?", publicNote.UUID).Preload("Book").Preload("User").First(&publicNoteRecord), "finding publicNote")
|
||||
var noteRecord database.Note
|
||||
testutils.MustExec(t, db.Where("uuid = ?", note.UUID).Preload("Book").Preload("User").First(¬eRecord), "finding note")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
|
@ -72,40 +60,26 @@ func TestGetNote(t *testing.T) {
|
|||
expectedNote database.Note
|
||||
}{
|
||||
{
|
||||
name: "owner accessing private note",
|
||||
name: "owner accessing note",
|
||||
user: user,
|
||||
note: privateNote,
|
||||
note: note,
|
||||
expectedOK: true,
|
||||
expectedNote: privateNoteRecord,
|
||||
expectedNote: noteRecord,
|
||||
},
|
||||
{
|
||||
name: "non-owner accessing private note",
|
||||
name: "non-owner accessing note",
|
||||
user: anotherUser,
|
||||
note: privateNote,
|
||||
note: note,
|
||||
expectedOK: false,
|
||||
expectedNote: database.Note{},
|
||||
},
|
||||
{
|
||||
name: "non-owner accessing public note",
|
||||
user: anotherUser,
|
||||
note: publicNote,
|
||||
expectedOK: true,
|
||||
expectedNote: publicNoteRecord,
|
||||
},
|
||||
{
|
||||
name: "guest accessing private note",
|
||||
name: "guest accessing note",
|
||||
user: database.User{},
|
||||
note: privateNote,
|
||||
note: note,
|
||||
expectedOK: false,
|
||||
expectedNote: database.Note{},
|
||||
},
|
||||
{
|
||||
name: "guest accessing public note",
|
||||
user: database.User{},
|
||||
note: publicNote,
|
||||
expectedOK: true,
|
||||
expectedNote: publicNoteRecord,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
|
@ -139,7 +113,6 @@ func TestGetNote_nonexistent(t *testing.T) {
|
|||
BookUUID: b1.UUID,
|
||||
Body: "n1 content",
|
||||
Deleted: false,
|
||||
Public: false,
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&n1), "preparing n1")
|
||||
|
||||
|
|
|
|||
|
|
@ -24,9 +24,6 @@ import (
|
|||
|
||||
// ViewNote checks if the given user can view the given note
|
||||
func ViewNote(user *database.User, note database.Note) bool {
|
||||
if note.Public {
|
||||
return true
|
||||
}
|
||||
if user == nil {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,53 +39,27 @@ func TestViewNote(t *testing.T) {
|
|||
}
|
||||
testutils.MustExec(t, db.Save(&b1), "preparing b1")
|
||||
|
||||
privateNote := database.Note{
|
||||
note := database.Note{
|
||||
UUID: testutils.MustUUID(t),
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Body: "privateNote content",
|
||||
Body: "note content",
|
||||
Deleted: false,
|
||||
Public: false,
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&privateNote), "preparing privateNote")
|
||||
testutils.MustExec(t, db.Save(¬e), "preparing note")
|
||||
|
||||
publicNote := database.Note{
|
||||
UUID: testutils.MustUUID(t),
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Body: "privateNote content",
|
||||
Deleted: false,
|
||||
Public: true,
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&publicNote), "preparing privateNote")
|
||||
|
||||
t.Run("owner accessing private note", func(t *testing.T) {
|
||||
result := ViewNote(&user, privateNote)
|
||||
t.Run("owner accessing note", func(t *testing.T) {
|
||||
result := ViewNote(&user, note)
|
||||
assert.Equal(t, result, true, "result mismatch")
|
||||
})
|
||||
|
||||
t.Run("owner accessing public note", func(t *testing.T) {
|
||||
result := ViewNote(&user, publicNote)
|
||||
assert.Equal(t, result, true, "result mismatch")
|
||||
})
|
||||
|
||||
t.Run("non-owner accessing private note", func(t *testing.T) {
|
||||
result := ViewNote(&anotherUser, privateNote)
|
||||
t.Run("non-owner accessing note", func(t *testing.T) {
|
||||
result := ViewNote(&anotherUser, note)
|
||||
assert.Equal(t, result, false, "result mismatch")
|
||||
})
|
||||
|
||||
t.Run("non-owner accessing public note", func(t *testing.T) {
|
||||
result := ViewNote(&anotherUser, publicNote)
|
||||
assert.Equal(t, result, true, "result mismatch")
|
||||
})
|
||||
|
||||
t.Run("guest accessing private note", func(t *testing.T) {
|
||||
result := ViewNote(nil, privateNote)
|
||||
t.Run("guest accessing note", func(t *testing.T) {
|
||||
result := ViewNote(nil, note)
|
||||
assert.Equal(t, result, false, "result mismatch")
|
||||
})
|
||||
|
||||
t.Run("guest accessing public note", func(t *testing.T) {
|
||||
result := ViewNote(nil, publicNote)
|
||||
assert.Equal(t, result, true, "result mismatch")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ type Note struct {
|
|||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Body string `json:"content"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
Public bool `json:"public"`
|
||||
USN int `json:"usn"`
|
||||
Book NoteBook `json:"book"`
|
||||
User NoteUser `json:"user"`
|
||||
|
|
@ -57,7 +56,6 @@ func PresentNote(note database.Note) Note {
|
|||
UpdatedAt: FormatTS(note.UpdatedAt),
|
||||
Body: note.Body,
|
||||
AddedOn: note.AddedOn,
|
||||
Public: note.Public,
|
||||
USN: note.USN,
|
||||
Book: NoteBook{
|
||||
UUID: note.Book.UUID,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ func TestPresentNote(t *testing.T) {
|
|||
BookUUID: "f1e2d3c4-b5a6-4987-b654-321fedcba098",
|
||||
Body: "Test note content",
|
||||
AddedOn: 1234567890,
|
||||
Public: true,
|
||||
USN: 100,
|
||||
Book: database.Book{
|
||||
UUID: "f1e2d3c4-b5a6-4987-b654-321fedcba098",
|
||||
|
|
@ -57,7 +56,6 @@ func TestPresentNote(t *testing.T) {
|
|||
assert.Equal(t, got.UUID, "a1b2c3d4-e5f6-4789-a012-3456789abcde", "UUID mismatch")
|
||||
assert.Equal(t, got.Body, "Test note content", "Body mismatch")
|
||||
assert.Equal(t, got.AddedOn, int64(1234567890), "AddedOn mismatch")
|
||||
assert.Equal(t, got.Public, true, "Public mismatch")
|
||||
assert.Equal(t, got.USN, 100, "USN mismatch")
|
||||
assert.Equal(t, got.CreatedAt, FormatTS(createdAt), "CreatedAt mismatch")
|
||||
assert.Equal(t, got.UpdatedAt, FormatTS(updatedAt), "UpdatedAt mismatch")
|
||||
|
|
@ -84,7 +82,6 @@ func TestPresentNotes(t *testing.T) {
|
|||
BookUUID: "f1e2d3c4-b5a6-4987-b654-321fedcba098",
|
||||
Body: "First note",
|
||||
AddedOn: 1000000000,
|
||||
Public: false,
|
||||
USN: 10,
|
||||
Book: database.Book{
|
||||
UUID: "f1e2d3c4-b5a6-4987-b654-321fedcba098",
|
||||
|
|
@ -105,7 +102,6 @@ func TestPresentNotes(t *testing.T) {
|
|||
BookUUID: "abcdef01-2345-4678-9abc-def012345678",
|
||||
Body: "Second note",
|
||||
AddedOn: 2000000000,
|
||||
Public: true,
|
||||
USN: 20,
|
||||
Book: database.Book{
|
||||
UUID: "abcdef01-2345-4678-9abc-def012345678",
|
||||
|
|
|
|||
|
|
@ -19,12 +19,10 @@
|
|||
package tmpl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/dnote/pkg/assert"
|
||||
"github.com/dnote/dnote/pkg/server/database"
|
||||
"github.com/dnote/dnote/pkg/server/testutils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
|
@ -50,42 +48,4 @@ func TestAppShellExecute(t *testing.T) {
|
|||
|
||||
assert.Equal(t, string(b), "<head><title>Dnote</title></head>", "result mismatch")
|
||||
})
|
||||
|
||||
t.Run("note", func(t *testing.T) {
|
||||
db := testutils.InitMemoryDB(t)
|
||||
|
||||
user := testutils.SetupUserData(db)
|
||||
b1 := database.Book{
|
||||
UUID: testutils.MustUUID(t),
|
||||
UserID: user.ID,
|
||||
Label: "js",
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&b1), "preparing b1")
|
||||
n1 := database.Note{
|
||||
UUID: testutils.MustUUID(t),
|
||||
UserID: user.ID,
|
||||
BookUUID: b1.UUID,
|
||||
Public: true,
|
||||
Body: "n1 content",
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&n1), "preparing note")
|
||||
|
||||
a, err := NewAppShell(db, []byte("{{ .MetaTags }}"))
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "preparing app shell"))
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("http://mock.url/notes/%s", n1.UUID)
|
||||
r, err := http.NewRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "preparing request"))
|
||||
}
|
||||
|
||||
b, err := a.Execute(r)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "executing"))
|
||||
}
|
||||
|
||||
assert.NotEqual(t, string(b), "", "result should not be empty")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue