zotero/test/tests/syncRunnerTest.js

1404 lines
38 KiB
JavaScript

"use strict";
describe("Zotero.Sync.Runner", function () {
Components.utils.import("resource://zotero/config.js");
var apiKey = Zotero.Utilities.randomString(24);
var baseURL = "http://local.zotero/";
var userLibraryID, runner, caller, server, stub, spy;
var responses = {
keyInfo: {
fullAccess: {
method: "GET",
url: "keys/current",
status: 200,
json: {
key: apiKey,
userID: 1,
username: "Username",
access: {
user: {
library: true,
files: true,
notes: true,
write: true
},
groups: {
all: {
library: true,
write: true
}
}
}
}
}
},
userGroups: {
groupVersions: {
method: "GET",
url: "users/1/groups?format=versions",
json: {
"1623562": 10,
"2694172": 11
}
},
groupVersionsEmpty: {
method: "GET",
url: "users/1/groups?format=versions",
json: {}
},
groupVersionsOnlyMemberGroup: {
method: "GET",
url: "users/1/groups?format=versions",
json: {
"2694172": 11
}
}
},
groups: {
ownerGroup: {
method: "GET",
url: "groups/1623562",
json: {
id: 1623562,
version: 10,
data: {
id: 1623562,
version: 10,
name: "Group Name",
description: "<p>Test group</p>",
owner: 1,
type: "Private",
libraryEditing: "members",
libraryReading: "all",
fileEditing: "members",
admins: [],
members: []
}
}
},
memberGroup: {
method: "GET",
url: "groups/2694172",
json: {
id: 2694172,
version: 11,
data: {
id: 2694172,
version: 11,
name: "Group Name 2",
description: "<p>Test group</p>",
owner: 123456,
type: "Private",
libraryEditing: "admins",
libraryReading: "all",
fileEditing: "admins",
admins: [],
members: [1]
}
}
}
}
};
//
// Helper functions
//
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: {}
});
setResponse({
method: "GET",
url: `${target}/fulltext?format=versions`,
status: 200,
headers,
json: {}
});
}
//
// Tests
//
beforeEach(function* () {
yield resetDB({
thisArg: this,
skipBundledFiles: true
});
userLibraryID = Zotero.Libraries.userLibraryID;
Zotero.HTTP.mock = sinon.FakeXMLHttpRequest;
server = sinon.fakeServer.create();
server.autoRespond = true;
runner = new Zotero.Sync.Runner_Module({ baseURL, apiKey });
Components.utils.import("resource://zotero/concurrentCaller.js");
caller = new ConcurrentCaller(1);
caller.setLogger(msg => Zotero.debug(msg));
caller.stopOnError = true;
caller.onError = function (e) {
Zotero.logError(e);
if (options.onError) {
options.onError(e);
}
if (e.fatal) {
caller.stop();
throw e;
}
};
yield Zotero.Users.setCurrentUserID(1);
yield Zotero.Users.setCurrentUsername("A");
})
afterEach(function () {
if (stub) stub.restore();
if (spy) spy.restore();
})
after(function () {
Zotero.HTTP.mock = null;
})
describe("#checkAccess()", function () {
it("should check key access", function* () {
setResponse('keyInfo.fullAccess');
var json = yield runner.checkAccess(runner.getAPIClient({ apiKey }));
var compare = {};
Object.assign(compare, responses.keyInfo.fullAccess.json);
delete compare.key;
assert.deepEqual(json, compare);
})
})
describe("#checkLibraries()", function () {
beforeEach(function* () {
Zotero.Prefs.clear('sync.librariesToSkip');
});
afterEach(function* () {
Zotero.Prefs.clear('sync.librariesToSkip');
var group = Zotero.Groups.get(responses.groups.ownerGroup.json.id);
if (group) {
yield group.eraseTx();
}
group = Zotero.Groups.get(responses.groups.memberGroup.json.id);
if (group) {
yield group.eraseTx();
}
})
it("should check library access and versions without library list", function* () {
// Create group with same id and version as groups response
var groupData = responses.groups.ownerGroup;
var group1 = yield createGroup({
id: groupData.json.id,
version: groupData.json.version
});
groupData = responses.groups.memberGroup;
var group2 = yield createGroup({
id: groupData.json.id,
version: groupData.json.version
});
setResponse('userGroups.groupVersions');
var libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json
);
assert.lengthOf(libraries, 3);
assert.sameMembers(
libraries,
[userLibraryID, group1.libraryID, group2.libraryID]
);
})
it("should check library access and versions with library list", function* () {
// Create groups with same id and version as groups response
var groupData = responses.groups.ownerGroup;
var group1 = yield createGroup({
id: groupData.json.id,
version: groupData.json.version
});
groupData = responses.groups.memberGroup;
var group2 = yield createGroup({
id: groupData.json.id,
version: groupData.json.version
});
setResponse('userGroups.groupVersions');
var libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }),
false,
responses.keyInfo.fullAccess.json,
[userLibraryID]
);
assert.lengthOf(libraries, 1);
assert.sameMembers(libraries, [userLibraryID]);
var libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }),
false,
responses.keyInfo.fullAccess.json,
[userLibraryID]
);
assert.lengthOf(libraries, 1);
assert.sameMembers(libraries, [userLibraryID]);
var libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }),
false,
responses.keyInfo.fullAccess.json,
[group1.libraryID]
);
assert.lengthOf(libraries, 1);
assert.sameMembers(libraries, [group1.libraryID]);
})
it("should filter out nonexistent skipped libraries if library list not provided", function* () {
var unskippedGroupID = responses.groups.ownerGroup.json.id;
var skippedGroupID = responses.groups.memberGroup.json.id;
Zotero.Prefs.set('sync.librariesToSkip', `["L4", "G${skippedGroupID}"]`);
setResponse('userGroups.groupVersions');
setResponse('groups.ownerGroup');
setResponse('groups.memberGroup');
var libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }),
false,
responses.keyInfo.fullAccess.json
);
var group = Zotero.Groups.get(unskippedGroupID);
assert.lengthOf(libraries, 2);
assert.sameMembers(libraries, [userLibraryID, group.libraryID]);
assert.isFalse(Zotero.Groups.get(skippedGroupID));
});
it("should filter out existing skipped libraries if library list not provided", function* () {
var unskippedGroupID = responses.groups.ownerGroup.json.id;
var skippedGroupID = responses.groups.memberGroup.json.id;
Zotero.Prefs.set('sync.librariesToSkip', `["L4", "G${skippedGroupID}"]`);
var skippedGroup = yield createGroup({
id: skippedGroupID,
version: responses.groups.memberGroup.json.version - 1
});
setResponse('userGroups.groupVersions');
setResponse('groups.ownerGroup');
setResponse('groups.memberGroup');
var libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }),
false,
responses.keyInfo.fullAccess.json
);
var group = Zotero.Groups.get(unskippedGroupID);
assert.lengthOf(libraries, 2);
assert.sameMembers(libraries, [userLibraryID, group.libraryID]);
assert.equal(skippedGroup.version, responses.groups.memberGroup.json.version - 1);
});
it("should filter out remotely missing archived libraries if library list not provided", function* () {
var ownerGroupID = responses.groups.ownerGroup.json.id;
var archivedGroupID = 162512451; // nonexistent group id
var ownerGroup = yield createGroup({
id: ownerGroupID,
version: responses.groups.ownerGroup.json.version
});
var archivedGroup = yield createGroup({
id: archivedGroupID,
editable: false,
archived: true
});
setResponse('userGroups.groupVersions');
setResponse('groups.memberGroup');
var libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }),
false,
responses.keyInfo.fullAccess.json
);
assert.lengthOf(libraries, 3);
assert.sameMembers(
libraries,
[
userLibraryID,
ownerGroup.libraryID,
// Nonexistent group should've been created
Zotero.Groups.getLibraryIDFromGroupID(responses.groups.memberGroup.json.id)
]
);
});
it("should unarchive library if available remotely", function* () {
var syncedGroupID = responses.groups.ownerGroup.json.id;
var archivedGroupID = responses.groups.memberGroup.json.id;
var syncedGroup = yield createGroup({
id: syncedGroupID,
version: responses.groups.ownerGroup.json.version
});
var archivedGroup = yield createGroup({
id: archivedGroupID,
version: responses.groups.memberGroup.json.version - 1,
editable: false,
archived: true
});
setResponse('userGroups.groupVersions');
setResponse('groups.ownerGroup');
setResponse('groups.memberGroup');
var libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }),
false,
responses.keyInfo.fullAccess.json
);
assert.lengthOf(libraries, 3);
assert.sameMembers(
libraries,
[userLibraryID, syncedGroup.libraryID, archivedGroup.libraryID]
);
assert.isFalse(archivedGroup.archived);
});
it("shouldn't filter out skipped libraries if library list is provided", function* () {
var groupData = responses.groups.memberGroup;
var group = yield createGroup({
id: groupData.json.id,
version: groupData.json.version
});
Zotero.Prefs.set('sync.librariesToSkip', `["L4", "G${group.id}"]`);
setResponse('userGroups.groupVersions');
setResponse('groups.ownerGroup');
setResponse('groups.memberGroup');
var libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }),
false,
responses.keyInfo.fullAccess.json,
[userLibraryID, group.libraryID]
);
assert.lengthOf(libraries, 2);
assert.sameMembers(libraries, [userLibraryID, group.libraryID]);
});
it("should update outdated group metadata", function* () {
// Create groups with same id as groups response but earlier versions
var groupData1 = responses.groups.ownerGroup;
var group1 = yield createGroup({
id: groupData1.json.id,
version: groupData1.json.version - 1,
editable: false
});
var groupData2 = responses.groups.memberGroup;
var group2 = yield createGroup({
id: groupData2.json.id,
version: groupData2.json.version - 1,
editable: true
});
setResponse('userGroups.groupVersions');
setResponse('groups.ownerGroup');
setResponse('groups.memberGroup');
// Simulate acceptance of library reset for group 2 editable change
var stub = sinon.stub(Zotero.Sync.Data.Local, "checkLibraryForAccess")
.returns(Zotero.Promise.resolve(true));
var libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json
);
assert.ok(stub.calledTwice);
stub.restore();
assert.lengthOf(libraries, 3);
assert.sameMembers(
libraries,
[userLibraryID, group1.libraryID, group2.libraryID]
);
assert.equal(group1.name, groupData1.json.data.name);
assert.equal(group1.version, groupData1.json.version);
assert.isTrue(group1.editable);
assert.equal(group2.name, groupData2.json.data.name);
assert.equal(group2.version, groupData2.json.version);
assert.isFalse(group2.editable);
})
it("should update outdated group metadata for group created with classic sync", function* () {
var groupData1 = responses.groups.ownerGroup;
var group1 = yield createGroup({
id: groupData1.json.id,
version: 0,
editable: false
});
var groupData2 = responses.groups.memberGroup;
var group2 = yield createGroup({
id: groupData2.json.id,
version: 0,
editable: true
});
yield Zotero.DB.queryAsync(
"UPDATE groups SET version=0 WHERE groupID IN (?, ?)", [group1.id, group2.id]
);
yield Zotero.Libraries.init();
group1 = Zotero.Groups.get(group1.id);
group2 = Zotero.Groups.get(group2.id);
setResponse('userGroups.groupVersions');
setResponse('groups.ownerGroup');
setResponse('groups.memberGroup');
// Simulate acceptance of library reset for group 2 editable change
var stub = sinon.stub(Zotero.Sync.Data.Local, "checkLibraryForAccess")
.returns(Zotero.Promise.resolve(true));
var libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }),
false,
responses.keyInfo.fullAccess.json,
[group1.libraryID, group2.libraryID]
);
assert.ok(stub.calledTwice);
stub.restore();
assert.lengthOf(libraries, 2);
assert.sameMembers(libraries, [group1.libraryID, group2.libraryID]);
assert.equal(group1.name, groupData1.json.data.name);
assert.equal(group1.version, groupData1.json.version);
assert.isTrue(group1.editable);
assert.equal(group2.name, groupData2.json.data.name);
assert.equal(group2.version, groupData2.json.version);
assert.isFalse(group2.editable);
})
it("should create locally missing groups", function* () {
setResponse('userGroups.groupVersions');
setResponse('groups.ownerGroup');
setResponse('groups.memberGroup');
var libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json
);
assert.lengthOf(libraries, 3);
var groupData1 = responses.groups.ownerGroup;
var group1 = Zotero.Groups.get(groupData1.json.id);
var groupData2 = responses.groups.memberGroup;
var group2 = Zotero.Groups.get(groupData2.json.id);
assert.ok(group1);
assert.ok(group2);
assert.sameMembers(
libraries,
[userLibraryID, group1.libraryID, group2.libraryID]
);
assert.equal(group1.name, groupData1.json.data.name);
assert.isTrue(group1.editable);
assert.equal(group2.name, groupData2.json.data.name);
assert.isFalse(group2.editable);
})
it("should delete remotely missing groups", function* () {
var groupData1 = responses.groups.ownerGroup;
var group1 = yield createGroup({ id: groupData1.json.id, version: groupData1.json.version });
var groupData2 = responses.groups.memberGroup;
var group2 = yield createGroup({ id: groupData2.json.id, version: groupData2.json.version });
setResponse('userGroups.groupVersionsOnlyMemberGroup');
waitForDialog(function (dialog) {
var text = dialog.document.documentElement.textContent;
assert.include(text, group1.name);
});
var libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json
);
assert.lengthOf(libraries, 2);
assert.sameMembers(libraries, [userLibraryID, group2.libraryID]);
assert.isFalse(Zotero.Groups.exists(groupData1.json.id));
assert.isTrue(Zotero.Groups.exists(groupData2.json.id));
})
it("should keep remotely missing groups", function* () {
var group1 = yield createGroup({ editable: true, filesEditable: true });
var group2 = yield createGroup({ editable: true, filesEditable: true });
setResponse('userGroups.groupVersionsEmpty');
var called = 0;
var otherGroup;
waitForDialog(function (dialog) {
called++;
var text = dialog.document.documentElement.textContent;
if (text.includes(group1.name)) {
otherGroup = group2;
}
else if (text.includes(group2.name)) {
otherGroup = group1;
}
else {
throw new Error("Dialog text does not include either group name");
}
waitForDialog(function (dialog) {
called++;
var text = dialog.document.documentElement.textContent;
assert.include(text, otherGroup.name);
}, "extra1");
}, "extra1");
var libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json
);
assert.equal(called, 2);
assert.lengthOf(libraries, 1);
assert.sameMembers(libraries, [userLibraryID]);
// Groups should still exist but be read-only and archived
[group1, group2].forEach((group) => {
assert.isTrue(Zotero.Groups.exists(group.id));
assert.isTrue(group.archived);
assert.isFalse(group.editable);
assert.isFalse(group.filesEditable);
});
})
it("should cancel sync with remotely missing groups", function* () {
var groupData = responses.groups.ownerGroup;
var group = yield createGroup({ id: groupData.json.id, version: groupData.json.version });
setResponse('userGroups.groupVersionsEmpty');
waitForDialog(function (dialog) {
var text = dialog.document.documentElement.textContent;
assert.include(text, group.name);
}, "cancel");
var libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json
);
assert.lengthOf(libraries, 0);
assert.isTrue(Zotero.Groups.exists(groupData.json.id));
})
it("should prompt to revert local changes on loss of library write access", function* () {
var group = yield createGroup({
version: 1,
libraryVersion: 2
});
var libraryID = group.libraryID;
setResponse({
method: "GET",
url: "users/1/groups?format=versions",
status: 200,
headers: {
"Last-Modified-Version": 3
},
json: {
[group.id]: 3
}
});
setResponse({
method: "GET",
url: "groups/" + group.id,
status: 200,
headers: {
"Last-Modified-Version": 3
},
json: {
id: group.id,
version: 2,
data: {
// Make group read-only
id: group.id,
version: 2,
name: group.name,
description: group.description,
owner: 2,
type: "Private",
libraryEditing: "admins",
libraryReading: "all",
fileEditing: "admins",
admins: [],
members: [1]
}
}
});
// First, test cancelling
var stub = sinon.stub(Zotero.Sync.Data.Local, "checkLibraryForAccess")
.returns(Zotero.Promise.resolve(false));
var libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json
);
assert.notInclude(libraries, group.libraryID);
assert.isTrue(stub.calledOnce);
assert.isTrue(group.editable);
stub.reset();
// Next, reset
stub.returns(Zotero.Promise.resolve(true));
libraries = yield runner.checkLibraries(
runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json
);
assert.include(libraries, group.libraryID);
assert.isTrue(stub.calledOnce);
assert.isFalse(group.editable);
stub.restore();
});
})
describe("#sync()", function () {
it("should perform a sync across all libraries and update library versions", function* () {
setResponse('keyInfo.fullAccess');
setResponse('userGroups.groupVersions');
setResponse('groups.ownerGroup');
setResponse('groups.memberGroup');
// My Library
setResponse({
method: "GET",
url: "users/1/settings",
status: 200,
headers: {
"Last-Modified-Version": 5
},
json: []
});
setResponse({
method: "GET",
url: "users/1/collections?format=versions",
status: 200,
headers: {
"Last-Modified-Version": 5
},
json: []
});
setResponse({
method: "GET",
url: "users/1/searches?format=versions",
status: 200,
headers: {
"Last-Modified-Version": 5
},
json: []
});
setResponse({
method: "GET",
url: "users/1/items/top?format=versions&includeTrashed=1",
status: 200,
headers: {
"Last-Modified-Version": 5
},
json: []
});
setResponse({
method: "GET",
url: "users/1/items?format=versions&includeTrashed=1",
status: 200,
headers: {
"Last-Modified-Version": 5
},
json: []
});
setResponse({
method: "GET",
url: "users/1/deleted?since=0",
status: 200,
headers: {
"Last-Modified-Version": 5
},
json: []
});
// Group library 1
setResponse({
method: "GET",
url: "groups/1623562/settings",
status: 200,
headers: {
"Last-Modified-Version": 15
},
json: []
});
setResponse({
method: "GET",
url: "groups/1623562/collections?format=versions",
status: 200,
headers: {
"Last-Modified-Version": 15
},
json: []
});
setResponse({
method: "GET",
url: "groups/1623562/searches?format=versions",
status: 200,
headers: {
"Last-Modified-Version": 15
},
json: []
});
setResponse({
method: "GET",
url: "groups/1623562/items/top?format=versions&includeTrashed=1",
status: 200,
headers: {
"Last-Modified-Version": 15
},
json: []
});
setResponse({
method: "GET",
url: "groups/1623562/items?format=versions&includeTrashed=1",
status: 200,
headers: {
"Last-Modified-Version": 15
},
json: []
});
setResponse({
method: "GET",
url: "groups/1623562/deleted?since=0",
status: 200,
headers: {
"Last-Modified-Version": 15
},
json: []
});
// Group library 2
setResponse({
method: "GET",
url: "groups/2694172/settings",
status: 200,
headers: {
"Last-Modified-Version": 20
},
json: []
});
setResponse({
method: "GET",
url: "groups/2694172/collections?format=versions",
status: 200,
headers: {
"Last-Modified-Version": 20
},
json: []
});
setResponse({
method: "GET",
url: "groups/2694172/searches?format=versions",
status: 200,
headers: {
"Last-Modified-Version": 20
},
json: []
});
setResponse({
method: "GET",
url: "groups/2694172/items/top?format=versions&includeTrashed=1",
status: 200,
headers: {
"Last-Modified-Version": 20
},
json: []
});
setResponse({
method: "GET",
url: "groups/2694172/items?format=versions&includeTrashed=1",
status: 200,
headers: {
"Last-Modified-Version": 20
},
json: []
});
setResponse({
method: "GET",
url: "groups/2694172/deleted?since=0",
status: 200,
headers: {
"Last-Modified-Version": 20
},
json: []
});
// Full-text syncing
setResponse({
method: "GET",
url: "users/1/fulltext?format=versions",
status: 200,
headers: {
"Last-Modified-Version": 5
},
json: {}
});
setResponse({
method: "GET",
url: "groups/1623562/fulltext?format=versions",
status: 200,
headers: {
"Last-Modified-Version": 15
},
json: {}
});
setResponse({
method: "GET",
url: "groups/2694172/fulltext?format=versions",
status: 200,
headers: {
"Last-Modified-Version": 20
},
json: {}
});
var startTime = new Date().getTime();
yield runner.sync({
onError: e => { throw e },
});
// Check local library versions
assert.equal(
Zotero.Libraries.getVersion(userLibraryID),
5
);
assert.equal(
Zotero.Libraries.getVersion(Zotero.Groups.getLibraryIDFromGroupID(1623562)),
15
);
assert.equal(
Zotero.Libraries.getVersion(Zotero.Groups.getLibraryIDFromGroupID(2694172)),
20
);
// Last sync time should be within the last few seconds
var lastSyncTime = Zotero.Sync.Data.Local.getLastSyncTime();
assert.isAbove(lastSyncTime.getTime(), startTime);
assert.isBelow(lastSyncTime.getTime(), new Date().getTime());
})
it("should handle user-initiated cancellation", function* () {
setResponse('keyInfo.fullAccess');
setResponse('userGroups.groupVersions');
setResponse('groups.ownerGroup');
setResponse('groups.memberGroup');
var stub = sinon.stub(Zotero.Sync.Data.Engine.prototype, "start");
stub.onCall(0).returns(Zotero.Promise.resolve());
var e = new Zotero.Sync.UserCancelledException();
e.handledRejection = true;
stub.onCall(1).returns(Zotero.Promise.reject(e));
// Shouldn't be reached
stub.onCall(2).throws();
yield runner.sync({
onError: e => { throw e },
});
stub.restore();
});
it("should handle user-initiated cancellation for current library", function* () {
setResponse('keyInfo.fullAccess');
setResponse('userGroups.groupVersions');
setResponse('groups.ownerGroup');
setResponse('groups.memberGroup');
var stub = sinon.stub(Zotero.Sync.Data.Engine.prototype, "start");
stub.returns(Zotero.Promise.resolve());
var e = new Zotero.Sync.UserCancelledException(true);
e.handledRejection = true;
stub.onCall(1).returns(Zotero.Promise.reject(e));
yield runner.sync({
onError: e => { throw e },
});
assert.equal(stub.callCount, 3);
stub.restore();
});
})
describe("#createAPIKeyFromCredentials()", function() {
var data = {
name: "Automatic Zotero Client Key",
username: "Username",
access: {
user: {
library: true,
files: true,
notes: true,
write: true
},
groups: {
all: {
library: true,
write: true
}
}
}
};
var correctPostData = Object.assign({password: 'correctPassword'}, data);
var incorrectPostData = Object.assign({password: 'incorrectPassword'}, data);
var responseData = Object.assign({userID: 1, key: apiKey}, data);
it("should return json with key when credentials valid", function* () {
server.respond(function (req) {
if (req.method == "POST") {
var json = JSON.parse(req.requestBody);
assert.deepEqual(json, correctPostData);
req.respond(201, {}, JSON.stringify(responseData));
}
});
var json = yield runner.createAPIKeyFromCredentials('Username', 'correctPassword');
assert.equal(json.key, apiKey);
});
it("should return false when credentials invalid", function* () {
server.respond(function (req) {
if (req.method == "POST") {
var json = JSON.parse(req.requestBody);
assert.deepEqual(json, incorrectPostData);
req.respond(403);
}
});
var key = yield runner.createAPIKeyFromCredentials('Username', 'incorrectPassword');
assert.isFalse(key);
});
});
describe("#deleteAPIKey()", function() {
it("should send DELETE request with correct key", function* (){
Zotero.Sync.Data.Local.setAPIKey(apiKey);
server.respond(function (req) {
if (req.method == "DELETE") {
assert.propertyVal(req.requestHeaders, 'Zotero-API-Key', apiKey);
assert.equal(req.url, baseURL + "keys/current");
}
req.respond(204);
});
yield runner.deleteAPIKey();
});
});
describe("Error Handling", function () {
var win;
afterEach(function () {
if (win) {
win.close();
}
});
it("should show the sync error icon on error", async function () {
let library = Zotero.Libraries.userLibrary;
library.libraryVersion = 1;
await library.save();
setResponse('keyInfo.fullAccess');
setResponse('userGroups.groupVersionsEmpty');
// No other responses, so settings response will be a 404
spy = sinon.spy(runner, "updateIcons");
await runner.sync();
assert.isTrue(spy.calledTwice);
assert.isArray(spy.args[1][0]);
assert.lengthOf(spy.args[1][0], 1);
// Not an instance of Error for some reason
var error = spy.args[1][0][0];
assert.equal(Object.getPrototypeOf(error).constructor.name, "Error");
});
it("should show a custom button in the error panel", function* () {
win = yield loadZoteroPane();
var libraryID = Zotero.Libraries.userLibraryID;
setResponse({
method: "GET",
url: "keys/current",
status: 403,
headers: {},
text: "Invalid Key"
});
yield runner.sync({
background: true
});
var doc = win.document;
var errorIcon = doc.getElementById('zotero-tb-sync-error');
assert.isFalse(errorIcon.hidden);
errorIcon.click();
var panel = win.document.getElementById('zotero-sync-error-panel');
var buttons = panel.getElementsByTagName('button');
assert.lengthOf(buttons, 1);
assert.equal(buttons[0].label, Zotero.getString('sync.openSyncPreferences'));
});
it("should show a button in error panel to select a too-long note", function* () {
win = yield loadZoteroPane();
var doc = win.document;
var text = "".padStart(256, "a");
var item = yield createDataObject('item', { itemType: 'note', note: text });
setResponse('keyInfo.fullAccess');
setResponse('userGroups.groupVersions');
setResponse('groups.ownerGroup');
setResponse('groups.memberGroup');
server.respond(function (req) {
if (req.method == "POST" && req.url == baseURL + "users/1/items") {
req.respond(
200,
{
"Last-Modified-Version": 5
},
JSON.stringify({
successful: {},
success: {},
unchanged: {},
failed: {
"0": {
code: 413,
message: `Note ${Zotero.Utilities.ellipsize(text, 100)} too long`
}
}
})
);
}
});
yield runner.sync({ libraries: [Zotero.Libraries.userLibraryID] });
var errorIcon = doc.getElementById('zotero-tb-sync-error');
assert.isFalse(errorIcon.hidden);
errorIcon.click();
var panel = win.document.getElementById('zotero-sync-error-panel');
assert.include(panel.innerHTML, text.substr(0, 10));
var buttons = panel.getElementsByTagName('button');
assert.lengthOf(buttons, 1);
assert.include(buttons[0].label, Zotero.getString('pane.items.showItemInLibrary'));
});
it("should show an error for invalid My Library data", async function () {
let library = Zotero.Libraries.userLibrary;
library.libraryVersion = 1;
await library.save();
var collection = await createDataObject('collection', { synced: true });
var json = collection.toResponseJSON();
json.version = json.data.version = 2;
json.data.INVALID = true;
setResponse('keyInfo.fullAccess');
setResponse('userGroups.groupVersionsEmpty');
setDefaultResponses({
lastLibraryVersion: 1,
libraryVersion: 2
});
setResponse({
method: "GET",
url: "users/1/collections?format=versions&since=1",
status: 200,
headers: {
"Last-Modified-Version": 2
},
json: {
[json.key]: 2
}
});
setResponse({
method: "GET",
url: `users/1/collections?collectionKey=${json.key}`,
status: 200,
headers: {
"Last-Modified-Version": 2
},
json: [json]
});
spy = sinon.spy(runner, "updateIcons");
await runner.sync();
assert.isTrue(spy.calledTwice);
assert.isArray(spy.args[1][0]);
assert.lengthOf(spy.args[1][0], 1);
// Not an instance of Error for some reason
var error = spy.args[1][0][0];
assert.equal(Object.getPrototypeOf(error).constructor.name, "Error");
assert.match(error.message, /^Some data in My Library/);
});
it("should show a warning in the sync button tooltip for invalid group data", async function () {
win = await loadZoteroPane();
var doc = win.document;
// Create group with same id and version as groups response
var groupData = responses.groups.memberGroup;
var group = await createGroup({
id: groupData.json.id,
version: groupData.json.version
});
group.libraryVersion = 1;
await group.save();
var collection = await createDataObject('collection', { synced: true });
var json = collection.toResponseJSON();
json.version = json.data.version = 2;
json.data.INVALID = true;
var target = 'groups/' + group.id;
setResponse('keyInfo.fullAccess');
setResponse('userGroups.groupVersionsOnlyMemberGroup');
setResponse('groups.memberGroup');
setDefaultResponses({
target,
lastLibraryVersion: 1,
libraryVersion: 2
});
setResponse({
method: "GET",
url: target + '/collections?format=versions&since=1',
status: 200,
headers: {
"Last-Modified-Version": 2
},
json: {
[json.key]: 2
}
});
setResponse({
method: "GET",
url: target + `/collections?collectionKey=${json.key}`,
status: 200,
headers: {
"Last-Modified-Version": 2
},
json: [json]
});
await runner.sync({ libraries: [group.libraryID] });
assert.isTrue(doc.getElementById('zotero-tb-sync-error').hidden);
// Fake what happens on button mouseover
var tooltip = doc.getElementById('zotero-tb-sync-tooltip');
runner.registerSyncStatus(tooltip);
var html = doc.getElementById('zotero-tb-sync-tooltip').innerHTML;
assert.match(html, /Some data in .+\. Other data will continue to sync\./);
runner.registerSyncStatus();
});
// TODO: Test multiple long tags and tags across libraries
describe("Long Tag Fixer", function () {
it("should split a tag", function* () {
win = yield loadZoteroPane();
var item = yield createDataObject('item');
var tag = "title;feeling;matter;drum;treatment;caring;earthy;shrill;unit;obedient;hover;healthy;cheap;clever;wren;wicked;clip;shoe;jittery;shape;clear;dime;increase;complete;level;milk;false;infamous;lamentable;measure;cuddly;tasteless;peace;top;pencil;caption;unusual;depressed;frantic";
item.addTag(tag, 1);
yield item.saveTx();
setResponse('keyInfo.fullAccess');
setResponse('userGroups.groupVersions');
setResponse('groups.ownerGroup');
setResponse('groups.memberGroup');
server.respond(function (req) {
if (req.method == "POST" && req.url == baseURL + "users/1/items") {
var json = JSON.parse(req.requestBody);
if (json[0].tags.length == 1) {
req.respond(
200,
{
"Last-Modified-Version": 5
},
JSON.stringify({
successful: {},
success: {},
unchanged: {},
failed: {
"0": {
code: 413,
message: "Tag 'title;feeling;matter;drum;treatment;caring;earthy;shrill;unit;obedient;hover…' is too long to sync",
data: {
tag
}
}
}
})
);
}
else {
let itemJSON = item.toResponseJSON();
itemJSON.version = 6;
itemJSON.data.version = 6;
req.respond(
200,
{
"Last-Modified-Version": 6
},
JSON.stringify({
successful: {
"0": itemJSON
},
success: {
"0": json[0].key
},
unchanged: {},
failed: {}
})
);
}
}
});
waitForDialog(null, 'accept', 'chrome://zotero/content/longTagFixer.xul');
yield runner.sync({ libraries: [Zotero.Libraries.userLibraryID] });
assert.isFalse(Zotero.Tags.getID(tag));
assert.isNumber(Zotero.Tags.getID('feeling'));
});
it("should delete a tag", function* () {
win = yield loadZoteroPane();
var item = yield createDataObject('item');
var tag = "title;feeling;matter;drum;treatment;caring;earthy;shrill;unit;obedient;hover;healthy;cheap;clever;wren;wicked;clip;shoe;jittery;shape;clear;dime;increase;complete;level;milk;false;infamous;lamentable;measure;cuddly;tasteless;peace;top;pencil;caption;unusual;depressed;frantic";
item.addTag(tag, 1);
yield item.saveTx();
setResponse('keyInfo.fullAccess');
setResponse('userGroups.groupVersions');
setResponse('groups.ownerGroup');
setResponse('groups.memberGroup');
server.respond(function (req) {
if (req.method == "POST" && req.url == baseURL + "users/1/items") {
var json = JSON.parse(req.requestBody);
if (json[0].tags.length == 1) {
req.respond(
200,
{
"Last-Modified-Version": 5
},
JSON.stringify({
successful: {},
success: {},
unchanged: {},
failed: {
"0": {
code: 413,
message: "Tag 'title;feeling;matter;drum;treatment;caring;earthy;shrill;unit;obedient;hover…' is too long to sync",
data: {
tag
}
}
}
})
);
}
else {
let itemJSON = item.toResponseJSON();
itemJSON.version = 6;
itemJSON.data.version = 6;
req.respond(
200,
{
"Last-Modified-Version": 6
},
JSON.stringify({
successful: {
"0": itemJSON
},
success: {
"0": json[0].key
},
unchanged: {},
failed: {}
})
);
}
}
});
waitForDialog(function (dialog) {
dialog.Zotero_Long_Tag_Fixer.switchMode(2);
}, 'accept', 'chrome://zotero/content/longTagFixer.xul');
yield runner.sync({ libraries: [Zotero.Libraries.userLibraryID] });
assert.isFalse(Zotero.Tags.getID(tag));
assert.isFalse(Zotero.Tags.getID('feeling'));
});
});
});
})