"use strict"; describe("Zotero.Sync.Storage.Local", function () { var win; before(function* () { win = yield loadBrowserWindow(); }); beforeEach(function* () { yield resetDB({ thisArg: this, skipBundledFiles: true }) }) after(function () { if (win) { win.close(); } }); describe("#checkForUpdatedFiles()", function () { it("should flag modified file for upload and return it", function* () { // Create attachment let item = yield importFileAttachment('test.txt') var hash = yield 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; yield OS.File.setDates((yield item.getFilePathAsync()), null, mtime); // Mark as synced, so it will be checked item.attachmentSyncedModificationTime = mtime; item.attachmentSyncedHash = hash; item.attachmentSyncState = "in_sync"; yield item.saveTx({ skipAll: true }); // Update mtime and contents var path = yield item.getFilePathAsync(); yield OS.File.setDates(path); yield Zotero.File.putContentsAsync(path, Zotero.Utilities.randomString()); // File should be returned var libraryID = Zotero.Libraries.userLibraryID; var changed = yield Zotero.Sync.Storage.Local.checkForUpdatedFiles(libraryID); yield 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", function* () { // Create attachment let item = yield importFileAttachment('test.txt') var hash = yield item.attachmentHash; var mtime = yield item.attachmentModificationTime; // Mark as synced, so it will be checked item.attachmentSyncedModificationTime = mtime; item.attachmentSyncedHash = hash; item.attachmentSyncState = "in_sync"; yield item.saveTx({ skipAll: true }); var libraryID = Zotero.Libraries.userLibraryID; var changed = yield Zotero.Sync.Storage.Local.checkForUpdatedFiles(libraryID); var syncState = item.attachmentSyncState; yield 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", function* () { // Create attachment let item = yield importFileAttachment('test.txt') var hash = yield 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; yield OS.File.setDates((yield item.getFilePathAsync()), null, mtime); // Mark as synced, so it will be checked item.attachmentSyncedModificationTime = mtime; item.attachmentSyncedHash = hash; item.attachmentSyncState = "in_sync"; yield item.saveTx({ skipAll: true }); // Update mtime, but not contents var path = yield item.getFilePathAsync(); yield OS.File.setDates(path); var libraryID = Zotero.Libraries.userLibraryID; var changed = yield Zotero.Sync.Storage.Local.checkForUpdatedFiles(libraryID); var syncState = item.attachmentSyncState; var syncedModTime = item.attachmentSyncedModificationTime; var newModTime = yield item.attachmentModificationTime; yield item.eraseTx(); assert.isFalse(changed); assert.equal(syncState, Zotero.Sync.Storage.Local.SYNC_STATE_IN_SYNC); assert.equal(syncedModTime, newModTime); }) }) describe("#resetModeSyncStates()", function () { it("should reset attachment sync states to 'to_upload'", function* () { var attachment = yield importFileAttachment('test.png'); attachment.attachmentSyncState = 'in_sync'; yield attachment.saveTx(); var local = Zotero.Sync.Storage.Local; yield local.resetModeSyncStates() assert.strictEqual(attachment.attachmentSyncState, local.SYNC_STATE_TO_UPLOAD); var state = yield Zotero.DB.valueQueryAsync( "SELECT syncState FROM itemAttachments WHERE itemID=?", attachment.id ); assert.strictEqual(state, local.SYNC_STATE_TO_UPLOAD); }); }); describe("#processDownload()", function () { var file1Name = 'index.html'; var file1Contents = 'Test'; var file2Name = 'aux1.txt'; var file2Contents = 'Test 1'; var subDirName = 'sub'; var file3Name = 'aux2'; var file3Contents = 'Test 2'; var createZIP = Zotero.Promise.coroutine(function* (zipFile) { var tmpDir = Zotero.getTempDirectory().path; var zipDir = OS.Path.join(tmpDir, Zotero.Utilities.randomString()); yield OS.File.makeDir(zipDir); yield Zotero.File.putContentsAsync(OS.Path.join(zipDir, file1Name), file1Contents); yield Zotero.File.putContentsAsync(OS.Path.join(zipDir, file2Name), file2Contents); // Subdirectory var subDir = OS.Path.join(zipDir, subDirName); yield OS.File.makeDir(subDir); yield Zotero.File.putContentsAsync(OS.Path.join(subDir, file3Name), file3Contents); yield Zotero.File.zipDirectory(zipDir, zipFile); yield removeDir(zipDir); }); it("should download and extract a ZIP file into the attachment directory", function* () { var libraryID = Zotero.Libraries.userLibraryID; var parentItem = yield createDataObject('item'); var key = Zotero.DataObjectUtilities.generateKey(); var tmpDir = Zotero.getTempDirectory().path; var zipFile = OS.Path.join(tmpDir, key + '.tmp'); yield createZIP(zipFile); // Create an existing attachment directory (and subdirectory) to replace var dir = Zotero.Attachments.getStorageDirectoryByLibraryAndKey(libraryID, key).path; yield OS.File.makeDir( OS.Path.join(dir, 'subdir'), { from: Zotero.DataDirectory.dir, unixMode: 0o755 } ); yield Zotero.File.putContentsAsync(OS.Path.join(dir, 'A'), ''); yield 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 }; yield Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, [json]); var item = yield Zotero.Items.getByLibraryAndKeyAsync(libraryID, key); yield Zotero.Sync.Storage.Local.processDownload({ item, md5, mtime, compressed: true }); yield OS.File.remove(zipFile); var storageDir = Zotero.Attachments.getStorageDirectory(item).path; // Make sure previous files don't exist assert.isFalse(yield OS.File.exists(OS.Path.join(storageDir, 'A'))); assert.isFalse(yield OS.File.exists(OS.Path.join(storageDir, 'subdir'))); assert.isFalse(yield OS.File.exists(OS.Path.join(storageDir, 'subdir', 'B'))); // Make sure main file matches attachment hash and mtime yield assert.eventually.equal( item.attachmentHash, Zotero.Utilities.Internal.md5(file1Contents) ); yield assert.eventually.equal(item.attachmentModificationTime, mtime); // Check second file yield assert.eventually.equal( Zotero.File.getContentsAsync(OS.Path.join(storageDir, file2Name)), file2Contents ); // Check subdirectory and file assert.isTrue((yield OS.File.stat(OS.Path.join(storageDir, subDirName))).isDir); yield assert.eventually.equal( Zotero.File.getContentsAsync(OS.Path.join(storageDir, subDirName, file3Name)), file3Contents ); }) }) describe("#_deleteExistingAttachmentFiles()", function () { it("should delete all files", function* () { var item = yield importFileAttachment('test.html'); var path = OS.Path.dirname(item.getFilePath()); var files = ['a', 'b', 'c', 'd']; for (let file of files) { yield Zotero.File.putContentsAsync(OS.Path.join(path, file), file); } yield Zotero.Sync.Storage.Local._deleteExistingAttachmentFiles(item); for (let file of files) { assert.isFalse( (yield OS.File.exists(OS.Path.join(path, file))), `File '${file}' doesn't exist` ); } }) }) describe("#getConflicts()", function () { it("should return an array of objects for attachments in conflict", function* () { var libraryID = Zotero.Libraries.userLibraryID; var item1 = yield importFileAttachment('test.png'); item1.version = 10; yield item1.saveTx(); var item2 = yield importFileAttachment('test.txt'); var item3 = yield importFileAttachment('test.html'); item3.version = 11; yield 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; yield Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json1, json3]); item1.attachmentSyncState = "in_conflict"; yield item1.saveTx({ skipAll: true }); item3.attachmentSyncState = "in_conflict"; yield item3.saveTx({ skipAll: true }); var conflicts = yield 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(yield 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(yield item3.attachmentModificationTime)) ); assert.equal( item3Conflict.right.dateModified, Zotero.Date.dateToISO(new Date(json3.mtime)) ); }) }) describe("#resolveConflicts()", function () { it("should show the conflict resolution window on attachment conflicts", function* () { var libraryID = Zotero.Libraries.userLibraryID; var item1 = yield importFileAttachment('test.png'); item1.version = 10; yield item1.saveTx(); var item2 = yield importFileAttachment('test.txt'); var item3 = yield importFileAttachment('test.html'); item3.version = 11; yield item3.saveTx(); var json1 = item1.toJSON(); var json3 = item3.toJSON(); // Change remote mtimes json1.mtime = new Date().getTime() + 10000; json3.mtime = new Date().getTime() - 10000; yield Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json1, json3]); item1.attachmentSyncState = "in_conflict"; yield item1.saveTx({ skipAll: true }); item3.attachmentSyncState = "in_conflict"; yield item3.saveTx({ skipAll: true }); var promise = waitForWindow('chrome://zotero/content/merge.xul', function (dialog) { var doc = dialog.document; var wizard = doc.documentElement; var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[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(); }) yield Zotero.Sync.Storage.Local.resolveConflicts(libraryID); yield promise; assert.equal(item1.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_UPLOAD); assert.equal(item3.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_DOWNLOAD); }) }) })