Use SQLite (#119)

* Make commands use sqlite

* Migrate system

* Fix tests to use sqlite

* Write test for reducer

* Avoid corrupt state in case error occurs in client after server succeeds

* Export test functions
This commit is contained in:
Sung Won Cho 2018-09-24 06:25:53 +10:00 committed by GitHub
commit e7940229cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 3051 additions and 2796 deletions

9
Gopkg.lock generated
View file

@ -57,6 +57,14 @@
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
digest = "1:bc03901fc8f0965ccba8bc453eae21a9b04f95999eab664c7de6dc7290f4e8f4"
name = "github.com/mattn/go-sqlite3"
packages = ["."]
pruneopts = ""
revision = "25ecb14adfc7543176f7d85291ec7dba82c6f7e4"
version = "v1.9.0"
[[projects]]
digest = "1:7365acd48986e205ccb8652cc746f09c8b7876030d53710ea6ef7d0bd0dcd7ca"
name = "github.com/pkg/errors"
@ -112,6 +120,7 @@
"github.com/dnote/actions",
"github.com/fatih/color",
"github.com/google/go-github/github",
"github.com/mattn/go-sqlite3",
"github.com/pkg/errors",
"github.com/satori/go.uuid",
"github.com/spf13/cobra",

View file

@ -44,3 +44,7 @@
[[constraint]]
name = "github.com/dnote/actions"
version = "0.1.0"
[[constraint]]
name = "github.com/mattn/go-sqlite3"
version = "1.9.0"

View file

@ -1,12 +1,14 @@
package add
import (
"database/sql"
"fmt"
"time"
"github.com/dnote/cli/core"
"github.com/dnote/cli/infra"
"github.com/dnote/cli/log"
"github.com/dnote/cli/utils"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -28,6 +30,7 @@ func preRun(cmd *cobra.Command, args []string) error {
return nil
}
// NewCmd returns a new add command
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
cmd := &cobra.Command{
Use: "add <content>",
@ -61,8 +64,7 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
}
ts := time.Now().Unix()
note := core.NewNote(content, ts)
err := writeNote(ctx, bookName, note, ts)
err := writeNote(ctx, bookName, content, ts)
if err != nil {
return errors.Wrap(err, "Failed to write note")
}
@ -80,38 +82,45 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
}
}
func writeNote(ctx infra.DnoteCtx, bookName string, note infra.Note, ts int64) error {
dnote, err := core.GetDnote(ctx)
func writeNote(ctx infra.DnoteCtx, bookLabel string, content string, ts int64) error {
tx, err := ctx.DB.Begin()
if err != nil {
return errors.Wrap(err, "Failed to get dnote")
return errors.Wrap(err, "beginning a transaction")
}
var book infra.Book
book, ok := dnote[bookName]
if ok {
notes := append(dnote[bookName].Notes, note)
dnote[bookName] = core.GetUpdatedBook(dnote[bookName], notes)
} else {
book = core.NewBook(bookName)
book.Notes = []infra.Note{note}
dnote[bookName] = book
err = core.LogActionAddBook(ctx, bookName)
var bookUUID string
err = tx.QueryRow("SELECT uuid FROM books WHERE label = ?", bookLabel).Scan(&bookUUID)
if err == sql.ErrNoRows {
bookUUID = utils.GenerateUUID()
_, err = tx.Exec("INSERT INTO books (uuid, label) VALUES (?, ?)", bookUUID, bookLabel)
if err != nil {
return errors.Wrap(err, "Failed to log action")
tx.Rollback()
return errors.Wrap(err, "creating the book")
}
err = core.LogActionAddBook(tx, bookLabel)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "logging action")
}
} else if err != nil {
return errors.Wrap(err, "finding the book")
}
err = core.LogActionAddNote(ctx, note.UUID, book.Name, note.Content, ts)
noteUUID := utils.GenerateUUID()
_, err = tx.Exec(`INSERT INTO notes (uuid, book_uuid, content, added_on, public)
VALUES (?, ?, ?, ?, ?);`, noteUUID, bookUUID, content, ts, false)
if err != nil {
return errors.Wrap(err, "Failed to log action")
tx.Rollback()
return errors.Wrap(err, "creating the note")
}
err = core.LogActionAddNote(tx, noteUUID, bookLabel, content, ts)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "logging action")
}
err = core.WriteDnote(ctx, dnote)
if err != nil {
return errors.Wrap(err, "Failed to write to dnote file")
}
tx.Commit()
return nil
}

View file

@ -1,8 +1,8 @@
package cat
import (
"database/sql"
"fmt"
"strconv"
"time"
"github.com/dnote/cli/core"
@ -30,6 +30,7 @@ func preRun(cmd *cobra.Command, args []string) error {
return nil
}
// NewCmd returns a new cat command
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
cmd := &cobra.Command{
Use: "cat <book name> <note index>",
@ -44,30 +45,48 @@ func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
return cmd
}
type noteInfo struct {
BookLabel string
UUID string
Content string
AddedOn int64
EditedOn int64
}
func NewRun(ctx infra.DnoteCtx) core.RunEFunc {
return func(cmd *cobra.Command, args []string) error {
dnote, err := core.GetDnote(ctx)
if err != nil {
return errors.Wrap(err, "reading dnote")
db := ctx.DB
bookLabel := args[0]
noteID := args[1]
var bookUUID string
err := db.QueryRow("SELECT uuid FROM books WHERE label = ?", bookLabel).Scan(&bookUUID)
if err == sql.ErrNoRows {
return errors.Errorf("book '%s' not found", bookLabel)
} else if err != nil {
return errors.Wrap(err, "querying the book")
}
bookName := args[0]
noteIdx, err := strconv.Atoi(args[1])
if err != nil {
return errors.Wrapf(err, "parsing note index '%+v'", args[1])
var info noteInfo
err = db.QueryRow(`SELECT books.label, notes.uuid, notes.content, notes.added_on, notes.edited_on
FROM notes
INNER JOIN books ON books.uuid = notes.book_uuid
WHERE notes.id = ? AND books.uuid = ?`, noteID, bookUUID).
Scan(&info.BookLabel, &info.UUID, &info.Content, &info.AddedOn, &info.EditedOn)
if err == sql.ErrNoRows {
return errors.Errorf("note %s not found in the book '%s'", noteID, bookLabel)
} else if err != nil {
return errors.Wrap(err, "querying the note")
}
book := dnote[bookName]
note := book.Notes[noteIdx]
log.Infof("book name: %s\n", bookName)
log.Infof("note uuid: %s\n", note.UUID)
log.Infof("created at: %s\n", time.Unix(note.AddedOn, 0).Format("Jan 2, 2006 3:04pm (MST)"))
if note.EditedOn != 0 {
log.Infof("updated at: %s\n", time.Unix(note.EditedOn, 0).Format("Jan 2, 2006 3:04pm (MST)"))
log.Infof("book name: %s\n", info.BookLabel)
log.Infof("note uuid: %s\n", info.UUID)
log.Infof("created at: %s\n", time.Unix(info.AddedOn, 0).Format("Jan 2, 2006 3:04pm (MST)"))
if info.EditedOn != 0 {
log.Infof("updated at: %s\n", time.Unix(info.EditedOn, 0).Format("Jan 2, 2006 3:04pm (MST)"))
}
fmt.Printf("\n------------------------content------------------------\n")
fmt.Printf("%s", note.Content)
fmt.Printf("%s", info.Content)
fmt.Printf("\n-------------------------------------------------------\n")
return nil

View file

@ -1,8 +1,8 @@
package edit
import (
"database/sql"
"io/ioutil"
"strconv"
"time"
"github.com/dnote/cli/core"
@ -21,6 +21,7 @@ var example = `
* Skip the prompt by providing new content directly
dnote edit js 3 -c "new content"`
// NewCmd returns a new edit command
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
cmd := &cobra.Command{
Use: "edit",
@ -47,62 +48,64 @@ func preRun(cmd *cobra.Command, args []string) error {
func newRun(ctx infra.DnoteCtx) core.RunEFunc {
return func(cmd *cobra.Command, args []string) error {
dnote, err := core.GetDnote(ctx)
db := ctx.DB
bookLabel := args[0]
noteID := args[1]
bookUUID, err := core.GetBookUUID(ctx, bookLabel)
if err != nil {
return errors.Wrap(err, "Failed to read dnote")
return errors.Wrap(err, "finding book uuid")
}
targetBookName := args[0]
targetIdx, err := strconv.Atoi(args[1])
if err != nil {
return errors.Wrapf(err, "Failed to parse the given index %+v", args[1])
var noteUUID, oldContent string
err = db.QueryRow("SELECT uuid, content FROM notes WHERE id = ? AND book_uuid = ?", noteID, bookUUID).Scan(&noteUUID, &oldContent)
if err == sql.ErrNoRows {
return errors.Errorf("note %s not found in the book '%s'", noteID, bookLabel)
} else if err != nil {
return errors.Wrap(err, "querying the book")
}
targetBook, exists := dnote[targetBookName]
if !exists {
return errors.Errorf("Book %s does not exist", targetBookName)
}
if targetIdx > len(targetBook.Notes)-1 {
return errors.Errorf("Book %s does not have note with index %d", targetBookName, targetIdx)
}
targetNote := targetBook.Notes[targetIdx]
if newContent == "" {
fpath := core.GetDnoteTmpContentPath(ctx)
e := ioutil.WriteFile(fpath, []byte(targetNote.Content), 0644)
e := ioutil.WriteFile(fpath, []byte(oldContent), 0644)
if e != nil {
return errors.Wrap(e, "Failed to prepare editor content")
return errors.Wrap(e, "preparing tmp content file")
}
e = core.GetEditorInput(ctx, fpath, &newContent)
if e != nil {
return errors.Wrap(err, "Failed to get editor input")
return errors.Wrap(err, "getting editor input")
}
}
if targetNote.Content == newContent {
if oldContent == newContent {
return errors.New("Nothing changed")
}
ts := time.Now().Unix()
newContent = core.SanitizeContent(newContent)
targetNote.Content = core.SanitizeContent(newContent)
targetNote.EditedOn = ts
targetBook.Notes[targetIdx] = targetNote
dnote[targetBookName] = targetBook
err = core.LogActionEditNote(ctx, targetNote.UUID, targetBook.Name, targetNote.Content, ts)
tx, err := db.Begin()
if err != nil {
return errors.Wrap(err, "Failed to log action")
return errors.Wrap(err, "beginning a transaction")
}
_, err = tx.Exec(`UPDATE notes
SET content = ?, edited_on = ?
WHERE id = ? AND book_uuid = ?`, newContent, ts, noteID, bookUUID)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "updating the note")
}
err = core.WriteDnote(ctx, dnote)
err = core.LogActionEditNote(tx, noteUUID, bookLabel, newContent, ts)
if err != nil {
return errors.Wrap(err, "Failed to write dnote")
tx.Rollback()
return errors.Wrap(err, "logging an action")
}
tx.Commit()
log.Printf("new content: %s\n", newContent)
log.Success("edited the note\n")

View file

@ -13,6 +13,7 @@ import (
var example = `
dnote login`
// NewCmd returns a new login command
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
cmd := &cobra.Command{
Use: "login",

View file

@ -1,8 +1,8 @@
package ls
import (
"database/sql"
"fmt"
"sort"
"strings"
"github.com/dnote/cli/core"
@ -33,6 +33,7 @@ func preRun(cmd *cobra.Command, args []string) error {
return nil
}
// NewCmd returns a new ls command
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
cmd := &cobra.Command{
Use: "ls <book name?>",
@ -49,22 +50,17 @@ func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
func NewRun(ctx infra.DnoteCtx) core.RunEFunc {
return func(cmd *cobra.Command, args []string) error {
dnote, err := core.GetDnote(ctx)
if err != nil {
return errors.Wrap(err, "Failed to read dnote")
}
if len(args) == 0 {
if err := printBooks(dnote); err != nil {
return errors.Wrap(err, "Failed to print books")
if err := printBooks(ctx); err != nil {
return errors.Wrap(err, "viewing books")
}
return nil
}
bookName := args[0]
if err := printNotes(dnote, bookName); err != nil {
return errors.Wrapf(err, "Failed to print notes for the book %s", bookName)
if err := printNotes(ctx, bookName); err != nil {
return errors.Wrapf(err, "viewing book '%s'", bookName)
}
return nil
@ -73,18 +69,14 @@ func NewRun(ctx infra.DnoteCtx) core.RunEFunc {
// bookInfo is an information about the book to be printed on screen
type bookInfo struct {
BookName string
BookLabel string
NoteCount int
}
func getBookInfos(dnote infra.Dnote) []bookInfo {
var ret []bookInfo
for bookName, book := range dnote {
ret = append(ret, bookInfo{BookName: bookName, NoteCount: len(book.Notes)})
}
return ret
// noteInfo is an information about the note to be printed on screen
type noteInfo struct {
ID int
Content string
}
// getNewlineIdx returns the index of newline character in a string
@ -114,30 +106,71 @@ func formatContent(noteContent string) (string, bool) {
return strings.Trim(noteContent, " "), false
}
func printBooks(dnote infra.Dnote) error {
infos := getBookInfos(dnote)
func printBooks(ctx infra.DnoteCtx) error {
db := ctx.DB
// Show books with more notes first
sort.SliceStable(infos, func(i, j int) bool {
return infos[i].NoteCount > infos[j].NoteCount
})
rows, err := db.Query(`SELECT books.label, count(notes.uuid) note_count
FROM books
INNER JOIN notes ON notes.book_uuid = books.uuid
GROUP BY books.uuid
ORDER BY books.label ASC;`)
if err != nil {
return errors.Wrap(err, "querying books")
}
defer rows.Close()
infos := []bookInfo{}
for rows.Next() {
var info bookInfo
err = rows.Scan(&info.BookLabel, &info.NoteCount)
if err != nil {
return errors.Wrap(err, "scanning a row")
}
infos = append(infos, info)
}
for _, info := range infos {
log.Printf("%s %s\n", info.BookName, log.SprintfYellow("(%d)", info.NoteCount))
log.Printf("%s %s\n", info.BookLabel, log.SprintfYellow("(%d)", info.NoteCount))
}
return nil
}
func printNotes(dnote infra.Dnote, bookName string) error {
func printNotes(ctx infra.DnoteCtx, bookName string) error {
db := ctx.DB
var bookUUID string
err := db.QueryRow("SELECT uuid FROM books WHERE label = ?", bookName).Scan(&bookUUID)
if err == sql.ErrNoRows {
return errors.New("book not found")
} else if err != nil {
return errors.Wrap(err, "querying the book")
}
rows, err := db.Query(`SELECT id, content FROM notes WHERE book_uuid = ? ORDER BY added_on ASC;`, bookUUID)
if err != nil {
return errors.Wrap(err, "querying notes")
}
defer rows.Close()
infos := []noteInfo{}
for rows.Next() {
var info noteInfo
err = rows.Scan(&info.ID, &info.Content)
if err != nil {
return errors.Wrap(err, "scanning a row")
}
infos = append(infos, info)
}
log.Infof("on book %s\n", bookName)
book := dnote[bookName]
for _, info := range infos {
content, isExcerpt := formatContent(info.Content)
for i, note := range book.Notes {
content, isExcerpt := formatContent(note.Content)
index := log.SprintfYellow("(%d)", i)
index := log.SprintfYellow("(%d)", info.ID)
if isExcerpt {
content = fmt.Sprintf("%s %s", content, log.SprintfYellow("[---More---]"))
}

View file

@ -1,8 +1,8 @@
package remove
import (
"database/sql"
"fmt"
"strconv"
"github.com/dnote/cli/core"
"github.com/dnote/cli/infra"
@ -39,111 +39,109 @@ func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
func newRun(ctx infra.DnoteCtx) core.RunEFunc {
return func(cmd *cobra.Command, args []string) error {
if targetBookName != "" {
err := book(ctx, targetBookName)
if err != nil {
return errors.Wrap(err, "Failed to delete the book")
}
} else {
if len(args) < 2 {
return errors.New("Missing argument")
if err := removeBook(ctx, targetBookName); err != nil {
return errors.Wrap(err, "removing the book")
}
targetBook := args[0]
noteIndex, err := strconv.Atoi(args[1])
if err != nil {
return err
}
return nil
}
err = note(ctx, noteIndex, targetBook)
if err != nil {
return errors.Wrap(err, "Failed to delete the note")
}
if len(args) < 2 {
return errors.New("Missing argument")
}
targetBook := args[0]
noteID := args[1]
if err := removeNote(ctx, noteID, targetBook); err != nil {
return errors.Wrap(err, "removing the note")
}
return nil
}
}
// note deletes the note in a certain index.
func note(ctx infra.DnoteCtx, index int, bookName string) error {
dnote, err := core.GetDnote(ctx)
func removeNote(ctx infra.DnoteCtx, noteID, bookLabel string) error {
db := ctx.DB
bookUUID, err := core.GetBookUUID(ctx, bookLabel)
if err != nil {
return errors.Wrap(err, "Failed to get dnote")
return errors.Wrap(err, "finding book uuid")
}
book, exists := dnote[bookName]
if !exists {
return errors.Errorf("Book with the name '%s' does not exist", bookName)
}
notes := book.Notes
if len(notes)-1 < index {
fmt.Println("Error : The note with that index is not found.")
return nil
var noteUUID, noteContent string
err = db.QueryRow("SELECT uuid, content FROM notes WHERE id = ? AND book_uuid = ?", noteID, bookUUID).Scan(&noteUUID, &noteContent)
if err == sql.ErrNoRows {
return errors.Errorf("note %s not found in the book '%s'", noteID, bookLabel)
} else if err != nil {
return errors.Wrap(err, "querying the book")
}
content := notes[index].Content
log.Printf("content: \"%s\"\n", content)
// todo: multiline
log.Printf("content: \"%s\"\n", noteContent)
ok, err := utils.AskConfirmation("remove this note?", false)
if err != nil {
return errors.Wrap(err, "Failed to get confirmation")
return errors.Wrap(err, "getting confirmation")
}
if !ok {
log.Warnf("aborted by user\n")
return nil
}
note := notes[index]
dnote[bookName] = core.GetUpdatedBook(dnote[bookName], append(notes[:index], notes[index+1:]...))
err = core.LogActionRemoveNote(ctx, note.UUID, book.Name)
tx, err := db.Begin()
if err != nil {
return errors.Wrap(err, "Failed to log action")
return errors.Wrap(err, "beginning a transaction")
}
err = core.WriteDnote(ctx, dnote)
if err != nil {
return errors.Wrap(err, "Failed to write dnote")
if _, err = tx.Exec("DELETE FROM notes WHERE uuid = ? AND book_uuid = ?", noteUUID, bookUUID); err != nil {
return errors.Wrap(err, "removing the note")
}
if err = core.LogActionRemoveNote(tx, noteUUID, bookLabel); err != nil {
return errors.Wrap(err, "logging the remove_note action")
}
tx.Commit()
log.Successf("removed from %s\n", bookLabel)
log.Successf("removed from %s\n", bookName)
return nil
}
// book deletes a book with the given name
func book(ctx infra.DnoteCtx, bookName string) error {
ok, err := utils.AskConfirmation(fmt.Sprintf("delete book '%s' and all its notes?", bookName), false)
func removeBook(ctx infra.DnoteCtx, bookLabel string) error {
db := ctx.DB
bookUUID, err := core.GetBookUUID(ctx, bookLabel)
if err != nil {
return err
return errors.Wrap(err, "finding book uuid")
}
ok, err := utils.AskConfirmation(fmt.Sprintf("delete book '%s' and all its notes?", bookLabel), false)
if err != nil {
return errors.Wrap(err, "getting confirmation")
}
if !ok {
log.Warnf("aborted by user\n")
return nil
}
dnote, err := core.GetDnote(ctx)
tx, err := db.Begin()
if err != nil {
return err
return errors.Wrap(err, "beginning a transaction")
}
for n, book := range dnote {
if n == bookName {
delete(dnote, n)
err = core.LogActionRemoveBook(ctx, book.Name)
if err != nil {
return errors.Wrap(err, "Failed to log action")
}
err := core.WriteDnote(ctx, dnote)
if err != nil {
return err
}
log.Success("removed book\n")
return nil
}
if _, err = tx.Exec("DELETE FROM notes WHERE book_uuid = ?", bookUUID); err != nil {
return errors.Wrap(err, "removing notes in the book")
}
if _, err = tx.Exec("DELETE FROM books WHERE uuid = ?", bookUUID); err != nil {
return errors.Wrap(err, "removing the book")
}
if err = core.LogActionRemoveBook(tx, bookLabel); err != nil {
return errors.Wrap(err, "loging the remove_book action")
}
return errors.Errorf("Book '%s' was not found", bookName)
tx.Commit()
log.Success("removed book\n")
return nil
}

View file

@ -27,18 +27,21 @@ func Execute() error {
// Prepare initializes necessary files
func Prepare(ctx infra.DnoteCtx) error {
err := core.MigrateToDnoteDir(ctx)
if err != nil {
return errors.Wrap(err, "initializing dnote dir")
if err := core.InitFiles(ctx); err != nil {
return errors.Wrap(err, "initializing files")
}
err = core.InitFiles(ctx)
if err != nil {
return errors.Wrap(err, "initiating files")
if err := infra.InitDB(ctx); err != nil {
return errors.Wrap(err, "initializing database")
}
if err := infra.InitSystem(ctx); err != nil {
return errors.Wrap(err, "initializing system data")
}
err = migrate.Migrate(ctx)
if err != nil {
if err := migrate.Legacy(ctx); err != nil {
return errors.Wrap(err, "running legacy migration")
}
if err := migrate.Run(ctx); err != nil {
return errors.Wrap(err, "running migration")
}

View file

@ -3,6 +3,7 @@ package sync
import (
"bytes"
"compress/gzip"
"database/sql"
"encoding/json"
"fmt"
"io"
@ -20,6 +21,7 @@ import (
var example = `
dnote sync`
// NewCmd returns a new sync command
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
cmd := &cobra.Command{
Use: "sync",
@ -44,38 +46,42 @@ type syncPayload struct {
func newRun(ctx infra.DnoteCtx) core.RunEFunc {
return func(cmd *cobra.Command, args []string) error {
db := ctx.DB
config, err := core.ReadConfig(ctx)
if err != nil {
return errors.Wrap(err, "Failed to read the config")
return errors.Wrap(err, "reading the config")
}
timestamp, err := core.ReadTimestamp(ctx)
if err != nil {
return errors.Wrap(err, "Failed to read the timestamp")
}
actions, err := core.ReadActionLog(ctx)
if err != nil {
return errors.Wrap(err, "Failed to read the action log")
}
if config.APIKey == "" {
log.Error("login required. please run `dnote login`\n")
return nil
}
payload, err := getPayload(actions, timestamp)
var bookmark int
err = db.QueryRow("SELECT value FROM system WHERE key = ?", "bookmark").Scan(&bookmark)
if err != nil {
return errors.Wrap(err, "Failed to get dnote payload")
return errors.Wrap(err, "getting bookmark")
}
actions, err := getLocalActions(db)
if err != nil {
return errors.Wrap(err, "getting local actions")
}
payload, err := newPayload(actions, bookmark)
if err != nil {
return errors.Wrap(err, "getting the request payload")
}
log.Infof("writing changes (total %d).", len(actions))
resp, err := postActions(ctx, config.APIKey, payload)
if err != nil {
return errors.Wrap(err, "Failed to post to the server ")
return errors.Wrap(err, "posting to the server")
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, "Failed to read failed response body")
return errors.Wrap(err, "reading the response body")
}
if resp.StatusCode != http.StatusOK {
@ -88,34 +94,36 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
fmt.Println(" done.")
var respData responseData
err = json.Unmarshal(body, &respData)
if err = json.Unmarshal(body, &respData); err != nil {
return errors.Wrap(err, "unmarshalling the payload")
}
// First, remove our actions because server has successfully ingested them
if _, err = db.Exec("DELETE FROM actions"); err != nil {
return errors.Wrap(err, "deleting actions")
}
tx, err := db.Begin()
if err != nil {
return errors.Wrap(err, "Failed to unmarshal payload")
return errors.Wrap(err, "beginning a transaction")
}
log.Infof("resolving delta (total %d).", len(respData.Actions))
err = core.ReduceAll(ctx, respData.Actions)
if err != nil {
return errors.Wrap(err, "Failed to reduce returned actions")
if err := core.ReduceAll(ctx, tx, respData.Actions); err != nil {
tx.Rollback()
return errors.Wrap(err, "reducing returned actions")
}
if _, err = tx.Exec("UPDATE system SET value = ? WHERE key = ?", respData.Bookmark, "bookmark"); err != nil {
tx.Rollback()
return errors.Wrap(err, "updating the bookmark")
}
fmt.Println(" done.")
// Update bookmark
ts, err := core.ReadTimestamp(ctx)
if err != nil {
return errors.Wrap(err, "Failed to read the timestamp")
}
ts.Bookmark = respData.Bookmark
err = core.WriteTimestamp(ctx, ts)
if err != nil {
return errors.Wrap(err, "Failed to update bookmark")
}
tx.Commit()
log.Success("success\n")
if err := core.ClearActionLog(ctx); err != nil {
return errors.Wrap(err, "Failed to clear the action log")
}
if err := core.CheckUpdate(ctx); err != nil {
log.Error(errors.Wrap(err, "automatically checking updates").Error())
@ -125,20 +133,20 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
}
}
func getPayload(actions []actions.Action, timestamp infra.Timestamp) (*bytes.Buffer, error) {
func newPayload(actions []actions.Action, bookmark int) (*bytes.Buffer, error) {
compressedActions, err := compressActions(actions)
if err != nil {
return &bytes.Buffer{}, errors.Wrap(err, "Failed to compress actions")
return &bytes.Buffer{}, errors.Wrap(err, "compressing actions")
}
payload := syncPayload{
Bookmark: timestamp.Bookmark,
Bookmark: bookmark,
Actions: compressedActions,
}
b, err := json.Marshal(payload)
if err != nil {
return &bytes.Buffer{}, errors.Wrap(err, "Failed to marshal paylaod into JSON")
return &bytes.Buffer{}, errors.Wrap(err, "marshalling paylaod into JSON")
}
ret := bytes.NewBuffer(b)
@ -148,7 +156,7 @@ func getPayload(actions []actions.Action, timestamp infra.Timestamp) (*bytes.Buf
func compressActions(actions []actions.Action) ([]byte, error) {
b, err := json.Marshal(&actions)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal actions into JSON")
return nil, errors.Wrap(err, "marshalling actions into JSON")
}
var buf bytes.Buffer
@ -156,11 +164,11 @@ func compressActions(actions []actions.Action) ([]byte, error) {
_, err = g.Write(b)
if err != nil {
return nil, errors.Wrap(err, "Failed to write to gzip writer")
return nil, errors.Wrap(err, "writing to gzip writer")
}
if err = g.Close(); err != nil {
return nil, errors.Wrap(err, "Failed to close gzip writer")
return nil, errors.Wrap(err, "closing gzip writer")
}
return buf.Bytes(), nil
@ -170,7 +178,7 @@ func postActions(ctx infra.DnoteCtx, APIKey string, payload io.Reader) (*http.Re
endpoint := fmt.Sprintf("%s/v1/sync", ctx.APIEndpoint)
req, err := http.NewRequest("POST", endpoint, payload)
if err != nil {
return &http.Response{}, errors.Wrap(err, "Failed to construct HTTP request")
return &http.Response{}, errors.Wrap(err, "forming an HTTP request")
}
req.Header.Set("Authorization", APIKey)
@ -179,8 +187,36 @@ func postActions(ctx infra.DnoteCtx, APIKey string, payload io.Reader) (*http.Re
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return &http.Response{}, errors.Wrap(err, "Failed to make request")
return &http.Response{}, errors.Wrap(err, "making a request")
}
return resp, nil
}
func getLocalActions(db *sql.DB) ([]actions.Action, error) {
ret := []actions.Action{}
rows, err := db.Query("SELECT uuid, schema, type, data, timestamp FROM actions")
if err != nil {
return ret, errors.Wrap(err, "querying actions")
}
defer rows.Close()
for rows.Next() {
var action actions.Action
err = rows.Scan(&action.UUID, &action.Schema, &action.Type, &action.Data, &action.Timestamp)
if err != nil {
return ret, errors.Wrap(err, "scanning a row")
}
ret = append(ret, action)
}
err = rows.Err()
if err != nil {
return ret, errors.Wrap(err, "scanning rows")
}
return ret, nil
}

View file

@ -1,16 +1,16 @@
package core
import (
"database/sql"
"encoding/json"
"time"
"github.com/dnote/actions"
"github.com/dnote/cli/infra"
"github.com/pkg/errors"
"github.com/satori/go.uuid"
)
func LogActionAddNote(ctx infra.DnoteCtx, noteUUID, bookName, content string, timestamp int64) error {
// LogActionAddNote logs an action for adding a note
func LogActionAddNote(tx *sql.Tx, noteUUID, bookName, content string, timestamp int64) error {
b, err := json.Marshal(actions.AddNoteDataV2{
NoteUUID: noteUUID,
BookName: bookName,
@ -19,113 +19,79 @@ func LogActionAddNote(ctx infra.DnoteCtx, noteUUID, bookName, content string, ti
Public: false,
})
if err != nil {
return errors.Wrap(err, "Failed to marshal data into JSON")
return errors.Wrap(err, "marshalling data into JSON")
}
action := actions.Action{
UUID: uuid.NewV4().String(),
Schema: 2,
Type: actions.ActionAddNote,
Data: b,
Timestamp: timestamp,
}
if err := LogAction(ctx, action); err != nil {
return errors.Wrapf(err, "Failed to log action type %s", actions.ActionAddNote)
if err := LogAction(tx, 2, actions.ActionAddNote, string(b), timestamp); err != nil {
return errors.Wrapf(err, "logging action")
}
return nil
}
func LogActionRemoveNote(ctx infra.DnoteCtx, noteUUID, bookName string) error {
// LogActionRemoveNote logs an action for removing a book
func LogActionRemoveNote(tx *sql.Tx, noteUUID, bookName string) error {
b, err := json.Marshal(actions.RemoveNoteDataV1{
NoteUUID: noteUUID,
BookName: bookName,
})
if err != nil {
return errors.Wrap(err, "Failed to marshal data into JSON")
return errors.Wrap(err, "marshalling data into JSON")
}
action := actions.Action{
UUID: uuid.NewV4().String(),
Schema: 1,
Type: actions.ActionRemoveNote,
Data: b,
Timestamp: time.Now().Unix(),
}
if err := LogAction(ctx, action); err != nil {
return errors.Wrapf(err, "Failed to log action type %s", actions.ActionRemoveNote)
ts := time.Now().Unix()
if err := LogAction(tx, 1, actions.ActionRemoveNote, string(b), ts); err != nil {
return errors.Wrapf(err, "logging action")
}
return nil
}
func LogActionEditNote(ctx infra.DnoteCtx, noteUUID, bookName, content string, ts int64) error {
// LogActionEditNote logs an action for editing a note
func LogActionEditNote(tx *sql.Tx, noteUUID, bookName, content string, ts int64) error {
b, err := json.Marshal(actions.EditNoteDataV2{
NoteUUID: noteUUID,
FromBook: bookName,
Content: &content,
})
if err != nil {
return errors.Wrap(err, "Failed to marshal data into JSON")
return errors.Wrap(err, "marshalling data into JSON")
}
action := actions.Action{
UUID: uuid.NewV4().String(),
Schema: 2,
Type: actions.ActionEditNote,
Data: b,
Timestamp: ts,
}
if err := LogAction(ctx, action); err != nil {
return errors.Wrapf(err, "Failed to log action type %s", actions.ActionEditNote)
if err := LogAction(tx, 2, actions.ActionEditNote, string(b), ts); err != nil {
return errors.Wrapf(err, "logging action")
}
return nil
}
func LogActionAddBook(ctx infra.DnoteCtx, name string) error {
// LogActionAddBook logs an action for adding a book
func LogActionAddBook(tx *sql.Tx, name string) error {
b, err := json.Marshal(actions.AddBookDataV1{
BookName: name,
})
if err != nil {
return errors.Wrap(err, "Failed to marshal data into JSON")
return errors.Wrap(err, "marshalling data into JSON")
}
action := actions.Action{
UUID: uuid.NewV4().String(),
Schema: 1,
Type: actions.ActionAddBook,
Data: b,
Timestamp: time.Now().Unix(),
}
if err := LogAction(ctx, action); err != nil {
return errors.Wrapf(err, "Failed to log action type %s", actions.ActionAddBook)
ts := time.Now().Unix()
if err := LogAction(tx, 1, actions.ActionAddBook, string(b), ts); err != nil {
return errors.Wrapf(err, "logging action")
}
return nil
}
func LogActionRemoveBook(ctx infra.DnoteCtx, name string) error {
// LogActionRemoveBook logs an action for removing book
func LogActionRemoveBook(tx *sql.Tx, name string) error {
b, err := json.Marshal(actions.RemoveBookDataV1{BookName: name})
if err != nil {
return errors.Wrap(err, "Failed to marshal data into JSON")
return errors.Wrap(err, "marshalling data into JSON")
}
action := actions.Action{
UUID: uuid.NewV4().String(),
Schema: 1,
Type: actions.ActionRemoveBook,
Data: b,
Timestamp: time.Now().Unix(),
}
if err := LogAction(ctx, action); err != nil {
return errors.Wrapf(err, "Failed to log action type %s", actions.ActionRemoveBook)
ts := time.Now().Unix()
if err := LogAction(tx, 1, actions.ActionRemoveBook, string(b), ts); err != nil {
return errors.Wrapf(err, "logging action")
}
return nil

View file

@ -11,34 +11,44 @@ import (
func TestLogActionEditNote(t *testing.T) {
// Setup
ctx := testutils.InitCtx("../tmp")
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
InitFiles(ctx)
// Execute
db := ctx.DB
tx, err := db.Begin()
if err != nil {
panic(errors.Wrap(err, "beginning a transaction"))
}
if err := LogActionEditNote(ctx, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "js", "updated content", 1536168581); err != nil {
if err := LogActionEditNote(tx, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "js", "updated content", 1536168581); err != nil {
t.Fatalf("Failed to perform %s", err.Error())
}
b := testutils.ReadFile(ctx, "actions")
var got []actions.Action
tx.Commit()
if err := json.Unmarshal(b, &got); err != nil {
panic(errors.Wrap(err, "unmarshalling actions"))
// Test
var actionCount int
if err := db.QueryRow("SELECT count(*) FROM actions;").Scan(&actionCount); err != nil {
panic(errors.Wrap(err, "counting actions"))
}
var action actions.Action
if err := db.QueryRow("SELECT uuid, schema, type, timestamp, data FROM actions").
Scan(&action.UUID, &action.Schema, &action.Type, &action.Timestamp, &action.Data); err != nil {
panic(errors.Wrap(err, "querying action"))
}
var actionData actions.EditNoteDataV2
if err := json.Unmarshal(got[0].Data, &actionData); err != nil {
if err := json.Unmarshal(action.Data, &actionData); err != nil {
panic(errors.Wrap(err, "unmarshalling action data"))
}
testutils.AssertEqual(t, len(got), 1, "action length mismatch")
testutils.AssertNotEqual(t, got[0].UUID, "", "action uuid mismatch")
testutils.AssertEqual(t, got[0].Schema, 2, "action schema mismatch")
testutils.AssertEqual(t, got[0].Type, actions.ActionEditNote, "action type mismatch")
testutils.AssertNotEqual(t, got[0].Timestamp, 0, "action timestamp mismatch")
if actionCount != 1 {
t.Fatalf("action count mismatch. got %d", actionCount)
}
testutils.AssertNotEqual(t, action.UUID, "", "action uuid mismatch")
testutils.AssertEqual(t, action.Schema, 2, "action schema mismatch")
testutils.AssertEqual(t, action.Type, actions.ActionEditNote, "action type mismatch")
testutils.AssertNotEqual(t, action.Timestamp, 0, "action timestamp mismatch")
testutils.AssertEqual(t, actionData.NoteUUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "action data note_uuid mismatch")
testutils.AssertEqual(t, actionData.FromBook, "js", "action data from_book mismatch")
testutils.AssertEqual(t, *actionData.Content, "updated content", "action data content mismatch")

View file

@ -1,34 +1,29 @@
package core
import (
"encoding/json"
"database/sql"
"fmt"
"io/ioutil"
"os"
"os/exec"
"strings"
"time"
"github.com/dnote/actions"
"github.com/dnote/cli/infra"
"github.com/dnote/cli/migrate"
"github.com/dnote/cli/utils"
"github.com/pkg/errors"
"github.com/satori/go.uuid"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
)
var (
// TimestampFilename is the name of the file containing upgrade info
TimestampFilename = "timestamps"
// DnoteDirName is the name of the directory containing dnote files
DnoteDirName = ".dnote"
ConfigFilename = "dnoterc"
DnoteFilename = "dnote"
ActionFilename = "actions"
// ConfigFilename is the name of the config file
ConfigFilename = "dnoterc"
// TmpContentFilename is the name of the temporary file that holds editor input
TmpContentFilename = "DNOTE_TMPCONTENT"
)
// RunEFunc is a function type of dnote commands
type RunEFunc func(*cobra.Command, []string) error
// GetConfigPath returns the path to the dnote config file
@ -36,45 +31,29 @@ func GetConfigPath(ctx infra.DnoteCtx) string {
return fmt.Sprintf("%s/%s", ctx.DnoteDir, ConfigFilename)
}
// GetDnotePath returns the path to the dnote file
func GetDnotePath(ctx infra.DnoteCtx) string {
return fmt.Sprintf("%s/%s", ctx.DnoteDir, DnoteFilename)
}
// GetTimestampPath returns the path to the file containing dnote upgrade
// information
func GetTimestampPath(ctx infra.DnoteCtx) string {
return fmt.Sprintf("%s/%s", ctx.DnoteDir, TimestampFilename)
}
// GetActionPath returns the path to the file containing user actions
func GetActionPath(ctx infra.DnoteCtx) string {
return fmt.Sprintf("%s/%s", ctx.DnoteDir, ActionFilename)
}
// GetDnoteTmpContentPath returns the path to the temporary file containing
// content being added or edited
func GetDnoteTmpContentPath(ctx infra.DnoteCtx) string {
return fmt.Sprintf("%s/%s", ctx.DnoteDir, TmpContentFilename)
}
// initActionFile populates action file if it does not exist
func initActionFile(ctx infra.DnoteCtx) error {
path := GetActionPath(ctx)
// GetBookUUID returns a uuid of a book given a label
func GetBookUUID(ctx infra.DnoteCtx, label string) (string, error) {
db := ctx.DB
if utils.FileExists(path) {
return nil
var ret string
err := db.QueryRow("SELECT uuid FROM books WHERE label = ?", label).Scan(&ret)
if err == sql.ErrNoRows {
return ret, errors.Errorf("book '%s' not found", label)
} else if err != nil {
return ret, errors.Wrap(err, "querying the book")
}
b, err := json.Marshal(&[]actions.Action{})
if err != nil {
return errors.Wrap(err, "Failed to get initial action content")
}
err = ioutil.WriteFile(path, b, 0644)
return err
return ret, nil
}
// getEditorCommand returns the system's editor command with appropriate flags,
// if necessary, to make the command wait until editor is close to exit.
func getEditorCommand() string {
editor := os.Getenv("EDITOR")
@ -100,34 +79,11 @@ func getEditorCommand() string {
// InitFiles creates, if necessary, the dnote directory and files inside
func InitFiles(ctx infra.DnoteCtx) error {
fresh, err := isFreshInstall(ctx)
if err != nil {
return errors.Wrap(err, "Failed to check if fresh install")
if err := initDnoteDir(ctx); err != nil {
return errors.Wrap(err, "creating the dnote dir")
}
err = initDnoteDir(ctx)
if err != nil {
return errors.Wrap(err, "Failed to create dnote dir")
}
err = initConfigFile(ctx)
if err != nil {
return errors.Wrap(err, "Failed to generate config file")
}
err = initDnoteFile(ctx)
if err != nil {
return errors.Wrap(err, "Failed to create dnote file")
}
err = initTimestampFile(ctx)
if err != nil {
return errors.Wrap(err, "Failed to create dnote upgrade file")
}
err = initActionFile(ctx)
if err != nil {
return errors.Wrap(err, "Failed to create action file")
}
err = migrate.InitSchemaFile(ctx, fresh)
if err != nil {
return errors.Wrap(err, "Failed to create migration file")
if err := initConfigFile(ctx); err != nil {
return errors.Wrap(err, "generating the config file")
}
return nil
@ -149,12 +105,12 @@ func initConfigFile(ctx infra.DnoteCtx) error {
b, err := yaml.Marshal(config)
if err != nil {
return errors.Wrap(err, "Failed to marshal config into YAML")
return errors.Wrap(err, "marshalling config into YAML")
}
err = ioutil.WriteFile(path, b, 0644)
if err != nil {
return errors.Wrapf(err, "Failed to write the config file at '%s'", path)
return errors.Wrap(err, "writing the config file")
}
return nil
@ -175,353 +131,59 @@ func initDnoteDir(ctx infra.DnoteCtx) error {
return nil
}
// initDnoteFile creates an empty dnote file
func initDnoteFile(ctx infra.DnoteCtx) error {
path := GetDnotePath(ctx)
if utils.FileExists(path) {
return nil
}
b, err := json.Marshal(&infra.Dnote{})
if err != nil {
return errors.Wrap(err, "Failed to get initial dnote content")
}
err = ioutil.WriteFile(path, b, 0644)
return err
}
// initTimestampFile creates an empty dnote upgrade file
func initTimestampFile(ctx infra.DnoteCtx) error {
path := GetTimestampPath(ctx)
if utils.FileExists(path) {
return nil
}
now := time.Now().Unix()
ts := infra.Timestamp{
LastUpgrade: now,
}
b, err := yaml.Marshal(&ts)
if err != nil {
return errors.Wrap(err, "Failed to get initial timestamp content")
}
err = ioutil.WriteFile(path, b, 0644)
return err
}
// ReadTimestamp gets the content of the timestamp file
func ReadTimestamp(ctx infra.DnoteCtx) (infra.Timestamp, error) {
var ret infra.Timestamp
path := GetTimestampPath(ctx)
b, err := ioutil.ReadFile(path)
if err != nil {
return ret, err
}
err = yaml.Unmarshal(b, &ret)
if err != nil {
return ret, errors.Wrap(err, "Failed to unmarshal timestamp content")
}
return ret, nil
}
func WriteTimestamp(ctx infra.DnoteCtx, timestamp infra.Timestamp) error {
d, err := yaml.Marshal(timestamp)
if err != nil {
return errors.Wrap(err, "Failed to marshal timestamp into YAML")
}
path := GetTimestampPath(ctx)
err = ioutil.WriteFile(path, d, 0644)
if err != nil {
return errors.Wrap(err, "Failed to write timestamp to the file")
}
return nil
}
// ReadNoteContent reads the content of dnote
func ReadNoteContent(ctx infra.DnoteCtx) ([]byte, error) {
notePath := GetDnotePath(ctx)
b, err := ioutil.ReadFile(notePath)
if err != nil {
return nil, err
}
return b, nil
}
// GetDnote reads and parses the dnote
func GetDnote(ctx infra.DnoteCtx) (infra.Dnote, error) {
ret := infra.Dnote{}
b, err := ReadNoteContent(ctx)
if err != nil {
return ret, errors.Wrap(err, "Failed to read note content")
}
err = json.Unmarshal(b, &ret)
if err != nil {
return ret, errors.Wrap(err, "Failed to unmarshal note content")
}
return ret, nil
}
// WriteDnote persists the state of Dnote into the dnote file
func WriteDnote(ctx infra.DnoteCtx, dnote infra.Dnote) error {
d, err := json.MarshalIndent(dnote, "", " ")
if err != nil {
return err
}
notePath := GetDnotePath(ctx)
err = ioutil.WriteFile(notePath, d, 0644)
if err != nil {
errors.Wrap(err, "Failed to write to the dnote file")
}
return nil
}
// WriteConfig writes the config to the config file
func WriteConfig(ctx infra.DnoteCtx, config infra.Config) error {
d, err := yaml.Marshal(config)
if err != nil {
return err
return errors.Wrap(err, "marhsalling config")
}
configPath := GetConfigPath(ctx)
err = ioutil.WriteFile(configPath, d, 0644)
if err != nil {
errors.Wrap(err, "Failed to write to the config file")
errors.Wrap(err, "writing the config file")
}
return nil
}
// LogAction appends the action to the action log and updates the last_action
// timestamp
func LogAction(ctx infra.DnoteCtx, action actions.Action) error {
actions, err := ReadActionLog(ctx)
// LogAction logs action and updates the last_action
func LogAction(tx *sql.Tx, schema int, actionType, data string, timestamp int64) error {
uuid := uuid.NewV4().String()
_, err := tx.Exec(`INSERT INTO actions (uuid, schema, type, data, timestamp)
VALUES (?, ?, ?, ?, ?)`, uuid, schema, actionType, data, timestamp)
if err != nil {
return errors.Wrap(err, "Failed to read the action log")
return errors.Wrap(err, "inserting an action")
}
actions = append(actions, action)
err = WriteActionLog(ctx, actions)
_, err = tx.Exec("UPDATE system SET value = ? WHERE key = ?", timestamp, "last_action")
if err != nil {
return errors.Wrap(err, "Failed to write action log")
}
err = UpdateLastActionTimestamp(ctx, action.Timestamp)
if err != nil {
return errors.Wrap(err, "Failed to update the last_action timestamp")
return errors.Wrap(err, "updating last_action")
}
return nil
}
func WriteActionLog(ctx infra.DnoteCtx, ats []actions.Action) error {
path := GetActionPath(ctx)
d, err := json.Marshal(ats)
if err != nil {
return errors.Wrap(err, "Failed to marshal newly generated actions to JSON")
}
err = ioutil.WriteFile(path, d, 0644)
if err != nil {
return errors.Wrap(err, "Failed to write to the actions file")
}
return nil
}
func ClearActionLog(ctx infra.DnoteCtx) error {
var content []actions.Action
if err := WriteActionLog(ctx, content); err != nil {
return errors.Wrap(err, "Failed to write action log")
}
return nil
}
func ReadActionLogContent(ctx infra.DnoteCtx) ([]byte, error) {
path := GetActionPath(ctx)
b, err := ioutil.ReadFile(path)
if err != nil {
return []byte{}, errors.Wrap(err, "Failed to read the action file")
}
return b, nil
}
// ReadActionLog returns the action log content
func ReadActionLog(ctx infra.DnoteCtx) ([]actions.Action, error) {
var ret []actions.Action
b, err := ReadActionLogContent(ctx)
if err != nil {
return ret, errors.Wrap(err, "Failed to read the action log content")
}
err = json.Unmarshal(b, &ret)
if err != nil {
return ret, errors.Wrap(err, "Failed to unmarshal action log JSON")
}
return ret, nil
}
// ReadConfig reads the config file
func ReadConfig(ctx infra.DnoteCtx) (infra.Config, error) {
var ret infra.Config
configPath := GetConfigPath(ctx)
b, err := ioutil.ReadFile(configPath)
if err != nil {
return ret, err
return ret, errors.Wrap(err, "reading config file")
}
err = yaml.Unmarshal(b, &ret)
if err != nil {
return ret, errors.Wrap(err, "Failed to unmarshal config YAML")
return ret, errors.Wrap(err, "unmarshalling config")
}
return ret, nil
}
func UpdateLastActionTimestamp(ctx infra.DnoteCtx, val int64) error {
ts, err := ReadTimestamp(ctx)
if err != nil {
return errors.Wrap(err, "Failed to read the timestamp file")
}
ts.LastAction = val
err = WriteTimestamp(ctx, ts)
if err != nil {
return errors.Wrap(err, "Failed to write the timestamp to the file")
}
return nil
}
// NewNote returns a note
func NewNote(content string, ts int64) infra.Note {
return infra.Note{
UUID: utils.GenerateUID(),
Content: content,
AddedOn: ts,
}
}
// NewBook returns a book
func NewBook(name string) infra.Book {
return infra.Book{
Name: name,
Notes: make([]infra.Note, 0),
}
}
func GetUpdatedBook(book infra.Book, notes []infra.Note) infra.Book {
b := NewBook(book.Name)
b.Notes = notes
return b
}
// MigrateToDnoteDir creates dnote directory if artifacts from the previous version
// of dnote are present, and moves the artifacts to the directory.
func MigrateToDnoteDir(ctx infra.DnoteCtx) error {
homeDir := ctx.HomeDir
temporaryDirPath := fmt.Sprintf("%s/.dnote-tmp", homeDir)
oldDnotePath := fmt.Sprintf("%s/.dnote", homeDir)
oldDnotercPath := fmt.Sprintf("%s/.dnoterc", homeDir)
oldDnoteUpgradePath := fmt.Sprintf("%s/.dnote-upgrade", homeDir)
// Check if a dnote file exists. Return early if it does not exist,
// or exists but already a directory.
fi, err := os.Stat(oldDnotePath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return errors.Wrap(err, "Failed to look up old dnote path")
}
if fi.IsDir() {
return nil
}
if err := os.Mkdir(temporaryDirPath, 0755); err != nil {
return errors.Wrap(err, "Failed to make temporary .dnote directory")
}
// In the beta release for v0.2, backup user's .dnote
if err := utils.CopyFile(oldDnotePath, fmt.Sprintf("%s/dnote-bak-5cdde2e83", homeDir)); err != nil {
return errors.Wrap(err, "Failed to back up the old .dnote file")
}
if err := os.Rename(oldDnotePath, fmt.Sprintf("%s/dnote", temporaryDirPath)); err != nil {
return errors.Wrap(err, "Failed to move .dnote file")
}
if err := os.Rename(oldDnotercPath, fmt.Sprintf("%s/dnoterc", temporaryDirPath)); err != nil {
return errors.Wrap(err, "Failed to move .dnoterc file")
}
if err := os.Remove(oldDnoteUpgradePath); err != nil {
return errors.Wrap(err, "Failed to delete the old upgrade file")
}
// Now that all files are moved to the temporary dir, rename the dir to .dnote
if err := os.Rename(temporaryDirPath, fmt.Sprintf("%s/.dnote", homeDir)); err != nil {
return errors.Wrap(err, "Failed to rename temporary dir to .dnote")
}
return nil
}
// isFreshInstall checks if the dnote files have been initialized
func isFreshInstall(ctx infra.DnoteCtx) (bool, error) {
path := ctx.DnoteDir
_, err := os.Stat(path)
if os.IsNotExist(err) {
return true, nil
}
if err != nil {
return false, errors.Wrap(err, "Failed to get file info for dnote directory")
}
return false, nil
}
func FilterNotes(notes []infra.Note, testFunc func(infra.Note) bool) []infra.Note {
var ret []infra.Note
for _, note := range notes {
if testFunc(note) {
ret = append(ret, note)
}
}
return ret
}
// SanitizeContent sanitizes note content
func SanitizeContent(s string) string {
var ret string
@ -536,10 +198,10 @@ func SanitizeContent(s string) string {
return ret
}
func getEditorCmd(ctx infra.DnoteCtx, fpath string) (*exec.Cmd, error) {
func newEditorCmd(ctx infra.DnoteCtx, fpath string) (*exec.Cmd, error) {
config, err := ReadConfig(ctx)
if err != nil {
return nil, errors.Wrap(err, "Failed to read the config")
return nil, errors.Wrap(err, "reading config")
}
args := strings.Fields(config.Editor)
@ -554,17 +216,17 @@ func GetEditorInput(ctx infra.DnoteCtx, fpath string, content *string) error {
if !utils.FileExists(fpath) {
f, err := os.Create(fpath)
if err != nil {
return errors.Wrap(err, "Failed to create a temporary file for content")
return errors.Wrap(err, "creating a temporary content file")
}
err = f.Close()
if err != nil {
return errors.Wrap(err, "Failed to close the temporary file for content")
return errors.Wrap(err, "closing the temporary content file")
}
}
cmd, err := getEditorCmd(ctx, fpath)
cmd, err := newEditorCmd(ctx, fpath)
if err != nil {
return errors.Wrap(err, "Failed to create the editor command")
return errors.Wrap(err, "creating an editor command")
}
cmd.Stdin = os.Stdin
@ -573,22 +235,22 @@ func GetEditorInput(ctx infra.DnoteCtx, fpath string, content *string) error {
err = cmd.Start()
if err != nil {
return errors.Wrapf(err, "Failed to launch the editor")
return errors.Wrapf(err, "launching an editor")
}
err = cmd.Wait()
if err != nil {
return errors.Wrap(err, "Failed to wait for the editor")
return errors.Wrap(err, "waiting for the editor")
}
b, err := ioutil.ReadFile(fpath)
if err != nil {
return errors.Wrap(err, "Failed to read the file")
return errors.Wrap(err, "reading the temporary content file")
}
err = os.Remove(fpath)
if err != nil {
return errors.Wrap(err, "Failed to remove the temporary content file")
return errors.Wrap(err, "removing the temporary content file")
}
raw := string(b)

View file

@ -1,87 +0,0 @@
package core
import (
"github.com/dnote/cli/testutils"
"github.com/dnote/cli/utils"
"github.com/pkg/errors"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func TestMigrateToDnoteDir(t *testing.T) {
ctx := testutils.InitCtx("../tmp")
t.Run("pre v1 files exist", func(t *testing.T) {
// set up
if err := os.MkdirAll(ctx.HomeDir, 0755); err != nil {
panic(err)
}
defer func() {
if err := os.RemoveAll(ctx.HomeDir); err != nil {
panic(err)
}
}()
dnotePath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnote"))
if err != nil {
panic(errors.Wrap(err, "Failed to get absolute .dnote path").Error())
}
dnotercPath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnoterc"))
if err != nil {
panic(errors.Wrap(err, "Failed to get absolute .dnote path").Error())
}
dnoteUpgradePath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnote-upgrade"))
if err != nil {
panic(errors.Wrap(err, "Failed to get absolute .dnote path").Error())
}
if err = ioutil.WriteFile(dnotePath, []byte{}, 0644); err != nil {
panic(errors.Wrap(err, "Failed prepare .dnote").Error())
}
if err = ioutil.WriteFile(dnotercPath, []byte{}, 0644); err != nil {
panic(errors.Wrap(err, "Failed prepare .dnoterc").Error())
}
if err = ioutil.WriteFile(dnoteUpgradePath, []byte{}, 0644); err != nil {
panic(errors.Wrap(err, "Failed prepare .dnote-upgrade").Error())
}
// execute
err = MigrateToDnoteDir(ctx)
if err != nil {
panic(errors.Wrap(err, "Failed to perform").Error())
}
// test
newDnotePath, err := filepath.Abs(filepath.Join(ctx.DnoteDir, "dnote"))
if err != nil {
panic(errors.Wrap(err, "Failed get new dnote path").Error())
}
newDnotercPath, err := filepath.Abs(filepath.Join(ctx.DnoteDir, "dnoterc"))
if err != nil {
panic(errors.Wrap(err, "Failed get new dnoterc path").Error())
}
fi, err := os.Stat(dnotePath)
if err != nil {
panic(errors.Wrap(err, "Failed to look up file"))
}
if !fi.IsDir() {
t.Fatal(".dnote must be a directory")
}
if utils.FileExists(dnotercPath) {
t.Error(".dnoterc must not exist in the original location")
}
if utils.FileExists(dnoteUpgradePath) {
t.Error(".dnote-upgrade must not exist in the original location")
}
if !utils.FileExists(newDnotePath) {
t.Error("dnote must exist")
}
if !utils.FileExists(newDnotercPath) {
t.Error("dnoterc must exist")
}
})
}

View file

@ -1,20 +1,22 @@
package core
import (
"database/sql"
"encoding/json"
"sort"
"fmt"
"github.com/dnote/actions"
"github.com/dnote/cli/infra"
"github.com/dnote/cli/log"
"github.com/dnote/cli/utils"
"github.com/pkg/errors"
)
// ReduceAll reduces all actions
func ReduceAll(ctx infra.DnoteCtx, ats []actions.Action) error {
for _, action := range ats {
if err := Reduce(ctx, action); err != nil {
return errors.Wrap(err, "Failed to reduce action")
func ReduceAll(ctx infra.DnoteCtx, tx *sql.Tx, actionSlice []actions.Action) error {
for _, action := range actionSlice {
if err := Reduce(ctx, tx, action); err != nil {
return errors.Wrap(err, "reducing action")
}
}
@ -23,312 +25,205 @@ func ReduceAll(ctx infra.DnoteCtx, ats []actions.Action) error {
// Reduce transitions the local dnote state by consuming the action returned
// from the server
func Reduce(ctx infra.DnoteCtx, action actions.Action) error {
func Reduce(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
var err error
switch action.Type {
case actions.ActionAddNote:
err = handleAddNote(ctx, action)
err = handleAddNote(ctx, tx, action)
case actions.ActionRemoveNote:
err = handleRemoveNote(ctx, action)
err = handleRemoveNote(ctx, tx, action)
case actions.ActionEditNote:
err = handleEditNote(ctx, action)
err = handleEditNote(ctx, tx, action)
case actions.ActionAddBook:
err = handleAddBook(ctx, action)
err = handleAddBook(ctx, tx, action)
case actions.ActionRemoveBook:
err = handleRemoveBook(ctx, action)
err = handleRemoveBook(ctx, tx, action)
default:
return errors.Errorf("Unsupported action %s", action.Type)
}
if err != nil {
return errors.Wrapf(err, "Failed to process the action %s", action.Type)
return errors.Wrapf(err, "reducing %s", action.Type)
}
return nil
}
func handleAddNote(ctx infra.DnoteCtx, action actions.Action) error {
func getBookUUIDWithTx(tx *sql.Tx, bookLabel string) (string, error) {
var ret string
err := tx.QueryRow("SELECT uuid FROM books WHERE label = ?", bookLabel).Scan(&ret)
if err == sql.ErrNoRows {
return ret, errors.Errorf("book '%s' not found", bookLabel)
} else if err != nil {
return ret, errors.Wrap(err, "querying the book")
}
return ret, nil
}
func handleAddNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
var data actions.AddNoteDataV1
err := json.Unmarshal(action.Data, &data)
if err != nil {
return errors.Wrap(err, "Failed to parse the action data")
if err := json.Unmarshal(action.Data, &data); err != nil {
return errors.Wrap(err, "parsing the action data")
}
log.Debug("reducing add_note. action: %+v. data: %+v\n", action, data)
note := infra.Note{
UUID: data.NoteUUID,
Content: data.Content,
AddedOn: action.Timestamp,
}
dnote, err := GetDnote(ctx)
bookUUID, err := getBookUUIDWithTx(tx, data.BookName)
if err != nil {
return errors.Wrap(err, "Failed to get dnote")
}
book, ok := dnote[data.BookName]
if !ok {
return errors.Errorf("Book with a name %s is not found", data.BookName)
return errors.Wrap(err, "getting book uuid")
}
// Check duplicate
for _, note := range book.Notes {
if note.UUID == data.NoteUUID {
return errors.New("Duplicate note exists")
}
}
notes := append(dnote[book.Name].Notes, note)
sort.SliceStable(notes, func(i, j int) bool {
return notes[i].AddedOn < notes[j].AddedOn
})
dnote[book.Name] = GetUpdatedBook(dnote[book.Name], notes)
err = WriteDnote(ctx, dnote)
var noteCount int
err = tx.QueryRow("SELECT count(uuid) FROM notes WHERE uuid = ? AND book_uuid = ?", data.NoteUUID, bookUUID).Scan(&noteCount)
if err != nil {
return errors.Wrap(err, "Failed to write dnote")
return errors.Wrap(err, "counting note")
}
if noteCount > 0 {
// if a duplicate exists, it is because the same action has been previously synced to the server
// but the client did not bring the bookmark up-to-date at the time because it had error reducing
// the returned actions.
// noop so that the client can update bookmark
return nil
}
_, err = tx.Exec(`INSERT INTO notes
(uuid, book_uuid, content, added_on, public)
VALUES (?, ?, ?, ?, ?)`, data.NoteUUID, bookUUID, data.Content, action.Timestamp, false)
if err != nil {
return errors.Wrap(err, "inserting a note")
}
return nil
}
func handleRemoveNote(ctx infra.DnoteCtx, action actions.Action) error {
func handleRemoveNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
var data actions.RemoveNoteDataV1
err := json.Unmarshal(action.Data, &data)
if err != nil {
return errors.Wrap(err, "Failed to parse the action data")
if err := json.Unmarshal(action.Data, &data); err != nil {
return errors.Wrap(err, "parsing the action data")
}
log.Debug("reducing remove_note. action: %+v. data: %+v\n", action, data)
dnote, err := GetDnote(ctx)
_, err := tx.Exec("DELETE FROM notes WHERE uuid = ?", data.NoteUUID)
if err != nil {
return errors.Wrap(err, "Failed to get dnote")
}
book, ok := dnote[data.BookName]
if !ok {
return errors.Errorf("Book with a name %s is not found", data.BookName)
}
notes := FilterNotes(book.Notes, func(note infra.Note) bool {
return note.UUID != data.NoteUUID
})
dnote[book.Name] = GetUpdatedBook(dnote[book.Name], notes)
err = WriteDnote(ctx, dnote)
if err != nil {
return errors.Wrap(err, "Failed to write dnote")
return errors.Wrap(err, "removing a note")
}
return nil
}
func handleEditNoteV1(ctx infra.DnoteCtx, action actions.Action) error {
var data actions.EditNoteDataV1
err := json.Unmarshal(action.Data, &data)
if err != nil {
return errors.Wrap(err, "Failed to parse the action data")
func buildEditNoteQuery(ctx infra.DnoteCtx, tx *sql.Tx, noteUUID, bookUUID string, ts int64, data actions.EditNoteDataV2) (string, []interface{}, error) {
setTmpl := "edited_on = ?"
queryArgs := []interface{}{ts}
if data.Content != nil {
setTmpl = fmt.Sprintf("%s, content = ?", setTmpl)
queryArgs = append(queryArgs, *data.Content)
}
log.Debug("reducing edit_note v1. action: %+v. data: %+v\n", action, data)
dnote, err := GetDnote(ctx)
if err != nil {
return errors.Wrap(err, "Failed to get dnote")
if data.Public != nil {
setTmpl = fmt.Sprintf("%s, public = ?", setTmpl)
queryArgs = append(queryArgs, *data.Public)
}
fromBook, ok := dnote[data.FromBook]
if !ok {
return errors.Errorf("Origin book with a name %s is not found", data.FromBook)
}
if data.ToBook == "" {
for idx, note := range fromBook.Notes {
if note.UUID == data.NoteUUID {
note.Content = data.Content
note.EditedOn = action.Timestamp
dnote[fromBook.Name].Notes[idx] = note
}
}
} else {
// Change the book
toBook, ok := dnote[data.ToBook]
if !ok {
return errors.Errorf("Destination book with a name %s is not found", data.FromBook)
if data.ToBook != nil {
bookUUID, err := getBookUUIDWithTx(tx, *data.ToBook)
if err != nil {
return "", []interface{}{}, errors.Wrap(err, "getting destination book uuid")
}
var index int
var note infra.Note
// Find the note
for idx := range fromBook.Notes {
note = fromBook.Notes[idx]
if note.UUID == data.NoteUUID {
index = idx
}
}
note.Content = data.Content
note.EditedOn = action.Timestamp
dnote[fromBook.Name] = GetUpdatedBook(dnote[fromBook.Name], append(fromBook.Notes[:index], fromBook.Notes[index+1:]...))
dnote[toBook.Name] = GetUpdatedBook(dnote[toBook.Name], append(toBook.Notes, note))
setTmpl = fmt.Sprintf("%s, book_uuid = ?", setTmpl)
queryArgs = append(queryArgs, bookUUID)
}
err = WriteDnote(ctx, dnote)
if err != nil {
return errors.Wrap(err, "Failed to write dnote")
}
queryTmpl := fmt.Sprintf("UPDATE notes SET %s WHERE uuid = ? AND book_uuid = ?", setTmpl)
queryArgs = append(queryArgs, noteUUID, bookUUID)
return nil
return queryTmpl, queryArgs, nil
}
func handleEditNoteV2(ctx infra.DnoteCtx, action actions.Action) error {
func handleEditNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
var data actions.EditNoteDataV2
err := json.Unmarshal(action.Data, &data)
if err != nil {
return errors.Wrap(err, "Failed to parse the action data")
return errors.Wrap(err, "parsing the action data")
}
log.Debug("reducing edit_note v2. action: %+v. data: %+v\n", action, data)
dnote, err := GetDnote(ctx)
bookUUID, err := getBookUUIDWithTx(tx, data.FromBook)
if err != nil {
return errors.Wrap(err, "Failed to get dnote")
return errors.Wrap(err, "getting book uuid")
}
fromBook, ok := dnote[data.FromBook]
if !ok {
return errors.Errorf("Origin book with a name %s is not found", data.FromBook)
}
if data.ToBook == nil {
for idx, note := range fromBook.Notes {
if note.UUID == data.NoteUUID {
if data.Content != nil {
note.Content = *data.Content
}
if data.Public != nil {
note.Public = *data.Public
}
note.EditedOn = action.Timestamp
dnote[fromBook.Name].Notes[idx] = note
}
}
} else {
// Change the book
toBook := *data.ToBook
dstBook, ok := dnote[toBook]
if !ok {
return errors.Errorf("Destination book with a name %s is not found", toBook)
}
var index int
var note infra.Note
// Find the note
for idx := range fromBook.Notes {
note = fromBook.Notes[idx]
if note.UUID == data.NoteUUID {
index = idx
}
}
if data.Content != nil {
note.Content = *data.Content
}
if data.Public != nil {
note.Public = *data.Public
}
note.EditedOn = action.Timestamp
dnote[fromBook.Name] = GetUpdatedBook(dnote[fromBook.Name], append(fromBook.Notes[:index], fromBook.Notes[index+1:]...))
dnote[toBook] = GetUpdatedBook(dnote[toBook], append(dstBook.Notes, note))
}
err = WriteDnote(ctx, dnote)
queryTmpl, queryArgs, err := buildEditNoteQuery(ctx, tx, data.NoteUUID, bookUUID, action.Timestamp, data)
if err != nil {
return errors.Wrap(err, "Failed to write dnote")
return errors.Wrap(err, "building edit note query")
}
_, err = tx.Exec(queryTmpl, queryArgs...)
if err != nil {
return errors.Wrap(err, "updating a note")
}
return nil
}
func handleEditNote(ctx infra.DnoteCtx, action actions.Action) error {
if action.Schema == 1 {
return handleEditNoteV1(ctx, action)
} else if action.Schema == 2 {
return handleEditNoteV2(ctx, action)
}
return errors.Errorf("Unsupported schema version for editing note: %d", action.Schema)
}
func handleAddBook(ctx infra.DnoteCtx, action actions.Action) error {
func handleAddBook(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
var data actions.AddBookDataV1
err := json.Unmarshal(action.Data, &data)
if err != nil {
return errors.Wrap(err, "Failed to parse the action data")
return errors.Wrap(err, "parsing the action data")
}
log.Debug("reducing add_book. action: %+v. data: %+v\n", action, data)
dnote, err := GetDnote(ctx)
var bookCount int
err = tx.QueryRow("SELECT count(uuid) FROM books WHERE label = ?", data.BookName).Scan(&bookCount)
if err != nil {
return errors.Wrap(err, "Failed to get dnote")
return errors.Wrap(err, "counting books")
}
_, exists := dnote[data.BookName]
if exists {
if bookCount > 0 {
// If book already exists, another machine added a book with the same name.
// noop
return nil
}
book := infra.Book{
Name: data.BookName,
Notes: []infra.Note{},
}
dnote[data.BookName] = book
err = WriteDnote(ctx, dnote)
_, err = tx.Exec("INSERT INTO books (uuid, label) VALUES (?, ?)", utils.GenerateUUID(), data.BookName)
if err != nil {
return errors.Wrap(err, "Failed to write dnote")
return errors.Wrap(err, "inserting a book")
}
return nil
}
func handleRemoveBook(ctx infra.DnoteCtx, action actions.Action) error {
func handleRemoveBook(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
var data actions.RemoveBookDataV1
err := json.Unmarshal(action.Data, &data)
if err != nil {
return errors.Wrap(err, "Failed to parse the action data")
if err := json.Unmarshal(action.Data, &data); err != nil {
return errors.Wrap(err, "parsing the action data")
}
log.Debug("reducing remove_book. action: %+v. data: %+v\n", action, data)
dnote, err := GetDnote(ctx)
if err != nil {
return errors.Wrap(err, "Failed to get dnote")
var bookUUID string
err := tx.QueryRow("SELECT uuid FROM books WHERE label = ?", data.BookName).Scan(&bookUUID)
if err == sql.ErrNoRows {
// If book does not exist, another client added and removed the book, making the add_book action
// obsolete. noop.
return nil
} else if err != nil {
return errors.Wrap(err, "querying the book")
}
for bookName := range dnote {
if bookName == data.BookName {
delete(dnote, bookName)
}
_, err = tx.Exec("DELETE FROM notes WHERE book_uuid = ?", bookUUID)
if err != nil {
return errors.Wrap(err, "removing notes")
}
err = WriteDnote(ctx, dnote)
_, err = tx.Exec("DELETE FROM books WHERE uuid = ?", bookUUID)
if err != nil {
return errors.Wrap(err, "Failed to write dnote")
return errors.Wrap(err, "removing a book")
}
return nil

View file

@ -5,17 +5,17 @@ import (
"testing"
"github.com/dnote/actions"
"github.com/dnote/cli/infra"
"github.com/dnote/cli/testutils"
"github.com/pkg/errors"
)
func TestReduceAddNote(t *testing.T) {
// Setup
ctx := testutils.InitCtx("../tmp")
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
testutils.WriteFile(ctx, "../testutils/fixtures/dnote4.json", "dnote")
testutils.Setup1(t, ctx)
// Execute
b, err := json.Marshal(&actions.AddNoteDataV1{
@ -28,23 +28,31 @@ func TestReduceAddNote(t *testing.T) {
Data: b,
Timestamp: 1517629805,
}
if err = Reduce(ctx, action); err != nil {
t.Fatal(errors.Wrap(err, "Failed to process action"))
db := ctx.DB
tx, err := db.Begin()
if err != nil {
panic(errors.Wrap(err, "beginning a transaction"))
}
if err = Reduce(ctx, tx, action); err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "processing action"))
}
tx.Commit()
// Test
dnote, err := GetDnote(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
}
var noteCount, jsNoteCount, linuxNoteCount int
testutils.MustScan(t, "counting note", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.MustScan(t, "counting js note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
testutils.MustScan(t, "counting linux note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
testutils.AssertEqual(t, noteCount, 2, "notes length mismatch")
testutils.AssertEqual(t, jsNoteCount, 2, "js notes length mismatch")
testutils.AssertEqual(t, linuxNoteCount, 0, "linux notes length mismatch")
book := dnote["js"]
otherBook := dnote["linux"]
existingNote := book.Notes[0]
newNote := book.Notes[1]
var existingNote, newNote infra.Note
testutils.MustScan(t, "scanning existing note", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "43827b9a-c2b0-4c06-a290-97991c896653"), &existingNote.UUID, &existingNote.Content)
testutils.MustScan(t, "scanning new note", db.QueryRow("SELECT uuid, content, added_on FROM notes WHERE uuid = ?", "06896551-8a06-4996-89cc-0d866308b0f6"), &newNote.UUID, &newNote.Content, &newNote.AddedOn)
testutils.AssertEqual(t, len(book.Notes), 2, "notes length mismatch")
testutils.AssertEqual(t, len(otherBook.Notes), 0, "other book notes length mismatch")
testutils.AssertEqual(t, existingNote.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "existing note uuid mismatch")
testutils.AssertEqual(t, existingNote.Content, "Booleans have toString()", "existing note content mismatch")
testutils.AssertEqual(t, newNote.UUID, "06896551-8a06-4996-89cc-0d866308b0f6", "new note uuid mismatch")
@ -52,59 +60,12 @@ func TestReduceAddNote(t *testing.T) {
testutils.AssertEqual(t, newNote.AddedOn, int64(1517629805), "new note added_on mismatch")
}
func TestReduceAddNote_SortByAddedOn(t *testing.T) {
// Setup
ctx := testutils.InitCtx("../tmp")
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
// Execute
b, err := json.Marshal(&actions.AddNoteDataV1{
Content: "new content",
BookName: "js",
NoteUUID: "06896551-8a06-4996-89cc-0d866308b0f6",
})
action := actions.Action{
Type: actions.ActionAddNote,
Data: b,
Timestamp: 1515199944,
}
if err = Reduce(ctx, action); err != nil {
t.Fatal(errors.Wrap(err, "Failed to process action"))
}
// Test
dnote, err := GetDnote(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
}
book := dnote["js"]
otherBook := dnote["linux"]
note1 := book.Notes[0]
note2 := book.Notes[1]
note3 := book.Notes[2]
testutils.AssertEqual(t, len(book.Notes), 3, "notes length mismatch")
testutils.AssertEqual(t, len(otherBook.Notes), 1, "other book notes length mismatch")
testutils.AssertEqual(t, note1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "existing note 1 uuid mismatch")
testutils.AssertEqual(t, note1.Content, "Booleans have toString()", "existing note 1 content mismatch")
testutils.AssertEqual(t, note2.UUID, "06896551-8a06-4996-89cc-0d866308b0f6", "new note uuid mismatch")
testutils.AssertEqual(t, note2.Content, "new content", "new note content mismatch")
testutils.AssertEqual(t, note2.AddedOn, int64(1515199944), "new note added_on mismatch")
testutils.AssertEqual(t, note3.UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "existing note 2 uuid mismatch")
testutils.AssertEqual(t, note3.Content, "Date object implements mathematical comparisons", "existing note 2 content mismatch")
}
func TestReduceRemoveNote(t *testing.T) {
// Setup
ctx := testutils.InitCtx("../tmp")
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
testutils.Setup2(t, ctx)
// Execute
b, err := json.Marshal(&actions.RemoveNoteDataV1{
@ -116,268 +77,149 @@ func TestReduceRemoveNote(t *testing.T) {
Data: b,
Timestamp: 1517629805,
}
if err = Reduce(ctx, action); err != nil {
t.Fatal(errors.Wrap(err, "Failed to process action"))
db := ctx.DB
tx, err := db.Begin()
if err != nil {
panic(errors.Wrap(err, "beginning a transaction"))
}
if err = Reduce(ctx, tx, action); err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "processing action"))
}
tx.Commit()
// Test
dnote, err := GetDnote(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
}
var bookCount, noteCount, jsNoteCount, linuxNoteCount int
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting note", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.MustScan(t, "counting js note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
testutils.MustScan(t, "counting linux note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
targetBook := dnote["js"]
otherBook := dnote["linux"]
var n1, n2 infra.Note
testutils.MustScan(t, "scanning note 1", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content)
testutils.MustScan(t, "scanning note 2", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "3e065d55-6d47-42f2-a6bf-f5844130b2d2"), &n2.UUID, &n2.Content)
testutils.AssertEqual(t, len(dnote), 2, "number of books mismatch")
testutils.AssertEqual(t, len(targetBook.Notes), 1, "target book notes length mismatch")
testutils.AssertEqual(t, targetBook.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
testutils.AssertEqual(t, targetBook.Notes[0].Content, "Booleans have toString()", "remaining note content mismatch")
testutils.AssertEqual(t, len(otherBook.Notes), 1, "other book notes length mismatch")
testutils.AssertEqual(t, otherBook.Notes[0].UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch")
testutils.AssertEqual(t, otherBook.Notes[0].Content, "wc -l to count words", "other book remaining note content mismatch")
testutils.AssertEqual(t, bookCount, 2, "number of books mismatch")
testutils.AssertEqual(t, jsNoteCount, 1, "target book notes length mismatch")
testutils.AssertEqual(t, linuxNoteCount, 1, "other book notes length mismatch")
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "remaining note content mismatch")
testutils.AssertEqual(t, n2.UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch")
testutils.AssertEqual(t, n2.Content, "wc -l to count words", "other book remaining note content mismatch")
}
func TestReduceEditNote_V1_Content(t *testing.T) {
// Setup
ctx := testutils.InitCtx("../tmp")
func TestReduceEditNote(t *testing.T) {
testCases := []struct {
data string
expectedNoteUUID string
expectedNoteBookUUID string
expectedNoteContent string
expectedNoteAddedOn int64
expectedNoteEditedOn int64
expectedNotePublic bool
expectedJsNoteCount int
expectedLinuxNoteCount int
}{
{
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "from_book": "js", "content": "updated content"}`,
expectedNoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
expectedNoteBookUUID: "js-book-uuid",
expectedNoteContent: "updated content",
expectedNoteAddedOn: int64(1515199951),
expectedNoteEditedOn: int64(1517629805),
expectedNotePublic: false,
expectedJsNoteCount: 2,
expectedLinuxNoteCount: 1,
},
{
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "from_book": "js", "public": true}`,
expectedNoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
expectedNoteBookUUID: "js-book-uuid",
expectedNoteContent: "Date object implements mathematical comparisons",
expectedNoteAddedOn: int64(1515199951),
expectedNoteEditedOn: int64(1517629805),
expectedNotePublic: true,
expectedJsNoteCount: 2,
expectedLinuxNoteCount: 1,
},
{
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "from_book": "js", "to_book": "linux", "content": "updated content"}`,
expectedNoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
expectedNoteBookUUID: "linux-book-uuid",
expectedNoteContent: "updated content",
expectedNoteAddedOn: int64(1515199951),
expectedNoteEditedOn: int64(1517629805),
expectedNotePublic: false,
expectedJsNoteCount: 1,
expectedLinuxNoteCount: 2,
}}
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
for _, tc := range testCases {
// Setup
func() {
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
// Execute
b, err := json.Marshal(&actions.EditNoteDataV1{
FromBook: "js",
NoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
Content: "updated content",
})
action := actions.Action{
Type: actions.ActionEditNote,
Data: b,
Schema: 1,
Timestamp: 1517629805,
testutils.Setup2(t, ctx)
db := ctx.DB
// Execute
action := actions.Action{
Type: actions.ActionEditNote,
Data: json.RawMessage(tc.data),
Schema: 2,
Timestamp: 1517629805,
}
tx, err := db.Begin()
if err != nil {
panic(errors.Wrap(err, "beginning a transaction"))
}
err = Reduce(ctx, tx, action)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "Failed to process action"))
}
tx.Commit()
// Test
var bookCount, noteCount, jsNoteCount, linuxNoteCount int
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting note", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.MustScan(t, "counting js note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
testutils.MustScan(t, "counting linux note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
var n1, n2, n3 infra.Note
testutils.MustScan(t, "scanning note 1", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content)
testutils.MustScan(t, "scanning note 2", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "3e065d55-6d47-42f2-a6bf-f5844130b2d2"), &n2.UUID, &n2.Content)
testutils.MustScan(t, "scanning note 2", db.QueryRow("SELECT uuid, content, added_on, edited_on, public FROM notes WHERE uuid = ?", "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f"), &n3.UUID, &n3.Content, &n3.AddedOn, &n3.EditedOn, &n3.Public)
testutils.AssertEqual(t, bookCount, 2, "number of books mismatch")
testutils.AssertEqual(t, noteCount, 3, "number of notes mismatch")
testutils.AssertEqual(t, jsNoteCount, tc.expectedJsNoteCount, "js book notes length mismatch")
testutils.AssertEqual(t, linuxNoteCount, tc.expectedLinuxNoteCount, "linux book notes length mismatch")
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "n1 mismatch")
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "n1 content mismatch")
testutils.AssertEqual(t, n2.UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "n2 uuid mismatch")
testutils.AssertEqual(t, n2.Content, "wc -l to count words", "n2 content mismatch")
testutils.AssertEqual(t, n3.UUID, tc.expectedNoteUUID, "edited note uuid mismatch")
testutils.AssertEqual(t, n3.Content, tc.expectedNoteContent, "edited note content mismatch")
testutils.AssertEqual(t, n3.AddedOn, tc.expectedNoteAddedOn, "edited note added_on mismatch")
testutils.AssertEqual(t, n3.EditedOn, tc.expectedNoteEditedOn, "edited note edited_on mismatch")
testutils.AssertEqual(t, n3.Public, tc.expectedNotePublic, "edited note public mismatch")
}()
}
err = Reduce(ctx, action)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to process action"))
}
// Test
dnote, err := GetDnote(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
}
targetBook := dnote["js"]
otherBook := dnote["linux"]
testutils.AssertEqual(t, len(dnote), 2, "number of books mismatch")
testutils.AssertEqual(t, len(targetBook.Notes), 2, "target book notes length mismatch")
testutils.AssertEqual(t, targetBook.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
testutils.AssertEqual(t, targetBook.Notes[0].Content, "Booleans have toString()", "remaining note content mismatch")
testutils.AssertEqual(t, targetBook.Notes[1].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch")
testutils.AssertEqual(t, targetBook.Notes[1].Content, "updated content", "edited note content mismatch")
testutils.AssertEqual(t, targetBook.Notes[1].EditedOn, int64(1517629805), "edited note edited_on mismatch")
testutils.AssertEqual(t, len(otherBook.Notes), 1, "other book notes length mismatch")
testutils.AssertEqual(t, otherBook.Notes[0].UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch")
testutils.AssertEqual(t, otherBook.Notes[0].Content, "wc -l to count words", "other book remaining note content mismatch")
}
func TestReduceEditNote_V1_ChangeBook(t *testing.T) {
// Setup
ctx := testutils.InitCtx("../tmp")
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
// Execute
b, err := json.Marshal(&actions.EditNoteDataV1{
FromBook: "js",
ToBook: "linux",
NoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
Content: "updated content",
})
action := actions.Action{
Type: actions.ActionEditNote,
Data: b,
Schema: 1,
Timestamp: 1517629805,
}
err = Reduce(ctx, action)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to process action"))
}
// Test
dnote, err := GetDnote(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
}
targetBook := dnote["js"]
otherBook := dnote["linux"]
if len(targetBook.Notes) != 1 {
t.Fatalf("target book length mismatch. Got %d", len(targetBook.Notes))
}
if len(otherBook.Notes) != 2 {
t.Fatalf("other book length mismatch. Got %d", len(targetBook.Notes))
}
testutils.AssertEqual(t, len(dnote), 2, "number of books mismatch")
testutils.AssertEqual(t, targetBook.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
testutils.AssertEqual(t, targetBook.Notes[0].Content, "Booleans have toString()", "remaining note content mismatch")
testutils.AssertEqual(t, otherBook.Notes[0].UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch")
testutils.AssertEqual(t, otherBook.Notes[0].Content, "wc -l to count words", "other book remaining note content mismatch")
testutils.AssertEqual(t, otherBook.Notes[1].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch")
testutils.AssertEqual(t, otherBook.Notes[1].Content, "updated content", "edited note content mismatch")
testutils.AssertEqual(t, otherBook.Notes[1].EditedOn, int64(1517629805), "edited note edited_on mismatch")
}
func TestReduceEditNote_V2_Content(t *testing.T) {
// Setup
ctx := testutils.InitCtx("../tmp")
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
// Execute
b := json.RawMessage(`{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "from_book": "js", "content": "updated content"}`)
action := actions.Action{
Type: actions.ActionEditNote,
Data: b,
Schema: 2,
Timestamp: 1517629805,
}
err := Reduce(ctx, action)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to process action"))
}
// Test
dnote, err := GetDnote(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
}
targetBook := dnote["js"]
otherBook := dnote["linux"]
testutils.AssertEqual(t, len(dnote), 2, "number of books mismatch")
testutils.AssertEqual(t, len(targetBook.Notes), 2, "target book notes length mismatch")
testutils.AssertEqual(t, targetBook.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
testutils.AssertEqual(t, targetBook.Notes[0].Content, "Booleans have toString()", "remaining note content mismatch")
testutils.AssertEqual(t, targetBook.Notes[1].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch")
testutils.AssertEqual(t, targetBook.Notes[1].Content, "updated content", "edited note content mismatch")
testutils.AssertEqual(t, targetBook.Notes[1].EditedOn, int64(1517629805), "edited note edited_on mismatch")
testutils.AssertEqual(t, targetBook.Notes[1].Public, false, "edited note public mismatch")
testutils.AssertEqual(t, len(otherBook.Notes), 1, "other book notes length mismatch")
testutils.AssertEqual(t, otherBook.Notes[0].UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch")
testutils.AssertEqual(t, otherBook.Notes[0].Content, "wc -l to count words", "other book remaining note content mismatch")
}
func TestReduceEditNote_V2_public(t *testing.T) {
// Setup
ctx := testutils.InitCtx("../tmp")
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
// Execute
b := json.RawMessage(`{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "from_book": "js", "public": true}`)
action := actions.Action{
Type: actions.ActionEditNote,
Data: b,
Schema: 2,
Timestamp: 1517629805,
}
err := Reduce(ctx, action)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to process action"))
}
// Test
dnote, err := GetDnote(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
}
targetBook := dnote["js"]
otherBook := dnote["linux"]
testutils.AssertEqual(t, len(dnote), 2, "number of books mismatch")
testutils.AssertEqual(t, len(targetBook.Notes), 2, "target book notes length mismatch")
testutils.AssertEqual(t, targetBook.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
testutils.AssertEqual(t, targetBook.Notes[0].Content, "Booleans have toString()", "remaining note content mismatch")
testutils.AssertEqual(t, targetBook.Notes[1].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch")
testutils.AssertEqual(t, targetBook.Notes[1].Content, "Date object implements mathematical comparisons", "edited note content mismatch")
testutils.AssertEqual(t, targetBook.Notes[1].EditedOn, int64(1517629805), "edited note edited_on mismatch")
testutils.AssertEqual(t, targetBook.Notes[1].Public, true, "edited note public mismatch")
testutils.AssertEqual(t, len(otherBook.Notes), 1, "other book notes length mismatch")
testutils.AssertEqual(t, otherBook.Notes[0].UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch")
testutils.AssertEqual(t, otherBook.Notes[0].Content, "wc -l to count words", "other book remaining note content mismatch")
}
func TestReduceEditNote_V2_changeBook(t *testing.T) {
// Setup
ctx := testutils.InitCtx("../tmp")
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
// Execute
b := json.RawMessage(`{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "from_book": "js", "to_book": "linux", "content": "updated content"}`)
action := actions.Action{
Type: actions.ActionEditNote,
Data: b,
Schema: 2,
Timestamp: 1517629805,
}
err := Reduce(ctx, action)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to process action"))
}
// Test
dnote, err := GetDnote(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
}
targetBook := dnote["js"]
otherBook := dnote["linux"]
if len(targetBook.Notes) != 1 {
t.Fatalf("target book length mismatch. Got %d", len(targetBook.Notes))
}
if len(otherBook.Notes) != 2 {
t.Fatalf("other book length mismatch. Got %d", len(targetBook.Notes))
}
testutils.AssertEqual(t, len(dnote), 2, "number of books mismatch")
testutils.AssertEqual(t, targetBook.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
testutils.AssertEqual(t, targetBook.Notes[0].Content, "Booleans have toString()", "remaining note content mismatch")
testutils.AssertEqual(t, otherBook.Notes[0].UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch")
testutils.AssertEqual(t, otherBook.Notes[0].Content, "wc -l to count words", "other book remaining note content mismatch")
testutils.AssertEqual(t, otherBook.Notes[1].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch")
testutils.AssertEqual(t, otherBook.Notes[1].Content, "updated content", "edited note content mismatch")
testutils.AssertEqual(t, otherBook.Notes[1].EditedOn, int64(1517629805), "edited note edited_on mismatch")
}
func TestReduceAddBook(t *testing.T) {
// Setup
ctx := testutils.InitCtx("../tmp")
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
testutils.WriteFile(ctx, "../testutils/fixtures/dnote4.json", "dnote")
testutils.Setup1(t, ctx)
// Execute
b, err := json.Marshal(&actions.AddBookDataV1{BookName: "new_book"})
@ -386,30 +228,32 @@ func TestReduceAddBook(t *testing.T) {
Data: b,
Timestamp: 1517629805,
}
if err = Reduce(ctx, action); err != nil {
db := ctx.DB
tx, err := db.Begin()
if err != nil {
panic(errors.Wrap(err, "beginning a transaction"))
}
if err = Reduce(ctx, tx, action); err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "Failed to process action"))
}
tx.Commit()
// Test
dnote, err := GetDnote(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
}
var bookCount, newBookNoteCount int
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting note in the new book", db.QueryRow("SELECT count(*) FROM notes INNER JOIN books ON books.uuid = notes.book_uuid WHERE books.label = ?", "new_book"), &newBookNoteCount)
newBook := dnote["new_book"]
testutils.AssertEqual(t, len(dnote), 3, "number of books mismatch")
testutils.AssertEqual(t, newBook.Name, "new_book", "new book name mismatch")
testutils.AssertEqual(t, len(newBook.Notes), 0, "new book number of notes mismatch")
testutils.AssertEqual(t, bookCount, 3, "number of books mismatch")
testutils.AssertEqual(t, newBookNoteCount, 0, "new book number of notes mismatch")
}
func TestReduceRemoveBook(t *testing.T) {
// Setup
ctx := testutils.InitCtx("../tmp")
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
testutils.Setup2(t, ctx)
// Execute
b, err := json.Marshal(&actions.RemoveBookDataV1{BookName: "linux"})
@ -418,23 +262,39 @@ func TestReduceRemoveBook(t *testing.T) {
Data: b,
Timestamp: 1517629805,
}
if err = Reduce(ctx, action); err != nil {
db := ctx.DB
tx, err := db.Begin()
if err != nil {
panic(errors.Wrap(err, "beginning a transaction"))
}
if err = Reduce(ctx, tx, action); err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "Failed to process action"))
}
tx.Commit()
// Test
dnote, err := GetDnote(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
}
var bookCount, noteCount, jsNoteCount, linuxNoteCount int
var jsBookLabel string
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting note", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.MustScan(t, "counting js note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
testutils.MustScan(t, "counting linux note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
testutils.MustScan(t, "scanning book", db.QueryRow("SELECT label FROM books WHERE uuid = ?", "js-book-uuid"), &jsBookLabel)
remainingBook := dnote["js"]
var n1, n2 infra.Note
testutils.MustScan(t, "scanning note 1", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content)
testutils.MustScan(t, "scanning note 2", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f"), &n2.UUID, &n2.Content)
testutils.AssertEqual(t, len(dnote), 1, "number of books mismatch")
testutils.AssertEqual(t, remainingBook.Name, "js", "remaining book name mismatch")
testutils.AssertEqual(t, len(remainingBook.Notes), 2, "remaining book number of notes mismatch")
testutils.AssertEqual(t, remainingBook.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
testutils.AssertEqual(t, remainingBook.Notes[0].Content, "Booleans have toString()", "remaining note content mismatch")
testutils.AssertEqual(t, remainingBook.Notes[1].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch")
testutils.AssertEqual(t, remainingBook.Notes[1].Content, "Date object implements mathematical comparisons", "edited note content mismatch")
testutils.AssertEqual(t, bookCount, 1, "number of books mismatch")
testutils.AssertEqual(t, noteCount, 2, "number of notes mismatch")
testutils.AssertEqual(t, jsNoteCount, 2, "js note count mismatch")
testutils.AssertEqual(t, linuxNoteCount, 0, "linux note count mismatch")
testutils.AssertEqual(t, jsBookLabel, "js", "remaining book name mismatch")
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "remaining note content mismatch")
testutils.AssertEqual(t, n2.UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch")
testutils.AssertEqual(t, n2.Content, "Date object implements mathematical comparisons", "edited note content mismatch")
}

View file

@ -17,27 +17,26 @@ var upgradeInterval int64 = 86400 * 7 * 3
// shouldCheckUpdate checks if update should be checked
func shouldCheckUpdate(ctx infra.DnoteCtx) (bool, error) {
timestamp, err := ReadTimestamp(ctx)
db := ctx.DB
var lastUpgrade int64
err := db.QueryRow("SELECT value FROM system WHERE key = ?", "last_upgrade").Scan(&lastUpgrade)
if err != nil {
return false, errors.Wrap(err, "Failed to get timestamp content")
return false, errors.Wrap(err, "getting last_udpate")
}
now := time.Now().Unix()
return now-timestamp.LastUpgrade > upgradeInterval, nil
return now-lastUpgrade > upgradeInterval, nil
}
func touchLastUpgrade(ctx infra.DnoteCtx) error {
timestamp, err := ReadTimestamp(ctx)
if err != nil {
return errors.Wrap(err, "Failed to get timestamp content")
}
db := ctx.DB
now := time.Now().Unix()
timestamp.LastUpgrade = now
if err := WriteTimestamp(ctx, timestamp); err != nil {
return errors.Wrap(err, "Failed to write the updated timestamp to the file")
_, err := db.Exec("UPDATE system SET value = ? WHERE key = ?", now, "last_upgrade")
if err != nil {
return errors.Wrap(err, "updating last_upgrade")
}
return nil

View file

@ -1,12 +1,31 @@
// Package infra defines dnote structure
package infra
import (
"database/sql"
"fmt"
"os"
"os/user"
"time"
// use sqlite
_ "github.com/mattn/go-sqlite3"
"github.com/pkg/errors"
)
var (
// DnoteDirName is the name of the directory containing dnote files
DnoteDirName = ".dnote"
)
// DnoteCtx is a context holding the information of the current runtime
type DnoteCtx struct {
HomeDir string
DnoteDir string
APIEndpoint string
Version string
DB *sql.DB
}
// Config holds dnote configuration
@ -41,3 +60,158 @@ type Timestamp struct {
// timestamp of the most recent action performed by the cli
LastAction int64 `yaml:"last_action"`
}
// NewCtx returns a new dnote context
func NewCtx(apiEndpoint, versionTag string) (DnoteCtx, error) {
homeDir, err := getHomeDir()
if err != nil {
return DnoteCtx{}, errors.Wrap(err, "Failed to get home dir")
}
dnoteDir := getDnoteDir(homeDir)
dnoteDBPath := fmt.Sprintf("%s/dnote.db", dnoteDir)
db, err := sql.Open("sqlite3", dnoteDBPath)
if err != nil {
return DnoteCtx{}, errors.Wrap(err, "conntecting to db")
}
ret := DnoteCtx{
HomeDir: homeDir,
DnoteDir: dnoteDir,
APIEndpoint: apiEndpoint,
Version: versionTag,
DB: db,
}
return ret, nil
}
func getDnoteDir(homeDir string) string {
var ret string
dnoteDirEnv := os.Getenv("DNOTE_DIR")
if dnoteDirEnv == "" {
ret = fmt.Sprintf("%s/%s", homeDir, DnoteDirName)
} else {
ret = dnoteDirEnv
}
return ret
}
func getHomeDir() (string, error) {
homeDirEnv := os.Getenv("DNOTE_HOME_DIR")
if homeDirEnv != "" {
return homeDirEnv, nil
}
usr, err := user.Current()
if err != nil {
return "", errors.Wrap(err, "Failed to get current user")
}
return usr.HomeDir, nil
}
// InitDB initializes the database.
// Ideally this process must be a part of migration sequence. But it is performed
// seaprately because it is a prerequisite for legacy migration.
func InitDB(ctx DnoteCtx) error {
db := ctx.DB
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS notes
(
id integer PRIMARY KEY AUTOINCREMENT,
uuid text NOT NULL,
book_uuid text NOT NULL,
content text NOT NULL,
added_on integer NOT NULL,
edited_on integer DEFAULT 0,
public bool DEFAULT false
)`)
if err != nil {
return errors.Wrap(err, "creating notes table")
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS books
(
uuid text PRIMARY KEY,
label text NOT NULL
)`)
if err != nil {
return errors.Wrap(err, "creating books table")
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS actions
(
uuid text PRIMARY KEY,
schema integer NOT NULL,
type text NOT NULL,
data text NOT NULL,
timestamp integer NOT NULL
)`)
if err != nil {
return errors.Wrap(err, "creating actions table")
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS system
(
key string NOT NULL,
value text NOT NULL
)`)
if err != nil {
return errors.Wrap(err, "creating system table")
}
_, err = db.Exec(`
CREATE UNIQUE INDEX IF NOT EXISTS idx_books_label ON books(label);
CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_uuid ON notes(uuid);
CREATE UNIQUE INDEX IF NOT EXISTS idx_books_uuid ON books(uuid);
CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_id ON notes(id);
CREATE INDEX IF NOT EXISTS idx_notes_book_uuid ON notes(book_uuid);`)
if err != nil {
return errors.Wrap(err, "creating indices")
}
return nil
}
// InitSystem inserts system data if missing
func InitSystem(ctx DnoteCtx) error {
db := ctx.DB
tx, err := db.Begin()
if err != nil {
return errors.Wrap(err, "beginning a transaction")
}
var bookmarkCount, lastUpgradeCount int
if err := db.QueryRow("SELECT count(*) FROM system WHERE key = ?", "bookmark").
Scan(&bookmarkCount); err != nil {
return errors.Wrap(err, "counting bookmarks")
}
if bookmarkCount == 0 {
_, err := tx.Exec("INSERT INTO system (key, value) VALUES (?, ?)", "bookmark", 0)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "inserting bookmark")
}
}
if err := db.QueryRow("SELECT count(*) FROM system WHERE key = ?", "last_upgrade").
Scan(&lastUpgradeCount); err != nil {
return errors.Wrap(err, "counting last_upgrade")
}
if lastUpgradeCount == 0 {
now := time.Now().Unix()
_, err := tx.Exec("INSERT INTO system (key, value) VALUES (?, ?)", "last_upgrade", now)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "inserting bookmark")
}
}
tx.Commit()
return nil
}

59
main.go
View file

@ -1,14 +1,12 @@
package main
import (
"fmt"
"os"
"os/user"
"github.com/dnote/cli/cmd/root"
"github.com/dnote/cli/core"
"github.com/dnote/cli/infra"
"github.com/dnote/cli/log"
_ "github.com/mattn/go-sqlite3"
"github.com/pkg/errors"
// commands
@ -17,6 +15,7 @@ import (
"github.com/dnote/cli/cmd/edit"
"github.com/dnote/cli/cmd/login"
"github.com/dnote/cli/cmd/ls"
"github.com/dnote/cli/cmd/remove"
"github.com/dnote/cli/cmd/sync"
"github.com/dnote/cli/cmd/version"
@ -28,14 +27,14 @@ var apiEndpoint string
var versionTag = "master"
func main() {
ctx, err := newCtx()
ctx, err := infra.NewCtx(apiEndpoint, versionTag)
if err != nil {
panic(errors.Wrap(err, "Failed to initialize the dnote context"))
panic(errors.Wrap(err, "initializing context"))
}
defer ctx.DB.Close()
err = root.Prepare(ctx)
if err != nil {
panic(errors.Wrap(err, "Failed to prepare dnote run"))
if err := root.Prepare(ctx); err != nil {
panic(errors.Wrap(err, "preparing dnote run"))
}
root.Register(remove.NewCmd(ctx))
@ -53,47 +52,3 @@ func main() {
os.Exit(1)
}
}
func newCtx() (infra.DnoteCtx, error) {
homeDir, err := getHomeDir()
if err != nil {
return infra.DnoteCtx{}, errors.Wrap(err, "Failed to get home dir")
}
dnoteDir := getDnoteDir(homeDir)
ret := infra.DnoteCtx{
HomeDir: homeDir,
DnoteDir: dnoteDir,
APIEndpoint: apiEndpoint,
Version: versionTag,
}
return ret, nil
}
func getDnoteDir(homeDir string) string {
var ret string
dnoteDirEnv := os.Getenv("DNOTE_DIR")
if dnoteDirEnv == "" {
ret = fmt.Sprintf("%s/%s", homeDir, core.DnoteDirName)
} else {
ret = dnoteDirEnv
}
return ret
}
func getHomeDir() (string, error) {
homeDirEnv := os.Getenv("DNOTE_HOME_DIR")
if homeDirEnv != "" {
return homeDirEnv, nil
}
usr, err := user.Current()
if err != nil {
return "", errors.Wrap(err, "Failed to get current user")
}
return usr.HomeDir, nil
}

View file

@ -1,14 +1,11 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/pkg/errors"
@ -24,353 +21,298 @@ var binaryName = "test-dnote"
func TestMain(m *testing.M) {
if err := exec.Command("go", "build", "-o", binaryName).Run(); err != nil {
log.Print(errors.Wrap(err, "Failed to build a binary").Error())
log.Print(errors.Wrap(err, "building a binary").Error())
os.Exit(1)
}
os.Exit(m.Run())
}
func newDnoteCmd(ctx infra.DnoteCtx, arg ...string) (*exec.Cmd, *bytes.Buffer, error) {
var stderr bytes.Buffer
binaryPath, err := filepath.Abs(binaryName)
if err != nil {
return &exec.Cmd{}, &stderr, errors.Wrap(err, "Failed to get the absolute path to the test binary")
}
cmd := exec.Command(binaryPath, arg...)
cmd.Env = []string{fmt.Sprintf("DNOTE_DIR=%s", ctx.DnoteDir), fmt.Sprintf("DNOTE_HOME_DIR=%s", ctx.HomeDir)}
cmd.Stderr = &stderr
return cmd, &stderr, nil
}
func runDnoteCmd(ctx infra.DnoteCtx, arg ...string) {
cmd, stderr, err := newDnoteCmd(ctx, arg...)
if err != nil {
panic(errors.Wrap(err, "Failed to get command").Error())
}
if err := cmd.Run(); err != nil {
panic(errors.Wrapf(err, "Failed to run command %s", stderr.String()))
}
}
func TestInit(t *testing.T) {
// Setup
ctx := testutils.InitCtx("./tmp")
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
// Set up
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
// Execute
runDnoteCmd(ctx)
testutils.RunDnoteCmd(t, ctx, binaryName)
// Test
if !utils.FileExists(ctx.DnoteDir) {
t.Errorf("dnote directory was not initialized")
}
if !utils.FileExists(fmt.Sprintf("%s/%s", ctx.DnoteDir, core.DnoteFilename)) {
t.Errorf("dnote file was not initialized")
}
if !utils.FileExists(fmt.Sprintf("%s/%s", ctx.DnoteDir, core.ConfigFilename)) {
t.Errorf("config file was not initialized")
}
if !utils.FileExists(fmt.Sprintf("%s/%s", ctx.DnoteDir, core.TimestampFilename)) {
t.Errorf("timestamp file was not initialized")
}
if !utils.FileExists(fmt.Sprintf("%s/%s", ctx.DnoteDir, core.ActionFilename)) {
t.Errorf("action file was not initialized")
}
db := ctx.DB
var notesTableCount, booksTableCount, actionsTableCount, systemTableCount int
testutils.MustScan(t, "counting notes",
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type = ? AND name = ?", "table", "notes"), &notesTableCount)
testutils.MustScan(t, "counting books",
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type = ? AND name = ?", "table", "books"), &booksTableCount)
testutils.MustScan(t, "counting actions",
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type = ? AND name = ?", "table", "actions"), &actionsTableCount)
testutils.MustScan(t, "counting system",
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type = ? AND name = ?", "table", "system"), &systemTableCount)
testutils.AssertEqual(t, notesTableCount, 1, "notes table count mismatch")
testutils.AssertEqual(t, booksTableCount, 1, "books table count mismatch")
testutils.AssertEqual(t, actionsTableCount, 1, "actions table count mismatch")
testutils.AssertEqual(t, systemTableCount, 1, "system table count mismatch")
}
func TestAdd_NewBook_ContentFlag(t *testing.T) {
// Setup
ctx := testutils.InitCtx("./tmp")
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
func TestAddNote_NewBook_ContentFlag(t *testing.T) {
// Set up
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
// Execute
runDnoteCmd(ctx, "add", "js", "-c", "foo")
testutils.RunDnoteCmd(t, ctx, binaryName, "add", "js", "-c", "foo")
// Test
dnote, err := core.GetDnote(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
}
actionSlice, err := core.ReadActionLog(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to read actions"))
}
db := ctx.DB
if len(actionSlice) != 2 {
t.Fatalf("action log length mismatch. got %d", len(actionSlice))
}
var actionCount, noteCount, bookCount int
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
book := dnote["js"]
note := book.Notes[0]
bookAction := actionSlice[0]
noteAction := actionSlice[1]
testutils.AssertEqualf(t, actionCount, 2, "action count mismatch")
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
testutils.AssertEqualf(t, noteCount, 1, "note count mismatch")
var jsBookUUID string
testutils.MustScan(t, "getting js book uuid", db.QueryRow("SELECT uuid FROM books where label = ?", "js"), &jsBookUUID)
var note infra.Note
testutils.MustScan(t, "getting note",
db.QueryRow("SELECT uuid, content, added_on FROM notes where book_uuid = ?", jsBookUUID), &note.UUID, &note.Content, &note.AddedOn)
var bookAction, noteAction actions.Action
testutils.MustScan(t, "getting book action",
db.QueryRow("SELECT data, timestamp FROM actions where type = ?", actions.ActionAddBook), &bookAction.Data, &bookAction.Timestamp)
testutils.MustScan(t, "getting note action",
db.QueryRow("SELECT data, timestamp FROM actions where type = ?", actions.ActionAddNote), &noteAction.Data, &noteAction.Timestamp)
var noteActionData actions.AddNoteDataV1
var bookActionData actions.AddBookDataV1
err = json.Unmarshal(bookAction.Data, &bookActionData)
if err != nil {
log.Fatalf("Failed to unmarshal the action data: %s", err)
if err := json.Unmarshal(bookAction.Data, &bookActionData); err != nil {
log.Fatalf("unmarshalling the action data: %s", err)
}
err = json.Unmarshal(noteAction.Data, &noteActionData)
if err != nil {
log.Fatalf("Failed to unmarshal the action data: %s", err)
if err := json.Unmarshal(noteAction.Data, &noteActionData); err != nil {
log.Fatalf("unmarshalling the action data: %s", err)
}
testutils.AssertEqual(t, bookAction.Type, actions.ActionAddBook, "bookAction type mismatch")
testutils.AssertNotEqual(t, bookActionData.BookName, "", "bookAction data note_uuid mismatch")
testutils.AssertNotEqual(t, bookAction.Timestamp, 0, "bookAction timestamp mismatch")
testutils.AssertEqual(t, noteAction.Type, actions.ActionAddNote, "noteAction type mismatch")
testutils.AssertEqual(t, noteActionData.Content, "foo", "noteAction data name mismatch")
testutils.AssertNotEqual(t, noteActionData.NoteUUID, nil, "noteAction data note_uuid mismatch")
testutils.AssertNotEqual(t, noteActionData.BookName, "", "noteAction data note_uuid mismatch")
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "noteAction timestamp mismatch")
testutils.AssertEqual(t, len(book.Notes), 1, "Book should have one note")
testutils.AssertNotEqual(t, note.UUID, "", "Note should have UUID")
testutils.AssertEqual(t, note.Content, "foo", "Note content mismatch")
testutils.AssertNotEqual(t, note.AddedOn, int64(0), "Note added_on mismatch")
}
func TestAdd_ExistingBook_ContentFlag(t *testing.T) {
// Setup
ctx := testutils.InitCtx("./tmp")
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
func TestAddNote_ExistingBook_ContentFlag(t *testing.T) {
// Set up
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
// init files by running root command
runDnoteCmd(ctx)
testutils.WriteFile(ctx, "./testutils/fixtures/dnote1.json", "dnote")
testutils.Setup3(t, ctx)
// Execute
runDnoteCmd(ctx, "add", "js", "-c", "foo")
testutils.RunDnoteCmd(t, ctx, binaryName, "add", "js", "-c", "foo")
// Test
dnote, err := core.GetDnote(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
}
actionSlice, err := core.ReadActionLog(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to read actions"))
db := ctx.DB
var actionCount, noteCount, bookCount int
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.AssertEqualf(t, actionCount, 1, "action count mismatch")
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
testutils.AssertEqualf(t, noteCount, 2, "note count mismatch")
var n1, n2 infra.Note
testutils.MustScan(t, "getting n1",
db.QueryRow("SELECT uuid, content, added_on FROM notes WHERE book_uuid = ? AND uuid = ?", "js-book-uuid", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content, &n1.AddedOn)
testutils.MustScan(t, "getting n2",
db.QueryRow("SELECT uuid, content, added_on FROM notes WHERE book_uuid = ? AND content = ?", "js-book-uuid", "foo"), &n2.UUID, &n2.Content, &n2.AddedOn)
var noteAction actions.Action
testutils.MustScan(t, "getting note action",
db.QueryRow("SELECT data, timestamp FROM actions WHERE type = ?", actions.ActionAddNote), &noteAction.Data, &noteAction.Timestamp)
var noteActionData actions.AddNoteDataV1
if err := json.Unmarshal(noteAction.Data, &noteActionData); err != nil {
log.Fatalf("unmarshalling the action data: %s", err)
}
book := dnote["js"]
action := actionSlice[0]
var actionData actions.AddNoteDataV1
err = json.Unmarshal(action.Data, &actionData)
if err != nil {
log.Fatalf("Failed to unmarshal the action data: %s", err)
}
testutils.AssertEqual(t, len(actionSlice), 1, "There should be 1 action")
testutils.AssertEqual(t, action.Type, actions.ActionAddNote, "action type mismatch")
testutils.AssertEqual(t, actionData.Content, "foo", "action data name mismatch")
testutils.AssertNotEqual(t, actionData.NoteUUID, "", "action data note_uuid mismatch")
testutils.AssertEqual(t, actionData.BookName, "js", "action data book_name mismatch")
testutils.AssertNotEqual(t, action.Timestamp, 0, "action timestamp mismatch")
testutils.AssertEqual(t, len(book.Notes), 2, "Book should have one note")
testutils.AssertNotEqual(t, book.Notes[0].UUID, "", "Note should have UUID")
testutils.AssertEqual(t, book.Notes[0].Content, "Booleans have toString()", "Note content mismatch")
testutils.AssertNotEqual(t, book.Notes[1].UUID, "", "Note should have UUID")
testutils.AssertEqual(t, book.Notes[1].Content, "foo", "Note content mismatch")
testutils.AssertEqual(t, noteActionData.Content, "foo", "action data name mismatch")
testutils.AssertNotEqual(t, noteActionData.NoteUUID, "", "action data note_uuid mismatch")
testutils.AssertEqual(t, noteActionData.BookName, "js", "action data book_name mismatch")
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "action timestamp mismatch")
testutils.AssertNotEqual(t, n1.UUID, "", "Note should have UUID")
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "Note content mismatch")
testutils.AssertEqual(t, n1.AddedOn, int64(1515199943), "Note added_on mismatch")
testutils.AssertNotEqual(t, n2.UUID, "", "Note should have UUID")
testutils.AssertEqual(t, n2.Content, "foo", "Note content mismatch")
}
func TestEdit_ContentFlag(t *testing.T) {
// Setup
ctx := testutils.InitCtx("./tmp")
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
func TestEditNote_ContentFlag(t *testing.T) {
// Set up
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
// init files by running root command
runDnoteCmd(ctx)
testutils.WriteFile(ctx, "./testutils/fixtures/dnote2.json", "dnote")
testutils.Setup4(t, ctx)
// Execute
runDnoteCmd(ctx, "edit", "js", "1", "-c", "foo bar")
testutils.RunDnoteCmd(t, ctx, binaryName, "edit", "js", "2", "-c", "foo bar")
// Test
dnote, err := core.GetDnote(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
}
actionSlice, err := core.ReadActionLog(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to read actions"))
}
db := ctx.DB
book := dnote["js"]
action := actionSlice[0]
var actionCount, noteCount, bookCount int
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.AssertEqualf(t, actionCount, 1, "action count mismatch")
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
testutils.AssertEqualf(t, noteCount, 2, "note count mismatch")
var n1, n2 infra.Note
testutils.MustScan(t, "getting n1",
db.QueryRow("SELECT uuid, content, added_on FROM notes where book_uuid = ? AND uuid = ?", "js-book-uuid", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content, &n1.AddedOn)
testutils.MustScan(t, "getting n2",
db.QueryRow("SELECT uuid, content, added_on FROM notes where book_uuid = ? AND uuid = ?", "js-book-uuid", "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f"), &n2.UUID, &n2.Content, &n2.AddedOn)
var noteAction actions.Action
testutils.MustScan(t, "getting note action",
db.QueryRow("SELECT data, type, schema FROM actions where type = ?", actions.ActionEditNote),
&noteAction.Data, &noteAction.Type, &noteAction.Schema)
var actionData actions.EditNoteDataV2
err = json.Unmarshal(action.Data, &actionData)
if err != nil {
if err := json.Unmarshal(noteAction.Data, &actionData); err != nil {
log.Fatalf("Failed to unmarshal the action data: %s", err)
}
testutils.AssertEqual(t, len(actionSlice), 1, "There should be 1 action")
testutils.AssertEqual(t, action.Type, actions.ActionEditNote, "action type mismatch")
testutils.AssertEqual(t, action.Schema, 2, "action schema mismatch")
testutils.AssertEqual(t, noteAction.Type, actions.ActionEditNote, "action type mismatch")
testutils.AssertEqual(t, noteAction.Schema, 2, "action schema mismatch")
testutils.AssertEqual(t, *actionData.Content, "foo bar", "action data name mismatch")
testutils.AssertEqual(t, actionData.FromBook, "js", "action data from_book mismatch")
if actionData.ToBook != nil {
t.Errorf("action data to_book mismatch. Expected %+v. Got %+v", nil, actionData.ToBook)
}
testutils.AssertEqual(t, actionData.NoteUUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "action data note_uuis mismatch")
testutils.AssertNotEqual(t, action.Timestamp, 0, "action timestamp mismatch")
testutils.AssertEqual(t, len(book.Notes), 2, "Book should have one note")
testutils.AssertEqual(t, book.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "Note should have UUID")
testutils.AssertEqual(t, book.Notes[0].Content, "Booleans have toString()", "Note content mismatch")
testutils.AssertEqual(t, book.Notes[1].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "Note should have UUID")
testutils.AssertEqual(t, book.Notes[1].Content, "foo bar", "Note content mismatch")
testutils.AssertNotEqual(t, book.Notes[1].EditedOn, int64(0), "Note edited_on mismatch")
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "action timestamp mismatch")
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "Note should have UUID")
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "Note content mismatch")
testutils.AssertEqual(t, n2.UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "Note should have UUID")
testutils.AssertEqual(t, n2.Content, "foo bar", "Note content mismatch")
testutils.AssertNotEqual(t, n2.EditedOn, 0, "Note edited_on mismatch")
}
func TestRemoveNote(t *testing.T) {
// Setup
ctx := testutils.InitCtx("./tmp")
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
// Set up
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
// init files by running root command
runDnoteCmd(ctx)
testutils.WriteFile(ctx, "./testutils/fixtures/dnote3.json", "dnote")
testutils.Setup2(t, ctx)
// Execute
cmd, stderr, err := newDnoteCmd(ctx, "remove", "js", "0")
if err != nil {
panic(errors.Wrap(err, "Failed to get command"))
}
stdin, err := cmd.StdinPipe()
if err != nil {
panic(errors.Wrap(err, "Failed to get stdin %s"))
}
defer stdin.Close()
// Start the program
err = cmd.Start()
if err != nil {
panic(errors.Wrap(err, "Failed to start command"))
}
// confirm
_, err = io.WriteString(stdin, "y\n")
if err != nil {
panic(errors.Wrap(err, "Failed to write to stdin"))
}
err = cmd.Wait()
if err != nil {
panic(errors.Wrapf(err, "Failed to run command %s", stderr.String()))
}
testutils.WaitDnoteCmd(t, ctx, testutils.UserConfirm, binaryName, "remove", "js", "1")
// Test
dnote, err := core.GetDnote(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
}
actionSlice, err := core.ReadActionLog(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to read actions"))
}
db := ctx.DB
if len(actionSlice) != 1 {
t.Fatalf("action log length mismatch. got %d", len(actionSlice))
}
var actionCount, noteCount, bookCount, jsNoteCount, linuxNoteCount int
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.MustScan(t, "counting js notes", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
testutils.MustScan(t, "counting linux notes", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
book := dnote["js"]
otherBook := dnote["linux"]
action := actionSlice[0]
testutils.AssertEqualf(t, actionCount, 1, "action count mismatch")
testutils.AssertEqualf(t, bookCount, 2, "book count mismatch")
testutils.AssertEqualf(t, noteCount, 2, "note count mismatch")
testutils.AssertEqual(t, jsNoteCount, 1, "Book should have one note")
testutils.AssertEqual(t, linuxNoteCount, 1, "Other book should have one note")
var b1, b2 infra.Book
var n1 infra.Note
testutils.MustScan(t, "getting b1",
db.QueryRow("SELECT label FROM books WHERE uuid = ?", "js-book-uuid"),
&b1.Name)
testutils.MustScan(t, "getting b2",
db.QueryRow("SELECT label FROM books WHERE uuid = ?", "linux-book-uuid"),
&b2.Name)
testutils.MustScan(t, "getting n1",
db.QueryRow("SELECT uuid, content, added_on FROM notes WHERE book_uuid = ? AND id = ?", "js-book-uuid", 2),
&n1.UUID, &n1.Content, &n1.AddedOn)
var noteAction actions.Action
testutils.MustScan(t, "getting note action",
db.QueryRow("SELECT type, schema, data FROM actions WHERE type = ?", actions.ActionRemoveNote), &noteAction.Type, &noteAction.Schema, &noteAction.Data)
var actionData actions.RemoveNoteDataV1
err = json.Unmarshal(action.Data, &actionData)
if err != nil {
log.Fatalf("Failed to unmarshal the action data: %s", err)
if err := json.Unmarshal(noteAction.Data, &actionData); err != nil {
log.Fatalf("unmarshalling the action data: %s", err)
}
testutils.AssertEqual(t, len(actionSlice), 1, "There should be 1 action")
testutils.AssertEqual(t, action.Type, actions.ActionRemoveNote, "action type mismatch")
testutils.AssertEqual(t, actionData.NoteUUID, "43827b9a-c2b0-4c06-a290-97991c896653", "action data note_uuid mismatch")
testutils.AssertEqual(t, b1.Name, "js", "b1 label mismatch")
testutils.AssertEqual(t, b2.Name, "linux", "b2 label mismatch")
testutils.AssertEqual(t, noteAction.Schema, 1, "action schema mismatch")
testutils.AssertEqual(t, noteAction.Type, actions.ActionRemoveNote, "action type mismatch")
testutils.AssertEqual(t, actionData.NoteUUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "action data note_uuid mismatch")
testutils.AssertEqual(t, actionData.BookName, "js", "action data book_name mismatch")
testutils.AssertNotEqual(t, action.Timestamp, 0, "action timestamp mismatch")
testutils.AssertEqual(t, len(book.Notes), 1, "Book should have one note")
testutils.AssertEqual(t, len(otherBook.Notes), 1, "Other book should have one note")
testutils.AssertEqual(t, book.Notes[0].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "Note should have UUID")
testutils.AssertEqual(t, book.Notes[0].Content, "Date object implements mathematical comparisons", "Note content mismatch")
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "action timestamp mismatch")
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "Note should have UUID")
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "Note content mismatch")
}
func TestRemoveBook(t *testing.T) {
// Setup
ctx := testutils.InitCtx("./tmp")
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
// Set up
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
// init files by running root command
runDnoteCmd(ctx)
testutils.WriteFile(ctx, "./testutils/fixtures/dnote3.json", "dnote")
testutils.Setup2(t, ctx)
// Execute
cmd, stderr, err := newDnoteCmd(ctx, "remove", "-b", "js")
if err != nil {
panic(errors.Wrap(err, "Failed to get command"))
}
stdin, err := cmd.StdinPipe()
if err != nil {
panic(errors.Wrap(err, "Failed to get stdin %s"))
}
defer stdin.Close()
// Start the program
err = cmd.Start()
if err != nil {
panic(errors.Wrap(err, "Failed to start command"))
}
// confirm
_, err = io.WriteString(stdin, "y\n")
if err != nil {
panic(errors.Wrap(err, "Failed to write to stdin"))
}
err = cmd.Wait()
if err != nil {
panic(errors.Wrapf(err, "Failed to run command %s", stderr.String()))
}
testutils.WaitDnoteCmd(t, ctx, testutils.UserConfirm, binaryName, "remove", "-b", "js")
// Test
dnote, err := core.GetDnote(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
}
actionSlice, err := core.ReadActionLog(ctx)
if err != nil {
t.Fatal(errors.Wrap(err, "Failed to read actions"))
}
db := ctx.DB
if len(actionSlice) != 1 {
t.Fatalf("action log length mismatch. got %d", len(actionSlice))
}
var actionCount, noteCount, bookCount, jsNoteCount, linuxNoteCount int
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.MustScan(t, "counting js notes", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
testutils.MustScan(t, "counting linux notes", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
book := dnote["linux"]
action := actionSlice[0]
testutils.AssertEqualf(t, actionCount, 1, "action count mismatch")
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
testutils.AssertEqualf(t, noteCount, 1, "note count mismatch")
testutils.AssertEqual(t, jsNoteCount, 0, "some notes in book were not deleted")
testutils.AssertEqual(t, linuxNoteCount, 1, "Other book should have one note")
var b1 infra.Book
testutils.MustScan(t, "getting b1",
db.QueryRow("SELECT label FROM books WHERE uuid = ?", "linux-book-uuid"),
&b1.Name)
var action actions.Action
testutils.MustScan(t, "getting an action",
db.QueryRow("SELECT type, schema, data FROM actions WHERE type = ?", actions.ActionRemoveBook), &action.Type, &action.Schema, &action.Data)
var actionData actions.RemoveBookDataV1
err = json.Unmarshal(action.Data, &actionData)
if err != nil {
log.Fatalf("Failed to unmarshal the action data: %s", err)
if err := json.Unmarshal(action.Data, &actionData); err != nil {
log.Fatalf("unmarshalling the action data: %s", err)
}
testutils.AssertEqual(t, len(actionSlice), 1, "There should be 1 action")
testutils.AssertEqual(t, action.Type, actions.ActionRemoveBook, "action type mismatch")
testutils.AssertEqual(t, actionData.BookName, "js", "action data name mismatch")
testutils.AssertNotEqual(t, action.Timestamp, 0, "action timestamp mismatch")
testutils.AssertEqual(t, len(dnote), 1, "There should be 1 book")
testutils.AssertEqual(t, book.Name, "linux", "Remaining book name mismatch")
testutils.AssertEqual(t, len(book.Notes), 1, "Remaining book should have one note")
testutils.AssertEqual(t, b1.Name, "linux", "Remaining book name mismatch")
}

View file

@ -0,0 +1,2 @@
[{"uuid":"6145c1b7-f286-4d9f-b0f6-00d274baefc6","schema":1,"type":"add_book","data":{"book_name":"js"},"timestamp":1536977229},{"uuid":"c048a56b-179c-4f31-9995-81e9b32b7dd6","schema":2,"type":"add_note","data":{"note_uuid":"35cbcab1-6a2a-4cc8-97e0-e73bbbd54626","book_name":"js","content":"js test 1","public":false},"timestamp":1536977229},{"uuid":"f557ef48-c304-47dc-adfb-46b7306e701f","schema":2,"type":"add_note","data":{"note_uuid":"7c1fcfb2-de8b-4350-88f0-fb3cbaf6630a","book_name":"js","content":"js test 2","public":false},"timestamp":1536977230},{"uuid":"8d79db34-343d-4331-ae5b-24743f17ca7f","schema":2,"type":"add_note","data":{"note_uuid":"b23a88ba-b291-4294-9795-86b394db5dcf","book_name":"js","content":"js test 3","public":false},"timestamp":1536977234},{"uuid":"b9c1ed4a-e6b3-41f2-983b-593ec7b8b7a1","schema":1,"type":"add_book","data":{"book_name":"css"},"timestamp":1536977237},{"uuid":"06ed7ef0-f171-4bd7-ae8e-97b5d06a4c49","schema":2,"type":"add_note","data":{"note_uuid":"d69edb54-5b31-4cdd-a4a5-34f0a0bfa153","book_name":"css","content":"js test 3","public":false},"timestamp":1536977237},{"uuid":"7f173cef-1688-4177-a373-145fcd822b2f","schema":2,"type":"edit_note","data":{"note_uuid":"d69edb54-5b31-4cdd-a4a5-34f0a0bfa153","from_book":"css","to_book":null,"content":"css test 1","public":null},"timestamp":1536977253},{"uuid":"64352e08-aa7a-45f4-b760-b3f38b5e11fa","schema":1,"type":"add_book","data":{"book_name":"sql"},"timestamp":1536977261},{"uuid":"82e20a12-bda8-45f7-ac42-b453b6daa5ec","schema":2,"type":"add_note","data":{"note_uuid":"2f47d390-685b-4b84-89ac-704c6fb8d3fb","book_name":"sql","content":"blah","public":false},"timestamp":1536977261},{"uuid":"a29055f4-ace4-44fd-8800-3396edbccaef","schema":1,"type":"remove_book","data":{"book_name":"sql"},"timestamp":1536977268},{"uuid":"871a5562-1bd0-43c1-b550-5bbb727ac7c4","schema":1,"type":"remove_note","data":{"note_uuid":"b23a88ba-b291-4294-9795-86b394db5dcf","book_name":"js"},"timestamp":1536977274}]

View file

@ -0,0 +1,33 @@
{
"css": {
"name": "css",
"notes": [
{
"uuid": "d69edb54-5b31-4cdd-a4a5-34f0a0bfa153",
"content": "css test 1",
"added_on": 1536977237,
"edited_on": 1536977253,
"public": false
}
]
},
"js": {
"name": "js",
"notes": [
{
"uuid": "35cbcab1-6a2a-4cc8-97e0-e73bbbd54626",
"content": "js test 1",
"added_on": 1536977229,
"edited_on": 0,
"public": false
},
{
"uuid": "7c1fcfb2-de8b-4350-88f0-fb3cbaf6630a",
"content": "js test 2",
"added_on": 1536977230,
"edited_on": 0,
"public": false
}
]
}
}

View file

@ -0,0 +1,3 @@
editor: vim
apikey: ""

View file

@ -0,0 +1 @@
current_version: 7

View file

@ -0,0 +1,3 @@
last_upgrade: 1536977220
bookmark: 9
last_action: 1536977274

950
migrate/legacy.go Normal file
View file

@ -0,0 +1,950 @@
// Package migrate provides migration logic for both sqlite and
// legacy JSON-based notes used until v0.4.x releases
package migrate
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"time"
"github.com/dnote/cli/infra"
"github.com/dnote/cli/utils"
"github.com/pkg/errors"
"github.com/satori/go.uuid"
"gopkg.in/yaml.v2"
)
var (
schemaFilename = "schema"
backupDirName = ".dnote-bak"
)
// migration IDs
const (
_ = iota
legacyMigrationV1
legacyMigrationV2
legacyMigrationV3
legacyMigrationV4
legacyMigrationV5
legacyMigrationV6
legacyMigrationV7
legacyMigrationV8
)
var migrationSequence = []int{
legacyMigrationV1,
legacyMigrationV2,
legacyMigrationV3,
legacyMigrationV4,
legacyMigrationV5,
legacyMigrationV6,
legacyMigrationV7,
legacyMigrationV8,
}
type schema struct {
currentVersion int `yaml:"current_version"`
}
func makeSchema(complete bool) schema {
s := schema{}
var currentVersion int
if complete {
currentVersion = len(migrationSequence)
}
s.currentVersion = currentVersion
return s
}
// Legacy performs migration on JSON-based dnote if necessary
func Legacy(ctx infra.DnoteCtx) error {
// If schema does not exist, no need run a legacy migration
schemaPath := getSchemaPath(ctx)
if ok := utils.FileExists(schemaPath); !ok {
return nil
}
unrunMigrations, err := getUnrunMigrations(ctx)
if err != nil {
return errors.Wrap(err, "Failed to get unrun migrations")
}
for _, mid := range unrunMigrations {
if err := performMigration(ctx, mid); err != nil {
return errors.Wrapf(err, "running migration #%d", mid)
}
}
return nil
}
// performMigration backs up current .dnote data, performs migration, and
// restores or cleans backups depending on if there is an error
func performMigration(ctx infra.DnoteCtx, migrationID int) error {
// legacyMigrationV8 is the final migration of the legacy JSON Dnote migration
// migrate to sqlite and return
if migrationID == legacyMigrationV8 {
if err := migrateToV8(ctx); err != nil {
return errors.Wrap(err, "migrating to sqlite")
}
return nil
}
if err := backupDnoteDir(ctx); err != nil {
return errors.Wrap(err, "Failed to back up dnote directory")
}
var migrationError error
switch migrationID {
case legacyMigrationV1:
migrationError = migrateToV1(ctx)
case legacyMigrationV2:
migrationError = migrateToV2(ctx)
case legacyMigrationV3:
migrationError = migrateToV3(ctx)
case legacyMigrationV4:
migrationError = migrateToV4(ctx)
case legacyMigrationV5:
migrationError = migrateToV5(ctx)
case legacyMigrationV6:
migrationError = migrateToV6(ctx)
case legacyMigrationV7:
migrationError = migrateToV7(ctx)
default:
return errors.Errorf("Unrecognized migration id %d", migrationID)
}
if migrationError != nil {
if err := restoreBackup(ctx); err != nil {
panic(errors.Wrap(err, "Failed to restore backup for a failed migration"))
}
return errors.Wrapf(migrationError, "Failed to perform migration #%d", migrationID)
}
if err := clearBackup(ctx); err != nil {
return errors.Wrap(err, "Failed to clear backup")
}
if err := updateSchemaVersion(ctx, migrationID); err != nil {
return errors.Wrap(err, "Failed to update schema version")
}
return nil
}
// backupDnoteDir backs up the dnote directory to a temporary backup directory
func backupDnoteDir(ctx infra.DnoteCtx) error {
srcPath := fmt.Sprintf("%s/.dnote", ctx.HomeDir)
tmpPath := fmt.Sprintf("%s/%s", ctx.HomeDir, backupDirName)
if err := utils.CopyDir(srcPath, tmpPath); err != nil {
return errors.Wrap(err, "Failed to copy the .dnote directory")
}
return nil
}
func restoreBackup(ctx infra.DnoteCtx) error {
var err error
defer func() {
if err != nil {
log.Printf(`Failed to restore backup for a failed migration.
Don't worry. Your data is still intact in the backup directory.
Get help on https://github.com/dnote/cli/issues`)
}
}()
srcPath := fmt.Sprintf("%s/.dnote", ctx.HomeDir)
backupPath := fmt.Sprintf("%s/%s", ctx.HomeDir, backupDirName)
if err = os.RemoveAll(srcPath); err != nil {
return errors.Wrapf(err, "Failed to clear current dnote data at %s", backupPath)
}
if err = os.Rename(backupPath, srcPath); err != nil {
return errors.Wrap(err, `Failed to copy backup data to the original directory.`)
}
return nil
}
func clearBackup(ctx infra.DnoteCtx) error {
backupPath := fmt.Sprintf("%s/%s", ctx.HomeDir, backupDirName)
if err := os.RemoveAll(backupPath); err != nil {
return errors.Wrapf(err, "Failed to remove backup at %s", backupPath)
}
return nil
}
// getSchemaPath returns the path to the file containing schema info
func getSchemaPath(ctx infra.DnoteCtx) string {
return fmt.Sprintf("%s/%s", ctx.DnoteDir, schemaFilename)
}
// initSchemaFile creates a migration file
func initSchemaFile(ctx infra.DnoteCtx, pristine bool) error {
path := getSchemaPath(ctx)
if utils.FileExists(path) {
return nil
}
s := makeSchema(pristine)
err := writeSchema(ctx, s)
if err != nil {
return errors.Wrap(err, "Failed to write schema")
}
return nil
}
func readSchema(ctx infra.DnoteCtx) (schema, error) {
var ret schema
path := getSchemaPath(ctx)
b, err := ioutil.ReadFile(path)
if err != nil {
return ret, errors.Wrap(err, "Failed to read schema file")
}
err = yaml.Unmarshal(b, &ret)
if err != nil {
return ret, errors.Wrap(err, "Failed to unmarshal the schema JSON")
}
return ret, nil
}
func writeSchema(ctx infra.DnoteCtx, s schema) error {
path := getSchemaPath(ctx)
d, err := yaml.Marshal(&s)
if err != nil {
return errors.Wrap(err, "Failed to marshal schema into yaml")
}
if err := ioutil.WriteFile(path, d, 0644); err != nil {
return errors.Wrap(err, "Failed to write schema file")
}
return nil
}
func getUnrunMigrations(ctx infra.DnoteCtx) ([]int, error) {
var ret []int
schema, err := readSchema(ctx)
if err != nil {
return ret, errors.Wrap(err, "Failed to read schema")
}
if schema.currentVersion == len(migrationSequence) {
return ret, nil
}
nextVersion := schema.currentVersion
ret = migrationSequence[nextVersion:]
return ret, nil
}
func updateSchemaVersion(ctx infra.DnoteCtx, mID int) error {
s, err := readSchema(ctx)
if err != nil {
return errors.Wrap(err, "Failed to read schema")
}
s.currentVersion = mID
err = writeSchema(ctx, s)
if err != nil {
return errors.Wrap(err, "Failed to write schema")
}
return nil
}
/***** snapshots **/
// v2
type migrateToV2PreNote struct {
UID string
Content string
AddedOn int64
}
type migrateToV2PostNote struct {
UUID string `json:"uuid"`
Content string `json:"content"`
AddedOn int64 `json:"added_on"`
EditedOn int64 `json:"editd_on"`
}
type migrateToV2PreBook []migrateToV2PreNote
type migrateToV2PostBook struct {
Name string `json:"name"`
Notes []migrateToV2PostNote `json:"notes"`
}
type migrateToV2PreDnote map[string]migrateToV2PreBook
type migrateToV2PostDnote map[string]migrateToV2PostBook
//v3
var (
migrateToV3ActionAddNote = "add_note"
migrateToV3ActionAddBook = "add_book"
)
type migrateToV3Note struct {
UUID string `json:"uuid"`
Content string `json:"content"`
AddedOn int64 `json:"added_on"`
EditedOn int64 `json:"edited_on"`
}
type migrateToV3Book struct {
UUID string `json:"uuid"`
Name string `json:"name"`
Notes []migrateToV3Note `json:"notes"`
}
type migrateToV3Dnote map[string]migrateToV3Book
type migrateToV3Action struct {
Type string `json:"type"`
Data map[string]interface{} `json:"data"`
Timestamp int64 `json:"timestamp"`
}
// v4
type migrateToV4PreConfig struct {
Book string
APIKey string
}
type migrateToV4PostConfig struct {
Editor string
APIKey string
}
// v5
type migrateToV5AddNoteData struct {
NoteUUID string `json:"note_uuid"`
BookName string `json:"book_name"`
Content string `json:"content"`
}
type migrateToV5RemoveNoteData struct {
NoteUUID string `json:"note_uuid"`
BookName string `json:"book_name"`
}
type migrateToV5AddBookData struct {
BookName string `json:"book_name"`
}
type migrateToV5RemoveBookData struct {
BookName string `json:"book_name"`
}
type migrateToV5PreEditNoteData struct {
NoteUUID string `json:"note_uuid"`
BookName string `json:"book_name"`
Content string `json:"content"`
}
type migrateToV5PostEditNoteData struct {
NoteUUID string `json:"note_uuid"`
FromBook string `json:"from_book"`
ToBook string `json:"to_book"`
Content string `json:"content"`
}
type migrateToV5PreAction struct {
ID int `json:"id"`
Type string `json:"type"`
Data json.RawMessage `json:"data"`
Timestamp int64 `json:"timestamp"`
}
type migrateToV5PostAction struct {
UUID string `json:"uuid"`
Schema int `json:"schema"`
Type string `json:"type"`
Data json.RawMessage `json:"data"`
Timestamp int64 `json:"timestamp"`
}
var (
migrateToV5ActionAddNote = "add_note"
migrateToV5ActionRemoveNote = "remove_note"
migrateToV5ActionEditNote = "edit_note"
migrateToV5ActionAddBook = "add_book"
migrateToV5ActionRemoveBook = "remove_book"
)
// v6
type migrateToV6PreNote struct {
UUID string `json:"uuid"`
Content string `json:"content"`
AddedOn int64 `json:"added_on"`
EditedOn int64 `json:"edited_on"`
}
type migrateToV6PostNote struct {
UUID string `json:"uuid"`
Content string `json:"content"`
AddedOn int64 `json:"added_on"`
EditedOn int64 `json:"edited_on"`
// Make a pointer to test absent values
Public *bool `json:"public"`
}
type migrateToV6PreBook struct {
Name string `json:"name"`
Notes []migrateToV6PreNote `json:"notes"`
}
type migrateToV6PostBook struct {
Name string `json:"name"`
Notes []migrateToV6PostNote `json:"notes"`
}
type migrateToV6PreDnote map[string]migrateToV6PreBook
type migrateToV6PostDnote map[string]migrateToV6PostBook
// v7
var migrateToV7ActionTypeEditNote = "edit_note"
type migrateToV7Action struct {
UUID string `json:"uuid"`
Schema int `json:"schema"`
Type string `json:"type"`
Data json.RawMessage `json:"data"`
Timestamp int64 `json:"timestamp"`
}
type migrateToV7EditNoteDataV1 struct {
NoteUUID string `json:"note_uuid"`
FromBook string `json:"from_book"`
ToBook string `json:"to_book"`
Content string `json:"content"`
}
type migrateToV7EditNoteDataV2 struct {
NoteUUID string `json:"note_uuid"`
FromBook string `json:"from_book"`
ToBook *string `json:"to_book"`
Content *string `json:"content"`
Public *bool `json:"public"`
}
// v8
type migrateToV8Action struct {
UUID string `json:"uuid"`
Schema int `json:"schema"`
Type string `json:"type"`
Data json.RawMessage `json:"data"`
Timestamp int64 `json:"timestamp"`
}
type migrateToV8Note struct {
UUID string `json:"uuid"`
Content string `json:"content"`
AddedOn int64 `json:"added_on"`
EditedOn int64 `json:"edited_on"`
// Make a pointer to test absent values
Public *bool `json:"public"`
}
type migrateToV8Book struct {
Name string `json:"name"`
Notes []migrateToV8Note `json:"notes"`
}
type migrateToV8Dnote map[string]migrateToV8Book
type migrateToV8Timestamp struct {
LastUpgrade int64 `yaml:"last_upgrade"`
Bookmark int `yaml:"bookmark"`
LastAction int64 `yaml:"last_action"`
}
var migrateToV8SystemKeyLastUpgrade = "last_upgrade"
var migrateToV8SystemKeyLastAction = "last_action"
var migrateToV8SystemKeyBookMark = "bookmark"
/***** migrations **/
// migrateToV1 deletes YAML archive if exists
func migrateToV1(ctx infra.DnoteCtx) error {
yamlPath := fmt.Sprintf("%s/%s", ctx.HomeDir, ".dnote-yaml-archived")
if !utils.FileExists(yamlPath) {
return nil
}
if err := os.Remove(yamlPath); err != nil {
return errors.Wrap(err, "Failed to delete .dnote archive")
}
return nil
}
func migrateToV2(ctx infra.DnoteCtx) error {
notePath := fmt.Sprintf("%s/dnote", ctx.DnoteDir)
b, err := ioutil.ReadFile(notePath)
if err != nil {
return errors.Wrap(err, "Failed to read the note file")
}
var preDnote migrateToV2PreDnote
postDnote := migrateToV2PostDnote{}
err = json.Unmarshal(b, &preDnote)
if err != nil {
return errors.Wrap(err, "Failed to unmarshal existing dnote into JSON")
}
for bookName, book := range preDnote {
var notes = make([]migrateToV2PostNote, 0, len(book))
for _, note := range book {
newNote := migrateToV2PostNote{
UUID: uuid.NewV4().String(),
Content: note.Content,
AddedOn: note.AddedOn,
EditedOn: 0,
}
notes = append(notes, newNote)
}
b := migrateToV2PostBook{
Name: bookName,
Notes: notes,
}
postDnote[bookName] = b
}
d, err := json.MarshalIndent(postDnote, "", " ")
if err != nil {
return errors.Wrap(err, "Failed to marshal new dnote into JSON")
}
err = ioutil.WriteFile(notePath, d, 0644)
if err != nil {
return errors.Wrap(err, "Failed to write the new dnote into the file")
}
return nil
}
// migrateToV3 generates actions for existing dnote
func migrateToV3(ctx infra.DnoteCtx) error {
notePath := fmt.Sprintf("%s/dnote", ctx.DnoteDir)
actionsPath := fmt.Sprintf("%s/actions", ctx.DnoteDir)
b, err := ioutil.ReadFile(notePath)
if err != nil {
return errors.Wrap(err, "Failed to read the note file")
}
var dnote migrateToV3Dnote
err = json.Unmarshal(b, &dnote)
if err != nil {
return errors.Wrap(err, "Failed to unmarshal existing dnote into JSON")
}
var actions []migrateToV3Action
for bookName, book := range dnote {
// Find the minimum added_on timestamp from the notes that belong to the book
// to give timstamp to the add_book action.
// Logically add_book must have happened no later than the first add_note
// to the book in order for sync to work.
minTs := time.Now().Unix()
for _, note := range book.Notes {
if note.AddedOn < minTs {
minTs = note.AddedOn
}
}
action := migrateToV3Action{
Type: migrateToV3ActionAddBook,
Data: map[string]interface{}{
"book_name": bookName,
},
Timestamp: minTs,
}
actions = append(actions, action)
for _, note := range book.Notes {
action := migrateToV3Action{
Type: migrateToV3ActionAddNote,
Data: map[string]interface{}{
"note_uuid": note.UUID,
"book_name": book.Name,
"content": note.Content,
},
Timestamp: note.AddedOn,
}
actions = append(actions, action)
}
}
a, err := json.Marshal(actions)
if err != nil {
return errors.Wrap(err, "Failed to marshal actions into JSON")
}
err = ioutil.WriteFile(actionsPath, a, 0644)
if err != nil {
return errors.Wrap(err, "Failed to write the actions into a file")
}
return nil
}
func getEditorCommand() string {
editor := os.Getenv("EDITOR")
switch editor {
case "atom":
return "atom -w"
case "subl":
return "subl -n -w"
case "mate":
return "mate -w"
case "vim":
return "vim"
case "nano":
return "nano"
case "emacs":
return "emacs"
default:
return "vi"
}
}
func migrateToV4(ctx infra.DnoteCtx) error {
configPath := fmt.Sprintf("%s/dnoterc", ctx.DnoteDir)
b, err := ioutil.ReadFile(configPath)
if err != nil {
return errors.Wrap(err, "Failed to read the config file")
}
var preConfig migrateToV4PreConfig
err = yaml.Unmarshal(b, &preConfig)
if err != nil {
return errors.Wrap(err, "Failed to unmarshal existing config into JSON")
}
postConfig := migrateToV4PostConfig{
APIKey: preConfig.APIKey,
Editor: getEditorCommand(),
}
data, err := yaml.Marshal(postConfig)
if err != nil {
return errors.Wrap(err, "Failed to marshal config into JSON")
}
err = ioutil.WriteFile(configPath, data, 0644)
if err != nil {
return errors.Wrap(err, "Failed to write the config into a file")
}
return nil
}
// migrateToV5 migrates actions
func migrateToV5(ctx infra.DnoteCtx) error {
actionsPath := fmt.Sprintf("%s/actions", ctx.DnoteDir)
b, err := ioutil.ReadFile(actionsPath)
if err != nil {
return errors.Wrap(err, "reading the actions file")
}
var actions []migrateToV5PreAction
err = json.Unmarshal(b, &actions)
if err != nil {
return errors.Wrap(err, "unmarshalling actions from JSON")
}
result := []migrateToV5PostAction{}
for _, action := range actions {
var data json.RawMessage
switch action.Type {
case migrateToV5ActionEditNote:
var oldData migrateToV5PreEditNoteData
if err = json.Unmarshal(action.Data, &oldData); err != nil {
return errors.Wrapf(err, "unmarshalling old data of an edit note action %d", action.ID)
}
migratedData := migrateToV5PostEditNoteData{
NoteUUID: oldData.NoteUUID,
FromBook: oldData.BookName,
Content: oldData.Content,
}
b, err = json.Marshal(migratedData)
if err != nil {
return errors.Wrap(err, "marshalling data")
}
data = b
default:
data = action.Data
}
migrated := migrateToV5PostAction{
UUID: uuid.NewV4().String(),
Schema: 1,
Type: action.Type,
Data: data,
Timestamp: action.Timestamp,
}
result = append(result, migrated)
}
a, err := json.Marshal(result)
if err != nil {
return errors.Wrap(err, "marshalling result into JSON")
}
err = ioutil.WriteFile(actionsPath, a, 0644)
if err != nil {
return errors.Wrap(err, "writing the result into a file")
}
return nil
}
// migrateToV6 adds a 'public' field to notes
func migrateToV6(ctx infra.DnoteCtx) error {
notePath := fmt.Sprintf("%s/dnote", ctx.DnoteDir)
b, err := ioutil.ReadFile(notePath)
if err != nil {
return errors.Wrap(err, "Failed to read the note file")
}
var preDnote migrateToV6PreDnote
postDnote := migrateToV6PostDnote{}
err = json.Unmarshal(b, &preDnote)
if err != nil {
return errors.Wrap(err, "Failed to unmarshal existing dnote into JSON")
}
for bookName, book := range preDnote {
var notes = make([]migrateToV6PostNote, 0, len(book.Notes))
public := false
for _, note := range book.Notes {
newNote := migrateToV6PostNote{
UUID: note.UUID,
Content: note.Content,
AddedOn: note.AddedOn,
EditedOn: note.EditedOn,
Public: &public,
}
notes = append(notes, newNote)
}
b := migrateToV6PostBook{
Name: bookName,
Notes: notes,
}
postDnote[bookName] = b
}
d, err := json.MarshalIndent(postDnote, "", " ")
if err != nil {
return errors.Wrap(err, "Failed to marshal new dnote into JSON")
}
err = ioutil.WriteFile(notePath, d, 0644)
if err != nil {
return errors.Wrap(err, "Failed to write the new dnote into the file")
}
return nil
}
// migrateToV7 migrates data of edit_note action to the proper version which is
// EditNoteDataV2. Due to a bug, edit logged actions with schema version '2'
// but with a data of EditNoteDataV1. https://github.com/dnote/cli/issues/107
func migrateToV7(ctx infra.DnoteCtx) error {
actionPath := fmt.Sprintf("%s/actions", ctx.DnoteDir)
b, err := ioutil.ReadFile(actionPath)
if err != nil {
return errors.Wrap(err, "reading actions file")
}
var preActions []migrateToV7Action
postActions := []migrateToV7Action{}
err = json.Unmarshal(b, &preActions)
if err != nil {
return errors.Wrap(err, "unmarshalling existing actions")
}
for _, action := range preActions {
var newAction migrateToV7Action
if action.Type == migrateToV7ActionTypeEditNote {
var oldData migrateToV7EditNoteDataV1
if e := json.Unmarshal(action.Data, &oldData); e != nil {
return errors.Wrapf(e, "unmarshalling data of action with uuid %s", action.Data)
}
newData := migrateToV7EditNoteDataV2{
NoteUUID: oldData.NoteUUID,
FromBook: oldData.FromBook,
ToBook: nil,
Content: &oldData.Content,
Public: nil,
}
d, e := json.Marshal(newData)
if e != nil {
return errors.Wrapf(e, "marshalling new data of action with uuid %s", action.Data)
}
newAction = migrateToV7Action{
UUID: action.UUID,
Schema: action.Schema,
Type: action.Type,
Timestamp: action.Timestamp,
Data: d,
}
} else {
newAction = action
}
postActions = append(postActions, newAction)
}
d, err := json.Marshal(postActions)
if err != nil {
return errors.Wrap(err, "marshalling new actions")
}
err = ioutil.WriteFile(actionPath, d, 0644)
if err != nil {
return errors.Wrap(err, "writing new actions to a file")
}
return nil
}
// migrateToV8 migrates dnote data to sqlite database
func migrateToV8(ctx infra.DnoteCtx) error {
tx, err := ctx.DB.Begin()
if err != nil {
return errors.Wrap(err, "beginning a transaction")
}
// 1. Migrate the the dnote file
dnoteFilePath := fmt.Sprintf("%s/dnote", ctx.DnoteDir)
b, err := ioutil.ReadFile(dnoteFilePath)
if err != nil {
return errors.Wrap(err, "reading the notes")
}
var dnote migrateToV8Dnote
err = json.Unmarshal(b, &dnote)
if err != nil {
return errors.Wrap(err, "unmarshalling notes to JSON")
}
for bookName, book := range dnote {
bookUUID := uuid.NewV4().String()
_, err = tx.Exec(`INSERT INTO books (uuid, label) VALUES (?, ?)`, bookUUID, bookName)
if err != nil {
tx.Rollback()
return errors.Wrapf(err, "inserting book %s", book.Name)
}
for _, note := range book.Notes {
_, err = tx.Exec(`INSERT INTO notes
(uuid, book_uuid, content, added_on, edited_on, public)
VALUES (?, ?, ?, ?, ?, ?)
`, note.UUID, bookUUID, note.Content, note.AddedOn, note.EditedOn, note.Public)
if err != nil {
tx.Rollback()
return errors.Wrapf(err, "inserting the note %s", note.UUID)
}
}
}
// 2. Migrate the actions file
actionsPath := fmt.Sprintf("%s/actions", ctx.DnoteDir)
b, err = ioutil.ReadFile(actionsPath)
if err != nil {
return errors.Wrap(err, "reading the actions")
}
var actions []migrateToV8Action
err = json.Unmarshal(b, &actions)
if err != nil {
return errors.Wrap(err, "unmarshalling actions from JSON")
}
for _, action := range actions {
_, err = tx.Exec(`INSERT INTO actions
(uuid, schema, type, data, timestamp)
VALUES (?, ?, ?, ?, ?)
`, action.UUID, action.Schema, action.Type, action.Data, action.Timestamp)
if err != nil {
tx.Rollback()
return errors.Wrapf(err, "inserting the action %s", action.UUID)
}
}
// 3. Migrate the timestamps file
timestampsPath := fmt.Sprintf("%s/timestamps", ctx.DnoteDir)
b, err = ioutil.ReadFile(timestampsPath)
if err != nil {
return errors.Wrap(err, "reading the timestamps")
}
var timestamp migrateToV8Timestamp
err = yaml.Unmarshal(b, &timestamp)
if err != nil {
return errors.Wrap(err, "unmarshalling timestamps from YAML")
}
_, err = tx.Exec(`INSERT INTO system (key, value) VALUES (?, ?)`,
migrateToV8SystemKeyLastUpgrade, timestamp.LastUpgrade)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "inserting the last_upgrade value")
}
_, err = tx.Exec(`INSERT INTO system (key, value) VALUES (?, ?)`,
migrateToV8SystemKeyLastAction, timestamp.LastAction)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "inserting the last_action value")
}
_, err = tx.Exec(`INSERT INTO system (key, value) VALUES (?, ?)`,
migrateToV8SystemKeyBookMark, timestamp.Bookmark)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "inserting the bookmark value")
}
tx.Commit()
if err := os.RemoveAll(dnoteFilePath); err != nil {
return errors.Wrap(err, "removing the old dnote file")
}
if err := os.RemoveAll(actionsPath); err != nil {
return errors.Wrap(err, "removing the actions file")
}
if err := os.RemoveAll(timestampsPath); err != nil {
return errors.Wrap(err, "removing the timestamps file")
}
schemaPath := fmt.Sprintf("%s/schema", ctx.DnoteDir)
if err := os.RemoveAll(schemaPath); err != nil {
return errors.Wrap(err, "removing the schema file")
}
return nil
}

590
migrate/legacy_test.go Normal file
View file

@ -0,0 +1,590 @@
package migrate
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/dnote/cli/testutils"
"github.com/dnote/cli/utils"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
)
func TestMigrateToV1(t *testing.T) {
t.Run("yaml exists", func(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
yamlPath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnote-yaml-archived"))
if err != nil {
panic(errors.Wrap(err, "Failed to get absolute YAML path").Error())
}
ioutil.WriteFile(yamlPath, []byte{}, 0644)
// execute
if err := migrateToV1(ctx); err != nil {
t.Fatal(errors.Wrapf(err, "Failed to migrate").Error())
}
// test
if utils.FileExists(yamlPath) {
t.Fatal("YAML archive file has not been deleted")
}
})
t.Run("yaml does not exist", func(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
yamlPath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnote-yaml-archived"))
if err != nil {
panic(errors.Wrap(err, "Failed to get absolute YAML path").Error())
}
// execute
if err := migrateToV1(ctx); err != nil {
t.Fatal(errors.Wrapf(err, "Failed to migrate").Error())
}
// test
if utils.FileExists(yamlPath) {
t.Fatal("YAML archive file must not exist")
}
})
}
func TestMigrateToV2(t *testing.T) {
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
testutils.CopyFixture(ctx, "./fixtures/2-pre-dnote.json", "dnote")
// execute
if err := migrateToV2(ctx); err != nil {
t.Fatal(errors.Wrap(err, "Failed to migrate").Error())
}
// test
b := testutils.ReadFile(ctx, "dnote")
var postDnote migrateToV2PostDnote
if err := json.Unmarshal(b, &postDnote); err != nil {
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
}
for _, book := range postDnote {
testutils.AssertNotEqual(t, book.Name, "", "Book name was not populated")
for _, note := range book.Notes {
if len(note.UUID) == 8 {
t.Errorf("Note UUID was not migrated. It has length of %d", len(note.UUID))
}
testutils.AssertNotEqual(t, note.AddedOn, int64(0), "AddedOn was not carried over")
testutils.AssertEqual(t, note.EditedOn, int64(0), "EditedOn was not created properly")
}
}
}
func TestMigrateToV3(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
testutils.CopyFixture(ctx, "./fixtures/3-pre-dnote.json", "dnote")
// execute
if err := migrateToV3(ctx); err != nil {
t.Fatal(errors.Wrap(err, "Failed to migrate").Error())
}
// test
b := testutils.ReadFile(ctx, "dnote")
var postDnote migrateToV3Dnote
if err := json.Unmarshal(b, &postDnote); err != nil {
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
}
b = testutils.ReadFile(ctx, "actions")
var actions []migrateToV3Action
if err := json.Unmarshal(b, &actions); err != nil {
t.Fatal(errors.Wrap(err, "Failed to unmarshal the actions").Error())
}
testutils.AssertEqual(t, len(actions), 6, "actions length mismatch")
for _, book := range postDnote {
for _, note := range book.Notes {
testutils.AssertNotEqual(t, note.AddedOn, int64(0), "AddedOn was not carried over")
}
}
}
func TestMigrateToV4(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
defer os.Setenv("EDITOR", "")
testutils.CopyFixture(ctx, "./fixtures/4-pre-dnoterc.yaml", "dnoterc")
// execute
os.Setenv("EDITOR", "vim")
if err := migrateToV4(ctx); err != nil {
t.Fatal(errors.Wrap(err, "Failed to migrate").Error())
}
// test
b := testutils.ReadFile(ctx, "dnoterc")
var config migrateToV4PostConfig
if err := yaml.Unmarshal(b, &config); err != nil {
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
}
testutils.AssertEqual(t, config.APIKey, "Oev6e1082ORasdf9rjkfjkasdfjhgei", "api key mismatch")
testutils.AssertEqual(t, config.Editor, "vim", "editor mismatch")
}
func TestMigrateToV5(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
testutils.CopyFixture(ctx, "./fixtures/5-pre-actions.json", "actions")
// execute
if err := migrateToV5(ctx); err != nil {
t.Fatal(errors.Wrap(err, "migrating").Error())
}
// test
var oldActions []migrateToV5PreAction
testutils.ReadJSON("./fixtures/5-pre-actions.json", &oldActions)
b := testutils.ReadFile(ctx, "actions")
var migratedActions []migrateToV5PostAction
if err := json.Unmarshal(b, &migratedActions); err != nil {
t.Fatal(errors.Wrap(err, "unmarshalling migrated actions").Error())
}
if len(oldActions) != len(migratedActions) {
t.Fatalf("There were %d actions but after migration there were %d", len(oldActions), len(migratedActions))
}
for idx := range migratedActions {
migrated := migratedActions[idx]
old := oldActions[idx]
testutils.AssertNotEqual(t, migrated.UUID, "", fmt.Sprintf("uuid mismatch for migrated item with index %d", idx))
testutils.AssertEqual(t, migrated.Schema, 1, fmt.Sprintf("schema mismatch for migrated item with index %d", idx))
testutils.AssertEqual(t, migrated.Timestamp, old.Timestamp, fmt.Sprintf("timestamp mismatch for migrated item with index %d", idx))
testutils.AssertEqual(t, migrated.Type, old.Type, fmt.Sprintf("timestamp mismatch for migrated item with index %d", idx))
switch migrated.Type {
case migrateToV5ActionAddNote:
var oldData, migratedData migrateToV5AddNoteData
if err := json.Unmarshal(old.Data, &oldData); err != nil {
t.Fatal(errors.Wrap(err, "unmarshalling old data").Error())
}
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
t.Fatal(errors.Wrap(err, "unmarshalling new data").Error())
}
testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx))
testutils.AssertEqual(t, oldData.Content, migratedData.Content, fmt.Sprintf("data content mismatch for item idx %d", idx))
testutils.AssertEqual(t, oldData.NoteUUID, migratedData.NoteUUID, fmt.Sprintf("data note_uuid mismatch for item idx %d", idx))
case migrateToV5ActionRemoveNote:
var oldData, migratedData migrateToV5RemoveNoteData
if err := json.Unmarshal(old.Data, &oldData); err != nil {
t.Fatal(errors.Wrap(err, "unmarshalling old data").Error())
}
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
t.Fatal(errors.Wrap(err, "unmarshalling new data").Error())
}
testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx))
testutils.AssertEqual(t, oldData.NoteUUID, migratedData.NoteUUID, fmt.Sprintf("data note_uuid mismatch for item idx %d", idx))
case migrateToV5ActionAddBook:
var oldData, migratedData migrateToV5AddBookData
if err := json.Unmarshal(old.Data, &oldData); err != nil {
t.Fatal(errors.Wrap(err, "unmarshalling old data").Error())
}
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
t.Fatal(errors.Wrap(err, "unmarshalling new data").Error())
}
testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx))
case migrateToV5ActionRemoveBook:
var oldData, migratedData migrateToV5RemoveBookData
if err := json.Unmarshal(old.Data, &oldData); err != nil {
t.Fatal(errors.Wrap(err, "unmarshalling old data").Error())
}
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
t.Fatal(errors.Wrap(err, "unmarshalling new data").Error())
}
testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx))
case migrateToV5ActionEditNote:
var oldData migrateToV5PreEditNoteData
var migratedData migrateToV5PostEditNoteData
if err := json.Unmarshal(old.Data, &oldData); err != nil {
t.Fatal(errors.Wrap(err, "unmarshalling old data").Error())
}
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
t.Fatal(errors.Wrap(err, "unmarshalling new data").Error())
}
testutils.AssertEqual(t, oldData.NoteUUID, migratedData.NoteUUID, fmt.Sprintf("data note_uuid mismatch for item idx %d", idx))
testutils.AssertEqual(t, oldData.Content, migratedData.Content, fmt.Sprintf("data content mismatch for item idx %d", idx))
testutils.AssertEqual(t, oldData.BookName, migratedData.FromBook, "book_name should have been renamed to from_book")
testutils.AssertEqual(t, migratedData.ToBook, "", "to_book should be empty")
}
}
}
func TestMigrateToV6(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
testutils.CopyFixture(ctx, "./fixtures/6-pre-dnote.json", "dnote")
// execute
if err := migrateToV6(ctx); err != nil {
t.Fatal(errors.Wrap(err, "Failed to migrate").Error())
}
// test
b := testutils.ReadFile(ctx, "dnote")
var got migrateToV6PostDnote
if err := json.Unmarshal(b, &got); err != nil {
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
}
b = testutils.ReadFileAbs("./fixtures/6-post-dnote.json")
var expected migrateToV6PostDnote
if err := json.Unmarshal(b, &expected); err != nil {
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
}
if ok := reflect.DeepEqual(expected, got); !ok {
t.Errorf("Payload does not match.\nActual: %+v\nExpected: %+v", got, expected)
}
}
func TestMigrateToV7(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
testutils.CopyFixture(ctx, "./fixtures/7-pre-actions.json", "actions")
// execute
if err := migrateToV7(ctx); err != nil {
t.Fatal(errors.Wrap(err, "migrating").Error())
}
// test
b := testutils.ReadFile(ctx, "actions")
var got []migrateToV7Action
if err := json.Unmarshal(b, &got); err != nil {
t.Fatal(errors.Wrap(err, "unmarshalling the result").Error())
}
b2 := testutils.ReadFileAbs("./fixtures/7-post-actions.json")
var expected []migrateToV7Action
if err := json.Unmarshal(b, &expected); err != nil {
t.Fatal(errors.Wrap(err, "unmarshalling the result into Dnote").Error())
}
ok, err := testutils.IsEqualJSON(b, b2)
if err != nil {
t.Fatal(errors.Wrap(err, "comparing JSON").Error())
}
if !ok {
t.Errorf("Result does not match.\nActual: %+v\nExpected: %+v", got, expected)
}
}
func TestMigrateToV8(t *testing.T) {
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
// set up
testutils.CopyFixture(ctx, "./fixtures/8-actions.json", "actions")
testutils.CopyFixture(ctx, "./fixtures/8-dnote.json", "dnote")
testutils.CopyFixture(ctx, "./fixtures/8-dnoterc.yaml", "dnoterc")
testutils.CopyFixture(ctx, "./fixtures/8-schema.yaml", "schema")
testutils.CopyFixture(ctx, "./fixtures/8-timestamps.yaml", "timestamps")
// execute
if err := migrateToV8(ctx); err != nil {
t.Fatal(errors.Wrap(err, "migrating").Error())
}
// test
// 1. test if files are migrated
dnoteFilePath := fmt.Sprintf("%s/dnote", ctx.DnoteDir)
dnotercPath := fmt.Sprintf("%s/dnoterc", ctx.DnoteDir)
schemaFilePath := fmt.Sprintf("%s/schema", ctx.DnoteDir)
timestampFilePath := fmt.Sprintf("%s/timestamps", ctx.DnoteDir)
if ok := utils.FileExists(dnoteFilePath); ok {
t.Errorf("%s still exists", dnoteFilePath)
}
if ok := utils.FileExists(schemaFilePath); ok {
t.Errorf("%s still exists", dnoteFilePath)
}
if ok := utils.FileExists(timestampFilePath); ok {
t.Errorf("%s still exists", dnoteFilePath)
}
if ok := utils.FileExists(dnotercPath); !ok {
t.Errorf("%s should exist", dnotercPath)
}
// 2. test if notes and books are migrated
db := ctx.DB
var bookCount, noteCount int
err := db.QueryRow("SELECT count(*) FROM books").Scan(&bookCount)
if err != nil {
panic(errors.Wrap(err, "counting books"))
}
err = db.QueryRow("SELECT count(*) FROM notes").Scan(&noteCount)
if err != nil {
panic(errors.Wrap(err, "counting notes"))
}
testutils.AssertEqual(t, bookCount, 2, "book count mismatch")
testutils.AssertEqual(t, noteCount, 3, "note count mismatch")
type bookInfo struct {
label string
uuid string
}
type noteInfo struct {
id int
uuid string
bookUUID string
content string
addedOn int64
editedOn int64
public bool
}
var b1, b2 bookInfo
var n1, n2, n3 noteInfo
err = db.QueryRow("SELECT label, uuid FROM books WHERE label = ?", "js").Scan(&b1.label, &b1.uuid)
if err != nil {
panic(errors.Wrap(err, "finding book 1"))
}
err = db.QueryRow("SELECT label, uuid FROM books WHERE label = ?", "css").Scan(&b2.label, &b2.uuid)
if err != nil {
panic(errors.Wrap(err, "finding book 2"))
}
err = db.QueryRow("SELECT id, uuid, book_uuid, content, added_on, edited_on, public FROM notes WHERE uuid = ?", "d69edb54-5b31-4cdd-a4a5-34f0a0bfa153").Scan(&n1.id, &n1.uuid, &n1.bookUUID, &n1.content, &n1.addedOn, &n1.editedOn, &n1.public)
if err != nil {
panic(errors.Wrap(err, "finding note 1"))
}
err = db.QueryRow("SELECT id, uuid, book_uuid, content, added_on, edited_on, public FROM notes WHERE uuid = ?", "35cbcab1-6a2a-4cc8-97e0-e73bbbd54626").Scan(&n2.id, &n2.uuid, &n2.bookUUID, &n2.content, &n2.addedOn, &n2.editedOn, &n2.public)
if err != nil {
panic(errors.Wrap(err, "finding note 2"))
}
err = db.QueryRow("SELECT id, uuid, book_uuid, content, added_on, edited_on, public FROM notes WHERE uuid = ?", "7c1fcfb2-de8b-4350-88f0-fb3cbaf6630a").Scan(&n3.id, &n3.uuid, &n3.bookUUID, &n3.content, &n3.addedOn, &n3.editedOn, &n3.public)
if err != nil {
panic(errors.Wrap(err, "finding note 3"))
}
testutils.AssertNotEqual(t, b1.uuid, "", "book 1 uuid should have been generated")
testutils.AssertEqual(t, b1.label, "js", "book 1 label mismatch")
testutils.AssertNotEqual(t, b2.uuid, "", "book 2 uuid should have been generated")
testutils.AssertEqual(t, b2.label, "css", "book 2 label mismatch")
testutils.AssertEqual(t, n1.uuid, "d69edb54-5b31-4cdd-a4a5-34f0a0bfa153", "note 1 uuid mismatch")
testutils.AssertNotEqual(t, n1.id, 0, "note 1 id should have been generated")
testutils.AssertEqual(t, n1.bookUUID, b2.uuid, "note 1 book_uuid mismatch")
testutils.AssertEqual(t, n1.content, "css test 1", "note 1 content mismatch")
testutils.AssertEqual(t, n1.addedOn, int64(1536977237), "note 1 added_on mismatch")
testutils.AssertEqual(t, n1.editedOn, int64(1536977253), "note 1 edited_on mismatch")
testutils.AssertEqual(t, n1.public, false, "note 1 public mismatch")
testutils.AssertEqual(t, n2.uuid, "35cbcab1-6a2a-4cc8-97e0-e73bbbd54626", "note 2 uuid mismatch")
testutils.AssertNotEqual(t, n2.id, 0, "note 2 id should have been generated")
testutils.AssertEqual(t, n2.bookUUID, b1.uuid, "note 2 book_uuid mismatch")
testutils.AssertEqual(t, n2.content, "js test 1", "note 2 content mismatch")
testutils.AssertEqual(t, n2.addedOn, int64(1536977229), "note 2 added_on mismatch")
testutils.AssertEqual(t, n2.editedOn, int64(0), "note 2 edited_on mismatch")
testutils.AssertEqual(t, n2.public, false, "note 2 public mismatch")
testutils.AssertEqual(t, n3.uuid, "7c1fcfb2-de8b-4350-88f0-fb3cbaf6630a", "note 3 uuid mismatch")
testutils.AssertNotEqual(t, n3.id, 0, "note 3 id should have been generated")
testutils.AssertEqual(t, n3.bookUUID, b1.uuid, "note 3 book_uuid mismatch")
testutils.AssertEqual(t, n3.content, "js test 2", "note 3 content mismatch")
testutils.AssertEqual(t, n3.addedOn, int64(1536977230), "note 3 added_on mismatch")
testutils.AssertEqual(t, n3.editedOn, int64(0), "note 3 edited_on mismatch")
testutils.AssertEqual(t, n3.public, false, "note 3 public mismatch")
// 3. test if actions are migrated
var actionCount int
err = db.QueryRow("SELECT count(*) FROM actions").Scan(&actionCount)
if err != nil {
panic(errors.Wrap(err, "counting actions"))
}
testutils.AssertEqual(t, actionCount, 11, "action count mismatch")
type actionInfo struct {
uuid string
schema int
actionType string
data string
timestamp int
}
var a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 actionInfo
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "6145c1b7-f286-4d9f-b0f6-00d274baefc6").Scan(&a1.uuid, &a1.schema, &a1.actionType, &a1.data, &a1.timestamp)
if err != nil {
panic(errors.Wrap(err, "finding a1"))
}
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "c048a56b-179c-4f31-9995-81e9b32b7dd6").Scan(&a2.uuid, &a2.schema, &a2.actionType, &a2.data, &a2.timestamp)
if err != nil {
panic(errors.Wrap(err, "finding a2"))
}
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "f557ef48-c304-47dc-adfb-46b7306e701f").Scan(&a3.uuid, &a3.schema, &a3.actionType, &a3.data, &a3.timestamp)
if err != nil {
panic(errors.Wrap(err, "finding a3"))
}
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "8d79db34-343d-4331-ae5b-24743f17ca7f").Scan(&a4.uuid, &a4.schema, &a4.actionType, &a4.data, &a4.timestamp)
if err != nil {
panic(errors.Wrap(err, "finding a4"))
}
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "b9c1ed4a-e6b3-41f2-983b-593ec7b8b7a1").Scan(&a5.uuid, &a5.schema, &a5.actionType, &a5.data, &a5.timestamp)
if err != nil {
panic(errors.Wrap(err, "finding a5"))
}
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "06ed7ef0-f171-4bd7-ae8e-97b5d06a4c49").Scan(&a6.uuid, &a6.schema, &a6.actionType, &a6.data, &a6.timestamp)
if err != nil {
panic(errors.Wrap(err, "finding a6"))
}
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "7f173cef-1688-4177-a373-145fcd822b2f").Scan(&a7.uuid, &a7.schema, &a7.actionType, &a7.data, &a7.timestamp)
if err != nil {
panic(errors.Wrap(err, "finding a7"))
}
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "64352e08-aa7a-45f4-b760-b3f38b5e11fa").Scan(&a8.uuid, &a8.schema, &a8.actionType, &a8.data, &a8.timestamp)
if err != nil {
panic(errors.Wrap(err, "finding a8"))
}
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "82e20a12-bda8-45f7-ac42-b453b6daa5ec").Scan(&a9.uuid, &a9.schema, &a9.actionType, &a9.data, &a9.timestamp)
if err != nil {
panic(errors.Wrap(err, "finding a9"))
}
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "a29055f4-ace4-44fd-8800-3396edbccaef").Scan(&a10.uuid, &a10.schema, &a10.actionType, &a10.data, &a10.timestamp)
if err != nil {
panic(errors.Wrap(err, "finding a10"))
}
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "871a5562-1bd0-43c1-b550-5bbb727ac7c4").Scan(&a11.uuid, &a11.schema, &a11.actionType, &a11.data, &a11.timestamp)
if err != nil {
panic(errors.Wrap(err, "finding a11"))
}
testutils.AssertEqual(t, a1.uuid, "6145c1b7-f286-4d9f-b0f6-00d274baefc6", "action 1 uuid mismatch")
testutils.AssertEqual(t, a1.schema, 1, "action 1 schema mismatch")
testutils.AssertEqual(t, a1.actionType, "add_book", "action 1 type mismatch")
testutils.AssertEqual(t, a1.data, `{"book_name":"js"}`, "action 1 data mismatch")
testutils.AssertEqual(t, a1.timestamp, 1536977229, "action 1 timestamp mismatch")
testutils.AssertEqual(t, a2.uuid, "c048a56b-179c-4f31-9995-81e9b32b7dd6", "action 2 uuid mismatch")
testutils.AssertEqual(t, a2.schema, 2, "action 2 schema mismatch")
testutils.AssertEqual(t, a2.actionType, "add_note", "action 2 type mismatch")
testutils.AssertEqual(t, a2.data, `{"note_uuid":"35cbcab1-6a2a-4cc8-97e0-e73bbbd54626","book_name":"js","content":"js test 1","public":false}`, "action 2 data mismatch")
testutils.AssertEqual(t, a2.timestamp, 1536977229, "action 2 timestamp mismatch")
testutils.AssertEqual(t, a3.uuid, "f557ef48-c304-47dc-adfb-46b7306e701f", "action 3 uuid mismatch")
testutils.AssertEqual(t, a3.schema, 2, "action 3 schema mismatch")
testutils.AssertEqual(t, a3.actionType, "add_note", "action 3 type mismatch")
testutils.AssertEqual(t, a3.data, `{"note_uuid":"7c1fcfb2-de8b-4350-88f0-fb3cbaf6630a","book_name":"js","content":"js test 2","public":false}`, "action 3 data mismatch")
testutils.AssertEqual(t, a3.timestamp, 1536977230, "action 3 timestamp mismatch")
testutils.AssertEqual(t, a4.uuid, "8d79db34-343d-4331-ae5b-24743f17ca7f", "action 4 uuid mismatch")
testutils.AssertEqual(t, a4.schema, 2, "action 4 schema mismatch")
testutils.AssertEqual(t, a4.actionType, "add_note", "action 4 type mismatch")
testutils.AssertEqual(t, a4.data, `{"note_uuid":"b23a88ba-b291-4294-9795-86b394db5dcf","book_name":"js","content":"js test 3","public":false}`, "action 4 data mismatch")
testutils.AssertEqual(t, a4.timestamp, 1536977234, "action 4 timestamp mismatch")
testutils.AssertEqual(t, a5.uuid, "b9c1ed4a-e6b3-41f2-983b-593ec7b8b7a1", "action 5 uuid mismatch")
testutils.AssertEqual(t, a5.schema, 1, "action 5 schema mismatch")
testutils.AssertEqual(t, a5.actionType, "add_book", "action 5 type mismatch")
testutils.AssertEqual(t, a5.data, `{"book_name":"css"}`, "action 5 data mismatch")
testutils.AssertEqual(t, a5.timestamp, 1536977237, "action 5 timestamp mismatch")
testutils.AssertEqual(t, a6.uuid, "06ed7ef0-f171-4bd7-ae8e-97b5d06a4c49", "action 6 uuid mismatch")
testutils.AssertEqual(t, a6.schema, 2, "action 6 schema mismatch")
testutils.AssertEqual(t, a6.actionType, "add_note", "action 6 type mismatch")
testutils.AssertEqual(t, a6.data, `{"note_uuid":"d69edb54-5b31-4cdd-a4a5-34f0a0bfa153","book_name":"css","content":"js test 3","public":false}`, "action 6 data mismatch")
testutils.AssertEqual(t, a6.timestamp, 1536977237, "action 6 timestamp mismatch")
testutils.AssertEqual(t, a7.uuid, "7f173cef-1688-4177-a373-145fcd822b2f", "action 7 uuid mismatch")
testutils.AssertEqual(t, a7.schema, 2, "action 7 schema mismatch")
testutils.AssertEqual(t, a7.actionType, "edit_note", "action 7 type mismatch")
testutils.AssertEqual(t, a7.data, `{"note_uuid":"d69edb54-5b31-4cdd-a4a5-34f0a0bfa153","from_book":"css","to_book":null,"content":"css test 1","public":null}`, "action 7 data mismatch")
testutils.AssertEqual(t, a7.timestamp, 1536977253, "action 7 timestamp mismatch")
testutils.AssertEqual(t, a8.uuid, "64352e08-aa7a-45f4-b760-b3f38b5e11fa", "action 8 uuid mismatch")
testutils.AssertEqual(t, a8.schema, 1, "action 8 schema mismatch")
testutils.AssertEqual(t, a8.actionType, "add_book", "action 8 type mismatch")
testutils.AssertEqual(t, a8.data, `{"book_name":"sql"}`, "action 8 data mismatch")
testutils.AssertEqual(t, a8.timestamp, 1536977261, "action 8 timestamp mismatch")
testutils.AssertEqual(t, a9.uuid, "82e20a12-bda8-45f7-ac42-b453b6daa5ec", "action 9 uuid mismatch")
testutils.AssertEqual(t, a9.schema, 2, "action 9 schema mismatch")
testutils.AssertEqual(t, a9.actionType, "add_note", "action 9 type mismatch")
testutils.AssertEqual(t, a9.data, `{"note_uuid":"2f47d390-685b-4b84-89ac-704c6fb8d3fb","book_name":"sql","content":"blah","public":false}`, "action 9 data mismatch")
testutils.AssertEqual(t, a9.timestamp, 1536977261, "action 9 timestamp mismatch")
testutils.AssertEqual(t, a10.uuid, "a29055f4-ace4-44fd-8800-3396edbccaef", "action 10 uuid mismatch")
testutils.AssertEqual(t, a10.schema, 1, "action 10 schema mismatch")
testutils.AssertEqual(t, a10.actionType, "remove_book", "action 10 type mismatch")
testutils.AssertEqual(t, a10.data, `{"book_name":"sql"}`, "action 10 data mismatch")
testutils.AssertEqual(t, a10.timestamp, 1536977268, "action 10 timestamp mismatch")
testutils.AssertEqual(t, a11.uuid, "871a5562-1bd0-43c1-b550-5bbb727ac7c4", "action 11 uuid mismatch")
testutils.AssertEqual(t, a11.schema, 1, "action 11 schema mismatch")
testutils.AssertEqual(t, a11.actionType, "remove_note", "action 11 type mismatch")
testutils.AssertEqual(t, a11.data, `{"note_uuid":"b23a88ba-b291-4294-9795-86b394db5dcf","book_name":"js"}`, "action 11 data mismatch")
testutils.AssertEqual(t, a11.timestamp, 1536977274, "action 11 timestamp mismatch")
// 3. test if system is migrated
var systemCount int
err = db.QueryRow("SELECT count(*) FROM system").Scan(&systemCount)
if err != nil {
panic(errors.Wrap(err, "counting system"))
}
testutils.AssertEqual(t, systemCount, 3, "action count mismatch")
var lastUpgrade, lastAction, bookmark int
err = db.QueryRow("SELECT value FROM system WHERE key = ?", "last_upgrade").Scan(&lastUpgrade)
if err != nil {
panic(errors.Wrap(err, "finding last_upgrade"))
}
err = db.QueryRow("SELECT value FROM system WHERE key = ?", "last_action").Scan(&lastAction)
if err != nil {
panic(errors.Wrap(err, "finding last_action"))
}
err = db.QueryRow("SELECT value FROM system WHERE key = ?", "bookmark").Scan(&bookmark)
if err != nil {
panic(errors.Wrap(err, "finding bookmark"))
}
testutils.AssertEqual(t, lastUpgrade, 1536977220, "last_upgrade mismatch")
testutils.AssertEqual(t, lastAction, 1536977274, "last_action mismatch")
testutils.AssertEqual(t, bookmark, 9, "bookmark mismatch")
}

View file

@ -1,253 +1,95 @@
package migrate
import (
"fmt"
"io/ioutil"
"log"
"os"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
"database/sql"
"github.com/dnote/cli/infra"
"github.com/dnote/cli/utils"
"github.com/dnote/cli/log"
"github.com/pkg/errors"
)
var (
schemaFilename = "schema"
backupDirName = ".dnote-bak"
)
// migration IDs
const (
_ = iota
migrationV1
migrationV2
migrationV3
migrationV4
migrationV5
migrationV6
migrationV7
)
var migrationSequence = []int{
migrationV1,
migrationV2,
migrationV3,
migrationV4,
migrationV5,
migrationV6,
migrationV7,
type migration struct {
name string
sql string
}
type schema struct {
CurrentVersion int `yaml:"current_version"`
}
var migrations = []migration{}
func makeSchema(complete bool) schema {
s := schema{}
func initSchema(db *sql.DB) (int, error) {
schemaVersion := 0
var currentVersion int
if complete {
currentVersion = len(migrationSequence)
}
s.CurrentVersion = currentVersion
return s
}
// Migrate determines migrations to be run and performs them
func Migrate(ctx infra.DnoteCtx) error {
unrunMigrations, err := getUnrunMigrations(ctx)
_, err := db.Exec("INSERT INTO system (key, value) VALUES (? ,?)", "schema", schemaVersion)
if err != nil {
return errors.Wrap(err, "Failed to get unrun migrations")
return schemaVersion, errors.Wrap(err, "inserting schema")
}
for _, mid := range unrunMigrations {
if err := performMigration(ctx, mid); err != nil {
return errors.Wrapf(err, "Failed to run migration #%d", mid)
}
}
return nil
return schemaVersion, nil
}
// performMigration backs up current .dnote data, performs migration, and
// restores or cleans backups depending on if there is an error
func performMigration(ctx infra.DnoteCtx, migrationID int) error {
if err := backupDnoteDir(ctx); err != nil {
return errors.Wrap(err, "Failed to back up dnote directory")
}
func getSchema(db *sql.DB) (int, error) {
var ret int
var migrationError error
err := db.QueryRow("SELECT value FROM system where key = ?", "schema").Scan(&ret)
if err == sql.ErrNoRows {
ret, err = initSchema(db)
switch migrationID {
case migrationV1:
migrationError = migrateToV1(ctx)
case migrationV2:
migrationError = migrateToV2(ctx)
case migrationV3:
migrationError = migrateToV3(ctx)
case migrationV4:
migrationError = migrateToV4(ctx)
case migrationV5:
migrationError = migrateToV5(ctx)
case migrationV6:
migrationError = migrateToV6(ctx)
case migrationV7:
migrationError = migrateToV7(ctx)
default:
return errors.Errorf("Unrecognized migration id %d", migrationID)
}
if migrationError != nil {
if err := restoreBackup(ctx); err != nil {
panic(errors.Wrap(err, "Failed to restore backup for a failed migration"))
}
return errors.Wrapf(migrationError, "Failed to perform migration #%d", migrationID)
}
if err := clearBackup(ctx); err != nil {
return errors.Wrap(err, "Failed to clear backup")
}
if err := updateSchemaVersion(ctx, migrationID); err != nil {
return errors.Wrap(err, "Failed to update schema version")
}
return nil
}
// backupDnoteDir backs up the dnote directory to a temporary backup directory
func backupDnoteDir(ctx infra.DnoteCtx) error {
srcPath := fmt.Sprintf("%s/.dnote", ctx.HomeDir)
tmpPath := fmt.Sprintf("%s/%s", ctx.HomeDir, backupDirName)
if err := utils.CopyDir(srcPath, tmpPath); err != nil {
return errors.Wrap(err, "Failed to copy the .dnote directory")
}
return nil
}
func restoreBackup(ctx infra.DnoteCtx) error {
var err error
defer func() {
if err != nil {
log.Printf(`Failed to restore backup for a failed migration.
Don't worry. Your data is still intact in the backup directory.
Get help on https://github.com/dnote/cli/issues`)
return ret, errors.Wrap(err, "initializing schema")
}
}()
srcPath := fmt.Sprintf("%s/.dnote", ctx.HomeDir)
backupPath := fmt.Sprintf("%s/%s", ctx.HomeDir, backupDirName)
if err = os.RemoveAll(srcPath); err != nil {
return errors.Wrapf(err, "Failed to clear current dnote data at %s", backupPath)
} else if err != nil {
return ret, errors.Wrap(err, "querying schema")
}
if err = os.Rename(backupPath, srcPath); err != nil {
return errors.Wrap(err, `Failed to copy backup data to the original directory.`)
return ret, nil
}
func execute(ctx infra.DnoteCtx, nextSchema int, m migration) error {
log.Debug("running migration %s\n", m.name)
tx, err := ctx.DB.Begin()
if err != nil {
return errors.Wrap(err, "beginning a transaction")
}
_, err = tx.Exec(m.sql)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "running sql")
}
_, err = tx.Exec("UPDATE system SET value = ? WHERE key = ?", nextSchema, "schema")
if err != nil {
tx.Rollback()
return errors.Wrap(err, "incrementing schema")
}
tx.Commit()
return nil
}
func clearBackup(ctx infra.DnoteCtx) error {
backupPath := fmt.Sprintf("%s/%s", ctx.HomeDir, backupDirName)
// Run performs unrun migrations
func Run(ctx infra.DnoteCtx) error {
db := ctx.DB
if err := os.RemoveAll(backupPath); err != nil {
return errors.Wrapf(err, "Failed to remove backup at %s", backupPath)
schema, err := getSchema(db)
if err != nil {
return errors.Wrap(err, "getting the current schema")
}
return nil
}
log.Debug("current schema %d\n", schema)
// getSchemaPath returns the path to the file containing schema info
func getSchemaPath(ctx infra.DnoteCtx) string {
return fmt.Sprintf("%s/%s", ctx.DnoteDir, schemaFilename)
}
// InitSchemaFile creates a migration file
func InitSchemaFile(ctx infra.DnoteCtx, pristine bool) error {
path := getSchemaPath(ctx)
if utils.FileExists(path) {
if schema == len(migrations) {
return nil
}
s := makeSchema(pristine)
err := writeSchema(ctx, s)
if err != nil {
return errors.Wrap(err, "Failed to write schema")
}
return nil
}
func readSchema(ctx infra.DnoteCtx) (schema, error) {
var ret schema
path := getSchemaPath(ctx)
b, err := ioutil.ReadFile(path)
if err != nil {
return ret, errors.Wrap(err, "Failed to read schema file")
}
err = yaml.Unmarshal(b, &ret)
if err != nil {
return ret, errors.Wrap(err, "Failed to unmarshal the schema JSON")
}
return ret, nil
}
func writeSchema(ctx infra.DnoteCtx, s schema) error {
path := getSchemaPath(ctx)
d, err := yaml.Marshal(&s)
if err != nil {
return errors.Wrap(err, "Failed to marshal schema into yaml")
}
if err := ioutil.WriteFile(path, d, 0644); err != nil {
return errors.Wrap(err, "Failed to write schema file")
}
return nil
}
func getUnrunMigrations(ctx infra.DnoteCtx) ([]int, error) {
var ret []int
schema, err := readSchema(ctx)
if err != nil {
return ret, errors.Wrap(err, "Failed to read schema")
}
if schema.CurrentVersion == len(migrationSequence) {
return ret, nil
}
nextVersion := schema.CurrentVersion
ret = migrationSequence[nextVersion:]
return ret, nil
}
func updateSchemaVersion(ctx infra.DnoteCtx, mID int) error {
s, err := readSchema(ctx)
if err != nil {
return errors.Wrap(err, "Failed to read schema")
}
s.CurrentVersion = mID
err = writeSchema(ctx, s)
if err != nil {
return errors.Wrap(err, "Failed to write schema")
toRun := migrations[schema:]
for idx, m := range toRun {
nextSchema := schema + idx + 1
if err := execute(ctx, nextSchema, m); err != nil {
return errors.Wrapf(err, "running migration %s", m.name)
}
}
return nil

View file

@ -1,362 +1,9 @@
package migrate
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/dnote/cli/testutils"
"github.com/dnote/cli/utils"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
)
func TestMigrateAll(t *testing.T) {
ctx := testutils.InitCtx("../tmp")
func TestExecute(t *testing.T) {
// set up
testutils.SetupTmp(ctx)
testutils.WriteFile(ctx, "./fixtures/2-pre-dnote.json", "dnote")
testutils.WriteFile(ctx, "./fixtures/4-pre-dnoterc.yaml", "dnoterc")
if err := InitSchemaFile(ctx, false); err != nil {
panic(errors.Wrap(err, "Failed to initialize schema file"))
}
defer testutils.ClearTmp(ctx)
// Execute
if err := Migrate(ctx); err != nil {
t.Fatalf("Failed to migrate %s", err.Error())
}
// Test
schema, err := readSchema(ctx)
if err != nil {
panic(errors.Wrap(err, "Failed to read the schema"))
}
b := testutils.ReadFile(ctx, "dnote")
var dnote migrateToV3Dnote
if err := json.Unmarshal(b, &dnote); err != nil {
t.Error(errors.Wrap(err, "Failed to unmarshal result into dnote").Error())
}
testutils.AssertEqual(t, schema.CurrentVersion, len(migrationSequence), "current schema version mismatch")
note := dnote["algorithm"].Notes[0]
testutils.AssertEqual(t, note.Content, "in-place means no extra space required. it mutates the input", "content was not carried over")
testutils.AssertNotEqual(t, note.UUID, "", "note uuid was not generated")
testutils.AssertNotEqual(t, note.AddedOn, int64(0), "note added_on was not generated")
testutils.AssertEqual(t, note.EditedOn, int64(0), "note edited_on was not propertly generated")
}
func TestMigrateToV1(t *testing.T) {
ctx := testutils.InitCtx("../tmp")
t.Run("yaml exists", func(t *testing.T) {
// set up
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
yamlPath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnote-yaml-archived"))
if err != nil {
panic(errors.Wrap(err, "Failed to get absolute YAML path").Error())
}
ioutil.WriteFile(yamlPath, []byte{}, 0644)
// execute
if err := migrateToV1(ctx); err != nil {
t.Fatal(errors.Wrapf(err, "Failed to migrate").Error())
}
// test
if utils.FileExists(yamlPath) {
t.Fatal("YAML archive file has not been deleted")
}
})
t.Run("yaml does not exist", func(t *testing.T) {
// set up
testutils.SetupTmp(ctx)
defer testutils.ClearTmp(ctx)
yamlPath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnote-yaml-archived"))
if err != nil {
panic(errors.Wrap(err, "Failed to get absolute YAML path").Error())
}
// execute
if err := migrateToV1(ctx); err != nil {
t.Fatal(errors.Wrapf(err, "Failed to migrate").Error())
}
// test
if utils.FileExists(yamlPath) {
t.Fatal("YAML archive file must not exist")
}
})
}
func TestMigrateToV2(t *testing.T) {
ctx := testutils.InitCtx("../tmp")
// set up
testutils.SetupTmp(ctx)
testutils.WriteFile(ctx, "./fixtures/2-pre-dnote.json", "dnote")
defer testutils.ClearTmp(ctx)
// execute
if err := migrateToV2(ctx); err != nil {
t.Fatal(errors.Wrap(err, "Failed to migrate").Error())
}
// test
b := testutils.ReadFile(ctx, "dnote")
var postDnote migrateToV2PostDnote
if err := json.Unmarshal(b, &postDnote); err != nil {
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
}
for _, book := range postDnote {
testutils.AssertNotEqual(t, book.Name, "", "Book name was not populated")
for _, note := range book.Notes {
if len(note.UUID) == 8 {
t.Errorf("Note UUID was not migrated. It has length of %d", len(note.UUID))
}
testutils.AssertNotEqual(t, note.AddedOn, int64(0), "AddedOn was not carried over")
testutils.AssertEqual(t, note.EditedOn, int64(0), "EditedOn was not created properly")
}
}
}
func TestMigrateToV3(t *testing.T) {
ctx := testutils.InitCtx("../tmp")
// set up
testutils.SetupTmp(ctx)
testutils.WriteFile(ctx, "./fixtures/3-pre-dnote.json", "dnote")
defer testutils.ClearTmp(ctx)
// execute
if err := migrateToV3(ctx); err != nil {
t.Fatal(errors.Wrap(err, "Failed to migrate").Error())
}
// test
b := testutils.ReadFile(ctx, "dnote")
var postDnote migrateToV3Dnote
if err := json.Unmarshal(b, &postDnote); err != nil {
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
}
b = testutils.ReadFile(ctx, "actions")
var actions []migrateToV3Action
if err := json.Unmarshal(b, &actions); err != nil {
t.Fatal(errors.Wrap(err, "Failed to unmarshal the actions").Error())
}
testutils.AssertEqual(t, len(actions), 6, "actions length mismatch")
for _, book := range postDnote {
for _, note := range book.Notes {
testutils.AssertNotEqual(t, note.AddedOn, int64(0), "AddedOn was not carried over")
}
}
}
func TestMigrateToV4(t *testing.T) {
ctx := testutils.InitCtx("../tmp")
// set up
testutils.SetupTmp(ctx)
testutils.WriteFile(ctx, "./fixtures/4-pre-dnoterc.yaml", "dnoterc")
defer testutils.ClearTmp(ctx)
defer os.Setenv("EDITOR", "")
// execute
os.Setenv("EDITOR", "vim")
if err := migrateToV4(ctx); err != nil {
t.Fatal(errors.Wrap(err, "Failed to migrate").Error())
}
// test
b := testutils.ReadFile(ctx, "dnoterc")
var config migrateToV4PostConfig
if err := yaml.Unmarshal(b, &config); err != nil {
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
}
testutils.AssertEqual(t, config.APIKey, "Oev6e1082ORasdf9rjkfjkasdfjhgei", "api key mismatch")
testutils.AssertEqual(t, config.Editor, "vim", "editor mismatch")
}
func TestMigrateToV5(t *testing.T) {
ctx := testutils.InitCtx("../tmp")
// set up
testutils.SetupTmp(ctx)
testutils.WriteFile(ctx, "./fixtures/5-pre-actions.json", "actions")
defer testutils.ClearTmp(ctx)
// execute
if err := migrateToV5(ctx); err != nil {
t.Fatal(errors.Wrap(err, "migrating").Error())
}
// test
var oldActions []migrateToV5PreAction
testutils.ReadJSON("./fixtures/5-pre-actions.json", &oldActions)
b := testutils.ReadFile(ctx, "actions")
var migratedActions []migrateToV5PostAction
if err := json.Unmarshal(b, &migratedActions); err != nil {
t.Fatal(errors.Wrap(err, "unmarhsalling migrated actions").Error())
}
if len(oldActions) != len(migratedActions) {
t.Fatalf("There were %d actions but after migration there were %d", len(oldActions), len(migratedActions))
}
for idx := range migratedActions {
migrated := migratedActions[idx]
old := oldActions[idx]
testutils.AssertNotEqual(t, migrated.UUID, "", fmt.Sprintf("uuid mismatch for migrated item with index %d", idx))
testutils.AssertEqual(t, migrated.Schema, 1, fmt.Sprintf("schema mismatch for migrated item with index %d", idx))
testutils.AssertEqual(t, migrated.Timestamp, old.Timestamp, fmt.Sprintf("timestamp mismatch for migrated item with index %d", idx))
testutils.AssertEqual(t, migrated.Type, old.Type, fmt.Sprintf("timestamp mismatch for migrated item with index %d", idx))
switch migrated.Type {
case migrateToV5ActionAddNote:
var oldData, migratedData migrateToV5AddNoteData
if err := json.Unmarshal(old.Data, &oldData); err != nil {
t.Fatal(errors.Wrap(err, "unmarhsalling old data").Error())
}
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
t.Fatal(errors.Wrap(err, "unmarhsalling new data").Error())
}
testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx))
testutils.AssertEqual(t, oldData.Content, migratedData.Content, fmt.Sprintf("data content mismatch for item idx %d", idx))
testutils.AssertEqual(t, oldData.NoteUUID, migratedData.NoteUUID, fmt.Sprintf("data note_uuid mismatch for item idx %d", idx))
case migrateToV5ActionRemoveNote:
var oldData, migratedData migrateToV5RemoveNoteData
if err := json.Unmarshal(old.Data, &oldData); err != nil {
t.Fatal(errors.Wrap(err, "unmarhsalling old data").Error())
}
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
t.Fatal(errors.Wrap(err, "unmarhsalling new data").Error())
}
testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx))
testutils.AssertEqual(t, oldData.NoteUUID, migratedData.NoteUUID, fmt.Sprintf("data note_uuid mismatch for item idx %d", idx))
case migrateToV5ActionAddBook:
var oldData, migratedData migrateToV5AddBookData
if err := json.Unmarshal(old.Data, &oldData); err != nil {
t.Fatal(errors.Wrap(err, "unmarhsalling old data").Error())
}
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
t.Fatal(errors.Wrap(err, "unmarhsalling new data").Error())
}
testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx))
case migrateToV5ActionRemoveBook:
var oldData, migratedData migrateToV5RemoveBookData
if err := json.Unmarshal(old.Data, &oldData); err != nil {
t.Fatal(errors.Wrap(err, "unmarhsalling old data").Error())
}
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
t.Fatal(errors.Wrap(err, "unmarhsalling new data").Error())
}
testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx))
case migrateToV5ActionEditNote:
var oldData migrateToV5PreEditNoteData
var migratedData migrateToV5PostEditNoteData
if err := json.Unmarshal(old.Data, &oldData); err != nil {
t.Fatal(errors.Wrap(err, "unmarhsalling old data").Error())
}
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
t.Fatal(errors.Wrap(err, "unmarhsalling new data").Error())
}
testutils.AssertEqual(t, oldData.NoteUUID, migratedData.NoteUUID, fmt.Sprintf("data note_uuid mismatch for item idx %d", idx))
testutils.AssertEqual(t, oldData.Content, migratedData.Content, fmt.Sprintf("data content mismatch for item idx %d", idx))
testutils.AssertEqual(t, oldData.BookName, migratedData.FromBook, "book_name should have been renamed to from_book")
testutils.AssertEqual(t, migratedData.ToBook, "", "to_book should be empty")
}
}
}
func TestMigrateToV6(t *testing.T) {
ctx := testutils.InitCtx("../tmp")
// set up
testutils.SetupTmp(ctx)
testutils.WriteFile(ctx, "./fixtures/6-pre-dnote.json", "dnote")
defer testutils.ClearTmp(ctx)
// execute
if err := migrateToV6(ctx); err != nil {
t.Fatal(errors.Wrap(err, "Failed to migrate").Error())
}
// test
b := testutils.ReadFile(ctx, "dnote")
var got migrateToV6PostDnote
if err := json.Unmarshal(b, &got); err != nil {
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
}
b = testutils.ReadFileAbs("./fixtures/6-post-dnote.json")
var expected migrateToV6PostDnote
if err := json.Unmarshal(b, &expected); err != nil {
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
}
if ok := reflect.DeepEqual(expected, got); !ok {
t.Errorf("Payload does not match.\nActual: %+v\nExpected: %+v", got, expected)
}
}
func TestMigrateToV7(t *testing.T) {
ctx := testutils.InitCtx("../tmp")
// set up
testutils.SetupTmp(ctx)
testutils.WriteFile(ctx, "./fixtures/7-pre-actions.json", "actions")
defer testutils.ClearTmp(ctx)
// execute
if err := migrateToV7(ctx); err != nil {
t.Fatal(errors.Wrap(err, "migrating").Error())
}
// test
b := testutils.ReadFile(ctx, "actions")
var got []migrateToV7Action
if err := json.Unmarshal(b, &got); err != nil {
t.Fatal(errors.Wrap(err, "unmarshalling the result").Error())
}
b2 := testutils.ReadFileAbs("./fixtures/7-post-actions.json")
var expected []migrateToV7Action
if err := json.Unmarshal(b, &expected); err != nil {
t.Fatal(errors.Wrap(err, "unmarshalling the result into Dnote").Error())
}
ok, err := testutils.IsEqualJSON(b, b2)
if err != nil {
t.Fatal(errors.Wrap(err, "comparing JSON").Error())
}
if !ok {
t.Errorf("Result does not match.\nActual: %+v\nExpected: %+v", got, expected)
}
}

View file

@ -1,383 +0,0 @@
package migrate
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"time"
"github.com/dnote/cli/infra"
"github.com/dnote/cli/utils"
"github.com/pkg/errors"
"github.com/satori/go.uuid"
"gopkg.in/yaml.v2"
)
// migrateToV1 deletes YAML archive if exists
func migrateToV1(ctx infra.DnoteCtx) error {
yamlPath := fmt.Sprintf("%s/%s", ctx.HomeDir, ".dnote-yaml-archived")
if !utils.FileExists(yamlPath) {
return nil
}
if err := os.Remove(yamlPath); err != nil {
return errors.Wrap(err, "Failed to delete .dnote archive")
}
return nil
}
func migrateToV2(ctx infra.DnoteCtx) error {
notePath := fmt.Sprintf("%s/dnote", ctx.DnoteDir)
b, err := ioutil.ReadFile(notePath)
if err != nil {
return errors.Wrap(err, "Failed to read the note file")
}
var preDnote migrateToV2PreDnote
postDnote := migrateToV2PostDnote{}
err = json.Unmarshal(b, &preDnote)
if err != nil {
return errors.Wrap(err, "Failed to unmarshal existing dnote into JSON")
}
for bookName, book := range preDnote {
var notes = make([]migrateToV2PostNote, 0, len(book))
for _, note := range book {
newNote := migrateToV2PostNote{
UUID: uuid.NewV4().String(),
Content: note.Content,
AddedOn: note.AddedOn,
EditedOn: 0,
}
notes = append(notes, newNote)
}
b := migrateToV2PostBook{
Name: bookName,
Notes: notes,
}
postDnote[bookName] = b
}
d, err := json.MarshalIndent(postDnote, "", " ")
if err != nil {
return errors.Wrap(err, "Failed to marshal new dnote into JSON")
}
err = ioutil.WriteFile(notePath, d, 0644)
if err != nil {
return errors.Wrap(err, "Failed to write the new dnote into the file")
}
return nil
}
// migrateToV3 generates actions for existing dnote
func migrateToV3(ctx infra.DnoteCtx) error {
notePath := fmt.Sprintf("%s/dnote", ctx.DnoteDir)
actionsPath := fmt.Sprintf("%s/actions", ctx.DnoteDir)
b, err := ioutil.ReadFile(notePath)
if err != nil {
return errors.Wrap(err, "Failed to read the note file")
}
var dnote migrateToV3Dnote
err = json.Unmarshal(b, &dnote)
if err != nil {
return errors.Wrap(err, "Failed to unmarshal existing dnote into JSON")
}
var actions []migrateToV3Action
for bookName, book := range dnote {
// Find the minimum added_on timestamp from the notes that belong to the book
// to give timstamp to the add_book action.
// Logically add_book must have happened no later than the first add_note
// to the book in order for sync to work.
minTs := time.Now().Unix()
for _, note := range book.Notes {
if note.AddedOn < minTs {
minTs = note.AddedOn
}
}
action := migrateToV3Action{
Type: migrateToV3ActionAddBook,
Data: map[string]interface{}{
"book_name": bookName,
},
Timestamp: minTs,
}
actions = append(actions, action)
for _, note := range book.Notes {
action := migrateToV3Action{
Type: migrateToV3ActionAddNote,
Data: map[string]interface{}{
"note_uuid": note.UUID,
"book_name": book.Name,
"content": note.Content,
},
Timestamp: note.AddedOn,
}
actions = append(actions, action)
}
}
a, err := json.Marshal(actions)
if err != nil {
return errors.Wrap(err, "Failed to marshal actions into JSON")
}
err = ioutil.WriteFile(actionsPath, a, 0644)
if err != nil {
return errors.Wrap(err, "Failed to write the actions into a file")
}
return nil
}
func getEditorCommand() string {
editor := os.Getenv("EDITOR")
switch editor {
case "atom":
return "atom -w"
case "subl":
return "subl -n -w"
case "mate":
return "mate -w"
case "vim":
return "vim"
case "nano":
return "nano"
case "emacs":
return "emacs"
default:
return "vi"
}
}
func migrateToV4(ctx infra.DnoteCtx) error {
configPath := fmt.Sprintf("%s/dnoterc", ctx.DnoteDir)
b, err := ioutil.ReadFile(configPath)
if err != nil {
return errors.Wrap(err, "Failed to read the config file")
}
var preConfig migrateToV4PreConfig
err = yaml.Unmarshal(b, &preConfig)
if err != nil {
return errors.Wrap(err, "Failed to unmarshal existing config into JSON")
}
postConfig := migrateToV4PostConfig{
APIKey: preConfig.APIKey,
Editor: getEditorCommand(),
}
data, err := yaml.Marshal(postConfig)
if err != nil {
return errors.Wrap(err, "Failed to marshal config into JSON")
}
err = ioutil.WriteFile(configPath, data, 0644)
if err != nil {
return errors.Wrap(err, "Failed to write the config into a file")
}
return nil
}
// migrateToV5 migrates actions
func migrateToV5(ctx infra.DnoteCtx) error {
actionsPath := fmt.Sprintf("%s/actions", ctx.DnoteDir)
b, err := ioutil.ReadFile(actionsPath)
if err != nil {
return errors.Wrap(err, "reading the actions file")
}
var actions []migrateToV5PreAction
err = json.Unmarshal(b, &actions)
if err != nil {
return errors.Wrap(err, "unmarshalling actions to JSON")
}
result := []migrateToV5PostAction{}
for _, action := range actions {
var data json.RawMessage
switch action.Type {
case migrateToV5ActionEditNote:
var oldData migrateToV5PreEditNoteData
if err = json.Unmarshal(action.Data, &oldData); err != nil {
return errors.Wrapf(err, "unmarshalling old data of an edit note action %d", action.ID)
}
migratedData := migrateToV5PostEditNoteData{
NoteUUID: oldData.NoteUUID,
FromBook: oldData.BookName,
Content: oldData.Content,
}
b, err = json.Marshal(migratedData)
if err != nil {
return errors.Wrap(err, "marshalling data")
}
data = b
default:
data = action.Data
}
migrated := migrateToV5PostAction{
UUID: uuid.NewV4().String(),
Schema: 1,
Type: action.Type,
Data: data,
Timestamp: action.Timestamp,
}
result = append(result, migrated)
}
a, err := json.Marshal(result)
if err != nil {
return errors.Wrap(err, "marshalling result into JSON")
}
err = ioutil.WriteFile(actionsPath, a, 0644)
if err != nil {
return errors.Wrap(err, "writing the result into a file")
}
return nil
}
// migrateToV6 adds a 'public' field to notes
func migrateToV6(ctx infra.DnoteCtx) error {
notePath := fmt.Sprintf("%s/dnote", ctx.DnoteDir)
b, err := ioutil.ReadFile(notePath)
if err != nil {
return errors.Wrap(err, "Failed to read the note file")
}
var preDnote migrateToV6PreDnote
postDnote := migrateToV6PostDnote{}
err = json.Unmarshal(b, &preDnote)
if err != nil {
return errors.Wrap(err, "Failed to unmarshal existing dnote into JSON")
}
for bookName, book := range preDnote {
var notes = make([]migrateToV6PostNote, 0, len(book.Notes))
public := false
for _, note := range book.Notes {
newNote := migrateToV6PostNote{
UUID: note.UUID,
Content: note.Content,
AddedOn: note.AddedOn,
EditedOn: note.EditedOn,
Public: &public,
}
notes = append(notes, newNote)
}
b := migrateToV6PostBook{
Name: bookName,
Notes: notes,
}
postDnote[bookName] = b
}
d, err := json.MarshalIndent(postDnote, "", " ")
if err != nil {
return errors.Wrap(err, "Failed to marshal new dnote into JSON")
}
err = ioutil.WriteFile(notePath, d, 0644)
if err != nil {
return errors.Wrap(err, "Failed to write the new dnote into the file")
}
return nil
}
// migrateToV7 migrates data of edit_note action to the proper version which is
// EditNoteDataV2. Due to a bug, edit logged actions with schema version '2'
// but with a data of EditNoteDataV1. https://github.com/dnote/cli/issues/107
func migrateToV7(ctx infra.DnoteCtx) error {
actionPath := fmt.Sprintf("%s/actions", ctx.DnoteDir)
b, err := ioutil.ReadFile(actionPath)
if err != nil {
return errors.Wrap(err, "reading actions file")
}
var preActions []migrateToV7Action
postActions := []migrateToV7Action{}
err = json.Unmarshal(b, &preActions)
if err != nil {
return errors.Wrap(err, "unmarhsalling existing actions")
}
for _, action := range preActions {
var newAction migrateToV7Action
if action.Type == migrateToV7ActionTypeEditNote {
var oldData migrateToV7EditNoteDataV1
if e := json.Unmarshal(action.Data, &oldData); e != nil {
return errors.Wrapf(e, "unmarshalling data of action with uuid %s", action.Data)
}
newData := migrateToV7EditNoteDataV2{
NoteUUID: oldData.NoteUUID,
FromBook: oldData.FromBook,
ToBook: nil,
Content: &oldData.Content,
Public: nil,
}
d, e := json.Marshal(newData)
if e != nil {
return errors.Wrapf(e, "marshalling new data of action with uuid %s", action.Data)
}
newAction = migrateToV7Action{
UUID: action.UUID,
Schema: action.Schema,
Type: action.Type,
Timestamp: action.Timestamp,
Data: d,
}
} else {
newAction = action
}
postActions = append(postActions, newAction)
}
d, err := json.Marshal(postActions)
if err != nil {
return errors.Wrap(err, "marshalling new actions")
}
err = ioutil.WriteFile(actionPath, d, 0644)
if err != nil {
return errors.Wrap(err, "writing new actions to a file")
}
return nil
}

View file

@ -1,156 +0,0 @@
package migrate
import "encoding/json"
// v2
type migrateToV2PreNote struct {
UID string
Content string
AddedOn int64
}
type migrateToV2PostNote struct {
UUID string `json:"uuid"`
Content string `json:"content"`
AddedOn int64 `json:"added_on"`
EditedOn int64 `json:"editd_on"`
}
type migrateToV2PreBook []migrateToV2PreNote
type migrateToV2PostBook struct {
Name string `json:"name"`
Notes []migrateToV2PostNote `json:"notes"`
}
type migrateToV2PreDnote map[string]migrateToV2PreBook
type migrateToV2PostDnote map[string]migrateToV2PostBook
//v3
var (
migrateToV3ActionAddNote = "add_note"
migrateToV3ActionAddBook = "add_book"
)
type migrateToV3Note struct {
UUID string `json:"uuid"`
Content string `json:"content"`
AddedOn int64 `json:"added_on"`
EditedOn int64 `json:"edited_on"`
}
type migrateToV3Book struct {
UUID string `json:"uuid"`
Name string `json:"name"`
Notes []migrateToV3Note `json:"notes"`
}
type migrateToV3Dnote map[string]migrateToV3Book
type migrateToV3Action struct {
Type string `json:"type"`
Data map[string]interface{} `json:"data"`
Timestamp int64 `json:"timestamp"`
}
// v4
type migrateToV4PreConfig struct {
Book string
APIKey string
}
type migrateToV4PostConfig struct {
Editor string
APIKey string
}
// v5
type migrateToV5AddNoteData struct {
NoteUUID string `json:"note_uuid"`
BookName string `json:"book_name"`
Content string `json:"content"`
}
type migrateToV5RemoveNoteData struct {
NoteUUID string `json:"note_uuid"`
BookName string `json:"book_name"`
}
type migrateToV5AddBookData struct {
BookName string `json:"book_name"`
}
type migrateToV5RemoveBookData struct {
BookName string `json:"book_name"`
}
type migrateToV5PreEditNoteData struct {
NoteUUID string `json:"note_uuid"`
BookName string `json:"book_name"`
Content string `json:"content"`
}
type migrateToV5PostEditNoteData struct {
NoteUUID string `json:"note_uuid"`
FromBook string `json:"from_book"`
ToBook string `json:"to_book"`
Content string `json:"content"`
}
type migrateToV5PreAction struct {
ID int `json:"id"`
Type string `json:"type"`
Data json.RawMessage `json:"data"`
Timestamp int64 `json:"timestamp"`
}
type migrateToV5PostAction struct {
UUID string `json:"uuid"`
Schema int `json:"schema"`
Type string `json:"type"`
Data json.RawMessage `json:"data"`
Timestamp int64 `json:"timestamp"`
}
var (
migrateToV5ActionAddNote = "add_note"
migrateToV5ActionRemoveNote = "remove_note"
migrateToV5ActionEditNote = "edit_note"
migrateToV5ActionAddBook = "add_book"
migrateToV5ActionRemoveBook = "remove_book"
)
// v6
type migrateToV6PreNote struct {
UUID string `json:"uuid"`
Content string `json:"content"`
AddedOn int64 `json:"added_on"`
EditedOn int64 `json:"edited_on"`
}
type migrateToV6PostNote struct {
UUID string `json:"uuid"`
Content string `json:"content"`
AddedOn int64 `json:"added_on"`
EditedOn int64 `json:"edited_on"`
// Make a pointer to test absent values
Public *bool `json:"public"`
}
type migrateToV6PreBook struct {
Name string `json:"name"`
Notes []migrateToV6PreNote `json:"notes"`
}
type migrateToV6PostBook struct {
Name string `json:"name"`
Notes []migrateToV6PostNote `json:"notes"`
}
type migrateToV6PreDnote map[string]migrateToV6PreBook
type migrateToV6PostDnote map[string]migrateToV6PostBook
// v7
var migrateToV7ActionTypeEditNote = "edit_note"
type migrateToV7Action struct {
UUID string `json:"uuid"`
Schema int `json:"schema"`
Type string `json:"type"`
Data json.RawMessage `json:"data"`
Timestamp int64 `json:"timestamp"`
}
type migrateToV7EditNoteDataV1 struct {
NoteUUID string `json:"note_uuid"`
FromBook string `json:"from_book"`
ToBook string `json:"to_book"`
Content string `json:"content"`
}
type migrateToV7EditNoteDataV2 struct {
NoteUUID string `json:"note_uuid"`
FromBook string `json:"from_book"`
ToBook *string `json:"to_book"`
Content *string `json:"content"`
Public *bool `json:"public"`
}

1
migrate/sql.go Normal file
View file

@ -0,0 +1 @@
package migrate

View file

@ -3,5 +3,5 @@
# dev.sh builds a new binary and replaces the old one in the PATH with it
rm "$(which dnote)" $GOPATH/bin/cli
make build-snapshot
ln -s $GOPATH/src/github.com/dnote/cli/dist/darwin_amd64/dnote /usr/local/bin/dnote
go install -ldflags "-X main.apiEndpoint=http://127.0.0.1:5000" --tags "darwin" .
ln -s $GOPATH/bin/cli /usr/local/bin/dnote

5
scripts/dump_schema.sh Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
# dump_schema.sh dumps the current system's dnote schema to testutils package
# to be used while setting up tests
sqlite3 ~/.dnote/dnote.db .schema > ./testutils/fixtures/schema.sql

View file

@ -4,3 +4,4 @@
# https://stackoverflow.com/questions/23715302/go-how-to-run-tests-for-multiple-packages
go test ./... -p 1

View file

@ -0,0 +1,32 @@
CREATE TABLE notes
(
id integer PRIMARY KEY AUTOINCREMENT,
uuid text NOT NULL,
book_uuid text NOT NULL,
content text NOT NULL,
added_on integer NOT NULL,
edited_on integer DEFAULT 0,
public bool DEFAULT false
);
CREATE TABLE books
(
uuid text PRIMARY KEY,
label text NOT NULL
);
CREATE TABLE actions
(
uuid text PRIMARY KEY,
schema integer NOT NULL,
type text NOT NULL,
data text NOT NULL,
timestamp integer NOT NULL
);
CREATE TABLE system
(
key string NOT NULL,
value text NOT NULL
);
CREATE UNIQUE INDEX idx_books_label ON books(label);
CREATE INDEX idx_books_uuid ON books(uuid);
CREATE INDEX idx_notes_id ON notes(id);
CREATE INDEX idx_notes_book_uuid ON notes(book_uuid);

View file

@ -2,12 +2,17 @@
package testutils
import (
"bytes"
"database/sql"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/dnote/cli/infra"
@ -15,21 +20,47 @@ import (
"github.com/pkg/errors"
)
func InitCtx(relPath string) infra.DnoteCtx {
// InitEnv sets up a test env and returns a new dnote context
func InitEnv(relPath string, relFixturePath string) infra.DnoteCtx {
path, err := filepath.Abs(relPath)
if err != nil {
panic(errors.Wrap(err, "pasrsing path").Error())
}
os.Setenv("DNOTE_HOME_DIR", path)
ctx, err := infra.NewCtx("", "")
if err != nil {
panic(errors.Wrap(err, "getting new ctx").Error())
}
// set up directory and db
if err := os.MkdirAll(ctx.DnoteDir, 0755); err != nil {
panic(err)
}
ctx := infra.DnoteCtx{
HomeDir: path,
DnoteDir: fmt.Sprintf("%s/.dnote", path),
b := ReadFileAbs(relFixturePath)
setupSQL := string(b)
db := ctx.DB
_, err = db.Exec(setupSQL)
if err != nil {
panic(errors.Wrap(err, "running schema sql").Error())
}
return ctx
}
func WriteFile(ctx infra.DnoteCtx, fixturePath string, filename string) {
// TeardownEnv cleans up the test env represented by the given context
func TeardownEnv(ctx infra.DnoteCtx) {
ctx.DB.Close()
if err := os.RemoveAll(ctx.DnoteDir); err != nil {
panic(err)
}
}
// CopyFixture writes the content of the given fixture to the filename inside the dnote dir
func CopyFixture(ctx infra.DnoteCtx, fixturePath string, filename string) {
fp, err := filepath.Abs(fixturePath)
if err != nil {
panic(err)
@ -45,7 +76,8 @@ func WriteFile(ctx infra.DnoteCtx, fixturePath string, filename string) {
}
}
func WriteFileWithContent(ctx infra.DnoteCtx, content []byte, filename string) {
// WriteFile writes a file with the given content and filename inside the dnote dir
func WriteFile(ctx infra.DnoteCtx, content []byte, filename string) {
dp, err := filepath.Abs(filepath.Join(ctx.DnoteDir, filename))
if err != nil {
panic(err)
@ -56,6 +88,7 @@ func WriteFileWithContent(ctx infra.DnoteCtx, content []byte, filename string) {
}
}
// ReadFile reads the content of the file with the given name in dnote dir
func ReadFile(ctx infra.DnoteCtx, filename string) []byte {
path := filepath.Join(ctx.DnoteDir, filename)
@ -67,8 +100,10 @@ func ReadFile(ctx infra.DnoteCtx, filename string) []byte {
return b
}
func ReadFileAbs(filename string) []byte {
fp, err := filepath.Abs(filename)
// ReadFileAbs reads the content of the file with the given file path by resolving
// it as an absolute path
func ReadFileAbs(relpath string) []byte {
fp, err := filepath.Abs(relpath)
if err != nil {
panic(err)
}
@ -81,27 +116,36 @@ func ReadFileAbs(filename string) []byte {
return b
}
func SetupTmp(ctx infra.DnoteCtx) {
if err := os.MkdirAll(ctx.DnoteDir, 0755); err != nil {
panic(err)
}
}
func ClearTmp(ctx infra.DnoteCtx) {
if err := os.RemoveAll(ctx.DnoteDir); err != nil {
panic(err)
}
}
// AssertEqual fails a test if the actual does not match the expected
func AssertEqual(t *testing.T, a interface{}, b interface{}, message string) {
func checkEqual(a interface{}, b interface{}, message string) (bool, string) {
if a == b {
return
return true, ""
}
var m string
if len(message) == 0 {
message = fmt.Sprintf("%v != %v", a, b)
m = fmt.Sprintf("%v != %v", a, b)
} else {
m = message
}
errorMessage := fmt.Sprintf("%s. Actual: %+v. Expected: %+v.", m, a, b)
return false, errorMessage
}
// AssertEqual errors a test if the actual does not match the expected
func AssertEqual(t *testing.T, a interface{}, b interface{}, message string) {
ok, m := checkEqual(a, b, message)
if !ok {
t.Error(m)
}
}
// AssertEqualf fails a test if the actual does not match the expected
func AssertEqualf(t *testing.T, a interface{}, b interface{}, message string) {
ok, m := checkEqual(a, b, message)
if !ok {
t.Fatal(m)
}
t.Errorf("%s. Actual: %+v. Expected: %+v.", message, a, b)
}
// AssertNotEqual fails a test if the actual matches the expected
@ -153,3 +197,109 @@ func IsEqualJSON(s1, s2 []byte) (bool, error) {
return reflect.DeepEqual(o1, o2), nil
}
// MustExec executes the given SQL query and fails a test if an error occurs
func MustExec(t *testing.T, message string, db *sql.DB, query string, args ...interface{}) sql.Result {
result, err := db.Exec(query, args...)
if err != nil {
t.Fatal(errors.Wrap(errors.Wrap(err, "executing sql"), message))
}
return result
}
// MustScan scans the given row and fails a test in case of any errors
func MustScan(t *testing.T, message string, row *sql.Row, args ...interface{}) {
err := row.Scan(args...)
if err != nil {
t.Fatal(errors.Wrap(errors.Wrap(err, "scanning a row"), message))
}
}
// NewDnoteCmd returns a new Dnote command and a pointer to stderr
func NewDnoteCmd(ctx infra.DnoteCtx, binaryName string, arg ...string) (*exec.Cmd, *bytes.Buffer, *bytes.Buffer, error) {
var stderr, stdout bytes.Buffer
binaryPath, err := filepath.Abs(binaryName)
if err != nil {
return &exec.Cmd{}, &stderr, &stdout, errors.Wrap(err, "getting the absolute path to the test binary")
}
cmd := exec.Command(binaryPath, arg...)
cmd.Env = []string{fmt.Sprintf("DNOTE_DIR=%s", ctx.DnoteDir), fmt.Sprintf("DNOTE_HOME_DIR=%s", ctx.HomeDir)}
cmd.Stderr = &stderr
cmd.Stdout = &stdout
return cmd, &stderr, &stdout, nil
}
// RunDnoteCmd runs a dnote command
func RunDnoteCmd(t *testing.T, ctx infra.DnoteCtx, binaryName string, arg ...string) {
t.Logf("running: %s %s", binaryName, strings.Join(arg, " "))
cmd, stderr, stdout, err := NewDnoteCmd(ctx, binaryName, arg...)
if err != nil {
t.Logf("\n%s", stdout)
t.Fatal(errors.Wrap(err, "getting command").Error())
}
cmd.Env = append(cmd.Env, "DNOTE_DEBUG=1")
if err := cmd.Run(); err != nil {
t.Logf("\n%s", stdout)
t.Fatal(errors.Wrapf(err, "running command %s", stderr.String()))
}
// Print stdout if and only if test fails later
t.Logf("\n%s", stdout)
}
// WaitDnoteCmd runs a dnote command and waits until the command is exited
func WaitDnoteCmd(t *testing.T, ctx infra.DnoteCtx, runFunc func(io.WriteCloser) error, binaryName string, arg ...string) {
t.Logf("running: %s %s", binaryName, strings.Join(arg, " "))
cmd, stderr, stdout, err := NewDnoteCmd(ctx, binaryName, arg...)
if err != nil {
t.Logf("\n%s", stdout)
t.Fatal(errors.Wrap(err, "getting command").Error())
}
stdin, err := cmd.StdinPipe()
if err != nil {
t.Logf("\n%s", stdout)
t.Fatal(errors.Wrap(err, "getting stdin %s"))
}
defer stdin.Close()
// Start the program
err = cmd.Start()
if err != nil {
t.Logf("\n%s", stdout)
t.Fatal(errors.Wrap(err, "starting command"))
}
err = runFunc(stdin)
if err != nil {
t.Logf("\n%s", stdout)
t.Fatal(errors.Wrap(err, "running with stdin"))
}
err = cmd.Wait()
if err != nil {
t.Logf("\n%s", stdout)
t.Fatal(errors.Wrapf(err, "running command %s", stderr.String()))
}
// Print stdout if and only if test fails later
t.Logf("\n%s", stdout)
}
// UserConfirm simulates confirmation from the user by writing to stdin
func UserConfirm(stdin io.WriteCloser) error {
// confirm
if _, err := io.WriteString(stdin, "y\n"); err != nil {
return errors.Wrap(err, "confirming deletion")
}
return nil
}

61
testutils/setup.go Normal file
View file

@ -0,0 +1,61 @@
package testutils
import (
"github.com/dnote/cli/infra"
"testing"
)
// Setup1 sets up a dnote env #1
// dnote4.json
func Setup1(t *testing.T, ctx infra.DnoteCtx) {
db := ctx.DB
b1UUID := "js-book-uuid"
b2UUID := "linux-book-uuid"
MustExec(t, "setting up book 1", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "js")
MustExec(t, "setting up book 2", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "linux")
MustExec(t, "setting up note 1", db, "INSERT INTO notes (uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?)", "43827b9a-c2b0-4c06-a290-97991c896653", b1UUID, "Booleans have toString()", 1515199943)
}
// Setup2 sets up a dnote env #2
// dnote3.json
func Setup2(t *testing.T, ctx infra.DnoteCtx) {
db := ctx.DB
b1UUID := "js-book-uuid"
b2UUID := "linux-book-uuid"
MustExec(t, "setting up book 1", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "js")
MustExec(t, "setting up book 2", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "linux")
MustExec(t, "setting up note 2", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?, ?)", 1, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", b1UUID, "Date object implements mathematical comparisons", 1515199951)
MustExec(t, "setting up note 1", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?, ?)", 2, "43827b9a-c2b0-4c06-a290-97991c896653", b1UUID, "Booleans have toString()", 1515199943)
MustExec(t, "setting up note 3", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?, ?)", 3, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", b2UUID, "wc -l to count words", 1515199961)
}
// Setup3 sets up a dnote env #1
// dnote1.json
func Setup3(t *testing.T, ctx infra.DnoteCtx) {
db := ctx.DB
b1UUID := "js-book-uuid"
MustExec(t, "setting up book 1", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "js")
MustExec(t, "setting up note 1", db, "INSERT INTO notes (uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?)", "43827b9a-c2b0-4c06-a290-97991c896653", b1UUID, "Booleans have toString()", 1515199943)
}
// Setup4 sets up a dnote env #1
// dnote2.json
func Setup4(t *testing.T, ctx infra.DnoteCtx) {
db := ctx.DB
b1UUID := "js-book-uuid"
MustExec(t, "setting up book 1", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "js")
MustExec(t, "setting up note 1", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?, ?)", 1, "43827b9a-c2b0-4c06-a290-97991c896653", b1UUID, "Booleans have toString()", 1515199943)
MustExec(t, "setting up note 2", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?, ?)", 2, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", b1UUID, "Date object implements mathematical comparisons", 1515199951)
}

View file

@ -4,35 +4,30 @@ import (
"bufio"
"io"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
"time"
"github.com/dnote/cli/log"
"github.com/pkg/errors"
"github.com/satori/go.uuid"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
// GenerateUID returns a uid
func GenerateUID() string {
// GenerateUUID returns a uid
func GenerateUUID() string {
return uuid.NewV4().String()
}
func GetInput() (string, error) {
func getInput() (string, error) {
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
return "", errors.Wrap(err, "Failed to read stdin")
return "", errors.Wrap(err, "reading stdin")
}
return input, nil
}
// AskConfirmation prompts for user input to confirm a choice
func AskConfirmation(question string, optimistic bool) (bool, error) {
var choices string
if optimistic {
@ -43,7 +38,7 @@ func AskConfirmation(question string, optimistic bool) (bool, error) {
log.Printf("%s %s: ", question, choices)
res, err := GetInput()
res, err := getInput()
if err != nil {
return false, errors.Wrap(err, "Failed to get user input")
}
@ -63,52 +58,39 @@ func FileExists(filepath string) bool {
return !os.IsNotExist(err)
}
func IsDir(path string) (bool, error) {
fileInfo, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, errors.Wrapf(err, "Failed to check if '%s' is directory", path)
}
return fileInfo.IsDir(), nil
}
// CopyFile copies a file from the src to dest
func CopyFile(src, dest string) error {
in, err := os.Open(src)
if err != nil {
return errors.Wrap(err, "Failed to open the input file")
return errors.Wrap(err, "opening the input file")
}
defer in.Close()
out, err := os.Create(dest)
if err != nil {
return errors.Wrap(err, "Failed to create the output file")
return errors.Wrap(err, "creating the output file")
}
if _, err = io.Copy(out, in); err != nil {
return errors.Wrap(err, "Failed to copy the file content")
return errors.Wrap(err, "copying the file content")
}
if err = out.Sync(); err != nil {
return errors.Wrap(err, "Failed to flush the output file to disk")
return errors.Wrap(err, "flushing the output file to disk")
}
fi, err := os.Stat(src)
if err != nil {
return errors.Wrap(err, "Failed to get file info for the input file")
return errors.Wrap(err, "getting the file info for the input file")
}
if err = os.Chmod(dest, fi.Mode()); err != nil {
return errors.Wrap(err, "Failed to copy permission to the output file")
return errors.Wrap(err, "copying permission to the output file")
}
// Close the output file
if err = out.Close(); err != nil {
return errors.Wrap(err, "Failed to close the output file")
return errors.Wrap(err, "closing the output file")
}
return nil
@ -122,25 +104,25 @@ func CopyDir(src, dest string) error {
fi, err := os.Stat(srcPath)
if err != nil {
return errors.Wrap(err, "Failed to get file info for the input")
return errors.Wrap(err, "getting the file info for the input")
}
if !fi.IsDir() {
return errors.Wrap(err, "Source is not a directory")
return errors.New("source is not a directory")
}
_, err = os.Stat(dest)
if err != nil && !os.IsNotExist(err) {
return errors.Wrap(err, "Failed to look up the destination")
return errors.Wrap(err, "looking up the destination")
}
err = os.MkdirAll(dest, fi.Mode())
if err != nil {
return errors.Wrap(err, "Failed to create destination")
return errors.Wrap(err, "creating destination")
}
entries, err := ioutil.ReadDir(src)
if err != nil {
return errors.Wrap(err, "Failed to read directory listing for the input")
return errors.Wrap(err, "reading the directory listing for the input")
}
for _, entry := range entries {
@ -149,11 +131,11 @@ func CopyDir(src, dest string) error {
if entry.IsDir() {
if err = CopyDir(srcEntryPath, destEntryPath); err != nil {
return errors.Wrapf(err, "Failed to copy %s", entry.Name())
return errors.Wrapf(err, "copying %s", entry.Name())
}
} else {
if err = CopyFile(srcEntryPath, destEntryPath); err != nil {
return errors.Wrapf(err, "Failed to copy %s", entry.Name())
return errors.Wrapf(err, "copying %s", entry.Name())
}
}
}