mirror of
https://github.com/dnote/dnote
synced 2026-03-14 22:45:50 +01:00
Implement state-based sync (#144)
* Migrate uuids of books that already exist in server * Remove actions * Add dirty flag for notes and books * Drop actions table * Implement sync * Add debug * Update uuid after posting resources to the server * Fix dev script
This commit is contained in:
parent
44238a272f
commit
fa1da50fc5
52 changed files with 6558 additions and 1389 deletions
420
client/client.go
Normal file
420
client/client.go
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
// Package client provides interfaces for interacting with the Dnote server
|
||||
// and the data structures for responses
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/cli/infra"
|
||||
"github.com/dnote/cli/utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// GetSyncStateResp is the response get sync state endpoint
|
||||
type GetSyncStateResp struct {
|
||||
FullSyncBefore int `json:"full_sync_before"`
|
||||
MaxUSN int `json:"max_usn"`
|
||||
CurrentTime int64 `json:"current_time"`
|
||||
}
|
||||
|
||||
// GetSyncState gets the sync state response from the server
|
||||
func GetSyncState(apiKey string, ctx infra.DnoteCtx) (GetSyncStateResp, error) {
|
||||
var ret GetSyncStateResp
|
||||
|
||||
res, err := utils.DoAuthorizedReq(ctx, apiKey, "GET", "/v1/sync/state", "")
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "constructing http request")
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "reading the response body")
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(body, &ret); err != nil {
|
||||
return ret, errors.Wrap(err, "unmarshalling the payload")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// SyncFragNote represents a note in a sync fragment and contains only the necessary information
|
||||
// for the client to sync the note locally
|
||||
type SyncFragNote struct {
|
||||
UUID string `json:"uuid"`
|
||||
BookUUID string `json:"book_uuid"`
|
||||
USN int `json:"usn"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
EditedOn int64 `json:"edited_on"`
|
||||
Content string `json:"content"`
|
||||
Public bool `json:"public"`
|
||||
Deleted bool `json:"deleted"`
|
||||
}
|
||||
|
||||
// SyncFragBook represents a book in a sync fragment and contains only the necessary information
|
||||
// for the client to sync the note locally
|
||||
type SyncFragBook struct {
|
||||
UUID string `json:"uuid"`
|
||||
USN int `json:"usn"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
Label string `json:"label"`
|
||||
Deleted bool `json:"deleted"`
|
||||
}
|
||||
|
||||
// SyncFragment contains a piece of information about the server's state.
|
||||
type SyncFragment struct {
|
||||
FragMaxUSN int `json:"frag_max_usn"`
|
||||
UserMaxUSN int `json:"user_max_usn"`
|
||||
CurrentTime int64 `json:"current_time"`
|
||||
Notes []SyncFragNote `json:"notes"`
|
||||
Books []SyncFragBook `json:"books"`
|
||||
ExpungedNotes []string `json:"expunged_notes"`
|
||||
ExpungedBooks []string `json:"expunged_books"`
|
||||
}
|
||||
|
||||
// GetSyncFragmentResp is the response from the get sync fragment endpoint
|
||||
type GetSyncFragmentResp struct {
|
||||
Fragment SyncFragment `json:"fragment"`
|
||||
}
|
||||
|
||||
// GetSyncFragment gets a sync fragment response from the server
|
||||
func GetSyncFragment(ctx infra.DnoteCtx, apiKey string, afterUSN int) (GetSyncFragmentResp, error) {
|
||||
v := url.Values{}
|
||||
v.Set("after_usn", strconv.Itoa(afterUSN))
|
||||
queryStr := v.Encode()
|
||||
|
||||
path := fmt.Sprintf("/v1/sync/fragment?%s", queryStr)
|
||||
res, err := utils.DoAuthorizedReq(ctx, apiKey, "GET", path, "")
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return GetSyncFragmentResp{}, errors.Wrap(err, "reading the response body")
|
||||
}
|
||||
|
||||
var resp GetSyncFragmentResp
|
||||
if err = json.Unmarshal(body, &resp); err != nil {
|
||||
return resp, errors.Wrap(err, "unmarshalling the payload")
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// RespBook is the book in the response from the create book api
|
||||
type RespBook struct {
|
||||
ID int `json:"id"`
|
||||
UUID string `json:"uuid"`
|
||||
USN int `json:"usn"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// CreateBookPayload is a payload for creating a book
|
||||
type CreateBookPayload struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// CreateBookResp is the response from create book api
|
||||
type CreateBookResp struct {
|
||||
Book RespBook `json:"book"`
|
||||
}
|
||||
|
||||
// checkRespErr checks if the given http response indicates an error. It returns a boolean indicating
|
||||
// if the response is an error, and a decoded error message.
|
||||
func checkRespErr(res *http.Response) (bool, string, error) {
|
||||
if res.StatusCode < 400 {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return true, "", errors.Wrapf(err, "server responded with %d but could not read the response body", res.StatusCode)
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
message := fmt.Sprintf(`response %d "%s"`, res.StatusCode, strings.TrimRight(bodyStr, "\n"))
|
||||
return true, message, nil
|
||||
}
|
||||
|
||||
// CreateBook creates a new book in the server
|
||||
func CreateBook(ctx infra.DnoteCtx, apiKey, label string) (CreateBookResp, error) {
|
||||
payload := CreateBookPayload{
|
||||
Name: label,
|
||||
}
|
||||
b, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return CreateBookResp{}, errors.Wrap(err, "marshaling payload")
|
||||
}
|
||||
|
||||
res, err := utils.DoAuthorizedReq(ctx, apiKey, "POST", "/v1/books", string(b))
|
||||
if err != nil {
|
||||
return CreateBookResp{}, errors.Wrap(err, "posting a book to the server")
|
||||
}
|
||||
|
||||
ok, message, err := checkRespErr(res)
|
||||
if err != nil {
|
||||
return CreateBookResp{}, errors.Wrap(err, "checking repsonse error")
|
||||
}
|
||||
if ok {
|
||||
return CreateBookResp{}, errors.New(message)
|
||||
}
|
||||
|
||||
var resp CreateBookResp
|
||||
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
|
||||
return resp, errors.Wrap(err, "decoding response payload")
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type updateBookPayload struct {
|
||||
Name *string `json:"name"`
|
||||
}
|
||||
|
||||
// UpdateBookResp is the response from create book api
|
||||
type UpdateBookResp struct {
|
||||
Book RespBook `json:"book"`
|
||||
}
|
||||
|
||||
// UpdateBook updates a book in the server
|
||||
func UpdateBook(ctx infra.DnoteCtx, apiKey, label, uuid string) (UpdateBookResp, error) {
|
||||
payload := updateBookPayload{
|
||||
Name: &label,
|
||||
}
|
||||
b, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return UpdateBookResp{}, errors.Wrap(err, "marshaling payload")
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("/v1/books/%s", uuid)
|
||||
res, err := utils.DoAuthorizedReq(ctx, apiKey, "PATCH", endpoint, string(b))
|
||||
if err != nil {
|
||||
return UpdateBookResp{}, errors.Wrap(err, "posting a book to the server")
|
||||
}
|
||||
|
||||
ok, message, err := checkRespErr(res)
|
||||
if err != nil {
|
||||
return UpdateBookResp{}, errors.Wrap(err, "checking repsonse error")
|
||||
}
|
||||
if ok {
|
||||
return UpdateBookResp{}, errors.New(message)
|
||||
}
|
||||
|
||||
var resp UpdateBookResp
|
||||
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
|
||||
return resp, errors.Wrap(err, "decoding payload")
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DeleteBookResp is the response from create book api
|
||||
type DeleteBookResp struct {
|
||||
Status int `json:"status"`
|
||||
Book RespBook `json:"book"`
|
||||
}
|
||||
|
||||
// DeleteBook deletes a book in the server
|
||||
func DeleteBook(ctx infra.DnoteCtx, apiKey, uuid string) (DeleteBookResp, error) {
|
||||
endpoint := fmt.Sprintf("/v1/books/%s", uuid)
|
||||
res, err := utils.DoAuthorizedReq(ctx, apiKey, "DELETE", endpoint, "")
|
||||
if err != nil {
|
||||
return DeleteBookResp{}, errors.Wrap(err, "deleting a book in the server")
|
||||
}
|
||||
|
||||
ok, message, err := checkRespErr(res)
|
||||
if err != nil {
|
||||
return DeleteBookResp{}, errors.Wrap(err, "checking repsonse error")
|
||||
}
|
||||
if ok {
|
||||
return DeleteBookResp{}, errors.New(message)
|
||||
}
|
||||
|
||||
var resp DeleteBookResp
|
||||
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
|
||||
return resp, errors.Wrap(err, "decoding the response")
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// CreateNotePayload is a payload for creating a note
|
||||
type CreateNotePayload struct {
|
||||
BookUUID string `json:"book_uuid"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// CreateNoteResp is the response from create note endpoint
|
||||
type CreateNoteResp struct {
|
||||
Result RespNote `json:"result"`
|
||||
}
|
||||
|
||||
type respNoteBook struct {
|
||||
UUID string `json:"uuid"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
type respNoteUser struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// RespNote is a note in the response
|
||||
type RespNote struct {
|
||||
UUID string `json:"uuid"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Content string `json:"content"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
Public bool `json:"public"`
|
||||
USN int `json:"usn"`
|
||||
Book respNoteBook `json:"book"`
|
||||
User respNoteUser `json:"user"`
|
||||
}
|
||||
|
||||
// CreateNote creates a note in the server
|
||||
func CreateNote(ctx infra.DnoteCtx, apiKey, bookUUID, content string) (CreateNoteResp, error) {
|
||||
payload := CreateNotePayload{
|
||||
BookUUID: bookUUID,
|
||||
Content: content,
|
||||
}
|
||||
b, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return CreateNoteResp{}, errors.Wrap(err, "marshaling payload")
|
||||
}
|
||||
|
||||
res, err := utils.DoAuthorizedReq(ctx, apiKey, "POST", "/v1/notes", string(b))
|
||||
if err != nil {
|
||||
return CreateNoteResp{}, errors.Wrap(err, "posting a book to the server")
|
||||
}
|
||||
|
||||
ok, message, err := checkRespErr(res)
|
||||
if err != nil {
|
||||
return CreateNoteResp{}, errors.Wrap(err, "checking repsonse error")
|
||||
}
|
||||
if ok {
|
||||
return CreateNoteResp{}, errors.New(message)
|
||||
}
|
||||
|
||||
var resp CreateNoteResp
|
||||
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
|
||||
return CreateNoteResp{}, errors.Wrap(err, "decoding payload")
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type updateNotePayload struct {
|
||||
BookUUID *string `json:"book_uuid"`
|
||||
Content *string `json:"content"`
|
||||
Public *bool `json:"public"`
|
||||
}
|
||||
|
||||
// UpdateNoteResp is the response from create book api
|
||||
type UpdateNoteResp struct {
|
||||
Status int `json:"status"`
|
||||
Result RespNote `json:"result"`
|
||||
}
|
||||
|
||||
// UpdateNote updates a note in the server
|
||||
func UpdateNote(ctx infra.DnoteCtx, apiKey, uuid, bookUUID, content string, public bool) (UpdateNoteResp, error) {
|
||||
payload := updateNotePayload{
|
||||
BookUUID: &bookUUID,
|
||||
Content: &content,
|
||||
Public: &public,
|
||||
}
|
||||
b, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return UpdateNoteResp{}, errors.Wrap(err, "marshaling payload")
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("/v1/notes/%s", uuid)
|
||||
res, err := utils.DoAuthorizedReq(ctx, apiKey, "PATCH", endpoint, string(b))
|
||||
if err != nil {
|
||||
return UpdateNoteResp{}, errors.Wrap(err, "patching a note to the server")
|
||||
}
|
||||
|
||||
ok, message, err := checkRespErr(res)
|
||||
if err != nil {
|
||||
return UpdateNoteResp{}, errors.Wrap(err, "checking repsonse error")
|
||||
}
|
||||
if ok {
|
||||
return UpdateNoteResp{}, errors.New(message)
|
||||
}
|
||||
|
||||
var resp UpdateNoteResp
|
||||
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
|
||||
return UpdateNoteResp{}, errors.Wrap(err, "decoding payload")
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DeleteNoteResp is the response from remove note api
|
||||
type DeleteNoteResp struct {
|
||||
Status int `json:"status"`
|
||||
Result RespNote `json:"result"`
|
||||
}
|
||||
|
||||
// DeleteNote removes a note in the server
|
||||
func DeleteNote(ctx infra.DnoteCtx, apiKey, uuid string) (DeleteNoteResp, error) {
|
||||
endpoint := fmt.Sprintf("/v1/notes/%s", uuid)
|
||||
res, err := utils.DoAuthorizedReq(ctx, apiKey, "DELETE", endpoint, "")
|
||||
if err != nil {
|
||||
return DeleteNoteResp{}, errors.Wrap(err, "patching a note to the server")
|
||||
}
|
||||
|
||||
ok, message, err := checkRespErr(res)
|
||||
if err != nil {
|
||||
return DeleteNoteResp{}, errors.Wrap(err, "checking repsonse error")
|
||||
}
|
||||
if ok {
|
||||
return DeleteNoteResp{}, errors.New(message)
|
||||
}
|
||||
|
||||
var resp DeleteNoteResp
|
||||
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
|
||||
return DeleteNoteResp{}, errors.Wrap(err, "decoding payload")
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetBooksResp is a response from get books endpoint
|
||||
type GetBooksResp []struct {
|
||||
UUID string `json:"uuid"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// GetBooks gets books from the server
|
||||
func GetBooks(ctx infra.DnoteCtx, apiKey string) (GetBooksResp, error) {
|
||||
res, err := utils.DoAuthorizedReq(ctx, apiKey, "GET", "/v1/books", "")
|
||||
if err != nil {
|
||||
return GetBooksResp{}, errors.Wrap(err, "making http request")
|
||||
}
|
||||
|
||||
ok, message, err := checkRespErr(res)
|
||||
if err != nil {
|
||||
return GetBooksResp{}, errors.Wrap(err, "checking repsonse error")
|
||||
}
|
||||
if ok {
|
||||
return GetBooksResp{}, errors.New(message)
|
||||
}
|
||||
|
||||
var resp GetBooksResp
|
||||
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
|
||||
return GetBooksResp{}, errors.Wrap(err, "decoding payload")
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var reservedBookNames = []string{"trash", "conflicts"}
|
||||
|
||||
var content string
|
||||
|
||||
var example = `
|
||||
|
|
@ -47,10 +49,24 @@ func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func isReservedName(name string) bool {
|
||||
for _, n := range reservedBookNames {
|
||||
if name == n {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func newRun(ctx infra.DnoteCtx) core.RunEFunc {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
bookName := args[0]
|
||||
|
||||
if isReservedName(bookName) {
|
||||
return errors.Errorf("book name '%s' is reserved", bookName)
|
||||
}
|
||||
|
||||
if content == "" {
|
||||
fpath := core.GetDnoteTmpContentPath(ctx)
|
||||
err := core.GetEditorInput(ctx, fpath, &content)
|
||||
|
|
@ -92,33 +108,25 @@ func writeNote(ctx infra.DnoteCtx, bookLabel string, content string, ts int64) e
|
|||
err = tx.QueryRow("SELECT uuid FROM books WHERE label = ?", bookLabel).Scan(&bookUUID)
|
||||
if err == sql.ErrNoRows {
|
||||
bookUUID = utils.GenerateUUID()
|
||||
_, err = tx.Exec("INSERT INTO books (uuid, label) VALUES (?, ?)", bookUUID, bookLabel)
|
||||
|
||||
b := core.NewBook(bookUUID, bookLabel, 0, false, true)
|
||||
err = b.Insert(tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "creating the book")
|
||||
}
|
||||
|
||||
err = core.LogActionAddBook(tx, bookLabel)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "logging action")
|
||||
}
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "finding the book")
|
||||
}
|
||||
|
||||
noteUUID := utils.GenerateUUID()
|
||||
_, err = tx.Exec(`INSERT INTO notes (uuid, book_uuid, content, added_on, public)
|
||||
VALUES (?, ?, ?, ?, ?);`, noteUUID, bookUUID, content, ts, false)
|
||||
n := core.NewNote(noteUUID, bookUUID, content, ts, 0, 0, false, false, true)
|
||||
|
||||
err = n.Insert(tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "creating the note")
|
||||
}
|
||||
err = core.LogActionAddNote(tx, noteUUID, bookLabel, content, ts)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "logging action")
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
|
|
|
|||
|
|
@ -91,20 +91,15 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "beginning a transaction")
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`UPDATE notes
|
||||
SET content = ?, edited_on = ?
|
||||
WHERE id = ? AND book_uuid = ?`, newContent, ts, noteID, bookUUID)
|
||||
SET content = ?, edited_on = ?, dirty = ?
|
||||
WHERE id = ? AND book_uuid = ?`, newContent, ts, true, noteID, bookUUID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "updating the note")
|
||||
}
|
||||
|
||||
err = core.LogActionEditNote(tx, noteUUID, bookLabel, newContent, ts)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "logging an action")
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
log.Success("edited the note\n")
|
||||
|
|
|
|||
|
|
@ -95,12 +95,9 @@ func removeNote(ctx infra.DnoteCtx, noteID, bookLabel string) error {
|
|||
return errors.Wrap(err, "beginning a transaction")
|
||||
}
|
||||
|
||||
if _, err = tx.Exec("DELETE FROM notes WHERE uuid = ? AND book_uuid = ?", noteUUID, bookUUID); err != nil {
|
||||
if _, err = tx.Exec("UPDATE notes SET deleted = ?, dirty = ?, content = ? WHERE uuid = ? AND book_uuid = ?", true, true, "", noteUUID, bookUUID); err != nil {
|
||||
return errors.Wrap(err, "removing the note")
|
||||
}
|
||||
if err = core.LogActionRemoveNote(tx, noteUUID, bookLabel); err != nil {
|
||||
return errors.Wrap(err, "logging the remove_note action")
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
log.Successf("removed from %s\n", bookLabel)
|
||||
|
|
@ -130,15 +127,15 @@ func removeBook(ctx infra.DnoteCtx, bookLabel string) error {
|
|||
return errors.Wrap(err, "beginning a transaction")
|
||||
}
|
||||
|
||||
if _, err = tx.Exec("DELETE FROM notes WHERE book_uuid = ?", bookUUID); err != nil {
|
||||
if _, err = tx.Exec("UPDATE notes SET deleted = ?, dirty = ?, content = ? WHERE book_uuid = ?", true, true, "", bookUUID); err != nil {
|
||||
return errors.Wrap(err, "removing notes in the book")
|
||||
}
|
||||
if _, err = tx.Exec("DELETE FROM books WHERE uuid = ?", bookUUID); err != nil {
|
||||
|
||||
// override the label with a random string
|
||||
uniqLabel := utils.GenerateUUID()
|
||||
if _, err = tx.Exec("UPDATE books SET deleted = ?, dirty = ?, label = ? WHERE uuid = ?", true, true, uniqLabel, bookUUID); err != nil {
|
||||
return errors.Wrap(err, "removing the book")
|
||||
}
|
||||
if err = core.LogActionRemoveBook(tx, bookLabel); err != nil {
|
||||
return errors.Wrap(err, "loging the remove_book action")
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
|
|
|
|||
|
|
@ -34,14 +34,14 @@ func Prepare(ctx infra.DnoteCtx) error {
|
|||
if err := infra.InitDB(ctx); err != nil {
|
||||
return errors.Wrap(err, "initializing database")
|
||||
}
|
||||
if err := infra.InitSystem(ctx); err != nil {
|
||||
if err := core.InitSystem(ctx); err != nil {
|
||||
return errors.Wrap(err, "initializing system data")
|
||||
}
|
||||
|
||||
if err := migrate.Legacy(ctx); err != nil {
|
||||
return errors.Wrap(err, "running legacy migration")
|
||||
}
|
||||
if err := migrate.Run(ctx, migrate.LocalSequence); err != nil {
|
||||
if err := migrate.Run(ctx, migrate.LocalSequence, migrate.LocalMode); err != nil {
|
||||
return errors.Wrap(err, "running migration")
|
||||
}
|
||||
|
||||
|
|
|
|||
1029
cmd/sync/sync.go
1029
cmd/sync/sync.go
File diff suppressed because it is too large
Load diff
3001
cmd/sync/sync_test.go
Normal file
3001
cmd/sync/sync_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewCmd returns a new version command
|
||||
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "version",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ func preRun(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// NewCmd returns a new view command
|
||||
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "view <book name?> <note index?>",
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/actions"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// LogActionAddNote logs an action for adding a note
|
||||
func LogActionAddNote(tx *sql.Tx, noteUUID, bookName, content string, timestamp int64) error {
|
||||
b, err := json.Marshal(actions.AddNoteDataV2{
|
||||
NoteUUID: noteUUID,
|
||||
BookName: bookName,
|
||||
Content: content,
|
||||
// TODO: support adding a public note
|
||||
Public: false,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshalling data into JSON")
|
||||
}
|
||||
|
||||
if err := LogAction(tx, 2, actions.ActionAddNote, string(b), timestamp); err != nil {
|
||||
return errors.Wrapf(err, "logging action")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogActionRemoveNote logs an action for removing a book
|
||||
func LogActionRemoveNote(tx *sql.Tx, noteUUID, bookName string) error {
|
||||
b, err := json.Marshal(actions.RemoveNoteDataV2{
|
||||
NoteUUID: noteUUID,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshalling data into JSON")
|
||||
}
|
||||
|
||||
ts := time.Now().UnixNano()
|
||||
if err := LogAction(tx, 2, actions.ActionRemoveNote, string(b), ts); err != nil {
|
||||
return errors.Wrapf(err, "logging action")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogActionEditNote logs an action for editing a note
|
||||
func LogActionEditNote(tx *sql.Tx, noteUUID, bookName, content string, ts int64) error {
|
||||
b, err := json.Marshal(actions.EditNoteDataV3{
|
||||
NoteUUID: noteUUID,
|
||||
Content: &content,
|
||||
BookName: nil,
|
||||
Public: nil,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshalling data into JSON")
|
||||
}
|
||||
|
||||
if err := LogAction(tx, 3, actions.ActionEditNote, string(b), ts); err != nil {
|
||||
return errors.Wrapf(err, "logging action")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogActionAddBook logs an action for adding a book
|
||||
func LogActionAddBook(tx *sql.Tx, name string) error {
|
||||
b, err := json.Marshal(actions.AddBookDataV1{
|
||||
BookName: name,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshalling data into JSON")
|
||||
}
|
||||
|
||||
ts := time.Now().UnixNano()
|
||||
if err := LogAction(tx, 1, actions.ActionAddBook, string(b), ts); err != nil {
|
||||
return errors.Wrapf(err, "logging action")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogActionRemoveBook logs an action for removing book
|
||||
func LogActionRemoveBook(tx *sql.Tx, name string) error {
|
||||
b, err := json.Marshal(actions.RemoveBookDataV1{BookName: name})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshalling data into JSON")
|
||||
}
|
||||
|
||||
ts := time.Now().UnixNano()
|
||||
if err := LogAction(tx, 1, actions.ActionRemoveBook, string(b), ts); err != nil {
|
||||
return errors.Wrapf(err, "logging action")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/actions"
|
||||
"github.com/dnote/cli/testutils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestLogActionEditNote(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
// Execute
|
||||
db := ctx.DB
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
|
||||
if err := LogActionEditNote(tx, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "js", "updated content", 1536168581); err != nil {
|
||||
t.Fatalf("Failed to perform %s", err.Error())
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
var actionCount int
|
||||
if err := db.QueryRow("SELECT count(*) FROM actions;").Scan(&actionCount); err != nil {
|
||||
panic(errors.Wrap(err, "counting actions"))
|
||||
}
|
||||
var action actions.Action
|
||||
if err := db.QueryRow("SELECT uuid, schema, type, timestamp, data FROM actions").
|
||||
Scan(&action.UUID, &action.Schema, &action.Type, &action.Timestamp, &action.Data); err != nil {
|
||||
panic(errors.Wrap(err, "querying action"))
|
||||
}
|
||||
var actionData actions.EditNoteDataV3
|
||||
if err := json.Unmarshal(action.Data, &actionData); err != nil {
|
||||
panic(errors.Wrap(err, "unmarshalling action data"))
|
||||
}
|
||||
|
||||
if actionCount != 1 {
|
||||
t.Fatalf("action count mismatch. got %d", actionCount)
|
||||
}
|
||||
testutils.AssertNotEqual(t, action.UUID, "", "action uuid mismatch")
|
||||
testutils.AssertEqual(t, action.Schema, 3, "action schema mismatch")
|
||||
testutils.AssertEqual(t, action.Type, actions.ActionEditNote, "action type mismatch")
|
||||
testutils.AssertNotEqual(t, action.Timestamp, 0, "action timestamp mismatch")
|
||||
testutils.AssertEqual(t, actionData.NoteUUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "action data note_uuid mismatch")
|
||||
testutils.AssertEqual(t, *actionData.Content, "updated content", "action data content mismatch")
|
||||
testutils.AssertEqual(t, actionData.BookName, (*string)(nil), "action data book_name mismatch")
|
||||
testutils.AssertEqual(t, actionData.Public, (*bool)(nil), "action data public mismatch")
|
||||
}
|
||||
46
core/core.go
46
core/core.go
|
|
@ -6,7 +6,9 @@ import (
|
|||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/cli/infra"
|
||||
"github.com/dnote/cli/utils"
|
||||
|
|
@ -260,3 +262,47 @@ func GetEditorInput(ctx infra.DnoteCtx, fpath string, content *string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initSystemKV(tx *sql.Tx, key string, val string) error {
|
||||
var count int
|
||||
if err := tx.QueryRow("SELECT count(*) FROM system WHERE key = ?", key).Scan(&count); err != nil {
|
||||
return errors.Wrapf(err, "counting %s", key)
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := tx.Exec("INSERT INTO system (key, value) VALUES (?, ?)", key, val); err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrapf(err, "inserting %s %s", key, val)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitSystem inserts system data if missing
|
||||
func InitSystem(ctx infra.DnoteCtx) error {
|
||||
db := ctx.DB
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "beginning a transaction")
|
||||
}
|
||||
|
||||
nowStr := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
if err := initSystemKV(tx, infra.SystemLastUpgrade, nowStr); err != nil {
|
||||
return errors.Wrapf(err, "initializing system config for %s", infra.SystemLastUpgrade)
|
||||
}
|
||||
if err := initSystemKV(tx, infra.SystemLastMaxUSN, "0"); err != nil {
|
||||
return errors.Wrapf(err, "initializing system config for %s", infra.SystemLastMaxUSN)
|
||||
}
|
||||
if err := initSystemKV(tx, infra.SystemLastSyncAt, "0"); err != nil {
|
||||
return errors.Wrapf(err, "initializing system config for %s", infra.SystemLastSyncAt)
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
77
core/core_test.go
Normal file
77
core/core_test.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/cli/testutils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestInitSystemKV(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
|
||||
var originalCount int
|
||||
testutils.MustScan(t, "counting system configs", db.QueryRow("SELECT count(*) FROM system"), &originalCount)
|
||||
|
||||
// Execute
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
|
||||
if err := initSystemKV(tx, "testKey", "testVal"); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "executing"))
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
var count int
|
||||
testutils.MustScan(t, "counting system configs", db.QueryRow("SELECT count(*) FROM system"), &count)
|
||||
testutils.AssertEqual(t, count, originalCount+1, "system count mismatch")
|
||||
|
||||
var val string
|
||||
testutils.MustScan(t, "getting system value",
|
||||
db.QueryRow("SELECT value FROM system WHERE key = ?", "testKey"), &val)
|
||||
testutils.AssertEqual(t, val, "testVal", "system value mismatch")
|
||||
}
|
||||
|
||||
func TestInitSystemKV_existing(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
testutils.MustExec(t, "inserting a system config", db, "INSERT INTO system (key, value) VALUES (?, ?)", "testKey", "testVal")
|
||||
|
||||
var originalCount int
|
||||
testutils.MustScan(t, "counting system configs", db.QueryRow("SELECT count(*) FROM system"), &originalCount)
|
||||
|
||||
// Execute
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
|
||||
if err := initSystemKV(tx, "testKey", "newTestVal"); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "executing"))
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
var count int
|
||||
testutils.MustScan(t, "counting system configs", db.QueryRow("SELECT count(*) FROM system"), &count)
|
||||
testutils.AssertEqual(t, count, originalCount, "system count mismatch")
|
||||
|
||||
var val string
|
||||
testutils.MustScan(t, "getting system value",
|
||||
db.QueryRow("SELECT value FROM system WHERE key = ?", "testKey"), &val)
|
||||
testutils.AssertEqual(t, val, "testVal", "system value should not have been updated")
|
||||
}
|
||||
150
core/models.go
Normal file
150
core/models.go
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Book holds a metadata and its notes
|
||||
type Book struct {
|
||||
UUID string `json:"uuid"`
|
||||
Label string `json:"label"`
|
||||
USN int `json:"usn"`
|
||||
Notes []Note `json:"notes"`
|
||||
Deleted bool `json:"deleted"`
|
||||
Dirty bool `json:"dirty"`
|
||||
}
|
||||
|
||||
// Note represents a note
|
||||
type Note struct {
|
||||
UUID string `json:"uuid"`
|
||||
BookUUID string `json:"book_uuid"`
|
||||
Content string `json:"content"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
EditedOn int64 `json:"edited_on"`
|
||||
USN int `json:"usn"`
|
||||
Public bool `json:"public"`
|
||||
Deleted bool `json:"deleted"`
|
||||
Dirty bool `json:"dirty"`
|
||||
}
|
||||
|
||||
// NewNote constructs a note with the given data
|
||||
func NewNote(uuid, bookUUID, content string, addedOn, editedOn int64, usn int, public, deleted, dirty bool) Note {
|
||||
return Note{
|
||||
UUID: uuid,
|
||||
BookUUID: bookUUID,
|
||||
Content: content,
|
||||
AddedOn: addedOn,
|
||||
EditedOn: editedOn,
|
||||
USN: usn,
|
||||
Public: public,
|
||||
Deleted: deleted,
|
||||
Dirty: dirty,
|
||||
}
|
||||
}
|
||||
|
||||
// Insert inserts a new note
|
||||
func (n Note) Insert(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("INSERT INTO notes (uuid, book_uuid, content, added_on, edited_on, usn, public, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
n.UUID, n.BookUUID, n.Content, n.AddedOn, n.EditedOn, n.USN, n.Public, n.Deleted, n.Dirty)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "inserting note with uuid %s", n.UUID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update updates the note with the given data
|
||||
func (n Note) Update(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("UPDATE notes SET book_uuid = ?, content = ?, added_on = ?, edited_on = ?, usn = ?, public = ?, deleted = ?, dirty = ? WHERE uuid = ?",
|
||||
n.BookUUID, n.Content, n.AddedOn, n.EditedOn, n.USN, n.Public, n.Deleted, n.Dirty, n.UUID)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "updating the note with uuid %s", n.UUID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUUID updates the uuid of a book
|
||||
func (n *Note) UpdateUUID(tx *sql.Tx, newUUID string) error {
|
||||
_, err := tx.Exec("UPDATE notes SET uuid = ? WHERE uuid = ?", newUUID, n.UUID)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "updating note uuid from '%s' to '%s'", n.UUID, newUUID)
|
||||
}
|
||||
|
||||
n.UUID = newUUID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Expunge hard-deletes the note from the database
|
||||
func (n Note) Expunge(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("DELETE FROM notes WHERE uuid = ?", n.UUID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "expunging a note locally")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewBook constructs a book with the given data
|
||||
func NewBook(uuid, label string, usn int, deleted, dirty bool) Book {
|
||||
return Book{
|
||||
UUID: uuid,
|
||||
Label: label,
|
||||
USN: usn,
|
||||
Deleted: deleted,
|
||||
Dirty: dirty,
|
||||
}
|
||||
}
|
||||
|
||||
// Insert inserts a new book
|
||||
func (b Book) Insert(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("INSERT INTO books (uuid, label, usn, dirty, deleted) VALUES (?, ?, ?, ?, ?)",
|
||||
b.UUID, b.Label, b.USN, b.Dirty, b.Deleted)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "inserting book with uuid %s", b.UUID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update updates the book with the given data
|
||||
func (b Book) Update(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("UPDATE books SET label = ?, usn = ?, dirty = ?, deleted = ? WHERE uuid = ?",
|
||||
b.Label, b.USN, b.Dirty, b.Deleted, b.UUID)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "updating the book with uuid %s", b.UUID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUUID updates the uuid of a book
|
||||
func (b *Book) UpdateUUID(tx *sql.Tx, newUUID string) error {
|
||||
_, err := tx.Exec("UPDATE books SET uuid = ? WHERE uuid = ?", newUUID, b.UUID)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "updating book uuid from '%s' to '%s'", b.UUID, newUUID)
|
||||
}
|
||||
|
||||
b.UUID = newUUID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Expunge hard-deletes the book from the database
|
||||
func (b Book) Expunge(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("DELETE FROM books WHERE uuid = ?", b.UUID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "expunging a book locally")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
795
core/models_test.go
Normal file
795
core/models_test.go
Normal file
|
|
@ -0,0 +1,795 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/cli/testutils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestNewNote(t *testing.T) {
|
||||
testCases := []struct {
|
||||
uuid string
|
||||
bookUUID string
|
||||
content string
|
||||
addedOn int64
|
||||
editedOn int64
|
||||
usn int
|
||||
public bool
|
||||
deleted bool
|
||||
dirty bool
|
||||
}{
|
||||
{
|
||||
uuid: "n1-uuid",
|
||||
bookUUID: "b1-uuid",
|
||||
content: "n1-content",
|
||||
addedOn: 1542058875,
|
||||
editedOn: 0,
|
||||
usn: 0,
|
||||
public: false,
|
||||
deleted: false,
|
||||
dirty: false,
|
||||
},
|
||||
{
|
||||
uuid: "n2-uuid",
|
||||
bookUUID: "b2-uuid",
|
||||
content: "n2-content",
|
||||
addedOn: 1542058875,
|
||||
editedOn: 1542058876,
|
||||
usn: 1008,
|
||||
public: true,
|
||||
deleted: true,
|
||||
dirty: true,
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
got := NewNote(tc.uuid, tc.bookUUID, tc.content, tc.addedOn, tc.editedOn, tc.usn, tc.public, tc.deleted, tc.dirty)
|
||||
|
||||
testutils.AssertEqual(t, got.UUID, tc.uuid, fmt.Sprintf("UUID mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, got.BookUUID, tc.bookUUID, fmt.Sprintf("BookUUID mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, got.Content, tc.content, fmt.Sprintf("Content mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, got.AddedOn, tc.addedOn, fmt.Sprintf("AddedOn mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, got.EditedOn, tc.editedOn, fmt.Sprintf("EditedOn mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, got.USN, tc.usn, fmt.Sprintf("USN mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, got.Public, tc.public, fmt.Sprintf("Public mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, got.Deleted, tc.deleted, fmt.Sprintf("Deleted mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, got.Dirty, tc.dirty, fmt.Sprintf("Dirty mismatch for test case %d", idx))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoteInsert(t *testing.T) {
|
||||
testCases := []struct {
|
||||
uuid string
|
||||
bookUUID string
|
||||
content string
|
||||
addedOn int64
|
||||
editedOn int64
|
||||
usn int
|
||||
public bool
|
||||
deleted bool
|
||||
dirty bool
|
||||
}{
|
||||
{
|
||||
uuid: "n1-uuid",
|
||||
bookUUID: "b1-uuid",
|
||||
content: "n1-content",
|
||||
addedOn: 1542058875,
|
||||
editedOn: 0,
|
||||
usn: 0,
|
||||
public: false,
|
||||
deleted: false,
|
||||
dirty: false,
|
||||
},
|
||||
{
|
||||
uuid: "n2-uuid",
|
||||
bookUUID: "b2-uuid",
|
||||
content: "n2-content",
|
||||
addedOn: 1542058875,
|
||||
editedOn: 1542058876,
|
||||
usn: 1008,
|
||||
public: true,
|
||||
deleted: true,
|
||||
dirty: true,
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
func() {
|
||||
// Setup
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
n := Note{
|
||||
UUID: tc.uuid,
|
||||
BookUUID: tc.bookUUID,
|
||||
Content: tc.content,
|
||||
AddedOn: tc.addedOn,
|
||||
EditedOn: tc.editedOn,
|
||||
USN: tc.usn,
|
||||
Public: tc.public,
|
||||
Deleted: tc.deleted,
|
||||
Dirty: tc.dirty,
|
||||
}
|
||||
|
||||
// execute
|
||||
db := ctx.DB
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error())
|
||||
}
|
||||
|
||||
if err := n.Insert(tx); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error())
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// test
|
||||
var uuid, bookUUID, content string
|
||||
var addedOn, editedOn int64
|
||||
var usn int
|
||||
var public, deleted, dirty bool
|
||||
testutils.MustScan(t, "getting n1",
|
||||
db.QueryRow("SELECT uuid, book_uuid, content, added_on, edited_on, usn, public, deleted, dirty FROM notes WHERE uuid = ?", tc.uuid),
|
||||
&uuid, &bookUUID, &content, &addedOn, &editedOn, &usn, &public, &deleted, &dirty)
|
||||
|
||||
testutils.AssertEqual(t, uuid, tc.uuid, fmt.Sprintf("uuid mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, bookUUID, tc.bookUUID, fmt.Sprintf("bookUUID mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, content, tc.content, fmt.Sprintf("content mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, addedOn, tc.addedOn, fmt.Sprintf("addedOn mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, editedOn, tc.editedOn, fmt.Sprintf("editedOn mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, usn, tc.usn, fmt.Sprintf("usn mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, public, tc.public, fmt.Sprintf("public mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, deleted, tc.deleted, fmt.Sprintf("deleted mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, dirty, tc.dirty, fmt.Sprintf("dirty mismatch for test case %d", idx))
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoteUpdate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
uuid string
|
||||
bookUUID string
|
||||
content string
|
||||
addedOn int64
|
||||
editedOn int64
|
||||
usn int
|
||||
public bool
|
||||
deleted bool
|
||||
dirty bool
|
||||
newBookUUID string
|
||||
newContent string
|
||||
newEditedOn int64
|
||||
newUSN int
|
||||
newPublic bool
|
||||
newDeleted bool
|
||||
newDirty bool
|
||||
}{
|
||||
{
|
||||
uuid: "n1-uuid",
|
||||
bookUUID: "b1-uuid",
|
||||
content: "n1-content",
|
||||
addedOn: 1542058875,
|
||||
editedOn: 0,
|
||||
usn: 0,
|
||||
public: false,
|
||||
deleted: false,
|
||||
dirty: false,
|
||||
newBookUUID: "b1-uuid",
|
||||
newContent: "n1-content edited",
|
||||
newEditedOn: 1542058879,
|
||||
newUSN: 0,
|
||||
newPublic: false,
|
||||
newDeleted: false,
|
||||
newDirty: false,
|
||||
},
|
||||
{
|
||||
uuid: "n1-uuid",
|
||||
bookUUID: "b1-uuid",
|
||||
content: "n1-content",
|
||||
addedOn: 1542058875,
|
||||
editedOn: 0,
|
||||
usn: 0,
|
||||
public: false,
|
||||
deleted: false,
|
||||
dirty: true,
|
||||
newBookUUID: "b2-uuid",
|
||||
newContent: "n1-content",
|
||||
newEditedOn: 1542058879,
|
||||
newUSN: 0,
|
||||
newPublic: true,
|
||||
newDeleted: false,
|
||||
newDirty: false,
|
||||
},
|
||||
{
|
||||
uuid: "n1-uuid",
|
||||
bookUUID: "b1-uuid",
|
||||
content: "n1-content",
|
||||
addedOn: 1542058875,
|
||||
editedOn: 0,
|
||||
usn: 10,
|
||||
public: false,
|
||||
deleted: false,
|
||||
dirty: false,
|
||||
newBookUUID: "",
|
||||
newContent: "",
|
||||
newEditedOn: 1542058879,
|
||||
newUSN: 151,
|
||||
newPublic: false,
|
||||
newDeleted: true,
|
||||
newDirty: false,
|
||||
},
|
||||
{
|
||||
uuid: "n1-uuid",
|
||||
bookUUID: "b1-uuid",
|
||||
content: "n1-content",
|
||||
addedOn: 1542058875,
|
||||
editedOn: 0,
|
||||
usn: 0,
|
||||
public: false,
|
||||
deleted: false,
|
||||
dirty: false,
|
||||
newBookUUID: "",
|
||||
newContent: "",
|
||||
newEditedOn: 1542058879,
|
||||
newUSN: 15,
|
||||
newPublic: false,
|
||||
newDeleted: true,
|
||||
newDirty: false,
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
func() {
|
||||
// Setup
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
n1 := Note{
|
||||
UUID: tc.uuid,
|
||||
BookUUID: tc.bookUUID,
|
||||
Content: tc.content,
|
||||
AddedOn: tc.addedOn,
|
||||
EditedOn: tc.editedOn,
|
||||
USN: tc.usn,
|
||||
Public: tc.public,
|
||||
Deleted: tc.deleted,
|
||||
Dirty: tc.dirty,
|
||||
}
|
||||
n2 := Note{
|
||||
UUID: "n2-uuid",
|
||||
BookUUID: "b10-uuid",
|
||||
Content: "n2 content",
|
||||
AddedOn: 1542058875,
|
||||
EditedOn: 0,
|
||||
USN: 39,
|
||||
Public: false,
|
||||
Deleted: false,
|
||||
Dirty: false,
|
||||
}
|
||||
|
||||
db := ctx.DB
|
||||
testutils.MustExec(t, fmt.Sprintf("inserting n1 for test case %d", idx), db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, content, public, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", n1.UUID, n1.BookUUID, n1.USN, n1.AddedOn, n1.EditedOn, n1.Content, n1.Public, n1.Deleted, n1.Dirty)
|
||||
testutils.MustExec(t, fmt.Sprintf("inserting n2 for test case %d", idx), db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, content, public, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", n2.UUID, n2.BookUUID, n2.USN, n2.AddedOn, n2.EditedOn, n2.Content, n2.Public, n2.Deleted, n2.Dirty)
|
||||
|
||||
// execute
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error())
|
||||
}
|
||||
|
||||
n1.BookUUID = tc.newBookUUID
|
||||
n1.Content = tc.newContent
|
||||
n1.EditedOn = tc.newEditedOn
|
||||
n1.USN = tc.newUSN
|
||||
n1.Public = tc.newPublic
|
||||
n1.Deleted = tc.newDeleted
|
||||
n1.Dirty = tc.newDirty
|
||||
|
||||
if err := n1.Update(tx); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error())
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// test
|
||||
var n1Record, n2Record Note
|
||||
testutils.MustScan(t, "getting n1",
|
||||
db.QueryRow("SELECT uuid, book_uuid, content, added_on, edited_on, usn, public, deleted, dirty FROM notes WHERE uuid = ?", tc.uuid),
|
||||
&n1Record.UUID, &n1Record.BookUUID, &n1Record.Content, &n1Record.AddedOn, &n1Record.EditedOn, &n1Record.USN, &n1Record.Public, &n1Record.Deleted, &n1Record.Dirty)
|
||||
testutils.MustScan(t, "getting n2",
|
||||
db.QueryRow("SELECT uuid, book_uuid, content, added_on, edited_on, usn, public, deleted, dirty FROM notes WHERE uuid = ?", n2.UUID),
|
||||
&n2Record.UUID, &n2Record.BookUUID, &n2Record.Content, &n2Record.AddedOn, &n2Record.EditedOn, &n2Record.USN, &n2Record.Public, &n2Record.Deleted, &n2Record.Dirty)
|
||||
|
||||
testutils.AssertEqual(t, n1Record.UUID, n1.UUID, fmt.Sprintf("n1 uuid mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, n1Record.BookUUID, tc.newBookUUID, fmt.Sprintf("n1 bookUUID mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, n1Record.Content, tc.newContent, fmt.Sprintf("n1 content mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, n1Record.AddedOn, n1.AddedOn, fmt.Sprintf("n1 addedOn mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, n1Record.EditedOn, tc.newEditedOn, fmt.Sprintf("n1 editedOn mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, n1Record.USN, tc.newUSN, fmt.Sprintf("n1 usn mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, n1Record.Public, tc.newPublic, fmt.Sprintf("n1 public mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, n1Record.Deleted, tc.newDeleted, fmt.Sprintf("n1 deleted mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, n1Record.Dirty, tc.newDirty, fmt.Sprintf("n1 dirty mismatch for test case %d", idx))
|
||||
|
||||
testutils.AssertEqual(t, n2Record.UUID, n2.UUID, fmt.Sprintf("n2 uuid mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, n2Record.BookUUID, n2.BookUUID, fmt.Sprintf("n2 bookUUID mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, n2Record.Content, n2.Content, fmt.Sprintf("n2 content mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, n2Record.AddedOn, n2.AddedOn, fmt.Sprintf("n2 addedOn mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, n2Record.EditedOn, n2.EditedOn, fmt.Sprintf("n2 editedOn mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, n2Record.USN, n2.USN, fmt.Sprintf("n2 usn mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, n2Record.Public, n2.Public, fmt.Sprintf("n2 public mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, n2Record.Deleted, n2.Deleted, fmt.Sprintf("n2 deleted mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, n2Record.Dirty, n2.Dirty, fmt.Sprintf("n2 dirty mismatch for test case %d", idx))
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoteUpdateUUID(t *testing.T) {
|
||||
testCases := []struct {
|
||||
newUUID string
|
||||
}{
|
||||
{
|
||||
newUUID: "n1-new-uuid",
|
||||
},
|
||||
{
|
||||
newUUID: "n2-new-uuid",
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("testCase%d", idx), func(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
n1 := Note{
|
||||
UUID: "n1-uuid",
|
||||
BookUUID: "b1-uuid",
|
||||
AddedOn: 1542058874,
|
||||
Content: "n1-content",
|
||||
USN: 1,
|
||||
Deleted: true,
|
||||
Dirty: false,
|
||||
}
|
||||
n2 := Note{
|
||||
UUID: "n2-uuid",
|
||||
BookUUID: "b1-uuid",
|
||||
AddedOn: 1542058874,
|
||||
Content: "n2-content",
|
||||
USN: 1,
|
||||
Deleted: true,
|
||||
Dirty: false,
|
||||
}
|
||||
|
||||
db := ctx.DB
|
||||
testutils.MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, content, added_on, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", n1.UUID, n1.BookUUID, n1.Content, n1.AddedOn, n1.USN, n1.Deleted, n1.Dirty)
|
||||
testutils.MustExec(t, "inserting n2", db, "INSERT INTO notes (uuid, book_uuid, content, added_on, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", n2.UUID, n2.BookUUID, n2.Content, n2.AddedOn, n2.USN, n2.Deleted, n2.Dirty)
|
||||
|
||||
// execute
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf(errors.Wrap(err, "beginning a transaction").Error())
|
||||
}
|
||||
if err := n1.UpdateUUID(tx, tc.newUUID); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf(errors.Wrap(err, "executing").Error())
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// test
|
||||
var n1Record, n2Record Note
|
||||
testutils.MustScan(t, "getting n1",
|
||||
db.QueryRow("SELECT uuid, content, usn, deleted, dirty FROM notes WHERE content = ?", "n1-content"),
|
||||
&n1Record.UUID, &n1Record.Content, &n1Record.USN, &n1Record.Deleted, &n1Record.Dirty)
|
||||
testutils.MustScan(t, "getting n2",
|
||||
db.QueryRow("SELECT uuid, content, usn, deleted, dirty FROM notes WHERE content = ?", "n2-content"),
|
||||
&n2Record.UUID, &n2Record.Content, &n2Record.USN, &n2Record.Deleted, &n2Record.Dirty)
|
||||
|
||||
testutils.AssertEqual(t, n1.UUID, tc.newUUID, "n1 original reference uuid mismatch")
|
||||
testutils.AssertEqual(t, n1Record.UUID, tc.newUUID, "n1 uuid mismatch")
|
||||
testutils.AssertEqual(t, n2Record.UUID, n2.UUID, "n2 uuid mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoteExpunge(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
n1 := Note{
|
||||
UUID: "n1-uuid",
|
||||
BookUUID: "b9-uuid",
|
||||
Content: "n1 content",
|
||||
AddedOn: 1542058874,
|
||||
EditedOn: 0,
|
||||
USN: 22,
|
||||
Public: false,
|
||||
Deleted: false,
|
||||
Dirty: false,
|
||||
}
|
||||
n2 := Note{
|
||||
UUID: "n2-uuid",
|
||||
BookUUID: "b10-uuid",
|
||||
Content: "n2 content",
|
||||
AddedOn: 1542058875,
|
||||
EditedOn: 0,
|
||||
USN: 39,
|
||||
Public: false,
|
||||
Deleted: false,
|
||||
Dirty: false,
|
||||
}
|
||||
|
||||
db := ctx.DB
|
||||
testutils.MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, content, public, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", n1.UUID, n1.BookUUID, n1.USN, n1.AddedOn, n1.EditedOn, n1.Content, n1.Public, n1.Deleted, n1.Dirty)
|
||||
testutils.MustExec(t, "inserting n2", db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, content, public, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", n2.UUID, n2.BookUUID, n2.USN, n2.AddedOn, n2.EditedOn, n2.Content, n2.Public, n2.Deleted, n2.Dirty)
|
||||
|
||||
// execute
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf(errors.Wrap(err, "beginning a transaction").Error())
|
||||
}
|
||||
|
||||
if err := n1.Expunge(tx); 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.AssertEqualf(t, noteCount, 1, "note count mismatch")
|
||||
|
||||
var n2Record Note
|
||||
testutils.MustScan(t, "getting n2",
|
||||
db.QueryRow("SELECT uuid, book_uuid, content, added_on, edited_on, usn, public, deleted, dirty FROM notes WHERE uuid = ?", n2.UUID),
|
||||
&n2Record.UUID, &n2Record.BookUUID, &n2Record.Content, &n2Record.AddedOn, &n2Record.EditedOn, &n2Record.USN, &n2Record.Public, &n2Record.Deleted, &n2Record.Dirty)
|
||||
|
||||
testutils.AssertEqual(t, n2Record.UUID, n2.UUID, "n2 uuid mismatch")
|
||||
testutils.AssertEqual(t, n2Record.BookUUID, n2.BookUUID, "n2 bookUUID mismatch")
|
||||
testutils.AssertEqual(t, n2Record.Content, n2.Content, "n2 content mismatch")
|
||||
testutils.AssertEqual(t, n2Record.AddedOn, n2.AddedOn, "n2 addedOn mismatch")
|
||||
testutils.AssertEqual(t, n2Record.EditedOn, n2.EditedOn, "n2 editedOn mismatch")
|
||||
testutils.AssertEqual(t, n2Record.USN, n2.USN, "n2 usn mismatch")
|
||||
testutils.AssertEqual(t, n2Record.Public, n2.Public, "n2 public mismatch")
|
||||
testutils.AssertEqual(t, n2Record.Deleted, n2.Deleted, "n2 deleted mismatch")
|
||||
testutils.AssertEqual(t, n2Record.Dirty, n2.Dirty, "n2 dirty mismatch")
|
||||
}
|
||||
|
||||
func TestNewBook(t *testing.T) {
|
||||
testCases := []struct {
|
||||
uuid string
|
||||
label string
|
||||
usn int
|
||||
deleted bool
|
||||
dirty bool
|
||||
}{
|
||||
{
|
||||
uuid: "b1-uuid",
|
||||
label: "b1-label",
|
||||
usn: 0,
|
||||
deleted: false,
|
||||
dirty: false,
|
||||
},
|
||||
{
|
||||
uuid: "b2-uuid",
|
||||
label: "b2-label",
|
||||
usn: 1008,
|
||||
deleted: false,
|
||||
dirty: true,
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
got := NewBook(tc.uuid, tc.label, tc.usn, tc.deleted, tc.dirty)
|
||||
|
||||
testutils.AssertEqual(t, got.UUID, tc.uuid, fmt.Sprintf("UUID mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, got.Label, tc.label, fmt.Sprintf("Label mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, got.USN, tc.usn, fmt.Sprintf("USN mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, got.Deleted, tc.deleted, fmt.Sprintf("Deleted mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, got.Dirty, tc.dirty, fmt.Sprintf("Dirty mismatch for test case %d", idx))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBookInsert(t *testing.T) {
|
||||
testCases := []struct {
|
||||
uuid string
|
||||
label string
|
||||
usn int
|
||||
deleted bool
|
||||
dirty bool
|
||||
}{
|
||||
{
|
||||
uuid: "b1-uuid",
|
||||
label: "b1-label",
|
||||
usn: 10808,
|
||||
deleted: false,
|
||||
dirty: false,
|
||||
},
|
||||
{
|
||||
uuid: "b1-uuid",
|
||||
label: "b1-label",
|
||||
usn: 10808,
|
||||
deleted: false,
|
||||
dirty: true,
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
func() {
|
||||
// Setup
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
b := Book{
|
||||
UUID: tc.uuid,
|
||||
Label: tc.label,
|
||||
USN: tc.usn,
|
||||
Dirty: tc.dirty,
|
||||
Deleted: tc.deleted,
|
||||
}
|
||||
|
||||
// execute
|
||||
db := ctx.DB
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error())
|
||||
}
|
||||
|
||||
if err := b.Insert(tx); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error())
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// test
|
||||
var uuid, label string
|
||||
var usn int
|
||||
var deleted, dirty bool
|
||||
testutils.MustScan(t, "getting b1",
|
||||
db.QueryRow("SELECT uuid, label, usn, deleted, dirty FROM books WHERE uuid = ?", tc.uuid),
|
||||
&uuid, &label, &usn, &deleted, &dirty)
|
||||
|
||||
testutils.AssertEqual(t, uuid, tc.uuid, fmt.Sprintf("uuid mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, label, tc.label, fmt.Sprintf("label mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, usn, tc.usn, fmt.Sprintf("usn mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, deleted, tc.deleted, fmt.Sprintf("deleted mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, dirty, tc.dirty, fmt.Sprintf("dirty mismatch for test case %d", idx))
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBookUpdate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
uuid string
|
||||
label string
|
||||
usn int
|
||||
deleted bool
|
||||
dirty bool
|
||||
newLabel string
|
||||
newUSN int
|
||||
newDeleted bool
|
||||
newDirty bool
|
||||
}{
|
||||
{
|
||||
uuid: "b1-uuid",
|
||||
label: "b1-label",
|
||||
usn: 0,
|
||||
deleted: false,
|
||||
dirty: false,
|
||||
newLabel: "b1-label-edited",
|
||||
newUSN: 0,
|
||||
newDeleted: false,
|
||||
newDirty: true,
|
||||
},
|
||||
{
|
||||
uuid: "b1-uuid",
|
||||
label: "b1-label",
|
||||
usn: 0,
|
||||
deleted: false,
|
||||
dirty: false,
|
||||
newLabel: "",
|
||||
newUSN: 10,
|
||||
newDeleted: true,
|
||||
newDirty: false,
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
func() {
|
||||
// Setup
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
b1 := Book{
|
||||
UUID: "b1-uuid",
|
||||
Label: "b1-label",
|
||||
USN: 1,
|
||||
Deleted: true,
|
||||
Dirty: false,
|
||||
}
|
||||
b2 := Book{
|
||||
UUID: "b2-uuid",
|
||||
Label: "b2-label",
|
||||
USN: 1,
|
||||
Deleted: true,
|
||||
Dirty: false,
|
||||
}
|
||||
|
||||
db := ctx.DB
|
||||
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, b1.USN, b1.Deleted, b1.Dirty)
|
||||
testutils.MustExec(t, fmt.Sprintf("inserting b2 for test case %d", idx), db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", b2.UUID, b2.Label, b2.USN, b2.Deleted, b2.Dirty)
|
||||
|
||||
// execute
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error())
|
||||
}
|
||||
|
||||
b1.Label = tc.newLabel
|
||||
b1.USN = tc.newUSN
|
||||
b1.Deleted = tc.newDeleted
|
||||
b1.Dirty = tc.newDirty
|
||||
|
||||
if err := b1.Update(tx); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error())
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// test
|
||||
var b1Record, b2Record Book
|
||||
testutils.MustScan(t, "getting b1",
|
||||
db.QueryRow("SELECT uuid, label, usn, deleted, dirty FROM books WHERE uuid = ?", tc.uuid),
|
||||
&b1Record.UUID, &b1Record.Label, &b1Record.USN, &b1Record.Deleted, &b1Record.Dirty)
|
||||
testutils.MustScan(t, "getting b2",
|
||||
db.QueryRow("SELECT uuid, label, usn, deleted, dirty FROM books WHERE uuid = ?", b2.UUID),
|
||||
&b2Record.UUID, &b2Record.Label, &b2Record.USN, &b2Record.Deleted, &b2Record.Dirty)
|
||||
|
||||
testutils.AssertEqual(t, b1Record.UUID, b1.UUID, fmt.Sprintf("b1 uuid mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, b1Record.Label, tc.newLabel, fmt.Sprintf("b1 label mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, b1Record.USN, tc.newUSN, fmt.Sprintf("b1 usn mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, b1Record.Deleted, tc.newDeleted, fmt.Sprintf("b1 deleted mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, b1Record.Dirty, tc.newDirty, fmt.Sprintf("b1 dirty mismatch for test case %d", idx))
|
||||
|
||||
testutils.AssertEqual(t, b2Record.UUID, b2.UUID, fmt.Sprintf("b2 uuid mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, b2Record.Label, b2.Label, fmt.Sprintf("b2 label mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, b2Record.USN, b2.USN, fmt.Sprintf("b2 usn mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, b2Record.Deleted, b2.Deleted, fmt.Sprintf("b2 deleted mismatch for test case %d", idx))
|
||||
testutils.AssertEqual(t, b2Record.Dirty, b2.Dirty, fmt.Sprintf("b2 dirty mismatch for test case %d", idx))
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBookUpdateUUID(t *testing.T) {
|
||||
testCases := []struct {
|
||||
newUUID string
|
||||
}{
|
||||
{
|
||||
newUUID: "b1-new-uuid",
|
||||
},
|
||||
{
|
||||
newUUID: "b2-new-uuid",
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("testCase%d", idx), func(t *testing.T) {
|
||||
|
||||
// Setup
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
b1 := Book{
|
||||
UUID: "b1-uuid",
|
||||
Label: "b1-label",
|
||||
USN: 1,
|
||||
Deleted: true,
|
||||
Dirty: false,
|
||||
}
|
||||
b2 := Book{
|
||||
UUID: "b2-uuid",
|
||||
Label: "b2-label",
|
||||
USN: 1,
|
||||
Deleted: true,
|
||||
Dirty: false,
|
||||
}
|
||||
|
||||
db := ctx.DB
|
||||
testutils.MustExec(t, "inserting b1", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", b1.UUID, b1.Label, b1.USN, b1.Deleted, b1.Dirty)
|
||||
testutils.MustExec(t, "inserting b2", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", b2.UUID, b2.Label, b2.USN, b2.Deleted, b2.Dirty)
|
||||
|
||||
// execute
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf(errors.Wrap(err, "beginning a transaction").Error())
|
||||
}
|
||||
if err := b1.UpdateUUID(tx, tc.newUUID); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf(errors.Wrap(err, "executing").Error())
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// test
|
||||
var b1Record, b2Record Book
|
||||
testutils.MustScan(t, "getting b1",
|
||||
db.QueryRow("SELECT uuid, label, usn, deleted, dirty FROM books WHERE label = ?", "b1-label"),
|
||||
&b1Record.UUID, &b1Record.Label, &b1Record.USN, &b1Record.Deleted, &b1Record.Dirty)
|
||||
testutils.MustScan(t, "getting b2",
|
||||
db.QueryRow("SELECT uuid, label, usn, deleted, dirty FROM books WHERE label = ?", "b2-label"),
|
||||
&b2Record.UUID, &b2Record.Label, &b2Record.USN, &b2Record.Deleted, &b2Record.Dirty)
|
||||
|
||||
testutils.AssertEqual(t, b1.UUID, tc.newUUID, "b1 original reference uuid mismatch")
|
||||
testutils.AssertEqual(t, b1Record.UUID, tc.newUUID, "b1 uuid mismatch")
|
||||
testutils.AssertEqual(t, b2Record.UUID, b2.UUID, "b2 uuid mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBookExpunge(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
b1 := Book{
|
||||
UUID: "b1-uuid",
|
||||
Label: "b1-label",
|
||||
USN: 1,
|
||||
Deleted: true,
|
||||
Dirty: false,
|
||||
}
|
||||
b2 := Book{
|
||||
UUID: "b2-uuid",
|
||||
Label: "b2-label",
|
||||
USN: 1,
|
||||
Deleted: true,
|
||||
Dirty: false,
|
||||
}
|
||||
|
||||
db := ctx.DB
|
||||
testutils.MustExec(t, "inserting b1", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", b1.UUID, b1.Label, b1.USN, b1.Deleted, b1.Dirty)
|
||||
testutils.MustExec(t, "inserting b2", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", b2.UUID, b2.Label, b2.USN, b2.Deleted, b2.Dirty)
|
||||
|
||||
// execute
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf(errors.Wrap(err, "beginning a transaction").Error())
|
||||
}
|
||||
|
||||
if err := b1.Expunge(tx); 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.AssertEqualf(t, bookCount, 1, "book count mismatch")
|
||||
|
||||
var b2Record Book
|
||||
testutils.MustScan(t, "getting b2",
|
||||
db.QueryRow("SELECT uuid, label, usn, deleted, dirty FROM books WHERE uuid = ?", "b2-uuid"),
|
||||
&b2Record.UUID, &b2Record.Label, &b2Record.USN, &b2Record.Deleted, &b2Record.Dirty)
|
||||
|
||||
testutils.AssertEqual(t, b2Record.UUID, b2.UUID, "b2 uuid mismatch")
|
||||
testutils.AssertEqual(t, b2Record.Label, b2.Label, "b2 label mismatch")
|
||||
testutils.AssertEqual(t, b2Record.USN, b2.USN, "b2 usn mismatch")
|
||||
testutils.AssertEqual(t, b2Record.Deleted, b2.Deleted, "b2 deleted mismatch")
|
||||
testutils.AssertEqual(t, b2Record.Dirty, b2.Dirty, "b2 dirty mismatch")
|
||||
}
|
||||
257
core/reducer.go
257
core/reducer.go
|
|
@ -1,257 +0,0 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/dnote/actions"
|
||||
"github.com/dnote/cli/infra"
|
||||
"github.com/dnote/cli/log"
|
||||
"github.com/dnote/cli/utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ReduceAll reduces all actions
|
||||
func ReduceAll(ctx infra.DnoteCtx, tx *sql.Tx, actionSlice []actions.Action) error {
|
||||
for _, action := range actionSlice {
|
||||
if err := Reduce(ctx, tx, action); err != nil {
|
||||
return errors.Wrap(err, "reducing action")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reduce transitions the local dnote state by consuming the action returned
|
||||
// from the server
|
||||
func Reduce(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
|
||||
log.Debug("reducing %s. uuid: %s, schema: %d, timestamp: %d\n", action.Type, action.UUID, action.Schema, action.Timestamp)
|
||||
|
||||
var err error
|
||||
|
||||
switch action.Type {
|
||||
case actions.ActionAddNote:
|
||||
err = handleAddNote(ctx, tx, action)
|
||||
case actions.ActionRemoveNote:
|
||||
err = handleRemoveNote(ctx, tx, action)
|
||||
case actions.ActionEditNote:
|
||||
err = handleEditNote(ctx, tx, action)
|
||||
case actions.ActionAddBook:
|
||||
err = handleAddBook(ctx, tx, action)
|
||||
case actions.ActionRemoveBook:
|
||||
err = handleRemoveBook(ctx, tx, action)
|
||||
default:
|
||||
return errors.Errorf("Unsupported action %s", action.Type)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "reducing %s", action.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getBookUUIDWithTx(tx *sql.Tx, bookLabel string) (string, error) {
|
||||
var ret string
|
||||
err := tx.QueryRow("SELECT uuid FROM books WHERE label = ?", bookLabel).Scan(&ret)
|
||||
if err == sql.ErrNoRows {
|
||||
return ret, errors.Errorf("book '%s' not found", bookLabel)
|
||||
} else if err != nil {
|
||||
return ret, errors.Wrap(err, "querying the book")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func handleAddNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
|
||||
if action.Schema != 2 {
|
||||
return errors.Errorf("data schema '%d' not supported", action.Schema)
|
||||
}
|
||||
|
||||
var data actions.AddNoteDataV2
|
||||
if err := json.Unmarshal(action.Data, &data); err != nil {
|
||||
return errors.Wrap(err, "parsing the action data")
|
||||
}
|
||||
|
||||
log.Debug("data: %+v\n", data)
|
||||
|
||||
bookUUID, err := getBookUUIDWithTx(tx, data.BookName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting book uuid")
|
||||
}
|
||||
|
||||
var noteCount int
|
||||
if err := tx.
|
||||
QueryRow("SELECT count(uuid) FROM notes WHERE uuid = ? AND book_uuid = ?", data.NoteUUID, bookUUID).
|
||||
Scan(¬eCount); err != nil {
|
||||
return errors.Wrap(err, "counting note")
|
||||
}
|
||||
|
||||
if noteCount > 0 {
|
||||
// if a duplicate exists, it is because the same action has been previously synced to the server
|
||||
// but the client did not bring the bookmark up-to-date at the time because it had error reducing
|
||||
// the returned actions.
|
||||
// noop so that the client can update bookmark
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`INSERT INTO notes
|
||||
(uuid, book_uuid, content, added_on, public)
|
||||
VALUES (?, ?, ?, ?, ?)`, data.NoteUUID, bookUUID, data.Content, action.Timestamp, data.Public)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "inserting a note")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleRemoveNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
|
||||
if action.Schema != 2 {
|
||||
return errors.Errorf("data schema '%d' not supported", action.Schema)
|
||||
}
|
||||
|
||||
var data actions.RemoveNoteDataV2
|
||||
if err := json.Unmarshal(action.Data, &data); err != nil {
|
||||
return errors.Wrap(err, "parsing the action data")
|
||||
}
|
||||
|
||||
log.Debug("data: %+v\n", data)
|
||||
|
||||
_, err := tx.Exec("DELETE FROM notes WHERE uuid = ?", data.NoteUUID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "removing a note")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildEditNoteQuery(ctx infra.DnoteCtx, tx *sql.Tx, noteUUID string, ts int64, data actions.EditNoteDataV3) (string, []interface{}, error) {
|
||||
setTmpl := "edited_on = ?"
|
||||
queryArgs := []interface{}{ts}
|
||||
|
||||
if data.Content != nil {
|
||||
setTmpl = fmt.Sprintf("%s, content = ?", setTmpl)
|
||||
queryArgs = append(queryArgs, *data.Content)
|
||||
}
|
||||
if data.Public != nil {
|
||||
setTmpl = fmt.Sprintf("%s, public = ?", setTmpl)
|
||||
queryArgs = append(queryArgs, *data.Public)
|
||||
}
|
||||
if data.BookName != nil {
|
||||
setTmpl = fmt.Sprintf("%s, book_uuid = ?", setTmpl)
|
||||
|
||||
bookUUID, err := getBookUUIDWithTx(tx, *data.BookName)
|
||||
if err != nil {
|
||||
return setTmpl, queryArgs, errors.Wrap(err, "getting book uuid")
|
||||
}
|
||||
|
||||
queryArgs = append(queryArgs, bookUUID)
|
||||
}
|
||||
|
||||
queryTmpl := fmt.Sprintf("UPDATE notes SET %s WHERE uuid = ?", setTmpl)
|
||||
queryArgs = append(queryArgs, noteUUID)
|
||||
|
||||
return queryTmpl, queryArgs, nil
|
||||
}
|
||||
|
||||
func handleEditNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
|
||||
if action.Schema != 3 {
|
||||
return errors.Errorf("data schema '%d' not supported", action.Schema)
|
||||
}
|
||||
|
||||
var data actions.EditNoteDataV3
|
||||
err := json.Unmarshal(action.Data, &data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parsing the action data")
|
||||
}
|
||||
|
||||
log.Debug("data: %+v\n", data)
|
||||
|
||||
queryTmpl, queryArgs, err := buildEditNoteQuery(ctx, tx, data.NoteUUID, action.Timestamp, data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "building edit note query")
|
||||
}
|
||||
_, err = tx.Exec(queryTmpl, queryArgs...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "updating a note")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleAddBook(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
|
||||
if action.Schema != 1 {
|
||||
return errors.Errorf("data schema '%d' not supported", action.Schema)
|
||||
}
|
||||
|
||||
var data actions.AddBookDataV1
|
||||
err := json.Unmarshal(action.Data, &data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parsing the action data")
|
||||
}
|
||||
|
||||
log.Debug("data: %+v\n", data)
|
||||
|
||||
var bookCount int
|
||||
err = tx.QueryRow("SELECT count(uuid) FROM books WHERE label = ?", data.BookName).Scan(&bookCount)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "counting books")
|
||||
}
|
||||
|
||||
if bookCount > 0 {
|
||||
// If book already exists, another machine added a book with the same name.
|
||||
// noop
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO books (uuid, label) VALUES (?, ?)", utils.GenerateUUID(), data.BookName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "inserting a book")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleRemoveBook(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
|
||||
if action.Schema != 1 {
|
||||
return errors.Errorf("data schema '%d' not supported", action.Schema)
|
||||
}
|
||||
|
||||
var data actions.RemoveBookDataV1
|
||||
if err := json.Unmarshal(action.Data, &data); err != nil {
|
||||
return errors.Wrap(err, "parsing the action data")
|
||||
}
|
||||
|
||||
log.Debug("data: %+v\n", data)
|
||||
|
||||
var bookCount int
|
||||
if err := tx.
|
||||
QueryRow("SELECT count(uuid) FROM books WHERE label = ?", data.BookName).
|
||||
Scan(&bookCount); err != nil {
|
||||
return errors.Wrap(err, "counting note")
|
||||
}
|
||||
|
||||
if bookCount == 0 {
|
||||
// If book does not exist, another client added and removed the book, making the add_book action
|
||||
// obsolete. noop.
|
||||
return nil
|
||||
}
|
||||
|
||||
bookUUID, err := getBookUUIDWithTx(tx, data.BookName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting book uuid")
|
||||
}
|
||||
|
||||
_, err = tx.Exec("DELETE FROM notes WHERE book_uuid = ?", bookUUID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "removing notes")
|
||||
}
|
||||
|
||||
_, err = tx.Exec("DELETE FROM books WHERE uuid = ?", bookUUID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "removing a book")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,304 +0,0 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/actions"
|
||||
"github.com/dnote/cli/infra"
|
||||
"github.com/dnote/cli/testutils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestReduceAddNote(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.Setup1(t, ctx)
|
||||
|
||||
// Execute
|
||||
b, err := json.Marshal(&actions.AddNoteDataV2{
|
||||
Content: "new content",
|
||||
BookName: "js",
|
||||
NoteUUID: "06896551-8a06-4996-89cc-0d866308b0f6",
|
||||
Public: false,
|
||||
})
|
||||
action := actions.Action{
|
||||
Type: actions.ActionAddNote,
|
||||
Schema: 2,
|
||||
Data: b,
|
||||
Timestamp: 1517629805,
|
||||
}
|
||||
|
||||
db := ctx.DB
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
if err = Reduce(ctx, tx, action); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "processing action"))
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
var noteCount, jsNoteCount, linuxNoteCount int
|
||||
testutils.MustScan(t, "counting note", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
testutils.MustScan(t, "counting js note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
|
||||
testutils.MustScan(t, "counting linux note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
|
||||
testutils.AssertEqual(t, noteCount, 2, "notes length mismatch")
|
||||
testutils.AssertEqual(t, jsNoteCount, 2, "js notes length mismatch")
|
||||
testutils.AssertEqual(t, linuxNoteCount, 0, "linux notes length mismatch")
|
||||
|
||||
var existingNote, newNote infra.Note
|
||||
testutils.MustScan(t, "scanning existing note", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "43827b9a-c2b0-4c06-a290-97991c896653"), &existingNote.UUID, &existingNote.Content)
|
||||
testutils.MustScan(t, "scanning new note", db.QueryRow("SELECT uuid, content, added_on FROM notes WHERE uuid = ?", "06896551-8a06-4996-89cc-0d866308b0f6"), &newNote.UUID, &newNote.Content, &newNote.AddedOn)
|
||||
|
||||
testutils.AssertEqual(t, existingNote.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "existing note uuid mismatch")
|
||||
testutils.AssertEqual(t, existingNote.Content, "Booleans have toString()", "existing note content mismatch")
|
||||
testutils.AssertEqual(t, newNote.UUID, "06896551-8a06-4996-89cc-0d866308b0f6", "new note uuid mismatch")
|
||||
testutils.AssertEqual(t, newNote.Content, "new content", "new note content mismatch")
|
||||
testutils.AssertEqual(t, newNote.AddedOn, int64(1517629805), "new note added_on mismatch")
|
||||
}
|
||||
|
||||
func TestReduceRemoveNote(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.Setup2(t, ctx)
|
||||
|
||||
// Execute
|
||||
b, err := json.Marshal(&actions.RemoveNoteDataV2{
|
||||
NoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
|
||||
})
|
||||
action := actions.Action{
|
||||
Type: actions.ActionRemoveNote,
|
||||
Schema: 2,
|
||||
Data: b,
|
||||
Timestamp: 1517629805,
|
||||
}
|
||||
|
||||
db := ctx.DB
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
if err = Reduce(ctx, tx, action); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "processing action"))
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
var bookCount, noteCount, jsNoteCount, linuxNoteCount int
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting note", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
testutils.MustScan(t, "counting js note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
|
||||
testutils.MustScan(t, "counting linux note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
|
||||
|
||||
var n1, n2 infra.Note
|
||||
testutils.MustScan(t, "scanning note 1", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content)
|
||||
testutils.MustScan(t, "scanning note 2", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "3e065d55-6d47-42f2-a6bf-f5844130b2d2"), &n2.UUID, &n2.Content)
|
||||
|
||||
testutils.AssertEqual(t, bookCount, 2, "number of books mismatch")
|
||||
testutils.AssertEqual(t, jsNoteCount, 1, "target book notes length mismatch")
|
||||
testutils.AssertEqual(t, linuxNoteCount, 1, "other book notes length mismatch")
|
||||
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "remaining note content mismatch")
|
||||
testutils.AssertEqual(t, n2.UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, n2.Content, "wc -l to count words", "other book remaining note content mismatch")
|
||||
}
|
||||
|
||||
func TestReduceEditNote(t *testing.T) {
|
||||
testCases := []struct {
|
||||
data string
|
||||
expectedNoteUUID string
|
||||
expectedNoteBookUUID string
|
||||
expectedNoteContent string
|
||||
expectedNoteAddedOn int64
|
||||
expectedNoteEditedOn int64
|
||||
expectedNotePublic bool
|
||||
expectedJsNoteCount int
|
||||
expectedLinuxNoteCount int
|
||||
}{
|
||||
{
|
||||
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "content": "updated content"}`,
|
||||
expectedNoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
|
||||
expectedNoteBookUUID: "js-book-uuid",
|
||||
expectedNoteContent: "updated content",
|
||||
expectedNoteAddedOn: int64(1515199951),
|
||||
expectedNoteEditedOn: int64(1517629805),
|
||||
expectedNotePublic: false,
|
||||
expectedJsNoteCount: 2,
|
||||
expectedLinuxNoteCount: 1,
|
||||
},
|
||||
{
|
||||
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "public": true}`,
|
||||
expectedNoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
|
||||
expectedNoteBookUUID: "js-book-uuid",
|
||||
expectedNoteContent: "Date object implements mathematical comparisons",
|
||||
expectedNoteAddedOn: int64(1515199951),
|
||||
expectedNoteEditedOn: int64(1517629805),
|
||||
expectedNotePublic: true,
|
||||
expectedJsNoteCount: 2,
|
||||
expectedLinuxNoteCount: 1,
|
||||
},
|
||||
{
|
||||
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "book_name": "linux", "content": "updated content"}`,
|
||||
expectedNoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
|
||||
expectedNoteBookUUID: "linux-book-uuid",
|
||||
expectedNoteContent: "updated content",
|
||||
expectedNoteAddedOn: int64(1515199951),
|
||||
expectedNoteEditedOn: int64(1517629805),
|
||||
expectedNotePublic: false,
|
||||
expectedJsNoteCount: 1,
|
||||
expectedLinuxNoteCount: 2,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
// Setup
|
||||
func() {
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.Setup2(t, ctx)
|
||||
db := ctx.DB
|
||||
|
||||
// Execute
|
||||
action := actions.Action{
|
||||
Type: actions.ActionEditNote,
|
||||
Data: json.RawMessage(tc.data),
|
||||
Schema: 3,
|
||||
Timestamp: 1517629805,
|
||||
}
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
err = Reduce(ctx, tx, action)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "Failed to process action"))
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
var bookCount, noteCount, jsNoteCount, linuxNoteCount int
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting note", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
testutils.MustScan(t, "counting js note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
|
||||
testutils.MustScan(t, "counting linux note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
|
||||
|
||||
var n1, n2, n3 infra.Note
|
||||
testutils.MustScan(t, "scanning note 1", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content)
|
||||
testutils.MustScan(t, "scanning note 2", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "3e065d55-6d47-42f2-a6bf-f5844130b2d2"), &n2.UUID, &n2.Content)
|
||||
testutils.MustScan(t, "scanning note 2", db.QueryRow("SELECT uuid, content, added_on, edited_on, public FROM notes WHERE uuid = ?", "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f"), &n3.UUID, &n3.Content, &n3.AddedOn, &n3.EditedOn, &n3.Public)
|
||||
|
||||
testutils.AssertEqual(t, bookCount, 2, "number of books mismatch")
|
||||
testutils.AssertEqual(t, noteCount, 3, "number of notes mismatch")
|
||||
testutils.AssertEqual(t, jsNoteCount, tc.expectedJsNoteCount, "js book notes length mismatch")
|
||||
testutils.AssertEqual(t, linuxNoteCount, tc.expectedLinuxNoteCount, "linux book notes length mismatch")
|
||||
|
||||
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "n1 mismatch")
|
||||
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "n1 content mismatch")
|
||||
testutils.AssertEqual(t, n2.UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "n2 uuid mismatch")
|
||||
testutils.AssertEqual(t, n2.Content, "wc -l to count words", "n2 content mismatch")
|
||||
testutils.AssertEqual(t, n3.UUID, tc.expectedNoteUUID, "edited note uuid mismatch")
|
||||
testutils.AssertEqual(t, n3.Content, tc.expectedNoteContent, "edited note content mismatch")
|
||||
testutils.AssertEqual(t, n3.AddedOn, tc.expectedNoteAddedOn, "edited note added_on mismatch")
|
||||
testutils.AssertEqual(t, n3.EditedOn, tc.expectedNoteEditedOn, "edited note edited_on mismatch")
|
||||
testutils.AssertEqual(t, n3.Public, tc.expectedNotePublic, "edited note public mismatch")
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func TestReduceAddBook(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.Setup1(t, ctx)
|
||||
|
||||
// Execute
|
||||
b, err := json.Marshal(&actions.AddBookDataV1{BookName: "new_book"})
|
||||
action := actions.Action{
|
||||
Type: actions.ActionAddBook,
|
||||
Schema: 1,
|
||||
Data: b,
|
||||
Timestamp: 1517629805,
|
||||
}
|
||||
db := ctx.DB
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
if err = Reduce(ctx, tx, action); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "Failed to process action"))
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
var bookCount, newBookNoteCount int
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting note in the new book", db.QueryRow("SELECT count(*) FROM notes INNER JOIN books ON books.uuid = notes.book_uuid WHERE books.label = ?", "new_book"), &newBookNoteCount)
|
||||
|
||||
testutils.AssertEqual(t, bookCount, 3, "number of books mismatch")
|
||||
testutils.AssertEqual(t, newBookNoteCount, 0, "new book number of notes mismatch")
|
||||
}
|
||||
|
||||
func TestReduceRemoveBook(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.Setup2(t, ctx)
|
||||
|
||||
// Execute
|
||||
b, err := json.Marshal(&actions.RemoveBookDataV1{BookName: "linux"})
|
||||
action := actions.Action{
|
||||
Type: actions.ActionRemoveBook,
|
||||
Schema: 1,
|
||||
Data: b,
|
||||
Timestamp: 1517629805,
|
||||
}
|
||||
|
||||
db := ctx.DB
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
if err = Reduce(ctx, tx, action); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "Failed to process action"))
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
var bookCount, noteCount, jsNoteCount, linuxNoteCount int
|
||||
var jsBookLabel string
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting note", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
testutils.MustScan(t, "counting js note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
|
||||
testutils.MustScan(t, "counting linux note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
|
||||
testutils.MustScan(t, "scanning book", db.QueryRow("SELECT label FROM books WHERE uuid = ?", "js-book-uuid"), &jsBookLabel)
|
||||
|
||||
var n1, n2 infra.Note
|
||||
testutils.MustScan(t, "scanning note 1", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content)
|
||||
testutils.MustScan(t, "scanning note 2", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f"), &n2.UUID, &n2.Content)
|
||||
|
||||
testutils.AssertEqual(t, bookCount, 1, "number of books mismatch")
|
||||
testutils.AssertEqual(t, noteCount, 2, "number of notes mismatch")
|
||||
testutils.AssertEqual(t, jsNoteCount, 2, "js note count mismatch")
|
||||
testutils.AssertEqual(t, linuxNoteCount, 0, "linux note count mismatch")
|
||||
testutils.AssertEqual(t, jsBookLabel, "js", "remaining book name mismatch")
|
||||
|
||||
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "remaining note content mismatch")
|
||||
testutils.AssertEqual(t, n2.UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch")
|
||||
testutils.AssertEqual(t, n2.Content, "Date object implements mathematical comparisons", "edited note content mismatch")
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ func shouldCheckUpdate(ctx infra.DnoteCtx) (bool, error) {
|
|||
db := ctx.DB
|
||||
|
||||
var lastUpgrade int64
|
||||
err := db.QueryRow("SELECT value FROM system WHERE key = ?", "last_upgrade").Scan(&lastUpgrade)
|
||||
err := db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemLastUpgrade).Scan(&lastUpgrade)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "getting last_udpate")
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ func touchLastUpgrade(ctx infra.DnoteCtx) error {
|
|||
db := ctx.DB
|
||||
|
||||
now := time.Now().Unix()
|
||||
_, err := db.Exec("UPDATE system SET value = ? WHERE key = ?", now, "last_upgrade")
|
||||
_, err := db.Exec("UPDATE system SET value = ? WHERE key = ?", now, infra.SystemLastUpgrade)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "updating last_upgrade")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"time"
|
||||
|
||||
// use sqlite
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
|
@ -20,6 +19,14 @@ var (
|
|||
|
||||
// SystemSchema is the key for schema in the system table
|
||||
SystemSchema = "schema"
|
||||
// SystemRemoteSchema is the key for remote schema in the system table
|
||||
SystemRemoteSchema = "remote_schema"
|
||||
// SystemLastSyncAt is the timestamp of the server at the last sync
|
||||
SystemLastSyncAt = "last_sync_time"
|
||||
// SystemLastMaxUSN is the user's max_usn from the server at the alst sync
|
||||
SystemLastMaxUSN = "last_max_usn"
|
||||
// SystemLastUpgrade is the timestamp at which the system more recently checked for an upgrade
|
||||
SystemLastUpgrade = "last_upgrade"
|
||||
)
|
||||
|
||||
// DnoteCtx is a context holding the information of the current runtime
|
||||
|
|
@ -37,33 +44,6 @@ type Config struct {
|
|||
APIKey string
|
||||
}
|
||||
|
||||
// Dnote holds the whole dnote data
|
||||
type Dnote map[string]Book
|
||||
|
||||
// Book holds a metadata and its notes
|
||||
type Book struct {
|
||||
Name string `json:"name"`
|
||||
Notes []Note `json:"notes"`
|
||||
}
|
||||
|
||||
// Note represents a single microlesson
|
||||
type Note struct {
|
||||
UUID string `json:"uuid"`
|
||||
Content string `json:"content"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
EditedOn int64 `json:"edited_on"`
|
||||
Public bool `json:"public"`
|
||||
}
|
||||
|
||||
// Timestamp holds time information
|
||||
type Timestamp struct {
|
||||
LastUpgrade int64 `yaml:"last_upgrade"`
|
||||
// id of the most recent action synced from the server
|
||||
Bookmark int `yaml:"bookmark"`
|
||||
// timestamp of the most recent action performed by the cli
|
||||
LastAction int64 `yaml:"last_action"`
|
||||
}
|
||||
|
||||
// NewCtx returns a new dnote context
|
||||
func NewCtx(apiEndpoint, versionTag string) (DnoteCtx, error) {
|
||||
homeDir, err := getHomeDir()
|
||||
|
|
@ -145,6 +125,15 @@ func InitDB(ctx DnoteCtx) error {
|
|||
return errors.Wrap(err, "creating books table")
|
||||
}
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS system
|
||||
(
|
||||
key string NOT NULL,
|
||||
value text NOT NULL
|
||||
)`)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating system table")
|
||||
}
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS actions
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
|
|
@ -157,15 +146,6 @@ func InitDB(ctx DnoteCtx) error {
|
|||
return errors.Wrap(err, "creating actions table")
|
||||
}
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS system
|
||||
(
|
||||
key string NOT NULL,
|
||||
value text NOT NULL
|
||||
)`)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating system table")
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_books_label ON books(label);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_uuid ON notes(uuid);
|
||||
|
|
@ -178,43 +158,3 @@ func InitDB(ctx DnoteCtx) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitSystem inserts system data if missing
|
||||
func InitSystem(ctx DnoteCtx) error {
|
||||
db := ctx.DB
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "beginning a transaction")
|
||||
}
|
||||
|
||||
var bookmarkCount, lastUpgradeCount int
|
||||
if err := db.QueryRow("SELECT count(*) FROM system WHERE key = ?", "bookmark").
|
||||
Scan(&bookmarkCount); err != nil {
|
||||
return errors.Wrap(err, "counting bookmarks")
|
||||
}
|
||||
if bookmarkCount == 0 {
|
||||
_, err := tx.Exec("INSERT INTO system (key, value) VALUES (?, ?)", "bookmark", 0)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "inserting bookmark")
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.QueryRow("SELECT count(*) FROM system WHERE key = ?", "last_upgrade").
|
||||
Scan(&lastUpgradeCount); err != nil {
|
||||
return errors.Wrap(err, "counting last_upgrade")
|
||||
}
|
||||
if lastUpgradeCount == 0 {
|
||||
now := time.Now().Unix()
|
||||
_, err := tx.Exec("INSERT INTO system (key, value) VALUES (?, ?)", "last_upgrade", now)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "inserting bookmark")
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
272
main_test.go
272
main_test.go
|
|
@ -1,7 +1,6 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
|
@ -10,7 +9,6 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/dnote/actions"
|
||||
"github.com/dnote/cli/core"
|
||||
"github.com/dnote/cli/infra"
|
||||
"github.com/dnote/cli/testutils"
|
||||
|
|
@ -30,7 +28,7 @@ func TestMain(m *testing.M) {
|
|||
|
||||
func TestInit(t *testing.T) {
|
||||
// Set up
|
||||
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "./tmp", "./testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
// Execute
|
||||
|
|
@ -46,25 +44,35 @@ func TestInit(t *testing.T) {
|
|||
|
||||
db := ctx.DB
|
||||
|
||||
var notesTableCount, booksTableCount, actionsTableCount, systemTableCount int
|
||||
var notesTableCount, booksTableCount, systemTableCount int
|
||||
testutils.MustScan(t, "counting notes",
|
||||
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type = ? AND name = ?", "table", "notes"), ¬esTableCount)
|
||||
testutils.MustScan(t, "counting books",
|
||||
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type = ? AND name = ?", "table", "books"), &booksTableCount)
|
||||
testutils.MustScan(t, "counting actions",
|
||||
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type = ? AND name = ?", "table", "actions"), &actionsTableCount)
|
||||
testutils.MustScan(t, "counting system",
|
||||
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type = ? AND name = ?", "table", "system"), &systemTableCount)
|
||||
|
||||
testutils.AssertEqual(t, notesTableCount, 1, "notes table count mismatch")
|
||||
testutils.AssertEqual(t, booksTableCount, 1, "books table count mismatch")
|
||||
testutils.AssertEqual(t, actionsTableCount, 1, "actions table count mismatch")
|
||||
testutils.AssertEqual(t, systemTableCount, 1, "system table count mismatch")
|
||||
|
||||
// test that all default system configurations are generated
|
||||
var lastUpgrade, lastMaxUSN, lastSyncAt string
|
||||
testutils.MustScan(t, "scanning last upgrade",
|
||||
db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemLastUpgrade), &lastUpgrade)
|
||||
testutils.MustScan(t, "scanning last max usn",
|
||||
db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemLastMaxUSN), &lastMaxUSN)
|
||||
testutils.MustScan(t, "scanning last sync at",
|
||||
db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemLastSyncAt), &lastSyncAt)
|
||||
|
||||
testutils.AssertNotEqual(t, lastUpgrade, "", "last upgrade should not be empty")
|
||||
testutils.AssertNotEqual(t, lastMaxUSN, "", "last max usn should not be empty")
|
||||
testutils.AssertNotEqual(t, lastSyncAt, "", "last sync at should not be empty")
|
||||
}
|
||||
|
||||
func TestAddNote_NewBook_ContentFlag(t *testing.T) {
|
||||
// Set up
|
||||
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "./tmp", "./testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
// Execute
|
||||
|
|
@ -73,49 +81,30 @@ func TestAddNote_NewBook_ContentFlag(t *testing.T) {
|
|||
// Test
|
||||
db := ctx.DB
|
||||
|
||||
var actionCount, noteCount, bookCount int
|
||||
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
|
||||
var noteCount, bookCount int
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
|
||||
testutils.AssertEqualf(t, actionCount, 2, "action count mismatch")
|
||||
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
|
||||
testutils.AssertEqualf(t, noteCount, 1, "note count mismatch")
|
||||
|
||||
var jsBookUUID string
|
||||
testutils.MustScan(t, "getting js book uuid", db.QueryRow("SELECT uuid FROM books where label = ?", "js"), &jsBookUUID)
|
||||
var note infra.Note
|
||||
var book core.Book
|
||||
testutils.MustScan(t, "getting book", db.QueryRow("SELECT uuid, dirty FROM books where label = ?", "js"), &book.UUID, &book.Dirty)
|
||||
var note core.Note
|
||||
testutils.MustScan(t, "getting note",
|
||||
db.QueryRow("SELECT uuid, content, added_on FROM notes where book_uuid = ?", jsBookUUID), ¬e.UUID, ¬e.Content, ¬e.AddedOn)
|
||||
var bookAction, noteAction actions.Action
|
||||
testutils.MustScan(t, "getting book action",
|
||||
db.QueryRow("SELECT data, timestamp FROM actions where type = ?", actions.ActionAddBook), &bookAction.Data, &bookAction.Timestamp)
|
||||
testutils.MustScan(t, "getting note action",
|
||||
db.QueryRow("SELECT data, timestamp FROM actions where type = ?", actions.ActionAddNote), ¬eAction.Data, ¬eAction.Timestamp)
|
||||
db.QueryRow("SELECT uuid, content, added_on, dirty FROM notes where book_uuid = ?", book.UUID), ¬e.UUID, ¬e.Content, ¬e.AddedOn, ¬e.Dirty)
|
||||
|
||||
var noteActionData actions.AddNoteDataV1
|
||||
var bookActionData actions.AddBookDataV1
|
||||
if err := json.Unmarshal(bookAction.Data, &bookActionData); err != nil {
|
||||
log.Fatalf("unmarshalling the action data: %s", err)
|
||||
}
|
||||
if err := json.Unmarshal(noteAction.Data, ¬eActionData); err != nil {
|
||||
log.Fatalf("unmarshalling the action data: %s", err)
|
||||
}
|
||||
testutils.AssertEqual(t, book.Dirty, true, "Book dirty mismatch")
|
||||
|
||||
testutils.AssertNotEqual(t, bookActionData.BookName, "", "bookAction data note_uuid mismatch")
|
||||
testutils.AssertNotEqual(t, bookAction.Timestamp, 0, "bookAction timestamp mismatch")
|
||||
testutils.AssertEqual(t, noteActionData.Content, "foo", "noteAction data name mismatch")
|
||||
testutils.AssertNotEqual(t, noteActionData.NoteUUID, nil, "noteAction data note_uuid mismatch")
|
||||
testutils.AssertNotEqual(t, noteActionData.BookName, "", "noteAction data note_uuid mismatch")
|
||||
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "noteAction timestamp mismatch")
|
||||
testutils.AssertNotEqual(t, note.UUID, "", "Note should have UUID")
|
||||
testutils.AssertEqual(t, note.Content, "foo", "Note content mismatch")
|
||||
testutils.AssertEqual(t, note.Dirty, true, "Note dirty mismatch")
|
||||
testutils.AssertNotEqual(t, note.AddedOn, int64(0), "Note added_on mismatch")
|
||||
}
|
||||
|
||||
func TestAddNote_ExistingBook_ContentFlag(t *testing.T) {
|
||||
// Set up
|
||||
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "./tmp", "./testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.Setup3(t, ctx)
|
||||
|
|
@ -126,43 +115,37 @@ func TestAddNote_ExistingBook_ContentFlag(t *testing.T) {
|
|||
// Test
|
||||
db := ctx.DB
|
||||
|
||||
var actionCount, noteCount, bookCount int
|
||||
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
|
||||
var noteCount, bookCount int
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
|
||||
testutils.AssertEqualf(t, actionCount, 1, "action count mismatch")
|
||||
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
|
||||
testutils.AssertEqualf(t, noteCount, 2, "note count mismatch")
|
||||
|
||||
var n1, n2 infra.Note
|
||||
var n1, n2 core.Note
|
||||
testutils.MustScan(t, "getting n1",
|
||||
db.QueryRow("SELECT uuid, content, added_on FROM notes WHERE book_uuid = ? AND uuid = ?", "js-book-uuid", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content, &n1.AddedOn)
|
||||
db.QueryRow("SELECT uuid, content, added_on, dirty FROM notes WHERE book_uuid = ? AND uuid = ?", "js-book-uuid", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content, &n1.AddedOn, &n1.Dirty)
|
||||
testutils.MustScan(t, "getting n2",
|
||||
db.QueryRow("SELECT uuid, content, added_on FROM notes WHERE book_uuid = ? AND content = ?", "js-book-uuid", "foo"), &n2.UUID, &n2.Content, &n2.AddedOn)
|
||||
var noteAction actions.Action
|
||||
testutils.MustScan(t, "getting note action",
|
||||
db.QueryRow("SELECT data, timestamp FROM actions WHERE type = ?", actions.ActionAddNote), ¬eAction.Data, ¬eAction.Timestamp)
|
||||
db.QueryRow("SELECT uuid, content, added_on, dirty FROM notes WHERE book_uuid = ? AND content = ?", "js-book-uuid", "foo"), &n2.UUID, &n2.Content, &n2.AddedOn, &n2.Dirty)
|
||||
|
||||
var noteActionData actions.AddNoteDataV1
|
||||
if err := json.Unmarshal(noteAction.Data, ¬eActionData); err != nil {
|
||||
log.Fatalf("unmarshalling the action data: %s", err)
|
||||
}
|
||||
var book core.Book
|
||||
testutils.MustScan(t, "getting book", db.QueryRow("SELECT dirty FROM books where label = ?", "js"), &book.Dirty)
|
||||
|
||||
testutils.AssertEqual(t, noteActionData.Content, "foo", "action data name mismatch")
|
||||
testutils.AssertNotEqual(t, noteActionData.NoteUUID, "", "action data note_uuid mismatch")
|
||||
testutils.AssertEqual(t, noteActionData.BookName, "js", "action data book_name mismatch")
|
||||
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "action timestamp mismatch")
|
||||
testutils.AssertNotEqual(t, n1.UUID, "", "Note should have UUID")
|
||||
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "Note content mismatch")
|
||||
testutils.AssertEqual(t, n1.AddedOn, int64(1515199943), "Note added_on mismatch")
|
||||
testutils.AssertNotEqual(t, n2.UUID, "", "Note should have UUID")
|
||||
testutils.AssertEqual(t, n2.Content, "foo", "Note content mismatch")
|
||||
testutils.AssertEqual(t, book.Dirty, false, "Book dirty mismatch")
|
||||
|
||||
testutils.AssertNotEqual(t, n1.UUID, "", "n1 should have UUID")
|
||||
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "n1 content mismatch")
|
||||
testutils.AssertEqual(t, n1.AddedOn, int64(1515199943), "n1 added_on mismatch")
|
||||
testutils.AssertEqual(t, n1.Dirty, false, "n1 dirty mismatch")
|
||||
|
||||
testutils.AssertNotEqual(t, n2.UUID, "", "n2 should have UUID")
|
||||
testutils.AssertEqual(t, n2.Content, "foo", "n2 content mismatch")
|
||||
testutils.AssertEqual(t, n2.Dirty, true, "n2 dirty mismatch")
|
||||
}
|
||||
|
||||
func TestEditNote_ContentFlag(t *testing.T) {
|
||||
// Set up
|
||||
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "./tmp", "./testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.Setup4(t, ctx)
|
||||
|
|
@ -173,46 +156,32 @@ func TestEditNote_ContentFlag(t *testing.T) {
|
|||
// Test
|
||||
db := ctx.DB
|
||||
|
||||
var actionCount, noteCount, bookCount int
|
||||
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
|
||||
var noteCount, bookCount int
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
|
||||
testutils.AssertEqualf(t, actionCount, 1, "action count mismatch")
|
||||
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
|
||||
testutils.AssertEqualf(t, noteCount, 2, "note count mismatch")
|
||||
|
||||
var n1, n2 infra.Note
|
||||
var n1, n2 core.Note
|
||||
testutils.MustScan(t, "getting n1",
|
||||
db.QueryRow("SELECT uuid, content, added_on FROM notes where book_uuid = ? AND uuid = ?", "js-book-uuid", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content, &n1.AddedOn)
|
||||
db.QueryRow("SELECT uuid, content, added_on, dirty FROM notes where book_uuid = ? AND uuid = ?", "js-book-uuid", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content, &n1.AddedOn, &n1.Dirty)
|
||||
testutils.MustScan(t, "getting n2",
|
||||
db.QueryRow("SELECT uuid, content, added_on FROM notes where book_uuid = ? AND uuid = ?", "js-book-uuid", "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f"), &n2.UUID, &n2.Content, &n2.AddedOn)
|
||||
var noteAction actions.Action
|
||||
testutils.MustScan(t, "getting note action",
|
||||
db.QueryRow("SELECT data, type, schema FROM actions where type = ?", actions.ActionEditNote),
|
||||
¬eAction.Data, ¬eAction.Type, ¬eAction.Schema)
|
||||
db.QueryRow("SELECT uuid, content, added_on, dirty FROM notes where book_uuid = ? AND uuid = ?", "js-book-uuid", "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f"), &n2.UUID, &n2.Content, &n2.AddedOn, &n2.Dirty)
|
||||
|
||||
var actionData actions.EditNoteDataV3
|
||||
if err := json.Unmarshal(noteAction.Data, &actionData); err != nil {
|
||||
log.Fatalf("Failed to unmarshal the action data: %s", err)
|
||||
}
|
||||
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "n1 should have UUID")
|
||||
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "n1 content mismatch")
|
||||
testutils.AssertEqual(t, n1.Dirty, false, "n1 dirty mismatch")
|
||||
|
||||
testutils.AssertEqual(t, noteAction.Type, actions.ActionEditNote, "action type mismatch")
|
||||
testutils.AssertEqual(t, noteAction.Schema, 3, "action schema mismatch")
|
||||
testutils.AssertEqual(t, *actionData.Content, "foo bar", "action data name mismatch")
|
||||
testutils.AssertEqual(t, actionData.BookName, (*string)(nil), "action data book_name mismatch")
|
||||
testutils.AssertEqual(t, actionData.NoteUUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "action data note_uuis mismatch")
|
||||
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "action timestamp mismatch")
|
||||
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "Note should have UUID")
|
||||
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "Note content mismatch")
|
||||
testutils.AssertEqual(t, n2.UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "Note should have UUID")
|
||||
testutils.AssertEqual(t, n2.Content, "foo bar", "Note content mismatch")
|
||||
testutils.AssertEqual(t, n2.Dirty, true, "n2 dirty mismatch")
|
||||
testutils.AssertNotEqual(t, n2.EditedOn, 0, "Note edited_on mismatch")
|
||||
}
|
||||
|
||||
func TestRemoveNote(t *testing.T) {
|
||||
// Set up
|
||||
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "./tmp", "./testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.Setup2(t, ctx)
|
||||
|
|
@ -223,53 +192,67 @@ func TestRemoveNote(t *testing.T) {
|
|||
// Test
|
||||
db := ctx.DB
|
||||
|
||||
var actionCount, noteCount, bookCount, jsNoteCount, linuxNoteCount int
|
||||
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
|
||||
var noteCount, bookCount, jsNoteCount, linuxNoteCount int
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
testutils.MustScan(t, "counting js notes", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
|
||||
testutils.MustScan(t, "counting linux notes", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
|
||||
|
||||
testutils.AssertEqualf(t, actionCount, 1, "action count mismatch")
|
||||
testutils.AssertEqualf(t, bookCount, 2, "book count mismatch")
|
||||
testutils.AssertEqualf(t, noteCount, 2, "note count mismatch")
|
||||
testutils.AssertEqual(t, jsNoteCount, 1, "Book should have one note")
|
||||
testutils.AssertEqual(t, linuxNoteCount, 1, "Other book should have one note")
|
||||
testutils.AssertEqualf(t, noteCount, 3, "note count mismatch")
|
||||
testutils.AssertEqual(t, jsNoteCount, 2, "js book should have 2 notes")
|
||||
testutils.AssertEqual(t, linuxNoteCount, 1, "linux book book should have 1 note")
|
||||
|
||||
var b1, b2 infra.Book
|
||||
var n1 infra.Note
|
||||
var b1, b2 core.Book
|
||||
var n1, n2, n3 core.Note
|
||||
testutils.MustScan(t, "getting b1",
|
||||
db.QueryRow("SELECT label FROM books WHERE uuid = ?", "js-book-uuid"),
|
||||
&b1.Name)
|
||||
db.QueryRow("SELECT label, deleted, usn FROM books WHERE uuid = ?", "js-book-uuid"),
|
||||
&b1.Label, &b1.Deleted, &b1.USN)
|
||||
testutils.MustScan(t, "getting b2",
|
||||
db.QueryRow("SELECT label FROM books WHERE uuid = ?", "linux-book-uuid"),
|
||||
&b2.Name)
|
||||
db.QueryRow("SELECT label, deleted, usn FROM books WHERE uuid = ?", "linux-book-uuid"),
|
||||
&b2.Label, &b2.Deleted, &b2.USN)
|
||||
testutils.MustScan(t, "getting n1",
|
||||
db.QueryRow("SELECT uuid, content, added_on FROM notes WHERE book_uuid = ? AND id = ?", "js-book-uuid", 2),
|
||||
&n1.UUID, &n1.Content, &n1.AddedOn)
|
||||
db.QueryRow("SELECT uuid, content, added_on, deleted, dirty, usn FROM notes WHERE book_uuid = ? AND id = ?", "js-book-uuid", 1),
|
||||
&n1.UUID, &n1.Content, &n1.AddedOn, &n1.Deleted, &n1.Dirty, &n1.USN)
|
||||
testutils.MustScan(t, "getting n2",
|
||||
db.QueryRow("SELECT uuid, content, added_on, deleted, dirty, usn FROM notes WHERE book_uuid = ? AND id = ?", "js-book-uuid", 2),
|
||||
&n2.UUID, &n2.Content, &n2.AddedOn, &n2.Deleted, &n2.Dirty, &n2.USN)
|
||||
testutils.MustScan(t, "getting n3",
|
||||
db.QueryRow("SELECT uuid, content, added_on, deleted, dirty, usn FROM notes WHERE book_uuid = ? AND id = ?", "linux-book-uuid", 3),
|
||||
&n3.UUID, &n3.Content, &n3.AddedOn, &n3.Deleted, &n3.Dirty, &n3.USN)
|
||||
|
||||
var noteAction actions.Action
|
||||
testutils.MustScan(t, "getting note action",
|
||||
db.QueryRow("SELECT type, schema, data FROM actions WHERE type = ?", actions.ActionRemoveNote), ¬eAction.Type, ¬eAction.Schema, ¬eAction.Data)
|
||||
testutils.AssertEqual(t, b1.Label, "js", "b1 label mismatch")
|
||||
testutils.AssertEqual(t, b1.Deleted, false, "b1 deleted mismatch")
|
||||
testutils.AssertEqual(t, b1.Dirty, false, "b1 Dirty mismatch")
|
||||
testutils.AssertEqual(t, b1.USN, 111, "b1 usn mismatch")
|
||||
|
||||
var actionData actions.RemoveNoteDataV1
|
||||
if err := json.Unmarshal(noteAction.Data, &actionData); err != nil {
|
||||
log.Fatalf("unmarshalling the action data: %s", err)
|
||||
}
|
||||
testutils.AssertEqual(t, b2.Label, "linux", "b2 label mismatch")
|
||||
testutils.AssertEqual(t, b2.Deleted, false, "b2 deleted mismatch")
|
||||
testutils.AssertEqual(t, b2.Dirty, false, "b2 Dirty mismatch")
|
||||
testutils.AssertEqual(t, b2.USN, 122, "b2 usn mismatch")
|
||||
|
||||
testutils.AssertEqual(t, b1.Name, "js", "b1 label mismatch")
|
||||
testutils.AssertEqual(t, b2.Name, "linux", "b2 label mismatch")
|
||||
testutils.AssertEqual(t, noteAction.Schema, 2, "action schema mismatch")
|
||||
testutils.AssertEqual(t, noteAction.Type, actions.ActionRemoveNote, "action type mismatch")
|
||||
testutils.AssertEqual(t, actionData.NoteUUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "action data note_uuid mismatch")
|
||||
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "action timestamp mismatch")
|
||||
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "Note should have UUID")
|
||||
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "Note content mismatch")
|
||||
testutils.AssertEqual(t, n1.UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "n1 should have UUID")
|
||||
testutils.AssertEqual(t, n1.Content, "", "n1 content mismatch")
|
||||
testutils.AssertEqual(t, n1.Deleted, true, "n1 deleted mismatch")
|
||||
testutils.AssertEqual(t, n1.Dirty, true, "n1 Dirty mismatch")
|
||||
testutils.AssertEqual(t, n1.USN, 11, "n1 usn mismatch")
|
||||
|
||||
testutils.AssertEqual(t, n2.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "n2 should have UUID")
|
||||
testutils.AssertEqual(t, n2.Content, "n2 content", "n2 content mismatch")
|
||||
testutils.AssertEqual(t, n2.Deleted, false, "n2 deleted mismatch")
|
||||
testutils.AssertEqual(t, n2.Dirty, false, "n2 Dirty mismatch")
|
||||
testutils.AssertEqual(t, n2.USN, 12, "n2 usn mismatch")
|
||||
|
||||
testutils.AssertEqual(t, n3.UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "n3 should have UUID")
|
||||
testutils.AssertEqual(t, n3.Content, "n3 content", "n3 content mismatch")
|
||||
testutils.AssertEqual(t, n3.Deleted, false, "n3 deleted mismatch")
|
||||
testutils.AssertEqual(t, n3.Dirty, false, "n3 Dirty mismatch")
|
||||
testutils.AssertEqual(t, n3.USN, 13, "n3 usn mismatch")
|
||||
}
|
||||
|
||||
func TestRemoveBook(t *testing.T) {
|
||||
// Set up
|
||||
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "./tmp", "./testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.Setup2(t, ctx)
|
||||
|
|
@ -280,35 +263,60 @@ func TestRemoveBook(t *testing.T) {
|
|||
// Test
|
||||
db := ctx.DB
|
||||
|
||||
var actionCount, noteCount, bookCount, jsNoteCount, linuxNoteCount int
|
||||
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
|
||||
var noteCount, bookCount, jsNoteCount, linuxNoteCount int
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
testutils.MustScan(t, "counting js notes", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
|
||||
testutils.MustScan(t, "counting linux notes", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
|
||||
|
||||
testutils.AssertEqualf(t, actionCount, 1, "action count mismatch")
|
||||
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
|
||||
testutils.AssertEqualf(t, noteCount, 1, "note count mismatch")
|
||||
testutils.AssertEqual(t, jsNoteCount, 0, "some notes in book were not deleted")
|
||||
testutils.AssertEqual(t, linuxNoteCount, 1, "Other book should have one note")
|
||||
testutils.AssertEqualf(t, bookCount, 2, "book count mismatch")
|
||||
testutils.AssertEqualf(t, noteCount, 3, "note count mismatch")
|
||||
testutils.AssertEqual(t, jsNoteCount, 2, "js book should have 2 notes")
|
||||
testutils.AssertEqual(t, linuxNoteCount, 1, "linux book book should have 1 note")
|
||||
|
||||
var b1 infra.Book
|
||||
var b1, b2 core.Book
|
||||
var n1, n2, n3 core.Note
|
||||
testutils.MustScan(t, "getting b1",
|
||||
db.QueryRow("SELECT label FROM books WHERE uuid = ?", "linux-book-uuid"),
|
||||
&b1.Name)
|
||||
db.QueryRow("SELECT label, dirty, deleted, usn FROM books WHERE uuid = ?", "js-book-uuid"),
|
||||
&b1.Label, &b1.Dirty, &b1.Deleted, &b1.USN)
|
||||
testutils.MustScan(t, "getting b2",
|
||||
db.QueryRow("SELECT label, dirty, deleted, usn FROM books WHERE uuid = ?", "linux-book-uuid"),
|
||||
&b2.Label, &b2.Dirty, &b2.Deleted, &b2.USN)
|
||||
testutils.MustScan(t, "getting n1",
|
||||
db.QueryRow("SELECT uuid, content, added_on, dirty, deleted, usn FROM notes WHERE book_uuid = ? AND id = ?", "js-book-uuid", 1),
|
||||
&n1.UUID, &n1.Content, &n1.AddedOn, &n1.Deleted, &n1.Dirty, &n1.USN)
|
||||
testutils.MustScan(t, "getting n2",
|
||||
db.QueryRow("SELECT uuid, content, added_on, dirty, deleted, usn FROM notes WHERE book_uuid = ? AND id = ?", "js-book-uuid", 2),
|
||||
&n2.UUID, &n2.Content, &n2.AddedOn, &n2.Deleted, &n2.Dirty, &n2.USN)
|
||||
testutils.MustScan(t, "getting n3",
|
||||
db.QueryRow("SELECT uuid, content, added_on, dirty, deleted, usn FROM notes WHERE book_uuid = ? AND id = ?", "linux-book-uuid", 3),
|
||||
&n3.UUID, &n3.Content, &n3.AddedOn, &n3.Deleted, &n3.Dirty, &n3.USN)
|
||||
|
||||
var action actions.Action
|
||||
testutils.MustScan(t, "getting an action",
|
||||
db.QueryRow("SELECT type, schema, data FROM actions WHERE type = ?", actions.ActionRemoveBook), &action.Type, &action.Schema, &action.Data)
|
||||
testutils.AssertNotEqual(t, b1.Label, "js", "b1 label mismatch")
|
||||
testutils.AssertEqual(t, b1.Dirty, true, "b1 Dirty mismatch")
|
||||
testutils.AssertEqual(t, b1.Deleted, true, "b1 deleted mismatch")
|
||||
testutils.AssertEqual(t, b1.USN, 111, "b1 usn mismatch")
|
||||
|
||||
var actionData actions.RemoveBookDataV1
|
||||
if err := json.Unmarshal(action.Data, &actionData); err != nil {
|
||||
log.Fatalf("unmarshalling the action data: %s", err)
|
||||
}
|
||||
testutils.AssertEqual(t, b2.Label, "linux", "b2 label mismatch")
|
||||
testutils.AssertEqual(t, b2.Dirty, false, "b2 Dirty mismatch")
|
||||
testutils.AssertEqual(t, b2.Deleted, false, "b2 deleted mismatch")
|
||||
testutils.AssertEqual(t, b2.USN, 122, "b2 usn mismatch")
|
||||
|
||||
testutils.AssertEqual(t, action.Type, actions.ActionRemoveBook, "action type mismatch")
|
||||
testutils.AssertEqual(t, actionData.BookName, "js", "action data name mismatch")
|
||||
testutils.AssertNotEqual(t, action.Timestamp, 0, "action timestamp mismatch")
|
||||
testutils.AssertEqual(t, b1.Name, "linux", "Remaining book name mismatch")
|
||||
testutils.AssertEqual(t, n1.UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "n1 should have UUID")
|
||||
testutils.AssertEqual(t, n1.Content, "", "n1 content mismatch")
|
||||
testutils.AssertEqual(t, n1.Dirty, true, "n1 Dirty mismatch")
|
||||
testutils.AssertEqual(t, n1.Deleted, true, "n1 deleted mismatch")
|
||||
testutils.AssertEqual(t, n1.USN, 11, "n1 usn mismatch")
|
||||
|
||||
testutils.AssertEqual(t, n2.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "n2 should have UUID")
|
||||
testutils.AssertEqual(t, n2.Content, "", "n2 content mismatch")
|
||||
testutils.AssertEqual(t, n2.Dirty, true, "n2 Dirty mismatch")
|
||||
testutils.AssertEqual(t, n2.Deleted, true, "n2 deleted mismatch")
|
||||
testutils.AssertEqual(t, n2.USN, 12, "n2 usn mismatch")
|
||||
|
||||
testutils.AssertEqual(t, n3.UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "n3 should have UUID")
|
||||
testutils.AssertEqual(t, n3.Content, "n3 content", "n3 content mismatch")
|
||||
testutils.AssertEqual(t, n3.Dirty, false, "n3 Dirty mismatch")
|
||||
testutils.AssertEqual(t, n3.Deleted, false, "n3 deleted mismatch")
|
||||
testutils.AssertEqual(t, n3.USN, 13, "n3 usn mismatch")
|
||||
}
|
||||
|
|
|
|||
35
migrate/fixtures/local-1-pre-schema.sql
Normal file
35
migrate/fixtures/local-1-pre-schema.sql
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
-- local-1-pre-schema.sql is the schema in which 'dirty' flags do not exist
|
||||
|
||||
CREATE TABLE notes
|
||||
(
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
uuid text NOT NULL,
|
||||
book_uuid text NOT NULL,
|
||||
content text NOT NULL,
|
||||
added_on integer NOT NULL,
|
||||
edited_on integer DEFAULT 0,
|
||||
public bool DEFAULT false
|
||||
);
|
||||
CREATE TABLE books
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
label text NOT NULL
|
||||
);
|
||||
CREATE TABLE actions
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
schema integer NOT NULL,
|
||||
type text NOT NULL,
|
||||
data text NOT NULL,
|
||||
timestamp integer NOT NULL
|
||||
);
|
||||
CREATE TABLE system
|
||||
(
|
||||
key string NOT NULL,
|
||||
value text NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_books_label ON books(label);
|
||||
CREATE UNIQUE INDEX idx_notes_uuid ON notes(uuid);
|
||||
CREATE UNIQUE INDEX idx_books_uuid ON books(uuid);
|
||||
CREATE UNIQUE INDEX idx_notes_id ON notes(id);
|
||||
CREATE INDEX idx_notes_book_uuid ON notes(book_uuid);
|
||||
35
migrate/fixtures/local-5-pre-schema.sql
Normal file
35
migrate/fixtures/local-5-pre-schema.sql
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
-- local-5-pre-schema.sql is the schema in which the actions table is present
|
||||
|
||||
CREATE TABLE notes
|
||||
(
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
uuid text NOT NULL,
|
||||
book_uuid text NOT NULL,
|
||||
content text NOT NULL,
|
||||
added_on integer NOT NULL,
|
||||
edited_on integer DEFAULT 0,
|
||||
public bool DEFAULT false
|
||||
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL);
|
||||
CREATE TABLE books
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
label text NOT NULL
|
||||
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL);
|
||||
CREATE TABLE actions
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
schema integer NOT NULL,
|
||||
type text NOT NULL,
|
||||
data text NOT NULL,
|
||||
timestamp integer NOT NULL
|
||||
);
|
||||
CREATE TABLE system
|
||||
(
|
||||
key string NOT NULL,
|
||||
value text NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_books_label ON books(label);
|
||||
CREATE UNIQUE INDEX idx_notes_uuid ON notes(uuid);
|
||||
CREATE UNIQUE INDEX idx_books_uuid ON books(uuid);
|
||||
CREATE UNIQUE INDEX idx_notes_id ON notes(id);
|
||||
CREATE INDEX idx_notes_book_uuid ON notes(book_uuid);
|
||||
25
migrate/fixtures/local-7-pre-schema.sql
Normal file
25
migrate/fixtures/local-7-pre-schema.sql
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
CREATE TABLE notes
|
||||
(
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
uuid text NOT NULL,
|
||||
book_uuid text NOT NULL,
|
||||
content text NOT NULL,
|
||||
added_on integer NOT NULL,
|
||||
edited_on integer DEFAULT 0,
|
||||
public bool DEFAULT false
|
||||
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL, deleted bool DEFAULT false);
|
||||
CREATE TABLE books
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
label text NOT NULL
|
||||
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL, deleted bool DEFAULT false);
|
||||
CREATE TABLE system
|
||||
(
|
||||
key string NOT NULL,
|
||||
value text NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_books_label ON books(label);
|
||||
CREATE UNIQUE INDEX idx_notes_uuid ON notes(uuid);
|
||||
CREATE UNIQUE INDEX idx_books_uuid ON books(uuid);
|
||||
CREATE UNIQUE INDEX idx_notes_id ON notes(id);
|
||||
CREATE INDEX idx_notes_book_uuid ON notes(book_uuid);
|
||||
35
migrate/fixtures/remote-1-pre-schema.sql
Normal file
35
migrate/fixtures/remote-1-pre-schema.sql
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
-- 5-pre-schema.sql is the schema in which the actions table is present
|
||||
|
||||
CREATE TABLE notes
|
||||
(
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
uuid text NOT NULL,
|
||||
book_uuid text NOT NULL,
|
||||
content text NOT NULL,
|
||||
added_on integer NOT NULL,
|
||||
edited_on integer DEFAULT 0,
|
||||
public bool DEFAULT false
|
||||
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL);
|
||||
CREATE TABLE books
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
label text NOT NULL
|
||||
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL);
|
||||
CREATE TABLE actions
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
schema integer NOT NULL,
|
||||
type text NOT NULL,
|
||||
data text NOT NULL,
|
||||
timestamp integer NOT NULL
|
||||
);
|
||||
CREATE TABLE system
|
||||
(
|
||||
key string NOT NULL,
|
||||
value text NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_books_label ON books(label);
|
||||
CREATE UNIQUE INDEX idx_notes_uuid ON notes(uuid);
|
||||
CREATE UNIQUE INDEX idx_books_uuid ON books(uuid);
|
||||
CREATE UNIQUE INDEX idx_notes_id ON notes(id);
|
||||
CREATE INDEX idx_notes_book_uuid ON notes(book_uuid);
|
||||
|
|
@ -19,7 +19,7 @@ func TestMigrateToV1(t *testing.T) {
|
|||
|
||||
t.Run("yaml exists", func(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
yamlPath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnote-yaml-archived"))
|
||||
|
|
@ -41,7 +41,7 @@ func TestMigrateToV1(t *testing.T) {
|
|||
|
||||
t.Run("yaml does not exist", func(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
yamlPath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnote-yaml-archived"))
|
||||
|
|
@ -62,10 +62,10 @@ func TestMigrateToV1(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMigrateToV2(t *testing.T) {
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.CopyFixture(ctx, "./fixtures/2-pre-dnote.json", "dnote")
|
||||
testutils.CopyFixture(ctx, "./fixtures/legacy-2-pre-dnote.json", "dnote")
|
||||
|
||||
// execute
|
||||
if err := migrateToV2(ctx); err != nil {
|
||||
|
|
@ -96,10 +96,10 @@ func TestMigrateToV2(t *testing.T) {
|
|||
|
||||
func TestMigrateToV3(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.CopyFixture(ctx, "./fixtures/3-pre-dnote.json", "dnote")
|
||||
testutils.CopyFixture(ctx, "./fixtures/legacy-3-pre-dnote.json", "dnote")
|
||||
|
||||
// execute
|
||||
if err := migrateToV3(ctx); err != nil {
|
||||
|
|
@ -130,11 +130,11 @@ func TestMigrateToV3(t *testing.T) {
|
|||
|
||||
func TestMigrateToV4(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
defer os.Setenv("EDITOR", "")
|
||||
|
||||
testutils.CopyFixture(ctx, "./fixtures/4-pre-dnoterc.yaml", "dnoterc")
|
||||
testutils.CopyFixture(ctx, "./fixtures/legacy-4-pre-dnoterc.yaml", "dnoterc")
|
||||
|
||||
// execute
|
||||
os.Setenv("EDITOR", "vim")
|
||||
|
|
@ -155,10 +155,10 @@ func TestMigrateToV4(t *testing.T) {
|
|||
|
||||
func TestMigrateToV5(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.CopyFixture(ctx, "./fixtures/5-pre-actions.json", "actions")
|
||||
testutils.CopyFixture(ctx, "./fixtures/legacy-5-pre-actions.json", "actions")
|
||||
|
||||
// execute
|
||||
if err := migrateToV5(ctx); err != nil {
|
||||
|
|
@ -167,7 +167,7 @@ func TestMigrateToV5(t *testing.T) {
|
|||
|
||||
// test
|
||||
var oldActions []migrateToV5PreAction
|
||||
testutils.ReadJSON("./fixtures/5-pre-actions.json", &oldActions)
|
||||
testutils.ReadJSON("./fixtures/legacy-5-pre-actions.json", &oldActions)
|
||||
|
||||
b := testutils.ReadFile(ctx, "actions")
|
||||
var migratedActions []migrateToV5PostAction
|
||||
|
|
@ -252,10 +252,10 @@ func TestMigrateToV5(t *testing.T) {
|
|||
|
||||
func TestMigrateToV6(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.CopyFixture(ctx, "./fixtures/6-pre-dnote.json", "dnote")
|
||||
testutils.CopyFixture(ctx, "./fixtures/legacy-6-pre-dnote.json", "dnote")
|
||||
|
||||
// execute
|
||||
if err := migrateToV6(ctx); err != nil {
|
||||
|
|
@ -269,7 +269,7 @@ func TestMigrateToV6(t *testing.T) {
|
|||
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
|
||||
}
|
||||
|
||||
b = testutils.ReadFileAbs("./fixtures/6-post-dnote.json")
|
||||
b = testutils.ReadFileAbs("./fixtures/legacy-6-post-dnote.json")
|
||||
var expected migrateToV6PostDnote
|
||||
if err := json.Unmarshal(b, &expected); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
|
||||
|
|
@ -282,10 +282,10 @@ func TestMigrateToV6(t *testing.T) {
|
|||
|
||||
func TestMigrateToV7(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.CopyFixture(ctx, "./fixtures/7-pre-actions.json", "actions")
|
||||
testutils.CopyFixture(ctx, "./fixtures/legacy-7-pre-actions.json", "actions")
|
||||
|
||||
// execute
|
||||
if err := migrateToV7(ctx); err != nil {
|
||||
|
|
@ -299,7 +299,7 @@ func TestMigrateToV7(t *testing.T) {
|
|||
t.Fatal(errors.Wrap(err, "unmarshalling the result").Error())
|
||||
}
|
||||
|
||||
b2 := testutils.ReadFileAbs("./fixtures/7-post-actions.json")
|
||||
b2 := testutils.ReadFileAbs("./fixtures/legacy-7-post-actions.json")
|
||||
var expected []migrateToV7Action
|
||||
if err := json.Unmarshal(b, &expected); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarshalling the result into Dnote").Error())
|
||||
|
|
@ -316,15 +316,15 @@ func TestMigrateToV7(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMigrateToV8(t *testing.T) {
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-1-pre-schema.sql", false)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
// set up
|
||||
testutils.CopyFixture(ctx, "./fixtures/8-actions.json", "actions")
|
||||
testutils.CopyFixture(ctx, "./fixtures/8-dnote.json", "dnote")
|
||||
testutils.CopyFixture(ctx, "./fixtures/8-dnoterc.yaml", "dnoterc")
|
||||
testutils.CopyFixture(ctx, "./fixtures/8-schema.yaml", "schema")
|
||||
testutils.CopyFixture(ctx, "./fixtures/8-timestamps.yaml", "timestamps")
|
||||
testutils.CopyFixture(ctx, "./fixtures/legacy-8-actions.json", "actions")
|
||||
testutils.CopyFixture(ctx, "./fixtures/legacy-8-dnote.json", "dnote")
|
||||
testutils.CopyFixture(ctx, "./fixtures/legacy-8-dnoterc.yaml", "dnoterc")
|
||||
testutils.CopyFixture(ctx, "./fixtures/legacy-8-schema.yaml", "schema")
|
||||
testutils.CopyFixture(ctx, "./fixtures/legacy-8-timestamps.yaml", "timestamps")
|
||||
|
||||
// execute
|
||||
if err := migrateToV8(ctx); err != nil {
|
||||
|
|
|
|||
|
|
@ -8,19 +8,34 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// LocalMode is a local migration mode
|
||||
LocalMode = iota
|
||||
// RemoteMode is a remote migration mode
|
||||
RemoteMode
|
||||
)
|
||||
|
||||
// LocalSequence is a list of local migrations to be run
|
||||
var LocalSequence = []migration{
|
||||
lm1,
|
||||
lm2,
|
||||
lm3,
|
||||
lm4,
|
||||
lm5,
|
||||
lm6,
|
||||
}
|
||||
|
||||
func initSchema(ctx infra.DnoteCtx) (int, error) {
|
||||
// RemoteSequence is a list of remote migrations to be run
|
||||
var RemoteSequence = []migration{
|
||||
rm1,
|
||||
}
|
||||
|
||||
func initSchema(ctx infra.DnoteCtx, schemaKey string) (int, error) {
|
||||
// schemaVersion is the index of the latest run migration in the sequence
|
||||
schemaVersion := 0
|
||||
|
||||
db := ctx.DB
|
||||
_, err := db.Exec("INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemSchema, schemaVersion)
|
||||
_, err := db.Exec("INSERT INTO system (key, value) VALUES (?, ?)", schemaKey, schemaVersion)
|
||||
if err != nil {
|
||||
return schemaVersion, errors.Wrap(err, "inserting schema")
|
||||
}
|
||||
|
|
@ -28,13 +43,25 @@ func initSchema(ctx infra.DnoteCtx) (int, error) {
|
|||
return schemaVersion, nil
|
||||
}
|
||||
|
||||
func getSchema(ctx infra.DnoteCtx) (int, error) {
|
||||
func getSchemaKey(mode int) (string, error) {
|
||||
if mode == LocalMode {
|
||||
return infra.SystemSchema, nil
|
||||
}
|
||||
|
||||
if mode == RemoteMode {
|
||||
return infra.SystemRemoteSchema, nil
|
||||
}
|
||||
|
||||
return "", errors.Errorf("unsupported migration type '%d'", mode)
|
||||
}
|
||||
|
||||
func getSchema(ctx infra.DnoteCtx, schemaKey string) (int, error) {
|
||||
var ret int
|
||||
|
||||
db := ctx.DB
|
||||
err := db.QueryRow("SELECT value FROM system where key = ?", infra.SystemSchema).Scan(&ret)
|
||||
err := db.QueryRow("SELECT value FROM system where key = ?", schemaKey).Scan(&ret)
|
||||
if err == sql.ErrNoRows {
|
||||
ret, err = initSchema(ctx)
|
||||
ret, err = initSchema(ctx, schemaKey)
|
||||
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "initializing schema")
|
||||
|
|
@ -46,7 +73,7 @@ func getSchema(ctx infra.DnoteCtx) (int, error) {
|
|||
return ret, nil
|
||||
}
|
||||
|
||||
func execute(ctx infra.DnoteCtx, m migration) error {
|
||||
func execute(ctx infra.DnoteCtx, m migration, schemaKey string) error {
|
||||
log.Debug("running migration %s\n", m.name)
|
||||
|
||||
tx, err := ctx.DB.Begin()
|
||||
|
|
@ -57,17 +84,17 @@ func execute(ctx infra.DnoteCtx, m migration) error {
|
|||
err = m.run(ctx, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrapf(err, "running migration '%s'", m.name)
|
||||
return errors.Wrapf(err, "running '%s'", m.name)
|
||||
}
|
||||
|
||||
var currentSchema int
|
||||
err = tx.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemSchema).Scan(¤tSchema)
|
||||
err = tx.QueryRow("SELECT value FROM system WHERE key = ?", schemaKey).Scan(¤tSchema)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "getting current schema")
|
||||
}
|
||||
|
||||
_, err = tx.Exec("UPDATE system SET value = ? WHERE key = ?", currentSchema+1, infra.SystemSchema)
|
||||
_, err = tx.Exec("UPDATE system SET value = value + 1 WHERE key = ?", schemaKey)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "incrementing schema")
|
||||
|
|
@ -79,8 +106,13 @@ func execute(ctx infra.DnoteCtx, m migration) error {
|
|||
}
|
||||
|
||||
// Run performs unrun migrations
|
||||
func Run(ctx infra.DnoteCtx, migrations []migration) error {
|
||||
schema, err := getSchema(ctx)
|
||||
func Run(ctx infra.DnoteCtx, migrations []migration, mode int) error {
|
||||
schemaKey, err := getSchemaKey(mode)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting schema key")
|
||||
}
|
||||
|
||||
schema, err := getSchema(ctx, schemaKey)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting the current schema")
|
||||
}
|
||||
|
|
@ -90,8 +122,8 @@ func Run(ctx infra.DnoteCtx, migrations []migration) error {
|
|||
toRun := migrations[schema:]
|
||||
|
||||
for _, m := range toRun {
|
||||
if err := execute(ctx, m); err != nil {
|
||||
return errors.Wrapf(err, "running migration %s", m.name)
|
||||
if err := execute(ctx, m, schemaKey); err != nil {
|
||||
return errors.Wrap(err, "running migration")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,12 @@ package migrate
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/actions"
|
||||
"github.com/dnote/cli/infra"
|
||||
|
|
@ -13,212 +17,281 @@ import (
|
|||
)
|
||||
|
||||
func TestExecute_bump_schema(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
testutils.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemSchema, 8)
|
||||
|
||||
m1 := migration{
|
||||
name: "noop",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
return nil
|
||||
testCases := []struct {
|
||||
schemaKey string
|
||||
}{
|
||||
{
|
||||
schemaKey: infra.SystemSchema,
|
||||
},
|
||||
}
|
||||
m2 := migration{
|
||||
name: "noop",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
return nil
|
||||
{
|
||||
schemaKey: infra.SystemRemoteSchema,
|
||||
},
|
||||
}
|
||||
|
||||
// execute
|
||||
err := execute(ctx, m1)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "failed to execute"))
|
||||
}
|
||||
err = execute(ctx, m2)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "failed to execute"))
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
func() {
|
||||
// set up
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", false)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
// test
|
||||
var schema int
|
||||
testutils.MustScan(t, "getting schema", db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemSchema), &schema)
|
||||
testutils.AssertEqual(t, schema, 10, "schema was not incremented properly")
|
||||
db := ctx.DB
|
||||
testutils.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", tc.schemaKey, 8)
|
||||
|
||||
m1 := migration{
|
||||
name: "noop",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
m2 := migration{
|
||||
name: "noop",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// execute
|
||||
err := execute(ctx, m1, tc.schemaKey)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "failed to execute"))
|
||||
}
|
||||
err = execute(ctx, m2, tc.schemaKey)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "failed to execute"))
|
||||
}
|
||||
|
||||
// test
|
||||
var schema int
|
||||
testutils.MustScan(t, "getting schema", db.QueryRow("SELECT value FROM system WHERE key = ?", tc.schemaKey), &schema)
|
||||
testutils.AssertEqual(t, schema, 10, "schema was not incremented properly")
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_nonfresh(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
testutils.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemSchema, 2)
|
||||
testutils.MustExec(t, "creating a temporary table for testing", db,
|
||||
"CREATE TABLE migrate_run_test ( name string )")
|
||||
|
||||
sequence := []migration{
|
||||
migration{
|
||||
name: "v1",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v1 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v1")
|
||||
return nil
|
||||
},
|
||||
testCases := []struct {
|
||||
mode int
|
||||
schemaKey string
|
||||
}{
|
||||
{
|
||||
mode: LocalMode,
|
||||
schemaKey: infra.SystemSchema,
|
||||
},
|
||||
migration{
|
||||
name: "v2",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v2 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v2")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
migration{
|
||||
name: "v3",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v3 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v3")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
migration{
|
||||
name: "v4",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v4 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v4")
|
||||
return nil
|
||||
},
|
||||
{
|
||||
mode: RemoteMode,
|
||||
schemaKey: infra.SystemRemoteSchema,
|
||||
},
|
||||
}
|
||||
|
||||
// execute
|
||||
err := Run(ctx, sequence)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "failed to run"))
|
||||
for _, tc := range testCases {
|
||||
func() {
|
||||
// set up
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", false)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
testutils.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", tc.schemaKey, 2)
|
||||
testutils.MustExec(t, "creating a temporary table for testing", db,
|
||||
"CREATE TABLE migrate_run_test ( name string )")
|
||||
|
||||
sequence := []migration{
|
||||
migration{
|
||||
name: "v1",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v1 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v1")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
migration{
|
||||
name: "v2",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v2 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v2")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
migration{
|
||||
name: "v3",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v3 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v3")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
migration{
|
||||
name: "v4",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v4 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v4")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// execute
|
||||
err := Run(ctx, sequence, tc.mode)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "failed to run"))
|
||||
}
|
||||
|
||||
// test
|
||||
var schema int
|
||||
testutils.MustScan(t, fmt.Sprintf("getting schema for %s", tc.schemaKey), db.QueryRow("SELECT value FROM system WHERE key = ?", tc.schemaKey), &schema)
|
||||
testutils.AssertEqual(t, schema, 4, fmt.Sprintf("schema was not updated for %s", tc.schemaKey))
|
||||
|
||||
var testRunCount int
|
||||
testutils.MustScan(t, "counting test runs", db.QueryRow("SELECT count(*) FROM migrate_run_test"), &testRunCount)
|
||||
testutils.AssertEqual(t, testRunCount, 2, "test run count mismatch")
|
||||
|
||||
var testRun1, testRun2 string
|
||||
testutils.MustScan(t, "finding test run 1", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v3"), &testRun1)
|
||||
testutils.MustScan(t, "finding test run 2", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v4"), &testRun2)
|
||||
}()
|
||||
}
|
||||
|
||||
// test
|
||||
var schema int
|
||||
testutils.MustScan(t, fmt.Sprintf("getting schema for %s", infra.SystemSchema), db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemSchema), &schema)
|
||||
testutils.AssertEqual(t, schema, 4, fmt.Sprintf("schema was not updated for %s", infra.SystemSchema))
|
||||
|
||||
var testRunCount int
|
||||
testutils.MustScan(t, "counting test runs", db.QueryRow("SELECT count(*) FROM migrate_run_test"), &testRunCount)
|
||||
testutils.AssertEqual(t, testRunCount, 2, "test run count mismatch")
|
||||
|
||||
var testRun1, testRun2 string
|
||||
testutils.MustScan(t, "finding test run 1", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v3"), &testRun1)
|
||||
testutils.MustScan(t, "finding test run 2", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v4"), &testRun2)
|
||||
|
||||
}
|
||||
|
||||
func TestRun_fresh(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
testutils.MustExec(t, "creating a temporary table for testing", db,
|
||||
"CREATE TABLE migrate_run_test ( name string )")
|
||||
|
||||
sequence := []migration{
|
||||
migration{
|
||||
name: "v1",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v1 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v1")
|
||||
return nil
|
||||
},
|
||||
testCases := []struct {
|
||||
mode int
|
||||
schemaKey string
|
||||
}{
|
||||
{
|
||||
mode: LocalMode,
|
||||
schemaKey: infra.SystemSchema,
|
||||
},
|
||||
migration{
|
||||
name: "v2",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v2 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v2")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
migration{
|
||||
name: "v3",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v3 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v3")
|
||||
return nil
|
||||
},
|
||||
{
|
||||
mode: RemoteMode,
|
||||
schemaKey: infra.SystemRemoteSchema,
|
||||
},
|
||||
}
|
||||
|
||||
// execute
|
||||
err := Run(ctx, sequence)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "failed to run"))
|
||||
for _, tc := range testCases {
|
||||
func() {
|
||||
// set up
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", false)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
testutils.MustExec(t, "creating a temporary table for testing", db,
|
||||
"CREATE TABLE migrate_run_test ( name string )")
|
||||
|
||||
sequence := []migration{
|
||||
migration{
|
||||
name: "v1",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v1 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v1")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
migration{
|
||||
name: "v2",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v2 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v2")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
migration{
|
||||
name: "v3",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v3 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v3")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// execute
|
||||
err := Run(ctx, sequence, tc.mode)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "failed to run"))
|
||||
}
|
||||
|
||||
// test
|
||||
var schema int
|
||||
testutils.MustScan(t, "getting schema", db.QueryRow("SELECT value FROM system WHERE key = ?", tc.schemaKey), &schema)
|
||||
testutils.AssertEqual(t, schema, 3, "schema was not updated")
|
||||
|
||||
var testRunCount int
|
||||
testutils.MustScan(t, "counting test runs", db.QueryRow("SELECT count(*) FROM migrate_run_test"), &testRunCount)
|
||||
testutils.AssertEqual(t, testRunCount, 3, "test run count mismatch")
|
||||
|
||||
var testRun1, testRun2, testRun3 string
|
||||
testutils.MustScan(t, "finding test run 1", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v1"), &testRun1)
|
||||
testutils.MustScan(t, "finding test run 2", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v2"), &testRun2)
|
||||
testutils.MustScan(t, "finding test run 2", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v3"), &testRun3)
|
||||
}()
|
||||
}
|
||||
|
||||
// test
|
||||
var schema int
|
||||
testutils.MustScan(t, "getting schema", db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemSchema), &schema)
|
||||
testutils.AssertEqual(t, schema, 3, "schema was not updated")
|
||||
|
||||
var testRunCount int
|
||||
testutils.MustScan(t, "counting test runs", db.QueryRow("SELECT count(*) FROM migrate_run_test"), &testRunCount)
|
||||
testutils.AssertEqual(t, testRunCount, 3, "test run count mismatch")
|
||||
|
||||
var testRun1, testRun2, testRun3 string
|
||||
testutils.MustScan(t, "finding test run 1", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v1"), &testRun1)
|
||||
testutils.MustScan(t, "finding test run 2", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v2"), &testRun2)
|
||||
testutils.MustScan(t, "finding test run 2", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v3"), &testRun3)
|
||||
}
|
||||
|
||||
func TestRun_up_to_date(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
testutils.MustExec(t, "creating a temporary table for testing", db,
|
||||
"CREATE TABLE migrate_run_test ( name string )")
|
||||
|
||||
testutils.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemSchema, 3)
|
||||
|
||||
sequence := []migration{
|
||||
migration{
|
||||
name: "v1",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v1 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v1")
|
||||
return nil
|
||||
},
|
||||
testCases := []struct {
|
||||
mode int
|
||||
schemaKey string
|
||||
}{
|
||||
{
|
||||
mode: LocalMode,
|
||||
schemaKey: infra.SystemSchema,
|
||||
},
|
||||
migration{
|
||||
name: "v2",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v2 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v2")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
migration{
|
||||
name: "v3",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v3 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v3")
|
||||
return nil
|
||||
},
|
||||
{
|
||||
mode: RemoteMode,
|
||||
schemaKey: infra.SystemRemoteSchema,
|
||||
},
|
||||
}
|
||||
|
||||
// execute
|
||||
err := Run(ctx, sequence)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "failed to run"))
|
||||
for _, tc := range testCases {
|
||||
func() {
|
||||
// set up
|
||||
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", false)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
testutils.MustExec(t, "creating a temporary table for testing", db,
|
||||
"CREATE TABLE migrate_run_test ( name string )")
|
||||
|
||||
testutils.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", tc.schemaKey, 3)
|
||||
|
||||
sequence := []migration{
|
||||
migration{
|
||||
name: "v1",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v1 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v1")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
migration{
|
||||
name: "v2",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v2 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v2")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
migration{
|
||||
name: "v3",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
testutils.MustExec(t, "marking v3 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v3")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// execute
|
||||
err := Run(ctx, sequence, tc.mode)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "failed to run"))
|
||||
}
|
||||
|
||||
// test
|
||||
var schema int
|
||||
testutils.MustScan(t, "getting schema", db.QueryRow("SELECT value FROM system WHERE key = ?", tc.schemaKey), &schema)
|
||||
testutils.AssertEqual(t, schema, 3, "schema was not updated")
|
||||
|
||||
var testRunCount int
|
||||
testutils.MustScan(t, "counting test runs", db.QueryRow("SELECT count(*) FROM migrate_run_test"), &testRunCount)
|
||||
testutils.AssertEqual(t, testRunCount, 0, "test run count mismatch")
|
||||
}()
|
||||
}
|
||||
|
||||
// test
|
||||
var schema int
|
||||
testutils.MustScan(t, "getting schema", db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemSchema), &schema)
|
||||
testutils.AssertEqual(t, schema, 3, "schema was not updated")
|
||||
|
||||
var testRunCount int
|
||||
testutils.MustScan(t, "counting test runs", db.QueryRow("SELECT count(*) FROM migrate_run_test"), &testRunCount)
|
||||
testutils.AssertEqual(t, testRunCount, 0, "test run count mismatch")
|
||||
}
|
||||
|
||||
func TestLocalMigration1(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-1-pre-schema.sql", false)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
|
|
@ -295,7 +368,7 @@ func TestLocalMigration1(t *testing.T) {
|
|||
|
||||
func TestLocalMigration2(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-1-pre-schema.sql", false)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
|
|
@ -381,7 +454,7 @@ func TestLocalMigration2(t *testing.T) {
|
|||
|
||||
func TestLocalMigration3(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-1-pre-schema.sql", false)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
|
|
@ -452,3 +525,314 @@ func TestLocalMigration3(t *testing.T) {
|
|||
testutils.AssertEqual(t, a3.Timestamp, int64(1537829463), "a3 timestamp mismatch")
|
||||
testutils.AssertEqual(t, a3Data.NoteUUID, "note-2-uuid", "a3 data note_uuid mismatch")
|
||||
}
|
||||
|
||||
func TestLocalMigration4(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-1-pre-schema.sql", false)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
|
||||
b1UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting css book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "css")
|
||||
n1UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting css note", db, "INSERT INTO notes (uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?)", n1UUID, b1UUID, "n1 content", time.Now().UnixNano())
|
||||
|
||||
// Execute
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
|
||||
err = lm4.run(ctx, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "failed to run"))
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
var n1Dirty, b1Dirty bool
|
||||
var n1Deleted, b1Deleted bool
|
||||
var n1USN, b1USN int
|
||||
testutils.MustScan(t, "scanning the newly added dirty flag of n1", db.QueryRow("SELECT dirty, deleted, usn FROM notes WHERE uuid = ?", n1UUID), &n1Dirty, &n1Deleted, &n1USN)
|
||||
testutils.MustScan(t, "scanning the newly added dirty flag of b1", db.QueryRow("SELECT dirty, deleted, usn FROM books WHERE uuid = ?", b1UUID), &b1Dirty, &b1Deleted, &b1USN)
|
||||
|
||||
testutils.AssertEqual(t, n1Dirty, false, "n1 dirty flag should be false by default")
|
||||
testutils.AssertEqual(t, b1Dirty, false, "b1 dirty flag should be false by default")
|
||||
|
||||
testutils.AssertEqual(t, n1Deleted, false, "n1 deleted flag should be false by default")
|
||||
testutils.AssertEqual(t, b1Deleted, false, "b1 deleted flag should be false by default")
|
||||
|
||||
testutils.AssertEqual(t, n1USN, 0, "n1 usn flag should be 0 by default")
|
||||
testutils.AssertEqual(t, b1USN, 0, "b1 usn flag should be 0 by default")
|
||||
}
|
||||
|
||||
func TestLocalMigration5(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-5-pre-schema.sql", false)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
|
||||
b1UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting css book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "css")
|
||||
b2UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting js book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "js")
|
||||
|
||||
n1UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting css note", db, "INSERT INTO notes (uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?)", n1UUID, b1UUID, "n1 content", time.Now().UnixNano())
|
||||
n2UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting css note", db, "INSERT INTO notes (uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?)", n2UUID, b1UUID, "n2 content", time.Now().UnixNano())
|
||||
n3UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting css note", db, "INSERT INTO notes (uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?)", n3UUID, b1UUID, "n3 content", time.Now().UnixNano())
|
||||
|
||||
data := testutils.MustMarshalJSON(t, actions.AddBookDataV1{BookName: "js"})
|
||||
testutils.MustExec(t, "inserting a1", db,
|
||||
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", "a1-uuid", 1, "add_book", string(data), 1537829463)
|
||||
|
||||
data = testutils.MustMarshalJSON(t, actions.AddNoteDataV2{NoteUUID: n1UUID, BookName: "css", Content: "n1 content", Public: false})
|
||||
testutils.MustExec(t, "inserting a2", db,
|
||||
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", "a2-uuid", 1, "add_note", string(data), 1537829463)
|
||||
|
||||
updatedContent := "updated content"
|
||||
data = testutils.MustMarshalJSON(t, actions.EditNoteDataV3{NoteUUID: n2UUID, BookName: (*string)(nil), Content: &updatedContent, Public: (*bool)(nil)})
|
||||
testutils.MustExec(t, "inserting a3", db,
|
||||
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", "a3-uuid", 1, "edit_note", string(data), 1537829463)
|
||||
|
||||
// Execute
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
|
||||
err = lm5.run(ctx, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "failed to run"))
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
var b1Dirty, b2Dirty, n1Dirty, n2Dirty, n3Dirty bool
|
||||
testutils.MustScan(t, "scanning the newly added dirty flag of b1", db.QueryRow("SELECT dirty FROM books WHERE uuid = ?", b1UUID), &b1Dirty)
|
||||
testutils.MustScan(t, "scanning the newly added dirty flag of b2", db.QueryRow("SELECT dirty FROM books WHERE uuid = ?", b2UUID), &b2Dirty)
|
||||
testutils.MustScan(t, "scanning the newly added dirty flag of n1", db.QueryRow("SELECT dirty FROM notes WHERE uuid = ?", n1UUID), &n1Dirty)
|
||||
testutils.MustScan(t, "scanning the newly added dirty flag of n2", db.QueryRow("SELECT dirty FROM notes WHERE uuid = ?", n2UUID), &n2Dirty)
|
||||
testutils.MustScan(t, "scanning the newly added dirty flag of n3", db.QueryRow("SELECT dirty FROM notes WHERE uuid = ?", n3UUID), &n3Dirty)
|
||||
|
||||
testutils.AssertEqual(t, b1Dirty, false, "b1 dirty flag should be false by default")
|
||||
testutils.AssertEqual(t, b2Dirty, true, "b2 dirty flag should be false by default")
|
||||
testutils.AssertEqual(t, n1Dirty, true, "n1 dirty flag should be false by default")
|
||||
testutils.AssertEqual(t, n2Dirty, true, "n2 dirty flag should be false by default")
|
||||
testutils.AssertEqual(t, n3Dirty, false, "n3 dirty flag should be false by default")
|
||||
}
|
||||
|
||||
func TestLocalMigration6(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-5-pre-schema.sql", false)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
|
||||
data := testutils.MustMarshalJSON(t, actions.AddBookDataV1{BookName: "js"})
|
||||
a1UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting action", db,
|
||||
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", a1UUID, 1, "add_book", string(data), 1537829463)
|
||||
|
||||
// Execute
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
|
||||
err = lm5.run(ctx, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "failed to run"))
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
var count int
|
||||
err = db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name = ?;", "actions").Scan(&count)
|
||||
testutils.AssertEqual(t, count, 0, "actions table should have been deleted")
|
||||
}
|
||||
|
||||
func TestLocalMigration7_trash(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-7-pre-schema.sql", false)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
|
||||
b1UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting trash book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "trash")
|
||||
|
||||
// Execute
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
|
||||
err = lm7.run(ctx, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "failed to run"))
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
var b1Label string
|
||||
var b1Dirty bool
|
||||
testutils.MustScan(t, "scanning b1 label", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b1UUID), &b1Label, &b1Dirty)
|
||||
testutils.AssertEqual(t, b1Label, "trash (2)", "b1 label was not migrated")
|
||||
testutils.AssertEqual(t, b1Dirty, true, "b1 was not marked dirty")
|
||||
}
|
||||
|
||||
func TestLocalMigration7_conflicts(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-7-pre-schema.sql", false)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
|
||||
b1UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "conflicts")
|
||||
|
||||
// Execute
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
|
||||
err = lm7.run(ctx, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "failed to run"))
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
var b1Label string
|
||||
var b1Dirty bool
|
||||
testutils.MustScan(t, "scanning b1 label", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b1UUID), &b1Label, &b1Dirty)
|
||||
testutils.AssertEqual(t, b1Label, "conflicts (2)", "b1 label was not migrated")
|
||||
testutils.AssertEqual(t, b1Dirty, true, "b1 was not marked dirty")
|
||||
}
|
||||
|
||||
func TestLocalMigration7_conflicts_dup(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-7-pre-schema.sql", false)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
|
||||
b1UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "conflicts")
|
||||
b2UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "conflicts (2)")
|
||||
|
||||
// Execute
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
|
||||
err = lm7.run(ctx, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "failed to run"))
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
var b1Label, b2Label string
|
||||
var b1Dirty, b2Dirty bool
|
||||
testutils.MustScan(t, "scanning b1 label", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b1UUID), &b1Label, &b1Dirty)
|
||||
testutils.MustScan(t, "scanning b2 label", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b2UUID), &b2Label, &b2Dirty)
|
||||
testutils.AssertEqual(t, b1Label, "conflicts (3)", "b1 label was not migrated")
|
||||
testutils.AssertEqual(t, b2Label, "conflicts (2)", "b1 label was not migrated")
|
||||
testutils.AssertEqual(t, b1Dirty, true, "b1 was not marked dirty")
|
||||
testutils.AssertEqual(t, b2Dirty, false, "b2 should not have been marked dirty")
|
||||
}
|
||||
|
||||
func TestRemoteMigration1(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/remote-1-pre-schema.sql", false)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
JSBookUUID := "existing-js-book-uuid"
|
||||
CSSBookUUID := "existing-css-book-uuid"
|
||||
linuxBookUUID := "existing-linux-book-uuid"
|
||||
newJSBookUUID := "new-js-book-uuid"
|
||||
newCSSBookUUID := "new-css-book-uuid"
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.String() == "/v1/books" {
|
||||
res := []struct {
|
||||
UUID string `json:"uuid"`
|
||||
Label string `json:"label"`
|
||||
}{
|
||||
{
|
||||
UUID: newJSBookUUID,
|
||||
Label: "js",
|
||||
},
|
||||
{
|
||||
UUID: newCSSBookUUID,
|
||||
Label: "css",
|
||||
},
|
||||
// book that only exists on the server. client must ignore.
|
||||
{
|
||||
UUID: "golang-book-uuid",
|
||||
Label: "golang",
|
||||
},
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "encoding response"))
|
||||
}
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
ctx.APIEndpoint = server.URL
|
||||
|
||||
confStr := fmt.Sprintf("apikey: mock_api_key")
|
||||
testutils.WriteFile(ctx, []byte(confStr), "dnoterc")
|
||||
|
||||
db := ctx.DB
|
||||
|
||||
testutils.MustExec(t, "inserting js book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", JSBookUUID, "js")
|
||||
testutils.MustExec(t, "inserting css book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", CSSBookUUID, "css")
|
||||
testutils.MustExec(t, "inserting linux book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", linuxBookUUID, "linux")
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
|
||||
err = rm1.run(ctx, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "failed to run"))
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// test
|
||||
var postJSBookUUID, postCSSBookUUID, postLinuxBookUUID string
|
||||
testutils.MustScan(t, "getting js book uuid", db.QueryRow("SELECT uuid FROM books WHERE label = ?", "js"), &postJSBookUUID)
|
||||
testutils.MustScan(t, "getting css book uuid", db.QueryRow("SELECT uuid FROM books WHERE label = ?", "css"), &postCSSBookUUID)
|
||||
testutils.MustScan(t, "getting linux book uuid", db.QueryRow("SELECT uuid FROM books WHERE label = ?", "linux"), &postLinuxBookUUID)
|
||||
|
||||
testutils.AssertEqual(t, postJSBookUUID, newJSBookUUID, "js book uuid was not updated correctly")
|
||||
testutils.AssertEqual(t, postCSSBookUUID, newCSSBookUUID, "css book uuid was not updated correctly")
|
||||
testutils.AssertEqual(t, postLinuxBookUUID, linuxBookUUID, "linux book uuid changed")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@ package migrate
|
|||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/dnote/actions"
|
||||
"github.com/dnote/cli/client"
|
||||
"github.com/dnote/cli/core"
|
||||
"github.com/dnote/cli/infra"
|
||||
"github.com/dnote/cli/log"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
|
@ -148,3 +152,223 @@ var lm3 = migration{
|
|||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var lm4 = migration{
|
||||
name: "add-dirty-usn-deleted-to-notes-and-books",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
_, err := tx.Exec("ALTER TABLE books ADD COLUMN dirty bool DEFAULT false")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "adding dirty column to books")
|
||||
}
|
||||
|
||||
_, err = tx.Exec("ALTER TABLE books ADD COLUMN usn int DEFAULT 0 NOT NULL")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "adding usn column to books")
|
||||
}
|
||||
|
||||
_, err = tx.Exec("ALTER TABLE books ADD COLUMN deleted bool DEFAULT false")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "adding deleted column to books")
|
||||
}
|
||||
|
||||
_, err = tx.Exec("ALTER TABLE notes ADD COLUMN dirty bool DEFAULT false")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "adding dirty column to notes")
|
||||
}
|
||||
|
||||
_, err = tx.Exec("ALTER TABLE notes ADD COLUMN usn int DEFAULT 0 NOT NULL")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "adding usn column to notes")
|
||||
}
|
||||
|
||||
_, err = tx.Exec("ALTER TABLE notes ADD COLUMN deleted bool DEFAULT false")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "adding deleted column to notes")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var lm5 = migration{
|
||||
name: "mark-action-targets-dirty",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
rows, err := tx.Query("SELECT uuid, data, type FROM actions")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "querying rows")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var uuid, dat, actionType string
|
||||
|
||||
err = rows.Scan(&uuid, &dat, &actionType)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "scanning a row")
|
||||
}
|
||||
|
||||
// removed notes and removed books cannot be reliably derived retrospectively
|
||||
// because books did not use to have uuid. Users will find locally deleted
|
||||
// notes and books coming back to existence if they have not synced the change.
|
||||
// But there will be no data loss.
|
||||
switch actionType {
|
||||
case "add_note":
|
||||
var data actions.AddNoteDataV2
|
||||
err = json.Unmarshal([]byte(dat), &data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unmarshalling existing data")
|
||||
}
|
||||
|
||||
_, err := tx.Exec("UPDATE notes SET dirty = true WHERE uuid = ?", data.NoteUUID)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "markig note dirty '%s'", data.NoteUUID)
|
||||
}
|
||||
case "edit_note":
|
||||
var data actions.EditNoteDataV3
|
||||
err = json.Unmarshal([]byte(dat), &data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unmarshalling existing data")
|
||||
}
|
||||
|
||||
_, err := tx.Exec("UPDATE notes SET dirty = true WHERE uuid = ?", data.NoteUUID)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "markig note dirty '%s'", data.NoteUUID)
|
||||
}
|
||||
case "add_book":
|
||||
var data actions.AddBookDataV1
|
||||
err = json.Unmarshal([]byte(dat), &data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unmarshalling existing data")
|
||||
}
|
||||
|
||||
_, err := tx.Exec("UPDATE books SET dirty = true WHERE label = ?", data.BookName)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "markig note dirty '%s'", data.BookName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var lm6 = migration{
|
||||
name: "drop-actions",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
_, err := tx.Exec("DROP TABLE actions;")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "dropping the actions table")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var lm7 = migration{
|
||||
name: "resolve-conflicts-with-reserved-book-names",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
migrateBook := func(name string) error {
|
||||
var uuid string
|
||||
|
||||
err := tx.QueryRow("SELECT uuid FROM books WHERE label = ?", name).Scan(&uuid)
|
||||
if err == sql.ErrNoRows {
|
||||
// if not found, noop
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "finding trash book")
|
||||
}
|
||||
|
||||
for i := 2; ; i++ {
|
||||
candidate := fmt.Sprintf("%s (%d)", name, i)
|
||||
|
||||
var count int
|
||||
err := tx.QueryRow("SELECT count(*) FROM books WHERE label = ?", candidate).Scan(&count)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "counting candidate")
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
_, err := tx.Exec("UPDATE books SET label = ?, dirty = ? WHERE uuid = ?", candidate, true, uuid)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "updating book '%s'", name)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := migrateBook("trash"); err != nil {
|
||||
return errors.Wrap(err, "migrating trash book")
|
||||
}
|
||||
if err := migrateBook("conflicts"); err != nil {
|
||||
return errors.Wrap(err, "migrating conflicts book")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var rm1 = migration{
|
||||
name: "sync-book-uuids-from-server",
|
||||
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
|
||||
config, err := core.ReadConfig(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading the config")
|
||||
}
|
||||
if config.APIKey == "" {
|
||||
return errors.New("login required")
|
||||
}
|
||||
|
||||
resp, err := client.GetBooks(ctx, config.APIKey)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting books from the server")
|
||||
}
|
||||
log.Debug("book details from the server: %+v\n", resp)
|
||||
|
||||
UUIDMap := map[string]string{}
|
||||
for _, book := range resp {
|
||||
// Build a map from uuid to label
|
||||
UUIDMap[book.Label] = book.UUID
|
||||
}
|
||||
|
||||
for _, book := range resp {
|
||||
// update uuid in the books table
|
||||
log.Debug("Updating book %s\n", book.Label)
|
||||
|
||||
//todo if does not exist, then continue loop
|
||||
var count int
|
||||
if err := tx.
|
||||
QueryRow("SELECT count(*) FROM books WHERE label = ?", book.Label).
|
||||
Scan(&count); err != nil {
|
||||
return errors.Wrapf(err, "checking if book exists: %s", book.Label)
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var originalUUID string
|
||||
if err := tx.
|
||||
QueryRow("SELECT uuid FROM books WHERE label = ?", book.Label).
|
||||
Scan(&originalUUID); err != nil {
|
||||
return errors.Wrapf(err, "scanning the orignal uuid of the book %s", book.Label)
|
||||
}
|
||||
log.Debug("original uuid: %s. new_uuid %s\n", originalUUID, book.UUID)
|
||||
|
||||
_, err := tx.Exec("UPDATE books SET uuid = ? WHERE label = ?", book.UUID, book.Label)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "updating book '%s'", book.Label)
|
||||
}
|
||||
|
||||
_, err = tx.Exec("UPDATE notes SET book_uuid = ? WHERE book_uuid = ?", book.UUID, originalUUID)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "updating book_uuids of notes")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
# dev.sh builds a new binary and replaces the old one in the PATH with it
|
||||
|
||||
rm "$(which dnote)" $GOPATH/bin/cli
|
||||
go install -ldflags "-X main.apiEndpoint=http://127.0.0.1:5000" --tags "darwin" .
|
||||
ln -s $GOPATH/bin/cli /usr/local/bin/dnote
|
||||
sudo rm "$(which dnote)" $GOPATH/bin/cli
|
||||
|
||||
# change tags to darwin if on macos
|
||||
go install -ldflags "-X main.apiEndpoint=http://127.0.0.1:5000" --tags "linux" .
|
||||
|
||||
sudo ln -s $GOPATH/bin/cli /usr/local/bin/dnote
|
||||
|
|
|
|||
|
|
@ -3,5 +3,9 @@
|
|||
# run_server_test.sh runs server test files sequentially
|
||||
# https://stackoverflow.com/questions/23715302/go-how-to-run-tests-for-multiple-packages
|
||||
|
||||
# clear tmp dir in case not properly torn down
|
||||
rm -rf ./tmp
|
||||
|
||||
# run test
|
||||
go test ./... -p 1
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"js": {
|
||||
"name": "js",
|
||||
"notes": [
|
||||
{
|
||||
"uuid": "43827b9a-c2b0-4c06-a290-97991c896653",
|
||||
"content": "Booleans have toString()",
|
||||
"added_on": 1515199943,
|
||||
"edited_on": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"js": {
|
||||
"name": "js",
|
||||
"notes": [
|
||||
{
|
||||
"uuid": "43827b9a-c2b0-4c06-a290-97991c896653",
|
||||
"content": "Booleans have toString()",
|
||||
"added_on": 1515199943,
|
||||
"edited_on": 0
|
||||
},
|
||||
{
|
||||
"uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
|
||||
"content": "Date object implements mathematical comparisons",
|
||||
"added_on": 1515199951,
|
||||
"edited_on": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"js": {
|
||||
"name": "js",
|
||||
"notes": [
|
||||
{
|
||||
"uuid": "43827b9a-c2b0-4c06-a290-97991c896653",
|
||||
"content": "Booleans have toString()",
|
||||
"added_on": 1515199943,
|
||||
"edited_on": 0
|
||||
},
|
||||
{
|
||||
"uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
|
||||
"content": "Date object implements mathematical comparisons",
|
||||
"added_on": 1515199951,
|
||||
"edited_on": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"name": "linux",
|
||||
"notes": [
|
||||
{
|
||||
"uuid": "3e065d55-6d47-42f2-a6bf-f5844130b2d2",
|
||||
"content": "wc -l to count words",
|
||||
"added_on": 1515199961,
|
||||
"edited_on": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"js": {
|
||||
"name": "js",
|
||||
"notes": [
|
||||
{
|
||||
"uuid": "43827b9a-c2b0-4c06-a290-97991c896653",
|
||||
"content": "Booleans have toString()",
|
||||
"added_on": 1515199943,
|
||||
"edited_on": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"name": "linux",
|
||||
"notes": []
|
||||
}
|
||||
}
|
||||
|
|
@ -7,20 +7,12 @@ CREATE TABLE notes
|
|||
added_on integer NOT NULL,
|
||||
edited_on integer DEFAULT 0,
|
||||
public bool DEFAULT false
|
||||
);
|
||||
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL, deleted bool DEFAULT false);
|
||||
CREATE TABLE books
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
label text NOT NULL
|
||||
);
|
||||
CREATE TABLE actions
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
schema integer NOT NULL,
|
||||
type text NOT NULL,
|
||||
data text NOT NULL,
|
||||
timestamp integer NOT NULL
|
||||
);
|
||||
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL, deleted bool DEFAULT false);
|
||||
CREATE TABLE system
|
||||
(
|
||||
key string NOT NULL,
|
||||
|
|
|
|||
|
|
@ -21,30 +21,41 @@ import (
|
|||
)
|
||||
|
||||
// InitEnv sets up a test env and returns a new dnote context
|
||||
func InitEnv(relPath string, relFixturePath string) infra.DnoteCtx {
|
||||
func InitEnv(t *testing.T, relPath string, relFixturePath string, migrated bool) infra.DnoteCtx {
|
||||
path, err := filepath.Abs(relPath)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "pasrsing path").Error())
|
||||
t.Fatal(errors.Wrap(err, "pasrsing path"))
|
||||
}
|
||||
|
||||
os.Setenv("DNOTE_HOME_DIR", path)
|
||||
ctx, err := infra.NewCtx("", "")
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "getting new ctx").Error())
|
||||
t.Fatal(errors.Wrap(err, "getting new ctx"))
|
||||
}
|
||||
|
||||
// set up directory and db
|
||||
// set up directory
|
||||
if err := os.MkdirAll(ctx.DnoteDir, 0755); err != nil {
|
||||
panic(err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// set up db
|
||||
b := ReadFileAbs(relFixturePath)
|
||||
setupSQL := string(b)
|
||||
|
||||
db := ctx.DB
|
||||
_, err = db.Exec(setupSQL)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "running schema sql").Error())
|
||||
if _, err := db.Exec(setupSQL); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "running schema sql"))
|
||||
}
|
||||
|
||||
if migrated {
|
||||
// mark migrations as done. When adding new migrations, bump the numbers here.
|
||||
if _, err := db.Exec("INSERT INTO system (key, value) VALUES (? , ?);", infra.SystemSchema, 6); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "inserting schema"))
|
||||
}
|
||||
|
||||
if _, err := db.Exec("INSERT INTO system (key, value) VALUES (? , ?);", infra.SystemRemoteSchema, 1); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "inserting remote schema"))
|
||||
}
|
||||
}
|
||||
|
||||
return ctx
|
||||
|
|
|
|||
|
|
@ -27,12 +27,12 @@ func Setup2(t *testing.T, ctx infra.DnoteCtx) {
|
|||
b1UUID := "js-book-uuid"
|
||||
b2UUID := "linux-book-uuid"
|
||||
|
||||
MustExec(t, "setting up book 1", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "js")
|
||||
MustExec(t, "setting up book 2", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "linux")
|
||||
MustExec(t, "setting up book 1", db, "INSERT INTO books (uuid, label, usn) VALUES (?, ?, ?)", b1UUID, "js", 111)
|
||||
MustExec(t, "setting up book 2", db, "INSERT INTO books (uuid, label, usn) VALUES (?, ?, ?)", b2UUID, "linux", 122)
|
||||
|
||||
MustExec(t, "setting up note 2", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?, ?)", 1, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", b1UUID, "Date object implements mathematical comparisons", 1515199951)
|
||||
MustExec(t, "setting up note 1", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?, ?)", 2, "43827b9a-c2b0-4c06-a290-97991c896653", b1UUID, "Booleans have toString()", 1515199943)
|
||||
MustExec(t, "setting up note 3", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?, ?)", 3, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", b2UUID, "wc -l to count words", 1515199961)
|
||||
MustExec(t, "setting up note 1", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on, usn) VALUES (?, ?, ?, ?, ?, ?)", 1, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", b1UUID, "n1 content", 1515199951, 11)
|
||||
MustExec(t, "setting up note 2", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on, usn) VALUES (?, ?, ?, ?, ?, ?)", 2, "43827b9a-c2b0-4c06-a290-97991c896653", b1UUID, "n2 content", 1515199943, 12)
|
||||
MustExec(t, "setting up note 3", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on, usn) VALUES (?, ?, ?, ?, ?, ?)", 3, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", b2UUID, "n3 content", 1515199961, 13)
|
||||
}
|
||||
|
||||
// Setup3 sets up a dnote env #1
|
||||
|
|
|
|||
|
|
@ -2,11 +2,15 @@ package utils
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/dnote/cli/infra"
|
||||
"github.com/dnote/cli/log"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/satori/go.uuid"
|
||||
|
|
@ -142,3 +146,24 @@ func CopyDir(src, dest string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DoAuthorizedReq does a http request to the given path in the api endpoint as a user,
|
||||
// with the appropriate headers. The given path should include the preceding slash.
|
||||
func DoAuthorizedReq(ctx infra.DnoteCtx, apiKey, method, path, body string) (*http.Response, error) {
|
||||
endpoint := fmt.Sprintf("%s%s", ctx.APIEndpoint, path)
|
||||
req, err := http.NewRequest(method, endpoint, strings.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "constructing http request")
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", apiKey)
|
||||
req.Header.Set("CLI-Version", ctx.Version)
|
||||
|
||||
hc := http.Client{}
|
||||
res, err := hc.Do(req)
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "making http request")
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue