diff --git a/chrome/content/zotero/xpcom/sync/syncEngine.js b/chrome/content/zotero/xpcom/sync/syncEngine.js index 94ef612eb..c0f26e046 100644 --- a/chrome/content/zotero/xpcom/sync/syncEngine.js +++ b/chrome/content/zotero/xpcom/sync/syncEngine.js @@ -655,6 +655,14 @@ Zotero.Sync.Data.Engine.prototype._downloadDeletions = Zotero.Promise.coroutine( } // Conflict resolution else if (objectType == 'item') { + // If item is already in trash locally, just delete it + if (obj.deleted) { + Zotero.debug("Local item is in trash -- applying remote deletion"); + obj.eraseTx({ + skipDeleteLog: true + }); + continue; + } conflicts.push({ libraryID: this.libraryID, left: obj.toJSON(), diff --git a/chrome/content/zotero/xpcom/sync/syncLocal.js b/chrome/content/zotero/xpcom/sync/syncLocal.js index 5e369a02e..ed7663fab 100644 --- a/chrome/content/zotero/xpcom/sync/syncLocal.js +++ b/chrome/content/zotero/xpcom/sync/syncLocal.js @@ -852,6 +852,16 @@ Zotero.Sync.Data.Local = { switch (objectType) { case 'item': + if (jsonData.deleted) { + Zotero.debug("Remote item is in trash -- allowing local deletion to propagate"); + results.push({ + libraryID, + key: objectKey, + processed: true + }); + return; + } + results.push({ libraryID, key: objectKey, diff --git a/test/tests/syncEngineTest.js b/test/tests/syncEngineTest.js index bbd21189f..790a2a17e 100644 --- a/test/tests/syncEngineTest.js +++ b/test/tests/syncEngineTest.js @@ -2480,7 +2480,89 @@ describe("Zotero.Sync.Data.Engine", function () { var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID); assert.lengthOf(keys, 0); - }) + }); + + it("should handle local deletion and remote move to trash", function* () { + var libraryID = Zotero.Libraries.userLibraryID; + ({ engine, client, caller } = yield setup()); + var type = 'item'; + var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type); + var responseJSON = []; + + // Create object, generate JSON, and delete + var obj = yield createDataObject(type, { version: 10 }); + var jsonData = obj.toJSON(); + var key = jsonData.key = obj.key; + jsonData.version = 10; + let json = { + key: obj.key, + version: jsonData.version, + data: jsonData + }; + yield obj.eraseTx(); + + json.version = jsonData.version = 15; + jsonData.deleted = true; + responseJSON.push(json); + + setResponse({ + method: "GET", + url: `users/1/items?format=json&itemKey=${key}&includeTrashed=1`, + status: 200, + headers: { + "Last-Modified-Version": 15 + }, + json: responseJSON + }); + + yield engine._downloadObjects('item', [key]); + + assert.isFalse(objectsClass.exists(libraryID, key)); + + var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID); + assert.lengthOf(keys, 0); + + // Deletion should still be in sync delete log for uploading + assert.ok(yield Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, key)); + }); + + it("should handle remote move to trash and local deletion", function* () { + var libraryID = Zotero.Libraries.userLibraryID; + ({ engine, client, caller } = yield setup()); + var type = 'item'; + var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type); + var responseJSON = []; + + // Create trashed object + var obj = createUnsavedDataObject(type); + obj.deleted = true; + yield obj.saveTx(); + + setResponse({ + method: "GET", + url: `users/1/deleted?since=10`, + status: 200, + headers: { + "Last-Modified-Version": 15 + }, + json: { + collections: [], + searches: [], + items: [obj.key], + } + }); + + yield engine._downloadDeletions(10, 15); + + // Local object should have been deleted + assert.isFalse(objectsClass.exists(libraryID, obj.key)); + + var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID); + assert.lengthOf(keys, 0); + + // Deletion shouldn't be in sync delete log + assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, obj.key)); + }); it("should handle note conflict", function* () { var libraryID = Zotero.Libraries.userLibraryID;