syncEngineTest.js (126866B)
1 "use strict"; 2 3 describe("Zotero.Sync.Data.Engine", function () { 4 Components.utils.import("resource://zotero/config.js"); 5 6 var apiKey = Zotero.Utilities.randomString(24); 7 var baseURL = "http://local.zotero/"; 8 var engine, server, client, caller, stub, spy; 9 var userID = 1; 10 11 var responses = {}; 12 13 var setup = Zotero.Promise.coroutine(function* (options = {}) { 14 server = sinon.fakeServer.create(); 15 server.respondImmediately = true; 16 var background = options.background === undefined ? true : options.background; 17 var stopOnError = options.stopOnError === undefined ? true : options.stopOnError; 18 19 Components.utils.import("resource://zotero/concurrentCaller.js"); 20 var caller = new ConcurrentCaller(1); 21 caller.setLogger(msg => Zotero.debug(msg)); 22 caller.stopOnError = stopOnError; 23 24 var client = new Zotero.Sync.APIClient({ 25 baseURL, 26 apiVersion: options.apiVersion || ZOTERO_CONFIG.API_VERSION, 27 apiKey, 28 caller, 29 background 30 }); 31 32 var engine = new Zotero.Sync.Data.Engine({ 33 userID, 34 apiClient: client, 35 libraryID: options.libraryID || Zotero.Libraries.userLibraryID, 36 stopOnError 37 }); 38 39 return { engine, client, caller }; 40 }); 41 42 function setResponse(response) { 43 setHTTPResponse(server, baseURL, response, responses); 44 } 45 46 function setDefaultResponses(options = {}) { 47 var target = options.target || 'users/1'; 48 var headers = { 49 "Last-Modified-Version": options.libraryVersion || 5 50 }; 51 var lastLibraryVersion = options.lastLibraryVersion || 4; 52 setResponse({ 53 method: "GET", 54 url: `${target}/settings?since=${lastLibraryVersion}`, 55 status: 200, 56 headers, 57 json: {} 58 }); 59 setResponse({ 60 method: "GET", 61 url: `${target}/collections?format=versions&since=${lastLibraryVersion}`, 62 status: 200, 63 headers, 64 json: {} 65 }); 66 setResponse({ 67 method: "GET", 68 url: `${target}/searches?format=versions&since=${lastLibraryVersion}`, 69 status: 200, 70 headers, 71 json: {} 72 }); 73 setResponse({ 74 method: "GET", 75 url: `${target}/items/top?format=versions&since=${lastLibraryVersion}&includeTrashed=1`, 76 status: 200, 77 headers, 78 json: {} 79 }); 80 setResponse({ 81 method: "GET", 82 url: `${target}/items?format=versions&since=${lastLibraryVersion}&includeTrashed=1`, 83 status: 200, 84 headers, 85 json: {} 86 }); 87 setResponse({ 88 method: "GET", 89 url: `${target}/deleted?since=${lastLibraryVersion}`, 90 status: 200, 91 headers, 92 json: {} 93 }); 94 } 95 96 function makeCollectionJSON(options) { 97 return { 98 key: options.key, 99 version: options.version, 100 data: { 101 key: options.key, 102 version: options.version, 103 name: options.name, 104 parentCollection: options.parentCollection 105 } 106 }; 107 } 108 109 function makeSearchJSON(options) { 110 return { 111 key: options.key, 112 version: options.version, 113 data: { 114 key: options.key, 115 version: options.version, 116 name: options.name, 117 conditions: options.conditions ? options.conditions : [ 118 { 119 condition: 'title', 120 operator: 'contains', 121 value: 'test' 122 } 123 ] 124 } 125 }; 126 } 127 128 function makeItemJSON(options) { 129 var json = { 130 key: options.key, 131 version: options.version, 132 data: { 133 key: options.key, 134 version: options.version, 135 itemType: options.itemType || 'book', 136 title: options.title || options.name 137 } 138 }; 139 Object.assign(json.data, options); 140 delete json.data.name; 141 return json; 142 } 143 144 // Allow functions to be called programmatically 145 var makeJSONFunctions = { 146 collection: makeCollectionJSON, 147 search: makeSearchJSON, 148 item: makeItemJSON 149 }; 150 151 var assertInCache = Zotero.Promise.coroutine(function* (obj) { 152 var cacheObject = yield Zotero.Sync.Data.Local.getCacheObject( 153 obj.objectType, obj.libraryID, obj.key, obj.version 154 ); 155 assert.isObject(cacheObject); 156 assert.propertyVal(cacheObject, 'key', obj.key); 157 }); 158 159 var assertNotInCache = Zotero.Promise.coroutine(function* (obj) { 160 assert.isFalse(yield Zotero.Sync.Data.Local.getCacheObject( 161 obj.objectType, obj.libraryID, obj.key, obj.version 162 )); 163 }); 164 165 // 166 // Tests 167 // 168 beforeEach(function* () { 169 yield resetDB({ 170 thisArg: this, 171 skipBundledFiles: true 172 }); 173 174 Zotero.HTTP.mock = sinon.FakeXMLHttpRequest; 175 176 yield Zotero.Users.setCurrentUserID(userID); 177 yield Zotero.Users.setCurrentUsername("testuser"); 178 }) 179 180 after(function () { 181 Zotero.HTTP.mock = null; 182 }); 183 184 describe("Syncing", function () { 185 it("should download items into a new library", function* () { 186 ({ engine, client, caller } = yield setup()); 187 188 var headers = { 189 "Last-Modified-Version": 3 190 }; 191 setResponse({ 192 method: "GET", 193 url: "users/1/settings", 194 status: 200, 195 headers: headers, 196 json: { 197 tagColors: { 198 value: [ 199 { 200 name: "A", 201 color: "#CC66CC" 202 } 203 ], 204 version: 2 205 } 206 } 207 }); 208 setResponse({ 209 method: "GET", 210 url: "users/1/collections?format=versions", 211 status: 200, 212 headers: headers, 213 json: { 214 "AAAAAAAA": 1 215 } 216 }); 217 setResponse({ 218 method: "GET", 219 url: "users/1/searches?format=versions", 220 status: 200, 221 headers: headers, 222 json: { 223 "AAAAAAAA": 2 224 } 225 }); 226 setResponse({ 227 method: "GET", 228 url: "users/1/items/top?format=versions&includeTrashed=1", 229 status: 200, 230 headers: headers, 231 json: { 232 "AAAAAAAA": 3 233 } 234 }); 235 setResponse({ 236 method: "GET", 237 url: "users/1/items?format=versions&includeTrashed=1", 238 status: 200, 239 headers: headers, 240 json: { 241 "AAAAAAAA": 3, 242 "BBBBBBBB": 3 243 } 244 }); 245 setResponse({ 246 method: "GET", 247 url: "users/1/collections?format=json&collectionKey=AAAAAAAA", 248 status: 200, 249 headers: headers, 250 json: [ 251 makeCollectionJSON({ 252 key: "AAAAAAAA", 253 version: 1, 254 name: "A" 255 }) 256 ] 257 }); 258 setResponse({ 259 method: "GET", 260 url: "users/1/searches?format=json&searchKey=AAAAAAAA", 261 status: 200, 262 headers: headers, 263 json: [ 264 makeSearchJSON({ 265 key: "AAAAAAAA", 266 version: 2, 267 name: "A" 268 }) 269 ] 270 }); 271 setResponse({ 272 method: "GET", 273 url: "users/1/items?format=json&itemKey=AAAAAAAA&includeTrashed=1", 274 status: 200, 275 headers: headers, 276 json: [ 277 makeItemJSON({ 278 key: "AAAAAAAA", 279 version: 3, 280 itemType: "book", 281 title: "A" 282 }) 283 ] 284 }); 285 setResponse({ 286 method: "GET", 287 url: "users/1/items?format=json&itemKey=BBBBBBBB&includeTrashed=1", 288 status: 200, 289 headers: headers, 290 json: [ 291 makeItemJSON({ 292 key: "BBBBBBBB", 293 version: 3, 294 itemType: "note", 295 parentItem: "AAAAAAAA", 296 note: "This is a note." 297 }) 298 ] 299 }); 300 setResponse({ 301 method: "GET", 302 url: "users/1/deleted?since=0", 303 status: 200, 304 headers: headers, 305 json: {} 306 }); 307 yield engine.start(); 308 309 var userLibraryID = Zotero.Libraries.userLibraryID; 310 311 // Check local library version 312 assert.equal(Zotero.Libraries.getVersion(userLibraryID), 3); 313 314 // Make sure local objects exist 315 var setting = Zotero.SyncedSettings.get(userLibraryID, "tagColors"); 316 assert.lengthOf(setting, 1); 317 assert.equal(setting[0].name, 'A'); 318 var settingMetadata = Zotero.SyncedSettings.getMetadata(userLibraryID, "tagColors"); 319 assert.equal(settingMetadata.version, 2); 320 assert.isTrue(settingMetadata.synced); 321 322 var obj = yield Zotero.Collections.getByLibraryAndKeyAsync(userLibraryID, "AAAAAAAA"); 323 assert.equal(obj.name, 'A'); 324 assert.equal(obj.version, 1); 325 assert.isTrue(obj.synced); 326 yield assertInCache(obj); 327 328 obj = yield Zotero.Searches.getByLibraryAndKeyAsync(userLibraryID, "AAAAAAAA"); 329 assert.equal(obj.name, 'A'); 330 assert.equal(obj.version, 2); 331 assert.isTrue(obj.synced); 332 yield assertInCache(obj); 333 334 obj = yield Zotero.Items.getByLibraryAndKeyAsync(userLibraryID, "AAAAAAAA"); 335 assert.equal(obj.getField('title'), 'A'); 336 assert.equal(obj.version, 3); 337 assert.isTrue(obj.synced); 338 var parentItemID = obj.id; 339 yield assertInCache(obj); 340 341 obj = yield Zotero.Items.getByLibraryAndKeyAsync(userLibraryID, "BBBBBBBB"); 342 assert.equal(obj.getNote(), 'This is a note.'); 343 assert.equal(obj.parentItemID, parentItemID); 344 assert.equal(obj.version, 3); 345 assert.isTrue(obj.synced); 346 yield assertInCache(obj); 347 }) 348 349 it("should download items into a new read-only group", function* () { 350 var group = yield createGroup({ 351 editable: false, 352 filesEditable: false 353 }); 354 var libraryID = group.libraryID; 355 var itemToDelete = yield createDataObject( 356 'item', { libraryID, synced: true }, { skipEditCheck: true } 357 ) 358 var itemToDeleteID = itemToDelete.id; 359 360 ({ engine, client, caller } = yield setup({ libraryID })); 361 362 var headers = { 363 "Last-Modified-Version": 3 364 }; 365 setResponse({ 366 method: "GET", 367 url: `groups/${group.id}/settings`, 368 status: 200, 369 headers: headers, 370 json: { 371 tagColors: { 372 value: [ 373 { 374 name: "A", 375 color: "#CC66CC" 376 } 377 ], 378 version: 2 379 } 380 } 381 }); 382 setResponse({ 383 method: "GET", 384 url: `groups/${group.id}/collections?format=versions`, 385 status: 200, 386 headers: headers, 387 json: { 388 "AAAAAAAA": 1 389 } 390 }); 391 setResponse({ 392 method: "GET", 393 url: `groups/${group.id}/searches?format=versions`, 394 status: 200, 395 headers: headers, 396 json: { 397 "AAAAAAAA": 2 398 } 399 }); 400 setResponse({ 401 method: "GET", 402 url: `groups/${group.id}/items/top?format=versions&includeTrashed=1`, 403 status: 200, 404 headers: headers, 405 json: { 406 "AAAAAAAA": 3 407 } 408 }); 409 setResponse({ 410 method: "GET", 411 url: `groups/${group.id}/items?format=versions&includeTrashed=1`, 412 status: 200, 413 headers: headers, 414 json: { 415 "AAAAAAAA": 3, 416 "BBBBBBBB": 3 417 } 418 }); 419 setResponse({ 420 method: "GET", 421 url: `groups/${group.id}/collections?format=json&collectionKey=AAAAAAAA`, 422 status: 200, 423 headers: headers, 424 json: [ 425 makeCollectionJSON({ 426 key: "AAAAAAAA", 427 version: 1, 428 name: "A" 429 }) 430 ] 431 }); 432 setResponse({ 433 method: "GET", 434 url: `groups/${group.id}/searches?format=json&searchKey=AAAAAAAA`, 435 status: 200, 436 headers: headers, 437 json: [ 438 makeSearchJSON({ 439 key: "AAAAAAAA", 440 version: 2, 441 name: "A" 442 }) 443 ] 444 }); 445 setResponse({ 446 method: "GET", 447 url: `groups/${group.id}/items?format=json&itemKey=AAAAAAAA&includeTrashed=1`, 448 status: 200, 449 headers: headers, 450 json: [ 451 makeItemJSON({ 452 key: "AAAAAAAA", 453 version: 3, 454 itemType: "book", 455 title: "A" 456 }) 457 ] 458 }); 459 setResponse({ 460 method: "GET", 461 url: `groups/${group.id}/items?format=json&itemKey=BBBBBBBB&includeTrashed=1`, 462 status: 200, 463 headers: headers, 464 json: [ 465 makeItemJSON({ 466 key: "BBBBBBBB", 467 version: 3, 468 itemType: "note", 469 parentItem: "AAAAAAAA", 470 note: "This is a note." 471 }) 472 ] 473 }); 474 setResponse({ 475 method: "GET", 476 url: `groups/${group.id}/deleted?since=0`, 477 status: 200, 478 headers: headers, 479 json: { 480 "items": [itemToDelete.key] 481 } 482 }); 483 yield engine.start(); 484 485 // Check local library version 486 assert.equal(group.libraryVersion, 3); 487 488 // Make sure local objects exist 489 var setting = Zotero.SyncedSettings.get(libraryID, "tagColors"); 490 assert.lengthOf(setting, 1); 491 assert.equal(setting[0].name, 'A'); 492 var settingMetadata = Zotero.SyncedSettings.getMetadata(libraryID, "tagColors"); 493 assert.equal(settingMetadata.version, 2); 494 assert.isTrue(settingMetadata.synced); 495 496 var obj = Zotero.Collections.getByLibraryAndKey(libraryID, "AAAAAAAA"); 497 assert.equal(obj.name, 'A'); 498 assert.equal(obj.version, 1); 499 assert.isTrue(obj.synced); 500 yield assertInCache(obj); 501 502 obj = Zotero.Searches.getByLibraryAndKey(libraryID, "AAAAAAAA"); 503 assert.equal(obj.name, 'A'); 504 assert.equal(obj.version, 2); 505 assert.isTrue(obj.synced); 506 yield assertInCache(obj); 507 508 obj = Zotero.Items.getByLibraryAndKey(libraryID, "AAAAAAAA"); 509 assert.equal(obj.getField('title'), 'A'); 510 assert.equal(obj.version, 3); 511 assert.isTrue(obj.synced); 512 var parentItemID = obj.id; 513 yield assertInCache(obj); 514 515 obj = Zotero.Items.getByLibraryAndKey(libraryID, "BBBBBBBB"); 516 assert.equal(obj.getNote(), 'This is a note.'); 517 assert.equal(obj.parentItemID, parentItemID); 518 assert.equal(obj.version, 3); 519 assert.isTrue(obj.synced); 520 yield assertInCache(obj); 521 522 assert.isFalse(Zotero.Items.exists(itemToDeleteID)); 523 }); 524 525 it("should upload new full items and subsequent patches", function* () { 526 ({ engine, client, caller } = yield setup()); 527 528 var library = Zotero.Libraries.userLibrary; 529 var libraryID = library.id; 530 var lastLibraryVersion = 5; 531 library.libraryVersion = library.storageVersion = lastLibraryVersion; 532 yield library.saveTx(); 533 534 yield Zotero.SyncedSettings.set(libraryID, "testSetting1", { foo: "bar" }); 535 yield Zotero.SyncedSettings.set(libraryID, "testSetting2", { bar: "foo" }); 536 537 var types = Zotero.DataObjectUtilities.getTypes(); 538 var objects = {}; 539 var objectResponseJSON = {}; 540 var objectVersions = {}; 541 for (let type of types) { 542 objects[type] = [yield createDataObject(type, { setTitle: true })]; 543 objectVersions[type] = {}; 544 objectResponseJSON[type] = objects[type].map(o => o.toResponseJSON()); 545 } 546 547 server.respond(function (req) { 548 if (req.method == "POST") { 549 assert.equal( 550 req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion 551 ); 552 553 // Both settings should be uploaded 554 if (req.url == baseURL + "users/1/settings") { 555 let json = JSON.parse(req.requestBody); 556 assert.lengthOf(Object.keys(json), 2); 557 assert.property(json, "testSetting1"); 558 assert.property(json, "testSetting2"); 559 assert.property(json.testSetting1, "value"); 560 assert.property(json.testSetting2, "value"); 561 assert.propertyVal(json.testSetting1.value, "foo", "bar"); 562 assert.propertyVal(json.testSetting2.value, "bar", "foo"); 563 req.respond( 564 204, 565 { 566 "Last-Modified-Version": ++lastLibraryVersion 567 }, 568 "" 569 ); 570 return; 571 } 572 573 for (let type of types) { 574 let typePlural = Zotero.DataObjectUtilities.getObjectTypePlural(type); 575 if (req.url == baseURL + "users/1/" + typePlural) { 576 let json = JSON.parse(req.requestBody); 577 assert.lengthOf(json, 1); 578 assert.equal(json[0].key, objects[type][0].key); 579 assert.equal(json[0].version, 0); 580 if (type == 'item') { 581 assert.equal(json[0].title, objects[type][0].getField('title')); 582 } 583 else { 584 assert.equal(json[0].name, objects[type][0].name); 585 } 586 let objectJSON = objectResponseJSON[type][0]; 587 objectJSON.version = ++lastLibraryVersion; 588 objectJSON.data.version = lastLibraryVersion; 589 req.respond( 590 200, 591 { 592 "Content-Type": "application/json", 593 "Last-Modified-Version": lastLibraryVersion 594 }, 595 JSON.stringify({ 596 successful: { 597 "0": objectJSON 598 }, 599 unchanged: {}, 600 failed: {} 601 }) 602 ); 603 objectVersions[type][objects[type][0].key] = lastLibraryVersion; 604 return; 605 } 606 } 607 } 608 }) 609 610 yield engine.start(); 611 612 yield Zotero.SyncedSettings.set(libraryID, "testSetting2", { bar: "bar" }); 613 614 assert.equal(library.libraryVersion, lastLibraryVersion); 615 assert.equal(library.storageVersion, lastLibraryVersion); 616 for (let type of types) { 617 // Make sure objects were set to the correct version and marked as synced 618 assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(type, libraryID)), 0); 619 let key = objects[type][0].key; 620 let version = objects[type][0].version; 621 assert.equal(version, objectVersions[type][key]); 622 // Make sure uploaded objects were added to cache 623 let cached = yield Zotero.Sync.Data.Local.getCacheObject(type, libraryID, key, version); 624 assert.typeOf(cached, 'object'); 625 assert.equal(cached.key, key); 626 assert.equal(cached.version, version); 627 628 yield modifyDataObject(objects[type][0]); 629 } 630 631 ({ engine, client, caller } = yield setup()); 632 633 server.respond(function (req) { 634 if (req.method == "POST") { 635 assert.equal( 636 req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion 637 ); 638 639 // Modified setting should be uploaded 640 if (req.url == baseURL + "users/1/settings") { 641 let json = JSON.parse(req.requestBody); 642 assert.lengthOf(Object.keys(json), 1); 643 assert.property(json, "testSetting2"); 644 assert.property(json.testSetting2, "value"); 645 assert.propertyVal(json.testSetting2.value, "bar", "bar"); 646 req.respond( 647 204, 648 { 649 "Last-Modified-Version": ++lastLibraryVersion 650 }, 651 "" 652 ); 653 return; 654 } 655 656 for (let type of types) { 657 let typePlural = Zotero.DataObjectUtilities.getObjectTypePlural(type); 658 if (req.url == baseURL + "users/1/" + typePlural) { 659 let json = JSON.parse(req.requestBody); 660 assert.lengthOf(json, 1); 661 let j = json[0]; 662 let o = objects[type][0]; 663 assert.equal(j.key, o.key); 664 assert.equal(j.version, objectVersions[type][o.key]); 665 if (type == 'item') { 666 assert.equal(j.title, o.getField('title')); 667 } 668 else { 669 assert.equal(j.name, o.name); 670 } 671 672 // Verify PATCH semantics instead of POST (i.e., only changed fields) 673 let changedFieldsExpected = ['key', 'version']; 674 if (type == 'item') { 675 changedFieldsExpected.push('title', 'dateModified'); 676 } 677 else { 678 changedFieldsExpected.push('name'); 679 } 680 let changedFields = Object.keys(j); 681 assert.lengthOf( 682 changedFields, changedFieldsExpected.length, "same " + type + " length" 683 ); 684 assert.sameMembers( 685 changedFields, changedFieldsExpected, "same " + type + " members" 686 ); 687 let objectJSON = objectResponseJSON[type][0]; 688 objectJSON.version = ++lastLibraryVersion; 689 objectJSON.data.version = lastLibraryVersion; 690 req.respond( 691 200, 692 { 693 "Content-Type": "application/json", 694 "Last-Modified-Version": lastLibraryVersion 695 }, 696 JSON.stringify({ 697 successful: { 698 "0": objectJSON 699 }, 700 unchanged: {}, 701 failed: {} 702 }) 703 ); 704 objectVersions[type][o.key] = lastLibraryVersion; 705 return; 706 } 707 } 708 } 709 }) 710 711 yield engine.start(); 712 713 assert.equal(library.libraryVersion, lastLibraryVersion); 714 assert.equal(library.storageVersion, lastLibraryVersion); 715 for (let type of types) { 716 // Make sure objects were set to the correct version and marked as synced 717 assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(type, libraryID)), 0); 718 let o = objects[type][0]; 719 let key = o.key; 720 let version = o.version; 721 assert.equal(version, objectVersions[type][key]); 722 // Make sure uploaded objects were added to cache 723 let cached = yield Zotero.Sync.Data.Local.getCacheObject(type, libraryID, key, version); 724 assert.typeOf(cached, 'object'); 725 assert.equal(cached.key, key); 726 assert.equal(cached.version, version); 727 728 switch (type) { 729 case 'collection': 730 assert.isFalse(cached.data.parentCollection); 731 break; 732 733 case 'item': 734 assert.equal(cached.data.dateAdded, Zotero.Date.sqlToISO8601(o.dateAdded)); 735 break; 736 737 case 'search': 738 assert.isArray(cached.data.conditions); 739 break; 740 } 741 742 // Make sure older versions have been removed from the cache 743 let versions = yield Zotero.Sync.Data.Local.getCacheObjectVersions(type, libraryID, key); 744 assert.sameMembers(versions, [version]); 745 } 746 }) 747 748 749 it("should upload child item after parent item", function* () { 750 ({ engine, client, caller } = yield setup()); 751 752 var library = Zotero.Libraries.userLibrary; 753 var lastLibraryVersion = 5; 754 library.libraryVersion = lastLibraryVersion; 755 yield library.saveTx(); 756 757 // Create top-level note, book, and child note 758 var item1 = new Zotero.Item('note'); 759 item1.setNote('A'); 760 yield item1.saveTx(); 761 var item2 = yield createDataObject('item'); 762 var item3 = new Zotero.Item('note'); 763 item3.parentItemID = item2.id; 764 item3.setNote('B'); 765 yield item3.saveTx(); 766 // Move note under parent 767 item1.parentItemID = item2.id; 768 yield item1.saveTx(); 769 var handled = false; 770 771 server.respond(function (req) { 772 if (req.method == "POST" && req.url == baseURL + "users/1/items") { 773 let json = JSON.parse(req.requestBody); 774 assert.lengthOf(json, 3); 775 assert.equal(json[0].key, item2.key); 776 assert.equal(json[1].key, item1.key); 777 assert.equal(json[2].key, item3.key); 778 handled = true; 779 req.respond( 780 200, 781 { 782 "Content-Type": "application/json", 783 "Last-Modified-Version": ++lastLibraryVersion 784 }, 785 JSON.stringify({ 786 successful: { 787 "0": item2.toResponseJSON({ version: lastLibraryVersion }), 788 "1": item1.toResponseJSON({ version: lastLibraryVersion }), 789 "2": item3.toResponseJSON({ version: lastLibraryVersion }) 790 }, 791 unchanged: {}, 792 failed: {} 793 }) 794 ); 795 return; 796 } 797 }); 798 799 yield engine.start(); 800 assert.isTrue(handled); 801 }); 802 803 804 it("should upload child collection after parent collection", function* () { 805 ({ engine, client, caller } = yield setup()); 806 807 var library = Zotero.Libraries.userLibrary; 808 var lastLibraryVersion = 5; 809 library.libraryVersion = lastLibraryVersion; 810 yield library.saveTx(); 811 812 var collection1 = yield createDataObject('collection'); 813 var collection2 = yield createDataObject('collection'); 814 var collection3 = yield createDataObject('collection', { parentID: collection2.id }); 815 // Move collection under the other 816 collection1.parentID = collection2.id; 817 yield collection1.saveTx(); 818 819 var handled = false; 820 821 server.respond(function (req) { 822 if (req.method == "POST" && req.url == baseURL + "users/1/collections") { 823 let json = JSON.parse(req.requestBody); 824 assert.lengthOf(json, 3); 825 assert.equal(json[0].key, collection2.key); 826 assert.equal(json[1].key, collection1.key); 827 assert.equal(json[2].key, collection3.key); 828 handled = true; 829 req.respond( 830 200, 831 { 832 "Content-Type": "application/json", 833 "Last-Modified-Version": ++lastLibraryVersion 834 }, 835 JSON.stringify({ 836 successful: { 837 "0": collection2.toResponseJSON(), 838 "1": collection1.toResponseJSON(), 839 "2": collection3.toResponseJSON() 840 }, 841 unchanged: {}, 842 failed: {} 843 }) 844 ); 845 return; 846 } 847 }); 848 849 yield engine.start(); 850 assert.isTrue(handled); 851 }); 852 853 854 it("should update library version after settings upload", function* () { 855 ({ engine, client, caller } = yield setup()); 856 857 var library = Zotero.Libraries.userLibrary; 858 var libraryID = library.id; 859 var lastLibraryVersion = 5; 860 library.libraryVersion = library.storageVersion = lastLibraryVersion; 861 yield library.saveTx(); 862 863 yield Zotero.SyncedSettings.set(libraryID, "testSetting", { foo: "bar" }); 864 865 server.respond(function (req) { 866 if (req.method == "POST") { 867 assert.equal( 868 req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion 869 ); 870 871 if (req.url == baseURL + "users/1/settings") { 872 let json = JSON.parse(req.requestBody); 873 req.respond( 874 204, 875 { 876 "Last-Modified-Version": ++lastLibraryVersion 877 }, 878 "" 879 ); 880 return; 881 } 882 } 883 }) 884 885 yield engine.start(); 886 887 assert.isAbove(library.libraryVersion, 5); 888 assert.equal(library.libraryVersion, lastLibraryVersion); 889 assert.equal(library.storageVersion, lastLibraryVersion); 890 }); 891 892 893 it("shouldn't update library storage version after settings upload if storage version was already behind", function* () { 894 ({ engine, client, caller } = yield setup()); 895 896 var library = Zotero.Libraries.userLibrary; 897 var libraryID = library.id; 898 var lastLibraryVersion = 5; 899 library.libraryVersion = lastLibraryVersion; 900 library.storageVersion = 4; 901 yield library.saveTx(); 902 903 yield Zotero.SyncedSettings.set(libraryID, "testSetting", { foo: "bar" }); 904 905 server.respond(function (req) { 906 if (req.method == "POST") { 907 assert.equal( 908 req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion 909 ); 910 911 if (req.url == baseURL + "users/1/settings") { 912 let json = JSON.parse(req.requestBody); 913 req.respond( 914 204, 915 { 916 "Last-Modified-Version": ++lastLibraryVersion 917 }, 918 "" 919 ); 920 return; 921 } 922 } 923 }) 924 925 yield engine.start(); 926 927 assert.isAbove(library.libraryVersion, 5); 928 assert.equal(library.libraryVersion, lastLibraryVersion); 929 assert.equal(library.storageVersion, 4); 930 }); 931 932 933 it("shouldn't update library storage version after item upload if storage version was already behind", function* () { 934 ({ engine, client, caller } = yield setup()); 935 936 var library = Zotero.Libraries.userLibrary; 937 var libraryID = library.id; 938 var lastLibraryVersion = 5; 939 library.libraryVersion = lastLibraryVersion; 940 library.storageVersion = 4; 941 yield library.saveTx(); 942 943 var item = yield createDataObject('item'); 944 var itemResponseJSON = item.toResponseJSON(); 945 946 server.respond(function (req) { 947 if (req.method == "POST") { 948 assert.equal( 949 req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion 950 ); 951 952 if (req.url == baseURL + "users/1/items") { 953 req.respond( 954 200, 955 { 956 "Content-Type": "application/json", 957 "Last-Modified-Version": lastLibraryVersion 958 }, 959 JSON.stringify({ 960 successful: { 961 "0": itemResponseJSON 962 }, 963 unchanged: {}, 964 failed: {} 965 }) 966 ); 967 return; 968 } 969 } 970 }) 971 972 yield engine.start(); 973 974 assert.equal(library.libraryVersion, lastLibraryVersion); 975 assert.equal(library.storageVersion, 4); 976 }); 977 978 979 it("should process downloads after upload failure", function* () { 980 ({ engine, client, caller } = yield setup({ 981 stopOnError: false 982 })); 983 984 var library = Zotero.Libraries.userLibrary; 985 var libraryID = library.id; 986 var lastLibraryVersion = 5; 987 library.libraryVersion = lastLibraryVersion; 988 yield library.saveTx(); 989 990 var collection = yield createDataObject('collection'); 991 992 var called = 0; 993 server.respond(function (req) { 994 if (called == 0) { 995 req.respond( 996 200, 997 { 998 "Last-Modified-Version": lastLibraryVersion 999 }, 1000 JSON.stringify({ 1001 successful: {}, 1002 unchanged: {}, 1003 failed: { 1004 0: { 1005 code: 400, 1006 message: "Upload failed" 1007 } 1008 } 1009 }) 1010 ); 1011 } 1012 called++; 1013 }); 1014 1015 var stub = sinon.stub(engine, "_startDownload") 1016 .returns(Zotero.Promise.resolve(engine.DOWNLOAD_RESULT_CONTINUE)); 1017 1018 var e = yield getPromiseError(engine.start()); 1019 assert.equal(called, 1); 1020 // start() should still fail 1021 assert.ok(e); 1022 assert.equal(e.message, "Made no progress during upload -- stopping"); 1023 // The collection shouldn't have been marked as synced 1024 assert.isFalse(collection.synced); 1025 // Download should have been performed 1026 assert.ok(stub.called); 1027 1028 stub.restore(); 1029 }); 1030 1031 1032 it("shouldn't update library storage version if there were storage metadata changes", function* () { 1033 ({ engine, client, caller } = yield setup()); 1034 1035 var library = Zotero.Libraries.userLibrary; 1036 var lastLibraryVersion = 2; 1037 library.libraryVersion = lastLibraryVersion; 1038 library.storageVersion = lastLibraryVersion; 1039 yield library.saveTx(); 1040 1041 var target = 'users/1'; 1042 var newLibraryVersion = 5; 1043 var headers = { 1044 "Last-Modified-Version": newLibraryVersion 1045 }; 1046 1047 // Create an attachment response with storage metadata 1048 var item = new Zotero.Item('attachment'); 1049 item.attachmentLinkMode = 'imported_file'; 1050 item.attachmentFilename = 'test.txt'; 1051 item.attachmentContentType = 'text/plain'; 1052 item.attachmentCharset = 'utf-8'; 1053 var itemResponseJSON = item.toResponseJSON(); 1054 itemResponseJSON.key = itemResponseJSON.data.key = Zotero.DataObjectUtilities.generateKey(); 1055 itemResponseJSON.version = itemResponseJSON.data.version = newLibraryVersion; 1056 itemResponseJSON.data.mtime = new Date().getTime(); 1057 itemResponseJSON.data.md5 = '57f8a4fda823187b91e1191487b87fe6'; 1058 1059 setDefaultResponses({ 1060 target, 1061 lastLibraryVersion: lastLibraryVersion, 1062 libraryVersion: newLibraryVersion 1063 }); 1064 1065 setResponse({ 1066 method: "GET", 1067 url: `${target}/items?format=versions&since=${lastLibraryVersion}&includeTrashed=1`, 1068 status: 200, 1069 headers, 1070 json: { 1071 [item.key]: newLibraryVersion 1072 } 1073 }); 1074 1075 setResponse({ 1076 method: "GET", 1077 url: `${target}/items?format=json&itemKey=${item.key}&includeTrashed=1`, 1078 status: 200, 1079 headers, 1080 json: [itemResponseJSON] 1081 }); 1082 1083 yield engine.start(); 1084 1085 assert.equal(library.libraryVersion, newLibraryVersion); 1086 assert.equal(library.storageVersion, lastLibraryVersion); 1087 }); 1088 1089 1090 it("should update library storage version if there were no storage metadata changes and storage version wasn't already behind", function* () { 1091 ({ engine, client, caller } = yield setup()); 1092 1093 var library = Zotero.Libraries.userLibrary; 1094 var lastLibraryVersion = 2; 1095 library.libraryVersion = lastLibraryVersion; 1096 library.storageVersion = lastLibraryVersion; 1097 yield library.saveTx(); 1098 1099 var target = 'users/1'; 1100 var newLibraryVersion = 5; 1101 var headers = { 1102 "Last-Modified-Version": newLibraryVersion 1103 }; 1104 1105 setDefaultResponses({ 1106 target, 1107 lastLibraryVersion: lastLibraryVersion, 1108 libraryVersion: newLibraryVersion 1109 }); 1110 1111 yield engine.start(); 1112 1113 assert.equal(library.libraryVersion, newLibraryVersion); 1114 assert.equal(library.storageVersion, newLibraryVersion); 1115 }); 1116 1117 1118 it("shouldn't update library storage version if there were no storage metadata changes but storage version was already behind", function* () { 1119 ({ engine, client, caller } = yield setup()); 1120 1121 var library = Zotero.Libraries.userLibrary; 1122 var lastLibraryVersion = 2; 1123 library.libraryVersion = lastLibraryVersion; 1124 library.storageVersion = 1; 1125 yield library.saveTx(); 1126 1127 var target = 'users/1'; 1128 var newLibraryVersion = 5; 1129 var headers = { 1130 "Last-Modified-Version": newLibraryVersion 1131 }; 1132 1133 setDefaultResponses({ 1134 target, 1135 lastLibraryVersion: lastLibraryVersion, 1136 libraryVersion: newLibraryVersion 1137 }); 1138 1139 yield engine.start(); 1140 1141 assert.equal(library.libraryVersion, newLibraryVersion); 1142 assert.equal(library.storageVersion, 1); 1143 }); 1144 1145 1146 it("shouldn't include mtime and md5 for attachments in ZFS libraries", function* () { 1147 ({ engine, client, caller } = yield setup()); 1148 1149 var library = Zotero.Libraries.userLibrary; 1150 var lastLibraryVersion = 2; 1151 library.libraryVersion = lastLibraryVersion; 1152 yield library.saveTx(); 1153 1154 var item = new Zotero.Item('attachment'); 1155 item.attachmentLinkMode = 'imported_file'; 1156 item.attachmentFilename = 'test.txt'; 1157 item.attachmentContentType = 'text/plain'; 1158 item.attachmentCharset = 'utf-8'; 1159 yield item.saveTx(); 1160 1161 var itemResponseJSON = item.toResponseJSON(); 1162 itemResponseJSON.version = itemResponseJSON.data.version = lastLibraryVersion; 1163 1164 server.respond(function (req) { 1165 if (req.method == "POST") { 1166 if (req.url == baseURL + "users/1/items") { 1167 let json = JSON.parse(req.requestBody); 1168 assert.lengthOf(json, 1); 1169 let itemJSON = json[0]; 1170 assert.equal(itemJSON.key, item.key); 1171 assert.equal(itemJSON.version, 0); 1172 assert.property(itemJSON, "contentType"); 1173 assert.property(itemJSON, "charset"); 1174 assert.property(itemJSON, "filename"); 1175 assert.notProperty(itemJSON, "mtime"); 1176 assert.notProperty(itemJSON, "md5"); 1177 req.respond( 1178 200, 1179 { 1180 "Content-Type": "application/json", 1181 "Last-Modified-Version": lastLibraryVersion 1182 }, 1183 JSON.stringify({ 1184 successful: { 1185 "0": itemResponseJSON 1186 }, 1187 unchanged: {}, 1188 failed: {} 1189 }) 1190 ); 1191 return; 1192 } 1193 } 1194 }) 1195 1196 yield engine.start(); 1197 }); 1198 1199 1200 it("should include storage properties for attachments in WebDAV libraries", function* () { 1201 ({ engine, client, caller } = yield setup()); 1202 1203 var library = Zotero.Libraries.userLibrary; 1204 var lastLibraryVersion = 2; 1205 library.libraryVersion = lastLibraryVersion; 1206 yield library.saveTx(); 1207 Zotero.Sync.Storage.Local.setModeForLibrary(library.id, 'webdav'); 1208 1209 var item = new Zotero.Item('attachment'); 1210 item.attachmentLinkMode = 'imported_file'; 1211 item.attachmentFilename = 'test.txt'; 1212 item.attachmentContentType = 'text/plain'; 1213 item.attachmentCharset = 'utf-8'; 1214 yield item.saveTx(); 1215 1216 var itemResponseJSON = item.toResponseJSON(); 1217 itemResponseJSON.version = itemResponseJSON.data.version = lastLibraryVersion; 1218 1219 server.respond(function (req) { 1220 if (req.method == "POST") { 1221 if (req.url == baseURL + "users/1/items") { 1222 let json = JSON.parse(req.requestBody); 1223 assert.lengthOf(json, 1); 1224 let itemJSON = json[0]; 1225 assert.equal(itemJSON.key, item.key); 1226 assert.equal(itemJSON.version, 0); 1227 assert.propertyVal(itemJSON, "contentType", item.attachmentContentType); 1228 assert.propertyVal(itemJSON, "charset", item.attachmentCharset); 1229 assert.propertyVal(itemJSON, "filename", item.attachmentFilename); 1230 assert.propertyVal(itemJSON, "mtime", null); 1231 assert.propertyVal(itemJSON, "md5", null); 1232 req.respond( 1233 200, 1234 { 1235 "Content-Type": "application/json", 1236 "Last-Modified-Version": lastLibraryVersion 1237 }, 1238 JSON.stringify({ 1239 successful: { 1240 "0": itemResponseJSON 1241 }, 1242 unchanged: {}, 1243 failed: {} 1244 }) 1245 ); 1246 return; 1247 } 1248 } 1249 }) 1250 1251 yield engine.start(); 1252 }); 1253 1254 1255 it("should include mtime and md5 synced to WebDAV in WebDAV libraries", function* () { 1256 ({ engine, client, caller } = yield setup()); 1257 1258 var library = Zotero.Libraries.userLibrary; 1259 var lastLibraryVersion = 2; 1260 library.libraryVersion = lastLibraryVersion; 1261 yield library.saveTx(); 1262 Zotero.Sync.Storage.Local.setModeForLibrary(library.id, 'webdav'); 1263 1264 var item = new Zotero.Item('attachment'); 1265 item.attachmentLinkMode = 'imported_file'; 1266 item.attachmentFilename = 'test1.txt'; 1267 yield item.saveTx(); 1268 1269 var mtime = new Date().getTime(); 1270 var md5 = '57f8a4fda823187b91e1191487b87fe6'; 1271 1272 item.attachmentSyncedModificationTime = mtime; 1273 item.attachmentSyncedHash = md5; 1274 yield item.saveTx({ skipAll: true }); 1275 1276 var itemResponseJSON = item.toResponseJSON(); 1277 itemResponseJSON.version = itemResponseJSON.data.version = lastLibraryVersion; 1278 itemResponseJSON.data.mtime = mtime; 1279 itemResponseJSON.data.md5 = md5; 1280 1281 server.respond(function (req) { 1282 if (req.method == "POST") { 1283 if (req.url == baseURL + "users/1/items") { 1284 let json = JSON.parse(req.requestBody); 1285 assert.lengthOf(json, 1); 1286 let itemJSON = json[0]; 1287 assert.equal(itemJSON.key, item.key); 1288 assert.equal(itemJSON.version, 0); 1289 assert.equal(itemJSON.mtime, mtime); 1290 assert.equal(itemJSON.md5, md5); 1291 req.respond( 1292 200, 1293 { 1294 "Content-Type": "application/json", 1295 "Last-Modified-Version": lastLibraryVersion 1296 }, 1297 JSON.stringify({ 1298 successful: { 1299 "0": itemResponseJSON 1300 }, 1301 unchanged: {}, 1302 failed: {} 1303 }) 1304 ); 1305 return; 1306 } 1307 } 1308 }) 1309 1310 yield engine.start(); 1311 1312 // Check data in cache 1313 var json = yield Zotero.Sync.Data.Local.getCacheObject( 1314 'item', library.id, item.key, lastLibraryVersion 1315 ); 1316 assert.equal(json.data.mtime, mtime); 1317 assert.equal(json.data.md5, md5); 1318 }) 1319 1320 it("should update local objects with remotely saved version after uploading if necessary", function* () { 1321 ({ engine, client, caller } = yield setup()); 1322 1323 var library = Zotero.Libraries.userLibrary; 1324 var libraryID = library.id; 1325 var lastLibraryVersion = 5; 1326 library.libraryVersion = lastLibraryVersion; 1327 yield library.saveTx(); 1328 1329 var types = Zotero.DataObjectUtilities.getTypes(); 1330 var objects = {}; 1331 var objectResponseJSON = {}; 1332 var objectNames = {}; 1333 var itemDateModified = {}; 1334 for (let type of types) { 1335 objects[type] = [ 1336 yield createDataObject( 1337 type, { setTitle: true, dateModified: '2016-05-21 01:00:00' } 1338 ) 1339 ]; 1340 objectNames[type] = {}; 1341 objectResponseJSON[type] = objects[type].map(o => o.toResponseJSON()); 1342 if (type == 'item') { 1343 let item = objects[type][0]; 1344 itemDateModified[item.key] = item.dateModified; 1345 } 1346 } 1347 1348 server.respond(function (req) { 1349 if (req.method == "POST") { 1350 assert.equal( 1351 req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion 1352 ); 1353 1354 for (let type of types) { 1355 let typePlural = Zotero.DataObjectUtilities.getObjectTypePlural(type); 1356 if (req.url == baseURL + "users/1/" + typePlural) { 1357 let key = objects[type][0].key; 1358 let objectJSON = objectResponseJSON[type][0]; 1359 objectJSON.version = ++lastLibraryVersion; 1360 objectJSON.data.version = lastLibraryVersion; 1361 let prop = type == 'item' ? 'title' : 'name'; 1362 objectNames[type][key] = objectJSON.data[prop] = Zotero.Utilities.randomString(); 1363 req.respond( 1364 200, 1365 { 1366 "Content-Type": "application/json", 1367 "Last-Modified-Version": lastLibraryVersion 1368 }, 1369 JSON.stringify({ 1370 successful: { 1371 "0": objectJSON 1372 }, 1373 unchanged: {}, 1374 failed: {} 1375 }) 1376 ); 1377 return; 1378 } 1379 } 1380 } 1381 }) 1382 1383 yield engine.start(); 1384 1385 assert.equal(library.libraryVersion, lastLibraryVersion); 1386 for (let type of types) { 1387 // Make sure local objects were updated with new metadata and marked as synced 1388 assert.lengthOf((yield Zotero.Sync.Data.Local.getUnsynced(type, libraryID)), 0); 1389 let o = objects[type][0]; 1390 let key = o.key; 1391 let version = o.version; 1392 let name = objectNames[type][key]; 1393 if (type == 'item') { 1394 assert.equal(name, o.getField('title')); 1395 1396 // But Date Modified shouldn't have changed for items 1397 assert.equal(itemDateModified[key], o.dateModified); 1398 } 1399 else { 1400 assert.equal(name, o.name); 1401 } 1402 } 1403 }) 1404 1405 it("should upload local deletions", function* () { 1406 var { engine, client, caller } = yield setup(); 1407 var library = Zotero.Libraries.userLibrary; 1408 var lastLibraryVersion = 5; 1409 library.libraryVersion = library.storageVersion = lastLibraryVersion; 1410 yield library.saveTx(); 1411 1412 1413 var types = Zotero.DataObjectUtilities.getTypes(); 1414 var objects = {}; 1415 for (let type of types) { 1416 let obj1 = yield createDataObject(type); 1417 let obj2 = yield createDataObject(type); 1418 objects[type] = [obj1.key, obj2.key]; 1419 yield obj1.eraseTx(); 1420 yield obj2.eraseTx(); 1421 } 1422 1423 var count = types.length; 1424 1425 server.respond(function (req) { 1426 if (req.method == "DELETE") { 1427 assert.equal( 1428 req.requestHeaders["If-Unmodified-Since-Version"], lastLibraryVersion 1429 ); 1430 1431 // TODO: Settings? 1432 1433 // Data objects 1434 for (let type of types) { 1435 let typePlural = Zotero.DataObjectUtilities.getObjectTypePlural(type); 1436 if (req.url.startsWith(baseURL + "users/1/" + typePlural)) { 1437 let matches = req.url.match(new RegExp("\\?" + type + "Key=(.+)")); 1438 let keys = decodeURIComponent(matches[1]).split(','); 1439 assert.sameMembers(keys, objects[type]); 1440 req.respond( 1441 204, 1442 { 1443 "Last-Modified-Version": ++lastLibraryVersion 1444 } 1445 ); 1446 count--; 1447 return; 1448 } 1449 } 1450 } 1451 }) 1452 1453 yield engine.start(); 1454 1455 assert.equal(count, 0); 1456 for (let type of types) { 1457 yield assert.eventually.lengthOf( 1458 Zotero.Sync.Data.Local.getDeleted(type, library.id), 0 1459 ); 1460 } 1461 assert.equal(library.libraryVersion, lastLibraryVersion); 1462 assert.equal(library.storageVersion, lastLibraryVersion); 1463 }) 1464 1465 it("should make only one request if in sync", function* () { 1466 var library = Zotero.Libraries.userLibrary; 1467 library.libraryVersion = 5; 1468 yield library.saveTx(); 1469 ({ engine, client, caller } = yield setup()); 1470 1471 server.respond(function (req) { 1472 if (req.method == "GET" && req.url == baseURL + "users/1/settings?since=5") { 1473 let since = req.requestHeaders["If-Modified-Since-Version"]; 1474 if (since == 5) { 1475 req.respond(304); 1476 return; 1477 } 1478 } 1479 }); 1480 yield engine.start(); 1481 }) 1482 1483 it("should ignore errors when saving downloaded objects", function* () { 1484 ({ engine, client, caller } = yield setup({ 1485 stopOnError: false 1486 })); 1487 1488 var headers = { 1489 "Last-Modified-Version": 3 1490 }; 1491 setResponse({ 1492 method: "GET", 1493 url: "users/1/settings", 1494 status: 200, 1495 headers: headers, 1496 json: {} 1497 }); 1498 setResponse({ 1499 method: "GET", 1500 url: "users/1/collections?format=versions", 1501 status: 200, 1502 headers: headers, 1503 json: { 1504 "AAAAAAAA": 1, 1505 "BBBBBBBB": 1, 1506 "CCCCCCCC": 1 1507 } 1508 }); 1509 setResponse({ 1510 method: "GET", 1511 url: "users/1/searches?format=versions", 1512 status: 200, 1513 headers: headers, 1514 json: { 1515 "DDDDDDDD": 2, 1516 "EEEEEEEE": 2, 1517 "FFFFFFFF": 2 1518 } 1519 }); 1520 setResponse({ 1521 method: "GET", 1522 url: "users/1/items/top?format=versions&includeTrashed=1", 1523 status: 200, 1524 headers: headers, 1525 json: { 1526 "GGGGGGGG": 3, 1527 "HHHHHHHH": 3 1528 } 1529 }); 1530 setResponse({ 1531 method: "GET", 1532 url: "users/1/items?format=versions&includeTrashed=1", 1533 status: 200, 1534 headers: headers, 1535 json: { 1536 "GGGGGGGG": 3, 1537 "HHHHHHHH": 3, 1538 "JJJJJJJJ": 3 1539 } 1540 }); 1541 setResponse({ 1542 method: "GET", 1543 url: "users/1/collections?format=json&collectionKey=AAAAAAAA%2CBBBBBBBB%2CCCCCCCCC", 1544 status: 200, 1545 headers: headers, 1546 json: [ 1547 makeCollectionJSON({ 1548 key: "AAAAAAAA", 1549 version: 1, 1550 name: "A" 1551 }), 1552 makeCollectionJSON({ 1553 key: "BBBBBBBB", 1554 version: 1, 1555 name: "B", 1556 // Missing parent -- collection should be queued 1557 parentCollection: "ZZZZZZZZ" 1558 }), 1559 makeCollectionJSON({ 1560 key: "CCCCCCCC", 1561 version: 1, 1562 name: "C", 1563 // Unknown field -- should be ignored 1564 unknownField: 5 1565 }) 1566 ] 1567 }); 1568 setResponse({ 1569 method: "GET", 1570 url: "users/1/searches?format=json&searchKey=DDDDDDDD%2CEEEEEEEE%2CFFFFFFFF", 1571 status: 200, 1572 headers: headers, 1573 json: [ 1574 makeSearchJSON({ 1575 key: "DDDDDDDD", 1576 version: 2, 1577 name: "D", 1578 conditions: [ 1579 { 1580 condition: "title", 1581 operator: "is", 1582 value: "a" 1583 } 1584 ] 1585 }), 1586 makeSearchJSON({ 1587 key: "EEEEEEEE", 1588 version: 2, 1589 name: "E", 1590 conditions: [ 1591 { 1592 // Unknown search condition -- search should be queued 1593 condition: "unknownCondition", 1594 operator: "is", 1595 value: "a" 1596 } 1597 ] 1598 }), 1599 makeSearchJSON({ 1600 key: "FFFFFFFF", 1601 version: 2, 1602 name: "F", 1603 conditions: [ 1604 { 1605 condition: "title", 1606 // Unknown search operator -- search should be queued 1607 operator: "unknownOperator", 1608 value: "a" 1609 } 1610 ] 1611 }) 1612 ] 1613 }); 1614 setResponse({ 1615 method: "GET", 1616 url: "users/1/items?format=json&itemKey=GGGGGGGG%2CHHHHHHHH&includeTrashed=1", 1617 status: 200, 1618 headers: headers, 1619 json: [ 1620 makeItemJSON({ 1621 key: "GGGGGGGG", 1622 version: 3, 1623 itemType: "book", 1624 title: "G", 1625 // Unknown item field -- should be ignored 1626 unknownField: "B" 1627 }), 1628 makeItemJSON({ 1629 key: "HHHHHHHH", 1630 version: 3, 1631 // Unknown item type -- item should be queued 1632 itemType: "unknownItemType", 1633 title: "H" 1634 }) 1635 ] 1636 }); 1637 setResponse({ 1638 method: "GET", 1639 url: "users/1/items?format=json&itemKey=JJJJJJJJ&includeTrashed=1", 1640 status: 200, 1641 headers: headers, 1642 json: [ 1643 makeItemJSON({ 1644 key: "JJJJJJJJ", 1645 version: 3, 1646 itemType: "note", 1647 // Parent that couldn't be saved -- item should be queued 1648 parentItem: "HHHHHHHH", 1649 note: "This is a note." 1650 }) 1651 ] 1652 }); 1653 setResponse({ 1654 method: "GET", 1655 url: "users/1/deleted?since=0", 1656 status: 200, 1657 headers: headers, 1658 json: {} 1659 }); 1660 var spy = sinon.spy(engine, "onError"); 1661 yield engine.start(); 1662 1663 var userLibraryID = Zotero.Libraries.userLibraryID; 1664 1665 // Library version should have been updated 1666 assert.equal(Zotero.Libraries.getVersion(userLibraryID), 3); 1667 1668 // Check for saved objects 1669 yield assert.eventually.ok(Zotero.Collections.getByLibraryAndKeyAsync(userLibraryID, "AAAAAAAA")); 1670 yield assert.eventually.ok(Zotero.Searches.getByLibraryAndKeyAsync(userLibraryID, "DDDDDDDD")); 1671 yield assert.eventually.ok(Zotero.Items.getByLibraryAndKeyAsync(userLibraryID, "GGGGGGGG")); 1672 1673 // Check for queued objects 1674 var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('collection', userLibraryID); 1675 assert.sameMembers(keys, ['BBBBBBBB']); 1676 1677 var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('search', userLibraryID); 1678 assert.sameMembers(keys, ['EEEEEEEE', 'FFFFFFFF']); 1679 1680 var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', userLibraryID); 1681 assert.sameMembers(keys, ['HHHHHHHH', 'JJJJJJJJ']); 1682 1683 assert.equal(spy.callCount, 3); 1684 }); 1685 1686 it("should delay on second upload conflict", function* () { 1687 var library = Zotero.Libraries.userLibrary; 1688 library.libraryVersion = 5; 1689 yield library.saveTx(); 1690 ({ engine, client, caller } = yield setup()); 1691 1692 // Try to upload, get 412 1693 // Download, get new version number 1694 // Try to upload again, get 412 1695 // Delay 1696 // Download, get new version number 1697 // Upload, get 200 1698 1699 var item = yield createDataObject('item'); 1700 1701 var lastLibraryVersion = 5; 1702 var calls = 0; 1703 var t; 1704 server.respond(function (req) { 1705 if (req.method == "POST") { 1706 calls++; 1707 } 1708 1709 // On first and second upload attempts, return 412 1710 if (req.method == "POST" && req.url.startsWith(baseURL + "users/1/items")) { 1711 if (calls == 1 || calls == 2) { 1712 if (calls == 2) { 1713 assert.isAbove(new Date() - t, 50); 1714 } 1715 t = new Date(); 1716 req.respond( 1717 412, 1718 { 1719 "Last-Modified-Version": ++lastLibraryVersion 1720 }, 1721 "" 1722 ); 1723 } 1724 else { 1725 req.respond( 1726 200, 1727 { 1728 "Last-Modified-Version": ++lastLibraryVersion 1729 }, 1730 JSON.stringify({ 1731 successful: { 1732 "0": item.toResponseJSON() 1733 }, 1734 unchanged: {}, 1735 failed: {} 1736 }) 1737 ); 1738 } 1739 return; 1740 } 1741 if (req.method == "GET") { 1742 req.respond( 1743 200, 1744 { 1745 "Last-Modified-Version": lastLibraryVersion 1746 }, 1747 JSON.stringify({}) 1748 ); 1749 return; 1750 } 1751 }); 1752 1753 Zotero.Sync.Data.conflictDelayIntervals = [50, 70000]; 1754 yield engine.start(); 1755 1756 assert.equal(calls, 3); 1757 assert.isTrue(item.synced); 1758 assert.equal(library.libraryVersion, lastLibraryVersion); 1759 }); 1760 }) 1761 1762 describe("#_startDownload()", function () { 1763 it("shouldn't redownload objects that are already up to date", function* () { 1764 var userLibraryID = Zotero.Libraries.userLibraryID; 1765 //yield Zotero.Libraries.setVersion(userLibraryID, 5); 1766 ({ engine, client, caller } = yield setup()); 1767 1768 var objects = {}; 1769 for (let type of Zotero.DataObjectUtilities.getTypes()) { 1770 let obj = objects[type] = createUnsavedDataObject(type); 1771 obj.version = 5; 1772 obj.synced = true; 1773 yield obj.saveTx({ skipSyncedUpdate: true }); 1774 1775 yield Zotero.Sync.Data.Local.saveCacheObjects( 1776 type, 1777 userLibraryID, 1778 [ 1779 { 1780 key: obj.key, 1781 version: obj.version, 1782 data: obj.toJSON() 1783 } 1784 ] 1785 ); 1786 } 1787 1788 var json; 1789 var headers = { 1790 "Last-Modified-Version": 5 1791 }; 1792 setResponse({ 1793 method: "GET", 1794 url: "users/1/settings", 1795 status: 200, 1796 headers: headers, 1797 json: {} 1798 }); 1799 json = {}; 1800 json[objects.collection.key] = 5; 1801 setResponse({ 1802 method: "GET", 1803 url: "users/1/collections?format=versions", 1804 status: 200, 1805 headers: headers, 1806 json: json 1807 }); 1808 json = {}; 1809 json[objects.search.key] = 5; 1810 setResponse({ 1811 method: "GET", 1812 url: "users/1/searches?format=versions", 1813 status: 200, 1814 headers: headers, 1815 json: json 1816 }); 1817 json = {}; 1818 json[objects.item.key] = 5; 1819 setResponse({ 1820 method: "GET", 1821 url: "users/1/items/top?format=versions&includeTrashed=1", 1822 status: 200, 1823 headers: headers, 1824 json: json 1825 }); 1826 json = {}; 1827 json[objects.item.key] = 5; 1828 setResponse({ 1829 method: "GET", 1830 url: "users/1/items?format=versions&includeTrashed=1", 1831 status: 200, 1832 headers: headers, 1833 json: json 1834 }); 1835 setResponse({ 1836 method: "GET", 1837 url: "users/1/deleted?since=0", 1838 status: 200, 1839 headers: headers, 1840 json: {} 1841 }); 1842 1843 yield engine._startDownload(); 1844 }) 1845 1846 it("should apply remote deletions", function* () { 1847 var library = Zotero.Libraries.userLibrary; 1848 library.libraryVersion = 5; 1849 yield library.saveTx(); 1850 ({ engine, client, caller } = yield setup()); 1851 1852 // Create objects and mark them as synced 1853 yield Zotero.SyncedSettings.set( 1854 library.id, 'tagColors', [{name: 'A', color: '#CC66CC'}], 1, true 1855 ); 1856 var collection = createUnsavedDataObject('collection'); 1857 collection.synced = true; 1858 var collectionID = yield collection.saveTx({ skipSyncedUpdate: true }); 1859 var collectionKey = collection.key; 1860 var search = createUnsavedDataObject('search'); 1861 search.synced = true; 1862 var searchID = yield search.saveTx({ skipSyncedUpdate: true }); 1863 var searchKey = search.key; 1864 var item = createUnsavedDataObject('item'); 1865 item.synced = true; 1866 var itemID = yield item.saveTx({ skipSyncedUpdate: true }); 1867 var itemKey = item.key; 1868 1869 var headers = { 1870 "Last-Modified-Version": 6 1871 }; 1872 setResponse({ 1873 method: "GET", 1874 url: "users/1/settings?since=5", 1875 status: 200, 1876 headers: headers, 1877 json: {} 1878 }); 1879 setResponse({ 1880 method: "GET", 1881 url: "users/1/collections?format=versions&since=5", 1882 status: 200, 1883 headers: headers, 1884 json: {} 1885 }); 1886 setResponse({ 1887 method: "GET", 1888 url: "users/1/searches?format=versions&since=5", 1889 status: 200, 1890 headers: headers, 1891 json: {} 1892 }); 1893 setResponse({ 1894 method: "GET", 1895 url: "users/1/items?format=versions&since=5&includeTrashed=1", 1896 status: 200, 1897 headers: headers, 1898 json: {} 1899 }); 1900 setResponse({ 1901 method: "GET", 1902 url: "users/1/items/top?format=versions&since=5&includeTrashed=1", 1903 status: 200, 1904 headers: headers, 1905 json: {} 1906 }); 1907 setResponse({ 1908 method: "GET", 1909 url: "users/1/deleted?since=5", 1910 status: 200, 1911 headers: headers, 1912 json: { 1913 settings: ['tagColors'], 1914 collections: [collection.key], 1915 searches: [search.key], 1916 items: [item.key] 1917 } 1918 }); 1919 yield engine._startDownload(); 1920 1921 // Make sure objects were deleted 1922 assert.isNull(Zotero.SyncedSettings.get(library.id, 'tagColors')); 1923 assert.isFalse(Zotero.Collections.exists(collectionID)); 1924 assert.isFalse(Zotero.Searches.exists(searchID)); 1925 assert.isFalse(Zotero.Items.exists(itemID)); 1926 1927 // Make sure objects weren't added to sync delete log 1928 assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted( 1929 'setting', library.id, 'tagColors' 1930 )); 1931 assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted( 1932 'collection', library.id, collectionKey 1933 )); 1934 assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted( 1935 'search', library.id, searchKey 1936 )); 1937 assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted( 1938 'item', library.id, itemKey 1939 )); 1940 }) 1941 1942 it("should ignore remote deletions for non-item objects if local objects changed", function* () { 1943 var library = Zotero.Libraries.userLibrary; 1944 library.libraryVersion = 5; 1945 yield library.saveTx(); 1946 ({ engine, client, caller } = yield setup()); 1947 1948 // Create objects marked as unsynced 1949 yield Zotero.SyncedSettings.set( 1950 library.id, 'tagColors', [{name: 'A', color: '#CC66CC'}] 1951 ); 1952 var collection = createUnsavedDataObject('collection'); 1953 var collectionID = yield collection.saveTx(); 1954 var collectionKey = collection.key; 1955 var search = createUnsavedDataObject('search'); 1956 var searchID = yield search.saveTx(); 1957 var searchKey = search.key; 1958 1959 var headers = { 1960 "Last-Modified-Version": 6 1961 }; 1962 setResponse({ 1963 method: "GET", 1964 url: "users/1/settings?since=5", 1965 status: 200, 1966 headers: headers, 1967 json: {} 1968 }); 1969 setResponse({ 1970 method: "GET", 1971 url: "users/1/collections?format=versions&since=5", 1972 status: 200, 1973 headers: headers, 1974 json: {} 1975 }); 1976 setResponse({ 1977 method: "GET", 1978 url: "users/1/searches?format=versions&since=5", 1979 status: 200, 1980 headers: headers, 1981 json: {} 1982 }); 1983 setResponse({ 1984 method: "GET", 1985 url: "users/1/items/top?format=versions&since=5&includeTrashed=1", 1986 status: 200, 1987 headers: headers, 1988 json: {} 1989 }); 1990 setResponse({ 1991 method: "GET", 1992 url: "users/1/items?format=versions&since=5&includeTrashed=1", 1993 status: 200, 1994 headers: headers, 1995 json: {} 1996 }); 1997 setResponse({ 1998 method: "GET", 1999 url: "users/1/deleted?since=5", 2000 status: 200, 2001 headers: headers, 2002 json: { 2003 settings: ['tagColors'], 2004 collections: [collection.key], 2005 searches: [search.key], 2006 items: [] 2007 } 2008 }); 2009 yield engine._startDownload(); 2010 2011 // Make sure objects weren't deleted 2012 assert.ok(Zotero.SyncedSettings.get(library.id, 'tagColors')); 2013 assert.ok(Zotero.Collections.exists(collectionID)); 2014 assert.ok(Zotero.Searches.exists(searchID)); 2015 }) 2016 2017 it("should show conflict resolution window for conflicting remote deletions", function* () { 2018 var library = Zotero.Libraries.userLibrary; 2019 library.libraryVersion = 5; 2020 yield library.saveTx(); 2021 ({ engine, client, caller } = yield setup()); 2022 2023 // Create local unsynced items 2024 var item = createUnsavedDataObject('item'); 2025 item.setField('title', 'A'); 2026 item.synced = false; 2027 var itemID1 = yield item.saveTx({ skipSyncedUpdate: true }); 2028 var itemKey1 = item.key; 2029 2030 item = createUnsavedDataObject('item'); 2031 item.setField('title', 'B'); 2032 item.synced = false; 2033 var itemID2 = yield item.saveTx({ skipSyncedUpdate: true }); 2034 var itemKey2 = item.key; 2035 2036 var headers = { 2037 "Last-Modified-Version": 6 2038 }; 2039 setResponse({ 2040 method: "GET", 2041 url: "users/1/settings?since=5", 2042 status: 200, 2043 headers: headers, 2044 json: {} 2045 }); 2046 setResponse({ 2047 method: "GET", 2048 url: "users/1/collections?format=versions&since=5", 2049 status: 200, 2050 headers: headers, 2051 json: {} 2052 }); 2053 setResponse({ 2054 method: "GET", 2055 url: "users/1/searches?format=versions&since=5", 2056 status: 200, 2057 headers: headers, 2058 json: {} 2059 }); 2060 setResponse({ 2061 method: "GET", 2062 url: "users/1/items/top?format=versions&since=5&includeTrashed=1", 2063 status: 200, 2064 headers: headers, 2065 json: {} 2066 }); 2067 setResponse({ 2068 method: "GET", 2069 url: "users/1/items?format=versions&since=5&includeTrashed=1", 2070 status: 200, 2071 headers: headers, 2072 json: {} 2073 }); 2074 setResponse({ 2075 method: "GET", 2076 url: "users/1/deleted?since=5", 2077 status: 200, 2078 headers: headers, 2079 json: { 2080 settings: [], 2081 collections: [], 2082 searches: [], 2083 items: [itemKey1, itemKey2] 2084 } 2085 }); 2086 2087 var crPromise = waitForWindow('chrome://zotero/content/merge.xul', function (dialog) { 2088 var doc = dialog.document; 2089 var wizard = doc.documentElement; 2090 var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0]; 2091 2092 // 1 (accept remote deletion) 2093 assert.equal(mergeGroup.leftpane.getAttribute('selected'), 'true'); 2094 mergeGroup.rightpane.click(); 2095 wizard.getButton('next').click(); 2096 2097 // 2 (ignore remote deletion) 2098 assert.equal(mergeGroup.leftpane.getAttribute('selected'), 'true'); 2099 wizard.getButton('finish').click(); 2100 }) 2101 yield engine._startDownload(); 2102 yield crPromise; 2103 2104 assert.isFalse(Zotero.Items.exists(itemID1)); 2105 assert.isTrue(Zotero.Items.exists(itemID2)); 2106 }) 2107 2108 2109 it("should handle new remote item referencing locally deleted collection", async function () { 2110 var lastLibraryVersion = 5; 2111 var newLibraryVersion = 6; 2112 var library = Zotero.Libraries.userLibrary; 2113 library.libraryVersion = lastLibraryVersion; 2114 await library.saveTx(); 2115 ({ engine, client, caller } = await setup()); 2116 2117 // Create local deleted collection 2118 var collection = await createDataObject('collection'); 2119 var collectionKey = collection.key; 2120 await collection.eraseTx(); 2121 var itemKey = "AAAAAAAA"; 2122 2123 var headers = { 2124 "Last-Modified-Version": newLibraryVersion 2125 }; 2126 setDefaultResponses({ 2127 lastLibraryVersion, 2128 libraryVersion: newLibraryVersion 2129 }); 2130 setResponse({ 2131 method: "GET", 2132 url: `users/1/items?format=versions&since=${lastLibraryVersion}&includeTrashed=1`, 2133 status: 200, 2134 headers, 2135 json: { 2136 [itemKey]: newLibraryVersion 2137 } 2138 }); 2139 setResponse({ 2140 method: "GET", 2141 url: `users/1/items?format=json&itemKey=${itemKey}&includeTrashed=1`, 2142 status: 200, 2143 headers, 2144 json: [ 2145 makeItemJSON({ 2146 key: itemKey, 2147 version: newLibraryVersion, 2148 itemType: "book", 2149 collections: [collectionKey] 2150 }) 2151 ] 2152 }); 2153 2154 await engine._startDownload(); 2155 2156 // Item should be skipped and added to queue, which will allow collection deletion to upload 2157 var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', library.id); 2158 assert.sameMembers(keys, [itemKey]); 2159 2160 // Collection should not be in sync queue 2161 assert.lengthOf( 2162 await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('collection', library.id), 0 2163 ); 2164 }); 2165 2166 2167 it("should handle new remote item referencing locally missing collection", async function () { 2168 var lastLibraryVersion = 5; 2169 var newLibraryVersion = 6; 2170 var library = Zotero.Libraries.userLibrary; 2171 library.libraryVersion = lastLibraryVersion; 2172 await library.saveTx(); 2173 ({ engine, client, caller } = await setup()); 2174 2175 var collectionKey = 'AAAAAAAA'; 2176 var itemKey = 'BBBBBBBB' 2177 2178 var headers = { 2179 "Last-Modified-Version": newLibraryVersion 2180 }; 2181 setDefaultResponses({ 2182 lastLibraryVersion, 2183 libraryVersion: newLibraryVersion 2184 }); 2185 setResponse({ 2186 method: "GET", 2187 url: `users/1/items?format=versions&since=${lastLibraryVersion}&includeTrashed=1`, 2188 status: 200, 2189 headers, 2190 json: { 2191 [itemKey]: newLibraryVersion 2192 } 2193 }); 2194 setResponse({ 2195 method: "GET", 2196 url: `users/1/items?format=json&itemKey=${itemKey}&includeTrashed=1`, 2197 status: 200, 2198 headers, 2199 json: [ 2200 makeItemJSON({ 2201 key: itemKey, 2202 version: newLibraryVersion, 2203 itemType: "book", 2204 collections: [collectionKey] 2205 }) 2206 ] 2207 }); 2208 2209 await engine._startDownload(); 2210 2211 // Item should be skipped and added to queue 2212 var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', library.id); 2213 assert.sameMembers(keys, [itemKey]); 2214 2215 // Collection should be in queue 2216 assert.sameMembers( 2217 await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('collection', library.id), 2218 [collectionKey] 2219 ); 2220 }); 2221 2222 2223 it("should handle conflict with remote item referencing deleted local collection", async function () { 2224 var lastLibraryVersion = 5; 2225 var newLibraryVersion = 6; 2226 var library = Zotero.Libraries.userLibrary; 2227 library.libraryVersion = lastLibraryVersion; 2228 await library.saveTx(); 2229 ({ engine, client, caller } = await setup()); 2230 2231 // Create local deleted collection and item 2232 var collection = await createDataObject('collection'); 2233 var collectionKey = collection.key; 2234 await collection.eraseTx(); 2235 var item = await createDataObject('item'); 2236 var itemResponseJSON = item.toResponseJSON(); 2237 // Add collection to remote item 2238 itemResponseJSON.data.collections = [collectionKey]; 2239 var itemKey = item.key; 2240 await item.eraseTx(); 2241 2242 var headers = { 2243 "Last-Modified-Version": newLibraryVersion 2244 }; 2245 setDefaultResponses({ 2246 lastLibraryVersion, 2247 libraryVersion: newLibraryVersion 2248 }); 2249 setResponse({ 2250 method: "GET", 2251 url: `users/1/items?format=versions&since=${lastLibraryVersion}&includeTrashed=1`, 2252 status: 200, 2253 headers, 2254 json: { 2255 [itemKey]: newLibraryVersion 2256 } 2257 }); 2258 setResponse({ 2259 method: "GET", 2260 url: `users/1/items?format=json&itemKey=${itemKey}&includeTrashed=1`, 2261 status: 200, 2262 headers, 2263 json: [itemResponseJSON] 2264 }); 2265 2266 await engine._startDownload(); 2267 2268 // Item should be skipped and added to queue, which will allow collection deletion to upload 2269 var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', library.id); 2270 assert.sameMembers(keys, [itemKey]); 2271 }); 2272 2273 2274 it("should handle cancellation of conflict resolution window", function* () { 2275 var library = Zotero.Libraries.userLibrary; 2276 library.libraryVersion = 5; 2277 yield library.saveTx(); 2278 ({ engine, client, caller } = yield setup()); 2279 2280 var item = yield createDataObject('item'); 2281 var itemID = yield item.saveTx(); 2282 var itemKey = item.key; 2283 2284 var headers = { 2285 "Last-Modified-Version": 6 2286 }; 2287 setResponse({ 2288 method: "GET", 2289 url: "users/1/settings?since=5", 2290 status: 200, 2291 headers: headers, 2292 json: {} 2293 }); 2294 setResponse({ 2295 method: "GET", 2296 url: "users/1/collections?format=versions&since=5", 2297 status: 200, 2298 headers: headers, 2299 json: {} 2300 }); 2301 setResponse({ 2302 method: "GET", 2303 url: "users/1/searches?format=versions&since=5", 2304 status: 200, 2305 headers: headers, 2306 json: {} 2307 }); 2308 setResponse({ 2309 method: "GET", 2310 url: "users/1/items/top?format=versions&since=5&includeTrashed=1", 2311 status: 200, 2312 headers: headers, 2313 json: { 2314 AAAAAAAA: 6, 2315 [itemKey]: 6 2316 } 2317 }); 2318 setResponse({ 2319 method: "GET", 2320 url: `users/1/items?format=json&itemKey=AAAAAAAA%2C${itemKey}&includeTrashed=1`, 2321 status: 200, 2322 headers: headers, 2323 json: [ 2324 makeItemJSON({ 2325 key: "AAAAAAAA", 2326 version: 6, 2327 itemType: "book", 2328 title: "B" 2329 }), 2330 makeItemJSON({ 2331 key: itemKey, 2332 version: 6, 2333 itemType: "book", 2334 title: "B" 2335 }) 2336 ] 2337 }); 2338 setResponse({ 2339 method: "GET", 2340 url: "users/1/items?format=versions&since=5&includeTrashed=1", 2341 status: 200, 2342 headers: headers, 2343 json: {} 2344 }); 2345 setResponse({ 2346 method: "GET", 2347 url: "users/1/deleted?since=5", 2348 status: 200, 2349 headers: headers, 2350 json: { 2351 settings: [], 2352 collections: [], 2353 searches: [], 2354 items: [] 2355 } 2356 }); 2357 2358 var crPromise = waitForWindow('chrome://zotero/content/merge.xul', function (dialog) { 2359 var doc = dialog.document; 2360 var wizard = doc.documentElement; 2361 wizard.getButton('cancel').click(); 2362 }) 2363 var e = yield getPromiseError(engine._startDownload()); 2364 yield crPromise 2365 assert.isTrue(e instanceof Zotero.Sync.UserCancelledException); 2366 2367 // Non-conflicted item should be saved 2368 assert.ok(Zotero.Items.getIDFromLibraryAndKey(library.id, "AAAAAAAA")); 2369 2370 // Conflicted item should be skipped and in queue 2371 assert.isFalse(Zotero.Items.exists(itemID)); 2372 var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', library.id); 2373 assert.sameMembers(keys, [itemKey]); 2374 2375 // Library version should not have advanced 2376 assert.equal(library.libraryVersion, 5); 2377 }); 2378 2379 2380 /** 2381 * The CR window for remote deletions is triggered separately, so test separately 2382 */ 2383 it("should handle cancellation of remote deletion conflict resolution window", function* () { 2384 var library = Zotero.Libraries.userLibrary; 2385 library.libraryVersion = 5; 2386 yield library.saveTx(); 2387 ({ engine, client, caller } = yield setup()); 2388 2389 // Create local unsynced items 2390 var item = createUnsavedDataObject('item'); 2391 item.setField('title', 'A'); 2392 item.synced = false; 2393 var itemID1 = yield item.saveTx(); 2394 var itemKey1 = item.key; 2395 2396 item = createUnsavedDataObject('item'); 2397 item.setField('title', 'B'); 2398 item.synced = false; 2399 var itemID2 = yield item.saveTx(); 2400 var itemKey2 = item.key; 2401 2402 var headers = { 2403 "Last-Modified-Version": 6 2404 }; 2405 setResponse({ 2406 method: "GET", 2407 url: "users/1/settings?since=5", 2408 status: 200, 2409 headers, 2410 json: {} 2411 }); 2412 setResponse({ 2413 method: "GET", 2414 url: "users/1/collections?format=versions&since=5", 2415 status: 200, 2416 headers, 2417 json: {} 2418 }); 2419 setResponse({ 2420 method: "GET", 2421 url: "users/1/searches?format=versions&since=5", 2422 status: 200, 2423 headers, 2424 json: {} 2425 }); 2426 setResponse({ 2427 method: "GET", 2428 url: "users/1/items/top?format=versions&since=5&includeTrashed=1", 2429 status: 200, 2430 headers, 2431 json: {} 2432 }); 2433 setResponse({ 2434 method: "GET", 2435 url: "users/1/items?format=versions&since=5&includeTrashed=1", 2436 status: 200, 2437 headers, 2438 json: {} 2439 }); 2440 setResponse({ 2441 method: "GET", 2442 url: "users/1/deleted?since=5", 2443 status: 200, 2444 headers, 2445 json: { 2446 settings: [], 2447 collections: [], 2448 searches: [], 2449 items: [itemKey1, itemKey2] 2450 } 2451 }); 2452 2453 var crPromise = waitForWindow('chrome://zotero/content/merge.xul', function (dialog) { 2454 var doc = dialog.document; 2455 var wizard = doc.documentElement; 2456 wizard.getButton('cancel').click(); 2457 }) 2458 var e = yield getPromiseError(engine._startDownload()); 2459 yield crPromise; 2460 assert.isTrue(e instanceof Zotero.Sync.UserCancelledException); 2461 2462 // Conflicted items should still exists 2463 assert.isTrue(Zotero.Items.exists(itemID1)); 2464 assert.isTrue(Zotero.Items.exists(itemID2)); 2465 2466 // Library version should not have advanced 2467 assert.equal(library.libraryVersion, 5); 2468 }); 2469 2470 it("should restart if remote library version changes", function* () { 2471 var library = Zotero.Libraries.userLibrary; 2472 library.libraryVersion = 5; 2473 yield library.saveTx(); 2474 ({ engine, client, caller } = yield setup()); 2475 2476 var lastLibraryVersion = 5; 2477 var calls = 0; 2478 var t; 2479 server.respond(function (req) { 2480 if (req.url.startsWith(baseURL + "users/1/settings")) { 2481 calls++; 2482 if (calls == 2) { 2483 assert.isAbove(new Date() - t, 50); 2484 } 2485 t = new Date(); 2486 req.respond( 2487 200, 2488 { 2489 "Last-Modified-Version": ++lastLibraryVersion 2490 }, 2491 JSON.stringify({}) 2492 ); 2493 return; 2494 } 2495 else if (req.url.startsWith(baseURL + "users/1/searches")) { 2496 if (calls == 1) { 2497 t = new Date(); 2498 req.respond( 2499 200, 2500 { 2501 // On the first pass, return a later library version to simulate data 2502 // being updated by a concurrent upload 2503 "Last-Modified-Version": lastLibraryVersion + 1 2504 }, 2505 JSON.stringify([]) 2506 ); 2507 return; 2508 } 2509 } 2510 else if (req.url.startsWith(baseURL + "users/1/items")) { 2511 // Since /searches is called before /items and it should cause a reset, 2512 // /items shouldn't be called until the second pass 2513 if (calls < 1) { 2514 throw new Error("/users/1/items called in first pass"); 2515 } 2516 } 2517 2518 t = new Date(); 2519 req.respond( 2520 200, 2521 { 2522 "Last-Modified-Version": lastLibraryVersion 2523 }, 2524 JSON.stringify([]) 2525 ); 2526 }); 2527 2528 Zotero.Sync.Data.conflictDelayIntervals = [50, 70000]; 2529 yield engine._startDownload(); 2530 2531 assert.equal(calls, 2); 2532 assert.equal(library.libraryVersion, lastLibraryVersion); 2533 }); 2534 }); 2535 2536 2537 describe("#_downloadUpdatedObjects()", function () { 2538 it("should include objects in sync queue", function* () { 2539 ({ engine, client, caller } = yield setup()); 2540 2541 var libraryID = Zotero.Libraries.userLibraryID; 2542 var objectType = 'collection'; 2543 var objectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType); 2544 yield Zotero.Sync.Data.Local.addObjectsToSyncQueue( 2545 objectType, libraryID, ["BBBBBBBB", "CCCCCCCC"] 2546 ); 2547 yield Zotero.DB.queryAsync( 2548 "UPDATE syncQueue SET lastCheck=lastCheck-3600 " 2549 + "WHERE syncObjectTypeID=? AND libraryID=? AND key IN (?, ?)", 2550 [objectTypeID, libraryID, 'BBBBBBBB', 'CCCCCCCC'] 2551 ); 2552 2553 var headers = { 2554 "Last-Modified-Version": 5 2555 }; 2556 setResponse({ 2557 method: "GET", 2558 url: "users/1/collections?format=versions&since=1", 2559 status: 200, 2560 headers, 2561 json: { 2562 AAAAAAAA: 5, 2563 BBBBBBBB: 5 2564 } 2565 }); 2566 2567 var stub = sinon.stub(engine, "_downloadObjects"); 2568 2569 yield engine._downloadUpdatedObjects(objectType, 1, 5); 2570 2571 assert.ok(stub.calledWith("collection", ["AAAAAAAA", "BBBBBBBB", "CCCCCCCC"])); 2572 stub.restore(); 2573 }); 2574 }); 2575 2576 2577 describe("#_downloadObjects()", function () { 2578 it("should remove object from sync queue if missing from response", function* () { 2579 ({ engine, client, caller } = yield setup({ 2580 stopOnError: false 2581 })); 2582 var libraryID = Zotero.Libraries.userLibraryID; 2583 var objectType = 'collection'; 2584 var objectTypeID = Zotero.Sync.Data.Utilities.getSyncObjectTypeID(objectType); 2585 yield Zotero.Sync.Data.Local.addObjectsToSyncQueue( 2586 objectType, libraryID, ["BBBBBBBB", "CCCCCCCC"] 2587 ); 2588 2589 var headers = { 2590 "Last-Modified-Version": 5 2591 }; 2592 setResponse({ 2593 method: "GET", 2594 url: "users/1/collections?format=json&collectionKey=AAAAAAAA%2CBBBBBBBB%2CCCCCCCCC", 2595 status: 200, 2596 headers, 2597 json: [ 2598 makeCollectionJSON({ 2599 key: "AAAAAAAA", 2600 version: 5, 2601 name: "A" 2602 }), 2603 makeCollectionJSON({ 2604 key: "BBBBBBBB", 2605 version: 5 2606 // Missing 'name', which causes a save error 2607 }) 2608 ] 2609 }); 2610 yield engine._downloadObjects(objectType, ["AAAAAAAA", "BBBBBBBB", "CCCCCCCC"]); 2611 2612 // Missing object should have been removed, but invalid object should remain 2613 var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue(objectType, libraryID); 2614 assert.sameMembers(keys, ['BBBBBBBB']); 2615 }); 2616 2617 2618 it("should add items that exist remotely in a locally deleted, remotely modified collection back to collection", async function () { 2619 ({ engine, client, caller } = await setup({ 2620 stopOnError: false 2621 })); 2622 var libraryID = Zotero.Libraries.userLibraryID; 2623 2624 var collection = await createDataObject('collection'); 2625 var collectionKey = collection.key; 2626 await collection.eraseTx(); 2627 var item1 = await createDataObject('item'); 2628 var item2 = await createDataObject('item', { deleted: true }); 2629 2630 var headers = { 2631 "Last-Modified-Version": 5 2632 }; 2633 setResponse({ 2634 method: "GET", 2635 url: `users/1/collections?format=json&collectionKey=${collectionKey}`, 2636 status: 200, 2637 headers, 2638 json: [ 2639 makeCollectionJSON({ 2640 key: collectionKey, 2641 version: 5, 2642 name: "A" 2643 }) 2644 ] 2645 }); 2646 setResponse({ 2647 method: "GET", 2648 url: `users/1/collections/${collectionKey}/items/top?format=keys`, 2649 status: 200, 2650 headers, 2651 text: item1.key + "\n" + item2.key + "\n" 2652 }); 2653 await engine._downloadObjects('collection', [collectionKey]); 2654 2655 var collection = Zotero.Collections.getByLibraryAndKey(libraryID, collectionKey); 2656 assert.sameMembers(collection.getChildItems(true), [item1.id, item2.id]); 2657 // Item should be removed from trash 2658 assert.isFalse(item2.deleted); 2659 }); 2660 2661 2662 it("should add locally deleted items that exist remotely in a locally deleted, remotely modified collection to sync queue and remove from delete log", async function () { 2663 ({ engine, client, caller } = await setup({ 2664 stopOnError: false 2665 })); 2666 var libraryID = Zotero.Libraries.userLibraryID; 2667 2668 var collection = await createDataObject('collection'); 2669 var collectionKey = collection.key; 2670 await collection.eraseTx(); 2671 var item = await createDataObject('item'); 2672 await item.eraseTx(); 2673 2674 var headers = { 2675 "Last-Modified-Version": 5 2676 }; 2677 setResponse({ 2678 method: "GET", 2679 url: `users/1/collections?format=json&collectionKey=${collectionKey}`, 2680 status: 200, 2681 headers, 2682 json: [ 2683 makeCollectionJSON({ 2684 key: collectionKey, 2685 version: 5, 2686 name: "A" 2687 }) 2688 ] 2689 }); 2690 setResponse({ 2691 method: "GET", 2692 url: `users/1/collections/${collectionKey}/items/top?format=keys`, 2693 status: 200, 2694 headers, 2695 text: item.key + "\n" 2696 }); 2697 await engine._downloadObjects('collection', [collectionKey]); 2698 2699 var collection = Zotero.Collections.getByLibraryAndKey(libraryID, collectionKey); 2700 2701 assert.sameMembers( 2702 await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID), 2703 [item.key] 2704 ); 2705 assert.isFalse( 2706 await Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, item.key) 2707 ); 2708 }); 2709 }); 2710 2711 2712 describe("#_startUpload()", function () { 2713 it("shouldn't upload unsynced objects if present in sync queue", function* () { 2714 ({ engine, client, caller } = yield setup()); 2715 var libraryID = Zotero.Libraries.userLibraryID; 2716 var objectType = 'item'; 2717 var obj = yield createDataObject(objectType); 2718 yield Zotero.Sync.Data.Local.addObjectsToSyncQueue(objectType, libraryID, [obj.key]); 2719 var result = yield engine._startUpload(); 2720 assert.equal(result, engine.UPLOAD_RESULT_NOTHING_TO_UPLOAD); 2721 }); 2722 2723 2724 it("should prompt to reset library on 403 write response and reset on accept", function* () { 2725 var group = yield createGroup({ 2726 libraryVersion: 5 2727 }); 2728 var libraryID = group.libraryID; 2729 ({ engine, client, caller } = yield setup({ libraryID })); 2730 2731 var item = createUnsavedDataObject('item'); 2732 item.libraryID = libraryID; 2733 item.setField('title', 'A'); 2734 item.synced = false; 2735 var itemID = yield item.saveTx(); 2736 2737 var headers = { 2738 "Last-Modified-Version": 5 2739 }; 2740 setResponse({ 2741 method: "POST", 2742 url: `groups/${group.id}/items`, 2743 status: 403, 2744 headers, 2745 text: "" 2746 }) 2747 2748 var promise = waitForDialog(function (dialog) { 2749 var text = dialog.document.documentElement.textContent; 2750 assert.include(text, group.name); 2751 }); 2752 2753 var result = yield engine._startUpload(); 2754 assert.equal(result, engine.UPLOAD_RESULT_RESTART); 2755 2756 assert.isFalse(Zotero.Items.exists(itemID)); 2757 2758 // Library version should have been reset to trigger full sync 2759 assert.equal(group.libraryVersion, -1); 2760 }); 2761 2762 2763 it("should prompt to reset library on 403 write response and skip on cancel", function* () { 2764 var group = yield createGroup({ 2765 libraryVersion: 5 2766 }); 2767 var libraryID = group.libraryID; 2768 ({ engine, client, caller } = yield setup({ libraryID })); 2769 2770 var item = createUnsavedDataObject('item'); 2771 item.libraryID = libraryID; 2772 item.setField('title', 'A'); 2773 item.synced = false; 2774 var itemID = yield item.saveTx(); 2775 2776 var headers = { 2777 "Last-Modified-Version": 5 2778 }; 2779 setResponse({ 2780 method: "POST", 2781 url: `groups/${group.id}/items`, 2782 status: 403, 2783 headers, 2784 text: "" 2785 }) 2786 2787 var promise = waitForDialog(function (dialog) { 2788 var text = dialog.document.documentElement.textContent; 2789 assert.include(text, group.name); 2790 }, "cancel"); 2791 2792 var result = yield engine._startUpload(); 2793 assert.equal(result, engine.UPLOAD_RESULT_CANCEL); 2794 2795 assert.isTrue(Zotero.Items.exists(itemID)); 2796 2797 // Library version shouldn't have changed 2798 assert.equal(group.libraryVersion, 5); 2799 }); 2800 2801 2802 it("should trigger full sync on object conflict", function* () { 2803 ({ engine, client, caller } = yield setup()); 2804 2805 var library = Zotero.Libraries.userLibrary; 2806 var libraryID = library.id; 2807 var lastLibraryVersion = 5; 2808 library.libraryVersion = lastLibraryVersion; 2809 yield library.saveTx(); 2810 2811 var item = createUnsavedDataObject('item'); 2812 item.version = lastLibraryVersion; 2813 yield item.saveTx(); 2814 2815 setResponse({ 2816 method: "POST", 2817 url: "users/1/items", 2818 status: 200, 2819 headers: { 2820 "Last-Modified-Version": lastLibraryVersion 2821 }, 2822 json: { 2823 successful: {}, 2824 unchanged: {}, 2825 failed: { 2826 "0": { 2827 "code": 412, 2828 "message": `Item doesn't exist (expected version ${lastLibraryVersion}; ` 2829 + "use 0 instead)" 2830 } 2831 } 2832 } 2833 }); 2834 2835 var result = yield engine._startUpload(); 2836 assert.equal(result, engine.UPLOAD_RESULT_OBJECT_CONFLICT); 2837 }); 2838 2839 2840 // Note: This shouldn't be necessary, since collections are sorted top-down before uploading 2841 it("should mark local collection as unsynced if it doesn't exist when uploading collection", function* () { 2842 ({ engine, client, caller } = yield setup()); 2843 2844 var library = Zotero.Libraries.userLibrary; 2845 var libraryID = library.id; 2846 var lastLibraryVersion = 5; 2847 library.libraryVersion = lastLibraryVersion; 2848 yield library.saveTx(); 2849 2850 var collection1 = createUnsavedDataObject('collection'); 2851 // Set the collection as synced (though this shouldn't happen) 2852 collection1.synced = true; 2853 yield collection1.saveTx(); 2854 var collection2 = yield createDataObject('collection', { collections: [collection1.id] }); 2855 2856 var called = 0; 2857 server.respond(function (req) { 2858 let requestJSON = JSON.parse(req.requestBody); 2859 2860 if (called == 0) { 2861 assert.lengthOf(requestJSON, 1); 2862 assert.equal(requestJSON[0].key, collection2.key); 2863 req.respond( 2864 200, 2865 { 2866 "Last-Modified-Version": lastLibraryVersion 2867 }, 2868 JSON.stringify({ 2869 successful: {}, 2870 unchanged: {}, 2871 failed: { 2872 0: { 2873 code: 409, 2874 message: `Parent collection ${collection1.key} doesn't exist`, 2875 data: { 2876 collection: collection1.key 2877 } 2878 } 2879 } 2880 }) 2881 ); 2882 } 2883 called++; 2884 }); 2885 2886 var e = yield getPromiseError(engine._startUpload()); 2887 assert.ok(e); 2888 assert.isFalse(collection1.synced); 2889 }); 2890 2891 2892 it("should mark local collection as unsynced if it doesn't exist when uploading item", function* () { 2893 ({ engine, client, caller } = yield setup()); 2894 2895 var library = Zotero.Libraries.userLibrary; 2896 var libraryID = library.id; 2897 var lastLibraryVersion = 5; 2898 library.libraryVersion = lastLibraryVersion; 2899 yield library.saveTx(); 2900 2901 var collection = createUnsavedDataObject('collection'); 2902 // Set the collection as synced (though this shouldn't happen) 2903 collection.synced = true; 2904 yield collection.saveTx(); 2905 var item = yield createDataObject('item', { collections: [collection.id] }); 2906 2907 var called = 0; 2908 server.respond(function (req) { 2909 let requestJSON = JSON.parse(req.requestBody); 2910 2911 if (called == 0) { 2912 assert.lengthOf(requestJSON, 1); 2913 assert.equal(requestJSON[0].key, item.key); 2914 req.respond( 2915 200, 2916 { 2917 "Last-Modified-Version": lastLibraryVersion 2918 }, 2919 JSON.stringify({ 2920 successful: {}, 2921 unchanged: {}, 2922 failed: { 2923 0: { 2924 code: 409, 2925 message: `Collection ${collection.key} doesn't exist`, 2926 data: { 2927 collection: collection.key 2928 } 2929 } 2930 } 2931 }) 2932 ); 2933 } 2934 called++; 2935 }); 2936 2937 var e = yield getPromiseError(engine._startUpload()); 2938 assert.ok(e); 2939 assert.isFalse(collection.synced); 2940 }); 2941 2942 2943 it("should mark local parent item as unsynced if it doesn't exist when uploading child", function* () { 2944 ({ engine, client, caller } = yield setup()); 2945 2946 var library = Zotero.Libraries.userLibrary; 2947 var libraryID = library.id; 2948 var lastLibraryVersion = 5; 2949 library.libraryVersion = lastLibraryVersion; 2950 yield library.saveTx(); 2951 2952 var item = createUnsavedDataObject('item'); 2953 // Set the parent item as synced (though this shouldn't happen) 2954 item.synced = true; 2955 yield item.saveTx(); 2956 var note = yield createDataObject('item', { itemType: 'note', parentID: item.id }); 2957 2958 var called = 0; 2959 server.respond(function (req) { 2960 let requestJSON = JSON.parse(req.requestBody); 2961 2962 if (called == 0) { 2963 assert.lengthOf(requestJSON, 1); 2964 assert.equal(requestJSON[0].key, note.key); 2965 req.respond( 2966 200, 2967 { 2968 "Last-Modified-Version": lastLibraryVersion 2969 }, 2970 JSON.stringify({ 2971 successful: {}, 2972 unchanged: {}, 2973 failed: { 2974 0: { 2975 code: 409, 2976 message: `Parent item ${item.key} doesn't exist`, 2977 data: { 2978 parentItem: item.key 2979 } 2980 } 2981 } 2982 }) 2983 ); 2984 } 2985 else if (called == 1) { 2986 assert.lengthOf(requestJSON, 2); 2987 assert.sameMembers(requestJSON.map(o => o.key), [item.key, note.key]); 2988 req.respond( 2989 200, 2990 { 2991 "Last-Modified-Version": ++lastLibraryVersion 2992 }, 2993 JSON.stringify({ 2994 successful: { 2995 0: item.toResponseJSON(), 2996 1: note.toResponseJSON() 2997 }, 2998 unchanged: {}, 2999 failed: {} 3000 }) 3001 ); 3002 } 3003 called++; 3004 }); 3005 3006 var result = yield engine._startUpload(); 3007 assert.equal(result, engine.UPLOAD_RESULT_SUCCESS); 3008 assert.equal(called, 2); 3009 }); 3010 3011 3012 it("shouldn't retry failed child item if parent item failed during this sync", async function () { 3013 ({ engine, client, caller } = await setup({ 3014 stopOnError: false 3015 })); 3016 3017 var library = Zotero.Libraries.userLibrary; 3018 var libraryID = library.id; 3019 var libraryVersion = 5; 3020 library.libraryVersion = libraryVersion; 3021 await library.saveTx(); 3022 3023 var item1 = await createDataObject('item'); 3024 var item1JSON = item1.toResponseJSON(); 3025 var tag = "A".repeat(300); 3026 var item2 = await createDataObject('item', { tags: [{ tag }] }); 3027 var note = await createDataObject('item', { itemType: 'note', parentID: item2.id }); 3028 3029 var called = 0; 3030 server.respond(function (req) { 3031 let requestJSON = JSON.parse(req.requestBody); 3032 if (called == 0) { 3033 assert.lengthOf(requestJSON, 3); 3034 assert.equal(requestJSON[0].key, item1.key); 3035 assert.equal(requestJSON[1].key, item2.key); 3036 assert.equal(requestJSON[2].key, note.key); 3037 req.respond( 3038 200, 3039 { 3040 "Last-Modified-Version": ++libraryVersion 3041 }, 3042 JSON.stringify({ 3043 successful: { 3044 "0": Object.assign(item1JSON, { version: libraryVersion }) 3045 }, 3046 unchanged: {}, 3047 failed: { 3048 1: { 3049 code: 413, 3050 message: `Tag '${"A".repeat(50)}…' too long`, 3051 data: { 3052 tag 3053 } 3054 }, 3055 // Normally this would retry, but that might result in a 409 3056 // without the parent 3057 2: { 3058 code: 500, 3059 message: `An error occurred` 3060 } 3061 } 3062 }) 3063 ); 3064 } 3065 called++; 3066 }); 3067 3068 var spy = sinon.spy(engine, "onError"); 3069 var result = await engine._startUpload(); 3070 assert.equal(result, engine.UPLOAD_RESULT_SUCCESS); 3071 assert.equal(called, 1); 3072 assert.equal(spy.callCount, 2); 3073 }); 3074 3075 3076 it("should show file-write-access-lost dialog on 403 for attachment upload in group", async function () { 3077 var group = await createGroup({ 3078 filesEditable: true 3079 }); 3080 var libraryID = group.libraryID; 3081 var libraryVersion = 5; 3082 group.libraryVersion = libraryVersion; 3083 await group.saveTx(); 3084 3085 ({ engine, client, caller } = await setup({ 3086 libraryID, 3087 stopOnError: false 3088 })); 3089 3090 var item1 = await createDataObject('item', { libraryID }); 3091 var item2 = await importFileAttachment( 3092 'test.png', 3093 { 3094 libraryID, 3095 parentID: item1.id, 3096 version: 5 3097 } 3098 ); 3099 3100 var called = 0; 3101 server.respond(function (req) { 3102 let requestJSON = JSON.parse(req.requestBody); 3103 if (called == 0) { 3104 req.respond( 3105 200, 3106 { 3107 "Last-Modified-Version": ++libraryVersion 3108 }, 3109 JSON.stringify({ 3110 successful: { 3111 0: item1.toResponseJSON({ version: libraryVersion }) 3112 }, 3113 unchanged: {}, 3114 failed: { 3115 1: { 3116 code: 403, 3117 message: "File editing access denied" 3118 } 3119 } 3120 }) 3121 ); 3122 } 3123 else if (called == 1 && req.url == baseURL + `groups/${group.id}`) { 3124 req.respond( 3125 200, 3126 { 3127 "Last-Modified-Version": group.libraryVersion 3128 }, 3129 JSON.stringify({ 3130 id: group.id, 3131 version: group.libraryVersion, 3132 data: { 3133 id: group.id, 3134 version: group.libraryVersion, 3135 name: group.name, 3136 owner: 10, 3137 type: "Private", 3138 description: "", 3139 url: "", 3140 libraryEditing: "members", 3141 libraryReading: "all", 3142 fileEditing: "admins" 3143 } 3144 }) 3145 ); 3146 } 3147 called++; 3148 }); 3149 3150 var promise = waitForDialog(); 3151 var spy = sinon.spy(engine, "onError"); 3152 var result = await engine._startUpload(); 3153 assert.isTrue(promise.isResolved()); 3154 assert.equal(result, engine.UPLOAD_RESULT_RESTART); 3155 assert.equal(called, 2); 3156 assert.equal(spy.callCount, 0); 3157 3158 assert.isFalse(group.filesEditable); 3159 3160 assert.ok(Zotero.Items.get(item1.id)); 3161 assert.isFalse(Zotero.Items.get(item2.id)); 3162 }); 3163 }); 3164 3165 3166 describe("Conflict Resolution", function () { 3167 beforeEach(function* () { 3168 yield Zotero.DB.queryAsync("DELETE FROM syncCache"); 3169 }) 3170 3171 after(function* () { 3172 yield Zotero.DB.queryAsync("DELETE FROM syncCache"); 3173 }) 3174 3175 it("should show conflict resolution window on item conflicts", async function () { 3176 var libraryID = Zotero.Libraries.userLibraryID; 3177 ({ engine, client, caller } = await setup()); 3178 var type = 'item'; 3179 var objects = []; 3180 var values = []; 3181 var dateAdded = Date.now() - 86400000; 3182 var responseJSON = []; 3183 3184 for (let i = 0; i < 2; i++) { 3185 values.push({ 3186 left: {}, 3187 right: {} 3188 }); 3189 3190 // Create local object 3191 let obj = objects[i] = await createDataObject( 3192 type, 3193 { 3194 version: 10, 3195 dateAdded: Zotero.Date.dateToSQL(new Date(dateAdded), true), 3196 // Set Date Modified values one minute apart to enforce order 3197 dateModified: Zotero.Date.dateToSQL( 3198 new Date(dateAdded + (i * 60000)), true 3199 ) 3200 } 3201 ); 3202 let jsonData = obj.toJSON(); 3203 jsonData.key = obj.key; 3204 jsonData.version = 10; 3205 let json = { 3206 key: obj.key, 3207 version: jsonData.version, 3208 data: jsonData 3209 }; 3210 // Save original version in cache 3211 await Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]); 3212 3213 // Create updated JSON for download 3214 values[i].right.title = jsonData.title = Zotero.Utilities.randomString(); 3215 values[i].right.version = json.version = jsonData.version = 15; 3216 responseJSON.push(json); 3217 3218 // Modify object locally 3219 await modifyDataObject(obj, undefined, { skipDateModifiedUpdate: true }); 3220 values[i].left.title = obj.getField('title'); 3221 values[i].left.version = obj.getField('version'); 3222 } 3223 3224 setResponse({ 3225 method: "GET", 3226 url: `users/1/items?format=json&itemKey=${objects.map(o => o.key).join('%2C')}` 3227 + `&includeTrashed=1`, 3228 status: 200, 3229 headers: { 3230 "Last-Modified-Version": 15 3231 }, 3232 json: responseJSON 3233 }); 3234 3235 var crPromise = waitForWindow('chrome://zotero/content/merge.xul', function (dialog) { 3236 var doc = dialog.document; 3237 var wizard = doc.documentElement; 3238 var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0]; 3239 3240 // 1 (remote) 3241 // Remote version should be selected by default 3242 assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true'); 3243 wizard.getButton('next').click(); 3244 3245 // 2 (local) 3246 assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true'); 3247 // Select local object 3248 mergeGroup.leftpane.click(); 3249 assert.equal(mergeGroup.leftpane.getAttribute('selected'), 'true'); 3250 if (Zotero.isMac) { 3251 assert.isTrue(wizard.getButton('next').hidden); 3252 assert.isFalse(wizard.getButton('finish').hidden); 3253 } 3254 else { 3255 // TODO 3256 } 3257 wizard.getButton('finish').click(); 3258 }) 3259 await engine._downloadObjects('item', objects.map(o => o.key)); 3260 await crPromise; 3261 3262 assert.equal(objects[0].getField('title'), values[0].right.title); 3263 assert.equal(objects[1].getField('title'), values[1].left.title); 3264 assert.equal(objects[0].getField('version'), values[0].right.version); 3265 assert.equal(objects[1].getField('version'), values[1].right.version); 3266 3267 // Cache versions should match remote 3268 for (let i = 0; i < 2; i++) { 3269 let cacheJSON = await Zotero.Sync.Data.Local.getCacheObject( 3270 'item', libraryID, objects[i].key, values[i].right.version 3271 ); 3272 assert.propertyVal(cacheJSON, 'version', values[i].right.version); 3273 assert.nestedPropertyVal(cacheJSON, 'data.title', values[i].right.title); 3274 } 3275 3276 var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID); 3277 assert.lengthOf(keys, 0); 3278 }); 3279 3280 it("should show conflict resolution window on note conflicts", async function () { 3281 var libraryID = Zotero.Libraries.userLibraryID; 3282 ({ engine, client, caller } = await setup()); 3283 var type = 'item'; 3284 var objects = []; 3285 var values = []; 3286 var dateAdded = Date.now() - 86400000; 3287 var responseJSON = []; 3288 3289 for (let i = 0; i < 2; i++) { 3290 values.push({ 3291 left: {}, 3292 right: {} 3293 }); 3294 3295 // Create local object 3296 let obj = objects[i] = new Zotero.Item('note'); 3297 obj.setNote(Zotero.Utilities.randomString()); 3298 obj.version = 10; 3299 obj.dateAdded = Zotero.Date.dateToSQL(new Date(dateAdded), true); 3300 // Set Date Modified values one minute apart to enforce order 3301 obj.dateModified = Zotero.Date.dateToSQL( 3302 new Date(dateAdded + (i * 60000)), true 3303 ); 3304 await obj.saveTx(); 3305 3306 let jsonData = obj.toJSON(); 3307 jsonData.key = obj.key; 3308 jsonData.version = 10; 3309 let json = { 3310 key: obj.key, 3311 version: jsonData.version, 3312 data: jsonData 3313 }; 3314 // Save original version in cache 3315 await Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]); 3316 3317 // Create updated JSON for download 3318 values[i].right.note = jsonData.note = Zotero.Utilities.randomString(); 3319 values[i].right.version = json.version = jsonData.version = 15; 3320 responseJSON.push(json); 3321 3322 // Modify object locally 3323 obj.setNote(Zotero.Utilities.randomString()); 3324 await obj.saveTx({ 3325 skipDateModifiedUpdate: true 3326 }); 3327 values[i].left.note = obj.getNote(); 3328 values[i].left.version = obj.getField('version'); 3329 } 3330 3331 setResponse({ 3332 method: "GET", 3333 url: `users/1/items?format=json&itemKey=${objects.map(o => o.key).join('%2C')}` 3334 + `&includeTrashed=1`, 3335 status: 200, 3336 headers: { 3337 "Last-Modified-Version": 15 3338 }, 3339 json: responseJSON 3340 }); 3341 3342 var crPromise = waitForWindow('chrome://zotero/content/merge.xul', function (dialog) { 3343 var doc = dialog.document; 3344 var wizard = doc.documentElement; 3345 var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0]; 3346 3347 // 1 (remote) 3348 // Remote version should be selected by default 3349 assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true'); 3350 wizard.getButton('next').click(); 3351 3352 // 2 (local) 3353 assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true'); 3354 // Select local object 3355 mergeGroup.leftpane.click(); 3356 assert.equal(mergeGroup.leftpane.getAttribute('selected'), 'true'); 3357 if (Zotero.isMac) { 3358 assert.isTrue(wizard.getButton('next').hidden); 3359 assert.isFalse(wizard.getButton('finish').hidden); 3360 } 3361 else { 3362 // TODO 3363 } 3364 wizard.getButton('finish').click(); 3365 }); 3366 await engine._downloadObjects('item', objects.map(o => o.key)); 3367 await crPromise; 3368 3369 assert.equal(objects[0].getNote(), values[0].right.note); 3370 assert.equal(objects[1].getNote(), values[1].left.note); 3371 assert.equal(objects[0].version, values[0].right.version); 3372 assert.equal(objects[1].version, values[1].right.version); 3373 assert.isTrue(objects[0].synced); 3374 assert.isFalse(objects[1].synced); 3375 3376 // Cache versions should match remote 3377 for (let i = 0; i < 2; i++) { 3378 let cacheJSON = await Zotero.Sync.Data.Local.getCacheObject( 3379 'item', libraryID, objects[i].key, values[i].right.version 3380 ); 3381 assert.propertyVal(cacheJSON, 'version', values[i].right.version); 3382 assert.nestedPropertyVal(cacheJSON, 'data.note', values[i].right.note); 3383 } 3384 3385 var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID); 3386 assert.lengthOf(keys, 0); 3387 }); 3388 3389 it("should resolve all remaining conflicts with local version", async function () { 3390 var libraryID = Zotero.Libraries.userLibraryID; 3391 ({ engine, client, caller } = await setup()); 3392 var collectionA = await createDataObject('collection'); 3393 var collectionB = await createDataObject('collection'); 3394 var objects = []; 3395 var values = []; 3396 var responseJSON = []; 3397 var dateAdded = Date.now() - 86400000; 3398 for (let i = 0; i < 3; i++) { 3399 values.push({ 3400 left: {}, 3401 right: {} 3402 }); 3403 3404 // Create object in cache 3405 let obj = objects[i] = await createDataObject( 3406 'item', 3407 { 3408 version: 10, 3409 dateAdded: Zotero.Date.dateToSQL(new Date(dateAdded), true), 3410 // Set Date Modified values one minute apart to enforce order 3411 dateModified: Zotero.Date.dateToSQL( 3412 new Date(dateAdded + (i * 60000)), true 3413 ) 3414 } 3415 ); 3416 let jsonData = obj.toJSON(); 3417 jsonData.key = obj.key; 3418 jsonData.version = 10; 3419 let json = { 3420 key: obj.key, 3421 version: jsonData.version, 3422 data: jsonData 3423 }; 3424 // Save original version in cache 3425 await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json]); 3426 3427 // Create remote version 3428 values[i].right.title = jsonData.title = Zotero.Utilities.randomString(); 3429 values[i].right.publisher = jsonData.publisher = Zotero.Utilities.randomString(); 3430 values[i].right.collections = jsonData.collections = [collectionB.key]; 3431 values[i].right.version = json.version = jsonData.version = 15; 3432 responseJSON.push(json); 3433 3434 // Modify object locally 3435 obj.setField('title', Zotero.Utilities.randomString()); 3436 obj.setField('extra', Zotero.Utilities.randomString()); 3437 obj.setCollections([collectionA.key]); 3438 await obj.saveTx({ 3439 skipDateModifiedUpdate: true 3440 }); 3441 values[i].left.title = obj.getField('title'); 3442 values[i].left.extra = obj.getField('extra'); 3443 values[i].left.collections = [collectionA.key]; 3444 values[i].left.version = obj.version; 3445 } 3446 3447 setResponse({ 3448 method: "GET", 3449 url: `users/1/items?format=json&itemKey=${objects.map(o => o.key).join('%2C')}` 3450 + `&includeTrashed=1`, 3451 status: 200, 3452 headers: { 3453 "Last-Modified-Version": 15 3454 }, 3455 json: responseJSON 3456 }); 3457 3458 var crPromise = waitForWindow('chrome://zotero/content/merge.xul', function (dialog) { 3459 var doc = dialog.document; 3460 var wizard = doc.documentElement; 3461 var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0]; 3462 var resolveAll = doc.getElementById('resolve-all'); 3463 3464 // 1 (remote) 3465 // Remote version should be selected by default 3466 assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true'); 3467 assert.equal( 3468 resolveAll.label, 3469 Zotero.getString('sync.conflict.resolveAllRemoteFields') 3470 ); 3471 wizard.getButton('next').click(); 3472 3473 // 2 (local and Resolve All checkbox) 3474 assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true'); 3475 mergeGroup.leftpane.click(); 3476 assert.equal( 3477 resolveAll.label, 3478 Zotero.getString('sync.conflict.resolveAllLocalFields') 3479 ); 3480 resolveAll.click(); 3481 3482 if (Zotero.isMac) { 3483 assert.isTrue(wizard.getButton('next').hidden); 3484 assert.isFalse(wizard.getButton('finish').hidden); 3485 } 3486 else { 3487 // TODO 3488 } 3489 wizard.getButton('finish').click(); 3490 }) 3491 await engine._downloadObjects('item', objects.map(o => o.key)); 3492 await crPromise; 3493 3494 // First object should match remote 3495 assert.equal(objects[0].getField('title'), values[0].right.title); 3496 assert.equal(objects[0].version, values[0].right.version); 3497 assert.isTrue(objects[0].synced); 3498 3499 // Remaining objects should be marked as unsynced, with remote versions but original values, 3500 // as if they were saved and then modified 3501 assert.isFalse(objects[1].synced); 3502 assert.equal(objects[1].version, values[1].right.version); 3503 assert.equal(objects[1].getField('title'), values[1].left.title); 3504 assert.isFalse(objects[2].synced); 3505 assert.equal(objects[2].getField('title'), values[2].left.title); 3506 assert.equal(objects[2].version, values[2].right.version); 3507 3508 // All cache versions should match remote 3509 for (let i = 0; i < 3; i++) { 3510 let cacheJSON = await Zotero.Sync.Data.Local.getCacheObject( 3511 'item', libraryID, objects[i].key, values[i].right.version 3512 ); 3513 assert.propertyVal(cacheJSON, 'version', values[i].right.version); 3514 assert.nestedPropertyVal(cacheJSON, 'data.title', values[i].right.title); 3515 } 3516 3517 var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID); 3518 assert.lengthOf(keys, 0); 3519 }); 3520 3521 3522 it("should resolve all remaining conflicts with remote version", async function () { 3523 var libraryID = Zotero.Libraries.userLibraryID; 3524 ({ engine, client, caller } = await setup()); 3525 var objects = []; 3526 var values = []; 3527 var responseJSON = []; 3528 var dateAdded = Date.now() - 86400000; 3529 for (let i = 0; i < 3; i++) { 3530 values.push({ 3531 left: {}, 3532 right: {} 3533 }); 3534 3535 // Create object in cache 3536 let obj = objects[i] = await createDataObject( 3537 'item', 3538 { 3539 version: 10, 3540 dateAdded: Zotero.Date.dateToSQL(new Date(dateAdded), true), 3541 // Set Date Modified values one minute apart to enforce order 3542 dateModified: Zotero.Date.dateToSQL( 3543 new Date(dateAdded + (i * 60000)), true 3544 ) 3545 } 3546 ); 3547 let jsonData = obj.toJSON(); 3548 jsonData.key = obj.key; 3549 jsonData.version = 10; 3550 let json = { 3551 key: obj.key, 3552 version: jsonData.version, 3553 data: jsonData 3554 }; 3555 // Save original version in cache 3556 await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json]); 3557 3558 // Create remote version 3559 values[i].right.title = jsonData.title = Zotero.Utilities.randomString(); 3560 values[i].right.version = json.version = jsonData.version = 15; 3561 responseJSON.push(json); 3562 3563 // Modify object locally 3564 await modifyDataObject(obj, undefined, { skipDateModifiedUpdate: true }); 3565 values[i].left.title = obj.getField('title'); 3566 values[i].left.version = obj.version; 3567 } 3568 3569 setResponse({ 3570 method: "GET", 3571 url: `users/1/items?format=json&itemKey=${objects.map(o => o.key).join('%2C')}` 3572 + `&includeTrashed=1`, 3573 status: 200, 3574 headers: { 3575 "Last-Modified-Version": 15 3576 }, 3577 json: responseJSON 3578 }); 3579 3580 var crPromise = waitForWindow('chrome://zotero/content/merge.xul', function (dialog) { 3581 var doc = dialog.document; 3582 var wizard = doc.documentElement; 3583 var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0]; 3584 var resolveAll = doc.getElementById('resolve-all'); 3585 3586 // 1 (remote) 3587 // Remote version should be selected by default 3588 assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true'); 3589 assert.equal( 3590 resolveAll.label, 3591 Zotero.getString('sync.conflict.resolveAllRemoteFields') 3592 ); 3593 wizard.getButton('next').click(); 3594 3595 // 2 click Resolve All checkbox 3596 assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true'); 3597 assert.equal( 3598 resolveAll.label, 3599 Zotero.getString('sync.conflict.resolveAllRemoteFields') 3600 ); 3601 resolveAll.click(); 3602 3603 if (Zotero.isMac) { 3604 assert.isTrue(wizard.getButton('next').hidden); 3605 assert.isFalse(wizard.getButton('finish').hidden); 3606 } 3607 else { 3608 // TODO 3609 } 3610 wizard.getButton('finish').click(); 3611 }) 3612 await engine._downloadObjects('item', objects.map(o => o.key)); 3613 await crPromise; 3614 3615 assert.equal(objects[0].getField('title'), values[0].right.title); 3616 assert.equal(objects[0].version, values[0].right.version); 3617 assert.isTrue(objects[0].synced); 3618 assert.equal(objects[1].getField('title'), values[1].right.title); 3619 assert.equal(objects[1].version, values[1].right.version); 3620 assert.isTrue(objects[1].synced); 3621 assert.equal(objects[2].getField('title'), values[2].right.title); 3622 assert.equal(objects[2].version, values[2].right.version); 3623 assert.isTrue(objects[2].synced); 3624 3625 var keys = await Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID); 3626 assert.lengthOf(keys, 0); 3627 }); 3628 3629 3630 // Note: Conflicts with remote deletions are handled in _startDownload() 3631 it("should handle local item deletion, keeping deletion", function* () { 3632 var libraryID = Zotero.Libraries.userLibraryID; 3633 ({ engine, client, caller } = yield setup()); 3634 var type = 'item'; 3635 var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type); 3636 var responseJSON = []; 3637 3638 // Create object, generate JSON, and delete 3639 var obj = yield createDataObject(type, { version: 10 }); 3640 var jsonData = obj.toJSON(); 3641 var key = jsonData.key = obj.key; 3642 jsonData.version = 10; 3643 let json = { 3644 key: obj.key, 3645 version: jsonData.version, 3646 data: jsonData 3647 }; 3648 // Delete object locally 3649 yield obj.eraseTx(); 3650 3651 json.version = jsonData.version = 15; 3652 jsonData.title = Zotero.Utilities.randomString(); 3653 responseJSON.push(json); 3654 3655 setResponse({ 3656 method: "GET", 3657 url: `users/1/items?format=json&itemKey=${obj.key}&includeTrashed=1`, 3658 status: 200, 3659 headers: { 3660 "Last-Modified-Version": 15 3661 }, 3662 json: responseJSON 3663 }); 3664 3665 var crPromise = waitForWindow('chrome://zotero/content/merge.xul', function (dialog) { 3666 var doc = dialog.document; 3667 var wizard = doc.documentElement; 3668 var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0]; 3669 3670 // Remote version should be selected by default 3671 assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true'); 3672 assert.ok(mergeGroup.leftpane.pane.onclick); 3673 // Select local deleted version 3674 mergeGroup.leftpane.pane.click(); 3675 wizard.getButton('finish').click(); 3676 }) 3677 yield engine._downloadObjects('item', [obj.key]); 3678 yield crPromise; 3679 3680 obj = objectsClass.getByLibraryAndKey(libraryID, key); 3681 assert.isFalse(obj); 3682 3683 var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID); 3684 assert.lengthOf(keys, 0); 3685 }) 3686 3687 it("should handle local child note deletion, keeping deletion", function* () { 3688 var libraryID = Zotero.Libraries.userLibraryID; 3689 ({ engine, client, caller } = yield setup()); 3690 var responseJSON = []; 3691 3692 var parent = yield createDataObject('item'); 3693 3694 // Create object, generate JSON, and delete 3695 var obj = new Zotero.Item('note'); 3696 obj.parentItemID = parent.id; 3697 obj.setNote(Zotero.Utilities.randomString()); 3698 obj.version = 10; 3699 yield obj.saveTx(); 3700 var jsonData = obj.toJSON(); 3701 var key = jsonData.key = obj.key; 3702 jsonData.version = 10; 3703 let json = { 3704 key: obj.key, 3705 version: jsonData.version, 3706 data: jsonData 3707 }; 3708 // Delete object locally 3709 yield obj.eraseTx(); 3710 3711 json.version = jsonData.version = 15; 3712 jsonData.note = Zotero.Utilities.randomString(); 3713 responseJSON.push(json); 3714 3715 setResponse({ 3716 method: "GET", 3717 url: `users/1/items?format=json&itemKey=${obj.key}&includeTrashed=1`, 3718 status: 200, 3719 headers: { 3720 "Last-Modified-Version": 15 3721 }, 3722 json: responseJSON 3723 }); 3724 3725 var crPromise = waitForWindow('chrome://zotero/content/merge.xul', function (dialog) { 3726 var doc = dialog.document; 3727 var wizard = doc.documentElement; 3728 var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0]; 3729 3730 // Remote version should be selected by default 3731 assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true'); 3732 assert.ok(mergeGroup.leftpane.pane.onclick); 3733 // Select local deleted version 3734 mergeGroup.leftpane.pane.click(); 3735 wizard.getButton('finish').click(); 3736 }); 3737 yield engine._downloadObjects('item', [obj.key]); 3738 yield crPromise; 3739 3740 obj = Zotero.Items.getByLibraryAndKey(libraryID, key); 3741 assert.isFalse(obj); 3742 3743 var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID); 3744 assert.lengthOf(keys, 0); 3745 }); 3746 3747 it("should restore locally deleted item", function* () { 3748 var libraryID = Zotero.Libraries.userLibraryID; 3749 ({ engine, client, caller } = yield setup()); 3750 var type = 'item'; 3751 var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type); 3752 var responseJSON = []; 3753 3754 // Create object, generate JSON, and delete 3755 var obj = yield createDataObject(type, { version: 10 }); 3756 var jsonData = obj.toJSON(); 3757 var key = jsonData.key = obj.key; 3758 jsonData.version = 10; 3759 let json = { 3760 key: obj.key, 3761 version: jsonData.version, 3762 data: jsonData 3763 }; 3764 yield obj.eraseTx(); 3765 3766 json.version = jsonData.version = 15; 3767 jsonData.title = Zotero.Utilities.randomString(); 3768 responseJSON.push(json); 3769 3770 setResponse({ 3771 method: "GET", 3772 url: `users/1/items?format=json&itemKey=${key}&includeTrashed=1`, 3773 status: 200, 3774 headers: { 3775 "Last-Modified-Version": 15 3776 }, 3777 json: responseJSON 3778 }); 3779 3780 var crPromise = waitForWindow('chrome://zotero/content/merge.xul', function (dialog) { 3781 var doc = dialog.document; 3782 var wizard = doc.documentElement; 3783 var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0]; 3784 3785 assert.isTrue(doc.getElementById('resolve-all').hidden); 3786 3787 // Remote version should be selected by default 3788 assert.equal(mergeGroup.rightpane.getAttribute('selected'), 'true'); 3789 wizard.getButton('finish').click(); 3790 }) 3791 yield engine._downloadObjects('item', [key]); 3792 yield crPromise; 3793 3794 obj = objectsClass.getByLibraryAndKey(libraryID, key); 3795 assert.ok(obj); 3796 assert.equal(obj.getField('title'), jsonData.title); 3797 3798 var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID); 3799 assert.lengthOf(keys, 0); 3800 }); 3801 3802 it("should handle local deletion and remote move to trash", function* () { 3803 var libraryID = Zotero.Libraries.userLibraryID; 3804 ({ engine, client, caller } = yield setup()); 3805 var type = 'item'; 3806 var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type); 3807 var responseJSON = []; 3808 3809 // Create object, generate JSON, and delete 3810 var obj = yield createDataObject(type, { version: 10 }); 3811 var jsonData = obj.toJSON(); 3812 var key = jsonData.key = obj.key; 3813 jsonData.version = 10; 3814 let json = { 3815 key: obj.key, 3816 version: jsonData.version, 3817 data: jsonData 3818 }; 3819 yield obj.eraseTx(); 3820 3821 json.version = jsonData.version = 15; 3822 jsonData.deleted = true; 3823 responseJSON.push(json); 3824 3825 setResponse({ 3826 method: "GET", 3827 url: `users/1/items?format=json&itemKey=${key}&includeTrashed=1`, 3828 status: 200, 3829 headers: { 3830 "Last-Modified-Version": 15 3831 }, 3832 json: responseJSON 3833 }); 3834 3835 yield engine._downloadObjects('item', [key]); 3836 3837 assert.isFalse(objectsClass.exists(libraryID, key)); 3838 3839 var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID); 3840 assert.lengthOf(keys, 0); 3841 3842 // Deletion should still be in sync delete log for uploading 3843 assert.ok(yield Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, key)); 3844 }); 3845 3846 it("should handle remote move to trash and local deletion", function* () { 3847 var libraryID = Zotero.Libraries.userLibraryID; 3848 ({ engine, client, caller } = yield setup()); 3849 var type = 'item'; 3850 var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type); 3851 var responseJSON = []; 3852 3853 // Create trashed object 3854 var obj = createUnsavedDataObject(type); 3855 obj.deleted = true; 3856 yield obj.saveTx(); 3857 3858 setResponse({ 3859 method: "GET", 3860 url: `users/1/deleted?since=10`, 3861 status: 200, 3862 headers: { 3863 "Last-Modified-Version": 15 3864 }, 3865 json: { 3866 collections: [], 3867 searches: [], 3868 items: [obj.key], 3869 } 3870 }); 3871 3872 yield engine._downloadDeletions(10, 15); 3873 3874 // Local object should have been deleted 3875 assert.isFalse(objectsClass.exists(libraryID, obj.key)); 3876 3877 var keys = yield Zotero.Sync.Data.Local.getObjectsFromSyncQueue('item', libraryID); 3878 assert.lengthOf(keys, 0); 3879 3880 // Deletion shouldn't be in sync delete log 3881 assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted('item', libraryID, obj.key)); 3882 }); 3883 }); 3884 3885 3886 describe("#_upgradeCheck()", function () { 3887 it("should upgrade a library last synced with the classic sync architecture", function* () { 3888 var userLibraryID = Zotero.Libraries.userLibraryID; 3889 ({ engine, client, caller } = yield setup()); 3890 3891 var types = Zotero.DataObjectUtilities.getTypes(); 3892 var objects = {}; 3893 3894 // Create objects added before the last classic sync time, 3895 // which should end up marked as synced 3896 for (let type of types) { 3897 objects[type] = [yield createDataObject(type)]; 3898 } 3899 3900 var time1 = "2015-05-01 01:23:45"; 3901 yield Zotero.DB.queryAsync("UPDATE collections SET clientDateModified=?", time1); 3902 yield Zotero.DB.queryAsync("UPDATE savedSearches SET clientDateModified=?", time1); 3903 yield Zotero.DB.queryAsync("UPDATE items SET clientDateModified=?", time1); 3904 3905 // Create objects added after the last sync time, which should be ignored and 3906 // therefore end up marked as unsynced 3907 for (let type of types) { 3908 objects[type].push(yield createDataObject(type)); 3909 } 3910 3911 var objectJSON = {}; 3912 for (let type of types) { 3913 objectJSON[type] = []; 3914 } 3915 3916 // Create JSON for objects created remotely after the last sync time, 3917 // which should be ignored 3918 objectJSON.collection.push(makeCollectionJSON({ 3919 key: Zotero.DataObjectUtilities.generateKey(), 3920 version: 20, 3921 name: Zotero.Utilities.randomString() 3922 })); 3923 objectJSON.search.push(makeSearchJSON({ 3924 key: Zotero.DataObjectUtilities.generateKey(), 3925 version: 20, 3926 name: Zotero.Utilities.randomString() 3927 })); 3928 objectJSON.item.push(makeItemJSON({ 3929 key: Zotero.DataObjectUtilities.generateKey(), 3930 version: 20, 3931 itemType: "book", 3932 title: Zotero.Utilities.randomString() 3933 })); 3934 3935 var lastSyncTime = Zotero.Date.toUnixTimestamp( 3936 Zotero.Date.sqlToDate("2015-05-02 00:00:00", true) 3937 ); 3938 yield Zotero.DB.queryAsync( 3939 "INSERT INTO version VALUES ('lastlocalsync', ?1), ('lastremotesync', ?1)", 3940 lastSyncTime 3941 ); 3942 3943 var headers = { 3944 "Last-Modified-Version": 20 3945 } 3946 for (let type of types) { 3947 var suffix = type == 'item' ? '&includeTrashed=1' : ''; 3948 3949 var json = {}; 3950 json[objects[type][0].key] = 10; 3951 json[objectJSON[type][0].key] = objectJSON[type][0].version; 3952 setResponse({ 3953 method: "GET", 3954 url: "users/1/" + Zotero.DataObjectUtilities.getObjectTypePlural(type) 3955 + "?format=versions" + suffix, 3956 status: 200, 3957 headers: headers, 3958 json: json 3959 }); 3960 json = {}; 3961 json[objectJSON[type][0].key] = objectJSON[type][0].version; 3962 setResponse({ 3963 method: "GET", 3964 url: "users/1/" + Zotero.DataObjectUtilities.getObjectTypePlural(type) 3965 + "?format=versions&sincetime=" + lastSyncTime + suffix, 3966 status: 200, 3967 headers: headers, 3968 json: json 3969 }); 3970 } 3971 var versionResults = yield engine._upgradeCheck(); 3972 3973 // Objects 1 should be marked as synced, with versions from the server 3974 // Objects 2 should be marked as unsynced 3975 for (let type of types) { 3976 var synced = yield Zotero.Sync.Data.Local.getSynced(type, userLibraryID); 3977 assert.deepEqual(synced, [objects[type][0].key]); 3978 assert.equal(objects[type][0].version, 10); 3979 var unsynced = yield Zotero.Sync.Data.Local.getUnsynced(type, userLibraryID); 3980 assert.deepEqual(unsynced, [objects[type][1].id]); 3981 3982 assert.equal(versionResults[type].libraryVersion, headers["Last-Modified-Version"]); 3983 assert.property(versionResults[type].versions, objectJSON[type][0].key); 3984 } 3985 3986 assert.equal(Zotero.Libraries.getVersion(userLibraryID), -1); 3987 }) 3988 }) 3989 3990 describe("#_fullSync()", function () { 3991 it("should download missing/updated local objects and flag remotely missing local objects for upload", function* () { 3992 var userLibraryID = Zotero.Libraries.userLibraryID; 3993 ({ engine, client, caller } = yield setup()); 3994 3995 var types = Zotero.DataObjectUtilities.getTypes(); 3996 var objects = {}; 3997 var objectJSON = {}; 3998 for (let type of types) { 3999 objectJSON[type] = []; 4000 } 4001 4002 for (let type of types) { 4003 // Create object with outdated version, which should be updated 4004 let obj = createUnsavedDataObject(type); 4005 obj.synced = true; 4006 obj.version = 5; 4007 yield obj.saveTx(); 4008 objects[type] = [obj]; 4009 4010 objectJSON[type].push(makeJSONFunctions[type]({ 4011 key: obj.key, 4012 version: 20, 4013 name: Zotero.Utilities.randomString() 4014 })); 4015 4016 // Create JSON for object that exists remotely and not locally, 4017 // which should be downloaded 4018 objectJSON[type].push(makeJSONFunctions[type]({ 4019 key: Zotero.DataObjectUtilities.generateKey(), 4020 version: 20, 4021 name: Zotero.Utilities.randomString() 4022 })); 4023 4024 // Create object marked as synced that doesn't exist remotely, 4025 // which should be flagged for upload 4026 obj = createUnsavedDataObject(type); 4027 obj.synced = true; 4028 obj.version = 10; 4029 yield obj.saveTx(); 4030 objects[type].push(obj); 4031 4032 // Create object marked as synced that doesn't exist remotely but is in the 4033 // remote delete log, which should be deleted locally 4034 obj = createUnsavedDataObject(type); 4035 obj.synced = true; 4036 obj.version = 10; 4037 yield obj.saveTx(); 4038 objects[type].push(obj); 4039 } 4040 4041 var headers = { 4042 "Last-Modified-Version": 20 4043 } 4044 setResponse({ 4045 method: "GET", 4046 url: "users/1/settings", 4047 status: 200, 4048 headers: headers, 4049 json: { 4050 tagColors: { 4051 value: [ 4052 { 4053 name: "A", 4054 color: "#CC66CC" 4055 } 4056 ], 4057 version: 2 4058 } 4059 } 4060 }); 4061 let deletedJSON = {}; 4062 for (let type of types) { 4063 let suffix = type == 'item' ? '&includeTrashed=1' : ''; 4064 let plural = Zotero.DataObjectUtilities.getObjectTypePlural(type); 4065 4066 var json = {}; 4067 json[objectJSON[type][0].key] = objectJSON[type][0].version; 4068 json[objectJSON[type][1].key] = objectJSON[type][1].version; 4069 setResponse({ 4070 method: "GET", 4071 url: "users/1/" + plural 4072 + "?format=versions" + suffix, 4073 status: 200, 4074 headers: headers, 4075 json: json 4076 }); 4077 4078 setResponse({ 4079 method: "GET", 4080 url: "users/1/" + plural 4081 + "?format=json" 4082 + "&" + type + "Key=" + objectJSON[type][0].key + "%2C" + objectJSON[type][1].key 4083 + suffix, 4084 status: 200, 4085 headers: headers, 4086 json: objectJSON[type] 4087 }); 4088 4089 deletedJSON[plural] = [objects[type][2].key]; 4090 } 4091 setResponse({ 4092 method: "GET", 4093 url: "users/1/deleted?since=0", 4094 status: 200, 4095 headers: headers, 4096 json: deletedJSON 4097 }); 4098 yield engine._fullSync(); 4099 4100 // Check settings 4101 var setting = Zotero.SyncedSettings.get(userLibraryID, "tagColors"); 4102 assert.lengthOf(setting, 1); 4103 assert.equal(setting[0].name, 'A'); 4104 var settingMetadata = Zotero.SyncedSettings.getMetadata(userLibraryID, "tagColors"); 4105 assert.equal(settingMetadata.version, 2); 4106 assert.isTrue(settingMetadata.synced); 4107 4108 // Check objects 4109 for (let type of types) { 4110 // Objects 1 should be updated with version from server 4111 assert.equal(objects[type][0].version, 20); 4112 assert.isTrue(objects[type][0].synced); 4113 4114 // JSON objects 1 should be created locally with version from server 4115 let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type); 4116 let obj = objectsClass.getByLibraryAndKey(userLibraryID, objectJSON[type][0].key); 4117 assert.equal(obj.version, 20); 4118 assert.isTrue(obj.synced); 4119 yield assertInCache(obj); 4120 4121 // JSON objects 2 should be marked as unsynced, with their version reset to 0 4122 assert.equal(objects[type][1].version, 0); 4123 assert.isFalse(objects[type][1].synced); 4124 4125 // JSON objects 3 should be deleted and not in the delete log 4126 assert.isFalse(objectsClass.getByLibraryAndKey(userLibraryID, objects[type][2].key)); 4127 assert.isFalse(yield Zotero.Sync.Data.Local.getDateDeleted( 4128 type, userLibraryID, objects[type][2].key 4129 )); 4130 } 4131 }); 4132 4133 it("should reprocess remote deletions", function* () { 4134 var userLibraryID = Zotero.Libraries.userLibraryID; 4135 ({ engine, client, caller } = yield setup()); 4136 4137 var types = Zotero.DataObjectUtilities.getTypes(); 4138 var objects = {}; 4139 var objectIDs = {}; 4140 4141 for (let type of types) { 4142 // Create object marked as synced that's in the remote delete log, which should be 4143 // deleted locally 4144 let obj = createUnsavedDataObject(type); 4145 obj.synced = true; 4146 obj.version = 5; 4147 yield obj.saveTx(); 4148 objects[type] = [obj]; 4149 objectIDs[type] = [obj.id]; 4150 4151 // Create object marked as unsynced that's in the remote delete log, which should 4152 // trigger a conflict in the case of items and otherwise reset version to 0 4153 obj = createUnsavedDataObject(type); 4154 obj.synced = false; 4155 obj.version = 5; 4156 yield obj.saveTx(); 4157 objects[type].push(obj); 4158 objectIDs[type].push(obj.id); 4159 } 4160 4161 var headers = { 4162 "Last-Modified-Version": 20 4163 } 4164 setResponse({ 4165 method: "GET", 4166 url: "users/1/settings", 4167 status: 200, 4168 headers, 4169 json: {} 4170 }); 4171 let deletedJSON = {}; 4172 for (let type of types) { 4173 let suffix = type == 'item' ? '&includeTrashed=1' : ''; 4174 let plural = Zotero.DataObjectUtilities.getObjectTypePlural(type); 4175 setResponse({ 4176 method: "GET", 4177 url: "users/1/" + plural + "?format=versions" + suffix, 4178 status: 200, 4179 headers, 4180 json: {} 4181 }); 4182 deletedJSON[plural] = objects[type].map(o => o.key); 4183 } 4184 setResponse({ 4185 method: "GET", 4186 url: "users/1/deleted?since=0", 4187 status: 200, 4188 headers: headers, 4189 json: deletedJSON 4190 }); 4191 4192 // Apply remote deletions 4193 var crPromise = waitForWindow('chrome://zotero/content/merge.xul', function (dialog) { 4194 var doc = dialog.document; 4195 var wizard = doc.documentElement; 4196 var mergeGroup = wizard.getElementsByTagName('zoteromergegroup')[0]; 4197 4198 // Should be one conflict for each object type; select local 4199 var numConflicts = Object.keys(objects).length; 4200 for (let i = 0; i < numConflicts; i++) { 4201 assert.equal(mergeGroup.leftpane.getAttribute('selected'), 'true'); 4202 4203 if (i < numConflicts - 1) { 4204 wizard.getButton('next').click(); 4205 } 4206 else { 4207 wizard.getButton('finish').click(); 4208 } 4209 } 4210 }); 4211 4212 yield engine._fullSync(); 4213 yield crPromise; 4214 4215 // Check objects 4216 for (let type of types) { 4217 let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type); 4218 4219 // Objects 0 should be deleted 4220 assert.isFalse(objectsClass.exists(objectIDs[type][0])); 4221 4222 // Objects 1 should be marked for reupload 4223 assert.isTrue(objectsClass.exists(objectIDs[type][1])); 4224 assert.strictEqual(objects[type][1].version, 0); 4225 assert.strictEqual(objects[type][1].synced, false); 4226 } 4227 }); 4228 }); 4229 4230 4231 describe("#_restoreToServer()", function () { 4232 it("should delete remote objects that don't exist locally and upload all local objects", async function () { 4233 ({ engine, client, caller } = await setup()); 4234 var library = Zotero.Libraries.userLibrary; 4235 var libraryID = library.id; 4236 var lastLibraryVersion = 10; 4237 library.libraryVersion = library.storageVersion = lastLibraryVersion; 4238 await library.saveTx(); 4239 lastLibraryVersion = 20; 4240 4241 var postData = {}; 4242 var deleteData = {}; 4243 4244 var types = Zotero.DataObjectUtilities.getTypes(); 4245 var objects = {}; 4246 var objectJSON = {}; 4247 for (let type of types) { 4248 objectJSON[type] = []; 4249 } 4250 4251 var obj; 4252 for (let type of types) { 4253 objects[type] = [null]; 4254 // Create JSON for object that exists remotely and not locally, 4255 // which should be deleted 4256 objectJSON[type].push(makeJSONFunctions[type]({ 4257 key: Zotero.DataObjectUtilities.generateKey(), 4258 version: lastLibraryVersion, 4259 name: Zotero.Utilities.randomString() 4260 })); 4261 4262 // All other objects should be uploaded 4263 4264 // Object with outdated version 4265 obj = await createDataObject(type, { synced: true, version: 5 }); 4266 objects[type].push(obj); 4267 objectJSON[type].push(makeJSONFunctions[type]({ 4268 key: obj.key, 4269 version: lastLibraryVersion, 4270 name: Zotero.Utilities.randomString() 4271 })); 4272 4273 // Object marked as synced that doesn't exist remotely 4274 obj = await createDataObject(type, { synced: true, version: 10 }); 4275 objects[type].push(obj); 4276 objectJSON[type].push(makeJSONFunctions[type]({ 4277 key: obj.key, 4278 version: lastLibraryVersion, 4279 name: Zotero.Utilities.randomString() 4280 })); 4281 4282 // Object marked as synced that doesn't exist remotely 4283 // but is in the remote delete log 4284 obj = await createDataObject(type, { synced: true, version: 10 }); 4285 objects[type].push(obj); 4286 objectJSON[type].push(makeJSONFunctions[type]({ 4287 key: obj.key, 4288 version: lastLibraryVersion, 4289 name: Zotero.Utilities.randomString() 4290 })); 4291 } 4292 4293 // Child attachment 4294 obj = await importFileAttachment( 4295 'test.png', 4296 { 4297 parentID: objects.item[1].id, 4298 synced: true, 4299 version: 5 4300 } 4301 ); 4302 obj.attachmentSyncedModificationTime = new Date().getTime(); 4303 obj.attachmentSyncedHash = 'b32e33f529942d73bea4ed112310f804'; 4304 obj.attachmentSyncState = Zotero.Sync.Storage.Local.SYNC_STATE_IN_SYNC; 4305 await obj.saveTx(); 4306 objects.item.push(obj); 4307 objectJSON.item.push(makeJSONFunctions.item({ 4308 key: obj.key, 4309 version: lastLibraryVersion, 4310 name: Zotero.Utilities.randomString(), 4311 itemType: 'attachment' 4312 })); 4313 4314 for (let type of types) { 4315 let plural = Zotero.DataObjectUtilities.getObjectTypePlural(type); 4316 let suffix = type == 'item' ? '&includeTrashed=1' : ''; 4317 4318 let json = {}; 4319 json[objectJSON[type][0].key] = objectJSON[type][0].version; 4320 json[objectJSON[type][1].key] = objectJSON[type][1].version; 4321 setResponse({ 4322 method: "GET", 4323 url: `users/1/${plural}?format=versions${suffix}`, 4324 status: 200, 4325 headers: { 4326 "Last-Modified-Version": lastLibraryVersion 4327 }, 4328 json 4329 }); 4330 4331 deleteData[type] = { 4332 expectedVersion: lastLibraryVersion++, 4333 keys: [objectJSON[type][0].key] 4334 }; 4335 } 4336 4337 await Zotero.SyncedSettings.set(libraryID, "testSetting", { foo: 2 }); 4338 var settingsJSON = { testSetting: { value: { foo: 2 } } } 4339 postData.setting = { 4340 expectedVersion: lastLibraryVersion++ 4341 }; 4342 4343 for (let type of types) { 4344 postData[type] = { 4345 expectedVersion: lastLibraryVersion++ 4346 }; 4347 } 4348 4349 server.respond(function (req) { 4350 try { 4351 4352 let plural = req.url.match(/users\/\d+\/([a-z]+e?s)/)[1]; 4353 let type = Zotero.DataObjectUtilities.getObjectTypeSingular(plural); 4354 // Deletions 4355 if (req.method == "DELETE") { 4356 let data = deleteData[type]; 4357 let version = data.expectedVersion + 1; 4358 if (req.url == baseURL + `users/1/${plural}?${type}Key=${data.keys.join(',')}`) { 4359 req.respond( 4360 204, 4361 { 4362 "Last-Modified-Version": version 4363 }, 4364 "" 4365 ); 4366 } 4367 } 4368 // Settings 4369 else if (req.method == "POST" && req.url.match(/users\/\d+\/settings/)) { 4370 let data = postData.setting; 4371 assert.equal( 4372 req.requestHeaders["If-Unmodified-Since-Version"], 4373 data.expectedVersion 4374 ); 4375 let version = data.expectedVersion + 1; 4376 let json = JSON.parse(req.requestBody); 4377 assert.deepEqual(json, settingsJSON); 4378 req.respond( 4379 204, 4380 { 4381 "Last-Modified-Version": version 4382 }, 4383 "" 4384 ); 4385 } 4386 // Uploads 4387 else if (req.method == "POST") { 4388 let data = postData[type]; 4389 assert.equal( 4390 req.requestHeaders["If-Unmodified-Since-Version"], 4391 data.expectedVersion 4392 ); 4393 let version = data.expectedVersion + 1; 4394 let json = JSON.parse(req.requestBody); 4395 let o1 = json.find(o => o.key == objectJSON[type][1].key); 4396 assert.notProperty(o1, 'version'); 4397 let o2 = json.find(o => o.key == objectJSON[type][2].key); 4398 assert.notProperty(o2, 'version'); 4399 let o3 = json.find(o => o.key == objectJSON[type][3].key); 4400 assert.notProperty(o3, 'version'); 4401 let response = { 4402 successful: { 4403 "0": Object.assign(objectJSON[type][1], { version }), 4404 "1": Object.assign(objectJSON[type][2], { version }), 4405 "2": Object.assign(objectJSON[type][3], { version }) 4406 }, 4407 unchanged: {}, 4408 failed: {} 4409 }; 4410 if (type == 'item') { 4411 let o = json.find(o => o.key == objectJSON.item[4].key); 4412 assert.notProperty(o, 'version'); 4413 // Attachment items should include storage properties 4414 assert.propertyVal(o, 'mtime', objects.item[4].attachmentSyncedModificationTime); 4415 assert.propertyVal(o, 'md5', objects.item[4].attachmentSyncedHash); 4416 response.successful["3"] = Object.assign(objectJSON[type][4], { version }) 4417 } 4418 req.respond( 4419 200, 4420 { 4421 "Last-Modified-Version": version 4422 }, 4423 JSON.stringify(response) 4424 ); 4425 } 4426 4427 } 4428 catch (e) { 4429 Zotero.logError(e); 4430 throw e; 4431 } 4432 }); 4433 4434 await engine._restoreToServer(); 4435 4436 // Check settings 4437 var setting = Zotero.SyncedSettings.get(libraryID, "testSetting"); 4438 assert.deepEqual(setting, { foo: 2 }); 4439 var settingMetadata = Zotero.SyncedSettings.getMetadata(libraryID, "testSetting"); 4440 assert.equal(settingMetadata.version, postData.setting.expectedVersion + 1); 4441 assert.isTrue(settingMetadata.synced); 4442 4443 // Objects should all be marked as synced and in the cache 4444 for (let type of types) { 4445 let version = postData[type].expectedVersion + 1; 4446 for (let i = 1; i <= 3; i++) { 4447 assert.equal(objects[type][i].version, version); 4448 assert.isTrue(objects[type][i].synced); 4449 await assertInCache(objects[type][i]); 4450 } 4451 } 4452 4453 // Files should be marked as unsynced 4454 assert.equal( 4455 objects.item[4].attachmentSyncState, 4456 Zotero.Sync.Storage.Local.SYNC_STATE_TO_UPLOAD 4457 ); 4458 }); 4459 }); 4460 })