13cc393840
* Manual tag splitting from tag selector * Only apply split to the tag in current library * Preserve tag type
1404 lines
38 KiB
JavaScript
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.xhtml');
|
|
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 (window) {
|
|
window.Zotero_Long_Tag_Fixer.switchMode(2);
|
|
}, 'accept', 'chrome://zotero/content/longTagFixer.xhtml');
|
|
yield runner.sync({ libraries: [Zotero.Libraries.userLibraryID] });
|
|
|
|
assert.isFalse(Zotero.Tags.getID(tag));
|
|
assert.isFalse(Zotero.Tags.getID('feeling'));
|
|
});
|
|
});
|
|
});
|
|
})
|