"use strict"; describe("Connector Server", function () { Components.utils.import("resource://zotero-unit/httpd.js"); var win, connectorServerPath, testServerPath, httpd; var testServerPort = 16213; before(function* () { this.timeout(20000); Zotero.Prefs.set("httpServer.enabled", true); yield resetDB({ thisArg: this, skipBundledFiles: true }); yield Zotero.Translators.init(); win = yield loadZoteroPane(); connectorServerPath = 'http://127.0.0.1:' + Zotero.Prefs.get('httpServer.port'); }); beforeEach(function () { // Alternate ports to prevent exceptions not catchable in JS testServerPort += (testServerPort & 1) ? 1 : -1; testServerPath = 'http://127.0.0.1:' + testServerPort; httpd = new HttpServer(); httpd.start(testServerPort); }); afterEach(function* () { var defer = new Zotero.Promise.defer(); httpd.stop(() => defer.resolve()); yield defer.promise; }); after(function () { win.close(); }); describe('/connector/getTranslatorCode', function() { it('should respond with translator code', function* () { var code = 'function detectWeb() {}\nfunction doImport() {}'; var translator = buildDummyTranslator(4, code); sinon.stub(Zotero.Translators, 'get').returns(translator); var response = yield Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/getTranslatorCode", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ translatorID: "dummy-translator", }) } ); assert.isTrue(Zotero.Translators.get.calledWith('dummy-translator')); let translatorCode = yield translator.getCode(); assert.equal(response.response, translatorCode); Zotero.Translators.get.restore(); }) }); describe("/connector/detect", function() { it("should return relevant translators with proxies", function* () { var code = 'function detectWeb() {return "newspaperArticle";}\nfunction doWeb() {}'; var translator = buildDummyTranslator("web", code, {target: "https://www.example.com/.*"}); sinon.stub(Zotero.Translators, 'getAllForType').resolves([translator]); var response = yield Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/detect", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uri: "https://www-example-com.proxy.example.com/article", html: "Owl

🦉

" }) } ); assert.equal(JSON.parse(response.response)[0].proxy.scheme, 'https://%h.proxy.example.com/%p'); Zotero.Translators.getAllForType.restore(); }); }); describe("/connector/saveItems", function () { // TODO: Test cookies it("should save a translated item to the current selected collection", function* () { var collection = yield createDataObject('collection'); yield waitForItemsLoad(win); var body = { items: [ { itemType: "newspaperArticle", title: "Title", creators: [ { firstName: "First", lastName: "Last", creatorType: "author" } ], attachments: [ { title: "Attachment", url: `${testServerPath}/attachment`, mimeType: "text/html" } ] } ], uri: "http://example.com" }; httpd.registerPathHandler( "/attachment", { handle: function (request, response) { response.setStatusLine(null, 200, "OK"); response.write("TitleBody"); } } ); var promise = waitForItemEvent('add'); var reqPromise = Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/saveItems", { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } ); // Check parent item var ids = yield promise; assert.lengthOf(ids, 1); var item = Zotero.Items.get(ids[0]); assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'newspaperArticle'); assert.isTrue(collection.hasItem(item.id)); // Check attachment promise = waitForItemEvent('add'); ids = yield promise; assert.lengthOf(ids, 1); item = Zotero.Items.get(ids[0]); assert.isTrue(item.isImportedAttachment()); var req = yield reqPromise; assert.equal(req.status, 201); }); it("should switch to My Library if read-only library is selected", function* () { var group = yield createGroup({ editable: false }); yield selectLibrary(win, group.libraryID); yield waitForItemsLoad(win); var body = { items: [ { itemType: "newspaperArticle", title: "Title", creators: [ { firstName: "First", lastName: "Last", creatorType: "author" } ], attachments: [] } ], uri: "http://example.com" }; var promise = waitForItemEvent('add'); var reqPromise = Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/saveItems", { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), successCodes: false } ); // My Library be selected, and the item should be in it var ids = yield promise; assert.equal( win.ZoteroPane.collectionsView.getSelectedLibraryID(), Zotero.Libraries.userLibraryID ); assert.lengthOf(ids, 1); var item = Zotero.Items.get(ids[0]); assert.equal(item.libraryID, Zotero.Libraries.userLibraryID); assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'newspaperArticle'); var req = yield reqPromise; assert.equal(req.status, 201); }); it("should use the provided proxy to deproxify item url", function* () { yield selectLibrary(win, Zotero.Libraries.userLibraryID); yield waitForItemsLoad(win); var body = { items: [ { itemType: "newspaperArticle", title: "Title", creators: [ { firstName: "First", lastName: "Last", creatorType: "author" } ], attachments: [], url: "https://www-example-com.proxy.example.com/path" } ], uri: "https://www-example-com.proxy.example.com/path", proxy: {scheme: 'https://%h.proxy.example.com/%p', dotsToHyphens: true} }; var promise = waitForItemEvent('add'); var req = yield Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/saveItems", { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } ); // Check item var ids = yield promise; assert.lengthOf(ids, 1); var item = Zotero.Items.get(ids[0]); assert.equal(item.getField('url'), 'https://www.example.com/path'); }); it("shouldn't return an attachment that isn't being saved", async function () { Zotero.Prefs.set('automaticSnapshots', false); await selectLibrary(win, Zotero.Libraries.userLibraryID); await waitForItemsLoad(win); var body = { items: [ { itemType: "webpage", title: "Title", creators: [], attachments: [ { url: "http://example.com/", mimeType: "text/html" } ], url: "http://example.com/" } ], uri: "http://example.com/" }; var req = await Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/saveItems", { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), responseType: 'json' } ); Zotero.Prefs.clear('automaticSnapshots'); assert.equal(req.status, 201); assert.lengthOf(req.response.items, 1); assert.lengthOf(req.response.items[0].attachments, 0); }); describe("PDF retrieval", function () { var oaDOI = '10.1111/abcd'; var nonOADOI = '10.2222/bcde'; var pdfURL; var badPDFURL; var stub; before(function () { var origFunc = Zotero.HTTP.request.bind(Zotero.HTTP); stub = sinon.stub(Zotero.HTTP, 'request'); stub.callsFake(function (method, url, options) { // OA PDF lookup if (url.startsWith(ZOTERO_CONFIG.SERVICES_URL)) { let json = JSON.parse(options.body); let response = []; if (json.doi == oaDOI) { response.push({ url: pdfURL, version: 'submittedVersion' }); } return { status: 200, response }; } return origFunc(...arguments); }); }); beforeEach(() => { pdfURL = testServerPath + '/pdf'; badPDFURL = testServerPath + '/badpdf'; httpd.registerFile( pdfURL.substr(testServerPath.length), Zotero.File.pathToFile(OS.Path.join(getTestDataDirectory().path, 'test.pdf')) ); // PDF URL that's actually an HTML page httpd.registerFile( badPDFURL.substr(testServerPath.length), Zotero.File.pathToFile(OS.Path.join(getTestDataDirectory().path, 'test.html')) ); }); afterEach(() => { stub.resetHistory(); }); after(() => { stub.restore(); }); it("should download a translated PDF", async function () { var collection = await createDataObject('collection'); await waitForItemsLoad(win); var sessionID = Zotero.Utilities.randomString(); // Save item var itemAddPromise = waitForItemEvent('add'); var saveItemsReq = await Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/saveItems", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionID, items: [ { itemType: 'journalArticle', title: 'Title', DOI: nonOADOI, attachments: [ { title: "PDF", url: pdfURL, mimeType: 'application/pdf' } ] } ], uri: 'http://website/article' }), responseType: 'json' } ); assert.equal(saveItemsReq.status, 201); assert.lengthOf(saveItemsReq.response.items, 1); // Translated attachment should show up in the initial response assert.lengthOf(saveItemsReq.response.items[0].attachments, 1); assert.notProperty(saveItemsReq.response.items[0], 'DOI'); assert.notProperty(saveItemsReq.response.items[0].attachments[0], 'progress'); // Check parent item var ids = await itemAddPromise; assert.lengthOf(ids, 1); var item = Zotero.Items.get(ids[0]); assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'journalArticle'); assert.isTrue(collection.hasItem(item.id)); // Legacy endpoint should show 0 let attachmentProgressReq = await Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/attachmentProgress", { headers: { "Content-Type": "application/json" }, body: JSON.stringify([saveItemsReq.response.items[0].attachments[0].id]), responseType: 'json' } ); assert.equal(attachmentProgressReq.status, 200); let progress = attachmentProgressReq.response; assert.sameOrderedMembers(progress, [0]); // Wait for the attachment to finish saving itemAddPromise = waitForItemEvent('add'); var i = 0; while (i < 3) { let sessionProgressReq = await Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/sessionProgress", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionID }), responseType: 'json' } ); assert.equal(sessionProgressReq.status, 200); let response = sessionProgressReq.response; assert.lengthOf(response.items, 1); let item = response.items[0]; if (item.attachments.length) { let attachments = item.attachments; assert.lengthOf(attachments, 1); let attachment = attachments[0]; switch (i) { // Translated PDF in progress case 0: if (attachment.title == "PDF" && Number.isInteger(attachment.progress) && attachment.progress < 100) { assert.isFalse(response.done); i++; } continue; // Translated PDF finished case 1: if (attachment.title == "PDF" && attachment.progress == 100) { i++; } continue; // done: true case 2: if (response.done) { i++; } continue; } } await Zotero.Promise.delay(10); } // Legacy endpoint should show 100 attachmentProgressReq = await Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/attachmentProgress", { headers: { "Content-Type": "application/json" }, body: JSON.stringify([saveItemsReq.response.items[0].attachments[0].id]), responseType: 'json' } ); assert.equal(attachmentProgressReq.status, 200); progress = attachmentProgressReq.response; assert.sameOrderedMembers(progress, [100]); // Check attachment var ids = await itemAddPromise; assert.lengthOf(ids, 1); item = Zotero.Items.get(ids[0]); assert.isTrue(item.isImportedAttachment()); assert.equal(item.getField('title'), 'PDF'); }); it("should download open-access PDF if no PDF provided", async function () { var collection = await createDataObject('collection'); await waitForItemsLoad(win); var sessionID = Zotero.Utilities.randomString(); // Save item var itemAddPromise = waitForItemEvent('add'); var saveItemsReq = await Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/saveItems", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionID, items: [ { itemType: 'journalArticle', title: 'Title', DOI: oaDOI, attachments: [] } ], uri: 'http://website/article' }), responseType: 'json' } ); assert.equal(saveItemsReq.status, 201); assert.lengthOf(saveItemsReq.response.items, 1); // Attachment shouldn't show up in the initial response assert.lengthOf(saveItemsReq.response.items[0].attachments, 0); // Check parent item var ids = await itemAddPromise; assert.lengthOf(ids, 1); var item = Zotero.Items.get(ids[0]); assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'journalArticle'); assert.isTrue(collection.hasItem(item.id)); // Wait for the attachment to finish saving itemAddPromise = waitForItemEvent('add'); var wasZero = false; var was100 = false; while (true) { let sessionProgressReq = await Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/sessionProgress", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionID }), responseType: 'json' } ); assert.equal(sessionProgressReq.status, 200); let response = sessionProgressReq.response; assert.typeOf(response.items, 'array'); assert.lengthOf(response.items, 1); let item = response.items[0]; if (item.attachments.length) { // 'progress' should have started at 0 if (item.attachments[0].progress === 0) { wasZero = true; } else if (!was100 && item.attachments[0].progress == 100) { if (response.done) { break; } was100 = true; } else if (response.done) { break; } } assert.isFalse(response.done); await Zotero.Promise.delay(10); } assert.isTrue(wasZero); // Check attachment var ids = await itemAddPromise; assert.lengthOf(ids, 1); item = Zotero.Items.get(ids[0]); assert.isTrue(item.isImportedAttachment()); assert.equal(item.getField('title'), Zotero.getString('attachment.submittedVersion')); }); it("should download open-access PDF if a translated PDF fails", async function () { var collection = await createDataObject('collection'); await waitForItemsLoad(win); var sessionID = Zotero.Utilities.randomString(); // Save item var itemAddPromise = waitForItemEvent('add'); var saveItemsReq = await Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/saveItems", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionID, items: [ { itemType: 'journalArticle', title: 'Title', DOI: oaDOI, attachments: [ { title: "PDF", url: badPDFURL, mimeType: 'application/pdf' } ] } ], uri: 'http://website/article' }), responseType: 'json' } ); assert.equal(saveItemsReq.status, 201); assert.lengthOf(saveItemsReq.response.items, 1); // Translated attachment should show up in the initial response assert.lengthOf(saveItemsReq.response.items[0].attachments, 1); assert.notProperty(saveItemsReq.response.items[0], 'DOI'); assert.notProperty(saveItemsReq.response.items[0].attachments[0], 'progress'); // Check parent item var ids = await itemAddPromise; assert.lengthOf(ids, 1); var item = Zotero.Items.get(ids[0]); assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'journalArticle'); assert.isTrue(collection.hasItem(item.id)); // Legacy endpoint should show 0 let attachmentProgressReq = await Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/attachmentProgress", { headers: { "Content-Type": "application/json" }, body: JSON.stringify([saveItemsReq.response.items[0].attachments[0].id]), responseType: 'json' } ); assert.equal(attachmentProgressReq.status, 200); let progress = attachmentProgressReq.response; assert.sameOrderedMembers(progress, [0]); // Wait for the attachment to finish saving itemAddPromise = waitForItemEvent('add'); var i = 0; while (i < 4) { let sessionProgressReq = await Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/sessionProgress", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionID }), responseType: 'json' } ); assert.equal(sessionProgressReq.status, 200); let response = sessionProgressReq.response; assert.lengthOf(response.items, 1); let item = response.items[0]; if (item.attachments.length) { let attachments = item.attachments; assert.lengthOf(attachments, 1); let attachment = attachments[0]; switch (i) { // Translated PDF in progress case 0: if (attachment.title == "PDF" && Number.isInteger(attachment.progress) && attachment.progress < 100) { assert.isFalse(response.done); i++; } continue; // OA PDF in progress case 1: if (attachment.title == Zotero.getString('findPDF.openAccessPDF') && Number.isInteger(attachment.progress) && attachment.progress < 100) { assert.isFalse(response.done); i++; } continue; // OA PDF finished case 2: if (attachment.progress === 100) { assert.equal(attachment.title, Zotero.getString('findPDF.openAccessPDF')); i++; } continue; // done: true case 3: if (response.done) { i++; } continue; } } await Zotero.Promise.delay(10); } // Legacy endpoint should show 100 attachmentProgressReq = await Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/attachmentProgress", { headers: { "Content-Type": "application/json" }, body: JSON.stringify([saveItemsReq.response.items[0].attachments[0].id]), responseType: 'json' } ); assert.equal(attachmentProgressReq.status, 200); progress = attachmentProgressReq.response; assert.sameOrderedMembers(progress, [100]); // Check attachment var ids = await itemAddPromise; assert.lengthOf(ids, 1); item = Zotero.Items.get(ids[0]); assert.isTrue(item.isImportedAttachment()); assert.equal(item.getField('title'), Zotero.getString('attachment.submittedVersion')); }); }); }); describe("/connector/saveSnapshot", function () { it("should save a webpage item and snapshot to the current selected collection", function* () { var collection = yield createDataObject('collection'); yield waitForItemsLoad(win); // saveSnapshot saves parent and child before returning var ids1, ids2; var promise = waitForItemEvent('add').then(function (ids) { ids1 = ids; return waitForItemEvent('add').then(function (ids) { ids2 = ids; }); }); yield Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/saveSnapshot", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: "http://example.com", html: "TitleBody" }) } ); assert.isTrue(promise.isFulfilled()); // Check parent item assert.lengthOf(ids1, 1); var item = Zotero.Items.get(ids1[0]); assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'webpage'); assert.isTrue(collection.hasItem(item.id)); assert.equal(item.getField('title'), 'Title'); // Check attachment assert.lengthOf(ids2, 1); item = Zotero.Items.get(ids2[0]); assert.isTrue(item.isImportedAttachment()); assert.equal(item.getField('title'), 'Title'); }); it("should save a PDF to the current selected collection and retrieve metadata", async function () { var collection = await createDataObject('collection'); await waitForItemsLoad(win); var file = getTestDataDirectory(); file.append('test.pdf'); httpd.registerFile("/test.pdf", file); var promise = waitForItemEvent('add'); var recognizerPromise = waitForRecognizer(); var origRequest = Zotero.HTTP.request.bind(Zotero.HTTP); var called = 0; var stub = sinon.stub(Zotero.HTTP, 'request').callsFake(function (method, url, options) { // Forward saveSnapshot request if (url.endsWith('saveSnapshot')) { return origRequest(...arguments); } // Fake recognizer response return Zotero.Promise.resolve({ getResponseHeader: () => {}, responseText: JSON.stringify({ title: 'Test', authors: [] }) }); }); await Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/saveSnapshot", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: testServerPath + "/test.pdf", pdf: true }) } ); var ids = await promise; assert.lengthOf(ids, 1); var item = Zotero.Items.get(ids[0]); assert.isTrue(item.isImportedAttachment()); assert.equal(item.attachmentContentType, 'application/pdf'); assert.isTrue(collection.hasItem(item.id)); var progressWindow = await recognizerPromise; progressWindow.close(); Zotero.RecognizePDF.cancel(); assert.isFalse(item.isTopLevelItem()); stub.restore(); }); it("should switch to My Library if a read-only library is selected", function* () { var group = yield createGroup({ editable: false }); yield selectLibrary(win, group.libraryID); yield waitForItemsLoad(win); var promise = waitForItemEvent('add'); var reqPromise = Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/saveSnapshot", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: "http://example.com", html: "TitleBody" }), successCodes: false } ); // My Library be selected, and the item should be in it var ids = yield promise; assert.equal( win.ZoteroPane.collectionsView.getSelectedLibraryID(), Zotero.Libraries.userLibraryID ); assert.lengthOf(ids, 1); var item = Zotero.Items.get(ids[0]); assert.equal(item.libraryID, Zotero.Libraries.userLibraryID); var req = yield reqPromise; assert.equal(req.status, 201); }); }); describe("/connector/savePage", function() { before(async function () { await selectLibrary(win); await waitForItemsLoad(win); }); it("should return 500 if no translator available for page", function* () { var xmlhttp = yield Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/savePage", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uri: "http://example.com", html: "TitleBody" }), successCodes: false } ); assert.equal(xmlhttp.status, 500); }); it("should translate a page if translators are available", function* () { var html = Zotero.File.getContentsFromURL(getTestDataUrl('coins.html')); var promise = waitForItemEvent('add'); var xmlhttp = yield Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/savePage", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uri: "https://example.com/test", html }), successCodes: false } ); let ids = yield promise; var item = Zotero.Items.get(ids[0]); var title = "Test Page"; assert.equal(JSON.parse(xmlhttp.responseText).items[0].title, title); assert.equal(item.getField('title'), title); assert.equal(xmlhttp.status, 201); }); }); describe("/connector/updateSession", function () { it("should update collections and tags of item saved via /saveItems", async function () { var collection1 = await createDataObject('collection'); var collection2 = await createDataObject('collection'); await waitForItemsLoad(win); var sessionID = Zotero.Utilities.randomString(); var body = { sessionID, items: [ { itemType: "newspaperArticle", title: "Title", creators: [ { firstName: "First", lastName: "Last", creatorType: "author" } ], attachments: [ { title: "Attachment", url: `${testServerPath}/attachment`, mimeType: "text/html" } ] } ], uri: "http://example.com" }; httpd.registerPathHandler( "/attachment", { handle: function (request, response) { response.setStatusLine(null, 200, "OK"); response.write("TitleBody"); } } ); var reqPromise = Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/saveItems", { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } ); var ids = await waitForItemEvent('add'); var item = Zotero.Items.get(ids[0]); assert.isTrue(collection2.hasItem(item.id)); await waitForItemEvent('add'); var req = await reqPromise; assert.equal(req.status, 201); // Update saved item var req = await Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/updateSession", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionID, target: collection1.treeViewID, tags: "A, B" }) } ); assert.equal(req.status, 200); assert.isTrue(collection1.hasItem(item.id)); assert.isTrue(item.hasTag("A")); assert.isTrue(item.hasTag("B")); }); it("should update collections and tags of PDF saved via /saveSnapshot", async function () { var sessionID = Zotero.Utilities.randomString(); var collection1 = await createDataObject('collection'); var collection2 = await createDataObject('collection'); await waitForItemsLoad(win); var file = getTestDataDirectory(); file.append('test.pdf'); httpd.registerFile("/test.pdf", file); var ids; var promise = waitForItemEvent('add'); var reqPromise = Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/saveSnapshot", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionID, url: testServerPath + "/test.pdf", pdf: true }) } ); var ids = await promise; var item = Zotero.Items.get(ids[0]); assert.isTrue(collection2.hasItem(item.id)); var req = await reqPromise; assert.equal(req.status, 201); // Update saved item var req = await Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/updateSession", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionID, target: collection1.treeViewID, tags: "A, B" }) } ); assert.equal(req.status, 200); assert.isTrue(collection1.hasItem(item.id)); assert.isTrue(item.hasTag("A")); assert.isTrue(item.hasTag("B")); }); it("should update collections and tags of webpage saved via /saveSnapshot", async function () { var sessionID = Zotero.Utilities.randomString(); var collection1 = await createDataObject('collection'); var collection2 = await createDataObject('collection'); await waitForItemsLoad(win); // saveSnapshot saves parent and child before returning var ids1, ids2; var promise = waitForItemEvent('add').then(function (ids) { ids1 = ids; return waitForItemEvent('add').then(function (ids) { ids2 = ids; }); }); await Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/saveSnapshot", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionID, url: "http://example.com", html: "TitleBody" }) } ); assert.isTrue(promise.isFulfilled()); var item = Zotero.Items.get(ids1[0]); // Update saved item var req = await Zotero.HTTP.request( 'POST', connectorServerPath + "/connector/updateSession", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionID, target: collection1.treeViewID, tags: "A, B" }) } ); assert.equal(req.status, 200); assert.isTrue(collection1.hasItem(item.id)); assert.isTrue(item.hasTag("A")); assert.isTrue(item.hasTag("B")); }); }); describe('/connector/installStyle', function() { var endpoint; before(function() { endpoint = connectorServerPath + "/connector/installStyle"; }); it('should reject styles with invalid text', function* () { var error = yield getPromiseError(Zotero.HTTP.request( 'POST', endpoint, { headers: { "Content-Type": "application/json" }, body: '{}' } )); assert.instanceOf(error, Zotero.HTTP.UnexpectedStatusException); assert.equal(error.xmlhttp.status, 400); assert.equal(error.xmlhttp.responseText, Zotero.getString("styles.installError", "(null)")); }); it('should import a style with application/vnd.citationstyles.style+xml content-type', function* () { sinon.stub(Zotero.Styles, 'install').callsFake(function(style) { var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] .createInstance(Components.interfaces.nsIDOMParser), doc = parser.parseFromString(style, "application/xml"); return Zotero.Promise.resolve({ styleTitle: Zotero.Utilities.xpathText( doc, '/csl:style/csl:info[1]/csl:title[1]', Zotero.Styles.ns ), styleID: Zotero.Utilities.xpathText( doc, '/csl:style/csl:info[1]/csl:id[1]', Zotero.Styles.ns ) }); }); var style = ` `; var response = yield Zotero.HTTP.request( 'POST', endpoint, { headers: { "Content-Type": "application/vnd.citationstyles.style+xml" }, body: style } ); assert.equal(response.status, 201); assert.equal(response.response, JSON.stringify({name: 'Test1'})); Zotero.Styles.install.restore(); }); }); describe('/connector/import', function() { var endpoint; before(function() { endpoint = connectorServerPath + "/connector/import"; }); it('should reject resources that do not contain import data', function* () { var error = yield getPromiseError(Zotero.HTTP.request( 'POST', endpoint, { headers: { "Content-Type": "text/plain" }, body: 'Owl' } )); assert.instanceOf(error, Zotero.HTTP.UnexpectedStatusException); assert.equal(error.xmlhttp.status, 400); }); it('should import resources (BibTeX) into selected collection', function* () { var collection = yield createDataObject('collection'); yield waitForItemsLoad(win); var resource = `@book{test1, title={Test1}, author={Owl}, year={1000}, publisher={Curly Braces Publishing}, keywords={A, B} }`; var addedItemIDsPromise = waitForItemEvent('add'); var req = yield Zotero.HTTP.request( 'POST', endpoint, { headers: { "Content-Type": "application/x-bibtex" }, body: resource } ); assert.equal(req.status, 201); assert.equal(JSON.parse(req.responseText)[0].title, 'Test1'); let itemIDs = yield addedItemIDsPromise; assert.isTrue(collection.hasItem(itemIDs[0])); var item = Zotero.Items.get(itemIDs[0]); assert.sameDeepMembers(item.getTags(), [{ tag: 'A', type: 1 }, { tag: 'B', type: 1 }]); }); it('should switch to My Library if read-only library is selected', function* () { var group = yield createGroup({ editable: false }); yield selectLibrary(win, group.libraryID); yield waitForItemsLoad(win); var resource = `@book{test1, title={Test1}, author={Owl}, year={1000}, publisher={Curly Braces Publishing} }`; var addedItemIDsPromise = waitForItemEvent('add'); var req = yield Zotero.HTTP.request( 'POST', endpoint, { headers: { "Content-Type": "application/x-bibtex" }, body: resource, successCodes: false } ); assert.equal(req.status, 201); assert.equal( win.ZoteroPane.collectionsView.getSelectedLibraryID(), Zotero.Libraries.userLibraryID ); let itemIDs = yield addedItemIDsPromise; var item = Zotero.Items.get(itemIDs[0]); assert.equal(item.libraryID, Zotero.Libraries.userLibraryID); }); }); });