zotero/test/tests/storageLocalTest.js
Dan Stillman a1ea7037c2 Fix locally missing attachments never being downloaded
Since f21e1b2d32, a full local file scan no longer runs periodically and
on every manual sync, so locally missed attachments stayed marked for
upload and were skipped as unavailable instead of being downloaded.
"Reset File Sync History" marked every attachment for upload, including
files that had never been downloaded, so the forced download check added
in 404fc41b88 found nothing to download.

Missing files are now marked for download when the upload queue is
filled, and in at-sync-time mode they're downloaded in the same sync.
The reset marks them for download directly, and downloads are no longer
skipped just because there were no remote storage changes.

https://forums.zotero.org/discussion/133414/
2026-08-26 10:36:54 -04:00

821 lines
29 KiB
JavaScript

"use strict";
describe("Zotero.Sync.Storage.Local", function () {
describe("#checkForUpdatedFiles()", function () {
it("should flag modified file for upload and return it", async function () {
// Create attachment
let item = await importTextAttachment();
var hash = await item.attachmentHash;
// Set file mtime to the past (without milliseconds, which aren't used on OS X)
var mtime = (Math.floor(new Date().getTime() / 1000) * 1000) - 1000;
await OS.File.setDates(((await item.getFilePathAsync())), null, mtime);
// Mark as synced, so it will be checked
item.attachmentSyncedModificationTime = mtime;
item.attachmentSyncedHash = hash;
item.attachmentSyncState = "in_sync";
await item.saveTx({ skipAll: true });
// Update mtime and contents
var path = await item.getFilePathAsync();
await OS.File.setDates(path);
await Zotero.File.putContentsAsync(path, Zotero.Utilities.randomString());
// File should be returned
var libraryID = Zotero.Libraries.userLibraryID;
var changed = await Zotero.Sync.Storage.Local.checkForUpdatedFiles(libraryID, [item.id]);
await item.eraseTx();
assert.equal(changed, true);
assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_UPLOAD);
});
it("should skip a file if mod time hasn't changed", async function () {
// Create attachment
let item = await importTextAttachment();
var hash = await item.attachmentHash;
var mtime = await item.attachmentModificationTime;
// Mark as synced, so it will be checked
item.attachmentSyncedModificationTime = mtime;
item.attachmentSyncedHash = hash;
item.attachmentSyncState = "in_sync";
await item.saveTx({ skipAll: true });
var libraryID = Zotero.Libraries.userLibraryID;
var changed = await Zotero.Sync.Storage.Local.checkForUpdatedFiles(libraryID, [item.id]);
var syncState = item.attachmentSyncState;
await item.eraseTx();
assert.isFalse(changed);
assert.equal(syncState, Zotero.Sync.Storage.Local.SYNC_STATE_IN_SYNC);
})
it("should skip a file if mod time has changed but contents haven't", async function () {
// Create attachment
let item = await importTextAttachment();
var hash = await item.attachmentHash;
// Set file mtime to the past (without milliseconds, which aren't used on OS X)
var mtime = (Math.floor(new Date().getTime() / 1000) * 1000) - 1000;
await OS.File.setDates(((await item.getFilePathAsync())), null, mtime);
// Mark as synced, so it will be checked
item.attachmentSyncedModificationTime = mtime;
item.attachmentSyncedHash = hash;
item.attachmentSyncState = "in_sync";
await item.saveTx({ skipAll: true });
// Update mtime, but not contents
var path = await item.getFilePathAsync();
await OS.File.setDates(path);
var libraryID = Zotero.Libraries.userLibraryID;
var changed = await Zotero.Sync.Storage.Local.checkForUpdatedFiles(libraryID, [item.id]);
var syncState = item.attachmentSyncState;
var syncedModTime = item.attachmentSyncedModificationTime;
var newModTime = await item.attachmentModificationTime;
await item.eraseTx();
assert.isFalse(changed);
assert.equal(syncState, Zotero.Sync.Storage.Local.SYNC_STATE_IN_SYNC);
assert.equal(syncedModTime, newModTime);
});
it("should flag a missing local file for download if already marked for upload", async function () {
// Create attachment
let item = await importFileAttachment('test.png');
var hash = await item.attachmentHash;
var mtime = await item.attachmentModificationTime;
// Mark as synced, so it will be checked
item.attachmentSyncedModificationTime = mtime;
item.attachmentSyncedHash = hash;
item.attachmentSyncState = "to_upload";
await item.saveTx({ skipAll: true });
// Delete local file
await IOUtils.remove(item.getFilePath());
// File should be marked for download and not returned
var libraryID = Zotero.Libraries.userLibraryID;
var changed = await Zotero.Sync.Storage.Local.checkForUpdatedFiles(libraryID, [item.id]);
await item.eraseTx();
assert.isTrue(changed);
assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD);
});
})
describe("#updateSyncStates()", function () {
it("should update attachment sync states to 'to_upload'", async function () {
var attachment1 = await importFileAttachment('test.png');
attachment1.attachmentSyncState = 'in_sync';
await attachment1.saveTx();
var attachment2 = await importFileAttachment('test.png');
attachment2.attachmentSyncState = 'in_sync';
await attachment2.saveTx();
var local = Zotero.Sync.Storage.Local;
await local.updateSyncStates([attachment1, attachment2], 'to_upload');
for (let attachment of [attachment1, attachment2]) {
assert.strictEqual(attachment.attachmentSyncState, local.SYNC_STATE_TO_UPLOAD);
let state = await Zotero.DB.valueQueryAsync(
"SELECT syncState FROM itemAttachments WHERE itemID=?", attachment.id
);
assert.strictEqual(state, local.SYNC_STATE_TO_UPLOAD);
}
});
});
describe("#resetAllSyncStates()", function () {
it("should reset attachment sync states to 'to_upload'", async function () {
var attachment = await importFileAttachment('test.png');
attachment.attachmentSyncState = 'in_sync';
await attachment.saveTx();
var local = Zotero.Sync.Storage.Local;
await local.resetAllSyncStates(attachment.libraryID)
assert.strictEqual(attachment.attachmentSyncState, local.SYNC_STATE_TO_UPLOAD);
var state = await Zotero.DB.valueQueryAsync(
"SELECT syncState FROM itemAttachments WHERE itemID=?", attachment.id
);
assert.strictEqual(state, local.SYNC_STATE_TO_UPLOAD);
});
it("should leave an attachment with a missing file marked for download", async function () {
var attachment = await importFileAttachment('test.png');
attachment.attachmentSyncState = 'to_download';
await attachment.saveTx();
await IOUtils.remove(attachment.getFilePath());
var local = Zotero.Sync.Storage.Local;
await local.resetAllSyncStates(attachment.libraryID);
assert.strictEqual(attachment.attachmentSyncState, local.SYNC_STATE_TO_DOWNLOAD);
var state = await Zotero.DB.valueQueryAsync(
"SELECT syncState FROM itemAttachments WHERE itemID=?", attachment.id
);
assert.strictEqual(state, local.SYNC_STATE_TO_DOWNLOAD);
});
});
describe("#processDownload()", function () {
describe("single file", function () {
it("should download a single file into the attachment directory", async function () {
var libraryID = Zotero.Libraries.userLibraryID;
var parentItem = await createDataObject('item');
var key = Zotero.DataObjectUtilities.generateKey();
var fileContents = Zotero.Utilities.randomString();
var oldFilename = "Old File";
var tmpDir = Zotero.getTempDirectory().path;
var tmpFile = OS.Path.join(tmpDir, key + '.tmp');
await Zotero.File.putContentsAsync(tmpFile, fileContents);
// Create an existing attachment directory to replace
var dir = Zotero.Attachments.getStorageDirectoryByLibraryAndKey(libraryID, key).path;
await OS.File.makeDir(
dir,
{
unixMode: 0o755
}
);
await Zotero.File.putContentsAsync(OS.Path.join(dir, oldFilename), '');
var md5 = Zotero.Utilities.Internal.md5(Zotero.File.pathToFile(tmpFile));
var mtime = 1445667239000;
var json = {
key,
version: 10,
itemType: 'attachment',
linkMode: 'imported_url',
url: 'https://example.com/foo.txt',
filename: 'foo.txt',
contentType: 'text/plain',
charset: 'utf-8',
md5,
mtime
};
await Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, [json]);
var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
await Zotero.Sync.Storage.Local.processDownload({
item,
md5,
mtime
});
await OS.File.remove(tmpFile);
var storageDir = Zotero.Attachments.getStorageDirectory(item).path;
// Make sure previous files don't exist
assert.isFalse(await OS.File.exists(OS.Path.join(storageDir, oldFilename)));
// Make sure main file matches attachment hash and mtime
assert.equal(
await item.attachmentHash, Zotero.Utilities.Internal.md5(fileContents)
);
assert.equal(await item.attachmentModificationTime, mtime);
});
it("should download and rename a single file with invalid filename into the attachment directory", async function () {
var libraryID = Zotero.Libraries.userLibraryID;
var parentItem = await createDataObject('item');
var key = Zotero.DataObjectUtilities.generateKey();
var fileContents = Zotero.Utilities.randomString();
var oldFilename = "Old File";
var newFilename = " ab — c :.txt.";
var filteredFilename = " ab — c .txt.";
var tmpDir = Zotero.getTempDirectory().path;
var tmpFile = OS.Path.join(tmpDir, key + '.tmp');
await Zotero.File.putContentsAsync(tmpFile, fileContents);
// Create an existing attachment directory to replace
var dir = Zotero.Attachments.getStorageDirectoryByLibraryAndKey(libraryID, key).path;
await OS.File.makeDir(
dir,
{
unixMode: 0o755
}
);
await Zotero.File.putContentsAsync(OS.Path.join(dir, oldFilename), '');
var md5 = Zotero.Utilities.Internal.md5(Zotero.File.pathToFile(tmpFile));
var mtime = 1445667239000;
var json = {
key,
version: 10,
itemType: 'attachment',
linkMode: 'imported_url',
url: 'https://example.com/foo.txt',
filename: newFilename,
contentType: 'text/plain',
charset: 'utf-8',
md5,
mtime
};
await Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, [json]);
var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
await Zotero.Sync.Storage.Local.processDownload({
item,
md5,
mtime
});
await OS.File.remove(tmpFile);
var storageDir = Zotero.Attachments.getStorageDirectory(item).path;
// Make sure previous file doesn't exist
assert.isFalse(await OS.File.exists(OS.Path.join(storageDir, oldFilename)));
// And new one does
assert.isTrue(await OS.File.exists(OS.Path.join(storageDir, filteredFilename)));
// Make sure main file matches attachment hash and mtime
assert.equal(
await item.attachmentHash, Zotero.Utilities.Internal.md5(fileContents)
);
assert.equal(await item.attachmentModificationTime, mtime);
});
it("should download and rename a single file with invalid filename using Windows parsing rules into the attachment directory", async function () {
var libraryID = Zotero.Libraries.userLibraryID;
var parentItem = await createDataObject('item');
var key = Zotero.DataObjectUtilities.generateKey();
var fileContents = Zotero.Utilities.randomString();
var oldFilename = "Old File";
var newFilename = "a:b.txt";
var filteredFilename = "ab.txt";
var tmpDir = Zotero.getTempDirectory().path;
var tmpFile = OS.Path.join(tmpDir, key + '.tmp');
await Zotero.File.putContentsAsync(tmpFile, fileContents);
// Create an existing attachment directory to replace
var dir = Zotero.Attachments.getStorageDirectoryByLibraryAndKey(libraryID, key).path;
await OS.File.makeDir(
dir,
{
unixMode: 0o755
}
);
await Zotero.File.putContentsAsync(OS.Path.join(dir, oldFilename), '');
var md5 = Zotero.Utilities.Internal.md5(Zotero.File.pathToFile(tmpFile));
var mtime = 1445667239000;
var json = {
key,
version: 10,
itemType: 'attachment',
linkMode: 'imported_url',
url: 'https://example.com/foo.txt',
filename: 'a:b.txt',
contentType: 'text/plain',
charset: 'utf-8',
md5,
mtime
};
await Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, [json]);
var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
// Stub functions to simulate OS.Path.basename() behavior on Windows
var basenameOrigFunc = OS.Path.basename.bind(OS.Path);
var basenameStub = sinon.stub(OS.Path, "basename").callsFake((path) => {
// Split on colon
if (path.endsWith("a:b.txt")) {
return "b.txt";
}
return basenameOrigFunc(path);
});
var pathToFileOrigFunc = Zotero.File.pathToFile.bind(Zotero.File);
var pathToFileStub = sinon.stub(Zotero.File, "pathToFile").callsFake((path) => {
if (path.includes(":")) {
throw new Error("Path contains colon");
}
return pathToFileOrigFunc(path);
});
await Zotero.Sync.Storage.Local.processDownload({
item,
md5,
mtime
});
await OS.File.remove(tmpFile);
var storageDir = Zotero.Attachments.getStorageDirectory(item).path;
basenameStub.restore();
pathToFileStub.restore();
// Make sure path is set correctly
assert.equal(item.getFilePath(), OS.Path.join(storageDir, filteredFilename));
// Make sure previous files don't exist
assert.isFalse(await OS.File.exists(OS.Path.join(storageDir, oldFilename)));
// And new one does
assert.isTrue(await OS.File.exists(OS.Path.join(storageDir, filteredFilename)));
// Make sure main file matches attachment hash and mtime
assert.equal(
await item.attachmentHash, Zotero.Utilities.Internal.md5(fileContents)
);
assert.equal(await item.attachmentModificationTime, mtime);
});
});
describe("ZIP", function () {
it("should download and extract a ZIP file into the attachment directory", async function () {
var file1Name = 'index.html';
var file1Contents = '<html><body>Test</body></html>';
var file2Name = 'aux1.txt';
var file2Contents = 'Test 1';
var subDirName = 'sub';
var file3Name = 'aux2';
var file3Contents = 'Test 2';
var libraryID = Zotero.Libraries.userLibraryID;
var parentItem = await createDataObject('item');
var key = Zotero.DataObjectUtilities.generateKey();
var tmpDir = Zotero.getTempDirectory().path;
var zipFile = OS.Path.join(tmpDir, key + '.tmp');
// Create ZIP file with subdirectory
var tmpDir = Zotero.getTempDirectory().path;
var zipDir = await getTempDirectory();
await Zotero.File.putContentsAsync(OS.Path.join(zipDir, file1Name), file1Contents);
await Zotero.File.putContentsAsync(OS.Path.join(zipDir, file2Name), file2Contents);
var subDir = OS.Path.join(zipDir, subDirName);
await OS.File.makeDir(subDir);
await Zotero.File.putContentsAsync(OS.Path.join(subDir, file3Name), file3Contents);
await Zotero.File.zipDirectory(zipDir, zipFile);
await removeDir(zipDir);
// Create an existing attachment directory (and subdirectory) to replace
var dir = Zotero.Attachments.getStorageDirectoryByLibraryAndKey(libraryID, key).path;
await OS.File.makeDir(
OS.Path.join(dir, 'subdir'),
{
from: Zotero.DataDirectory.dir,
unixMode: 0o755
}
);
await Zotero.File.putContentsAsync(OS.Path.join(dir, 'A'), '');
await Zotero.File.putContentsAsync(OS.Path.join(dir, 'subdir', 'B'), '');
var md5 = Zotero.Utilities.Internal.md5(Zotero.File.pathToFile(zipFile));
var mtime = 1445667239000;
var json = {
key,
version: 10,
itemType: 'attachment',
linkMode: 'imported_url',
url: 'https://example.com',
filename: file1Name,
contentType: 'text/html',
charset: 'utf-8',
md5,
mtime
};
await Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, [json]);
var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
await Zotero.Sync.Storage.Local.processDownload({
item,
md5,
mtime,
compressed: true
});
await OS.File.remove(zipFile);
var storageDir = Zotero.Attachments.getStorageDirectory(item).path;
// Make sure previous files don't exist
assert.isFalse(await OS.File.exists(OS.Path.join(storageDir, 'A')));
assert.isFalse(await OS.File.exists(OS.Path.join(storageDir, 'subdir')));
assert.isFalse(await OS.File.exists(OS.Path.join(storageDir, 'subdir', 'B')));
// Make sure main file matches attachment hash and mtime
assert.equal(
await item.attachmentHash, Zotero.Utilities.Internal.md5(file1Contents)
);
assert.equal(await item.attachmentModificationTime, mtime);
// Check second file
assert.equal(
await Zotero.File.getContentsAsync(OS.Path.join(storageDir, file2Name)),
file2Contents
);
// Check subdirectory and file
assert.isTrue(((await OS.File.stat(OS.Path.join(storageDir, subDirName)))).isDir);
assert.equal(
await Zotero.File.getContentsAsync(OS.Path.join(storageDir, subDirName, file3Name)),
file3Contents
);
});
it("should download and rename a ZIP file with invalid filename using Windows parsing rules into the attachment directory", async function () {
var libraryID = Zotero.Libraries.userLibraryID;
var parentItem = await createDataObject('item');
var key = Zotero.DataObjectUtilities.generateKey();
var oldFilename = "Old File";
var oldAuxFilename = "a.gif";
var newFilename = "a:b.html";
var fileContents = Zotero.Utilities.randomString();
var newAuxFilename = "b.gif";
var filteredFilename = "ab.html";
var tmpDir = Zotero.getTempDirectory().path;
var zipFile = OS.Path.join(tmpDir, key + '.tmp');
// Create ZIP file
var tmpDir = Zotero.getTempDirectory().path;
var zipDir = await getTempDirectory();
await Zotero.File.putContentsAsync(OS.Path.join(zipDir, newFilename), fileContents);
await Zotero.File.putContentsAsync(OS.Path.join(zipDir, newAuxFilename), '');
await Zotero.File.zipDirectory(zipDir, zipFile);
await removeDir(zipDir);
// Create an existing attachment directory to replace
var dir = Zotero.Attachments.getStorageDirectoryByLibraryAndKey(libraryID, key).path;
await OS.File.makeDir(
dir,
{
unixMode: 0o755
}
);
await Zotero.File.putContentsAsync(OS.Path.join(dir, oldFilename), '');
await Zotero.File.putContentsAsync(OS.Path.join(dir, oldAuxFilename), '');
var md5 = Zotero.Utilities.Internal.md5(Zotero.File.pathToFile(zipFile));
var mtime = 1445667239000;
var json = {
key,
version: 10,
itemType: 'attachment',
linkMode: 'imported_url',
url: 'https://example.com/foo.html',
filename: 'a:b.html',
contentType: 'text/plain',
charset: 'utf-8',
md5,
mtime
};
await Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, [json]);
var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
// Stub functions to simulate OS.Path.basename() behavior on Windows
var basenameOrigFunc = OS.Path.basename.bind(OS.Path);
var basenameStub = sinon.stub(OS.Path, "basename").callsFake((path) => {
// Split on colon
if (path.endsWith("a:b.html")) {
return "b.html";
}
return basenameOrigFunc(path);
});
var pathToFileOrigFunc = Zotero.File.pathToFile.bind(Zotero.File);
var pathToFileStub = sinon.stub(Zotero.File, "pathToFile").callsFake((path) => {
if (path.includes(":")) {
throw new Error("Path contains colon");
}
return pathToFileOrigFunc(path);
});
await Zotero.Sync.Storage.Local.processDownload({
item,
md5,
mtime,
compressed: true
});
await OS.File.remove(zipFile);
var storageDir = Zotero.Attachments.getStorageDirectory(item).path;
basenameStub.restore();
pathToFileStub.restore();
// Make sure path is set correctly
assert.equal(item.getFilePath(), OS.Path.join(storageDir, filteredFilename));
// Make sure previous files don't exist
assert.isFalse(await OS.File.exists(OS.Path.join(storageDir, oldFilename)));
assert.isFalse(await OS.File.exists(OS.Path.join(storageDir, oldAuxFilename)));
// And new ones do
assert.isTrue(await OS.File.exists(OS.Path.join(storageDir, filteredFilename)));
assert.isTrue(await OS.File.exists(OS.Path.join(storageDir, newAuxFilename)));
// Make sure main file matches attachment hash and mtime
assert.equal(
await item.attachmentHash, Zotero.Utilities.Internal.md5(fileContents)
);
assert.equal(await item.attachmentModificationTime, mtime);
});
it("should rename a single HTML file in an old multi-file ZIP to match the current primary filename", async function () {
var libraryID = Zotero.Libraries.userLibraryID;
var parentItem = await createDataObject('item');
var key = Zotero.DataObjectUtilities.generateKey();
var oldFilename = "a.html";
var auxFilename = "a.gif";
var newFilename = "b.html";
var fileContents = Zotero.Utilities.randomString();
var tmpDir = Zotero.getTempDirectory().path;
var zipFile = OS.Path.join(tmpDir, key + '.tmp');
// Create ZIP file
var tmpDir = Zotero.getTempDirectory().path;
var zipDir = await getTempDirectory();
await Zotero.File.putContentsAsync(PathUtils.join(zipDir, oldFilename), fileContents);
await Zotero.File.putContentsAsync(PathUtils.join(zipDir, auxFilename), '');
await Zotero.File.zipDirectory(zipDir, zipFile);
await removeDir(zipDir);
var md5 = Zotero.Utilities.Internal.md5(Zotero.File.pathToFile(zipFile));
var mtime = 1445667239000;
var json = {
key,
version: 10,
itemType: 'attachment',
linkMode: 'imported_url',
url: 'https://example.com/foo.html',
filename: 'b.html',
contentType: 'text/plain',
charset: 'utf-8',
md5,
mtime
};
await Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, [json]);
var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
await Zotero.Sync.Storage.Local.processDownload({
item,
md5,
mtime,
compressed: true
});
await OS.File.remove(zipFile);
var storageDir = Zotero.Attachments.getStorageDirectory(item).path;
// Make sure path is set correctly
assert.equal(item.getFilePath(), PathUtils.join(storageDir, newFilename));
// Make sure previous file doesn't exist
assert.isFalse(await IOUtils.exists(PathUtils.join(storageDir, oldFilename)));
// And new ones do
assert.isTrue(await IOUtils.exists(PathUtils.join(storageDir, newFilename)));
assert.isTrue(await IOUtils.exists(PathUtils.join(storageDir, auxFilename)));
// Make sure main file matches attachment hash and mtime
assert.equal(
await item.attachmentHash, Zotero.Utilities.Internal.md5(fileContents)
);
assert.equal(await item.attachmentModificationTime, mtime);
});
});
})
describe("#getConflicts()", function () {
it("should return an array of objects for attachments in conflict", async function () {
var libraryID = Zotero.Libraries.userLibraryID;
var item1 = await importFileAttachment('test.png');
item1.version = 10;
await item1.saveTx();
var item2 = await importTextAttachment();
var item3 = await importHTMLAttachment();
item3.version = 11;
await item3.saveTx();
var json1 = item1.toJSON();
var json3 = item3.toJSON();
// Change remote mtimes
// Round to nearest second because OS X doesn't support ms resolution
var now = Math.round(new Date().getTime() / 1000) * 1000;
json1.mtime = now - 10000;
json3.mtime = now - 20000;
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json1, json3]);
item1.attachmentSyncState = "in_conflict";
await item1.saveTx({ skipAll: true });
item3.attachmentSyncState = "in_conflict";
await item3.saveTx({ skipAll: true });
var conflicts = await Zotero.Sync.Storage.Local.getConflicts(libraryID);
assert.lengthOf(conflicts, 2);
var item1Conflict = conflicts.find(x => x.left.key == item1.key);
assert.equal(
item1Conflict.left.dateModified,
Zotero.Date.dateToISO(new Date(await item1.attachmentModificationTime))
);
assert.equal(
item1Conflict.right.dateModified,
Zotero.Date.dateToISO(new Date(json1.mtime))
);
var item3Conflict = conflicts.find(x => x.left.key == item3.key);
assert.equal(
item3Conflict.left.dateModified,
Zotero.Date.dateToISO(new Date(await item3.attachmentModificationTime))
);
assert.equal(
item3Conflict.right.dateModified,
Zotero.Date.dateToISO(new Date(json3.mtime))
);
})
})
describe("#resolveConflicts()", function () {
var win;
before(async function () {
await resetDB({
thisArg: this,
skipBundledFiles: true
});
win = await loadZoteroWindow();
});
after(function () {
if (win) {
win.close();
}
});
it("should show the conflict resolution window on attachment conflicts", async function () {
var libraryID = Zotero.Libraries.userLibraryID;
var item1 = await importFileAttachment('test.png');
item1.version = 10;
await item1.saveTx();
var item2 = await importTextAttachment();
var item3 = await importHTMLAttachment();
item3.version = 11;
await item3.saveTx();
var json1 = item1.toJSON();
var json3 = item3.toJSON();
// Change remote mtimes and hashes
json1.mtime = new Date().getTime() + 10000;
json1.md5 = 'f4ce1167f3a854896c257a0cc1ac387f';
json3.mtime = new Date().getTime() - 10000;
json3.md5 = 'fcd080b1c2cad562237823ec27671bbd';
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json1, json3]);
item1.attachmentSyncState = "in_conflict";
await item1.saveTx({ skipAll: true });
item3.attachmentSyncState = "in_conflict";
await item3.saveTx({ skipAll: true });
var promise = waitForWindow('chrome://zotero/content/merge.xhtml', async function (dialog) {
var doc = dialog.document;
var wizard = doc.querySelector('wizard');
var mergeGroup = wizard.getElementsByTagName('merge-group')[0];
// 1 (remote)
// Later remote version should be selected
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
// Check checkbox text
assert.equal(
doc.getElementById('resolve-all').label,
Zotero.getString('sync.conflict.resolveAllRemote')
);
// Select local object
mergeGroup.leftPane.click();
assert.equal(mergeGroup.leftPane.getAttribute('selected'), 'true');
wizard.getButton('next').click();
// 2 (local)
// Later local version should be selected
assert.equal(mergeGroup.leftPane.getAttribute('selected'), 'true');
// Select remote object
mergeGroup.rightPane.click();
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
if (Zotero.isMac) {
assert.isTrue(wizard.getButton('next').hidden);
assert.isFalse(wizard.getButton('finish').hidden);
}
else {
// TODO
}
wizard.getButton('finish').click();
})
await Zotero.Sync.Storage.Local.resolveConflicts(libraryID);
await promise;
assert.equal(item1.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_UPLOAD);
assert.equal(item1.attachmentSyncedModificationTime, json1.mtime);
assert.equal(item1.attachmentSyncedHash, json1.md5);
assert.equal(item3.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_DOWNLOAD);
assert.isNull(item3.attachmentSyncedModificationTime);
assert.isNull(item3.attachmentSyncedHash);
});
it("should handle attachment conflicts with no remote mtime/md5", async function () {
var libraryID = Zotero.Libraries.userLibraryID;
var item1 = await importFileAttachment('test.png');
item1.version = 10;
await item1.saveTx();
var json1 = item1.toJSON();
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json1]);
item1.attachmentSyncState = "in_conflict";
await item1.saveTx({ skipAll: true });
var promise = waitForWindow('chrome://zotero/content/merge.xhtml', async function (dialog) {
var doc = dialog.document;
var wizard = doc.querySelector('wizard');
var mergeGroup = wizard.getElementsByTagName('merge-group')[0];
// 1 (remote)
// Identical, so remote version should be selected
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
// Select local object
mergeGroup.leftPane.click();
assert.equal(mergeGroup.leftPane.getAttribute('selected'), 'true');
if (Zotero.isMac) {
assert.isTrue(wizard.getButton('next').hidden);
assert.isFalse(wizard.getButton('finish').hidden);
}
else {
// TODO
}
wizard.getButton('finish').click();
});
await Zotero.Sync.Storage.Local.resolveConflicts(libraryID);
await promise;
assert.equal(item1.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_UPLOAD);
assert.isNull(item1.attachmentSyncedModificationTime);
assert.isNull(item1.attachmentSyncedHash);
});
})
})