server_connectorTest.js (21215B)
1 "use strict"; 2 3 describe("Connector Server", function () { 4 Components.utils.import("resource://zotero-unit/httpd.js"); 5 var win, connectorServerPath, testServerPath, httpd; 6 var testServerPort = 16213; 7 8 before(function* () { 9 this.timeout(20000); 10 Zotero.Prefs.set("httpServer.enabled", true); 11 yield resetDB({ 12 thisArg: this, 13 skipBundledFiles: true 14 }); 15 yield Zotero.Translators.init(); 16 17 win = yield loadZoteroPane(); 18 connectorServerPath = 'http://127.0.0.1:' + Zotero.Prefs.get('httpServer.port'); 19 }); 20 21 beforeEach(function () { 22 // Alternate ports to prevent exceptions not catchable in JS 23 testServerPort += (testServerPort & 1) ? 1 : -1; 24 testServerPath = 'http://127.0.0.1:' + testServerPort; 25 httpd = new HttpServer(); 26 httpd.start(testServerPort); 27 }); 28 29 afterEach(function* () { 30 var defer = new Zotero.Promise.defer(); 31 httpd.stop(() => defer.resolve()); 32 yield defer.promise; 33 }); 34 35 after(function () { 36 win.close(); 37 }); 38 39 40 describe('/connector/getTranslatorCode', function() { 41 it('should respond with translator code', function* () { 42 var code = 'function detectWeb() {}\nfunction doImport() {}'; 43 var translator = buildDummyTranslator(4, code); 44 sinon.stub(Zotero.Translators, 'get').returns(translator); 45 46 var response = yield Zotero.HTTP.request( 47 'POST', 48 connectorServerPath + "/connector/getTranslatorCode", 49 { 50 headers: { 51 "Content-Type": "application/json" 52 }, 53 body: JSON.stringify({ 54 translatorID: "dummy-translator", 55 }) 56 } 57 ); 58 59 assert.isTrue(Zotero.Translators.get.calledWith('dummy-translator')); 60 let translatorCode = yield translator.getCode(); 61 assert.equal(response.response, translatorCode); 62 63 Zotero.Translators.get.restore(); 64 }) 65 }); 66 67 68 describe("/connector/detect", function() { 69 it("should return relevant translators with proxies", function* () { 70 var code = 'function detectWeb() {return "newspaperArticle";}\nfunction doWeb() {}'; 71 var translator = buildDummyTranslator("web", code, {target: "https://www.example.com/.*"}); 72 sinon.stub(Zotero.Translators, 'getAllForType').resolves([translator]); 73 74 var response = yield Zotero.HTTP.request( 75 'POST', 76 connectorServerPath + "/connector/detect", 77 { 78 headers: { 79 "Content-Type": "application/json" 80 }, 81 body: JSON.stringify({ 82 uri: "https://www-example-com.proxy.example.com/article", 83 html: "<head><title>Owl</title></head><body><p>🦉</p></body>" 84 }) 85 } 86 ); 87 88 assert.equal(JSON.parse(response.response)[0].proxy.scheme, 'https://%h.proxy.example.com/%p'); 89 90 Zotero.Translators.getAllForType.restore(); 91 }); 92 }); 93 94 95 describe("/connector/saveItems", function () { 96 // TODO: Test cookies 97 it("should save a translated item to the current selected collection", function* () { 98 var collection = yield createDataObject('collection'); 99 yield waitForItemsLoad(win); 100 101 var body = { 102 items: [ 103 { 104 itemType: "newspaperArticle", 105 title: "Title", 106 creators: [ 107 { 108 firstName: "First", 109 lastName: "Last", 110 creatorType: "author" 111 } 112 ], 113 attachments: [ 114 { 115 title: "Attachment", 116 url: `${testServerPath}/attachment`, 117 mimeType: "text/html" 118 } 119 ] 120 } 121 ], 122 uri: "http://example.com" 123 }; 124 125 httpd.registerPathHandler( 126 "/attachment", 127 { 128 handle: function (request, response) { 129 response.setStatusLine(null, 200, "OK"); 130 response.write("<html><head><title>Title</title><body>Body</body></html>"); 131 } 132 } 133 ); 134 135 var promise = waitForItemEvent('add'); 136 var reqPromise = Zotero.HTTP.request( 137 'POST', 138 connectorServerPath + "/connector/saveItems", 139 { 140 headers: { 141 "Content-Type": "application/json" 142 }, 143 body: JSON.stringify(body) 144 } 145 ); 146 147 // Check parent item 148 var ids = yield promise; 149 assert.lengthOf(ids, 1); 150 var item = Zotero.Items.get(ids[0]); 151 assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'newspaperArticle'); 152 assert.isTrue(collection.hasItem(item.id)); 153 154 // Check attachment 155 promise = waitForItemEvent('add'); 156 ids = yield promise; 157 assert.lengthOf(ids, 1); 158 item = Zotero.Items.get(ids[0]); 159 assert.isTrue(item.isImportedAttachment()); 160 161 var req = yield reqPromise; 162 assert.equal(req.status, 201); 163 }); 164 165 166 it("should switch to My Library if read-only library is selected", function* () { 167 var group = yield createGroup({ 168 editable: false 169 }); 170 yield selectLibrary(win, group.libraryID); 171 yield waitForItemsLoad(win); 172 173 var body = { 174 items: [ 175 { 176 itemType: "newspaperArticle", 177 title: "Title", 178 creators: [ 179 { 180 firstName: "First", 181 lastName: "Last", 182 creatorType: "author" 183 } 184 ], 185 attachments: [] 186 } 187 ], 188 uri: "http://example.com" 189 }; 190 191 var promise = waitForItemEvent('add'); 192 var reqPromise = Zotero.HTTP.request( 193 'POST', 194 connectorServerPath + "/connector/saveItems", 195 { 196 headers: { 197 "Content-Type": "application/json" 198 }, 199 body: JSON.stringify(body), 200 successCodes: false 201 } 202 ); 203 204 // My Library be selected, and the item should be in it 205 var ids = yield promise; 206 assert.equal( 207 win.ZoteroPane.collectionsView.getSelectedLibraryID(), 208 Zotero.Libraries.userLibraryID 209 ); 210 assert.lengthOf(ids, 1); 211 var item = Zotero.Items.get(ids[0]); 212 assert.equal(item.libraryID, Zotero.Libraries.userLibraryID); 213 assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'newspaperArticle'); 214 215 var req = yield reqPromise; 216 assert.equal(req.status, 201); 217 }); 218 219 it("should use the provided proxy to deproxify item url", function* () { 220 yield selectLibrary(win, Zotero.Libraries.userLibraryID); 221 yield waitForItemsLoad(win); 222 223 var body = { 224 items: [ 225 { 226 itemType: "newspaperArticle", 227 title: "Title", 228 creators: [ 229 { 230 firstName: "First", 231 lastName: "Last", 232 creatorType: "author" 233 } 234 ], 235 attachments: [], 236 url: "https://www-example-com.proxy.example.com/path" 237 } 238 ], 239 uri: "https://www-example-com.proxy.example.com/path", 240 proxy: {scheme: 'https://%h.proxy.example.com/%p', dotsToHyphens: true} 241 }; 242 243 var promise = waitForItemEvent('add'); 244 var req = yield Zotero.HTTP.request( 245 'POST', 246 connectorServerPath + "/connector/saveItems", 247 { 248 headers: { 249 "Content-Type": "application/json" 250 }, 251 body: JSON.stringify(body) 252 } 253 ); 254 255 // Check item 256 var ids = yield promise; 257 assert.lengthOf(ids, 1); 258 var item = Zotero.Items.get(ids[0]); 259 assert.equal(item.getField('url'), 'https://www.example.com/path'); 260 }); 261 }); 262 263 describe("/connector/saveSnapshot", function () { 264 it("should save a webpage item and snapshot to the current selected collection", function* () { 265 var collection = yield createDataObject('collection'); 266 yield waitForItemsLoad(win); 267 268 // saveSnapshot saves parent and child before returning 269 var ids1, ids2; 270 var promise = waitForItemEvent('add').then(function (ids) { 271 ids1 = ids; 272 return waitForItemEvent('add').then(function (ids) { 273 ids2 = ids; 274 }); 275 }); 276 yield Zotero.HTTP.request( 277 'POST', 278 connectorServerPath + "/connector/saveSnapshot", 279 { 280 headers: { 281 "Content-Type": "application/json" 282 }, 283 body: JSON.stringify({ 284 url: "http://example.com", 285 html: "<html><head><title>Title</title><body>Body</body></html>" 286 }) 287 } 288 ); 289 290 assert.isTrue(promise.isFulfilled()); 291 292 // Check parent item 293 assert.lengthOf(ids1, 1); 294 var item = Zotero.Items.get(ids1[0]); 295 assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'webpage'); 296 assert.isTrue(collection.hasItem(item.id)); 297 assert.equal(item.getField('title'), 'Title'); 298 299 // Check attachment 300 assert.lengthOf(ids2, 1); 301 item = Zotero.Items.get(ids2[0]); 302 assert.isTrue(item.isImportedAttachment()); 303 assert.equal(item.getField('title'), 'Title'); 304 }); 305 306 it("should save a PDF to the current selected collection and retrieve metadata", async function () { 307 var collection = await createDataObject('collection'); 308 await waitForItemsLoad(win); 309 310 var file = getTestDataDirectory(); 311 file.append('test.pdf'); 312 httpd.registerFile("/test.pdf", file); 313 314 var promise = waitForItemEvent('add'); 315 var recognizerPromise = waitForRecognizer(); 316 317 var origRequest = Zotero.HTTP.request.bind(Zotero.HTTP); 318 var called = 0; 319 var stub = sinon.stub(Zotero.HTTP, 'request').callsFake(function (method, url, options) { 320 // Forward saveSnapshot request 321 if (url.endsWith('saveSnapshot')) { 322 return origRequest(...arguments); 323 } 324 325 // Fake recognizer response 326 return Zotero.Promise.resolve({ 327 getResponseHeader: () => {}, 328 responseText: JSON.stringify({ 329 title: 'Test', 330 authors: [] 331 }) 332 }); 333 }); 334 335 await Zotero.HTTP.request( 336 'POST', 337 connectorServerPath + "/connector/saveSnapshot", 338 { 339 headers: { 340 "Content-Type": "application/json" 341 }, 342 body: JSON.stringify({ 343 url: testServerPath + "/test.pdf", 344 pdf: true 345 }) 346 } 347 ); 348 349 var ids = await promise; 350 351 assert.lengthOf(ids, 1); 352 var item = Zotero.Items.get(ids[0]); 353 assert.isTrue(item.isImportedAttachment()); 354 assert.equal(item.attachmentContentType, 'application/pdf'); 355 assert.isTrue(collection.hasItem(item.id)); 356 357 var progressWindow = await recognizerPromise; 358 progressWindow.close(); 359 Zotero.RecognizePDF.cancel(); 360 assert.isFalse(item.isTopLevelItem()); 361 362 stub.restore(); 363 }); 364 365 it("should switch to My Library if a read-only library is selected", function* () { 366 var group = yield createGroup({ 367 editable: false 368 }); 369 yield selectLibrary(win, group.libraryID); 370 yield waitForItemsLoad(win); 371 372 var promise = waitForItemEvent('add'); 373 var reqPromise = Zotero.HTTP.request( 374 'POST', 375 connectorServerPath + "/connector/saveSnapshot", 376 { 377 headers: { 378 "Content-Type": "application/json" 379 }, 380 body: JSON.stringify({ 381 url: "http://example.com", 382 html: "<html><head><title>Title</title><body>Body</body></html>" 383 }), 384 successCodes: false 385 } 386 ); 387 388 // My Library be selected, and the item should be in it 389 var ids = yield promise; 390 assert.equal( 391 win.ZoteroPane.collectionsView.getSelectedLibraryID(), 392 Zotero.Libraries.userLibraryID 393 ); 394 assert.lengthOf(ids, 1); 395 var item = Zotero.Items.get(ids[0]); 396 assert.equal(item.libraryID, Zotero.Libraries.userLibraryID); 397 398 var req = yield reqPromise; 399 assert.equal(req.status, 201); 400 }); 401 }); 402 403 describe("/connector/savePage", function() { 404 before(async function () { 405 await selectLibrary(win); 406 await waitForItemsLoad(win); 407 }); 408 409 it("should return 500 if no translator available for page", function* () { 410 var xmlhttp = yield Zotero.HTTP.request( 411 'POST', 412 connectorServerPath + "/connector/savePage", 413 { 414 headers: { 415 "Content-Type": "application/json" 416 }, 417 body: JSON.stringify({ 418 uri: "http://example.com", 419 html: "<html><head><title>Title</title><body>Body</body></html>" 420 }), 421 successCodes: false 422 } 423 ); 424 assert.equal(xmlhttp.status, 500); 425 }); 426 427 it("should translate a page if translators are available", function* () { 428 var html = Zotero.File.getContentsFromURL(getTestDataUrl('coins.html')); 429 var promise = waitForItemEvent('add'); 430 var xmlhttp = yield Zotero.HTTP.request( 431 'POST', 432 connectorServerPath + "/connector/savePage", 433 { 434 headers: { 435 "Content-Type": "application/json" 436 }, 437 body: JSON.stringify({ 438 uri: "https://example.com/test", 439 html 440 }), 441 successCodes: false 442 } 443 ); 444 445 let ids = yield promise; 446 var item = Zotero.Items.get(ids[0]); 447 var title = "Test Page"; 448 assert.equal(JSON.parse(xmlhttp.responseText).items[0].title, title); 449 assert.equal(item.getField('title'), title); 450 assert.equal(xmlhttp.status, 201); 451 }); 452 }); 453 454 describe("/connector/updateSession", function () { 455 it("should update collections and tags of item saved via /saveItems", async function () { 456 var collection1 = await createDataObject('collection'); 457 var collection2 = await createDataObject('collection'); 458 await waitForItemsLoad(win); 459 460 var sessionID = Zotero.Utilities.randomString(); 461 var body = { 462 sessionID, 463 items: [ 464 { 465 itemType: "newspaperArticle", 466 title: "Title", 467 creators: [ 468 { 469 firstName: "First", 470 lastName: "Last", 471 creatorType: "author" 472 } 473 ], 474 attachments: [ 475 { 476 title: "Attachment", 477 url: `${testServerPath}/attachment`, 478 mimeType: "text/html" 479 } 480 ] 481 } 482 ], 483 uri: "http://example.com" 484 }; 485 486 httpd.registerPathHandler( 487 "/attachment", 488 { 489 handle: function (request, response) { 490 response.setStatusLine(null, 200, "OK"); 491 response.write("<html><head><title>Title</title><body>Body</body></html>"); 492 } 493 } 494 ); 495 496 var reqPromise = Zotero.HTTP.request( 497 'POST', 498 connectorServerPath + "/connector/saveItems", 499 { 500 headers: { 501 "Content-Type": "application/json" 502 }, 503 body: JSON.stringify(body) 504 } 505 ); 506 507 var ids = await waitForItemEvent('add'); 508 var item = Zotero.Items.get(ids[0]); 509 assert.isTrue(collection2.hasItem(item.id)); 510 await waitForItemEvent('add'); 511 512 var req = await reqPromise; 513 assert.equal(req.status, 201); 514 515 // Update saved item 516 var req = await Zotero.HTTP.request( 517 'POST', 518 connectorServerPath + "/connector/updateSession", 519 { 520 headers: { 521 "Content-Type": "application/json" 522 }, 523 body: JSON.stringify({ 524 sessionID, 525 target: collection1.treeViewID, 526 tags: "A, B" 527 }) 528 } 529 ); 530 531 assert.equal(req.status, 200); 532 assert.isTrue(collection1.hasItem(item.id)); 533 assert.isTrue(item.hasTag("A")); 534 assert.isTrue(item.hasTag("B")); 535 }); 536 537 it("should update collections and tags of PDF saved via /saveSnapshot", async function () { 538 var sessionID = Zotero.Utilities.randomString(); 539 540 var collection1 = await createDataObject('collection'); 541 var collection2 = await createDataObject('collection'); 542 await waitForItemsLoad(win); 543 544 var file = getTestDataDirectory(); 545 file.append('test.pdf'); 546 httpd.registerFile("/test.pdf", file); 547 548 var ids; 549 var promise = waitForItemEvent('add'); 550 var reqPromise = Zotero.HTTP.request( 551 'POST', 552 connectorServerPath + "/connector/saveSnapshot", 553 { 554 headers: { 555 "Content-Type": "application/json" 556 }, 557 body: JSON.stringify({ 558 sessionID, 559 url: testServerPath + "/test.pdf", 560 pdf: true 561 }) 562 } 563 ); 564 565 var ids = await promise; 566 var item = Zotero.Items.get(ids[0]); 567 assert.isTrue(collection2.hasItem(item.id)); 568 var req = await reqPromise; 569 assert.equal(req.status, 201); 570 571 // Update saved item 572 var req = await Zotero.HTTP.request( 573 'POST', 574 connectorServerPath + "/connector/updateSession", 575 { 576 headers: { 577 "Content-Type": "application/json" 578 }, 579 body: JSON.stringify({ 580 sessionID, 581 target: collection1.treeViewID, 582 tags: "A, B" 583 }) 584 } 585 ); 586 587 assert.equal(req.status, 200); 588 assert.isTrue(collection1.hasItem(item.id)); 589 assert.isTrue(item.hasTag("A")); 590 assert.isTrue(item.hasTag("B")); 591 }); 592 593 it("should update collections and tags of webpage saved via /saveSnapshot", async function () { 594 var sessionID = Zotero.Utilities.randomString(); 595 596 var collection1 = await createDataObject('collection'); 597 var collection2 = await createDataObject('collection'); 598 await waitForItemsLoad(win); 599 600 // saveSnapshot saves parent and child before returning 601 var ids1, ids2; 602 var promise = waitForItemEvent('add').then(function (ids) { 603 ids1 = ids; 604 return waitForItemEvent('add').then(function (ids) { 605 ids2 = ids; 606 }); 607 }); 608 await Zotero.HTTP.request( 609 'POST', 610 connectorServerPath + "/connector/saveSnapshot", 611 { 612 headers: { 613 "Content-Type": "application/json" 614 }, 615 body: JSON.stringify({ 616 sessionID, 617 url: "http://example.com", 618 html: "<html><head><title>Title</title><body>Body</body></html>" 619 }) 620 } 621 ); 622 623 assert.isTrue(promise.isFulfilled()); 624 625 var item = Zotero.Items.get(ids1[0]); 626 627 // Update saved item 628 var req = await Zotero.HTTP.request( 629 'POST', 630 connectorServerPath + "/connector/updateSession", 631 { 632 headers: { 633 "Content-Type": "application/json" 634 }, 635 body: JSON.stringify({ 636 sessionID, 637 target: collection1.treeViewID, 638 tags: "A, B" 639 }) 640 } 641 ); 642 643 assert.equal(req.status, 200); 644 assert.isTrue(collection1.hasItem(item.id)); 645 assert.isTrue(item.hasTag("A")); 646 assert.isTrue(item.hasTag("B")); 647 }); 648 }); 649 650 describe('/connector/installStyle', function() { 651 var endpoint; 652 653 before(function() { 654 endpoint = connectorServerPath + "/connector/installStyle"; 655 }); 656 657 it('should reject styles with invalid text', function* () { 658 var error = yield getPromiseError(Zotero.HTTP.request( 659 'POST', 660 endpoint, 661 { 662 headers: { "Content-Type": "application/json" }, 663 body: '{}' 664 } 665 )); 666 assert.instanceOf(error, Zotero.HTTP.UnexpectedStatusException); 667 assert.equal(error.xmlhttp.status, 400); 668 assert.equal(error.xmlhttp.responseText, Zotero.getString("styles.installError", "(null)")); 669 }); 670 671 it('should import a style with application/vnd.citationstyles.style+xml content-type', function* () { 672 sinon.stub(Zotero.Styles, 'install').callsFake(function(style) { 673 var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] 674 .createInstance(Components.interfaces.nsIDOMParser), 675 doc = parser.parseFromString(style, "application/xml"); 676 677 return Zotero.Promise.resolve( 678 Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:title[1]', 679 Zotero.Styles.ns) 680 ); 681 }); 682 683 var style = `<?xml version="1.0" encoding="utf-8"?> 684 <style xmlns="http://purl.org/net/xbiblio/csl" version="1.0" default-locale="de-DE"> 685 <info> 686 <title>Test1</title> 687 <id>http://www.example.com/test2</id> 688 <link href="http://www.zotero.org/styles/cell" rel="independent-parent"/> 689 </info> 690 </style> 691 `; 692 var response = yield Zotero.HTTP.request( 693 'POST', 694 endpoint, 695 { 696 headers: { "Content-Type": "application/vnd.citationstyles.style+xml" }, 697 body: style 698 } 699 ); 700 assert.equal(response.status, 201); 701 assert.equal(response.response, JSON.stringify({name: 'Test1'})); 702 Zotero.Styles.install.restore(); 703 }); 704 }); 705 706 describe('/connector/import', function() { 707 var endpoint; 708 709 before(function() { 710 endpoint = connectorServerPath + "/connector/import"; 711 }); 712 713 it('should reject resources that do not contain import data', function* () { 714 var error = yield getPromiseError(Zotero.HTTP.request( 715 'POST', 716 endpoint, 717 { 718 headers: { "Content-Type": "text/plain" }, 719 body: 'Owl' 720 } 721 )); 722 assert.instanceOf(error, Zotero.HTTP.UnexpectedStatusException); 723 assert.equal(error.xmlhttp.status, 400); 724 }); 725 726 it('should import resources (BibTeX) into selected collection', function* () { 727 var collection = yield createDataObject('collection'); 728 yield waitForItemsLoad(win); 729 730 var resource = `@book{test1, 731 title={Test1}, 732 author={Owl}, 733 year={1000}, 734 publisher={Curly Braces Publishing}, 735 keywords={A, B} 736 }`; 737 738 var addedItemIDsPromise = waitForItemEvent('add'); 739 var req = yield Zotero.HTTP.request( 740 'POST', 741 endpoint, 742 { 743 headers: { "Content-Type": "application/x-bibtex" }, 744 body: resource 745 } 746 ); 747 assert.equal(req.status, 201); 748 assert.equal(JSON.parse(req.responseText)[0].title, 'Test1'); 749 750 let itemIDs = yield addedItemIDsPromise; 751 assert.isTrue(collection.hasItem(itemIDs[0])); 752 var item = Zotero.Items.get(itemIDs[0]); 753 assert.sameDeepMembers(item.getTags(), [{ tag: 'A', type: 1 }, { tag: 'B', type: 1 }]); 754 }); 755 756 757 it('should switch to My Library if read-only library is selected', function* () { 758 var group = yield createGroup({ 759 editable: false 760 }); 761 yield selectLibrary(win, group.libraryID); 762 yield waitForItemsLoad(win); 763 764 var resource = `@book{test1, 765 title={Test1}, 766 author={Owl}, 767 year={1000}, 768 publisher={Curly Braces Publishing} 769 }`; 770 771 var addedItemIDsPromise = waitForItemEvent('add'); 772 var req = yield Zotero.HTTP.request( 773 'POST', 774 endpoint, 775 { 776 headers: { "Content-Type": "application/x-bibtex" }, 777 body: resource, 778 successCodes: false 779 } 780 ); 781 782 assert.equal(req.status, 201); 783 assert.equal( 784 win.ZoteroPane.collectionsView.getSelectedLibraryID(), 785 Zotero.Libraries.userLibraryID 786 ); 787 788 let itemIDs = yield addedItemIDsPromise; 789 var item = Zotero.Items.get(itemIDs[0]); 790 assert.equal(item.libraryID, Zotero.Libraries.userLibraryID); 791 }); 792 }); 793 });