From 0e803c31ccbd0a2a76d8e55f73e846102736f14e Mon Sep 17 00:00:00 2001 From: Sung Won Cho Date: Sat, 26 Jan 2019 21:18:43 +1100 Subject: [PATCH] License (#158) Revert patches --- .travis.yml | 10 - Gopkg.lock | 130 -- Gopkg.toml | 5 +- README.md | 24 +- cmd/add/add.go | 7 +- cmd/edit/edit.go | 112 -- cmd/find/find.go | 6 +- cmd/login/login.go | 4 +- cmd/ls/ls.go | 6 +- cmd/sync/sync.go | 946 ------------- cmd/sync/sync_test.go | 3001 ----------------------------------------- core/core.go | 22 +- log/log.go | 47 +- log/output.go | 12 + main_test.go | 5 +- migrate/migrate.go | 1 - utils/utils.go | 76 +- 17 files changed, 112 insertions(+), 4302 deletions(-) delete mode 100644 .travis.yml delete mode 100644 Gopkg.lock delete mode 100644 cmd/edit/edit.go delete mode 100644 cmd/sync/sync.go delete mode 100644 cmd/sync/sync_test.go create mode 100644 log/output.go diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c92613d2..00000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: go - -go: - - 1.11.x - -install: - - curl -L https://github.com/golang/dep/releases/download/v0.3.2/dep-linux-amd64 -o ./dep && chmod +x ./dep - - ./dep ensure -script: - - ./scripts/test.sh diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index 231a4cc2..00000000 --- a/Gopkg.lock +++ /dev/null @@ -1,130 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - digest = "1:cd0089a5b5d872ac1b772087c7ee0ff2e71de50aa3a51826be64a63963a85287" - name = "github.com/dnote/actions" - packages = ["."] - pruneopts = "" - revision = "60e81aff027d39f4494c5ee5c1db9c3efc015ccf" - version = "v0.2.0" - -[[projects]] - digest = "1:e988ed0ca0d81f4d28772760c02ee95084961311291bdfefc1b04617c178b722" - name = "github.com/fatih/color" - packages = ["."] - pruneopts = "" - revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4" - version = "v1.7.0" - -[[projects]] - branch = "master" - digest = "1:3c62bfc6435a5a004b8b90b6088463d19c825868cadb0cd77ed675d2bd12864f" - name = "github.com/google/go-github" - packages = ["github"] - pruneopts = "" - revision = "dd29b543e14c33e6373773f2c5ea008b29aeac95" - -[[projects]] - digest = "1:cea4aa2038169ee558bf507d5ea02c94ca85bcca28a4c7bb99fd59b31e43a686" - name = "github.com/google/go-querystring" - packages = ["query"] - pruneopts = "" - revision = "44c6ddd0a2342c386950e880b658017258da92fc" - version = "v1.0.0" - -[[projects]] - digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be" - name = "github.com/inconshreveable/mousetrap" - packages = ["."] - pruneopts = "" - revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" - version = "v1.0" - -[[projects]] - digest = "1:9ea83adf8e96d6304f394d40436f2eb44c1dc3250d223b74088cc253a6cd0a1c" - name = "github.com/mattn/go-colorable" - packages = ["."] - pruneopts = "" - revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" - version = "v0.0.9" - -[[projects]] - digest = "1:3140e04675a6a91d2a20ea9d10bdadf6072085502e6def6768361260aee4b967" - name = "github.com/mattn/go-isatty" - packages = ["."] - pruneopts = "" - revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c" - version = "v0.0.4" - -[[projects]] - digest = "1:8bbdb2b3dce59271877770d6fe7dcbb8362438fa7d2e1e1f688e4bf2aac72706" - name = "github.com/mattn/go-sqlite3" - packages = ["."] - pruneopts = "" - revision = "c7c4067b79cc51e6dfdcef5c702e74b1e0fa7c75" - version = "v1.10.0" - -[[projects]] - digest = "1:7365acd48986e205ccb8652cc746f09c8b7876030d53710ea6ef7d0bd0dcd7ca" - name = "github.com/pkg/errors" - packages = ["."] - pruneopts = "" - revision = "645ef00459ed84a119197bfb8d8205042c6df63d" - version = "v0.8.0" - -[[projects]] - digest = "1:7f569d906bdd20d906b606415b7d794f798f91a62fcfb6a4daa6d50690fb7a3f" - name = "github.com/satori/go.uuid" - packages = ["."] - pruneopts = "" - revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3" - version = "v1.2.0" - -[[projects]] - digest = "1:a1403cc8a94b8d7956ee5e9694badef0e7b051af289caad1cf668331e3ffa4f6" - name = "github.com/spf13/cobra" - packages = ["."] - pruneopts = "" - revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385" - version = "v0.0.3" - -[[projects]] - digest = "1:cbaf13cdbfef0e4734ed8a7504f57fe893d471d62a35b982bf6fb3f036449a66" - name = "github.com/spf13/pflag" - packages = ["."] - pruneopts = "" - revision = "298182f68c66c05229eb03ac171abe6e309ee79a" - version = "v1.0.3" - -[[projects]] - branch = "master" - digest = "1:a4bda6e065eb3ccf1e6adeaa863cb416e0ae6b43e613f20a8931d52d19dc20e2" - name = "golang.org/x/sys" - packages = ["unix"] - pruneopts = "" - revision = "4497e2df6f9e69048a54498c7affbbec3294ad47" - -[[projects]] - branch = "v2" - digest = "1:f0620375dd1f6251d9973b5f2596228cc8042e887cd7f827e4220bc1ce8c30e2" - name = "gopkg.in/yaml.v2" - packages = ["."] - pruneopts = "" - revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - input-imports = [ - "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", - "gopkg.in/yaml.v2", - ] - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 17dea643..206df2ef 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -1,4 +1,3 @@ - # Gopkg.toml example # # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md @@ -48,3 +47,7 @@ [[constraint]] name = "github.com/mattn/go-sqlite3" version = "1.10.0" + +[[constraint]] + name = "github.com/dnote/color" + version = "1.7.0" diff --git a/README.md b/README.md index 9388d138..31a15f80 100644 --- a/README.md +++ b/README.md @@ -33,32 +33,14 @@ Write technical notes without getting distracted from programming. The reasons a - We forget exponentially unless we write down what we learn and come back. - Ideas cannot be grokked unless we can put them down in clear words. -## Examples - -- Add a note to a book named `linux` - -``` -dnote add linux -c "find - recursively walk the directory" -``` - -- See the notes in a book - -``` -dnote view linux - -• on book linux -(0) find - recursively walk the directory -``` - ## Commands -Please refer to [commands](/COMMANDS.md). +Please refer to ![commands](/COMMANDS.md). ## Links -- [Dnote](https://dnote.io) -- [Dnote Cloud](https://dnote.io/pricing) -- [Browser Extension](https://github.com/dnote/browser-extension) +* [Website](https://dnote.io) +* [Making Dnote (blog article)](https://github.com/dnote-io/cli) ## License diff --git a/cmd/add/add.go b/cmd/add/add.go index 55c3967b..f5c282c3 100644 --- a/cmd/add/add.go +++ b/cmd/add/add.go @@ -2,7 +2,6 @@ package add import ( "database/sql" - "fmt" "time" "github.com/dnote/cli/core" @@ -36,7 +35,7 @@ func preRun(cmd *cobra.Command, args []string) error { func NewCmd(ctx infra.DnoteCtx) *cobra.Command { cmd := &cobra.Command{ Use: "add ", - Short: "Add a note", + Short: "Add a new note", Aliases: []string{"a", "n", "new"}, Example: example, PreRunE: preRun, @@ -86,9 +85,7 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc { } log.Successf("added to %s\n", bookName) - fmt.Printf("\n------------------------content------------------------\n") - fmt.Printf("%s", content) - fmt.Printf("\n-------------------------------------------------------\n") + log.PrintContent(content) if err := core.CheckUpdate(ctx); err != nil { log.Error(errors.Wrap(err, "automatically checking updates").Error()) diff --git a/cmd/edit/edit.go b/cmd/edit/edit.go deleted file mode 100644 index 8dedd7aa..00000000 --- a/cmd/edit/edit.go +++ /dev/null @@ -1,112 +0,0 @@ -package edit - -import ( - "database/sql" - "fmt" - "io/ioutil" - "time" - - "github.com/dnote/cli/core" - "github.com/dnote/cli/infra" - "github.com/dnote/cli/log" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -var newContent string - -var example = ` - * Edit the note by index in a book - dnote edit js 3 - - * 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", - Short: "Edit a note or a book", - Aliases: []string{"e"}, - Example: example, - PreRunE: preRun, - RunE: newRun(ctx), - } - - f := cmd.Flags() - f.StringVarP(&newContent, "content", "c", "", "The new content for the note") - - return cmd -} - -func preRun(cmd *cobra.Command, args []string) error { - if len(args) != 2 { - return errors.New("Incorrect number of argument") - } - - return nil -} - -func newRun(ctx infra.DnoteCtx) core.RunEFunc { - return func(cmd *cobra.Command, args []string) error { - db := ctx.DB - bookLabel := args[0] - noteRowID := args[1] - - bookUUID, err := core.GetBookUUID(ctx, bookLabel) - if err != nil { - return errors.Wrap(err, "finding book uuid") - } - - var noteUUID, oldContent string - err = db.QueryRow("SELECT uuid, body FROM notes WHERE rowid = ? AND book_uuid = ?", noteRowID, bookUUID).Scan(¬eUUID, &oldContent) - if err == sql.ErrNoRows { - return errors.Errorf("note %s not found in the book '%s'", noteRowID, bookLabel) - } else if err != nil { - return errors.Wrap(err, "querying the book") - } - - if newContent == "" { - fpath := core.GetDnoteTmpContentPath(ctx) - - e := ioutil.WriteFile(fpath, []byte(oldContent), 0644) - if e != nil { - return errors.Wrap(e, "preparing tmp content file") - } - - e = core.GetEditorInput(ctx, fpath, &newContent) - if e != nil { - return errors.Wrap(err, "getting editor input") - } - } - - if oldContent == newContent { - return errors.New("Nothing changed") - } - - ts := time.Now().UnixNano() - newContent = core.SanitizeContent(newContent) - - tx, err := db.Begin() - if err != nil { - return errors.Wrap(err, "beginning a transaction") - } - - _, err = tx.Exec(`UPDATE notes - SET body = ?, edited_on = ?, dirty = ? - WHERE rowid = ? AND book_uuid = ?`, newContent, ts, true, noteRowID, bookUUID) - if err != nil { - tx.Rollback() - return errors.Wrap(err, "updating the note") - } - - tx.Commit() - - log.Success("edited the note\n") - fmt.Printf("\n------------------------content------------------------\n") - fmt.Printf("%s", newContent) - fmt.Printf("\n-------------------------------------------------------\n") - - return nil - } -} diff --git a/cmd/find/find.go b/cmd/find/find.go index 38fb1c0e..2b7d49a4 100644 --- a/cmd/find/find.go +++ b/cmd/find/find.go @@ -76,7 +76,7 @@ func formatFTSSnippet(s string) (string, error) { buf.Reset() } else if tok.Kind == tokenKindHLEnd { format.WriteString("%s") - str := log.SprintfYellow("%s", buf.String()) + str := log.ColorYellow.Sprintf("%s", buf.String()) args = append(args, str) buf.Reset() @@ -169,8 +169,8 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc { } for _, info := range infos { - bookLabel := log.SprintfYellow("(%s)", info.BookLabel) - rowid := log.SprintfYellow("(%d)", info.RowID) + bookLabel := log.ColorYellow.Sprintf("(%s)", info.BookLabel) + rowid := log.ColorYellow.Sprintf("(%d)", info.RowID) log.Plainf("%s %s %s\n", bookLabel, rowid, info.Body) } diff --git a/cmd/login/login.go b/cmd/login/login.go index 40f806a3..77f0f060 100644 --- a/cmd/login/login.go +++ b/cmd/login/login.go @@ -37,9 +37,7 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc { log.Printf("API key: ") var apiKey string - if _, err := fmt.Scanln(&apiKey); err != nil { - return err - } + fmt.Scanln(&apiKey) if apiKey == "" { return errors.New("Empty API key") diff --git a/cmd/ls/ls.go b/cmd/ls/ls.go index 32a472ce..2dc4e08b 100644 --- a/cmd/ls/ls.go +++ b/cmd/ls/ls.go @@ -132,7 +132,7 @@ func printBooks(ctx infra.DnoteCtx) error { } for _, info := range infos { - log.Printf("%s %s\n", info.BookLabel, log.SprintfYellow("(%d)", info.NoteCount)) + log.Printf("%s %s\n", info.BookLabel, log.ColorYellow.Sprintf("(%d)", info.NoteCount)) } return nil @@ -171,9 +171,9 @@ func printNotes(ctx infra.DnoteCtx, bookName string) error { for _, info := range infos { body, isExcerpt := formatBody(info.Body) - rowid := log.SprintfYellow("(%d)", info.RowID) + rowid := log.ColorYellow.Sprintf("(%d)", info.RowID) if isExcerpt { - body = fmt.Sprintf("%s %s", body, log.SprintfYellow("[---More---]")) + body = fmt.Sprintf("%s %s", body, log.ColorYellow.Sprintf("[---More---]")) } log.Plainf("%s %s\n", rowid, body) diff --git a/cmd/sync/sync.go b/cmd/sync/sync.go deleted file mode 100644 index 6968e1c5..00000000 --- a/cmd/sync/sync.go +++ /dev/null @@ -1,946 +0,0 @@ -package sync - -import ( - "database/sql" - "fmt" - - "github.com/dnote/cli/client" - "github.com/dnote/cli/core" - "github.com/dnote/cli/infra" - "github.com/dnote/cli/log" - "github.com/dnote/cli/migrate" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -const ( - modeInsert = iota - modeUpdate -) - -var example = ` - dnote sync` - -var isFullSync bool - -// NewCmd returns a new sync command -func NewCmd(ctx infra.DnoteCtx) *cobra.Command { - cmd := &cobra.Command{ - Use: "sync", - Aliases: []string{"s"}, - Short: "Sync data with the server", - Example: example, - RunE: newRun(ctx), - } - - f := cmd.Flags() - f.BoolVarP(&isFullSync, "full", "f", false, "perform a full sync instead of incrementally syncing only the changed data.") - - return cmd -} - -func getLastSyncAt(tx *sql.Tx) (int, error) { - var ret int - - err := tx.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemLastSyncAt).Scan(&ret) - if err != nil { - return ret, errors.Wrap(err, "querying last sync time") - } - - return ret, nil -} - -func getLastMaxUSN(tx *sql.Tx) (int, error) { - var ret int - - err := tx.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemLastMaxUSN).Scan(&ret) - if err != nil { - return ret, errors.Wrap(err, "querying last user max_usn") - } - - return ret, nil -} - -// syncList is an aggregation of resources represented in the sync fragments -type syncList struct { - Notes map[string]client.SyncFragNote - Books map[string]client.SyncFragBook - ExpungedNotes map[string]bool - ExpungedBooks map[string]bool - MaxUSN int - MaxCurrentTime int64 -} - -func (l syncList) getLength() int { - return len(l.Notes) + len(l.Books) + len(l.ExpungedNotes) + len(l.ExpungedBooks) -} - -func newSyncList(fragments []client.SyncFragment) syncList { - notes := map[string]client.SyncFragNote{} - books := map[string]client.SyncFragBook{} - expungedNotes := map[string]bool{} - expungedBooks := map[string]bool{} - var maxUSN int - var maxCurrentTime int64 - - for _, fragment := range fragments { - for _, note := range fragment.Notes { - notes[note.UUID] = note - } - for _, book := range fragment.Books { - books[book.UUID] = book - } - for _, uuid := range fragment.ExpungedBooks { - expungedBooks[uuid] = true - } - for _, uuid := range fragment.ExpungedNotes { - expungedNotes[uuid] = true - } - - if fragment.FragMaxUSN > maxUSN { - maxUSN = fragment.FragMaxUSN - } - if fragment.CurrentTime > maxCurrentTime { - maxCurrentTime = fragment.CurrentTime - } - } - - return syncList{ - Notes: notes, - Books: books, - ExpungedNotes: expungedNotes, - ExpungedBooks: expungedBooks, - MaxUSN: maxUSN, - MaxCurrentTime: maxCurrentTime, - } -} - -// getSyncList gets a list of all sync fragments after the specified usn -// and aggregates them into a syncList data structure -func getSyncList(ctx infra.DnoteCtx, apiKey string, afterUSN int) (syncList, error) { - fragments, err := getSyncFragments(ctx, apiKey, afterUSN) - if err != nil { - return syncList{}, errors.Wrap(err, "getting sync fragments") - } - - ret := newSyncList(fragments) - - return ret, nil -} - -// getSyncFragments repeatedly gets all sync fragments after the specified usn until there is no more new data -// remaining and returns the buffered list -func getSyncFragments(ctx infra.DnoteCtx, apiKey string, afterUSN int) ([]client.SyncFragment, error) { - var buf []client.SyncFragment - - nextAfterUSN := afterUSN - - for { - resp, err := client.GetSyncFragment(ctx, apiKey, nextAfterUSN) - if err != nil { - return buf, errors.Wrap(err, "getting sync fragment") - } - - frag := resp.Fragment - buf = append(buf, frag) - - nextAfterUSN = frag.FragMaxUSN - - // if there is no more data, break - if nextAfterUSN == 0 { - break - } - } - - log.Debug("received sync fragments: %+v\n", buf) - - return buf, nil -} - -// resolveLabel resolves a book label conflict by repeatedly appending an increasing integer -// to the label until it finds a unique label. It returns the first non-conflicting label. -func resolveLabel(tx *sql.Tx, label string) (string, error) { - var ret string - - for i := 2; ; i++ { - ret = fmt.Sprintf("%s (%d)", label, i) - - var cnt int - if err := tx.QueryRow("SELECT count(*) FROM books WHERE label = ?", ret).Scan(&cnt); err != nil { - return "", errors.Wrapf(err, "checking availability of label %s", ret) - } - - if cnt == 0 { - break - } - } - - return ret, nil -} - -// mergeBook inserts or updates the given book in the local database. -// If a book with a duplicate label exists locally, it renames the duplicate by appending a number. -func mergeBook(tx *sql.Tx, b client.SyncFragBook, mode int) error { - var count int - if err := tx.QueryRow("SELECT count(*) FROM books WHERE label = ?", b.Label).Scan(&count); err != nil { - return errors.Wrapf(err, "checking for books with a duplicate label %s", b.Label) - } - - // if duplicate exists locally, rename it and mark it dirty - if count > 0 { - newLabel, err := resolveLabel(tx, b.Label) - if err != nil { - return errors.Wrap(err, "getting a new book label for conflict resolution") - } - - if _, err := tx.Exec("UPDATE books SET label = ?, dirty = ? WHERE label = ?", newLabel, true, b.Label); err != nil { - return errors.Wrap(err, "resolving duplicate book label") - } - } - - if mode == modeInsert { - book := core.NewBook(b.UUID, b.Label, b.USN, false, false) - if err := book.Insert(tx); err != nil { - return errors.Wrapf(err, "inserting note with uuid %s", b.UUID) - } - } else if mode == modeUpdate { - // TODO: if the client copy is dirty, perform field-by-field merge and report conflict instead of overwriting - if _, err := tx.Exec("UPDATE books SET usn = ?, uuid = ?, label = ?, deleted = ? WHERE uuid = ?", - b.USN, b.UUID, b.Label, b.Deleted, b.UUID); err != nil { - return errors.Wrapf(err, "updating local book %s", b.UUID) - } - } - - return nil -} - -func stepSyncBook(tx *sql.Tx, b client.SyncFragBook) error { - var localUSN int - var dirty bool - err := tx.QueryRow("SELECT usn, dirty FROM books WHERE uuid = ?", b.UUID).Scan(&localUSN, &dirty) - if err != nil && err != sql.ErrNoRows { - return errors.Wrapf(err, "getting local book %s", b.UUID) - } - - // if book exists in the server and does not exist in the client - if err == sql.ErrNoRows { - if e := mergeBook(tx, b, modeInsert); e != nil { - return errors.Wrapf(e, "resolving book") - } - - return nil - } - - if e := mergeBook(tx, b, modeUpdate); e != nil { - return errors.Wrapf(e, "resolving book") - } - - return nil -} - -func mergeNote(tx *sql.Tx, serverNote client.SyncFragNote, localNote core.Note) error { - var bookDeleted bool - err := tx.QueryRow("SELECT deleted FROM books WHERE uuid = ?", localNote.BookUUID).Scan(&bookDeleted) - if err != nil { - return errors.Wrapf(err, "checking if local book %s is deleted", localNote.BookUUID) - } - - // if the book is deleted, noop - if bookDeleted { - return nil - } - - // if the local copy is deleted, and the it was edited on the server, override with server values and mark it not dirty. - if localNote.Deleted { - if _, err := tx.Exec("UPDATE notes SET usn = ?, book_uuid = ?, body = ?, edited_on = ?, deleted = ?, public = ?, dirty = ? WHERE uuid = ?", - serverNote.USN, serverNote.BookUUID, serverNote.Body, serverNote.EditedOn, serverNote.Deleted, serverNote.Public, false, serverNote.UUID); err != nil { - return errors.Wrapf(err, "updating local note %s", serverNote.UUID) - } - - return nil - } - - // TODO: if the client copy is dirty, perform field-by-field merge and report conflict instead of overwriting - if _, err := tx.Exec("UPDATE notes SET usn = ?, book_uuid = ?, body = ?, edited_on = ?, deleted = ?, public = ? WHERE uuid = ?", - serverNote.USN, serverNote.BookUUID, serverNote.Body, serverNote.EditedOn, serverNote.Deleted, serverNote.Public, serverNote.UUID); err != nil { - return errors.Wrapf(err, "updating local note %s", serverNote.UUID) - } - - return nil -} - -func stepSyncNote(tx *sql.Tx, n client.SyncFragNote) error { - var localNote core.Note - err := tx.QueryRow("SELECT usn, book_uuid, dirty, deleted FROM notes WHERE uuid = ?", n.UUID). - Scan(&localNote.USN, &localNote.BookUUID, &localNote.Dirty, &localNote.Deleted) - if err != nil && err != sql.ErrNoRows { - return errors.Wrapf(err, "getting local note %s", n.UUID) - } - - // if note exists in the server and does not exist in the client, insert the note. - if err == sql.ErrNoRows { - note := core.NewNote(n.UUID, n.BookUUID, n.Body, n.AddedOn, n.EditedOn, n.USN, n.Public, n.Deleted, false) - - if err := note.Insert(tx); err != nil { - return errors.Wrapf(err, "inserting note with uuid %s", n.UUID) - } - } else { - if err := mergeNote(tx, n, localNote); err != nil { - return errors.Wrap(err, "merging local note") - } - } - - return nil -} - -func fullSyncNote(tx *sql.Tx, n client.SyncFragNote) error { - var localNote core.Note - err := tx.QueryRow("SELECT usn,book_uuid, dirty, deleted FROM notes WHERE uuid = ?", n.UUID). - Scan(&localNote.USN, &localNote.BookUUID, &localNote.Dirty, &localNote.Deleted) - if err != nil && err != sql.ErrNoRows { - return errors.Wrapf(err, "getting local note %s", n.UUID) - } - - // if note exists in the server and does not exist in the client, insert the note. - if err == sql.ErrNoRows { - note := core.NewNote(n.UUID, n.BookUUID, n.Body, n.AddedOn, n.EditedOn, n.USN, n.Public, n.Deleted, false) - - if err := note.Insert(tx); err != nil { - return errors.Wrapf(err, "inserting note with uuid %s", n.UUID) - } - } else if n.USN > localNote.USN { - if err := mergeNote(tx, n, localNote); err != nil { - return errors.Wrap(err, "merging local note") - } - } - - return nil -} - -func syncDeleteNote(tx *sql.Tx, noteUUID string) error { - var localUSN int - var dirty bool - err := tx.QueryRow("SELECT usn, dirty FROM notes WHERE uuid = ?", noteUUID).Scan(&localUSN, &dirty) - if err != nil && err != sql.ErrNoRows { - return errors.Wrapf(err, "getting local note %s", noteUUID) - } - - // if note does not exist on client, noop - if err == sql.ErrNoRows { - return nil - } - - // if local copy is not dirty, delete - if !dirty { - _, err = tx.Exec("DELETE FROM notes WHERE uuid = ?", noteUUID) - if err != nil { - return errors.Wrapf(err, "deleting local note %s", noteUUID) - } - } - - return nil -} - -// checkNotesPristine checks that none of the notes in the given book are dirty -func checkNotesPristine(tx *sql.Tx, bookUUID string) (bool, error) { - var count int - if err := tx.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ? AND dirty = ?", bookUUID, true).Scan(&count); err != nil { - return false, errors.Wrapf(err, "counting notes that are dirty in book %s", bookUUID) - } - - if count > 0 { - return false, nil - } - - return true, nil -} - -func syncDeleteBook(tx *sql.Tx, bookUUID string) error { - var localUSN int - var dirty bool - err := tx.QueryRow("SELECT usn, dirty FROM books WHERE uuid = ?", bookUUID).Scan(&localUSN, &dirty) - if err != nil && err != sql.ErrNoRows { - return errors.Wrapf(err, "getting local book %s", bookUUID) - } - - // if book does not exist on client, noop - if err == sql.ErrNoRows { - return nil - } - - // if local copy is dirty, noop. it will be uploaded to the server later - if dirty { - return nil - } - - ok, err := checkNotesPristine(tx, bookUUID) - if err != nil { - return errors.Wrap(err, "checking if any notes are dirty in book") - } - // if the local book is not pristine, do not delete but mark it as dirty - // so that it can be uploaded to the server later and become un-deleted - if !ok { - _, err = tx.Exec("UPDATE books SET dirty = ? WHERE uuid = ?", true, bookUUID) - if err != nil { - return errors.Wrapf(err, "marking a book dirty with uuid %s", bookUUID) - } - - return nil - } - - _, err = tx.Exec("DELETE FROM notes WHERE book_uuid = ?", bookUUID) - if err != nil { - return errors.Wrapf(err, "deleting local notes of the book %s", bookUUID) - } - - _, err = tx.Exec("DELETE FROM books WHERE uuid = ?", bookUUID) - if err != nil { - return errors.Wrapf(err, "deleting local book %s", bookUUID) - } - - return nil -} - -func fullSyncBook(tx *sql.Tx, b client.SyncFragBook) error { - var localUSN int - var dirty bool - err := tx.QueryRow("SELECT usn, dirty FROM books WHERE uuid = ?", b.UUID).Scan(&localUSN, &dirty) - if err != nil && err != sql.ErrNoRows { - return errors.Wrapf(err, "getting local book %s", b.UUID) - } - - // if book exists in the server and does not exist in the client - if err == sql.ErrNoRows { - if e := mergeBook(tx, b, modeInsert); e != nil { - return errors.Wrapf(e, "resolving book") - } - } else if b.USN > localUSN { - if e := mergeBook(tx, b, modeUpdate); e != nil { - return errors.Wrapf(e, "resolving book") - } - } - - return nil -} - -// checkNoteInList checks if the given syncList contains the note with the given uuid -func checkNoteInList(uuid string, list *syncList) bool { - if _, ok := list.Notes[uuid]; ok { - return true - } - - if _, ok := list.ExpungedNotes[uuid]; ok { - return true - } - - return false -} - -// checkBookInList checks if the given syncList contains the book with the given uuid -func checkBookInList(uuid string, list *syncList) bool { - if _, ok := list.Books[uuid]; ok { - return true - } - - if _, ok := list.ExpungedBooks[uuid]; ok { - return true - } - - return false -} - -// cleanLocalNotes deletes from the local database any notes that are in invalid state -// judging by the full list of resources in the server. Concretely, the only acceptable -// situation in which a local note is not present in the server is if it is new and has not been -// uploaded (i.e. dirty and usn is 0). Otherwise, it is a result of some kind of error and should be cleaned. -func cleanLocalNotes(tx *sql.Tx, fullList *syncList) error { - rows, err := tx.Query("SELECT uuid, usn, dirty FROM notes") - if err != nil { - return errors.Wrap(err, "getting local notes") - } - defer rows.Close() - - for rows.Next() { - var note core.Note - if err := rows.Scan(¬e.UUID, ¬e.USN, ¬e.Dirty); err != nil { - return errors.Wrap(err, "scanning a row for local note") - } - - ok := checkNoteInList(note.UUID, fullList) - if !ok && (!note.Dirty || note.USN != 0) { - err = note.Expunge(tx) - if err != nil { - return errors.Wrap(err, "expunging a note") - } - } - } - - return nil -} - -// cleanLocalBooks deletes from the local database any books that are in invalid state -func cleanLocalBooks(tx *sql.Tx, fullList *syncList) error { - rows, err := tx.Query("SELECT uuid, usn, dirty FROM books") - if err != nil { - return errors.Wrap(err, "getting local books") - } - defer rows.Close() - - for rows.Next() { - var book core.Book - if err := rows.Scan(&book.UUID, &book.USN, &book.Dirty); err != nil { - return errors.Wrap(err, "scanning a row for local book") - } - - ok := checkBookInList(book.UUID, fullList) - if !ok && (!book.Dirty || book.USN != 0) { - err = book.Expunge(tx) - if err != nil { - return errors.Wrap(err, "expunging a book") - } - } - } - - return nil -} - -func fullSync(ctx infra.DnoteCtx, tx *sql.Tx, apiKey string) error { - log.Debug("performing a full sync\n") - log.Info("resolving delta.") - - list, err := getSyncList(ctx, apiKey, 0) - if err != nil { - return errors.Wrap(err, "getting sync list") - } - - fmt.Printf(" (total %d).", list.getLength()) - - // clean resources that are in erroneous states - if err := cleanLocalNotes(tx, &list); err != nil { - return errors.Wrap(err, "cleaning up local notes") - } - if err := cleanLocalBooks(tx, &list); err != nil { - return errors.Wrap(err, "cleaning up local books") - } - - for _, note := range list.Notes { - if err := fullSyncNote(tx, note); err != nil { - return errors.Wrap(err, "merging note") - } - } - for _, book := range list.Books { - if err := fullSyncBook(tx, book); err != nil { - return errors.Wrap(err, "merging book") - } - } - - for noteUUID := range list.ExpungedNotes { - if err := syncDeleteNote(tx, noteUUID); err != nil { - return errors.Wrap(err, "deleting note") - } - } - for bookUUID := range list.ExpungedBooks { - if err := syncDeleteBook(tx, bookUUID); err != nil { - return errors.Wrap(err, "deleting book") - } - } - - err = saveSyncState(tx, list.MaxCurrentTime, list.MaxUSN) - if err != nil { - return errors.Wrap(err, "saving sync state") - } - - fmt.Println(" done.") - - return nil -} - -func stepSync(ctx infra.DnoteCtx, tx *sql.Tx, apiKey string, afterUSN int) error { - log.Debug("performing a step sync\n") - - log.Info("resolving delta.") - - list, err := getSyncList(ctx, apiKey, afterUSN) - if err != nil { - return errors.Wrap(err, "getting sync list") - } - - fmt.Printf(" (total %d).", list.getLength()) - - for _, note := range list.Notes { - if err := stepSyncNote(tx, note); err != nil { - return errors.Wrap(err, "merging note") - } - } - for _, book := range list.Books { - if err := stepSyncBook(tx, book); err != nil { - return errors.Wrap(err, "merging book") - } - } - - for noteUUID := range list.ExpungedNotes { - if err := syncDeleteNote(tx, noteUUID); err != nil { - return errors.Wrap(err, "deleting note") - } - } - for bookUUID := range list.ExpungedBooks { - if err := syncDeleteBook(tx, bookUUID); err != nil { - return errors.Wrap(err, "deleting book") - } - } - - err = saveSyncState(tx, list.MaxCurrentTime, list.MaxUSN) - if err != nil { - return errors.Wrap(err, "saving sync state") - } - - fmt.Println(" done.") - - return nil -} - -func sendBooks(ctx infra.DnoteCtx, tx *sql.Tx, apiKey string) (bool, error) { - isBehind := false - - rows, err := tx.Query("SELECT uuid, label, usn, deleted FROM books WHERE dirty") - if err != nil { - return isBehind, errors.Wrap(err, "getting syncable books") - } - defer rows.Close() - - for rows.Next() { - var book core.Book - - if err = rows.Scan(&book.UUID, &book.Label, &book.USN, &book.Deleted); err != nil { - return isBehind, errors.Wrap(err, "scanning a syncable book") - } - - log.Debug("sending book %s\n", book.UUID) - - var respUSN int - - // if new, create it in the server, or else, update. - if book.USN == 0 { - if book.Deleted { - err = book.Expunge(tx) - if err != nil { - return isBehind, errors.Wrap(err, "expunging a book locally") - } - - continue - } else { - resp, err := client.CreateBook(ctx, apiKey, book.Label) - if err != nil { - return isBehind, errors.Wrap(err, "creating a book") - } - - _, err = tx.Exec("UPDATE notes SET book_uuid = ? WHERE book_uuid = ?", resp.Book.UUID, book.UUID) - if err != nil { - return isBehind, errors.Wrap(err, "updating book_uuids of notes") - } - - book.Dirty = false - book.USN = resp.Book.USN - err = book.Update(tx) - if err != nil { - return isBehind, errors.Wrap(err, "marking book dirty") - } - - err = book.UpdateUUID(tx, resp.Book.UUID) - if err != nil { - return isBehind, errors.Wrap(err, "updating book uuid") - } - - respUSN = resp.Book.USN - } - } else { - if book.Deleted { - resp, err := client.DeleteBook(ctx, apiKey, book.UUID) - if err != nil { - return isBehind, errors.Wrap(err, "deleting a book") - } - - err = book.Expunge(tx) - if err != nil { - return isBehind, errors.Wrap(err, "expunging a book locally") - } - - respUSN = resp.Book.USN - } else { - resp, err := client.UpdateBook(ctx, apiKey, book.Label, book.UUID) - if err != nil { - return isBehind, errors.Wrap(err, "updating a book") - } - - book.Dirty = false - book.USN = resp.Book.USN - err = book.Update(tx) - if err != nil { - return isBehind, errors.Wrap(err, "marking book dirty") - } - - respUSN = resp.Book.USN - } - } - - lastMaxUSN, err := getLastMaxUSN(tx) - if err != nil { - return isBehind, errors.Wrap(err, "getting last max usn") - } - - log.Debug("sent book %s. response USN %d. last max usn: %d\n", book.UUID, respUSN, lastMaxUSN) - - if respUSN == lastMaxUSN+1 { - err = updateLastMaxUSN(tx, lastMaxUSN+1) - if err != nil { - return isBehind, errors.Wrap(err, "updating last max usn") - } - } else { - isBehind = true - } - } - - return isBehind, nil -} - -func sendNotes(ctx infra.DnoteCtx, tx *sql.Tx, apiKey string) (bool, error) { - isBehind := false - - rows, err := tx.Query("SELECT uuid, book_uuid, body, public, deleted, usn FROM notes WHERE dirty") - if err != nil { - return isBehind, errors.Wrap(err, "getting syncable notes") - } - defer rows.Close() - - for rows.Next() { - var note core.Note - - if err = rows.Scan(¬e.UUID, ¬e.BookUUID, ¬e.Body, ¬e.Public, ¬e.Deleted, ¬e.USN); err != nil { - return isBehind, errors.Wrap(err, "scanning a syncable note") - } - - log.Debug("sending note %s\n", note.UUID) - - var respUSN int - - // if new, create it in the server, or else, update. - if note.USN == 0 { - if note.Deleted { - // if a note was added and deleted locally, simply expunge - err = note.Expunge(tx) - if err != nil { - return isBehind, errors.Wrap(err, "expunging a note locally") - } - - continue - } else { - resp, err := client.CreateNote(ctx, apiKey, note.BookUUID, note.Body) - if err != nil { - return isBehind, errors.Wrap(err, "creating a note") - } - - note.Dirty = false - note.USN = resp.Result.USN - err = note.Update(tx) - if err != nil { - return isBehind, errors.Wrap(err, "marking note dirty") - } - - err = note.UpdateUUID(tx, resp.Result.UUID) - if err != nil { - return isBehind, errors.Wrap(err, "updating note uuid") - } - - respUSN = resp.Result.USN - } - } else { - if note.Deleted { - resp, err := client.DeleteNote(ctx, apiKey, note.UUID) - if err != nil { - return isBehind, errors.Wrap(err, "deleting a note") - } - - err = note.Expunge(tx) - if err != nil { - return isBehind, errors.Wrap(err, "expunging a note locally") - } - - respUSN = resp.Result.USN - } else { - resp, err := client.UpdateNote(ctx, apiKey, note.UUID, note.BookUUID, note.Body, note.Public) - if err != nil { - return isBehind, errors.Wrap(err, "updating a note") - } - - note.Dirty = false - note.USN = resp.Result.USN - err = note.Update(tx) - if err != nil { - return isBehind, errors.Wrap(err, "marking note dirty") - } - - respUSN = resp.Result.USN - } - } - - lastMaxUSN, err := getLastMaxUSN(tx) - if err != nil { - return isBehind, errors.Wrap(err, "getting last max usn") - } - - log.Debug("sent note %s. response USN %d. last max usn: %d\n", note.UUID, respUSN, lastMaxUSN) - - if respUSN == lastMaxUSN+1 { - err = updateLastMaxUSN(tx, lastMaxUSN+1) - if err != nil { - return isBehind, errors.Wrap(err, "updating last max usn") - } - } else { - isBehind = true - } - } - - return isBehind, nil -} - -func sendChanges(ctx infra.DnoteCtx, tx *sql.Tx, apiKey string) (bool, error) { - log.Info("sending changes.") - - var delta int - err := tx.QueryRow("SELECT (SELECT count(*) FROM notes WHERE dirty) + (SELECT count(*) FROM books WHERE dirty)").Scan(&delta) - - fmt.Printf(" (total %d).", delta) - - behind1, err := sendBooks(ctx, tx, apiKey) - if err != nil { - return behind1, errors.Wrap(err, "sending books") - } - - behind2, err := sendNotes(ctx, tx, apiKey) - if err != nil { - return behind2, errors.Wrap(err, "sending notes") - } - - fmt.Println(" done.") - - isBehind := behind1 || behind2 - - return isBehind, nil -} - -func updateLastMaxUSN(tx *sql.Tx, val int) error { - _, err := tx.Exec("UPDATE system SET value = ? WHERE key = ?", val, infra.SystemLastMaxUSN) - if err != nil { - return errors.Wrapf(err, "updating %s", infra.SystemLastMaxUSN) - } - - return nil -} - -func updateLastSyncAt(tx *sql.Tx, val int64) error { - _, err := tx.Exec("UPDATE system SET value = ? WHERE key = ?", val, infra.SystemLastSyncAt) - if err != nil { - return errors.Wrapf(err, "updating %s", infra.SystemLastSyncAt) - } - - return nil -} - -func saveSyncState(tx *sql.Tx, serverTime int64, serverMaxUSN int) error { - if err := updateLastMaxUSN(tx, serverMaxUSN); err != nil { - return errors.Wrap(err, "updating last max usn") - } - if err := updateLastSyncAt(tx, serverTime); err != nil { - return errors.Wrap(err, "updating last sync at") - } - - return nil -} - -func newRun(ctx infra.DnoteCtx) core.RunEFunc { - return func(cmd *cobra.Command, args []string) error { - config, err := core.ReadConfig(ctx) - if err != nil { - return errors.Wrap(err, "reading the config") - } - if config.APIKey == "" { - log.Error("login required. please run `dnote login`\n") - return nil - } - - if err := migrate.Run(ctx, migrate.RemoteSequence, migrate.RemoteMode); err != nil { - return errors.Wrap(err, "running remote migrations") - } - - db := ctx.DB - tx, err := db.Begin() - if err != nil { - return errors.Wrap(err, "beginning a transaction") - } - - syncState, err := client.GetSyncState(config.APIKey, ctx) - if err != nil { - return errors.Wrap(err, "getting the sync state from the server") - } - lastSyncAt, err := getLastSyncAt(tx) - if err != nil { - return errors.Wrap(err, "getting the last sync time") - } - lastMaxUSN, err := getLastMaxUSN(tx) - if err != nil { - return errors.Wrap(err, "getting the last max_usn") - } - - log.Debug("lastSyncAt: %d, lastMaxUSN: %d, syncState: %+v\n", lastSyncAt, lastMaxUSN, syncState) - - var syncErr error - if isFullSync || lastSyncAt < syncState.FullSyncBefore { - syncErr = fullSync(ctx, tx, config.APIKey) - } else if lastMaxUSN != syncState.MaxUSN { - syncErr = stepSync(ctx, tx, config.APIKey, lastMaxUSN) - } else { - // if no need to sync from the server, simply update the last sync timestamp and proceed to send changes - err = updateLastSyncAt(tx, syncState.CurrentTime) - if err != nil { - return errors.Wrap(err, "updating last sync at") - } - } - if syncErr != nil { - tx.Rollback() - return errors.Wrap(err, "syncing changes from the server") - } - - isBehind, err := sendChanges(ctx, tx, config.APIKey) - if err != nil { - tx.Rollback() - return errors.Wrap(err, "sending changes") - } - - // if server state gets ahead of that of client during the sync, do an additional step sync - if isBehind { - log.Debug("performing another step sync because client is behind\n") - - updatedLastMaxUSN, err := getLastMaxUSN(tx) - if err != nil { - tx.Rollback() - return errors.Wrap(err, "getting the new last max_usn") - } - - err = stepSync(ctx, tx, config.APIKey, updatedLastMaxUSN) - if err != nil { - tx.Rollback() - return errors.Wrap(err, "performing the follow-up step sync") - } - } - - tx.Commit() - - log.Success("success\n") - - if err := core.CheckUpdate(ctx); err != nil { - log.Error(errors.Wrap(err, "automatically checking updates").Error()) - } - - return nil - } -} diff --git a/cmd/sync/sync_test.go b/cmd/sync/sync_test.go deleted file mode 100644 index 9ca9b44a..00000000 --- a/cmd/sync/sync_test.go +++ /dev/null @@ -1,3001 +0,0 @@ -package sync - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "sort" - "strings" - "testing" - - "github.com/dnote/cli/client" - "github.com/dnote/cli/core" - "github.com/dnote/cli/infra" - "github.com/dnote/cli/testutils" - "github.com/dnote/cli/utils" - "github.com/pkg/errors" -) - -func TestGetLastSyncAt(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - testutils.MustExec(t, "setting up last_sync_at", db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemLastSyncAt, 1541108743) - - // exec - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - got, err := getLastSyncAt(tx) - if err != nil { - t.Fatalf(errors.Wrap(err, "getting last_sync_at").Error()) - } - - tx.Commit() - - // test - testutils.AssertEqual(t, got, 1541108743, "last_sync_at mismatch") -} - -func TestGetLastMaxUSN(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - testutils.MustExec(t, "setting up last_max_usn", db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemLastMaxUSN, 20001) - - // exec - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - got, err := getLastMaxUSN(tx) - if err != nil { - t.Fatalf(errors.Wrap(err, "getting last_max_usn").Error()) - } - - tx.Commit() - - // test - testutils.AssertEqual(t, got, 20001, "last_max_usn mismatch") -} - -func TestResolveLabel(t *testing.T) { - testCases := []struct { - input string - expected string - }{ - { - input: "js", - expected: "js (2)", - }, - { - input: "css", - expected: "css (3)", - }, - { - input: "linux", - expected: "linux (4)", - }, - } - - for idx, tc := range testCases { - func() { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, fmt.Sprintf("inserting book for test case %d", idx), db, "INSERT INTO books (uuid, label) VALUES (?, ?)", "b1-uuid", "js") - testutils.MustExec(t, fmt.Sprintf("inserting book for test case %d", idx), db, "INSERT INTO books (uuid, label) VALUES (?, ?)", "b2-uuid", "css (2)") - testutils.MustExec(t, fmt.Sprintf("inserting book for test case %d", idx), db, "INSERT INTO books (uuid, label) VALUES (?, ?)", "b3-uuid", "linux (1)") - testutils.MustExec(t, fmt.Sprintf("inserting book for test case %d", idx), db, "INSERT INTO books (uuid, label) VALUES (?, ?)", "b4-uuid", "linux (2)") - testutils.MustExec(t, fmt.Sprintf("inserting book for test case %d", idx), db, "INSERT INTO books (uuid, label) VALUES (?, ?)", "b5-uuid", "linux (3)") - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error()) - } - - got, err := resolveLabel(tx, tc.input) - if err != nil { - t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error()) - } - tx.Rollback() - - testutils.AssertEqual(t, got, tc.expected, fmt.Sprintf("output mismatch for test case %d", idx)) - }() - } -} - -func TestSyncDeleteNote(t *testing.T) { - t.Run("exists on server only", func(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - if err := syncDeleteNote(tx, "nonexistent-note-uuid"); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - var noteCount, bookCount int - testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 0, "note count mismatch") - testutils.AssertEqualf(t, bookCount, 0, "book count mismatch") - }) - - t.Run("local copy is dirty", func(t *testing.T) { - b1UUID := utils.GenerateUUID() - - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, "inserting b1 for test case %d", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "b1-label") - testutils.MustExec(t, "inserting n1 for test case %d", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n1-uuid", b1UUID, 10, "n1 body", 1541108743, false, true) - testutils.MustExec(t, "inserting n2 for test case %d", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n2-uuid", b1UUID, 11, "n2 body", 1541108743, false, true) - - var n1 core.Note - testutils.MustScan(t, "getting n1 for test case", - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", "n1-uuid"), - &n1.UUID, &n1.BookUUID, &n1.USN, &n1.AddedOn, &n1.EditedOn, &n1.Body, &n1.Public, &n1.Deleted, &n1.Dirty) - var n2 core.Note - testutils.MustScan(t, "getting n2 for test case", - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", "n2-uuid"), - &n2.UUID, &n2.BookUUID, &n2.USN, &n2.AddedOn, &n2.EditedOn, &n2.Body, &n2.Public, &n2.Deleted, &n2.Dirty) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction for test case").Error()) - } - - if err := syncDeleteNote(tx, "n1-uuid"); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - var noteCount, bookCount int - testutils.MustScan(t, "counting notes for test case", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books for test case", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - // do not delete note if local copy is dirty - testutils.AssertEqualf(t, noteCount, 2, "note count mismatch for test case") - testutils.AssertEqualf(t, bookCount, 1, "book count mismatch for test case") - - var n1Record core.Note - testutils.MustScan(t, "getting n1 for test case", - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", n1.UUID), - &n1Record.UUID, &n1Record.BookUUID, &n1Record.USN, &n1Record.AddedOn, &n1Record.EditedOn, &n1Record.Body, &n1Record.Public, &n1Record.Deleted, &n1Record.Dirty) - var n2Record core.Note - testutils.MustScan(t, "getting n2 for test case", - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", n2.UUID), - &n2Record.UUID, &n2Record.BookUUID, &n2Record.USN, &n2Record.AddedOn, &n2Record.EditedOn, &n2Record.Body, &n2Record.Public, &n2Record.Deleted, &n2Record.Dirty) - - testutils.AssertEqual(t, n1Record.UUID, n1.UUID, "n1 UUID mismatch for test case") - testutils.AssertEqual(t, n1Record.BookUUID, n1.BookUUID, "n1 BookUUID mismatch for test case") - testutils.AssertEqual(t, n1Record.USN, n1.USN, "n1 USN mismatch for test case") - testutils.AssertEqual(t, n1Record.AddedOn, n1.AddedOn, "n1 AddedOn mismatch for test case") - testutils.AssertEqual(t, n1Record.EditedOn, n1.EditedOn, "n1 EditedOn mismatch for test case") - testutils.AssertEqual(t, n1Record.Body, n1.Body, "n1 Body mismatch for test case") - testutils.AssertEqual(t, n1Record.Public, n1.Public, "n1 Public mismatch for test case") - testutils.AssertEqual(t, n1Record.Deleted, n1.Deleted, "n1 Deleted mismatch for test case") - testutils.AssertEqual(t, n1Record.Dirty, n1.Dirty, "n1 Dirty mismatch for test case") - - testutils.AssertEqual(t, n2Record.UUID, n2.UUID, "n2 UUID mismatch for test case") - testutils.AssertEqual(t, n2Record.BookUUID, n2.BookUUID, "n2 BookUUID mismatch for test case") - testutils.AssertEqual(t, n2Record.USN, n2.USN, "n2 USN mismatch for test case") - testutils.AssertEqual(t, n2Record.AddedOn, n2.AddedOn, "n2 AddedOn mismatch for test case") - testutils.AssertEqual(t, n2Record.EditedOn, n2.EditedOn, "n2 EditedOn mismatch for test case") - testutils.AssertEqual(t, n2Record.Body, n2.Body, "n2 Body mismatch for test case") - testutils.AssertEqual(t, n2Record.Public, n2.Public, "n2 Public mismatch for test case") - testutils.AssertEqual(t, n2Record.Deleted, n2.Deleted, "n2 Deleted mismatch for test case") - testutils.AssertEqual(t, n2Record.Dirty, n2.Dirty, "n2 Dirty mismatch for test case") - }) - - t.Run("local copy is not dirty", func(t *testing.T) { - b1UUID := utils.GenerateUUID() - - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, "inserting b1 for test case %d", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "b1-label") - testutils.MustExec(t, "inserting n1 for test case %d", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n1-uuid", b1UUID, 10, "n1 body", 1541108743, false, false) - testutils.MustExec(t, "inserting n2 for test case %d", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n2-uuid", b1UUID, 11, "n2 body", 1541108743, false, false) - - var n1 core.Note - testutils.MustScan(t, "getting n1 for test case", - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", "n1-uuid"), - &n1.UUID, &n1.BookUUID, &n1.USN, &n1.AddedOn, &n1.EditedOn, &n1.Body, &n1.Public, &n1.Deleted, &n1.Dirty) - var n2 core.Note - testutils.MustScan(t, "getting n2 for test case", - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", "n2-uuid"), - &n2.UUID, &n2.BookUUID, &n2.USN, &n2.AddedOn, &n2.EditedOn, &n2.Body, &n2.Public, &n2.Deleted, &n2.Dirty) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction for test case").Error()) - } - - if err := syncDeleteNote(tx, "n1-uuid"); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - var noteCount, bookCount int - testutils.MustScan(t, "counting notes for test case", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books for test case", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 1, "note count mismatch for test case") - testutils.AssertEqualf(t, bookCount, 1, "book count mismatch for test case") - - var n2Record core.Note - testutils.MustScan(t, "getting n2 for test case", - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", n2.UUID), - &n2Record.UUID, &n2Record.BookUUID, &n2Record.USN, &n2Record.AddedOn, &n2Record.EditedOn, &n2Record.Body, &n2Record.Public, &n2Record.Deleted, &n2Record.Dirty) - - testutils.AssertEqual(t, n2Record.UUID, n2.UUID, "n2 UUID mismatch for test case") - testutils.AssertEqual(t, n2Record.BookUUID, n2.BookUUID, "n2 BookUUID mismatch for test case") - testutils.AssertEqual(t, n2Record.USN, n2.USN, "n2 USN mismatch for test case") - testutils.AssertEqual(t, n2Record.AddedOn, n2.AddedOn, "n2 AddedOn mismatch for test case") - testutils.AssertEqual(t, n2Record.EditedOn, n2.EditedOn, "n2 EditedOn mismatch for test case") - testutils.AssertEqual(t, n2Record.Body, n2.Body, "n2 Body mismatch for test case") - testutils.AssertEqual(t, n2Record.Public, n2.Public, "n2 Public mismatch for test case") - testutils.AssertEqual(t, n2Record.Deleted, n2.Deleted, "n2 Deleted mismatch for test case") - testutils.AssertEqual(t, n2Record.Dirty, n2.Dirty, "n2 Dirty mismatch for test case") - }) -} - -func TestSyncDeleteBook(t *testing.T) { - t.Run("exists on server only", func(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - testutils.MustExec(t, "inserting b1 for test case %d", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", "b1-uuid", "b1-label") - - var b1 core.Book - testutils.MustScan(t, "getting b1 for test case", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", "b1-uuid"), - &b1.UUID, &b1.Label, &b1.USN, &b1.Dirty) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - if err := syncDeleteBook(tx, "nonexistent-book-uuid"); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - var noteCount, bookCount int - testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 0, "note count mismatch") - testutils.AssertEqualf(t, bookCount, 1, "book count mismatch") - - var b1Record core.Book - testutils.MustScan(t, "getting b1 for test case", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", "b1-uuid"), - &b1Record.UUID, &b1Record.Label, &b1Record.USN, &b1Record.Dirty) - - testutils.AssertEqual(t, b1Record.UUID, b1.UUID, "b1 UUID mismatch for test case") - testutils.AssertEqual(t, b1Record.Label, b1.Label, "b1 Label mismatch for test case") - testutils.AssertEqual(t, b1Record.USN, b1.USN, "b1 USN mismatch for test case") - testutils.AssertEqual(t, b1Record.Dirty, b1.Dirty, "b1 Dirty mismatch for test case") - }) - - t.Run("local copy is dirty", func(t *testing.T) { - b1UUID := utils.GenerateUUID() - - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, "inserting b1 for test case %d", db, "INSERT INTO books (uuid, label, usn, dirty) VALUES (?, ?, ?, ?)", b1UUID, "b1-label", 12, true) - testutils.MustExec(t, "inserting n1 for test case %d", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n1-uuid", b1UUID, 10, "n1 body", 1541108743, false, true) - - var b1 core.Book - testutils.MustScan(t, "getting b1 for test case", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", b1UUID), - &b1.UUID, &b1.Label, &b1.USN, &b1.Dirty) - var n1 core.Note - testutils.MustScan(t, "getting n1 for test case", - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", "n1-uuid"), - &n1.UUID, &n1.BookUUID, &n1.USN, &n1.AddedOn, &n1.EditedOn, &n1.Body, &n1.Public, &n1.Deleted, &n1.Dirty) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction for test case").Error()) - } - - if err := syncDeleteBook(tx, b1UUID); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - var noteCount, bookCount int - testutils.MustScan(t, "counting notes for test case", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books for test case", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - // do not delete note if local copy is dirty - testutils.AssertEqualf(t, noteCount, 1, "note count mismatch for test case") - testutils.AssertEqualf(t, bookCount, 1, "book count mismatch for test case") - - var b1Record core.Book - testutils.MustScan(t, "getting b1Record for test case", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", b1UUID), - &b1Record.UUID, &b1Record.Label, &b1Record.USN, &b1Record.Dirty) - var n1Record core.Note - testutils.MustScan(t, "getting n1 for test case", - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", n1.UUID), - &n1Record.UUID, &n1Record.BookUUID, &n1Record.USN, &n1Record.AddedOn, &n1Record.EditedOn, &n1Record.Body, &n1Record.Public, &n1Record.Deleted, &n1Record.Dirty) - - testutils.AssertEqual(t, b1Record.UUID, b1.UUID, "b1 UUID mismatch for test case") - testutils.AssertEqual(t, b1Record.Label, b1.Label, "b1 Label mismatch for test case") - testutils.AssertEqual(t, b1Record.USN, b1.USN, "b1 USN mismatch for test case") - testutils.AssertEqual(t, b1Record.Dirty, b1.Dirty, "b1 Dirty mismatch for test case") - - testutils.AssertEqual(t, n1Record.UUID, n1.UUID, "n1 UUID mismatch for test case") - testutils.AssertEqual(t, n1Record.BookUUID, n1.BookUUID, "n1 BookUUID mismatch for test case") - testutils.AssertEqual(t, n1Record.USN, n1.USN, "n1 USN mismatch for test case") - testutils.AssertEqual(t, n1Record.AddedOn, n1.AddedOn, "n1 AddedOn mismatch for test case") - testutils.AssertEqual(t, n1Record.EditedOn, n1.EditedOn, "n1 EditedOn mismatch for test case") - testutils.AssertEqual(t, n1Record.Body, n1.Body, "n1 Body mismatch for test case") - testutils.AssertEqual(t, n1Record.Public, n1.Public, "n1 Public mismatch for test case") - testutils.AssertEqual(t, n1Record.Deleted, n1.Deleted, "n1 Deleted mismatch for test case") - testutils.AssertEqual(t, n1Record.Dirty, n1.Dirty, "n1 Dirty mismatch for test case") - }) - - t.Run("local copy is not dirty", func(t *testing.T) { - b1UUID := utils.GenerateUUID() - b2UUID := utils.GenerateUUID() - - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, "inserting b1 for test case %d", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "b1-label") - testutils.MustExec(t, "inserting n1 for test case %d", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n1-uuid", b1UUID, 10, "n1 body", 1541108743, false, false) - testutils.MustExec(t, "inserting b2 for test case %d", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "b2-label") - testutils.MustExec(t, "inserting n2 for test case %d", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n2-uuid", b2UUID, 11, "n2 body", 1541108743, false, false) - - var b2 core.Book - testutils.MustScan(t, "getting b2 for test case", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", b2UUID), - &b2.UUID, &b2.Label, &b2.USN, &b2.Dirty) - var n2 core.Note - testutils.MustScan(t, "getting n2 for test case", - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", "n2-uuid"), - &n2.UUID, &n2.BookUUID, &n2.USN, &n2.AddedOn, &n2.EditedOn, &n2.Body, &n2.Public, &n2.Deleted, &n2.Dirty) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction for test case").Error()) - } - - if err := syncDeleteBook(tx, b1UUID); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - var noteCount, bookCount int - testutils.MustScan(t, "counting notes for test case", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books for test case", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 1, "note count mismatch for test case") - testutils.AssertEqualf(t, bookCount, 1, "book count mismatch for test case") - - var b2Record core.Book - testutils.MustScan(t, "getting b2 for test case", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", b2UUID), - &b2Record.UUID, &b2Record.Label, &b2Record.USN, &b2Record.Dirty) - var n2Record core.Note - testutils.MustScan(t, "getting n2 for test case", - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", n2.UUID), - &n2Record.UUID, &n2Record.BookUUID, &n2Record.USN, &n2Record.AddedOn, &n2Record.EditedOn, &n2Record.Body, &n2Record.Public, &n2Record.Deleted, &n2Record.Dirty) - - testutils.AssertEqual(t, b2Record.UUID, b2.UUID, "b2 UUID mismatch for test case") - testutils.AssertEqual(t, b2Record.Label, b2.Label, "b2 Label mismatch for test case") - testutils.AssertEqual(t, b2Record.USN, b2.USN, "b2 USN mismatch for test case") - testutils.AssertEqual(t, b2Record.Dirty, b2.Dirty, "b2 Dirty mismatch for test case") - - testutils.AssertEqual(t, n2Record.UUID, n2.UUID, "n2 UUID mismatch for test case") - testutils.AssertEqual(t, n2Record.BookUUID, n2.BookUUID, "n2 BookUUID mismatch for test case") - testutils.AssertEqual(t, n2Record.USN, n2.USN, "n2 USN mismatch for test case") - testutils.AssertEqual(t, n2Record.AddedOn, n2.AddedOn, "n2 AddedOn mismatch for test case") - testutils.AssertEqual(t, n2Record.EditedOn, n2.EditedOn, "n2 EditedOn mismatch for test case") - testutils.AssertEqual(t, n2Record.Body, n2.Body, "n2 Body mismatch for test case") - testutils.AssertEqual(t, n2Record.Public, n2.Public, "n2 Public mismatch for test case") - testutils.AssertEqual(t, n2Record.Deleted, n2.Deleted, "n2 Deleted mismatch for test case") - testutils.AssertEqual(t, n2Record.Dirty, n2.Dirty, "n2 Dirty mismatch for test case") - }) - - t.Run("local copy has at least one note that is dirty", func(t *testing.T) { - b1UUID := utils.GenerateUUID() - - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, "inserting b1 for test case %d", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "b1-label") - testutils.MustExec(t, "inserting n1 for test case %d", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n1-uuid", b1UUID, 10, "n1 body", 1541108743, false, true) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction for test case").Error()) - } - - if err := syncDeleteBook(tx, b1UUID); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - var noteCount, bookCount int - testutils.MustScan(t, "counting notes for test case", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books for test case", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - testutils.AssertEqualf(t, noteCount, 1, "note count mismatch for test case") - testutils.AssertEqualf(t, bookCount, 1, "book count mismatch for test case") - - var b1Record core.Book - testutils.MustScan(t, "getting b1 for test case", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", b1UUID), - &b1Record.UUID, &b1Record.Label, &b1Record.USN, &b1Record.Dirty) - var n1Record core.Note - testutils.MustScan(t, "getting n1 for test case", - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, body,deleted, dirty FROM notes WHERE uuid = ?", "n1-uuid"), - &n1Record.UUID, &n1Record.BookUUID, &n1Record.USN, &n1Record.AddedOn, &n1Record.Body, &n1Record.Deleted, &n1Record.Dirty) - - testutils.AssertEqual(t, b1Record.UUID, b1UUID, "b1 UUID mismatch for test case") - testutils.AssertEqual(t, b1Record.Label, "b1-label", "b1 Label mismatch for test case") - testutils.AssertEqual(t, b1Record.Dirty, true, "b1 Dirty mismatch for test case") - - testutils.AssertEqual(t, n1Record.UUID, "n1-uuid", "n1 UUID mismatch for test case") - testutils.AssertEqual(t, n1Record.BookUUID, b1UUID, "n1 BookUUID mismatch for test case") - testutils.AssertEqual(t, n1Record.USN, 10, "n1 USN mismatch for test case") - testutils.AssertEqual(t, n1Record.AddedOn, int64(1541108743), "n1 AddedOn mismatch for test case") - testutils.AssertEqual(t, n1Record.Body, "n1 body", "n1 Body mismatch for test case") - testutils.AssertEqual(t, n1Record.Deleted, false, "n1 Deleted mismatch for test case") - testutils.AssertEqual(t, n1Record.Dirty, true, "n1 Dirty mismatch for test case") - }) -} - -func TestFullSyncNote(t *testing.T) { - t.Run("exists on server only", func(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - b1UUID := utils.GenerateUUID() - testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "b1-label") - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - n := client.SyncFragNote{ - UUID: "n1-uuid", - BookUUID: b1UUID, - USN: 128, - AddedOn: 1541232118, - EditedOn: 1541219321, - Body: "n1-body", - Public: true, - Deleted: false, - } - - if err := fullSyncNote(tx, n); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - var noteCount, bookCount int - testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 1, "note count mismatch") - testutils.AssertEqualf(t, bookCount, 1, "book count mismatch") - - var n1 core.Note - testutils.MustScan(t, "getting n1", - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", n.UUID), - &n1.UUID, &n1.BookUUID, &n1.USN, &n1.AddedOn, &n1.EditedOn, &n1.Body, &n1.Public, &n1.Deleted, &n1.Dirty) - - testutils.AssertEqual(t, n1.UUID, n.UUID, "n1 UUID mismatch") - testutils.AssertEqual(t, n1.BookUUID, n.BookUUID, "n1 BookUUID mismatch") - testutils.AssertEqual(t, n1.USN, n.USN, "n1 USN mismatch") - testutils.AssertEqual(t, n1.AddedOn, n.AddedOn, "n1 AddedOn mismatch") - testutils.AssertEqual(t, n1.EditedOn, n.EditedOn, "n1 EditedOn mismatch") - testutils.AssertEqual(t, n1.Body, n.Body, "n1 Body mismatch") - testutils.AssertEqual(t, n1.Public, n.Public, "n1 Public mismatch") - testutils.AssertEqual(t, n1.Deleted, n.Deleted, "n1 Deleted mismatch") - testutils.AssertEqual(t, n1.Dirty, false, "n1 Dirty mismatch") - }) - - t.Run("exists on server and client", func(t *testing.T) { - b1UUID := utils.GenerateUUID() - b2UUID := utils.GenerateUUID() - - testCases := []struct { - addedOn int64 - clientUSN int - clientEditedOn int64 - clientBody string - clientPublic bool - clientDeleted bool - clientBookUUID string - clientDirty bool - serverUSN int - serverEditedOn int64 - serverBody string - serverPublic bool - serverDeleted bool - serverBookUUID string - expectedUSN int - expectedAddedOn int64 - expectedEditedOn int64 - expectedBody string - expectedPublic bool - expectedDeleted bool - expectedBookUUID string - expectedDirty bool - }{ - // server has higher usn and client is dirty - { - clientDirty: true, - clientUSN: 1, - clientEditedOn: 0, - clientBody: "n1 body", - clientPublic: false, - clientDeleted: false, - clientBookUUID: b1UUID, - addedOn: 1541232118, - serverUSN: 21, - serverEditedOn: 1541219321, - serverBody: "n1 body edited", - serverPublic: true, - serverDeleted: false, - serverBookUUID: b2UUID, - expectedUSN: 21, - expectedAddedOn: 1541232118, - expectedEditedOn: 1541219321, - expectedBody: "n1 body edited", - expectedPublic: true, - expectedDeleted: false, - expectedBookUUID: b2UUID, - expectedDirty: true, - }, - // server has higher usn and client deleted locally - { - clientDirty: true, - clientUSN: 1, - clientEditedOn: 0, - clientBody: "", - clientPublic: false, - clientDeleted: true, - clientBookUUID: b1UUID, - addedOn: 1541232118, - serverUSN: 21, - serverEditedOn: 1541219321, - serverBody: "n1 body server", - serverPublic: false, - serverDeleted: false, - serverBookUUID: b2UUID, - expectedUSN: 21, - expectedAddedOn: 1541232118, - expectedEditedOn: 1541219321, - expectedBody: "n1 body server", - expectedPublic: false, - expectedDeleted: false, - expectedBookUUID: b2UUID, - expectedDirty: false, - }, - // server has higher usn and client is not dirty - { - clientDirty: false, - clientUSN: 1, - clientEditedOn: 0, - clientBody: "n1 body", - clientPublic: false, - clientDeleted: false, - clientBookUUID: b1UUID, - addedOn: 1541232118, - serverUSN: 21, - serverEditedOn: 1541219321, - serverBody: "n1 body edited", - serverPublic: true, - serverDeleted: false, - serverBookUUID: b2UUID, - expectedUSN: 21, - expectedAddedOn: 1541232118, - expectedEditedOn: 1541219321, - expectedBody: "n1 body edited", - expectedPublic: true, - expectedDeleted: false, - expectedBookUUID: b2UUID, - expectedDirty: false, - }, - // they're in sync - { - clientDirty: true, - clientUSN: 21, - clientEditedOn: 1541219321, - clientBody: "n1 body", - clientPublic: false, - clientDeleted: false, - clientBookUUID: b2UUID, - addedOn: 1541232118, - serverUSN: 21, - serverEditedOn: 1541219321, - serverBody: "n1 body", - serverPublic: false, - serverDeleted: false, - serverBookUUID: b2UUID, - expectedUSN: 21, - expectedAddedOn: 1541232118, - expectedEditedOn: 1541219321, - expectedBody: "n1 body", - expectedPublic: false, - expectedDeleted: false, - expectedBookUUID: b2UUID, - expectedDirty: true, - }, - // they have the same usn but client is dirty - // not sure if this is a possible scenario but if it happens, the local copy will - // be uploaded to the server anyway. - { - clientDirty: true, - clientUSN: 21, - clientEditedOn: 1541219320, - clientBody: "n1 body client", - clientPublic: false, - clientDeleted: false, - clientBookUUID: b2UUID, - addedOn: 1541232118, - serverUSN: 21, - serverEditedOn: 1541219321, - serverBody: "n1 body server", - serverPublic: true, - serverDeleted: false, - serverBookUUID: b2UUID, - expectedUSN: 21, - expectedAddedOn: 1541232118, - expectedEditedOn: 1541219320, - expectedBody: "n1 body client", - expectedPublic: false, - expectedDeleted: false, - expectedBookUUID: b2UUID, - expectedDirty: true, - }, - } - - for idx, tc := range testCases { - func() { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, fmt.Sprintf("inserting b1 for test case %d", idx), db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "b1-label") - testutils.MustExec(t, fmt.Sprintf("inserting b2 for test case %d", idx), db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "b2-label") - n1UUID := utils.GenerateUUID() - testutils.MustExec(t, fmt.Sprintf("inserting n1 for test case %d", idx), db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", n1UUID, tc.clientBookUUID, tc.clientUSN, tc.addedOn, tc.clientEditedOn, tc.clientBody, tc.clientPublic, tc.clientDeleted, tc.clientDirty) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error()) - } - - // update all fields but uuid and bump usn - n := client.SyncFragNote{ - UUID: n1UUID, - BookUUID: tc.serverBookUUID, - USN: tc.serverUSN, - AddedOn: tc.addedOn, - EditedOn: tc.serverEditedOn, - Body: tc.serverBody, - Public: tc.serverPublic, - Deleted: tc.serverDeleted, - } - - if err := fullSyncNote(tx, n); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error()) - } - - tx.Commit() - - // test - var noteCount, bookCount int - testutils.MustScan(t, fmt.Sprintf("counting notes for test case %d", idx), db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, fmt.Sprintf("counting books for test case %d", idx), db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 1, fmt.Sprintf("note count mismatch for test case %d", idx)) - testutils.AssertEqualf(t, bookCount, 2, fmt.Sprintf("book count mismatch for test case %d", idx)) - - var n1 core.Note - testutils.MustScan(t, fmt.Sprintf("getting n1 for test case %d", idx), - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", n.UUID), - &n1.UUID, &n1.BookUUID, &n1.USN, &n1.AddedOn, &n1.EditedOn, &n1.Body, &n1.Public, &n1.Deleted, &n1.Dirty) - - testutils.AssertEqual(t, n1.UUID, n.UUID, fmt.Sprintf("n1 UUID mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1.BookUUID, tc.expectedBookUUID, fmt.Sprintf("n1 BookUUID mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1.USN, tc.expectedUSN, fmt.Sprintf("n1 USN mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1.AddedOn, tc.expectedAddedOn, fmt.Sprintf("n1 AddedOn mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1.EditedOn, tc.expectedEditedOn, fmt.Sprintf("n1 EditedOn mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1.Body, tc.expectedBody, fmt.Sprintf("n1 Body mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1.Public, tc.expectedPublic, fmt.Sprintf("n1 Public mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1.Deleted, tc.expectedDeleted, fmt.Sprintf("n1 Deleted mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1.Dirty, tc.expectedDirty, fmt.Sprintf("n1 Dirty mismatch for test case %d", idx)) - }() - } - }) -} - -func TestFullSyncBook(t *testing.T) { - t.Run("exists on server only", func(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - b1UUID := utils.GenerateUUID() - testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, usn, label, dirty, deleted) VALUES (?, ?, ?, ?, ?)", b1UUID, 555, "b1-label", true, false) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - b2UUID := utils.GenerateUUID() - b := client.SyncFragBook{ - UUID: b2UUID, - USN: 1, - AddedOn: 1541108743, - Label: "b2-label", - Deleted: false, - } - - if err := fullSyncBook(tx, b); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - var noteCount, bookCount int - testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 0, "note count mismatch") - testutils.AssertEqualf(t, bookCount, 2, "book count mismatch") - - var b1, b2 core.Book - testutils.MustScan(t, "getting b1", - db.QueryRow("SELECT uuid, usn, label, dirty, deleted FROM books WHERE uuid = ?", b1UUID), - &b1.UUID, &b1.USN, &b1.Label, &b1.Dirty, &b1.Deleted) - testutils.MustScan(t, "getting b2", - db.QueryRow("SELECT uuid, usn, label, dirty, deleted FROM books WHERE uuid = ?", b2UUID), - &b2.UUID, &b2.USN, &b2.Label, &b2.Dirty, &b2.Deleted) - - testutils.AssertEqual(t, b1.UUID, b1UUID, "b1 UUID mismatch") - testutils.AssertEqual(t, b1.USN, 555, "b1 USN mismatch") - testutils.AssertEqual(t, b1.Label, "b1-label", "b1 Label mismatch") - testutils.AssertEqual(t, b1.Dirty, true, "b1 Dirty mismatch") - testutils.AssertEqual(t, b1.Deleted, false, "b1 Deleted mismatch") - - testutils.AssertEqual(t, b2.UUID, b2UUID, "b2 UUID mismatch") - testutils.AssertEqual(t, b2.USN, b.USN, "b2 USN mismatch") - testutils.AssertEqual(t, b2.Label, b.Label, "b2 Label mismatch") - testutils.AssertEqual(t, b2.Dirty, false, "b2 Dirty mismatch") - testutils.AssertEqual(t, b2.Deleted, b.Deleted, "b2 Deleted mismatch") - }) - - t.Run("exists on server and client", func(t *testing.T) { - testCases := []struct { - clientDirty bool - clientUSN int - clientLabel string - clientDeleted bool - serverUSN int - serverLabel string - serverDeleted bool - expectedUSN int - expectedLabel string - expectedDeleted bool - }{ - // server has higher usn and client is dirty - { - clientDirty: true, - clientUSN: 1, - clientLabel: "b2-label", - clientDeleted: false, - serverUSN: 3, - serverLabel: "b2-label-updated", - serverDeleted: false, - expectedUSN: 3, - expectedLabel: "b2-label-updated", - expectedDeleted: false, - }, - { - clientDirty: true, - clientUSN: 1, - clientLabel: "b2-label", - clientDeleted: false, - serverUSN: 3, - serverLabel: "", - serverDeleted: true, - expectedUSN: 3, - expectedLabel: "", - expectedDeleted: true, - }, - // server has higher usn and client is not dirty - { - clientDirty: false, - clientUSN: 1, - clientLabel: "b2-label", - clientDeleted: false, - serverUSN: 3, - serverLabel: "b2-label-updated", - serverDeleted: false, - expectedUSN: 3, - expectedLabel: "b2-label-updated", - expectedDeleted: false, - }, - // they are in sync - { - clientDirty: false, - clientUSN: 3, - clientLabel: "b2-label", - clientDeleted: false, - serverUSN: 3, - serverLabel: "b2-label", - serverDeleted: false, - expectedUSN: 3, - expectedLabel: "b2-label", - expectedDeleted: false, - }, - // they have the same usn but client is dirty - { - clientDirty: true, - clientUSN: 3, - clientLabel: "b2-label-client", - clientDeleted: false, - serverUSN: 3, - serverLabel: "b2-label", - serverDeleted: false, - expectedUSN: 3, - expectedLabel: "b2-label-client", - expectedDeleted: false, - }, - } - - for idx, tc := range testCases { - func() { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - b1UUID := utils.GenerateUUID() - testutils.MustExec(t, fmt.Sprintf("inserting book for test case %d", idx), db, "INSERT INTO books (uuid, usn, label, dirty, deleted) VALUES (?, ?, ?, ?, ?)", b1UUID, tc.clientUSN, tc.clientLabel, tc.clientDirty, tc.clientDeleted) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error()) - } - - // update all fields but uuid and bump usn - b := client.SyncFragBook{ - UUID: b1UUID, - USN: tc.serverUSN, - Label: tc.serverLabel, - Deleted: tc.serverDeleted, - } - - if err := fullSyncBook(tx, b); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error()) - } - - tx.Commit() - - // test - var noteCount, bookCount int - testutils.MustScan(t, fmt.Sprintf("counting notes for test case %d", idx), db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, fmt.Sprintf("counting books for test case %d", idx), db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 0, fmt.Sprintf("note count mismatch for test case %d", idx)) - testutils.AssertEqualf(t, bookCount, 1, fmt.Sprintf("book count mismatch for test case %d", idx)) - - var b1 core.Book - testutils.MustScan(t, "getting b1", - db.QueryRow("SELECT uuid, usn, label, dirty, deleted FROM books WHERE uuid = ?", b1UUID), - &b1.UUID, &b1.USN, &b1.Label, &b1.Dirty, &b1.Deleted) - - testutils.AssertEqual(t, b1.UUID, b1UUID, fmt.Sprintf("b1 UUID mismatch for idx %d", idx)) - testutils.AssertEqual(t, b1.USN, tc.expectedUSN, fmt.Sprintf("b1 USN mismatch for test case %d", idx)) - testutils.AssertEqual(t, b1.Label, tc.expectedLabel, fmt.Sprintf("b1 Label mismatch for test case %d", idx)) - testutils.AssertEqual(t, b1.Dirty, tc.clientDirty, fmt.Sprintf("b1 Dirty mismatch for test case %d", idx)) - testutils.AssertEqual(t, b1.Deleted, tc.expectedDeleted, fmt.Sprintf("b1 Deleted mismatch for test case %d", idx)) - }() - } - }) -} - -func TestStepSyncNote(t *testing.T) { - t.Run("exists on server only", func(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - b1UUID := utils.GenerateUUID() - testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "b1-label") - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - n := client.SyncFragNote{ - UUID: "n1-uuid", - BookUUID: b1UUID, - USN: 128, - AddedOn: 1541232118, - EditedOn: 1541219321, - Body: "n1-body", - Public: true, - Deleted: false, - } - - if err := stepSyncNote(tx, n); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - var noteCount, bookCount int - testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 1, "note count mismatch") - testutils.AssertEqualf(t, bookCount, 1, "book count mismatch") - - var n1 core.Note - testutils.MustScan(t, "getting n1", - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", n.UUID), - &n1.UUID, &n1.BookUUID, &n1.USN, &n1.AddedOn, &n1.EditedOn, &n1.Body, &n1.Public, &n1.Deleted, &n1.Dirty) - - testutils.AssertEqual(t, n1.UUID, n.UUID, "n1 UUID mismatch") - testutils.AssertEqual(t, n1.BookUUID, n.BookUUID, "n1 BookUUID mismatch") - testutils.AssertEqual(t, n1.USN, n.USN, "n1 USN mismatch") - testutils.AssertEqual(t, n1.AddedOn, n.AddedOn, "n1 AddedOn mismatch") - testutils.AssertEqual(t, n1.EditedOn, n.EditedOn, "n1 EditedOn mismatch") - testutils.AssertEqual(t, n1.Body, n.Body, "n1 Body mismatch") - testutils.AssertEqual(t, n1.Public, n.Public, "n1 Public mismatch") - testutils.AssertEqual(t, n1.Deleted, n.Deleted, "n1 Deleted mismatch") - testutils.AssertEqual(t, n1.Dirty, false, "n1 Dirty mismatch") - }) - - t.Run("exists on server and client", func(t *testing.T) { - b1UUID := utils.GenerateUUID() - b2UUID := utils.GenerateUUID() - - testCases := []struct { - addedOn int64 - clientUSN int - clientEditedOn int64 - clientBody string - clientPublic bool - clientDeleted bool - clientBookUUID string - clientDirty bool - serverUSN int - serverEditedOn int64 - serverBody string - serverPublic bool - serverDeleted bool - serverBookUUID string - expectedUSN int - expectedAddedOn int64 - expectedEditedOn int64 - expectedBody string - expectedPublic bool - expectedDeleted bool - expectedBookUUID string - expectedDirty bool - }{ - { - clientDirty: true, - clientUSN: 1, - clientEditedOn: 0, - clientBody: "n1 body", - clientPublic: false, - clientDeleted: false, - clientBookUUID: b1UUID, - addedOn: 1541232118, - serverUSN: 21, - serverEditedOn: 1541219321, - serverBody: "n1 body edited", - serverPublic: true, - serverDeleted: false, - serverBookUUID: b2UUID, - expectedUSN: 21, - expectedAddedOn: 1541232118, - expectedEditedOn: 1541219321, - expectedBody: "n1 body edited", - expectedPublic: true, - expectedDeleted: false, - expectedBookUUID: b2UUID, - expectedDirty: true, - }, - // if deleted locally, resurrect it - { - clientDirty: true, - clientUSN: 1, - clientEditedOn: 1541219321, - clientBody: "", - clientPublic: false, - clientDeleted: true, - clientBookUUID: b1UUID, - addedOn: 1541232118, - serverUSN: 21, - serverEditedOn: 1541219321, - serverBody: "n1 body edited", - serverPublic: false, - serverDeleted: false, - serverBookUUID: b2UUID, - expectedUSN: 21, - expectedAddedOn: 1541232118, - expectedEditedOn: 1541219321, - expectedBody: "n1 body edited", - expectedPublic: false, - expectedDeleted: false, - expectedBookUUID: b2UUID, - expectedDirty: false, - }, - { - clientDirty: false, - clientUSN: 1, - clientEditedOn: 0, - clientBody: "n1 body", - clientPublic: false, - clientDeleted: false, - clientBookUUID: b1UUID, - addedOn: 1541232118, - serverUSN: 21, - serverEditedOn: 1541219321, - serverBody: "n1 body edited", - serverPublic: true, - serverDeleted: false, - serverBookUUID: b2UUID, - expectedUSN: 21, - expectedAddedOn: 1541232118, - expectedEditedOn: 1541219321, - expectedBody: "n1 body edited", - expectedPublic: true, - expectedDeleted: false, - expectedBookUUID: b2UUID, - expectedDirty: false, - }, - } - - for idx, tc := range testCases { - func() { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, fmt.Sprintf("inserting b1 for test case %d", idx), db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "b1-label") - testutils.MustExec(t, fmt.Sprintf("inserting b2 for test case %d", idx), db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "b2-label") - n1UUID := utils.GenerateUUID() - testutils.MustExec(t, fmt.Sprintf("inserting n1 for test case %d", idx), db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", n1UUID, tc.clientBookUUID, tc.clientUSN, tc.addedOn, tc.clientEditedOn, tc.clientBody, tc.clientPublic, tc.clientDeleted, tc.clientDirty) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error()) - } - - // update all fields but uuid and bump usn - n := client.SyncFragNote{ - UUID: n1UUID, - BookUUID: tc.serverBookUUID, - USN: tc.serverUSN, - AddedOn: tc.addedOn, - EditedOn: tc.serverEditedOn, - Body: tc.serverBody, - Public: tc.serverPublic, - Deleted: tc.serverDeleted, - } - - if err := stepSyncNote(tx, n); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error()) - } - - tx.Commit() - - // test - var noteCount, bookCount int - testutils.MustScan(t, fmt.Sprintf("counting notes for test case %d", idx), db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, fmt.Sprintf("counting books for test case %d", idx), db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 1, fmt.Sprintf("note count mismatch for test case %d", idx)) - testutils.AssertEqualf(t, bookCount, 2, fmt.Sprintf("book count mismatch for test case %d", idx)) - - var n1 core.Note - testutils.MustScan(t, fmt.Sprintf("getting n1 for test case %d", idx), - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", n.UUID), - &n1.UUID, &n1.BookUUID, &n1.USN, &n1.AddedOn, &n1.EditedOn, &n1.Body, &n1.Public, &n1.Deleted, &n1.Dirty) - - testutils.AssertEqual(t, n1.UUID, n.UUID, fmt.Sprintf("n1 UUID mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1.BookUUID, tc.expectedBookUUID, fmt.Sprintf("n1 BookUUID mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1.USN, tc.expectedUSN, fmt.Sprintf("n1 USN mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1.AddedOn, tc.expectedAddedOn, fmt.Sprintf("n1 AddedOn mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1.EditedOn, tc.expectedEditedOn, fmt.Sprintf("n1 EditedOn mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1.Body, tc.expectedBody, fmt.Sprintf("n1 Body mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1.Public, tc.expectedPublic, fmt.Sprintf("n1 Public mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1.Deleted, tc.expectedDeleted, fmt.Sprintf("n1 Deleted mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1.Dirty, tc.expectedDirty, fmt.Sprintf("n1 Dirty mismatch for test case %d", idx)) - }() - } - }) -} - -func TestStepSyncBook(t *testing.T) { - t.Run("exists on server only", func(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - b1UUID := utils.GenerateUUID() - testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, usn, label, dirty, deleted) VALUES (?, ?, ?, ?, ?)", b1UUID, 555, "b1-label", true, false) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - b2UUID := utils.GenerateUUID() - b := client.SyncFragBook{ - UUID: b2UUID, - USN: 1, - AddedOn: 1541108743, - Label: "b2-label", - Deleted: false, - } - - if err := stepSyncBook(tx, b); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - var noteCount, bookCount int - testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 0, "note count mismatch") - testutils.AssertEqualf(t, bookCount, 2, "book count mismatch") - - var b1, b2 core.Book - testutils.MustScan(t, "getting b1", - db.QueryRow("SELECT uuid, usn, label, dirty, deleted FROM books WHERE uuid = ?", b1UUID), - &b1.UUID, &b1.USN, &b1.Label, &b1.Dirty, &b1.Deleted) - testutils.MustScan(t, "getting b2", - db.QueryRow("SELECT uuid, usn, label, dirty, deleted FROM books WHERE uuid = ?", b2UUID), - &b2.UUID, &b2.USN, &b2.Label, &b2.Dirty, &b2.Deleted) - - testutils.AssertEqual(t, b1.UUID, b1UUID, "b1 UUID mismatch") - testutils.AssertEqual(t, b1.USN, 555, "b1 USN mismatch") - testutils.AssertEqual(t, b1.Label, "b1-label", "b1 Label mismatch") - testutils.AssertEqual(t, b1.Dirty, true, "b1 Dirty mismatch") - testutils.AssertEqual(t, b1.Deleted, false, "b1 Deleted mismatch") - - testutils.AssertEqual(t, b2.UUID, b2UUID, "b2 UUID mismatch") - testutils.AssertEqual(t, b2.USN, b.USN, "b2 USN mismatch") - testutils.AssertEqual(t, b2.Label, b.Label, "b2 Label mismatch") - testutils.AssertEqual(t, b2.Dirty, false, "b2 Dirty mismatch") - testutils.AssertEqual(t, b2.Deleted, b.Deleted, "b2 Deleted mismatch") - }) - - t.Run("exists on server and client", func(t *testing.T) { - testCases := []struct { - clientDirty bool - clientUSN int - clientLabel string - clientDeleted bool - serverUSN int - serverLabel string - serverDeleted bool - expectedUSN int - expectedLabel string - expectedDeleted bool - anotherBookLabel string - expectedAnotherBookLabel string - expectedAnotherBookDirty bool - }{ - { - clientDirty: true, - clientUSN: 1, - clientLabel: "b2-label", - clientDeleted: false, - serverUSN: 3, - serverLabel: "b2-label-updated", - serverDeleted: false, - expectedUSN: 3, - expectedLabel: "b2-label-updated", - expectedDeleted: false, - anotherBookLabel: "foo", - expectedAnotherBookLabel: "foo", - expectedAnotherBookDirty: false, - }, - { - clientDirty: false, - clientUSN: 1, - clientLabel: "b2-label", - clientDeleted: false, - serverUSN: 3, - serverLabel: "b2-label-updated", - serverDeleted: false, - expectedUSN: 3, - expectedLabel: "b2-label-updated", - expectedDeleted: false, - anotherBookLabel: "foo", - expectedAnotherBookLabel: "foo", - expectedAnotherBookDirty: false, - }, - { - clientDirty: false, - clientUSN: 1, - clientLabel: "b2-label", - clientDeleted: false, - serverUSN: 3, - serverLabel: "foo", - serverDeleted: false, - expectedUSN: 3, - expectedLabel: "foo", - expectedDeleted: false, - anotherBookLabel: "foo", - expectedAnotherBookLabel: "foo (2)", - expectedAnotherBookDirty: true, - }, - } - - for idx, tc := range testCases { - func() { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - b1UUID := utils.GenerateUUID() - testutils.MustExec(t, fmt.Sprintf("inserting book for test case %d", idx), db, "INSERT INTO books (uuid, usn, label, dirty, deleted) VALUES (?, ?, ?, ?, ?)", b1UUID, tc.clientUSN, tc.clientLabel, tc.clientDirty, tc.clientDeleted) - b2UUID := utils.GenerateUUID() - testutils.MustExec(t, fmt.Sprintf("inserting book for test case %d", idx), db, "INSERT INTO books (uuid, usn, label, dirty, deleted) VALUES (?, ?, ?, ?, ?)", b2UUID, 2, tc.anotherBookLabel, false, false) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error()) - } - - // update all fields but uuid and bump usn - b := client.SyncFragBook{ - UUID: b1UUID, - USN: tc.serverUSN, - Label: tc.serverLabel, - Deleted: tc.serverDeleted, - } - - if err := fullSyncBook(tx, b); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error()) - } - - tx.Commit() - - // test - var noteCount, bookCount int - testutils.MustScan(t, fmt.Sprintf("counting notes for test case %d", idx), db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, fmt.Sprintf("counting books for test case %d", idx), db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 0, fmt.Sprintf("note count mismatch for test case %d", idx)) - testutils.AssertEqualf(t, bookCount, 2, fmt.Sprintf("book count mismatch for test case %d", idx)) - - var b1Record, b2Record core.Book - testutils.MustScan(t, "getting b1Record", - db.QueryRow("SELECT uuid, usn, label, dirty, deleted FROM books WHERE uuid = ?", b1UUID), - &b1Record.UUID, &b1Record.USN, &b1Record.Label, &b1Record.Dirty, &b1Record.Deleted) - testutils.MustScan(t, "getting b2Record", - db.QueryRow("SELECT uuid, usn, label, dirty, deleted FROM books WHERE uuid = ?", b2UUID), - &b2Record.UUID, &b2Record.USN, &b2Record.Label, &b2Record.Dirty, &b2Record.Deleted) - - testutils.AssertEqual(t, b1Record.UUID, b1UUID, fmt.Sprintf("b1Record UUID mismatch for idx %d", idx)) - testutils.AssertEqual(t, b1Record.USN, tc.expectedUSN, fmt.Sprintf("b1Record USN mismatch for test case %d", idx)) - testutils.AssertEqual(t, b1Record.Label, tc.expectedLabel, fmt.Sprintf("b1Record Label mismatch for test case %d", idx)) - testutils.AssertEqual(t, b1Record.Dirty, tc.clientDirty, fmt.Sprintf("b1Record Dirty mismatch for test case %d", idx)) - testutils.AssertEqual(t, b1Record.Deleted, tc.expectedDeleted, fmt.Sprintf("b1Record Deleted mismatch for test case %d", idx)) - - testutils.AssertEqual(t, b2Record.UUID, b2UUID, fmt.Sprintf("b2Record UUID mismatch for idx %d", idx)) - testutils.AssertEqual(t, b2Record.USN, 2, fmt.Sprintf("b2Record USN mismatch for test case %d", idx)) - testutils.AssertEqual(t, b2Record.Label, tc.expectedAnotherBookLabel, fmt.Sprintf("b2Record Label mismatch for test case %d", idx)) - testutils.AssertEqual(t, b2Record.Dirty, tc.expectedAnotherBookDirty, fmt.Sprintf("b2Record Dirty mismatch for test case %d", idx)) - testutils.AssertEqual(t, b2Record.Deleted, false, fmt.Sprintf("b2Record Deleted mismatch for test case %d", idx)) - }() - } - }) -} - -func TestMergeBook(t *testing.T) { - t.Run("insert, no duplicates", func(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - // test - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - b1 := client.SyncFragBook{ - UUID: "b1-uuid", - USN: 12, - AddedOn: 1541108743, - Label: "b1-label", - Deleted: false, - } - - if err := mergeBook(tx, b1, modeInsert); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // execute - var noteCount, bookCount int - testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 0, "note count mismatch") - testutils.AssertEqualf(t, bookCount, 1, "book count mismatch") - - var b1Record core.Book - testutils.MustScan(t, "getting b1", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", "b1-uuid"), - &b1Record.UUID, &b1Record.Label, &b1Record.USN, &b1Record.Dirty) - - testutils.AssertEqual(t, b1Record.UUID, b1.UUID, "b1 UUID mismatch") - testutils.AssertEqual(t, b1Record.Label, b1.Label, "b1 Label mismatch") - testutils.AssertEqual(t, b1Record.USN, b1.USN, "b1 USN mismatch") - }) - - t.Run("insert, 1 duplicate", func(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, usn, label, dirty, deleted) VALUES (?, ?, ?, ?, ?)", "b1-uuid", 1, "foo", false, false) - - // test - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - b := client.SyncFragBook{ - UUID: "b2-uuid", - USN: 12, - AddedOn: 1541108743, - Label: "foo", - Deleted: false, - } - - if err := mergeBook(tx, b, modeInsert); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // execute - var noteCount, bookCount int - testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 0, "note count mismatch") - testutils.AssertEqualf(t, bookCount, 2, "book count mismatch") - - var b1Record, b2Record core.Book - testutils.MustScan(t, "getting b1", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", "b1-uuid"), - &b1Record.UUID, &b1Record.Label, &b1Record.USN, &b1Record.Dirty) - testutils.MustScan(t, "getting b2", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", "b2-uuid"), - &b2Record.UUID, &b2Record.Label, &b2Record.USN, &b2Record.Dirty) - - testutils.AssertEqual(t, b1Record.Label, "foo (2)", "b1 Label mismatch") - testutils.AssertEqual(t, b1Record.USN, 1, "b1 USN mismatch") - testutils.AssertEqual(t, b1Record.Dirty, true, "b1 should have been marked dirty") - - testutils.AssertEqual(t, b2Record.Label, "foo", "b2 Label mismatch") - testutils.AssertEqual(t, b2Record.USN, 12, "b2 USN mismatch") - testutils.AssertEqual(t, b2Record.Dirty, false, "b2 Dirty mismatch") - }) - - t.Run("insert, 3 duplicates", func(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, usn, label, dirty, deleted) VALUES (?, ?, ?, ?, ?)", "b1-uuid", 1, "foo", false, false) - testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, usn, label, dirty, deleted) VALUES (?, ?, ?, ?, ?)", "b2-uuid", 2, "foo (2)", true, false) - testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, usn, label, dirty, deleted) VALUES (?, ?, ?, ?, ?)", "b3-uuid", 3, "foo (3)", false, false) - - // test - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - b := client.SyncFragBook{ - UUID: "b4-uuid", - USN: 12, - AddedOn: 1541108743, - Label: "foo", - Deleted: false, - } - - if err := mergeBook(tx, b, modeInsert); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // execute - var noteCount, bookCount int - testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 0, "note count mismatch") - testutils.AssertEqualf(t, bookCount, 4, "book count mismatch") - - var b1Record, b2Record, b3Record, b4Record core.Book - testutils.MustScan(t, "getting b1", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", "b1-uuid"), - &b1Record.UUID, &b1Record.Label, &b1Record.USN, &b1Record.Dirty) - testutils.MustScan(t, "getting b2", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", "b2-uuid"), - &b2Record.UUID, &b2Record.Label, &b2Record.USN, &b2Record.Dirty) - testutils.MustScan(t, "getting b3", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", "b3-uuid"), - &b3Record.UUID, &b3Record.Label, &b3Record.USN, &b3Record.Dirty) - testutils.MustScan(t, "getting b4", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", "b4-uuid"), - &b4Record.UUID, &b4Record.Label, &b4Record.USN, &b4Record.Dirty) - - testutils.AssertEqual(t, b1Record.Label, "foo (4)", "b1 Label mismatch") - testutils.AssertEqual(t, b1Record.USN, 1, "b1 USN mismatch") - testutils.AssertEqual(t, b1Record.Dirty, true, "b1 Dirty mismatch") - - testutils.AssertEqual(t, b2Record.Label, "foo (2)", "b2 Label mismatch") - testutils.AssertEqual(t, b2Record.USN, 2, "b2 USN mismatch") - testutils.AssertEqual(t, b2Record.Dirty, true, "b2 Dirty mismatch") - - testutils.AssertEqual(t, b3Record.Label, "foo (3)", "b3 Label mismatch") - testutils.AssertEqual(t, b3Record.USN, 3, "b3 USN mismatch") - testutils.AssertEqual(t, b3Record.Dirty, false, "b3 Dirty mismatch") - - testutils.AssertEqual(t, b4Record.Label, "foo", "b4 Label mismatch") - testutils.AssertEqual(t, b4Record.USN, 12, "b4 USN mismatch") - testutils.AssertEqual(t, b4Record.Dirty, false, "b4 Dirty mismatch") - }) - - t.Run("update, no duplicates", func(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - // test - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - b1UUID := utils.GenerateUUID() - testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, usn, label, dirty, deleted) VALUES (?, ?, ?, ?, ?)", b1UUID, 1, "b1-label", false, false) - - b1 := client.SyncFragBook{ - UUID: b1UUID, - USN: 12, - AddedOn: 1541108743, - Label: "b1-label-edited", - Deleted: false, - } - - if err := mergeBook(tx, b1, modeUpdate); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // execute - var noteCount, bookCount int - testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 0, "note count mismatch") - testutils.AssertEqualf(t, bookCount, 1, "book count mismatch") - - var b1Record core.Book - testutils.MustScan(t, "getting b1", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", b1UUID), - &b1Record.UUID, &b1Record.Label, &b1Record.USN, &b1Record.Dirty) - - testutils.AssertEqual(t, b1Record.UUID, b1UUID, "b1 UUID mismatch") - testutils.AssertEqual(t, b1Record.Label, "b1-label-edited", "b1 Label mismatch") - testutils.AssertEqual(t, b1Record.USN, 12, "b1 USN mismatch") - }) - - t.Run("update, 1 duplicate", func(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, usn, label, dirty, deleted) VALUES (?, ?, ?, ?, ?)", "b1-uuid", 1, "foo", false, false) - testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, usn, label, dirty, deleted) VALUES (?, ?, ?, ?, ?)", "b2-uuid", 2, "bar", false, false) - - // test - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - b := client.SyncFragBook{ - UUID: "b1-uuid", - USN: 12, - AddedOn: 1541108743, - Label: "bar", - Deleted: false, - } - - if err := mergeBook(tx, b, modeUpdate); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // execute - var noteCount, bookCount int - testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 0, "note count mismatch") - testutils.AssertEqualf(t, bookCount, 2, "book count mismatch") - - var b1Record, b2Record core.Book - testutils.MustScan(t, "getting b1", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", "b1-uuid"), - &b1Record.UUID, &b1Record.Label, &b1Record.USN, &b1Record.Dirty) - testutils.MustScan(t, "getting b2", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", "b2-uuid"), - &b2Record.UUID, &b2Record.Label, &b2Record.USN, &b2Record.Dirty) - - testutils.AssertEqual(t, b1Record.Label, "bar", "b1 Label mismatch") - testutils.AssertEqual(t, b1Record.USN, 12, "b1 USN mismatch") - testutils.AssertEqual(t, b1Record.Dirty, false, "b1 Dirty mismatch") - - testutils.AssertEqual(t, b2Record.Label, "bar (2)", "b2 Label mismatch") - testutils.AssertEqual(t, b2Record.USN, 2, "b2 USN mismatch") - testutils.AssertEqual(t, b2Record.Dirty, true, "b2 Dirty mismatch") - }) - - t.Run("update, 3 duplicate", func(t *testing.T) { - // set uj - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, usn, label, dirty, deleted) VALUES (?, ?, ?, ?, ?)", "b1-uuid", 1, "foo", false, false) - testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, usn, label, dirty, deleted) VALUES (?, ?, ?, ?, ?)", "b2-uuid", 2, "bar", false, false) - testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, usn, label, dirty, deleted) VALUES (?, ?, ?, ?, ?)", "b3-uuid", 3, "bar (2)", true, false) - testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, usn, label, dirty, deleted) VALUES (?, ?, ?, ?, ?)", "b4-uuid", 4, "bar (3)", false, false) - - // test - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - b := client.SyncFragBook{ - UUID: "b1-uuid", - USN: 12, - AddedOn: 1541108743, - Label: "bar", - Deleted: false, - } - - if err := mergeBook(tx, b, modeUpdate); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // execute - var noteCount, bookCount int - testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 0, "note count mismatch") - testutils.AssertEqualf(t, bookCount, 4, "book count mismatch") - - var b1Record, b2Record, b3Record, b4Record core.Book - testutils.MustScan(t, "getting b1", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", "b1-uuid"), - &b1Record.UUID, &b1Record.Label, &b1Record.USN, &b1Record.Dirty) - testutils.MustScan(t, "getting b2", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", "b2-uuid"), - &b2Record.UUID, &b2Record.Label, &b2Record.USN, &b2Record.Dirty) - testutils.MustScan(t, "getting b3", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", "b3-uuid"), - &b3Record.UUID, &b3Record.Label, &b3Record.USN, &b3Record.Dirty) - testutils.MustScan(t, "getting b4", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", "b4-uuid"), - &b4Record.UUID, &b4Record.Label, &b4Record.USN, &b4Record.Dirty) - - testutils.AssertEqual(t, b1Record.Label, "bar", "b1 Label mismatch") - testutils.AssertEqual(t, b1Record.USN, 12, "b1 USN mismatch") - testutils.AssertEqual(t, b1Record.Dirty, false, "b1 Dirty mismatch") - - testutils.AssertEqual(t, b2Record.Label, "bar (4)", "b2 Label mismatch") - testutils.AssertEqual(t, b2Record.USN, 2, "b2 USN mismatch") - testutils.AssertEqual(t, b2Record.Dirty, true, "b2 Dirty mismatch") - - testutils.AssertEqual(t, b3Record.Label, "bar (2)", "b3 Label mismatch") - testutils.AssertEqual(t, b3Record.USN, 3, "b3 USN mismatch") - testutils.AssertEqual(t, b3Record.Dirty, true, "b3 Dirty mismatch") - - testutils.AssertEqual(t, b4Record.Label, "bar (3)", "b4 Label mismatch") - testutils.AssertEqual(t, b4Record.USN, 4, "b4 USN mismatch") - testutils.AssertEqual(t, b4Record.Dirty, false, "b4 Dirty mismatch") - }) -} - -func TestSaveServerState(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, "inserting last synced at", db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemLastSyncAt, int64(1231108742)) - testutils.MustExec(t, "inserting last max usn", db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemLastMaxUSN, 8) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - serverTime := int64(1541108743) - serverMaxUSN := 100 - - err = saveSyncState(tx, serverTime, serverMaxUSN) - if err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - var lastSyncedAt int64 - var lastMaxUSN int - - testutils.MustScan(t, "getting system value", - db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemLastSyncAt), &lastSyncedAt) - testutils.MustScan(t, "getting system value", - db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemLastMaxUSN), &lastMaxUSN) - - testutils.AssertEqual(t, lastSyncedAt, serverTime, "last synced at mismatch") - testutils.AssertEqual(t, lastMaxUSN, serverMaxUSN, "last max usn mismatch") -} - -// TestSendBooks tests that books are put to correct 'buckets' by running a test server and recording the -// uuid from the incoming data. It also tests that the uuid of the created books and book_uuids of their notes -// are updated accordingly based on the server response. -func TestSendBooks(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, "inserting last max usn", db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemLastMaxUSN, 0) - - // should be ignored - testutils.MustExec(t, "inserting b1", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b1-uuid", "b1-label", 1, false, false) - testutils.MustExec(t, "inserting b2", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b2-uuid", "b2-label", 2, false, false) - // should be created - testutils.MustExec(t, "inserting b3", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b3-uuid", "b3-label", 0, false, true) - testutils.MustExec(t, "inserting b4", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b4-uuid", "b4-label", 0, false, true) - // should be only expunged locally without syncing to server - testutils.MustExec(t, "inserting b5", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b5-uuid", "b5-label", 0, true, true) - // should be deleted - testutils.MustExec(t, "inserting b6", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b6-uuid", "b6-label", 10, true, true) - // should be updated - testutils.MustExec(t, "inserting b7", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b7-uuid", "b7-label", 11, false, true) - testutils.MustExec(t, "inserting b8", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b8-uuid", "b8-label", 18, false, true) - - // some random notes - testutils.MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n1-uuid", "b1-uuid", 10, "n1 body", 1541108743, false, false) - testutils.MustExec(t, "inserting n2", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n2-uuid", "b5-uuid", 10, "n2 body", 1541108743, false, false) - testutils.MustExec(t, "inserting n3", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n3-uuid", "b6-uuid", 10, "n3 body", 1541108743, false, false) - testutils.MustExec(t, "inserting n4", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n4-uuid", "b7-uuid", 10, "n4 body", 1541108743, false, false) - // notes that belong to the created book. Their book_uuid should be updated. - testutils.MustExec(t, "inserting n5", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n5-uuid", "b3-uuid", 10, "n5 body", 1541108743, false, false) - testutils.MustExec(t, "inserting n6", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n6-uuid", "b3-uuid", 10, "n6 body", 1541108743, false, false) - testutils.MustExec(t, "inserting n7", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n7-uuid", "b4-uuid", 10, "n7 body", 1541108743, false, false) - - var createdLabels []string - var updatesUUIDs []string - var deletedUUIDs []string - - // fire up a test server - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.String() == "/v1/books" && r.Method == "POST" { - var payload client.CreateBookPayload - - err := json.NewDecoder(r.Body).Decode(&payload) - if err != nil { - t.Fatalf(errors.Wrap(err, "decoding payload in the test server").Error()) - return - } - - createdLabels = append(createdLabels, payload.Name) - - resp := client.CreateBookResp{ - Book: client.RespBook{ - UUID: fmt.Sprintf("server-%s-uuid", payload.Name), - }, - } - - w.Header().Set("Body-Type", "application/json") - if err := json.NewEncoder(w).Encode(resp); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - return - } - - p := strings.Split(r.URL.Path, "/") - if len(p) == 4 && p[0] == "" && p[1] == "v1" && p[2] == "books" { - if r.Method == "PATCH" { - uuid := p[3] - updatesUUIDs = append(updatesUUIDs, uuid) - - w.Header().Set("Body-Type", "application/json") - w.Write([]byte("{}")) - return - } else if r.Method == "DELETE" { - uuid := p[3] - deletedUUIDs = append(deletedUUIDs, uuid) - - w.Header().Set("Body-Type", "application/json") - w.Write([]byte("{}")) - return - } - } - - t.Fatalf("unrecognized endpoint reached Method: %s Path: %s", r.Method, r.URL.Path) - })) - defer ts.Close() - - ctx.APIEndpoint = ts.URL - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - if _, err := sendBooks(ctx, tx, "mockAPIKey"); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - sort.SliceStable(createdLabels, func(i, j int) bool { - return strings.Compare(createdLabels[i], createdLabels[j]) < 0 - }) - - testutils.AssertDeepEqual(t, createdLabels, []string{"b3-label", "b4-label"}, "createdLabels mismatch") - testutils.AssertDeepEqual(t, updatesUUIDs, []string{"b7-uuid", "b8-uuid"}, "updatesUUIDs mismatch") - testutils.AssertDeepEqual(t, deletedUUIDs, []string{"b6-uuid"}, "deletedUUIDs mismatch") - - var b1, b2, b3, b4, b7, b8 core.Book - testutils.MustScan(t, "getting b1", db.QueryRow("SELECT uuid, dirty FROM books WHERE label = ?", "b1-label"), &b1.UUID, &b1.Dirty) - testutils.MustScan(t, "getting b2", db.QueryRow("SELECT uuid, dirty FROM books WHERE label = ?", "b2-label"), &b2.UUID, &b2.Dirty) - testutils.MustScan(t, "getting b3", db.QueryRow("SELECT uuid, dirty FROM books WHERE label = ?", "b3-label"), &b3.UUID, &b3.Dirty) - testutils.MustScan(t, "getting b4", db.QueryRow("SELECT uuid, dirty FROM books WHERE label = ?", "b4-label"), &b4.UUID, &b4.Dirty) - testutils.MustScan(t, "getting b7", db.QueryRow("SELECT uuid, dirty FROM books WHERE label = ?", "b7-label"), &b7.UUID, &b7.Dirty) - testutils.MustScan(t, "getting b8", db.QueryRow("SELECT uuid, dirty FROM books WHERE label = ?", "b8-label"), &b8.UUID, &b8.Dirty) - - var bookCount int - testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - testutils.AssertEqualf(t, bookCount, 6, "book count mismatch") - - testutils.AssertEqual(t, b1.Dirty, false, "b1 Dirty mismatch") - testutils.AssertEqual(t, b2.Dirty, false, "b2 Dirty mismatch") - testutils.AssertEqual(t, b3.Dirty, false, "b3 Dirty mismatch") - testutils.AssertEqual(t, b4.Dirty, false, "b4 Dirty mismatch") - testutils.AssertEqual(t, b7.Dirty, false, "b7 Dirty mismatch") - testutils.AssertEqual(t, b8.Dirty, false, "b8 Dirty mismatch") - testutils.AssertEqual(t, b1.UUID, "b1-uuid", "b1 UUID mismatch") - testutils.AssertEqual(t, b2.UUID, "b2-uuid", "b2 UUID mismatch") - // uuids of created books should have been updated - testutils.AssertEqual(t, b3.UUID, "server-b3-label-uuid", "b3 UUID mismatch") - testutils.AssertEqual(t, b4.UUID, "server-b4-label-uuid", "b4 UUID mismatch") - testutils.AssertEqual(t, b7.UUID, "b7-uuid", "b7 UUID mismatch") - testutils.AssertEqual(t, b8.UUID, "b8-uuid", "b8 UUID mismatch") - - var n1, n2, n3, n4, n5, n6, n7 core.Note - testutils.MustScan(t, "getting n1", db.QueryRow("SELECT book_uuid FROM notes WHERE body = ?", "n1 body"), &n1.BookUUID) - testutils.MustScan(t, "getting n2", db.QueryRow("SELECT book_uuid FROM notes WHERE body = ?", "n2 body"), &n2.BookUUID) - testutils.MustScan(t, "getting n3", db.QueryRow("SELECT book_uuid FROM notes WHERE body = ?", "n3 body"), &n3.BookUUID) - testutils.MustScan(t, "getting n4", db.QueryRow("SELECT book_uuid FROM notes WHERE body = ?", "n4 body"), &n4.BookUUID) - testutils.MustScan(t, "getting n5", db.QueryRow("SELECT book_uuid FROM notes WHERE body = ?", "n5 body"), &n5.BookUUID) - testutils.MustScan(t, "getting n6", db.QueryRow("SELECT book_uuid FROM notes WHERE body = ?", "n6 body"), &n6.BookUUID) - testutils.MustScan(t, "getting n7", db.QueryRow("SELECT book_uuid FROM notes WHERE body = ?", "n7 body"), &n7.BookUUID) - testutils.AssertEqual(t, n1.BookUUID, "b1-uuid", "n1 bookUUID mismatch") - testutils.AssertEqual(t, n2.BookUUID, "b5-uuid", "n2 bookUUID mismatch") - testutils.AssertEqual(t, n3.BookUUID, "b6-uuid", "n3 bookUUID mismatch") - testutils.AssertEqual(t, n4.BookUUID, "b7-uuid", "n4 bookUUID mismatch") - testutils.AssertEqual(t, n5.BookUUID, "server-b3-label-uuid", "n5 bookUUID mismatch") - testutils.AssertEqual(t, n6.BookUUID, "server-b3-label-uuid", "n6 bookUUID mismatch") - testutils.AssertEqual(t, n7.BookUUID, "server-b4-label-uuid", "n7 bookUUID mismatch") -} - -func TestSendBooks_isBehind(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.String() == "/v1/books" && r.Method == "POST" { - var payload client.CreateBookPayload - - err := json.NewDecoder(r.Body).Decode(&payload) - if err != nil { - t.Fatalf(errors.Wrap(err, "decoding payload in the test server").Error()) - return - } - - resp := client.CreateBookResp{ - Book: client.RespBook{ - USN: 11, - }, - } - - w.Header().Set("Body-Type", "application/json") - if err := json.NewEncoder(w).Encode(resp); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - return - } - - p := strings.Split(r.URL.Path, "/") - if len(p) == 4 && p[0] == "" && p[1] == "v1" && p[2] == "books" { - if r.Method == "PATCH" { - resp := client.UpdateBookResp{ - Book: client.RespBook{ - USN: 11, - }, - } - - w.Header().Set("Body-Type", "application/json") - if err := json.NewEncoder(w).Encode(resp); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - return - } else if r.Method == "DELETE" { - resp := client.DeleteBookResp{ - Book: client.RespBook{ - USN: 11, - }, - } - - w.Header().Set("Body-Type", "application/json") - if err := json.NewEncoder(w).Encode(resp); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - return - } - } - - t.Fatalf("unrecognized endpoint reached Method: %s Path: %s", r.Method, r.URL.Path) - })) - defer ts.Close() - - t.Run("create book", func(t *testing.T) { - testCases := []struct { - systemLastMaxUSN int - expectedIsBehind bool - }{ - { - systemLastMaxUSN: 10, - expectedIsBehind: false, - }, - { - systemLastMaxUSN: 9, - expectedIsBehind: true, - }, - } - - for idx, tc := range testCases { - func() { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - ctx.APIEndpoint = ts.URL - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, fmt.Sprintf("inserting last max usn for test case %d", idx), db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemLastMaxUSN, tc.systemLastMaxUSN) - testutils.MustExec(t, fmt.Sprintf("inserting b1 for test case %d", idx), db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b1-uuid", "b1-label", 0, false, true) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error()) - } - - isBehind, err := sendBooks(ctx, tx, "mockAPIKey") - if err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error()) - } - - tx.Commit() - - // test - testutils.AssertEqual(t, isBehind, tc.expectedIsBehind, fmt.Sprintf("isBehind mismatch for test case %d", idx)) - }() - } - }) - - t.Run("delete book", func(t *testing.T) { - testCases := []struct { - systemLastMaxUSN int - expectedIsBehind bool - }{ - { - systemLastMaxUSN: 10, - expectedIsBehind: false, - }, - { - systemLastMaxUSN: 9, - expectedIsBehind: true, - }, - } - - for idx, tc := range testCases { - func() { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - ctx.APIEndpoint = ts.URL - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, fmt.Sprintf("inserting last max usn for test case %d", idx), db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemLastMaxUSN, tc.systemLastMaxUSN) - testutils.MustExec(t, fmt.Sprintf("inserting b1 for test case %d", idx), db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b1-uuid", "b1-label", 1, true, true) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error()) - } - - isBehind, err := sendBooks(ctx, tx, "mockAPIKey") - if err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error()) - } - - tx.Commit() - - // test - testutils.AssertEqual(t, isBehind, tc.expectedIsBehind, fmt.Sprintf("isBehind mismatch for test case %d", idx)) - }() - } - }) - - t.Run("update book", func(t *testing.T) { - testCases := []struct { - systemLastMaxUSN int - expectedIsBehind bool - }{ - { - systemLastMaxUSN: 10, - expectedIsBehind: false, - }, - { - systemLastMaxUSN: 9, - expectedIsBehind: true, - }, - } - - for idx, tc := range testCases { - func() { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - ctx.APIEndpoint = ts.URL - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, fmt.Sprintf("inserting last max usn for test case %d", idx), db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemLastMaxUSN, tc.systemLastMaxUSN) - testutils.MustExec(t, fmt.Sprintf("inserting b1 for test case %d", idx), db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b1-uuid", "b1-label", 11, false, true) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error()) - } - - isBehind, err := sendBooks(ctx, tx, "mockAPIKey") - if err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error()) - } - - tx.Commit() - - // test - testutils.AssertEqual(t, isBehind, tc.expectedIsBehind, fmt.Sprintf("isBehind mismatch for test case %d", idx)) - }() - } - }) -} - -// TestSendNotes tests that notes are put to correct 'buckets' by running a test server and recording the -// uuid from the incoming data. -func TestSendNotes(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, "inserting last max usn", db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemLastMaxUSN, 0) - - b1UUID := "b1-uuid" - testutils.MustExec(t, "inserting b1", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", b1UUID, "b1-label", 1, false, false) - - // should be ignored - testutils.MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n1-uuid", b1UUID, 10, "n1-body", 1541108743, false, false) - // should be created - testutils.MustExec(t, "inserting n2", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n2-uuid", b1UUID, 0, "n2-body", 1541108743, false, true) - // should be updated - testutils.MustExec(t, "inserting n3", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n3-uuid", b1UUID, 11, "n3-body", 1541108743, false, true) - // should be only expunged locally without syncing to server - testutils.MustExec(t, "inserting n4", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n4-uuid", b1UUID, 0, "n4-body", 1541108743, true, true) - // should be deleted - testutils.MustExec(t, "inserting n5", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n5-uuid", b1UUID, 17, "n5-body", 1541108743, true, true) - // should be created - testutils.MustExec(t, "inserting n6", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n6-uuid", b1UUID, 0, "n6-body", 1541108743, false, true) - // should be ignored - testutils.MustExec(t, "inserting n7", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n7-uuid", b1UUID, 12, "n7-body", 1541108743, false, false) - // should be updated - testutils.MustExec(t, "inserting n8", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n8-uuid", b1UUID, 17, "n8-body", 1541108743, false, true) - // should be deleted - testutils.MustExec(t, "inserting n9", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n9-uuid", b1UUID, 17, "n9-body", 1541108743, true, true) - // should be created - testutils.MustExec(t, "inserting n10", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n10-uuid", b1UUID, 0, "n10-body", 1541108743, false, true) - - var createdBodys []string - var updatedUUIDs []string - var deletedUUIDs []string - - // fire up a test server - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.String() == "/v1/notes" && r.Method == "POST" { - var payload client.CreateNotePayload - - err := json.NewDecoder(r.Body).Decode(&payload) - if err != nil { - t.Fatalf(errors.Wrap(err, "decoding payload in the test server").Error()) - return - } - - createdBodys = append(createdBodys, payload.Body) - - resp := client.CreateNoteResp{ - Result: client.RespNote{ - UUID: fmt.Sprintf("server-%s-uuid", payload.Body), - }, - } - - w.Header().Set("Body-Type", "application/json") - if err := json.NewEncoder(w).Encode(resp); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - return - } - - p := strings.Split(r.URL.Path, "/") - if len(p) == 4 && p[0] == "" && p[1] == "v1" && p[2] == "notes" { - if r.Method == "PATCH" { - uuid := p[3] - updatedUUIDs = append(updatedUUIDs, uuid) - - w.Header().Set("Body-Type", "application/json") - w.Write([]byte("{}")) - return - } else if r.Method == "DELETE" { - uuid := p[3] - deletedUUIDs = append(deletedUUIDs, uuid) - - w.Header().Set("Body-Type", "application/json") - w.Write([]byte("{}")) - return - } - } - - t.Fatalf("unrecognized endpoint reached Method: %s Path: %s", r.Method, r.URL.Path) - })) - defer ts.Close() - - ctx.APIEndpoint = ts.URL - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - if _, err := sendNotes(ctx, tx, "mockAPIKey"); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - sort.SliceStable(createdBodys, func(i, j int) bool { - return strings.Compare(createdBodys[i], createdBodys[j]) < 0 - }) - - testutils.AssertDeepEqual(t, createdBodys, []string{"n10-body", "n2-body", "n6-body"}, "createdBodys mismatch") - testutils.AssertDeepEqual(t, updatedUUIDs, []string{"n3-uuid", "n8-uuid"}, "updatedUUIDs mismatch") - testutils.AssertDeepEqual(t, deletedUUIDs, []string{"n5-uuid", "n9-uuid"}, "deletedUUIDs mismatch") - - var noteCount int - testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.AssertEqualf(t, noteCount, 7, "note count mismatch") - - var n1, n2, n3, n6, n7, n8, n10 core.Note - testutils.MustScan(t, "getting n1", db.QueryRow("SELECT uuid, dirty FROM notes WHERE body = ?", "n1-body"), &n1.UUID, &n1.Dirty) - testutils.MustScan(t, "getting n2", db.QueryRow("SELECT uuid, dirty FROM notes WHERE body = ?", "n2-body"), &n2.UUID, &n2.Dirty) - testutils.MustScan(t, "getting n3", db.QueryRow("SELECT uuid, dirty FROM notes WHERE body = ?", "n3-body"), &n3.UUID, &n3.Dirty) - testutils.MustScan(t, "getting n6", db.QueryRow("SELECT uuid, dirty FROM notes WHERE body = ?", "n6-body"), &n6.UUID, &n6.Dirty) - testutils.MustScan(t, "getting n7", db.QueryRow("SELECT uuid, dirty FROM notes WHERE body = ?", "n7-body"), &n7.UUID, &n7.Dirty) - testutils.MustScan(t, "getting n8", db.QueryRow("SELECT uuid, dirty FROM notes WHERE body = ?", "n8-body"), &n8.UUID, &n8.Dirty) - testutils.MustScan(t, "getting n10", db.QueryRow("SELECT uuid, dirty FROM notes WHERE body = ?", "n10-body"), &n10.UUID, &n10.Dirty) - - testutils.AssertEqualf(t, noteCount, 7, "note count mismatch") - - testutils.AssertEqual(t, n1.Dirty, false, "n1 Dirty mismatch") - testutils.AssertEqual(t, n2.Dirty, false, "n2 Dirty mismatch") - testutils.AssertEqual(t, n3.Dirty, false, "n3 Dirty mismatch") - testutils.AssertEqual(t, n6.Dirty, false, "n6 Dirty mismatch") - testutils.AssertEqual(t, n7.Dirty, false, "n7 Dirty mismatch") - testutils.AssertEqual(t, n8.Dirty, false, "n8 Dirty mismatch") - testutils.AssertEqual(t, n10.Dirty, false, "n10 Dirty mismatch") - - // UUIDs of created notes should have been updated with those from the server response - testutils.AssertEqual(t, n1.UUID, "n1-uuid", "n1 UUID mismatch") - testutils.AssertEqual(t, n2.UUID, "server-n2-body-uuid", "n2 UUID mismatch") - testutils.AssertEqual(t, n3.UUID, "n3-uuid", "n3 UUID mismatch") - testutils.AssertEqual(t, n6.UUID, "server-n6-body-uuid", "n6 UUID mismatch") - testutils.AssertEqual(t, n7.UUID, "n7-uuid", "n7 UUID mismatch") - testutils.AssertEqual(t, n8.UUID, "n8-uuid", "n8 UUID mismatch") - testutils.AssertEqual(t, n10.UUID, "server-n10-body-uuid", "n10 UUID mismatch") -} - -func TestSendNotes_isBehind(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.String() == "/v1/notes" && r.Method == "POST" { - var payload client.CreateBookPayload - - err := json.NewDecoder(r.Body).Decode(&payload) - if err != nil { - t.Fatalf(errors.Wrap(err, "decoding payload in the test server").Error()) - return - } - - resp := client.CreateNoteResp{ - Result: client.RespNote{ - USN: 11, - }, - } - - w.Header().Set("Body-Type", "application/json") - if err := json.NewEncoder(w).Encode(resp); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - return - } - - p := strings.Split(r.URL.Path, "/") - if len(p) == 4 && p[0] == "" && p[1] == "v1" && p[2] == "notes" { - if r.Method == "PATCH" { - resp := client.UpdateNoteResp{ - Result: client.RespNote{ - USN: 11, - }, - } - - w.Header().Set("Body-Type", "application/json") - if err := json.NewEncoder(w).Encode(resp); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - return - } else if r.Method == "DELETE" { - resp := client.DeleteNoteResp{ - Result: client.RespNote{ - USN: 11, - }, - } - - w.Header().Set("Body-Type", "application/json") - if err := json.NewEncoder(w).Encode(resp); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - return - } - } - - t.Fatalf("unrecognized endpoint reached Method: %s Path: %s", r.Method, r.URL.Path) - })) - defer ts.Close() - - t.Run("create note", func(t *testing.T) { - testCases := []struct { - systemLastMaxUSN int - expectedIsBehind bool - }{ - { - systemLastMaxUSN: 10, - expectedIsBehind: false, - }, - { - systemLastMaxUSN: 9, - expectedIsBehind: true, - }, - } - - for idx, tc := range testCases { - func() { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - ctx.APIEndpoint = ts.URL - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, fmt.Sprintf("inserting last max usn for test case %d", idx), db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemLastMaxUSN, tc.systemLastMaxUSN) - testutils.MustExec(t, "inserting b1", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b1-uuid", "b1-label", 1, false, false) - testutils.MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n1-uuid", "b1-uuid", 1, "n1 body", 1541108743, false, true) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error()) - } - - isBehind, err := sendNotes(ctx, tx, "mockAPIKey") - if err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error()) - } - - tx.Commit() - - // test - testutils.AssertEqual(t, isBehind, tc.expectedIsBehind, fmt.Sprintf("isBehind mismatch for test case %d", idx)) - }() - } - }) - - t.Run("delete note", func(t *testing.T) { - testCases := []struct { - systemLastMaxUSN int - expectedIsBehind bool - }{ - { - systemLastMaxUSN: 10, - expectedIsBehind: false, - }, - { - systemLastMaxUSN: 9, - expectedIsBehind: true, - }, - } - - for idx, tc := range testCases { - func() { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - ctx.APIEndpoint = ts.URL - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, fmt.Sprintf("inserting last max usn for test case %d", idx), db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemLastMaxUSN, tc.systemLastMaxUSN) - testutils.MustExec(t, "inserting b1", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b1-uuid", "b1-label", 1, false, false) - testutils.MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n1-uuid", "b1-uuid", 2, "n1 body", 1541108743, true, true) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error()) - } - - isBehind, err := sendNotes(ctx, tx, "mockAPIKey") - if err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error()) - } - - tx.Commit() - - // test - testutils.AssertEqual(t, isBehind, tc.expectedIsBehind, fmt.Sprintf("isBehind mismatch for test case %d", idx)) - }() - } - }) - - t.Run("update note", func(t *testing.T) { - testCases := []struct { - systemLastMaxUSN int - expectedIsBehind bool - }{ - { - systemLastMaxUSN: 10, - expectedIsBehind: false, - }, - { - systemLastMaxUSN: 9, - expectedIsBehind: true, - }, - } - - for idx, tc := range testCases { - func() { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - ctx.APIEndpoint = ts.URL - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, fmt.Sprintf("inserting last max usn for test case %d", idx), db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemLastMaxUSN, tc.systemLastMaxUSN) - testutils.MustExec(t, "inserting b1", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b1-uuid", "b1-label", 1, false, false) - testutils.MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n1-uuid", "b1-uuid", 8, "n1 body", 1541108743, false, true) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error()) - } - - isBehind, err := sendNotes(ctx, tx, "mockAPIKey") - if err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error()) - } - - tx.Commit() - - // test - testutils.AssertEqual(t, isBehind, tc.expectedIsBehind, fmt.Sprintf("isBehind mismatch for test case %d", idx)) - }() - } - }) -} - -func TestMergeNote(t *testing.T) { - b1UUID := "b1-uuid" - b2UUID := "b2-uuid" - - testCases := []struct { - addedOn int64 - clientUSN int - clientEditedOn int64 - clientBody string - clientPublic bool - clientDeleted bool - clientBookUUID string - clientDirty bool - serverUSN int - serverEditedOn int64 - serverBody string - serverPublic bool - serverDeleted bool - serverBookUUID string - expectedUSN int - expectedAddedOn int64 - expectedEditedOn int64 - expectedBody string - expectedPublic bool - expectedDeleted bool - expectedBookUUID string - expectedDirty bool - }{ - { - clientDirty: false, - clientUSN: 1, - clientEditedOn: 0, - clientBody: "n1 body", - clientPublic: false, - clientDeleted: false, - clientBookUUID: b1UUID, - addedOn: 1541232118, - serverUSN: 21, - serverEditedOn: 1541219321, - serverBody: "n1 body edited", - serverPublic: true, - serverDeleted: false, - serverBookUUID: b2UUID, - expectedUSN: 21, - expectedAddedOn: 1541232118, - expectedEditedOn: 1541219321, - expectedBody: "n1 body edited", - expectedPublic: true, - expectedDeleted: false, - expectedBookUUID: b2UUID, - expectedDirty: false, - }, - // deleted locally and edited on server - { - clientDirty: true, - clientUSN: 1, - clientEditedOn: 1541219321, - clientBody: "", - clientPublic: false, - clientDeleted: true, - clientBookUUID: b1UUID, - addedOn: 1541232118, - serverUSN: 21, - serverEditedOn: 1541219321, - serverBody: "n1 body edited", - serverPublic: false, - serverDeleted: false, - serverBookUUID: b2UUID, - expectedUSN: 21, - expectedAddedOn: 1541232118, - expectedEditedOn: 1541219321, - expectedBody: "n1 body edited", - expectedPublic: false, - expectedDeleted: false, - expectedBookUUID: b2UUID, - expectedDirty: false, - }, - } - - for idx, tc := range testCases { - func() { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, fmt.Sprintf("inserting b1 for test case %d", idx), db, "INSERT INTO books (uuid, label, usn, dirty) VALUES (?, ?, ?, ?)", b1UUID, "b1-label", 5, false) - testutils.MustExec(t, fmt.Sprintf("inserting b2 for test case %d", idx), db, "INSERT INTO books (uuid, label, usn, dirty) VALUES (?, ?, ?, ?)", b2UUID, "b2-label", 6, false) - n1UUID := utils.GenerateUUID() - testutils.MustExec(t, fmt.Sprintf("inserting n1 for test case %d", idx), db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", n1UUID, b1UUID, tc.clientUSN, tc.addedOn, tc.clientEditedOn, tc.clientBody, tc.clientPublic, tc.clientDeleted, tc.clientDirty) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error()) - } - - // update all fields but uuid and bump usn - fragNote := client.SyncFragNote{ - UUID: n1UUID, - BookUUID: tc.serverBookUUID, - USN: tc.serverUSN, - AddedOn: tc.addedOn, - EditedOn: tc.serverEditedOn, - Body: tc.serverBody, - Public: tc.serverPublic, - Deleted: tc.serverDeleted, - } - var localNote core.Note - testutils.MustScan(t, fmt.Sprintf("getting localNote for test case %d", idx), - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", n1UUID), - &localNote.UUID, &localNote.BookUUID, &localNote.USN, &localNote.AddedOn, &localNote.EditedOn, &localNote.Body, &localNote.Public, &localNote.Deleted, &localNote.Dirty) - - if err := mergeNote(tx, fragNote, localNote); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error()) - } - - tx.Commit() - - // test - var noteCount, bookCount int - testutils.MustScan(t, fmt.Sprintf("counting notes for test case %d", idx), db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.MustScan(t, fmt.Sprintf("counting books for test case %d", idx), db.QueryRow("SELECT count(*) FROM books"), &bookCount) - - testutils.AssertEqualf(t, noteCount, 1, fmt.Sprintf("note count mismatch for test case %d", idx)) - testutils.AssertEqualf(t, bookCount, 2, fmt.Sprintf("book count mismatch for test case %d", idx)) - - var n1Record core.Note - testutils.MustScan(t, fmt.Sprintf("getting n1Record for test case %d", idx), - db.QueryRow("SELECT uuid, book_uuid, usn, added_on, edited_on, body, public, deleted, dirty FROM notes WHERE uuid = ?", n1UUID), - &n1Record.UUID, &n1Record.BookUUID, &n1Record.USN, &n1Record.AddedOn, &n1Record.EditedOn, &n1Record.Body, &n1Record.Public, &n1Record.Deleted, &n1Record.Dirty) - var b1Record core.Book - testutils.MustScan(t, "getting b1Record for test case", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", b1UUID), - &b1Record.UUID, &b1Record.Label, &b1Record.USN, &b1Record.Dirty) - var b2Record core.Book - testutils.MustScan(t, "getting b2Record for test case", - db.QueryRow("SELECT uuid, label, usn, dirty FROM books WHERE uuid = ?", b2UUID), - &b2Record.UUID, &b2Record.Label, &b2Record.USN, &b2Record.Dirty) - - testutils.AssertEqual(t, b1Record.UUID, b1UUID, fmt.Sprintf("b1Record UUID mismatch for test case %d", idx)) - testutils.AssertEqual(t, b1Record.Label, "b1-label", fmt.Sprintf("b1Record Label mismatch for test case %d", idx)) - testutils.AssertEqual(t, b1Record.USN, 5, fmt.Sprintf("b1Record USN mismatch for test case %d", idx)) - testutils.AssertEqual(t, b1Record.Dirty, false, fmt.Sprintf("b1Record Dirty mismatch for test case %d", idx)) - - testutils.AssertEqual(t, b2Record.UUID, b2UUID, fmt.Sprintf("b2Record UUID mismatch for test case %d", idx)) - testutils.AssertEqual(t, b2Record.Label, "b2-label", fmt.Sprintf("b2Record Label mismatch for test case %d", idx)) - testutils.AssertEqual(t, b2Record.USN, 6, fmt.Sprintf("b2Record USN mismatch for test case %d", idx)) - testutils.AssertEqual(t, b2Record.Dirty, false, fmt.Sprintf("b2Record Dirty mismatch for test case %d", idx)) - - testutils.AssertEqual(t, n1Record.UUID, n1UUID, fmt.Sprintf("n1Record UUID mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1Record.BookUUID, tc.expectedBookUUID, fmt.Sprintf("n1Record BookUUID mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1Record.USN, tc.expectedUSN, fmt.Sprintf("n1Record USN mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1Record.AddedOn, tc.expectedAddedOn, fmt.Sprintf("n1Record AddedOn mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1Record.EditedOn, tc.expectedEditedOn, fmt.Sprintf("n1Record EditedOn mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1Record.Body, tc.expectedBody, fmt.Sprintf("n1Record Body mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1Record.Public, tc.expectedPublic, fmt.Sprintf("n1Record Public mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1Record.Deleted, tc.expectedDeleted, fmt.Sprintf("n1Record Deleted mismatch for test case %d", idx)) - testutils.AssertEqual(t, n1Record.Dirty, tc.expectedDirty, fmt.Sprintf("n1Record Dirty mismatch for test case %d", idx)) - }() - } -} - -func TestCheckBookPristine(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - testutils.MustExec(t, "inserting b1", db, "INSERT INTO books (uuid, label, usn, dirty) VALUES (?, ?, ?, ?)", "b1-uuid", "b1-label", 5, false) - testutils.MustExec(t, "inserting b2", db, "INSERT INTO books (uuid, label, usn, dirty) VALUES (?, ?, ?, ?)", "b2-uuid", "b2-label", 6, false) - testutils.MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, added_on, body, dirty) VALUES (?, ?, ?, ?, ?)", "n1-uuid", "b1-uuid", 1541108743, "n1 body", false) - testutils.MustExec(t, "inserting n2", db, "INSERT INTO notes (uuid, book_uuid, added_on, body, dirty) VALUES (?, ?, ?, ?, ?)", "n2-uuid", "b1-uuid", 1541108743, "n2 body", false) - testutils.MustExec(t, "inserting n3", db, "INSERT INTO notes (uuid, book_uuid, added_on, body, dirty) VALUES (?, ?, ?, ?, ?)", "n3-uuid", "b1-uuid", 1541108743, "n3 body", true) - testutils.MustExec(t, "inserting n4", db, "INSERT INTO notes (uuid, book_uuid, added_on, body, dirty) VALUES (?, ?, ?, ?, ?)", "n4-uuid", "b2-uuid", 1541108743, "n4 body", false) - testutils.MustExec(t, "inserting n5", db, "INSERT INTO notes (uuid, book_uuid, added_on, body, dirty) VALUES (?, ?, ?, ?, ?)", "n5-uuid", "b2-uuid", 1541108743, "n5 body", false) - - t.Run("b1", func(t *testing.T) { - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - got, err := checkNotesPristine(tx, "b1-uuid") - if err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - testutils.AssertEqual(t, got, false, "b1 should not be pristine") - }) - - t.Run("b2", func(t *testing.T) { - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - got, err := checkNotesPristine(tx, "b2-uuid") - if err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - testutils.AssertEqual(t, got, true, "b2 should be pristine") - }) -} - -func TestCheckNoteInList(t *testing.T) { - list := syncList{ - Notes: map[string]client.SyncFragNote{ - "n1-uuid": client.SyncFragNote{ - UUID: "n1-uuid", - }, - "n2-uuid": client.SyncFragNote{ - UUID: "n2-uuid", - }, - }, - Books: map[string]client.SyncFragBook{ - "b1-uuid": client.SyncFragBook{ - UUID: "b1-uuid", - }, - "b2-uuid": client.SyncFragBook{ - UUID: "b2-uuid", - }, - }, - ExpungedNotes: map[string]bool{ - "n3-uuid": true, - "n4-uuid": true, - }, - ExpungedBooks: map[string]bool{ - "b3-uuid": true, - "b4-uuid": true, - }, - MaxUSN: 1, - MaxCurrentTime: 2, - } - - testCases := []struct { - uuid string - expected bool - }{ - { - uuid: "n1-uuid", - expected: true, - }, - { - uuid: "n2-uuid", - expected: true, - }, - { - uuid: "n3-uuid", - expected: true, - }, - { - uuid: "n4-uuid", - expected: true, - }, - { - uuid: "nonexistent-note-uuid", - expected: false, - }, - } - - for idx, tc := range testCases { - got := checkNoteInList(tc.uuid, &list) - testutils.AssertEqual(t, got, tc.expected, fmt.Sprintf("result mismatch for test case %d", idx)) - } -} - -func TestCheckBookInList(t *testing.T) { - list := syncList{ - Notes: map[string]client.SyncFragNote{ - "n1-uuid": client.SyncFragNote{ - UUID: "n1-uuid", - }, - "n2-uuid": client.SyncFragNote{ - UUID: "n2-uuid", - }, - }, - Books: map[string]client.SyncFragBook{ - "b1-uuid": client.SyncFragBook{ - UUID: "b1-uuid", - }, - "b2-uuid": client.SyncFragBook{ - UUID: "b2-uuid", - }, - }, - ExpungedNotes: map[string]bool{ - "n3-uuid": true, - "n4-uuid": true, - }, - ExpungedBooks: map[string]bool{ - "b3-uuid": true, - "b4-uuid": true, - }, - MaxUSN: 1, - MaxCurrentTime: 2, - } - - testCases := []struct { - uuid string - expected bool - }{ - { - uuid: "b1-uuid", - expected: true, - }, - { - uuid: "b2-uuid", - expected: true, - }, - { - uuid: "b3-uuid", - expected: true, - }, - { - uuid: "b4-uuid", - expected: true, - }, - { - uuid: "nonexistent-book-uuid", - expected: false, - }, - } - - for idx, tc := range testCases { - got := checkBookInList(tc.uuid, &list) - testutils.AssertEqual(t, got, tc.expected, fmt.Sprintf("result mismatch for test case %d", idx)) - } -} - -func TestCleanLocalNotes(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - list := syncList{ - Notes: map[string]client.SyncFragNote{ - "n1-uuid": client.SyncFragNote{ - UUID: "n1-uuid", - }, - "n2-uuid": client.SyncFragNote{ - UUID: "n2-uuid", - }, - }, - Books: map[string]client.SyncFragBook{ - "b1-uuid": client.SyncFragBook{ - UUID: "b1-uuid", - }, - "b2-uuid": client.SyncFragBook{ - UUID: "b2-uuid", - }, - }, - ExpungedNotes: map[string]bool{ - "n3-uuid": true, - "n4-uuid": true, - }, - ExpungedBooks: map[string]bool{ - "b3-uuid": true, - "b4-uuid": true, - }, - MaxUSN: 1, - MaxCurrentTime: 2, - } - - b1UUID := "b1-uuid" - testutils.MustExec(t, "inserting b1", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", b1UUID, "b1-label", 1, false, false) - - // exists in the list - testutils.MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n1-uuid", b1UUID, 10, "n1 body", 1541108743, false, false) - testutils.MustExec(t, "inserting n2", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n2-uuid", b1UUID, 0, "n2 body", 1541108743, false, true) - // non-existent in the list but in valid state - // (created in the cli and hasn't been uploaded) - testutils.MustExec(t, "inserting n6", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n6-uuid", b1UUID, 0, "n6 body", 1541108743, false, true) - // non-existent in the list and in an invalid state - testutils.MustExec(t, "inserting n5", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n5-uuid", b1UUID, 7, "n5 body", 1541108743, true, true) - testutils.MustExec(t, "inserting n9", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n9-uuid", b1UUID, 17, "n9 body", 1541108743, true, false) - testutils.MustExec(t, "inserting n10", db, "INSERT INTO notes (uuid, book_uuid, usn, body, added_on, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", "n10-uuid", b1UUID, 0, "n10 body", 1541108743, false, false) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - if err := cleanLocalNotes(tx, &list); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - var noteCount int - testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount) - testutils.AssertEqual(t, noteCount, 3, "note count mismatch") - - var n1, n2, n6 core.Note - testutils.MustScan(t, "getting n1", db.QueryRow("SELECT dirty FROM notes WHERE uuid = ?", "n1-uuid"), &n1.Dirty) - testutils.MustScan(t, "getting n2", db.QueryRow("SELECT dirty FROM notes WHERE uuid = ?", "n2-uuid"), &n2.Dirty) - testutils.MustScan(t, "getting n6", db.QueryRow("SELECT dirty FROM notes WHERE uuid = ?", "n6-uuid"), &n6.Dirty) -} - -func TestCleanLocalBooks(t *testing.T) { - // set up - ctx := testutils.InitEnv(t, "../../tmp", "../../testutils/fixtures/schema.sql", true) - defer testutils.TeardownEnv(ctx) - - db := ctx.DB - - list := syncList{ - Notes: map[string]client.SyncFragNote{ - "n1-uuid": client.SyncFragNote{ - UUID: "n1-uuid", - }, - "n2-uuid": client.SyncFragNote{ - UUID: "n2-uuid", - }, - }, - Books: map[string]client.SyncFragBook{ - "b1-uuid": client.SyncFragBook{ - UUID: "b1-uuid", - }, - "b2-uuid": client.SyncFragBook{ - UUID: "b2-uuid", - }, - }, - ExpungedNotes: map[string]bool{ - "n3-uuid": true, - "n4-uuid": true, - }, - ExpungedBooks: map[string]bool{ - "b3-uuid": true, - "b4-uuid": true, - }, - MaxUSN: 1, - MaxCurrentTime: 2, - } - - // existent in the server - testutils.MustExec(t, "inserting b1", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b1-uuid", "b1-label", 1, false, false) - testutils.MustExec(t, "inserting b3", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b3-uuid", "b3-label", 0, false, true) - // non-existent in the server but in valid state - testutils.MustExec(t, "inserting b5", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b5-uuid", "b5-label", 0, true, true) - // non-existent in the server and in an invalid state - testutils.MustExec(t, "inserting b6", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b6-uuid", "b6-label", 10, true, true) - testutils.MustExec(t, "inserting b7", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b7-uuid", "b7-label", 11, false, false) - testutils.MustExec(t, "inserting b8", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", "b8-uuid", "b8-label", 0, false, false) - - // execute - tx, err := db.Begin() - if err != nil { - t.Fatalf(errors.Wrap(err, "beginning a transaction").Error()) - } - - if err := cleanLocalBooks(tx, &list); err != nil { - tx.Rollback() - t.Fatalf(errors.Wrap(err, "executing").Error()) - } - - tx.Commit() - - // test - var bookCount int - testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount) - testutils.AssertEqual(t, bookCount, 3, "note count mismatch") - - var b1, b3, b5 core.Book - testutils.MustScan(t, "getting b1", db.QueryRow("SELECT label FROM books WHERE uuid = ?", "b1-uuid"), &b1.Label) - testutils.MustScan(t, "getting b3", db.QueryRow("SELECT label FROM books WHERE uuid = ?", "b3-uuid"), &b3.Label) - testutils.MustScan(t, "getting b5", db.QueryRow("SELECT label FROM books WHERE uuid = ?", "b5-uuid"), &b5.Label) -} diff --git a/core/core.go b/core/core.go index 85c03c59..ad94d574 100644 --- a/core/core.go +++ b/core/core.go @@ -59,24 +59,28 @@ func GetBookUUID(ctx infra.DnoteCtx, label string) (string, error) { func getEditorCommand() string { editor := os.Getenv("EDITOR") + var ret string + switch editor { case "atom": - return "atom -w" + ret = "atom -w" case "subl": - return "subl -n -w" + ret = "subl -n -w" case "mate": - return "mate -w" + ret = "mate -w" case "vim": - return "vim" - case "nvim": - return "nvim" + ret = "vim" case "nano": - return "nano" + ret = "nano" case "emacs": - return "emacs" + ret = "emacs" + case "nvim": + ret = "nvim" default: - return "vi" + ret = "vi" } + + return ret } // InitFiles creates, if necessary, the dnote directory and files inside diff --git a/log/log.go b/log/log.go index 72cedada..ba99c087 100644 --- a/log/log.go +++ b/log/log.go @@ -4,62 +4,77 @@ import ( "fmt" "os" - "github.com/fatih/color" + "github.com/dnote/color" ) var ( - SprintfRed = color.New(color.FgRed).SprintfFunc() - SprintfGreen = color.New(color.FgGreen).SprintfFunc() - SprintfYellow = color.New(color.FgYellow).SprintfFunc() - SprintfBlue = color.New(color.FgBlue).SprintfFunc() - SprintfGray = color.New(color.FgWhite).SprintfFunc() + // ColorRed is a red foreground color + ColorRed = color.New(color.FgRed) + // ColorGreen is a green foreground color + ColorGreen = color.New(color.FgGreen) + // ColorYellow is a yellow foreground color + ColorYellow = color.New(color.FgYellow) + // ColorBlue is a blue foreground color + ColorBlue = color.New(color.FgBlue) + // ColorGray is a gray foreground color + ColorGray = color.New(color.FgWhite) ) var indent = " " +// Info prints information func Info(msg string) { - fmt.Fprintf(color.Output, "%s%s %s", indent, SprintfBlue("•"), msg) + fmt.Fprintf(color.Output, "%s%s %s\n", indent, ColorBlue.Sprint("•"), msg) } +// Infof prints information with optional format verbs func Infof(msg string, v ...interface{}) { - fmt.Fprintf(color.Output, "%s%s %s", indent, SprintfBlue("•"), fmt.Sprintf(msg, v...)) + fmt.Fprintf(color.Output, "%s%s %s", indent, ColorBlue.Sprint("•"), fmt.Sprintf(msg, v...)) } +// Success prints a success message func Success(msg string) { - fmt.Fprintf(color.Output, "%s%s %s", indent, SprintfGreen("✔"), msg) + fmt.Fprintf(color.Output, "%s%s %s", indent, ColorGreen.Sprint("✔"), msg) } +// Successf prints a success message with optional format verbs func Successf(msg string, v ...interface{}) { - fmt.Fprintf(color.Output, "%s%s %s", indent, SprintfGreen("✔"), fmt.Sprintf(msg, v...)) + fmt.Fprintf(color.Output, "%s%s %s", indent, ColorGreen.Sprint("✔"), fmt.Sprintf(msg, v...)) } +// Plain prints a plain message without any prefix symbol func Plain(msg string) { fmt.Printf("%s%s", indent, msg) } +// Plainf prints a plain message without any prefix symbol. It takes optional format verbs. func Plainf(msg string, v ...interface{}) { - fmt.Fprintf(color.Output, "%s%s", indent, fmt.Sprintf(msg, v...)) + fmt.Printf("%s%s", indent, fmt.Sprintf(msg, v...)) } +// Warnf prints a warning message with optional format verbs func Warnf(msg string, v ...interface{}) { - fmt.Fprintf(color.Output, "%s%s %s", indent, SprintfRed("•"), fmt.Sprintf(msg, v...)) + fmt.Fprintf(color.Output, "%s%s %s", indent, ColorRed.Sprint("•"), fmt.Sprintf(msg, v...)) } +// Error prints an error message func Error(msg string) { - fmt.Fprintf(color.Output, "%s%s %s", indent, SprintfRed("⨯"), msg) + fmt.Fprintf(color.Output, "%s%s %s\n", indent, ColorRed.Sprint("⨯"), msg) } +// Errorf prints an error message with optional format verbs func Errorf(msg string, v ...interface{}) { - fmt.Fprintf(color.Output, "%s%s %s", indent, SprintfRed("⨯"), fmt.Sprintf(msg, v...)) + fmt.Fprintf(color.Output, "%s%s %s", indent, ColorRed.Sprintf("⨯"), fmt.Sprintf(msg, v...)) } +// Printf prints an normal message func Printf(msg string, v ...interface{}) { - fmt.Fprintf(color.Output, "%s%s %s", indent, SprintfGray("•"), fmt.Sprintf(msg, v...)) + fmt.Fprintf(color.Output, "%s%s %s", indent, ColorGray.Sprint("•"), fmt.Sprintf(msg, v...)) } // Debug prints to the console if DNOTE_DEBUG is set func Debug(msg string, v ...interface{}) { if os.Getenv("DNOTE_DEBUG") == "1" { - fmt.Fprintf(color.Output, "%s %s", SprintfGray("DEBUG:"), fmt.Sprintf(msg, v...)) + fmt.Fprintf(color.Output, "%s %s", ColorGray.Sprint("DEBUG:"), fmt.Sprintf(msg, v...)) } } diff --git a/log/output.go b/log/output.go new file mode 100644 index 00000000..88925361 --- /dev/null +++ b/log/output.go @@ -0,0 +1,12 @@ +package log + +import ( + "fmt" +) + +// PrintContent prints the note content with an appropriate format. +func PrintContent(content string) { + fmt.Printf("\n-----------------------content-----------------------\n") + fmt.Printf("%s", content) + fmt.Printf("\n-----------------------------------------------------\n") +} diff --git a/main_test.go b/main_test.go index a93a532b..b2dc28ae 100644 --- a/main_test.go +++ b/main_test.go @@ -7,12 +7,11 @@ import ( "os/exec" "testing" - "github.com/pkg/errors" - "github.com/dnote/cli/core" "github.com/dnote/cli/infra" "github.com/dnote/cli/testutils" "github.com/dnote/cli/utils" + "github.com/pkg/errors" ) var binaryName = "test-dnote" @@ -35,7 +34,7 @@ func TestInit(t *testing.T) { testutils.RunDnoteCmd(t, ctx, binaryName) // Test - if !utils.FileExists(ctx.DnoteDir) { + if !utils.FileExists(fmt.Sprintf("%s", ctx.DnoteDir)) { t.Errorf("dnote directory was not initialized") } if !utils.FileExists(fmt.Sprintf("%s/%s", ctx.DnoteDir, core.ConfigFilename)) { diff --git a/migrate/migrate.go b/migrate/migrate.go index 1b53c940..96193dd0 100644 --- a/migrate/migrate.go +++ b/migrate/migrate.go @@ -2,7 +2,6 @@ package migrate import ( "database/sql" - "github.com/dnote/cli/infra" "github.com/dnote/cli/log" "github.com/pkg/errors" diff --git a/utils/utils.go b/utils/utils.go index 29c2592e..14593b3f 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -62,44 +62,6 @@ func FileExists(filepath string) bool { return !os.IsNotExist(err) } -// 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, "opening the input file") - } - defer in.Close() - - out, err := os.Create(dest) - if err != nil { - return errors.Wrap(err, "creating the output file") - } - - if _, err = io.Copy(out, in); err != nil { - return errors.Wrap(err, "copying the file content") - } - - if err = out.Sync(); err != nil { - return errors.Wrap(err, "flushing the output file to disk") - } - - fi, err := os.Stat(src) - if err != nil { - 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, "copying permission to the output file") - } - - // Close the output file - if err = out.Close(); err != nil { - return errors.Wrap(err, "closing the output file") - } - - return nil -} - // CopyDir copies a directory from src to dest, recursively copying nested // directories func CopyDir(src, dest string) error { @@ -167,3 +129,41 @@ func DoAuthorizedReq(ctx infra.DnoteCtx, apiKey, method, path, body string) (*ht return res, 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, "opening the input file") + } + defer in.Close() + + out, err := os.Create(dest) + if err != nil { + return errors.Wrap(err, "creating the output file") + } + + if _, err = io.Copy(out, in); err != nil { + return errors.Wrap(err, "copying the file content") + } + + if err = out.Sync(); err != nil { + return errors.Wrap(err, "flushing the output file to disk") + } + + fi, err := os.Stat(src) + if err != nil { + 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, "copying permission to the output file") + } + + // Close the output file + if err = out.Close(); err != nil { + return errors.Wrap(err, "closing the output file") + } + + return nil +}