zotero/test/tests/syncLocalTest.js

1578 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 () {
var win;
beforeEach(function* () {
win = yield loadBrowserWindow();
});
afterEach(function () {
win.close();
});
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(win, 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(win, 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(win, 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 for download", function* () {
var libraryID = Zotero.Libraries.userLibraryID;
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);
})
it("should mark updated attachment items for download", function* () {
var libraryID = Zotero.Libraries.userLibraryID;
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);
})
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"
}
]
]
);
})
})
})