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:
Sung 2025-10-19 18:32:20 -07:00 committed by GitHub
commit b03ca999a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 175 additions and 653 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(&note).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(&note).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 != "" {

View file

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

View file

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

View file

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

View file

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

View file

@ -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(&note), "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(&noteRecord), "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(&note), "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")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(&note), "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(&noteRecord), "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")

View file

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

View file

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

View file

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

View file

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

View file

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