webdavTest.js (28748B)
1 "use strict"; 2 3 describe("Zotero.Sync.Storage.Mode.WebDAV", function () { 4 // 5 // Setup 6 // 7 Components.utils.import("resource://zotero-unit/httpd.js"); 8 9 var davScheme = "http"; 10 var davPort = 16214; 11 var davBasePath = "/webdav/"; 12 var davHostPath = `localhost:${davPort}${davBasePath}`; 13 var davUsername = "user"; 14 var davPassword = "password"; 15 var davURL = `${davScheme}://${davUsername}:${davPassword}@${davHostPath}`; 16 17 var win, controller, server, requestCount; 18 var responses = {}; 19 20 function setResponse(response) { 21 setHTTPResponse(server, davURL, response, responses); 22 } 23 24 function resetRequestCount() { 25 requestCount = server.requests.filter(r => r.responseHeaders["Fake-Server-Match"]).length; 26 } 27 28 function assertRequestCount(count) { 29 assert.equal( 30 server.requests.filter(r => r.responseHeaders["Fake-Server-Match"]).length - requestCount, 31 count 32 ); 33 } 34 35 function generateLastSyncID() { 36 return "" + Zotero.Utilities.randomString(controller._lastSyncIDLength); 37 } 38 39 function parseQueryString(str) { 40 var queryStringParams = str.split('&'); 41 var params = {}; 42 for (let param of queryStringParams) { 43 let [ key, val ] = param.split('='); 44 params[key] = decodeURIComponent(val); 45 } 46 return params; 47 } 48 49 beforeEach(function* () { 50 yield resetDB({ 51 thisArg: this, 52 skipBundledFiles: true 53 }); 54 55 Zotero.HTTP.mock = sinon.FakeXMLHttpRequest; 56 server = sinon.fakeServer.create(); 57 server.autoRespond = true; 58 59 this.httpd = new HttpServer(); 60 this.httpd.start(davPort); 61 62 yield Zotero.Users.setCurrentUserID(1); 63 yield Zotero.Users.setCurrentUsername("testuser"); 64 65 Zotero.Sync.Storage.Local.setModeForLibrary(Zotero.Libraries.userLibraryID, 'webdav'); 66 controller = new Zotero.Sync.Storage.Mode.WebDAV; 67 Zotero.Prefs.set("sync.storage.scheme", davScheme); 68 Zotero.Prefs.set("sync.storage.url", davHostPath); 69 Zotero.Prefs.set("sync.storage.username", davUsername); 70 controller.password = davPassword; 71 72 // Set download-on-sync by default 73 Zotero.Sync.Storage.Local.downloadOnSync( 74 Zotero.Libraries.userLibraryID, true 75 ); 76 }) 77 78 var setup = Zotero.Promise.coroutine(function* (options = {}) { 79 var engine = new Zotero.Sync.Storage.Engine({ 80 libraryID: options.libraryID || Zotero.Libraries.userLibraryID, 81 controller, 82 stopOnError: true 83 }); 84 85 if (!controller.verified) { 86 setResponse({ 87 method: "OPTIONS", 88 url: "zotero/", 89 headers: { 90 DAV: 1 91 }, 92 status: 200 93 }) 94 setResponse({ 95 method: "PROPFIND", 96 url: "zotero/", 97 status: 207 98 }) 99 setResponse({ 100 method: "PUT", 101 url: "zotero/zotero-test-file.prop", 102 status: 201 103 }) 104 setResponse({ 105 method: "GET", 106 url: "zotero/zotero-test-file.prop", 107 status: 200 108 }) 109 setResponse({ 110 method: "DELETE", 111 url: "zotero/zotero-test-file.prop", 112 status: 200 113 }) 114 yield controller.checkServer(); 115 116 yield controller.cacheCredentials(); 117 } 118 119 resetRequestCount(); 120 121 return engine; 122 }) 123 124 afterEach(function* () { 125 var defer = new Zotero.Promise.defer(); 126 this.httpd.stop(() => defer.resolve()); 127 yield defer.promise; 128 }) 129 130 after(function* () { 131 Zotero.HTTP.mock = null; 132 if (win) { 133 win.close(); 134 } 135 }) 136 137 138 // 139 // Tests 140 // 141 describe("Syncing", function () { 142 beforeEach(function* () { 143 win = yield loadZoteroPane(); 144 }) 145 146 afterEach(function () { 147 win.close(); 148 }) 149 150 it("should skip downloads if not marked as needed", function* () { 151 var engine = yield setup(); 152 153 var library = Zotero.Libraries.userLibrary; 154 library.libraryVersion = 5; 155 yield library.saveTx(); 156 157 var result = yield engine.start(); 158 159 assertRequestCount(0); 160 161 assert.isFalse(result.localChanges); 162 assert.isFalse(result.remoteChanges); 163 assert.isFalse(result.syncRequired); 164 165 assert.equal(library.storageVersion, library.libraryVersion); 166 }) 167 168 it("should ignore a remotely missing file", function* () { 169 var engine = yield setup(); 170 171 var library = Zotero.Libraries.userLibrary; 172 library.libraryVersion = 5; 173 yield library.saveTx(); 174 library.storageDownloadNeeded = true; 175 176 var item = new Zotero.Item("attachment"); 177 item.attachmentLinkMode = 'imported_file'; 178 item.attachmentPath = 'storage:test.txt'; 179 item.attachmentSyncState = "to_download"; 180 yield item.saveTx(); 181 182 setResponse({ 183 method: "GET", 184 url: `zotero/${item.key}.prop`, 185 status: 404 186 }); 187 var result = yield engine.start(); 188 189 assertRequestCount(1); 190 191 assert.isFalse(result.localChanges); 192 assert.isFalse(result.remoteChanges); 193 assert.isFalse(result.syncRequired); 194 195 assert.isFalse(library.storageDownloadNeeded); 196 assert.equal(library.storageVersion, library.libraryVersion); 197 }) 198 199 it("should handle a remotely failing .prop file", function* () { 200 var engine = yield setup(); 201 202 var library = Zotero.Libraries.userLibrary; 203 library.libraryVersion = 5; 204 yield library.saveTx(); 205 library.storageDownloadNeeded = true; 206 207 var item = new Zotero.Item("attachment"); 208 item.attachmentLinkMode = 'imported_file'; 209 item.attachmentPath = 'storage:test.txt'; 210 item.attachmentSyncState = "to_download"; 211 yield item.saveTx(); 212 213 setResponse({ 214 method: "GET", 215 url: `zotero/${item.key}.prop`, 216 status: 500 217 }); 218 219 // TODO: In stopOnError mode, the promise is rejected. 220 // This should probably test with stopOnError mode turned off instead. 221 var e = yield getPromiseError(engine.start()); 222 assert.include( 223 e.message, 224 Zotero.getString('sync.storage.error.webdav.requestError', [500, "GET"]) 225 ); 226 227 assertRequestCount(1); 228 229 assert.isTrue(library.storageDownloadNeeded); 230 assert.equal(library.storageVersion, 0); 231 }) 232 233 it("should handle a remotely failing .zip file", function* () { 234 var engine = yield setup(); 235 236 var library = Zotero.Libraries.userLibrary; 237 library.libraryVersion = 5; 238 yield library.saveTx(); 239 library.storageDownloadNeeded = true; 240 241 var item = new Zotero.Item("attachment"); 242 item.attachmentLinkMode = 'imported_file'; 243 item.attachmentPath = 'storage:test.txt'; 244 item.attachmentSyncState = "to_download"; 245 yield item.saveTx(); 246 247 setResponse({ 248 method: "GET", 249 url: `zotero/${item.key}.prop`, 250 status: 200, 251 text: '<properties version="1">' 252 + '<mtime>1234567890</mtime>' 253 + '<hash>8286300a280f64a4b5cfaac547c21d32</hash>' 254 + '</properties>' 255 }); 256 this.httpd.registerPathHandler( 257 `${davBasePath}zotero/${item.key}.zip`, 258 { 259 handle: function (request, response) { 260 response.setStatusLine(null, 500, null); 261 } 262 } 263 ); 264 // TODO: In stopOnError mode, the promise is rejected. 265 // This should probably test with stopOnError mode turned off instead. 266 var e = yield getPromiseError(engine.start()); 267 assert.include( 268 e.message, 269 Zotero.getString('sync.storage.error.webdav.requestError', [500, "GET"]) 270 ); 271 272 assert.isTrue(library.storageDownloadNeeded); 273 assert.equal(library.storageVersion, 0); 274 }) 275 276 277 it("should download a missing file", function* () { 278 var engine = yield setup(); 279 280 var library = Zotero.Libraries.userLibrary; 281 library.libraryVersion = 5; 282 yield library.saveTx(); 283 library.storageDownloadNeeded = true; 284 285 var fileName = "test.txt"; 286 var item = new Zotero.Item("attachment"); 287 item.attachmentLinkMode = 'imported_file'; 288 item.attachmentPath = 'storage:' + fileName; 289 // TODO: Test binary data 290 var text = Zotero.Utilities.randomString(); 291 item.attachmentSyncState = "to_download"; 292 yield item.saveTx(); 293 294 // Create ZIP file containing above text file 295 var tmpPath = Zotero.getTempDirectory().path; 296 var tmpID = "webdav_download_" + Zotero.Utilities.randomString(); 297 var zipDirPath = OS.Path.join(tmpPath, tmpID); 298 var zipPath = OS.Path.join(tmpPath, tmpID + ".zip"); 299 yield OS.File.makeDir(zipDirPath); 300 yield Zotero.File.putContentsAsync(OS.Path.join(zipDirPath, fileName), text); 301 yield Zotero.File.zipDirectory(zipDirPath, zipPath); 302 yield OS.File.removeDir(zipDirPath); 303 yield Zotero.Promise.delay(1000); 304 var zipContents = yield Zotero.File.getBinaryContentsAsync(zipPath); 305 306 var mtime = "1441252524905"; 307 var md5 = yield Zotero.Utilities.Internal.md5Async(zipPath); 308 309 yield OS.File.remove(zipPath); 310 311 setResponse({ 312 method: "GET", 313 url: `zotero/${item.key}.prop`, 314 status: 200, 315 text: '<properties version="1">' 316 + `<mtime>${mtime}</mtime>` 317 + `<hash>${md5}</hash>` 318 + '</properties>' 319 }); 320 this.httpd.registerPathHandler( 321 `${davBasePath}zotero/${item.key}.zip`, 322 { 323 handle: function (request, response) { 324 response.setStatusLine(null, 200, "OK"); 325 response.write(zipContents); 326 } 327 } 328 ); 329 330 var result = yield engine.start(); 331 332 assert.isTrue(result.localChanges); 333 assert.isFalse(result.remoteChanges); 334 assert.isFalse(result.syncRequired); 335 336 var contents = yield Zotero.File.getContentsAsync(yield item.getFilePathAsync()); 337 assert.equal(contents, text); 338 339 assert.isFalse(library.storageDownloadNeeded); 340 assert.equal(library.storageVersion, library.libraryVersion); 341 }) 342 343 it("should upload new files", function* () { 344 var engine = yield setup(); 345 346 var file = getTestDataDirectory(); 347 file.append('test.png'); 348 var item = yield Zotero.Attachments.importFromFile({ file }); 349 item.synced = true; 350 yield item.saveTx(); 351 var mtime = yield item.attachmentModificationTime; 352 var hash = yield item.attachmentHash; 353 var path = item.getFilePath(); 354 var filename = 'test.png'; 355 var size = (yield OS.File.stat(path)).size; 356 var contentType = 'image/png'; 357 var fileContents = yield Zotero.File.getContentsAsync(path); 358 359 var deferreds = []; 360 361 setResponse({ 362 method: "GET", 363 url: `zotero/${item.key}.prop`, 364 status: 404 365 }); 366 // https://github.com/cjohansen/Sinon.JS/issues/607 367 let fixSinonBug = ";charset=utf-8"; 368 server.respond(function (req) { 369 if (req.method == "PUT" && req.url == `${davURL}zotero/${item.key}.zip`) { 370 assert.equal(req.requestHeaders["Content-Type"], "application/zip" + fixSinonBug); 371 372 let deferred = Zotero.Promise.defer(); 373 deferreds.push(deferred); 374 var reader = new FileReader(); 375 reader.addEventListener("loadend", Zotero.Promise.coroutine(function* () { 376 try { 377 let tmpZipPath = OS.Path.join( 378 Zotero.getTempDirectory().path, 379 Zotero.Utilities.randomString() + '.zip' 380 ); 381 let file = yield OS.File.open(tmpZipPath, { 382 create: true 383 }); 384 var contents = new Uint8Array(reader.result); 385 yield file.write(contents); 386 yield file.close(); 387 388 // Make sure ZIP file contains the necessary entries 389 var zr = Components.classes["@mozilla.org/libjar/zip-reader;1"] 390 .createInstance(Components.interfaces.nsIZipReader); 391 zr.open(Zotero.File.pathToFile(tmpZipPath)); 392 zr.test(null); 393 var entries = zr.findEntries('*'); 394 var entryNames = []; 395 while (entries.hasMore()) { 396 entryNames.push(entries.getNext()); 397 } 398 assert.equal(entryNames.length, 1); 399 assert.sameMembers(entryNames, [filename]); 400 assert.equal(zr.getEntry(filename).realSize, size); 401 402 yield OS.File.remove(tmpZipPath); 403 404 deferred.resolve(); 405 } 406 catch (e) { 407 deferred.reject(e); 408 } 409 })); 410 reader.readAsArrayBuffer(req.requestBody); 411 412 req.respond(201, { "Fake-Server-Match": 1 }, ""); 413 } 414 else if (req.method == "PUT" && req.url == `${davURL}zotero/${item.key}.prop`) { 415 var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] 416 .createInstance(Components.interfaces.nsIDOMParser); 417 var doc = parser.parseFromString(req.requestBody, "text/xml"); 418 assert.equal( 419 doc.documentElement.getElementsByTagName('mtime')[0].textContent, mtime 420 ); 421 assert.equal( 422 doc.documentElement.getElementsByTagName('hash')[0].textContent, hash 423 ); 424 425 req.respond(204, { "Fake-Server-Match": 1 }, ""); 426 } 427 }); 428 429 var result = yield engine.start(); 430 431 yield Zotero.Promise.all(deferreds.map(d => d.promise)); 432 433 assertRequestCount(3); 434 435 assert.isTrue(result.localChanges); 436 assert.isTrue(result.remoteChanges); 437 assert.isTrue(result.syncRequired); 438 439 // Check local objects 440 assert.equal(item.attachmentSyncedModificationTime, mtime); 441 assert.equal(item.attachmentSyncedHash, hash); 442 assert.isFalse(item.synced); 443 }) 444 445 it("should upload an updated file", function* () { 446 var engine = yield setup(); 447 448 var file = getTestDataDirectory(); 449 file.append('test.txt'); 450 var item = yield Zotero.Attachments.importFromFile({ file }); 451 item.synced = true; 452 yield item.saveTx(); 453 454 var syncedModTime = Date.now() - 10000; 455 var syncedHash = "3a2f092dd62178eb8bbfda42e07e64da"; 456 457 item.attachmentSyncedModificationTime = syncedModTime; 458 item.attachmentSyncedHash = syncedHash; 459 yield item.saveTx({ skipAll: true }); 460 461 var mtime = yield item.attachmentModificationTime; 462 var hash = yield item.attachmentHash; 463 464 setResponse({ 465 method: "GET", 466 url: `zotero/${item.key}.prop`, 467 text: '<properties version="1">' 468 + `<mtime>${syncedModTime}</mtime>` 469 + `<hash>${syncedHash}</hash>` 470 + '</properties>' 471 }); 472 setResponse({ 473 method: "DELETE", 474 url: `zotero/${item.key}.prop`, 475 status: 204 476 }); 477 setResponse({ 478 method: "PUT", 479 url: `zotero/${item.key}.zip`, 480 status: 204 481 }); 482 setResponse({ 483 method: "PUT", 484 url: `zotero/${item.key}.prop`, 485 status: 204 486 }); 487 488 var result = yield engine.start(); 489 assertRequestCount(4); 490 491 assert.isTrue(result.localChanges); 492 assert.isTrue(result.remoteChanges); 493 assert.isTrue(result.syncRequired); 494 assert.isFalse(result.fileSyncRequired); 495 496 // Check local objects 497 assert.equal(item.attachmentSyncedModificationTime, mtime); 498 assert.equal(item.attachmentSyncedHash, hash); 499 assert.isFalse(item.synced); 500 }) 501 502 it("should skip upload that already exists on the server", function* () { 503 var engine = yield setup(); 504 505 var file = getTestDataDirectory(); 506 file.append('test.png'); 507 var item = yield Zotero.Attachments.importFromFile({ file }); 508 item.synced = true; 509 yield item.saveTx(); 510 var mtime = yield item.attachmentModificationTime; 511 var hash = yield item.attachmentHash; 512 var path = item.getFilePath(); 513 var filename = 'test.png'; 514 var size = (yield OS.File.stat(path)).size; 515 var contentType = 'image/png'; 516 var fileContents = yield Zotero.File.getContentsAsync(path); 517 518 setResponse({ 519 method: "GET", 520 url: `zotero/${item.key}.prop`, 521 status: 200, 522 text: '<properties version="1">' 523 + `<mtime>${mtime}</mtime>` 524 + `<hash>${hash}</hash>` 525 + '</properties>' 526 }); 527 528 var result = yield engine.start(); 529 530 assertRequestCount(1); 531 532 assert.isFalse(result.localChanges); 533 assert.isFalse(result.remoteChanges); 534 assert.isFalse(result.syncRequired); 535 536 // Check local object 537 assert.equal(item.attachmentSyncedModificationTime, mtime); 538 assert.equal(item.attachmentSyncedHash, hash); 539 assert.isFalse(item.synced); 540 }) 541 542 it("should mark item as in conflict if mod time and hash on storage server don't match synced values", function* () { 543 var engine = yield setup(); 544 545 var file = getTestDataDirectory(); 546 file.append('test.png'); 547 var item = yield Zotero.Attachments.importFromFile({ file }); 548 item.synced = true; 549 yield item.saveTx(); 550 var mtime = yield item.attachmentModificationTime; 551 var hash = yield item.attachmentHash; 552 var path = item.getFilePath(); 553 var filename = 'test.png'; 554 var size = (yield OS.File.stat(path)).size; 555 var contentType = 'image/png'; 556 var fileContents = yield Zotero.File.getContentsAsync(path); 557 558 var newModTime = mtime + 5000; 559 var newHash = "4f69f43d8ac8788190b13ff7f4a0a915"; 560 561 setResponse({ 562 method: "GET", 563 url: `zotero/${item.key}.prop`, 564 status: 200, 565 text: '<properties version="1">' 566 + `<mtime>${newModTime}</mtime>` 567 + `<hash>${newHash}</hash>` 568 + '</properties>' 569 }); 570 571 var result = yield engine.start(); 572 573 assertRequestCount(1); 574 575 assert.isFalse(result.localChanges); 576 assert.isFalse(result.remoteChanges); 577 assert.isFalse(result.syncRequired); 578 assert.isTrue(result.fileSyncRequired); 579 580 // Check local object 581 // 582 // Item should be marked as in conflict 583 assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_IN_CONFLICT); 584 // Synced mod time should have been changed, because that's what's shown in the 585 // conflict dialog 586 assert.equal(item.attachmentSyncedModificationTime, newModTime); 587 assert.isTrue(item.synced); 588 }) 589 }); 590 591 describe("Verify Server", function () { 592 it("should show an error for a connection error", function* () { 593 Zotero.HTTP.mock = null; 594 Zotero.Prefs.set("sync.storage.url", "127.0.0.1:9999"); 595 596 // Begin install procedure 597 var win = yield loadPrefPane('sync'); 598 var button = win.document.getElementById('storage-verify'); 599 600 var spy = sinon.spy(win.Zotero_Preferences.Sync, "verifyStorageServer"); 601 var promise1 = waitForDialog(function (dialog) { 602 assert.include( 603 dialog.document.documentElement.textContent, 604 Zotero.getString('sync.storage.error.serverCouldNotBeReached', '127.0.0.1') 605 ); 606 }); 607 button.click(); 608 yield promise1; 609 610 var promise2 = spy.returnValues[0]; 611 spy.restore(); 612 yield promise2; 613 614 win.close(); 615 }); 616 617 it("should show an error for a 403", function* () { 618 Zotero.HTTP.mock = null; 619 this.httpd.registerPathHandler( 620 `${davBasePath}zotero/`, 621 { 622 handle: function (request, response) { 623 response.setStatusLine(null, 403, null); 624 } 625 } 626 ); 627 628 // Use httpd.js instead of sinon so we get a real nsIURL with a channel 629 Zotero.Prefs.set("sync.storage.url", davHostPath); 630 631 // Begin install procedure 632 var win = yield loadPrefPane('sync'); 633 var button = win.document.getElementById('storage-verify'); 634 635 var spy = sinon.spy(win.Zotero_Preferences.Sync, "verifyStorageServer"); 636 var promise1 = waitForDialog(function (dialog) { 637 assert.include( 638 dialog.document.documentElement.textContent, 639 Zotero.getString('sync.storage.error.webdav.permissionDenied', davBasePath + 'zotero/') 640 ); 641 }); 642 button.click(); 643 yield promise1; 644 645 var promise2 = spy.returnValues[0]; 646 spy.restore(); 647 yield promise2; 648 649 win.close(); 650 }); 651 }); 652 653 describe("#purgeDeletedStorageFiles()", function () { 654 beforeEach(function () { 655 resetRequestCount(); 656 }) 657 658 it("should delete files on storage server that were deleted locally", function* () { 659 var libraryID = Zotero.Libraries.userLibraryID; 660 661 var file = getTestDataDirectory(); 662 file.append('test.png'); 663 var item = yield Zotero.Attachments.importFromFile({ file }); 664 item.synced = true; 665 yield item.saveTx(); 666 yield item.eraseTx(); 667 668 assert.lengthOf((yield Zotero.Sync.Storage.Local.getDeletedFiles(libraryID)), 1); 669 670 setResponse({ 671 method: "DELETE", 672 url: `zotero/${item.key}.prop`, 673 status: 204 674 }); 675 setResponse({ 676 method: "DELETE", 677 url: `zotero/${item.key}.zip`, 678 status: 204 679 }); 680 var results = yield controller.purgeDeletedStorageFiles(libraryID); 681 assertRequestCount(2); 682 683 assert.lengthOf(results.deleted, 2); 684 assert.sameMembers(results.deleted, [`${item.key}.prop`, `${item.key}.zip`]); 685 assert.lengthOf(results.missing, 0); 686 assert.lengthOf(results.error, 0); 687 688 // Storage delete log should be empty 689 assert.lengthOf((yield Zotero.Sync.Storage.Local.getDeletedFiles(libraryID)), 0); 690 }) 691 }) 692 693 describe("#purgeOrphanedStorageFiles()", function () { 694 beforeEach(function () { 695 resetRequestCount(); 696 Zotero.Prefs.clear('lastWebDAVOrphanPurge'); 697 }) 698 699 it("should delete orphaned files more than a week older than the last sync time", function* () { 700 var library = Zotero.Libraries.userLibrary; 701 library.updateLastSyncTime(); 702 yield library.saveTx(); 703 704 // Create one item 705 var item1 = yield createDataObject('item'); 706 var item1Key = item1.key; 707 // Add another item to sync queue 708 var item2Key = Zotero.DataObjectUtilities.generateKey(); 709 yield Zotero.Sync.Data.Local.addObjectsToSyncQueue('item', library.id, [item2Key]); 710 711 const daysBeforeSyncTime = 7; 712 713 var beforeTime = new Date(Date.now() - (daysBeforeSyncTime * 86400 * 1000 + 1)).toUTCString(); 714 var currentTime = new Date(Date.now() - 3600000).toUTCString(); 715 716 setResponse({ 717 method: "PROPFIND", 718 url: `zotero/`, 719 status: 207, 720 headers: { 721 "Content-Type": 'text/xml; charset="utf-8"' 722 }, 723 text: '<?xml version="1.0" encoding="utf-8"?>' 724 + '<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">' 725 + '<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">' 726 + `<D:href>${davBasePath}zotero/</D:href>` 727 + '<D:propstat>' 728 + '<D:prop>' 729 + `<lp1:getlastmodified>${beforeTime}</lp1:getlastmodified>` 730 + '</D:prop>' 731 + '<D:status>HTTP/1.1 200 OK</D:status>' 732 + '</D:propstat>' 733 + '</D:response>' 734 + '<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">' 735 + `<D:href>${davBasePath}zotero/lastsync.txt</D:href>` 736 + '<D:propstat>' 737 + '<D:prop>' 738 + `<lp1:getlastmodified>${beforeTime}</lp1:getlastmodified>` 739 + '</D:prop>' 740 + '<D:status>HTTP/1.1 200 OK</D:status>' 741 + '</D:propstat>' 742 + '</D:response>' 743 + '<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">' 744 + `<D:href>${davBasePath}zotero/lastsync</D:href>` 745 + '<D:propstat>' 746 + '<D:prop>' 747 + `<lp1:getlastmodified>${beforeTime}</lp1:getlastmodified>` 748 + '</D:prop>' 749 + '<D:status>HTTP/1.1 200 OK</D:status>' 750 + '</D:propstat>' 751 + '</D:response>' 752 753 + '<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">' 754 + `<D:href>${davBasePath}zotero/AAAAAAAA.zip</D:href>` 755 + '<D:propstat>' 756 + '<D:prop>' 757 + `<lp1:getlastmodified>${beforeTime}</lp1:getlastmodified>` 758 + '</D:prop>' 759 + '<D:status>HTTP/1.1 200 OK</D:status>' 760 + '</D:propstat>' 761 + '</D:response>' 762 + '<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">' 763 + `<D:href>${davBasePath}zotero/AAAAAAAA.prop</D:href>` 764 + '<D:propstat>' 765 + '<D:prop>' 766 + `<lp1:getlastmodified>${beforeTime}</lp1:getlastmodified>` 767 + '</D:prop>' 768 + '<D:status>HTTP/1.1 200 OK</D:status>' 769 + '</D:propstat>' 770 + '</D:response>' 771 772 + '<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">' 773 + `<D:href>${davBasePath}zotero/BBBBBBBB.zip</D:href>` 774 + '<D:propstat>' 775 + '<D:prop>' 776 + `<lp1:getlastmodified>${currentTime}</lp1:getlastmodified>` 777 + '</D:prop>' 778 + '<D:status>HTTP/1.1 200 OK</D:status>' 779 + '</D:propstat>' 780 + '</D:response>' 781 + '<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">' 782 + `<D:href>${davBasePath}zotero/BBBBBBBB.prop</D:href>` 783 + '<D:propstat>' 784 + '<D:prop>' 785 + `<lp1:getlastmodified>${currentTime}</lp1:getlastmodified>` 786 + '</D:prop>' 787 + '<D:status>HTTP/1.1 200 OK</D:status>' 788 + '</D:propstat>' 789 + '</D:response>' 790 791 // Item that exists 792 + '<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">' 793 + `<D:href>${davBasePath}zotero/${item1Key}.zip</D:href>` 794 + '<D:propstat>' 795 + '<D:prop>' 796 + `<lp1:getlastmodified>${beforeTime}</lp1:getlastmodified>` 797 + '</D:prop>' 798 + '<D:status>HTTP/1.1 200 OK</D:status>' 799 + '</D:propstat>' 800 + '</D:response>' 801 + '<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">' 802 + `<D:href>${davBasePath}zotero/${item1Key}.prop</D:href>` 803 + '<D:propstat>' 804 + '<D:prop>' 805 + `<lp1:getlastmodified>${beforeTime}</lp1:getlastmodified>` 806 + '</D:prop>' 807 + '<D:status>HTTP/1.1 200 OK</D:status>' 808 + '</D:propstat>' 809 + '</D:response>' 810 811 // Item in sync queue 812 + '<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">' 813 + `<D:href>${davBasePath}zotero/${item2Key}.zip</D:href>` 814 + '<D:propstat>' 815 + '<D:prop>' 816 + `<lp1:getlastmodified>${beforeTime}</lp1:getlastmodified>` 817 + '</D:prop>' 818 + '<D:status>HTTP/1.1 200 OK</D:status>' 819 + '</D:propstat>' 820 + '</D:response>' 821 + '<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">' 822 + `<D:href>${davBasePath}zotero/${item2Key}.prop</D:href>` 823 + '<D:propstat>' 824 + '<D:prop>' 825 + `<lp1:getlastmodified>${beforeTime}</lp1:getlastmodified>` 826 + '</D:prop>' 827 + '<D:status>HTTP/1.1 200 OK</D:status>' 828 + '</D:propstat>' 829 + '</D:response>' 830 + '</D:multistatus>' 831 }); 832 setResponse({ 833 method: "DELETE", 834 url: 'zotero/AAAAAAAA.prop', 835 status: 204 836 }); 837 setResponse({ 838 method: "DELETE", 839 url: 'zotero/AAAAAAAA.zip', 840 status: 204 841 }); 842 setResponse({ 843 method: "DELETE", 844 url: 'zotero/lastsync.txt', 845 status: 204 846 }); 847 setResponse({ 848 method: "DELETE", 849 url: 'zotero/lastsync', 850 status: 204 851 }); 852 853 var results = yield controller.purgeOrphanedStorageFiles(); 854 assertRequestCount(5); 855 856 assert.sameMembers(results.deleted, ['lastsync.txt', 'lastsync', 'AAAAAAAA.prop', 'AAAAAAAA.zip']); 857 assert.lengthOf(results.missing, 0); 858 assert.lengthOf(results.error, 0); 859 }) 860 861 it("shouldn't purge if purged recently", function* () { 862 Zotero.Prefs.set("lastWebDAVOrphanPurge", Math.round(new Date().getTime() / 1000) - 3600); 863 yield assert.eventually.equal(controller.purgeOrphanedStorageFiles(), false); 864 assertRequestCount(0); 865 }); 866 867 868 it("should handle unnormalized Unicode characters", function* () { 869 var library = Zotero.Libraries.userLibrary; 870 library.updateLastSyncTime(); 871 yield library.saveTx(); 872 873 const daysBeforeSyncTime = 7; 874 875 var beforeTime = new Date(Date.now() - (daysBeforeSyncTime * 86400 * 1000 + 1)).toUTCString(); 876 var currentTime = new Date(Date.now() - 3600000).toUTCString(); 877 878 var strC = '\u1E9B\u0323'; 879 var encodedStrC = encodeURIComponent(strC); 880 var strD = '\u1E9B\u0323'.normalize('NFD'); 881 var encodedStrD = encodeURIComponent(strD); 882 883 setResponse({ 884 method: "PROPFIND", 885 url: `${encodedStrC}/zotero/`, 886 status: 207, 887 headers: { 888 "Content-Type": 'text/xml; charset="utf-8"' 889 }, 890 text: '<?xml version="1.0" encoding="utf-8"?>' 891 + '<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">' 892 + '<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">' 893 + `<D:href>${davBasePath}${encodedStrD}/zotero/</D:href>` 894 + '<D:propstat>' 895 + '<D:prop>' 896 + `<lp1:getlastmodified>${beforeTime}</lp1:getlastmodified>` 897 + '</D:prop>' 898 + '<D:status>HTTP/1.1 200 OK</D:status>' 899 + '</D:propstat>' 900 + '</D:response>' 901 + '<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">' 902 + `<D:href>${davBasePath}${encodedStrD}/zotero/lastsync</D:href>` 903 + '<D:propstat>' 904 + '<D:prop>' 905 + `<lp1:getlastmodified>${beforeTime}</lp1:getlastmodified>` 906 + '</D:prop>' 907 + '<D:status>HTTP/1.1 200 OK</D:status>' 908 + '</D:propstat>' 909 + '</D:response>' 910 911 + '<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">' 912 + `<D:href>${davBasePath}${encodedStrD}/zotero/AAAAAAAA.zip</D:href>` 913 + '<D:propstat>' 914 + '<D:prop>' 915 + `<lp1:getlastmodified>${beforeTime}</lp1:getlastmodified>` 916 + '</D:prop>' 917 + '<D:status>HTTP/1.1 200 OK</D:status>' 918 + '</D:propstat>' 919 + '</D:response>' 920 + '<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">' 921 + `<D:href>${davBasePath}${encodedStrD}/zotero/AAAAAAAA.prop</D:href>` 922 + '<D:propstat>' 923 + '<D:prop>' 924 + `<lp1:getlastmodified>${beforeTime}</lp1:getlastmodified>` 925 + '</D:prop>' 926 + '<D:status>HTTP/1.1 200 OK</D:status>' 927 + '</D:propstat>' 928 + '</D:response>' 929 + '</D:multistatus>' 930 }); 931 932 Zotero.Prefs.set("sync.storage.url", davHostPath + strC + "/"); 933 yield controller.purgeOrphanedStorageFiles(); 934 }) 935 }) 936 })