mirror of
https://github.com/zotero/zotero.git
synced 2026-09-05 08:06:16 +00:00
resetData() clears user data and sync state using application APIs instead of doing a full Zotero shutdown/reinit cycle, making it much faster for tests that just need clean state between runs. Also make Tags.init(), Creators.init(), Users.init(), and SyncedSettings.loadAll() properly clear their caches before reloading, so they are safe to call more than once.
2838 lines
76 KiB
JavaScript
2838 lines
76 KiB
JavaScript
"use strict";
|
|
|
|
describe("Zotero.Sync.Data.Local", function () {
|
|
describe("#getAPIKey()/#setAPIKey()", function () {
|
|
it("should get and set an API key", async function () {
|
|
var apiKey1 = Zotero.Utilities.randomString(24);
|
|
var apiKey2 = Zotero.Utilities.randomString(24);
|
|
await Zotero.Sync.Data.Local.setAPIKey(apiKey1);
|
|
assert.equal(await Zotero.Sync.Data.Local.getAPIKey(apiKey1), apiKey1);
|
|
await Zotero.Sync.Data.Local.setAPIKey(apiKey2);
|
|
assert.equal(await Zotero.Sync.Data.Local.getAPIKey(apiKey2), apiKey2);
|
|
})
|
|
|
|
|
|
it("should clear an API key by setting an empty string", async function () {
|
|
var apiKey = Zotero.Utilities.randomString(24);
|
|
await Zotero.Sync.Data.Local.setAPIKey(apiKey);
|
|
await Zotero.Sync.Data.Local.setAPIKey("");
|
|
assert.strictEqual(await Zotero.Sync.Data.Local.getAPIKey(apiKey), "");
|
|
})
|
|
})
|
|
|
|
|
|
describe("#checkUser()", function () {
|
|
var resetDataDirFile;
|
|
|
|
before(function () {
|
|
resetDataDirFile = OS.Path.join(Zotero.DataDirectory.dir, 'reset-data-directory');
|
|
sinon.stub(Zotero.Utilities.Internal, 'quitZotero');
|
|
});
|
|
|
|
beforeEach(function* () {
|
|
yield OS.File.remove(resetDataDirFile, {ignoreAbsent: true});
|
|
Zotero.Utilities.Internal.quitZotero.reset();
|
|
});
|
|
|
|
after(function () {
|
|
Zotero.Utilities.Internal.quitZotero.restore();
|
|
});
|
|
|
|
it("should prompt for data reset and create a temp 'reset-data-directory' file on accept", async function () {
|
|
await Zotero.Users.setCurrentUserID(1);
|
|
await Zotero.Users.setCurrentUsername("A");
|
|
|
|
var handled = false;
|
|
waitForDialog(function (window) {
|
|
var text = window.document.documentElement.textContent;
|
|
var matches = text.match(/“[^”]*”/g);
|
|
assert.equal(matches.length, 5);
|
|
assert.equal(matches[0], "“A”");
|
|
assert.equal(matches[1], "“B”");
|
|
assert.equal(matches[2], "“A”");
|
|
assert.equal(matches[3], "“A”");
|
|
// Checkbox
|
|
assert.equal(matches[4], "“A”");
|
|
|
|
window.document.getElementById('zotero-hardConfirmationDialog-checkbox').checked = true;
|
|
window.document.getElementById('zotero-hardConfirmationDialog-checkbox')
|
|
.dispatchEvent(new Event('command'));
|
|
|
|
handled = true;
|
|
}, 'accept', 'chrome://zotero/content/hardConfirmationDialog.xhtml');
|
|
var cont = await Zotero.Sync.Data.Local.checkUser(window, 2, "B");
|
|
var resetDataDirFileExists = await OS.File.exists(resetDataDirFile);
|
|
assert.isTrue(handled);
|
|
assert.isTrue(cont);
|
|
assert.isTrue(resetDataDirFileExists);
|
|
});
|
|
|
|
it("should prompt for data reset and cancel", async function () {
|
|
await Zotero.Users.setCurrentUserID(1);
|
|
await Zotero.Users.setCurrentUsername("A");
|
|
|
|
waitForDialog(false, 'cancel', 'chrome://zotero/content/hardConfirmationDialog.xhtml');
|
|
var cont = await Zotero.Sync.Data.Local.checkUser(window, 2, "B");
|
|
var resetDataDirFileExists = await OS.File.exists(resetDataDirFile);
|
|
assert.isFalse(cont);
|
|
assert.isFalse(resetDataDirFileExists);
|
|
|
|
assert.equal(Zotero.Users.getCurrentUserID(), 1);
|
|
assert.equal(Zotero.Users.getCurrentUsername(), "A");
|
|
});
|
|
|
|
// extra1 functionality not used at the moment
|
|
it.skip("should prompt for data reset and allow to choose a new data directory", function* (){
|
|
sinon.stub(Zotero.DataDirectory, 'forceChange').returns(Promise.resolve(true));
|
|
yield Zotero.Users.setCurrentUserID(1);
|
|
yield Zotero.Users.setCurrentUsername("A");
|
|
|
|
waitForDialog(null, 'extra1', 'chrome://zotero/content/hardConfirmationDialog.xhtml');
|
|
waitForDialog();
|
|
var cont = yield Zotero.Sync.Data.Local.checkUser(window, 2, "B");
|
|
var resetDataDirFileExists = yield OS.File.exists(resetDataDirFile);
|
|
assert.isTrue(cont);
|
|
assert.isTrue(Zotero.DataDirectory.forceChange.called);
|
|
assert.isFalse(resetDataDirFileExists);
|
|
|
|
Zotero.DataDirectory.forceChange.restore();
|
|
});
|
|
|
|
it("should migrate relations using local user key", async function () {
|
|
await Zotero.DB.queryAsync("DELETE FROM settings WHERE setting='account'");
|
|
await Zotero.Users.init();
|
|
|
|
var item1 = await createDataObject('item');
|
|
var item2 = createUnsavedDataObject('item');
|
|
item2.addRelatedItem(item1);
|
|
await item2.save();
|
|
|
|
var pred = Zotero.Relations.relatedItemPredicate;
|
|
assert.isTrue(
|
|
item2.toJSON().relations[pred][0].startsWith('http://zotero.org/users/local/')
|
|
);
|
|
|
|
waitForDialog(false, 'accept', 'chrome://zotero/content/hardConfirmationDialog.xhtml');
|
|
await Zotero.Sync.Data.Local.checkUser(window, 1, "A");
|
|
|
|
assert.isTrue(
|
|
item2.toJSON().relations[pred][0].startsWith('http://zotero.org/users/1/items/')
|
|
);
|
|
});
|
|
});
|
|
|
|
|
|
describe("#checkLibraryForAccess()", function () {
|
|
//
|
|
// editable
|
|
//
|
|
it("should prompt if library is changing from editable to non-editable and reset library on accept", async function () {
|
|
var group = await createGroup();
|
|
var libraryID = group.libraryID;
|
|
var promise = waitForDialog(function (dialog) {
|
|
var text = dialog.document.documentElement.textContent;
|
|
assert.include(text, group.name);
|
|
});
|
|
|
|
var mock = sinon.mock(Zotero.Sync.Data.Local);
|
|
mock.expects("_libraryHasUnsyncedData").once().returns(Promise.resolve(true));
|
|
mock.expects("resetUnsyncedLibraryData").once().returns(Promise.resolve());
|
|
mock.expects("resetUnsyncedLibraryFiles").never();
|
|
|
|
assert.isTrue(
|
|
await Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, false, false)
|
|
);
|
|
await promise;
|
|
|
|
mock.verify();
|
|
});
|
|
|
|
it("should prompt if library is changing from editable to non-editable but not reset library on cancel", async function () {
|
|
var group = await createGroup();
|
|
var libraryID = group.libraryID;
|
|
var promise = waitForDialog(function (dialog) {
|
|
var text = dialog.document.documentElement.textContent;
|
|
assert.include(text, group.name);
|
|
}, "cancel");
|
|
|
|
var mock = sinon.mock(Zotero.Sync.Data.Local);
|
|
mock.expects("_libraryHasUnsyncedData").once().returns(Promise.resolve(true));
|
|
mock.expects("resetUnsyncedLibraryData").never();
|
|
mock.expects("resetUnsyncedLibraryFiles").never();
|
|
|
|
assert.isFalse(
|
|
await Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, false, false)
|
|
);
|
|
await promise;
|
|
|
|
mock.verify();
|
|
});
|
|
|
|
it("should not prompt if library is changing from editable to non-editable", async function () {
|
|
var group = await createGroup({ editable: false, filesEditable: false });
|
|
var libraryID = group.libraryID;
|
|
await Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, true, true);
|
|
});
|
|
|
|
//
|
|
// filesEditable
|
|
//
|
|
it("should prompt if library is changing from filesEditable to non-filesEditable and reset library files on accept", async function () {
|
|
var group = await createGroup();
|
|
var libraryID = group.libraryID;
|
|
var promise = waitForDialog(function (dialog) {
|
|
var text = dialog.document.documentElement.textContent;
|
|
assert.include(text, group.name);
|
|
});
|
|
|
|
var mock = sinon.mock(Zotero.Sync.Data.Local);
|
|
mock.expects("_libraryHasUnsyncedFiles").once().returns(Promise.resolve(true));
|
|
mock.expects("resetUnsyncedLibraryData").never();
|
|
mock.expects("resetUnsyncedLibraryFiles").once().returns(Promise.resolve());
|
|
|
|
assert.isTrue(
|
|
await Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, true, false)
|
|
);
|
|
await promise;
|
|
|
|
mock.verify();
|
|
});
|
|
|
|
it("should prompt if library is changing from filesEditable to non-filesEditable but not reset library files on cancel", async function () {
|
|
var group = await createGroup();
|
|
var libraryID = group.libraryID;
|
|
var promise = waitForDialog(function (dialog) {
|
|
var text = dialog.document.documentElement.textContent;
|
|
assert.include(text, group.name);
|
|
}, "cancel");
|
|
|
|
var mock = sinon.mock(Zotero.Sync.Data.Local);
|
|
mock.expects("_libraryHasUnsyncedFiles").once().returns(Promise.resolve(true));
|
|
mock.expects("resetUnsyncedLibraryData").never();
|
|
mock.expects("resetUnsyncedLibraryFiles").never();
|
|
|
|
assert.isFalse(
|
|
await Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, true, false)
|
|
);
|
|
await promise;
|
|
|
|
mock.verify();
|
|
});
|
|
});
|
|
|
|
|
|
describe("#_libraryHasUnsyncedData()", function () {
|
|
it("should return true for unsynced setting", async function () {
|
|
var group = await createGroup();
|
|
var libraryID = group.libraryID;
|
|
await Zotero.SyncedSettings.set(libraryID, "testSetting", { foo: "bar" });
|
|
assert.isTrue(await Zotero.Sync.Data.Local._libraryHasUnsyncedData(libraryID));
|
|
});
|
|
|
|
it("should return true for unsynced item", async function () {
|
|
var group = await createGroup();
|
|
var libraryID = group.libraryID;
|
|
await createDataObject('item', { libraryID });
|
|
assert.isTrue(await Zotero.Sync.Data.Local._libraryHasUnsyncedData(libraryID));
|
|
});
|
|
|
|
it("should return false if no changes", async function () {
|
|
var group = await createGroup();
|
|
var libraryID = group.libraryID;
|
|
assert.isFalse(await Zotero.Sync.Data.Local._libraryHasUnsyncedData(libraryID));
|
|
});
|
|
});
|
|
|
|
|
|
describe("#resetUnsyncedLibraryData()", function () {
|
|
it("should revert group and mark for full sync", async function () {
|
|
var group = await createGroup({
|
|
version: 1,
|
|
libraryVersion: 2
|
|
});
|
|
var libraryID = group.libraryID;
|
|
|
|
// New setting
|
|
await Zotero.SyncedSettings.set(libraryID, "testSetting", { foo: "bar" });
|
|
|
|
// Changed collection
|
|
var changedCollection = await createDataObject('collection', { libraryID, version: 1 });
|
|
var originalCollectionName = changedCollection.name;
|
|
await Zotero.Sync.Data.Local.saveCacheObject(
|
|
'collection', libraryID, changedCollection.toJSON()
|
|
);
|
|
await modifyDataObject(changedCollection);
|
|
|
|
// Unchanged item
|
|
var unchangedItem = await createDataObject('item', { libraryID, version: 1, synced: true });
|
|
await Zotero.Sync.Data.Local.saveCacheObject(
|
|
'item', libraryID, unchangedItem.toJSON()
|
|
);
|
|
|
|
// Changed item
|
|
var changedItem = await createDataObject('item', { libraryID, version: 1 });
|
|
var originalChangedItemTitle = changedItem.getField('title');
|
|
await Zotero.Sync.Data.Local.saveCacheObject('item', libraryID, changedItem.toJSON());
|
|
await modifyDataObject(changedItem);
|
|
|
|
// New item
|
|
var newItem = await createDataObject('item', { libraryID, version: 1 });
|
|
var newItemKey = newItem.key;
|
|
|
|
// Delete item
|
|
var deletedItem = await createDataObject('item', { libraryID });
|
|
var deletedItemKey = deletedItem.key;
|
|
await deletedItem.eraseTx();
|
|
|
|
// Make group read-only
|
|
group.editable = false;
|
|
await group.saveTx();
|
|
|
|
await Zotero.Sync.Data.Local.resetUnsyncedLibraryData(libraryID);
|
|
|
|
assert.isNull(Zotero.SyncedSettings.get(group.libraryID, "testSetting"));
|
|
|
|
assert.equal(changedCollection.name, originalCollectionName);
|
|
assert.isTrue(changedCollection.synced);
|
|
|
|
assert.isTrue(unchangedItem.synced);
|
|
|
|
assert.equal(changedItem.getField('title'), originalChangedItemTitle);
|
|
assert.isTrue(changedItem.synced);
|
|
|
|
assert.isFalse(Zotero.Items.get(newItemKey));
|
|
|
|
assert.isFalse(await Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, deletedItemKey));
|
|
|
|
assert.equal(group.libraryVersion, -1);
|
|
});
|
|
|
|
|
|
it("should revert modified file attachment item", async function () {
|
|
var group = await createGroup({
|
|
version: 1,
|
|
libraryVersion: 2
|
|
});
|
|
var libraryID = group.libraryID;
|
|
|
|
// File attachment that's changed but file is in sync -- reset item, keep file
|
|
var attachment = await importFileAttachment('test.png', { libraryID });
|
|
var originalTitle = attachment.getField('title');
|
|
attachment.attachmentSyncedModificationTime = await attachment.attachmentModificationTime;
|
|
attachment.attachmentSyncedHash = await attachment.attachmentHash;
|
|
attachment.attachmentSyncState = "in_sync";
|
|
attachment.synced = true;
|
|
attachment.version = 2;
|
|
await attachment.saveTx({
|
|
skipSyncedUpdate: true
|
|
});
|
|
// Save original in cache
|
|
await Zotero.Sync.Data.Local.saveCacheObject(
|
|
'item',
|
|
libraryID,
|
|
Object.assign(
|
|
attachment.toJSON(),
|
|
// TEMP: md5 and mtime aren't currently included in JSON, and without it the
|
|
// file gets marked for download when the item gets reset from the cache
|
|
{
|
|
md5: attachment.attachmentHash,
|
|
mtime: attachment.attachmentSyncedModificationTime
|
|
}
|
|
)
|
|
);
|
|
// Modify title
|
|
attachment.setField('title', "New Title");
|
|
await attachment.saveTx();
|
|
|
|
await Zotero.Sync.Data.Local.resetUnsyncedLibraryFiles(libraryID);
|
|
|
|
assert.isTrue(await attachment.fileExists());
|
|
assert.equal(attachment.getField('title'), originalTitle);
|
|
assert.equal(
|
|
attachment.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_IN_SYNC
|
|
);
|
|
});
|
|
});
|
|
|
|
|
|
describe("#resetUnsyncedLibraryFiles()", function () {
|
|
it("should delete unsynced files", async function () {
|
|
var group = await createGroup({
|
|
version: 1,
|
|
libraryVersion: 2
|
|
});
|
|
var libraryID = group.libraryID;
|
|
|
|
// File attachment that's totally in sync -- leave alone
|
|
var attachment1 = await importFileAttachment('test.png', { libraryID });
|
|
attachment1.attachmentSyncState = "in_sync";
|
|
attachment1.attachmentSyncedModificationTime = await attachment1.attachmentModificationTime;
|
|
attachment1.attachmentSyncedHash = await attachment1.attachmentHash;
|
|
attachment1.synced = true;
|
|
await attachment1.saveTx({
|
|
skipSyncedUpdate: true
|
|
});
|
|
|
|
// File attachment that's in sync with changed file -- delete file and mark for download
|
|
var attachment2 = await importFileAttachment('test.png', { libraryID });
|
|
attachment2.synced = true;
|
|
await attachment2.saveTx({
|
|
skipSyncedUpdate: true
|
|
});
|
|
|
|
// File attachment that's unsynced -- delete item and file
|
|
var attachment3 = await importFileAttachment('test.pdf', { libraryID });
|
|
|
|
// Embedded image that's in synced with changed file -- delete file and mark for download
|
|
var note = await createDataObject('item', { libraryID, itemType: 'note' });
|
|
var embeddedImage = await createEmbeddedImage(note);
|
|
embeddedImage.synced = true;
|
|
await embeddedImage.saveTx({
|
|
skipSyncedUpdate: true
|
|
});
|
|
|
|
// Has to be called before resetUnsyncedLibraryFiles()
|
|
assert.isTrue(await Zotero.Sync.Data.Local._libraryHasUnsyncedFiles(libraryID));
|
|
|
|
await Zotero.Sync.Data.Local.resetUnsyncedLibraryFiles(libraryID);
|
|
|
|
assert.isTrue(await attachment1.fileExists());
|
|
assert.isFalse(await attachment2.fileExists());
|
|
assert.isFalse(await attachment3.fileExists());
|
|
assert.isFalse(await embeddedImage.fileExists());
|
|
assert.equal(
|
|
attachment1.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_IN_SYNC
|
|
);
|
|
assert.equal(
|
|
attachment2.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD
|
|
);
|
|
assert.isFalse(Zotero.Items.get(attachment3.id));
|
|
assert.ok(Zotero.Items.get(embeddedImage.id));
|
|
});
|
|
});
|
|
|
|
|
|
describe("#getLatestCacheObjectVersions", function () {
|
|
before(function* () {
|
|
yield resetDB({
|
|
thisArg: this,
|
|
skipBundledFiles: true
|
|
});
|
|
|
|
yield Zotero.Sync.Data.Local.saveCacheObjects(
|
|
'item',
|
|
Zotero.Libraries.userLibraryID,
|
|
[
|
|
{
|
|
key: 'AAAAAAAA',
|
|
version: 2,
|
|
title: "A2"
|
|
},
|
|
{
|
|
key: 'AAAAAAAA',
|
|
version: 1,
|
|
title: "A1"
|
|
},
|
|
{
|
|
key: 'BBBBBBBB',
|
|
version: 1,
|
|
title: "B1"
|
|
},
|
|
{
|
|
key: 'BBBBBBBB',
|
|
version: 2,
|
|
title: "B2"
|
|
},
|
|
{
|
|
key: 'CCCCCCCC',
|
|
version: 3,
|
|
title: "C"
|
|
}
|
|
]
|
|
);
|
|
})
|
|
|
|
it("should return latest version of all objects if no keys passed", async function () {
|
|
var versions = await Zotero.Sync.Data.Local.getLatestCacheObjectVersions(
|
|
'item',
|
|
Zotero.Libraries.userLibraryID
|
|
);
|
|
var keys = Object.keys(versions);
|
|
assert.lengthOf(keys, 3);
|
|
assert.sameMembers(keys, ['AAAAAAAA', 'BBBBBBBB', 'CCCCCCCC']);
|
|
assert.equal(versions.AAAAAAAA, 2);
|
|
assert.equal(versions.BBBBBBBB, 2);
|
|
assert.equal(versions.CCCCCCCC, 3);
|
|
})
|
|
|
|
it("should return latest version of objects with passed keys", async function () {
|
|
var versions = await Zotero.Sync.Data.Local.getLatestCacheObjectVersions(
|
|
'item',
|
|
Zotero.Libraries.userLibraryID,
|
|
['AAAAAAAA', 'CCCCCCCC']
|
|
);
|
|
var keys = Object.keys(versions);
|
|
assert.lengthOf(keys, 2);
|
|
assert.sameMembers(keys, ['AAAAAAAA', 'CCCCCCCC']);
|
|
assert.equal(versions.AAAAAAAA, 2);
|
|
assert.equal(versions.CCCCCCCC, 3);
|
|
})
|
|
})
|
|
|
|
|
|
describe("#getUnsynced()", function () {
|
|
// See also: "shouldn't upload external annotations" in syncEngineTest.js
|
|
it("shouldn't include external annotations", async function () {
|
|
var attachment = await importFileAttachment('test.pdf');
|
|
var annotation1 = await createAnnotation('highlight', attachment);
|
|
var annotation2 = await createAnnotation('highlight', attachment, { isExternal: true });
|
|
|
|
var ids = await Zotero.Sync.Data.Local.getUnsynced('item', Zotero.Libraries.userLibraryID);
|
|
assert.include(ids, attachment.id);
|
|
assert.include(ids, annotation1.id);
|
|
});
|
|
|
|
it("should correct incorrectly nested collections", async function () {
|
|
var c1 = await createDataObject('collection');
|
|
var c2 = await createDataObject('collection');
|
|
|
|
c1.parentID = c2.id;
|
|
await c1.saveTx();
|
|
|
|
await Zotero.DB.queryAsync(
|
|
"UPDATE collections SET parentCollectionID=? WHERE collectionID=?",
|
|
[
|
|
c1.id,
|
|
c2.id
|
|
]
|
|
);
|
|
await c2.reload(['primaryData'], true);
|
|
|
|
var ids = await Zotero.Sync.Data.Local.getUnsynced('collection', Zotero.Libraries.userLibraryID);
|
|
|
|
// One of the items should still be the parent of the other, though which one is undefined
|
|
assert.isTrue(
|
|
(c1.parentID == c2.id && !c2.parentID) || (c2.parentID == c1.id && !c1.parentID)
|
|
);
|
|
});
|
|
|
|
it("should load items if library is unloaded", async function () {
|
|
var group = await createGroup();
|
|
var libraryID = group.libraryID;
|
|
var item = await createDataObject('item', { libraryID });
|
|
|
|
// Simulate an unloaded library
|
|
// TODO: Make a way to actually unload a library's data, and use it for inactive libraries!
|
|
Zotero.Items.unload(item.id);
|
|
assert.isTrue(group._dataLoaded.item);
|
|
assert.isObject(group._dataLoadedDeferreds.item);
|
|
group._dataLoaded.item = false;
|
|
delete group._dataLoadedDeferreds.item;
|
|
await Zotero.Items._loadIDsAndKeys();
|
|
|
|
var ids = await Zotero.Sync.Data.Local.getUnsynced('item', libraryID);
|
|
assert.include(ids, item.id);
|
|
});
|
|
});
|
|
|
|
|
|
describe("#processObjectsFromJSON()", function () {
|
|
var types = Zotero.DataObjectUtilities.getTypes();
|
|
|
|
beforeEach(function* () {
|
|
yield resetData();
|
|
})
|
|
|
|
it("shouldn't trigger an auto-sync", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var item = createUnsavedDataObject('item');
|
|
let data = item.toJSON();
|
|
data.key = Zotero.DataObjectUtilities.generateKey();
|
|
data.version = 10;
|
|
let json = {
|
|
key: data.key,
|
|
version: 10,
|
|
data
|
|
};
|
|
|
|
// Make sure the pref in question is still disabled by default during tests
|
|
assert.isFalse(Zotero.Prefs.get('sync.autoSync'));
|
|
Zotero.Prefs.set('sync.autoSync', true);
|
|
var stub = sinon.stub(Zotero.Sync.Runner, 'setSyncTimeout');
|
|
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
'item', libraryID, [json], { stopOnError: true }
|
|
);
|
|
|
|
// setSyncTimeout() shouldn't have been called at all
|
|
assert.isFalse(stub.called);
|
|
|
|
stub.restore();
|
|
Zotero.Prefs.set('sync.autoSync', false);
|
|
});
|
|
|
|
it("should update local version number and mark as synced if remote version is identical", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
for (let type of types) {
|
|
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
|
let obj = await createDataObject(type);
|
|
let data = obj.toJSON();
|
|
data.key = obj.key;
|
|
data.version = 10;
|
|
let json = {
|
|
key: obj.key,
|
|
version: 10,
|
|
data: data
|
|
};
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
type, libraryID, [json], { stopOnError: true }
|
|
);
|
|
let localObj = objectsClass.getByLibraryAndKey(libraryID, obj.key);
|
|
assert.equal(localObj.version, 10);
|
|
assert.isTrue(localObj.synced);
|
|
}
|
|
})
|
|
|
|
it("should keep local item changes while applying non-conflicting remote changes", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
let item = await createDataObject('item', { version: 5 });
|
|
let data = item.toJSON();
|
|
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [data]);
|
|
|
|
// Change local title
|
|
await modifyDataObject(item)
|
|
item.setTags([
|
|
{ tag: 'A' }
|
|
]);
|
|
await item.saveTx();
|
|
var changedTitle = item.getField('title');
|
|
|
|
// Create remote version without title but with changed place
|
|
data.key = item.key;
|
|
data.version = 10;
|
|
data.tags = [
|
|
{
|
|
tag: 'B'
|
|
}
|
|
]
|
|
var changedPlace = data.place = 'New York';
|
|
let json = {
|
|
key: item.key,
|
|
version: 10,
|
|
data
|
|
};
|
|
var results = await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
'item', libraryID, [json], { stopOnError: true }
|
|
);
|
|
assert.isTrue(results[0].processed);
|
|
assert.isUndefined(results[0].changes);
|
|
assert.isUndefined(results[0].conflicts);
|
|
assert.equal(item.version, 10);
|
|
assert.equal(item.getField('title'), changedTitle);
|
|
assert.equal(item.getField('place'), changedPlace);
|
|
assert.sameDeepMembers(item.getTags(), [{ tag: 'A' }, { tag: 'B' }]);
|
|
// Item should be marked as unsynced so the local changes are uploaded
|
|
assert.isFalse(item.synced);
|
|
// Sync cache should match remote
|
|
var cacheJSON = await Zotero.Sync.Data.Local.getCacheObject(
|
|
'item', libraryID, data.key, data.version
|
|
);
|
|
assert.notProperty(cacheJSON.data, 'title');
|
|
assert.equal(cacheJSON.data.place, data.place);
|
|
assert.sameDeepMembers(cacheJSON.data.tags, data.tags);
|
|
});
|
|
|
|
it("should keep local item changes while ignoring matching remote changes", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var type = 'item';
|
|
let obj = await createDataObject(type, { version: 5 });
|
|
let data = obj.toJSON();
|
|
await Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [data]);
|
|
|
|
// Change local title and place
|
|
await modifyDataObject(obj)
|
|
var changedTitle = obj.getField('title');
|
|
var changedPlace = 'New York';
|
|
obj.setField('place', changedPlace);
|
|
await obj.saveTx();
|
|
|
|
// Create remote version without title but with changed place
|
|
data.key = obj.key;
|
|
data.version = 10;
|
|
data.place = changedPlace;
|
|
let json = {
|
|
key: obj.key,
|
|
version: 10,
|
|
data: data
|
|
};
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
type, libraryID, [json], { stopOnError: true }
|
|
);
|
|
assert.equal(obj.version, 10);
|
|
assert.equal(obj.getField('title'), changedTitle);
|
|
assert.equal(obj.getField('place'), changedPlace);
|
|
// Item should be marked as unsynced so the local changes are uploaded
|
|
assert.isFalse(obj.synced);
|
|
});
|
|
|
|
it("should save item with overriding local conflict as unsynced", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var isbn = '978-0-335-22006-9';
|
|
var type = 'item';
|
|
let obj = createUnsavedDataObject(type, { version: 5 });
|
|
obj.setField('ISBN', isbn);
|
|
await obj.saveTx();
|
|
let data = obj.toJSON();
|
|
|
|
data.key = obj.key;
|
|
data.version = 10;
|
|
data.ISBN = '9780335220069';
|
|
let json = {
|
|
key: obj.key,
|
|
version: 10,
|
|
data
|
|
};
|
|
var results = await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
type, libraryID, [json], { stopOnError: true }
|
|
);
|
|
assert.isTrue(results[0].processed);
|
|
assert.isUndefined(results[0].changes);
|
|
assert.isUndefined(results[0].conflicts);
|
|
assert.equal(obj.version, 10);
|
|
assert.equal(obj.getField('ISBN'), isbn);
|
|
assert.isFalse(obj.synced);
|
|
// Sync cache should match remote
|
|
var cacheJSON = await Zotero.Sync.Data.Local.getCacheObject(type, libraryID, data.key, data.version);
|
|
assert.propertyVal(cacheJSON.data, "ISBN", data.ISBN);
|
|
});
|
|
|
|
it("should restore locally deleted collections and searches that changed remotely", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
for (let type of ['collection', 'search']) {
|
|
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
|
let obj = await createDataObject(type, { version: 1 });
|
|
let data = obj.toJSON();
|
|
|
|
await obj.eraseTx();
|
|
|
|
data.key = obj.key;
|
|
data.version = 2;
|
|
let json = {
|
|
key: obj.key,
|
|
version: 2,
|
|
data
|
|
};
|
|
let results = await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
type, libraryID, [json], { stopOnError: true }
|
|
);
|
|
assert.isTrue(results[0].processed);
|
|
assert.notOk(results[0].conflict);
|
|
assert.isTrue(results[0].restored);
|
|
assert.isUndefined(results[0].changes);
|
|
assert.isUndefined(results[0].conflicts);
|
|
obj = objectsClass.getByLibraryAndKey(libraryID, data.key);
|
|
assert.equal(obj.version, 2);
|
|
assert.isTrue(obj.synced);
|
|
assert.isFalse(await Zotero.Sync.Data.Local.getDateDeleted(type, libraryID, data.key));
|
|
}
|
|
});
|
|
|
|
it("should handle local item becoming child and remote being added to a collection", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var collection = await createDataObject('collection');
|
|
var parentItem = await createDataObject('item');
|
|
var childItem = await createDataObject('item', { itemType: 'note' });
|
|
var data = childItem.toJSON();
|
|
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [data]);
|
|
|
|
// Add to parent locally
|
|
childItem.parentID = parentItem.id;
|
|
await childItem.saveTx();
|
|
|
|
// Add to collection remotely
|
|
data.collections = [collection.key];
|
|
var json = {
|
|
key: childItem.key,
|
|
version: 10,
|
|
data
|
|
};
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
'item', libraryID, [json], { stopOnError: true }
|
|
);
|
|
|
|
assert.isTrue(collection.hasItem(parentItem));
|
|
});
|
|
|
|
it("should automatically resolve collection name conflict", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var type = 'collection';
|
|
let obj = await createDataObject(type, { version: 5 });
|
|
let data = obj.toJSON();
|
|
await Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [data]);
|
|
|
|
// Change local name
|
|
await modifyDataObject(obj);
|
|
var changedName = Zotero.Utilities.randomString();
|
|
|
|
// Create remote version with changed name
|
|
data.version = 10;
|
|
data.name = changedName;
|
|
let json = {
|
|
key: obj.key,
|
|
version: 10,
|
|
data
|
|
};
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
type, libraryID, [json], { stopOnError: true }
|
|
);
|
|
assert.equal(obj.version, 10);
|
|
assert.equal(obj.name, changedName);
|
|
assert.isTrue(obj.synced);
|
|
// Sync cache should match remote
|
|
var cacheJSON = await Zotero.Sync.Data.Local.getCacheObject(type, libraryID, data.key, data.version);
|
|
assert.propertyVal(cacheJSON.data, "name", changedName);
|
|
});
|
|
|
|
it("should remove creators that were removed remotely and change existing", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
let item = await createDataObject(
|
|
'item',
|
|
{
|
|
version: 5,
|
|
creators: [
|
|
{
|
|
name: "A",
|
|
creatorType: "author"
|
|
},
|
|
{
|
|
name: "B",
|
|
creatorType: "author"
|
|
},
|
|
{
|
|
name: "C",
|
|
creatorType: "author"
|
|
}
|
|
]
|
|
}
|
|
);
|
|
let data = item.toJSON();
|
|
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [data]);
|
|
|
|
var newCreators = [
|
|
{
|
|
name: "D",
|
|
creatorType: "author"
|
|
}
|
|
];
|
|
|
|
// Create remote version with removed creators
|
|
data.version = 10;
|
|
data.creators = newCreators;
|
|
let json = {
|
|
key: item.key,
|
|
version: 10,
|
|
data
|
|
};
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
'item', libraryID, [json], { stopOnError: true }
|
|
);
|
|
assert.equal(item.version, 10);
|
|
var creatorJSON = item.getCreatorsJSON();
|
|
assert.sameDeepMembers(creatorJSON, newCreators);
|
|
// Sync cache should match remote
|
|
var cacheJSON = await Zotero.Sync.Data.Local.getCacheObject('item', libraryID, data.key, data.version);
|
|
assert.sameDeepMembers(cacheJSON.data.creators, newCreators);
|
|
});
|
|
|
|
it("should delete older versions in sync cache after processing", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
for (let type of types) {
|
|
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
|
|
|
// Save original version
|
|
let obj = await createDataObject(type, { version: 5 });
|
|
let data = obj.toJSON();
|
|
await Zotero.Sync.Data.Local.saveCacheObjects(
|
|
type, libraryID, [data]
|
|
);
|
|
|
|
// Save newer version
|
|
data.version = 10;
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
type, libraryID, [data], { stopOnError: true }
|
|
);
|
|
|
|
let localObj = objectsClass.getByLibraryAndKey(libraryID, obj.key);
|
|
assert.equal(localObj.version, 10);
|
|
|
|
let versions = await Zotero.Sync.Data.Local.getCacheObjectVersions(
|
|
type, libraryID, obj.key
|
|
);
|
|
assert.sameMembers(
|
|
versions,
|
|
[10],
|
|
"should have only latest version of " + type + " in cache"
|
|
);
|
|
}
|
|
});
|
|
|
|
it("should delete object from sync queue after processing", async function () {
|
|
var objectType = 'item';
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
var key = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
await Zotero.Sync.Data.Local.addObjectsToSyncQueue(objectType, libraryID, [key]);
|
|
|
|
var versions = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue(objectType, libraryID);
|
|
assert.include(versions, key);
|
|
|
|
var json = {
|
|
key,
|
|
version: 10,
|
|
data: {
|
|
key,
|
|
version: 10,
|
|
itemType: "book",
|
|
title: "Test"
|
|
}
|
|
};
|
|
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
objectType, libraryID, [json], { stopOnError: true }
|
|
);
|
|
|
|
var versions = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue(objectType, libraryID);
|
|
assert.notInclude(versions, key);
|
|
});
|
|
|
|
it("should mark new attachment items and library for download", async function () {
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
Zotero.Sync.Storage.Local.setModeForLibrary(libraryID, 'zfs');
|
|
|
|
var key = Zotero.DataObjectUtilities.generateKey();
|
|
var version = 10;
|
|
var json = {
|
|
key,
|
|
version,
|
|
data: {
|
|
key,
|
|
version,
|
|
itemType: 'attachment',
|
|
linkMode: 'imported_file',
|
|
md5: '57f8a4fda823187b91e1191487b87fe6',
|
|
mtime: 1442261130615
|
|
}
|
|
};
|
|
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
'item', libraryID, [json], { stopOnError: true }
|
|
);
|
|
var item = Zotero.Items.getByLibraryAndKey(libraryID, key);
|
|
assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD);
|
|
assert.isTrue(library.storageDownloadNeeded);
|
|
})
|
|
|
|
it("should mark remotely updated attachment item for forced download", async function () {
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
Zotero.Sync.Storage.Local.setModeForLibrary(libraryID, 'zfs');
|
|
|
|
var item = await importFileAttachment('test.png');
|
|
item.version = 5;
|
|
item.synced = true;
|
|
await item.saveTx();
|
|
|
|
// Set file as synced
|
|
item.attachmentSyncedModificationTime = await item.attachmentModificationTime;
|
|
item.attachmentSyncedHash = await item.attachmentHash;
|
|
item.attachmentSyncState = "in_sync";
|
|
await item.saveTx({ skipAll: true });
|
|
|
|
// Simulate download of version with updated attachment
|
|
var json = item.toResponseJSON();
|
|
json.version = 10;
|
|
json.data.version = 10;
|
|
json.data.md5 = '57f8a4fda823187b91e1191487b87fe6';
|
|
json.data.mtime = new Date().getTime() + 10000;
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
'item', libraryID, [json], { stopOnError: true }
|
|
);
|
|
|
|
assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_DOWNLOAD);
|
|
assert.isTrue(library.storageDownloadNeeded);
|
|
})
|
|
|
|
it("should mark remotely updated attachment item with missing file for download", async function () {
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
Zotero.Sync.Storage.Local.setModeForLibrary(libraryID, 'zfs');
|
|
|
|
var item = await importFileAttachment('test.png');
|
|
item.version = 5;
|
|
item.synced = true;
|
|
await item.saveTx();
|
|
|
|
// Set file as synced
|
|
item.attachmentSyncedModificationTime = await item.attachmentModificationTime;
|
|
item.attachmentSyncedHash = await item.attachmentHash;
|
|
item.attachmentSyncState = "in_sync";
|
|
await item.saveTx({ skipAll: true });
|
|
|
|
// Delete file
|
|
await OS.File.remove(item.getFilePath());
|
|
|
|
// Simulate download of version with updated attachment
|
|
var json = item.toResponseJSON();
|
|
json.version = 10;
|
|
json.data.version = 10;
|
|
json.data.md5 = '57f8a4fda823187b91e1191487b87fe6';
|
|
json.data.mtime = new Date().getTime() + 10000;
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
'item', libraryID, [json], { stopOnError: true }
|
|
);
|
|
|
|
assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD);
|
|
assert.isTrue(library.storageDownloadNeeded);
|
|
});
|
|
|
|
it("should rename existing local file on remote filename change", async function () {
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
Zotero.Sync.Storage.Local.setModeForLibrary(libraryID, 'zfs');
|
|
|
|
var item = await importFileAttachment('test.png');
|
|
item.version = 5;
|
|
item.synced = true;
|
|
await item.saveTx();
|
|
|
|
// Set file as synced
|
|
item.attachmentSyncedModificationTime = await item.attachmentModificationTime;
|
|
item.attachmentSyncedHash = await item.attachmentHash;
|
|
item.attachmentSyncState = "in_sync";
|
|
await item.saveTx({ skipAll: true });
|
|
|
|
var newFilename = "Title.png";
|
|
|
|
// Simulate download of version with updated attachment
|
|
var json = item.toResponseJSON();
|
|
json.version = 10;
|
|
json.data.version = 10;
|
|
json.data.filename = newFilename;
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
'item', libraryID, [json], { stopOnError: true }
|
|
);
|
|
|
|
var newPath = await item.getFilePathAsync();
|
|
assert.ok(newPath);
|
|
assert.match(newPath, new RegExp("[\\/]" + newFilename + "$"));
|
|
});
|
|
|
|
it("should ignore attachment metadata when resolving metadata conflict", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
Zotero.Sync.Storage.Local.setModeForLibrary(libraryID, 'zfs');
|
|
|
|
var item = await importFileAttachment('test.png');
|
|
item.version = 5;
|
|
await item.saveTx();
|
|
var json = item.toResponseJSON();
|
|
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json]);
|
|
|
|
// Set file as synced
|
|
item.attachmentSyncedModificationTime = await item.attachmentModificationTime;
|
|
item.attachmentSyncedHash = await item.attachmentHash;
|
|
item.attachmentSyncState = "in_sync";
|
|
await item.saveTx({ skipAll: true });
|
|
|
|
// Modify title locally, leaving item unsynced
|
|
var newTitle = Zotero.Utilities.randomString();
|
|
item.setField('title', newTitle);
|
|
await item.saveTx();
|
|
|
|
// Simulate download of version with original title but updated attachment
|
|
json.version = 10;
|
|
json.data.version = 10;
|
|
json.data.md5 = '57f8a4fda823187b91e1191487b87fe6';
|
|
json.data.mtime = new Date().getTime() + 10000;
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
'item', libraryID, [json], { stopOnError: true }
|
|
);
|
|
|
|
assert.equal(item.getField('title'), newTitle);
|
|
assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_DOWNLOAD);
|
|
})
|
|
|
|
it("should roll back partial object changes on error", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
var key1 = "AAAAAAAA";
|
|
var key2 = "BBBBBBBB";
|
|
var json = [
|
|
{
|
|
key: key1,
|
|
version: 1,
|
|
data: {
|
|
key: key1,
|
|
version: 1,
|
|
itemType: "book",
|
|
title: "Test A"
|
|
}
|
|
},
|
|
{
|
|
key: key2,
|
|
version: 1,
|
|
data: {
|
|
key: key2,
|
|
version: 1,
|
|
itemType: "invalidType",
|
|
title: "Test B"
|
|
}
|
|
}
|
|
];
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, json);
|
|
|
|
// Shouldn't roll back the successful item
|
|
assert.equal(await Zotero.DB.valueQueryAsync(
|
|
"SELECT COUNT(*) FROM items WHERE libraryID=? AND key=?", [libraryID, key1]
|
|
), 1);
|
|
// Should rollback the unsuccessful item
|
|
assert.equal(await Zotero.DB.valueQueryAsync(
|
|
"SELECT COUNT(*) FROM items WHERE libraryID=? AND key=?", [libraryID, key2]
|
|
), 0);
|
|
});
|
|
|
|
it("should update createdByUser and lastModifiedBy when saving group item", async function () {
|
|
var { libraryID } = await getGroup();
|
|
let item = await createDataObject('item', { libraryID });
|
|
let data = item.toJSON();
|
|
data.key = item.key;
|
|
data.version = 10;
|
|
let json = {
|
|
key: item.key,
|
|
version: 10,
|
|
meta: {
|
|
createdByUser: {
|
|
id: 12345,
|
|
username: 'foo',
|
|
name: 'Foo Foo'
|
|
},
|
|
lastModifiedByUser: {
|
|
id: 23456,
|
|
username: 'bar',
|
|
name: 'Bar Bar'
|
|
}
|
|
},
|
|
data
|
|
};
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
'item', libraryID, [json], { stopOnError: true }
|
|
);
|
|
let localItem = Zotero.Items.getByLibraryAndKey(libraryID, item.key);
|
|
assert.isTrue(localItem.synced);
|
|
|
|
assert.equal(localItem.createdByUserID, 12345);
|
|
assert.equal(localItem.lastModifiedByUserID, 23456);
|
|
assert.equal(Zotero.Users.getName(12345), 'Foo Foo');
|
|
assert.equal(Zotero.Users.getName(23456), 'Bar Bar');
|
|
});
|
|
|
|
it("should use username if empty name for createdByUser when saving group item", async function () {
|
|
var { libraryID } = await getGroup();
|
|
let item = await createDataObject('item', { libraryID });
|
|
let data = item.toJSON();
|
|
data.key = item.key;
|
|
data.version = 10;
|
|
let json = {
|
|
key: item.key,
|
|
version: 10,
|
|
meta: {
|
|
createdByUser: {
|
|
id: 12345,
|
|
username: 'foo',
|
|
name: ''
|
|
},
|
|
},
|
|
data
|
|
};
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
|
'item', libraryID, [json], { stopOnError: true }
|
|
);
|
|
let localItem = Zotero.Items.getByLibraryAndKey(libraryID, item.key);
|
|
assert.isTrue(localItem.synced);
|
|
|
|
assert.equal(localItem.createdByUserID, 12345);
|
|
assert.equal(Zotero.Users.getName(12345), 'foo');
|
|
});
|
|
})
|
|
|
|
describe("Sync Queue", function () {
|
|
var lib1, lib2;
|
|
|
|
before(function* () {
|
|
lib1 = Zotero.Libraries.userLibraryID;
|
|
lib2 = (yield getGroup()).libraryID;
|
|
});
|
|
|
|
beforeEach(function* () {
|
|
yield Zotero.DB.queryAsync("DELETE FROM syncQueue");
|
|
});
|
|
|
|
after(function* () {
|
|
yield Zotero.DB.queryAsync("DELETE FROM syncQueue");
|
|
});
|
|
|
|
describe("#addObjectsToSyncQueue()", function () {
|
|
it("should add new objects and update lastCheck and tries for existing objects", async function () {
|
|
var objectType = 'item';
|
|
var syncObjectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType);
|
|
var now = Zotero.Date.getUnixTimestamp();
|
|
var key1 = Zotero.DataObjectUtilities.generateKey();
|
|
var key2 = Zotero.DataObjectUtilities.generateKey();
|
|
var key3 = Zotero.DataObjectUtilities.generateKey();
|
|
var key4 = Zotero.DataObjectUtilities.generateKey();
|
|
await Zotero.DB.queryAsync(
|
|
"INSERT INTO syncQueue (libraryID, key, syncObjectTypeID, lastCheck, tries) "
|
|
+ "VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)",
|
|
[
|
|
lib1, key1, syncObjectTypeID, now - 3700, 0,
|
|
lib1, key2, syncObjectTypeID, now - 7000, 1,
|
|
lib2, key3, syncObjectTypeID, now - 86400, 2
|
|
]
|
|
);
|
|
|
|
await Zotero.Sync.Data.Local.addObjectsToSyncQueue(objectType, lib1, [key1, key2]);
|
|
await Zotero.Sync.Data.Local.addObjectsToSyncQueue(objectType, lib2, [key4]);
|
|
|
|
var sql = "SELECT lastCheck, tries FROM syncQueue WHERE libraryID=? "
|
|
+ `AND syncObjectTypeID=${syncObjectTypeID} AND key=?`;
|
|
var row;
|
|
// key1
|
|
row = await Zotero.DB.rowQueryAsync(sql, [lib1, key1]);
|
|
assert.approximately(row.lastCheck, now, 1);
|
|
assert.equal(row.tries, 1);
|
|
// key2
|
|
row = await Zotero.DB.rowQueryAsync(sql, [lib1, key2]);
|
|
assert.approximately(row.lastCheck, now, 1);
|
|
assert.equal(row.tries, 2);
|
|
// key3
|
|
row = await Zotero.DB.rowQueryAsync(sql, [lib2, key3]);
|
|
assert.equal(row.lastCheck, now - 86400);
|
|
assert.equal(row.tries, 2);
|
|
// key4
|
|
row = await Zotero.DB.rowQueryAsync(sql, [lib2, key4]);
|
|
assert.approximately(row.lastCheck, now, 1);
|
|
assert.equal(row.tries, 0);
|
|
});
|
|
});
|
|
|
|
describe("#getObjectsToTryFromSyncQueue()", function () {
|
|
it("should get objects that should be retried", async function () {
|
|
var objectType = 'item';
|
|
var syncObjectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType);
|
|
var now = Zotero.Date.getUnixTimestamp();
|
|
var key1 = Zotero.DataObjectUtilities.generateKey();
|
|
var key2 = Zotero.DataObjectUtilities.generateKey();
|
|
var key3 = Zotero.DataObjectUtilities.generateKey();
|
|
var key4 = Zotero.DataObjectUtilities.generateKey();
|
|
await Zotero.DB.queryAsync(
|
|
"INSERT INTO syncQueue (libraryID, key, syncObjectTypeID, lastCheck, tries) "
|
|
+ "VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)",
|
|
[
|
|
lib1, key1, syncObjectTypeID, now - (30 * 60) - 10, 0, // more than half an hour, so should be retried
|
|
lib1, key2, syncObjectTypeID, now - (16 * 60 * 60) + 10, 4, // less than 16 hours, shouldn't be retried
|
|
lib2, key3, syncObjectTypeID, now - 86400 * 7, 20 // more than 64 hours, so should be retried
|
|
]
|
|
);
|
|
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsToTryFromSyncQueue('item', lib1);
|
|
assert.sameMembers(keys, [key1]);
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsToTryFromSyncQueue('item', lib2);
|
|
assert.sameMembers(keys, [key3]);
|
|
});
|
|
});
|
|
|
|
describe("#removeObjectsFromSyncQueue()", function () {
|
|
it("should remove objects from the sync queue", async function () {
|
|
var objectType = 'item';
|
|
var syncObjectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType);
|
|
var now = Zotero.Date.getUnixTimestamp();
|
|
var key1 = Zotero.DataObjectUtilities.generateKey();
|
|
var key2 = Zotero.DataObjectUtilities.generateKey();
|
|
var key3 = Zotero.DataObjectUtilities.generateKey();
|
|
await Zotero.DB.queryAsync(
|
|
"INSERT INTO syncQueue (libraryID, key, syncObjectTypeID, lastCheck, tries) "
|
|
+ "VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)",
|
|
[
|
|
lib1, key1, syncObjectTypeID, now, 0,
|
|
lib1, key2, syncObjectTypeID, now, 4,
|
|
lib2, key3, syncObjectTypeID, now, 20
|
|
]
|
|
);
|
|
|
|
await Zotero.Sync.Data.Local.removeObjectsFromSyncQueue('item', lib1, [key1]);
|
|
|
|
var sql = "SELECT COUNT(*) FROM syncQueue WHERE libraryID=? "
|
|
+ `AND syncObjectTypeID=${syncObjectTypeID} AND key=?`;
|
|
assert.notOk(await Zotero.DB.valueQueryAsync(sql, [lib1, key1]));
|
|
assert.ok(await Zotero.DB.valueQueryAsync(sql, [lib1, key2]));
|
|
assert.ok(await Zotero.DB.valueQueryAsync(sql, [lib2, key3]));
|
|
})
|
|
});
|
|
|
|
describe("#resetSyncQueueTries", function () {
|
|
var spy;
|
|
|
|
after(function () {
|
|
if (spy) {
|
|
spy.restore();
|
|
}
|
|
})
|
|
|
|
it("should be run on version upgrade", async function () {
|
|
var sql = "REPLACE INTO settings (setting, key, value) VALUES ('client', 'lastVersion', ?)";
|
|
await Zotero.DB.queryAsync(sql, "5.0foo");
|
|
|
|
spy = sinon.spy(Zotero.Sync.Data.Local, "resetSyncQueueTries");
|
|
await Zotero.Schema.updateSchema();
|
|
assert.ok(spy.called);
|
|
});
|
|
});
|
|
});
|
|
|
|
|
|
describe("#showConflictResolutionWindow()", function () {
|
|
it("should show title of note parent", async function () {
|
|
var parentItem = await createDataObject('item', { title: "Parent" });
|
|
var note = new Zotero.Item('note');
|
|
note.parentKey = parentItem.key;
|
|
note.setNote("Test");
|
|
await note.saveTx();
|
|
|
|
var promise = waitForWindow('chrome://zotero/content/merge.xhtml', function (dialog) {
|
|
var doc = dialog.document;
|
|
var wizard = doc.querySelector('wizard');
|
|
var mergeGroup = wizard.getElementsByTagName('merge-group')[0];
|
|
|
|
// Show title for middle and right panes
|
|
var parentText = Zotero.getString('pane.item.parentItem') + " Parent";
|
|
assert.equal(mergeGroup.leftPane.parentRow.textContent, "");
|
|
assert.equal(mergeGroup.rightPane.parentRow.textContent, parentText);
|
|
assert.equal(mergeGroup.mergePane.parentRow.textContent, parentText);
|
|
|
|
wizard.getButton('finish').click();
|
|
});
|
|
|
|
Zotero.Sync.Data.Local.showConflictResolutionWindow([
|
|
{
|
|
libraryID: note.libraryID,
|
|
key: note.key,
|
|
processed: false,
|
|
conflict: true,
|
|
left: {
|
|
deleted: true,
|
|
dateDeleted: "2016-07-07 12:34:56"
|
|
},
|
|
right: note.toJSON()
|
|
}
|
|
]);
|
|
|
|
await promise;
|
|
});
|
|
|
|
it("should switch types by showing regular item after note", async function () {
|
|
var note = await createDataObject('item', { itemType: 'note' });
|
|
var item = await createDataObject('item');
|
|
|
|
var promise = waitForWindow('chrome://zotero/content/merge.xhtml', function (dialog) {
|
|
var doc = dialog.document;
|
|
var wizard = doc.querySelector('wizard');
|
|
var mergeGroup = wizard.getElementsByTagName('merge-group')[0];
|
|
|
|
// 1 (accept remote deletion)
|
|
assert.equal(mergeGroup.leftPane.getAttribute('selected'), 'true');
|
|
mergeGroup.rightPane.click();
|
|
wizard.getButton('next').click();
|
|
|
|
// 2 (accept remote deletion)
|
|
mergeGroup.rightPane.click();
|
|
if (Zotero.isMac) {
|
|
assert.isTrue(wizard.getButton('next').hidden);
|
|
assert.isFalse(wizard.getButton('finish').hidden);
|
|
}
|
|
else {
|
|
// TODO
|
|
}
|
|
wizard.getButton('finish').click();
|
|
});
|
|
|
|
var mergeData = Zotero.Sync.Data.Local.showConflictResolutionWindow([
|
|
{
|
|
libraryID: note.libraryID,
|
|
key: note.key,
|
|
processed: false,
|
|
conflict: true,
|
|
left: note.toJSON(),
|
|
right: {
|
|
deleted: true,
|
|
dateDeleted: "2019-09-01 00:00:00"
|
|
}
|
|
},
|
|
{
|
|
libraryID: item.libraryID,
|
|
key: item.key,
|
|
processed: false,
|
|
conflict: true,
|
|
left: item.toJSON(),
|
|
right: {
|
|
deleted: true,
|
|
dateDeleted: "2019-09-01 01:00:00"
|
|
}
|
|
}
|
|
]);
|
|
|
|
await promise;
|
|
|
|
assert.isTrue(mergeData[0].data.deleted);
|
|
assert.isTrue(mergeData[1].data.deleted);
|
|
});
|
|
});
|
|
|
|
|
|
describe("#_reconcileChanges()", function () {
|
|
describe("items", function () {
|
|
it("should ignore non-conflicting local changes and return remote changes", function () {
|
|
var cacheJSON = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
itemType: "book",
|
|
title: "Title 1",
|
|
creators: [
|
|
{
|
|
firstName: "First1",
|
|
lastName: "Last1",
|
|
creatorType: "author"
|
|
}
|
|
],
|
|
url: "http://zotero.org/",
|
|
publicationTitle: "Publisher", // Remove locally
|
|
extra: "Extra", // Removed on both
|
|
dateModified: "2015-05-14 12:34:56",
|
|
collections: [
|
|
'AAAAAAAA', // Removed locally
|
|
'DDDDDDDD', // Removed remotely,
|
|
'EEEEEEEE' // Removed from both
|
|
],
|
|
relations: {
|
|
a: 'A', // Unchanged string
|
|
c: ['C1', 'C2'], // Unchanged array
|
|
d: 'D', // String removed locally
|
|
e: ['E'], // Array removed locally
|
|
f: 'F1', // String changed locally
|
|
g: [
|
|
'G1', // Unchanged
|
|
'G2', // Removed remotely
|
|
'G3' // Removed from both
|
|
],
|
|
h: 'H', // String removed remotely
|
|
i: ['I'], // Array removed remotely
|
|
},
|
|
tags: [
|
|
{ tag: 'A' }, // Removed locally
|
|
{ tag: 'D' }, // Removed remotely
|
|
{ tag: 'E' } // Removed from both
|
|
]
|
|
};
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
itemType: "book",
|
|
title: "Title 2", // Changed locally
|
|
creators: [
|
|
{
|
|
firstName: "First1",
|
|
lastName: "Last1",
|
|
creatorType: "author"
|
|
},
|
|
// Same new creator on local and remote
|
|
{
|
|
firstName: "First2",
|
|
lastName: "Last2",
|
|
creatorType: "editor"
|
|
}
|
|
],
|
|
url: "https://www.zotero.org/", // Same change on local and remote
|
|
place: "Place", // Added locally
|
|
dateModified: "2015-05-14 14:12:34", // Changed locally and remotely, but ignored
|
|
collections: [
|
|
'BBBBBBBB', // Added locally
|
|
'DDDDDDDD',
|
|
'FFFFFFFF' // Added on both
|
|
],
|
|
relations: {
|
|
'a': 'A',
|
|
'b': 'B', // String added locally
|
|
'f': 'F2',
|
|
'g': [
|
|
'G1',
|
|
'G2',
|
|
'G6' // Added locally and remotely
|
|
],
|
|
h: 'H', // String removed remotely
|
|
i: ['I'], // Array removed remotely
|
|
|
|
},
|
|
tags: [
|
|
{ tag: 'B' },
|
|
{ tag: 'D' },
|
|
{ tag: 'F', type: 1 }, // Added on both
|
|
{ tag: 'G' }, // Added on both, but with different types
|
|
{ tag: 'H', type: 1 } // Added on both, but with different types
|
|
]
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1235,
|
|
itemType: "book",
|
|
title: "Title 1",
|
|
creators: [
|
|
{
|
|
firstName: "First1",
|
|
lastName: "Last1",
|
|
creatorType: "author"
|
|
},
|
|
// Same new creator on local and remote
|
|
{
|
|
firstName: "First2",
|
|
lastName: "Last2",
|
|
creatorType: "editor"
|
|
}
|
|
],
|
|
url: "https://www.zotero.org/",
|
|
publicationTitle: "Publisher",
|
|
date: "2015-05-15", // Added remotely
|
|
dateModified: "2015-05-14 13:45:12",
|
|
collections: [
|
|
'AAAAAAAA',
|
|
'CCCCCCCC', // Added remotely
|
|
'FFFFFFFF'
|
|
],
|
|
relations: {
|
|
'a': 'A',
|
|
'd': 'D',
|
|
'e': ['E'],
|
|
'f': 'F1',
|
|
'g': [
|
|
'G1',
|
|
'G4', // Added remotely
|
|
'G6'
|
|
],
|
|
},
|
|
tags: [
|
|
{ tag: 'A' },
|
|
{ tag: 'C' },
|
|
{ tag: 'F', type: 1 },
|
|
{ tag: 'G', type: 1 },
|
|
{ tag: 'H' }
|
|
]
|
|
};
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
'item', cacheJSON, json1, json2, ignoreFields
|
|
);
|
|
assert.sameDeepMembers(
|
|
result.changes,
|
|
[
|
|
{
|
|
field: "date",
|
|
op: "add",
|
|
value: "2015-05-15"
|
|
},
|
|
{
|
|
field: "collections",
|
|
op: "member-add",
|
|
value: "CCCCCCCC"
|
|
},
|
|
{
|
|
field: "collections",
|
|
op: "member-remove",
|
|
value: "DDDDDDDD"
|
|
},
|
|
// Relations
|
|
{
|
|
field: "relations",
|
|
op: "property-member-remove",
|
|
value: {
|
|
key: 'g',
|
|
value: 'G2'
|
|
}
|
|
},
|
|
{
|
|
field: "relations",
|
|
op: "property-member-add",
|
|
value: {
|
|
key: 'g',
|
|
value: 'G4'
|
|
}
|
|
},
|
|
{
|
|
field: "relations",
|
|
op: "property-member-remove",
|
|
value: {
|
|
key: 'h',
|
|
value: 'H'
|
|
}
|
|
},
|
|
{
|
|
field: "relations",
|
|
op: "property-member-remove",
|
|
value: {
|
|
key: 'i',
|
|
value: 'I'
|
|
}
|
|
},
|
|
// Tags
|
|
{
|
|
field: "tags",
|
|
op: "member-add",
|
|
value: {
|
|
tag: 'C'
|
|
}
|
|
},
|
|
{
|
|
field: "tags",
|
|
op: "member-remove",
|
|
value: {
|
|
tag: 'D'
|
|
}
|
|
},
|
|
{
|
|
field: "tags",
|
|
op: "member-remove",
|
|
value: {
|
|
tag: 'H',
|
|
type: 1
|
|
}
|
|
},
|
|
{
|
|
field: "tags",
|
|
op: "member-add",
|
|
value: {
|
|
tag: 'H'
|
|
}
|
|
}
|
|
]
|
|
);
|
|
assert.lengthOf(result.conflicts, 0);
|
|
})
|
|
|
|
it("should return empty arrays when no remote changes to apply", function () {
|
|
// Similar to above but without differing remote changes
|
|
var cacheJSON = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
itemType: "book",
|
|
title: "Title 1",
|
|
url: "http://zotero.org/",
|
|
publicationTitle: "Publisher", // Remove locally
|
|
extra: "Extra", // Removed on both
|
|
dateModified: "2015-05-14 12:34:56",
|
|
collections: [
|
|
'AAAAAAAA', // Removed locally
|
|
'DDDDDDDD',
|
|
'EEEEEEEE' // Removed from both
|
|
],
|
|
tags: [
|
|
{
|
|
tag: 'A' // Removed locally
|
|
},
|
|
{
|
|
tag: 'D' // Removed remotely
|
|
},
|
|
{
|
|
tag: 'E' // Removed from both
|
|
}
|
|
]
|
|
};
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
itemType: "book",
|
|
title: "Title 2", // Changed locally
|
|
url: "https://www.zotero.org/", // Same change on local and remote
|
|
place: "Place", // Added locally
|
|
dateModified: "2015-05-14 14:12:34", // Changed locally and remotely, but ignored
|
|
collections: [
|
|
'BBBBBBBB', // Added locally
|
|
'DDDDDDDD',
|
|
'FFFFFFFF' // Added on both
|
|
],
|
|
tags: [
|
|
{
|
|
tag: 'B'
|
|
},
|
|
{
|
|
tag: 'D'
|
|
},
|
|
{
|
|
tag: 'F', // Added on both
|
|
type: 1
|
|
},
|
|
{
|
|
tag: 'G' // Added on both, but with different types
|
|
}
|
|
]
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1235,
|
|
itemType: "book",
|
|
title: "Title 1",
|
|
url: "https://www.zotero.org/",
|
|
publicationTitle: "Publisher",
|
|
dateModified: "2015-05-14 13:45:12",
|
|
collections: [
|
|
'AAAAAAAA',
|
|
'DDDDDDDD',
|
|
'FFFFFFFF'
|
|
],
|
|
tags: [
|
|
{
|
|
tag: 'A'
|
|
},
|
|
{
|
|
tag: 'D'
|
|
},
|
|
{
|
|
tag: 'F',
|
|
type: 1
|
|
},
|
|
{
|
|
tag: 'G',
|
|
type: 1
|
|
}
|
|
]
|
|
};
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
'item', cacheJSON, json1, json2, ignoreFields
|
|
);
|
|
assert.lengthOf(result.changes, 0);
|
|
assert.lengthOf(result.conflicts, 0);
|
|
})
|
|
|
|
it("should return conflict when changes can't be automatically resolved", function () {
|
|
var cacheJSON = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
title: "Title 1",
|
|
dateModified: "2015-05-14 12:34:56"
|
|
};
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
title: "Title 2",
|
|
dateModified: "2015-05-14 14:12:34"
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1235,
|
|
title: "Title 3",
|
|
dateModified: "2015-05-14 13:45:12"
|
|
};
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
'item', cacheJSON, json1, json2, ignoreFields
|
|
);
|
|
assert.lengthOf(result.changes, 0);
|
|
assert.sameDeepMembers(
|
|
result.conflicts,
|
|
[
|
|
[
|
|
{
|
|
field: "title",
|
|
op: "modify",
|
|
value: "Title 2"
|
|
},
|
|
{
|
|
field: "title",
|
|
op: "modify",
|
|
value: "Title 3"
|
|
}
|
|
]
|
|
]
|
|
);
|
|
});
|
|
|
|
it("should return conflict when creator changes can't be automatically resolved", function () {
|
|
var cacheJSON = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
title: "Title",
|
|
creators: [
|
|
{
|
|
firstName: "First1",
|
|
lastName: "Last1",
|
|
creatorType: "author"
|
|
}
|
|
],
|
|
dateModified: "2015-05-14 12:34:56"
|
|
};
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
title: "Title",
|
|
creators: [
|
|
{
|
|
firstName: "First2",
|
|
lastName: "Last2",
|
|
creatorType: "author"
|
|
}
|
|
],
|
|
dateModified: "2015-05-14 14:12:34"
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1235,
|
|
title: "Title",
|
|
creators: [
|
|
{
|
|
firstName: "First3",
|
|
lastName: "Last3",
|
|
creatorType: "author"
|
|
}
|
|
],
|
|
dateModified: "2015-05-14 13:45:12"
|
|
};
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
'item', cacheJSON, json1, json2, ignoreFields
|
|
);
|
|
assert.lengthOf(result.changes, 0);
|
|
assert.lengthOf(result.conflicts, 1);
|
|
assert.propertyVal(result.conflicts[0][0], 'field', 'creators');
|
|
assert.propertyVal(result.conflicts[0][0], 'op', 'modify');
|
|
assert.lengthOf(result.conflicts[0][0].value, 1);
|
|
assert.include(
|
|
result.conflicts[0][0].value[0],
|
|
{
|
|
firstName: 'First2',
|
|
lastName: 'Last2',
|
|
creatorType: 'author'
|
|
}
|
|
);
|
|
assert.propertyVal(result.conflicts[0][1], 'field', 'creators');
|
|
assert.propertyVal(result.conflicts[0][1], 'op', 'modify');
|
|
assert.lengthOf(result.conflicts[0][1].value, 1);
|
|
assert.include(
|
|
result.conflicts[0][1].value[0],
|
|
{
|
|
firstName: 'First3',
|
|
lastName: 'Last3',
|
|
creatorType: 'author'
|
|
}
|
|
);
|
|
});
|
|
|
|
it("should automatically merge array/object members and generate conflicts for field changes in absence of cached version", function () {
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
itemType: "book",
|
|
title: "Title",
|
|
creators: [
|
|
{
|
|
name: "Center for History and New Media",
|
|
creatorType: "author"
|
|
}
|
|
],
|
|
place: "Place", // Local
|
|
dateModified: "2015-05-14 14:12:34", // Changed on both, but ignored
|
|
collections: [
|
|
'AAAAAAAA' // Local
|
|
],
|
|
relations: {
|
|
'a': 'A',
|
|
'b': 'B', // Local
|
|
'e': 'E1',
|
|
'f': [
|
|
'F1',
|
|
'F2' // Local
|
|
],
|
|
h: 'H', // String removed remotely
|
|
i: ['I'], // Array removed remotely
|
|
},
|
|
tags: [
|
|
{ tag: 'A' }, // Local
|
|
{ tag: 'C' },
|
|
{ tag: 'F', type: 1 },
|
|
{ tag: 'G' }, // Different types
|
|
{ tag: 'H', type: 1 } // Different types
|
|
]
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1235,
|
|
itemType: "book",
|
|
title: "Title",
|
|
creators: [
|
|
{
|
|
creatorType: "author", // Different property order shouldn't matter
|
|
name: "Center for History and New Media"
|
|
}
|
|
],
|
|
date: "2015-05-15", // Remote
|
|
dateModified: "2015-05-14 13:45:12",
|
|
collections: [
|
|
'BBBBBBBB' // Remote
|
|
],
|
|
relations: {
|
|
'a': 'A',
|
|
'c': 'C', // Remote
|
|
'd': ['D'], // Remote
|
|
'e': 'E2',
|
|
'f': [
|
|
'F1',
|
|
'F3' // Remote
|
|
],
|
|
},
|
|
tags: [
|
|
{ tag: 'B' }, // Remote
|
|
{ tag: 'C' },
|
|
{ tag: 'F', type: 1 },
|
|
{ tag: 'G', type: 1 }, // Different types
|
|
{ tag: 'H' } // Different types
|
|
]
|
|
};
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
'item', false, json1, json2, ignoreFields
|
|
);
|
|
Zotero.debug(result);
|
|
assert.sameDeepMembers(
|
|
result.changes,
|
|
[
|
|
// Collections
|
|
{
|
|
field: "collections",
|
|
op: "member-add",
|
|
value: "BBBBBBBB"
|
|
},
|
|
// Relations
|
|
{
|
|
field: "relations",
|
|
op: "property-member-add",
|
|
value: {
|
|
key: 'c',
|
|
value: 'C'
|
|
}
|
|
},
|
|
{
|
|
field: "relations",
|
|
op: "property-member-add",
|
|
value: {
|
|
key: 'd',
|
|
value: 'D'
|
|
}
|
|
},
|
|
{
|
|
field: "relations",
|
|
op: "property-member-add",
|
|
value: {
|
|
key: 'e',
|
|
value: 'E2'
|
|
}
|
|
},
|
|
{
|
|
field: "relations",
|
|
op: "property-member-add",
|
|
value: {
|
|
key: 'f',
|
|
value: 'F3'
|
|
}
|
|
},
|
|
// Tags
|
|
{
|
|
field: "tags",
|
|
op: "member-add",
|
|
value: {
|
|
tag: 'B'
|
|
}
|
|
},
|
|
{
|
|
field: "tags",
|
|
op: "member-add",
|
|
value: {
|
|
tag: 'G',
|
|
type: 1
|
|
}
|
|
},
|
|
{
|
|
field: "tags",
|
|
op: "member-add",
|
|
value: {
|
|
tag: 'H'
|
|
}
|
|
}
|
|
]
|
|
);
|
|
assert.sameDeepMembers(
|
|
result.conflicts,
|
|
[
|
|
[
|
|
{
|
|
field: "place",
|
|
op: "add",
|
|
value: "Place"
|
|
},
|
|
{
|
|
field: "place",
|
|
op: "delete"
|
|
}
|
|
],
|
|
[
|
|
{
|
|
field: "date",
|
|
op: "delete"
|
|
},
|
|
{
|
|
field: "date",
|
|
op: "add",
|
|
value: "2015-05-15"
|
|
}
|
|
]
|
|
]
|
|
);
|
|
})
|
|
|
|
it("should automatically use remote version for unresolvable conflicts when both sides are in trash", function () {
|
|
var cacheJSON = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
title: "Title 1",
|
|
dateModified: "2015-05-14 12:34:56"
|
|
};
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
title: "Title 2",
|
|
deleted: true,
|
|
dateModified: "2015-05-14 14:12:34"
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1235,
|
|
title: "Title 3",
|
|
deleted: true,
|
|
dateModified: "2015-05-14 13:45:12"
|
|
};
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
'item', cacheJSON, json1, json2, ignoreFields
|
|
);
|
|
assert.lengthOf(result.changes, 1);
|
|
assert.sameDeepMembers(
|
|
result.changes,
|
|
[
|
|
{
|
|
field: "title",
|
|
op: "modify",
|
|
value: "Title 3"
|
|
},
|
|
]
|
|
);
|
|
});
|
|
|
|
it("should automatically apply inPublications setting from remote", function () {
|
|
var cacheJSON = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
title: "Title 1",
|
|
dateModified: "2017-04-02 12:34:56"
|
|
};
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
title: "Title 1",
|
|
dateModified: "2017-04-02 12:34:56"
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1235,
|
|
title: "Title 1",
|
|
inPublications: true,
|
|
dateModified: "2017-04-03 12:34:56"
|
|
};
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
'item', cacheJSON, json1, json2, ignoreFields
|
|
);
|
|
assert.lengthOf(result.changes, 1);
|
|
assert.sameDeepMembers(
|
|
result.changes,
|
|
[
|
|
{
|
|
field: "inPublications",
|
|
op: "add",
|
|
value: true
|
|
}
|
|
]
|
|
);
|
|
});
|
|
})
|
|
|
|
|
|
describe("collections", function () {
|
|
it("should ignore non-conflicting local changes and return remote changes", function () {
|
|
var cacheJSON = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 1",
|
|
parentCollection: null,
|
|
relations: {
|
|
A: "A", // Removed locally
|
|
C: "C" // Removed on both
|
|
}
|
|
};
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 2", // Changed locally
|
|
parentCollection: null,
|
|
relations: {}
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 1",
|
|
parentCollection: "BBBBBBBB", // Added remotely
|
|
relations: {
|
|
A: "A",
|
|
B: "B" // Added remotely
|
|
}
|
|
};
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
'collection', cacheJSON, json1, json2
|
|
);
|
|
assert.sameDeepMembers(
|
|
result.changes,
|
|
[
|
|
{
|
|
field: "parentCollection",
|
|
op: "add",
|
|
value: "BBBBBBBB"
|
|
},
|
|
{
|
|
field: "relations",
|
|
op: "property-member-add",
|
|
value: {
|
|
key: "B",
|
|
value: "B"
|
|
}
|
|
}
|
|
]
|
|
);
|
|
assert.lengthOf(result.conflicts, 0);
|
|
})
|
|
|
|
it("should return empty arrays when no remote changes to apply", function () {
|
|
// Similar to above but without differing remote changes
|
|
var cacheJSON = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 1",
|
|
conditions: [
|
|
{
|
|
condition: "title",
|
|
operator: "contains",
|
|
value: "A"
|
|
},
|
|
{
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "Chicago"
|
|
}
|
|
]
|
|
};
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 2", // Changed locally
|
|
conditions: [
|
|
{
|
|
condition: "title",
|
|
operator: "contains",
|
|
value: "A"
|
|
},
|
|
// Added locally
|
|
{
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "New York"
|
|
},
|
|
{
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "Chicago"
|
|
}
|
|
]
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 1",
|
|
conditions: [
|
|
{
|
|
condition: "title",
|
|
operator: "contains",
|
|
value: "A"
|
|
},
|
|
{
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "Chicago"
|
|
}
|
|
]
|
|
};
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
'search', cacheJSON, json1, json2
|
|
);
|
|
assert.lengthOf(result.changes, 0);
|
|
assert.lengthOf(result.conflicts, 0);
|
|
})
|
|
|
|
it("should automatically resolve conflicts with remote version", function () {
|
|
var cacheJSON = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 1"
|
|
};
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 2"
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 3"
|
|
};
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
'search', cacheJSON, json1, json2
|
|
);
|
|
assert.sameDeepMembers(
|
|
result.changes,
|
|
[
|
|
{
|
|
field: "name",
|
|
op: "modify",
|
|
value: "Name 3"
|
|
}
|
|
]
|
|
);
|
|
assert.lengthOf(result.conflicts, 0);
|
|
})
|
|
|
|
it("should automatically resolve conflicts in absence of cached version", function () {
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 1",
|
|
conditions: [
|
|
{
|
|
condition: "title",
|
|
operator: "contains",
|
|
value: "A"
|
|
},
|
|
{
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "New York"
|
|
}
|
|
]
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 2",
|
|
conditions: [
|
|
{
|
|
condition: "title",
|
|
operator: "contains",
|
|
value: "A"
|
|
},
|
|
{
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "Chicago"
|
|
}
|
|
]
|
|
};
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
'search', false, json1, json2
|
|
);
|
|
assert.sameDeepMembers(
|
|
result.changes,
|
|
[
|
|
{
|
|
field: "name",
|
|
op: "modify",
|
|
value: "Name 2"
|
|
},
|
|
{
|
|
field: "conditions",
|
|
op: "member-add",
|
|
value: {
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "Chicago"
|
|
}
|
|
}
|
|
]
|
|
);
|
|
assert.lengthOf(result.conflicts, 0);
|
|
})
|
|
})
|
|
|
|
|
|
describe("searches", function () {
|
|
it("should ignore non-conflicting local changes and return remote changes", function () {
|
|
var cacheJSON = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 1",
|
|
conditions: [
|
|
{
|
|
condition: "title",
|
|
operator: "contains",
|
|
value: "A"
|
|
},
|
|
{
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "Chicago"
|
|
}
|
|
]
|
|
};
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 2", // Changed locally
|
|
conditions: [
|
|
{
|
|
condition: "title",
|
|
operator: "contains",
|
|
value: "A"
|
|
},
|
|
// Removed remotely
|
|
{
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "Chicago"
|
|
}
|
|
]
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 1",
|
|
conditions: [
|
|
{
|
|
condition: "title",
|
|
operator: "contains",
|
|
value: "A"
|
|
},
|
|
// Added remotely
|
|
{
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "New York"
|
|
}
|
|
]
|
|
};
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
'search', cacheJSON, json1, json2
|
|
);
|
|
assert.sameDeepMembers(
|
|
result.changes,
|
|
[
|
|
{
|
|
field: "conditions",
|
|
op: "member-add",
|
|
value: {
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "New York"
|
|
}
|
|
},
|
|
{
|
|
field: "conditions",
|
|
op: "member-remove",
|
|
value: {
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "Chicago"
|
|
}
|
|
}
|
|
]
|
|
);
|
|
assert.lengthOf(result.conflicts, 0);
|
|
})
|
|
|
|
it("should return empty arrays when no remote changes to apply", function () {
|
|
// Similar to above but without differing remote changes
|
|
var cacheJSON = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 1",
|
|
conditions: [
|
|
{
|
|
condition: "title",
|
|
operator: "contains",
|
|
value: "A"
|
|
},
|
|
{
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "Chicago"
|
|
}
|
|
]
|
|
};
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 2", // Changed locally
|
|
conditions: [
|
|
{
|
|
condition: "title",
|
|
operator: "contains",
|
|
value: "A"
|
|
},
|
|
// Added locally
|
|
{
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "New York"
|
|
},
|
|
{
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "Chicago"
|
|
}
|
|
]
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 1",
|
|
conditions: [
|
|
{
|
|
condition: "title",
|
|
operator: "contains",
|
|
value: "A"
|
|
},
|
|
{
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "Chicago"
|
|
}
|
|
]
|
|
};
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
'search', cacheJSON, json1, json2
|
|
);
|
|
assert.lengthOf(result.changes, 0);
|
|
assert.lengthOf(result.conflicts, 0);
|
|
})
|
|
|
|
it("should automatically resolve conflicts with remote version", function () {
|
|
var cacheJSON = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 1"
|
|
};
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 2"
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 3"
|
|
};
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
'search', cacheJSON, json1, json2
|
|
);
|
|
assert.sameDeepMembers(
|
|
result.changes,
|
|
[
|
|
{
|
|
field: "name",
|
|
op: "modify",
|
|
value: "Name 3"
|
|
}
|
|
]
|
|
);
|
|
assert.lengthOf(result.conflicts, 0);
|
|
})
|
|
|
|
it("should automatically resolve conflicts in absence of cached version", function () {
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 1",
|
|
conditions: [
|
|
{
|
|
condition: "title",
|
|
operator: "contains",
|
|
value: "A"
|
|
},
|
|
{
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "New York"
|
|
}
|
|
]
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
name: "Name 2",
|
|
conditions: [
|
|
{
|
|
condition: "title",
|
|
operator: "contains",
|
|
value: "A"
|
|
},
|
|
{
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "Chicago"
|
|
}
|
|
]
|
|
};
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
'search', false, json1, json2
|
|
);
|
|
assert.sameDeepMembers(
|
|
result.changes,
|
|
[
|
|
{
|
|
field: "name",
|
|
op: "modify",
|
|
value: "Name 2"
|
|
},
|
|
{
|
|
field: "conditions",
|
|
op: "member-add",
|
|
value: {
|
|
condition: "place",
|
|
operator: "is",
|
|
value: "Chicago"
|
|
}
|
|
}
|
|
]
|
|
);
|
|
assert.lengthOf(result.conflicts, 0);
|
|
})
|
|
});
|
|
|
|
|
|
describe("tags", function () {
|
|
// https://forums.zotero.org/discussion/79429/syncing-error-c1-is-undefined
|
|
it("should handle multiple local type 1 and remote type 0", async function () {
|
|
var cacheJSON = {
|
|
tags: []
|
|
};
|
|
var json1 = {
|
|
tags: [
|
|
{
|
|
tag: 'C',
|
|
type: 1
|
|
},
|
|
{
|
|
tag: 'D',
|
|
type: 1
|
|
}
|
|
]
|
|
};
|
|
var json2 = {
|
|
tags: [
|
|
{
|
|
tag: 'C'
|
|
},
|
|
{
|
|
tag: 'D'
|
|
}
|
|
]
|
|
};
|
|
var result = Zotero.Sync.Data.Local._reconcileChanges(
|
|
'tag', cacheJSON, json1, json2
|
|
);
|
|
assert.lengthOf(result.changes, 4);
|
|
assert.sameDeepMembers(
|
|
result.changes,
|
|
[
|
|
{
|
|
field: "tags",
|
|
op: "member-remove",
|
|
value: {
|
|
tag: "C",
|
|
type: 1
|
|
}
|
|
},
|
|
{
|
|
field: "tags",
|
|
op: "member-add",
|
|
value: {
|
|
tag: "C"
|
|
}
|
|
},
|
|
{
|
|
field: "tags",
|
|
op: "member-remove",
|
|
value: {
|
|
tag: "D",
|
|
type: 1
|
|
}
|
|
},
|
|
{
|
|
field: "tags",
|
|
op: "member-add",
|
|
value: {
|
|
tag: "D"
|
|
}
|
|
}
|
|
]
|
|
);
|
|
});
|
|
});
|
|
})
|
|
|
|
|
|
describe("#reconcileChangesWithoutCache()", function () {
|
|
it("should return conflict for conflicting fields", function () {
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
title: "Title 1",
|
|
pages: 10,
|
|
dateModified: "2015-05-14 14:12:34"
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1235,
|
|
title: "Title 2",
|
|
place: "New York",
|
|
dateModified: "2015-05-14 13:45:12"
|
|
};
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
var result = Zotero.Sync.Data.Local._reconcileChangesWithoutCache(
|
|
'item', json1, json2, ignoreFields
|
|
);
|
|
assert.lengthOf(result.changes, 0);
|
|
assert.sameDeepMembers(
|
|
result.conflicts,
|
|
[
|
|
[
|
|
{
|
|
field: "title",
|
|
op: "add",
|
|
value: "Title 1"
|
|
},
|
|
{
|
|
field: "title",
|
|
op: "add",
|
|
value: "Title 2"
|
|
}
|
|
],
|
|
[
|
|
{
|
|
field: "pages",
|
|
op: "add",
|
|
value: 10
|
|
},
|
|
{
|
|
field: "pages",
|
|
op: "delete"
|
|
}
|
|
],
|
|
[
|
|
{
|
|
field: "place",
|
|
op: "delete"
|
|
},
|
|
{
|
|
field: "place",
|
|
op: "add",
|
|
value: "New York"
|
|
}
|
|
]
|
|
]
|
|
);
|
|
})
|
|
|
|
it("should automatically use remote version for note markup differences when text content matches", function () {
|
|
var val2 = "<p>Foo bar<br />bar foo</p>";
|
|
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 0,
|
|
itemType: "note",
|
|
note: "Foo bar<br/>bar foo",
|
|
dateModified: "2017-06-13 13:45:12"
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 5,
|
|
itemType: "note",
|
|
note: val2,
|
|
dateModified: "2017-06-13 13:45:12"
|
|
};
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
var result = Zotero.Sync.Data.Local._reconcileChangesWithoutCache(
|
|
'item', json1, json2, ignoreFields
|
|
);
|
|
assert.lengthOf(result.changes, 1);
|
|
assert.sameDeepMembers(
|
|
result.changes,
|
|
[
|
|
{
|
|
field: "note",
|
|
op: "add",
|
|
value: val2
|
|
}
|
|
]
|
|
);
|
|
assert.lengthOf(result.conflicts, 0);
|
|
});
|
|
|
|
it("should show conflict for note markup differences when text content doesn't match", function () {
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 0,
|
|
itemType: "note",
|
|
note: "Foo bar?",
|
|
dateModified: "2017-06-13 13:45:12"
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 5,
|
|
itemType: "note",
|
|
note: "<p>Foo bar!</p>",
|
|
dateModified: "2017-06-13 13:45:12"
|
|
};
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
var result = Zotero.Sync.Data.Local._reconcileChangesWithoutCache(
|
|
'item', json1, json2, ignoreFields
|
|
);
|
|
assert.lengthOf(result.changes, 0);
|
|
assert.lengthOf(result.conflicts, 1);
|
|
});
|
|
|
|
it("should automatically use remote version for conflicting fields when both sides are in trash", function () {
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
title: "Title 1",
|
|
pages: 10,
|
|
deleted: true,
|
|
dateModified: "2015-05-14 14:12:34"
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1235,
|
|
title: "Title 2",
|
|
place: "New York",
|
|
deleted: true,
|
|
dateModified: "2015-05-14 13:45:12"
|
|
};
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
var result = Zotero.Sync.Data.Local._reconcileChangesWithoutCache(
|
|
'item', json1, json2, ignoreFields
|
|
);
|
|
assert.lengthOf(result.changes, 3);
|
|
assert.sameDeepMembers(
|
|
result.changes,
|
|
[
|
|
{
|
|
field: "title",
|
|
op: "modify",
|
|
value: "Title 2"
|
|
},
|
|
{
|
|
field: "pages",
|
|
op: "delete"
|
|
},
|
|
{
|
|
field: "place",
|
|
op: "add",
|
|
value: "New York"
|
|
}
|
|
]
|
|
);
|
|
});
|
|
|
|
it("should automatically use local hyphenated ISBN value if only difference", function () {
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
itemType: "book",
|
|
ISBN: "978-0-335-22006-9"
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1235,
|
|
itemType: "book",
|
|
ISBN: "9780335220069"
|
|
};
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
var result = Zotero.Sync.Data.Local._reconcileChangesWithoutCache(
|
|
'item', json1, json2, ignoreFields
|
|
);
|
|
assert.lengthOf(result.changes, 0);
|
|
assert.lengthOf(result.conflicts, 0);
|
|
assert.isTrue(result.localChanged);
|
|
});
|
|
|
|
it("should automatically use remote hyphenated ISBN value if only difference", function () {
|
|
var json1 = {
|
|
key: "AAAAAAAA",
|
|
version: 1234,
|
|
itemType: "book",
|
|
ISBN: "9780335220069"
|
|
};
|
|
var json2 = {
|
|
key: "AAAAAAAA",
|
|
version: 1235,
|
|
itemType: "book",
|
|
ISBN: "978-0-335-22006-9"
|
|
};
|
|
var ignoreFields = ['dateAdded', 'dateModified'];
|
|
var result = Zotero.Sync.Data.Local._reconcileChangesWithoutCache(
|
|
'item', json1, json2, ignoreFields
|
|
);
|
|
assert.sameDeepMembers(
|
|
result.changes,
|
|
[
|
|
{
|
|
field: "ISBN",
|
|
op: "add",
|
|
value: "978-0-335-22006-9"
|
|
}
|
|
]
|
|
);
|
|
assert.lengthOf(result.conflicts, 0);
|
|
assert.isFalse(result.localChanged);
|
|
});
|
|
})
|
|
})
|