2015-07-20 21:27:55 +00:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
describe("Zotero.Sync.Data.Engine", function () {
|
2015-09-18 05:18:09 +00:00
|
|
|
Components.utils.import("resource://zotero/config.js");
|
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
var apiKey = Zotero.Utilities.randomString(24);
|
|
|
|
var baseURL = "http://local.zotero/";
|
|
|
|
var engine, server, client, caller, stub, spy;
|
|
|
|
|
|
|
|
var responses = {};
|
|
|
|
|
2015-11-12 07:48:41 +00:00
|
|
|
var setup = Zotero.Promise.coroutine(function* (options = {}) {
|
2015-07-20 21:27:55 +00:00
|
|
|
server = sinon.fakeServer.create();
|
|
|
|
server.autoRespond = true;
|
|
|
|
|
2015-10-29 06:49:31 +00:00
|
|
|
Components.utils.import("resource://zotero/concurrentCaller.js");
|
2015-07-20 21:27:55 +00:00
|
|
|
var caller = new ConcurrentCaller(1);
|
|
|
|
caller.setLogger(msg => Zotero.debug(msg));
|
|
|
|
caller.stopOnError = true;
|
|
|
|
|
|
|
|
var client = new Zotero.Sync.APIClient({
|
2015-10-29 07:41:54 +00:00
|
|
|
baseURL,
|
2015-07-20 21:27:55 +00:00
|
|
|
apiVersion: options.apiVersion || ZOTERO_CONFIG.API_VERSION,
|
2015-10-29 07:41:54 +00:00
|
|
|
apiKey,
|
|
|
|
caller,
|
2015-07-20 21:27:55 +00:00
|
|
|
background: options.background || true
|
|
|
|
});
|
|
|
|
|
|
|
|
var engine = new Zotero.Sync.Data.Engine({
|
|
|
|
apiClient: client,
|
2015-10-29 07:41:54 +00:00
|
|
|
libraryID: options.libraryID || Zotero.Libraries.userLibraryID,
|
|
|
|
stopOnError: true
|
2015-07-20 21:27:55 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
return { engine, client, caller };
|
|
|
|
});
|
|
|
|
|
|
|
|
function setResponse(response) {
|
|
|
|
setHTTPResponse(server, baseURL, response, responses);
|
|
|
|
}
|
|
|
|
|
|
|
|
function makeCollectionJSON(options) {
|
|
|
|
return {
|
|
|
|
key: options.key,
|
|
|
|
version: options.version,
|
|
|
|
data: {
|
|
|
|
key: options.key,
|
|
|
|
version: options.version,
|
2016-02-29 09:23:00 +00:00
|
|
|
name: options.name,
|
|
|
|
parentCollection: options.parentCollection
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
};
|
|
|
|
|
2016-02-29 09:23:00 +00:00
|
|
|
var assertInCache = Zotero.Promise.coroutine(function* (obj) {
|
|
|
|
var cacheObject = yield Zotero.Sync.Data.Local.getCacheObject(
|
|
|
|
obj.objectType, obj.libraryID, obj.key, obj.version
|
|
|
|
);
|
|
|
|
assert.isObject(cacheObject);
|
|
|
|
assert.propertyVal(cacheObject, 'key', obj.key);
|
|
|
|
});
|
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
//
|
|
|
|
// Tests
|
|
|
|
//
|
|
|
|
beforeEach(function* () {
|
|
|
|
yield resetDB({
|
2015-09-29 08:07:26 +00:00
|
|
|
thisArg: this,
|
2015-07-20 21:27:55 +00:00
|
|
|
skipBundledFiles: true
|
|
|
|
});
|
|
|
|
|
|
|
|
Zotero.HTTP.mock = sinon.FakeXMLHttpRequest;
|
|
|
|
|
|
|
|
yield Zotero.Users.setCurrentUserID(1);
|
|
|
|
yield Zotero.Users.setCurrentUsername("testuser");
|
|
|
|
})
|
|
|
|
|
2016-04-24 08:04:40 +00:00
|
|
|
after(function () {
|
|
|
|
Zotero.HTTP.mock = null;
|
|
|
|
});
|
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
describe("Syncing", function () {
|
2015-07-22 09:21:32 +00:00
|
|
|
it("should download items into a new library", function* () {
|
2015-07-20 21:27:55 +00:00
|
|
|
({ engine, client, caller } = yield 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",
|
2016-01-14 06:50:13 +00:00
|
|
|
url: "users/1/items/top?format=versions&includeTrashed=1",
|
2015-07-20 21:27:55 +00:00
|
|
|
status: 200,
|
|
|
|
headers: headers,
|
|
|
|
json: {
|
|
|
|
"AAAAAAAA": 3
|
|
|
|
}
|
|
|
|
});
|
2016-01-14 06:50:13 +00:00
|
|
|
setResponse({
|
|
|
|
method: "GET",
|
|
|
|
url: "users/1/items?format=versions&includeTrashed=1",
|
|
|
|
status: 200,
|
|
|
|
headers: headers,
|
|
|
|
json: {
|
|
|
|
"AAAAAAAA": 3,
|
|
|
|
"BBBBBBBB": 3
|
|
|
|
}
|
|
|
|
});
|
2015-07-20 21:27:55 +00:00
|
|
|
setResponse({
|
|
|
|
method: "GET",
|
|
|
|
url: "users/1/collections?format=json&collectionKey=AAAAAAAA",
|
|
|
|
status: 200,
|
|
|
|
headers: headers,
|
|
|
|
json: [
|
|
|
|
makeCollectionJSON({
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
version: 1,
|
|
|
|
name: "A"
|
|
|
|
})
|
|
|
|
]
|
|
|
|
});
|
|
|
|
setResponse({
|
|
|
|
method: "GET",
|
|
|
|
url: "users/1/searches?format=json&searchKey=AAAAAAAA",
|
|
|
|
status: 200,
|
|
|
|
headers: headers,
|
|
|
|
json: [
|
|
|
|
makeSearchJSON({
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
version: 2,
|
|
|
|
name: "A"
|
|
|
|
})
|
|
|
|
]
|
|
|
|
});
|
|
|
|
setResponse({
|
|
|
|
method: "GET",
|
|
|
|
url: "users/1/items?format=json&itemKey=AAAAAAAA&includeTrashed=1",
|
|
|
|
status: 200,
|
|
|
|
headers: headers,
|
|
|
|
json: [
|
|
|
|
makeItemJSON({
|
|
|
|
key: "AAAAAAAA",
|
|
|
|
version: 3,
|
|
|
|
itemType: "book",
|
|
|
|
title: "A"
|
|
|
|
})
|
|
|
|
]
|
|
|
|
});
|
2016-01-14 06:50:13 +00:00
|
|
|
setResponse({
|
|
|
|
method: "GET",
|
|
|
|
url: "users/1/items?format=json&itemKey=BBBBBBBB&includeTrashed=1",
|
|
|
|
status: 200,
|
|
|
|
headers: headers,
|
|
|
|
json: [
|
|
|
|
makeItemJSON({
|
|
|
|
key: "BBBBBBBB",
|
|
|
|
version: 3,
|
|
|
|
itemType: "note",
|
|
|
|
parentItem: "AAAAAAAA",
|
|
|
|
note: "This is a note."
|
|
|
|
})
|
|
|
|
]
|
|
|
|
});
|
2015-07-20 21:27:55 +00:00
|
|
|
setResponse({
|
|
|
|
method: "GET",
|
|
|
|
url: "users/1/deleted?since=0",
|
|
|
|
status: 200,
|
|
|
|
headers: headers,
|
|
|
|
json: {}
|
|
|
|
});
|
|
|
|
yield engine.start();
|
|
|
|
|
|
|
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
|
|
|
|
// Check local library version
|
|
|
|
assert.equal(Zotero.Libraries.getVersion(userLibraryID), 3);
|
|
|
|
|
|
|
|
// Make sure local objects exist
|
2016-03-14 21:10:18 +00:00
|
|
|
var setting = Zotero.SyncedSettings.get(userLibraryID, "tagColors");
|
2015-07-20 21:27:55 +00:00
|
|
|
assert.lengthOf(setting, 1);
|
|
|
|
assert.equal(setting[0].name, 'A');
|
2016-03-14 21:10:18 +00:00
|
|
|
var settingMetadata = Zotero.SyncedSettings.getMetadata(userLibraryID, "tagColors");
|
2015-07-20 21:27:55 +00:00
|
|
|
assert.equal(settingMetadata.version, 2);
|
|
|
|
assert.isTrue(settingMetadata.synced);
|
|
|
|
|
|
|
|
var obj = yield Zotero.Collections.getByLibraryAndKeyAsync(userLibraryID, "AAAAAAAA");
|
|
|
|
assert.equal(obj.name, 'A');
|
|
|
|
assert.equal(obj.version, 1);
|
|
|
|
assert.isTrue(obj.synced);
|
2016-02-29 09:23:00 +00:00
|
|
|
yield assertInCache(obj);
|
2015-07-20 21:27:55 +00:00
|
|
|
|
|
|
|
obj = yield Zotero.Searches.getByLibraryAndKeyAsync(userLibraryID, "AAAAAAAA");
|
|
|
|
assert.equal(obj.name, 'A');
|
|
|
|
assert.equal(obj.version, 2);
|
|
|
|
assert.isTrue(obj.synced);
|
2016-02-29 09:23:00 +00:00
|
|
|
yield assertInCache(obj);
|
2015-07-20 21:27:55 +00:00
|
|
|
|
|
|
|
obj = yield Zotero.Items.getByLibraryAndKeyAsync(userLibraryID, "AAAAAAAA");
|
|
|
|
assert.equal(obj.getField('title'), 'A');
|
|
|
|
assert.equal(obj.version, 3);
|
|
|
|
assert.isTrue(obj.synced);
|
2016-01-14 06:50:13 +00:00
|
|
|
var parentItemID = obj.id;
|
2016-02-29 09:23:00 +00:00
|
|
|
yield assertInCache(obj);
|
2016-01-14 06:50:13 +00:00
|
|
|
|
|
|
|
obj = yield Zotero.Items.getByLibraryAndKeyAsync(userLibraryID, "BBBBBBBB");
|
|
|
|
assert.equal(obj.getNote(), 'This is a note.');
|
|
|
|
assert.equal(obj.parentItemID, parentItemID);
|
|
|
|
assert.equal(obj.version, 3);
|
|
|
|
assert.isTrue(obj.synced);
|
2016-02-29 09:23:00 +00:00
|
|
|
yield assertInCache(obj);
|
2015-07-20 21:27:55 +00:00
|
|
|
})
|
|
|
|
|
2015-07-22 09:21:32 +00:00
|
|
|
it("should upload new full items and subsequent patches", function* () {
|
|
|
|
({ engine, client, caller } = yield setup());
|
|
|
|
|
|
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var lastLibraryVersion = 5;
|
|
|
|
yield Zotero.Libraries.setVersion(libraryID, lastLibraryVersion);
|
|
|
|
|
2016-04-19 09:22:16 +00:00
|
|
|
yield Zotero.SyncedSettings.set(libraryID, "testSetting1", { foo: "bar" });
|
|
|
|
yield Zotero.SyncedSettings.set(libraryID, "testSetting2", { bar: "foo" });
|
|
|
|
|
2015-07-22 09:21:32 +00:00
|
|
|
var types = Zotero.DataObjectUtilities.getTypes();
|
|
|
|
var objects = {};
|
|
|
|
var objectResponseJSON = {};
|
|
|
|
var objectVersions = {};
|
|
|
|
for (let type of types) {
|
|
|
|
objects[type] = [yield createDataObject(type, { setTitle: true })];
|
|
|
|
objectVersions[type] = {};
|
Deasyncification :back: :cry:
While trying to get translation and citing working with asynchronously
generated data, we realized that drag-and-drop support was going to
be...problematic. Firefox only supports synchronous methods for
providing drag data (unlike, it seems, the DataTransferItem interface
supported by Chrome), which means that we'd need to preload all relevant
data on item selection (bounded by export.quickCopy.dragLimit) and keep
the translate/cite methods synchronous (or maintain two separate
versions).
What we're trying instead is doing what I said in #518 we weren't going
to do: loading most object data on startup and leaving many more
functions synchronous. Essentially, this takes the various load*()
methods described in #518, moves them to startup, and makes them operate
on entire libraries rather than individual objects.
The obvious downside here (other than undoing much of the work of the
last many months) is that it increases startup time, potentially quite a
lot for larger libraries. On my laptop, with a 3,000-item library, this
adds about 3 seconds to startup time. I haven't yet tested with larger
libraries. But I'm hoping that we can optimize this further to reduce
that delay. Among other things, this is loading data for all libraries,
when it should be able to load data only for the library being viewed.
But this is also fundamentally just doing some SELECT queries and
storing the results, so it really shouldn't need to be that slow (though
performance may be bounded a bit here by XPCOM overhead).
If we can make this fast enough, it means that third-party plugins
should be able to remain much closer to their current designs. (Some
things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
|
|
|
objectResponseJSON[type] = objects[type].map(o => o.toResponseJSON());
|
2015-07-22 09:21:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
server.respond(function (req) {
|
|
|
|
if (req.method == "POST") {
|
|
|
|
assert.equal(
|
|
|
|
req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion
|
|
|
|
);
|
|
|
|
|
2016-04-19 09:22:16 +00:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2015-07-22 09:21:32 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
yield engine.start();
|
|
|
|
|
2016-04-19 09:22:16 +00:00
|
|
|
yield Zotero.SyncedSettings.set(libraryID, "testSetting2", { bar: "bar" });
|
|
|
|
|
2015-07-22 09:21:32 +00:00
|
|
|
assert.equal(Zotero.Libraries.getVersion(libraryID), lastLibraryVersion);
|
|
|
|
for (let type of types) {
|
|
|
|
// Make sure objects were set to the correct version and marked as synced
|
2015-11-01 08:43:04 +00:00
|
|
|
assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(type, libraryID)), 0);
|
2015-07-22 09:21:32 +00:00
|
|
|
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 = yield Zotero.Sync.Data.Local.getCacheObject(type, libraryID, key, version);
|
|
|
|
assert.typeOf(cached, 'object');
|
|
|
|
assert.equal(cached.key, key);
|
|
|
|
assert.equal(cached.version, version);
|
|
|
|
|
|
|
|
yield modifyDataObject(objects[type][0]);
|
|
|
|
}
|
|
|
|
|
|
|
|
({ engine, client, caller } = yield setup());
|
|
|
|
|
|
|
|
server.respond(function (req) {
|
|
|
|
if (req.method == "POST") {
|
|
|
|
assert.equal(
|
|
|
|
req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion
|
|
|
|
);
|
|
|
|
|
2016-04-19 09:22:16 +00:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2015-07-22 09:21:32 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
yield engine.start();
|
|
|
|
|
|
|
|
assert.equal(Zotero.Libraries.getVersion(libraryID), lastLibraryVersion);
|
|
|
|
for (let type of types) {
|
|
|
|
// Make sure objects were set to the correct version and marked as synced
|
2015-11-01 08:43:04 +00:00
|
|
|
assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(type, libraryID)), 0);
|
2015-07-22 09:21:32 +00:00
|
|
|
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 = yield 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':
|
2016-03-26 06:59:54 +00:00
|
|
|
assert.isArray(cached.data.conditions);
|
2015-07-22 09:21:32 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2016-04-26 00:16:31 +00:00
|
|
|
|
|
|
|
it("should upload child item after parent item", function* () {
|
|
|
|
({ engine, client, caller } = yield setup());
|
|
|
|
|
|
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var lastLibraryVersion = 5;
|
|
|
|
yield Zotero.Libraries.setVersion(libraryID, lastLibraryVersion);
|
|
|
|
|
|
|
|
// Create top-level note, book, and child note
|
|
|
|
var item1 = new Zotero.Item('note');
|
|
|
|
item1.setNote('A');
|
|
|
|
yield item1.saveTx();
|
|
|
|
var item2 = yield createDataObject('item');
|
|
|
|
var item3 = new Zotero.Item('note');
|
|
|
|
item3.parentItemID = item2.id;
|
|
|
|
item3.setNote('B');
|
|
|
|
yield item3.saveTx();
|
|
|
|
// Move note under parent
|
|
|
|
item1.parentItemID = item2.id;
|
|
|
|
yield item1.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, 3);
|
|
|
|
assert.equal(json[0].key, item2.key);
|
|
|
|
assert.equal(json[1].key, item1.key);
|
|
|
|
assert.equal(json[2].key, item3.key);
|
|
|
|
handled = true;
|
|
|
|
req.respond(
|
|
|
|
200,
|
|
|
|
{
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
"Last-Modified-Version": ++lastLibraryVersion
|
|
|
|
},
|
|
|
|
JSON.stringify({
|
|
|
|
successful: {
|
|
|
|
"0": item2.toResponseJSON(),
|
|
|
|
"1": item1.toResponseJSON(),
|
|
|
|
"2": item3.toResponseJSON()
|
|
|
|
},
|
|
|
|
unchanged: {},
|
|
|
|
failed: {}
|
|
|
|
})
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
yield engine.start();
|
|
|
|
assert.isTrue(handled);
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("should upload child collection after parent collection", function* () {
|
|
|
|
({ engine, client, caller } = yield setup());
|
|
|
|
|
|
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var lastLibraryVersion = 5;
|
|
|
|
yield Zotero.Libraries.setVersion(libraryID, lastLibraryVersion);
|
|
|
|
|
|
|
|
var collection1 = yield createDataObject('collection');
|
|
|
|
var collection2 = yield createDataObject('collection');
|
|
|
|
var collection3 = yield createDataObject('collection', { parentID: collection2.id });
|
|
|
|
// Move collection under the other
|
|
|
|
collection1.parentID = collection2.id;
|
|
|
|
yield 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;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
yield engine.start();
|
|
|
|
assert.isTrue(handled);
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2015-12-23 09:52:09 +00:00
|
|
|
it("should upload synced storage properties", function* () {
|
|
|
|
({ engine, client, caller } = yield setup());
|
|
|
|
|
|
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var lastLibraryVersion = 2;
|
|
|
|
yield Zotero.Libraries.setVersion(libraryID, lastLibraryVersion);
|
|
|
|
|
|
|
|
var item = new Zotero.Item('attachment');
|
|
|
|
item.attachmentLinkMode = 'imported_file';
|
|
|
|
item.attachmentFilename = 'test1.txt';
|
|
|
|
yield item.saveTx();
|
|
|
|
|
|
|
|
var mtime = new Date().getTime();
|
|
|
|
var md5 = '57f8a4fda823187b91e1191487b87fe6';
|
|
|
|
|
Deasyncification :back: :cry:
While trying to get translation and citing working with asynchronously
generated data, we realized that drag-and-drop support was going to
be...problematic. Firefox only supports synchronous methods for
providing drag data (unlike, it seems, the DataTransferItem interface
supported by Chrome), which means that we'd need to preload all relevant
data on item selection (bounded by export.quickCopy.dragLimit) and keep
the translate/cite methods synchronous (or maintain two separate
versions).
What we're trying instead is doing what I said in #518 we weren't going
to do: loading most object data on startup and leaving many more
functions synchronous. Essentially, this takes the various load*()
methods described in #518, moves them to startup, and makes them operate
on entire libraries rather than individual objects.
The obvious downside here (other than undoing much of the work of the
last many months) is that it increases startup time, potentially quite a
lot for larger libraries. On my laptop, with a 3,000-item library, this
adds about 3 seconds to startup time. I haven't yet tested with larger
libraries. But I'm hoping that we can optimize this further to reduce
that delay. Among other things, this is loading data for all libraries,
when it should be able to load data only for the library being viewed.
But this is also fundamentally just doing some SELECT queries and
storing the results, so it really shouldn't need to be that slow (though
performance may be bounded a bit here by XPCOM overhead).
If we can make this fast enough, it means that third-party plugins
should be able to remain much closer to their current designs. (Some
things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
|
|
|
item.attachmentSyncedModificationTime = mtime;
|
|
|
|
item.attachmentSyncedHash = md5;
|
|
|
|
yield item.saveTx({ skipAll: true });
|
2015-12-23 09:52:09 +00:00
|
|
|
|
Deasyncification :back: :cry:
While trying to get translation and citing working with asynchronously
generated data, we realized that drag-and-drop support was going to
be...problematic. Firefox only supports synchronous methods for
providing drag data (unlike, it seems, the DataTransferItem interface
supported by Chrome), which means that we'd need to preload all relevant
data on item selection (bounded by export.quickCopy.dragLimit) and keep
the translate/cite methods synchronous (or maintain two separate
versions).
What we're trying instead is doing what I said in #518 we weren't going
to do: loading most object data on startup and leaving many more
functions synchronous. Essentially, this takes the various load*()
methods described in #518, moves them to startup, and makes them operate
on entire libraries rather than individual objects.
The obvious downside here (other than undoing much of the work of the
last many months) is that it increases startup time, potentially quite a
lot for larger libraries. On my laptop, with a 3,000-item library, this
adds about 3 seconds to startup time. I haven't yet tested with larger
libraries. But I'm hoping that we can optimize this further to reduce
that delay. Among other things, this is loading data for all libraries,
when it should be able to load data only for the library being viewed.
But this is also fundamentally just doing some SELECT queries and
storing the results, so it really shouldn't need to be that slow (though
performance may be bounded a bit here by XPCOM overhead).
If we can make this fast enough, it means that third-party plugins
should be able to remain much closer to their current designs. (Some
things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
|
|
|
var itemResponseJSON = item.toResponseJSON();
|
2015-12-23 09:52:09 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
yield engine.start();
|
|
|
|
|
|
|
|
// Check data in cache
|
|
|
|
var json = yield Zotero.Sync.Data.Local.getCacheObject(
|
|
|
|
'item', libraryID, item.key, lastLibraryVersion
|
|
|
|
);
|
|
|
|
assert.equal(json.data.mtime, mtime);
|
|
|
|
assert.equal(json.data.md5, md5);
|
|
|
|
})
|
|
|
|
|
2015-07-22 09:21:32 +00:00
|
|
|
it("should update local objects with remotely saved version after uploading if necessary", function* () {
|
|
|
|
({ engine, client, caller } = yield setup());
|
|
|
|
|
|
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var lastLibraryVersion = 5;
|
|
|
|
yield Zotero.Libraries.setVersion(libraryID, lastLibraryVersion);
|
|
|
|
|
|
|
|
var types = Zotero.DataObjectUtilities.getTypes();
|
|
|
|
var objects = {};
|
|
|
|
var objectResponseJSON = {};
|
|
|
|
var objectNames = {};
|
|
|
|
for (let type of types) {
|
|
|
|
objects[type] = [yield createDataObject(type, { setTitle: true })];
|
|
|
|
objectNames[type] = {};
|
Deasyncification :back: :cry:
While trying to get translation and citing working with asynchronously
generated data, we realized that drag-and-drop support was going to
be...problematic. Firefox only supports synchronous methods for
providing drag data (unlike, it seems, the DataTransferItem interface
supported by Chrome), which means that we'd need to preload all relevant
data on item selection (bounded by export.quickCopy.dragLimit) and keep
the translate/cite methods synchronous (or maintain two separate
versions).
What we're trying instead is doing what I said in #518 we weren't going
to do: loading most object data on startup and leaving many more
functions synchronous. Essentially, this takes the various load*()
methods described in #518, moves them to startup, and makes them operate
on entire libraries rather than individual objects.
The obvious downside here (other than undoing much of the work of the
last many months) is that it increases startup time, potentially quite a
lot for larger libraries. On my laptop, with a 3,000-item library, this
adds about 3 seconds to startup time. I haven't yet tested with larger
libraries. But I'm hoping that we can optimize this further to reduce
that delay. Among other things, this is loading data for all libraries,
when it should be able to load data only for the library being viewed.
But this is also fundamentally just doing some SELECT queries and
storing the results, so it really shouldn't need to be that slow (though
performance may be bounded a bit here by XPCOM overhead).
If we can make this fast enough, it means that third-party plugins
should be able to remain much closer to their current designs. (Some
things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
|
|
|
objectResponseJSON[type] = objects[type].map(o => o.toResponseJSON());
|
2015-07-22 09:21:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
yield engine.start();
|
|
|
|
|
|
|
|
assert.equal(Zotero.Libraries.getVersion(libraryID), lastLibraryVersion);
|
|
|
|
for (let type of types) {
|
|
|
|
// Make sure local objects were updated with new metadata and marked as synced
|
2015-11-01 08:43:04 +00:00
|
|
|
assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(type, libraryID)), 0);
|
2015-07-22 09:21:32 +00:00
|
|
|
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'));
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
assert.equal(name, o.name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2015-11-01 08:43:04 +00:00
|
|
|
it("should upload local deletions", function* () {
|
|
|
|
var { engine, client, caller } = yield setup();
|
|
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var lastLibraryVersion = 5;
|
|
|
|
yield Zotero.Libraries.setVersion(libraryID, lastLibraryVersion);
|
|
|
|
|
|
|
|
var types = Zotero.DataObjectUtilities.getTypes();
|
|
|
|
var objects = {};
|
|
|
|
for (let type of types) {
|
|
|
|
let obj1 = yield createDataObject(type);
|
|
|
|
let obj2 = yield createDataObject(type);
|
|
|
|
objects[type] = [obj1.key, obj2.key];
|
|
|
|
yield obj1.eraseTx();
|
|
|
|
yield 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
yield engine.start();
|
|
|
|
|
|
|
|
assert.equal(count, 0);
|
|
|
|
for (let type of types) {
|
|
|
|
yield assert.eventually.lengthOf(
|
|
|
|
Zotero.Sync.Data.Local.getDeleted(type, libraryID), 0
|
|
|
|
);
|
|
|
|
}
|
|
|
|
assert.equal(
|
|
|
|
Zotero.Libraries.get(libraryID).libraryVersion,
|
|
|
|
lastLibraryVersion
|
|
|
|
);
|
|
|
|
})
|
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
it("should make only one request if in sync", function* () {
|
|
|
|
yield Zotero.Libraries.setVersion(Zotero.Libraries.userLibraryID, 5);
|
|
|
|
({ engine, client, caller } = yield 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
yield engine.start();
|
|
|
|
})
|
2016-02-29 09:23:00 +00:00
|
|
|
|
|
|
|
it("should ignore errors when saving downloaded objects", function* () {
|
|
|
|
({ engine, client, caller } = yield setup());
|
|
|
|
engine.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?format=json&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 -- should be ignored
|
|
|
|
unknownField: 5
|
|
|
|
})
|
|
|
|
]
|
|
|
|
});
|
|
|
|
setResponse({
|
|
|
|
method: "GET",
|
|
|
|
url: "users/1/searches?format=json&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?format=json&itemKey=GGGGGGGG%2CHHHHHHHH&includeTrashed=1",
|
|
|
|
status: 200,
|
|
|
|
headers: headers,
|
|
|
|
json: [
|
|
|
|
makeItemJSON({
|
|
|
|
key: "GGGGGGGG",
|
|
|
|
version: 3,
|
|
|
|
itemType: "book",
|
|
|
|
title: "G",
|
|
|
|
// Unknown item field -- should be ignored
|
|
|
|
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?format=json&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");
|
|
|
|
yield engine.start();
|
|
|
|
|
|
|
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
|
|
|
|
// Library version should have been updated
|
|
|
|
assert.equal(Zotero.Libraries.getVersion(userLibraryID), 3);
|
|
|
|
|
|
|
|
// Check for saved objects
|
|
|
|
yield assert.eventually.ok(Zotero.Collections.getByLibraryAndKeyAsync(userLibraryID, "AAAAAAAA"));
|
|
|
|
yield assert.eventually.ok(Zotero.Searches.getByLibraryAndKeyAsync(userLibraryID, "DDDDDDDD"));
|
|
|
|
yield assert.eventually.ok(Zotero.Items.getByLibraryAndKeyAsync(userLibraryID, "GGGGGGGG"));
|
|
|
|
|
2016-04-23 04:59:20 +00:00
|
|
|
// Check for queued objects
|
2016-02-29 09:23:00 +00:00
|
|
|
var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('collection', userLibraryID);
|
|
|
|
assert.sameMembers(keys, ['BBBBBBBB']);
|
|
|
|
|
|
|
|
var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('search', userLibraryID);
|
|
|
|
assert.sameMembers(keys, ['EEEEEEEE', 'FFFFFFFF']);
|
|
|
|
|
|
|
|
var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', userLibraryID);
|
|
|
|
assert.sameMembers(keys, ['HHHHHHHH', 'JJJJJJJJ']);
|
|
|
|
|
|
|
|
assert.equal(spy.callCount, 3);
|
|
|
|
});
|
2015-07-20 21:27:55 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
describe("#_startDownload()", function () {
|
2016-02-29 09:23:00 +00:00
|
|
|
it("shouldn't redownload objects that are already up to date", function* () {
|
2015-07-20 21:27:55 +00:00
|
|
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
//yield Zotero.Libraries.setVersion(userLibraryID, 5);
|
|
|
|
({ engine, client, caller } = yield setup());
|
|
|
|
|
|
|
|
var objects = {};
|
|
|
|
for (let type of Zotero.DataObjectUtilities.getTypes()) {
|
|
|
|
let obj = objects[type] = createUnsavedDataObject(type);
|
|
|
|
obj.version = 5;
|
|
|
|
obj.synced = true;
|
|
|
|
yield obj.saveTx({ skipSyncedUpdate: true });
|
|
|
|
|
|
|
|
yield Zotero.Sync.Data.Local.saveCacheObjects(
|
|
|
|
type,
|
|
|
|
userLibraryID,
|
|
|
|
[
|
|
|
|
{
|
|
|
|
key: obj.key,
|
|
|
|
version: obj.version,
|
Deasyncification :back: :cry:
While trying to get translation and citing working with asynchronously
generated data, we realized that drag-and-drop support was going to
be...problematic. Firefox only supports synchronous methods for
providing drag data (unlike, it seems, the DataTransferItem interface
supported by Chrome), which means that we'd need to preload all relevant
data on item selection (bounded by export.quickCopy.dragLimit) and keep
the translate/cite methods synchronous (or maintain two separate
versions).
What we're trying instead is doing what I said in #518 we weren't going
to do: loading most object data on startup and leaving many more
functions synchronous. Essentially, this takes the various load*()
methods described in #518, moves them to startup, and makes them operate
on entire libraries rather than individual objects.
The obvious downside here (other than undoing much of the work of the
last many months) is that it increases startup time, potentially quite a
lot for larger libraries. On my laptop, with a 3,000-item library, this
adds about 3 seconds to startup time. I haven't yet tested with larger
libraries. But I'm hoping that we can optimize this further to reduce
that delay. Among other things, this is loading data for all libraries,
when it should be able to load data only for the library being viewed.
But this is also fundamentally just doing some SELECT queries and
storing the results, so it really shouldn't need to be that slow (though
performance may be bounded a bit here by XPCOM overhead).
If we can make this fast enough, it means that third-party plugins
should be able to remain much closer to their current designs. (Some
things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
|
|
|
data: obj.toJSON()
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2016-01-14 06:50:13 +00:00
|
|
|
setResponse({
|
|
|
|
method: "GET",
|
|
|
|
url: "users/1/items/top?format=versions&includeTrashed=1",
|
|
|
|
status: 200,
|
|
|
|
headers: headers,
|
|
|
|
json: json
|
|
|
|
});
|
|
|
|
json = {};
|
|
|
|
json[objects.item.key] = 5;
|
2015-07-20 21:27:55 +00:00
|
|
|
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: {}
|
|
|
|
});
|
|
|
|
|
|
|
|
yield engine._startDownload();
|
|
|
|
})
|
|
|
|
|
|
|
|
it("should apply remote deletions", function* () {
|
|
|
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
yield Zotero.Libraries.setVersion(userLibraryID, 5);
|
|
|
|
({ engine, client, caller } = yield setup());
|
|
|
|
|
|
|
|
// Create objects and mark them as synced
|
|
|
|
yield Zotero.SyncedSettings.set(
|
|
|
|
userLibraryID, 'tagColors', [{name: 'A', color: '#CC66CC'}], 1, true
|
|
|
|
);
|
|
|
|
var collection = createUnsavedDataObject('collection');
|
|
|
|
collection.synced = true;
|
|
|
|
var collectionID = yield collection.saveTx({ skipSyncedUpdate: true });
|
|
|
|
var collectionKey = collection.key;
|
|
|
|
var search = createUnsavedDataObject('search');
|
|
|
|
search.synced = true;
|
|
|
|
var searchID = yield search.saveTx({ skipSyncedUpdate: true });
|
|
|
|
var searchKey = search.key;
|
|
|
|
var item = createUnsavedDataObject('item');
|
|
|
|
item.synced = true;
|
|
|
|
var itemID = yield 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: {}
|
|
|
|
});
|
2016-01-14 06:50:13 +00:00
|
|
|
setResponse({
|
|
|
|
method: "GET",
|
|
|
|
url: "users/1/items/top?format=versions&since=5&includeTrashed=1",
|
|
|
|
status: 200,
|
|
|
|
headers: headers,
|
|
|
|
json: {}
|
|
|
|
});
|
2015-07-20 21:27:55 +00:00
|
|
|
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]
|
|
|
|
}
|
|
|
|
});
|
|
|
|
yield engine._startDownload();
|
|
|
|
|
|
|
|
// Make sure objects were deleted
|
2016-03-14 21:10:18 +00:00
|
|
|
assert.isNull(Zotero.SyncedSettings.get(userLibraryID, 'tagColors'));
|
2015-07-20 21:27:55 +00:00
|
|
|
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
|
2015-08-06 08:04:37 +00:00
|
|
|
assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted(
|
2015-07-20 21:27:55 +00:00
|
|
|
'setting', userLibraryID, 'tagColors'
|
|
|
|
));
|
2015-08-06 08:04:37 +00:00
|
|
|
assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted(
|
2015-07-20 21:27:55 +00:00
|
|
|
'collection', userLibraryID, collectionKey
|
|
|
|
));
|
2015-08-06 08:04:37 +00:00
|
|
|
assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted(
|
2015-07-20 21:27:55 +00:00
|
|
|
'search', userLibraryID, searchKey
|
|
|
|
));
|
2015-08-06 08:04:37 +00:00
|
|
|
assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted(
|
2015-07-20 21:27:55 +00:00
|
|
|
'item', userLibraryID, itemKey
|
|
|
|
));
|
|
|
|
})
|
|
|
|
|
|
|
|
it("should ignore remote deletions for non-item objects if local objects changed", function* () {
|
|
|
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
yield Zotero.Libraries.setVersion(userLibraryID, 5);
|
|
|
|
({ engine, client, caller } = yield setup());
|
|
|
|
|
|
|
|
// Create objects marked as unsynced
|
|
|
|
yield Zotero.SyncedSettings.set(
|
|
|
|
userLibraryID, 'tagColors', [{name: 'A', color: '#CC66CC'}]
|
|
|
|
);
|
|
|
|
var collection = createUnsavedDataObject('collection');
|
|
|
|
var collectionID = yield collection.saveTx();
|
|
|
|
var collectionKey = collection.key;
|
|
|
|
var search = createUnsavedDataObject('search');
|
|
|
|
var searchID = yield 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: {}
|
|
|
|
});
|
2016-01-14 06:50:13 +00:00
|
|
|
setResponse({
|
|
|
|
method: "GET",
|
|
|
|
url: "users/1/items/top?format=versions&since=5&includeTrashed=1",
|
|
|
|
status: 200,
|
|
|
|
headers: headers,
|
|
|
|
json: {}
|
|
|
|
});
|
2015-07-20 21:27:55 +00:00
|
|
|
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: []
|
|
|
|
}
|
|
|
|
});
|
|
|
|
yield engine._startDownload();
|
|
|
|
|
|
|
|
// Make sure objects weren't deleted
|
2016-03-14 21:10:18 +00:00
|
|
|
assert.ok(Zotero.SyncedSettings.get(userLibraryID, 'tagColors'));
|
2015-07-20 21:27:55 +00:00
|
|
|
assert.ok(Zotero.Collections.exists(collectionID));
|
|
|
|
assert.ok(Zotero.Searches.exists(searchID));
|
|
|
|
})
|
2015-08-06 10:03:17 +00:00
|
|
|
|
|
|
|
it("should show conflict resolution window for conflicting remote deletions", function* () {
|
|
|
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
yield Zotero.Libraries.setVersion(userLibraryID, 5);
|
|
|
|
({ engine, client, caller } = yield setup());
|
|
|
|
|
|
|
|
// Create local unsynced items
|
|
|
|
var item = createUnsavedDataObject('item');
|
|
|
|
item.setField('title', 'A');
|
|
|
|
item.synced = false;
|
|
|
|
var itemID1 = yield item.saveTx({ skipSyncedUpdate: true });
|
|
|
|
var itemKey1 = item.key;
|
|
|
|
|
|
|
|
item = createUnsavedDataObject('item');
|
|
|
|
item.setField('title', 'B');
|
|
|
|
item.synced = false;
|
|
|
|
var itemID2 = yield 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: {}
|
|
|
|
});
|
2016-01-14 06:50:13 +00:00
|
|
|
setResponse({
|
|
|
|
method: "GET",
|
|
|
|
url: "users/1/items/top?format=versions&since=5&includeTrashed=1",
|
|
|
|
status: 200,
|
|
|
|
headers: headers,
|
|
|
|
json: {}
|
|
|
|
});
|
2015-08-06 10:03:17 +00:00
|
|
|
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]
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
|
|
|
|
var doc = dialog.document;
|
|
|
|
var wizard = doc.documentElement;
|
|
|
|
var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[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();
|
|
|
|
})
|
|
|
|
yield engine._startDownload();
|
|
|
|
|
|
|
|
assert.isFalse(Zotero.Items.exists(itemID1));
|
|
|
|
assert.isTrue(Zotero.Items.exists(itemID2));
|
|
|
|
})
|
2016-04-27 09:41:51 +00:00
|
|
|
|
|
|
|
it("should handle cancellation of conflict resolution window", function* () {
|
|
|
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
yield Zotero.Libraries.setVersion(userLibraryID, 5);
|
|
|
|
({ engine, client, caller } = yield setup());
|
|
|
|
|
|
|
|
var item = yield createDataObject('item');
|
|
|
|
var itemID = yield 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?format=json&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: []
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
waitForWindow('chrome://zotero/content/merge.xul', function (dialog) {
|
|
|
|
var doc = dialog.document;
|
|
|
|
var wizard = doc.documentElement;
|
|
|
|
wizard.getButton('cancel').click();
|
|
|
|
})
|
|
|
|
yield engine._startDownload();
|
|
|
|
|
|
|
|
// Non-conflicted item should be saved
|
|
|
|
assert.ok(Zotero.Items.getIDFromLibraryAndKey(userLibraryID, "AAAAAAAA"));
|
|
|
|
|
|
|
|
// Conflicted item should be skipped and in queue
|
|
|
|
assert.isFalse(Zotero.Items.exists(itemID));
|
|
|
|
var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', userLibraryID);
|
|
|
|
assert.sameMembers(keys, [itemKey]);
|
|
|
|
});
|
2015-07-20 21:27:55 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
describe("#_upgradeCheck()", function () {
|
|
|
|
it("should upgrade a library last synced with the classic sync architecture", function* () {
|
|
|
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
({ engine, client, caller } = yield setup());
|
|
|
|
|
|
|
|
yield Zotero.Items.erase([1, 2], { skipDeleteLog: true });
|
|
|
|
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] = [yield createDataObject(type)];
|
|
|
|
}
|
|
|
|
|
|
|
|
var time1 = "2015-05-01 01:23:45";
|
|
|
|
yield Zotero.DB.queryAsync("UPDATE collections SET clientDateModified=?", time1);
|
|
|
|
yield Zotero.DB.queryAsync("UPDATE savedSearches SET clientDateModified=?", time1);
|
|
|
|
yield 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(yield 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)
|
|
|
|
);
|
|
|
|
yield 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 = yield 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) {
|
2015-11-01 08:43:04 +00:00
|
|
|
var synced = yield Zotero.Sync.Data.Local.getSynced(type, userLibraryID);
|
2015-07-20 21:27:55 +00:00
|
|
|
assert.deepEqual(synced, [objects[type][0].key]);
|
|
|
|
assert.equal(objects[type][0].version, 10);
|
2015-11-01 08:43:04 +00:00
|
|
|
var unsynced = yield Zotero.Sync.Data.Local.getUnsynced(type, userLibraryID);
|
2015-07-20 21:27:55 +00:00
|
|
|
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", function* () {
|
|
|
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
({ engine, client, caller } = yield setup());
|
|
|
|
|
|
|
|
var types = Zotero.DataObjectUtilities.getTypes();
|
|
|
|
var objects = {};
|
|
|
|
var objectJSON = {};
|
|
|
|
for (let type of types) {
|
|
|
|
objectJSON[type] = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let type of types) {
|
2015-08-02 07:42:03 +00:00
|
|
|
// Create object with outdated version, which should be updated
|
2015-07-20 21:27:55 +00:00
|
|
|
let obj = createUnsavedDataObject(type);
|
|
|
|
obj.synced = true;
|
|
|
|
obj.version = 5;
|
|
|
|
yield obj.saveTx();
|
|
|
|
objects[type] = [obj];
|
|
|
|
|
|
|
|
objectJSON[type].push(makeJSONFunctions[type]({
|
|
|
|
key: obj.key,
|
|
|
|
version: 20,
|
|
|
|
name: Zotero.Utilities.randomString()
|
|
|
|
}));
|
|
|
|
|
2015-08-02 07:42:03 +00:00
|
|
|
// Create JSON for object that exists remotely and not locally,
|
2015-07-20 21:27:55 +00:00
|
|
|
// which should be downloaded
|
|
|
|
objectJSON[type].push(makeJSONFunctions[type]({
|
|
|
|
key: Zotero.DataObjectUtilities.generateKey(),
|
|
|
|
version: 20,
|
|
|
|
name: Zotero.Utilities.randomString()
|
|
|
|
}));
|
|
|
|
|
2015-08-02 07:42:03 +00:00
|
|
|
// Create object marked as synced that doesn't exist remotely,
|
2015-07-20 21:27:55 +00:00
|
|
|
// which should be flagged for upload
|
|
|
|
obj = createUnsavedDataObject(type);
|
|
|
|
obj.synced = true;
|
|
|
|
obj.version = 10;
|
|
|
|
yield obj.saveTx();
|
|
|
|
objects[type].push(obj);
|
2015-08-02 07:42:03 +00:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
yield obj.saveTx();
|
|
|
|
objects[type].push(obj);
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2015-08-02 07:42:03 +00:00
|
|
|
let deletedJSON = {};
|
2015-07-20 21:27:55 +00:00
|
|
|
for (let type of types) {
|
2015-08-02 07:42:03 +00:00
|
|
|
let suffix = type == 'item' ? '&includeTrashed=1' : '';
|
|
|
|
let plural = Zotero.DataObjectUtilities.getObjectTypePlural(type);
|
2015-07-20 21:27:55 +00:00
|
|
|
|
|
|
|
var json = {};
|
|
|
|
json[objectJSON[type][0].key] = objectJSON[type][0].version;
|
|
|
|
json[objectJSON[type][1].key] = objectJSON[type][1].version;
|
|
|
|
setResponse({
|
|
|
|
method: "GET",
|
2015-08-02 07:42:03 +00:00
|
|
|
url: "users/1/" + plural
|
2015-07-20 21:27:55 +00:00
|
|
|
+ "?format=versions" + suffix,
|
|
|
|
status: 200,
|
|
|
|
headers: headers,
|
|
|
|
json: json
|
|
|
|
});
|
|
|
|
|
|
|
|
setResponse({
|
|
|
|
method: "GET",
|
2015-08-02 07:42:03 +00:00
|
|
|
url: "users/1/" + plural
|
2015-07-20 21:27:55 +00:00
|
|
|
+ "?format=json"
|
|
|
|
+ "&" + type + "Key=" + objectJSON[type][0].key + "%2C" + objectJSON[type][1].key
|
|
|
|
+ suffix,
|
|
|
|
status: 200,
|
|
|
|
headers: headers,
|
|
|
|
json: objectJSON[type]
|
|
|
|
});
|
2015-08-02 07:42:03 +00:00
|
|
|
|
|
|
|
deletedJSON[plural] = [objects[type][2].key];
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
2015-08-02 07:42:03 +00:00
|
|
|
setResponse({
|
|
|
|
method: "GET",
|
|
|
|
url: "users/1/deleted?since=0",
|
|
|
|
status: 200,
|
|
|
|
headers: headers,
|
|
|
|
json: deletedJSON
|
|
|
|
});
|
2015-07-20 21:27:55 +00:00
|
|
|
yield engine._fullSync();
|
|
|
|
|
|
|
|
// Check settings
|
2016-03-14 21:10:18 +00:00
|
|
|
var setting = Zotero.SyncedSettings.get(userLibraryID, "tagColors");
|
2015-07-20 21:27:55 +00:00
|
|
|
assert.lengthOf(setting, 1);
|
|
|
|
assert.equal(setting[0].name, 'A');
|
2016-03-14 21:10:18 +00:00
|
|
|
var settingMetadata = Zotero.SyncedSettings.getMetadata(userLibraryID, "tagColors");
|
2015-07-20 21:27:55 +00:00
|
|
|
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);
|
2016-02-29 09:23:00 +00:00
|
|
|
yield assertInCache(obj);
|
2015-07-20 21:27:55 +00:00
|
|
|
|
|
|
|
// 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);
|
2015-08-02 07:42:03 +00:00
|
|
|
|
|
|
|
// JSON objects 3 should be deleted and not in the delete log
|
|
|
|
assert.isFalse(objectsClass.getByLibraryAndKey(userLibraryID, objects[type][2].key));
|
2015-08-06 08:04:37 +00:00
|
|
|
assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted(
|
2015-08-02 07:42:03 +00:00
|
|
|
type, userLibraryID, objects[type][2].key
|
|
|
|
));
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|