mirror of
https://github.com/zotero/zotero.git
synced 2026-09-26 01:11:23 +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.
5067 lines
141 KiB
JavaScript
5067 lines
141 KiB
JavaScript
"use strict";
|
|
|
|
describe("Zotero.Sync.Data.Engine", function () {
|
|
var apiKey = Zotero.Utilities.randomString(24);
|
|
var baseURL = "http://local.zotero/";
|
|
var engine, server, client, caller, stub, spy;
|
|
var userID = 1;
|
|
|
|
var responses = {};
|
|
|
|
var setup = async function (options = {}) {
|
|
server = sinon.fakeServer.create();
|
|
server.respondImmediately = true;
|
|
var background = options.background === undefined ? true : options.background;
|
|
var stopOnError = options.stopOnError === undefined ? true : options.stopOnError;
|
|
|
|
const { ConcurrentCaller } = ChromeUtils.importESModule("resource://zotero/concurrentCaller.mjs");
|
|
var caller = new ConcurrentCaller(1);
|
|
caller.setLogger(msg => Zotero.debug(msg));
|
|
caller.stopOnError = stopOnError;
|
|
|
|
var client = new Zotero.Sync.APIClient({
|
|
baseURL,
|
|
apiVersion: options.apiVersion || ZOTERO_CONFIG.API_VERSION,
|
|
apiKey,
|
|
caller,
|
|
background
|
|
});
|
|
|
|
var engine = new Zotero.Sync.Data.Engine({
|
|
userID,
|
|
apiClient: client,
|
|
libraryID: options.libraryID || Zotero.Libraries.userLibraryID,
|
|
stopOnError
|
|
});
|
|
|
|
return { engine, client, caller };
|
|
};
|
|
|
|
function setResponse(response) {
|
|
setHTTPResponse(server, baseURL, response, responses);
|
|
}
|
|
|
|
function setDefaultResponses(options = {}) {
|
|
var target = options.target || 'users/1';
|
|
var headers = {
|
|
"Last-Modified-Version": options.libraryVersion || 5
|
|
};
|
|
var lastLibraryVersion = options.lastLibraryVersion || 4;
|
|
setResponse({
|
|
method: "GET",
|
|
url: `${target}/settings?since=${lastLibraryVersion}`,
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `${target}/collections?format=versions&since=${lastLibraryVersion}`,
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `${target}/searches?format=versions&since=${lastLibraryVersion}`,
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `${target}/items/top?format=versions&since=${lastLibraryVersion}&includeTrashed=1`,
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `${target}/items?format=versions&since=${lastLibraryVersion}&includeTrashed=1`,
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `${target}/deleted?since=${lastLibraryVersion}`,
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
}
|
|
|
|
function makeCollectionJSON(options) {
|
|
return {
|
|
key: options.key,
|
|
version: options.version,
|
|
data: {
|
|
key: options.key,
|
|
version: options.version,
|
|
name: options.name,
|
|
parentCollection: options.parentCollection
|
|
}
|
|
};
|
|
}
|
|
|
|
function makeSearchJSON(options) {
|
|
return {
|
|
key: options.key,
|
|
version: options.version,
|
|
data: {
|
|
key: options.key,
|
|
version: options.version,
|
|
name: options.name,
|
|
conditions: options.conditions ? options.conditions : [
|
|
{
|
|
condition: 'title',
|
|
operator: 'contains',
|
|
value: 'test'
|
|
}
|
|
]
|
|
}
|
|
};
|
|
}
|
|
|
|
function makeItemJSON(options) {
|
|
var json = {
|
|
key: options.key,
|
|
version: options.version,
|
|
data: {
|
|
key: options.key,
|
|
version: options.version,
|
|
itemType: options.itemType || 'book',
|
|
title: options.title || options.name
|
|
}
|
|
};
|
|
Object.assign(json.data, options);
|
|
delete json.data.name;
|
|
return json;
|
|
}
|
|
|
|
// Allow functions to be called programmatically
|
|
var makeJSONFunctions = {
|
|
collection: makeCollectionJSON,
|
|
search: makeSearchJSON,
|
|
item: makeItemJSON
|
|
};
|
|
|
|
var assertInCache = async function (obj) {
|
|
var cacheObject = await Zotero.Sync.Data.Local.getCacheObject(
|
|
obj.objectType, obj.libraryID, obj.key, obj.version
|
|
);
|
|
assert.isObject(cacheObject);
|
|
assert.propertyVal(cacheObject, 'key', obj.key);
|
|
};
|
|
|
|
var assertNotInCache = async function (obj) {
|
|
assert.isFalse(await Zotero.Sync.Data.Local.getCacheObject(
|
|
obj.objectType, obj.libraryID, obj.key, obj.version
|
|
));
|
|
};
|
|
|
|
//
|
|
// Tests
|
|
//
|
|
beforeEach(async function () {
|
|
await resetData();
|
|
|
|
Zotero.HTTP.mock = sinon.FakeXMLHttpRequest;
|
|
|
|
await Zotero.Users.setCurrentUserID(userID);
|
|
await Zotero.Users.setCurrentUsername("testuser");
|
|
await Zotero.Users.setCurrentName("Test User");
|
|
})
|
|
|
|
after(function () {
|
|
Zotero.HTTP.mock = null;
|
|
});
|
|
|
|
describe("Syncing", function () {
|
|
it("should download items into a new library", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 3
|
|
};
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/settings",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
tagColors: {
|
|
value: [
|
|
{
|
|
name: "A",
|
|
color: "#CC66CC"
|
|
}
|
|
],
|
|
version: 2
|
|
}
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/collections?format=versions",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
"AAAAAAAA": 1
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/searches?format=versions",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
"AAAAAAAA": 2
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items/top?format=versions&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
"AAAAAAAA": 3
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items?format=versions&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
"AAAAAAAA": 3,
|
|
"BBBBBBBB": 3
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/collections?collectionKey=AAAAAAAA",
|
|
status: 200,
|
|
headers: headers,
|
|
json: [
|
|
makeCollectionJSON({
|
|
key: "AAAAAAAA",
|
|
version: 1,
|
|
name: "A"
|
|
})
|
|
]
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/searches?searchKey=AAAAAAAA",
|
|
status: 200,
|
|
headers: headers,
|
|
json: [
|
|
makeSearchJSON({
|
|
key: "AAAAAAAA",
|
|
version: 2,
|
|
name: "A"
|
|
})
|
|
]
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items?itemKey=AAAAAAAA&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: [
|
|
makeItemJSON({
|
|
key: "AAAAAAAA",
|
|
version: 3,
|
|
itemType: "book",
|
|
title: "A"
|
|
})
|
|
]
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items?itemKey=BBBBBBBB&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: [
|
|
makeItemJSON({
|
|
key: "BBBBBBBB",
|
|
version: 3,
|
|
itemType: "note",
|
|
parentItem: "AAAAAAAA",
|
|
note: "This is a note."
|
|
})
|
|
]
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/deleted?since=0",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
await engine.start();
|
|
|
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
// Check local library version
|
|
assert.equal(Zotero.Libraries.getVersion(userLibraryID), 3);
|
|
|
|
// Make sure local objects exist
|
|
var setting = Zotero.SyncedSettings.get(userLibraryID, "tagColors");
|
|
assert.lengthOf(setting, 1);
|
|
assert.equal(setting[0].name, 'A');
|
|
var settingMetadata = Zotero.SyncedSettings.getMetadata(userLibraryID, "tagColors");
|
|
assert.equal(settingMetadata.version, 2);
|
|
assert.isTrue(settingMetadata.synced);
|
|
|
|
var obj = await Zotero.Collections.getByLibraryAndKeyAsync(userLibraryID, "AAAAAAAA");
|
|
assert.equal(obj.name, 'A');
|
|
assert.equal(obj.version, 1);
|
|
assert.isTrue(obj.synced);
|
|
await assertInCache(obj);
|
|
|
|
obj = await Zotero.Searches.getByLibraryAndKeyAsync(userLibraryID, "AAAAAAAA");
|
|
assert.equal(obj.name, 'A');
|
|
assert.equal(obj.version, 2);
|
|
assert.isTrue(obj.synced);
|
|
await assertInCache(obj);
|
|
|
|
obj = await Zotero.Items.getByLibraryAndKeyAsync(userLibraryID, "AAAAAAAA");
|
|
assert.equal(obj.getField('title'), 'A');
|
|
assert.equal(obj.version, 3);
|
|
assert.isTrue(obj.synced);
|
|
var parentItemID = obj.id;
|
|
await assertInCache(obj);
|
|
|
|
obj = await Zotero.Items.getByLibraryAndKeyAsync(userLibraryID, "BBBBBBBB");
|
|
assert.equal(obj.note, 'This is a note.');
|
|
assert.equal(obj.parentItemID, parentItemID);
|
|
assert.equal(obj.version, 3);
|
|
assert.isTrue(obj.synced);
|
|
await assertInCache(obj);
|
|
})
|
|
|
|
it("should download items into a new read-only group", async function () {
|
|
var group = await createGroup({
|
|
editable: false,
|
|
filesEditable: false
|
|
});
|
|
var libraryID = group.libraryID;
|
|
var itemToDelete = await createDataObject(
|
|
'item', { libraryID, synced: true }, { skipEditCheck: true }
|
|
)
|
|
var itemToDeleteID = itemToDelete.id;
|
|
|
|
({ engine, client, caller } = await setup({ libraryID }));
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 3
|
|
};
|
|
setResponse({
|
|
method: "GET",
|
|
url: `groups/${group.id}/settings`,
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
tagColors: {
|
|
value: [
|
|
{
|
|
name: "A",
|
|
color: "#CC66CC"
|
|
}
|
|
],
|
|
version: 2
|
|
}
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `groups/${group.id}/collections?format=versions`,
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
"AAAAAAAA": 1
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `groups/${group.id}/searches?format=versions`,
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
"AAAAAAAA": 2
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `groups/${group.id}/items/top?format=versions&includeTrashed=1`,
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
"AAAAAAAA": 3
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `groups/${group.id}/items?format=versions&includeTrashed=1`,
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
"AAAAAAAA": 3,
|
|
"BBBBBBBB": 3
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `groups/${group.id}/collections?collectionKey=AAAAAAAA`,
|
|
status: 200,
|
|
headers: headers,
|
|
json: [
|
|
makeCollectionJSON({
|
|
key: "AAAAAAAA",
|
|
version: 1,
|
|
name: "A"
|
|
})
|
|
]
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `groups/${group.id}/searches?searchKey=AAAAAAAA`,
|
|
status: 200,
|
|
headers: headers,
|
|
json: [
|
|
makeSearchJSON({
|
|
key: "AAAAAAAA",
|
|
version: 2,
|
|
name: "A"
|
|
})
|
|
]
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `groups/${group.id}/items?itemKey=AAAAAAAA&includeTrashed=1`,
|
|
status: 200,
|
|
headers: headers,
|
|
json: [
|
|
makeItemJSON({
|
|
key: "AAAAAAAA",
|
|
version: 3,
|
|
itemType: "book",
|
|
title: "A"
|
|
})
|
|
]
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `groups/${group.id}/items?itemKey=BBBBBBBB&includeTrashed=1`,
|
|
status: 200,
|
|
headers: headers,
|
|
json: [
|
|
makeItemJSON({
|
|
key: "BBBBBBBB",
|
|
version: 3,
|
|
itemType: "note",
|
|
parentItem: "AAAAAAAA",
|
|
note: "This is a note."
|
|
})
|
|
]
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `groups/${group.id}/deleted?since=0`,
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
"items": [itemToDelete.key]
|
|
}
|
|
});
|
|
await engine.start();
|
|
|
|
// Check local library version
|
|
assert.equal(group.libraryVersion, 3);
|
|
|
|
// Make sure local objects exist
|
|
var setting = Zotero.SyncedSettings.get(libraryID, "tagColors");
|
|
assert.lengthOf(setting, 1);
|
|
assert.equal(setting[0].name, 'A');
|
|
var settingMetadata = Zotero.SyncedSettings.getMetadata(libraryID, "tagColors");
|
|
assert.equal(settingMetadata.version, 2);
|
|
assert.isTrue(settingMetadata.synced);
|
|
|
|
var obj = Zotero.Collections.getByLibraryAndKey(libraryID, "AAAAAAAA");
|
|
assert.equal(obj.name, 'A');
|
|
assert.equal(obj.version, 1);
|
|
assert.isTrue(obj.synced);
|
|
await assertInCache(obj);
|
|
|
|
obj = Zotero.Searches.getByLibraryAndKey(libraryID, "AAAAAAAA");
|
|
assert.equal(obj.name, 'A');
|
|
assert.equal(obj.version, 2);
|
|
assert.isTrue(obj.synced);
|
|
await assertInCache(obj);
|
|
|
|
obj = Zotero.Items.getByLibraryAndKey(libraryID, "AAAAAAAA");
|
|
assert.equal(obj.getField('title'), 'A');
|
|
assert.equal(obj.version, 3);
|
|
assert.isTrue(obj.synced);
|
|
var parentItemID = obj.id;
|
|
await assertInCache(obj);
|
|
|
|
obj = Zotero.Items.getByLibraryAndKey(libraryID, "BBBBBBBB");
|
|
assert.equal(obj.note, 'This is a note.');
|
|
assert.equal(obj.parentItemID, parentItemID);
|
|
assert.equal(obj.version, 3);
|
|
assert.isTrue(obj.synced);
|
|
await assertInCache(obj);
|
|
|
|
assert.isFalse(Zotero.Items.exists(itemToDeleteID));
|
|
});
|
|
|
|
it("should upload new full items and subsequent patches", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
var lastLibraryVersion = 5;
|
|
library.libraryVersion = library.storageVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
|
|
await Zotero.SyncedSettings.set(libraryID, "testSetting1", { foo: "bar" });
|
|
await Zotero.SyncedSettings.set(libraryID, "testSetting2", { bar: "foo" });
|
|
|
|
var types = Zotero.DataObjectUtilities.getTypes();
|
|
var objects = {};
|
|
var objectResponseJSON = {};
|
|
var objectVersions = {};
|
|
for (let type of types) {
|
|
objects[type] = [await createDataObject(type, { setTitle: true })];
|
|
objectVersions[type] = {};
|
|
objectResponseJSON[type] = objects[type].map(o => o.toResponseJSON());
|
|
}
|
|
|
|
server.respond(function (req) {
|
|
if (req.method == "POST") {
|
|
assert.equal(
|
|
req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion
|
|
);
|
|
|
|
// Both settings should be uploaded
|
|
if (req.url == baseURL + "users/1/settings") {
|
|
let json = JSON.parse(req.requestBody);
|
|
assert.lengthOf(Object.keys(json), 2);
|
|
assert.property(json, "testSetting1");
|
|
assert.property(json, "testSetting2");
|
|
assert.property(json.testSetting1, "value");
|
|
assert.property(json.testSetting2, "value");
|
|
assert.propertyVal(json.testSetting1.value, "foo", "bar");
|
|
assert.propertyVal(json.testSetting2.value, "bar", "foo");
|
|
req.respond(
|
|
204,
|
|
{
|
|
"Last-Modified-Version": ++lastLibraryVersion
|
|
},
|
|
""
|
|
);
|
|
return;
|
|
}
|
|
|
|
for (let type of types) {
|
|
let typePlural = Zotero.DataObjectUtilities.getObjectTypePlural(type);
|
|
if (req.url == baseURL + "users/1/" + typePlural) {
|
|
let json = JSON.parse(req.requestBody);
|
|
assert.lengthOf(json, 1);
|
|
assert.equal(json[0].key, objects[type][0].key);
|
|
assert.equal(json[0].version, 0);
|
|
if (type == 'item') {
|
|
assert.equal(json[0].title, objects[type][0].getField('title'));
|
|
}
|
|
else {
|
|
assert.equal(json[0].name, objects[type][0].name);
|
|
}
|
|
let objectJSON = objectResponseJSON[type][0];
|
|
objectJSON.version = ++lastLibraryVersion;
|
|
objectJSON.data.version = lastLibraryVersion;
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Content-Type": "application/json",
|
|
"Last-Modified-Version": lastLibraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {
|
|
"0": objectJSON
|
|
},
|
|
unchanged: {},
|
|
failed: {}
|
|
})
|
|
);
|
|
objectVersions[type][objects[type][0].key] = lastLibraryVersion;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
await engine.start();
|
|
|
|
await Zotero.SyncedSettings.set(libraryID, "testSetting2", { bar: "bar" });
|
|
|
|
assert.equal(library.libraryVersion, lastLibraryVersion);
|
|
assert.equal(library.storageVersion, lastLibraryVersion);
|
|
for (let type of types) {
|
|
// Make sure objects were set to the correct version and marked as synced
|
|
assert.lengthOf(((await Zotero.Sync.Data.Local.getUnsynced(type, libraryID))), 0);
|
|
let key = objects[type][0].key;
|
|
let version = objects[type][0].version;
|
|
assert.equal(version, objectVersions[type][key]);
|
|
// Make sure uploaded objects were added to cache
|
|
let cached = await Zotero.Sync.Data.Local.getCacheObject(type, libraryID, key, version);
|
|
assert.typeOf(cached, 'object');
|
|
assert.equal(cached.key, key);
|
|
assert.equal(cached.version, version);
|
|
|
|
await modifyDataObject(objects[type][0]);
|
|
}
|
|
|
|
({ engine, client, caller } = await setup());
|
|
|
|
server.respond(function (req) {
|
|
if (req.method == "POST") {
|
|
assert.equal(
|
|
req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion
|
|
);
|
|
|
|
// Modified setting should be uploaded
|
|
if (req.url == baseURL + "users/1/settings") {
|
|
let json = JSON.parse(req.requestBody);
|
|
assert.lengthOf(Object.keys(json), 1);
|
|
assert.property(json, "testSetting2");
|
|
assert.property(json.testSetting2, "value");
|
|
assert.propertyVal(json.testSetting2.value, "bar", "bar");
|
|
req.respond(
|
|
204,
|
|
{
|
|
"Last-Modified-Version": ++lastLibraryVersion
|
|
},
|
|
""
|
|
);
|
|
return;
|
|
}
|
|
|
|
for (let type of types) {
|
|
let typePlural = Zotero.DataObjectUtilities.getObjectTypePlural(type);
|
|
if (req.url == baseURL + "users/1/" + typePlural) {
|
|
let json = JSON.parse(req.requestBody);
|
|
assert.lengthOf(json, 1);
|
|
let j = json[0];
|
|
let o = objects[type][0];
|
|
assert.equal(j.key, o.key);
|
|
assert.equal(j.version, objectVersions[type][o.key]);
|
|
if (type == 'item') {
|
|
assert.equal(j.title, o.getField('title'));
|
|
}
|
|
else {
|
|
assert.equal(j.name, o.name);
|
|
}
|
|
|
|
// Verify PATCH semantics instead of POST (i.e., only changed fields)
|
|
let changedFieldsExpected = ['key', 'version'];
|
|
if (type == 'item') {
|
|
changedFieldsExpected.push('title', 'dateModified');
|
|
}
|
|
else {
|
|
changedFieldsExpected.push('name');
|
|
}
|
|
let changedFields = Object.keys(j);
|
|
assert.lengthOf(
|
|
changedFields, changedFieldsExpected.length, "same " + type + " length"
|
|
);
|
|
assert.sameMembers(
|
|
changedFields, changedFieldsExpected, "same " + type + " members"
|
|
);
|
|
let objectJSON = objectResponseJSON[type][0];
|
|
objectJSON.version = ++lastLibraryVersion;
|
|
objectJSON.data.version = lastLibraryVersion;
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Content-Type": "application/json",
|
|
"Last-Modified-Version": lastLibraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {
|
|
"0": objectJSON
|
|
},
|
|
unchanged: {},
|
|
failed: {}
|
|
})
|
|
);
|
|
objectVersions[type][o.key] = lastLibraryVersion;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
await engine.start();
|
|
|
|
assert.equal(library.libraryVersion, lastLibraryVersion);
|
|
assert.equal(library.storageVersion, lastLibraryVersion);
|
|
for (let type of types) {
|
|
// Make sure objects were set to the correct version and marked as synced
|
|
assert.lengthOf(((await Zotero.Sync.Data.Local.getUnsynced(type, libraryID))), 0);
|
|
let o = objects[type][0];
|
|
let key = o.key;
|
|
let version = o.version;
|
|
assert.equal(version, objectVersions[type][key]);
|
|
// Make sure uploaded objects were added to cache
|
|
let cached = await Zotero.Sync.Data.Local.getCacheObject(type, libraryID, key, version);
|
|
assert.typeOf(cached, 'object');
|
|
assert.equal(cached.key, key);
|
|
assert.equal(cached.version, version);
|
|
|
|
switch (type) {
|
|
case 'collection':
|
|
assert.isFalse(cached.data.parentCollection);
|
|
break;
|
|
|
|
case 'item':
|
|
assert.equal(cached.data.dateAdded, Zotero.Date.sqlToISO8601(o.dateAdded));
|
|
break;
|
|
|
|
case 'search':
|
|
assert.isArray(cached.data.conditions);
|
|
break;
|
|
}
|
|
|
|
// Make sure older versions have been removed from the cache
|
|
let versions = await Zotero.Sync.Data.Local.getCacheObjectVersions(type, libraryID, key);
|
|
assert.sameMembers(versions, [version]);
|
|
}
|
|
})
|
|
|
|
|
|
it("should upload child items after parent items", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var lastLibraryVersion = 5;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
|
|
// Create top-level note, embedded-image attachment, book, and child note
|
|
var note1 = await createDataObject('item', { itemType: 'note', note: 'A' });
|
|
var attachment = await Zotero.Attachments.importEmbeddedImage({
|
|
blob: await File.createFromFileName(
|
|
OS.Path.join(getTestDataDirectory().path, 'test.png')
|
|
),
|
|
parentItemID: note1.id
|
|
});
|
|
var item = await createDataObject('item');
|
|
var note2 = await createDataObject('item', { itemType: 'note', parentID: item.id, note: 'B' });
|
|
// Move note under parent
|
|
note1.parentItemID = item.id;
|
|
await note1.saveTx();
|
|
var handled = false;
|
|
|
|
server.respond(function (req) {
|
|
if (req.method == "POST" && req.url == baseURL + "users/1/items") {
|
|
let json = JSON.parse(req.requestBody);
|
|
assert.lengthOf(json, 4);
|
|
assert.equal(json[0].key, item.key);
|
|
assert.oneOf(
|
|
[json[1].key, json[2].key, json[3].key].join(''),
|
|
[
|
|
[note1.key, attachment.key, note2.key].join(''),
|
|
[note2.key, note1.key, attachment.key].join(''),
|
|
]
|
|
);
|
|
let successful;
|
|
if (json[1].key == note1.key) {
|
|
successful = {
|
|
"0": item.toResponseJSON({ version: lastLibraryVersion }),
|
|
"1": note1.toResponseJSON({ version: lastLibraryVersion }),
|
|
"2": attachment.toResponseJSON({ version: lastLibraryVersion }),
|
|
"3": note2.toResponseJSON({ version: lastLibraryVersion }),
|
|
};
|
|
}
|
|
else {
|
|
successful = {
|
|
"0": item.toResponseJSON({ version: lastLibraryVersion }),
|
|
"1": note2.toResponseJSON({ version: lastLibraryVersion }),
|
|
"2": note1.toResponseJSON({ version: lastLibraryVersion }),
|
|
"3": attachment.toResponseJSON({ version: lastLibraryVersion }),
|
|
};
|
|
}
|
|
handled = true;
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Content-Type": "application/json",
|
|
"Last-Modified-Version": ++lastLibraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful,
|
|
unchanged: {},
|
|
failed: {}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
});
|
|
|
|
await engine.start();
|
|
assert.isTrue(handled);
|
|
});
|
|
|
|
|
|
it("shouldn't update existing cache object after upload on 'unchanged' response", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var lastLibraryVersion = 5;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
|
|
var item = await createDataObject('item', { version: 1, title: "A" });
|
|
var json = item.toJSON();
|
|
// Save current version to cache so the patch object is empty, as if the item had been
|
|
// added to a collection and removed from it (such that even dateModified didn't change)
|
|
await Zotero.Sync.Data.Local.saveCacheObjects('item', library.id, [json]);
|
|
|
|
server.respond(function (req) {
|
|
if (req.method == "POST" && req.url == baseURL + "users/1/items") {
|
|
let json = JSON.parse(req.requestBody);
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Content-Type": "application/json",
|
|
"Last-Modified-Version": ++lastLibraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {},
|
|
unchanged: {
|
|
"0": item.key
|
|
},
|
|
failed: {}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
});
|
|
|
|
await engine.start();
|
|
|
|
// Check data in cache
|
|
var version = await Zotero.Sync.Data.Local.getLatestCacheObjectVersion(
|
|
'item', library.id, item.key
|
|
);
|
|
assert.equal(version, 1);
|
|
json = await Zotero.Sync.Data.Local.getCacheObject(
|
|
'item', library.id, item.key, 1
|
|
);
|
|
assert.propertyVal(json.data, 'itemType', 'book');
|
|
assert.propertyVal(json.data, 'title', 'A');
|
|
});
|
|
|
|
|
|
it("should upload child collection after parent collection", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var lastLibraryVersion = 5;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
|
|
var collection1 = await createDataObject('collection');
|
|
var collection2 = await createDataObject('collection');
|
|
var collection3 = await createDataObject('collection', { parentID: collection2.id });
|
|
// Move collection under the other
|
|
collection1.parentID = collection2.id;
|
|
await collection1.saveTx();
|
|
|
|
var handled = false;
|
|
|
|
server.respond(function (req) {
|
|
if (req.method == "POST" && req.url == baseURL + "users/1/collections") {
|
|
let json = JSON.parse(req.requestBody);
|
|
assert.lengthOf(json, 3);
|
|
assert.equal(json[0].key, collection2.key);
|
|
assert.equal(json[1].key, collection1.key);
|
|
assert.equal(json[2].key, collection3.key);
|
|
handled = true;
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Content-Type": "application/json",
|
|
"Last-Modified-Version": ++lastLibraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {
|
|
"0": collection2.toResponseJSON(),
|
|
"1": collection1.toResponseJSON(),
|
|
"2": collection3.toResponseJSON()
|
|
},
|
|
unchanged: {},
|
|
failed: {}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
});
|
|
|
|
await engine.start();
|
|
assert.isTrue(handled);
|
|
});
|
|
|
|
|
|
it("should update library version after settings upload", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
var lastLibraryVersion = 5;
|
|
library.libraryVersion = library.storageVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
|
|
await Zotero.SyncedSettings.set(libraryID, "testSetting", { foo: "bar" });
|
|
|
|
server.respond(function (req) {
|
|
if (req.method == "POST") {
|
|
assert.equal(
|
|
req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion
|
|
);
|
|
|
|
if (req.url == baseURL + "users/1/settings") {
|
|
let json = JSON.parse(req.requestBody);
|
|
req.respond(
|
|
204,
|
|
{
|
|
"Last-Modified-Version": ++lastLibraryVersion
|
|
},
|
|
""
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
})
|
|
|
|
await engine.start();
|
|
|
|
assert.isAbove(library.libraryVersion, 5);
|
|
assert.equal(library.libraryVersion, lastLibraryVersion);
|
|
assert.equal(library.storageVersion, lastLibraryVersion);
|
|
});
|
|
|
|
|
|
it("should upload settings in batches", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var batchSize = 250;
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
var lastLibraryVersion = 5;
|
|
library.libraryVersion = library.storageVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
|
|
for (let i = 0; i < (batchSize * 2 + 5); i++) {
|
|
let key = Zotero.DataObjectUtilities.generateKey();
|
|
let page = Zotero.Utilities.rand(1, 20);
|
|
await Zotero.SyncedSettings.set(libraryID, `lastPageIndex_u_${key}`, page);
|
|
}
|
|
|
|
var keys = new Set();
|
|
|
|
var numRequests = 0;
|
|
server.respond(function (req) {
|
|
if (req.method == "POST") {
|
|
numRequests++;
|
|
assert.equal(
|
|
req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion
|
|
);
|
|
|
|
if (req.url == baseURL + "users/1/settings") {
|
|
let json = JSON.parse(req.requestBody);
|
|
|
|
Zotero.debug(json);
|
|
if (numRequests == 1 || numRequests == 2) {
|
|
assert.lengthOf(Object.keys(json), batchSize);
|
|
}
|
|
else {
|
|
assert.lengthOf(Object.keys(json), 5);
|
|
}
|
|
|
|
for (let key of Object.keys(json)) {
|
|
keys.add(key);
|
|
}
|
|
|
|
req.respond(
|
|
204,
|
|
{
|
|
"Last-Modified-Version": ++lastLibraryVersion
|
|
},
|
|
""
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
})
|
|
|
|
await engine.start();
|
|
|
|
assert.equal(numRequests, 3);
|
|
assert.equal(keys.size, batchSize * 2 + 5);
|
|
});
|
|
|
|
|
|
it("shouldn't update library storage version after settings upload if storage version was already behind", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
var lastLibraryVersion = 5;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
library.storageVersion = 4;
|
|
await library.saveTx();
|
|
|
|
await Zotero.SyncedSettings.set(libraryID, "testSetting", { foo: "bar" });
|
|
|
|
server.respond(function (req) {
|
|
if (req.method == "POST") {
|
|
assert.equal(
|
|
req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion
|
|
);
|
|
|
|
if (req.url == baseURL + "users/1/settings") {
|
|
let json = JSON.parse(req.requestBody);
|
|
req.respond(
|
|
204,
|
|
{
|
|
"Last-Modified-Version": ++lastLibraryVersion
|
|
},
|
|
""
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
})
|
|
|
|
await engine.start();
|
|
|
|
assert.isAbove(library.libraryVersion, 5);
|
|
assert.equal(library.libraryVersion, lastLibraryVersion);
|
|
assert.equal(library.storageVersion, 4);
|
|
});
|
|
|
|
|
|
it("shouldn't update library storage version after item upload if storage version was already behind", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
var lastLibraryVersion = 5;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
library.storageVersion = 4;
|
|
await library.saveTx();
|
|
|
|
var item = await createDataObject('item');
|
|
var itemResponseJSON = item.toResponseJSON();
|
|
|
|
server.respond(function (req) {
|
|
if (req.method == "POST") {
|
|
assert.equal(
|
|
req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion
|
|
);
|
|
|
|
if (req.url == baseURL + "users/1/items") {
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Content-Type": "application/json",
|
|
"Last-Modified-Version": lastLibraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {
|
|
"0": itemResponseJSON
|
|
},
|
|
unchanged: {},
|
|
failed: {}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
})
|
|
|
|
await engine.start();
|
|
|
|
assert.equal(library.libraryVersion, lastLibraryVersion);
|
|
assert.equal(library.storageVersion, 4);
|
|
});
|
|
|
|
|
|
it("should process downloads after upload failure", async function () {
|
|
({ engine, client, caller } = await setup({
|
|
stopOnError: false
|
|
}));
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
var lastLibraryVersion = 5;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
|
|
var collection = await createDataObject('collection');
|
|
|
|
var called = 0;
|
|
server.respond(function (req) {
|
|
if (called == 0) {
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Last-Modified-Version": lastLibraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {},
|
|
unchanged: {},
|
|
failed: {
|
|
0: {
|
|
code: 400,
|
|
message: "Upload failed"
|
|
}
|
|
}
|
|
})
|
|
);
|
|
}
|
|
called++;
|
|
});
|
|
|
|
var stub = sinon.stub(engine, "_startDownload")
|
|
.returns(Promise.resolve(engine.DOWNLOAD_RESULT_CONTINUE));
|
|
|
|
var e = await getPromiseError(engine.start());
|
|
assert.equal(called, 1);
|
|
// start() should still fail
|
|
assert.ok(e);
|
|
assert.equal(e.message, "Made no progress during upload -- stopping");
|
|
// The collection shouldn't have been marked as synced
|
|
assert.isFalse(collection.synced);
|
|
// Download should have been performed
|
|
assert.ok(stub.called);
|
|
|
|
stub.restore();
|
|
});
|
|
|
|
|
|
it("shouldn't update library storage version if there were storage metadata changes", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var lastLibraryVersion = 2;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
library.storageVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
|
|
var target = 'users/1';
|
|
var newLibraryVersion = 5;
|
|
var headers = {
|
|
"Last-Modified-Version": newLibraryVersion
|
|
};
|
|
|
|
// Create an attachment response with storage metadata
|
|
var item = new Zotero.Item('attachment');
|
|
item.libraryID = Zotero.Libraries.userLibraryID;
|
|
item.attachmentLinkMode = 'imported_file';
|
|
item.attachmentFilename = 'test.txt';
|
|
item.attachmentContentType = 'text/plain';
|
|
item.attachmentCharset = 'utf-8';
|
|
var itemResponseJSON = item.toResponseJSON();
|
|
itemResponseJSON.key = itemResponseJSON.data.key = Zotero.DataObjectUtilities.generateKey();
|
|
itemResponseJSON.version = itemResponseJSON.data.version = newLibraryVersion;
|
|
itemResponseJSON.data.mtime = new Date().getTime();
|
|
itemResponseJSON.data.md5 = '57f8a4fda823187b91e1191487b87fe6';
|
|
|
|
setDefaultResponses({
|
|
target,
|
|
lastLibraryVersion: lastLibraryVersion,
|
|
libraryVersion: newLibraryVersion
|
|
});
|
|
|
|
setResponse({
|
|
method: "GET",
|
|
url: `${target}/items?format=versions&since=${lastLibraryVersion}&includeTrashed=1`,
|
|
status: 200,
|
|
headers,
|
|
json: {
|
|
[item.key]: newLibraryVersion
|
|
}
|
|
});
|
|
|
|
setResponse({
|
|
method: "GET",
|
|
url: `${target}/items?itemKey=${item.key}&includeTrashed=1`,
|
|
status: 200,
|
|
headers,
|
|
json: [itemResponseJSON]
|
|
});
|
|
|
|
await engine.start();
|
|
|
|
assert.equal(library.libraryVersion, newLibraryVersion);
|
|
assert.equal(library.storageVersion, lastLibraryVersion);
|
|
});
|
|
|
|
|
|
it("should update library storage version if there were no storage metadata changes and storage version wasn't already behind", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var lastLibraryVersion = 2;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
library.storageVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
|
|
var target = 'users/1';
|
|
var newLibraryVersion = 5;
|
|
var headers = {
|
|
"Last-Modified-Version": newLibraryVersion
|
|
};
|
|
|
|
setDefaultResponses({
|
|
target,
|
|
lastLibraryVersion: lastLibraryVersion,
|
|
libraryVersion: newLibraryVersion
|
|
});
|
|
|
|
await engine.start();
|
|
|
|
assert.equal(library.libraryVersion, newLibraryVersion);
|
|
assert.equal(library.storageVersion, newLibraryVersion);
|
|
});
|
|
|
|
|
|
it("shouldn't update library storage version if there were no storage metadata changes but storage version was already behind", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var lastLibraryVersion = 2;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
library.storageVersion = 1;
|
|
await library.saveTx();
|
|
|
|
var target = 'users/1';
|
|
var newLibraryVersion = 5;
|
|
var headers = {
|
|
"Last-Modified-Version": newLibraryVersion
|
|
};
|
|
|
|
setDefaultResponses({
|
|
target,
|
|
lastLibraryVersion: lastLibraryVersion,
|
|
libraryVersion: newLibraryVersion
|
|
});
|
|
|
|
await engine.start();
|
|
|
|
assert.equal(library.libraryVersion, newLibraryVersion);
|
|
assert.equal(library.storageVersion, 1);
|
|
});
|
|
|
|
|
|
it("shouldn't include mtime and md5 for attachments in ZFS libraries", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var lastLibraryVersion = 2;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
|
|
var item = new Zotero.Item('attachment');
|
|
item.attachmentLinkMode = 'imported_file';
|
|
item.attachmentFilename = 'test.txt';
|
|
item.attachmentContentType = 'text/plain';
|
|
item.attachmentCharset = 'utf-8';
|
|
await item.saveTx();
|
|
|
|
var itemResponseJSON = item.toResponseJSON();
|
|
itemResponseJSON.version = itemResponseJSON.data.version = lastLibraryVersion;
|
|
|
|
server.respond(function (req) {
|
|
if (req.method == "POST") {
|
|
if (req.url == baseURL + "users/1/items") {
|
|
let json = JSON.parse(req.requestBody);
|
|
assert.lengthOf(json, 1);
|
|
let itemJSON = json[0];
|
|
assert.equal(itemJSON.key, item.key);
|
|
assert.equal(itemJSON.version, 0);
|
|
assert.property(itemJSON, "contentType");
|
|
assert.property(itemJSON, "charset");
|
|
assert.property(itemJSON, "filename");
|
|
assert.notProperty(itemJSON, "mtime");
|
|
assert.notProperty(itemJSON, "md5");
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Content-Type": "application/json",
|
|
"Last-Modified-Version": lastLibraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {
|
|
"0": itemResponseJSON
|
|
},
|
|
unchanged: {},
|
|
failed: {}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
})
|
|
|
|
await engine.start();
|
|
});
|
|
|
|
|
|
it("should include storage properties for attachments in WebDAV libraries", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var lastLibraryVersion = 2;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
Zotero.Sync.Storage.Local.setModeForLibrary(library.id, 'webdav');
|
|
|
|
var item = new Zotero.Item('attachment');
|
|
item.attachmentLinkMode = 'imported_file';
|
|
item.attachmentFilename = 'test.txt';
|
|
item.attachmentContentType = 'text/plain';
|
|
item.attachmentCharset = 'utf-8';
|
|
await item.saveTx();
|
|
|
|
var itemResponseJSON = item.toResponseJSON();
|
|
itemResponseJSON.version = itemResponseJSON.data.version = lastLibraryVersion;
|
|
|
|
server.respond(function (req) {
|
|
if (req.method == "POST") {
|
|
if (req.url == baseURL + "users/1/items") {
|
|
let json = JSON.parse(req.requestBody);
|
|
assert.lengthOf(json, 1);
|
|
let itemJSON = json[0];
|
|
assert.equal(itemJSON.key, item.key);
|
|
assert.equal(itemJSON.version, 0);
|
|
assert.propertyVal(itemJSON, "contentType", item.attachmentContentType);
|
|
assert.propertyVal(itemJSON, "charset", item.attachmentCharset);
|
|
assert.propertyVal(itemJSON, "filename", item.attachmentFilename);
|
|
assert.notPropertyVal(itemJSON, "mtime");
|
|
assert.notPropertyVal(itemJSON, "md5");
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Content-Type": "application/json",
|
|
"Last-Modified-Version": lastLibraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {
|
|
"0": itemResponseJSON
|
|
},
|
|
unchanged: {},
|
|
failed: {}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
})
|
|
|
|
await engine.start();
|
|
});
|
|
|
|
|
|
it("should include mtime and md5 synced to WebDAV in WebDAV libraries", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var lastLibraryVersion = 2;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
Zotero.Sync.Storage.Local.setModeForLibrary(library.id, 'webdav');
|
|
|
|
var item = new Zotero.Item('attachment');
|
|
item.attachmentLinkMode = 'imported_file';
|
|
item.attachmentFilename = 'test1.txt';
|
|
await item.saveTx();
|
|
|
|
var mtime = new Date().getTime();
|
|
var md5 = '57f8a4fda823187b91e1191487b87fe6';
|
|
|
|
item.attachmentSyncedModificationTime = mtime;
|
|
item.attachmentSyncedHash = md5;
|
|
await item.saveTx({ skipAll: true });
|
|
|
|
var itemResponseJSON = item.toResponseJSON();
|
|
itemResponseJSON.version = itemResponseJSON.data.version = lastLibraryVersion;
|
|
itemResponseJSON.data.mtime = mtime;
|
|
itemResponseJSON.data.md5 = md5;
|
|
|
|
server.respond(function (req) {
|
|
if (req.method == "POST") {
|
|
if (req.url == baseURL + "users/1/items") {
|
|
let json = JSON.parse(req.requestBody);
|
|
assert.lengthOf(json, 1);
|
|
let itemJSON = json[0];
|
|
assert.equal(itemJSON.key, item.key);
|
|
assert.equal(itemJSON.version, 0);
|
|
assert.equal(itemJSON.mtime, mtime);
|
|
assert.equal(itemJSON.md5, md5);
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Content-Type": "application/json",
|
|
"Last-Modified-Version": lastLibraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {
|
|
"0": itemResponseJSON
|
|
},
|
|
unchanged: {},
|
|
failed: {}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
})
|
|
|
|
await engine.start();
|
|
|
|
// Check data in cache
|
|
var json = await Zotero.Sync.Data.Local.getCacheObject(
|
|
'item', library.id, item.key, lastLibraryVersion
|
|
);
|
|
assert.equal(json.data.mtime, mtime);
|
|
assert.equal(json.data.md5, md5);
|
|
})
|
|
|
|
// See also: "shouldn't include external annotations" in syncLocalTest.js
|
|
it("shouldn't upload external annotations", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
var lastLibraryVersion = 5;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
var nextLibraryVersion = lastLibraryVersion + 1;
|
|
|
|
var attachment = await importFileAttachment('test.pdf');
|
|
var annotation1 = await createAnnotation('highlight', attachment);
|
|
var annotation2 = await createAnnotation('highlight', attachment, { isExternal: true });
|
|
|
|
var item1ResponseJSON = attachment.toResponseJSON();
|
|
item1ResponseJSON.version = item1ResponseJSON.data.version = nextLibraryVersion;
|
|
var item2ResponseJSON = annotation1.toResponseJSON();
|
|
item2ResponseJSON.version = item2ResponseJSON.data.version = nextLibraryVersion;
|
|
|
|
server.respond(function (req) {
|
|
if (req.method == "POST") {
|
|
assert.equal(
|
|
req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion
|
|
);
|
|
|
|
if (req.url == baseURL + "users/1/items") {
|
|
let json = JSON.parse(req.requestBody);
|
|
assert.lengthOf(json, 2);
|
|
let keys = [json[0].key, json[1].key];
|
|
assert.include(keys, attachment.key);
|
|
assert.include(keys, annotation1.key);
|
|
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Content-Type": "application/json",
|
|
"Last-Modified-Version": nextLibraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {
|
|
"0": item1ResponseJSON,
|
|
"1": item2ResponseJSON
|
|
},
|
|
unchanged: {},
|
|
failed: {}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
})
|
|
|
|
await engine.start();
|
|
});
|
|
|
|
it("should update local objects with remotely saved version after uploading if necessary", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
var lastLibraryVersion = 5;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
|
|
var types = Zotero.DataObjectUtilities.getTypes();
|
|
var objects = {};
|
|
var objectResponseJSON = {};
|
|
var objectNames = {};
|
|
var itemDateModified = {};
|
|
for (let type of types) {
|
|
objects[type] = [
|
|
await createDataObject(
|
|
type, { setTitle: true, dateModified: '2016-05-21 01:00:00' }
|
|
)
|
|
];
|
|
objectNames[type] = {};
|
|
objectResponseJSON[type] = objects[type].map(o => o.toResponseJSON());
|
|
if (type == 'item') {
|
|
let item = objects[type][0];
|
|
itemDateModified[item.key] = item.dateModified;
|
|
}
|
|
}
|
|
|
|
server.respond(function (req) {
|
|
if (req.method == "POST") {
|
|
assert.equal(
|
|
req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion
|
|
);
|
|
|
|
for (let type of types) {
|
|
let typePlural = Zotero.DataObjectUtilities.getObjectTypePlural(type);
|
|
if (req.url == baseURL + "users/1/" + typePlural) {
|
|
let key = objects[type][0].key;
|
|
let objectJSON = objectResponseJSON[type][0];
|
|
objectJSON.version = ++lastLibraryVersion;
|
|
objectJSON.data.version = lastLibraryVersion;
|
|
let prop = type == 'item' ? 'title' : 'name';
|
|
objectNames[type][key] = objectJSON.data[prop] = Zotero.Utilities.randomString();
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Content-Type": "application/json",
|
|
"Last-Modified-Version": lastLibraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {
|
|
"0": objectJSON
|
|
},
|
|
unchanged: {},
|
|
failed: {}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
await engine.start();
|
|
|
|
assert.equal(library.libraryVersion, lastLibraryVersion);
|
|
for (let type of types) {
|
|
// Make sure local objects were updated with new metadata and marked as synced
|
|
assert.lengthOf(((await Zotero.Sync.Data.Local.getUnsynced(type, libraryID))), 0);
|
|
let o = objects[type][0];
|
|
let key = o.key;
|
|
let version = o.version;
|
|
let name = objectNames[type][key];
|
|
if (type == 'item') {
|
|
assert.equal(name, o.getField('title'));
|
|
|
|
// But Date Modified shouldn't have changed for items
|
|
assert.equal(itemDateModified[key], o.dateModified);
|
|
}
|
|
else {
|
|
assert.equal(name, o.name);
|
|
}
|
|
}
|
|
})
|
|
|
|
it("should upload local deletions", async function () {
|
|
var { engine, client, caller } = await setup();
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var lastLibraryVersion = 5;
|
|
library.libraryVersion = library.storageVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
|
|
|
|
var types = Zotero.DataObjectUtilities.getTypes();
|
|
var objects = {};
|
|
for (let type of types) {
|
|
let obj1 = await createDataObject(type);
|
|
let obj2 = await createDataObject(type);
|
|
objects[type] = [obj1.key, obj2.key];
|
|
await obj1.eraseTx();
|
|
await obj2.eraseTx();
|
|
}
|
|
|
|
var count = types.length;
|
|
|
|
server.respond(function (req) {
|
|
if (req.method == "DELETE") {
|
|
assert.equal(
|
|
req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion
|
|
);
|
|
|
|
// TODO: Settings?
|
|
|
|
// Data objects
|
|
for (let type of types) {
|
|
let typePlural = Zotero.DataObjectUtilities.getObjectTypePlural(type);
|
|
if (req.url.startsWith(baseURL + "users/1/" + typePlural)) {
|
|
let matches = req.url.match(new RegExp("\\?" + type + "Key=(.+)"));
|
|
let keys = decodeURIComponent(matches[1]).split(',');
|
|
assert.sameMembers(keys, objects[type]);
|
|
req.respond(
|
|
204,
|
|
{
|
|
"Last-Modified-Version": ++lastLibraryVersion
|
|
}
|
|
);
|
|
count--;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
await engine.start();
|
|
|
|
assert.equal(count, 0);
|
|
for (let type of types) {
|
|
assert.lengthOf(
|
|
await Zotero.Sync.Data.Local.getDeleted(type, library.id), 0
|
|
);
|
|
}
|
|
assert.equal(library.libraryVersion, lastLibraryVersion);
|
|
assert.equal(library.storageVersion, lastLibraryVersion);
|
|
})
|
|
|
|
it("should make only one request if in sync", async function () {
|
|
var library = Zotero.Libraries.userLibrary;
|
|
library.libraryVersion = 5;
|
|
await library.saveTx();
|
|
({ engine, client, caller } = await setup());
|
|
|
|
server.respond(function (req) {
|
|
if (req.method == "GET" && req.url == baseURL + "users/1/settings?since=5") {
|
|
let since = req.requestHeaders["If-Modified-Since-Version"];
|
|
if (since == 5) {
|
|
req.respond(304);
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
await engine.start();
|
|
})
|
|
|
|
it("should add objects to sync queue if they can't be saved", async function () {
|
|
({ engine, client, caller } = await setup({
|
|
stopOnError: false
|
|
}));
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 3
|
|
};
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/settings",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/collections?format=versions",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
"AAAAAAAA": 1,
|
|
"BBBBBBBB": 1,
|
|
"CCCCCCCC": 1
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/searches?format=versions",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
"DDDDDDDD": 2,
|
|
"EEEEEEEE": 2,
|
|
"FFFFFFFF": 2
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items/top?format=versions&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
"GGGGGGGG": 3,
|
|
"HHHHHHHH": 3
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items?format=versions&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
"GGGGGGGG": 3,
|
|
"HHHHHHHH": 3,
|
|
"JJJJJJJJ": 3
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/collections?collectionKey=AAAAAAAA%2CBBBBBBBB%2CCCCCCCCC",
|
|
status: 200,
|
|
headers: headers,
|
|
json: [
|
|
makeCollectionJSON({
|
|
key: "AAAAAAAA",
|
|
version: 1,
|
|
name: "A"
|
|
}),
|
|
makeCollectionJSON({
|
|
key: "BBBBBBBB",
|
|
version: 1,
|
|
name: "B",
|
|
// Missing parent -- collection should be queued
|
|
parentCollection: "ZZZZZZZZ"
|
|
}),
|
|
makeCollectionJSON({
|
|
key: "CCCCCCCC",
|
|
version: 1,
|
|
name: "C",
|
|
// Unknown field -- collection should be queued
|
|
unknownField: 5
|
|
})
|
|
]
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/searches?searchKey=DDDDDDDD%2CEEEEEEEE%2CFFFFFFFF",
|
|
status: 200,
|
|
headers: headers,
|
|
json: [
|
|
makeSearchJSON({
|
|
key: "DDDDDDDD",
|
|
version: 2,
|
|
name: "D",
|
|
conditions: [
|
|
{
|
|
condition: "title",
|
|
operator: "is",
|
|
value: "a"
|
|
}
|
|
]
|
|
}),
|
|
makeSearchJSON({
|
|
key: "EEEEEEEE",
|
|
version: 2,
|
|
name: "E",
|
|
conditions: [
|
|
{
|
|
// Unknown search condition -- search should be queued
|
|
condition: "unknownCondition",
|
|
operator: "is",
|
|
value: "a"
|
|
}
|
|
]
|
|
}),
|
|
makeSearchJSON({
|
|
key: "FFFFFFFF",
|
|
version: 2,
|
|
name: "F",
|
|
conditions: [
|
|
{
|
|
condition: "title",
|
|
// Unknown search operator -- search should be queued
|
|
operator: "unknownOperator",
|
|
value: "a"
|
|
}
|
|
]
|
|
})
|
|
]
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items?itemKey=GGGGGGGG%2CHHHHHHHH&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: [
|
|
makeItemJSON({
|
|
key: "GGGGGGGG",
|
|
version: 3,
|
|
itemType: "book",
|
|
title: "G",
|
|
// Unknown item field -- item should be queued
|
|
unknownField: "B"
|
|
}),
|
|
makeItemJSON({
|
|
key: "HHHHHHHH",
|
|
version: 3,
|
|
// Unknown item type -- item should be queued
|
|
itemType: "unknownItemType",
|
|
title: "H"
|
|
})
|
|
]
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items?itemKey=JJJJJJJJ&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: [
|
|
makeItemJSON({
|
|
key: "JJJJJJJJ",
|
|
version: 3,
|
|
itemType: "note",
|
|
// Parent that couldn't be saved -- item should be queued
|
|
parentItem: "HHHHHHHH",
|
|
note: "This is a note."
|
|
})
|
|
]
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/deleted?since=0",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
var spy = sinon.spy(engine, "onError");
|
|
await engine.start();
|
|
|
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
// Library version should have been updated
|
|
assert.equal(Zotero.Libraries.getVersion(userLibraryID), 3);
|
|
|
|
// Check for saved objects
|
|
assert.ok(await Zotero.Collections.getByLibraryAndKeyAsync(userLibraryID, "AAAAAAAA"));
|
|
assert.ok(await Zotero.Searches.getByLibraryAndKeyAsync(userLibraryID, "DDDDDDDD"));
|
|
|
|
// Check for queued objects
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('collection', userLibraryID);
|
|
assert.sameMembers(keys, ['BBBBBBBB']);
|
|
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('search', userLibraryID);
|
|
assert.sameMembers(keys, ['EEEEEEEE', 'FFFFFFFF']);
|
|
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', userLibraryID);
|
|
assert.sameMembers(keys, ['GGGGGGGG', 'HHHHHHHH', 'JJJJJJJJ']);
|
|
|
|
// Unknown search condition, search operator, item field, and item type
|
|
//
|
|
// Missing parent collection and item collection don't throw errors because they don't
|
|
// indicate a guaranteed problem with the client
|
|
assert.equal(spy.callCount, 4);
|
|
});
|
|
|
|
it("shouldn't show CR window if remote data contains unknown field", async function () {
|
|
({ engine, client, caller } = await setup({
|
|
stopOnError: false
|
|
}));
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
library.libraryVersion = 1;
|
|
await library.saveTx();
|
|
|
|
var item = createUnsavedDataObject('item', { title: 'a' });
|
|
item.version = 1;
|
|
await item.saveTx();
|
|
|
|
var json = item.toResponseJSON();
|
|
json.data.title = 'b';
|
|
json.data.invalidField = 'abcd';
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 2
|
|
};
|
|
server.respond(function (req) {
|
|
// Return 412, because item has changed remotely
|
|
if (req.method == "POST") {
|
|
if (!req.url.startsWith(baseURL + "users/1/items")) {
|
|
throw new Error("Unexpected POST");
|
|
}
|
|
req.respond(
|
|
412,
|
|
{
|
|
"Last-Modified-Version": 2
|
|
},
|
|
""
|
|
);
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/settings?since=1",
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/collections?format=versions&since=1",
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/searches?format=versions&since=1",
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items/top?format=versions&since=1&includeTrashed=1",
|
|
status: 200,
|
|
headers,
|
|
json: {
|
|
[item.key]: 2
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items?format=versions&since=1&includeTrashed=1",
|
|
status: 200,
|
|
headers,
|
|
json: {
|
|
[item.key]: 2
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?itemKey=${item.key}&includeTrashed=1`,
|
|
status: 200,
|
|
headers,
|
|
json: [json]
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/deleted?since=1",
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
var spy = sinon.spy(engine, "onError");
|
|
await engine.start();
|
|
|
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
// Library version should have been updated
|
|
assert.equal(Zotero.Libraries.getVersion(userLibraryID), 2);
|
|
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', userLibraryID);
|
|
assert.sameMembers(keys, [item.key]);
|
|
});
|
|
|
|
it("should delay on second upload conflict", async function () {
|
|
var library = Zotero.Libraries.userLibrary;
|
|
library.libraryVersion = 5;
|
|
await library.saveTx();
|
|
({ engine, client, caller } = await setup());
|
|
|
|
// Try to upload, get 412
|
|
// Download, get new version number
|
|
// Try to upload again, get 412
|
|
// Delay
|
|
// Download, get new version number
|
|
// Upload, get 200
|
|
|
|
var item = await createDataObject('item');
|
|
|
|
var lastLibraryVersion = 5;
|
|
var postCalls = 0;
|
|
var settingsCalls = 0;
|
|
var t;
|
|
server.respond(function (req) {
|
|
// On first and second upload attempts, return 412
|
|
if (req.method == "POST") {
|
|
if (!req.url.startsWith(baseURL + "users/1/items")) {
|
|
throw new Error("Unexpected POST");
|
|
}
|
|
postCalls++;
|
|
// 1st and 2nd requests
|
|
if (postCalls == 1 || postCalls == 2) {
|
|
req.respond(
|
|
412,
|
|
{
|
|
"Last-Modified-Version": ++lastLibraryVersion
|
|
},
|
|
""
|
|
);
|
|
}
|
|
// 3rd request
|
|
else {
|
|
let json = item.toResponseJSON();
|
|
json.version = ++lastLibraryVersion;
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Last-Modified-Version": json.version
|
|
},
|
|
JSON.stringify({
|
|
successful: {
|
|
"0": json
|
|
},
|
|
unchanged: {},
|
|
failed: {}
|
|
})
|
|
);
|
|
}
|
|
t = new Date();
|
|
return;
|
|
}
|
|
if (req.method == "GET") {
|
|
if (req.url.startsWith(baseURL + "users/1/settings")) {
|
|
settingsCalls++;
|
|
if (settingsCalls == 2) {
|
|
assert.isAtLeast(new Date() - t, 75);
|
|
}
|
|
}
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Last-Modified-Version": lastLibraryVersion
|
|
},
|
|
JSON.stringify({})
|
|
);
|
|
t = new Date();
|
|
return;
|
|
}
|
|
});
|
|
|
|
Zotero.Sync.Data.conflictDelayIntervals = [75, 70000];
|
|
await engine.start();
|
|
|
|
assert.equal(postCalls, 3);
|
|
assert.equal(settingsCalls, 2);
|
|
assert.isTrue(item.synced);
|
|
assert.equal(library.libraryVersion, lastLibraryVersion);
|
|
});
|
|
})
|
|
|
|
describe("#_startDownload()", function () {
|
|
it("shouldn't redownload objects that are already up to date", async function () {
|
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
|
//yield Zotero.Libraries.setVersion(userLibraryID, 5);
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var objects = {};
|
|
for (let type of Zotero.DataObjectUtilities.getTypes()) {
|
|
let obj = objects[type] = createUnsavedDataObject(type);
|
|
obj.version = 5;
|
|
obj.synced = true;
|
|
await obj.saveTx({ skipSyncedUpdate: true });
|
|
|
|
await Zotero.Sync.Data.Local.saveCacheObjects(
|
|
type,
|
|
userLibraryID,
|
|
[
|
|
{
|
|
key: obj.key,
|
|
version: obj.version,
|
|
data: obj.toJSON()
|
|
}
|
|
]
|
|
);
|
|
}
|
|
|
|
var json;
|
|
var headers = {
|
|
"Last-Modified-Version": 5
|
|
};
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/settings",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
json = {};
|
|
json[objects.collection.key] = 5;
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/collections?format=versions",
|
|
status: 200,
|
|
headers: headers,
|
|
json: json
|
|
});
|
|
json = {};
|
|
json[objects.search.key] = 5;
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/searches?format=versions",
|
|
status: 200,
|
|
headers: headers,
|
|
json: json
|
|
});
|
|
json = {};
|
|
json[objects.item.key] = 5;
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items/top?format=versions&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: json
|
|
});
|
|
json = {};
|
|
json[objects.item.key] = 5;
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items?format=versions&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: json
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/deleted?since=0",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
|
|
await engine._startDownload();
|
|
})
|
|
|
|
it("should apply remote deletions", async function () {
|
|
var library = Zotero.Libraries.userLibrary;
|
|
library.libraryVersion = 5;
|
|
await library.saveTx();
|
|
({ engine, client, caller } = await setup());
|
|
|
|
// Create objects and mark them as synced
|
|
await Zotero.SyncedSettings.set(
|
|
library.id, 'tagColors', [{name: 'A', color: '#CC66CC'}], 1, true
|
|
);
|
|
var collection = createUnsavedDataObject('collection');
|
|
collection.synced = true;
|
|
var collectionID = await collection.saveTx({ skipSyncedUpdate: true });
|
|
var collectionKey = collection.key;
|
|
var search = createUnsavedDataObject('search');
|
|
search.synced = true;
|
|
var searchID = await search.saveTx({ skipSyncedUpdate: true });
|
|
var searchKey = search.key;
|
|
var item = createUnsavedDataObject('item');
|
|
item.synced = true;
|
|
var itemID = await item.saveTx({ skipSyncedUpdate: true });
|
|
var itemKey = item.key;
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 6
|
|
};
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/settings?since=5",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/collections?format=versions&since=5",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/searches?format=versions&since=5",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items?format=versions&since=5&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items/top?format=versions&since=5&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/deleted?since=5",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
settings: ['tagColors'],
|
|
collections: [collection.key],
|
|
searches: [search.key],
|
|
items: [item.key]
|
|
}
|
|
});
|
|
await engine._startDownload();
|
|
|
|
// Make sure objects were deleted
|
|
assert.isNull(Zotero.SyncedSettings.get(library.id, 'tagColors'));
|
|
assert.isFalse(Zotero.Collections.exists(collectionID));
|
|
assert.isFalse(Zotero.Searches.exists(searchID));
|
|
assert.isFalse(Zotero.Items.exists(itemID));
|
|
|
|
// Make sure objects weren't added to sync delete log
|
|
assert.isFalse(await Zotero.Sync.Data.Local.getDateDeleted(
|
|
'setting', library.id, 'tagColors'
|
|
));
|
|
assert.isFalse(await Zotero.Sync.Data.Local.getDateDeleted(
|
|
'collection', library.id, collectionKey
|
|
));
|
|
assert.isFalse(await Zotero.Sync.Data.Local.getDateDeleted(
|
|
'search', library.id, searchKey
|
|
));
|
|
assert.isFalse(await Zotero.Sync.Data.Local.getDateDeleted(
|
|
'item', library.id, itemKey
|
|
));
|
|
})
|
|
|
|
it("should ignore remote deletions for non-item objects if local objects changed", async function () {
|
|
var library = Zotero.Libraries.userLibrary;
|
|
library.libraryVersion = 5;
|
|
await library.saveTx();
|
|
({ engine, client, caller } = await setup());
|
|
|
|
// Create objects marked as unsynced
|
|
await Zotero.SyncedSettings.set(
|
|
library.id, 'tagColors', [{name: 'A', color: '#CC66CC'}]
|
|
);
|
|
var collection = createUnsavedDataObject('collection');
|
|
var collectionID = await collection.saveTx();
|
|
var collectionKey = collection.key;
|
|
var search = createUnsavedDataObject('search');
|
|
var searchID = await search.saveTx();
|
|
var searchKey = search.key;
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 6
|
|
};
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/settings?since=5",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/collections?format=versions&since=5",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/searches?format=versions&since=5",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items/top?format=versions&since=5&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items?format=versions&since=5&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/deleted?since=5",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
settings: ['tagColors'],
|
|
collections: [collection.key],
|
|
searches: [search.key],
|
|
items: []
|
|
}
|
|
});
|
|
await engine._startDownload();
|
|
|
|
// Make sure objects weren't deleted
|
|
assert.ok(Zotero.SyncedSettings.get(library.id, 'tagColors'));
|
|
assert.ok(Zotero.Collections.exists(collectionID));
|
|
assert.ok(Zotero.Searches.exists(searchID));
|
|
})
|
|
|
|
it("should show conflict resolution window for conflicting remote deletions", async function () {
|
|
var library = Zotero.Libraries.userLibrary;
|
|
library.libraryVersion = 5;
|
|
await library.saveTx();
|
|
({ engine, client, caller } = await setup());
|
|
|
|
// Create local unsynced items
|
|
var item = createUnsavedDataObject('item');
|
|
item.setField('title', 'A');
|
|
item.synced = false;
|
|
var itemID1 = await item.saveTx({ skipSyncedUpdate: true });
|
|
var itemKey1 = item.key;
|
|
|
|
item = createUnsavedDataObject('item');
|
|
item.setField('title', 'B');
|
|
item.synced = false;
|
|
var itemID2 = await item.saveTx({ skipSyncedUpdate: true });
|
|
var itemKey2 = item.key;
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 6
|
|
};
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/settings?since=5",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/collections?format=versions&since=5",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/searches?format=versions&since=5",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items/top?format=versions&since=5&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items?format=versions&since=5&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/deleted?since=5",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
settings: [],
|
|
collections: [],
|
|
searches: [],
|
|
items: [itemKey1, itemKey2]
|
|
}
|
|
});
|
|
|
|
var crPromise = 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 (ignore remote deletion)
|
|
assert.equal(mergeGroup.leftPane.getAttribute('selected'), 'true');
|
|
wizard.getButton('finish').click();
|
|
})
|
|
await engine._startDownload();
|
|
await crPromise;
|
|
|
|
assert.isFalse(Zotero.Items.exists(itemID1));
|
|
assert.isTrue(Zotero.Items.exists(itemID2));
|
|
})
|
|
|
|
|
|
it("should handle new remote item referencing locally deleted collection", async function () {
|
|
var lastLibraryVersion = 5;
|
|
var newLibraryVersion = 6;
|
|
var library = Zotero.Libraries.userLibrary;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
({ engine, client, caller } = await setup());
|
|
|
|
// Create local deleted collection
|
|
var collection = await createDataObject('collection');
|
|
var collectionKey = collection.key;
|
|
await collection.eraseTx();
|
|
var itemKey = "AAAAAAAA";
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": newLibraryVersion
|
|
};
|
|
setDefaultResponses({
|
|
lastLibraryVersion,
|
|
libraryVersion: newLibraryVersion
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?format=versions&since=${lastLibraryVersion}&includeTrashed=1`,
|
|
status: 200,
|
|
headers,
|
|
json: {
|
|
[itemKey]: newLibraryVersion
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?itemKey=${itemKey}&includeTrashed=1`,
|
|
status: 200,
|
|
headers,
|
|
json: [
|
|
makeItemJSON({
|
|
key: itemKey,
|
|
version: newLibraryVersion,
|
|
itemType: "book",
|
|
collections: [collectionKey]
|
|
})
|
|
]
|
|
});
|
|
|
|
await engine._startDownload();
|
|
|
|
// Item should be skipped and added to queue, which will allow collection deletion to upload
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', library.id);
|
|
assert.sameMembers(keys, [itemKey]);
|
|
|
|
// Collection should not be in sync queue
|
|
assert.lengthOf(
|
|
await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('collection', library.id), 0
|
|
);
|
|
});
|
|
|
|
|
|
it("should handle new remote item referencing locally missing collection", async function () {
|
|
var lastLibraryVersion = 5;
|
|
var newLibraryVersion = 6;
|
|
var library = Zotero.Libraries.userLibrary;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var collectionKey = 'AAAAAAAA';
|
|
var itemKey = 'BBBBBBBB'
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": newLibraryVersion
|
|
};
|
|
setDefaultResponses({
|
|
lastLibraryVersion,
|
|
libraryVersion: newLibraryVersion
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?format=versions&since=${lastLibraryVersion}&includeTrashed=1`,
|
|
status: 200,
|
|
headers,
|
|
json: {
|
|
[itemKey]: newLibraryVersion
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?itemKey=${itemKey}&includeTrashed=1`,
|
|
status: 200,
|
|
headers,
|
|
json: [
|
|
makeItemJSON({
|
|
key: itemKey,
|
|
version: newLibraryVersion,
|
|
itemType: "book",
|
|
collections: [collectionKey]
|
|
})
|
|
]
|
|
});
|
|
|
|
await engine._startDownload();
|
|
|
|
// Item should be skipped and added to queue
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', library.id);
|
|
assert.sameMembers(keys, [itemKey]);
|
|
|
|
// Collection should be in queue
|
|
assert.sameMembers(
|
|
await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('collection', library.id),
|
|
[collectionKey]
|
|
);
|
|
});
|
|
|
|
|
|
it("should handle conflict with remote item referencing deleted local collection", async function () {
|
|
var lastLibraryVersion = 5;
|
|
var newLibraryVersion = 6;
|
|
var library = Zotero.Libraries.userLibrary;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
({ engine, client, caller } = await setup());
|
|
|
|
// Create local deleted collection and item
|
|
var collection = await createDataObject('collection');
|
|
var collectionKey = collection.key;
|
|
await collection.eraseTx();
|
|
var item = await createDataObject('item');
|
|
var itemResponseJSON = item.toResponseJSON();
|
|
// Add collection to remote item
|
|
itemResponseJSON.data.collections = [collectionKey];
|
|
var itemKey = item.key;
|
|
await item.eraseTx();
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": newLibraryVersion
|
|
};
|
|
setDefaultResponses({
|
|
lastLibraryVersion,
|
|
libraryVersion: newLibraryVersion
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?format=versions&since=${lastLibraryVersion}&includeTrashed=1`,
|
|
status: 200,
|
|
headers,
|
|
json: {
|
|
[itemKey]: newLibraryVersion
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?itemKey=${itemKey}&includeTrashed=1`,
|
|
status: 200,
|
|
headers,
|
|
json: [itemResponseJSON]
|
|
});
|
|
|
|
await engine._startDownload();
|
|
|
|
// Item should be skipped and added to queue, which will allow collection deletion to upload
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', library.id);
|
|
assert.sameMembers(keys, [itemKey]);
|
|
});
|
|
|
|
|
|
it("should handle cancellation of conflict resolution window", async function () {
|
|
var library = Zotero.Libraries.userLibrary;
|
|
library.libraryVersion = 5;
|
|
await library.saveTx();
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var item = await createDataObject('item');
|
|
var itemID = await item.saveTx();
|
|
var itemKey = item.key;
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 6
|
|
};
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/settings?since=5",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/collections?format=versions&since=5",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/searches?format=versions&since=5",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items/top?format=versions&since=5&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
AAAAAAAA: 6,
|
|
[itemKey]: 6
|
|
}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?itemKey=AAAAAAAA%2C${itemKey}&includeTrashed=1`,
|
|
status: 200,
|
|
headers: headers,
|
|
json: [
|
|
makeItemJSON({
|
|
key: "AAAAAAAA",
|
|
version: 6,
|
|
itemType: "book",
|
|
title: "B"
|
|
}),
|
|
makeItemJSON({
|
|
key: itemKey,
|
|
version: 6,
|
|
itemType: "book",
|
|
title: "B"
|
|
})
|
|
]
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items?format=versions&since=5&includeTrashed=1",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/deleted?since=5",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
settings: [],
|
|
collections: [],
|
|
searches: [],
|
|
items: []
|
|
}
|
|
});
|
|
|
|
var crPromise = waitForWindow('chrome://zotero/content/merge.xhtml', function (dialog) {
|
|
var doc = dialog.document;
|
|
var wizard = doc.querySelector('wizard');
|
|
wizard.getButton('cancel').click();
|
|
})
|
|
var e = await getPromiseError(engine._startDownload());
|
|
await crPromise
|
|
assert.isTrue(e instanceof Zotero.Sync.UserCancelledException);
|
|
|
|
// Non-conflicted item should be saved
|
|
assert.ok(Zotero.Items.getIDFromLibraryAndKey(library.id, "AAAAAAAA"));
|
|
|
|
// Conflicted item should be skipped and in queue
|
|
assert.isFalse(Zotero.Items.exists(itemID));
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', library.id);
|
|
assert.sameMembers(keys, [itemKey]);
|
|
|
|
// Library version should not have advanced
|
|
assert.equal(library.libraryVersion, 5);
|
|
});
|
|
|
|
|
|
/**
|
|
* The CR window for remote deletions is triggered separately, so test separately
|
|
*/
|
|
it("should handle cancellation of remote deletion conflict resolution window", async function () {
|
|
var library = Zotero.Libraries.userLibrary;
|
|
library.libraryVersion = 5;
|
|
await library.saveTx();
|
|
({ engine, client, caller } = await setup());
|
|
|
|
// Create local unsynced items
|
|
var item = createUnsavedDataObject('item');
|
|
item.setField('title', 'A');
|
|
item.synced = false;
|
|
var itemID1 = await item.saveTx();
|
|
var itemKey1 = item.key;
|
|
|
|
item = createUnsavedDataObject('item');
|
|
item.setField('title', 'B');
|
|
item.synced = false;
|
|
var itemID2 = await item.saveTx();
|
|
var itemKey2 = item.key;
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 6
|
|
};
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/settings?since=5",
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/collections?format=versions&since=5",
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/searches?format=versions&since=5",
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items/top?format=versions&since=5&includeTrashed=1",
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/items?format=versions&since=5&includeTrashed=1",
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/deleted?since=5",
|
|
status: 200,
|
|
headers,
|
|
json: {
|
|
settings: [],
|
|
collections: [],
|
|
searches: [],
|
|
items: [itemKey1, itemKey2]
|
|
}
|
|
});
|
|
|
|
var crPromise = waitForWindow('chrome://zotero/content/merge.xhtml', function (dialog) {
|
|
var doc = dialog.document;
|
|
var wizard = doc.querySelector('wizard');
|
|
wizard.getButton('cancel').click();
|
|
})
|
|
var e = await getPromiseError(engine._startDownload());
|
|
await crPromise;
|
|
assert.isTrue(e instanceof Zotero.Sync.UserCancelledException);
|
|
|
|
// Conflicted items should still exists
|
|
assert.isTrue(Zotero.Items.exists(itemID1));
|
|
assert.isTrue(Zotero.Items.exists(itemID2));
|
|
|
|
// Library version should not have advanced
|
|
assert.equal(library.libraryVersion, 5);
|
|
});
|
|
|
|
it("should restart if remote library version changes", async function () {
|
|
var library = Zotero.Libraries.userLibrary;
|
|
library.libraryVersion = 5;
|
|
await library.saveTx();
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var lastLibraryVersion = 5;
|
|
var calls = 0;
|
|
var t;
|
|
server.respond(function (req) {
|
|
if (req.url.startsWith(baseURL + "users/1/settings")) {
|
|
calls++;
|
|
if (calls == 2) {
|
|
assert.isAtLeast(new Date() - t, 50);
|
|
}
|
|
t = new Date();
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Last-Modified-Version": ++lastLibraryVersion
|
|
},
|
|
JSON.stringify({})
|
|
);
|
|
return;
|
|
}
|
|
else if (req.url.startsWith(baseURL + "users/1/searches")) {
|
|
if (calls == 1) {
|
|
t = new Date();
|
|
req.respond(
|
|
200,
|
|
{
|
|
// On the first pass, return a later library version to simulate data
|
|
// being updated by a concurrent upload
|
|
"Last-Modified-Version": lastLibraryVersion + 1
|
|
},
|
|
JSON.stringify([])
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
else if (req.url.startsWith(baseURL + "users/1/items")) {
|
|
// Since /searches is called before /items and it should cause a reset,
|
|
// /items shouldn't be called until the second pass
|
|
if (calls < 1) {
|
|
throw new Error("/users/1/items called in first pass");
|
|
}
|
|
}
|
|
|
|
t = new Date();
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Last-Modified-Version": lastLibraryVersion
|
|
},
|
|
JSON.stringify([])
|
|
);
|
|
});
|
|
|
|
Zotero.Sync.Data.conflictDelayIntervals = [50, 70000];
|
|
await engine._startDownload();
|
|
|
|
assert.equal(calls, 2);
|
|
assert.equal(library.libraryVersion, lastLibraryVersion);
|
|
});
|
|
});
|
|
|
|
|
|
describe("#_downloadUpdatedObjects()", function () {
|
|
it("should include objects in sync queue", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
var objectType = 'collection';
|
|
var objectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType);
|
|
await Zotero.Sync.Data.Local.addObjectsToSyncQueue(
|
|
objectType, libraryID, ["BBBBBBBB", "CCCCCCCC"]
|
|
);
|
|
await Zotero.DB.queryAsync(
|
|
"UPDATE syncQueue SET lastCheck=lastCheck-3600 "
|
|
+ "WHERE syncObjectTypeID=? AND libraryID=? AND key IN (?, ?)",
|
|
[objectTypeID, libraryID, 'BBBBBBBB', 'CCCCCCCC']
|
|
);
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 5
|
|
};
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/collections?format=versions&since=1",
|
|
status: 200,
|
|
headers,
|
|
json: {
|
|
AAAAAAAA: 5,
|
|
BBBBBBBB: 5
|
|
}
|
|
});
|
|
|
|
var stub = sinon.stub(engine, "_downloadObjects");
|
|
|
|
await engine._downloadUpdatedObjects(objectType, 1, 5);
|
|
|
|
assert.ok(stub.calledWith("collection", ["AAAAAAAA", "BBBBBBBB", "CCCCCCCC"]));
|
|
stub.restore();
|
|
});
|
|
});
|
|
|
|
|
|
describe("#_downloadObjects()", function () {
|
|
it("should remove object from sync queue if missing from response", async function () {
|
|
({ engine, client, caller } = await setup({
|
|
stopOnError: false
|
|
}));
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
var objectType = 'collection';
|
|
var objectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType);
|
|
await Zotero.Sync.Data.Local.addObjectsToSyncQueue(
|
|
objectType, libraryID, ["BBBBBBBB", "CCCCCCCC"]
|
|
);
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 5
|
|
};
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/collections?collectionKey=AAAAAAAA%2CBBBBBBBB%2CCCCCCCCC",
|
|
status: 200,
|
|
headers,
|
|
json: [
|
|
makeCollectionJSON({
|
|
key: "AAAAAAAA",
|
|
version: 5,
|
|
name: "A"
|
|
}),
|
|
makeCollectionJSON({
|
|
key: "BBBBBBBB",
|
|
version: 5
|
|
// Missing 'name', which causes a save error
|
|
})
|
|
]
|
|
});
|
|
await engine._downloadObjects(objectType, ["AAAAAAAA", "BBBBBBBB", "CCCCCCCC"]);
|
|
|
|
// Missing object should have been removed, but invalid object should remain
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue(objectType, libraryID);
|
|
assert.sameMembers(keys, ['BBBBBBBB']);
|
|
});
|
|
|
|
|
|
it("should add items that exist remotely in a locally deleted, remotely modified collection back to collection", async function () {
|
|
({ engine, client, caller } = await setup({
|
|
stopOnError: false
|
|
}));
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var collection = await createDataObject('collection');
|
|
var collectionKey = collection.key;
|
|
await collection.eraseTx();
|
|
var item1 = await createDataObject('item');
|
|
var item2 = await createDataObject('item', { deleted: true });
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 5
|
|
};
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/collections?collectionKey=${collectionKey}`,
|
|
status: 200,
|
|
headers,
|
|
json: [
|
|
makeCollectionJSON({
|
|
key: collectionKey,
|
|
version: 5,
|
|
name: "A"
|
|
})
|
|
]
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/collections/${collectionKey}/items/top?format=keys`,
|
|
status: 200,
|
|
headers,
|
|
text: item1.key + "\n" + item2.key + "\n"
|
|
});
|
|
await engine._downloadObjects('collection', [collectionKey]);
|
|
|
|
var collection = Zotero.Collections.getByLibraryAndKey(libraryID, collectionKey);
|
|
assert.sameMembers(collection.getChildItems(true), [item1.id, item2.id]);
|
|
// Item should be removed from trash
|
|
assert.isFalse(item2.deleted);
|
|
});
|
|
|
|
|
|
it("should add locally deleted items that exist remotely in a locally deleted, remotely modified collection to sync queue and remove from delete log", async function () {
|
|
({ engine, client, caller } = await setup({
|
|
stopOnError: false
|
|
}));
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var collection = await createDataObject('collection');
|
|
var collectionKey = collection.key;
|
|
await collection.eraseTx();
|
|
var item = await createDataObject('item');
|
|
await item.eraseTx();
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 5
|
|
};
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/collections?collectionKey=${collectionKey}`,
|
|
status: 200,
|
|
headers,
|
|
json: [
|
|
makeCollectionJSON({
|
|
key: collectionKey,
|
|
version: 5,
|
|
name: "A"
|
|
})
|
|
]
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/collections/${collectionKey}/items/top?format=keys`,
|
|
status: 200,
|
|
headers,
|
|
text: item.key + "\n"
|
|
});
|
|
await engine._downloadObjects('collection', [collectionKey]);
|
|
|
|
var collection = Zotero.Collections.getByLibraryAndKey(libraryID, collectionKey);
|
|
|
|
assert.sameMembers(
|
|
await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID),
|
|
[item.key]
|
|
);
|
|
assert.isFalse(
|
|
await Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, item.key)
|
|
);
|
|
});
|
|
|
|
|
|
it("should add object from failed download request to sync queue", async function () {
|
|
({ engine, client, caller } = await setup({
|
|
stopOnError: false
|
|
}));
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var item = await createDataObject('item');
|
|
var itemKey = item.key;
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 5
|
|
};
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?itemKey=${itemKey}&includeTrashed=1`,
|
|
status: 0,
|
|
headers,
|
|
body: ""
|
|
});
|
|
await engine._downloadObjects('item', [itemKey]);
|
|
|
|
assert.sameMembers(
|
|
await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID),
|
|
[itemKey]
|
|
);
|
|
});
|
|
});
|
|
|
|
|
|
describe("#_startUpload()", function () {
|
|
it("shouldn't upload unsynced objects if present in sync queue", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
var objectType = 'item';
|
|
var obj = await createDataObject(objectType);
|
|
await Zotero.Sync.Data.Local.addObjectsToSyncQueue(objectType, libraryID, [obj.key]);
|
|
var result = await engine._startUpload();
|
|
assert.equal(result, engine.UPLOAD_RESULT_NOTHING_TO_UPLOAD);
|
|
});
|
|
|
|
|
|
it("should allow applying remotely saved version to local annotation in group library", async function () {
|
|
var group = await createGroup({
|
|
libraryVersion: 5
|
|
});
|
|
var libraryID = group.libraryID;
|
|
({ engine, client, caller } = await setup({ libraryID }));
|
|
|
|
var createdByUserID = 2352512;
|
|
await Zotero.Users.setName(createdByUserID, 'user');
|
|
|
|
var attachment = await importFileAttachment('test.pdf', { libraryID });
|
|
attachment.synced = true;
|
|
await attachment.saveTx();
|
|
var annotation = await createAnnotation('highlight', attachment);
|
|
annotation.createdByUserID = createdByUserID;
|
|
await annotation.saveTx({
|
|
skipEditCheck: true
|
|
});
|
|
var responseJSON = annotation.toResponseJSON();
|
|
responseJSON.version = 10;
|
|
responseJSON.data.version = 10;
|
|
var newComment = 'new comment';
|
|
responseJSON.data.annotationComment = newComment;
|
|
|
|
let response = {
|
|
successful: {
|
|
"0": responseJSON
|
|
},
|
|
unchanged: {},
|
|
failed: {}
|
|
};
|
|
setResponse({
|
|
method: "POST",
|
|
url: `groups/${group.id}/items`,
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": 10
|
|
},
|
|
json: response
|
|
})
|
|
|
|
var result = await engine._startUpload();
|
|
|
|
assert.equal(result, engine.UPLOAD_RESULT_SUCCESS);
|
|
assert.equal(annotation.version, 10);
|
|
assert.equal(annotation.annotationComment, newComment);
|
|
});
|
|
|
|
|
|
it("should prompt to reset library on 403 write response and reset on accept", async function () {
|
|
var group = await createGroup({
|
|
libraryVersion: 5
|
|
});
|
|
var libraryID = group.libraryID;
|
|
({ engine, client, caller } = await setup({ libraryID }));
|
|
|
|
var item = createUnsavedDataObject('item');
|
|
item.libraryID = libraryID;
|
|
item.setField('title', 'A');
|
|
item.synced = false;
|
|
var itemID = await item.saveTx();
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 5
|
|
};
|
|
setResponse({
|
|
method: "POST",
|
|
url: `groups/${group.id}/items`,
|
|
status: 403,
|
|
headers,
|
|
text: ""
|
|
})
|
|
|
|
var promise = waitForDialog(function (dialog) {
|
|
var text = dialog.document.documentElement.textContent;
|
|
assert.include(text, group.name);
|
|
});
|
|
|
|
var result = await engine._startUpload();
|
|
assert.equal(result, engine.UPLOAD_RESULT_RESTART);
|
|
|
|
assert.isFalse(Zotero.Items.exists(itemID));
|
|
|
|
// Library version should have been reset to trigger full sync
|
|
assert.equal(group.libraryVersion, -1);
|
|
});
|
|
|
|
|
|
it("should prompt to reset library on 403 write response and skip on cancel", async function () {
|
|
var group = await createGroup({
|
|
libraryVersion: 5
|
|
});
|
|
var libraryID = group.libraryID;
|
|
({ engine, client, caller } = await setup({ libraryID }));
|
|
|
|
var item = createUnsavedDataObject('item');
|
|
item.libraryID = libraryID;
|
|
item.setField('title', 'A');
|
|
item.synced = false;
|
|
var itemID = await item.saveTx();
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 5
|
|
};
|
|
setResponse({
|
|
method: "POST",
|
|
url: `groups/${group.id}/items`,
|
|
status: 403,
|
|
headers,
|
|
text: ""
|
|
})
|
|
|
|
var promise = waitForDialog(function (dialog) {
|
|
var text = dialog.document.documentElement.textContent;
|
|
assert.include(text, group.name);
|
|
}, "cancel");
|
|
|
|
var result = await engine._startUpload();
|
|
assert.equal(result, engine.UPLOAD_RESULT_CANCEL);
|
|
|
|
assert.isTrue(Zotero.Items.exists(itemID));
|
|
|
|
// Library version shouldn't have changed
|
|
assert.equal(group.libraryVersion, 5);
|
|
});
|
|
|
|
|
|
it("should trigger full sync on object conflict", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
var lastLibraryVersion = 5;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
|
|
var item = createUnsavedDataObject('item');
|
|
item.version = lastLibraryVersion;
|
|
await item.saveTx();
|
|
|
|
setResponse({
|
|
method: "POST",
|
|
url: "users/1/items",
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": lastLibraryVersion
|
|
},
|
|
json: {
|
|
successful: {},
|
|
unchanged: {},
|
|
failed: {
|
|
"0": {
|
|
"code": 412,
|
|
"message": `Item doesn't exist (expected version ${lastLibraryVersion}; `
|
|
+ "use 0 instead)"
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
var result = await engine._startUpload();
|
|
assert.equal(result, engine.UPLOAD_RESULT_OBJECT_CONFLICT);
|
|
});
|
|
|
|
|
|
// Note: This shouldn't be necessary, since collections are sorted top-down before uploading
|
|
it("should mark local collection as unsynced if it doesn't exist when uploading collection", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
var lastLibraryVersion = 5;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
|
|
var collection1 = createUnsavedDataObject('collection');
|
|
// Set the collection as synced (though this shouldn't happen)
|
|
collection1.synced = true;
|
|
await collection1.saveTx();
|
|
var collection2 = await createDataObject('collection', { collections: [collection1.id] });
|
|
|
|
var called = 0;
|
|
server.respond(function (req) {
|
|
let requestJSON = JSON.parse(req.requestBody);
|
|
|
|
if (called == 0) {
|
|
assert.lengthOf(requestJSON, 1);
|
|
assert.equal(requestJSON[0].key, collection2.key);
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Last-Modified-Version": lastLibraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {},
|
|
unchanged: {},
|
|
failed: {
|
|
0: {
|
|
code: 409,
|
|
message: `Parent collection ${collection1.key} doesn't exist`,
|
|
data: {
|
|
collection: collection1.key
|
|
}
|
|
}
|
|
}
|
|
})
|
|
);
|
|
}
|
|
called++;
|
|
});
|
|
|
|
var e = await getPromiseError(engine._startUpload());
|
|
assert.ok(e);
|
|
assert.isFalse(collection1.synced);
|
|
});
|
|
|
|
|
|
it("should mark local collection as unsynced if it doesn't exist when uploading item", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
var lastLibraryVersion = 5;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
|
|
var collection = createUnsavedDataObject('collection');
|
|
// Set the collection as synced (though this shouldn't happen)
|
|
collection.synced = true;
|
|
await collection.saveTx();
|
|
var item = await createDataObject('item', { collections: [collection.id] });
|
|
|
|
var called = 0;
|
|
server.respond(function (req) {
|
|
let requestJSON = JSON.parse(req.requestBody);
|
|
|
|
if (called == 0) {
|
|
assert.lengthOf(requestJSON, 1);
|
|
assert.equal(requestJSON[0].key, item.key);
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Last-Modified-Version": lastLibraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {},
|
|
unchanged: {},
|
|
failed: {
|
|
0: {
|
|
code: 409,
|
|
message: `Collection ${collection.key} doesn't exist`,
|
|
data: {
|
|
collection: collection.key
|
|
}
|
|
}
|
|
}
|
|
})
|
|
);
|
|
}
|
|
called++;
|
|
});
|
|
|
|
var e = await getPromiseError(engine._startUpload());
|
|
assert.ok(e);
|
|
assert.isFalse(collection.synced);
|
|
});
|
|
|
|
|
|
it("should mark local parent item as unsynced if it doesn't exist when uploading child", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
var lastLibraryVersion = 5;
|
|
library.libraryVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
|
|
var item = createUnsavedDataObject('item');
|
|
// Set the parent item as synced (though this shouldn't happen)
|
|
item.synced = true;
|
|
await item.saveTx();
|
|
var note = await createDataObject('item', { itemType: 'note', parentID: item.id });
|
|
|
|
var called = 0;
|
|
server.respond(function (req) {
|
|
let requestJSON = JSON.parse(req.requestBody);
|
|
|
|
if (called == 0) {
|
|
assert.lengthOf(requestJSON, 1);
|
|
assert.equal(requestJSON[0].key, note.key);
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Last-Modified-Version": lastLibraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {},
|
|
unchanged: {},
|
|
failed: {
|
|
0: {
|
|
code: 409,
|
|
message: `Parent item ${item.key} doesn't exist`,
|
|
data: {
|
|
parentItem: item.key
|
|
}
|
|
}
|
|
}
|
|
})
|
|
);
|
|
}
|
|
else if (called == 1) {
|
|
assert.lengthOf(requestJSON, 2);
|
|
assert.sameMembers(requestJSON.map(o => o.key), [item.key, note.key]);
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Last-Modified-Version": ++lastLibraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {
|
|
0: item.toResponseJSON(),
|
|
1: note.toResponseJSON()
|
|
},
|
|
unchanged: {},
|
|
failed: {}
|
|
})
|
|
);
|
|
}
|
|
called++;
|
|
});
|
|
|
|
var result = await engine._startUpload();
|
|
assert.equal(result, engine.UPLOAD_RESULT_SUCCESS);
|
|
assert.equal(called, 2);
|
|
});
|
|
|
|
|
|
it("shouldn't retry failed child item if parent item failed during this sync", async function () {
|
|
({ engine, client, caller } = await setup({
|
|
stopOnError: false
|
|
}));
|
|
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
var libraryVersion = 5;
|
|
library.libraryVersion = libraryVersion;
|
|
await library.saveTx();
|
|
|
|
var item1 = await createDataObject('item');
|
|
var item1JSON = item1.toResponseJSON();
|
|
var tag = "A".repeat(300);
|
|
var item2 = await createDataObject('item', { tags: [{ tag }] });
|
|
var note = await createDataObject('item', { itemType: 'note', parentID: item2.id });
|
|
|
|
var called = 0;
|
|
server.respond(function (req) {
|
|
let requestJSON = JSON.parse(req.requestBody);
|
|
if (called == 0) {
|
|
assert.lengthOf(requestJSON, 3);
|
|
assert.equal(requestJSON[0].key, item1.key);
|
|
assert.equal(requestJSON[1].key, item2.key);
|
|
assert.equal(requestJSON[2].key, note.key);
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Last-Modified-Version": ++libraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {
|
|
"0": Object.assign(item1JSON, { version: libraryVersion })
|
|
},
|
|
unchanged: {},
|
|
failed: {
|
|
1: {
|
|
code: 413,
|
|
message: `Tag '${"A".repeat(50)}…' too long`,
|
|
data: {
|
|
tag
|
|
}
|
|
},
|
|
// Normally this would retry, but that might result in a 409
|
|
// without the parent
|
|
2: {
|
|
code: 500,
|
|
message: `An error occurred`
|
|
}
|
|
}
|
|
})
|
|
);
|
|
}
|
|
called++;
|
|
});
|
|
|
|
var spy = sinon.spy(engine, "onError");
|
|
var result = await engine._startUpload();
|
|
assert.equal(result, engine.UPLOAD_RESULT_SUCCESS);
|
|
assert.equal(called, 1);
|
|
assert.equal(spy.callCount, 2);
|
|
});
|
|
|
|
|
|
it("should show file-write-access-lost dialog on 403 for attachment upload in group", async function () {
|
|
var group = await createGroup({
|
|
filesEditable: true
|
|
});
|
|
var libraryID = group.libraryID;
|
|
var libraryVersion = 5;
|
|
group.libraryVersion = libraryVersion;
|
|
await group.saveTx();
|
|
|
|
({ engine, client, caller } = await setup({
|
|
libraryID,
|
|
stopOnError: false
|
|
}));
|
|
|
|
var item1 = await createDataObject('item', { libraryID });
|
|
var item2 = await importFileAttachment(
|
|
'test.png',
|
|
{
|
|
libraryID,
|
|
parentID: item1.id,
|
|
version: 5
|
|
}
|
|
);
|
|
|
|
var called = 0;
|
|
server.respond(function (req) {
|
|
let requestJSON = JSON.parse(req.requestBody);
|
|
if (called == 0) {
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Last-Modified-Version": ++libraryVersion
|
|
},
|
|
JSON.stringify({
|
|
successful: {
|
|
0: item1.toResponseJSON({ version: libraryVersion })
|
|
},
|
|
unchanged: {},
|
|
failed: {
|
|
1: {
|
|
code: 403,
|
|
message: "File editing access denied"
|
|
}
|
|
}
|
|
})
|
|
);
|
|
}
|
|
else if (called == 1 && req.url == baseURL + `groups/${group.id}`) {
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Last-Modified-Version": group.libraryVersion
|
|
},
|
|
JSON.stringify({
|
|
id: group.id,
|
|
version: group.libraryVersion,
|
|
data: {
|
|
id: group.id,
|
|
version: group.libraryVersion,
|
|
name: group.name,
|
|
owner: 10,
|
|
type: "Private",
|
|
description: "",
|
|
url: "",
|
|
libraryEditing: "members",
|
|
libraryReading: "all",
|
|
fileEditing: "admins"
|
|
}
|
|
})
|
|
);
|
|
}
|
|
called++;
|
|
});
|
|
|
|
var dialogShown = false;
|
|
waitForDialog().then(() => dialogShown = true);
|
|
var spy = sinon.spy(engine, "onError");
|
|
var result = await engine._startUpload();
|
|
assert.isTrue(dialogShown);
|
|
assert.equal(result, engine.UPLOAD_RESULT_RESTART);
|
|
assert.equal(called, 2);
|
|
assert.equal(spy.callCount, 0);
|
|
|
|
assert.isFalse(group.filesEditable);
|
|
|
|
assert.ok(Zotero.Items.get(item1.id));
|
|
assert.isFalse(Zotero.Items.get(item2.id));
|
|
});
|
|
});
|
|
|
|
|
|
describe("Conflict Resolution", function () {
|
|
beforeEach(function* () {
|
|
yield Zotero.DB.queryAsync("DELETE FROM syncCache");
|
|
})
|
|
|
|
after(function* () {
|
|
yield Zotero.DB.queryAsync("DELETE FROM syncCache");
|
|
})
|
|
|
|
it("should show conflict resolution window on item conflicts", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
({ engine, client, caller } = await setup());
|
|
var type = 'item';
|
|
var objects = [];
|
|
var values = [];
|
|
var dateAdded = Date.now() - 86400000;
|
|
var responseJSON = [];
|
|
|
|
for (let i = 0; i < 2; i++) {
|
|
values.push({
|
|
left: {},
|
|
right: {}
|
|
});
|
|
|
|
// Create local object
|
|
let obj = objects[i] = await createDataObject(
|
|
type,
|
|
{
|
|
version: 10,
|
|
dateAdded: Zotero.Date.dateToSQL(new Date(dateAdded), true),
|
|
// Set Date Modified values one minute apart to enforce order
|
|
dateModified: Zotero.Date.dateToSQL(
|
|
new Date(dateAdded + (i * 60000)), true
|
|
)
|
|
}
|
|
);
|
|
let jsonData = obj.toJSON();
|
|
jsonData.key = obj.key;
|
|
jsonData.version = 10;
|
|
let json = {
|
|
key: obj.key,
|
|
version: jsonData.version,
|
|
data: jsonData
|
|
};
|
|
// Save original version in cache
|
|
await Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
|
|
|
// Create updated JSON for download
|
|
values[i].right.title = jsonData.title = Zotero.Utilities.randomString();
|
|
values[i].right.version = json.version = jsonData.version = 15;
|
|
responseJSON.push(json);
|
|
|
|
// Modify object locally
|
|
await modifyDataObject(obj, undefined, { skipDateModifiedUpdate: true });
|
|
values[i].left.title = obj.getField('title');
|
|
values[i].left.version = obj.getField('version');
|
|
}
|
|
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?itemKey=${objects.map(o => o.key).join('%2C')}`
|
|
+ `&includeTrashed=1`,
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": 15
|
|
},
|
|
json: responseJSON
|
|
});
|
|
|
|
var crPromise = 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 (remote)
|
|
// Remote version should be selected by default
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
wizard.getButton('next').click();
|
|
|
|
// 2 (local)
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
// Select local object
|
|
mergeGroup.leftPane.click();
|
|
assert.equal(mergeGroup.leftPane.getAttribute('selected'), 'true');
|
|
if (Zotero.isMac) {
|
|
assert.isTrue(wizard.getButton('next').hidden);
|
|
assert.isFalse(wizard.getButton('finish').hidden);
|
|
}
|
|
else {
|
|
// TODO
|
|
}
|
|
wizard.getButton('finish').click();
|
|
})
|
|
await engine._downloadObjects('item', objects.map(o => o.key));
|
|
await crPromise;
|
|
|
|
assert.equal(objects[0].getField('title'), values[0].right.title);
|
|
assert.equal(objects[1].getField('title'), values[1].left.title);
|
|
assert.equal(objects[0].getField('version'), values[0].right.version);
|
|
assert.equal(objects[1].getField('version'), values[1].right.version);
|
|
|
|
// Cache versions should match remote
|
|
for (let i = 0; i < 2; i++) {
|
|
let cacheJSON = await Zotero.Sync.Data.Local.getCacheObject(
|
|
'item', libraryID, objects[i].key, values[i].right.version
|
|
);
|
|
assert.propertyVal(cacheJSON, 'version', values[i].right.version);
|
|
assert.nestedPropertyVal(cacheJSON, 'data.title', values[i].right.title);
|
|
}
|
|
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
|
assert.lengthOf(keys, 0);
|
|
});
|
|
|
|
it("should show conflict resolution window on note conflicts", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
({ engine, client, caller } = await setup());
|
|
var type = 'item';
|
|
var objects = [];
|
|
var values = [];
|
|
var dateAdded = Date.now() - 86400000;
|
|
var responseJSON = [];
|
|
|
|
for (let i = 0; i < 2; i++) {
|
|
values.push({
|
|
left: {},
|
|
right: {}
|
|
});
|
|
|
|
// Create local object
|
|
let obj = objects[i] = new Zotero.Item('note');
|
|
obj.setNote(Zotero.Utilities.randomString());
|
|
obj.version = 10;
|
|
obj.dateAdded = Zotero.Date.dateToSQL(new Date(dateAdded), true);
|
|
// Set Date Modified values one minute apart to enforce order
|
|
obj.dateModified = Zotero.Date.dateToSQL(
|
|
new Date(dateAdded + (i * 60000)), true
|
|
);
|
|
await obj.saveTx();
|
|
|
|
let jsonData = obj.toJSON();
|
|
jsonData.key = obj.key;
|
|
jsonData.version = 10;
|
|
let json = {
|
|
key: obj.key,
|
|
version: jsonData.version,
|
|
data: jsonData
|
|
};
|
|
// Save original version in cache
|
|
await Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
|
|
|
// Create updated JSON for download
|
|
values[i].right.note = jsonData.note = Zotero.Utilities.randomString();
|
|
values[i].right.version = json.version = jsonData.version = 15;
|
|
responseJSON.push(json);
|
|
|
|
// Modify object locally
|
|
obj.setNote(Zotero.Utilities.randomString());
|
|
await obj.saveTx({
|
|
skipDateModifiedUpdate: true
|
|
});
|
|
values[i].left.note = obj.note;
|
|
values[i].left.version = obj.getField('version');
|
|
}
|
|
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?itemKey=${objects.map(o => o.key).join('%2C')}`
|
|
+ `&includeTrashed=1`,
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": 15
|
|
},
|
|
json: responseJSON
|
|
});
|
|
|
|
var crPromise = 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 (remote)
|
|
// Remote version should be selected by default
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
wizard.getButton('next').click();
|
|
|
|
// 2 (local)
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
// Select local object
|
|
mergeGroup.leftPane.click();
|
|
assert.equal(mergeGroup.leftPane.getAttribute('selected'), 'true');
|
|
if (Zotero.isMac) {
|
|
assert.isTrue(wizard.getButton('next').hidden);
|
|
assert.isFalse(wizard.getButton('finish').hidden);
|
|
}
|
|
else {
|
|
// TODO
|
|
}
|
|
wizard.getButton('finish').click();
|
|
});
|
|
await engine._downloadObjects('item', objects.map(o => o.key));
|
|
await crPromise;
|
|
|
|
assert.equal(objects[0].note, values[0].right.note);
|
|
assert.equal(objects[1].note, values[1].left.note);
|
|
assert.equal(objects[0].version, values[0].right.version);
|
|
assert.equal(objects[1].version, values[1].right.version);
|
|
assert.isTrue(objects[0].synced);
|
|
assert.isFalse(objects[1].synced);
|
|
|
|
// Cache versions should match remote
|
|
for (let i = 0; i < 2; i++) {
|
|
let cacheJSON = await Zotero.Sync.Data.Local.getCacheObject(
|
|
'item', libraryID, objects[i].key, values[i].right.version
|
|
);
|
|
assert.propertyVal(cacheJSON, 'version', values[i].right.version);
|
|
assert.nestedPropertyVal(cacheJSON, 'data.note', values[i].right.note);
|
|
}
|
|
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
|
assert.lengthOf(keys, 0);
|
|
});
|
|
|
|
|
|
it("should show conflict resolution window on annotation conflicts", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
({ engine, client, caller } = await setup());
|
|
var values = [];
|
|
var dateAdded = Date.now() - 86400000;
|
|
var responseJSON = [];
|
|
|
|
var attachment = await importFileAttachment('test.pdf');
|
|
var annotation1 = await createAnnotation('highlight', attachment);
|
|
var annotation2 = await createAnnotation('note', attachment);
|
|
var annotation3 = await createAnnotation('image', attachment);
|
|
var objects = [annotation1, annotation2, annotation3];
|
|
|
|
for (let i = 0; i < objects.length; i++) {
|
|
values.push({
|
|
left: {},
|
|
right: {}
|
|
});
|
|
|
|
let item = objects[i];
|
|
item.version = 10;
|
|
item.dateAdded = Zotero.Date.dateToSQL(new Date(dateAdded), true);
|
|
// Set Date Modified values one minute apart to enforce order
|
|
item.dateModified = Zotero.Date.dateToSQL(
|
|
new Date(dateAdded + (i * 60000)), true
|
|
);
|
|
await item.saveTx();
|
|
|
|
let jsonData = item.toJSON();
|
|
jsonData.key = item.key;
|
|
jsonData.version = 10;
|
|
let json = {
|
|
key: item.key,
|
|
version: jsonData.version,
|
|
data: jsonData
|
|
};
|
|
// Save original version in cache
|
|
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json]);
|
|
|
|
// Make conflicting local and remote changes
|
|
if (i == 0) {
|
|
values[i].left.annotationText
|
|
= item.annotationText = Zotero.Utilities.randomString();
|
|
values[i].right.annotationText
|
|
= jsonData.annotationText = Zotero.Utilities.randomString();
|
|
}
|
|
else if (i == 1) {
|
|
values[i].left.annotationComment
|
|
= item.annotationComment = Zotero.Utilities.randomString();
|
|
values[i].right.annotationComment
|
|
= jsonData.annotationComment = Zotero.Utilities.randomString();
|
|
}
|
|
else if (i == 2) {
|
|
values[i].left.annotationColor
|
|
= item.annotationColor = '#000000';
|
|
values[i].right.annotationColor = jsonData.annotationColor = '#ffffff';
|
|
}
|
|
await item.saveTx({
|
|
skipDateModifiedUpdate: true
|
|
});
|
|
|
|
values[i].left.version = item.version;
|
|
values[i].right.version = json.version = jsonData.version = 15;
|
|
responseJSON.push(json);
|
|
}
|
|
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?itemKey=${objects.map(x => x.key).join('%2C')}`
|
|
+ `&includeTrashed=1`,
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": 15
|
|
},
|
|
json: responseJSON
|
|
});
|
|
|
|
var crPromise = waitForWindow('chrome://zotero/content/merge.xhtml', function (dialog) {
|
|
var doc = dialog.document;
|
|
var wizard = doc.querySelector('wizard');
|
|
var mergeGroup = wizard.getElementsByTagName('merge-group')[0];
|
|
|
|
// TODO: Make this function async and verify that annotation widgets show up here
|
|
// after rendering. This may not be possible as long as this is within XBL.
|
|
|
|
// 1 (remote)
|
|
// Remote version should be selected by default
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
wizard.getButton('next').click();
|
|
|
|
// 2 (local)
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
// Select local object
|
|
mergeGroup.leftPane.click();
|
|
assert.equal(mergeGroup.leftPane.getAttribute('selected'), 'true');
|
|
wizard.getButton('next').click();
|
|
|
|
// 2 (remote)
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
|
|
if (Zotero.isMac) {
|
|
assert.isTrue(wizard.getButton('next').hidden);
|
|
assert.isFalse(wizard.getButton('finish').hidden);
|
|
}
|
|
else {
|
|
// TODO
|
|
}
|
|
wizard.getButton('finish').click();
|
|
});
|
|
await engine._downloadObjects('item', objects.map(o => o.key));
|
|
await crPromise;
|
|
|
|
assert.equal(objects[0].annotationText, values[0].right.annotationText);
|
|
assert.equal(objects[1].annotationComment, values[1].left.annotationComment);
|
|
assert.equal(objects[2].annotationColor, values[2].right.annotationColor);
|
|
|
|
assert.equal(objects[0].version, values[0].right.version);
|
|
assert.equal(objects[1].version, values[1].right.version);
|
|
assert.equal(objects[2].version, values[2].right.version);
|
|
assert.isTrue(objects[0].synced);
|
|
assert.isFalse(objects[1].synced);
|
|
assert.isTrue(objects[2].synced);
|
|
|
|
// Cache versions should match remote
|
|
for (let i = 0; i < objects.length; i++) {
|
|
let cacheJSON = await Zotero.Sync.Data.Local.getCacheObject(
|
|
'item', libraryID, objects[i].key, values[i].right.version
|
|
);
|
|
assert.propertyVal(cacheJSON, 'version', values[i].right.version);
|
|
if (i == 0) {
|
|
assert.nestedPropertyVal(
|
|
cacheJSON, 'data.annotationText', values[i].right.annotationText
|
|
);
|
|
}
|
|
else if (i == 1) {
|
|
assert.nestedPropertyVal(
|
|
cacheJSON, 'data.annotationComment', values[i].right.annotationComment
|
|
);
|
|
}
|
|
else if (i == 2) {
|
|
assert.nestedPropertyVal(
|
|
cacheJSON, 'data.annotationColor', values[i].right.annotationColor
|
|
);
|
|
}
|
|
}
|
|
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
|
assert.lengthOf(keys, 0);
|
|
});
|
|
|
|
|
|
it("should resolve all remaining conflicts with local version", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
({ engine, client, caller } = await setup());
|
|
var collectionA = await createDataObject('collection');
|
|
var collectionB = await createDataObject('collection');
|
|
var objects = [];
|
|
var values = [];
|
|
var responseJSON = [];
|
|
var dateAdded = Date.now() - 86400000;
|
|
for (let i = 0; i < 3; i++) {
|
|
values.push({
|
|
left: {},
|
|
right: {}
|
|
});
|
|
|
|
// Create object in cache
|
|
let obj = objects[i] = await createDataObject(
|
|
'item',
|
|
{
|
|
version: 10,
|
|
dateAdded: Zotero.Date.dateToSQL(new Date(dateAdded), true),
|
|
// Set Date Modified values one minute apart to enforce order
|
|
dateModified: Zotero.Date.dateToSQL(
|
|
new Date(dateAdded + (i * 60000)), true
|
|
)
|
|
}
|
|
);
|
|
let jsonData = obj.toJSON();
|
|
jsonData.key = obj.key;
|
|
jsonData.version = 10;
|
|
let json = {
|
|
key: obj.key,
|
|
version: jsonData.version,
|
|
data: jsonData
|
|
};
|
|
// Save original version in cache
|
|
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json]);
|
|
|
|
// Create remote version
|
|
values[i].right.title = jsonData.title = Zotero.Utilities.randomString();
|
|
values[i].right.publisher = jsonData.publisher = Zotero.Utilities.randomString();
|
|
values[i].right.collections = jsonData.collections = [collectionB.key];
|
|
values[i].right.version = json.version = jsonData.version = 15;
|
|
responseJSON.push(json);
|
|
|
|
// Modify object locally
|
|
obj.setField('title', Zotero.Utilities.randomString());
|
|
obj.setField('extra', Zotero.Utilities.randomString());
|
|
obj.setCollections([collectionA.key]);
|
|
await obj.saveTx({
|
|
skipDateModifiedUpdate: true
|
|
});
|
|
values[i].left.title = obj.getField('title');
|
|
values[i].left.extra = obj.getField('extra');
|
|
values[i].left.collections = [collectionA.key];
|
|
values[i].left.version = obj.version;
|
|
}
|
|
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?itemKey=${objects.map(o => o.key).join('%2C')}`
|
|
+ `&includeTrashed=1`,
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": 15
|
|
},
|
|
json: responseJSON
|
|
});
|
|
|
|
var crPromise = waitForWindow('chrome://zotero/content/merge.xhtml', function (dialog) {
|
|
var doc = dialog.document;
|
|
var wizard = doc.querySelector('wizard');
|
|
var mergeGroup = wizard.getElementsByTagName('merge-group')[0];
|
|
var resolveAll = doc.getElementById('resolve-all');
|
|
|
|
// 1 (remote)
|
|
// Remote version should be selected by default
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
assert.equal(
|
|
resolveAll.label,
|
|
Zotero.getString('sync.conflict.resolveAllRemote')
|
|
);
|
|
wizard.getButton('next').click();
|
|
|
|
// 2 (local and Resolve All checkbox)
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
mergeGroup.leftPane.click();
|
|
assert.equal(
|
|
resolveAll.label,
|
|
Zotero.getString('sync.conflict.resolveAllLocal')
|
|
);
|
|
resolveAll.click();
|
|
|
|
if (Zotero.isMac) {
|
|
assert.isTrue(wizard.getButton('next').hidden);
|
|
assert.isFalse(wizard.getButton('finish').hidden);
|
|
}
|
|
else {
|
|
// TODO
|
|
}
|
|
wizard.getButton('finish').click();
|
|
})
|
|
await engine._downloadObjects('item', objects.map(o => o.key));
|
|
await crPromise;
|
|
|
|
// First object should match remote
|
|
assert.equal(objects[0].getField('title'), values[0].right.title);
|
|
assert.equal(objects[0].version, values[0].right.version);
|
|
assert.isTrue(objects[0].synced);
|
|
|
|
// Remaining objects should be marked as unsynced, with remote versions but original values,
|
|
// as if they were saved and then modified
|
|
assert.isFalse(objects[1].synced);
|
|
assert.equal(objects[1].version, values[1].right.version);
|
|
assert.equal(objects[1].getField('title'), values[1].left.title);
|
|
assert.isFalse(objects[2].synced);
|
|
assert.equal(objects[2].getField('title'), values[2].left.title);
|
|
assert.equal(objects[2].version, values[2].right.version);
|
|
|
|
// All cache versions should match remote
|
|
for (let i = 0; i < 3; i++) {
|
|
let cacheJSON = await Zotero.Sync.Data.Local.getCacheObject(
|
|
'item', libraryID, objects[i].key, values[i].right.version
|
|
);
|
|
assert.propertyVal(cacheJSON, 'version', values[i].right.version);
|
|
assert.nestedPropertyVal(cacheJSON, 'data.title', values[i].right.title);
|
|
}
|
|
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
|
assert.lengthOf(keys, 0);
|
|
});
|
|
|
|
|
|
it("should resolve all remaining conflicts with remote version", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
({ engine, client, caller } = await setup());
|
|
var objects = [];
|
|
var values = [];
|
|
var responseJSON = [];
|
|
var dateAdded = Date.now() - 86400000;
|
|
for (let i = 0; i < 3; i++) {
|
|
values.push({
|
|
left: {},
|
|
right: {}
|
|
});
|
|
|
|
// Create object in cache
|
|
let obj = objects[i] = await createDataObject(
|
|
'item',
|
|
{
|
|
version: 10,
|
|
dateAdded: Zotero.Date.dateToSQL(new Date(dateAdded), true),
|
|
// Set Date Modified values one minute apart to enforce order
|
|
dateModified: Zotero.Date.dateToSQL(
|
|
new Date(dateAdded + (i * 60000)), true
|
|
)
|
|
}
|
|
);
|
|
let jsonData = obj.toJSON();
|
|
jsonData.key = obj.key;
|
|
jsonData.version = 10;
|
|
let json = {
|
|
key: obj.key,
|
|
version: jsonData.version,
|
|
data: jsonData
|
|
};
|
|
// Save original version in cache
|
|
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json]);
|
|
|
|
// Create remote version
|
|
values[i].right.title = jsonData.title = Zotero.Utilities.randomString();
|
|
values[i].right.version = json.version = jsonData.version = 15;
|
|
responseJSON.push(json);
|
|
|
|
// Modify object locally
|
|
await modifyDataObject(obj, undefined, { skipDateModifiedUpdate: true });
|
|
values[i].left.title = obj.getField('title');
|
|
values[i].left.version = obj.version;
|
|
}
|
|
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?itemKey=${objects.map(o => o.key).join('%2C')}`
|
|
+ `&includeTrashed=1`,
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": 15
|
|
},
|
|
json: responseJSON
|
|
});
|
|
|
|
var crPromise = waitForWindow('chrome://zotero/content/merge.xhtml', function (dialog) {
|
|
var doc = dialog.document;
|
|
var wizard = doc.querySelector('wizard');
|
|
var mergeGroup = wizard.getElementsByTagName('merge-group')[0];
|
|
var resolveAll = doc.getElementById('resolve-all');
|
|
|
|
// 1 (remote)
|
|
// Remote version should be selected by default
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
assert.equal(
|
|
resolveAll.label,
|
|
Zotero.getString('sync.conflict.resolveAllRemote')
|
|
);
|
|
wizard.getButton('next').click();
|
|
|
|
// 2 click Resolve All checkbox
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
assert.equal(
|
|
resolveAll.label,
|
|
Zotero.getString('sync.conflict.resolveAllRemote')
|
|
);
|
|
resolveAll.click();
|
|
|
|
if (Zotero.isMac) {
|
|
assert.isTrue(wizard.getButton('next').hidden);
|
|
assert.isFalse(wizard.getButton('finish').hidden);
|
|
}
|
|
else {
|
|
// TODO
|
|
}
|
|
wizard.getButton('finish').click();
|
|
})
|
|
await engine._downloadObjects('item', objects.map(o => o.key));
|
|
await crPromise;
|
|
|
|
assert.equal(objects[0].getField('title'), values[0].right.title);
|
|
assert.equal(objects[0].version, values[0].right.version);
|
|
assert.isTrue(objects[0].synced);
|
|
assert.equal(objects[1].getField('title'), values[1].right.title);
|
|
assert.equal(objects[1].version, values[1].right.version);
|
|
assert.isTrue(objects[1].synced);
|
|
assert.equal(objects[2].getField('title'), values[2].right.title);
|
|
assert.equal(objects[2].version, values[2].right.version);
|
|
assert.isTrue(objects[2].synced);
|
|
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
|
assert.lengthOf(keys, 0);
|
|
});
|
|
|
|
|
|
// Note: Conflicts with remote deletions are handled in _startDownload()
|
|
it("should handle local item deletion, keeping deletion", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
({ engine, client, caller } = await setup());
|
|
var type = 'item';
|
|
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
|
var responseJSON = [];
|
|
|
|
// Create object, generate JSON, and delete
|
|
var obj = await createDataObject(type, { version: 10 });
|
|
var jsonData = obj.toJSON();
|
|
var key = jsonData.key = obj.key;
|
|
jsonData.version = 10;
|
|
let json = {
|
|
key: obj.key,
|
|
version: jsonData.version,
|
|
data: jsonData
|
|
};
|
|
// Delete object locally
|
|
await obj.eraseTx();
|
|
|
|
json.version = jsonData.version = 15;
|
|
jsonData.title = Zotero.Utilities.randomString();
|
|
responseJSON.push(json);
|
|
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?itemKey=${obj.key}&includeTrashed=1`,
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": 15
|
|
},
|
|
json: responseJSON
|
|
});
|
|
|
|
var crPromise = waitForWindow('chrome://zotero/content/merge.xhtml', function (dialog) {
|
|
var doc = dialog.document;
|
|
var wizard = doc.querySelector('wizard');
|
|
var mergeGroup = wizard.getElementsByTagName('merge-group')[0];
|
|
|
|
// Remote version should be selected by default
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
assert.ok(mergeGroup.leftPane.groupbox.onclick);
|
|
// Select local deleted version
|
|
mergeGroup.leftPane.groupbox.click();
|
|
wizard.getButton('finish').click();
|
|
})
|
|
await engine._downloadObjects('item', [obj.key]);
|
|
await crPromise;
|
|
|
|
obj = objectsClass.getByLibraryAndKey(libraryID, key);
|
|
assert.isFalse(obj);
|
|
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
|
assert.lengthOf(keys, 0);
|
|
})
|
|
|
|
it("should handle local child note deletion, keeping deletion", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
({ engine, client, caller } = await setup());
|
|
var responseJSON = [];
|
|
|
|
var parent = await createDataObject('item');
|
|
|
|
// Create object, generate JSON, and delete
|
|
var obj = new Zotero.Item('note');
|
|
obj.parentItemID = parent.id;
|
|
obj.setNote(Zotero.Utilities.randomString());
|
|
obj.version = 10;
|
|
await obj.saveTx();
|
|
var jsonData = obj.toJSON();
|
|
var key = jsonData.key = obj.key;
|
|
jsonData.version = 10;
|
|
let json = {
|
|
key: obj.key,
|
|
version: jsonData.version,
|
|
data: jsonData
|
|
};
|
|
// Delete object locally
|
|
await obj.eraseTx();
|
|
|
|
json.version = jsonData.version = 15;
|
|
jsonData.note = Zotero.Utilities.randomString();
|
|
responseJSON.push(json);
|
|
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?itemKey=${obj.key}&includeTrashed=1`,
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": 15
|
|
},
|
|
json: responseJSON
|
|
});
|
|
|
|
var crPromise = waitForWindow('chrome://zotero/content/merge.xhtml', function (dialog) {
|
|
var doc = dialog.document;
|
|
var wizard = doc.querySelector('wizard');
|
|
var mergeGroup = wizard.getElementsByTagName('merge-group')[0];
|
|
|
|
// Remote version should be selected by default
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
assert.ok(mergeGroup.leftPane.groupbox.onclick);
|
|
// Select local deleted version
|
|
mergeGroup.leftPane.groupbox.click();
|
|
wizard.getButton('finish').click();
|
|
});
|
|
await engine._downloadObjects('item', [obj.key]);
|
|
await crPromise;
|
|
|
|
obj = Zotero.Items.getByLibraryAndKey(libraryID, key);
|
|
assert.isFalse(obj);
|
|
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
|
assert.lengthOf(keys, 0);
|
|
});
|
|
|
|
it("should restore locally deleted item", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
({ engine, client, caller } = await setup());
|
|
var type = 'item';
|
|
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
|
var responseJSON = [];
|
|
|
|
// Create object, generate JSON, and delete
|
|
var obj = await createDataObject(type, { version: 10 });
|
|
var jsonData = obj.toJSON();
|
|
var key = jsonData.key = obj.key;
|
|
jsonData.version = 10;
|
|
let json = {
|
|
key: obj.key,
|
|
version: jsonData.version,
|
|
data: jsonData
|
|
};
|
|
await obj.eraseTx();
|
|
|
|
json.version = jsonData.version = 15;
|
|
jsonData.title = Zotero.Utilities.randomString();
|
|
responseJSON.push(json);
|
|
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?itemKey=${key}&includeTrashed=1`,
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": 15
|
|
},
|
|
json: responseJSON
|
|
});
|
|
|
|
var crPromise = waitForWindow('chrome://zotero/content/merge.xhtml', function (dialog) {
|
|
var doc = dialog.document;
|
|
var wizard = doc.querySelector('wizard');
|
|
var mergeGroup = wizard.getElementsByTagName('merge-group')[0];
|
|
|
|
assert.isTrue(doc.getElementById('resolve-all').hidden);
|
|
|
|
// Remote version should be selected by default
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
wizard.getButton('finish').click();
|
|
})
|
|
await engine._downloadObjects('item', [key]);
|
|
await crPromise;
|
|
|
|
obj = objectsClass.getByLibraryAndKey(libraryID, key);
|
|
assert.ok(obj);
|
|
assert.equal(obj.getField('title'), jsonData.title);
|
|
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
|
assert.lengthOf(keys, 0);
|
|
});
|
|
|
|
it("should handle local deletion and remote move to trash", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
({ engine, client, caller } = await setup());
|
|
var type = 'item';
|
|
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
|
var responseJSON = [];
|
|
|
|
// Create object, generate JSON, and delete
|
|
var obj = await createDataObject(type, { version: 10 });
|
|
var jsonData = obj.toJSON();
|
|
var key = jsonData.key = obj.key;
|
|
jsonData.version = 10;
|
|
let json = {
|
|
key: obj.key,
|
|
version: jsonData.version,
|
|
data: jsonData
|
|
};
|
|
await obj.eraseTx();
|
|
|
|
json.version = jsonData.version = 15;
|
|
jsonData.deleted = true;
|
|
responseJSON.push(json);
|
|
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/items?itemKey=${key}&includeTrashed=1`,
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": 15
|
|
},
|
|
json: responseJSON
|
|
});
|
|
|
|
await engine._downloadObjects('item', [key]);
|
|
|
|
assert.isFalse(objectsClass.exists(libraryID, key));
|
|
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
|
assert.lengthOf(keys, 0);
|
|
|
|
// Deletion should still be in sync delete log for uploading
|
|
assert.ok(await Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, key));
|
|
});
|
|
|
|
it("should delete locally trashed item on remote deletion", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
({ engine, client, caller } = await setup());
|
|
var type = 'item';
|
|
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
|
var responseJSON = [];
|
|
|
|
// Create trashed object
|
|
var obj = createUnsavedDataObject(type);
|
|
obj.deleted = true;
|
|
await obj.saveTx();
|
|
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/deleted?since=10`,
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": 15
|
|
},
|
|
json: {
|
|
collections: [],
|
|
searches: [],
|
|
items: [obj.key],
|
|
}
|
|
});
|
|
|
|
await engine._downloadDeletions(10, 15);
|
|
|
|
// Local object should have been deleted
|
|
assert.isFalse(objectsClass.exists(libraryID, obj.key));
|
|
|
|
var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID);
|
|
assert.lengthOf(keys, 0);
|
|
|
|
// Deletion shouldn't be in sync delete log
|
|
assert.isFalse(await Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, obj.key));
|
|
});
|
|
});
|
|
|
|
|
|
describe("#_updateGroupItemUsers()", function () {
|
|
it("should update createdByUserID and lastModifiedByUserID", async function () {
|
|
var { id: groupID, libraryID } = await createGroup();
|
|
({ engine, client, caller } = await setup({ libraryID }));
|
|
|
|
var item1 = await createDataObject('item', { libraryID }, { skipGroupItemsUserUpdate: true });
|
|
var item1DateModified = item1.dateModified;
|
|
var item2 = await createDataObject('item', { libraryID }, { skipGroupItemsUserUpdate: true });
|
|
var responseJSON = [
|
|
item1.toResponseJSON(),
|
|
item2.toResponseJSON()
|
|
];
|
|
responseJSON[0].meta.createdByUser = {
|
|
id: 152315,
|
|
username: "user152315",
|
|
name: "User 152315"
|
|
};
|
|
responseJSON[0].meta.lastModifiedByUser = {
|
|
id: 352352,
|
|
username: "user352352",
|
|
name: "User 352352"
|
|
};
|
|
responseJSON[1].meta.createdByUser = {
|
|
id: 346534,
|
|
username: "user346534",
|
|
name: "User 346534"
|
|
};
|
|
|
|
setResponse({
|
|
method: "GET",
|
|
url: `groups/${groupID}/items?itemKey=${item1.key}%2C${item2.key}&includeTrashed=1`,
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": 5
|
|
},
|
|
json: responseJSON
|
|
});
|
|
|
|
await engine._updateGroupItemUsers();
|
|
|
|
assert.equal(item1.createdByUserID, 152315);
|
|
assert.equal(item1.lastModifiedByUserID, 352352);
|
|
assert.equal(item1.dateModified, item1DateModified);
|
|
assert.equal(item2.createdByUserID, 346534);
|
|
});
|
|
|
|
|
|
it("should use username if no name", async function () {
|
|
var { id: groupID, libraryID } = await createGroup();
|
|
({ engine, client, caller } = await setup({ libraryID }));
|
|
|
|
var item = await createDataObject('item', { libraryID }, { skipGroupItemsUserUpdate: true });
|
|
var responseJSON = [
|
|
item.toResponseJSON()
|
|
];
|
|
responseJSON[0].meta.createdByUser = {
|
|
id: 235235,
|
|
username: "user235235",
|
|
name: ""
|
|
};
|
|
|
|
setResponse({
|
|
method: "GET",
|
|
url: `groups/${groupID}/items?itemKey=${item.key}&includeTrashed=1`,
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": 6
|
|
},
|
|
json: responseJSON
|
|
});
|
|
|
|
await engine._updateGroupItemUsers();
|
|
|
|
assert.equal(item.createdByUserID, 235235);
|
|
assert.equal(Zotero.Users.getName(235235), 'user235235');
|
|
});
|
|
});
|
|
|
|
|
|
describe("#_upgradeCheck()", function () {
|
|
it("should upgrade a library last synced with the classic sync architecture", async function () {
|
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var types = Zotero.DataObjectUtilities.getTypes();
|
|
var objects = {};
|
|
|
|
// Create objects added before the last classic sync time,
|
|
// which should end up marked as synced
|
|
for (let type of types) {
|
|
objects[type] = [await createDataObject(type)];
|
|
}
|
|
|
|
var time1 = "2015-05-01 01:23:45";
|
|
await Zotero.DB.queryAsync("UPDATE collections SET clientDateModified=?", time1);
|
|
await Zotero.DB.queryAsync("UPDATE savedSearches SET clientDateModified=?", time1);
|
|
await Zotero.DB.queryAsync("UPDATE items SET clientDateModified=?", time1);
|
|
|
|
// Create objects added after the last sync time, which should be ignored and
|
|
// therefore end up marked as unsynced
|
|
for (let type of types) {
|
|
objects[type].push(await createDataObject(type));
|
|
}
|
|
|
|
var objectJSON = {};
|
|
for (let type of types) {
|
|
objectJSON[type] = [];
|
|
}
|
|
|
|
// Create JSON for objects created remotely after the last sync time,
|
|
// which should be ignored
|
|
objectJSON.collection.push(makeCollectionJSON({
|
|
key: Zotero.DataObjectUtilities.generateKey(),
|
|
version: 20,
|
|
name: Zotero.Utilities.randomString()
|
|
}));
|
|
objectJSON.search.push(makeSearchJSON({
|
|
key: Zotero.DataObjectUtilities.generateKey(),
|
|
version: 20,
|
|
name: Zotero.Utilities.randomString()
|
|
}));
|
|
objectJSON.item.push(makeItemJSON({
|
|
key: Zotero.DataObjectUtilities.generateKey(),
|
|
version: 20,
|
|
itemType: "book",
|
|
title: Zotero.Utilities.randomString()
|
|
}));
|
|
|
|
var lastSyncTime = Zotero.Date.toUnixTimestamp(
|
|
Zotero.Date.sqlToDate("2015-05-02 00:00:00", true)
|
|
);
|
|
await Zotero.DB.queryAsync(
|
|
"INSERT INTO version VALUES ('lastlocalsync', ?1), ('lastremotesync', ?1)",
|
|
lastSyncTime
|
|
);
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 20
|
|
}
|
|
for (let type of types) {
|
|
var suffix = type == 'item' ? '&includeTrashed=1' : '';
|
|
|
|
var json = {};
|
|
json[objects[type][0].key] = 10;
|
|
json[objectJSON[type][0].key] = objectJSON[type][0].version;
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/" + Zotero.DataObjectUtilities.getObjectTypePlural(type)
|
|
+ "?format=versions" + suffix,
|
|
status: 200,
|
|
headers: headers,
|
|
json: json
|
|
});
|
|
json = {};
|
|
json[objectJSON[type][0].key] = objectJSON[type][0].version;
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/" + Zotero.DataObjectUtilities.getObjectTypePlural(type)
|
|
+ "?format=versions&sincetime=" + lastSyncTime + suffix,
|
|
status: 200,
|
|
headers: headers,
|
|
json: json
|
|
});
|
|
}
|
|
var versionResults = await engine._upgradeCheck();
|
|
|
|
// Objects 1 should be marked as synced, with versions from the server
|
|
// Objects 2 should be marked as unsynced
|
|
for (let type of types) {
|
|
var synced = await Zotero.Sync.Data.Local.getSynced(type, userLibraryID);
|
|
assert.deepEqual(synced, [objects[type][0].key]);
|
|
assert.equal(objects[type][0].version, 10);
|
|
var unsynced = await Zotero.Sync.Data.Local.getUnsynced(type, userLibraryID);
|
|
assert.deepEqual(unsynced, [objects[type][1].id]);
|
|
|
|
assert.equal(versionResults[type].libraryVersion, headers["Last-Modified-Version"]);
|
|
assert.property(versionResults[type].versions, objectJSON[type][0].key);
|
|
}
|
|
|
|
assert.equal(Zotero.Libraries.getVersion(userLibraryID), -1);
|
|
})
|
|
})
|
|
|
|
describe("#_fullSync()", function () {
|
|
it("should download missing/updated local objects and flag remotely missing local objects for upload", async function () {
|
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var types = Zotero.DataObjectUtilities.getTypes();
|
|
var objects = {};
|
|
var objectJSON = {};
|
|
for (let type of types) {
|
|
objectJSON[type] = [];
|
|
}
|
|
|
|
for (let type of types) {
|
|
// Create object with outdated version, which should be updated
|
|
let obj = createUnsavedDataObject(type);
|
|
obj.synced = true;
|
|
obj.version = 5;
|
|
await obj.saveTx();
|
|
objects[type] = [obj];
|
|
|
|
objectJSON[type].push(makeJSONFunctions[type]({
|
|
key: obj.key,
|
|
version: 20,
|
|
name: Zotero.Utilities.randomString()
|
|
}));
|
|
|
|
// Create JSON for object that exists remotely and not locally,
|
|
// which should be downloaded
|
|
objectJSON[type].push(makeJSONFunctions[type]({
|
|
key: Zotero.DataObjectUtilities.generateKey(),
|
|
version: 20,
|
|
name: Zotero.Utilities.randomString()
|
|
}));
|
|
|
|
// Create object marked as synced that doesn't exist remotely,
|
|
// which should be flagged for upload
|
|
obj = createUnsavedDataObject(type);
|
|
obj.synced = true;
|
|
obj.version = 10;
|
|
await obj.saveTx();
|
|
objects[type].push(obj);
|
|
|
|
// Create object marked as synced that doesn't exist remotely but is in the
|
|
// remote delete log, which should be deleted locally
|
|
obj = createUnsavedDataObject(type);
|
|
obj.synced = true;
|
|
obj.version = 10;
|
|
await obj.saveTx();
|
|
objects[type].push(obj);
|
|
}
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 20
|
|
}
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/settings",
|
|
status: 200,
|
|
headers: headers,
|
|
json: {
|
|
tagColors: {
|
|
value: [
|
|
{
|
|
name: "A",
|
|
color: "#CC66CC"
|
|
}
|
|
],
|
|
version: 2
|
|
}
|
|
}
|
|
});
|
|
let deletedJSON = {};
|
|
for (let type of types) {
|
|
let suffix = type == 'item' ? '&includeTrashed=1' : '';
|
|
let plural = Zotero.DataObjectUtilities.getObjectTypePlural(type);
|
|
|
|
var json = {};
|
|
json[objectJSON[type][0].key] = objectJSON[type][0].version;
|
|
json[objectJSON[type][1].key] = objectJSON[type][1].version;
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/" + plural
|
|
+ "?format=versions" + suffix,
|
|
status: 200,
|
|
headers: headers,
|
|
json: json
|
|
});
|
|
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/" + plural
|
|
+ "?" + type + "Key=" + objectJSON[type][0].key + "%2C" + objectJSON[type][1].key
|
|
+ suffix,
|
|
status: 200,
|
|
headers: headers,
|
|
json: objectJSON[type]
|
|
});
|
|
|
|
deletedJSON[plural] = [objects[type][2].key];
|
|
}
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/deleted?since=0",
|
|
status: 200,
|
|
headers: headers,
|
|
json: deletedJSON
|
|
});
|
|
await engine._fullSync();
|
|
|
|
// Check settings
|
|
var setting = Zotero.SyncedSettings.get(userLibraryID, "tagColors");
|
|
assert.lengthOf(setting, 1);
|
|
assert.equal(setting[0].name, 'A');
|
|
var settingMetadata = Zotero.SyncedSettings.getMetadata(userLibraryID, "tagColors");
|
|
assert.equal(settingMetadata.version, 2);
|
|
assert.isTrue(settingMetadata.synced);
|
|
|
|
// Check objects
|
|
for (let type of types) {
|
|
// Objects 1 should be updated with version from server
|
|
assert.equal(objects[type][0].version, 20);
|
|
assert.isTrue(objects[type][0].synced);
|
|
|
|
// JSON objects 1 should be created locally with version from server
|
|
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
|
let obj = objectsClass.getByLibraryAndKey(userLibraryID, objectJSON[type][0].key);
|
|
assert.equal(obj.version, 20);
|
|
assert.isTrue(obj.synced);
|
|
await assertInCache(obj);
|
|
|
|
// JSON objects 2 should be marked as unsynced, with their version reset to 0
|
|
assert.equal(objects[type][1].version, 0);
|
|
assert.isFalse(objects[type][1].synced);
|
|
|
|
// JSON objects 3 should be deleted and not in the delete log
|
|
assert.isFalse(objectsClass.getByLibraryAndKey(userLibraryID, objects[type][2].key));
|
|
assert.isFalse(await Zotero.Sync.Data.Local.getDateDeleted(
|
|
type, userLibraryID, objects[type][2].key
|
|
));
|
|
}
|
|
});
|
|
|
|
it("should reprocess remote deletions", async function () {
|
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
|
({ engine, client, caller } = await setup());
|
|
|
|
var types = Zotero.DataObjectUtilities.getTypes();
|
|
var objects = {};
|
|
var objectIDs = {};
|
|
|
|
for (let type of types) {
|
|
// Create object marked as synced that's in the remote delete log, which should be
|
|
// deleted locally
|
|
let obj = createUnsavedDataObject(type);
|
|
obj.synced = true;
|
|
obj.version = 5;
|
|
await obj.saveTx();
|
|
objects[type] = [obj];
|
|
objectIDs[type] = [obj.id];
|
|
|
|
// Create object marked as unsynced that's in the remote delete log, which should
|
|
// trigger a conflict in the case of items and otherwise reset version to 0
|
|
obj = createUnsavedDataObject(type);
|
|
obj.synced = false;
|
|
obj.version = 5;
|
|
await obj.saveTx();
|
|
objects[type].push(obj);
|
|
objectIDs[type].push(obj.id);
|
|
}
|
|
|
|
var headers = {
|
|
"Last-Modified-Version": 20
|
|
}
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/settings",
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
let deletedJSON = {};
|
|
for (let type of types) {
|
|
let suffix = type == 'item' ? '&includeTrashed=1' : '';
|
|
let plural = Zotero.DataObjectUtilities.getObjectTypePlural(type);
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/" + plural + "?format=versions" + suffix,
|
|
status: 200,
|
|
headers,
|
|
json: {}
|
|
});
|
|
deletedJSON[plural] = objects[type].map(o => o.key);
|
|
}
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/deleted?since=0",
|
|
status: 200,
|
|
headers: headers,
|
|
json: deletedJSON
|
|
});
|
|
|
|
// Apply remote deletions
|
|
var crPromise = waitForWindow('chrome://zotero/content/merge.xhtml', function (dialog) {
|
|
var doc = dialog.document;
|
|
var wizard = doc.querySelector('wizard');
|
|
var mergeGroup = wizard.getElementsByTagName('merge-group')[0];
|
|
|
|
// Should be one conflict for each object type; select local
|
|
var numConflicts = Object.keys(objects).length;
|
|
for (let i = 0; i < numConflicts; i++) {
|
|
assert.equal(mergeGroup.leftPane.getAttribute('selected'), 'true');
|
|
|
|
if (i < numConflicts - 1) {
|
|
wizard.getButton('next').click();
|
|
}
|
|
else {
|
|
wizard.getButton('finish').click();
|
|
}
|
|
}
|
|
});
|
|
|
|
await engine._fullSync();
|
|
await crPromise;
|
|
|
|
// Check objects
|
|
for (let type of types) {
|
|
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
|
|
|
// Objects 0 should be deleted
|
|
assert.isFalse(objectsClass.exists(objectIDs[type][0]));
|
|
|
|
// Objects 1 should be marked for reupload
|
|
assert.isTrue(objectsClass.exists(objectIDs[type][1]));
|
|
assert.strictEqual(objects[type][1].version, 0);
|
|
assert.strictEqual(objects[type][1].synced, false);
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
describe("#_restoreToServer()", function () {
|
|
it("should delete remote objects that don't exist locally and upload all local objects", async function () {
|
|
({ engine, client, caller } = await setup());
|
|
var library = Zotero.Libraries.userLibrary;
|
|
var libraryID = library.id;
|
|
var lastLibraryVersion = 10;
|
|
library.libraryVersion = library.storageVersion = lastLibraryVersion;
|
|
await library.saveTx();
|
|
lastLibraryVersion = 20;
|
|
|
|
var postData = {};
|
|
var deleteData = {};
|
|
|
|
var types = Zotero.DataObjectUtilities.getTypes();
|
|
var objects = {};
|
|
var objectJSON = {};
|
|
for (let type of types) {
|
|
objectJSON[type] = [];
|
|
}
|
|
|
|
var obj;
|
|
for (let type of types) {
|
|
objects[type] = [null];
|
|
// Create JSON for object that exists remotely and not locally,
|
|
// which should be deleted
|
|
objectJSON[type].push(makeJSONFunctions[type]({
|
|
key: Zotero.DataObjectUtilities.generateKey(),
|
|
version: lastLibraryVersion,
|
|
name: Zotero.Utilities.randomString()
|
|
}));
|
|
|
|
// All other objects should be uploaded
|
|
|
|
// Object with outdated version
|
|
obj = await createDataObject(type, { synced: true, version: 5 });
|
|
objects[type].push(obj);
|
|
objectJSON[type].push(makeJSONFunctions[type]({
|
|
key: obj.key,
|
|
version: lastLibraryVersion,
|
|
name: Zotero.Utilities.randomString()
|
|
}));
|
|
|
|
// Object marked as synced that doesn't exist remotely
|
|
obj = await createDataObject(type, { synced: true, version: 10 });
|
|
objects[type].push(obj);
|
|
objectJSON[type].push(makeJSONFunctions[type]({
|
|
key: obj.key,
|
|
version: lastLibraryVersion,
|
|
name: Zotero.Utilities.randomString()
|
|
}));
|
|
|
|
// Object marked as synced that doesn't exist remotely
|
|
// but is in the remote delete log
|
|
obj = await createDataObject(type, { synced: true, version: 10 });
|
|
objects[type].push(obj);
|
|
objectJSON[type].push(makeJSONFunctions[type]({
|
|
key: obj.key,
|
|
version: lastLibraryVersion,
|
|
name: Zotero.Utilities.randomString()
|
|
}));
|
|
}
|
|
|
|
// Child attachment
|
|
obj = await importFileAttachment(
|
|
'test.png',
|
|
{
|
|
parentID: objects.item[1].id,
|
|
synced: true,
|
|
version: 5
|
|
}
|
|
);
|
|
obj.attachmentSyncedModificationTime = new Date().getTime();
|
|
obj.attachmentSyncedHash = 'b32e33f529942d73bea4ed112310f804';
|
|
obj.attachmentSyncState = Zotero.Sync.Storage.Local.SYNC_STATE_IN_SYNC;
|
|
await obj.saveTx();
|
|
objects.item.push(obj);
|
|
objectJSON.item.push(makeJSONFunctions.item({
|
|
key: obj.key,
|
|
version: lastLibraryVersion,
|
|
name: Zotero.Utilities.randomString(),
|
|
itemType: 'attachment'
|
|
}));
|
|
|
|
for (let type of types) {
|
|
let plural = Zotero.DataObjectUtilities.getObjectTypePlural(type);
|
|
let suffix = type == 'item' ? '&includeTrashed=1' : '';
|
|
|
|
let json = {};
|
|
json[objectJSON[type][0].key] = objectJSON[type][0].version;
|
|
json[objectJSON[type][1].key] = objectJSON[type][1].version;
|
|
setResponse({
|
|
method: "GET",
|
|
url: `users/1/${plural}?format=versions${suffix}`,
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": lastLibraryVersion
|
|
},
|
|
json
|
|
});
|
|
|
|
deleteData[type] = {
|
|
expectedVersion: lastLibraryVersion++,
|
|
keys: [objectJSON[type][0].key]
|
|
};
|
|
}
|
|
|
|
await Zotero.SyncedSettings.set(libraryID, "testSetting", { foo: 2 });
|
|
var settingsJSON = { testSetting: { value: { foo: 2 } } }
|
|
postData.setting = {
|
|
expectedVersion: lastLibraryVersion++
|
|
};
|
|
|
|
for (let type of types) {
|
|
postData[type] = {
|
|
expectedVersion: lastLibraryVersion++
|
|
};
|
|
}
|
|
|
|
server.respond(function (req) {
|
|
try {
|
|
|
|
let plural = req.url.match(/users\/\d+\/([a-z]+e?s)/)[1];
|
|
let type = Zotero.DataObjectUtilities.getObjectTypeSingular(plural);
|
|
// Deletions
|
|
if (req.method == "DELETE") {
|
|
let data = deleteData[type];
|
|
let version = data.expectedVersion + 1;
|
|
if (req.url == baseURL + `users/1/${plural}?${type}Key=${data.keys.join(',')}`) {
|
|
req.respond(
|
|
204,
|
|
{
|
|
"Last-Modified-Version": version
|
|
},
|
|
""
|
|
);
|
|
}
|
|
}
|
|
// Settings
|
|
else if (req.method == "POST" && req.url.match(/users\/\d+\/settings/)) {
|
|
let data = postData.setting;
|
|
assert.equal(
|
|
req.requestHeaders["If-Unmodified-Since-Version"],
|
|
data.expectedVersion
|
|
);
|
|
let version = data.expectedVersion + 1;
|
|
let json = JSON.parse(req.requestBody);
|
|
assert.deepEqual(json, settingsJSON);
|
|
req.respond(
|
|
204,
|
|
{
|
|
"Last-Modified-Version": version
|
|
},
|
|
""
|
|
);
|
|
}
|
|
// Uploads
|
|
else if (req.method == "POST") {
|
|
let data = postData[type];
|
|
assert.equal(
|
|
req.requestHeaders["If-Unmodified-Since-Version"],
|
|
data.expectedVersion
|
|
);
|
|
let version = data.expectedVersion + 1;
|
|
let json = JSON.parse(req.requestBody);
|
|
|
|
let o1 = json.find(o => o.key == objectJSON[type][1].key);
|
|
assert.notProperty(o1, 'version');
|
|
let o2 = json.find(o => o.key == objectJSON[type][2].key);
|
|
assert.notProperty(o2, 'version');
|
|
let o3 = json.find(o => o.key == objectJSON[type][3].key);
|
|
assert.notProperty(o3, 'version');
|
|
if (type == 'item') {
|
|
let o = json.find(o => o.key == objectJSON.item[4].key);
|
|
assert.notProperty(o, 'version');
|
|
// Attachment items should include storage properties
|
|
assert.propertyVal(o, 'mtime', objects.item[4].attachmentSyncedModificationTime);
|
|
assert.propertyVal(o, 'md5', objects.item[4].attachmentSyncedHash);
|
|
}
|
|
let response = {
|
|
successful: {},
|
|
unchanged: {},
|
|
failed: {}
|
|
};
|
|
// Return objects in the order provided
|
|
json.map(x => x.key).forEach((key, index) => {
|
|
response.successful[index] = Object.assign(
|
|
objectJSON[type].find(x => x.key == key),
|
|
{ version }
|
|
);
|
|
});
|
|
req.respond(
|
|
200,
|
|
{
|
|
"Last-Modified-Version": version
|
|
},
|
|
JSON.stringify(response)
|
|
);
|
|
}
|
|
|
|
}
|
|
catch (e) {
|
|
Zotero.logError(e);
|
|
throw e;
|
|
}
|
|
});
|
|
|
|
await engine._restoreToServer();
|
|
|
|
// Check settings
|
|
var setting = Zotero.SyncedSettings.get(libraryID, "testSetting");
|
|
assert.deepEqual(setting, { foo: 2 });
|
|
var settingMetadata = Zotero.SyncedSettings.getMetadata(libraryID, "testSetting");
|
|
assert.equal(settingMetadata.version, postData.setting.expectedVersion + 1);
|
|
assert.isTrue(settingMetadata.synced);
|
|
|
|
// Objects should all be marked as synced and in the cache
|
|
for (let type of types) {
|
|
let version = postData[type].expectedVersion + 1;
|
|
for (let i = 1; i <= 3; i++) {
|
|
assert.equal(objects[type][i].version, version);
|
|
assert.isTrue(objects[type][i].synced);
|
|
await assertInCache(objects[type][i]);
|
|
}
|
|
}
|
|
|
|
// Files should be marked as unsynced
|
|
assert.equal(
|
|
objects.item[4].attachmentSyncState,
|
|
Zotero.Sync.Storage.Local.SYNC_STATE_TO_UPLOAD
|
|
);
|
|
});
|
|
});
|
|
})
|