mirror of
https://github.com/dnote/dnote
synced 2026-03-14 14:35:50 +01:00
Compare commits
5 commits
server-v3.
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f34a96abbe |
||
|
|
9fa312e3fc |
||
|
|
8f37d34df6 |
||
|
|
5c416e3a32 |
||
|
|
d5e11c23f6 |
23 changed files with 632 additions and 234 deletions
|
|
@ -6,27 +6,28 @@ Please see the [doc](https://www.getdnote.com/docs/server) for more.
|
|||
|
||||
1. Install [Docker](https://docs.docker.com/install/).
|
||||
2. Install Docker [Compose plugin](https://docs.docker.com/compose/install/linux/).
|
||||
3. Download the [compose.yml](https://raw.githubusercontent.com/dnote/dnote/master/host/docker/compose.yml) file by running:
|
||||
3. Create a `compose.yml` file with the following content:
|
||||
|
||||
```
|
||||
curl https://raw.githubusercontent.com/dnote/dnote/master/host/docker/compose.yml > compose.yml
|
||||
```yaml
|
||||
services:
|
||||
dnote:
|
||||
image: dnote/dnote:latest
|
||||
container_name: dnote
|
||||
ports:
|
||||
- 3001:3001
|
||||
volumes:
|
||||
- ./dnote_data:/data
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
4. Run the following to download the images and run the containers
|
||||
4. Run the following to download the image and start the container
|
||||
|
||||
```
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Visit http://localhost:3001 in your browser to see Dnote running.
|
||||
|
||||
### Supported platform
|
||||
|
||||
Currently, the official Docker image for Dnote supports Linux running AMD64 CPU architecture.
|
||||
|
||||
If you run ARM64, please install Dnote server by downloading a binary distribution (see below).
|
||||
|
||||
## Manual Installation
|
||||
|
||||
Download from [releases](https://github.com/dnote/dnote/releases), extract, and run:
|
||||
|
|
@ -34,7 +35,7 @@ Download from [releases](https://github.com/dnote/dnote/releases), extract, and
|
|||
```bash
|
||||
tar -xzf dnote-server-$version-$os.tar.gz
|
||||
mv ./dnote-server /usr/local/bin
|
||||
dnote-server start --webUrl=https://your.server
|
||||
dnote-server start --baseUrl=https://your.server
|
||||
```
|
||||
|
||||
You're up and running. Database: `~/.local/share/dnote/server.db` (customize with `--dbPath`). Run `dnote-server start --help` for options.
|
||||
|
|
|
|||
9
go.mod
9
go.mod
|
|
@ -14,10 +14,9 @@ require (
|
|||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/radovskyb/watcher v1.0.7
|
||||
github.com/robfig/cron v1.2.0
|
||||
github.com/sergi/go-diff v1.3.1
|
||||
github.com/spf13/cobra v1.10.1
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/time v0.13.0
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
|
|
@ -36,9 +35,9 @@ require (
|
|||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/stretchr/testify v1.8.1 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
)
|
||||
|
|
|
|||
18
go.sum
18
go.sum
|
|
@ -53,8 +53,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
|
||||
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
|
||||
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
|
|
@ -73,15 +71,15 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ func newRun(ctx context.DnoteCtx) infra.RunEFunc {
|
|||
return err
|
||||
}
|
||||
|
||||
output.NoteInfo(info)
|
||||
output.NoteInfo(os.Stdout, info)
|
||||
|
||||
if err := upgrade.Check(ctx); err != nil {
|
||||
log.Error(errors.Wrap(err, "automatically checking updates").Error())
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
/* Copyright 2025 Dnote Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cat
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/dnote/dnote/pkg/cli/context"
|
||||
"github.com/dnote/dnote/pkg/cli/database"
|
||||
"github.com/dnote/dnote/pkg/cli/infra"
|
||||
"github.com/dnote/dnote/pkg/cli/log"
|
||||
"github.com/dnote/dnote/pkg/cli/output"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var example = `
|
||||
* See the notes with index 2 from a book 'javascript'
|
||||
dnote cat javascript 2
|
||||
`
|
||||
|
||||
var deprecationWarning = `and "view" will replace it in the future version.
|
||||
|
||||
Run "dnote view --help" for more information.
|
||||
`
|
||||
|
||||
func preRun(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 2 {
|
||||
return errors.New("Incorrect number of arguments")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewCmd returns a new cat command
|
||||
func NewCmd(ctx context.DnoteCtx) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "cat <book name> <note index>",
|
||||
Aliases: []string{"c"},
|
||||
Short: "See a note",
|
||||
Example: example,
|
||||
RunE: NewRun(ctx, false),
|
||||
PreRunE: preRun,
|
||||
Deprecated: deprecationWarning,
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewRun returns a new run function
|
||||
func NewRun(ctx context.DnoteCtx, contentOnly bool) infra.RunEFunc {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
var noteRowIDArg string
|
||||
|
||||
if len(args) == 2 {
|
||||
log.Plain(log.ColorYellow.Sprintf("DEPRECATED: you no longer need to pass book name to the view command. e.g. `dnote view 123`.\n\n"))
|
||||
|
||||
noteRowIDArg = args[1]
|
||||
} else {
|
||||
noteRowIDArg = args[0]
|
||||
}
|
||||
|
||||
noteRowID, err := strconv.Atoi(noteRowIDArg)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid rowid")
|
||||
}
|
||||
|
||||
db := ctx.DB
|
||||
info, err := database.GetNoteInfo(db, noteRowID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if contentOnly {
|
||||
output.NoteContent(info)
|
||||
} else {
|
||||
output.NoteInfo(info)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -166,7 +166,7 @@ func runNote(ctx context.DnoteCtx, rowIDArg string) error {
|
|||
}
|
||||
|
||||
log.Success("edited the note\n")
|
||||
output.NoteInfo(noteInfo)
|
||||
output.NoteInfo(os.Stdout, noteInfo)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package remove
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/dnote/dnote/pkg/cli/context"
|
||||
|
|
@ -129,7 +130,7 @@ func runNote(ctx context.DnoteCtx, rowIDArg string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
output.NoteInfo(noteInfo)
|
||||
output.NoteInfo(os.Stdout, noteInfo)
|
||||
|
||||
ok, err := maybeConfirm("remove this note?", false)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -13,76 +13,19 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ls
|
||||
package view
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/dnote/dnote/pkg/cli/context"
|
||||
"github.com/dnote/dnote/pkg/cli/infra"
|
||||
"github.com/dnote/dnote/pkg/cli/log"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var example = `
|
||||
* List all books
|
||||
dnote ls
|
||||
|
||||
* List notes in a book
|
||||
dnote ls javascript
|
||||
`
|
||||
|
||||
var deprecationWarning = `and "view" will replace it in the future version.
|
||||
|
||||
Run "dnote view --help" for more information.
|
||||
`
|
||||
|
||||
func preRun(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 1 {
|
||||
return errors.New("Incorrect number of argument")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewCmd returns a new ls command
|
||||
func NewCmd(ctx context.DnoteCtx) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls <book name?>",
|
||||
Aliases: []string{"l", "notes"},
|
||||
Short: "List all notes",
|
||||
Example: example,
|
||||
RunE: NewRun(ctx, false),
|
||||
PreRunE: preRun,
|
||||
Deprecated: deprecationWarning,
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewRun returns a new run function for ls
|
||||
func NewRun(ctx context.DnoteCtx, nameOnly bool) infra.RunEFunc {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
if err := printBooks(ctx, nameOnly); err != nil {
|
||||
return errors.Wrap(err, "viewing books")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
bookName := args[0]
|
||||
if err := printNotes(ctx, bookName); err != nil {
|
||||
return errors.Wrapf(err, "viewing book '%s'", bookName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// bookInfo is an information about the book to be printed on screen
|
||||
type bookInfo struct {
|
||||
BookLabel string
|
||||
|
|
@ -97,15 +40,13 @@ type noteInfo struct {
|
|||
|
||||
// getNewlineIdx returns the index of newline character in a string
|
||||
func getNewlineIdx(str string) int {
|
||||
var ret int
|
||||
|
||||
ret = strings.Index(str, "\n")
|
||||
|
||||
if ret == -1 {
|
||||
ret = strings.Index(str, "\r\n")
|
||||
// Check for \r\n first
|
||||
if idx := strings.Index(str, "\r\n"); idx != -1 {
|
||||
return idx
|
||||
}
|
||||
|
||||
return ret
|
||||
// Then check for \n
|
||||
return strings.Index(str, "\n")
|
||||
}
|
||||
|
||||
// formatBody returns an excerpt of the given raw note content and a boolean
|
||||
|
|
@ -123,15 +64,15 @@ func formatBody(noteBody string) (string, bool) {
|
|||
return strings.Trim(trimmed, " "), false
|
||||
}
|
||||
|
||||
func printBookLine(info bookInfo, nameOnly bool) {
|
||||
func printBookLine(w io.Writer, info bookInfo, nameOnly bool) {
|
||||
if nameOnly {
|
||||
fmt.Println(info.BookLabel)
|
||||
fmt.Fprintln(w, info.BookLabel)
|
||||
} else {
|
||||
log.Printf("%s %s\n", info.BookLabel, log.ColorYellow.Sprintf("(%d)", info.NoteCount))
|
||||
fmt.Fprintf(w, "%s %s\n", info.BookLabel, log.ColorYellow.Sprintf("(%d)", info.NoteCount))
|
||||
}
|
||||
}
|
||||
|
||||
func printBooks(ctx context.DnoteCtx, nameOnly bool) error {
|
||||
func listBooks(ctx context.DnoteCtx, w io.Writer, nameOnly bool) error {
|
||||
db := ctx.DB
|
||||
|
||||
rows, err := db.Query(`SELECT books.label, count(notes.uuid) note_count
|
||||
|
|
@ -157,13 +98,13 @@ func printBooks(ctx context.DnoteCtx, nameOnly bool) error {
|
|||
}
|
||||
|
||||
for _, info := range infos {
|
||||
printBookLine(info, nameOnly)
|
||||
printBookLine(w, info, nameOnly)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printNotes(ctx context.DnoteCtx, bookName string) error {
|
||||
func listNotes(ctx context.DnoteCtx, w io.Writer, bookName string) error {
|
||||
db := ctx.DB
|
||||
|
||||
var bookUUID string
|
||||
|
|
@ -191,7 +132,7 @@ func printNotes(ctx context.DnoteCtx, bookName string) error {
|
|||
infos = append(infos, info)
|
||||
}
|
||||
|
||||
log.Infof("on book %s\n", bookName)
|
||||
fmt.Fprintf(w, "on book %s\n", bookName)
|
||||
|
||||
for _, info := range infos {
|
||||
body, isExcerpt := formatBody(info.Body)
|
||||
|
|
@ -201,7 +142,7 @@ func printNotes(ctx context.DnoteCtx, bookName string) error {
|
|||
body = fmt.Sprintf("%s %s", body, log.ColorYellow.Sprintf("[---More---]"))
|
||||
}
|
||||
|
||||
log.Plainf("%s %s\n", rowid, body)
|
||||
fmt.Fprintf(w, "%s %s\n", rowid, body)
|
||||
}
|
||||
|
||||
return nil
|
||||
184
pkg/cli/cmd/view/book_test.go
Normal file
184
pkg/cli/cmd/view/book_test.go
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
/* Copyright 2025 Dnote Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package view
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/dnote/pkg/assert"
|
||||
"github.com/dnote/dnote/pkg/cli/context"
|
||||
"github.com/dnote/dnote/pkg/cli/database"
|
||||
)
|
||||
|
||||
func TestGetNewlineIdx(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
input: "hello\nworld",
|
||||
expected: 5,
|
||||
},
|
||||
{
|
||||
input: "hello\r\nworld",
|
||||
expected: 5,
|
||||
},
|
||||
{
|
||||
input: "no newline here",
|
||||
expected: -1,
|
||||
},
|
||||
{
|
||||
input: "",
|
||||
expected: -1,
|
||||
},
|
||||
{
|
||||
input: "\n",
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
input: "\r\n",
|
||||
expected: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("input: %q", tc.input), func(t *testing.T) {
|
||||
got := getNewlineIdx(tc.input)
|
||||
assert.Equal(t, got, tc.expected, "newline index mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatBody(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expectedBody string
|
||||
expectedExcerpt bool
|
||||
}{
|
||||
{
|
||||
input: "single line",
|
||||
expectedBody: "single line",
|
||||
expectedExcerpt: false,
|
||||
},
|
||||
{
|
||||
input: "first line\nsecond line",
|
||||
expectedBody: "first line",
|
||||
expectedExcerpt: true,
|
||||
},
|
||||
{
|
||||
input: "first line\r\nsecond line",
|
||||
expectedBody: "first line",
|
||||
expectedExcerpt: true,
|
||||
},
|
||||
{
|
||||
input: " spaced line ",
|
||||
expectedBody: "spaced line",
|
||||
expectedExcerpt: false,
|
||||
},
|
||||
{
|
||||
input: " first line \nsecond line",
|
||||
expectedBody: "first line",
|
||||
expectedExcerpt: true,
|
||||
},
|
||||
{
|
||||
input: "",
|
||||
expectedBody: "",
|
||||
expectedExcerpt: false,
|
||||
},
|
||||
{
|
||||
input: "line with trailing newline\n",
|
||||
expectedBody: "line with trailing newline",
|
||||
expectedExcerpt: false,
|
||||
},
|
||||
{
|
||||
input: "line with trailing newlines\n\n",
|
||||
expectedBody: "line with trailing newlines",
|
||||
expectedExcerpt: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("input: %q", tc.input), func(t *testing.T) {
|
||||
gotBody, gotExcerpt := formatBody(tc.input)
|
||||
assert.Equal(t, gotBody, tc.expectedBody, "formatted body mismatch")
|
||||
assert.Equal(t, gotExcerpt, tc.expectedExcerpt, "excerpt flag mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListNotes(t *testing.T) {
|
||||
// Setup
|
||||
db := database.InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
bookUUID := "js-book-uuid"
|
||||
database.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", bookUUID, "javascript")
|
||||
database.MustExec(t, "inserting note 1", db, "INSERT INTO notes (uuid, book_uuid, body, added_on) VALUES (?, ?, ?, ?)", "note-1", bookUUID, "first note", 1515199943)
|
||||
database.MustExec(t, "inserting note 2", db, "INSERT INTO notes (uuid, book_uuid, body, added_on) VALUES (?, ?, ?, ?)", "note-2", bookUUID, "multiline note\nwith second line", 1515199945)
|
||||
|
||||
ctx := context.DnoteCtx{DB: db}
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Execute
|
||||
err := listNotes(ctx, &buf, "javascript")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := buf.String()
|
||||
|
||||
// Verify output
|
||||
assert.Equal(t, strings.Contains(got, "on book javascript"), true, "should show book name")
|
||||
assert.Equal(t, strings.Contains(got, "first note"), true, "should contain first note")
|
||||
assert.Equal(t, strings.Contains(got, "multiline note"), true, "should show first line of multiline note")
|
||||
assert.Equal(t, strings.Contains(got, "[---More---]"), true, "should show more indicator for multiline note")
|
||||
assert.Equal(t, strings.Contains(got, "with second line"), false, "should not show second line of multiline note")
|
||||
}
|
||||
|
||||
func TestListBooks(t *testing.T) {
|
||||
// Setup
|
||||
db := database.InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
b1UUID := "js-book-uuid"
|
||||
b2UUID := "linux-book-uuid"
|
||||
|
||||
database.MustExec(t, "inserting book 1", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "javascript")
|
||||
database.MustExec(t, "inserting book 2", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "linux")
|
||||
|
||||
// Add notes to test count
|
||||
database.MustExec(t, "inserting note 1", db, "INSERT INTO notes (uuid, book_uuid, body, added_on) VALUES (?, ?, ?, ?)", "note-1", b1UUID, "note body 1", 1515199943)
|
||||
database.MustExec(t, "inserting note 2", db, "INSERT INTO notes (uuid, book_uuid, body, added_on) VALUES (?, ?, ?, ?)", "note-2", b1UUID, "note body 2", 1515199944)
|
||||
|
||||
ctx := context.DnoteCtx{DB: db}
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Execute
|
||||
err := listBooks(ctx, &buf, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := buf.String()
|
||||
|
||||
// Verify output
|
||||
assert.Equal(t, strings.Contains(got, "javascript"), true, "should contain javascript book")
|
||||
assert.Equal(t, strings.Contains(got, "linux"), true, "should contain linux book")
|
||||
assert.Equal(t, strings.Contains(got, "(2)"), true, "should show 2 notes for javascript")
|
||||
}
|
||||
47
pkg/cli/cmd/view/note.go
Normal file
47
pkg/cli/cmd/view/note.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/* Copyright 2025 Dnote Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package view
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/dnote/dnote/pkg/cli/context"
|
||||
"github.com/dnote/dnote/pkg/cli/database"
|
||||
"github.com/dnote/dnote/pkg/cli/output"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func viewNote(ctx context.DnoteCtx, w io.Writer, noteRowIDArg string, contentOnly bool) error {
|
||||
noteRowID, err := strconv.Atoi(noteRowIDArg)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid rowid")
|
||||
}
|
||||
|
||||
db := ctx.DB
|
||||
info, err := database.GetNoteInfo(db, noteRowID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if contentOnly {
|
||||
output.NoteContent(w, info)
|
||||
} else {
|
||||
output.NoteInfo(w, info)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
90
pkg/cli/cmd/view/note_test.go
Normal file
90
pkg/cli/cmd/view/note_test.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/* Copyright 2025 Dnote Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package view
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/dnote/pkg/assert"
|
||||
"github.com/dnote/dnote/pkg/cli/context"
|
||||
"github.com/dnote/dnote/pkg/cli/database"
|
||||
)
|
||||
|
||||
func TestViewNote(t *testing.T) {
|
||||
db := database.InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
bookUUID := "test-book-uuid"
|
||||
database.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", bookUUID, "golang")
|
||||
database.MustExec(t, "inserting note", db, "INSERT INTO notes (uuid, book_uuid, body, added_on) VALUES (?, ?, ?, ?)",
|
||||
"note-uuid", bookUUID, "test note content", 1515199943000000000)
|
||||
|
||||
ctx := context.DnoteCtx{DB: db}
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := viewNote(ctx, &buf, "1", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := buf.String()
|
||||
assert.Equal(t, strings.Contains(got, "test note content"), true, "should contain note content")
|
||||
}
|
||||
|
||||
func TestViewNoteContentOnly(t *testing.T) {
|
||||
db := database.InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
bookUUID := "test-book-uuid"
|
||||
database.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", bookUUID, "golang")
|
||||
database.MustExec(t, "inserting note", db, "INSERT INTO notes (uuid, book_uuid, body, added_on) VALUES (?, ?, ?, ?)",
|
||||
"note-uuid", bookUUID, "test note content", 1515199943000000000)
|
||||
|
||||
ctx := context.DnoteCtx{DB: db}
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := viewNote(ctx, &buf, "1", true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := buf.String()
|
||||
assert.Equal(t, got, "test note content", "should contain only note content")
|
||||
}
|
||||
|
||||
func TestViewNoteInvalidRowID(t *testing.T) {
|
||||
db := database.InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
ctx := context.DnoteCtx{DB: db}
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := viewNote(ctx, &buf, "not-a-number", false)
|
||||
assert.NotEqual(t, err, nil, "should return error for invalid rowid")
|
||||
}
|
||||
|
||||
func TestViewNoteNotFound(t *testing.T) {
|
||||
db := database.InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
ctx := context.DnoteCtx{DB: db}
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := viewNote(ctx, &buf, "999", false)
|
||||
assert.NotEqual(t, err, nil, "should return error for non-existent note")
|
||||
}
|
||||
|
|
@ -16,14 +16,13 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/dnote/dnote/pkg/cli/context"
|
||||
"github.com/dnote/dnote/pkg/cli/infra"
|
||||
"github.com/dnote/dnote/pkg/cli/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/dnote/dnote/pkg/cli/cmd/cat"
|
||||
"github.com/dnote/dnote/pkg/cli/cmd/ls"
|
||||
"github.com/dnote/dnote/pkg/cli/utils"
|
||||
)
|
||||
|
||||
var example = `
|
||||
|
|
@ -68,27 +67,26 @@ func NewCmd(ctx context.DnoteCtx) *cobra.Command {
|
|||
|
||||
func newRun(ctx context.DnoteCtx) infra.RunEFunc {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
var run infra.RunEFunc
|
||||
|
||||
if len(args) == 0 {
|
||||
run = ls.NewRun(ctx, nameOnly)
|
||||
// List all books
|
||||
return listBooks(ctx, os.Stdout, nameOnly)
|
||||
} else if len(args) == 1 {
|
||||
if nameOnly {
|
||||
return errors.New("--name-only flag is only valid when viewing books")
|
||||
}
|
||||
|
||||
if utils.IsNumber(args[0]) {
|
||||
run = cat.NewRun(ctx, contentOnly)
|
||||
// View a note by index
|
||||
return viewNote(ctx, os.Stdout, args[0], contentOnly)
|
||||
} else {
|
||||
run = ls.NewRun(ctx, false)
|
||||
// List notes in a book
|
||||
return listNotes(ctx, os.Stdout, args[0])
|
||||
}
|
||||
} else if len(args) == 2 {
|
||||
// DEPRECATED: passing book name to view command is deprecated
|
||||
run = cat.NewRun(ctx, false)
|
||||
} else {
|
||||
return errors.New("Incorrect number of arguments")
|
||||
// View a note in a book (book name + note index)
|
||||
return viewNote(ctx, os.Stdout, args[1], contentOnly)
|
||||
}
|
||||
|
||||
return run(cmd, args)
|
||||
return errors.New("Incorrect number of arguments")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,12 +26,10 @@ import (
|
|||
|
||||
// commands
|
||||
"github.com/dnote/dnote/pkg/cli/cmd/add"
|
||||
"github.com/dnote/dnote/pkg/cli/cmd/cat"
|
||||
"github.com/dnote/dnote/pkg/cli/cmd/edit"
|
||||
"github.com/dnote/dnote/pkg/cli/cmd/find"
|
||||
"github.com/dnote/dnote/pkg/cli/cmd/login"
|
||||
"github.com/dnote/dnote/pkg/cli/cmd/logout"
|
||||
"github.com/dnote/dnote/pkg/cli/cmd/ls"
|
||||
"github.com/dnote/dnote/pkg/cli/cmd/remove"
|
||||
"github.com/dnote/dnote/pkg/cli/cmd/root"
|
||||
"github.com/dnote/dnote/pkg/cli/cmd/sync"
|
||||
|
|
@ -79,10 +77,8 @@ func main() {
|
|||
root.Register(login.NewCmd(*ctx))
|
||||
root.Register(logout.NewCmd(*ctx))
|
||||
root.Register(add.NewCmd(*ctx))
|
||||
root.Register(ls.NewCmd(*ctx))
|
||||
root.Register(sync.NewCmd(*ctx))
|
||||
root.Register(version.NewCmd(*ctx))
|
||||
root.Register(cat.NewCmd(*ctx))
|
||||
root.Register(view.NewCmd(*ctx))
|
||||
root.Register(find.NewCmd(*ctx))
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/dnote/pkg/assert"
|
||||
|
|
@ -568,3 +569,65 @@ func TestDBPathFlag(t *testing.T) {
|
|||
db2.QueryRow("SELECT count(*) FROM books WHERE label = ?", "db1-book").Scan(&db2HasDB1Book)
|
||||
assert.Equal(t, db2HasDB1Book, 0, "db2 should not have db1's book")
|
||||
}
|
||||
|
||||
func TestView(t *testing.T) {
|
||||
t.Run("view note by rowid", func(t *testing.T) {
|
||||
_, opts := setupTestEnv(t)
|
||||
|
||||
db, dbPath := database.InitTestFileDB(t)
|
||||
testutils.Setup4(t, db)
|
||||
|
||||
output := testutils.RunDnoteCmd(t, opts, binaryName, "--dbPath", dbPath, "view", "1")
|
||||
|
||||
assert.Equal(t, strings.Contains(output, "Booleans have toString()"), true, "should contain note content")
|
||||
assert.Equal(t, strings.Contains(output, "book name"), true, "should show metadata")
|
||||
})
|
||||
|
||||
t.Run("view note content only", func(t *testing.T) {
|
||||
_, opts := setupTestEnv(t)
|
||||
|
||||
db, dbPath := database.InitTestFileDB(t)
|
||||
testutils.Setup4(t, db)
|
||||
|
||||
output := testutils.RunDnoteCmd(t, opts, binaryName, "--dbPath", dbPath, "view", "1", "--content-only")
|
||||
|
||||
assert.Equal(t, strings.Contains(output, "Booleans have toString()"), true, "should contain note content")
|
||||
assert.Equal(t, strings.Contains(output, "book name"), false, "should not show metadata")
|
||||
})
|
||||
|
||||
t.Run("list books", func(t *testing.T) {
|
||||
_, opts := setupTestEnv(t)
|
||||
|
||||
db, dbPath := database.InitTestFileDB(t)
|
||||
testutils.Setup1(t, db)
|
||||
|
||||
output := testutils.RunDnoteCmd(t, opts, binaryName, "--dbPath", dbPath, "view")
|
||||
|
||||
assert.Equal(t, strings.Contains(output, "js"), true, "should list js book")
|
||||
assert.Equal(t, strings.Contains(output, "linux"), true, "should list linux book")
|
||||
})
|
||||
|
||||
t.Run("list notes in book", func(t *testing.T) {
|
||||
_, opts := setupTestEnv(t)
|
||||
|
||||
db, dbPath := database.InitTestFileDB(t)
|
||||
testutils.Setup2(t, db)
|
||||
|
||||
output := testutils.RunDnoteCmd(t, opts, binaryName, "--dbPath", dbPath, "view", "js")
|
||||
|
||||
assert.Equal(t, strings.Contains(output, "n1 body"), true, "should list note 1")
|
||||
assert.Equal(t, strings.Contains(output, "n2 body"), true, "should list note 2")
|
||||
})
|
||||
|
||||
t.Run("view note by book name and rowid", func(t *testing.T) {
|
||||
_, opts := setupTestEnv(t)
|
||||
|
||||
db, dbPath := database.InitTestFileDB(t)
|
||||
testutils.Setup4(t, db)
|
||||
|
||||
output := testutils.RunDnoteCmd(t, opts, binaryName, "--dbPath", dbPath, "view", "js", "2")
|
||||
|
||||
assert.Equal(t, strings.Contains(output, "Date object implements mathematical comparisons"), true, "should contain note content")
|
||||
assert.Equal(t, strings.Contains(output, "book name"), true, "should show metadata")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package output
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/dnote/pkg/cli/database"
|
||||
|
|
@ -26,7 +27,7 @@ import (
|
|||
)
|
||||
|
||||
// NoteInfo prints a note information
|
||||
func NoteInfo(info database.NoteInfo) {
|
||||
func NoteInfo(w io.Writer, info database.NoteInfo) {
|
||||
log.Infof("book name: %s\n", info.BookLabel)
|
||||
log.Infof("created at: %s\n", time.Unix(0, info.AddedOn).Format("Jan 2, 2006 3:04pm (MST)"))
|
||||
if info.EditedOn != 0 {
|
||||
|
|
@ -35,13 +36,13 @@ func NoteInfo(info database.NoteInfo) {
|
|||
log.Infof("note id: %d\n", info.RowID)
|
||||
log.Infof("note uuid: %s\n", info.UUID)
|
||||
|
||||
fmt.Printf("\n------------------------content------------------------\n")
|
||||
fmt.Printf("%s", info.Content)
|
||||
fmt.Printf("\n-------------------------------------------------------\n")
|
||||
fmt.Fprintf(w, "\n------------------------content------------------------\n")
|
||||
fmt.Fprintf(w, "%s", info.Content)
|
||||
fmt.Fprintf(w, "\n-------------------------------------------------------\n")
|
||||
}
|
||||
|
||||
func NoteContent(info database.NoteInfo) {
|
||||
fmt.Printf("%s", info.Content)
|
||||
func NoteContent(w io.Writer, info database.NoteInfo) {
|
||||
fmt.Fprintf(w, "%s", info.Content)
|
||||
}
|
||||
|
||||
// BookInfo prints a note information
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ type RunDnoteCmdOptions struct {
|
|||
}
|
||||
|
||||
// RunDnoteCmd runs a dnote command
|
||||
func RunDnoteCmd(t *testing.T, opts RunDnoteCmdOptions, binaryName string, arg ...string) {
|
||||
func RunDnoteCmd(t *testing.T, opts RunDnoteCmdOptions, binaryName string, arg ...string) string {
|
||||
t.Logf("running: %s %s", binaryName, strings.Join(arg, " "))
|
||||
|
||||
cmd, stderr, stdout, err := NewDnoteCmd(opts, binaryName, arg...)
|
||||
|
|
@ -162,6 +162,8 @@ func RunDnoteCmd(t *testing.T, opts RunDnoteCmdOptions, binaryName string, arg .
|
|||
|
||||
// Print stdout if and only if test fails later
|
||||
t.Logf("\n%s", stdout)
|
||||
|
||||
return stdout.String()
|
||||
}
|
||||
|
||||
// WaitDnoteCmd runs a dnote command and passes stdout to the callback.
|
||||
|
|
|
|||
|
|
@ -331,3 +331,23 @@ func TestServerUserCreateHelp(t *testing.T) {
|
|||
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)")
|
||||
}
|
||||
|
||||
func TestServerUserList(t *testing.T) {
|
||||
tmpDB := t.TempDir() + "/test.db"
|
||||
|
||||
// Create two users
|
||||
exec.Command(testServerBinary, "user", "create", "--dbPath", tmpDB, "--email", "alice@example.com", "--password", "password123").CombinedOutput()
|
||||
exec.Command(testServerBinary, "user", "create", "--dbPath", tmpDB, "--email", "bob@example.com", "--password", "password123").CombinedOutput()
|
||||
|
||||
// List users
|
||||
listCmd := exec.Command(testServerBinary, "user", "list", "--dbPath", tmpDB)
|
||||
output, err := listCmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("user list failed: %v\nOutput: %s", err, output)
|
||||
}
|
||||
|
||||
outputStr := string(output)
|
||||
assert.Equal(t, strings.Contains(outputStr, "alice@example.com"), true, "output should have alice")
|
||||
assert.Equal(t, strings.Contains(outputStr, "bob@example.com"), true, "output should have bob")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,17 @@ func (a *App) GetUserByEmail(email string) (*database.User, error) {
|
|||
return &user, nil
|
||||
}
|
||||
|
||||
// GetAllUsers retrieves all users from the database
|
||||
func (a *App) GetAllUsers() ([]database.User, error) {
|
||||
var users []database.User
|
||||
err := a.DB.Find(&users).Error
|
||||
if err != nil {
|
||||
return nil, pkgErrors.Wrap(err, "finding users")
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// Authenticate authenticates a user
|
||||
func (a *App) Authenticate(email, password string) (*database.User, error) {
|
||||
user, err := a.GetUserByEmail(email)
|
||||
|
|
|
|||
|
|
@ -108,6 +108,72 @@ func TestGetUserByEmail(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGetAllUsers(t *testing.T) {
|
||||
t.Run("success with multiple users", func(t *testing.T) {
|
||||
db := testutils.InitMemoryDB(t)
|
||||
|
||||
user1 := testutils.SetupUserData(db, "alice@example.com", "password123")
|
||||
user2 := testutils.SetupUserData(db, "bob@example.com", "password123")
|
||||
user3 := testutils.SetupUserData(db, "charlie@example.com", "password123")
|
||||
|
||||
a := NewTest()
|
||||
a.DB = db
|
||||
|
||||
users, err := a.GetAllUsers()
|
||||
|
||||
assert.Equal(t, err, nil, "should not error")
|
||||
assert.Equal(t, len(users), 3, "should return 3 users")
|
||||
|
||||
// Verify all users are returned
|
||||
emails := make(map[string]bool)
|
||||
for _, user := range users {
|
||||
emails[user.Email.String] = true
|
||||
}
|
||||
assert.Equal(t, emails["alice@example.com"], true, "alice should be in results")
|
||||
assert.Equal(t, emails["bob@example.com"], true, "bob should be in results")
|
||||
assert.Equal(t, emails["charlie@example.com"], true, "charlie should be in results")
|
||||
|
||||
// Verify user details match
|
||||
for _, user := range users {
|
||||
if user.Email.String == "alice@example.com" {
|
||||
assert.Equal(t, user.ID, user1.ID, "alice ID mismatch")
|
||||
} else if user.Email.String == "bob@example.com" {
|
||||
assert.Equal(t, user.ID, user2.ID, "bob ID mismatch")
|
||||
} else if user.Email.String == "charlie@example.com" {
|
||||
assert.Equal(t, user.ID, user3.ID, "charlie ID mismatch")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty database", func(t *testing.T) {
|
||||
db := testutils.InitMemoryDB(t)
|
||||
|
||||
a := NewTest()
|
||||
a.DB = db
|
||||
|
||||
users, err := a.GetAllUsers()
|
||||
|
||||
assert.Equal(t, err, nil, "should not error")
|
||||
assert.Equal(t, len(users), 0, "should return 0 users")
|
||||
})
|
||||
|
||||
t.Run("single user", func(t *testing.T) {
|
||||
db := testutils.InitMemoryDB(t)
|
||||
|
||||
user := testutils.SetupUserData(db, "alice@example.com", "password123")
|
||||
|
||||
a := NewTest()
|
||||
a.DB = db
|
||||
|
||||
users, err := a.GetAllUsers()
|
||||
|
||||
assert.Equal(t, err, nil, "should not error")
|
||||
assert.Equal(t, len(users), 1, "should return 1 user")
|
||||
assert.Equal(t, users[0].Email.String, "alice@example.com", "email mismatch")
|
||||
assert.Equal(t, users[0].ID, user.ID, "user ID mismatch")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateUser(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
db := testutils.InitMemoryDB(t)
|
||||
|
|
|
|||
9
pkg/server/assets/package-lock.json
generated
9
pkg/server/assets/package-lock.json
generated
|
|
@ -363,10 +363,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
|
||||
"integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
|
||||
"dev": true
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
|
||||
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
|
|
|
|||
|
|
@ -111,8 +111,8 @@ func requireString(fs *flag.FlagSet, value, fieldName string) {
|
|||
}
|
||||
}
|
||||
|
||||
// setupAppWithDB creates config, initializes app, and returns cleanup function
|
||||
func setupAppWithDB(fs *flag.FlagSet, dbPath string) (*app.App, func()) {
|
||||
// createApp creates config, initializes app, and returns cleanup function
|
||||
func createApp(fs *flag.FlagSet, dbPath string) (*app.App, func()) {
|
||||
cfg, err := config.New(config.Params{
|
||||
DBPath: dbPath,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ func userCreateCmd(args []string) {
|
|||
requireString(fs, *email, "email")
|
||||
requireString(fs, *password, "password")
|
||||
|
||||
a, cleanup := setupAppWithDB(fs, *dbPath)
|
||||
a, cleanup := createApp(fs, *dbPath)
|
||||
defer cleanup()
|
||||
|
||||
_, err := a.CreateUser(*email, *password, *password)
|
||||
|
|
@ -74,7 +74,7 @@ func userRemoveCmd(args []string, stdin io.Reader) {
|
|||
|
||||
requireString(fs, *email, "email")
|
||||
|
||||
a, cleanup := setupAppWithDB(fs, *dbPath)
|
||||
a, cleanup := createApp(fs, *dbPath)
|
||||
defer cleanup()
|
||||
|
||||
// Check if user exists first
|
||||
|
|
@ -127,7 +127,7 @@ func userResetPasswordCmd(args []string) {
|
|||
requireString(fs, *email, "email")
|
||||
requireString(fs, *password, "password")
|
||||
|
||||
a, cleanup := setupAppWithDB(fs, *dbPath)
|
||||
a, cleanup := createApp(fs, *dbPath)
|
||||
defer cleanup()
|
||||
|
||||
// Find the user
|
||||
|
|
@ -151,6 +151,27 @@ func userResetPasswordCmd(args []string) {
|
|||
fmt.Printf("Email: %s\n", *email)
|
||||
}
|
||||
|
||||
func userListCmd(args []string, output io.Writer) {
|
||||
fs := setupFlagSet("list", "dnote-server user list")
|
||||
|
||||
dbPath := fs.String("dbPath", "", "Path to SQLite database file (env: DBPath, default: $XDG_DATA_HOME/dnote/server.db)")
|
||||
|
||||
fs.Parse(args)
|
||||
|
||||
a, cleanup := createApp(fs, *dbPath)
|
||||
defer cleanup()
|
||||
|
||||
users, err := a.GetAllUsers()
|
||||
if err != nil {
|
||||
log.ErrorWrap(err, "listing users")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
fmt.Fprintf(output, "%s,%s,%s\n", user.UUID, user.Email.String, user.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"))
|
||||
}
|
||||
}
|
||||
|
||||
func userCmd(args []string) {
|
||||
if len(args) < 1 {
|
||||
fmt.Println(`Usage:
|
||||
|
|
@ -158,6 +179,7 @@ func userCmd(args []string) {
|
|||
|
||||
Available commands:
|
||||
create: Create a new user
|
||||
list: List all users
|
||||
remove: Remove a user
|
||||
reset-password: Reset a user's password`)
|
||||
os.Exit(1)
|
||||
|
|
@ -172,6 +194,8 @@ Available commands:
|
|||
switch subcommand {
|
||||
case "create":
|
||||
userCreateCmd(subArgs)
|
||||
case "list":
|
||||
userListCmd(subArgs, os.Stdout)
|
||||
case "remove":
|
||||
userRemoveCmd(subArgs, os.Stdin)
|
||||
case "reset-password":
|
||||
|
|
@ -180,6 +204,7 @@ Available commands:
|
|||
fmt.Printf("Unknown subcommand: %s\n\n", subcommand)
|
||||
fmt.Println(`Available commands:
|
||||
create: Create a new user
|
||||
list: List all users
|
||||
remove: Remove a user (only if they have no notes or books)
|
||||
reset-password: Reset a user's password`)
|
||||
os.Exit(1)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
|
@ -107,3 +109,50 @@ func TestUserResetPasswordCmd(t *testing.T) {
|
|||
err = bcrypt.CompareHashAndPassword([]byte(updatedUser.Password.String), []byte("oldpassword123"))
|
||||
assert.Equal(t, err != nil, true, "old password should not match")
|
||||
}
|
||||
|
||||
func TestUserListCmd(t *testing.T) {
|
||||
t.Run("multiple users", func(t *testing.T) {
|
||||
tmpDB := t.TempDir() + "/test.db"
|
||||
|
||||
// Create multiple users
|
||||
db := testutils.InitDB(tmpDB)
|
||||
user1 := testutils.SetupUserData(db, "alice@example.com", "password123")
|
||||
user2 := testutils.SetupUserData(db, "bob@example.com", "password123")
|
||||
user3 := testutils.SetupUserData(db, "charlie@example.com", "password123")
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.Close()
|
||||
|
||||
// Capture output
|
||||
var buf bytes.Buffer
|
||||
userListCmd([]string{"--dbPath", tmpDB}, &buf)
|
||||
|
||||
// Verify output matches expected format
|
||||
output := strings.TrimSpace(buf.String())
|
||||
lines := strings.Split(output, "\n")
|
||||
|
||||
expectedLine1 := fmt.Sprintf("%s,alice@example.com,%s", user1.UUID, user1.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"))
|
||||
expectedLine2 := fmt.Sprintf("%s,bob@example.com,%s", user2.UUID, user2.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"))
|
||||
expectedLine3 := fmt.Sprintf("%s,charlie@example.com,%s", user3.UUID, user3.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"))
|
||||
|
||||
assert.Equal(t, lines[0], expectedLine1, "line 1 should match")
|
||||
assert.Equal(t, lines[1], expectedLine2, "line 2 should match")
|
||||
assert.Equal(t, lines[2], expectedLine3, "line 3 should match")
|
||||
})
|
||||
|
||||
t.Run("empty database", func(t *testing.T) {
|
||||
tmpDB := t.TempDir() + "/test.db"
|
||||
|
||||
// Initialize empty database
|
||||
db := testutils.InitDB(tmpDB)
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.Close()
|
||||
|
||||
// Capture output
|
||||
var buf bytes.Buffer
|
||||
userListCmd([]string{"--dbPath", tmpDB}, &buf)
|
||||
|
||||
// Verify no output
|
||||
output := buf.String()
|
||||
assert.Equal(t, output, "", "should have no output for empty database")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue