mirror of
https://github.com/zotero/zotero.git
synced 2026-09-29 01:41:24 +00:00
resetData() clears user data and sync state using application APIs instead of doing a full Zotero shutdown/reinit cycle, making it much faster for tests that just need clean state between runs. Also make Tags.init(), Creators.init(), Users.init(), and SyncedSettings.loadAll() properly clear their caches before reloading, so they are safe to call more than once.
1399 lines
38 KiB
JavaScript
1399 lines
38 KiB
JavaScript
"use strict";
|
|
|
|
describe("Zotero.Sync.Runner", function () {
|
|
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 resetData();
|
|
|
|
userLibraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
Zotero.HTTP.mock = sinon.FakeXMLHttpRequest;
|
|
server = sinon.fakeServer.create();
|
|
server.autoRespond = true;
|
|
|
|
runner = new Zotero.Sync.Runner_Module({ baseURL, apiKey });
|
|
|
|
const { ConcurrentCaller } = ChromeUtils.importESModule("resource://zotero/concurrentCaller.mjs");
|
|
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", async function () {
|
|
setResponse('keyInfo.fullAccess');
|
|
var json = await 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", async function () {
|
|
// Create group with same id and version as groups response
|
|
var groupData = responses.groups.ownerGroup;
|
|
var group1 = await createGroup({
|
|
id: groupData.json.id,
|
|
version: groupData.json.version
|
|
});
|
|
groupData = responses.groups.memberGroup;
|
|
var group2 = await createGroup({
|
|
id: groupData.json.id,
|
|
version: groupData.json.version
|
|
});
|
|
|
|
setResponse('userGroups.groupVersions');
|
|
var libraries = await 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", async function () {
|
|
// Create groups with same id and version as groups response
|
|
var groupData = responses.groups.ownerGroup;
|
|
var group1 = await createGroup({
|
|
id: groupData.json.id,
|
|
version: groupData.json.version
|
|
});
|
|
groupData = responses.groups.memberGroup;
|
|
var group2 = await createGroup({
|
|
id: groupData.json.id,
|
|
version: groupData.json.version
|
|
});
|
|
|
|
setResponse('userGroups.groupVersions');
|
|
var libraries = await runner.checkLibraries(
|
|
runner.getAPIClient({ apiKey }),
|
|
false,
|
|
responses.keyInfo.fullAccess.json,
|
|
[userLibraryID]
|
|
);
|
|
assert.lengthOf(libraries, 1);
|
|
assert.sameMembers(libraries, [userLibraryID]);
|
|
|
|
var libraries = await runner.checkLibraries(
|
|
runner.getAPIClient({ apiKey }),
|
|
false,
|
|
responses.keyInfo.fullAccess.json,
|
|
[userLibraryID]
|
|
);
|
|
assert.lengthOf(libraries, 1);
|
|
assert.sameMembers(libraries, [userLibraryID]);
|
|
|
|
var libraries = await 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", async 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 = await 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", async 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 = await createGroup({
|
|
id: skippedGroupID,
|
|
version: responses.groups.memberGroup.json.version - 1
|
|
});
|
|
|
|
setResponse('userGroups.groupVersions');
|
|
setResponse('groups.ownerGroup');
|
|
setResponse('groups.memberGroup');
|
|
var libraries = await 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", async function () {
|
|
var ownerGroupID = responses.groups.ownerGroup.json.id;
|
|
var archivedGroupID = 162512451; // nonexistent group id
|
|
|
|
var ownerGroup = await createGroup({
|
|
id: ownerGroupID,
|
|
version: responses.groups.ownerGroup.json.version
|
|
});
|
|
var archivedGroup = await createGroup({
|
|
id: archivedGroupID,
|
|
editable: false,
|
|
archived: true
|
|
});
|
|
|
|
setResponse('userGroups.groupVersions');
|
|
setResponse('groups.memberGroup');
|
|
var libraries = await 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", async function () {
|
|
var syncedGroupID = responses.groups.ownerGroup.json.id;
|
|
var archivedGroupID = responses.groups.memberGroup.json.id;
|
|
|
|
var syncedGroup = await createGroup({
|
|
id: syncedGroupID,
|
|
version: responses.groups.ownerGroup.json.version
|
|
});
|
|
var archivedGroup = await 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 = await 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", async function () {
|
|
var groupData = responses.groups.memberGroup;
|
|
var group = await 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 = await 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", async function () {
|
|
// Create groups with same id as groups response but earlier versions
|
|
var groupData1 = responses.groups.ownerGroup;
|
|
var group1 = await createGroup({
|
|
id: groupData1.json.id,
|
|
version: groupData1.json.version - 1,
|
|
editable: false
|
|
});
|
|
var groupData2 = responses.groups.memberGroup;
|
|
var group2 = await 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(Promise.resolve(true));
|
|
|
|
var libraries = await 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", async function () {
|
|
var groupData1 = responses.groups.ownerGroup;
|
|
var group1 = await createGroup({
|
|
id: groupData1.json.id,
|
|
version: 0,
|
|
editable: false
|
|
});
|
|
var groupData2 = responses.groups.memberGroup;
|
|
var group2 = await createGroup({
|
|
id: groupData2.json.id,
|
|
version: 0,
|
|
editable: true
|
|
});
|
|
|
|
await Zotero.DB.queryAsync(
|
|
"UPDATE groups SET version=0 WHERE groupID IN (?, ?)", [group1.id, group2.id]
|
|
);
|
|
await 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(Promise.resolve(true));
|
|
|
|
var libraries = await 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", async function () {
|
|
setResponse('userGroups.groupVersions');
|
|
setResponse('groups.ownerGroup');
|
|
setResponse('groups.memberGroup');
|
|
var libraries = await 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", async function () {
|
|
var groupData1 = responses.groups.ownerGroup;
|
|
var group1 = await createGroup({ id: groupData1.json.id, version: groupData1.json.version });
|
|
var groupData2 = responses.groups.memberGroup;
|
|
var group2 = await 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 = await 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", async function () {
|
|
var group1 = await createGroup({ editable: true, filesEditable: true });
|
|
var group2 = await 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 = await 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", async function () {
|
|
var groupData = responses.groups.ownerGroup;
|
|
var group = await 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 = await 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", async function () {
|
|
var group = await 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(Promise.resolve(false));
|
|
var libraries = await 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(Promise.resolve(true));
|
|
libraries = await 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", async 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();
|
|
|
|
await 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", async 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(Promise.resolve());
|
|
var e = new Zotero.Sync.UserCancelledException();
|
|
e.handledRejection = true;
|
|
stub.onCall(1).returns(Promise.reject(e));
|
|
// Shouldn't be reached
|
|
stub.onCall(2).throws();
|
|
|
|
await runner.sync({
|
|
onError: e => { throw e },
|
|
});
|
|
|
|
stub.restore();
|
|
});
|
|
|
|
|
|
it("should handle user-initiated cancellation for current library", async 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(Promise.resolve());
|
|
var e = new Zotero.Sync.UserCancelledException(true);
|
|
e.handledRejection = true;
|
|
stub.onCall(1).returns(Promise.reject(e));
|
|
|
|
await 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", async 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 = await runner.createAPIKeyFromCredentials('Username', 'correctPassword');
|
|
assert.equal(json.key, apiKey);
|
|
});
|
|
|
|
it("should return false when credentials invalid", async function () {
|
|
server.respond(function (req) {
|
|
if (req.method == "POST") {
|
|
var json = JSON.parse(req.requestBody);
|
|
assert.deepEqual(json, incorrectPostData);
|
|
req.respond(403);
|
|
}
|
|
});
|
|
|
|
var key = await runner.createAPIKeyFromCredentials('Username', 'incorrectPassword');
|
|
assert.isFalse(key);
|
|
});
|
|
});
|
|
|
|
describe("#deleteAPIKey()", function () {
|
|
it("should send DELETE request with correct key", async 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);
|
|
});
|
|
|
|
await 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", async function () {
|
|
win = await loadZoteroPane();
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
setResponse({
|
|
method: "GET",
|
|
url: "keys/current",
|
|
status: 403,
|
|
headers: {},
|
|
text: "Invalid Key"
|
|
});
|
|
await 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", async function () {
|
|
win = await loadZoteroPane();
|
|
var doc = win.document;
|
|
|
|
var text = "".padStart(256, "a");
|
|
var item = await 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`
|
|
}
|
|
}
|
|
})
|
|
);
|
|
}
|
|
});
|
|
|
|
await 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", async function () {
|
|
win = await loadZoteroPane();
|
|
|
|
var item = await 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);
|
|
await 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');
|
|
await runner.sync({ libraries: [Zotero.Libraries.userLibraryID] });
|
|
|
|
assert.isFalse(Zotero.Tags.getID(tag));
|
|
assert.isNumber(Zotero.Tags.getID('feeling'));
|
|
});
|
|
|
|
it("should delete a tag", async function () {
|
|
win = await loadZoteroPane();
|
|
|
|
var item = await 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);
|
|
await 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');
|
|
await runner.sync({ libraries: [Zotero.Libraries.userLibraryID] });
|
|
|
|
assert.isFalse(Zotero.Tags.getID(tag));
|
|
assert.isFalse(Zotero.Tags.getID('feeling'));
|
|
});
|
|
});
|
|
});
|
|
})
|