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)
This commit is contained in:
parent
ac34f2c4f4
commit
5fee2bf4ca
8 changed files with 552 additions and 35 deletions
|
@ -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({
|
||||
|
|
|
@ -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 () {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue