Support 'successful' property in upload response

Save uploaded data to cache, and update local object if necessary (which
it mostly shouldn't be except for invalid characters and HTML filtering
in notes)

Also add some upload and JSON tests
This commit is contained in:
Dan Stillman 2015-07-22 05:21:32 -04:00
parent 70d9b9870c
commit 4600318ad7
12 changed files with 578 additions and 187 deletions

View file

@ -678,14 +678,43 @@ Zotero.Collection.prototype.fromJSON = Zotero.Promise.coroutine(function* (json)
});
Zotero.Collection.prototype.toJSON = Zotero.Promise.coroutine(function* (options) {
var obj = {};
Zotero.Collection.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options = {}) {
var json = yield this.constructor._super.prototype.toResponseJSON.apply(this, options);
// TODO: library block?
// creatorSummary
var firstCreator = this.getField('firstCreator');
if (firstCreator) {
json.meta.creatorSummary = firstCreator;
}
// parsedDate
var parsedDate = Zotero.Date.multipartToSQL(this.getField('date', true, true));
if (parsedDate) {
// 0000?
json.meta.parsedDate = parsedDate;
}
// numChildren
if (this.isRegularItem()) {
json.meta.numChildren = this.numChildren();
}
return json;
})
Zotero.Collection.prototype.toJSON = Zotero.Promise.coroutine(function* (options = {}) {
var env = this._preToJSON(options);
var mode = env.mode;
var obj = env.obj = {};
obj.key = this.key;
obj.version = this.version;
obj.name = this.name;
obj.parentCollection = this.parentKey ? this.parentKey : false;
obj.relations = {}; // TEMP
return obj;
return this._postToJSON(env);
});

View file

@ -1190,6 +1190,47 @@ Zotero.DataObject.prototype._finalizeErase = function (env) {
}
}
Zotero.DataObject.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options) {
// TODO: library block?
return {
key: this.key,
version: this.version,
meta: {},
data: yield this.toJSON(options)
};
});
Zotero.DataObject.prototype._preToJSON = function (options) {
var env = { options };
env.mode = options.mode || 'new';
if (env.mode == 'patch') {
if (!options.patchBase) {
throw new Error("Cannot use patch mode if patchBase not provided");
}
}
else if (options.patchBase) {
if (options.mode) {
Zotero.debug("Zotero.Item.toJSON: ignoring provided patchBase in " + env.mode + " mode", 2);
}
// If patchBase provided and no explicit mode, use 'patch'
else {
env.mode = 'patch';
}
}
return env;
}
Zotero.DataObject.prototype._postToJSON = function (env) {
if (env.mode == 'patch') {
env.obj = Zotero.DataObjectUtilities.patch(env.options.patchBase, env.obj);
}
return env.obj;
}
/**
* Generates data object key
* @return {String} key

View file

@ -101,6 +101,42 @@ Zotero.DataObjectUtilities = {
return Zotero[className]
},
patch: function (base, obj) {
var target = {};
Object.assign(target, obj);
for (let i in base) {
switch (i) {
case 'key':
case 'version':
case 'dateModified':
continue;
}
// If field from base exists in the new version, delete it if it's the same
if (i in target) {
if (!this._fieldChanged(i, base[i], target[i])) {
delete target[i];
}
}
// If field from base doesn't exist in new version, clear it
else {
switch (i) {
case 'deleted':
target[i] = false;
break;
default:
target[i] = '';
}
}
}
return target;
},
/**
* Determine whether two API JSON objects are equivalent
*
@ -129,24 +165,9 @@ Zotero.DataObjectUtilities = {
continue;
}
let changed;
switch (field) {
case 'creators':
case 'collections':
case 'tags':
case 'relations':
changed = this["_" + field + "Changed"](val1, val2);
if (changed) {
return true;
}
break;
default:
changed = val1 !== val2;
if (changed) {
return true;
}
let changed = this._fieldChanged(field, val1, val2);
if (changed) {
return true;
}
skipFields[field] = true;
@ -170,6 +191,20 @@ Zotero.DataObjectUtilities = {
return false;
},
_fieldChanged: function (fieldName, field1, field2) {
switch (fieldName) {
case 'collections':
case 'conditions':
case 'creators':
case 'tags':
case 'relations':
return this["_" + fieldName + "Changed"](field1, field2);
default:
return field1 !== field2;
}
},
_creatorsChanged: function (data1, data2) {
if (!data2 || data1.length != data2.length) return true;
for (let i = 0; i < data1.length; i++) {

View file

@ -534,7 +534,6 @@ Zotero.DataObjects.prototype._load = Zotero.Promise.coroutine(function* (library
return loaded;
}
// getPrimaryDataSQL() should use "O" for the primary table alias
var sql = this.primaryDataSQL;
var params = [];
if (libraryID !== false) {
@ -551,7 +550,7 @@ Zotero.DataObjects.prototype._load = Zotero.Promise.coroutine(function* (library
params,
{
onRow: function (row) {
var id = row.getResultByIndex(this._ZDO_id);
var id = row.getResultByName(this._ZDO_id);
var columns = Object.keys(this._primaryDataSQLParts);
var rowObj = {};
for (let i=0; i<columns.length; i++) {

View file

@ -4008,32 +4008,11 @@ Zotero.Item.prototype.fromJSON = Zotero.Promise.coroutine(function* (json) {
/**
* @param {Object} options
*/
Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options) {
options = options || {};
Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options = {}) {
var env = this._preToJSON(options);
var mode = env.mode;
if (options) {
var mode = options.mode;
}
else {
var mode = 'new';
}
if (mode == 'patch') {
if (!options.patchBase) {
throw new Error("Cannot use patch mode if patchBase not provided");
}
}
else if (options.patchBase) {
if (options.mode) {
Zotero.debug("Zotero.Item.toJSON: ignoring provided patchBase in " + mode + " mode", 2);
}
// If patchBase provided and no explicit mode, use 'patch'
else {
mode = 'patch';
}
}
var obj = {};
var obj = env.obj = {};
obj.key = this.key;
obj.version = this.version;
obj.itemType = Zotero.ItemTypes.getName(this.itemTypeID);
@ -4106,39 +4085,12 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options) {
obj.dateModified = Zotero.Date.sqlToISO8601(this.dateModified);
if (obj.accessDate) obj.accessDate = Zotero.Date.sqlToISO8601(obj.accessDate);
if (mode == 'patch') {
for (let i in options.patchBase) {
switch (i) {
case 'key':
case 'version':
case 'dateModified':
continue;
}
if (i in obj) {
if (obj[i] === options.patchBase[i]) {
delete obj[i];
}
}
else {
obj[i] = '';
}
}
}
return obj;
return this._postToJSON(env);
});
Zotero.Item.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options) {
var json = {
key: this.key,
version: this.version,
meta: {},
data: yield this.toJSON(options)
};
// TODO: library block?
Zotero.Item.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options = {}) {
var json = yield this.constructor._super.prototype.toResponseJSON.apply(this, options);
// creatorSummary
var firstCreator = this.getField('firstCreator');

View file

@ -822,15 +822,24 @@ Zotero.Search.prototype.fromJSON = Zotero.Promise.coroutine(function* (json) {
}
});
Zotero.Collection.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options = {}) {
var json = yield this.constructor._super.prototype.toResponseJSON.apply(this, options);
return json;
});
Zotero.Search.prototype.toJSON = Zotero.Promise.coroutine(function* (options) {
var obj = {};
Zotero.Search.prototype.toJSON = Zotero.Promise.coroutine(function* (options = {}) {
var env = this._preToJSON(options);
var mode = env.mode;
var obj = env.obj = {};
obj.key = this.key;
obj.version = this.version;
obj.name = this.name;
yield this.loadConditions();
obj.conditions = this.getConditions();
return obj;
return this._postToJSON(env);
});

View file

@ -269,8 +269,6 @@ Zotero.Sync.APIClient.prototype = {
uploadObjects: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, objectType, method, version, objects) {
throw new Error("Uploading disabled");
if (method != 'POST' && method != 'PATCH') {
throw new Error("Invalid method '" + method + "'");
}

View file

@ -550,9 +550,8 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi
let objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType);
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
let ids = objectIDs[objectType];
let queue = [];
for (let id of ids) {
for (let id of objectIDs[objectType]) {
queue.push({
id: id,
json: null,
@ -604,26 +603,53 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi
Zotero.debug(json);
libraryVersion = json.libraryVersion;
yield Zotero.Libraries.setVersion(this.libraryID, json.libraryVersion);
// Mark successful and unchanged objects as synced with new version
var toRemove = [];
for (let state of ['success', 'unchanged']) {
// Mark successful and unchanged objects as synced with new version,
// and save uploaded JSON to cache
let ids = [];
let toSave = [];
let toCache = [];
for (let state of ['successful', 'unchanged']) {
for (let index in json.results[state]) {
let key = json.results[state][index];
let current = json.results[state][index];
// 'successful' includes objects, not keys
let key = state == 'successful' ? current.key : current;
if (key != batch[index].key) {
throw new Error("Key mismatch (" + key + " != " + batch[index].key + ")");
}
let obj = yield objectsClass.getByLibraryAndKeyAsync(
this.libraryID, key, { noCache: true }
);
obj.version = json.libraryVersion;
yield Zotero.Sync.Data.Local.markObjectAsSynced(obj)
let obj = objectsClass.getByLibraryAndKey(this.libraryID, key, { noCache: true })
ids.push(obj.id);
if (state == 'successful') {
// Update local object with saved data if necessary
yield obj.fromJSON(current.data);
toSave.push(obj);
toCache.push(current);
}
else {
let j = yield obj.toJSON();
j.version = json.libraryVersion;
toCache.push(j);
}
numSuccessful++;
// Remove from batch to mark as successful
delete batch[index];
}
}
yield Zotero.Sync.Data.Local.saveCacheObjects(
objectType, this.libraryID, toCache
);
yield Zotero.DB.executeTransaction(function* () {
for (let i = 0; i < toSave.length; i++) {
yield toSave[i].save();
}
yield Zotero.Libraries.setVersion(this.libraryID, json.libraryVersion);
objectsClass.updateVersion(ids, json.libraryVersion);
objectsClass.updateSynced(ids, true);
}.bind(this));
// Handle failed objects
for (let index in json.results.failed) {

View file

@ -159,6 +159,27 @@ Zotero.Sync.Data.Local = {
throw new Error("'json' must be an array");
}
if (!jsonArray.length) {
Zotero.debug("No " + Zotero.DataObjectUtilities.getObjectTypePlural(objectType)
+ " to save to sync cache");
return;
}
jsonArray = jsonArray.map(o => {
if (o.key === undefined) {
throw new Error("Missing 'key' property in JSON");
}
if (o.version === undefined) {
throw new Error("Missing 'version' property in JSON");
}
// If direct data object passed, wrap in fake response object
return o.data === undefined ? {
key: o.key,
version: o.version,
data: o
} : o;
});
Zotero.debug("Saving to sync cache:");
Zotero.debug(jsonArray);
@ -174,12 +195,6 @@ Zotero.Sync.Data.Local = {
var params = [];
for (let i = 0; i < chunk.length; i++) {
let o = chunk[i];
if (o.key === undefined) {
throw new Error("Missing 'key' property in JSON");
}
if (o.version === undefined) {
throw new Error("Missing 'version' property in JSON");
}
params.push(libraryID, o.key, syncObjectTypeID, o.version, JSON.stringify(o));
}
return Zotero.DB.queryAsync(

View file

@ -260,12 +260,24 @@ var createGroup = Zotero.Promise.coroutine(function* (props) {
//
// Data objects
//
function createUnsavedDataObject(objectType, params) {
/**
* @param {String} objectType - 'collection', 'item', 'search'
* @param {Object} [params]
* @param {Integer} [params.libraryID]
* @param {String} [params.itemType] - Item type
* @param {String} [params.title] - Item title
* @param {Boolean} [params.setTitle] - Assign a random item title
* @param {String} [params.name] - Collection/search name
* @param {Integer} [params.parentID]
* @param {String} [params.parentKey]
* @param {Boolean} [params.synced]
* @param {Integer} [params.version]
*/
function createUnsavedDataObject(objectType, params = {}) {
if (!objectType) {
throw new Error("Object type not provided");
}
params = params || {};
if (objectType == 'item') {
var param = params.itemType || 'book';
}
@ -275,14 +287,14 @@ function createUnsavedDataObject(objectType, params) {
}
switch (objectType) {
case 'item':
if (params.title) {
obj.setField('title', params.title);
if (params.title !== undefined || params.setTitle) {
obj.setField('title', params.title !== undefined ? params.title : Zotero.Utilities.randomString());
}
break;
case 'collection':
case 'search':
obj.name = params.name !== undefined ? params.name : "Test";
obj.name = params.name !== undefined ? params.name : Zotero.Utilities.randomString();
break;
}
var allowedParams = ['parentID', 'parentKey', 'synced', 'version'];
@ -294,12 +306,32 @@ function createUnsavedDataObject(objectType, params) {
return obj;
}
var createDataObject = Zotero.Promise.coroutine(function* (objectType, params, saveOptions) {
var createDataObject = Zotero.Promise.coroutine(function* (objectType, params = {}, saveOptions) {
var obj = createUnsavedDataObject(objectType, params);
yield obj.saveTx(saveOptions);
return obj;
});
function getNameProperty(objectType) {
return objectType == 'item' ? 'title' : 'name';
}
var modifyDataObject = Zotero.Promise.coroutine(function* (obj, params = {}) {
switch (obj.objectType) {
case 'item':
yield obj.loadItemData();
obj.setField(
'title',
params.title !== undefined ? params.title : Zotero.Utilities.randomString()
);
break;
default:
obj.name = params.name !== undefined ? params.name : Zotero.Utilities.randomString();
}
return obj.saveTx();
});
/**
* Return a promise for the error thrown by a promise, or false if none
*/

View file

@ -709,56 +709,96 @@ describe("Zotero.Item", function () {
})
describe("#toJSON()", function () {
it("should output only fields with values in default mode", function* () {
var itemType = "book";
var title = "Test";
var item = new Zotero.Item(itemType);
item.setField("title", title);
var id = yield item.saveTx();
item = yield Zotero.Items.getAsync(id);
var json = yield item.toJSON();
assert.equal(json.itemType, itemType);
assert.equal(json.title, title);
assert.isUndefined(json.date);
assert.isUndefined(json.numPages);
})
it("should output all fields in 'full' mode", function* () {
var itemType = "book";
var title = "Test";
var item = new Zotero.Item(itemType);
item.setField("title", title);
var id = yield item.saveTx();
item = yield Zotero.Items.getAsync(id);
var json = yield item.toJSON({ mode: 'full' });
assert.equal(json.title, title);
assert.equal(json.date, "");
assert.equal(json.numPages, "");
})
it("should output only fields that differ in 'patch' mode", function* () {
var itemType = "book";
var title = "Test";
var date = "2015-05-12";
var item = new Zotero.Item(itemType);
item.setField("title", title);
var id = yield item.saveTx();
item = yield Zotero.Items.getAsync(id);
var patchBase = yield item.toJSON();
item.setField("date", date);
yield item.saveTx();
var json = yield item.toJSON({
patchBase: patchBase
describe("default mode", function () {
it("should output only fields with values", function* () {
var itemType = "book";
var title = "Test";
var item = new Zotero.Item(itemType);
item.setField("title", title);
var id = yield item.saveTx();
item = yield Zotero.Items.getAsync(id);
var json = yield item.toJSON();
assert.equal(json.itemType, itemType);
assert.equal(json.title, title);
assert.isUndefined(json.date);
assert.isUndefined(json.numPages);
})
})
describe("'full' mode", function () {
it("should output all fields", function* () {
var itemType = "book";
var title = "Test";
var item = new Zotero.Item(itemType);
item.setField("title", title);
var id = yield item.saveTx();
item = yield Zotero.Items.getAsync(id);
var json = yield item.toJSON({ mode: 'full' });
assert.equal(json.title, title);
assert.equal(json.date, "");
assert.equal(json.numPages, "");
})
})
describe("'patch' mode", function () {
it("should output only fields that differ", function* () {
var itemType = "book";
var title = "Test";
var date = "2015-05-12";
var item = new Zotero.Item(itemType);
item.setField("title", title);
var id = yield item.saveTx();
item = yield Zotero.Items.getAsync(id);
var patchBase = yield item.toJSON();
item.setField("date", date);
yield item.saveTx();
var json = yield item.toJSON({
patchBase: patchBase
})
assert.isUndefined(json.itemType);
assert.isUndefined(json.title);
assert.equal(json.date, date);
assert.isUndefined(json.numPages);
assert.isUndefined(json.deleted);
assert.isUndefined(json.creators);
assert.isUndefined(json.relations);
assert.isUndefined(json.tags);
})
it("should include changed 'deleted' field", function* () {
// True to false
var item = new Zotero.Item('book');
item.deleted = true;
var id = yield item.saveTx();
item = yield Zotero.Items.getAsync(id);
var patchBase = yield item.toJSON();
item.deleted = false;
var json = yield item.toJSON({
patchBase: patchBase
})
assert.isUndefined(json.title);
assert.isFalse(json.deleted);
// False to true
var item = new Zotero.Item('book');
item.deleted = false;
var id = yield item.saveTx();
item = yield Zotero.Items.getAsync(id);
var patchBase = yield item.toJSON();
item.deleted = true;
var json = yield item.toJSON({
patchBase: patchBase
})
assert.isUndefined(json.title);
assert.isTrue(json.deleted);
})
assert.isUndefined(json.itemType);
assert.isUndefined(json.title);
assert.equal(json.date, date);
assert.isUndefined(json.numPages);
})
})

View file

@ -122,39 +122,9 @@ describe("Zotero.Sync.Data.Engine", function () {
})
describe("Syncing", function () {
it("should perform a sync for a new library", function* () {
it("should download items into a new library", function* () {
({ engine, client, caller } = yield setup());
server.respond(function (req) {
if (req.method == "POST" && req.url == baseURL + "users/1/items") {
let ifUnmodifiedSince = req.requestHeaders["If-Unmodified-Since-Version"];
if (ifUnmodifiedSince == 0) {
req.respond(412, {}, "Library has been modified since specified version");
return;
}
if (ifUnmodifiedSince == 3) {
let json = JSON.parse(req.requestBody);
req.respond(
200,
{
"Content-Type": "application/json",
"Last-Modified-Version": 3
},
JSON.stringify({
success: {
"0": json[0].key,
"1": json[1].key
},
unchanged: {},
failed: {}
})
);
return;
}
}
})
var headers = {
"Last-Modified-Version": 3
};
@ -280,6 +250,251 @@ describe("Zotero.Sync.Data.Engine", function () {
assert.isTrue(obj.synced);
})
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);
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] = {};
objectResponseJSON[type] = yield Zotero.Promise.all(objects[type].map(o => o.toResponseJSON()));
}
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 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();
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
assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(libraryID, type)), 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 = 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
);
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
assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(libraryID, type)), 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 = 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':
assert.typeOf(cached.data.conditions, 'object');
break;
}
}
})
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] = {};
objectResponseJSON[type] = yield Zotero.Promise.all(objects[type].map(o => o.toResponseJSON()));
}
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
assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(libraryID, type)), 0);
let o = objects[type][0];
let key = o.key;
let version = o.version;
let name = objectNames[type][key];
if (type == 'item') {
yield o.loadItemData();
assert.equal(name, o.getField('title'));
}
else {
assert.equal(name, o.name);
}
}
})
it("should make only one request if in sync", function* () {
yield Zotero.Libraries.setVersion(Zotero.Libraries.userLibraryID, 5);
({ engine, client, caller } = yield setup());