/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors * * This file is part of Dnote. * * Dnote is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Dnote is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Dnote. If not, see . */ package main import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "net/http/httptest" "os" "os/exec" "strings" "testing" "time" "github.com/dnote/dnote/pkg/assert" "github.com/dnote/dnote/pkg/cli/consts" "github.com/dnote/dnote/pkg/cli/context" cliDatabase "github.com/dnote/dnote/pkg/cli/database" "github.com/dnote/dnote/pkg/cli/testutils" clitest "github.com/dnote/dnote/pkg/cli/testutils" "github.com/dnote/dnote/pkg/clock" "github.com/dnote/dnote/pkg/server/app" "github.com/dnote/dnote/pkg/server/controllers" "github.com/dnote/dnote/pkg/server/database" "github.com/dnote/dnote/pkg/server/mailer" apitest "github.com/dnote/dnote/pkg/server/testutils" "github.com/pkg/errors" "gorm.io/gorm" ) var cliBinaryName string var server *httptest.Server var serverDb *gorm.DB var serverTime = time.Date(2017, time.March, 14, 21, 15, 0, 0, time.UTC) var tmpDirPath string var dnoteCmdOpts clitest.RunDnoteCmdOptions var paths context.Paths var testDir = "./tmp/.dnote" func init() { tmpDirPath = fmt.Sprintf("%s/tmp", testDir) cliBinaryName = fmt.Sprintf("%s/test/cli/test-cli", testDir) dnoteCmdOpts = clitest.RunDnoteCmdOptions{ Env: []string{ fmt.Sprintf("XDG_CONFIG_HOME=%s", tmpDirPath), fmt.Sprintf("XDG_DATA_HOME=%s", tmpDirPath), fmt.Sprintf("XDG_CACHE_HOME=%s", tmpDirPath), }, } paths = context.Paths{ Data: tmpDirPath, Cache: tmpDirPath, Config: tmpDirPath, } } func clearTmp(t *testing.T) { if err := os.RemoveAll(tmpDirPath); err != nil { t.Fatal("cleaning tmp dir") } } // setupTestServer creates a test server with its own database func setupTestServer(dbPath string, serverTime time.Time) (*httptest.Server, *gorm.DB, error) { db := apitest.InitDB(dbPath) mockClock := clock.NewMock() mockClock.SetNow(serverTime) a := app.NewTest() a.Clock = mockClock a.EmailTemplates = mailer.Templates{} a.EmailBackend = &apitest.MockEmailbackendImplementation{} a.DB = db server, err := controllers.NewServer(&a) if err != nil { return nil, nil, errors.Wrap(err, "initializing server") } return server, db, nil } func TestMain(m *testing.M) { // Set up server database - use file-based DB for e2e tests dbPath := fmt.Sprintf("%s/server.db", testDir) var err error server, serverDb, err = setupTestServer(dbPath, serverTime) if err != nil { panic(err) } defer server.Close() // Build binaries apiEndpoint := fmt.Sprintf("%s/api", server.URL) ldflags := fmt.Sprintf("-X main.apiEndpoint=%s", apiEndpoint) cmd := exec.Command("go", "build", "--tags", "fts5", "-o", cliBinaryName, "-ldflags", ldflags, "github.com/dnote/dnote/pkg/cli") var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { log.Print(errors.Wrap(err, "building a CLI binary").Error()) log.Print(stderr.String()) os.Exit(1) } os.Exit(m.Run()) } // helpers func setupUser(t *testing.T, db *cliDatabase.DB) database.User { user := apitest.SetupUserData(serverDb) apitest.SetupAccountData(serverDb, user, "alice@example.com", "pass1234") return user } func setupUserAndLogin(t *testing.T, db *cliDatabase.DB) database.User { user := setupUser(t, db) login(t, db, user) return user } // log in the user in CLI func login(t *testing.T, db *cliDatabase.DB, user database.User) { session := apitest.SetupSession(serverDb, user) cliDatabase.MustExec(t, "inserting session_key", db, "INSERT INTO system (key, value) VALUES (?, ?)", consts.SystemSessionKey, session.Key) cliDatabase.MustExec(t, "inserting session_key_expiry", db, "INSERT INTO system (key, value) VALUES (?, ?)", consts.SystemSessionKeyExpiry, session.ExpiresAt.Unix()) } func apiCreateBook(t *testing.T, user database.User, name, message string) string { res := doHTTPReq(t, "POST", "/v3/books", fmt.Sprintf(`{"name": "%s"}`, name), message, user) var resp controllers.CreateBookResp if err := json.NewDecoder(res.Body).Decode(&resp); err != nil { t.Fatal(errors.Wrap(err, "decoding payload for adding book")) return "" } return resp.Book.UUID } func apiPatchBook(t *testing.T, user database.User, uuid, payload, message string) { doHTTPReq(t, "PATCH", fmt.Sprintf("/v3/books/%s", uuid), payload, message, user) } func apiDeleteBook(t *testing.T, user database.User, uuid, message string) { doHTTPReq(t, "DELETE", fmt.Sprintf("/v3/books/%s", uuid), "", message, user) } func apiCreateNote(t *testing.T, user database.User, bookUUID, body, message string) string { res := doHTTPReq(t, "POST", "/v3/notes", fmt.Sprintf(`{"book_uuid": "%s", "content": "%s"}`, bookUUID, body), message, user) var resp controllers.CreateNoteResp if err := json.NewDecoder(res.Body).Decode(&resp); err != nil { t.Fatal(errors.Wrap(err, "decoding payload for adding note")) return "" } return resp.Result.UUID } func apiPatchNote(t *testing.T, user database.User, noteUUID, payload, message string) { doHTTPReq(t, "PATCH", fmt.Sprintf("/v3/notes/%s", noteUUID), payload, message, user) } func apiDeleteNote(t *testing.T, user database.User, noteUUID, message string) { doHTTPReq(t, "DELETE", fmt.Sprintf("/v3/notes/%s", noteUUID), "", message, user) } func doHTTPReq(t *testing.T, method, path, payload, message string, user database.User) *http.Response { apiEndpoint := fmt.Sprintf("%s/api", server.URL) endpoint := fmt.Sprintf("%s%s", apiEndpoint, path) req, err := http.NewRequest(method, endpoint, strings.NewReader(payload)) if err != nil { panic(errors.Wrap(err, "constructing http request")) } res := apitest.HTTPAuthDo(t, serverDb, req, user) if res.StatusCode >= 400 { bs, err := io.ReadAll(res.Body) if err != nil { panic(errors.Wrap(err, "parsing response body for error")) } t.Errorf("%s. HTTP status %d. Message: %s", message, res.StatusCode, string(bs)) } return res } type setupFunc func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string type assertFunc func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) func testSyncCmd(t *testing.T, fullSync bool, setup setupFunc, assert assertFunc) { // clean up apitest.ClearData(serverDb) defer apitest.ClearData(serverDb) clearTmp(t) ctx := context.InitTestCtx(t, paths, nil) defer context.TeardownTestCtx(t, ctx) user := setupUserAndLogin(t, ctx.DB) ids := setup(t, ctx, user) if fullSync { clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync", "-f") } else { clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") } assert(t, ctx, user, ids) } type systemState struct { clientNoteCount int clientBookCount int clientLastMaxUSN int clientLastSyncAt int64 serverNoteCount int64 serverBookCount int64 serverUserMaxUSN int } // checkState compares the state of the client and the server with the given system state func checkState(t *testing.T, ctx context.DnoteCtx, user database.User, expected systemState) { checkStateWithDB(t, ctx.DB, user, serverDb, expected) } func checkStateWithDB(t *testing.T, clientDB *cliDatabase.DB, user database.User, serverDB *gorm.DB, expected systemState) { var clientBookCount, clientNoteCount int cliDatabase.MustScan(t, "counting client notes", clientDB.QueryRow("SELECT count(*) FROM notes"), &clientNoteCount) cliDatabase.MustScan(t, "counting client books", clientDB.QueryRow("SELECT count(*) FROM books"), &clientBookCount) assert.Equal(t, clientNoteCount, expected.clientNoteCount, "client note count mismatch") assert.Equal(t, clientBookCount, expected.clientBookCount, "client book count mismatch") var clientLastMaxUSN int var clientLastSyncAt int64 cliDatabase.MustScan(t, "finding system last_max_usn", clientDB.QueryRow("SELECT value FROM system WHERE key = ?", consts.SystemLastMaxUSN), &clientLastMaxUSN) cliDatabase.MustScan(t, "finding system last_sync_at", clientDB.QueryRow("SELECT value FROM system WHERE key = ?", consts.SystemLastSyncAt), &clientLastSyncAt) assert.Equal(t, clientLastMaxUSN, expected.clientLastMaxUSN, "client last_max_usn mismatch") assert.Equal(t, clientLastSyncAt, expected.clientLastSyncAt, "client last_sync_at mismatch") var serverBookCount, serverNoteCount int64 apitest.MustExec(t, serverDB.Model(&database.Note{}).Count(&serverNoteCount), "counting server notes") apitest.MustExec(t, serverDB.Model(&database.Book{}).Count(&serverBookCount), "counting api notes") assert.Equal(t, serverNoteCount, expected.serverNoteCount, "server note count mismatch") assert.Equal(t, serverBookCount, expected.serverBookCount, "server book count mismatch") var serverUser database.User apitest.MustExec(t, serverDB.Where("id = ?", user.ID).First(&serverUser), "finding user") assert.Equal(t, serverUser.MaxUSN, expected.serverUserMaxUSN, "user max_usn mismatch") } // tests func TestSync_Empty(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { return map[string]string{} } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { // Test checkState(t, ctx, user, systemState{ clientNoteCount: 0, clientBookCount: 0, clientLastMaxUSN: 0, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 0, serverBookCount: 0, serverUserMaxUSN: 0, }) } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) } func TestSync_oneway(t *testing.T) { t.Run("cli to api only", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) { apitest.MustExec(t, serverDb.Model(&user).Update("max_usn", 0), "updating user max_usn") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js2") } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User) { cliDB := ctx.DB // test client checkState(t, ctx, user, systemState{ clientNoteCount: 3, clientBookCount: 2, clientLastMaxUSN: 5, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 3, serverBookCount: 2, serverUserMaxUSN: 5, }) var cliBookJS, cliBookCSS cliDatabase.Book var cliNote1JS, cliNote2JS, cliNote1CSS cliDatabase.Note cliDatabase.MustScan(t, "finding cli book js", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cli book css", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css"), &cliBookCSS.UUID, &cliBookCSS.Label, &cliBookCSS.USN) cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js1"), &cliNote1JS.UUID, &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliNote2JS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js2"), &cliNote2JS.UUID, &cliNote2JS.Body, &cliNote2JS.USN) cliDatabase.MustScan(t, "finding cliNote1CSS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "css1"), &cliNote1CSS.UUID, &cliNote1CSS.Body, &cliNote1CSS.USN) // assert on usn assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") assert.NotEqual(t, cliNote2JS.USN, 0, "cliNote2JS USN mismatch") assert.NotEqual(t, cliNote1CSS.USN, 0, "cliNote1CSS USN mismatch") assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") assert.NotEqual(t, cliBookCSS.USN, 0, "cliBookCSS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote2JS.Body, "js2", "cliNote2JS Body mismatch") assert.Equal(t, cliNote1CSS.Body, "css1", "cliNote1CSS Body mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") assert.Equal(t, cliBookCSS.Label, "css", "cliBookCSS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliNote2JS.Deleted, false, "cliNote2JS Deleted mismatch") assert.Equal(t, cliNote1CSS.Deleted, false, "cliNote1CSS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") assert.Equal(t, cliBookCSS.Deleted, false, "cliBookCSS Deleted mismatch") // test server var apiBookJS, apiBookCSS database.Book var apiNote1JS, apiNote2JS, apiNote1CSS database.Note apitest.MustExec(t, serverDb.Model(&database.Note{}).Where("uuid = ?", cliNote1JS.UUID).First(&apiNote1JS), "getting js1 note") apitest.MustExec(t, serverDb.Model(&database.Note{}).Where("uuid = ?", cliNote2JS.UUID).First(&apiNote2JS), "getting js2 note") apitest.MustExec(t, serverDb.Model(&database.Note{}).Where("uuid = ?", cliNote1CSS.UUID).First(&apiNote1CSS), "getting css1 note") apitest.MustExec(t, serverDb.Model(&database.Book{}).Where("uuid = ?", cliBookJS.UUID).First(&apiBookJS), "getting js book") apitest.MustExec(t, serverDb.Model(&database.Book{}).Where("uuid = ?", cliBookCSS.UUID).First(&apiBookCSS), "getting css book") // assert usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS usn mismatch") assert.NotEqual(t, apiNote2JS.USN, 0, "apiNote2JS usn mismatch") assert.NotEqual(t, apiNote1CSS.USN, 0, "apiNote1CSS usn mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS usn mismatch") assert.NotEqual(t, apiBookCSS.USN, 0, "apiBookCSS usn mismatch") // client must have generated uuids assert.NotEqual(t, apiNote1JS.UUID, "", "apiNote1JS UUID mismatch") assert.NotEqual(t, apiNote2JS.UUID, "", "apiNote2JS UUID mismatch") assert.NotEqual(t, apiNote1CSS.UUID, "", "apiNote1CSS UUID mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiNote2JS.Deleted, false, "apiNote2JS Deleted mismatch") assert.Equal(t, apiNote1CSS.Deleted, false, "apiNote1CSS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") assert.Equal(t, apiBookCSS.Deleted, false, "apiBookCSS Deleted mismatch") // assert on body and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiNote2JS.Body, "js2", "apiNote2JS Body mismatch") assert.Equal(t, apiNote1CSS.Body, "css1", "apiNote1CSS Body mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") assert.Equal(t, apiBookCSS.Label, "css", "apiBookCSS Label mismatch") } t.Run("stepSync", func(t *testing.T) { clearTmp(t) defer apitest.ClearData(serverDb) ctx := context.InitTestCtx(t, paths, nil) defer context.TeardownTestCtx(t, ctx) user := setupUserAndLogin(t, ctx.DB) setup(t, ctx, user) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") assert(t, ctx, user) }) t.Run("fullSync", func(t *testing.T) { clearTmp(t) defer apitest.ClearData(serverDb) ctx := context.InitTestCtx(t, paths, nil) defer context.TeardownTestCtx(t, ctx) user := setupUserAndLogin(t, ctx.DB) setup(t, ctx, user) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync", "-f") assert(t, ctx, user) }) }) t.Run("cli to api with edit and delete", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) { apiDB := serverDb apitest.MustExec(t, apiDB.Model(&user).Update("max_usn", 0), "updating user max_usn") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js2") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js3") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css2") var nid, nid2 string cliDB := ctx.DB cliDatabase.MustScan(t, "getting id of note to edit", cliDB.QueryRow("SELECT rowid FROM notes WHERE body = ?", "js3"), &nid) cliDatabase.MustScan(t, "getting id of note to delete", cliDB.QueryRow("SELECT rowid FROM notes WHERE body = ?", "css2"), &nid2) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "edit", "js", nid, "-c", "js3-edited") clitest.MustWaitDnoteCmd(t, dnoteCmdOpts, clitest.ConfirmRemoveNote, cliBinaryName, "remove", "css", nid2) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css3") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css4") } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 6, clientBookCount: 2, clientLastMaxUSN: 8, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 6, serverBookCount: 2, serverUserMaxUSN: 8, }) // test cli var cliN1, cliN2, cliN3, cliN4, cliN5, cliN6 cliDatabase.Note var cliB1, cliB2 cliDatabase.Book cliDatabase.MustScan(t, "finding cliN1", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js1"), &cliN1.UUID, &cliN1.Body, &cliN1.USN) cliDatabase.MustScan(t, "finding cliN2", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js2"), &cliN2.UUID, &cliN2.Body, &cliN2.USN) cliDatabase.MustScan(t, "finding cliN3", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js3-edited"), &cliN3.UUID, &cliN3.Body, &cliN3.USN) cliDatabase.MustScan(t, "finding cliN4", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "css1"), &cliN4.UUID, &cliN4.Body, &cliN4.USN) cliDatabase.MustScan(t, "finding cliN5", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "css3"), &cliN5.UUID, &cliN5.Body, &cliN5.USN) cliDatabase.MustScan(t, "finding cliN6", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "css4"), &cliN6.UUID, &cliN6.Body, &cliN6.USN) cliDatabase.MustScan(t, "finding cliB1", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliB1.UUID, &cliB1.Label, &cliB1.USN) cliDatabase.MustScan(t, "finding cliB2", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css"), &cliB2.UUID, &cliB2.Label, &cliB2.USN) // assert on usn assert.NotEqual(t, cliN1.USN, 0, "cliN1 USN mismatch") assert.NotEqual(t, cliN2.USN, 0, "cliN2 USN mismatch") assert.NotEqual(t, cliN3.USN, 0, "cliN3 USN mismatch") assert.NotEqual(t, cliN4.USN, 0, "cliN4 USN mismatch") assert.NotEqual(t, cliN5.USN, 0, "cliN5 USN mismatch") assert.NotEqual(t, cliN6.USN, 0, "cliN6 USN mismatch") assert.NotEqual(t, cliB1.USN, 0, "cliB1 USN mismatch") assert.NotEqual(t, cliB2.USN, 0, "cliB2 USN mismatch") // assert on bodys and labels assert.Equal(t, cliN1.Body, "js1", "cliN1 Body mismatch") assert.Equal(t, cliN2.Body, "js2", "cliN2 Body mismatch") assert.Equal(t, cliN3.Body, "js3-edited", "cliN3 Body mismatch") assert.Equal(t, cliN4.Body, "css1", "cliN4 Body mismatch") assert.Equal(t, cliN5.Body, "css3", "cliN5 Body mismatch") assert.Equal(t, cliN6.Body, "css4", "cliN6 Body mismatch") assert.Equal(t, cliB1.Label, "js", "cliB1 Label mismatch") assert.Equal(t, cliB2.Label, "css", "cliB2 Label mismatch") // assert on deleted assert.Equal(t, cliN1.Deleted, false, "cliN1 Deleted mismatch") assert.Equal(t, cliN2.Deleted, false, "cliN2 Deleted mismatch") assert.Equal(t, cliN3.Deleted, false, "cliN3 Deleted mismatch") assert.Equal(t, cliN4.Deleted, false, "cliN4 Deleted mismatch") assert.Equal(t, cliN5.Deleted, false, "cliN5 Deleted mismatch") assert.Equal(t, cliN6.Deleted, false, "cliN6 Deleted mismatch") assert.Equal(t, cliB1.Deleted, false, "cliB1 Deleted mismatch") assert.Equal(t, cliB2.Deleted, false, "cliB2 Deleted mismatch") // test api var apiN1, apiN2, apiN3, apiN4, apiN5, apiN6 database.Note var apiB1, apiB2 database.Book apitest.MustExec(t, apiDB.Where("uuid = ?", cliN1.UUID).First(&apiN1), "finding apiN1") apitest.MustExec(t, apiDB.Where("uuid = ?", cliN2.UUID).First(&apiN2), "finding apiN2") apitest.MustExec(t, apiDB.Where("uuid = ?", cliN3.UUID).First(&apiN3), "finding apiN3") apitest.MustExec(t, apiDB.Where("uuid = ?", cliN4.UUID).First(&apiN4), "finding apiN4") apitest.MustExec(t, apiDB.Where("uuid = ?", cliN5.UUID).First(&apiN5), "finding apiN5") apitest.MustExec(t, apiDB.Where("uuid = ?", cliN6.UUID).First(&apiN6), "finding apiN6") apitest.MustExec(t, apiDB.Where("uuid = ?", cliB1.UUID).First(&apiB1), "finding apiB1") apitest.MustExec(t, apiDB.Where("uuid = ?", cliB2.UUID).First(&apiB2), "finding apiB2") // assert on usn assert.NotEqual(t, apiN1.USN, 0, "apiN1 usn mismatch") assert.NotEqual(t, apiN2.USN, 0, "apiN2 usn mismatch") assert.NotEqual(t, apiN3.USN, 0, "apiN3 usn mismatch") assert.NotEqual(t, apiN4.USN, 0, "apiN4 usn mismatch") assert.NotEqual(t, apiN5.USN, 0, "apiN5 usn mismatch") assert.NotEqual(t, apiN6.USN, 0, "apiN6 usn mismatch") assert.NotEqual(t, apiB1.USN, 0, "apiB1 usn mismatch") assert.NotEqual(t, apiB2.USN, 0, "apiB2 usn mismatch") // client must have generated uuids assert.NotEqual(t, apiN1.UUID, "", "apiN1 UUID mismatch") assert.NotEqual(t, apiN2.UUID, "", "apiN2 UUID mismatch") assert.NotEqual(t, apiN3.UUID, "", "apiN3 UUID mismatch") assert.NotEqual(t, apiN4.UUID, "", "apiN4 UUID mismatch") assert.NotEqual(t, apiN5.UUID, "", "apiN5 UUID mismatch") assert.NotEqual(t, apiN6.UUID, "", "apiN6 UUID mismatch") assert.NotEqual(t, apiB1.UUID, "", "apiB1 UUID mismatch") assert.NotEqual(t, apiB2.UUID, "", "apiB2 UUID mismatch") // assert on deleted assert.Equal(t, apiN1.Deleted, false, "apiN1 Deleted mismatch") assert.Equal(t, apiN2.Deleted, false, "apiN2 Deleted mismatch") assert.Equal(t, apiN3.Deleted, false, "apiN3 Deleted mismatch") assert.Equal(t, apiN4.Deleted, false, "apiN4 Deleted mismatch") assert.Equal(t, apiN5.Deleted, false, "apiN5 Deleted mismatch") assert.Equal(t, apiN6.Deleted, false, "apiN6 Deleted mismatch") assert.Equal(t, apiB1.Deleted, false, "apiB1 Deleted mismatch") assert.Equal(t, apiB2.Deleted, false, "apiB2 Deleted mismatch") // assert on body and labels assert.Equal(t, apiN1.Body, "js1", "apiN1 Body mismatch") assert.Equal(t, apiN2.Body, "js2", "apiN2 Body mismatch") assert.Equal(t, apiN3.Body, "js3-edited", "apiN3 Body mismatch") assert.Equal(t, apiN4.Body, "css1", "apiN4 Body mismatch") assert.Equal(t, apiN5.Body, "css3", "apiN5 Body mismatch") assert.Equal(t, apiN6.Body, "css4", "apiN6 Body mismatch") assert.Equal(t, apiB1.Label, "js", "apiB1 Label mismatch") assert.Equal(t, apiB2.Label, "css", "apiB2 Label mismatch") } t.Run("stepSync", func(t *testing.T) { clearTmp(t) defer apitest.ClearData(serverDb) ctx := context.InitTestCtx(t, paths, nil) defer context.TeardownTestCtx(t, ctx) user := setupUserAndLogin(t, ctx.DB) setup(t, ctx, user) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") assert(t, ctx, user) }) t.Run("fullSync", func(t *testing.T) { clearTmp(t) defer apitest.ClearData(serverDb) ctx := context.InitTestCtx(t, paths, nil) defer context.TeardownTestCtx(t, ctx) user := setupUserAndLogin(t, ctx.DB) setup(t, ctx, user) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync", "-f") assert(t, ctx, user) }) }) t.Run("api to cli", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { apiDB := serverDb apitest.MustExec(t, apiDB.Model(&user).Update("max_usn", 0), "updating user max_usn") jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") cssBookUUID := apiCreateBook(t, user, "css", "adding css book") cssNote1UUID := apiCreateNote(t, user, cssBookUUID, "css1", "adding css note 1") jsNote2UUID := apiCreateNote(t, user, jsBookUUID, "js2", "adding js note 2") cssNote2UUID := apiCreateNote(t, user, cssBookUUID, "css2", "adding css note 2") linuxBookUUID := apiCreateBook(t, user, "linux", "adding linux book") linuxNote1UUID := apiCreateNote(t, user, linuxBookUUID, "linux1", "adding linux note 1") apiPatchNote(t, user, jsNote2UUID, fmt.Sprintf(`{"book_uuid": "%s"}`, linuxBookUUID), "moving js note 2 to linux") apiDeleteNote(t, user, jsNote1UUID, "deleting js note 1") cssNote3UUID := apiCreateNote(t, user, cssBookUUID, "css3", "adding css note 3") bashBookUUID := apiCreateBook(t, user, "bash", "adding bash book") bashNote1UUID := apiCreateNote(t, user, bashBookUUID, "bash1", "adding bash note 1") // delete the linux book and its two notes apiDeleteBook(t, user, linuxBookUUID, "deleting linux book") apiPatchNote(t, user, cssNote2UUID, fmt.Sprintf(`{"content": "%s"}`, "css2-edited"), "editing css 2 body") bashNote2UUID := apiCreateNote(t, user, bashBookUUID, "bash2", "adding bash note 2") linuxBook2UUID := apiCreateBook(t, user, "linux", "adding new linux book") linux2Note1UUID := apiCreateNote(t, user, linuxBookUUID, "linux-new-1", "adding linux note 1") apiDeleteBook(t, user, jsBookUUID, "deleting js book") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, "jsNote2UUID": jsNote2UUID, "cssBookUUID": cssBookUUID, "cssNote1UUID": cssNote1UUID, "cssNote2UUID": cssNote2UUID, "cssNote3UUID": cssNote3UUID, "linuxBookUUID": linuxBookUUID, "linuxNote1UUID": linuxNote1UUID, "bashBookUUID": bashBookUUID, "bashNote1UUID": bashNote1UUID, "bashNote2UUID": bashNote2UUID, "linuxBook2UUID": linuxBook2UUID, "linux2Note1UUID": linux2Note1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 6, clientBookCount: 3, clientLastMaxUSN: 21, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 9, serverBookCount: 5, serverUserMaxUSN: 21, }) // test server var apiNote1JS, apiNote2JS, apiNote1CSS, apiNote2CSS, apiNote3CSS, apiNote1Bash, apiNote2Bash, apiNote1Linux, apiNote2Linux, apiNote1LinuxDup database.Note var apiBookJS, apiBookCSS, apiBookBash, apiBookLinux, apiBookLinuxDup database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding api js note 1") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote2UUID"]).First(&apiNote2JS), "finding api js note 2") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssNote1UUID"]).First(&apiNote1CSS), "finding api css note 1") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssNote2UUID"]).First(&apiNote2CSS), "finding api css note 2") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssNote3UUID"]).First(&apiNote3CSS), "finding api css note 3") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["linuxNote1UUID"]).First(&apiNote1Linux), "finding api linux note 1") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote2UUID"]).First(&apiNote2Linux), "finding api linux note 2") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["bashNote1UUID"]).First(&apiNote1Bash), "finding api bash note 1") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["bashNote2UUID"]).First(&apiNote2Bash), "finding api bash note 2") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["linux2Note1UUID"]).First(&apiNote1LinuxDup), "finding api linux 2 note 1") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding api js book") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssBookUUID"]).First(&apiBookCSS), "finding api css book") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["bashBookUUID"]).First(&apiBookBash), "finding api bash book") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["linuxBookUUID"]).First(&apiBookLinux), "finding api linux book") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["linuxBook2UUID"]).First(&apiBookLinuxDup), "finding api linux book 2") // assert on server Label assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiNote2JS.USN, 0, "apiNote2JS USN mismatch") assert.NotEqual(t, apiNote1CSS.USN, 0, "apiNote1CSS USN mismatch") assert.NotEqual(t, apiNote2CSS.USN, 0, "apiNote2CSS USN mismatch") assert.NotEqual(t, apiNote3CSS.USN, 0, "apiNote3CSS USN mismatch") assert.NotEqual(t, apiNote1Linux.USN, 0, "apiNote1Linux USN mismatch") assert.NotEqual(t, apiNote2Linux.USN, 0, "apiNote2Linux USN mismatch") assert.NotEqual(t, apiNote1Bash.USN, 0, "apiNote1Bash USN mismatch") assert.NotEqual(t, apiNote2Bash.USN, 0, "apiNote2Bash USN mismatch") assert.NotEqual(t, apiNote1LinuxDup.USN, 0, "apiNote1LinuxDup USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apibookJS USN mismatch") assert.NotEqual(t, apiBookCSS.USN, 0, "apibookCSS USN mismatch") assert.NotEqual(t, apiBookBash.USN, 0, "apibookBash USN mismatch") assert.NotEqual(t, apiBookLinux.USN, 0, "apibookLinux USN mismatch") assert.NotEqual(t, apiBookLinuxDup.USN, 0, "apiBookLinuxDup USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "", "apiNote1JS Body mismatch") assert.Equal(t, apiNote2JS.Body, "", "apiNote2JS Body mismatch") assert.Equal(t, apiNote1CSS.Body, "css1", "apiNote1CSS Body mismatch") assert.Equal(t, apiNote2CSS.Body, "css2-edited", "apiNote2CSS Body mismatch") assert.Equal(t, apiNote3CSS.Body, "css3", "apiNote3CSS Body mismatch") assert.Equal(t, apiNote1Linux.Body, "", "apiNote1Linux Body mismatch") assert.Equal(t, apiNote2Linux.Body, "", "apiNote2Linux Body mismatch") assert.Equal(t, apiNote1Bash.Body, "bash1", "apiNote1Bash Body mismatch") assert.Equal(t, apiNote2Bash.Body, "bash2", "apiNote2Bash Body mismatch") assert.Equal(t, apiNote1LinuxDup.Body, "linux-new-1", "apiNote1LinuxDup Body mismatch") assert.Equal(t, apiBookJS.Label, "", "apibookJS Label mismatch") assert.Equal(t, apiBookCSS.Label, "css", "apibookCSS Label mismatch") assert.Equal(t, apiBookBash.Label, "bash", "apibookBash Label mismatch") assert.Equal(t, apiBookLinux.Label, "", "apibookLinux Label mismatch") assert.Equal(t, apiBookLinuxDup.Label, "linux", "apiBookLinuxDup Label mismatch") // assert on uuids assert.NotEqual(t, apiNote1JS.UUID, "", "apiNote1JS UUID mismatch") assert.NotEqual(t, apiNote2JS.UUID, "", "apiNote2JS UUID mismatch") assert.NotEqual(t, apiNote1CSS.UUID, "", "apiNote1CSS UUID mismatch") assert.NotEqual(t, apiNote2CSS.UUID, "", "apiNote2CSS UUID mismatch") assert.NotEqual(t, apiNote3CSS.UUID, "", "apiNote3CSS UUID mismatch") assert.NotEqual(t, apiNote1Linux.UUID, "", "apiNote1Linux UUID mismatch") assert.NotEqual(t, apiNote2Linux.UUID, "", "apiNote2Linux UUID mismatch") assert.NotEqual(t, apiNote1Bash.UUID, "", "apiNote1Bash UUID mismatch") assert.NotEqual(t, apiNote2Bash.UUID, "", "apiNote2Bash UUID mismatch") assert.NotEqual(t, apiNote2Bash.UUID, "", "apiNote2Bash UUID mismatch") assert.NotEqual(t, apiBookJS.UUID, "", "apibookJS UUID mismatch") assert.NotEqual(t, apiBookCSS.UUID, "", "apibookCSS UUID mismatch") assert.NotEqual(t, apiBookBash.UUID, "", "apibookBash UUID mismatch") assert.NotEqual(t, apiBookLinux.UUID, "", "apibookLinux UUID mismatch") assert.NotEqual(t, apiBookLinuxDup.UUID, "", "apiBookLinuxDup UUID mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, true, "apiNote1JS Deleted mismatch") assert.Equal(t, apiNote2JS.Deleted, true, "apiNote2JS Deleted mismatch") assert.Equal(t, apiNote1CSS.Deleted, false, "apiNote1CSS Deleted mismatch") assert.Equal(t, apiNote2CSS.Deleted, false, "apiNote2CSS Deleted mismatch") assert.Equal(t, apiNote3CSS.Deleted, false, "apiNote3CSS Deleted mismatch") assert.Equal(t, apiNote1Linux.Deleted, true, "apiNote1Linux Deleted mismatch") assert.Equal(t, apiNote2Linux.Deleted, true, "apiNote2Linux Deleted mismatch") assert.Equal(t, apiNote1Bash.Deleted, false, "apiNote1Bash Deleted mismatch") assert.Equal(t, apiNote2Bash.Deleted, false, "apiNote2Bash Deleted mismatch") assert.Equal(t, apiNote2Bash.Deleted, false, "apiNote2Bash Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, true, "apibookJS Deleted mismatch") assert.Equal(t, apiBookCSS.Deleted, false, "apibookCSS Deleted mismatch") assert.Equal(t, apiBookBash.Deleted, false, "apibookBash Deleted mismatch") assert.Equal(t, apiBookLinux.Deleted, true, "apibookLinux Deleted mismatch") assert.Equal(t, apiBookLinuxDup.Deleted, false, "apiBookLinuxDup Deleted mismatch") // test client var cliBookCSS, cliBookBash, cliBookLinux cliDatabase.Book var cliNote1CSS, cliNote2CSS, cliNote3CSS, cliNote1Bash, cliNote2Bash, cliNote1Linux cliDatabase.Note cliDatabase.MustScan(t, "finding cli book css", cliDB.QueryRow("SELECT label FROM books WHERE uuid = ?", ids["cssBookUUID"]), &cliBookCSS.Label) cliDatabase.MustScan(t, "finding cli book bash", cliDB.QueryRow("SELECT label FROM books WHERE uuid = ?", ids["bashBookUUID"]), &cliBookBash.Label) cliDatabase.MustScan(t, "finding cli book linux2", cliDB.QueryRow("SELECT label FROM books WHERE uuid = ?", ids["linuxBook2UUID"]), &cliBookLinux.Label) cliDatabase.MustScan(t, "finding cliNote1CSS", cliDB.QueryRow("SELECT body, usn FROM notes WHERE uuid = ?", apiNote1CSS.UUID), &cliNote1CSS.Body, &cliNote1CSS.USN) cliDatabase.MustScan(t, "finding cliNote2CSS", cliDB.QueryRow("SELECT body, usn FROM notes WHERE uuid = ?", apiNote2CSS.UUID), &cliNote2CSS.Body, &cliNote2CSS.USN) cliDatabase.MustScan(t, "finding cliNote3CSS", cliDB.QueryRow("SELECT body, usn FROM notes WHERE uuid = ?", apiNote3CSS.UUID), &cliNote3CSS.Body, &cliNote3CSS.USN) cliDatabase.MustScan(t, "finding cliNote1Bash", cliDB.QueryRow("SELECT body, usn FROM notes WHERE uuid = ?", apiNote1Bash.UUID), &cliNote1Bash.Body, &cliNote1Bash.USN) cliDatabase.MustScan(t, "finding cliNote2Bash", cliDB.QueryRow("SELECT body, usn FROM notes WHERE uuid = ?", apiNote2Bash.UUID), &cliNote2Bash.Body, &cliNote2Bash.USN) cliDatabase.MustScan(t, "finding cliNote2Bash", cliDB.QueryRow("SELECT body, usn FROM notes WHERE uuid = ?", apiNote1LinuxDup.UUID), &cliNote1Linux.Body, &cliNote1Linux.USN) // assert on usn assert.NotEqual(t, cliNote1CSS.USN, 0, "cliNote1CSS usn mismatch") assert.NotEqual(t, cliNote2CSS.USN, 0, "cliNote2CSS usn mismatch") assert.NotEqual(t, cliNote3CSS.USN, 0, "cliNote3CSS usn mismatch") assert.NotEqual(t, cliNote1Bash.USN, 0, "cliNote1Bash usn mismatch") assert.NotEqual(t, cliNote2Bash.USN, 0, "cliNote2Bash usn mismatch") assert.NotEqual(t, cliNote1Linux.USN, 0, "cliNote1Linux usn mismatch") // assert on bodys and labels assert.Equal(t, cliNote1CSS.Body, "css1", "cliNote1CSS Body mismatch") assert.Equal(t, cliNote2CSS.Body, "css2-edited", "cliNote2CSS Body mismatch") assert.Equal(t, cliNote3CSS.Body, "css3", "cliNote3CSS Body mismatch") assert.Equal(t, cliNote1Bash.Body, "bash1", "cliNote1Bash Body mismatch") assert.Equal(t, cliNote2Bash.Body, "bash2", "cliNote2Bash Body mismatch") assert.Equal(t, cliNote1Linux.Body, "linux-new-1", "cliNote1Linux Body mismatch") assert.Equal(t, cliBookCSS.Label, "css", "cliBookCSS Label mismatch") assert.Equal(t, cliBookBash.Label, "bash", "cliBookBash Label mismatch") assert.Equal(t, cliBookLinux.Label, "linux", "cliBookLinux Label mismatch") // assert on deleted assert.Equal(t, cliNote1CSS.Deleted, false, "cliNote1CSS Deleted mismatch") assert.Equal(t, cliNote2CSS.Deleted, false, "cliNote2CSS Deleted mismatch") assert.Equal(t, cliNote3CSS.Deleted, false, "cliNote3CSS Deleted mismatch") assert.Equal(t, cliNote1Bash.Deleted, false, "cliNote1Bash Deleted mismatch") assert.Equal(t, cliNote2Bash.Deleted, false, "cliNote2Bash Deleted mismatch") assert.Equal(t, cliNote1Linux.Deleted, false, "cliNote1Linux Deleted mismatch") assert.Equal(t, cliBookCSS.Deleted, false, "cliBookCSS Deleted mismatch") assert.Equal(t, cliBookBash.Deleted, false, "cliBookBash Deleted mismatch") assert.Equal(t, cliBookLinux.Deleted, false, "cliBookLinux Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) } func TestSync_twoway(t *testing.T) { t.Run("once", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { cliDB := ctx.DB jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") cssBookUUID := apiCreateBook(t, user, "css", "adding css book") cssNote1UUID := apiCreateNote(t, user, cssBookUUID, "css1", "adding css note 1") jsNote2UUID := apiCreateNote(t, user, jsBookUUID, "js2", "adding js note 2") cssNote2UUID := apiCreateNote(t, user, cssBookUUID, "css2", "adding css note 2") linuxBookUUID := apiCreateBook(t, user, "linux", "adding linux book") linuxNote1UUID := apiCreateNote(t, user, linuxBookUUID, "linux1", "adding linux note 1") apiPatchNote(t, user, jsNote2UUID, fmt.Sprintf(`{"book_uuid": "%s"}`, linuxBookUUID), "moving js note 2 to linux") apiDeleteNote(t, user, jsNote1UUID, "deleting js note 1") cssNote3UUID := apiCreateNote(t, user, cssBookUUID, "css3", "adding css note 3") bashBookUUID := apiCreateBook(t, user, "bash", "adding bash book") bashNote1UUID := apiCreateNote(t, user, bashBookUUID, "bash1", "adding bash note 1") apiDeleteBook(t, user, linuxBookUUID, "deleting linux book") apiPatchNote(t, user, cssNote2UUID, fmt.Sprintf(`{"content": "%s"}`, "css2-edited"), "editing css 2 body") bashNote2UUID := apiCreateNote(t, user, bashBookUUID, "bash2", "adding bash note 2") linuxBook2UUID := apiCreateBook(t, user, "linux", "adding new linux book") linux2Note1UUID := apiCreateNote(t, user, linuxBookUUID, "linux-new-1", "adding linux note 1") apiDeleteBook(t, user, jsBookUUID, "deleting js book") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js3") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "algorithms", "-c", "algorithms1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js4") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "algorithms", "-c", "algorithms2") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "math", "-c", "math1") var nid string cliDatabase.MustScan(t, "getting id of note to remove", cliDB.QueryRow("SELECT rowid FROM notes WHERE body = ?", "js3"), &nid) clitest.MustWaitDnoteCmd(t, dnoteCmdOpts, clitest.ConfirmRemoveBook, cliBinaryName, "remove", "algorithms") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css4") clitest.MustWaitDnoteCmd(t, dnoteCmdOpts, clitest.ConfirmRemoveNote, cliBinaryName, "remove", "js", nid) return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, "jsNote2UUID": jsNote2UUID, "cssBookUUID": cssBookUUID, "cssNote1UUID": cssNote1UUID, "cssNote2UUID": cssNote2UUID, "cssNote3UUID": cssNote3UUID, "linuxBookUUID": linuxBookUUID, "linuxNote1UUID": linuxNote1UUID, "bashBookUUID": bashBookUUID, "bashNote1UUID": bashNote1UUID, "bashNote2UUID": bashNote2UUID, "linuxBook2UUID": linuxBook2UUID, "linux2Note1UUID": linux2Note1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 9, clientBookCount: 6, clientLastMaxUSN: 27, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 12, serverBookCount: 8, serverUserMaxUSN: 27, }) // test client var cliNote1CSS, cliNote2CSS, cliNote3CSS, cliNote1CSS2, cliNote1Bash, cliNote2Bash, cliNote1Linux, cliNote1Math, cliNote1JS cliDatabase.Note var cliBookCSS, cliBookCSS2, cliBookBash, cliBookLinux, cliBookMath, cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1CSS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "css1"), &cliNote1CSS.UUID, &cliNote1CSS.Body, &cliNote1CSS.USN) cliDatabase.MustScan(t, "finding cliNote2CSS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "css2-edited"), &cliNote2CSS.UUID, &cliNote2CSS.Body, &cliNote2CSS.USN) cliDatabase.MustScan(t, "finding cliNote3CSS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "css3"), &cliNote3CSS.UUID, &cliNote3CSS.Body, &cliNote3CSS.USN) cliDatabase.MustScan(t, "finding cliNote1CSS2", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "css4"), &cliNote1CSS2.UUID, &cliNote1CSS2.Body, &cliNote1CSS2.USN) cliDatabase.MustScan(t, "finding cliNote1Bash", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "bash1"), &cliNote1Bash.UUID, &cliNote1Bash.Body, &cliNote1Bash.USN) cliDatabase.MustScan(t, "finding cliNote2Bash", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "bash2"), &cliNote2Bash.UUID, &cliNote2Bash.Body, &cliNote2Bash.USN) cliDatabase.MustScan(t, "finding cliNote1Linux", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "linux-new-1"), &cliNote1Linux.UUID, &cliNote1Linux.Body, &cliNote1Linux.USN) cliDatabase.MustScan(t, "finding cliNote1Math", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "math1"), &cliNote1Math.UUID, &cliNote1Math.Body, &cliNote1Math.USN) cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js4"), &cliNote1JS.UUID, &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliBookCSS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css"), &cliBookCSS.UUID, &cliBookCSS.Label, &cliBookCSS.USN) cliDatabase.MustScan(t, "finding cliBookCSS2", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css_2"), &cliBookCSS2.UUID, &cliBookCSS2.Label, &cliBookCSS2.USN) cliDatabase.MustScan(t, "finding cliBookBash", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "bash"), &cliBookBash.UUID, &cliBookBash.Label, &cliBookBash.USN) cliDatabase.MustScan(t, "finding cliBookLinux", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "linux"), &cliBookLinux.UUID, &cliBookLinux.Label, &cliBookLinux.USN) cliDatabase.MustScan(t, "finding cliBookMath", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "math"), &cliBookMath.UUID, &cliBookMath.Label, &cliBookMath.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) // test usn assert.NotEqual(t, cliNote1CSS.USN, 0, "cliNote1CSS USN mismatch") assert.NotEqual(t, cliNote2CSS.USN, 0, "cliNote2CSS USN mismatch") assert.NotEqual(t, cliNote3CSS.USN, 0, "cliNote3CSS USN mismatch") assert.NotEqual(t, cliNote1CSS2.USN, 0, "cliNote1CSS2 USN mismatch") assert.NotEqual(t, cliNote1Bash.USN, 0, "cliNote1Bash USN mismatch") assert.NotEqual(t, cliNote2Bash.USN, 0, "cliNote2Bash USN mismatch") assert.NotEqual(t, cliNote1Linux.USN, 0, "cliNote1Linux USN mismatch") assert.NotEqual(t, cliNote1Math.USN, 0, "cliNote1Math USN mismatch") assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") assert.NotEqual(t, cliBookCSS.USN, 0, "cliBookCSS USN mismatch") assert.NotEqual(t, cliBookCSS2.USN, 0, "cliBookCSS2 USN mismatch") assert.NotEqual(t, cliBookBash.USN, 0, "cliBookBash USN mismatch") assert.NotEqual(t, cliBookMath.USN, 0, "cliBookMath USN mismatch") assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1CSS.Body, "css1", "cliNote1CSS Body mismatch") assert.Equal(t, cliNote2CSS.Body, "css2-edited", "cliNote2CSS Body mismatch") assert.Equal(t, cliNote3CSS.Body, "css3", "cliNote3CSS Body mismatch") assert.Equal(t, cliNote1CSS2.Body, "css4", "cliNote1CSS2 Body mismatch") assert.Equal(t, cliNote1Bash.Body, "bash1", "cliNote1Bash Body mismatch") assert.Equal(t, cliNote2Bash.Body, "bash2", "cliNote2Bash Body mismatch") assert.Equal(t, cliNote1Linux.Body, "linux-new-1", "cliNote1Linux Body mismatch") assert.Equal(t, cliNote1Math.Body, "math1", "cliNote1Math Body mismatch") assert.Equal(t, cliNote1JS.Body, "js4", "cliNote1JS Body mismatch") assert.Equal(t, cliBookCSS.Label, "css", "cliBookCSS Label mismatch") assert.Equal(t, cliBookCSS2.Label, "css_2", "cliBookCSS2 Label mismatch") assert.Equal(t, cliBookBash.Label, "bash", "cliBookBash Label mismatch") assert.Equal(t, cliBookMath.Label, "math", "cliBookMath Label mismatch") assert.Equal(t, cliBookLinux.Label, "linux", "cliBookLinux Label mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliNote1CSS.Deleted, false, "cliNote1CSS Deleted mismatch") assert.Equal(t, cliNote2CSS.Deleted, false, "cliNote2CSS Deleted mismatch") assert.Equal(t, cliNote3CSS.Deleted, false, "cliNote3CSS Deleted mismatch") assert.Equal(t, cliNote1CSS2.Deleted, false, "cliNote1CSS2 Deleted mismatch") assert.Equal(t, cliNote1Bash.Deleted, false, "cliNote1Bash Deleted mismatch") assert.Equal(t, cliNote2Bash.Deleted, false, "cliNote2Bash Deleted mismatch") assert.Equal(t, cliNote1Linux.Deleted, false, "cliNote1Linux Deleted mismatch") assert.Equal(t, cliNote1Math.Deleted, false, "cliNote1Math Deleted mismatch") assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookCSS.Deleted, false, "cliBookCSS Deleted mismatch") assert.Equal(t, cliBookCSS2.Deleted, false, "cliBookCSS2 Deleted mismatch") assert.Equal(t, cliBookBash.Deleted, false, "cliBookBash Deleted mismatch") assert.Equal(t, cliBookMath.Deleted, false, "cliBookMath Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") // test server var apiNote1JS, apiNote1CSS, apiNote2CSS, apiNote3CSS, apiNote1Linux, apiNote2Linux, apiNote1Bash, apiNote2Bash, apiNote1LinuxDup, apiNote1CSS2, apiNote1Math, apiNote1JS2 database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding api js note 1") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssNote1UUID"]).First(&apiNote1CSS), "finding api css note 1") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssNote2UUID"]).First(&apiNote2CSS), "finding api css note 2") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssNote3UUID"]).First(&apiNote3CSS), "finding api css note 3") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["linuxNote1UUID"]).First(&apiNote1Linux), "finding api linux note 1") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote2UUID"]).First(&apiNote2Linux), "finding api linux note 2") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["bashNote1UUID"]).First(&apiNote1Bash), "finding api bash note 1") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["bashNote2UUID"]).First(&apiNote2Bash), "finding api bash note 2") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["linux2Note1UUID"]).First(&apiNote1LinuxDup), "finding api linux 2 note 1") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote1CSS2.UUID).First(&apiNote1CSS2), "finding apiNote1CSS2") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote1Math.UUID).First(&apiNote1Math), "finding apiNote1Math") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote1JS.UUID).First(&apiNote1JS2), "finding apiNote1JS2") var apiBookJS, apiBookCSS, apiBookLinux, apiBookBash, apiBookLinuxDup, apiBookCSS2, apiBookMath, apiBookJS2 database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding api js book") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssBookUUID"]).First(&apiBookCSS), "finding api css book") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["bashBookUUID"]).First(&apiBookBash), "finding api bash book") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["linuxBookUUID"]).First(&apiBookLinux), "finding api linux book") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["linuxBook2UUID"]).First(&apiBookLinuxDup), "finding api linux book 2") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliBookCSS2.UUID).First(&apiBookCSS2), "finding apiBookCSS2") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliBookMath.UUID).First(&apiBookMath), "finding apiBookMath") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliBookJS.UUID).First(&apiBookJS2), "finding apiBookJS2") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS usn mismatch") assert.NotEqual(t, apiNote1CSS.USN, 0, "apiNote1CSS usn mismatch") assert.NotEqual(t, apiNote2CSS.USN, 0, "apiNote2CSS usn mismatch") assert.NotEqual(t, apiNote3CSS.USN, 0, "apiNote3CSS usn mismatch") assert.NotEqual(t, apiNote1Linux.USN, 0, "apiNote1Linux usn mismatch") assert.NotEqual(t, apiNote2Linux.USN, 0, "apiNote2Linux usn mismatch") assert.NotEqual(t, apiNote1Bash.USN, 0, "apiNote1Bash usn mismatch") assert.NotEqual(t, apiNote2Bash.USN, 0, "apiNote2Bash usn mismatch") assert.NotEqual(t, apiNote1LinuxDup.USN, 0, "apiNote1LinuxDup usn mismatch") assert.NotEqual(t, apiNote1CSS2.USN, 0, "apiNoteCSS2 usn mismatch") assert.NotEqual(t, apiNote1Math.USN, 0, "apiNote1Math usn mismatch") assert.NotEqual(t, apiNote1JS2.USN, 0, "apiNote1JS2 usn mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS usn mismatch") assert.NotEqual(t, apiBookCSS.USN, 0, "apiBookCSS usn mismatch") assert.NotEqual(t, apiBookLinux.USN, 0, "apiBookLinux usn mismatch") assert.NotEqual(t, apiBookBash.USN, 0, "apiBookBash usn mismatch") assert.NotEqual(t, apiBookLinuxDup.USN, 0, "apiBookLinuxDup usn mismatch") assert.NotEqual(t, apiBookCSS2.USN, 0, "apiBookCSS2 usn mismatch") assert.NotEqual(t, apiBookMath.USN, 0, "apiBookMath usn mismatch") assert.NotEqual(t, apiBookJS2.USN, 0, "apiBookJS2 usn mismatch") // assert on note bodys assert.Equal(t, apiNote1JS.Body, "", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1CSS.Body, "css1", "apiNote1CSS Body mismatch") assert.Equal(t, apiNote2CSS.Body, "css2-edited", "apiNote2CSS Body mismatch") assert.Equal(t, apiNote3CSS.Body, "css3", "apiNote3CSS Body mismatch") assert.Equal(t, apiNote1Linux.Body, "", "apiNote1Linux Body mismatch") assert.Equal(t, apiNote2Linux.Body, "", "apiNote2Linux Body mismatch") assert.Equal(t, apiNote1Bash.Body, "bash1", "apiNote1Bash Body mismatch") assert.Equal(t, apiNote2Bash.Body, "bash2", "apiNote2Bash Body mismatch") assert.Equal(t, apiNote1LinuxDup.Body, "linux-new-1", "apiNote1LinuxDup Body mismatch") assert.Equal(t, apiNote1CSS2.Body, "css4", "apiNote1CSS2 Body mismatch") assert.Equal(t, apiNote1Math.Body, "math1", "apiNote1Math Body mismatch") assert.Equal(t, apiNote1JS2.Body, "js4", "apiNote1JS2 Body mismatch") // client must have generated uuids assert.NotEqual(t, apiNote1CSS2.UUID, "", "apiNote1CSS2 uuid mismatch") assert.NotEqual(t, apiNote1Math.UUID, "", "apiNote1Math uuid mismatch") assert.NotEqual(t, apiNote1JS2.UUID, "", "apiNote1JS2 uuid mismatch") // assert on labels assert.Equal(t, apiBookJS.Label, "", "apiBookJS Label mismatch") assert.Equal(t, apiBookCSS.Label, "css", "apiBookCSS Label mismatch") assert.Equal(t, apiBookLinux.Label, "", "apiBookLinux Label mismatch") assert.Equal(t, apiBookBash.Label, "bash", "apiBookBash Label mismatch") assert.Equal(t, apiBookLinuxDup.Label, "linux", "apiBookLinuxDup Label mismatch") assert.Equal(t, apiBookCSS2.Label, "css_2", "apiBookCSS2 Label mismatch") assert.Equal(t, apiBookMath.Label, "math", "apiBookMath Label mismatch") assert.Equal(t, apiBookJS2.Label, "js", "apiBookJS2 Label mismatch") // assert on note deleted assert.Equal(t, apiNote1JS.Deleted, true, "apiNote1JS Deleted mismatch") assert.Equal(t, apiNote1CSS.Deleted, false, "apiNote1CSS Deleted mismatch") assert.Equal(t, apiNote2CSS.Deleted, false, "apiNote2CSS Deleted mismatch") assert.Equal(t, apiNote3CSS.Deleted, false, "apiNote3CSS Deleted mismatch") assert.Equal(t, apiNote1Linux.Deleted, true, "apiNote1Linux Deleted mismatch") assert.Equal(t, apiNote2Linux.Deleted, true, "apiNote2Linux Deleted mismatch") assert.Equal(t, apiNote1Bash.Deleted, false, "apiNote1Bash Deleted mismatch") assert.Equal(t, apiNote2Bash.Deleted, false, "apiNote2Bash Deleted mismatch") assert.Equal(t, apiNote1LinuxDup.Deleted, false, "apiNote1LinuxDup Deleted mismatch") assert.Equal(t, apiNote1CSS2.Deleted, false, "apiNote1CSS2 Deleted mismatch") assert.Equal(t, apiNote1Math.Deleted, false, "apiNote1Math Deleted mismatch") assert.Equal(t, apiNote1JS2.Deleted, false, "apiNote1JS2 Deleted mismatch") // assert on book deleted assert.Equal(t, apiBookJS.Deleted, true, "apiBookJS Deleted mismatch") assert.Equal(t, apiBookCSS.Deleted, false, "apiBookCSS Deleted mismatch") assert.Equal(t, apiBookLinux.Deleted, true, "apiBookLinux Deleted mismatch") assert.Equal(t, apiBookBash.Deleted, false, "apiBookBash Deleted mismatch") assert.Equal(t, apiBookLinuxDup.Deleted, false, "apiBookLinuxDup Deleted mismatch") assert.Equal(t, apiBookCSS2.Deleted, false, "apiBookCSS2 Deleted mismatch") assert.Equal(t, apiBookMath.Deleted, false, "apiBookMath Deleted mismatch") assert.Equal(t, apiBookJS2.Deleted, false, "apiBookJS2 Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("twice", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") cssBookUUID := apiCreateBook(t, user, "css", "adding css book") cssNote1UUID := apiCreateNote(t, user, cssBookUUID, "css1", "adding css note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js2") clitest.MustWaitDnoteCmd(t, dnoteCmdOpts, clitest.ConfirmRemoveBook, cliBinaryName, "remove", "js") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "math", "-c", "math1") var nid string cliDB := ctx.DB cliDatabase.MustScan(t, "getting id of note to edit", cliDB.QueryRow("SELECT rowid FROM notes WHERE body = ?", "math1"), &nid) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "edit", "math", nid, "-c", "math1-edited") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server scssBookUUID := apiCreateBook(t, user, "scss", "adding a scss book") apiPatchNote(t, user, cssNote1UUID, fmt.Sprintf(`{"book_uuid": "%s"}`, scssBookUUID), "moving css note 1 to scss") var n1UUID string cliDatabase.MustScan(t, "getting math1-edited note UUID", cliDB.QueryRow("SELECT uuid FROM notes WHERE body = ?", "math1-edited"), &n1UUID) apiPatchNote(t, user, n1UUID, fmt.Sprintf(`{"content": "%s", "public": true}`, "math1-edited"), "editing math1 note") cssNote2UUID := apiCreateNote(t, user, cssBookUUID, "css2", "adding css note 2") apiDeleteBook(t, user, cssBookUUID, "deleting css book") bashBookUUID := apiCreateBook(t, user, "bash", "adding a bash book") algorithmsBookUUID := apiCreateBook(t, user, "algorithms", "adding a algorithms book") // 4. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js3") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "algorithms", "-c", "algorithms1") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, "cssBookUUID": cssBookUUID, "scssBookUUID": scssBookUUID, "cssNote1UUID": cssNote1UUID, "cssNote2UUID": cssNote2UUID, "bashBookUUID": bashBookUUID, "algorithmsBookUUID": algorithmsBookUUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { apiDB := serverDb cliDB := ctx.DB checkState(t, ctx, user, systemState{ clientNoteCount: 5, clientBookCount: 6, clientLastMaxUSN: 17, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 6, serverBookCount: 7, serverUserMaxUSN: 17, }) // test client var cliNote1JS, cliNote2JS, cliNote1SCSS, cliNote1Math, cliNote1Alg2 cliDatabase.Note var cliBookJS, cliBookSCSS, cliBookMath, cliBookBash, cliBookAlg, cliBookAlg2 cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js1"), &cliNote1JS.UUID, &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliNote2JS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js3"), &cliNote2JS.UUID, &cliNote2JS.Body, &cliNote2JS.USN) cliDatabase.MustScan(t, "finding cliNote1SCSS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "css1"), &cliNote1SCSS.UUID, &cliNote1SCSS.Body, &cliNote1SCSS.USN) cliDatabase.MustScan(t, "finding cliNote1Math", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "math1-edited"), &cliNote1Math.UUID, &cliNote1Math.Body, &cliNote1Math.USN) cliDatabase.MustScan(t, "finding cliNote1Alg2", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "algorithms1"), &cliNote1Alg2.UUID, &cliNote1Alg2.Body, &cliNote1Alg2.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cliBookSCSS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "scss"), &cliBookSCSS.UUID, &cliBookSCSS.Label, &cliBookSCSS.USN) cliDatabase.MustScan(t, "finding cliBookMath", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "math"), &cliBookMath.UUID, &cliBookMath.Label, &cliBookMath.USN) cliDatabase.MustScan(t, "finding cliBookBash", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "bash"), &cliBookBash.UUID, &cliBookBash.Label, &cliBookBash.USN) cliDatabase.MustScan(t, "finding cliBookAlg", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "algorithms"), &cliBookAlg.UUID, &cliBookAlg.Label, &cliBookAlg.USN) cliDatabase.MustScan(t, "finding cliBookAlg2", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "algorithms_2"), &cliBookAlg2.UUID, &cliBookAlg2.Label, &cliBookAlg2.USN) // test usn assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") assert.NotEqual(t, cliNote2JS.USN, 0, "cliNote2JS USN mismatch") assert.NotEqual(t, cliNote1SCSS.USN, 0, "cliNote1SCSS USN mismatch") assert.NotEqual(t, cliNote1Math.USN, 0, "cliNote1Math USN mismatch") assert.NotEqual(t, cliNote1Alg2.USN, 0, "cliNote1Alg2 USN mismatch") assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") assert.NotEqual(t, cliBookSCSS.USN, 0, "cliBookSCSS USN mismatch") assert.NotEqual(t, cliBookMath.USN, 0, "cliBookMath USN mismatch") assert.NotEqual(t, cliBookBash.USN, 0, "cliBookBash USN mismatch") assert.NotEqual(t, cliBookAlg.USN, 0, "cliBookAlg USN mismatch") assert.NotEqual(t, cliBookAlg2.USN, 0, "cliBookAlg2 USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote2JS.Body, "js3", "cliNote2JS Body mismatch") assert.Equal(t, cliNote1SCSS.Body, "css1", "cliNote1SCSS Body mismatch") assert.Equal(t, cliNote1Math.Body, "math1-edited", "cliNote1Math Body mismatch") assert.Equal(t, cliNote1Alg2.Body, "algorithms1", "cliNote1Alg2 Body mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") assert.Equal(t, cliBookSCSS.Label, "scss", "cliBookSCSS Label mismatch") assert.Equal(t, cliBookMath.Label, "math", "cliBookMath Label mismatch") assert.Equal(t, cliBookBash.Label, "bash", "cliBookBash Label mismatch") assert.Equal(t, cliBookAlg.Label, "algorithms", "cliBookAlg Label mismatch") assert.Equal(t, cliBookAlg2.Label, "algorithms_2", "cliBookAlg2 Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliNote2JS.Deleted, false, "cliNote2JS Deleted mismatch") assert.Equal(t, cliNote1SCSS.Deleted, false, "cliNote1SCSS Deleted mismatch") assert.Equal(t, cliNote1Math.Deleted, false, "cliNote1Math Deleted mismatch") assert.Equal(t, cliNote1Alg2.Deleted, false, "cliNote1Alg2 Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") assert.Equal(t, cliBookSCSS.Deleted, false, "cliBookSCSS Deleted mismatch") assert.Equal(t, cliBookMath.Deleted, false, "cliBookMath Deleted mismatch") assert.Equal(t, cliBookBash.Deleted, false, "cliBookBash Deleted mismatch") assert.Equal(t, cliBookAlg.Deleted, false, "cliBookAlg Deleted mismatch") assert.Equal(t, cliBookAlg2.Deleted, false, "cliBookAlg2 Deleted mismatch") // test server var apiNote1JS, apiNote2JS, apiNote1SCSS, apiNote2CSS, apiNote1Math, apiNote1Alg database.Note var apiBookJS, apiBookCSS, apiBookSCSS, apiBookMath, apiBookBash, apiBookAlg, apiBookAlg2 database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssNote2UUID"]).First(&apiNote2CSS), "finding apiNote2CSS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote2JS.UUID).First(&apiNote2JS), "finding apiNote2JS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssNote1UUID"]).First(&apiNote1SCSS), "finding apiNote1SCSS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote1Math.UUID).First(&apiNote1Math), "finding apiNote1Math") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote1Alg2.UUID).First(&apiNote1Alg), "finding apiNote1Alg") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssBookUUID"]).First(&apiBookCSS), "finding apiBookCSS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["bashBookUUID"]).First(&apiBookBash), "finding apiBookBash") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["scssBookUUID"]).First(&apiBookSCSS), "finding apiBookSCSS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["algorithmsBookUUID"]).First(&apiBookAlg), "finding apiBookAlg") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliBookAlg2.UUID).First(&apiBookAlg2), "finding apiBookAlg2") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliBookMath.UUID).First(&apiBookMath), "finding apiBookMath") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS usn mismatch") assert.NotEqual(t, apiNote2JS.USN, 0, "apiNote1JS usn mismatch") assert.NotEqual(t, apiNote1SCSS.USN, 0, "apiNote1JS usn mismatch") assert.NotEqual(t, apiNote2CSS.USN, 0, "apiNote1JS usn mismatch") assert.NotEqual(t, apiNote1Math.USN, 0, "apiNote1JS usn mismatch") assert.NotEqual(t, apiNote1Alg.USN, 0, "apiNote1JS usn mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBook1Alg usn mismatch") assert.NotEqual(t, apiBookCSS.USN, 0, "apiBookCSS usn mismatch") assert.NotEqual(t, apiBookSCSS.USN, 0, "apibookSCSS usn mismatch") assert.NotEqual(t, apiBookMath.USN, 0, "apiBookMath usn mismatch") assert.NotEqual(t, apiBookBash.USN, 0, "apiBookBash usn mismatch") assert.NotEqual(t, apiBookAlg.USN, 0, "apiBookAlg usn mismatch") assert.NotEqual(t, apiBookAlg2.USN, 0, "apiBookAlg2 usn mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiNote2JS.Body, "js3", "apiNote2JS Body mismatch") assert.Equal(t, apiNote1SCSS.Body, "css1", "apiNote1SCSS Body mismatch") assert.Equal(t, apiNote2CSS.Body, "", "apiNote2CSS Body mismatch") assert.Equal(t, apiNote1Math.Body, "math1-edited", "apiNote1Math Body mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") assert.Equal(t, apiBookCSS.Label, "", "apiBookCSS Label mismatch") assert.Equal(t, apiBookSCSS.Label, "scss", "apiBookSCSS Label mismatch") assert.Equal(t, apiBookMath.Label, "math", "apiBookMath Label mismatch") assert.Equal(t, apiBookBash.Label, "bash", "apiBookBash Label mismatch") assert.Equal(t, apiBookAlg.Label, "algorithms", "apiBookAlg Label mismatch") assert.Equal(t, apiBookAlg2.Label, "algorithms_2", "apiBookAlg2 Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiNote2JS.Deleted, false, "apiNote2JS Deleted mismatch") assert.Equal(t, apiNote1SCSS.Deleted, false, "apiNote1SCSS Deleted mismatch") assert.Equal(t, apiNote2CSS.Deleted, true, "apiNote2CSS Deleted mismatch") assert.Equal(t, apiNote1Math.Deleted, false, "apiNote1Math Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") assert.Equal(t, apiBookCSS.Deleted, true, "apiBookCSS Deleted mismatch") assert.Equal(t, apiBookSCSS.Deleted, false, "apiBookSCSS Deleted mismatch") assert.Equal(t, apiBookMath.Deleted, false, "apiBookMath Deleted mismatch") assert.Equal(t, apiBookBash.Deleted, false, "apiBookBash Deleted mismatch") assert.Equal(t, apiBookAlg.Deleted, false, "apiBookAlg Deleted mismatch") assert.Equal(t, apiBookAlg2.Deleted, false, "apiBookAlg2 Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("three times", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server goBookUUID := apiCreateBook(t, user, "go", "adding a go book") goNote1UUID := apiCreateNote(t, user, goBookUUID, "go1", "adding go note 1") // 4. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "html", "-c", "html1") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, "goBookUUID": goBookUUID, "goNote1UUID": goNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 4, clientBookCount: 4, clientLastMaxUSN: 8, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 4, serverBookCount: 4, serverUserMaxUSN: 8, }) // test client var cliNote1JS, cliNote1CSS, cliNote1Go, cliNote1HTML cliDatabase.Note var cliBookJS, cliBookCSS, cliBookGo, cliBookHTML cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js1"), &cliNote1JS.UUID, &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliNote1CSS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "css1"), &cliNote1CSS.UUID, &cliNote1CSS.Body, &cliNote1CSS.USN) cliDatabase.MustScan(t, "finding cliNote1Go", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "go1"), &cliNote1Go.UUID, &cliNote1Go.Body, &cliNote1Go.USN) cliDatabase.MustScan(t, "finding cliNote1HTML", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "html1"), &cliNote1HTML.UUID, &cliNote1HTML.Body, &cliNote1HTML.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cliBookCSS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css"), &cliBookCSS.UUID, &cliBookCSS.Label, &cliBookCSS.USN) cliDatabase.MustScan(t, "finding cliBookGo", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "go"), &cliBookGo.UUID, &cliBookGo.Label, &cliBookGo.USN) cliDatabase.MustScan(t, "finding cliBookHTML", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "html"), &cliBookHTML.UUID, &cliBookHTML.Label, &cliBookHTML.USN) // test usn assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") assert.NotEqual(t, cliNote1CSS.USN, 0, "cliNote1CSS USN mismatch") assert.NotEqual(t, cliNote1Go.USN, 0, "cliNote1Go USN mismatch") assert.NotEqual(t, cliNote1HTML.USN, 0, "cliNote1HTML USN mismatch") assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") assert.NotEqual(t, cliBookCSS.USN, 0, "cliBookCSS USN mismatch") assert.NotEqual(t, cliBookGo.USN, 0, "cliBookGo USN mismatch") assert.NotEqual(t, cliBookHTML.USN, 0, "cliBookHTML USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote1CSS.Body, "css1", "cliNote1CSS Body mismatch") assert.Equal(t, cliNote1Go.Body, "go1", "cliNote1Go Body mismatch") assert.Equal(t, cliNote1HTML.Body, "html1", "cliNote1HTML Body mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") assert.Equal(t, cliBookCSS.Label, "css", "cliBookCSS Label mismatch") assert.Equal(t, cliBookGo.Label, "go", "cliBookGo Label mismatch") assert.Equal(t, cliBookHTML.Label, "html", "cliBookHTML Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliNote1CSS.Deleted, false, "cliNote1CSS Deleted mismatch") assert.Equal(t, cliNote1Go.Deleted, false, "cliNote1Go Deleted mismatch") assert.Equal(t, cliNote1HTML.Deleted, false, "cliNote1HTML Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") assert.Equal(t, cliBookCSS.Deleted, false, "cliBookCSS Deleted mismatch") assert.Equal(t, cliBookGo.Deleted, false, "cliBookGo Deleted mismatch") assert.Equal(t, cliBookHTML.Deleted, false, "cliBookHTML Deleted mismatch") // test server var apiNote1JS, apiNote1CSS, apiNote1Go, apiNote1HTML database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["goNote1UUID"]).First(&apiNote1Go), "finding apiNote1Go") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote1CSS.UUID).First(&apiNote1CSS), "finding apiNote1CSS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote1HTML.UUID).First(&apiNote1HTML), "finding apiNote1HTML") var apiBookJS, apiBookCSS, apiBookGo, apiBookHTML database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["goBookUUID"]).First(&apiBookGo), "finding apiBookGo") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliBookCSS.UUID).First(&apiBookCSS), "finding apiBookCSS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliBookHTML.UUID).First(&apiBookHTML), "finding apiBookHTML") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiNote1CSS.USN, 0, "apiNote1CSS USN mismatch") assert.NotEqual(t, apiNote1Go.USN, 0, "apiNote1Go USN mismatch") assert.NotEqual(t, apiNote1HTML.USN, 0, "apiNote1HTM USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") assert.NotEqual(t, apiBookGo.USN, 0, "apiBookGo USN mismatch") assert.NotEqual(t, apiBookCSS.USN, 0, "apiBookCSS USN mismatch") assert.NotEqual(t, apiBookHTML.USN, 0, "apiBookHTML USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1CSS.Body, "css1", "apiNote1CSS Body mismatch") assert.Equal(t, apiNote1Go.Body, "go1", "apiNote1Go Body mismatch") assert.Equal(t, apiNote1HTML.Body, "html1", "apiNote1HTM Body mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") assert.Equal(t, apiBookGo.Label, "go", "apiBookGo Label mismatch") assert.Equal(t, apiBookCSS.Label, "css", "apiBookCSS Label mismatch") assert.Equal(t, apiBookHTML.Label, "html", "apiBookHTML Label mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) } func TestSync(t *testing.T) { t.Run("client adds a book and a note", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js1") return map[string]string{} } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 1, clientBookCount: 1, clientLastMaxUSN: 2, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 2, }) // test client // assert on bodys and labels var cliNote1JS cliDatabase.Note var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js1"), &cliNote1JS.UUID, &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote1JS.UUID).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliBookJS.UUID).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("client deletes a book", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") clitest.MustWaitDnoteCmd(t, dnoteCmdOpts, clitest.ConfirmRemoveBook, cliBinaryName, "remove", "js") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 0, clientBookCount: 0, clientLastMaxUSN: 5, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 5, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "", "apiNote1JS Body mismatch") assert.Equal(t, apiBookJS.Label, "", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, true, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, true, "apiBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("client deletes a note", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { cliDB := ctx.DB // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") var nid string cliDatabase.MustScan(t, "getting id of note to remove", cliDB.QueryRow("SELECT rowid FROM notes WHERE uuid = ?", jsNote1UUID), &nid) clitest.MustWaitDnoteCmd(t, dnoteCmdOpts, clitest.ConfirmRemoveNote, cliBinaryName, "remove", "js", nid) return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 0, clientBookCount: 1, clientLastMaxUSN: 3, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 3, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "", "apiNote1JS Body mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, true, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") // test client var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT label, usn FROM books WHERE label = ?", "js"), &cliBookJS.Label, &cliBookJS.USN) // test usn assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("client edits a note", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { cliDB := ctx.DB // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") var nid string cliDatabase.MustScan(t, "getting id of note to edit", cliDB.QueryRow("SELECT rowid FROM notes WHERE uuid = ?", jsNote1UUID), &nid) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "edit", "js", nid, "-c", "js1-edited") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 1, clientBookCount: 1, clientLastMaxUSN: 3, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 3, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1-edited", "apiNote1JS Body mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") // test client var cliNote1JS cliDatabase.Note var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT body, usn FROM notes WHERE uuid = ?", ids["jsNote1UUID"]), &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.Label, &cliBookJS.USN) // test usn assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1-edited", "cliNote1JS Body mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("client edits a book by renaming it", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "edit", "js", "-n", "js-edited") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 1, clientBookCount: 1, clientLastMaxUSN: 3, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 3, }) // test client var cliBookJS cliDatabase.Book var cliNote1JS cliDatabase.Note cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, body, book_uuid, usn FROM notes WHERE uuid = ?", ids["jsNote1UUID"]), &cliNote1JS.UUID, &cliNote1JS.Body, &cliNote1JS.BookUUID, &cliNote1JS.USN) // assert on usn assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote1JS.BookUUID, cliBookJS.UUID, "cliNote1JS BookUUID mismatch") assert.Equal(t, cliBookJS.Label, "js-edited", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") // assert on dirty assert.Equal(t, cliNote1JS.Dirty, false, "cliNote1JS Dirty mismatch") assert.Equal(t, cliBookJS.Dirty, false, "cliBookJS Dirty mismatch") // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiBookJS.Label, "js-edited", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("server adds a book", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") return map[string]string{ "jsBookUUID": jsBookUUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 0, clientBookCount: 1, clientLastMaxUSN: 1, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 0, serverBookCount: 1, serverUserMaxUSN: 1, }) // test server var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") // test client // assert on bodys and labels var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT label, usn FROM books WHERE label = ?", "js"), &cliBookJS.Label, &cliBookJS.USN) assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("server edits a book by renaming it", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server apiPatchBook(t, user, jsBookUUID, fmt.Sprintf(`{"name": "%s"}`, "js-new-label"), "editing js book") return map[string]string{ "jsBookUUID": jsBookUUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 0, clientBookCount: 1, clientLastMaxUSN: 2, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 0, serverBookCount: 1, serverUserMaxUSN: 2, }) // test server var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiBookJS.Label, "js-new-label", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") // test client // assert on bodys and labels var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.Label, &cliBookJS.USN) assert.Equal(t, cliBookJS.Label, "js-new-label", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("server deletes a book", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server apiDeleteBook(t, user, jsBookUUID, "deleting js book") return map[string]string{ "jsBookUUID": jsBookUUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 0, clientBookCount: 0, clientLastMaxUSN: 2, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 0, serverBookCount: 1, serverUserMaxUSN: 2, }) // test server var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiBookJS.Label, "", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiBookJS.Deleted, true, "apiBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("server adds a note", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 1, clientBookCount: 1, clientLastMaxUSN: 2, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 2, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, ids["jsBookUUID"], "apiNote1JS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") // test client var cliNote1JS cliDatabase.Note var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT body, usn FROM notes WHERE uuid = ?", ids["jsNote1UUID"]), &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.Label, &cliBookJS.USN) // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("server edits a note body", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server apiPatchNote(t, user, jsNote1UUID, fmt.Sprintf(`{"content": "%s"}`, "js1-edited"), "editing js note 1") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 1, clientBookCount: 1, clientLastMaxUSN: 3, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 3, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1-edited", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, ids["jsBookUUID"], "apiNote1JS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") // test client var cliNote1JS cliDatabase.Note var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT body, usn FROM notes WHERE uuid = ?", ids["jsNote1UUID"]), &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.Label, &cliBookJS.USN) // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1-edited", "cliNote1JS Body mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("server moves a note to another book", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") cssBookUUID := apiCreateBook(t, user, "css", "adding css book") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server apiPatchNote(t, user, jsNote1UUID, fmt.Sprintf(`{"book_uuid": "%s"}`, cssBookUUID), "moving js note 1 to css book") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, "cssBookUUID": cssBookUUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 1, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 2, serverUserMaxUSN: 4, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS, apiBookCSS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssBookUUID"]).First(&apiBookCSS), "finding apiBookCSS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") assert.NotEqual(t, apiBookCSS.USN, 0, "apiBookCSS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, ids["cssBookUUID"], "apiNote1JS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") assert.Equal(t, apiBookCSS.Label, "css", "apiBookCSS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") assert.Equal(t, apiBookCSS.Deleted, false, "apiBookCSS Deleted mismatch") // test client var cliNote1JS cliDatabase.Note var cliBookJS, cliBookCSS cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT body, usn FROM notes WHERE uuid = ?", ids["jsNote1UUID"]), &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cliBookCSS", cliDB.QueryRow("SELECT label, usn FROM books WHERE uuid = ?", ids["cssBookUUID"]), &cliBookCSS.Label, &cliBookCSS.USN) // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") assert.Equal(t, cliBookCSS.Label, "css", "cliBookCSS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") assert.Equal(t, cliBookCSS.Deleted, false, "cliBookCSS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("server deletes a note", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server apiDeleteNote(t, user, jsNote1UUID, "deleting js note 1") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 0, clientBookCount: 1, clientLastMaxUSN: 3, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 3, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, ids["jsBookUUID"], "apiNote1JS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, true, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") // test client var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.Label, &cliBookJS.USN) // assert on bodys and labels assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("client and server deletes the same book", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server apiDeleteBook(t, user, jsBookUUID, "deleting js book") // 4. on cli clitest.MustWaitDnoteCmd(t, dnoteCmdOpts, clitest.ConfirmRemoveBook, cliBinaryName, "remove", "js") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 0, clientBookCount: 0, clientLastMaxUSN: 6, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 6, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, ids["jsBookUUID"], "apiNote1JS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, true, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, true, "apiBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("client and server deletes the same note", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { cliDB := ctx.DB // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server apiDeleteNote(t, user, jsNote1UUID, "deleting js note 1") // 4. on cli var nid string cliDatabase.MustScan(t, "getting id of note to remove", cliDB.QueryRow("SELECT rowid FROM notes WHERE body = ?", "js1"), &nid) clitest.MustWaitDnoteCmd(t, dnoteCmdOpts, clitest.ConfirmRemoveNote, cliBinaryName, "remove", "js", nid) return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { apiDB := serverDb cliDB := ctx.DB checkState(t, ctx, user, systemState{ clientNoteCount: 0, clientBookCount: 1, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 4, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, ids["jsBookUUID"], "apiNote1JS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, true, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") // test client var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.Label, &cliBookJS.USN) // test usn assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("server and client adds a note with same body", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 4. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js1") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 2, clientBookCount: 1, clientLastMaxUSN: 3, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 1, serverUserMaxUSN: 3, }) // test client var cliNote1JS, cliNote2JS cliDatabase.Note var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE uuid = ?", ids["jsNote1UUID"]), &cliNote1JS.UUID, &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliNote2JS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ? and uuid != ?", "js1", ids["jsNote1UUID"]), &cliNote2JS.UUID, &cliNote2JS.Body, &cliNote2JS.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) // test usn assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote2JS.Body, "js1", "cliNote2JS Body mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliNote2JS.Deleted, false, "cliNote2JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") // test server var apiNote1JS, apiNote2JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote2JS.UUID).First(&apiNote2JS), "finding apiNote2JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiNote2JS.USN, 0, "apiNote2JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiNote2JS.Body, "js1", "apiNote2JS Body mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("server and client adds a book with same label", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js1") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 2, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 2, serverUserMaxUSN: 4, }) // test client var cliNote1JS, cliNote1JS2 cliDatabase.Note var cliBookJS, cliBookJS2 cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE uuid = ?", ids["jsNote1UUID"]), &cliNote1JS.UUID, &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliNote1JS2", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ? AND uuid !=?", "js1", ids["jsNote1UUID"]), &cliNote1JS2.UUID, &cliNote1JS2.Body, &cliNote1JS2.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cliBookJS2", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js_2"), &cliBookJS2.UUID, &cliBookJS2.Label, &cliBookJS2.USN) // test usn assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote1JS2.Body, "js1", "cliNote1JS2 Body mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") assert.Equal(t, cliBookJS2.Label, "js_2", "cliBookJS2 Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliNote1JS2.Deleted, false, "cliNote1JS2 Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") assert.Equal(t, cliBookJS2.Deleted, false, "cliBookJS2 Deleted mismatch") // test server var apiNote1JS, apiNote1JS2 database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote1JS2.UUID).First(&apiNote1JS2), "finding apiNote1JS2") var apiBookJS, apiBookJS2 database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliBookJS2.UUID).First(&apiBookJS2), "finding apiBookJS2") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiNote1JS2.USN, 0, "apiNote1JS2 USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") assert.NotEqual(t, apiBookJS2.USN, 0, "apiBookJS2 USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS2.Body, "js1", "apiNote1JS2 Body mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") assert.Equal(t, apiBookJS2.Label, "js_2", "apiBookJS2 Label mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("server and client adds two sets of books with same labels", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") cssBookUUID := apiCreateBook(t, user, "css", "adding css book") cssNote1UUID := apiCreateNote(t, user, cssBookUUID, "css1", "adding css note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css1") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, "cssBookUUID": cssBookUUID, "cssNote1UUID": cssNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 4, clientBookCount: 4, clientLastMaxUSN: 8, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 4, serverBookCount: 4, serverUserMaxUSN: 8, }) // test client var cliNote1JS, cliNote1JS2, cliNote1CSS, cliNote1CSS2 cliDatabase.Note var cliBookJS, cliBookJS2, cliBookCSS, cliBookCSS2 cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE uuid = ?", ids["jsNote1UUID"]), &cliNote1JS.UUID, &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliNote1JS2", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ? AND uuid != ?", "js1", ids["jsNote1UUID"]), &cliNote1JS2.UUID, &cliNote1JS2.Body, &cliNote1JS2.USN) cliDatabase.MustScan(t, "finding cliNote1CSS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE uuid = ?", ids["cssNote1UUID"]), &cliNote1CSS.UUID, &cliNote1CSS.Body, &cliNote1CSS.USN) cliDatabase.MustScan(t, "finding cliNote1CSS2", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ? AND uuid != ?", "css1", ids["cssNote1UUID"]), &cliNote1CSS2.UUID, &cliNote1CSS2.Body, &cliNote1CSS2.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cliBookJS2", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js_2"), &cliBookJS2.UUID, &cliBookJS2.Label, &cliBookJS2.USN) cliDatabase.MustScan(t, "finding cliBookCSS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css"), &cliBookCSS.UUID, &cliBookCSS.Label, &cliBookCSS.USN) cliDatabase.MustScan(t, "finding cliBookCSS2", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css_2"), &cliBookCSS2.UUID, &cliBookCSS2.Label, &cliBookCSS2.USN) // test usn assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote1JS2.Body, "js1", "cliNote1JS2 Body mismatch") assert.Equal(t, cliNote1CSS.Body, "css1", "cliNote1CSS Body mismatch") assert.Equal(t, cliNote1CSS2.Body, "css1", "cliNote1CSS2 Body mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") assert.Equal(t, cliBookJS2.Label, "js_2", "cliBookJS2 Label mismatch") assert.Equal(t, cliBookCSS.Label, "css", "cliBookCSS Label mismatch") assert.Equal(t, cliBookCSS2.Label, "css_2", "cliBookCSS2 Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliNote1JS2.Deleted, false, "cliNote1JS2 Deleted mismatch") assert.Equal(t, cliNote1CSS.Deleted, false, "cliNote1CSS Deleted mismatch") assert.Equal(t, cliNote1CSS2.Deleted, false, "cliNote1CSS2 Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") assert.Equal(t, cliBookJS2.Deleted, false, "cliBookJS2 Deleted mismatch") assert.Equal(t, cliBookCSS.Deleted, false, "cliBookCSS Deleted mismatch") assert.Equal(t, cliBookCSS2.Deleted, false, "cliBookCSS2 Deleted mismatch") // test server var apiNote1JS, apiNote1JS2, apiNote1CSS, apiNote1CSS2 database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote1JS2.UUID).First(&apiNote1JS2), "finding apiNote1JS2") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssNote1UUID"]).First(&apiNote1CSS), "finding apiNote1CSS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote1CSS2.UUID).First(&apiNote1CSS2), "finding apiNote1CSS2") var apiBookJS, apiBookJS2, apiBookCSS, apiBookCSS2 database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliBookJS2.UUID).First(&apiBookJS2), "finding apiBookJS2") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssBookUUID"]).First(&apiBookCSS), "finding apiBookCSS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliBookCSS2.UUID).First(&apiBookCSS2), "finding apiBookCSS2") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiNote1JS2.USN, 0, "apiNote1JS2 USN mismatch") assert.NotEqual(t, apiNote1CSS.USN, 0, "apiNote1CSS USN mismatch") assert.NotEqual(t, apiNote1CSS2.USN, 0, "apiNote1CSS2 USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") assert.NotEqual(t, apiBookJS2.USN, 0, "apiBookJS2 USN mismatch") assert.NotEqual(t, apiBookCSS.USN, 0, "apiBookCSS USN mismatch") assert.NotEqual(t, apiBookCSS2.USN, 0, "apiBookCSS2 USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS2.Body, "js1", "apiNote1JS2 Body mismatch") assert.Equal(t, apiNote1CSS.Body, "css1", "apiNote1CSS2 Body mismatch") assert.Equal(t, apiNote1CSS2.Body, "css1", "apiNote1CSS2 Body mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") assert.Equal(t, apiBookJS2.Label, "js_2", "apiBookJS2 Label mismatch") assert.Equal(t, apiBookCSS.Label, "css", "apiBookCSS Label mismatch") assert.Equal(t, apiBookCSS2.Label, "css_2", "apiBookCSS2 Label mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("server and client adds notes to the same book", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 4. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js2") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 2, clientBookCount: 1, clientLastMaxUSN: 3, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 1, serverUserMaxUSN: 3, }) // test client var cliNote1JS, cliNote2JS cliDatabase.Note var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js1"), &cliNote1JS.UUID, &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliNote2JS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js2"), &cliNote2JS.UUID, &cliNote2JS.Body, &cliNote2JS.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) // test usn assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote2JS.Body, "js2", "cliNote2JS Body mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliNote2JS.Deleted, false, "cliNote2JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") // test server var apiNote1JS, apiNote2JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote2JS.UUID).First(&apiNote2JS), "finding apiNote2JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiNote2JS.USN, 0, "apiNote2JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiNote2JS.Body, "js2", "apiNote2JS Body mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("server and client adds a book with the same label and notes in it", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js2") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 2, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 2, serverUserMaxUSN: 4, }) // test client var cliNote1JS, cliNote2JS cliDatabase.Note var cliBookJS, cliBookJS2 cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js1"), &cliNote1JS.UUID, &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliNote2JS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js2"), &cliNote2JS.UUID, &cliNote2JS.Body, &cliNote2JS.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cliBookJS2", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js_2"), &cliBookJS2.UUID, &cliBookJS2.Label, &cliBookJS2.USN) // test usn assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote2JS.Body, "js2", "cliNote2JS Body mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") assert.Equal(t, cliBookJS2.Label, "js_2", "cliBookJS2 Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliNote2JS.Deleted, false, "cliNote2JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") assert.Equal(t, cliBookJS2.Deleted, false, "cliBookJS2 Deleted mismatch") // test server var apiNote1JS, apiNote2JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote2JS.UUID).First(&apiNote2JS), "finding apiNote2JS") var apiBookJS, apiBookJS2 database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliBookJS2.UUID).First(&apiBookJS2), "finding apiBookJS2") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiNote2JS.USN, 0, "apiNote2JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") assert.NotEqual(t, apiBookJS2.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiNote2JS.Body, "js2", "apiNote2JS Body mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") assert.Equal(t, apiBookJS2.Label, "js_2", "apiBookJS2 USN mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("client and server edits bodys of the same note", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { cliDB := ctx.DB // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") var nid string cliDatabase.MustScan(t, "getting id of note to edit", cliDB.QueryRow("SELECT rowid FROM notes WHERE uuid = ?", jsNote1UUID), &nid) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "edit", "js", nid, "-c", "js1-edited-from-client") // 3. on server apiPatchNote(t, user, jsNote1UUID, fmt.Sprintf(`{"content": "%s"}`, "js1-edited-from-server"), "editing js note 1") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb resolvedBody := "<<<<<<< Local\njs1-edited-from-client\n=======\njs1-edited-from-server\n>>>>>>> Server\n" checkState(t, ctx, user, systemState{ clientNoteCount: 1, clientBookCount: 1, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 4, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, resolvedBody, "apiNote1JS Body mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") // test client var cliNote1JS cliDatabase.Note var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT body, usn FROM notes WHERE uuid = ?", ids["jsNote1UUID"]), &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.Label, &cliBookJS.USN) // test usn assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, resolvedBody, "cliNote1JS Body mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("clients deletes a note and server edits its body", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { cliDB := ctx.DB // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") var nid string cliDatabase.MustScan(t, "getting id of note to remove", cliDB.QueryRow("SELECT rowid FROM notes WHERE uuid = ?", jsNote1UUID), &nid) clitest.MustWaitDnoteCmd(t, dnoteCmdOpts, clitest.ConfirmRemoveNote, cliBinaryName, "remove", "js", nid) // 3. on server apiPatchNote(t, user, jsNote1UUID, fmt.Sprintf(`{"content": "%s"}`, "js1-edited"), "editing js note 1") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 1, clientBookCount: 1, clientLastMaxUSN: 3, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 3, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1-edited", "apiNote1JS Body mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") // test client var cliNote1JS cliDatabase.Note var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT body, usn FROM notes WHERE uuid = ?", ids["jsNote1UUID"]), &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.Label, &cliBookJS.USN) // test usn assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1-edited", "cliNote1JS Body mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("clients deletes a note and server moves it to another book", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { cliDB := ctx.DB // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") cssBookUUID := apiCreateBook(t, user, "css", "adding css book") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") var nid string cliDatabase.MustScan(t, "getting id of note to remove", cliDB.QueryRow("SELECT rowid FROM notes WHERE uuid = ?", jsNote1UUID), &nid) clitest.MustWaitDnoteCmd(t, dnoteCmdOpts, clitest.ConfirmRemoveNote, cliBinaryName, "remove", "js", nid) // 3. on server apiPatchNote(t, user, jsNote1UUID, fmt.Sprintf(`{"book_uuid": "%s"}`, cssBookUUID), "moving js note 1 to css book") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, "cssBookUUID": cssBookUUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 1, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 2, serverUserMaxUSN: 4, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS, apiBookCSS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssBookUUID"]).First(&apiBookCSS), "finding apiBookCSS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") assert.NotEqual(t, apiBookCSS.USN, 0, "apiBookCSS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, ids["cssBookUUID"], "apiNote1JS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") assert.Equal(t, apiBookCSS.Label, "css", "apiBookCSS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") assert.Equal(t, apiBookCSS.Deleted, false, "apiBookCSS Deleted mismatch") // test client var cliNote1JS cliDatabase.Note var cliBookJS, cliBookCSS cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT body, usn FROM notes WHERE uuid = ?", ids["jsNote1UUID"]), &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cliBookCSS", cliDB.QueryRow("SELECT label, usn FROM books WHERE uuid = ?", ids["cssBookUUID"]), &cliBookCSS.Label, &cliBookCSS.USN) // test usn assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote1JS.BookUUID, ids["cssNote1UUID"], "cliNote1JS BookUUID mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") assert.Equal(t, cliBookCSS.Label, "css", "cliBookCSS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") assert.Equal(t, cliBookCSS.Deleted, false, "cliBookCSS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("server deletes a note and client edits it", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { cliDB := ctx.DB // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server apiDeleteNote(t, user, jsNote1UUID, "deleting js note 1") // 4. on cli var nid string cliDatabase.MustScan(t, "getting id of note to edit", cliDB.QueryRow("SELECT rowid FROM notes WHERE body = ?", "js1"), &nid) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "edit", "js", nid, "-c", "js1-edited") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 1, clientBookCount: 1, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 4, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1-edited", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, ids["jsBookUUID"], "apiNote1JS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") // test client var cliNote1JS cliDatabase.Note var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT body, book_uuid, usn FROM notes WHERE uuid = ?", ids["jsNote1UUID"]), &cliNote1JS.Body, &cliNote1JS.BookUUID, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.Label, &cliBookJS.USN) // test usn assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1-edited", "cliNote1JS Body mismatch") assert.Equal(t, cliNote1JS.BookUUID, ids["jsBookUUID"], "cliNote1JS BookUUID mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("server deletes a book and client edits it by renaming it", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server apiDeleteNote(t, user, jsNote1UUID, "deleting js note 1") // 4. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "edit", "js", "-n", "js-edited") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 0, clientBookCount: 1, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 4, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, ids["jsBookUUID"], "apiNote1JS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "js-edited", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, true, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") // test client var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.Label, &cliBookJS.USN) // test usn assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, cliBookJS.Label, "js-edited", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("server deletes a book and client edits a note in it", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { cliDB := ctx.DB // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server apiDeleteBook(t, user, jsBookUUID, "deleting js book") // 4. on cli var nid string cliDatabase.MustScan(t, "getting id of note to edit", cliDB.QueryRow("SELECT rowid FROM notes WHERE body = ?", "js1"), &nid) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "edit", "js", nid, "-c", "js1-edited") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 1, clientBookCount: 1, clientLastMaxUSN: 6, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 6, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1-edited", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, ids["jsBookUUID"], "apiNote1JS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") // test client var cliNote1JS cliDatabase.Note var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT body, book_uuid, usn FROM notes WHERE uuid = ?", ids["jsNote1UUID"]), &cliNote1JS.Body, &cliNote1JS.BookUUID, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.Label, &cliBookJS.USN) // test usn assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1-edited", "cliNote1JS Body mismatch") assert.Equal(t, cliNote1JS.BookUUID, ids["jsBookUUID"], "cliNote1JS BookUUID mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("client deletes a book and server edits it by renaming it", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") clitest.MustWaitDnoteCmd(t, dnoteCmdOpts, clitest.ConfirmRemoveBook, cliBinaryName, "remove", "js") // 3. on server apiPatchBook(t, user, jsBookUUID, fmt.Sprintf(`{"name": "%s"}`, "js-edited"), "editing js book") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 0, clientBookCount: 1, clientLastMaxUSN: 5, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 5, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, ids["jsBookUUID"], "apiNote1JS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "js-edited", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, true, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") // test client var cliBookJS cliDatabase.Book cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.Label, &cliBookJS.USN) // test usn assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, cliBookJS.Label, "js-edited", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("client deletes a book and server edits a note in it", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server apiPatchNote(t, user, jsNote1UUID, fmt.Sprintf(`{"content": "%s"}`, "js1-edited"), "editing js1 note") // 4. on cli clitest.MustWaitDnoteCmd(t, dnoteCmdOpts, clitest.ConfirmRemoveBook, cliBinaryName, "remove", "js") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 0, clientBookCount: 0, clientLastMaxUSN: 6, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 6, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, ids["jsBookUUID"], "apiNote1JS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, true, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, true, "apiBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("client and server edit a book by renaming it to a same name", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "edit", "js", "-n", "js-edited") // 3. on server apiPatchBook(t, user, jsBookUUID, fmt.Sprintf(`{"name": "%s"}`, "js-edited"), "editing js book") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 1, clientBookCount: 1, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 4, }) // test client var cliBookJS cliDatabase.Book var cliNote1JS cliDatabase.Note cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, body, book_uuid, usn FROM notes WHERE uuid = ?", ids["jsNote1UUID"]), &cliNote1JS.UUID, &cliNote1JS.Body, &cliNote1JS.BookUUID, &cliNote1JS.USN) // assert on usn assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote1JS.BookUUID, cliBookJS.UUID, "cliNote1JS BookUUID mismatch") assert.Equal(t, cliBookJS.Label, "js-edited", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") // assert on dirty assert.Equal(t, cliNote1JS.Dirty, false, "cliNote1JS Dirty mismatch") assert.Equal(t, cliBookJS.Dirty, false, "cliBookJS Dirty mismatch") // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiBookJS.Label, "js-edited", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("client and server edit a book by renaming it to different names", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "edit", "js", "-n", "js-edited-client") // 3. on server apiPatchBook(t, user, jsBookUUID, fmt.Sprintf(`{"name": "%s"}`, "js-edited-server"), "editing js book") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { // In this case, server's change wins and overwrites that of client's cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 1, clientBookCount: 1, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 1, serverUserMaxUSN: 4, }) // test client var cliBookJS cliDatabase.Book var cliNote1JS cliDatabase.Note cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE uuid = ?", ids["jsBookUUID"]), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, body, book_uuid, usn FROM notes WHERE uuid = ?", ids["jsNote1UUID"]), &cliNote1JS.UUID, &cliNote1JS.Body, &cliNote1JS.BookUUID, &cliNote1JS.USN) // assert on usn assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote1JS.BookUUID, cliBookJS.UUID, "cliNote1JS BookUUID mismatch") assert.Equal(t, cliBookJS.Label, "js-edited-server", "cliBookJS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") // assert on dirty assert.Equal(t, cliNote1JS.Dirty, false, "cliNote1JS Dirty mismatch") assert.Equal(t, cliBookJS.Dirty, false, "cliBookJS Dirty mismatch") // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiBookJS.Label, "js-edited-server", "apiBookJS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("client moves a note", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") cssBookUUID := apiCreateBook(t, user, "css", "adding a css book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "edit", "1", "-b", "css") return map[string]string{ "jsBookUUID": jsBookUUID, "cssBookUUID": cssBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 1, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 2, serverUserMaxUSN: 4, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS, apiBookCSS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssBookUUID"]).First(&apiBookCSS), "finding apiBookCSS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, ids["cssBookUUID"], "apiNote1JS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") assert.Equal(t, apiBookCSS.Label, "css", "apiBookCSS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") assert.Equal(t, apiBookCSS.Deleted, false, "apiBookCSS Deleted mismatch") // test client var cliBookJS, cliBookCSS cliDatabase.Book var cliNote1JS cliDatabase.Note cliDatabase.MustScan(t, "finding cli book js", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cli book css", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css"), &cliBookCSS.UUID, &cliBookCSS.Label, &cliBookCSS.USN) cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, book_uuid, body, usn FROM notes WHERE body = ?", "js1"), &cliNote1JS.UUID, &cliNote1JS.BookUUID, &cliNote1JS.Body, &cliNote1JS.USN) // assert on usn assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") assert.NotEqual(t, cliBookCSS.USN, 0, "cliBookCSS USN mismatch") assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote1JS.BookUUID, cliBookCSS.UUID, "cliNote1JS BookUUID mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") assert.Equal(t, cliBookCSS.Label, "css", "cliBookCSS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") assert.Equal(t, cliBookCSS.Deleted, false, "cliBookCSS Deleted mismatch") // assert on dirty assert.Equal(t, cliNote1JS.Dirty, false, "cliNote1JS Dirty mismatch") assert.Equal(t, cliBookJS.Dirty, false, "cliBookJS Dirty mismatch") assert.Equal(t, cliBookCSS.Dirty, false, "cliBookCSS Dirty mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("client and server each moves a note to a same book", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") cssBookUUID := apiCreateBook(t, user, "css", "adding a css book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server apiPatchNote(t, user, jsNote1UUID, fmt.Sprintf(`{"book_uuid": "%s"}`, cssBookUUID), "moving js note 1 to css book") // 3. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "edit", "1", "-b", "css") return map[string]string{ "jsBookUUID": jsBookUUID, "cssBookUUID": cssBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 1, clientBookCount: 2, clientLastMaxUSN: 5, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 2, serverUserMaxUSN: 5, }) // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS, apiBookCSS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssBookUUID"]).First(&apiBookCSS), "finding apiBookCSS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, ids["cssBookUUID"], "apiNote1JS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") assert.Equal(t, apiBookCSS.Label, "css", "apiBookCSS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") assert.Equal(t, apiBookCSS.Deleted, false, "apiBookCSS Deleted mismatch") // test client var cliBookJS, cliBookCSS cliDatabase.Book var cliNote1JS cliDatabase.Note cliDatabase.MustScan(t, "finding cli book js", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cli book css", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css"), &cliBookCSS.UUID, &cliBookCSS.Label, &cliBookCSS.USN) cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, book_uuid, body, usn FROM notes WHERE body = ?", "js1"), &cliNote1JS.UUID, &cliNote1JS.BookUUID, &cliNote1JS.Body, &cliNote1JS.USN) // assert on usn assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") assert.NotEqual(t, cliBookCSS.USN, 0, "cliBookCSS USN mismatch") assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote1JS.BookUUID, cliBookCSS.UUID, "cliNote1JS BookUUID mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") assert.Equal(t, cliBookCSS.Label, "css", "cliBookCSS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") assert.Equal(t, cliBookCSS.Deleted, false, "cliBookCSS Deleted mismatch") // assert on dirty assert.Equal(t, cliNote1JS.Dirty, false, "cliNote1JS Dirty mismatch") assert.Equal(t, cliBookJS.Dirty, false, "cliBookJS Dirty mismatch") assert.Equal(t, cliBookCSS.Dirty, false, "cliBookCSS Dirty mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("client and server each moves a note to different books", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") cssBookUUID := apiCreateBook(t, user, "css", "adding a css book") linuxBookUUID := apiCreateBook(t, user, "linux", "adding a linux book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server apiPatchNote(t, user, jsNote1UUID, fmt.Sprintf(`{"book_uuid": "%s"}`, cssBookUUID), "moving js note 1 to css book") // 3. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "edit", "1", "-b", "linux") return map[string]string{ "jsBookUUID": jsBookUUID, "cssBookUUID": cssBookUUID, "jsNote1UUID": jsNote1UUID, "linuxBookUUID": linuxBookUUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb expectedNote1JSBody := `<<<<<<< Local Moved to the book linux ======= Moved to the book css >>>>>>> Server js1` checkState(t, ctx, user, systemState{ clientNoteCount: 1, clientBookCount: 4, clientLastMaxUSN: 7, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 1, serverBookCount: 4, serverUserMaxUSN: 7, }) // test client var cliBookJS, cliBookCSS, cliBookLinux, cliBookConflicts cliDatabase.Book var cliNote1JS cliDatabase.Note cliDatabase.MustScan(t, "finding cli book js", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cli book css", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css"), &cliBookCSS.UUID, &cliBookCSS.Label, &cliBookCSS.USN) cliDatabase.MustScan(t, "finding cliBookLinux", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "linux"), &cliBookLinux.UUID, &cliBookLinux.Label, &cliBookLinux.USN) cliDatabase.MustScan(t, "finding cliBookConflicts", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "conflicts"), &cliBookConflicts.UUID, &cliBookConflicts.Label, &cliBookConflicts.USN) cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, book_uuid, body, usn FROM notes WHERE uuid = ?", ids["jsNote1UUID"]), &cliNote1JS.UUID, &cliNote1JS.BookUUID, &cliNote1JS.Body, &cliNote1JS.USN) // assert on usn assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") assert.NotEqual(t, cliBookCSS.USN, 0, "cliBookCSS USN mismatch") assert.NotEqual(t, cliBookLinux.USN, 0, "cliBookLinux USN mismatch") assert.NotEqual(t, cliBookConflicts.USN, 0, "cliBookConflicts USN mismatch") assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, expectedNote1JSBody, "cliNote1JS Body mismatch") assert.Equal(t, cliNote1JS.BookUUID, cliBookConflicts.UUID, "cliNote1JS BookUUID mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") assert.Equal(t, cliBookCSS.Label, "css", "cliBookCSS Label mismatch") assert.Equal(t, cliBookLinux.Label, "linux", "cliBookLinux Label mismatch") assert.Equal(t, cliBookConflicts.Label, "conflicts", "cliBookConflicts Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") assert.Equal(t, cliBookCSS.Deleted, false, "cliBookCSS Deleted mismatch") assert.Equal(t, cliBookLinux.Deleted, false, "cliBookLinux Deleted mismatch") assert.Equal(t, cliBookConflicts.Deleted, false, "cliBookConflicts Deleted mismatch") // assert on dirty assert.Equal(t, cliNote1JS.Dirty, false, "cliNote1JS Dirty mismatch") assert.Equal(t, cliBookJS.Dirty, false, "cliBookJS Dirty mismatch") assert.Equal(t, cliBookCSS.Dirty, false, "cliBookCSS Dirty mismatch") assert.Equal(t, cliBookLinux.Dirty, false, "cliBookLinux Dirty mismatch") assert.Equal(t, cliBookConflicts.Dirty, false, "cliBookConflicts Dirty mismatch") // test server var apiNote1JS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") var apiBookJS, apiBookCSS, apiBookLinux, apiBookConflicts database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssBookUUID"]).First(&apiBookCSS), "finding apiBookCSS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["linuxBookUUID"]).First(&apiBookLinux), "finding apiBookLinux") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliBookConflicts.UUID).First(&apiBookConflicts), "finding apiBookConflicts") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") assert.NotEqual(t, apiBookCSS.USN, 0, "apiBookCSS USN mismatch") assert.NotEqual(t, apiBookConflicts.USN, 0, "apiBookConflicts USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, expectedNote1JSBody, "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, apiBookConflicts.UUID, "apiNote1JS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") assert.Equal(t, apiBookCSS.Label, "css", "apiBookCSS Label mismatch") assert.Equal(t, apiBookLinux.Label, "linux", "apiBookLinux Label mismatch") assert.Equal(t, apiBookConflicts.Label, "conflicts", "apiBookConflicts Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") assert.Equal(t, apiBookCSS.Deleted, false, "apiBookCSS Deleted mismatch") assert.Equal(t, apiBookLinux.Deleted, false, "apiBookLinux Deleted mismatch") assert.Equal(t, apiBookConflicts.Deleted, false, "apiBookConflicts Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("client adds a new book and moves a note into it", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css1") cliDB := ctx.DB var nid string cliDatabase.MustScan(t, "getting id of note to edit", cliDB.QueryRow("SELECT rowid FROM notes WHERE body = ?", "js1"), &nid) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "edit", "js", nid, "-b", "css") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 2, clientBookCount: 2, clientLastMaxUSN: 5, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 2, serverUserMaxUSN: 5, }) // test client var cliBookJS, cliBookCSS cliDatabase.Book var cliNote1JS, cliNote1CSS cliDatabase.Note cliDatabase.MustScan(t, "finding cli book js", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cli book css", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css"), &cliBookCSS.UUID, &cliBookCSS.Label, &cliBookCSS.USN) cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, book_uuid, body, usn FROM notes WHERE body = ?", "js1"), &cliNote1JS.UUID, &cliNote1JS.BookUUID, &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliNote1CSS", cliDB.QueryRow("SELECT uuid, book_uuid, body, usn FROM notes WHERE body = ?", "css1"), &cliNote1CSS.UUID, &cliNote1CSS.BookUUID, &cliNote1CSS.Body, &cliNote1CSS.USN) // assert on usn assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") assert.NotEqual(t, cliBookCSS.USN, 0, "cliBookCSS USN mismatch") assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") assert.NotEqual(t, cliNote1CSS.USN, 0, "cliNote1CSS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote1JS.BookUUID, cliBookCSS.UUID, "cliNote1JS BookUUID mismatch") assert.Equal(t, cliNote1CSS.Body, "css1", "cliNote1CSS Body mismatch") assert.Equal(t, cliNote1CSS.BookUUID, cliBookCSS.UUID, "cliNote1CSS BookUUID mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") assert.Equal(t, cliBookCSS.Label, "css", "cliBookCSS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliNote1CSS.Deleted, false, "cliNote1CSS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") assert.Equal(t, cliBookCSS.Deleted, false, "cliBookCSS Deleted mismatch") // assert on dirty assert.Equal(t, cliNote1JS.Dirty, false, "cliNote1JS Dirty mismatch") assert.Equal(t, cliNote1CSS.Dirty, false, "cliNote1CSS Dirty mismatch") assert.Equal(t, cliBookJS.Dirty, false, "cliBookJS Dirty mismatch") assert.Equal(t, cliBookCSS.Dirty, false, "cliBookCSS Dirty mismatch") // test server var apiNote1JS, apiNote1CSS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote1CSS.UUID).First(&apiNote1CSS), "finding apiNote1CSS") var apiBookJS, apiBookCSS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliBookCSS.UUID).First(&apiBookCSS), "finding apiBookCSS") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiNote1CSS.USN, 0, "apiNote1CSS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") assert.NotEqual(t, apiBookCSS.USN, 0, "apiBookCSS USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, apiBookCSS.UUID, "apiNote1JS BookUUID mismatch") assert.Equal(t, apiNote1CSS.Body, "css1", "apiNote1CSS Body mismatch") assert.Equal(t, apiNote1CSS.BookUUID, apiBookCSS.UUID, "apiNote1CSS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") assert.Equal(t, apiBookCSS.Label, "css", "apiBookCSS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiNote1CSS.Deleted, false, "apiNote1CSS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") assert.Equal(t, apiBookCSS.Deleted, false, "apiBookCSS Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) t.Run("client adds a duplicate book and moves a note into it", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { // 1. on server jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") // 2. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // 3. on server cssBookUUID := apiCreateBook(t, user, "css", "adding a css book") // 3. on cli clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css1") var nid string cliDatabase.MustScan(t, "getting id of note to edit", ctx.DB.QueryRow("SELECT rowid FROM notes WHERE body = ?", "js1"), &nid) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "edit", nid, "-b", "css") return map[string]string{ "jsBookUUID": jsBookUUID, "cssBookUUID": cssBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 2, clientBookCount: 3, clientLastMaxUSN: 6, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 3, serverUserMaxUSN: 6, }) // test client var cliBookJS, cliBookCSS, cliBookCSS2 cliDatabase.Book var cliNote1JS, cliNote1CSS cliDatabase.Note cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cliBookCSS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css"), &cliBookCSS.UUID, &cliBookCSS.Label, &cliBookCSS.USN) cliDatabase.MustScan(t, "finding cliBookCSS2", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css_2"), &cliBookCSS2.UUID, &cliBookCSS2.Label, &cliBookCSS2.USN) cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, body, book_uuid, usn FROM notes WHERE body = ?", "js1"), &cliNote1JS.UUID, &cliNote1JS.Body, &cliNote1JS.BookUUID, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliNote1CSS", cliDB.QueryRow("SELECT uuid, body, book_uuid, usn FROM notes WHERE body = ?", "css1"), &cliNote1CSS.UUID, &cliNote1CSS.Body, &cliNote1CSS.BookUUID, &cliNote1CSS.USN) // assert on usn assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") assert.NotEqual(t, cliBookCSS.USN, 0, "cliBookCSS USN mismatch") assert.NotEqual(t, cliBookCSS2.USN, 0, "cliBookCSS2 USN mismatch") assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") assert.NotEqual(t, cliNote1CSS.USN, 0, "cliNote1CSS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote1JS.BookUUID, cliBookCSS2.UUID, "cliNote1JS BookUUID mismatch") assert.Equal(t, cliNote1CSS.Body, "css1", "cliNote1CSS Body mismatch") assert.Equal(t, cliNote1CSS.BookUUID, cliBookCSS2.UUID, "cliNote1CSS BookUUID mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") assert.Equal(t, cliBookCSS.Label, "css", "cliBookCSS Label mismatch") assert.Equal(t, cliBookCSS2.Label, "css_2", "cliBookCSS2 Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliNote1CSS.Deleted, false, "cliNote1CSS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") assert.Equal(t, cliBookCSS.Deleted, false, "cliBookCSS Deleted mismatch") assert.Equal(t, cliBookCSS2.Deleted, false, "cliBookCSS2 Deleted mismatch") // assert on dirty assert.Equal(t, cliNote1JS.Dirty, false, "cliNote1JS Dirty mismatch") assert.Equal(t, cliNote1CSS.Dirty, false, "cliNote1CSS Dirty mismatch") assert.Equal(t, cliBookJS.Dirty, false, "cliBookJS Dirty mismatch") assert.Equal(t, cliBookCSS.Dirty, false, "cliBookCSS Dirty mismatch") assert.Equal(t, cliBookCSS2.Dirty, false, "cliBookCSS2 Dirty mismatch") // test server var apiNote1JS, apiNote1CSS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding apiNote1JS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote1CSS.UUID).First(&apiNote1CSS), "finding apiNote1CSS") var apiBookJS, apiBookCSS, apiBookCSS2 database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding apiBookJS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["cssBookUUID"]).First(&apiBookCSS), "finding apiBookCSS") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliBookCSS2.UUID).First(&apiBookCSS2), "finding apiBookCSS2") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS USN mismatch") assert.NotEqual(t, apiNote1CSS.USN, 0, "apiNote1CSS USN mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS USN mismatch") assert.NotEqual(t, apiBookCSS.USN, 0, "apiBookCSS USN mismatch") assert.NotEqual(t, apiBookCSS2.USN, 0, "apiBookCSS2 USN mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1JS.BookUUID, apiBookCSS2.UUID, "apiNote1JS BookUUID mismatch") assert.Equal(t, apiNote1CSS.Body, "css1", "apiNote1CSS Body mismatch") assert.Equal(t, apiNote1CSS.BookUUID, apiBookCSS2.UUID, "apiNote1CSS BookUUID mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") assert.Equal(t, apiBookCSS.Label, "css", "apiBookCSS Label mismatch") assert.Equal(t, apiBookCSS2.Label, "css_2", "apiBookCSS2 Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiNote1CSS.Deleted, false, "apiNote1CSS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") assert.Equal(t, apiBookCSS.Deleted, false, "apiBookCSS Deleted mismatch") assert.Equal(t, apiBookCSS2.Deleted, false, "apiBookCSS2 Deleted mismatch") } testSyncCmd(t, false, setup, assert) testSyncCmd(t, true, setup, assert) }) } func TestFullSync(t *testing.T) { t.Run("consecutively with stepSync", func(t *testing.T) { setup := func(t *testing.T, ctx context.DnoteCtx, user database.User) map[string]string { jsBookUUID := apiCreateBook(t, user, "js", "adding a js book") jsNote1UUID := apiCreateNote(t, user, jsBookUUID, "js1", "adding js note 1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css1") return map[string]string{ "jsBookUUID": jsBookUUID, "jsNote1UUID": jsNote1UUID, } } assert := func(t *testing.T, ctx context.DnoteCtx, user database.User, ids map[string]string) { cliDB := ctx.DB apiDB := serverDb checkState(t, ctx, user, systemState{ clientNoteCount: 2, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 2, serverUserMaxUSN: 4, }) // test client var cliNote1JS, cliNote1CSS cliDatabase.Note var cliBookJS, cliBookCSS cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js1"), &cliNote1JS.UUID, &cliNote1JS.Body, &cliNote1JS.USN) cliDatabase.MustScan(t, "finding cliNote1CSS", cliDB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "css1"), &cliNote1CSS.UUID, &cliNote1CSS.Body, &cliNote1CSS.USN) cliDatabase.MustScan(t, "finding cliBookJS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding cliBookCSS", cliDB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css"), &cliBookCSS.UUID, &cliBookCSS.Label, &cliBookCSS.USN) // test usn assert.NotEqual(t, cliNote1JS.USN, 0, "cliNote1JS USN mismatch") assert.NotEqual(t, cliNote1CSS.USN, 0, "cliNote1CSS USN mismatch") assert.NotEqual(t, cliBookJS.USN, 0, "cliBookJS USN mismatch") assert.NotEqual(t, cliBookCSS.USN, 0, "cliBookCSS USN mismatch") // assert on bodys and labels assert.Equal(t, cliNote1JS.Body, "js1", "cliNote1JS Body mismatch") assert.Equal(t, cliNote1CSS.Body, "css1", "cliNote1CSS Body mismatch") assert.Equal(t, cliBookJS.Label, "js", "cliBookJS Label mismatch") assert.Equal(t, cliBookCSS.Label, "css", "cliBookCSS Label mismatch") // assert on deleted assert.Equal(t, cliNote1JS.Deleted, false, "cliNote1JS Deleted mismatch") assert.Equal(t, cliNote1CSS.Deleted, false, "cliNote1CSS Deleted mismatch") assert.Equal(t, cliBookJS.Deleted, false, "cliBookJS Deleted mismatch") assert.Equal(t, cliBookCSS.Deleted, false, "cliBookCSS Deleted mismatch") // test server var apiNote1JS, apiNote1CSS database.Note apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsNote1UUID"]).First(&apiNote1JS), "finding api js note 1") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliNote1CSS.UUID).First(&apiNote1CSS), "finding api css note 1") var apiBookJS, apiBookCSS database.Book apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, ids["jsBookUUID"]).First(&apiBookJS), "finding api js book") apitest.MustExec(t, apiDB.Where("user_id = ? AND uuid = ?", user.ID, cliBookCSS.UUID).First(&apiBookCSS), "finding api css book") // assert on usn assert.NotEqual(t, apiNote1JS.USN, 0, "apiNote1JS usn mismatch") assert.NotEqual(t, apiNote1CSS.USN, 0, "apiNote1CSS usn mismatch") assert.NotEqual(t, apiBookJS.USN, 0, "apiBookJS usn mismatch") assert.NotEqual(t, apiBookCSS.USN, 0, "apiBookCSS usn mismatch") // assert on bodys and labels assert.Equal(t, apiNote1JS.Body, "js1", "apiNote1JS Body mismatch") assert.Equal(t, apiNote1CSS.Body, "css1", "apiNote1CSS Body mismatch") assert.Equal(t, apiBookJS.Label, "js", "apiBookJS Label mismatch") assert.Equal(t, apiBookCSS.Label, "css", "apiBookCSS Label mismatch") // assert on deleted assert.Equal(t, apiNote1JS.Deleted, false, "apiNote1JS Deleted mismatch") assert.Equal(t, apiNote1CSS.Deleted, false, "apiNote1CSS Deleted mismatch") assert.Equal(t, apiBookJS.Deleted, false, "apiBookJS Deleted mismatch") assert.Equal(t, apiBookCSS.Deleted, false, "apiBookCSS Deleted mismatch") } t.Run("stepSync then fullSync", func(t *testing.T) { // clean up os.RemoveAll(tmpDirPath) apitest.ClearData(serverDb) defer apitest.ClearData(serverDb) ctx := context.InitTestCtx(t, paths, nil) defer context.TeardownTestCtx(t, ctx) user := setupUserAndLogin(t, ctx.DB) ids := setup(t, ctx, user) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") assert(t, ctx, user, ids) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync", "-f") assert(t, ctx, user, ids) }) t.Run("fullSync then stepSync", func(t *testing.T) { // clean up os.RemoveAll(tmpDirPath) apitest.ClearData(serverDb) defer apitest.ClearData(serverDb) ctx := context.InitTestCtx(t, paths, nil) defer context.TeardownTestCtx(t, ctx) user := setupUserAndLogin(t, ctx.DB) ids := setup(t, ctx, user) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync", "-f") assert(t, ctx, user, ids) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") assert(t, ctx, user, ids) }) }) } func TestSync_EmptyServer(t *testing.T) { t.Run("sync to empty server after syncing to non-empty server", func(t *testing.T) { // Test server data loss/wipe scenario (disaster recovery): // Verify empty server detection works when the server loses all its data // clean up apitest.ClearData(serverDb) defer apitest.ClearData(serverDb) clearTmp(t) ctx := context.InitTestCtx(t, paths, nil) defer context.TeardownTestCtx(t, ctx) user := setupUserAndLogin(t, ctx.DB) // Step 1: Create local data and sync to server clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // Verify sync succeeded checkState(t, ctx, user, systemState{ clientNoteCount: 2, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 2, serverUserMaxUSN: 4, }) // Step 2: Clear all server data to simulate switching to a completely new empty server apitest.ClearData(serverDb) // Recreate user and session (simulating a new server) user = setupUserAndLogin(t, ctx.DB) // Step 3: Sync again - should detect empty server and prompt user // User confirms with "y" clitest.MustWaitDnoteCmd(t, dnoteCmdOpts, clitest.UserConfirmEmptyServerSync, cliBinaryName, "sync") // Step 4: Verify data was uploaded to the empty server checkState(t, ctx, user, systemState{ clientNoteCount: 2, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 2, serverUserMaxUSN: 4, }) // Verify the content is correct on both client and server var cliNote1JS, cliNote1CSS cliDatabase.Note var cliBookJS, cliBookCSS cliDatabase.Book cliDatabase.MustScan(t, "finding cliNote1JS", ctx.DB.QueryRow("SELECT uuid, body FROM notes WHERE body = ?", "js1"), &cliNote1JS.UUID, &cliNote1JS.Body) cliDatabase.MustScan(t, "finding cliNote1CSS", ctx.DB.QueryRow("SELECT uuid, body FROM notes WHERE body = ?", "css1"), &cliNote1CSS.UUID, &cliNote1CSS.Body) cliDatabase.MustScan(t, "finding cliBookJS", ctx.DB.QueryRow("SELECT uuid, label FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label) cliDatabase.MustScan(t, "finding cliBookCSS", ctx.DB.QueryRow("SELECT uuid, label FROM books WHERE label = ?", "css"), &cliBookCSS.UUID, &cliBookCSS.Label) assert.Equal(t, cliNote1JS.Body, "js1", "js note body mismatch") assert.Equal(t, cliNote1CSS.Body, "css1", "css note body mismatch") assert.Equal(t, cliBookJS.Label, "js", "js book label mismatch") assert.Equal(t, cliBookCSS.Label, "css", "css book label mismatch") // Verify on server side var serverNoteJS, serverNoteCSS database.Note var serverBookJS, serverBookCSS database.Book apitest.MustExec(t, serverDb.Where("body = ?", "js1").First(&serverNoteJS), "finding server note js1") apitest.MustExec(t, serverDb.Where("body = ?", "css1").First(&serverNoteCSS), "finding server note css1") apitest.MustExec(t, serverDb.Where("label = ?", "js").First(&serverBookJS), "finding server book js") apitest.MustExec(t, serverDb.Where("label = ?", "css").First(&serverBookCSS), "finding server book css") assert.Equal(t, serverNoteJS.Body, "js1", "server js note body mismatch") assert.Equal(t, serverNoteCSS.Body, "css1", "server css note body mismatch") assert.Equal(t, serverBookJS.Label, "js", "server js book label mismatch") assert.Equal(t, serverBookCSS.Label, "css", "server css book label mismatch") }) t.Run("user cancels empty server prompt", func(t *testing.T) { // clean up apitest.ClearData(serverDb) defer apitest.ClearData(serverDb) clearTmp(t) ctx := context.InitTestCtx(t, paths, nil) defer context.TeardownTestCtx(t, ctx) user := setupUserAndLogin(t, ctx.DB) // Step 1: Create local data and sync to server clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // Verify initial sync succeeded checkState(t, ctx, user, systemState{ clientNoteCount: 2, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 2, serverUserMaxUSN: 4, }) // Step 2: Clear all server data apitest.ClearData(serverDb) user = setupUserAndLogin(t, ctx.DB) // Step 3: Sync again but user cancels with "n" output, err := clitest.WaitDnoteCmd(t, dnoteCmdOpts, clitest.UserCancelEmptyServerSync, cliBinaryName, "sync") if err == nil { t.Fatal("Expected sync to fail when user cancels, but it succeeded") } // Verify the prompt appeared if !strings.Contains(output, clitest.PromptEmptyServer) { t.Fatalf("Expected empty server warning in output, got: %s", output) } // Step 4: Verify local state unchanged (transaction rolled back) checkState(t, ctx, user, systemState{ clientNoteCount: 2, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 0, serverBookCount: 0, serverUserMaxUSN: 0, }) // Verify items still have original USN and dirty=false var book cliDatabase.Book var note cliDatabase.Note cliDatabase.MustScan(t, "checking book state", ctx.DB.QueryRow("SELECT usn, dirty FROM books WHERE label = ?", "js"), &book.USN, &book.Dirty) cliDatabase.MustScan(t, "checking note state", ctx.DB.QueryRow("SELECT usn, dirty FROM notes WHERE body = ?", "js1"), ¬e.USN, ¬e.Dirty) assert.NotEqual(t, book.USN, 0, "book USN should not be reset") assert.NotEqual(t, note.USN, 0, "note USN should not be reset") assert.Equal(t, book.Dirty, false, "book should not be marked dirty") assert.Equal(t, note.Dirty, false, "note should not be marked dirty") }) t.Run("all local data is marked deleted - should not upload", func(t *testing.T) { // Test edge case: Server MaxUSN=0, local MaxUSN>0, but all items are deleted=true // Should NOT prompt because there's nothing to upload // clean up apitest.ClearData(serverDb) defer apitest.ClearData(serverDb) clearTmp(t) ctx := context.InitTestCtx(t, paths, nil) defer context.TeardownTestCtx(t, ctx) user := setupUserAndLogin(t, ctx.DB) // Step 1: Create local data and sync to server clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // Verify initial sync succeeded checkState(t, ctx, user, systemState{ clientNoteCount: 2, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 2, serverUserMaxUSN: 4, }) // Step 2: Delete all local notes and books (mark as deleted) cliDatabase.MustExec(t, "marking all books deleted", ctx.DB, "UPDATE books SET deleted = 1") cliDatabase.MustExec(t, "marking all notes deleted", ctx.DB, "UPDATE notes SET deleted = 1") // Step 3: Clear server data to simulate switching to empty server apitest.ClearData(serverDb) user = setupUserAndLogin(t, ctx.DB) // Step 4: Sync - should NOT prompt because bookCount=0 and noteCount=0 (counting only deleted=0) // This should complete without user interaction clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // Verify no data was uploaded (server still empty, but client still has deleted items) // Check server is empty var serverNoteCount, serverBookCount int64 apitest.MustExec(t, serverDb.Model(&database.Note{}).Count(&serverNoteCount), "counting server notes") apitest.MustExec(t, serverDb.Model(&database.Book{}).Count(&serverBookCount), "counting server books") assert.Equal(t, serverNoteCount, int64(0), "server should have no notes") assert.Equal(t, serverBookCount, int64(0), "server should have no books") // Check client still has the deleted items locally var clientNoteCount, clientBookCount int cliDatabase.MustScan(t, "counting client notes", ctx.DB.QueryRow("SELECT count(*) FROM notes WHERE deleted = 1"), &clientNoteCount) cliDatabase.MustScan(t, "counting client books", ctx.DB.QueryRow("SELECT count(*) FROM books WHERE deleted = 1"), &clientBookCount) assert.Equal(t, clientNoteCount, 2, "client should still have 2 deleted notes") assert.Equal(t, clientBookCount, 2, "client should still have 2 deleted books") // Verify lastMaxUSN was reset to 0 var lastMaxUSN int cliDatabase.MustScan(t, "getting lastMaxUSN", ctx.DB.QueryRow("SELECT value FROM system WHERE key = ?", consts.SystemLastMaxUSN), &lastMaxUSN) assert.Equal(t, lastMaxUSN, 0, "lastMaxUSN should be reset to 0") }) t.Run("race condition - other client uploads first", func(t *testing.T) { // This test exercises a race condition that can occur during sync: // While Client A is waiting for user input, Client B uploads data to the server. // // The empty server scenario is the natural place to test this because // an empty server detection triggers a prompt, at which point the test // can make client B upload data. We trigger the race condition deterministically. // // Test flow: // - Client A detects empty server and prompts user // - While waiting for confirmation, Client B uploads the same data via API // - Client A continues and handles the 409 conflict gracefully by: // 1. Detecting the 409 error when trying to CREATE books that already exist // 2. Running stepSync to pull the server's books (js, css) // 3. mergeBook renames local conflicts (js→js_2, css→css_2) // 4. Retrying sendChanges to upload the renamed books // - Result: Both clients' data is preserved (4 books total) // Clean up apitest.ClearData(serverDb) defer apitest.ClearData(serverDb) clearTmp(t) ctx := context.InitTestCtx(t, paths, nil) defer context.TeardownTestCtx(t, ctx) user := setupUserAndLogin(t, ctx.DB) // Step 1: Create local data and sync to establish lastMaxUSN > 0 clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // Verify initial sync succeeded checkState(t, ctx, user, systemState{ clientNoteCount: 2, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 2, serverUserMaxUSN: 4, }) // Step 2: Clear server to simulate switching to empty server apitest.ClearData(serverDb) user = setupUserAndLogin(t, ctx.DB) // Step 3: Trigger sync which will detect empty server and prompt user // Inside the callback (before confirming), we simulate Client B uploading via API. // We wait for the empty server prompt to ensure Client B uploads AFTER // GetSyncState but BEFORE the sync decision, creating the race condition deterministically raceCallback := func(stdout io.Reader, stdin io.WriteCloser) error { // First, wait for the prompt to ensure Client A has obtained the sync state from the server. clitest.MustWaitForPrompt(t, stdout, clitest.PromptEmptyServer) // Now Client B uploads the same data via API (after Client A got the sync state from the server // but before its sync decision) // This creates the race condition: Client A thinks server is empty, but Client B uploads data jsBookUUID := apiCreateBook(t, user, "js", "client B creating js book") cssBookUUID := apiCreateBook(t, user, "css", "client B creating css book") apiCreateNote(t, user, jsBookUUID, "js1", "client B creating js note") apiCreateNote(t, user, cssBookUUID, "css1", "client B creating css note") // Now user confirms if _, err := io.WriteString(stdin, "y\n"); err != nil { return errors.Wrap(err, "confirming sync") } return nil } // Step 4: Client A runs sync with race condition // The 409 conflict is automatically handled: // - When 409 is detected, isBehind flag is set // - stepSync pulls Client B's data // - mergeBook renames Client A's books to js_2, css_2 // - Renamed books are uploaded // - Both clients' data is preserved. clitest.MustWaitDnoteCmd(t, dnoteCmdOpts, raceCallback, cliBinaryName, "sync") // Verify final state - both clients' data preserved checkStateWithDB(t, ctx.DB, user, serverDb, systemState{ clientNoteCount: 4, // Both clients' notes clientBookCount: 4, // js, css, js_2, css_2 clientLastMaxUSN: 8, // 4 from Client B + 4 from Client A's renamed books/notes clientLastSyncAt: serverTime.Unix(), serverNoteCount: 4, serverBookCount: 4, serverUserMaxUSN: 8, }) // Verify server has both clients' books var svrBookJS, svrBookCSS, svrBookJS2, svrBookCSS2 database.Book apitest.MustExec(t, serverDb.Where("label = ?", "js").First(&svrBookJS), "finding server book 'js'") apitest.MustExec(t, serverDb.Where("label = ?", "css").First(&svrBookCSS), "finding server book 'css'") apitest.MustExec(t, serverDb.Where("label = ?", "js_2").First(&svrBookJS2), "finding server book 'js_2'") apitest.MustExec(t, serverDb.Where("label = ?", "css_2").First(&svrBookCSS2), "finding server book 'css_2'") assert.Equal(t, svrBookJS.Label, "js", "server should have book 'js' (Client B)") assert.Equal(t, svrBookCSS.Label, "css", "server should have book 'css' (Client B)") assert.Equal(t, svrBookJS2.Label, "js_2", "server should have book 'js_2' (Client A renamed)") assert.Equal(t, svrBookCSS2.Label, "css_2", "server should have book 'css_2' (Client A renamed)") // Verify client has all books var cliBookJS, cliBookCSS, cliBookJS2, cliBookCSS2 cliDatabase.Book cliDatabase.MustScan(t, "finding client book 'js'", ctx.DB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding client book 'css'", ctx.DB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css"), &cliBookCSS.UUID, &cliBookCSS.Label, &cliBookCSS.USN) cliDatabase.MustScan(t, "finding client book 'js_2'", ctx.DB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js_2"), &cliBookJS2.UUID, &cliBookJS2.Label, &cliBookJS2.USN) cliDatabase.MustScan(t, "finding client book 'css_2'", ctx.DB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css_2"), &cliBookCSS2.UUID, &cliBookCSS2.Label, &cliBookCSS2.USN) // Verify client UUIDs match server assert.Equal(t, cliBookJS.UUID, svrBookJS.UUID, "client 'js' UUID should match server") assert.Equal(t, cliBookCSS.UUID, svrBookCSS.UUID, "client 'css' UUID should match server") assert.Equal(t, cliBookJS2.UUID, svrBookJS2.UUID, "client 'js_2' UUID should match server") assert.Equal(t, cliBookCSS2.UUID, svrBookCSS2.UUID, "client 'css_2' UUID should match server") // Verify all items have non-zero USN (synced successfully) assert.NotEqual(t, cliBookJS.USN, 0, "client 'js' should have non-zero USN") assert.NotEqual(t, cliBookCSS.USN, 0, "client 'css' should have non-zero USN") assert.NotEqual(t, cliBookJS2.USN, 0, "client 'js_2' should have non-zero USN") assert.NotEqual(t, cliBookCSS2.USN, 0, "client 'css_2' should have non-zero USN") }) t.Run("sync to server A, then B, then back to A, then back to B", func(t *testing.T) { // Test switching between two actual servers to verify: // 1. Empty server detection works when switching to empty server // 2. No false detection when switching back to non-empty servers // 3. Both servers maintain independent state across multiple switches // Clean up clearTmp(t) ctx := context.InitTestCtx(t, paths, nil) defer context.TeardownTestCtx(t, ctx) // Create Server A with its own database dbPathA := fmt.Sprintf("%s/serverA.db", testDir) defer os.Remove(dbPathA) serverA, serverDbA, err := setupTestServer(dbPathA, serverTime) if err != nil { t.Fatal(errors.Wrap(err, "setting up server A")) } defer serverA.Close() // Create Server B with its own database dbPathB := fmt.Sprintf("%s/serverB.db", testDir) defer os.Remove(dbPathB) serverB, serverDbB, err := setupTestServer(dbPathB, serverTime) if err != nil { t.Fatal(errors.Wrap(err, "setting up server B")) } defer serverB.Close() // Step 1: Set up user on Server A and sync apiEndpointA := fmt.Sprintf("%s/api", serverA.URL) userA := apitest.SetupUserData(serverDbA) apitest.SetupAccountData(serverDbA, userA, "alice@example.com", "pass1234") sessionA := apitest.SetupSession(serverDbA, userA) cliDatabase.MustExec(t, "inserting session_key", ctx.DB, "INSERT INTO system (key, value) VALUES (?, ?)", consts.SystemSessionKey, sessionA.Key) cliDatabase.MustExec(t, "inserting session_key_expiry", ctx.DB, "INSERT INTO system (key, value) VALUES (?, ?)", consts.SystemSessionKeyExpiry, sessionA.ExpiresAt.Unix()) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync", "--apiEndpoint", apiEndpointA) // Verify sync to Server A succeeded checkStateWithDB(t, ctx.DB, userA, serverDbA, systemState{ clientNoteCount: 2, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 2, serverUserMaxUSN: 4, }) // Step 2: Switch to Server B (empty) and sync apiEndpointB := fmt.Sprintf("%s/api", serverB.URL) // Set up user on Server B userB := apitest.SetupUserData(serverDbB) apitest.SetupAccountData(serverDbB, userB, "alice@example.com", "pass1234") sessionB := apitest.SetupSession(serverDbB, userB) cliDatabase.MustExec(t, "updating session_key for B", ctx.DB, "UPDATE system SET value = ? WHERE key = ?", sessionB.Key, consts.SystemSessionKey) cliDatabase.MustExec(t, "updating session_key_expiry for B", ctx.DB, "UPDATE system SET value = ? WHERE key = ?", sessionB.ExpiresAt.Unix(), consts.SystemSessionKeyExpiry) // Should detect empty server and prompt clitest.MustWaitDnoteCmd(t, dnoteCmdOpts, clitest.UserConfirmEmptyServerSync, cliBinaryName, "sync", "--apiEndpoint", apiEndpointB) // Verify Server B now has data checkStateWithDB(t, ctx.DB, userB, serverDbB, systemState{ clientNoteCount: 2, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 2, serverUserMaxUSN: 4, }) // Step 3: Switch back to Server A and sync cliDatabase.MustExec(t, "updating session_key back to A", ctx.DB, "UPDATE system SET value = ? WHERE key = ?", sessionA.Key, consts.SystemSessionKey) cliDatabase.MustExec(t, "updating session_key_expiry back to A", ctx.DB, "UPDATE system SET value = ? WHERE key = ?", sessionA.ExpiresAt.Unix(), consts.SystemSessionKeyExpiry) // Should NOT trigger empty server detection (Server A has MaxUSN > 0) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync", "--apiEndpoint", apiEndpointA) // Verify Server A still has its data checkStateWithDB(t, ctx.DB, userA, serverDbA, systemState{ clientNoteCount: 2, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 2, serverUserMaxUSN: 4, }) // Step 4: Switch back to Server B and sync again cliDatabase.MustExec(t, "updating session_key back to B", ctx.DB, "UPDATE system SET value = ? WHERE key = ?", sessionB.Key, consts.SystemSessionKey) cliDatabase.MustExec(t, "updating session_key_expiry back to B", ctx.DB, "UPDATE system SET value = ? WHERE key = ?", sessionB.ExpiresAt.Unix(), consts.SystemSessionKeyExpiry) // Should NOT trigger empty server detection (Server B now has MaxUSN > 0 from Step 2) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync", "--apiEndpoint", apiEndpointB) // Verify both servers maintain independent state checkStateWithDB(t, ctx.DB, userB, serverDbB, systemState{ clientNoteCount: 2, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 2, serverUserMaxUSN: 4, }) }) } func TestSync_FreshClientConcurrent(t *testing.T) { // Test the core issue: Fresh client (never synced, lastMaxUSN=0) syncing to a server // that already has data uploaded by another client. // // Scenario: // 1. Client A creates local notes (never synced, lastMaxUSN=0, lastSyncAt=0) // 2. Client B uploads same book names to server first // 3. Client A syncs // // Expected: Client A should pull server data first, detect duplicate book names, // rename local books to avoid conflicts (js→js_2), then upload successfully. // Clean up apitest.ClearData(serverDb) defer apitest.ClearData(serverDb) clearTmp(t) ctx := context.InitTestCtx(t, paths, nil) defer context.TeardownTestCtx(t, ctx) user := setupUserAndLogin(t, ctx.DB) // Client A: Create local data (never sync) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "js", "-c", "js1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "add", "css", "-c", "css1") // Client B: Upload same book names to server via API jsBookUUID := apiCreateBook(t, user, "js", "client B creating js book") cssBookUUID := apiCreateBook(t, user, "css", "client B creating css book") apiCreateNote(t, user, jsBookUUID, "js2", "client B note") apiCreateNote(t, user, cssBookUUID, "css2", "client B note") // Client A syncs - should handle the conflict gracefully // Expected: pulls server data, renames local books to js_2/css_2, uploads successfully clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "sync") // Verify: Should have 4 books and 4 notes on both client and server // USN breakdown: 2 books + 2 notes from Client B (USN 1-4), then 2 books + 2 notes from Client A (USN 5-8) checkStateWithDB(t, ctx.DB, user, serverDb, systemState{ clientNoteCount: 4, clientBookCount: 4, clientLastMaxUSN: 8, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 4, serverBookCount: 4, serverUserMaxUSN: 8, }) // Verify server has all 4 books with correct names var svrBookJS, svrBookCSS, svrBookJS2, svrBookCSS2 database.Book apitest.MustExec(t, serverDb.Where("label = ?", "js").First(&svrBookJS), "finding server book 'js'") apitest.MustExec(t, serverDb.Where("label = ?", "css").First(&svrBookCSS), "finding server book 'css'") apitest.MustExec(t, serverDb.Where("label = ?", "js_2").First(&svrBookJS2), "finding server book 'js_2'") apitest.MustExec(t, serverDb.Where("label = ?", "css_2").First(&svrBookCSS2), "finding server book 'css_2'") assert.Equal(t, svrBookJS.Label, "js", "server should have book 'js' (Client B)") assert.Equal(t, svrBookCSS.Label, "css", "server should have book 'css' (Client B)") assert.Equal(t, svrBookJS2.Label, "js_2", "server should have book 'js_2' (Client A renamed)") assert.Equal(t, svrBookCSS2.Label, "css_2", "server should have book 'css_2' (Client A renamed)") // Verify server has all 4 notes with correct content var svrNoteJS1, svrNoteJS2, svrNoteCSS1, svrNoteCSS2 database.Note apitest.MustExec(t, serverDb.Where("body = ?", "js1").First(&svrNoteJS1), "finding server note 'js1'") apitest.MustExec(t, serverDb.Where("body = ?", "js2").First(&svrNoteJS2), "finding server note 'js2'") apitest.MustExec(t, serverDb.Where("body = ?", "css1").First(&svrNoteCSS1), "finding server note 'css1'") apitest.MustExec(t, serverDb.Where("body = ?", "css2").First(&svrNoteCSS2), "finding server note 'css2'") assert.Equal(t, svrNoteJS1.BookUUID, svrBookJS2.UUID, "note 'js1' should belong to book 'js_2' (Client A)") assert.Equal(t, svrNoteJS2.BookUUID, svrBookJS.UUID, "note 'js2' should belong to book 'js' (Client B)") assert.Equal(t, svrNoteCSS1.BookUUID, svrBookCSS2.UUID, "note 'css1' should belong to book 'css_2' (Client A)") assert.Equal(t, svrNoteCSS2.BookUUID, svrBookCSS.UUID, "note 'css2' should belong to book 'css' (Client B)") // Verify client has all 4 books var cliBookJS, cliBookCSS, cliBookJS2, cliBookCSS2 cliDatabase.Book cliDatabase.MustScan(t, "finding client book 'js'", ctx.DB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js"), &cliBookJS.UUID, &cliBookJS.Label, &cliBookJS.USN) cliDatabase.MustScan(t, "finding client book 'css'", ctx.DB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css"), &cliBookCSS.UUID, &cliBookCSS.Label, &cliBookCSS.USN) cliDatabase.MustScan(t, "finding client book 'js_2'", ctx.DB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "js_2"), &cliBookJS2.UUID, &cliBookJS2.Label, &cliBookJS2.USN) cliDatabase.MustScan(t, "finding client book 'css_2'", ctx.DB.QueryRow("SELECT uuid, label, usn FROM books WHERE label = ?", "css_2"), &cliBookCSS2.UUID, &cliBookCSS2.Label, &cliBookCSS2.USN) // Verify client UUIDs match server assert.Equal(t, cliBookJS.UUID, svrBookJS.UUID, "client 'js' UUID should match server") assert.Equal(t, cliBookCSS.UUID, svrBookCSS.UUID, "client 'css' UUID should match server") assert.Equal(t, cliBookJS2.UUID, svrBookJS2.UUID, "client 'js_2' UUID should match server") assert.Equal(t, cliBookCSS2.UUID, svrBookCSS2.UUID, "client 'css_2' UUID should match server") // Verify all books have non-zero USN (synced successfully) assert.NotEqual(t, cliBookJS.USN, 0, "client 'js' should have non-zero USN") assert.NotEqual(t, cliBookCSS.USN, 0, "client 'css' should have non-zero USN") assert.NotEqual(t, cliBookJS2.USN, 0, "client 'js_2' should have non-zero USN") assert.NotEqual(t, cliBookCSS2.USN, 0, "client 'css_2' should have non-zero USN") // Verify client has all 4 notes var cliNoteJS1, cliNoteJS2, cliNoteCSS1, cliNoteCSS2 cliDatabase.Note cliDatabase.MustScan(t, "finding client note 'js1'", ctx.DB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js1"), &cliNoteJS1.UUID, &cliNoteJS1.Body, &cliNoteJS1.USN) cliDatabase.MustScan(t, "finding client note 'js2'", ctx.DB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "js2"), &cliNoteJS2.UUID, &cliNoteJS2.Body, &cliNoteJS2.USN) cliDatabase.MustScan(t, "finding client note 'css1'", ctx.DB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "css1"), &cliNoteCSS1.UUID, &cliNoteCSS1.Body, &cliNoteCSS1.USN) cliDatabase.MustScan(t, "finding client note 'css2'", ctx.DB.QueryRow("SELECT uuid, body, usn FROM notes WHERE body = ?", "css2"), &cliNoteCSS2.UUID, &cliNoteCSS2.Body, &cliNoteCSS2.USN) // Verify client note UUIDs match server assert.Equal(t, cliNoteJS1.UUID, svrNoteJS1.UUID, "client note 'js1' UUID should match server") assert.Equal(t, cliNoteJS2.UUID, svrNoteJS2.UUID, "client note 'js2' UUID should match server") assert.Equal(t, cliNoteCSS1.UUID, svrNoteCSS1.UUID, "client note 'css1' UUID should match server") assert.Equal(t, cliNoteCSS2.UUID, svrNoteCSS2.UUID, "client note 'css2' UUID should match server") // Verify all notes have non-zero USN (synced successfully) assert.NotEqual(t, cliNoteJS1.USN, 0, "client note 'js1' should have non-zero USN") assert.NotEqual(t, cliNoteJS2.USN, 0, "client note 'js2' should have non-zero USN") assert.NotEqual(t, cliNoteCSS1.USN, 0, "client note 'css1' should have non-zero USN") assert.NotEqual(t, cliNoteCSS2.USN, 0, "client note 'css2' should have non-zero USN") } // TestSync_ConvergeSameBookNames tests that two clients don't enter an infinite sync loop if they // try to sync books with the same names. Books shouldn't get marked dirty when re-downloaded from server. func TestSync_ConvergeSameBookNames(t *testing.T) { // Clean up and prepare server apitest.ClearData(serverDb) defer apitest.ClearData(serverDb) clearTmp(t) ctx := context.InitTestCtx(t, paths, nil) defer context.TeardownTestCtx(t, ctx) // Setup two separate client databases client1DB := fmt.Sprintf("%s/client1.db", tmpDirPath) client2DB := fmt.Sprintf("%s/client2.db", tmpDirPath) defer os.Remove(client1DB) defer os.Remove(client2DB) // Set up sessions user := setupUser(t, ctx.DB) db1 := testutils.MustOpenDatabase(t, client1DB) db2 := testutils.MustOpenDatabase(t, client2DB) defer db1.Close() defer db2.Close() // Client 1: First sync to empty server clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "--dbPath", client1DB, "add", "testbook", "-c", "client1 note1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "--dbPath", client1DB, "add", "anotherbook", "-c", "client1 note2") login(t, db1, user) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "--dbPath", client1DB, "sync") checkStateWithDB(t, db1, user, serverDb, systemState{ clientNoteCount: 2, clientBookCount: 2, clientLastMaxUSN: 4, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 2, serverBookCount: 2, serverUserMaxUSN: 4, }) // Client 2: Sync (downloads client 1's data, adds own notes) ===== clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "--dbPath", client2DB, "add", "testbook", "-c", "client2 note1") clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "--dbPath", client2DB, "add", "anotherbook", "-c", "client2 note2") login(t, db2, user) clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "--dbPath", client2DB, "sync") // Verify state after client2 sync checkStateWithDB(t, db2, user, serverDb, systemState{ clientNoteCount: 4, clientBookCount: 2, clientLastMaxUSN: 8, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 4, serverBookCount: 2, serverUserMaxUSN: 8, }) // Client 1: Sync again. It downloads client2's changes (2 extra notes). clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "--dbPath", client1DB, "sync") // Verify MaxUSN did not increase (client1 should only download, not upload) // Client1 still has: 2 original books + 4 notes (2 own + 2 from client2) checkStateWithDB(t, db1, user, serverDb, systemState{ clientNoteCount: 4, clientBookCount: 2, clientLastMaxUSN: 8, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 4, serverBookCount: 2, serverUserMaxUSN: 8, }) // Verify no infinite loop: alternate syncing // Both clients should be able to sync without any changes (MaxUSN stays at 8) for range 3 { // Client 2 syncs clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "--dbPath", client2DB, "sync") // Verify client2 state unchanged checkStateWithDB(t, db2, user, serverDb, systemState{ clientNoteCount: 4, clientBookCount: 2, clientLastMaxUSN: 8, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 4, serverBookCount: 2, serverUserMaxUSN: 8, }) // Client 1 syncs clitest.RunDnoteCmd(t, dnoteCmdOpts, cliBinaryName, "--dbPath", client1DB, "sync") // Verify client1 state unchanged checkStateWithDB(t, db1, user, serverDb, systemState{ clientNoteCount: 4, clientBookCount: 2, clientLastMaxUSN: 8, clientLastSyncAt: serverTime.Unix(), serverNoteCount: 4, serverBookCount: 2, serverUserMaxUSN: 8, }) } }