commit 5fee2bf4ca1b69268f2ffb4b24f4206fc00a86c3
parent ac34f2c4f4db59f2e29284c00f69434320786d3d
Author: Dan Stillman <dstillman@zotero.org>
Date: Tue, 19 Jul 2016 18:58:48 -0400
Prompt to reset library data/files on loss of write access
On reset, items are overwritten with pristine versions if available and deleted
otherwise, and then the library is marked for a full sync. Unsynced/changed
files are deleted and marked for download.
Closes #1002
Todo:
- Handle API key access change (#953, in part)
- Handle 403 from data/file upload for existing users (#1041)
Diffstat:
8 files changed, 552 insertions(+), 35 deletions(-)
diff --git a/chrome/content/zotero/xpcom/data/group.js b/chrome/content/zotero/xpcom/data/group.js
@@ -23,6 +23,8 @@
***** END LICENSE BLOCK *****
*/
+"use strict";
+
Zotero.Group = function (params = {}) {
params.libraryType = 'group';
Zotero.Group._super.call(this, params);
@@ -240,23 +242,7 @@ Zotero.Group.prototype.fromJSON = function (json, userID) {
var editable = false;
var filesEditable = false;
if (userID) {
- // If user is owner or admin, make library editable, and make files editable unless they're
- // disabled altogether
- if (json.owner == userID || (json.admins && json.admins.indexOf(userID) != -1)) {
- editable = true;
- if (json.fileEditing != 'none') {
- filesEditable = true;
- }
- }
- // If user is member, make library and files editable if they're editable by all members
- else if (json.members && json.members.indexOf(userID) != -1) {
- if (json.libraryEditing == 'members') {
- editable = true;
- if (json.fileEditing == 'members') {
- filesEditable = true;
- }
- }
- }
+ ({ editable, filesEditable } = Zotero.Groups.getPermissionsFromJSON(json, userID));
}
this.editable = editable;
this.filesEditable = filesEditable;
diff --git a/chrome/content/zotero/xpcom/data/groups.js b/chrome/content/zotero/xpcom/data/groups.js
@@ -116,4 +116,31 @@ Zotero.Groups = new function () {
return this._cache.libraryIDByGroupID[groupID] || false;
}
+
+
+ this.getPermissionsFromJSON = function (json, userID) {
+ if (!json.owner) throw new Error("Invalid JSON provided for group data");
+ if (!userID) throw new Error("userID not provided");
+
+ var editable = false;
+ var filesEditable = false;
+ // If user is owner or admin, make library editable, and make files editable unless they're
+ // disabled altogether
+ if (json.owner == userID || (json.admins && json.admins.indexOf(userID) != -1)) {
+ editable = true;
+ if (json.fileEditing != 'none') {
+ filesEditable = true;
+ }
+ }
+ // If user is member, make library and files editable if they're editable by all members
+ else if (json.members && json.members.indexOf(userID) != -1) {
+ if (json.libraryEditing == 'members') {
+ editable = true;
+ if (json.fileEditing == 'members') {
+ filesEditable = true;
+ }
+ }
+ }
+ return { editable, filesEditable };
+ };
}
diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js
@@ -2399,7 +2399,7 @@ Zotero.Item.prototype.getFilename = function () {
/**
- * Asynchronous cached check for file existence, used for items view
+ * Asynchronous check for file existence
*/
Zotero.Item.prototype.fileExists = Zotero.Promise.coroutine(function* () {
if (!this.isAttachment()) {
diff --git a/chrome/content/zotero/xpcom/sync/syncLocal.js b/chrome/content/zotero/xpcom/sync/syncLocal.js
@@ -155,6 +155,207 @@ Zotero.Sync.Data.Local = {
}),
+ /**
+ * @return {Promise<Boolean>} - True if library updated, false to cancel
+ */
+ checkLibraryForAccess: Zotero.Promise.coroutine(function* (win, libraryID, editable, filesEditable) {
+ var library = Zotero.Libraries.get(libraryID);
+
+ // If library is going from editable to non-editable and there's unsynced local data, prompt
+ if (library.editable && !editable
+ && ((yield this._libraryHasUnsyncedData(libraryID))
+ || (yield this._libraryHasUnsyncedFiles(libraryID)))) {
+ let index = this._showWriteAccessLostPrompt(win, library);
+
+ // Reset library
+ if (index == 0) {
+ yield this._resetUnsyncedLibraryData(libraryID);
+ return true;
+ }
+
+ // Skip library
+ return false;
+ }
+
+ if (library.filesEditable && !filesEditable && (yield this._libraryHasUnsyncedFiles(libraryID))) {
+ let index = this._showFileWriteAccessLostPrompt(win, library);
+
+ // Reset library files
+ if (index == 0) {
+ yield this._resetUnsyncedLibraryFiles(libraryID);
+ return true;
+ }
+
+ // Skip library
+ return false;
+ }
+
+ return true;
+ }),
+
+
+ _libraryHasUnsyncedData: Zotero.Promise.coroutine(function* (libraryID) {
+ let settings = yield Zotero.SyncedSettings.getUnsynced(libraryID);
+ if (Object.keys(settings).length) {
+ return true;
+ }
+
+ for (let objectType of Zotero.DataObjectUtilities.getTypesForLibrary(libraryID)) {
+ let ids = yield Zotero.Sync.Data.Local.getUnsynced(objectType, libraryID);
+ if (ids.length) {
+ return true;
+ }
+
+ let keys = yield Zotero.Sync.Data.Local.getDeleted(objectType, libraryID);
+ if (keys.length) {
+ return true;
+ }
+ }
+
+ return false;
+ }),
+
+
+ _libraryHasUnsyncedFiles: Zotero.Promise.coroutine(function* (libraryID) {
+ yield Zotero.Sync.Storage.Local.checkForUpdatedFiles(libraryID);
+ return !!(yield Zotero.Sync.Storage.Local.getFilesToUpload(libraryID));
+ }),
+
+
+ _showWriteAccessLostPrompt: function (win, library) {
+ var libraryType = library.libraryType;
+ switch (libraryType) {
+ case 'group':
+ var msg = Zotero.getString('sync.error.groupWriteAccessLost',
+ [library.name, ZOTERO_CONFIG.DOMAIN_NAME])
+ + "\n\n"
+ + Zotero.getString('sync.error.groupCopyChangedItems')
+ var button1Text = Zotero.getString('sync.resetGroupAndSync');
+ var button2Text = Zotero.getString('sync.skipGroup');
+ break;
+
+ default:
+ throw new Error("Unsupported library type " + libraryType);
+ }
+
+ var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Components.interfaces.nsIPromptService);
+ var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
+ + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING)
+ + ps.BUTTON_DELAY_ENABLE;
+
+ return ps.confirmEx(
+ win,
+ Zotero.getString('general.permissionDenied'),
+ msg,
+ buttonFlags,
+ button1Text,
+ button2Text,
+ null,
+ null, {}
+ );
+ },
+
+
+ _showFileWriteAccessLostPrompt: function (win, library) {
+ var libraryType = library.libraryType;
+ switch (libraryType) {
+ case 'group':
+ var msg = Zotero.getString('sync.error.groupFileWriteAccessLost',
+ [library.name, ZOTERO_CONFIG.DOMAIN_NAME])
+ + "\n\n"
+ + Zotero.getString('sync.error.groupCopyChangedFiles')
+ var button1Text = Zotero.getString('sync.resetGroupFilesAndSync');
+ var button2Text = Zotero.getString('sync.skipGroup');
+ break;
+
+ default:
+ throw new Error("Unsupported library type " + libraryType);
+ }
+
+ var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Components.interfaces.nsIPromptService);
+ var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
+ + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING)
+ + ps.BUTTON_DELAY_ENABLE;
+
+ return ps.confirmEx(
+ win,
+ Zotero.getString('general.permissionDenied'),
+ msg,
+ buttonFlags,
+ button1Text,
+ button2Text,
+ null,
+ null, {}
+ );
+ },
+
+
+ _resetUnsyncedLibraryData: Zotero.Promise.coroutine(function* (libraryID) {
+ let settings = yield Zotero.SyncedSettings.getUnsynced(libraryID);
+ if (Object.keys(settings).length) {
+ yield Zotero.Promise.each(Object.keys(settings), function (key) {
+ return Zotero.SyncedSettings.clear(libraryID, key, { skipDeleteLog: true });
+ });
+ }
+
+ for (let objectType of Zotero.DataObjectUtilities.getTypesForLibrary(libraryID)) {
+ let objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType);
+ let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
+
+ // New/modified objects
+ let ids = yield Zotero.Sync.Data.Local.getUnsynced(objectType, libraryID);
+ let keys = ids.map(id => objectsClass.getLibraryAndKeyFromID(id).key);
+ let cacheVersions = yield this.getLatestCacheObjectVersions(objectType, libraryID, keys);
+ let toDelete = [];
+ for (let key of keys) {
+ let obj = objectsClass.getByLibraryAndKey(libraryID, key);
+
+ // If object is in cache, overwrite with pristine data
+ if (cacheVersions[key]) {
+ let json = yield this.getCacheObject(objectType, libraryID, key, cacheVersions[key]);
+ yield Zotero.DB.executeTransaction(function* () {
+ yield this._saveObjectFromJSON(obj, json, {});
+ }.bind(this));
+ }
+ // Otherwise, erase
+ else {
+ toDelete.push(objectsClass.getIDFromLibraryAndKey(libraryID, key));
+ }
+ }
+ if (toDelete.length) {
+ yield objectsClass.erase(toDelete, { skipDeleteLog: true });
+ }
+
+ // Deleted objects
+ keys = yield Zotero.Sync.Data.Local.getDeleted(objectType, libraryID);
+ yield this.removeObjectsFromDeleteLog(objectType, libraryID, keys);
+ }
+
+ // Mark library for full sync
+ var library = Zotero.Libraries.get(libraryID);
+ library.libraryVersion = -1;
+ yield library.saveTx();
+
+ yield this._resetUnsyncedLibraryFiles(libraryID);
+ }),
+
+
+ /**
+ * Delete unsynced files from library
+ *
+ * _libraryHasUnsyncedFiles(), which checks for updated files, must be called first.
+ */
+ _resetUnsyncedLibraryFiles: Zotero.Promise.coroutine(function* (libraryID) {
+ var itemIDs = yield Zotero.Sync.Storage.Local.getFilesToUpload(libraryID);
+ for (let itemID of itemIDs) {
+ let item = Zotero.Items.get(itemID);
+ yield item.deleteAttachmentFile();
+ }
+ }),
+
+
getSkippedLibraries: function () {
return this._getSkippedLibrariesByPrefix("L");
},
@@ -1117,11 +1318,11 @@ Zotero.Sync.Data.Local = {
}),
_saveObjectFromJSON: Zotero.Promise.coroutine(function* (obj, json, options) {
+ var results = {};
try {
+ results.key = json.key;
json = this._checkCacheJSON(json);
- var results = {
- key: json.key
- };
+
if (!options.skipData) {
obj.fromJSON(json.data);
}
@@ -1385,6 +1586,8 @@ Zotero.Sync.Data.Local = {
* @return {Promise}
*/
removeObjectsFromDeleteLog: function (objectType, libraryID, keys) {
+ if (!keys.length) Zotero.Promise.resolve();
+
var syncObjectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType);
var sql = "DELETE FROM syncDeleteLog WHERE libraryID=? AND syncObjectTypeID=? AND key IN (";
return Zotero.DB.executeTransaction(function* () {
diff --git a/chrome/content/zotero/xpcom/sync/syncRunner.js b/chrome/content/zotero/xpcom/sync/syncRunner.js
@@ -293,15 +293,6 @@ Zotero.Sync.Runner_Module = function (options = {}) {
*/
this.checkLibraries = Zotero.Promise.coroutine(function* (client, options, keyInfo, libraries = []) {
var access = keyInfo.access;
-
-/* var libraries = [
- Zotero.Libraries.userLibraryID,
- Zotero.Libraries.publicationsLibraryID,
- // Groups sorted by name
- ...(Zotero.Groups.getAll().map(x => x.libraryID))
- ];
-*/
-
var syncAllLibraries = !libraries || !libraries.length;
// TODO: Ability to remove or disable editing of user library?
@@ -309,7 +300,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
if (syncAllLibraries) {
if (access.user && access.user.library) {
libraries = [Zotero.Libraries.userLibraryID, Zotero.Libraries.publicationsLibraryID];
- // Remove skipped libraries
+ // If syncing all libraries, remove skipped libraries
libraries = Zotero.Utilities.arrayDiff(
libraries, Zotero.Sync.Data.Local.getSkippedLibraries()
);
@@ -472,7 +463,22 @@ Zotero.Sync.Runner_Module = function (options = {}) {
throw new Error("Group " + groupID + " not found");
}
let group = Zotero.Groups.get(groupID);
- if (!group) {
+ if (group) {
+ // Check if the user's permissions for the group have changed, and prompt to reset
+ // data if so
+ let { editable, filesEditable } = Zotero.Groups.getPermissionsFromJSON(
+ info.data, keyInfo.userID
+ );
+ let keepGoing = yield Zotero.Sync.Data.Local.checkLibraryForAccess(
+ null, group.libraryID, editable, filesEditable
+ );
+ // User chose to skip library
+ if (!keepGoing) {
+ Zotero.debug("Skipping sync of group " + group.id);
+ continue;
+ }
+ }
+ else {
group = new Zotero.Group;
group.id = groupID;
}
diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties
@@ -825,6 +825,8 @@ sync.syncWith = Sync with %S
sync.cancel = Cancel Sync
sync.openSyncPreferences = Open Sync Preferences
sync.resetGroupAndSync = Reset Group and Sync
+sync.resetGroupFilesAndSync = Reset Group Files and Sync
+sync.skipGroup = Skip Group
sync.removeGroupsAndSync = Remove Groups and Sync
sync.error.usernameNotSet = Username not set
@@ -840,9 +842,10 @@ sync.error.loginManagerCorrupted1 = Zotero cannot access your login information,
sync.error.loginManagerCorrupted2 = Close %1$S, remove cert8.db, key3.db, and logins.json from your %2$S profile directory, and re-enter your Zotero login information in the Sync pane of the Zotero preferences.
sync.error.syncInProgress = A sync operation is already in progress.
sync.error.syncInProgress.wait = Wait for the previous sync to complete or restart %S.
-sync.error.writeAccessLost = You no longer have write access to the Zotero group '%S', and items you've added or edited cannot be synced to the server.
-sync.error.groupWillBeReset = If you continue, your copy of the group will be reset to its state on the server, and local modifications to items and files will be lost.
-sync.error.copyChangedItems = If you would like a chance to copy your changes elsewhere or to request write access from a group administrator, cancel the sync now.
+sync.error.groupWriteAccessLost = You no longer have write access to the group ‘%1$S’, and changes you’ve made locally cannot be uploaded. If you continue, your copy of the group will be reset to its state on %2$S, and local changes to items and files will be lost.
+sync.error.groupFileWriteAccessLost = You no longer have file editing access for the group ‘%1$S’, and files you’ve changed locally cannot be uploaded. If you continue, all group files will be reset to their state on %2$S.
+sync.error.groupCopyChangedItems = If you would like a chance to copy your changes elsewhere or to request write access from a group administrator, you can skip syncing of the group now.
+sync.error.groupCopyChangedFiles = If you would like a chance to copy modified files elsewhere or to request file editing access from a group administrator, you can skip syncing of the group now.
sync.error.manualInterventionRequired = Conflicts have suspended automatic syncing.
sync.error.clickSyncIcon = Click the sync icon to resolve them.
sync.error.invalidClock = The system clock is set to an invalid time. You will need to correct this to sync with the Zotero server.
diff --git a/test/tests/syncLocalTest.js b/test/tests/syncLocalTest.js
@@ -96,6 +96,216 @@ describe("Zotero.Sync.Data.Local", function() {
});
+ describe("#checkLibraryForAccess()", function () {
+ //
+ // editable
+ //
+ it("should prompt if library is changing from editable to non-editable and reset library on accept", function* () {
+ var group = yield createGroup();
+ var libraryID = group.libraryID;
+ var promise = waitForDialog(function (dialog) {
+ var text = dialog.document.documentElement.textContent;
+ assert.include(text, group.name);
+ });
+
+ var mock = sinon.mock(Zotero.Sync.Data.Local);
+ mock.expects("_resetUnsyncedLibraryData").once().returns(Zotero.Promise.resolve());
+ mock.expects("_resetUnsyncedLibraryFiles").never();
+
+ assert.isTrue(
+ yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, false, false)
+ );
+ yield promise;
+
+ mock.verify();
+ });
+
+ it("should prompt if library is changing from editable to non-editable but not reset library on cancel", function* () {
+ var group = yield createGroup();
+ var libraryID = group.libraryID;
+ var promise = waitForDialog(function (dialog) {
+ var text = dialog.document.documentElement.textContent;
+ assert.include(text, group.name);
+ }, "cancel");
+
+ var mock = sinon.mock(Zotero.Sync.Data.Local);
+ mock.expects("_resetUnsyncedLibraryData").never();
+ mock.expects("_resetUnsyncedLibraryFiles").never();
+
+ assert.isFalse(
+ yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, false, false)
+ );
+ yield promise;
+
+ mock.verify();
+ });
+
+ it("should not prompt if library is changing from editable to non-editable", function* () {
+ var group = yield createGroup({ editable: false, filesEditable: false });
+ var libraryID = group.libraryID;
+ yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, true, true);
+ });
+
+ //
+ // filesEditable
+ //
+ it("should prompt if library is changing from filesEditable to non-filesEditable and reset library files on accept", function* () {
+ var group = yield createGroup();
+ var libraryID = group.libraryID;
+ var promise = waitForDialog(function (dialog) {
+ var text = dialog.document.documentElement.textContent;
+ assert.include(text, group.name);
+ });
+
+ var mock = sinon.mock(Zotero.Sync.Data.Local);
+ mock.expects("_resetUnsyncedLibraryData").never();
+ mock.expects("_resetUnsyncedLibraryFiles").once().returns(Zotero.Promise.resolve());
+
+ assert.isTrue(
+ yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, true, false)
+ );
+ yield promise;
+
+ mock.verify();
+ });
+
+ it("should prompt if library is changing from filesEditable to non-filesEditable but not reset library files on cancel", function* () {
+ var group = yield createGroup();
+ var libraryID = group.libraryID;
+ var promise = waitForDialog(function (dialog) {
+ var text = dialog.document.documentElement.textContent;
+ assert.include(text, group.name);
+ }, "cancel");
+
+ var mock = sinon.mock(Zotero.Sync.Data.Local);
+ mock.expects("_resetUnsyncedLibraryData").never();
+ mock.expects("_resetUnsyncedLibraryFiles").never();
+
+ assert.isFalse(
+ yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, true, false)
+ );
+ yield promise;
+
+ mock.verify();
+ });
+ });
+
+
+ describe("#_libraryHasUnsyncedData()", function () {
+ it("should return true for unsynced setting", function* () {
+ var group = yield createGroup();
+ var libraryID = group.libraryID;
+ yield Zotero.SyncedSettings.set(libraryID, "testSetting", { foo: "bar" });
+ assert.isTrue(yield Zotero.Sync.Data.Local._libraryHasUnsyncedData(libraryID));
+ });
+
+ it("should return true for unsynced item", function* () {
+ var group = yield createGroup();
+ var libraryID = group.libraryID;
+ yield createDataObject('item', { libraryID });
+ assert.isTrue(yield Zotero.Sync.Data.Local._libraryHasUnsyncedData(libraryID));
+ });
+
+ it("should return false if no changes", function* () {
+ var group = yield createGroup();
+ var libraryID = group.libraryID;
+ assert.isFalse(yield Zotero.Sync.Data.Local._libraryHasUnsyncedData(libraryID));
+ });
+ });
+
+
+ describe("#_resetUnsyncedLibraryData()", function () {
+ it("should revert group and mark for full sync", function* () {
+ var group = yield createGroup({
+ version: 1,
+ libraryVersion: 2
+ });
+ var libraryID = group.libraryID;
+
+ // New setting
+ yield Zotero.SyncedSettings.set(libraryID, "testSetting", { foo: "bar" });
+
+ // Changed collection
+ var changedCollection = yield createDataObject('collection', { libraryID, version: 1 });
+ var originalCollectionName = changedCollection.name;
+ yield Zotero.Sync.Data.Local.saveCacheObject(
+ 'collection', libraryID, changedCollection.toJSON()
+ );
+ yield modifyDataObject(changedCollection);
+
+ // Unchanged item
+ var unchangedItem = yield createDataObject('item', { libraryID, version: 1, synced: true });
+ yield Zotero.Sync.Data.Local.saveCacheObject(
+ 'item', libraryID, unchangedItem.toJSON()
+ );
+
+ // Changed item
+ var changedItem = yield createDataObject('item', { libraryID, version: 1 });
+ var originalChangedItemTitle = changedItem.getField('title');
+ yield Zotero.Sync.Data.Local.saveCacheObject('item', libraryID, changedItem.toJSON());
+ yield modifyDataObject(changedItem);
+
+ // New item
+ var newItem = yield createDataObject('item', { libraryID, version: 1 });
+ var newItemKey = newItem.key;
+
+ // Delete item
+ var deletedItem = yield createDataObject('item', { libraryID });
+ var deletedItemKey = deletedItem.key;
+ yield deletedItem.eraseTx();
+
+ yield Zotero.Sync.Data.Local._resetUnsyncedLibraryData(libraryID);
+
+ assert.isNull(Zotero.SyncedSettings.get(group.libraryID, "testSetting"));
+
+ assert.equal(changedCollection.name, originalCollectionName);
+ assert.isTrue(changedCollection.synced);
+
+ assert.isTrue(unchangedItem.synced);
+
+ assert.equal(changedItem.getField('title'), originalChangedItemTitle);
+ assert.isTrue(changedItem.synced);
+
+ assert.isFalse(Zotero.Items.get(newItemKey));
+
+ assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, deletedItemKey));
+
+ assert.equal(group.libraryVersion, -1);
+ });
+
+
+ describe("#_resetUnsyncedLibraryFiles", function () {
+ it("should delete unsynced files", function* () {
+ var group = yield createGroup({
+ version: 1,
+ libraryVersion: 2
+ });
+ var libraryID = group.libraryID;
+
+ var attachment1 = yield importFileAttachment('test.png', { libraryID });
+ attachment1.attachmentSyncState = "in_sync";
+ attachment1.attachmentSyncedModificationTime = 1234567890000;
+ attachment1.attachmentSyncedHash = "8caf2ee22919d6725eb0648b98ef6bad";
+ var attachment2 = yield importFileAttachment('test.pdf', { libraryID });
+
+ // Has to be called before _resetUnsyncedLibraryFiles()
+ assert.isTrue(yield Zotero.Sync.Data.Local._libraryHasUnsyncedFiles(libraryID));
+
+ yield Zotero.Sync.Data.Local._resetUnsyncedLibraryFiles(libraryID);
+
+ assert.isFalse(yield attachment1.fileExists());
+ assert.isFalse(yield attachment2.fileExists());
+ assert.equal(
+ attachment1.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD
+ );
+ assert.equal(
+ attachment2.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD
+ );
+ });
+ });
+ });
+
+
describe("#getLatestCacheObjectVersions", function () {
before(function* () {
yield resetDB({
diff --git a/test/tests/syncRunnerTest.js b/test/tests/syncRunnerTest.js
@@ -309,9 +309,16 @@ describe("Zotero.Sync.Runner", function () {
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, 4);
assert.sameMembers(
libraries,
@@ -350,12 +357,19 @@ describe("Zotero.Sync.Runner", function () {
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]);
@@ -443,6 +457,74 @@ describe("Zotero.Sync.Runner", function () {
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.reset();
+ });
})
describe("#sync()", function () {