syncRunnerTest.js (34108B)
1 "use strict"; 2 3 describe("Zotero.Sync.Runner", 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 userLibraryID, runner, caller, server, stub, spy; 9 10 var responses = { 11 keyInfo: { 12 fullAccess: { 13 method: "GET", 14 url: "keys/current", 15 status: 200, 16 json: { 17 key: apiKey, 18 userID: 1, 19 username: "Username", 20 access: { 21 user: { 22 library: true, 23 files: true, 24 notes: true, 25 write: true 26 }, 27 groups: { 28 all: { 29 library: true, 30 write: true 31 } 32 } 33 } 34 } 35 } 36 }, 37 userGroups: { 38 groupVersions: { 39 method: "GET", 40 url: "users/1/groups?format=versions", 41 json: { 42 "1623562": 10, 43 "2694172": 11 44 } 45 }, 46 groupVersionsEmpty: { 47 method: "GET", 48 url: "users/1/groups?format=versions", 49 json: {} 50 }, 51 groupVersionsOnlyMemberGroup: { 52 method: "GET", 53 url: "users/1/groups?format=versions", 54 json: { 55 "2694172": 11 56 } 57 } 58 }, 59 groups: { 60 ownerGroup: { 61 method: "GET", 62 url: "groups/1623562", 63 json: { 64 id: 1623562, 65 version: 10, 66 data: { 67 id: 1623562, 68 version: 10, 69 name: "Group Name", 70 description: "<p>Test group</p>", 71 owner: 1, 72 type: "Private", 73 libraryEditing: "members", 74 libraryReading: "all", 75 fileEditing: "members", 76 admins: [], 77 members: [] 78 } 79 } 80 }, 81 memberGroup: { 82 method: "GET", 83 url: "groups/2694172", 84 json: { 85 id: 2694172, 86 version: 11, 87 data: { 88 id: 2694172, 89 version: 11, 90 name: "Group Name 2", 91 description: "<p>Test group</p>", 92 owner: 123456, 93 type: "Private", 94 libraryEditing: "admins", 95 libraryReading: "all", 96 fileEditing: "admins", 97 admins: [], 98 members: [1] 99 } 100 } 101 } 102 } 103 }; 104 105 // 106 // Helper functions 107 // 108 function setResponse(response) { 109 setHTTPResponse(server, baseURL, response, responses); 110 } 111 112 113 // 114 // Tests 115 // 116 beforeEach(function* () { 117 yield resetDB({ 118 thisArg: this, 119 skipBundledFiles: true 120 }); 121 122 userLibraryID = Zotero.Libraries.userLibraryID; 123 124 Zotero.HTTP.mock = sinon.FakeXMLHttpRequest; 125 server = sinon.fakeServer.create(); 126 server.autoRespond = true; 127 128 runner = new Zotero.Sync.Runner_Module({ baseURL, apiKey }); 129 130 Components.utils.import("resource://zotero/concurrentCaller.js"); 131 caller = new ConcurrentCaller(1); 132 caller.setLogger(msg => Zotero.debug(msg)); 133 caller.stopOnError = true; 134 caller.onError = function (e) { 135 Zotero.logError(e); 136 if (options.onError) { 137 options.onError(e); 138 } 139 if (e.fatal) { 140 caller.stop(); 141 throw e; 142 } 143 }; 144 145 yield Zotero.Users.setCurrentUserID(1); 146 yield Zotero.Users.setCurrentUsername("A"); 147 }) 148 afterEach(function () { 149 if (stub) stub.restore(); 150 if (spy) spy.restore(); 151 }) 152 after(function () { 153 Zotero.HTTP.mock = null; 154 }) 155 156 describe("#checkAccess()", function () { 157 it("should check key access", function* () { 158 setResponse('keyInfo.fullAccess'); 159 var json = yield runner.checkAccess(runner.getAPIClient({ apiKey })); 160 var compare = {}; 161 Object.assign(compare, responses.keyInfo.fullAccess.json); 162 delete compare.key; 163 assert.deepEqual(json, compare); 164 }) 165 }) 166 167 describe("#checkLibraries()", function () { 168 beforeEach(function* () { 169 Zotero.Prefs.clear('sync.librariesToSkip'); 170 }); 171 172 afterEach(function* () { 173 Zotero.Prefs.clear('sync.librariesToSkip'); 174 175 var group = Zotero.Groups.get(responses.groups.ownerGroup.json.id); 176 if (group) { 177 yield group.eraseTx(); 178 } 179 group = Zotero.Groups.get(responses.groups.memberGroup.json.id); 180 if (group) { 181 yield group.eraseTx(); 182 } 183 }) 184 185 it("should check library access and versions without library list", function* () { 186 // Create group with same id and version as groups response 187 var groupData = responses.groups.ownerGroup; 188 var group1 = yield createGroup({ 189 id: groupData.json.id, 190 version: groupData.json.version 191 }); 192 groupData = responses.groups.memberGroup; 193 var group2 = yield createGroup({ 194 id: groupData.json.id, 195 version: groupData.json.version 196 }); 197 198 setResponse('userGroups.groupVersions'); 199 var libraries = yield runner.checkLibraries( 200 runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json 201 ); 202 assert.lengthOf(libraries, 3); 203 assert.sameMembers( 204 libraries, 205 [userLibraryID, group1.libraryID, group2.libraryID] 206 ); 207 }) 208 209 it("should check library access and versions with library list", function* () { 210 // Create groups with same id and version as groups response 211 var groupData = responses.groups.ownerGroup; 212 var group1 = yield createGroup({ 213 id: groupData.json.id, 214 version: groupData.json.version 215 }); 216 groupData = responses.groups.memberGroup; 217 var group2 = yield createGroup({ 218 id: groupData.json.id, 219 version: groupData.json.version 220 }); 221 222 setResponse('userGroups.groupVersions'); 223 var libraries = yield runner.checkLibraries( 224 runner.getAPIClient({ apiKey }), 225 false, 226 responses.keyInfo.fullAccess.json, 227 [userLibraryID] 228 ); 229 assert.lengthOf(libraries, 1); 230 assert.sameMembers(libraries, [userLibraryID]); 231 232 var libraries = yield runner.checkLibraries( 233 runner.getAPIClient({ apiKey }), 234 false, 235 responses.keyInfo.fullAccess.json, 236 [userLibraryID] 237 ); 238 assert.lengthOf(libraries, 1); 239 assert.sameMembers(libraries, [userLibraryID]); 240 241 var libraries = yield runner.checkLibraries( 242 runner.getAPIClient({ apiKey }), 243 false, 244 responses.keyInfo.fullAccess.json, 245 [group1.libraryID] 246 ); 247 assert.lengthOf(libraries, 1); 248 assert.sameMembers(libraries, [group1.libraryID]); 249 }) 250 251 it("should filter out nonexistent skipped libraries if library list not provided", function* () { 252 var unskippedGroupID = responses.groups.ownerGroup.json.id; 253 var skippedGroupID = responses.groups.memberGroup.json.id; 254 Zotero.Prefs.set('sync.librariesToSkip', `["L4", "G${skippedGroupID}"]`); 255 256 setResponse('userGroups.groupVersions'); 257 setResponse('groups.ownerGroup'); 258 setResponse('groups.memberGroup'); 259 var libraries = yield runner.checkLibraries( 260 runner.getAPIClient({ apiKey }), 261 false, 262 responses.keyInfo.fullAccess.json 263 ); 264 265 var group = Zotero.Groups.get(unskippedGroupID); 266 assert.lengthOf(libraries, 2); 267 assert.sameMembers(libraries, [userLibraryID, group.libraryID]); 268 269 assert.isFalse(Zotero.Groups.get(skippedGroupID)); 270 }); 271 272 it("should filter out existing skipped libraries if library list not provided", function* () { 273 var unskippedGroupID = responses.groups.ownerGroup.json.id; 274 var skippedGroupID = responses.groups.memberGroup.json.id; 275 Zotero.Prefs.set('sync.librariesToSkip', `["L4", "G${skippedGroupID}"]`); 276 277 var skippedGroup = yield createGroup({ 278 id: skippedGroupID, 279 version: responses.groups.memberGroup.json.version - 1 280 }); 281 282 setResponse('userGroups.groupVersions'); 283 setResponse('groups.ownerGroup'); 284 setResponse('groups.memberGroup'); 285 var libraries = yield runner.checkLibraries( 286 runner.getAPIClient({ apiKey }), 287 false, 288 responses.keyInfo.fullAccess.json 289 ); 290 291 var group = Zotero.Groups.get(unskippedGroupID); 292 assert.lengthOf(libraries, 2); 293 assert.sameMembers(libraries, [userLibraryID, group.libraryID]); 294 295 assert.equal(skippedGroup.version, responses.groups.memberGroup.json.version - 1); 296 }); 297 298 it("should filter out remotely missing archived libraries if library list not provided", function* () { 299 var ownerGroupID = responses.groups.ownerGroup.json.id; 300 var archivedGroupID = 162512451; // nonexistent group id 301 302 var ownerGroup = yield createGroup({ 303 id: ownerGroupID, 304 version: responses.groups.ownerGroup.json.version 305 }); 306 var archivedGroup = yield createGroup({ 307 id: archivedGroupID, 308 editable: false, 309 archived: true 310 }); 311 312 setResponse('userGroups.groupVersions'); 313 setResponse('groups.memberGroup'); 314 var libraries = yield runner.checkLibraries( 315 runner.getAPIClient({ apiKey }), 316 false, 317 responses.keyInfo.fullAccess.json 318 ); 319 320 assert.lengthOf(libraries, 3); 321 assert.sameMembers( 322 libraries, 323 [ 324 userLibraryID, 325 ownerGroup.libraryID, 326 // Nonexistent group should've been created 327 Zotero.Groups.getLibraryIDFromGroupID(responses.groups.memberGroup.json.id) 328 ] 329 ); 330 }); 331 332 it("should unarchive library if available remotely", function* () { 333 var syncedGroupID = responses.groups.ownerGroup.json.id; 334 var archivedGroupID = responses.groups.memberGroup.json.id; 335 336 var syncedGroup = yield createGroup({ 337 id: syncedGroupID, 338 version: responses.groups.ownerGroup.json.version 339 }); 340 var archivedGroup = yield createGroup({ 341 id: archivedGroupID, 342 version: responses.groups.memberGroup.json.version - 1, 343 editable: false, 344 archived: true 345 }); 346 347 setResponse('userGroups.groupVersions'); 348 setResponse('groups.ownerGroup'); 349 setResponse('groups.memberGroup'); 350 var libraries = yield runner.checkLibraries( 351 runner.getAPIClient({ apiKey }), 352 false, 353 responses.keyInfo.fullAccess.json 354 ); 355 356 assert.lengthOf(libraries, 3); 357 assert.sameMembers( 358 libraries, 359 [userLibraryID, syncedGroup.libraryID, archivedGroup.libraryID] 360 ); 361 assert.isFalse(archivedGroup.archived); 362 }); 363 364 it("shouldn't filter out skipped libraries if library list is provided", function* () { 365 var groupData = responses.groups.memberGroup; 366 var group = yield createGroup({ 367 id: groupData.json.id, 368 version: groupData.json.version 369 }); 370 371 Zotero.Prefs.set('sync.librariesToSkip', `["L4", "G${group.id}"]`); 372 373 setResponse('userGroups.groupVersions'); 374 setResponse('groups.ownerGroup'); 375 setResponse('groups.memberGroup'); 376 var libraries = yield runner.checkLibraries( 377 runner.getAPIClient({ apiKey }), 378 false, 379 responses.keyInfo.fullAccess.json, 380 [userLibraryID, group.libraryID] 381 ); 382 383 assert.lengthOf(libraries, 2); 384 assert.sameMembers(libraries, [userLibraryID, group.libraryID]); 385 }); 386 387 it("should update outdated group metadata", function* () { 388 // Create groups with same id as groups response but earlier versions 389 var groupData1 = responses.groups.ownerGroup; 390 var group1 = yield createGroup({ 391 id: groupData1.json.id, 392 version: groupData1.json.version - 1, 393 editable: false 394 }); 395 var groupData2 = responses.groups.memberGroup; 396 var group2 = yield createGroup({ 397 id: groupData2.json.id, 398 version: groupData2.json.version - 1, 399 editable: true 400 }); 401 402 setResponse('userGroups.groupVersions'); 403 setResponse('groups.ownerGroup'); 404 setResponse('groups.memberGroup'); 405 // Simulate acceptance of library reset for group 2 editable change 406 var stub = sinon.stub(Zotero.Sync.Data.Local, "checkLibraryForAccess") 407 .returns(Zotero.Promise.resolve(true)); 408 409 var libraries = yield runner.checkLibraries( 410 runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json 411 ); 412 413 assert.ok(stub.calledTwice); 414 stub.restore(); 415 assert.lengthOf(libraries, 3); 416 assert.sameMembers( 417 libraries, 418 [userLibraryID, group1.libraryID, group2.libraryID] 419 ); 420 421 assert.equal(group1.name, groupData1.json.data.name); 422 assert.equal(group1.version, groupData1.json.version); 423 assert.isTrue(group1.editable); 424 assert.equal(group2.name, groupData2.json.data.name); 425 assert.equal(group2.version, groupData2.json.version); 426 assert.isFalse(group2.editable); 427 }) 428 429 it("should update outdated group metadata for group created with classic sync", function* () { 430 var groupData1 = responses.groups.ownerGroup; 431 var group1 = yield createGroup({ 432 id: groupData1.json.id, 433 version: 0, 434 editable: false 435 }); 436 var groupData2 = responses.groups.memberGroup; 437 var group2 = yield createGroup({ 438 id: groupData2.json.id, 439 version: 0, 440 editable: true 441 }); 442 443 yield Zotero.DB.queryAsync( 444 "UPDATE groups SET version=0 WHERE groupID IN (?, ?)", [group1.id, group2.id] 445 ); 446 yield Zotero.Libraries.init(); 447 group1 = Zotero.Groups.get(group1.id); 448 group2 = Zotero.Groups.get(group2.id); 449 450 setResponse('userGroups.groupVersions'); 451 setResponse('groups.ownerGroup'); 452 setResponse('groups.memberGroup'); 453 // Simulate acceptance of library reset for group 2 editable change 454 var stub = sinon.stub(Zotero.Sync.Data.Local, "checkLibraryForAccess") 455 .returns(Zotero.Promise.resolve(true)); 456 457 var libraries = yield runner.checkLibraries( 458 runner.getAPIClient({ apiKey }), 459 false, 460 responses.keyInfo.fullAccess.json, 461 [group1.libraryID, group2.libraryID] 462 ); 463 464 assert.ok(stub.calledTwice); 465 stub.restore(); 466 assert.lengthOf(libraries, 2); 467 assert.sameMembers(libraries, [group1.libraryID, group2.libraryID]); 468 469 assert.equal(group1.name, groupData1.json.data.name); 470 assert.equal(group1.version, groupData1.json.version); 471 assert.isTrue(group1.editable); 472 assert.equal(group2.name, groupData2.json.data.name); 473 assert.equal(group2.version, groupData2.json.version); 474 assert.isFalse(group2.editable); 475 }) 476 477 it("should create locally missing groups", function* () { 478 setResponse('userGroups.groupVersions'); 479 setResponse('groups.ownerGroup'); 480 setResponse('groups.memberGroup'); 481 var libraries = yield runner.checkLibraries( 482 runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json 483 ); 484 assert.lengthOf(libraries, 3); 485 var groupData1 = responses.groups.ownerGroup; 486 var group1 = Zotero.Groups.get(groupData1.json.id); 487 var groupData2 = responses.groups.memberGroup; 488 var group2 = Zotero.Groups.get(groupData2.json.id); 489 assert.ok(group1); 490 assert.ok(group2); 491 assert.sameMembers( 492 libraries, 493 [userLibraryID, group1.libraryID, group2.libraryID] 494 ); 495 assert.equal(group1.name, groupData1.json.data.name); 496 assert.isTrue(group1.editable); 497 assert.equal(group2.name, groupData2.json.data.name); 498 assert.isFalse(group2.editable); 499 }) 500 501 it("should delete remotely missing groups", function* () { 502 var groupData1 = responses.groups.ownerGroup; 503 var group1 = yield createGroup({ id: groupData1.json.id, version: groupData1.json.version }); 504 var groupData2 = responses.groups.memberGroup; 505 var group2 = yield createGroup({ id: groupData2.json.id, version: groupData2.json.version }); 506 507 setResponse('userGroups.groupVersionsOnlyMemberGroup'); 508 waitForDialog(function (dialog) { 509 var text = dialog.document.documentElement.textContent; 510 assert.include(text, group1.name); 511 }); 512 var libraries = yield runner.checkLibraries( 513 runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json 514 ); 515 assert.lengthOf(libraries, 2); 516 assert.sameMembers(libraries, [userLibraryID, group2.libraryID]); 517 assert.isFalse(Zotero.Groups.exists(groupData1.json.id)); 518 assert.isTrue(Zotero.Groups.exists(groupData2.json.id)); 519 }) 520 521 it("should keep remotely missing groups", function* () { 522 var group1 = yield createGroup({ editable: true, filesEditable: true }); 523 var group2 = yield createGroup({ editable: true, filesEditable: true }); 524 525 setResponse('userGroups.groupVersionsEmpty'); 526 var called = 0; 527 var otherGroup; 528 waitForDialog(function (dialog) { 529 called++; 530 var text = dialog.document.documentElement.textContent; 531 if (text.includes(group1.name)) { 532 otherGroup = group2; 533 } 534 else if (text.includes(group2.name)) { 535 otherGroup = group1; 536 } 537 else { 538 throw new Error("Dialog text does not include either group name"); 539 } 540 541 waitForDialog(function (dialog) { 542 called++; 543 var text = dialog.document.documentElement.textContent; 544 assert.include(text, otherGroup.name); 545 }, "extra1"); 546 }, "extra1"); 547 var libraries = yield runner.checkLibraries( 548 runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json 549 ); 550 assert.equal(called, 2); 551 assert.lengthOf(libraries, 1); 552 assert.sameMembers(libraries, [userLibraryID]); 553 // Groups should still exist but be read-only and archived 554 [group1, group2].forEach((group) => { 555 assert.isTrue(Zotero.Groups.exists(group.id)); 556 assert.isTrue(group.archived); 557 assert.isFalse(group.editable); 558 assert.isFalse(group.filesEditable); 559 }); 560 }) 561 562 it("should cancel sync with remotely missing groups", function* () { 563 var groupData = responses.groups.ownerGroup; 564 var group = yield createGroup({ id: groupData.json.id, version: groupData.json.version }); 565 566 setResponse('userGroups.groupVersionsEmpty'); 567 waitForDialog(function (dialog) { 568 var text = dialog.document.documentElement.textContent; 569 assert.include(text, group.name); 570 }, "cancel"); 571 var libraries = yield runner.checkLibraries( 572 runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json 573 ); 574 assert.lengthOf(libraries, 0); 575 assert.isTrue(Zotero.Groups.exists(groupData.json.id)); 576 }) 577 578 it("should prompt to revert local changes on loss of library write access", function* () { 579 var group = yield createGroup({ 580 version: 1, 581 libraryVersion: 2 582 }); 583 var libraryID = group.libraryID; 584 585 setResponse({ 586 method: "GET", 587 url: "users/1/groups?format=versions", 588 status: 200, 589 headers: { 590 "Last-Modified-Version": 3 591 }, 592 json: { 593 [group.id]: 3 594 } 595 }); 596 setResponse({ 597 method: "GET", 598 url: "groups/" + group.id, 599 status: 200, 600 headers: { 601 "Last-Modified-Version": 3 602 }, 603 json: { 604 id: group.id, 605 version: 2, 606 data: { 607 // Make group read-only 608 id: group.id, 609 version: 2, 610 name: group.name, 611 description: group.description, 612 owner: 2, 613 type: "Private", 614 libraryEditing: "admins", 615 libraryReading: "all", 616 fileEditing: "admins", 617 admins: [], 618 members: [1] 619 } 620 } 621 }); 622 623 // First, test cancelling 624 var stub = sinon.stub(Zotero.Sync.Data.Local, "checkLibraryForAccess") 625 .returns(Zotero.Promise.resolve(false)); 626 var libraries = yield runner.checkLibraries( 627 runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json 628 ); 629 assert.notInclude(libraries, group.libraryID); 630 assert.isTrue(stub.calledOnce); 631 assert.isTrue(group.editable); 632 stub.reset(); 633 634 // Next, reset 635 stub.returns(Zotero.Promise.resolve(true)); 636 libraries = yield runner.checkLibraries( 637 runner.getAPIClient({ apiKey }), false, responses.keyInfo.fullAccess.json 638 ); 639 assert.include(libraries, group.libraryID); 640 assert.isTrue(stub.calledOnce); 641 assert.isFalse(group.editable); 642 643 stub.restore(); 644 }); 645 }) 646 647 describe("#sync()", function () { 648 it("should perform a sync across all libraries and update library versions", function* () { 649 setResponse('keyInfo.fullAccess'); 650 setResponse('userGroups.groupVersions'); 651 setResponse('groups.ownerGroup'); 652 setResponse('groups.memberGroup'); 653 // My Library 654 setResponse({ 655 method: "GET", 656 url: "users/1/settings", 657 status: 200, 658 headers: { 659 "Last-Modified-Version": 5 660 }, 661 json: [] 662 }); 663 setResponse({ 664 method: "GET", 665 url: "users/1/collections?format=versions", 666 status: 200, 667 headers: { 668 "Last-Modified-Version": 5 669 }, 670 json: [] 671 }); 672 setResponse({ 673 method: "GET", 674 url: "users/1/searches?format=versions", 675 status: 200, 676 headers: { 677 "Last-Modified-Version": 5 678 }, 679 json: [] 680 }); 681 setResponse({ 682 method: "GET", 683 url: "users/1/items/top?format=versions&includeTrashed=1", 684 status: 200, 685 headers: { 686 "Last-Modified-Version": 5 687 }, 688 json: [] 689 }); 690 setResponse({ 691 method: "GET", 692 url: "users/1/items?format=versions&includeTrashed=1", 693 status: 200, 694 headers: { 695 "Last-Modified-Version": 5 696 }, 697 json: [] 698 }); 699 setResponse({ 700 method: "GET", 701 url: "users/1/deleted?since=0", 702 status: 200, 703 headers: { 704 "Last-Modified-Version": 5 705 }, 706 json: [] 707 }); 708 // Group library 1 709 setResponse({ 710 method: "GET", 711 url: "groups/1623562/settings", 712 status: 200, 713 headers: { 714 "Last-Modified-Version": 15 715 }, 716 json: [] 717 }); 718 setResponse({ 719 method: "GET", 720 url: "groups/1623562/collections?format=versions", 721 status: 200, 722 headers: { 723 "Last-Modified-Version": 15 724 }, 725 json: [] 726 }); 727 setResponse({ 728 method: "GET", 729 url: "groups/1623562/searches?format=versions", 730 status: 200, 731 headers: { 732 "Last-Modified-Version": 15 733 }, 734 json: [] 735 }); 736 setResponse({ 737 method: "GET", 738 url: "groups/1623562/items/top?format=versions&includeTrashed=1", 739 status: 200, 740 headers: { 741 "Last-Modified-Version": 15 742 }, 743 json: [] 744 }); 745 setResponse({ 746 method: "GET", 747 url: "groups/1623562/items?format=versions&includeTrashed=1", 748 status: 200, 749 headers: { 750 "Last-Modified-Version": 15 751 }, 752 json: [] 753 }); 754 setResponse({ 755 method: "GET", 756 url: "groups/1623562/deleted?since=0", 757 status: 200, 758 headers: { 759 "Last-Modified-Version": 15 760 }, 761 json: [] 762 }); 763 // Group library 2 764 setResponse({ 765 method: "GET", 766 url: "groups/2694172/settings", 767 status: 200, 768 headers: { 769 "Last-Modified-Version": 20 770 }, 771 json: [] 772 }); 773 setResponse({ 774 method: "GET", 775 url: "groups/2694172/collections?format=versions", 776 status: 200, 777 headers: { 778 "Last-Modified-Version": 20 779 }, 780 json: [] 781 }); 782 setResponse({ 783 method: "GET", 784 url: "groups/2694172/searches?format=versions", 785 status: 200, 786 headers: { 787 "Last-Modified-Version": 20 788 }, 789 json: [] 790 }); 791 setResponse({ 792 method: "GET", 793 url: "groups/2694172/items/top?format=versions&includeTrashed=1", 794 status: 200, 795 headers: { 796 "Last-Modified-Version": 20 797 }, 798 json: [] 799 }); 800 setResponse({ 801 method: "GET", 802 url: "groups/2694172/items?format=versions&includeTrashed=1", 803 status: 200, 804 headers: { 805 "Last-Modified-Version": 20 806 }, 807 json: [] 808 }); 809 setResponse({ 810 method: "GET", 811 url: "groups/2694172/deleted?since=0", 812 status: 200, 813 headers: { 814 "Last-Modified-Version": 20 815 }, 816 json: [] 817 }); 818 // Full-text syncing 819 setResponse({ 820 method: "GET", 821 url: "users/1/fulltext?format=versions", 822 status: 200, 823 headers: { 824 "Last-Modified-Version": 5 825 }, 826 json: {} 827 }); 828 setResponse({ 829 method: "GET", 830 url: "groups/1623562/fulltext?format=versions", 831 status: 200, 832 headers: { 833 "Last-Modified-Version": 15 834 }, 835 json: {} 836 }); 837 setResponse({ 838 method: "GET", 839 url: "groups/2694172/fulltext?format=versions", 840 status: 200, 841 headers: { 842 "Last-Modified-Version": 20 843 }, 844 json: {} 845 }); 846 847 var startTime = new Date().getTime(); 848 849 yield runner.sync({ 850 onError: e => { throw e }, 851 }); 852 853 // Check local library versions 854 assert.equal( 855 Zotero.Libraries.getVersion(userLibraryID), 856 5 857 ); 858 assert.equal( 859 Zotero.Libraries.getVersion(Zotero.Groups.getLibraryIDFromGroupID(1623562)), 860 15 861 ); 862 assert.equal( 863 Zotero.Libraries.getVersion(Zotero.Groups.getLibraryIDFromGroupID(2694172)), 864 20 865 ); 866 867 // Last sync time should be within the last few seconds 868 var lastSyncTime = Zotero.Sync.Data.Local.getLastSyncTime(); 869 assert.isAbove(lastSyncTime.getTime(), startTime); 870 assert.isBelow(lastSyncTime.getTime(), new Date().getTime()); 871 }) 872 873 874 it("should handle user-initiated cancellation", function* () { 875 setResponse('keyInfo.fullAccess'); 876 setResponse('userGroups.groupVersions'); 877 setResponse('groups.ownerGroup'); 878 setResponse('groups.memberGroup'); 879 880 var stub = sinon.stub(Zotero.Sync.Data.Engine.prototype, "start"); 881 882 stub.onCall(0).returns(Zotero.Promise.resolve()); 883 var e = new Zotero.Sync.UserCancelledException(); 884 e.handledRejection = true; 885 stub.onCall(1).returns(Zotero.Promise.reject(e)); 886 // Shouldn't be reached 887 stub.onCall(2).throws(); 888 889 yield runner.sync({ 890 onError: e => { throw e }, 891 }); 892 893 stub.restore(); 894 }); 895 896 897 it("should handle user-initiated cancellation for current library", function* () { 898 setResponse('keyInfo.fullAccess'); 899 setResponse('userGroups.groupVersions'); 900 setResponse('groups.ownerGroup'); 901 setResponse('groups.memberGroup'); 902 903 var stub = sinon.stub(Zotero.Sync.Data.Engine.prototype, "start"); 904 905 stub.returns(Zotero.Promise.resolve()); 906 var e = new Zotero.Sync.UserCancelledException(true); 907 e.handledRejection = true; 908 stub.onCall(1).returns(Zotero.Promise.reject(e)); 909 910 yield runner.sync({ 911 onError: e => { throw e }, 912 }); 913 914 assert.equal(stub.callCount, 3); 915 stub.restore(); 916 }); 917 }) 918 919 920 describe("#createAPIKeyFromCredentials()", function() { 921 var data = { 922 name: "Automatic Zotero Client Key", 923 username: "Username", 924 access: { 925 user: { 926 library: true, 927 files: true, 928 notes: true, 929 write: true 930 }, 931 groups: { 932 all: { 933 library: true, 934 write: true 935 } 936 } 937 } 938 }; 939 var correctPostData = Object.assign({password: 'correctPassword'}, data); 940 var incorrectPostData = Object.assign({password: 'incorrectPassword'}, data); 941 var responseData = Object.assign({userID: 1, key: apiKey}, data); 942 943 it("should return json with key when credentials valid", function* () { 944 server.respond(function (req) { 945 if (req.method == "POST") { 946 var json = JSON.parse(req.requestBody); 947 assert.deepEqual(json, correctPostData); 948 req.respond(201, {}, JSON.stringify(responseData)); 949 } 950 }); 951 952 var json = yield runner.createAPIKeyFromCredentials('Username', 'correctPassword'); 953 assert.equal(json.key, apiKey); 954 }); 955 956 it("should return false when credentials invalid", function* () { 957 server.respond(function (req) { 958 if (req.method == "POST") { 959 var json = JSON.parse(req.requestBody); 960 assert.deepEqual(json, incorrectPostData); 961 req.respond(403); 962 } 963 }); 964 965 var key = yield runner.createAPIKeyFromCredentials('Username', 'incorrectPassword'); 966 assert.isFalse(key); 967 }); 968 }); 969 970 describe("#deleteAPIKey()", function() { 971 it("should send DELETE request with correct key", function* (){ 972 Zotero.Sync.Data.Local.setAPIKey(apiKey); 973 974 server.respond(function (req) { 975 if (req.method == "DELETE") { 976 assert.propertyVal(req.requestHeaders, 'Zotero-API-Key', apiKey); 977 assert.equal(req.url, baseURL + "keys/current"); 978 } 979 req.respond(204); 980 }); 981 982 yield runner.deleteAPIKey(); 983 }); 984 }); 985 986 987 describe("Error Handling", function () { 988 var win; 989 990 afterEach(function () { 991 if (win) { 992 win.close(); 993 } 994 }); 995 996 it("should show the sync error icon on error", function* () { 997 let library = Zotero.Libraries.userLibrary; 998 library.libraryVersion = 5; 999 yield library.save(); 1000 1001 setResponse('keyInfo.fullAccess'); 1002 setResponse('userGroups.groupVersionsEmpty'); 1003 // My Library 1004 setResponse({ 1005 method: "GET", 1006 url: "users/1/settings", 1007 status: 200, 1008 headers: { 1009 "Last-Modified-Version": 5 1010 }, 1011 json: { 1012 INVALID: true // TODO: Find a cleaner error 1013 } 1014 }); 1015 1016 spy = sinon.spy(runner, "updateIcons"); 1017 yield runner.sync(); 1018 assert.isTrue(spy.calledTwice); 1019 assert.isArray(spy.args[1][0]); 1020 assert.lengthOf(spy.args[1][0], 1); 1021 // Not an instance of Error for some reason 1022 var error = spy.args[1][0][0]; 1023 assert.equal(Object.getPrototypeOf(error).constructor.name, "Error"); 1024 }); 1025 1026 1027 it("should show a custom button in the error panel", function* () { 1028 win = yield loadZoteroPane(); 1029 var libraryID = Zotero.Libraries.userLibraryID; 1030 1031 setResponse({ 1032 method: "GET", 1033 url: "keys/current", 1034 status: 403, 1035 headers: {}, 1036 text: "Invalid Key" 1037 }); 1038 yield runner.sync({ 1039 background: true 1040 }); 1041 1042 var doc = win.document; 1043 var errorIcon = doc.getElementById('zotero-tb-sync-error'); 1044 assert.isFalse(errorIcon.hidden); 1045 errorIcon.click(); 1046 var panel = win.document.getElementById('zotero-sync-error-panel'); 1047 var buttons = panel.getElementsByTagName('button'); 1048 assert.lengthOf(buttons, 1); 1049 assert.equal(buttons[0].label, Zotero.getString('sync.openSyncPreferences')); 1050 }); 1051 1052 1053 it("should show a button in error panel to select a too-long note", function* () { 1054 win = yield loadZoteroPane(); 1055 var doc = win.document; 1056 1057 var text = "".padStart(256, "a"); 1058 var item = yield createDataObject('item', { itemType: 'note', note: text }); 1059 1060 setResponse('keyInfo.fullAccess'); 1061 setResponse('userGroups.groupVersions'); 1062 setResponse('groups.ownerGroup'); 1063 setResponse('groups.memberGroup'); 1064 1065 server.respond(function (req) { 1066 if (req.method == "POST" && req.url == baseURL + "users/1/items") { 1067 req.respond( 1068 200, 1069 { 1070 "Last-Modified-Version": 5 1071 }, 1072 JSON.stringify({ 1073 successful: {}, 1074 success: {}, 1075 unchanged: {}, 1076 failed: { 1077 "0": { 1078 code: 413, 1079 message: `Note ${Zotero.Utilities.ellipsize(text, 100)} too long` 1080 } 1081 } 1082 }) 1083 ); 1084 } 1085 }); 1086 1087 yield runner.sync({ libraries: [Zotero.Libraries.userLibraryID] }); 1088 1089 var errorIcon = doc.getElementById('zotero-tb-sync-error'); 1090 assert.isFalse(errorIcon.hidden); 1091 errorIcon.click(); 1092 var panel = win.document.getElementById('zotero-sync-error-panel'); 1093 assert.include(panel.innerHTML, text.substr(0, 10)); 1094 var buttons = panel.getElementsByTagName('button'); 1095 assert.lengthOf(buttons, 1); 1096 assert.include(buttons[0].label, Zotero.getString('pane.items.showItemInLibrary')); 1097 }); 1098 1099 1100 // TODO: Test multiple long tags and tags across libraries 1101 describe("Long Tag Fixer", function () { 1102 it("should split a tag", function* () { 1103 win = yield loadZoteroPane(); 1104 1105 var item = yield createDataObject('item'); 1106 var tag = "title;feeling;matter;drum;treatment;caring;earthy;shrill;unit;obedient;hover;healthy;cheap;clever;wren;wicked;clip;shoe;jittery;shape;clear;dime;increase;complete;level;milk;false;infamous;lamentable;measure;cuddly;tasteless;peace;top;pencil;caption;unusual;depressed;frantic"; 1107 item.addTag(tag, 1); 1108 yield item.saveTx(); 1109 1110 setResponse('keyInfo.fullAccess'); 1111 setResponse('userGroups.groupVersions'); 1112 setResponse('groups.ownerGroup'); 1113 setResponse('groups.memberGroup'); 1114 1115 server.respond(function (req) { 1116 if (req.method == "POST" && req.url == baseURL + "users/1/items") { 1117 var json = JSON.parse(req.requestBody); 1118 if (json[0].tags.length == 1) { 1119 req.respond( 1120 200, 1121 { 1122 "Last-Modified-Version": 5 1123 }, 1124 JSON.stringify({ 1125 successful: {}, 1126 success: {}, 1127 unchanged: {}, 1128 failed: { 1129 "0": { 1130 code: 413, 1131 message: "Tag 'title;feeling;matter;drum;treatment;caring;earthy;shrill;unit;obedient;hover…' is too long to sync", 1132 data: { 1133 tag 1134 } 1135 } 1136 } 1137 }) 1138 ); 1139 } 1140 else { 1141 let itemJSON = item.toResponseJSON(); 1142 itemJSON.version = 6; 1143 itemJSON.data.version = 6; 1144 1145 req.respond( 1146 200, 1147 { 1148 "Last-Modified-Version": 6 1149 }, 1150 JSON.stringify({ 1151 successful: { 1152 "0": itemJSON 1153 }, 1154 success: { 1155 "0": json[0].key 1156 }, 1157 unchanged: {}, 1158 failed: {} 1159 }) 1160 ); 1161 } 1162 } 1163 }); 1164 1165 waitForDialog(null, 'accept', 'chrome://zotero/content/longTagFixer.xul'); 1166 yield runner.sync({ libraries: [Zotero.Libraries.userLibraryID] }); 1167 1168 assert.isFalse(Zotero.Tags.getID(tag)); 1169 assert.isNumber(Zotero.Tags.getID('feeling')); 1170 }); 1171 1172 it("should delete a tag", function* () { 1173 win = yield loadZoteroPane(); 1174 1175 var item = yield createDataObject('item'); 1176 var tag = "title;feeling;matter;drum;treatment;caring;earthy;shrill;unit;obedient;hover;healthy;cheap;clever;wren;wicked;clip;shoe;jittery;shape;clear;dime;increase;complete;level;milk;false;infamous;lamentable;measure;cuddly;tasteless;peace;top;pencil;caption;unusual;depressed;frantic"; 1177 item.addTag(tag, 1); 1178 yield item.saveTx(); 1179 1180 setResponse('keyInfo.fullAccess'); 1181 setResponse('userGroups.groupVersions'); 1182 setResponse('groups.ownerGroup'); 1183 setResponse('groups.memberGroup'); 1184 1185 server.respond(function (req) { 1186 if (req.method == "POST" && req.url == baseURL + "users/1/items") { 1187 var json = JSON.parse(req.requestBody); 1188 if (json[0].tags.length == 1) { 1189 req.respond( 1190 200, 1191 { 1192 "Last-Modified-Version": 5 1193 }, 1194 JSON.stringify({ 1195 successful: {}, 1196 success: {}, 1197 unchanged: {}, 1198 failed: { 1199 "0": { 1200 code: 413, 1201 message: "Tag 'title;feeling;matter;drum;treatment;caring;earthy;shrill;unit;obedient;hover…' is too long to sync", 1202 data: { 1203 tag 1204 } 1205 } 1206 } 1207 }) 1208 ); 1209 } 1210 else { 1211 let itemJSON = item.toResponseJSON(); 1212 itemJSON.version = 6; 1213 itemJSON.data.version = 6; 1214 1215 req.respond( 1216 200, 1217 { 1218 "Last-Modified-Version": 6 1219 }, 1220 JSON.stringify({ 1221 successful: { 1222 "0": itemJSON 1223 }, 1224 success: { 1225 "0": json[0].key 1226 }, 1227 unchanged: {}, 1228 failed: {} 1229 }) 1230 ); 1231 } 1232 } 1233 }); 1234 1235 waitForDialog(function (dialog) { 1236 dialog.Zotero_Long_Tag_Fixer.switchMode(2); 1237 }, 'accept', 'chrome://zotero/content/longTagFixer.xul'); 1238 yield runner.sync({ libraries: [Zotero.Libraries.userLibraryID] }); 1239 1240 assert.isFalse(Zotero.Tags.getID(tag)); 1241 assert.isFalse(Zotero.Tags.getID('feeling')); 1242 }); 1243 }); 1244 }); 1245 })