From f5fc98146b37ebb0391c90fb9219e18148635ee9 Mon Sep 17 00:00:00 2001 From: Jonathan Druart Date: Thu, 12 Jun 2025 15:42:17 +0200 Subject: [PATCH] Bug 40170: Replace cypress-mysql with mysql2 - Cypress tests This patch suggests to replace cypress-mysql with mysql2 cypress-mysql provides a custom Cypress command (cy.query) that run within the browser context. This approach mixes test-side and server-side and is limited in flexibility and error handling. mysql2 runs inside Cypress plugins (Node process) via cy.task calls, which are executed server-side (outside the browser) This is needed for the follow-up bug reports where we need to request the DB from other plugins Test plan: Run `yarn install` to install the mysql2 All the Cypress tests modified by this patch must still pass The new test t/cypress/integration/t/db.ts must also pass --- cypress.config.ts | 8 --- package.json | 2 +- t/cypress/integration/Auth/csrf.ts | 66 +++++++++---------- .../Islands/AcquisitionsMenu_spec.ts | 10 +-- .../integration/KohaTable/Holdings_spec.ts | 24 ++++--- .../KohaTable/PatronSearch_spec.ts | 36 +++++----- t/cypress/integration/t/db.ts | 15 +++++ t/cypress/plugins/db.js | 17 +++++ t/cypress/plugins/index.js | 7 +- t/cypress/support/e2e.js | 3 - 10 files changed, 107 insertions(+), 81 deletions(-) create mode 100644 t/cypress/integration/t/db.ts create mode 100644 t/cypress/plugins/db.js diff --git a/cypress.config.ts b/cypress.config.ts index e90da4e019d..e67eb344f30 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -15,13 +15,5 @@ export default defineConfig({ baseUrl: "http://localhost:8081", specPattern: "t/cypress/integration/**/*.*", supportFile: "t/cypress/support/e2e.js", - env: { - db: { - host: "db", - user: "koha_kohadev", - password: "password", - database: "koha_kohadev", - }, - }, }, }); diff --git a/package.json b/package.json index d16166e856f..66748e8699d 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "bootstrap": "^5.3.3", "css-loader": "^6.6.0", "cypress": "^12.17.2", - "cypress-mysql": "^1.0.0", "datatables.net-buttons": "^2.3.4", "datatables.net-vue3": "^2.0.0", "gulp": "^4.0.2", @@ -82,6 +81,7 @@ "globals": "^16.0.0", "gulp-tap": "^1.0.1", "html-webpack-plugin": "^5.5.0", + "mysql2": "^3.14.1", "node-sass-tilde-importer": "^1.0.2", "postcss": "^8.4.14", "postcss-selector-parser": "^6.0.10", diff --git a/t/cypress/integration/Auth/csrf.ts b/t/cypress/integration/Auth/csrf.ts index c17763090b8..c24b691d04a 100644 --- a/t/cypress/integration/Auth/csrf.ts +++ b/t/cypress/integration/Auth/csrf.ts @@ -5,7 +5,7 @@ const branchname = "test_branchname"; function cleanup() { const sql = "DELETE FROM branches WHERE branchcode=?"; - cy.query(sql, branchcode); + cy.task("query", { sql, values: [branchcode] }); } describe("CSRF", () => { @@ -32,10 +32,10 @@ describe("CSRF", () => { .find(".alert") .contains(/No CSRF token passed for POST/); - cy.query( - "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", - branchcode - ).then(result => { + cy.task("query", { + sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", + values: [branchcode], + }).then(result => { expect(result[0].count).to.equal(0); }); }); @@ -53,10 +53,10 @@ describe("CSRF", () => { .find(".alert") .contains(/Wrong CSRF token/); - cy.query( - "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", - branchcode - ).then(result => { + cy.task("query", { + sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", + values: [branchcode], + }).then(result => { expect(result[0].count).to.equal(0); }); }); @@ -89,10 +89,10 @@ describe("CSRF", () => { // We do not want Wrong CSRF token here cy.get(".message").should("not.exist"); - cy.query( - "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", - branchcode - ).then(result => { + cy.task("query", { + sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", + values: [branchcode], + }).then(result => { expect(result[0].count).to.equal(0); }); }); @@ -112,19 +112,19 @@ describe("CSRF", () => { cy.get("select[name='libraries_length']").select("-1"); cy.get("td").contains(branchcode); - cy.query( - "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", - branchcode - ).then(result => { + cy.task("query", { + sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", + values: [branchcode], + }).then(result => { expect(result[0].count).to.equal(1); }); }); it("Delete without CSRF", () => { - cy.query("INSERT INTO branches(branchcode, branchname) VALUES (?, ?)", [ - branchcode, - branchname, - ]); + cy.task("query", { + sql: "INSERT INTO branches(branchcode, branchname) VALUES (?, ?)", + values: [branchcode, branchname], + }); cy.visit("/cgi-bin/koha/admin/branches.pl"); cy.get("select[name='libraries_length']").select("-1"); @@ -141,19 +141,19 @@ describe("CSRF", () => { .find(".alert") .contains(/No CSRF token passed for POST/); - cy.query( - "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", - branchcode - ).then(result => { + cy.task("query", { + sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", + values: [branchcode], + }).then(result => { expect(result[0].count).to.equal(1); }); }); it("Delete", () => { - cy.query("INSERT INTO branches(branchcode, branchname) VALUES (?, ?)", [ - branchcode, - branchname, - ]); + cy.task("query", { + sql: "INSERT INTO branches(branchcode, branchname) VALUES (?, ?)", + values: [branchcode, branchname], + }); cy.visit("/cgi-bin/koha/admin/branches.pl"); cy.get("select[name='libraries_length']").select("-1"); @@ -165,10 +165,10 @@ describe("CSRF", () => { .find(".alert") .contains(/Library deleted successfully/); - cy.query( - "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", - branchcode - ).then(result => { + cy.task("query", { + sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", + values: [branchcode], + }).then(result => { expect(result[0].count).to.equal(0); }); }); diff --git a/t/cypress/integration/Islands/AcquisitionsMenu_spec.ts b/t/cypress/integration/Islands/AcquisitionsMenu_spec.ts index 8d2413324db..121e1c9402e 100644 --- a/t/cypress/integration/Islands/AcquisitionsMenu_spec.ts +++ b/t/cypress/integration/Islands/AcquisitionsMenu_spec.ts @@ -23,12 +23,14 @@ describe("Acquisitions menu", () => { it("Should show/hide links based on permissions", () => { cy.get(".sidebar_menu").should("be.visible"); - cy.query( - "UPDATE borrowers SET flags=2052 WHERE borrowernumber=51" - ).then(() => { + cy.task("query", { + sql: "UPDATE borrowers SET flags=2052 WHERE borrowernumber=51", + }).then(() => { cy.reload(true); cy.get(".sidebar_menu a").should("have.length", 8); - cy.query("UPDATE borrowers SET flags=1 WHERE borrowernumber=51"); + cy.task("query", { + sql: "UPDATE borrowers SET flags=1 WHERE borrowernumber=51", + }); }); }); it("Should correctly apply the 'current' class", () => { diff --git a/t/cypress/integration/KohaTable/Holdings_spec.ts b/t/cypress/integration/KohaTable/Holdings_spec.ts index beee5044af9..380ac9d2d8d 100644 --- a/t/cypress/integration/KohaTable/Holdings_spec.ts +++ b/t/cypress/integration/KohaTable/Holdings_spec.ts @@ -42,7 +42,9 @@ describe("catalogue/detail/holdings_table with items", () => { cy.wrap(Promise.resolve()) .then(() => { return queries.reduce((chain, { query, values }) => { - return chain.then(() => cy.query(query, values)); + return chain.then(() => + cy.task("query", { sql: query, values }) + ); }, Promise.resolve()); }) .then(() => { @@ -102,9 +104,9 @@ describe("catalogue/detail/holdings_table with items", () => { }); }); }); - cy.query( - "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'" - ).then(value => { + cy.task("query", { + sql: "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'", + }).then(value => { cy.wrap(value).as("syspref_AlwaysShowHoldingsTableFilters"); }); }); @@ -435,14 +437,16 @@ describe("catalogue/detail/holdings_table without items", () => { const item_type = generated_objects["item_type"]; const queries = [ { - query: "INSERT INTO itemtypes(itemtype, description) VALUES (?, ?)", + sql: "INSERT INTO itemtypes(itemtype, description) VALUES (?, ?)", values: [item_type.item_type_id, item_type.description], }, ]; cy.wrap(Promise.resolve()) .then(() => { - return queries.reduce((chain, { query, values }) => { - return chain.then(() => cy.query(query, values)); + return queries.reduce((chain, { sql, values }) => { + return chain.then(() => + cy.task("query", { sql, values }) + ); }, Promise.resolve()); }) .then(() => { @@ -491,9 +495,9 @@ describe("catalogue/detail/holdings_table without items", () => { }); }); }); - cy.query( - "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'" - ).then(value => { + cy.task("query", { + sql: "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'", + }).then(value => { cy.wrap(value).as("syspref_AlwaysShowHoldingsTableFilters"); }); }); diff --git a/t/cypress/integration/KohaTable/PatronSearch_spec.ts b/t/cypress/integration/KohaTable/PatronSearch_spec.ts index a9cc064f409..782cb45f4f2 100644 --- a/t/cypress/integration/KohaTable/PatronSearch_spec.ts +++ b/t/cypress/integration/KohaTable/PatronSearch_spec.ts @@ -9,7 +9,7 @@ const patron_attr_type = "attribute_type4TEST"; function cleanup() { const sql = "DELETE FROM borrower_attribute_types WHERE code=?"; - cy.query(sql, patron_attr_type); + cy.task("query", { sql, values: [patron_attr_type] }); } describe("ExtendedPatronAttributes", () => { beforeEach(() => { @@ -19,9 +19,9 @@ describe("ExtendedPatronAttributes", () => { cy.window().then(win => { win.localStorage.clear(); }); - cy.query( - "SELECT value FROM systempreferences WHERE variable='ExtendedPatronAttributes'" - ).then(value => { + cy.task("query", { + sql: "SELECT value FROM systempreferences WHERE variable='ExtendedPatronAttributes'", + }).then(value => { cy.wrap(value).as("syspref_ExtendedPatronAttributes"); }); }); @@ -45,9 +45,9 @@ describe("ExtendedPatronAttributes", () => { cy.get("#search_patron_filter").type("something"); cy.get("form.patron_search_form input[type='submit']").click(); - cy.query( - "select count(*) as nb_searchable from borrower_attribute_types where staff_searchable=1" - ).then(result => { + cy.task("query", { + sql: "select count(*) as nb_searchable from borrower_attribute_types where staff_searchable=1", + }).then(result => { const has_searchable = result[0].nb_searchable; cy.wait("@searchPatrons").then(interception => { const q = interception.request.query.q; @@ -55,10 +55,10 @@ describe("ExtendedPatronAttributes", () => { }); }); - cy.query( - "INSERT INTO borrower_attribute_types(code, description, staff_searchable, searched_by_default) VALUES (?, 'only for tests', 1, 1)", - patron_attr_type - ).then(() => { + cy.task("query", { + sql: "INSERT INTO borrower_attribute_types(code, description, staff_searchable, searched_by_default) VALUES (?, 'only for tests', 1, 1)", + values: [patron_attr_type], + }).then(() => { cy.visit("/cgi-bin/koha/members/members-home.pl"); cy.get("#search_patron_filter").type("something"); @@ -80,9 +80,9 @@ describe("ExtendedPatronAttributes", () => { cy.get("#search_patron_filter").type("something"); cy.get("form.patron_search_form input[type='submit']").click(); - cy.query( - "select count(*) as nb_searchable from borrower_attribute_types where staff_searchable=1 AND searched_by_default=1" - ).then(result => { + cy.task("query", { + sql: "select count(*) as nb_searchable from borrower_attribute_types where staff_searchable=1 AND searched_by_default=1", + }).then(result => { const has_searchable = result[0].nb_searchable; cy.wait("@searchPatrons").then(interception => { const q = interception.request.query.q; @@ -94,10 +94,10 @@ describe("ExtendedPatronAttributes", () => { }); }); - cy.query( - "INSERT INTO borrower_attribute_types(code, description, staff_searchable, searched_by_default) VALUES (?, 'only for tests', 1, 1)", - patron_attr_type - ).then(() => { + cy.task("query", { + sql: "INSERT INTO borrower_attribute_types(code, description, staff_searchable, searched_by_default) VALUES (?, 'only for tests', 1, 1)", + values: [patron_attr_type], + }).then(() => { cy.visit("/cgi-bin/koha/members/members-home.pl"); cy.get("#search_patron_filter").type("something"); diff --git a/t/cypress/integration/t/db.ts b/t/cypress/integration/t/db.ts new file mode 100644 index 00000000000..5715c8870fc --- /dev/null +++ b/t/cypress/integration/t/db.ts @@ -0,0 +1,15 @@ +describe("DB tests", () => { + it("should be able to SELECT", () => { + cy.task("query", { sql: "SELECT count(*) FROM borrowers" }).then( + rows => { + expect(typeof rows.length).to.be.equal("number"); + } + ); + cy.task("query", { + sql: "SELECT count(*) FROM borrowers WHERE `surname` = ?", + values: ["john"], + }).then(rows => { + expect(typeof rows.length).to.be.equal("number"); + }); + }); +}); diff --git a/t/cypress/plugins/db.js b/t/cypress/plugins/db.js new file mode 100644 index 00000000000..fb4568886cf --- /dev/null +++ b/t/cypress/plugins/db.js @@ -0,0 +1,17 @@ +const mysql = require("mysql2/promise"); + +const connectionConfig = { + host: "db", + user: "koha_kohadev", + password: "password", + database: "koha_kohadev", +}; + +async function query(sql, params = []) { + const connection = await mysql.createConnection(connectionConfig); + const [rows] = await connection.execute(sql, params); + await connection.end(); + return rows; +} + +module.exports = { query }; diff --git a/t/cypress/plugins/index.js b/t/cypress/plugins/index.js index 837e6d747cb..52214717796 100644 --- a/t/cypress/plugins/index.js +++ b/t/cypress/plugins/index.js @@ -1,9 +1,9 @@ const { startDevServer } = require("@cypress/webpack-dev-server"); -const mysql = require("cypress-mysql"); - const { buildSampleObject, buildSampleObjects } = require("./mockData.js"); +const { query } = require("./db.js"); + module.exports = (on, config) => { on("dev-server:start", options => startDevServer({ @@ -11,11 +11,10 @@ module.exports = (on, config) => { }) ); - mysql.configurePlugin(on); - on("task", { buildSampleObject, buildSampleObjects, + query, }); return config; }; diff --git a/t/cypress/support/e2e.js b/t/cypress/support/e2e.js index e420889c3b5..40265d607bf 100644 --- a/t/cypress/support/e2e.js +++ b/t/cypress/support/e2e.js @@ -1874,9 +1874,6 @@ cy.getVendor = () => { }; }; -const mysql = require("cypress-mysql"); -mysql.addCommands(); - Cypress.Commands.add("set_syspref", (variable, value) => { cy.window().then(win => { const client = win.APIClient.sysprefs; -- 2.34.1