1572 lines
38 KiB
JavaScript
1572 lines
38 KiB
JavaScript
"use strict";
|
||
|
||
describe("Zotero.Sync.Data.Local", function() {
|
||
describe("#getAPIKey()/#setAPIKey()", function () {
|
||
it("should get and set an API key", function* () {
|
||
var apiKey1 = Zotero.Utilities.randomString(24);
|
||
var apiKey2 = Zotero.Utilities.randomString(24);
|
||
Zotero.Sync.Data.Local.setAPIKey(apiKey1);
|
||
assert.equal(Zotero.Sync.Data.Local.getAPIKey(apiKey1), apiKey1);
|
||
Zotero.Sync.Data.Local.setAPIKey(apiKey2);
|
||
assert.equal(Zotero.Sync.Data.Local.getAPIKey(apiKey2), apiKey2);
|
||
})
|
||
|
||
|
||
it("should clear an API key by setting an empty string", function* () {
|
||
var apiKey = Zotero.Utilities.randomString(24);
|
||
Zotero.Sync.Data.Local.setAPIKey(apiKey);
|
||
Zotero.Sync.Data.Local.setAPIKey("");
|
||
assert.strictEqual(Zotero.Sync.Data.Local.getAPIKey(apiKey), "");
|
||
})
|
||
})
|
||
|
||
|
||
describe("#checkUser()", function () {
|
||
it("should prompt for user update and perform on accept", function* () {
|
||
yield Zotero.Users.setCurrentUserID(1);
|
||
yield Zotero.Users.setCurrentUsername("A");
|
||
|
||
var handled = false;
|
||
waitForDialog(function (dialog) {
|
||
var text = dialog.document.documentElement.textContent;
|
||
var matches = text.match(/‘[^’]*’/g);
|
||
assert.equal(matches.length, 4);
|
||
assert.equal(matches[0], "‘A’");
|
||
assert.equal(matches[1], "‘B’");
|
||
assert.equal(matches[2], "‘B’");
|
||
assert.equal(matches[3], "‘A’");
|
||
handled = true;
|
||
});
|
||
var cont = yield Zotero.Sync.Data.Local.checkUser(null, 2, "B");
|
||
assert.isTrue(handled);
|
||
assert.isTrue(cont);
|
||
|
||
assert.equal(Zotero.Users.getCurrentUserID(), 2);
|
||
assert.equal(Zotero.Users.getCurrentUsername(), "B");
|
||
})
|
||
|
||
it("should prompt for user update and cancel", function* () {
|
||
yield Zotero.Users.setCurrentUserID(1);
|
||
yield Zotero.Users.setCurrentUsername("A");
|
||
|
||
waitForDialog(false, 'cancel');
|
||
var cont = yield Zotero.Sync.Data.Local.checkUser(null, 2, "B");
|
||
assert.isFalse(cont);
|
||
|
||
assert.equal(Zotero.Users.getCurrentUserID(), 1);
|
||
assert.equal(Zotero.Users.getCurrentUsername(), "A");
|
||
})
|
||
|
||
it("should update local relations when syncing for the first time", function* () {
|
||
yield resetDB({
|
||
thisArg: this,
|
||
skipBundledFiles: true
|
||
});
|
||
|
||
var item1 = yield createDataObject('item');
|
||
var item2 = yield createDataObject(
|
||
'item', { libraryID: Zotero.Libraries.publicationsLibraryID }
|
||
);
|
||
|
||
yield item1.addLinkedItem(item2);
|
||
|
||
var cont = yield Zotero.Sync.Data.Local.checkUser(null, 1, "A");
|
||
assert.isTrue(cont);
|
||
|
||
var json = item1.toJSON();
|
||
var uri = json.relations[Zotero.Relations.linkedObjectPredicate][0];
|
||
assert.notInclude(uri, 'users/local');
|
||
assert.include(uri, 'users/1/publications');
|
||
})
|
||
});
|
||
|
||
|
||
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", function* () {
|
||
var versions = yield 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", function* () {
|
||
var versions = yield 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("#processObjectsFromJSON()", function () {
|
||
var types = Zotero.DataObjectUtilities.getTypes();
|
||
|
||
beforeEach(function* () {
|
||
yield resetDB({
|
||
thisArg: this,
|
||
skipBundledFiles: true
|
||
});
|
||
})
|
||
|
||
it("should update local version number and mark as synced if remote version is identical", function* () {
|
||
var libraryID = Zotero.Libraries.userLibraryID;
|
||
|
||
for (let type of types) {
|
||
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
||
let obj = yield createDataObject(type);
|
||
let data = obj.toJSON();
|
||
data.key = obj.key;
|
||
data.version = 10;
|
||
let json = {
|
||
key: obj.key,
|
||
version: 10,
|
||
data: data
|
||
};
|
||
yield 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", function* () {
|
||
var libraryID = Zotero.Libraries.userLibraryID;
|
||
|
||
var type = 'item';
|
||
let obj = yield createDataObject(type, { version: 5 });
|
||
let data = obj.toJSON();
|
||
yield Zotero.Sync.Data.Local.saveCacheObjects(
|
||
type, libraryID, [data]
|
||
);
|
||
|
||
// Change local title
|
||
yield modifyDataObject(obj)
|
||
var changedTitle = obj.getField('title');
|
||
|
||
// Save remote version to cache without title but with changed place
|
||
data.key = obj.key;
|
||
data.version = 10;
|
||
var changedPlace = data.place = 'New York';
|
||
let json = {
|
||
key: obj.key,
|
||
version: 10,
|
||
data: data
|
||
};
|
||
yield 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);
|
||
})
|
||
|
||
it("should delete older versions in sync cache after processing", function* () {
|
||
var libraryID = Zotero.Libraries.userLibraryID;
|
||
|
||
for (let type of types) {
|
||
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
||
|
||
// Save original version
|
||
let obj = yield createDataObject(type, { version: 5 });
|
||
let data = obj.toJSON();
|
||
yield Zotero.Sync.Data.Local.saveCacheObjects(
|
||
type, libraryID, [data]
|
||
);
|
||
|
||
// Save newer version
|
||
data.version = 10;
|
||
yield Zotero.Sync.Data.Local.processObjectsFromJSON(
|
||
type, libraryID, [data], { stopOnError: true }
|
||
);
|
||
|
||
let localObj = objectsClass.getByLibraryAndKey(libraryID, obj.key);
|
||
assert.equal(localObj.version, 10);
|
||
|
||
let versions = yield 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", function* () {
|
||
var objectType = 'item';
|
||
var libraryID = Zotero.Libraries.userLibraryID;
|
||
var key = Zotero.DataObjectUtilities.generateKey();
|
||
|
||
yield Zotero.Sync.Data.Local.addObjectsToSyncQueue(objectType, libraryID, [key]);
|
||
|
||
var versions = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue(objectType, libraryID);
|
||
assert.include(versions, key);
|
||
|
||
var json = {
|
||
key,
|
||
version: 10,
|
||
data: {
|
||
key,
|
||
version: 10,
|
||
itemType: "book",
|
||
title: "Test"
|
||
}
|
||
};
|
||
|
||
yield Zotero.Sync.Data.Local.processObjectsFromJSON(
|
||
objectType, libraryID, [json], { stopOnError: true }
|
||
);
|
||
|
||
var versions = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue(objectType, libraryID);
|
||
assert.notInclude(versions, key);
|
||
});
|
||
|
||
it("should mark new attachment items and library for download", 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
|
||
}
|
||
};
|
||
|
||
yield 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 updated attachment items for download", function* () {
|
||
var library = Zotero.Libraries.userLibrary;
|
||
var libraryID = library.id;
|
||
Zotero.Sync.Storage.Local.setModeForLibrary(libraryID, 'zfs');
|
||
|
||
var item = yield importFileAttachment('test.png');
|
||
item.version = 5;
|
||
item.synced = true;
|
||
yield item.saveTx();
|
||
|
||
// Set file as synced
|
||
item.attachmentSyncedModificationTime = yield item.attachmentModificationTime;
|
||
item.attachmentSyncedHash = yield item.attachmentHash;
|
||
item.attachmentSyncState = "in_sync";
|
||
yield 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;
|
||
yield 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 ignore attachment metadata when resolving metadata conflict", function* () {
|
||
var libraryID = Zotero.Libraries.userLibraryID;
|
||
Zotero.Sync.Storage.Local.setModeForLibrary(libraryID, 'zfs');
|
||
|
||
var item = yield importFileAttachment('test.png');
|
||
item.version = 5;
|
||
yield item.saveTx();
|
||
var json = item.toResponseJSON();
|
||
yield Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json]);
|
||
|
||
// Set file as synced
|
||
item.attachmentSyncedModificationTime = yield item.attachmentModificationTime;
|
||
item.attachmentSyncedHash = yield item.attachmentHash;
|
||
item.attachmentSyncState = "in_sync";
|
||
yield item.saveTx({ skipAll: true });
|
||
|
||
// Modify title locally, leaving item unsynced
|
||
var newTitle = Zotero.Utilities.randomString();
|
||
item.setField('title', newTitle);
|
||
yield 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;
|
||
yield 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_TO_DOWNLOAD);
|
||
})
|
||
|
||
it("should roll back partial object changes on error", function* () {
|
||
var libraryID = Zotero.Libraries.publicationsLibraryID;
|
||
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: "journalArticle",
|
||
title: "Test B",
|
||
deleted: true // Not allowed in My Publications
|
||
}
|
||
}
|
||
];
|
||
yield Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, json);
|
||
|
||
// Shouldn't roll back the successful item
|
||
yield assert.eventually.equal(Zotero.DB.valueQueryAsync(
|
||
"SELECT COUNT(*) FROM items WHERE libraryID=? AND key=?", [libraryID, key1]
|
||
), 1);
|
||
// Should rollback the unsuccessful item
|
||
yield assert.eventually.equal(Zotero.DB.valueQueryAsync(
|
||
"SELECT COUNT(*) FROM items WHERE libraryID=? AND key=?", [libraryID, key2]
|
||
), 0);
|
||
});
|
||
})
|
||
|
||
describe("Sync Queue", function () {
|
||
var lib1, lib2;
|
||
|
||
before(function* () {
|
||
lib1 = Zotero.Libraries.userLibraryID;
|
||
lib2 = Zotero.Libraries.publicationsLibraryID;
|
||
});
|
||
|
||
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", 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();
|
||
yield 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
|
||
]
|
||
);
|
||
|
||
yield Zotero.Sync.Data.Local.addObjectsToSyncQueue(objectType, lib1, [key1, key2]);
|
||
yield 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 = yield Zotero.DB.rowQueryAsync(sql, [lib1, key1]);
|
||
assert.approximately(row.lastCheck, now, 1);
|
||
assert.equal(row.tries, 1);
|
||
// key2
|
||
row = yield Zotero.DB.rowQueryAsync(sql, [lib1, key2]);
|
||
assert.approximately(row.lastCheck, now, 1);
|
||
assert.equal(row.tries, 2);
|
||
// key3
|
||
row = yield Zotero.DB.rowQueryAsync(sql, [lib2, key3]);
|
||
assert.equal(row.lastCheck, now - 86400);
|
||
assert.equal(row.tries, 2);
|
||
// key4
|
||
row = yield 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", 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();
|
||
yield 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 = yield Zotero.Sync.Data.Local.getObjectsToTryFromSyncQueue('item', lib1);
|
||
assert.sameMembers(keys, [key1]);
|
||
var keys = yield Zotero.Sync.Data.Local.getObjectsToTryFromSyncQueue('item', lib2);
|
||
assert.sameMembers(keys, [key3]);
|
||
});
|
||
});
|
||
|
||
describe("#removeObjectsFromSyncQueue()", function () {
|
||
it("should remove objects from the sync queue", 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();
|
||
yield 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
|
||
]
|
||
);
|
||
|
||
yield Zotero.Sync.Data.Local.removeObjectsFromSyncQueue('item', lib1, [key1]);
|
||
|
||
var sql = "SELECT COUNT(*) FROM syncQueue WHERE libraryID=? "
|
||
+ `AND syncObjectTypeID=${syncObjectTypeID} AND key=?`;
|
||
assert.notOk(yield Zotero.DB.valueQueryAsync(sql, [lib1, key1]));
|
||
assert.ok(yield Zotero.DB.valueQueryAsync(sql, [lib1, key2]));
|
||
assert.ok(yield Zotero.DB.valueQueryAsync(sql, [lib2, key3]));
|
||
})
|
||
});
|
||
|
||
describe("#resetSyncQueueTries", function () {
|
||
var spy;
|
||
|
||
after(function () {
|
||
if (spy) {
|
||
spy.restore();
|
||
}
|
||
})
|
||
|
||
it("should be run on version upgrade", function* () {
|
||
var sql = "REPLACE INTO settings (setting, key, value) VALUES ('client', 'lastVersion', ?)";
|
||
yield Zotero.DB.queryAsync(sql, "5.0foo");
|
||
|
||
spy = sinon.spy(Zotero.Sync.Data.Local, "resetSyncQueueTries");
|
||
yield Zotero.Schema.updateSchema();
|
||
assert.ok(spy.called);
|
||
});
|
||
});
|
||
});
|
||
|
||
|
||
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",
|
||
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
|
||
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",
|
||
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
|
||
);
|
||
Zotero.debug('=-=-=-=');
|
||
Zotero.debug(result);
|
||
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 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"
|
||
}
|
||
]
|
||
]
|
||
);
|
||
})
|
||
})
|
||
|
||
|
||
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("#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"
|
||
}
|
||
]
|
||
]
|
||
);
|
||
})
|
||
})
|
||
})
|