syncLocalTest.js (59872B)
1 "use strict"; 2 3 describe("Zotero.Sync.Data.Local", function() { 4 describe("#getAPIKey()/#setAPIKey()", function () { 5 it("should get and set an API key", function* () { 6 var apiKey1 = Zotero.Utilities.randomString(24); 7 var apiKey2 = Zotero.Utilities.randomString(24); 8 Zotero.Sync.Data.Local.setAPIKey(apiKey1); 9 yield assert.eventually.equal(Zotero.Sync.Data.Local.getAPIKey(apiKey1), apiKey1); 10 Zotero.Sync.Data.Local.setAPIKey(apiKey2); 11 yield assert.eventually.equal(Zotero.Sync.Data.Local.getAPIKey(apiKey2), apiKey2); 12 }) 13 14 15 it("should clear an API key by setting an empty string", function* () { 16 var apiKey = Zotero.Utilities.randomString(24); 17 Zotero.Sync.Data.Local.setAPIKey(apiKey); 18 Zotero.Sync.Data.Local.setAPIKey(""); 19 yield assert.eventually.strictEqual(Zotero.Sync.Data.Local.getAPIKey(apiKey), ""); 20 }) 21 }) 22 23 24 describe("#checkUser()", function () { 25 var resetDataDirFile; 26 27 before(function() { 28 resetDataDirFile = OS.Path.join(Zotero.DataDirectory.dir, 'reset-data-directory'); 29 sinon.stub(Zotero.Utilities.Internal, 'quitZotero'); 30 }); 31 32 beforeEach(function* () { 33 yield OS.File.remove(resetDataDirFile, {ignoreAbsent: true}); 34 Zotero.Utilities.Internal.quitZotero.reset(); 35 }); 36 37 after(function() { 38 Zotero.Utilities.Internal.quitZotero.restore(); 39 }); 40 41 it("should prompt for data reset and create a temp 'reset-data-directory' file on accept", function* (){ 42 yield Zotero.Users.setCurrentUserID(1); 43 yield Zotero.Users.setCurrentUsername("A"); 44 45 var handled = false; 46 waitForDialog(function (dialog) { 47 var text = dialog.document.documentElement.textContent; 48 var matches = text.match(/‘[^’]*’/g); 49 assert.equal(matches.length, 3); 50 assert.equal(matches[0], "‘A’"); 51 assert.equal(matches[1], "‘B’"); 52 assert.equal(matches[2], "‘A’"); 53 54 dialog.document.getElementById('zotero-hardConfirmationDialog-checkbox').checked = true; 55 dialog.document.getElementById('zotero-hardConfirmationDialog-checkbox') 56 .dispatchEvent(new Event('command')); 57 58 handled = true; 59 }, 'accept', 'chrome://zotero/content/hardConfirmationDialog.xul'); 60 var cont = yield Zotero.Sync.Data.Local.checkUser(window, 2, "B"); 61 var resetDataDirFileExists = yield OS.File.exists(resetDataDirFile); 62 assert.isTrue(handled); 63 assert.isTrue(cont); 64 assert.isTrue(resetDataDirFileExists); 65 }); 66 67 it("should prompt for data reset and cancel", function* () { 68 yield Zotero.Users.setCurrentUserID(1); 69 yield Zotero.Users.setCurrentUsername("A"); 70 71 waitForDialog(false, 'cancel', 'chrome://zotero/content/hardConfirmationDialog.xul'); 72 var cont = yield Zotero.Sync.Data.Local.checkUser(window, 2, "B"); 73 var resetDataDirFileExists = yield OS.File.exists(resetDataDirFile); 74 assert.isFalse(cont); 75 assert.isFalse(resetDataDirFileExists); 76 77 assert.equal(Zotero.Users.getCurrentUserID(), 1); 78 assert.equal(Zotero.Users.getCurrentUsername(), "A"); 79 }); 80 81 // extra1 functionality not used at the moment 82 it.skip("should prompt for data reset and allow to choose a new data directory", function* (){ 83 sinon.stub(Zotero.DataDirectory, 'forceChange').returns(true); 84 yield Zotero.Users.setCurrentUserID(1); 85 yield Zotero.Users.setCurrentUsername("A"); 86 87 waitForDialog(null, 'extra1', 'chrome://zotero/content/hardConfirmationDialog.xul'); 88 waitForDialog(); 89 var cont = yield Zotero.Sync.Data.Local.checkUser(window, 2, "B"); 90 var resetDataDirFileExists = yield OS.File.exists(resetDataDirFile); 91 assert.isTrue(cont); 92 assert.isTrue(Zotero.DataDirectory.forceChange.called); 93 assert.isFalse(resetDataDirFileExists); 94 95 Zotero.DataDirectory.forceChange.restore(); 96 }); 97 98 it("should migrate relations using local user key", function* () { 99 yield Zotero.DB.queryAsync("DELETE FROM settings WHERE setting='account'"); 100 yield Zotero.Users.init(); 101 102 var item1 = yield createDataObject('item'); 103 var item2 = createUnsavedDataObject('item'); 104 item2.addRelatedItem(item1); 105 yield item2.save(); 106 107 var pred = Zotero.Relations.relatedItemPredicate; 108 assert.isTrue( 109 item2.toJSON().relations[pred][0].startsWith('http://zotero.org/users/local/') 110 ); 111 112 waitForDialog(false, 'accept', 'chrome://zotero/content/hardConfirmationDialog.xul'); 113 yield Zotero.Sync.Data.Local.checkUser(window, 1, "A"); 114 115 assert.isTrue( 116 item2.toJSON().relations[pred][0].startsWith('http://zotero.org/users/1/items/') 117 ); 118 }); 119 }); 120 121 122 describe("#checkLibraryForAccess()", function () { 123 // 124 // editable 125 // 126 it("should prompt if library is changing from editable to non-editable and reset library on accept", function* () { 127 var group = yield createGroup(); 128 var libraryID = group.libraryID; 129 var promise = waitForDialog(function (dialog) { 130 var text = dialog.document.documentElement.textContent; 131 assert.include(text, group.name); 132 }); 133 134 var mock = sinon.mock(Zotero.Sync.Data.Local); 135 mock.expects("_libraryHasUnsyncedData").once().returns(Zotero.Promise.resolve(true)); 136 mock.expects("resetUnsyncedLibraryData").once().returns(Zotero.Promise.resolve()); 137 mock.expects("resetUnsyncedLibraryFiles").never(); 138 139 assert.isTrue( 140 yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, false, false) 141 ); 142 yield promise; 143 144 mock.verify(); 145 }); 146 147 it("should prompt if library is changing from editable to non-editable but not reset library on cancel", function* () { 148 var group = yield createGroup(); 149 var libraryID = group.libraryID; 150 var promise = waitForDialog(function (dialog) { 151 var text = dialog.document.documentElement.textContent; 152 assert.include(text, group.name); 153 }, "cancel"); 154 155 var mock = sinon.mock(Zotero.Sync.Data.Local); 156 mock.expects("_libraryHasUnsyncedData").once().returns(Zotero.Promise.resolve(true)); 157 mock.expects("resetUnsyncedLibraryData").never(); 158 mock.expects("resetUnsyncedLibraryFiles").never(); 159 160 assert.isFalse( 161 yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, false, false) 162 ); 163 yield promise; 164 165 mock.verify(); 166 }); 167 168 it("should not prompt if library is changing from editable to non-editable", function* () { 169 var group = yield createGroup({ editable: false, filesEditable: false }); 170 var libraryID = group.libraryID; 171 yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, true, true); 172 }); 173 174 // 175 // filesEditable 176 // 177 it("should prompt if library is changing from filesEditable to non-filesEditable and reset library files on accept", function* () { 178 var group = yield createGroup(); 179 var libraryID = group.libraryID; 180 var promise = waitForDialog(function (dialog) { 181 var text = dialog.document.documentElement.textContent; 182 assert.include(text, group.name); 183 }); 184 185 var mock = sinon.mock(Zotero.Sync.Data.Local); 186 mock.expects("_libraryHasUnsyncedFiles").once().returns(Zotero.Promise.resolve(true)); 187 mock.expects("resetUnsyncedLibraryData").never(); 188 mock.expects("resetUnsyncedLibraryFiles").once().returns(Zotero.Promise.resolve()); 189 190 assert.isTrue( 191 yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, true, false) 192 ); 193 yield promise; 194 195 mock.verify(); 196 }); 197 198 it("should prompt if library is changing from filesEditable to non-filesEditable but not reset library files on cancel", function* () { 199 var group = yield createGroup(); 200 var libraryID = group.libraryID; 201 var promise = waitForDialog(function (dialog) { 202 var text = dialog.document.documentElement.textContent; 203 assert.include(text, group.name); 204 }, "cancel"); 205 206 var mock = sinon.mock(Zotero.Sync.Data.Local); 207 mock.expects("_libraryHasUnsyncedFiles").once().returns(Zotero.Promise.resolve(true)); 208 mock.expects("resetUnsyncedLibraryData").never(); 209 mock.expects("resetUnsyncedLibraryFiles").never(); 210 211 assert.isFalse( 212 yield Zotero.Sync.Data.Local.checkLibraryForAccess(null, libraryID, true, false) 213 ); 214 yield promise; 215 216 mock.verify(); 217 }); 218 }); 219 220 221 describe("#_libraryHasUnsyncedData()", function () { 222 it("should return true for unsynced setting", function* () { 223 var group = yield createGroup(); 224 var libraryID = group.libraryID; 225 yield Zotero.SyncedSettings.set(libraryID, "testSetting", { foo: "bar" }); 226 assert.isTrue(yield Zotero.Sync.Data.Local._libraryHasUnsyncedData(libraryID)); 227 }); 228 229 it("should return true for unsynced item", function* () { 230 var group = yield createGroup(); 231 var libraryID = group.libraryID; 232 yield createDataObject('item', { libraryID }); 233 assert.isTrue(yield Zotero.Sync.Data.Local._libraryHasUnsyncedData(libraryID)); 234 }); 235 236 it("should return false if no changes", function* () { 237 var group = yield createGroup(); 238 var libraryID = group.libraryID; 239 assert.isFalse(yield Zotero.Sync.Data.Local._libraryHasUnsyncedData(libraryID)); 240 }); 241 }); 242 243 244 describe("#resetUnsyncedLibraryData()", function () { 245 it("should revert group and mark for full sync", function* () { 246 var group = yield createGroup({ 247 version: 1, 248 libraryVersion: 2 249 }); 250 var libraryID = group.libraryID; 251 252 // New setting 253 yield Zotero.SyncedSettings.set(libraryID, "testSetting", { foo: "bar" }); 254 255 // Changed collection 256 var changedCollection = yield createDataObject('collection', { libraryID, version: 1 }); 257 var originalCollectionName = changedCollection.name; 258 yield Zotero.Sync.Data.Local.saveCacheObject( 259 'collection', libraryID, changedCollection.toJSON() 260 ); 261 yield modifyDataObject(changedCollection); 262 263 // Unchanged item 264 var unchangedItem = yield createDataObject('item', { libraryID, version: 1, synced: true }); 265 yield Zotero.Sync.Data.Local.saveCacheObject( 266 'item', libraryID, unchangedItem.toJSON() 267 ); 268 269 // Changed item 270 var changedItem = yield createDataObject('item', { libraryID, version: 1 }); 271 var originalChangedItemTitle = changedItem.getField('title'); 272 yield Zotero.Sync.Data.Local.saveCacheObject('item', libraryID, changedItem.toJSON()); 273 yield modifyDataObject(changedItem); 274 275 // New item 276 var newItem = yield createDataObject('item', { libraryID, version: 1 }); 277 var newItemKey = newItem.key; 278 279 // Delete item 280 var deletedItem = yield createDataObject('item', { libraryID }); 281 var deletedItemKey = deletedItem.key; 282 yield deletedItem.eraseTx(); 283 284 // Make group read-only 285 group.editable = false; 286 yield group.saveTx(); 287 288 yield Zotero.Sync.Data.Local.resetUnsyncedLibraryData(libraryID); 289 290 assert.isNull(Zotero.SyncedSettings.get(group.libraryID, "testSetting")); 291 292 assert.equal(changedCollection.name, originalCollectionName); 293 assert.isTrue(changedCollection.synced); 294 295 assert.isTrue(unchangedItem.synced); 296 297 assert.equal(changedItem.getField('title'), originalChangedItemTitle); 298 assert.isTrue(changedItem.synced); 299 300 assert.isFalse(Zotero.Items.get(newItemKey)); 301 302 assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, deletedItemKey)); 303 304 assert.equal(group.libraryVersion, -1); 305 }); 306 307 308 describe("#resetUnsyncedLibraryFiles", function () { 309 it("should delete unsynced files", function* () { 310 var group = yield createGroup({ 311 version: 1, 312 libraryVersion: 2 313 }); 314 var libraryID = group.libraryID; 315 316 // File attachment that's totally in sync -- leave alone 317 var attachment1 = yield importFileAttachment('test.png', { libraryID }); 318 attachment1.attachmentSyncState = "in_sync"; 319 attachment1.attachmentSyncedModificationTime = yield attachment1.attachmentModificationTime; 320 attachment1.attachmentSyncedHash = yield attachment1.attachmentHash; 321 attachment1.synced = true; 322 yield attachment1.saveTx({ 323 skipSyncedUpdate: true 324 }); 325 326 // File attachment that's in sync with changed file -- delete file and mark for download 327 var attachment2 = yield importFileAttachment('test.png', { libraryID }); 328 attachment2.synced = true; 329 yield attachment2.saveTx({ 330 skipSyncedUpdate: true 331 }); 332 333 // File attachment that's unsynced -- delete item and file 334 var attachment3 = yield importFileAttachment('test.pdf', { libraryID }); 335 336 // Has to be called before resetUnsyncedLibraryFiles() 337 assert.isTrue(yield Zotero.Sync.Data.Local._libraryHasUnsyncedFiles(libraryID)); 338 339 yield Zotero.Sync.Data.Local.resetUnsyncedLibraryFiles(libraryID); 340 341 assert.isTrue(yield attachment1.fileExists()); 342 assert.isFalse(yield attachment2.fileExists()); 343 assert.isFalse(yield attachment3.fileExists()); 344 assert.equal( 345 attachment1.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_IN_SYNC 346 ); 347 assert.equal( 348 attachment2.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD 349 ); 350 assert.isFalse(Zotero.Items.get(attachment3.id)); 351 }); 352 }); 353 354 it("should revert modified file attachment item", async function () { 355 var group = await createGroup({ 356 version: 1, 357 libraryVersion: 2 358 }); 359 var libraryID = group.libraryID; 360 361 // File attachment that's changed but file is in sync -- reset item, keep file 362 var attachment = await importFileAttachment('test.png', { libraryID }); 363 var originalTitle = attachment.getField('title'); 364 attachment.attachmentSyncedModificationTime = await attachment.attachmentModificationTime; 365 attachment.attachmentSyncedHash = await attachment.attachmentHash; 366 attachment.attachmentSyncState = "in_sync"; 367 attachment.synced = true; 368 attachment.version = 2; 369 await attachment.saveTx({ 370 skipSyncedUpdate: true 371 }); 372 // Save original in cache 373 await Zotero.Sync.Data.Local.saveCacheObject( 374 'item', 375 libraryID, 376 Object.assign( 377 attachment.toJSON(), 378 // TEMP: md5 and mtime aren't currently included in JSON, and without it the 379 // file gets marked for download when the item gets reset from the cache 380 { 381 md5: attachment.attachmentHash, 382 mtime: attachment.attachmentSyncedModificationTime 383 } 384 ) 385 ); 386 // Modify title 387 attachment.setField('title', "New Title"); 388 await attachment.saveTx(); 389 390 await Zotero.Sync.Data.Local.resetUnsyncedLibraryFiles(libraryID); 391 392 assert.isTrue(await attachment.fileExists()); 393 assert.equal(attachment.getField('title'), originalTitle); 394 assert.equal( 395 attachment.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_IN_SYNC 396 ); 397 }); 398 }); 399 400 401 describe("#getLatestCacheObjectVersions", function () { 402 before(function* () { 403 yield resetDB({ 404 thisArg: this, 405 skipBundledFiles: true 406 }); 407 408 yield Zotero.Sync.Data.Local.saveCacheObjects( 409 'item', 410 Zotero.Libraries.userLibraryID, 411 [ 412 { 413 key: 'AAAAAAAA', 414 version: 2, 415 title: "A2" 416 }, 417 { 418 key: 'AAAAAAAA', 419 version: 1, 420 title: "A1" 421 }, 422 { 423 key: 'BBBBBBBB', 424 version: 1, 425 title: "B1" 426 }, 427 { 428 key: 'BBBBBBBB', 429 version: 2, 430 title: "B2" 431 }, 432 { 433 key: 'CCCCCCCC', 434 version: 3, 435 title: "C" 436 } 437 ] 438 ); 439 }) 440 441 it("should return latest version of all objects if no keys passed", function* () { 442 var versions = yield Zotero.Sync.Data.Local.getLatestCacheObjectVersions( 443 'item', 444 Zotero.Libraries.userLibraryID 445 ); 446 var keys = Object.keys(versions); 447 assert.lengthOf(keys, 3); 448 assert.sameMembers(keys, ['AAAAAAAA', 'BBBBBBBB', 'CCCCCCCC']); 449 assert.equal(versions.AAAAAAAA, 2); 450 assert.equal(versions.BBBBBBBB, 2); 451 assert.equal(versions.CCCCCCCC, 3); 452 }) 453 454 it("should return latest version of objects with passed keys", function* () { 455 var versions = yield Zotero.Sync.Data.Local.getLatestCacheObjectVersions( 456 'item', 457 Zotero.Libraries.userLibraryID, 458 ['AAAAAAAA', 'CCCCCCCC'] 459 ); 460 var keys = Object.keys(versions); 461 assert.lengthOf(keys, 2); 462 assert.sameMembers(keys, ['AAAAAAAA', 'CCCCCCCC']); 463 assert.equal(versions.AAAAAAAA, 2); 464 assert.equal(versions.CCCCCCCC, 3); 465 }) 466 }) 467 468 469 describe("#processObjectsFromJSON()", function () { 470 var types = Zotero.DataObjectUtilities.getTypes(); 471 472 beforeEach(function* () { 473 yield resetDB({ 474 thisArg: this, 475 skipBundledFiles: true 476 }); 477 }) 478 479 it("should update local version number and mark as synced if remote version is identical", function* () { 480 var libraryID = Zotero.Libraries.userLibraryID; 481 482 for (let type of types) { 483 let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type); 484 let obj = yield createDataObject(type); 485 let data = obj.toJSON(); 486 data.key = obj.key; 487 data.version = 10; 488 let json = { 489 key: obj.key, 490 version: 10, 491 data: data 492 }; 493 yield Zotero.Sync.Data.Local.processObjectsFromJSON( 494 type, libraryID, [json], { stopOnError: true } 495 ); 496 let localObj = objectsClass.getByLibraryAndKey(libraryID, obj.key); 497 assert.equal(localObj.version, 10); 498 assert.isTrue(localObj.synced); 499 } 500 }) 501 502 it("should keep local item changes while applying non-conflicting remote changes", function* () { 503 var libraryID = Zotero.Libraries.userLibraryID; 504 505 var type = 'item'; 506 let obj = yield createDataObject(type, { version: 5 }); 507 let data = obj.toJSON(); 508 yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [data]); 509 510 // Change local title 511 yield modifyDataObject(obj) 512 var changedTitle = obj.getField('title'); 513 514 // Create remote version without title but with changed place 515 data.key = obj.key; 516 data.version = 10; 517 var changedPlace = data.place = 'New York'; 518 let json = { 519 key: obj.key, 520 version: 10, 521 data: data 522 }; 523 yield Zotero.Sync.Data.Local.processObjectsFromJSON( 524 type, libraryID, [json], { stopOnError: true } 525 ); 526 assert.equal(obj.version, 10); 527 assert.equal(obj.getField('title'), changedTitle); 528 assert.equal(obj.getField('place'), changedPlace); 529 }) 530 531 it("should save item with overriding local conflict as unsynced", async function () { 532 var libraryID = Zotero.Libraries.userLibraryID; 533 534 var isbn = '978-0-335-22006-9'; 535 var type = 'item'; 536 let obj = createUnsavedDataObject(type, { version: 5 }); 537 obj.setField('ISBN', isbn); 538 await obj.saveTx(); 539 let data = obj.toJSON(); 540 541 data.key = obj.key; 542 data.version = 10; 543 data.ISBN = '9780335220069'; 544 let json = { 545 key: obj.key, 546 version: 10, 547 data 548 }; 549 var results = await Zotero.Sync.Data.Local.processObjectsFromJSON( 550 type, libraryID, [json], { stopOnError: true } 551 ); 552 assert.isTrue(results[0].processed); 553 assert.isUndefined(results[0].changes); 554 assert.isUndefined(results[0].conflicts); 555 assert.equal(obj.version, 10); 556 assert.equal(obj.getField('ISBN'), isbn); 557 assert.isFalse(obj.synced); 558 // Sync cache should match remote 559 var cacheJSON = await Zotero.Sync.Data.Local.getCacheObject(type, libraryID, data.key, data.version); 560 assert.propertyVal(cacheJSON.data, "ISBN", data.ISBN); 561 }); 562 563 it("should restore locally deleted collections and searches that changed remotely", async function () { 564 var libraryID = Zotero.Libraries.userLibraryID; 565 566 for (let type of ['collection', 'search']) { 567 let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type); 568 let obj = await createDataObject(type, { version: 1 }); 569 let data = obj.toJSON(); 570 571 await obj.eraseTx(); 572 573 data.key = obj.key; 574 data.version = 2; 575 let json = { 576 key: obj.key, 577 version: 2, 578 data 579 }; 580 let results = await Zotero.Sync.Data.Local.processObjectsFromJSON( 581 type, libraryID, [json], { stopOnError: true } 582 ); 583 assert.isTrue(results[0].processed); 584 assert.notOk(results[0].conflict); 585 assert.isTrue(results[0].restored); 586 assert.isUndefined(results[0].changes); 587 assert.isUndefined(results[0].conflicts); 588 obj = objectsClass.getByLibraryAndKey(libraryID, data.key); 589 assert.equal(obj.version, 2); 590 assert.isTrue(obj.synced); 591 assert.isFalse(await Zotero.Sync.Data.Local.getDateDeleted(type, libraryID, data.key)); 592 } 593 }); 594 595 it("should delete older versions in sync cache after processing", function* () { 596 var libraryID = Zotero.Libraries.userLibraryID; 597 598 for (let type of types) { 599 let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type); 600 601 // Save original version 602 let obj = yield createDataObject(type, { version: 5 }); 603 let data = obj.toJSON(); 604 yield Zotero.Sync.Data.Local.saveCacheObjects( 605 type, libraryID, [data] 606 ); 607 608 // Save newer version 609 data.version = 10; 610 yield Zotero.Sync.Data.Local.processObjectsFromJSON( 611 type, libraryID, [data], { stopOnError: true } 612 ); 613 614 let localObj = objectsClass.getByLibraryAndKey(libraryID, obj.key); 615 assert.equal(localObj.version, 10); 616 617 let versions = yield Zotero.Sync.Data.Local.getCacheObjectVersions( 618 type, libraryID, obj.key 619 ); 620 assert.sameMembers( 621 versions, 622 [10], 623 "should have only latest version of " + type + " in cache" 624 ); 625 } 626 }); 627 628 it("should delete object from sync queue after processing", function* () { 629 var objectType = 'item'; 630 var libraryID = Zotero.Libraries.userLibraryID; 631 var key = Zotero.DataObjectUtilities.generateKey(); 632 633 yield Zotero.Sync.Data.Local.addObjectsToSyncQueue(objectType, libraryID, [key]); 634 635 var versions = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue(objectType, libraryID); 636 assert.include(versions, key); 637 638 var json = { 639 key, 640 version: 10, 641 data: { 642 key, 643 version: 10, 644 itemType: "book", 645 title: "Test" 646 } 647 }; 648 649 yield Zotero.Sync.Data.Local.processObjectsFromJSON( 650 objectType, libraryID, [json], { stopOnError: true } 651 ); 652 653 var versions = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue(objectType, libraryID); 654 assert.notInclude(versions, key); 655 }); 656 657 it("should mark new attachment items and library for download", function* () { 658 var library = Zotero.Libraries.userLibrary; 659 var libraryID = library.id; 660 Zotero.Sync.Storage.Local.setModeForLibrary(libraryID, 'zfs'); 661 662 var key = Zotero.DataObjectUtilities.generateKey(); 663 var version = 10; 664 var json = { 665 key, 666 version, 667 data: { 668 key, 669 version, 670 itemType: 'attachment', 671 linkMode: 'imported_file', 672 md5: '57f8a4fda823187b91e1191487b87fe6', 673 mtime: 1442261130615 674 } 675 }; 676 677 yield Zotero.Sync.Data.Local.processObjectsFromJSON( 678 'item', libraryID, [json], { stopOnError: true } 679 ); 680 var item = Zotero.Items.getByLibraryAndKey(libraryID, key); 681 assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD); 682 assert.isTrue(library.storageDownloadNeeded); 683 }) 684 685 it("should mark updated attachment items for download", function* () { 686 var library = Zotero.Libraries.userLibrary; 687 var libraryID = library.id; 688 Zotero.Sync.Storage.Local.setModeForLibrary(libraryID, 'zfs'); 689 690 var item = yield importFileAttachment('test.png'); 691 item.version = 5; 692 item.synced = true; 693 yield item.saveTx(); 694 695 // Set file as synced 696 item.attachmentSyncedModificationTime = yield item.attachmentModificationTime; 697 item.attachmentSyncedHash = yield item.attachmentHash; 698 item.attachmentSyncState = "in_sync"; 699 yield item.saveTx({ skipAll: true }); 700 701 // Simulate download of version with updated attachment 702 var json = item.toResponseJSON(); 703 json.version = 10; 704 json.data.version = 10; 705 json.data.md5 = '57f8a4fda823187b91e1191487b87fe6'; 706 json.data.mtime = new Date().getTime() + 10000; 707 yield Zotero.Sync.Data.Local.processObjectsFromJSON( 708 'item', libraryID, [json], { stopOnError: true } 709 ); 710 711 assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD); 712 assert.isTrue(library.storageDownloadNeeded); 713 }) 714 715 it("should ignore attachment metadata when resolving metadata conflict", function* () { 716 var libraryID = Zotero.Libraries.userLibraryID; 717 Zotero.Sync.Storage.Local.setModeForLibrary(libraryID, 'zfs'); 718 719 var item = yield importFileAttachment('test.png'); 720 item.version = 5; 721 yield item.saveTx(); 722 var json = item.toResponseJSON(); 723 yield Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json]); 724 725 // Set file as synced 726 item.attachmentSyncedModificationTime = yield item.attachmentModificationTime; 727 item.attachmentSyncedHash = yield item.attachmentHash; 728 item.attachmentSyncState = "in_sync"; 729 yield item.saveTx({ skipAll: true }); 730 731 // Modify title locally, leaving item unsynced 732 var newTitle = Zotero.Utilities.randomString(); 733 item.setField('title', newTitle); 734 yield item.saveTx(); 735 736 // Simulate download of version with original title but updated attachment 737 json.version = 10; 738 json.data.version = 10; 739 json.data.md5 = '57f8a4fda823187b91e1191487b87fe6'; 740 json.data.mtime = new Date().getTime() + 10000; 741 yield Zotero.Sync.Data.Local.processObjectsFromJSON( 742 'item', libraryID, [json], { stopOnError: true } 743 ); 744 745 assert.equal(item.getField('title'), newTitle); 746 assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD); 747 }) 748 749 it("should roll back partial object changes on error", function* () { 750 var libraryID = Zotero.Libraries.userLibraryID; 751 var key1 = "AAAAAAAA"; 752 var key2 = "BBBBBBBB"; 753 var json = [ 754 { 755 key: key1, 756 version: 1, 757 data: { 758 key: key1, 759 version: 1, 760 itemType: "book", 761 title: "Test A" 762 } 763 }, 764 { 765 key: key2, 766 version: 1, 767 data: { 768 key: key2, 769 version: 1, 770 itemType: "invalidType", 771 title: "Test B" 772 } 773 } 774 ]; 775 yield Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, json); 776 777 // Shouldn't roll back the successful item 778 yield assert.eventually.equal(Zotero.DB.valueQueryAsync( 779 "SELECT COUNT(*) FROM items WHERE libraryID=? AND key=?", [libraryID, key1] 780 ), 1); 781 // Should rollback the unsuccessful item 782 yield assert.eventually.equal(Zotero.DB.valueQueryAsync( 783 "SELECT COUNT(*) FROM items WHERE libraryID=? AND key=?", [libraryID, key2] 784 ), 0); 785 }); 786 }) 787 788 describe("Sync Queue", function () { 789 var lib1, lib2; 790 791 before(function* () { 792 lib1 = Zotero.Libraries.userLibraryID; 793 lib2 = (yield getGroup()).libraryID; 794 }); 795 796 beforeEach(function* () { 797 yield Zotero.DB.queryAsync("DELETE FROM syncQueue"); 798 }); 799 800 after(function* () { 801 yield Zotero.DB.queryAsync("DELETE FROM syncQueue"); 802 }); 803 804 describe("#addObjectsToSyncQueue()", function () { 805 it("should add new objects and update lastCheck and tries for existing objects", function* () { 806 var objectType = 'item'; 807 var syncObjectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType); 808 var now = Zotero.Date.getUnixTimestamp(); 809 var key1 = Zotero.DataObjectUtilities.generateKey(); 810 var key2 = Zotero.DataObjectUtilities.generateKey(); 811 var key3 = Zotero.DataObjectUtilities.generateKey(); 812 var key4 = Zotero.DataObjectUtilities.generateKey(); 813 yield Zotero.DB.queryAsync( 814 "INSERT INTO syncQueue (libraryID, key, syncObjectTypeID, lastCheck, tries) " 815 + "VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)", 816 [ 817 lib1, key1, syncObjectTypeID, now - 3700, 0, 818 lib1, key2, syncObjectTypeID, now - 7000, 1, 819 lib2, key3, syncObjectTypeID, now - 86400, 2 820 ] 821 ); 822 823 yield Zotero.Sync.Data.Local.addObjectsToSyncQueue(objectType, lib1, [key1, key2]); 824 yield Zotero.Sync.Data.Local.addObjectsToSyncQueue(objectType, lib2, [key4]); 825 826 var sql = "SELECT lastCheck, tries FROM syncQueue WHERE libraryID=? " 827 + `AND syncObjectTypeID=${syncObjectTypeID} AND key=?`; 828 var row; 829 // key1 830 row = yield Zotero.DB.rowQueryAsync(sql, [lib1, key1]); 831 assert.approximately(row.lastCheck, now, 1); 832 assert.equal(row.tries, 1); 833 // key2 834 row = yield Zotero.DB.rowQueryAsync(sql, [lib1, key2]); 835 assert.approximately(row.lastCheck, now, 1); 836 assert.equal(row.tries, 2); 837 // key3 838 row = yield Zotero.DB.rowQueryAsync(sql, [lib2, key3]); 839 assert.equal(row.lastCheck, now - 86400); 840 assert.equal(row.tries, 2); 841 // key4 842 row = yield Zotero.DB.rowQueryAsync(sql, [lib2, key4]); 843 assert.approximately(row.lastCheck, now, 1); 844 assert.equal(row.tries, 0); 845 }); 846 }); 847 848 describe("#getObjectsToTryFromSyncQueue()", function () { 849 it("should get objects that should be retried", function* () { 850 var objectType = 'item'; 851 var syncObjectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType); 852 var now = Zotero.Date.getUnixTimestamp(); 853 var key1 = Zotero.DataObjectUtilities.generateKey(); 854 var key2 = Zotero.DataObjectUtilities.generateKey(); 855 var key3 = Zotero.DataObjectUtilities.generateKey(); 856 var key4 = Zotero.DataObjectUtilities.generateKey(); 857 yield Zotero.DB.queryAsync( 858 "INSERT INTO syncQueue (libraryID, key, syncObjectTypeID, lastCheck, tries) " 859 + "VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)", 860 [ 861 lib1, key1, syncObjectTypeID, now - (30 * 60) - 10, 0, // more than half an hour, so should be retried 862 lib1, key2, syncObjectTypeID, now - (16 * 60 * 60) + 10, 4, // less than 16 hours, shouldn't be retried 863 lib2, key3, syncObjectTypeID, now - 86400 * 7, 20 // more than 64 hours, so should be retried 864 ] 865 ); 866 867 var keys = yield Zotero.Sync.Data.Local.getObjectsToTryFromSyncQueue('item', lib1); 868 assert.sameMembers(keys, [key1]); 869 var keys = yield Zotero.Sync.Data.Local.getObjectsToTryFromSyncQueue('item', lib2); 870 assert.sameMembers(keys, [key3]); 871 }); 872 }); 873 874 describe("#removeObjectsFromSyncQueue()", function () { 875 it("should remove objects from the sync queue", function* () { 876 var objectType = 'item'; 877 var syncObjectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType); 878 var now = Zotero.Date.getUnixTimestamp(); 879 var key1 = Zotero.DataObjectUtilities.generateKey(); 880 var key2 = Zotero.DataObjectUtilities.generateKey(); 881 var key3 = Zotero.DataObjectUtilities.generateKey(); 882 yield Zotero.DB.queryAsync( 883 "INSERT INTO syncQueue (libraryID, key, syncObjectTypeID, lastCheck, tries) " 884 + "VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)", 885 [ 886 lib1, key1, syncObjectTypeID, now, 0, 887 lib1, key2, syncObjectTypeID, now, 4, 888 lib2, key3, syncObjectTypeID, now, 20 889 ] 890 ); 891 892 yield Zotero.Sync.Data.Local.removeObjectsFromSyncQueue('item', lib1, [key1]); 893 894 var sql = "SELECT COUNT(*) FROM syncQueue WHERE libraryID=? " 895 + `AND syncObjectTypeID=${syncObjectTypeID} AND key=?`; 896 assert.notOk(yield Zotero.DB.valueQueryAsync(sql, [lib1, key1])); 897 assert.ok(yield Zotero.DB.valueQueryAsync(sql, [lib1, key2])); 898 assert.ok(yield Zotero.DB.valueQueryAsync(sql, [lib2, key3])); 899 }) 900 }); 901 902 describe("#resetSyncQueueTries", function () { 903 var spy; 904 905 after(function () { 906 if (spy) { 907 spy.restore(); 908 } 909 }) 910 911 it("should be run on version upgrade", function* () { 912 var sql = "REPLACE INTO settings (setting, key, value) VALUES ('client', 'lastVersion', ?)"; 913 yield Zotero.DB.queryAsync(sql, "5.0foo"); 914 915 spy = sinon.spy(Zotero.Sync.Data.Local, "resetSyncQueueTries"); 916 yield Zotero.Schema.updateSchema(); 917 assert.ok(spy.called); 918 }); 919 }); 920 }); 921 922 923 describe("#showConflictResolutionWindow()", function () { 924 it("should show title of note parent", function* () { 925 var parentItem = yield createDataObject('item', { title: "Parent" }); 926 var note = new Zotero.Item('note'); 927 note.parentKey = parentItem.key; 928 note.setNote("Test"); 929 yield note.saveTx(); 930 931 var promise = waitForWindow('chrome://zotero/content/merge.xul', function (dialog) { 932 var doc = dialog.document; 933 var wizard = doc.documentElement; 934 var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0]; 935 936 // Show title for middle and right panes 937 var parentText = Zotero.getString('pane.item.parentItem') + " Parent"; 938 assert.equal(mergeGroup.leftpane._id('parent-row').textContent, ""); 939 assert.equal(mergeGroup.rightpane._id('parent-row').textContent, parentText); 940 assert.equal(mergeGroup.mergepane._id('parent-row').textContent, parentText); 941 942 wizard.getButton('finish').click(); 943 }); 944 945 Zotero.Sync.Data.Local.showConflictResolutionWindow([ 946 { 947 libraryID: note.libraryID, 948 key: note.key, 949 processed: false, 950 conflict: true, 951 left: { 952 deleted: true, 953 dateDeleted: "2016-07-07 12:34:56" 954 }, 955 right: note.toJSON() 956 } 957 ]); 958 959 yield promise; 960 }); 961 }); 962 963 964 describe("#_reconcileChanges()", function () { 965 describe("items", function () { 966 it("should ignore non-conflicting local changes and return remote changes", function () { 967 var cacheJSON = { 968 key: "AAAAAAAA", 969 version: 1234, 970 itemType: "book", 971 title: "Title 1", 972 url: "http://zotero.org/", 973 publicationTitle: "Publisher", // Remove locally 974 extra: "Extra", // Removed on both 975 dateModified: "2015-05-14 12:34:56", 976 collections: [ 977 'AAAAAAAA', // Removed locally 978 'DDDDDDDD', // Removed remotely, 979 'EEEEEEEE' // Removed from both 980 ], 981 relations: { 982 a: 'A', // Unchanged string 983 c: ['C1', 'C2'], // Unchanged array 984 d: 'D', // String removed locally 985 e: ['E'], // Array removed locally 986 f: 'F1', // String changed locally 987 g: [ 988 'G1', // Unchanged 989 'G2', // Removed remotely 990 'G3' // Removed from both 991 ], 992 h: 'H', // String removed remotely 993 i: ['I'], // Array removed remotely 994 }, 995 tags: [ 996 { tag: 'A' }, // Removed locally 997 { tag: 'D' }, // Removed remotely 998 { tag: 'E' } // Removed from both 999 ] 1000 }; 1001 var json1 = { 1002 key: "AAAAAAAA", 1003 version: 1234, 1004 itemType: "book", 1005 title: "Title 2", // Changed locally 1006 url: "https://www.zotero.org/", // Same change on local and remote 1007 place: "Place", // Added locally 1008 dateModified: "2015-05-14 14:12:34", // Changed locally and remotely, but ignored 1009 collections: [ 1010 'BBBBBBBB', // Added locally 1011 'DDDDDDDD', 1012 'FFFFFFFF' // Added on both 1013 ], 1014 relations: { 1015 'a': 'A', 1016 'b': 'B', // String added locally 1017 'f': 'F2', 1018 'g': [ 1019 'G1', 1020 'G2', 1021 'G6' // Added locally and remotely 1022 ], 1023 h: 'H', // String removed remotely 1024 i: ['I'], // Array removed remotely 1025 1026 }, 1027 tags: [ 1028 { tag: 'B' }, 1029 { tag: 'D' }, 1030 { tag: 'F', type: 1 }, // Added on both 1031 { tag: 'G' }, // Added on both, but with different types 1032 { tag: 'H', type: 1 } // Added on both, but with different types 1033 ] 1034 }; 1035 var json2 = { 1036 key: "AAAAAAAA", 1037 version: 1235, 1038 itemType: "book", 1039 title: "Title 1", 1040 url: "https://www.zotero.org/", 1041 publicationTitle: "Publisher", 1042 date: "2015-05-15", // Added remotely 1043 dateModified: "2015-05-14 13:45:12", 1044 collections: [ 1045 'AAAAAAAA', 1046 'CCCCCCCC', // Added remotely 1047 'FFFFFFFF' 1048 ], 1049 relations: { 1050 'a': 'A', 1051 'd': 'D', 1052 'e': ['E'], 1053 'f': 'F1', 1054 'g': [ 1055 'G1', 1056 'G4', // Added remotely 1057 'G6' 1058 ], 1059 }, 1060 tags: [ 1061 { tag: 'A' }, 1062 { tag: 'C' }, 1063 { tag: 'F', type: 1 }, 1064 { tag: 'G', type: 1 }, 1065 { tag: 'H' } 1066 ] 1067 }; 1068 var ignoreFields = ['dateAdded', 'dateModified']; 1069 var result = Zotero.Sync.Data.Local._reconcileChanges( 1070 'item', cacheJSON, json1, json2, ignoreFields 1071 ); 1072 assert.sameDeepMembers( 1073 result.changes, 1074 [ 1075 { 1076 field: "date", 1077 op: "add", 1078 value: "2015-05-15" 1079 }, 1080 { 1081 field: "collections", 1082 op: "member-add", 1083 value: "CCCCCCCC" 1084 }, 1085 { 1086 field: "collections", 1087 op: "member-remove", 1088 value: "DDDDDDDD" 1089 }, 1090 // Relations 1091 { 1092 field: "relations", 1093 op: "property-member-remove", 1094 value: { 1095 key: 'g', 1096 value: 'G2' 1097 } 1098 }, 1099 { 1100 field: "relations", 1101 op: "property-member-add", 1102 value: { 1103 key: 'g', 1104 value: 'G4' 1105 } 1106 }, 1107 { 1108 field: "relations", 1109 op: "property-member-remove", 1110 value: { 1111 key: 'h', 1112 value: 'H' 1113 } 1114 }, 1115 { 1116 field: "relations", 1117 op: "property-member-remove", 1118 value: { 1119 key: 'i', 1120 value: 'I' 1121 } 1122 }, 1123 // Tags 1124 { 1125 field: "tags", 1126 op: "member-add", 1127 value: { 1128 tag: 'C' 1129 } 1130 }, 1131 { 1132 field: "tags", 1133 op: "member-remove", 1134 value: { 1135 tag: 'D' 1136 } 1137 }, 1138 { 1139 field: "tags", 1140 op: "member-remove", 1141 value: { 1142 tag: 'H', 1143 type: 1 1144 } 1145 }, 1146 { 1147 field: "tags", 1148 op: "member-add", 1149 value: { 1150 tag: 'H' 1151 } 1152 } 1153 ] 1154 ); 1155 assert.lengthOf(result.conflicts, 0); 1156 }) 1157 1158 it("should return empty arrays when no remote changes to apply", function () { 1159 // Similar to above but without differing remote changes 1160 var cacheJSON = { 1161 key: "AAAAAAAA", 1162 version: 1234, 1163 itemType: "book", 1164 title: "Title 1", 1165 url: "http://zotero.org/", 1166 publicationTitle: "Publisher", // Remove locally 1167 extra: "Extra", // Removed on both 1168 dateModified: "2015-05-14 12:34:56", 1169 collections: [ 1170 'AAAAAAAA', // Removed locally 1171 'DDDDDDDD', 1172 'EEEEEEEE' // Removed from both 1173 ], 1174 tags: [ 1175 { 1176 tag: 'A' // Removed locally 1177 }, 1178 { 1179 tag: 'D' // Removed remotely 1180 }, 1181 { 1182 tag: 'E' // Removed from both 1183 } 1184 ] 1185 }; 1186 var json1 = { 1187 key: "AAAAAAAA", 1188 version: 1234, 1189 itemType: "book", 1190 title: "Title 2", // Changed locally 1191 url: "https://www.zotero.org/", // Same change on local and remote 1192 place: "Place", // Added locally 1193 dateModified: "2015-05-14 14:12:34", // Changed locally and remotely, but ignored 1194 collections: [ 1195 'BBBBBBBB', // Added locally 1196 'DDDDDDDD', 1197 'FFFFFFFF' // Added on both 1198 ], 1199 tags: [ 1200 { 1201 tag: 'B' 1202 }, 1203 { 1204 tag: 'D' 1205 }, 1206 { 1207 tag: 'F', // Added on both 1208 type: 1 1209 }, 1210 { 1211 tag: 'G' // Added on both, but with different types 1212 } 1213 ] 1214 }; 1215 var json2 = { 1216 key: "AAAAAAAA", 1217 version: 1235, 1218 itemType: "book", 1219 title: "Title 1", 1220 url: "https://www.zotero.org/", 1221 publicationTitle: "Publisher", 1222 dateModified: "2015-05-14 13:45:12", 1223 collections: [ 1224 'AAAAAAAA', 1225 'DDDDDDDD', 1226 'FFFFFFFF' 1227 ], 1228 tags: [ 1229 { 1230 tag: 'A' 1231 }, 1232 { 1233 tag: 'D' 1234 }, 1235 { 1236 tag: 'F', 1237 type: 1 1238 }, 1239 { 1240 tag: 'G', 1241 type: 1 1242 } 1243 ] 1244 }; 1245 var ignoreFields = ['dateAdded', 'dateModified']; 1246 var result = Zotero.Sync.Data.Local._reconcileChanges( 1247 'item', cacheJSON, json1, json2, ignoreFields 1248 ); 1249 assert.lengthOf(result.changes, 0); 1250 assert.lengthOf(result.conflicts, 0); 1251 }) 1252 1253 it("should return conflict when changes can't be automatically resolved", function () { 1254 var cacheJSON = { 1255 key: "AAAAAAAA", 1256 version: 1234, 1257 title: "Title 1", 1258 dateModified: "2015-05-14 12:34:56" 1259 }; 1260 var json1 = { 1261 key: "AAAAAAAA", 1262 version: 1234, 1263 title: "Title 2", 1264 dateModified: "2015-05-14 14:12:34" 1265 }; 1266 var json2 = { 1267 key: "AAAAAAAA", 1268 version: 1235, 1269 title: "Title 3", 1270 dateModified: "2015-05-14 13:45:12" 1271 }; 1272 var ignoreFields = ['dateAdded', 'dateModified']; 1273 var result = Zotero.Sync.Data.Local._reconcileChanges( 1274 'item', cacheJSON, json1, json2, ignoreFields 1275 ); 1276 Zotero.debug('=-=-=-='); 1277 Zotero.debug(result); 1278 assert.lengthOf(result.changes, 0); 1279 assert.sameDeepMembers( 1280 result.conflicts, 1281 [ 1282 [ 1283 { 1284 field: "title", 1285 op: "modify", 1286 value: "Title 2" 1287 }, 1288 { 1289 field: "title", 1290 op: "modify", 1291 value: "Title 3" 1292 } 1293 ] 1294 ] 1295 ); 1296 }) 1297 1298 it("should automatically merge array/object members and generate conflicts for field changes in absence of cached version", function () { 1299 var json1 = { 1300 key: "AAAAAAAA", 1301 version: 1234, 1302 itemType: "book", 1303 title: "Title", 1304 creators: [ 1305 { 1306 name: "Center for History and New Media", 1307 creatorType: "author" 1308 } 1309 ], 1310 place: "Place", // Local 1311 dateModified: "2015-05-14 14:12:34", // Changed on both, but ignored 1312 collections: [ 1313 'AAAAAAAA' // Local 1314 ], 1315 relations: { 1316 'a': 'A', 1317 'b': 'B', // Local 1318 'e': 'E1', 1319 'f': [ 1320 'F1', 1321 'F2' // Local 1322 ], 1323 h: 'H', // String removed remotely 1324 i: ['I'], // Array removed remotely 1325 }, 1326 tags: [ 1327 { tag: 'A' }, // Local 1328 { tag: 'C' }, 1329 { tag: 'F', type: 1 }, 1330 { tag: 'G' }, // Different types 1331 { tag: 'H', type: 1 } // Different types 1332 ] 1333 }; 1334 var json2 = { 1335 key: "AAAAAAAA", 1336 version: 1235, 1337 itemType: "book", 1338 title: "Title", 1339 creators: [ 1340 { 1341 creatorType: "author", // Different property order shouldn't matter 1342 name: "Center for History and New Media" 1343 } 1344 ], 1345 date: "2015-05-15", // Remote 1346 dateModified: "2015-05-14 13:45:12", 1347 collections: [ 1348 'BBBBBBBB' // Remote 1349 ], 1350 relations: { 1351 'a': 'A', 1352 'c': 'C', // Remote 1353 'd': ['D'], // Remote 1354 'e': 'E2', 1355 'f': [ 1356 'F1', 1357 'F3' // Remote 1358 ], 1359 }, 1360 tags: [ 1361 { tag: 'B' }, // Remote 1362 { tag: 'C' }, 1363 { tag: 'F', type: 1 }, 1364 { tag: 'G', type: 1 }, // Different types 1365 { tag: 'H' } // Different types 1366 ] 1367 }; 1368 var ignoreFields = ['dateAdded', 'dateModified']; 1369 var result = Zotero.Sync.Data.Local._reconcileChanges( 1370 'item', false, json1, json2, ignoreFields 1371 ); 1372 Zotero.debug(result); 1373 assert.sameDeepMembers( 1374 result.changes, 1375 [ 1376 // Collections 1377 { 1378 field: "collections", 1379 op: "member-add", 1380 value: "BBBBBBBB" 1381 }, 1382 // Relations 1383 { 1384 field: "relations", 1385 op: "property-member-add", 1386 value: { 1387 key: 'c', 1388 value: 'C' 1389 } 1390 }, 1391 { 1392 field: "relations", 1393 op: "property-member-add", 1394 value: { 1395 key: 'd', 1396 value: 'D' 1397 } 1398 }, 1399 { 1400 field: "relations", 1401 op: "property-member-add", 1402 value: { 1403 key: 'e', 1404 value: 'E2' 1405 } 1406 }, 1407 { 1408 field: "relations", 1409 op: "property-member-add", 1410 value: { 1411 key: 'f', 1412 value: 'F3' 1413 } 1414 }, 1415 // Tags 1416 { 1417 field: "tags", 1418 op: "member-add", 1419 value: { 1420 tag: 'B' 1421 } 1422 }, 1423 { 1424 field: "tags", 1425 op: "member-add", 1426 value: { 1427 tag: 'G', 1428 type: 1 1429 } 1430 }, 1431 { 1432 field: "tags", 1433 op: "member-add", 1434 value: { 1435 tag: 'H' 1436 } 1437 } 1438 ] 1439 ); 1440 assert.sameDeepMembers( 1441 result.conflicts, 1442 [ 1443 [ 1444 { 1445 field: "place", 1446 op: "add", 1447 value: "Place" 1448 }, 1449 { 1450 field: "place", 1451 op: "delete" 1452 } 1453 ], 1454 [ 1455 { 1456 field: "date", 1457 op: "delete" 1458 }, 1459 { 1460 field: "date", 1461 op: "add", 1462 value: "2015-05-15" 1463 } 1464 ] 1465 ] 1466 ); 1467 }) 1468 1469 it("should automatically use remote version for unresolvable conflicts when both sides are in trash", function () { 1470 var cacheJSON = { 1471 key: "AAAAAAAA", 1472 version: 1234, 1473 title: "Title 1", 1474 dateModified: "2015-05-14 12:34:56" 1475 }; 1476 var json1 = { 1477 key: "AAAAAAAA", 1478 version: 1234, 1479 title: "Title 2", 1480 deleted: true, 1481 dateModified: "2015-05-14 14:12:34" 1482 }; 1483 var json2 = { 1484 key: "AAAAAAAA", 1485 version: 1235, 1486 title: "Title 3", 1487 deleted: true, 1488 dateModified: "2015-05-14 13:45:12" 1489 }; 1490 var ignoreFields = ['dateAdded', 'dateModified']; 1491 var result = Zotero.Sync.Data.Local._reconcileChanges( 1492 'item', cacheJSON, json1, json2, ignoreFields 1493 ); 1494 assert.lengthOf(result.changes, 1); 1495 assert.sameDeepMembers( 1496 result.changes, 1497 [ 1498 { 1499 field: "title", 1500 op: "modify", 1501 value: "Title 3" 1502 }, 1503 ] 1504 ); 1505 }); 1506 1507 it("should automatically apply inPublications setting from remote", function () { 1508 var cacheJSON = { 1509 key: "AAAAAAAA", 1510 version: 1234, 1511 title: "Title 1", 1512 dateModified: "2017-04-02 12:34:56" 1513 }; 1514 var json1 = { 1515 key: "AAAAAAAA", 1516 version: 1234, 1517 title: "Title 1", 1518 dateModified: "2017-04-02 12:34:56" 1519 }; 1520 var json2 = { 1521 key: "AAAAAAAA", 1522 version: 1235, 1523 title: "Title 1", 1524 inPublications: true, 1525 dateModified: "2017-04-03 12:34:56" 1526 }; 1527 var ignoreFields = ['dateAdded', 'dateModified']; 1528 var result = Zotero.Sync.Data.Local._reconcileChanges( 1529 'item', cacheJSON, json1, json2, ignoreFields 1530 ); 1531 assert.lengthOf(result.changes, 1); 1532 assert.sameDeepMembers( 1533 result.changes, 1534 [ 1535 { 1536 field: "inPublications", 1537 op: "add", 1538 value: true 1539 } 1540 ] 1541 ); 1542 }); 1543 }) 1544 1545 1546 describe("collections", function () { 1547 it("should ignore non-conflicting local changes and return remote changes", function () { 1548 var cacheJSON = { 1549 key: "AAAAAAAA", 1550 version: 1234, 1551 name: "Name 1", 1552 parentCollection: null, 1553 relations: { 1554 A: "A", // Removed locally 1555 C: "C" // Removed on both 1556 } 1557 }; 1558 var json1 = { 1559 key: "AAAAAAAA", 1560 version: 1234, 1561 name: "Name 2", // Changed locally 1562 parentCollection: null, 1563 relations: {} 1564 }; 1565 var json2 = { 1566 key: "AAAAAAAA", 1567 version: 1234, 1568 name: "Name 1", 1569 parentCollection: "BBBBBBBB", // Added remotely 1570 relations: { 1571 A: "A", 1572 B: "B" // Added remotely 1573 } 1574 }; 1575 var result = Zotero.Sync.Data.Local._reconcileChanges( 1576 'collection', cacheJSON, json1, json2 1577 ); 1578 assert.sameDeepMembers( 1579 result.changes, 1580 [ 1581 { 1582 field: "parentCollection", 1583 op: "add", 1584 value: "BBBBBBBB" 1585 }, 1586 { 1587 field: "relations", 1588 op: "property-member-add", 1589 value: { 1590 key: "B", 1591 value: "B" 1592 } 1593 } 1594 ] 1595 ); 1596 assert.lengthOf(result.conflicts, 0); 1597 }) 1598 1599 it("should return empty arrays when no remote changes to apply", function () { 1600 // Similar to above but without differing remote changes 1601 var cacheJSON = { 1602 key: "AAAAAAAA", 1603 version: 1234, 1604 name: "Name 1", 1605 conditions: [ 1606 { 1607 condition: "title", 1608 operator: "contains", 1609 value: "A" 1610 }, 1611 { 1612 condition: "place", 1613 operator: "is", 1614 value: "Chicago" 1615 } 1616 ] 1617 }; 1618 var json1 = { 1619 key: "AAAAAAAA", 1620 version: 1234, 1621 name: "Name 2", // Changed locally 1622 conditions: [ 1623 { 1624 condition: "title", 1625 operator: "contains", 1626 value: "A" 1627 }, 1628 // Added locally 1629 { 1630 condition: "place", 1631 operator: "is", 1632 value: "New York" 1633 }, 1634 { 1635 condition: "place", 1636 operator: "is", 1637 value: "Chicago" 1638 } 1639 ] 1640 }; 1641 var json2 = { 1642 key: "AAAAAAAA", 1643 version: 1234, 1644 name: "Name 1", 1645 conditions: [ 1646 { 1647 condition: "title", 1648 operator: "contains", 1649 value: "A" 1650 }, 1651 { 1652 condition: "place", 1653 operator: "is", 1654 value: "Chicago" 1655 } 1656 ] 1657 }; 1658 var result = Zotero.Sync.Data.Local._reconcileChanges( 1659 'search', cacheJSON, json1, json2 1660 ); 1661 assert.lengthOf(result.changes, 0); 1662 assert.lengthOf(result.conflicts, 0); 1663 }) 1664 1665 it("should automatically resolve conflicts with remote version", function () { 1666 var cacheJSON = { 1667 key: "AAAAAAAA", 1668 version: 1234, 1669 name: "Name 1" 1670 }; 1671 var json1 = { 1672 key: "AAAAAAAA", 1673 version: 1234, 1674 name: "Name 2" 1675 }; 1676 var json2 = { 1677 key: "AAAAAAAA", 1678 version: 1234, 1679 name: "Name 3" 1680 }; 1681 var result = Zotero.Sync.Data.Local._reconcileChanges( 1682 'search', cacheJSON, json1, json2 1683 ); 1684 assert.sameDeepMembers( 1685 result.changes, 1686 [ 1687 { 1688 field: "name", 1689 op: "modify", 1690 value: "Name 3" 1691 } 1692 ] 1693 ); 1694 assert.lengthOf(result.conflicts, 0); 1695 }) 1696 1697 it("should automatically resolve conflicts in absence of cached version", function () { 1698 var json1 = { 1699 key: "AAAAAAAA", 1700 version: 1234, 1701 name: "Name 1", 1702 conditions: [ 1703 { 1704 condition: "title", 1705 operator: "contains", 1706 value: "A" 1707 }, 1708 { 1709 condition: "place", 1710 operator: "is", 1711 value: "New York" 1712 } 1713 ] 1714 }; 1715 var json2 = { 1716 key: "AAAAAAAA", 1717 version: 1234, 1718 name: "Name 2", 1719 conditions: [ 1720 { 1721 condition: "title", 1722 operator: "contains", 1723 value: "A" 1724 }, 1725 { 1726 condition: "place", 1727 operator: "is", 1728 value: "Chicago" 1729 } 1730 ] 1731 }; 1732 var result = Zotero.Sync.Data.Local._reconcileChanges( 1733 'search', false, json1, json2 1734 ); 1735 assert.sameDeepMembers( 1736 result.changes, 1737 [ 1738 { 1739 field: "name", 1740 op: "modify", 1741 value: "Name 2" 1742 }, 1743 { 1744 field: "conditions", 1745 op: "member-add", 1746 value: { 1747 condition: "place", 1748 operator: "is", 1749 value: "Chicago" 1750 } 1751 } 1752 ] 1753 ); 1754 assert.lengthOf(result.conflicts, 0); 1755 }) 1756 }) 1757 1758 1759 describe("searches", function () { 1760 it("should ignore non-conflicting local changes and return remote changes", function () { 1761 var cacheJSON = { 1762 key: "AAAAAAAA", 1763 version: 1234, 1764 name: "Name 1", 1765 conditions: [ 1766 { 1767 condition: "title", 1768 operator: "contains", 1769 value: "A" 1770 }, 1771 { 1772 condition: "place", 1773 operator: "is", 1774 value: "Chicago" 1775 } 1776 ] 1777 }; 1778 var json1 = { 1779 key: "AAAAAAAA", 1780 version: 1234, 1781 name: "Name 2", // Changed locally 1782 conditions: [ 1783 { 1784 condition: "title", 1785 operator: "contains", 1786 value: "A" 1787 }, 1788 // Removed remotely 1789 { 1790 condition: "place", 1791 operator: "is", 1792 value: "Chicago" 1793 } 1794 ] 1795 }; 1796 var json2 = { 1797 key: "AAAAAAAA", 1798 version: 1234, 1799 name: "Name 1", 1800 conditions: [ 1801 { 1802 condition: "title", 1803 operator: "contains", 1804 value: "A" 1805 }, 1806 // Added remotely 1807 { 1808 condition: "place", 1809 operator: "is", 1810 value: "New York" 1811 } 1812 ] 1813 }; 1814 var result = Zotero.Sync.Data.Local._reconcileChanges( 1815 'search', cacheJSON, json1, json2 1816 ); 1817 assert.sameDeepMembers( 1818 result.changes, 1819 [ 1820 { 1821 field: "conditions", 1822 op: "member-add", 1823 value: { 1824 condition: "place", 1825 operator: "is", 1826 value: "New York" 1827 } 1828 }, 1829 { 1830 field: "conditions", 1831 op: "member-remove", 1832 value: { 1833 condition: "place", 1834 operator: "is", 1835 value: "Chicago" 1836 } 1837 } 1838 ] 1839 ); 1840 assert.lengthOf(result.conflicts, 0); 1841 }) 1842 1843 it("should return empty arrays when no remote changes to apply", function () { 1844 // Similar to above but without differing remote changes 1845 var cacheJSON = { 1846 key: "AAAAAAAA", 1847 version: 1234, 1848 name: "Name 1", 1849 conditions: [ 1850 { 1851 condition: "title", 1852 operator: "contains", 1853 value: "A" 1854 }, 1855 { 1856 condition: "place", 1857 operator: "is", 1858 value: "Chicago" 1859 } 1860 ] 1861 }; 1862 var json1 = { 1863 key: "AAAAAAAA", 1864 version: 1234, 1865 name: "Name 2", // Changed locally 1866 conditions: [ 1867 { 1868 condition: "title", 1869 operator: "contains", 1870 value: "A" 1871 }, 1872 // Added locally 1873 { 1874 condition: "place", 1875 operator: "is", 1876 value: "New York" 1877 }, 1878 { 1879 condition: "place", 1880 operator: "is", 1881 value: "Chicago" 1882 } 1883 ] 1884 }; 1885 var json2 = { 1886 key: "AAAAAAAA", 1887 version: 1234, 1888 name: "Name 1", 1889 conditions: [ 1890 { 1891 condition: "title", 1892 operator: "contains", 1893 value: "A" 1894 }, 1895 { 1896 condition: "place", 1897 operator: "is", 1898 value: "Chicago" 1899 } 1900 ] 1901 }; 1902 var result = Zotero.Sync.Data.Local._reconcileChanges( 1903 'search', cacheJSON, json1, json2 1904 ); 1905 assert.lengthOf(result.changes, 0); 1906 assert.lengthOf(result.conflicts, 0); 1907 }) 1908 1909 it("should automatically resolve conflicts with remote version", function () { 1910 var cacheJSON = { 1911 key: "AAAAAAAA", 1912 version: 1234, 1913 name: "Name 1" 1914 }; 1915 var json1 = { 1916 key: "AAAAAAAA", 1917 version: 1234, 1918 name: "Name 2" 1919 }; 1920 var json2 = { 1921 key: "AAAAAAAA", 1922 version: 1234, 1923 name: "Name 3" 1924 }; 1925 var result = Zotero.Sync.Data.Local._reconcileChanges( 1926 'search', cacheJSON, json1, json2 1927 ); 1928 assert.sameDeepMembers( 1929 result.changes, 1930 [ 1931 { 1932 field: "name", 1933 op: "modify", 1934 value: "Name 3" 1935 } 1936 ] 1937 ); 1938 assert.lengthOf(result.conflicts, 0); 1939 }) 1940 1941 it("should automatically resolve conflicts in absence of cached version", function () { 1942 var json1 = { 1943 key: "AAAAAAAA", 1944 version: 1234, 1945 name: "Name 1", 1946 conditions: [ 1947 { 1948 condition: "title", 1949 operator: "contains", 1950 value: "A" 1951 }, 1952 { 1953 condition: "place", 1954 operator: "is", 1955 value: "New York" 1956 } 1957 ] 1958 }; 1959 var json2 = { 1960 key: "AAAAAAAA", 1961 version: 1234, 1962 name: "Name 2", 1963 conditions: [ 1964 { 1965 condition: "title", 1966 operator: "contains", 1967 value: "A" 1968 }, 1969 { 1970 condition: "place", 1971 operator: "is", 1972 value: "Chicago" 1973 } 1974 ] 1975 }; 1976 var result = Zotero.Sync.Data.Local._reconcileChanges( 1977 'search', false, json1, json2 1978 ); 1979 assert.sameDeepMembers( 1980 result.changes, 1981 [ 1982 { 1983 field: "name", 1984 op: "modify", 1985 value: "Name 2" 1986 }, 1987 { 1988 field: "conditions", 1989 op: "member-add", 1990 value: { 1991 condition: "place", 1992 operator: "is", 1993 value: "Chicago" 1994 } 1995 } 1996 ] 1997 ); 1998 assert.lengthOf(result.conflicts, 0); 1999 }) 2000 }) 2001 }) 2002 2003 2004 describe("#reconcileChangesWithoutCache()", function () { 2005 it("should return conflict for conflicting fields", function () { 2006 var json1 = { 2007 key: "AAAAAAAA", 2008 version: 1234, 2009 title: "Title 1", 2010 pages: 10, 2011 dateModified: "2015-05-14 14:12:34" 2012 }; 2013 var json2 = { 2014 key: "AAAAAAAA", 2015 version: 1235, 2016 title: "Title 2", 2017 place: "New York", 2018 dateModified: "2015-05-14 13:45:12" 2019 }; 2020 var ignoreFields = ['dateAdded', 'dateModified']; 2021 var result = Zotero.Sync.Data.Local._reconcileChangesWithoutCache( 2022 'item', json1, json2, ignoreFields 2023 ); 2024 assert.lengthOf(result.changes, 0); 2025 assert.sameDeepMembers( 2026 result.conflicts, 2027 [ 2028 [ 2029 { 2030 field: "title", 2031 op: "add", 2032 value: "Title 1" 2033 }, 2034 { 2035 field: "title", 2036 op: "add", 2037 value: "Title 2" 2038 } 2039 ], 2040 [ 2041 { 2042 field: "pages", 2043 op: "add", 2044 value: 10 2045 }, 2046 { 2047 field: "pages", 2048 op: "delete" 2049 } 2050 ], 2051 [ 2052 { 2053 field: "place", 2054 op: "delete" 2055 }, 2056 { 2057 field: "place", 2058 op: "add", 2059 value: "New York" 2060 } 2061 ] 2062 ] 2063 ); 2064 }) 2065 2066 it("should automatically use remote version for note markup differences when text content matches", function () { 2067 var val2 = "<p>Foo bar<br />bar foo</p>"; 2068 2069 var json1 = { 2070 key: "AAAAAAAA", 2071 version: 0, 2072 itemType: "note", 2073 note: "Foo bar<br/>bar foo", 2074 dateModified: "2017-06-13 13:45:12" 2075 }; 2076 var json2 = { 2077 key: "AAAAAAAA", 2078 version: 5, 2079 itemType: "note", 2080 note: val2, 2081 dateModified: "2017-06-13 13:45:12" 2082 }; 2083 var ignoreFields = ['dateAdded', 'dateModified']; 2084 var result = Zotero.Sync.Data.Local._reconcileChangesWithoutCache( 2085 'item', json1, json2, ignoreFields 2086 ); 2087 assert.lengthOf(result.changes, 1); 2088 assert.sameDeepMembers( 2089 result.changes, 2090 [ 2091 { 2092 field: "note", 2093 op: "add", 2094 value: val2 2095 } 2096 ] 2097 ); 2098 assert.lengthOf(result.conflicts, 0); 2099 }); 2100 2101 it("should show conflict for note markup differences when text content doesn't match", function () { 2102 var json1 = { 2103 key: "AAAAAAAA", 2104 version: 0, 2105 itemType: "note", 2106 note: "Foo bar?", 2107 dateModified: "2017-06-13 13:45:12" 2108 }; 2109 var json2 = { 2110 key: "AAAAAAAA", 2111 version: 5, 2112 itemType: "note", 2113 note: "<p>Foo bar!</p>", 2114 dateModified: "2017-06-13 13:45:12" 2115 }; 2116 var ignoreFields = ['dateAdded', 'dateModified']; 2117 var result = Zotero.Sync.Data.Local._reconcileChangesWithoutCache( 2118 'item', json1, json2, ignoreFields 2119 ); 2120 assert.lengthOf(result.changes, 0); 2121 assert.lengthOf(result.conflicts, 1); 2122 }); 2123 2124 it("should automatically use remote version for conflicting fields when both sides are in trash", function () { 2125 var json1 = { 2126 key: "AAAAAAAA", 2127 version: 1234, 2128 title: "Title 1", 2129 pages: 10, 2130 deleted: true, 2131 dateModified: "2015-05-14 14:12:34" 2132 }; 2133 var json2 = { 2134 key: "AAAAAAAA", 2135 version: 1235, 2136 title: "Title 2", 2137 place: "New York", 2138 deleted: true, 2139 dateModified: "2015-05-14 13:45:12" 2140 }; 2141 var ignoreFields = ['dateAdded', 'dateModified']; 2142 var result = Zotero.Sync.Data.Local._reconcileChangesWithoutCache( 2143 'item', json1, json2, ignoreFields 2144 ); 2145 assert.lengthOf(result.changes, 3); 2146 assert.sameDeepMembers( 2147 result.changes, 2148 [ 2149 { 2150 field: "title", 2151 op: "modify", 2152 value: "Title 2" 2153 }, 2154 { 2155 field: "pages", 2156 op: "delete" 2157 }, 2158 { 2159 field: "place", 2160 op: "add", 2161 value: "New York" 2162 } 2163 ] 2164 ); 2165 }); 2166 2167 it("should automatically use local hyphenated ISBN value if only difference", function () { 2168 var json1 = { 2169 key: "AAAAAAAA", 2170 version: 1234, 2171 itemType: "book", 2172 ISBN: "978-0-335-22006-9" 2173 }; 2174 var json2 = { 2175 key: "AAAAAAAA", 2176 version: 1235, 2177 itemType: "book", 2178 ISBN: "9780335220069" 2179 }; 2180 var ignoreFields = ['dateAdded', 'dateModified']; 2181 var result = Zotero.Sync.Data.Local._reconcileChangesWithoutCache( 2182 'item', json1, json2, ignoreFields 2183 ); 2184 assert.lengthOf(result.changes, 0); 2185 assert.lengthOf(result.conflicts, 0); 2186 assert.isTrue(result.localChanged); 2187 }); 2188 2189 it("should automatically use remote hyphenated ISBN value if only difference", function () { 2190 var json1 = { 2191 key: "AAAAAAAA", 2192 version: 1234, 2193 itemType: "book", 2194 ISBN: "9780335220069" 2195 }; 2196 var json2 = { 2197 key: "AAAAAAAA", 2198 version: 1235, 2199 itemType: "book", 2200 ISBN: "978-0-335-22006-9" 2201 }; 2202 var ignoreFields = ['dateAdded', 'dateModified']; 2203 var result = Zotero.Sync.Data.Local._reconcileChangesWithoutCache( 2204 'item', json1, json2, ignoreFields 2205 ); 2206 assert.sameDeepMembers( 2207 result.changes, 2208 [ 2209 { 2210 field: "ISBN", 2211 op: "add", 2212 value: "978-0-335-22006-9" 2213 } 2214 ] 2215 ); 2216 assert.lengthOf(result.conflicts, 0); 2217 assert.isFalse(result.localChanged); 2218 }); 2219 }) 2220 })