From 391ef08e30fab965d1631955237a7734be052ba9 Mon Sep 17 00:00:00 2001 From: Jonathan Druart Date: Mon, 18 May 2026 12:07:43 +0200 Subject: [PATCH] Bug 42080: Add Cypress tests Signed-off-by: Jonathan Druart Signed-off-by: Pedro Amorim --- .../Acquisitions/InvoiceFiles_spec.ts | 117 ++++++++++++++++++ t/cypress/plugins/insertData.js | 35 ++++++ 2 files changed, 152 insertions(+) create mode 100644 t/cypress/integration/Acquisitions/InvoiceFiles_spec.ts diff --git a/t/cypress/integration/Acquisitions/InvoiceFiles_spec.ts b/t/cypress/integration/Acquisitions/InvoiceFiles_spec.ts new file mode 100644 index 0000000000..3c719e9a12 --- /dev/null +++ b/t/cypress/integration/Acquisitions/InvoiceFiles_spec.ts @@ -0,0 +1,117 @@ +describe("Invoice files - Bug 42080: File type handling and CSP headers", () => { + beforeEach(() => { + cy.login(); + cy.title().should("eq", "Koha staff interface"); + + cy.task("buildSampleObject", { + object: "vendor", + values: { active: 1 }, + }) + .then(generatedVendor => { + delete generatedVendor.list_currency; + delete generatedVendor.invoice_currency; + return cy.task("insertObject", { + type: "vendor", + object: generatedVendor, + }); + }) + .then(vendor => { + cy.wrap(vendor).as("vendor"); + return cy.task("buildSampleObject", { + object: "invoice", + values: { vendor_id: vendor.id }, + }); + }) + .then(generatedInvoice => { + return cy.task("insertObject", { + type: "invoice", + object: generatedInvoice, + }); + }) + .then(invoice => { + cy.wrap(invoice).as("invoice"); + }); + + cy.task("query", { + sql: "SELECT value FROM systempreferences WHERE variable='AcqEnableFiles'", + }).then(value => { + cy.wrap(value).as("syspref_AcqEnableFiles"); + }); + + cy.set_syspref("AcqEnableFiles", 1); + }); + + afterEach(function () { + const invoice = this.invoice; + if (invoice && invoice.invoice_id) { + cy.task("query", { + sql: "DELETE FROM misc_files WHERE table_tag = 'aqinvoices' AND record_id = ?", + values: [invoice.invoice_id], + }); + } + cy.task("deleteSampleObjects", [ + { vendor: this.vendor, invoice: this.invoice }, + ]); + cy.set_syspref("AcqEnableFiles", this.syspref_AcqEnableFiles); + }); + + it("should serve PDF files inline with CSP header", function () { + const invoice = this.invoice; + + cy.task("insertObject", { + type: "invoice_file", + object: { + invoice_id: invoice.invoice_id, + file_name: "test.pdf", + file_type: "application/pdf", + file_content: "hex:255044462d312e340a0a", + file_description: "Test PDF", + }, + }).then(file => { + cy.wrap(file).as("pdfFile"); + }); + + cy.get("@pdfFile").then(file => { + cy.request( + `/cgi-bin/koha/acqui/invoice-files.pl?invoiceid=${invoice.invoice_id}&op=download&view=1&file_id=${file.file_id}` + ).then(response => { + expect(response.headers["content-security-policy"]).to.include( + "default-src 'none'" + ); + expect(response.headers["content-type"]).to.include( + "application/pdf" + ); + }); + }); + }); + + it("should force download when view parameter is not present", function () { + const invoice = this.invoice; + + cy.task("insertObject", { + type: "invoice_file", + object: { + invoice_id: invoice.invoice_id, + file_name: "test.pdf", + file_type: "application/pdf", + file_content: "hex:255044462d312e340a0a", + file_description: "Test PDF", + }, + }).then(file => { + cy.wrap(file).as("pdfFile"); + }); + + cy.get("@pdfFile").then(file => { + cy.request( + `/cgi-bin/koha/acqui/invoice-files.pl?invoiceid=${invoice.invoice_id}&op=download&file_id=${file.file_id}` + ).then(response => { + expect(response.headers["content-disposition"]).to.include( + "attachment" + ); + expect(response.headers["content-disposition"]).to.include( + file.file_name + ); + }); + }); + }); +}); diff --git a/t/cypress/plugins/insertData.js b/t/cypress/plugins/insertData.js index 329d78f6d3..c8b4894e61 100644 --- a/t/cypress/plugins/insertData.js +++ b/t/cypress/plugins/insertData.js @@ -582,6 +582,18 @@ const deleteSampleObjects = async allObjects => { whereColumn: "categorycode", idField: "patron_category_id", }, + invoice: { + plural: "invoices", + table: "aqinvoices", + whereColumn: "invoiceid", + idField: "invoice_id", + }, + invoice_file: { + plural: "invoice_files", + table: "misc_files", + whereColumn: "file_id", + idField: "file_id", + }, }; // Merge by type const mergedObjects = {}; @@ -604,7 +616,9 @@ const deleteSampleObjects = async allObjects => { "holds", "checkouts", "old_checkouts", + "invoice_files", "baskets", + "invoices", "vendors", "patrons", "items", @@ -922,6 +936,27 @@ const insertObject = async ({ type, object, baseUrl, authHeader }) => { baseUrl, authHeader, }); + } else if (type == "invoice_file") { + let content = object.file_content; + content = `UNHEX('${content.substring(4)}')`; + return query({ + sql: `INSERT INTO misc_files (table_tag, record_id, file_name, file_type, file_description, file_content) + VALUES ('aqinvoices', ?, ?, ?, ?, ${content})`, + values: [ + object.invoice_id, + object.file_name, + object.file_type, + object.file_description, + ], + }).then(result => { + return { + file_id: result.insertId, + invoice_id: object.invoice_id, + file_name: object.file_name, + file_type: object.file_type, + file_description: object.file_description, + }; + }); } else { throw Error(`Unsupported object type '${type}' to insert`); }