"use strict"; let httpRequest = (method, url, options) => { if (!options) { options = {}; } if (!('errorDelayMax' in options)) { options.errorDelayMax = 0; } return Zotero.HTTP.request(method, url, options); } describe("Connector Server", function () { var { HttpServer } = ChromeUtils.import("chrome://remote/content/server/HTTPD.jsm"); var win, connectorServerPath, testServerPath, httpd; var testServerPort = 16213; var snapshotHTML = "
🦉
" }) } ); assert.equal(JSON.parse(response.response)[0].proxy.scheme, 'https://%h.proxy.example.com/%p'); Zotero.Translators.getAllForType.restore(); }); }); describe("/connector/saveItems", function () { it("should save a translated item to the current selected collection", function* () { var collection = yield createDataObject('collection'); yield select(win, collection); var body = { items: [ { itemType: "newspaperArticle", title: "Title", creators: [ { firstName: "First", lastName: "Last", creatorType: "author" } ], } ], uri: "http://example.com" }; var promise = waitForItemEvent('add'); var reqPromise = httpRequest( '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)); 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 select(win, group); 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 = httpRequest( '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'} }; var promise = waitForItemEvent('add'); var req = yield httpRequest( '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'); }); }); describe("/connector/saveSingleFile", function () { it("should save a webpage item with /saveSnapshot", async function () { var collection = await createDataObject('collection'); await select(win, collection); // Promise for item save let promise = waitForItemEvent('add'); let testDataDirectory = getTestDataDirectory().path; let indexPath = OS.Path.join(testDataDirectory, 'snapshot', 'index.html'); let title = Zotero.Utilities.randomString(); let sessionID = Zotero.Utilities.randomString(); let payload = { sessionID, url: "http://example.com/test", title, }; await httpRequest( 'POST', connectorServerPath + "/connector/saveSnapshot", { headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) } ); // Await item save let parentIDs = await promise; // Check parent item assert.lengthOf(parentIDs, 1); var item = Zotero.Items.get(parentIDs[0]); assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'webpage'); assert.isTrue(collection.hasItem(item.id)); assert.equal(item.getField('title'), title); // Promise for attachment save promise = waitForItemEvent('add'); let body = JSON.stringify(Object.assign(payload, { snapshotContent: await Zotero.File.getContentsAsync(indexPath) })); await httpRequest( 'POST', connectorServerPath + "/connector/saveSingleFile", { headers: { "Content-Type": "application/json" }, body } ); // Await attachment save let attachmentIDs = await promise; // Check attachment assert.lengthOf(attachmentIDs, 1); item = Zotero.Items.get(attachmentIDs[0]); assert.isTrue(item.isImportedAttachment()); assert.equal(item.getField('title'), title); // Check attachment html file let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item).path; let path = OS.Path.join(attachmentDirectory, item.attachmentFilename); assert.isTrue(await OS.File.exists(path)); let contents = await Zotero.File.getContentsAsync(path); let expectedContents = await Zotero.File.getContentsAsync(indexPath); assert.equal(contents, expectedContents); }); it("should save a webpage item with /saveItems", async function () { let collection = await createDataObject('collection'); await select(win, collection); let title = Zotero.Utilities.randomString(); let sessionID = Zotero.Utilities.randomString(); let payload = { sessionID: sessionID, items: [ { itemType: "newspaperArticle", title: title, creators: [ { firstName: "First", lastName: "Last", creatorType: "author" } ] } ], uri: "http://example.com" }; let promise = waitForItemEvent('add'); let req = await httpRequest( 'POST', connectorServerPath + "/connector/saveItems", { headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) } ); assert.equal(req.status, 201); // Check parent item let itemIDs = await promise; assert.lengthOf(itemIDs, 1); let item = Zotero.Items.get(itemIDs[0]); assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'newspaperArticle'); assert.isTrue(collection.hasItem(item.id)); // Promise for attachment save promise = waitForItemEvent('add'); let testDataDirectory = getTestDataDirectory().path; let indexPath = OS.Path.join(testDataDirectory, 'snapshot', 'index.html'); let body = JSON.stringify(Object.assign(payload, { url: `${testServerPath}/attachment`, snapshotContent: await Zotero.File.getContentsAsync(indexPath) })); req = await httpRequest( 'POST', connectorServerPath + "/connector/saveSingleFile", { headers: { "Content-Type": "application/json" }, body } ); assert.equal(req.status, 201); // Await attachment save let attachmentIDs = await promise; // Check attachment assert.lengthOf(attachmentIDs, 1); item = Zotero.Items.get(attachmentIDs[0]); assert.isTrue(item.isImportedAttachment()); assert.equal(item.getField('title'), 'Test'); // Check attachment html file let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item).path; let path = OS.Path.join(attachmentDirectory, item.attachmentFilename); assert.isTrue(await OS.File.exists(path)); let contents = await Zotero.File.getContentsAsync(path); let expectedContents = await Zotero.File.getContentsAsync(indexPath); assert.equal(contents, expectedContents); }); }); describe("/connector/saveSnapshot", function () { it("should save a webpage item to the current selected collection", function* () { var collection = yield createDataObject('collection'); yield select(win, collection); // saveSnapshot saves parent and child before returning var ids; var promise = waitForItemEvent('add').then(function (_ids) { ids = _ids; }); var file = getTestDataDirectory(); file.append('snapshot'); file.append('index.html'); httpd.registerFile("/test", file); yield httpRequest( 'POST', connectorServerPath + "/connector/saveSnapshot", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: `${testServerPath}/test`, title: "Title" }) } ); assert.isTrue(promise.isFulfilled()); // Check item assert.lengthOf(ids, 1); var item = Zotero.Items.get(ids[0]); assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'webpage'); assert.isTrue(collection.hasItem(item.id)); assert.equal(item.getField('title'), 'Title'); }); it("should switch to My Library if a read-only library is selected", function* () { var group = yield createGroup({ editable: false }); yield select(win, group); var promise = waitForItemEvent('add'); var reqPromise = httpRequest( 'POST', connectorServerPath + "/connector/saveSnapshot", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: testServerPath + '/snapshot', html: snapshotHTML }), 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/saveAttachment", function () { const pdfPath = OS.Path.join(getTestDataDirectory().path, 'test.pdf'); let pdfSample, pdfArrayBuffer; before(async function () { await selectLibrary(win, Zotero.Libraries.userLibraryID); pdfSample = await Zotero.File.getSample(pdfPath); pdfArrayBuffer = (await OS.File.read(pdfPath)).buffer; }); it("should save a child item attachment to the specified parent item", async function () { // First, save multiple items const sessionID = Zotero.Utilities.randomString(); const bookItemID = Zotero.Utilities.randomString(); const articleItemID = Zotero.Utilities.randomString(); const body = { sessionID, items: [ { id: bookItemID, itemType: "book", title: "Book Title", }, { id: articleItemID, itemType: "journalArticle", title: "Article Title", } ] }; let itemAddPromise = waitForItemEvent('add'); let saveItemsReq = await httpRequest( 'POST', connectorServerPath + "/connector/saveItems", { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } ); assert.equal(saveItemsReq.status, 201); let itemIDs = await itemAddPromise; let bookItem = Zotero.Items.get(itemIDs[0]); let articleItem = Zotero.Items.get(itemIDs[1]); assert.equal(bookItem.numAttachments(), 0); assert.equal(articleItem.numAttachments(), 0); // Now save an attachment to the first parent item (book) let attachmentAddPromise = waitForItemEvent('add'); let attachmentReq = await httpRequest( 'POST', connectorServerPath + "/connector/saveAttachment", { headers: { "Content-Type": "application/pdf", "X-Metadata": JSON.stringify({ sessionID, title: "Book Attachment", parentItemID: bookItemID, url: `${testServerPath}/attachment1.pdf`, }) }, body: pdfArrayBuffer } ); assert.equal(attachmentReq.status, 201); let attachmentIds = await attachmentAddPromise; assert.lengthOf(attachmentIds, 1); let attachment1 = Zotero.Items.get(attachmentIds[0]); assert.equal(bookItem.numAttachments(), 1); assert.equal(articleItem.numAttachments(), 0); // Verify attachment was saved correctly assert.equal(attachment1.parentItemID, bookItem.id); assert.equal(attachment1.getField('title'), "Book Attachment"); assert.isTrue(attachment1.isPDFAttachment()); // Save a second attachment to the second parent item (article) attachmentAddPromise = waitForItemEvent('add'); attachmentReq = await httpRequest( 'POST', connectorServerPath + "/connector/saveAttachment", { headers: { "Content-Type": "application/pdf", "X-Metadata": JSON.stringify({ sessionID, title: "Article Attachment", parentItemID: articleItemID, url: `${testServerPath}/attachment2.pdf`, }) }, body: pdfArrayBuffer } ); assert.equal(attachmentReq.status, 201); attachmentIds = await attachmentAddPromise; assert.lengthOf(attachmentIds, 1); var attachment2 = Zotero.Items.get(attachmentIds[0]); // Verify second attachment was saved correctly assert.equal(attachment2.parentItemID, articleItem.id); assert.equal(attachment2.getField('title'), "Article Attachment"); assert.isTrue(attachment2.isPDFAttachment()); assert.equal(bookItem.numAttachments(), 1); assert.equal(articleItem.numAttachments(), 1); // Verify attachment content let attachmentDirectory = Zotero.Attachments.getStorageDirectory(attachment1).path; let path = OS.Path.join(attachmentDirectory, attachment1.attachmentFilename); assert.isTrue(await OS.File.exists(path)); let contents = await Zotero.File.getSample(path); assert.equal(contents, pdfSample); }); }); describe("/connector/hasAttachmentResolvers", function () { it("should respond with 'true' if the item has OA attachments", async function () { const sessionID = Zotero.Utilities.randomString(); const itemID = Zotero.Utilities.randomString(); const body = { sessionID, items: [ { id: itemID, itemType: "journalArticle", title: "Test Article with DOI", DOI: "10.1234/example.doi", } ] }; let response = await httpRequest( "POST", connectorServerPath + "/connector/saveItems", { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } ); assert.equal(response.status, 201); response = await httpRequest( "POST", connectorServerPath + "/connector/hasAttachmentResolvers", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionID, itemID }), } ); assert.equal(response.status, 200); assert.isTrue(JSON.parse(response.responseText)); }); it("should respond with 'false' if the item has no OA attachments", async function () { const sessionID = Zotero.Utilities.randomString(); const itemID = Zotero.Utilities.randomString(); const body = { sessionID, items: [ { id: itemID, itemType: "journalArticle", title: "Test Article", } ] }; let response = await httpRequest( "POST", connectorServerPath + "/connector/saveItems", { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } ); assert.equal(response.status, 201); response = await httpRequest( "POST", connectorServerPath + "/connector/hasAttachmentResolvers", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionID, itemID }), } ); assert.equal(response.status, 200); assert.isFalse(JSON.parse(response.responseText)); }); }); describe("/connector/saveAttachmentFromResolver", function () { it("should save an OA attachment for the specified item and return 201 if OA attachment is available", async function () { let stub = sinon.stub(Zotero.Attachments, 'addFileFromURLs').returns({ id: Zotero.Utilities.randomString(), getDisplayTitle: () => "OA Attachment" }); try { const sessionID = Zotero.Utilities.randomString(); const itemID = Zotero.Utilities.randomString(); const body = { sessionID, items: [ { id: itemID, itemType: "journalArticle", title: "Test Article with DOI", DOI: "10.1234/example.doi", } ] }; let response = await httpRequest( "POST", connectorServerPath + "/connector/saveItems", { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } ); assert.equal(response.status, 201); response = await httpRequest( "POST", connectorServerPath + "/connector/saveAttachmentFromResolver", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionID, itemID }), } ); assert.equal(response.status, 201); assert.equal(response.responseText, "OA Attachment"); } finally { stub.restore(); } }); it("should return 500 if OA attachment is not available", async function () { let stub = sinon.stub(Zotero.Attachments, 'addFileFromURLs').returns(null); try { const sessionID = Zotero.Utilities.randomString(); const itemID = Zotero.Utilities.randomString(); const body = { sessionID, items: [ { id: itemID, itemType: "journalArticle", title: "Test Article with DOI", DOI: "10.1234/example.doi", } ] }; let response = await httpRequest( "POST", connectorServerPath + "/connector/saveItems", { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } ); assert.equal(response.status, 201); response = await httpRequest( "POST", connectorServerPath + "/connector/saveAttachmentFromResolver", { headers: { "Content-Type": "application/json" }, successCodes: false, body: JSON.stringify({ sessionID, itemID }), } ); assert.equal(response.status, 500); assert.equal(response.responseText, "Failed to save an attachment"); } finally { stub.restore(); } }); }); describe("/connector/saveStandaloneAttachment", function () { before(async function () { await selectLibrary(win, Zotero.Libraries.userLibraryID); }); it("should save a standalone PDF attachment", async function () { const pdfPath = OS.Path.join(getTestDataDirectory().path, 'test.pdf'); const pdfSample = await Zotero.File.getSample(pdfPath); const pdfArrayBuffer = (await OS.File.read(pdfPath)).buffer; const attachmentInfo = { url: `${testServerPath}/test1.pdf`, title: "Test PDF1", contentType: "application/pdf", sessionID: Zotero.Utilities.randomString() }; let itemIDsPromise = waitForItemEvent('add'); let xhr = await httpRequest( 'POST', connectorServerPath + "/connector/saveStandaloneAttachment", { headers: { "Content-Type": attachmentInfo.contentType, "X-Metadata": JSON.stringify(attachmentInfo) }, body: pdfArrayBuffer } ); assert.equal(xhr.status, 201); assert.isTrue(JSON.parse(xhr.responseText).canRecognize); let itemIDs = await itemIDsPromise; let item = Zotero.Items.get(itemIDs[0]); assert.equal(item.itemType, "attachment"); assert.equal(item.attachmentContentType, attachmentInfo.contentType); assert.equal(item.getField("title"), attachmentInfo.title); assert.equal(item.getField("url"), attachmentInfo.url); // Check content let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item).path; let path = OS.Path.join(attachmentDirectory, item.attachmentFilename); assert.isTrue(await OS.File.exists(path)); let contents = await Zotero.File.getSample(path); assert.equal(contents, pdfSample); }); it("should save a standalone image attachment", async function () { const imagePath = OS.Path.join(getTestDataDirectory().path, 'test.png'); const imageSample = await Zotero.File.getSample(imagePath); const imageArrayBuffer = (await OS.File.read(imagePath)).buffer; const attachmentInfo = { url: `${testServerPath}/test.png`, title: "Test PNG", contentType: "image/png", sessionID: Zotero.Utilities.randomString() }; let itemIDsPromise = waitForItemEvent('add'); let xhr = await httpRequest( 'POST', connectorServerPath + "/connector/saveStandaloneAttachment", { headers: { "Content-Type": attachmentInfo.contentType, "X-Metadata": JSON.stringify(attachmentInfo) }, body: imageArrayBuffer } ); assert.equal(xhr.status, 201); assert.isFalse(JSON.parse(xhr.responseText).canRecognize); let itemIDs = await itemIDsPromise; let item = Zotero.Items.get(itemIDs[0]); assert.equal(item.itemType, "attachment"); assert.equal(item.attachmentContentType, attachmentInfo.contentType); assert.equal(item.getField("title"), attachmentInfo.title); assert.equal(item.getField("url"), attachmentInfo.url); // Check content let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item).path; let path = OS.Path.join(attachmentDirectory, item.attachmentFilename); assert.isTrue(await OS.File.exists(path)); let contents = await Zotero.File.getSample(path); assert.equal(contents, imageSample); }); }); describe("/connector/getRecognizedItem", function () { it("should return the recognized parent item", async function () { const stub = sinon.stub(Zotero.RecognizeDocument, '_recognize').callsFake(async () => { return await createDataObject('item', { title: "Recognized Item", }); }); try { const pdfPath = OS.Path.join(getTestDataDirectory().path, 'test.pdf'); const pdfArrayBuffer = (await OS.File.read(pdfPath)).buffer; const sessionID = Zotero.Utilities.randomString(); const attachmentInfo = { url: `${testServerPath}/test2.pdf`, title: "Test PDF2", contentType: "application/pdf", sessionID }; let itemIDsPromise = waitForItemEvent('add'); let xhr = await httpRequest( 'POST', connectorServerPath + "/connector/saveStandaloneAttachment", { headers: { "Content-Type": attachmentInfo.contentType, "X-Metadata": JSON.stringify(attachmentInfo) }, body: pdfArrayBuffer } ); assert.equal(xhr.status, 201); assert.isTrue(JSON.parse(xhr.responseText).canRecognize); let itemIDs = await itemIDsPromise; let standaloneAttachment = Zotero.Items.get(itemIDs[0]); assert.isFalse(standaloneAttachment.parentID); let recognizedItemIDsPromise = waitForItemEvent('add'); xhr = await httpRequest( 'POST', connectorServerPath + "/connector/getRecognizedItem", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionID }) } ); assert.isTrue(stub.called); assert.equal(xhr.status, 200); assert.equal(JSON.parse(xhr.responseText).title, "Recognized Item"); let recognizedItemIDs = await recognizedItemIDsPromise; let recognizedItem = Zotero.Items.get(recognizedItemIDs[0]); assert.equal(standaloneAttachment.parentID, recognizedItem.id); } finally { stub.restore(); } }); }); 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 select(win, collection2); const id = Zotero.Utilities.randomString(); var sessionID = Zotero.Utilities.randomString(); var body = { sessionID, items: [ { itemType: "newspaperArticle", title: "Title", id, creators: [ { firstName: "First", lastName: "Last", creatorType: "author" } ] } ], uri: "http://example.com" }; var reqPromise = httpRequest( '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)); var req = await reqPromise; assert.equal(req.status, 201); reqPromise = httpRequest( 'POST', connectorServerPath + "/connector/saveAttachment", { headers: { "Content-Type": "text/html", "X-Metadata": JSON.stringify({ sessionID, title: "Attachment", parentItemID: id, url: `${testServerPath}/attachment`, }) }, body: "