diff --git a/CliClient/run_test.sh b/CliClient/run_test.sh index f152edba1..9267b88b6 100755 --- a/CliClient/run_test.sh +++ b/CliClient/run_test.sh @@ -5,7 +5,7 @@ rm -f "$CLIENT_DIR/tests-build/lib" mkdir -p "$CLIENT_DIR/tests-build/data" ln -s "$CLIENT_DIR/build/lib" "$CLIENT_DIR/tests-build" -npm run build && NODE_PATH="$CLIENT_DIR/tests-build/" npm test tests-build/synchronizer.js tests-build/base-model.js +npm run build && NODE_PATH="$CLIENT_DIR/tests-build/" npm test tests-build/synchronizer.js #npm run build && NODE_PATH="$CLIENT_DIR/tests-build/" npm test tests-build/synchronizer.js #npm run build && NODE_PATH="$CLIENT_DIR/tests-build/" npm test tests-build/base-model.js \ No newline at end of file diff --git a/CliClient/tests/synchronizer.js b/CliClient/tests/synchronizer.js index 9ffc036d8..5fa6aa15e 100644 --- a/CliClient/tests/synchronizer.js +++ b/CliClient/tests/synchronizer.js @@ -3,6 +3,7 @@ import { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, import { createFoldersAndNotes } from 'test-data.js'; import { Folder } from 'lib/models/folder.js'; import { Note } from 'lib/models/note.js'; +import { Tag } from 'lib/models/tag.js'; import { Setting } from 'lib/models/setting.js'; import { BaseItem } from 'lib/models/base-item.js'; import { BaseModel } from 'lib/base-model.js'; @@ -460,5 +461,44 @@ describe('Synchronizer', function() { done(); }); + + it('should sync tags', async (done) => { + let f1 = await Folder.save({ title: "folder" }); + let n1 = await Note.save({ title: "mynote" }); + let n2 = await Note.save({ title: "mynote2" }); + let tag = await Tag.save({ title: 'mytag' }); + await synchronizer().start(); + + await switchClient(2); + + await synchronizer().start(); + let remoteTag = await Tag.loadByTitle(tag.title); + expect(!!remoteTag).toBe(true); + expect(remoteTag.id).toBe(tag.id); + await Tag.addNote(remoteTag.id, n1.id); + await Tag.addNote(remoteTag.id, n2.id); + let noteIds = await Tag.tagNoteIds(tag.id); + expect(noteIds.length).toBe(2); + await synchronizer().start(); + + await switchClient(1); + + await synchronizer().start(); + let remoteNoteIds = await Tag.tagNoteIds(tag.id); + expect(remoteNoteIds.length).toBe(2); + Tag.removeNote(tag.id, n1.id); + remoteNoteIds = await Tag.tagNoteIds(tag.id); + expect(remoteNoteIds.length).toBe(1); + await synchronizer().start(); + + await switchClient(2); + + await synchronizer().start(); + noteIds = await Tag.tagNoteIds(tag.id); + expect(noteIds.length).toBe(1); + expect(remoteNoteIds[0]).toBe(noteIds[0]); + + done(); + }); }); \ No newline at end of file diff --git a/CliClient/tests/test-utils.js b/CliClient/tests/test-utils.js index 77823fccd..61b8a39b2 100644 --- a/CliClient/tests/test-utils.js +++ b/CliClient/tests/test-utils.js @@ -10,6 +10,7 @@ import { BaseItem } from 'lib/models/base-item.js'; import { Synchronizer } from 'lib/synchronizer.js'; import { FileApi } from 'lib/file-api.js'; import { FileApiDriverMemory } from 'lib/file-api-driver-memory.js'; +import { time } from 'lib/time-utils.js'; let databases_ = []; let synchronizers_ = []; @@ -29,8 +30,9 @@ function sleep(n) { }); } -function switchClient(id) { - Setting.saveAll(); +async function switchClient(id) { + await time.msleep(200); + await Setting.saveAll(); currentClient_ = id; BaseModel.db_ = databases_[id]; diff --git a/joplin.sublime-project b/joplin.sublime-project index 8b48df5d0..1b48a9fec 100755 --- a/joplin.sublime-project +++ b/joplin.sublime-project @@ -15,6 +15,7 @@ "CliClient/app/src", "CliClient/app/lib", "CliClient/tests/src", + "CliClient/tests/fuzzing", "ReactNativeClient/node_modules", "ReactNativeClient/android/app/build", "ReactNativeClient/android/build", diff --git a/lib/models/tag.js b/lib/models/tag.js index 5c83ec203..efc861b4f 100644 --- a/lib/models/tag.js +++ b/lib/models/tag.js @@ -1,6 +1,7 @@ import { BaseModel } from 'lib/base-model.js'; import { Database } from 'lib/database.js'; import { BaseItem } from 'lib/models/base-item.js'; +import { time } from 'lib/time-utils.js'; import lodash from 'lodash'; class Tag extends BaseItem { @@ -18,7 +19,6 @@ class Tag extends BaseItem { fieldNames.push('type_'); fieldNames.push(async () => { let noteIds = await this.tagNoteIds(item.id); - console.info('NOTE IDS', noteIds); return { key: 'notes_', value: noteIds.join(','), @@ -37,10 +37,6 @@ class Tag extends BaseItem { return output; } - // TODO: in order for a sync to happen, the updated_time property should somehow be changed - // whenever an tag is applied or removed from an item. Either the updated_time property - // is changed here or by the caller? - static async addNote(tagId, noteId) { let hasIt = await this.hasNote(tagId, noteId); if (hasIt) return; @@ -51,18 +47,17 @@ class Tag extends BaseItem { }); await this.db().exec(query); - //await this.save({ id: tagId, updated_time: time.unixMs() }); //type_: BaseModel.MODEL_TYPE_TAG + await this.save({ id: tagId, updated_time: time.unixMs() }); } - static async addNotes(tagId, noteIds) { - for (let i = 0; i < noteIds.length; i++) { - await this.addNote(tagId, noteIds[i]); - } + static async removeNote(tagId, noteId) { + await this.db().exec('DELETE FROM note_tags WHERE tag_id = ? AND note_id = ?', [tagId, noteId]); + await this.save({ id: tagId, updated_time: time.unixMs() }); } - // Note: updated_time must not change since this is only called from - // the synchronizer, which manages and sets the correct updated_time - static async setAssociatedNotes(tagId, noteIds) { + // Note: updated_time must not change here since this is only called from + // save(), which already handles how the updated_time property is set. + static async setAssociatedNotes_(tagId, noteIds) { let queries = [{ sql: 'DELETE FROM note_tags WHERE tag_id = ?', params: [tagId], @@ -80,8 +75,17 @@ class Tag extends BaseItem { return !!r; } - static removeNote(tagId, noteId) { - return this.db().exec('DELETE FROM note_tags WHERE tag_id = ? AND note_id = ?', [tagId, noteId]); + static async save(o, options = null) { + let result = await super.save(o, options); + + if (options && options.applyMetadataChanges === true) { + if (o.notes_) { + let noteIds = o.notes_.split(','); + await this.setAssociatedNotes_(o.id, noteIds); + } + } + + return result; } } diff --git a/lib/synchronizer.js b/lib/synchronizer.js index 319c1d0e5..369bae9f6 100644 --- a/lib/synchronizer.js +++ b/lib/synchronizer.js @@ -309,7 +309,10 @@ class Synchronizer { let newContent = Object.assign({}, content); newContent.sync_time = time.unixMs(); - let options = { autoTimestamp: false }; + let options = { + autoTimestamp: false, + applyMetadataChanges: true, + }; if (action == 'createLocal') options.isNew = true; if (newContent.type_ == BaseModel.MODEL_TYPE_RESOURCE && action == 'createLocal') { @@ -321,11 +324,6 @@ class Synchronizer { await ItemClass.save(newContent, options); - if (newContent.type_ == BaseModel.MODEL_TYPE_TAG) { - let noteIds = newContent.notes_.split(','); - await ItemClass.setAssociatedNotes(newContent.id, noteIds); - } - this.logSyncOperation(action, local, content, reason); } else { this.logSyncOperation(action, local, remote, reason);