73f4d28ab2
This mostly gets ZFS file syncing and file conflict resolution working with the API sync process. WebDAV will need to be updated separately. Known issues: - File sync progress is temporarily gone - File uploads can result in an unnecessary 412 loop on the next data sync - This causes Firefox to crash on one of my computers during tests, which would be easier to debug if it produced a crash log. Also: - Adds httpd.js for use in tests when FakeXMLHttpRequest can't be used (e.g., saveURI()). - Adds some additional test data files for attachment tests
664 lines
17 KiB
JavaScript
664 lines
17 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, publicationsLibraryID, runner, caller, server, stub, spy;
|
|
|
|
var responses = {
|
|
keyInfo: {
|
|
fullAccess: {
|
|
method: "GET",
|
|
url: "keys/" + apiKey,
|
|
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
|
|
//
|
|
var setup = Zotero.Promise.coroutine(function* (options = {}) {
|
|
yield Zotero.DB.queryAsync("DELETE FROM settings WHERE setting='account'");
|
|
yield Zotero.Users.init();
|
|
|
|
var runner = new Zotero.Sync.Runner_Module({
|
|
baseURL: baseURL,
|
|
apiKey: apiKey
|
|
});
|
|
|
|
Components.utils.import("resource://zotero/concurrentCaller.js");
|
|
var 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;
|
|
}
|
|
};
|
|
|
|
return { runner, caller };
|
|
})
|
|
|
|
function setResponse(response) {
|
|
setHTTPResponse(server, baseURL, response, responses);
|
|
}
|
|
|
|
|
|
//
|
|
// Tests
|
|
//
|
|
let win;
|
|
before(function* () {
|
|
userLibraryID = Zotero.Libraries.userLibraryID;
|
|
publicationsLibraryID = Zotero.Libraries.publicationsLibraryID;
|
|
win = yield loadBrowserWindow();
|
|
})
|
|
beforeEach(function* () {
|
|
Zotero.HTTP.mock = sinon.FakeXMLHttpRequest;
|
|
|
|
server = sinon.fakeServer.create();
|
|
server.autoRespond = true;
|
|
|
|
({ runner, caller } = yield setup());
|
|
|
|
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;
|
|
if (win) {
|
|
win.close();
|
|
}
|
|
})
|
|
|
|
describe("#checkAccess()", function () {
|
|
it("should check key access", function* () {
|
|
spy = sinon.spy(runner, "checkUser");
|
|
setResponse('keyInfo.fullAccess');
|
|
var json = yield runner.checkAccess(runner.getAPIClient());
|
|
sinon.assert.calledWith(spy, 1, "Username");
|
|
var compare = {};
|
|
Object.assign(compare, responses.keyInfo.fullAccess.json);
|
|
delete compare.key;
|
|
assert.deepEqual(json, compare);
|
|
})
|
|
})
|
|
|
|
describe("#checkLibraries()", function () {
|
|
afterEach(function* () {
|
|
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(), false, responses.keyInfo.fullAccess.json
|
|
);
|
|
assert.lengthOf(libraries, 4);
|
|
assert.sameMembers(
|
|
libraries,
|
|
[userLibraryID, publicationsLibraryID, 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(), false, responses.keyInfo.fullAccess.json, [userLibraryID]
|
|
);
|
|
assert.lengthOf(libraries, 1);
|
|
assert.sameMembers(libraries, [userLibraryID]);
|
|
|
|
var libraries = yield runner.checkLibraries(
|
|
runner.getAPIClient(),
|
|
false,
|
|
responses.keyInfo.fullAccess.json,
|
|
[userLibraryID, publicationsLibraryID]
|
|
);
|
|
assert.lengthOf(libraries, 2);
|
|
assert.sameMembers(libraries, [userLibraryID, publicationsLibraryID]);
|
|
|
|
var libraries = yield runner.checkLibraries(
|
|
runner.getAPIClient(),
|
|
false,
|
|
responses.keyInfo.fullAccess.json,
|
|
[group1.libraryID]
|
|
);
|
|
assert.lengthOf(libraries, 1);
|
|
assert.sameMembers(libraries, [group1.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');
|
|
var libraries = yield runner.checkLibraries(
|
|
runner.getAPIClient(), false, responses.keyInfo.fullAccess.json
|
|
);
|
|
assert.lengthOf(libraries, 4);
|
|
assert.sameMembers(
|
|
libraries,
|
|
[userLibraryID, publicationsLibraryID, 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');
|
|
var libraries = yield runner.checkLibraries(
|
|
runner.getAPIClient(),
|
|
false,
|
|
responses.keyInfo.fullAccess.json,
|
|
[group1.libraryID, group2.libraryID]
|
|
);
|
|
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(), false, responses.keyInfo.fullAccess.json
|
|
);
|
|
assert.lengthOf(libraries, 4);
|
|
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, publicationsLibraryID, 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(), false, responses.keyInfo.fullAccess.json
|
|
);
|
|
assert.lengthOf(libraries, 3);
|
|
assert.sameMembers(libraries, [userLibraryID, publicationsLibraryID, group2.libraryID]);
|
|
assert.isFalse(Zotero.Groups.exists(groupData1.json.id));
|
|
assert.isTrue(Zotero.Groups.exists(groupData2.json.id));
|
|
})
|
|
|
|
it.skip("should keep 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);
|
|
}, "extra1");
|
|
var libraries = yield runner.checkLibraries(
|
|
runner.getAPIClient(), false, responses.keyInfo.fullAccess.json
|
|
);
|
|
assert.lengthOf(libraries, 3);
|
|
assert.sameMembers(libraries, [userLibraryID, publicationsLibraryID, group.libraryID]);
|
|
assert.isTrue(Zotero.Groups.exists(groupData.json.id));
|
|
})
|
|
|
|
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(), false, responses.keyInfo.fullAccess.json
|
|
);
|
|
assert.lengthOf(libraries, 0);
|
|
assert.isTrue(Zotero.Groups.exists(groupData.json.id));
|
|
})
|
|
})
|
|
|
|
describe("#checkUser()", function () {
|
|
it("should prompt for user update and perform on accept", function* () {
|
|
waitForDialog(function (dialog) {
|
|
var text = dialog.document.documentElement.textContent;
|
|
var matches = text.match(/'[^']*'/g);
|
|
assert.equal(matches.length, 4);
|
|
assert.equal(matches[0], "'A'");
|
|
assert.equal(matches[1], "'B'");
|
|
assert.equal(matches[2], "'B'");
|
|
assert.equal(matches[3], "'A'");
|
|
});
|
|
var cont = yield runner.checkUser(2, "B");
|
|
assert.isTrue(cont);
|
|
|
|
assert.equal(Zotero.Users.getCurrentUserID(), 2);
|
|
assert.equal(Zotero.Users.getCurrentUsername(), "B");
|
|
})
|
|
|
|
it("should prompt for user update and cancel", function* () {
|
|
yield Zotero.Users.setCurrentUserID(1);
|
|
yield Zotero.Users.setCurrentUsername("A");
|
|
|
|
waitForDialog(false, 'cancel');
|
|
var cont = yield runner.checkUser(2, "B");
|
|
assert.isFalse(cont);
|
|
|
|
assert.equal(Zotero.Users.getCurrentUserID(), 1);
|
|
assert.equal(Zotero.Users.getCurrentUsername(), "A");
|
|
})
|
|
})
|
|
|
|
describe("#sync()", function () {
|
|
before(function* () {
|
|
yield resetDB({
|
|
thisArg: this,
|
|
skipBundledFiles: true
|
|
});
|
|
|
|
yield Zotero.Libraries.init();
|
|
})
|
|
after(function* () {
|
|
yield resetDB({
|
|
thisArg: this
|
|
});
|
|
})
|
|
|
|
it("should perform a sync across all libraries", function* () {
|
|
yield Zotero.Users.setCurrentUserID(1);
|
|
yield Zotero.Users.setCurrentUsername("A");
|
|
|
|
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?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: []
|
|
});
|
|
// My Publications
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/publications/settings",
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": 10
|
|
},
|
|
json: []
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/publications/items?format=versions&includeTrashed=1",
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": 10
|
|
},
|
|
json: []
|
|
});
|
|
setResponse({
|
|
method: "GET",
|
|
url: "users/1/publications/deleted?since=0",
|
|
status: 200,
|
|
headers: {
|
|
"Last-Modified-Version": 10
|
|
},
|
|
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?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?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: []
|
|
});
|
|
|
|
yield runner.sync({
|
|
baseURL: baseURL,
|
|
apiKey: apiKey,
|
|
onError: e => { throw e },
|
|
});
|
|
|
|
// Check local library versions
|
|
assert.equal(
|
|
Zotero.Libraries.getVersion(Zotero.Libraries.userLibraryID),
|
|
5
|
|
);
|
|
assert.equal(
|
|
Zotero.Libraries.getVersion(Zotero.Libraries.publicationsLibraryID),
|
|
10
|
|
);
|
|
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 second
|
|
var lastSyncTime = Zotero.Sync.Data.Local.getLastSyncTime();
|
|
assert.isAbove(lastSyncTime, new Date().getTime() - 1000);
|
|
assert.isBelow(lastSyncTime, new Date().getTime());
|
|
})
|
|
})
|
|
})
|