diff --git a/.eslintrc b/.eslintrc index ad18212..bc028d0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,13 +6,5 @@ }, "parserOptions": { "ecmaVersion": 2022 - }, - "overrides": [ - { - "files": ["test/**/*.js"], - "env": { - "mocha": true - } - } - ] + } } diff --git a/index.js b/index.js index 582bf9b..3e708b9 100644 --- a/index.js +++ b/index.js @@ -34,7 +34,7 @@ function hasSqlEquivalent(attribute, columns) { function checkSqlEquivalents(attributes, columns) { attributes.forEach((attribute) => { if (!hasSqlEquivalent(attribute, columns)) { - throw new Error('Attribute "' + attribute + '" is not provided by SQL query'); + throw new ImplementationError('Attribute "' + attribute + '" is not provided by SQL query'); } }); } diff --git a/lib/sql-query-checker.js b/lib/sql-query-checker.js index 0ecd9e9..5c97ade 100644 --- a/lib/sql-query-checker.js +++ b/lib/sql-query-checker.js @@ -1,12 +1,14 @@ 'use strict'; +const { ImplementationError } = require('@florajs/errors'); + function isColumn(expr) { return Object.hasOwn(expr, 'type') && expr.type === 'column_ref'; } function checkColumn(expr) { if (expr.table !== null) return; - throw new Error(`Column "${expr.column}" must be fully qualified`); + throw new ImplementationError(`Column "${expr.column}" must be fully qualified`); } /** diff --git a/package.json b/package.json index f851fea..313bec4 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,9 @@ "test:start-testdb": "docker run -d --name flora-mysql-testdb -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -e MYSQL_DATABASE=${MYSQL_DATABASE:-flora_mysql_testdb} -p ${MYSQL_PORT:-3306}:3306 --mount type=bind,src=\"$PWD/test/integration/fixtures\",target=/docker-entrypoint-initdb.d,readonly --tmpfs \"/var/lib/mysql\" mysql:${MYSQL_VERSION:-8.0}", "test:mysql": "./test/integration/tool/wait-for-mysql.sh ${MYSQL_HOST:-localhost} ${MYSQL_PORT:-3306} ${MYSQL_DATABASE:-flora_mysql_testdb} && npm run test:ci", "test:stop": "docker stop flora-mysql-testdb", - "test:ci": "mocha test/integration/*.spec.js", + "test:ci": "node --test test/integration/*.spec.js", "test-ci": "npm run test:cleanup; npm run test:start-testdb && npm run test:mysql; npm run test:stop", - "test-unit": "mocha test/unit/*.spec.js test/unit/**/*.spec.js", + "test-unit": "node --test test/unit/*.spec.js test/unit/**/*.spec.js", "test": "npm run test-unit && npm run test-ci", "lint": "eslint ." }, @@ -54,13 +54,9 @@ }, "devDependencies": { "bunyan": "^1.8.15", - "chai": "^4.3.7", "eslint": "^8.46.0", "eslint-config-prettier": "^8.10.0", "eslint-plugin-prettier": "^5.0.0", - "mocha": "^10.2.0", - "prettier": "^3.0.1", - "sinon": "^15.2.0", - "sinon-chai": "^3.7.0" + "prettier": "^3.0.1" } } diff --git a/test/ast-tpl.js b/test/ast-tpl.js index 7911111..a2e8472 100644 --- a/test/ast-tpl.js +++ b/test/ast-tpl.js @@ -5,11 +5,11 @@ module.exports = { options: null, distinct: null, columns: [ - { expr: { type: 'column_ref', table: 't', column: 'id' }, as: null }, - { expr: { type: 'column_ref', table: 't', column: 'col1' }, as: null }, - { expr: { type: 'column_ref', table: 't', column: 'col2' }, as: null } + { expr: { type: 'column_ref', table: 'flora_request_processing', column: 'id' }, as: null }, + { expr: { type: 'column_ref', table: 'flora_request_processing', column: 'col1' }, as: null }, + { expr: { type: 'column_ref', table: 'flora_request_processing', column: 'col2' }, as: null } ], - from: [{ db: null, table: 't', as: null }], + from: [{ db: null, table: 'flora_request_processing', as: null }], where: null, groupby: null, having: null, diff --git a/test/integration/context.spec.js b/test/integration/context.spec.js index 69b53c6..a89ad06 100644 --- a/test/integration/context.spec.js +++ b/test/integration/context.spec.js @@ -1,9 +1,9 @@ 'use strict'; -const { expect } = require('chai'); +const assert = require('node:assert/strict'); +const { after, describe, it } = require('node:test'); const { FloraMysqlFactory } = require('../FloraMysqlFactory'); -const tableWithAutoIncrement = require('./table-with-auto-increment'); const ciCfg = require('./ci-config'); describe('context', () => { @@ -11,268 +11,329 @@ describe('context', () => { const db = process.env.MYSQL_DATABASE || 'flora_mysql_testdb'; const ctx = ds.getContext({ db }); - afterEach(() => ctx.exec('TRUNCATE TABLE "t"')); after(() => ds.close()); describe('#query', () => { it('should return an array for empty results', async () => { - const result = await ctx.query('SELECT "col1" FROM "t"'); - expect(result).to.be.an('array').and.to.have.length(0); + const table = 'ctx_query_empty_results'; + + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + const result = await ctx.query(`SELECT id FROM ${table}`); + + assert.ok(Array.isArray(result)); + assert.equal(result.length, 0); }); it('should accept query params as an array', async () => { - await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - const result = await ctx.query('SELECT "id", "col1" FROM "t" WHERE "id" = ?', [1]); + const table = 'ctx_query_params_array'; - expect(result).to.eql([{ id: 1, col1: 'foo' }]); + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }, { id: 2 }]); + const result = await ctx.query(`SELECT id FROM ${table} WHERE id = ?`, [1]); + + assert.equal(result.length, 1); + assert.deepEqual({ ...result[0] }, { id: 1 }); }); it('should accept query params as an object', async () => { - await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - const result = await ctx.query('SELECT "id", "col1" FROM "t" WHERE "id" = :id', { id: 1 }); + const table = 'ctx_query_params_object'; - expect(result).to.eql([{ id: 1, col1: 'foo' }]); + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }, { id: 2 }]); + const [item] = await ctx.query(`SELECT id FROM ${table} WHERE id = :id`, { id: 1 }); + + assert.deepEqual({ ...item }, { id: 1 }); }); it('should return typecasted result', async () => { - await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - const [item] = await ctx.query('SELECT "id" FROM "t" WHERE "id" = 1'); + const table = 'ctx_query_typecasted_result'; + + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }, { id: 2 }]); + const [item] = await ctx.query(`SELECT id FROM ${table} WHERE id = 1`); - expect(item).to.have.property('id', 1); + assert.ok(Object.hasOwn(item, 'id')); + assert.equal(item.id, 1); }); }); describe('#queryRow', () => { it('should return null for empty results', async () => { - const result = await ctx.queryRow('SELECT "col1" FROM "t" WHERE "id" = 1337'); - expect(result).to.be.null; + const table = 'ctx_queryrow_empty_results'; + + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + const row = await ctx.queryRow(`SELECT id FROM ${table} WHERE id = 1337`); + + assert.equal(row, null); }); - [ - ['should resolve to an object', 'SELECT "id", "col1" FROM "t" WHERE "id" = 1'], - ['should handle multiple rows', 'SELECT "id", "col1" FROM "t" ORDER BY "id"'] - ].forEach(([description, sql]) => { - it(description, async () => { - await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - const result = await ctx.queryRow(sql); + it('should resolve to an object for single result', async () => { + const table = 'ctx_queryrow_single_row'; - expect(result).to.eql({ id: 1, col1: 'foo' }); - }); + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }, { id: 2 }]); + const row = await ctx.queryRow(`SELECT id FROM ${table} WHERE id = 1`); + + assert.deepEqual({ ...row }, { id: 1 }); + }); + + it('should resolve to an object for single result', async () => { + const table = 'ctx_queryrow_multiple_rows'; + + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }, { id: 2 }]); + const row = await ctx.queryRow(`SELECT id FROM ${table} ORDER BY id`); + + assert.deepEqual({ ...row }, { id: 1 }); }); it('should accept query params as an array', async () => { - await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - const result = await ctx.queryRow('SELECT "id", "col1" FROM "t" WHERE "id" = ?', [1]); + const table = 'ctx_queryrow_params_array'; + + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }, { id: 2 }]); + const row = await ctx.queryRow(`SELECT id FROM ${table} WHERE id = ?`, [1]); - expect(result).to.eql({ id: 1, col1: 'foo' }); + assert.deepEqual({ ...row }, { id: 1 }); }); it('should accept query params as an object', async () => { - await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - const result = await ctx.queryRow('SELECT "id", "col1" FROM "t" WHERE "id" = :id', { id: 1 }); + const table = 'ctx_queryrow_params_object'; + + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }, { id: 2, col: 'bar' }]); + const row = await ctx.queryRow(`SELECT id FROM ${table} WHERE id = :id`, { id: 1 }); - expect(result).to.eql({ id: 1, col1: 'foo' }); + assert.deepEqual({ ...row }, { id: 1 }); }); it('should return typecasted result', async () => { - await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - const row = await ctx.queryRow('SELECT "id", "col1" FROM "t" WHERE "id" = 1'); + const table = 'ctx_queryrow_typecasted_result'; + + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }, { id: 2, col1: 'bar' }]); + const row = await ctx.queryRow(`SELECT id FROM ${table} WHERE id = 1`); - expect(row).to.have.property('id', 1); + assert.equal(row.id, 1); }); }); describe('#queryOne', () => { it('should return null for empty results', async () => { - const result = await ctx.queryOne('SELECT "col1" FROM "t" WHERE "id" = 1337'); - expect(result).to.be.null; + const table = 'ctx_queryone_empty_results'; + + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + const result = await ctx.queryOne(`SELECT id FROM ${table} WHERE id = 1337`); + + assert.equal(result, null); }); [ - ['should resolve to single of value', 'SELECT "col1" FROM "t" WHERE "id" = 1'], - ['should handle aliases', 'SELECT "col1" AS "funkyAlias" FROM "t" WHERE "id" = 1'], - ['should handle multiple columns', 'SELECT "col1", "id" FROM "t" WHERE "id" = 1'], - ['should handle multiple rows', 'SELECT "col1" FROM "t" ORDER BY "id"'] - ].forEach(([description, sql]) => { + [ + 'should resolve to single of value', + 'ctx_queryone_single_value_0', + 'SELECT id FROM ctx_queryone_single_value_0 WHERE id = 1' + ], + [ + 'should handle aliases', + 'ctx_queryone_single_value_1', + 'SELECT id AS funkyAlias FROM ctx_queryone_single_value_1 WHERE id = 1' + ] + ].forEach(([description, table, sql]) => { it(description, async () => { - await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }]); const result = await ctx.queryOne(sql); - expect(result).to.equal('foo'); + assert.equal(result, 1); }); }); + it('should handle multiple columns', async () => { + const table = 'ctx_queryone_multiple_columns'; + + await ctx.exec(`CREATE TABLE ${table} (id INT, txt CHAR(3))`); + await ctx.insert(table, [{ id: 1, txt: 'foo' }]); + const result = await ctx.queryOne(`SELECT id, txt FROM ${table} WHERE id = 1`); + + assert.equal(result, 1); + }); + + it('should handle multiple rows', async () => { + const table = 'ctx_queryone_multiple_rows'; + + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }, { id: 2 }]); + const result = await ctx.queryOne(`SELECT id FROM ${table} ORDER BY id`); + + assert.equal(result, 1); + }); + it('should accept query params as an array', async () => { - await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - const result = await ctx.queryOne('SELECT "col1" FROM "t" WHERE "id" = ?', [1]); + const table = 'ctx_queryone_params_array'; + + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }, { id: 2 }]); + const result = await ctx.queryOne(`SELECT id FROM ${table} WHERE id = ?`, [1]); - expect(result).to.equal('foo'); + assert.equal(result, 1); }); it('should accept query params as an object', async () => { - await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - const result = await ctx.queryOne('SELECT "col1" FROM "t" WHERE "id" = :id', { id: 1 }); + const table = 'ctx_queryone_params_object'; - expect(result).to.equal('foo'); + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }, { id: 2 }]); + const result = await ctx.queryOne(`SELECT id FROM ${table} WHERE id = :id`, { id: 1 }); + + assert.equal(result, 1); }); it('should return typecasted result', async () => { - await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - const id = await ctx.queryOne('SELECT "id" FROM "t" WHERE "id" = 1'); + const table = 'ctx_queryone_typecasted_result'; - expect(id).to.equal(1); + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }, { id: 2 }]); + const id = await ctx.queryOne(`SELECT id FROM ${table} WHERE id = 1`); + + assert.equal(id, 1); }); }); describe('#queryCol', () => { [ - ['should resolve to an array of values', 'SELECT "col1" FROM "t"'], - ['should handle aliases', 'SELECT "col1" AS "funkyAlias" FROM "t"'], - ['should handle multiple columns', 'SELECT "col1", "id" FROM "t"'] - ].forEach(([description, sql]) => { + [ + 'should resolve to an array of values', + 'ctx_querycol_array_values', + 'SELECT id FROM ctx_querycol_array_values' + ], + ['should handle aliases', 'ctx_querycol_aliases', 'SELECT id AS funkyAlias FROM ctx_querycol_aliases'] + ].forEach(([description, table, sql]) => { it(description, async () => { - await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }, { id: 2 }]); const result = await ctx.queryCol(sql); - expect(result).to.eql(['foo', 'bar']); + assert.deepEqual(result, [1, 2]); }); }); - it('should accept query params as an array', async () => { - await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } + it('should handle multiple columns', async () => { + const table = 'ctx_querycol_multiple_columns'; + + await ctx.exec(`CREATE TABLE ${table} (id INT, txt CHAR(3))`); + await ctx.insert(table, [ + { id: 1, txt: 'foo' }, + { id: 2, txt: 'bar' } ]); - const result = await ctx.queryCol('SELECT "col1" FROM "t" WHERE "id" = ?', [1]); + const result = await ctx.queryCol(`SELECT id, txt FROM ${table} ORDER BY id`); + + assert.deepEqual(result, [1, 2]); + }); + + it('should accept query params as an array', async () => { + const table = 'ctx_querycol_params_array'; - expect(result).to.eql(['foo']); + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }]); + const result = await ctx.queryCol(`SELECT id FROM ${table} WHERE id = ?`, [1]); + + assert.deepEqual(result, [1]); }); it('should accept query params as an object', async () => { - await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - const result = await ctx.queryCol('SELECT "col1" FROM "t" WHERE "id" = :id', { id: 1 }); + const table = 'ctx_querycol_params_object'; + + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }]); + const result = await ctx.queryCol(`SELECT id FROM ${table} WHERE id = :id`, { id: 1 }); - expect(result).to.eql(['foo']); + assert.deepEqual(result, [1]); }); it('should return typecasted result', async () => { - await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - const [id] = await ctx.queryCol('SELECT "id" FROM "t" WHERE "id" = 1'); + const table = 'ctx_querycol_typecasted_result'; + + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }]); + const result = await ctx.queryCol(`SELECT id FROM ${table}`); - expect(id).to.equal(1); + assert.deepEqual(result, [1]); }); }); describe('DML statements', () => { describe('#insert', () => { it('should return last inserted id', async () => { - const { insertId } = await tableWithAutoIncrement( - ctx, - 't1', - async () => await ctx.insert('t1', { col1: 'foo' }) - ); + const table = 'ctx_insert_insert_id'; - expect(insertId).to.equal(1); + await ctx.exec(`CREATE TABLE ${table} (id INT PRIMARY KEY AUTO_INCREMENT, col1 VARCHAR(10))`); + const { insertId } = await ctx.insert(table, { col1: 'foo' }); + + assert.equal(insertId, 1); }); it('should return number of affected rows', async () => { - const { affectedRows } = await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); + const table = 'ctx_insert_affected_rows'; + + await ctx.exec(`CREATE TABLE ${table} (id INT PRIMARY KEY AUTO_INCREMENT, col1 VARCHAR(10))`); + const { affectedRows } = await ctx.insert(table, [{ col1: 'foo' }, { col1: 'bar' }]); - expect(affectedRows).to.equal(2); + assert.equal(affectedRows, 2); }); }); describe('#update', () => { it('should return number of changed rows', async () => { - await ctx.insert('t', [ + const table = 'ctx_update_changed_rows'; + + await ctx.exec(`CREATE TABLE ${table} (id INT, col1 VARCHAR(10))`); + await ctx.insert(table, [ { id: 1, col1: 'foo' }, { id: 2, col1: 'bar' } ]); - const { changedRows } = await ctx.update('t', { col1: 'test' }, { id: 1 }); + const { changedRows } = await ctx.update(table, { col1: 'test' }, { id: 1 }); - expect(changedRows).to.equal(1); + assert.equal(changedRows, 1); }); it('should return number of affected rows', async () => { - await ctx.insert('t', [ + const table = 'ctx_update_affected_rows'; + + await ctx.exec(`CREATE TABLE ${table} (id INT, col1 VARCHAR(10))`); + await ctx.insert(table, [ { id: 1, col1: 'foo' }, { id: 2, col1: 'bar' } ]); - const { affectedRows } = await ctx.update('t', { col1: 'test' }, '1 = 1'); + const { affectedRows } = await ctx.update(table, { col1: 'test' }, '1 = 1'); - expect(affectedRows).to.equal(2); + assert.equal(affectedRows, 2); }); }); describe('#delete', () => { it('should return number of affected rows', async () => { - await ctx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); + const table = 'ctx_delete_affected_rows'; - const { affectedRows } = await ctx.delete('t', { id: 1 }); + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.insert(table, [{ id: 1 }, { id: 2 }]); - expect(affectedRows).to.equal(1); + const { affectedRows } = await ctx.delete(table, { id: 1 }); + + assert.equal(affectedRows, 1); }); }); describe('#exec', () => { it(`should resolve/return with insertId property`, async () => { - const { insertId } = await tableWithAutoIncrement( - ctx, - 't1', - async () => await ctx.exec(`INSERT INTO t1 (col1) VALUES ('insertId')`) - ); + const table = 'ctx_exec_insert_id'; + + await ctx.exec(`CREATE TABLE ${table} (id INT PRIMARY KEY AUTO_INCREMENT, col1 VARCHAR(10))`); + const { insertId } = await ctx.exec(`INSERT INTO ${table} (col1) VALUES ('insertId')`); - expect(insertId).to.equal(1); + assert.equal(insertId, 1); }); Object.entries({ @@ -281,10 +342,14 @@ describe('context', () => { insertId: 0 }).forEach(([property, value]) => { it(`should resolve/return with ${property} property`, async () => { - await ctx.insert('t', [{ id: 1, col1: 'foo' }]); - const result = await ctx.exec(`UPDATE t SET col1 = 'changedRows' WHERE id = 1`); + const table = `ctx_exec_update_${property.toLowerCase()}`; + + await ctx.exec(`CREATE TABLE ${table} (id INT, col1 VARCHAR(10))`); + await ctx.insert(table, [{ id: 1, col1: 'foo' }]); + const result = await ctx.exec(`UPDATE ${table} SET col1 = 'changedRows' WHERE id = 1`); - expect(result).to.have.property(property, value); + assert.ok(Object.hasOwn(result, property)); + assert.equal(result[property], value); }); }); }); @@ -292,46 +357,46 @@ describe('context', () => { describe('#transaction', () => { it('should support callback parameter', async () => { + const table = 'ctx_transaction_callback'; + + await ctx.exec(`CREATE TABLE ${table} (id INT, col1 VARCHAR(10))`); await ctx.transaction(async (trx) => { - await trx.insert('t', { id: 1, col1: 'foobar' }); + await trx.insert(table, { id: 1, col1: 'foobar' }); }); - const values = await ctx.queryRow('SELECT id, col1 FROM t'); + const row = await ctx.queryRow(`SELECT id, col1 FROM ${table}`); - expect(values).to.eql({ id: 1, col1: 'foobar' }); + assert.notEqual(row, null); + assert.deepEqual({ ...row }, { id: 1, col1: 'foobar' }); }); it('should automatically rollback on errors', async () => { - try { - await ctx.transaction(async (trx) => { - await trx.insert('t', { id: 1, col1: 'foo' }); - await trx.insert('nonexistent_table', { id: 1, col1: 'bar' }); - }); - } catch (e) { - const values = await ctx.query('SELECT id, col1 FROM t'); - - expect(e).to.have.property('code', 'ER_NO_SUCH_TABLE'); - expect(values).to.eql([]); - - return; - } - - throw new Error('Expected an error to be thrown'); + const table = 'ctx_transaction_rollback'; + + await ctx.exec(`CREATE TABLE ${table} (id INT, col1 VARCHAR(10))`); + await assert.rejects( + async () => + await ctx.transaction(async (trx) => { + await trx.insert(table, { id: 1, col1: 'foo' }); + await trx.insert('nonexistent_table', { id: 1, col1: 'bar' }); + }), + { code: 'ER_NO_SUCH_TABLE' } + ); + + const row = await ctx.queryRow(`SELECT id, col1 FROM ${table}`); + assert.equal(row, null); }); it('should rethrow errors', async () => { - try { - await ctx.transaction(async (trx) => { - await trx.insert('nonexistent_table', { col1: 'blablub' }); - }); - } catch (e) { - expect(e) - .to.be.an.instanceof(Error) - .and.to.have.property('message') - .and.to.contain(`Table 'flora_mysql_testdb.nonexistent_table' doesn't exist`); - return; - } - - throw new Error('Expected an error to be thrown'); + await assert.rejects( + async () => + await ctx.transaction(async (trx) => { + await trx.insert('nonexistent_table', { col1: 'blablub' }); + }), + { + name: 'Error', + message: /Table 'flora_mysql_testdb.nonexistent_table' doesn't exist/ + } + ); }); }); }); diff --git a/test/integration/error-handling.spec.js b/test/integration/error-handling.spec.js index 4578445..b98b6a8 100644 --- a/test/integration/error-handling.spec.js +++ b/test/integration/error-handling.spec.js @@ -1,6 +1,7 @@ 'use strict'; -const { expect } = require('chai'); +const assert = require('node:assert/strict'); +const { after, describe, it } = require('node:test'); const astTpl = require('../ast-tpl'); const { FloraMysqlFactory } = require('../FloraMysqlFactory'); @@ -19,15 +20,10 @@ describe('error handling', () => { database }; - try { - await ds.process(floraRequest); - } catch (e) { - expect(e).to.be.instanceof(Error); - expect(e.message).to.equal('Attribute "nonexistentAttr" is not provided by SQL query'); - return; - } - - throw new Error('Expected an error'); + await assert.rejects(async () => await ds.process(floraRequest), { + name: 'ImplementationError', + message: 'Attribute "nonexistentAttr" is not provided by SQL query' + }); }); it('should return an error if selected attribute has no corresponding alias', async () => { @@ -37,15 +33,10 @@ describe('error handling', () => { database }; - try { - await ds.process(floraRequest); - } catch (e) { - expect(e).to.be.instanceof(Error); - expect(e.message).to.equal('Attribute "nonexistentAttr" is not provided by SQL query'); - return; - } - - throw new Error('Expected an error'); + await assert.rejects(async () => await ds.process(floraRequest), { + name: 'ImplementationError', + message: 'Attribute "nonexistentAttr" is not provided by SQL query' + }); }); it('should log query in case of an error', async () => { @@ -57,16 +48,19 @@ describe('error handling', () => { _explain }; - try { - await ds.process(floraRequest); - } catch (e) { - expect(e).to.be.an.instanceOf(Error); - expect(_explain).to.have.property('sql', 'SELECT "t"."col1" FROM "nonexistent_table"'); - expect(_explain).to.have.property('host'); - return; - } + await assert.rejects( + async () => await ds.process(floraRequest), + (err) => { + assert.ok(err instanceof Error); + + assert.ok(Object.hasOwn(_explain, 'sql')); + assert.equal(_explain.sql, 'SELECT "flora_request_processing"."col1" FROM "nonexistent_table"'); - throw new Error('Expected an error'); + assert.ok(Object.hasOwn(_explain, 'host')); + + return true; + } + ); }); it('should log connection errors', async () => { @@ -78,16 +72,9 @@ describe('error handling', () => { _explain }; - try { - await ds.process(floraRequest); - } catch (e) { - expect(e) - .to.be.an.instanceOf(Error) - .and.to.have.property('message') - .to.include("Unknown database 'nonexistent_database'"); - return; - } - - throw new Error('Expected an error'); + await assert.rejects(async () => await ds.process(floraRequest), { + name: 'Error', + message: /Unknown database 'nonexistent_database'/ + }); }); }); diff --git a/test/integration/fixtures/2_create_table.sql b/test/integration/fixtures/2_create_table.sql deleted file mode 100644 index a4b7759..0000000 --- a/test/integration/fixtures/2_create_table.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TABLE t ( - id INT UNSIGNED PRIMARY KEY, - col1 VARCHAR(50) DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/test/integration/flora-request-processing.spec.js b/test/integration/flora-request-processing.spec.js index 6287eb4..598d93b 100644 --- a/test/integration/flora-request-processing.spec.js +++ b/test/integration/flora-request-processing.spec.js @@ -1,30 +1,26 @@ 'use strict'; -const chai = require('chai'); -const { expect } = chai; -const sinon = require('sinon'); +const assert = require('node:assert/strict'); +const { before, after, describe, it, mock } = require('node:test'); const astTpl = require('../ast-tpl'); const { FloraMysqlFactory } = require('../FloraMysqlFactory'); const ciCfg = require('./ci-config'); -chai.use(require('sinon-chai')); - describe('flora request processing', () => { const ds = FloraMysqlFactory.create(ciCfg); const database = process.env.MYSQL_DATABASE || 'flora_mysql_testdb'; const ctx = ds.getContext({ db: database }); + const table = 'flora_request_processing'; before(async () => { - await ctx.insert('t', [ + await ctx.exec(`CREATE TABLE ${table} (id INT PRIMARY KEY, col1 VARCHAR(10), col2 VARCHAR(10))`); + await ctx.insert(table, [ { id: 1, col1: 'foo' }, { id: 2, col1: 'bar' } ]); }); - after(async () => { - await ctx.exec('TRUNCATE TABLE t'); - await ds.close(); - }); + after(() => ds.close()); it('should handle flora requests', async () => { const result = await ds.process({ @@ -33,11 +29,13 @@ describe('flora request processing', () => { database }); - expect(result).to.have.property('totalCount').and.to.be.null; - expect(result).to.have.property('data').and.to.be.an('array'); + assert.ok(Object.hasOwn(result, 'totalCount')); + assert.equal(result.totalCount, null); + assert.ok(Object.hasOwn(result, 'data')); + assert.ok(Array.isArray(result.data)); const data = result.data.map(({ id, col1 }) => ({ id: parseInt(id, 10), col1 })); - expect(data).to.eql([ + assert.deepEqual(data, [ { id: 1, col1: 'foo' }, { id: 2, col1: 'bar' } ]); @@ -51,7 +49,10 @@ describe('flora request processing', () => { }); const [item] = data; - expect(item).to.be.an('object').and.to.have.property('id').and.to.eql(Buffer.from('1')); + assert.ok(typeof item === 'object'); + assert.ok(Object.hasOwn(item, 'id')); + assert.ok(item.id instanceof Buffer); + assert.equal(item.id.toString(), '1'); }); it('should query available results if "page" attribute is set in request', async () => { @@ -63,11 +64,11 @@ describe('flora request processing', () => { page: 2 }); - expect(result).to.have.property('totalCount').and.to.equal(2); + assert.ok(Object.hasOwn(result, 'totalCount')); + assert.equal(result.totalCount, 2); }); it('should respect useMaster flag', async () => { - const querySpy = sinon.spy(ds, '_query'); const floraRequest = { database, useMaster: true, @@ -77,14 +78,19 @@ describe('flora request processing', () => { page: 2 }; + mock.method(ds, '_query'); await ds.process(floraRequest); - expect(querySpy).to.have.been.calledWithMatch({ type: 'MASTER' }); - querySpy.restore(); + const [call] = ds._query.mock.calls; + const [ctx] = call.arguments; + assert.ok(Object.hasOwn(ctx, 'type')); + assert.equal(ctx.type, 'MASTER'); + + ds._query.mock.restore(); }); it('should use modified query AST', async () => { - const querySpy = sinon.spy(ds, '_query'); + mock.method(ds, '_query'); const floraRequest = { database, attributes: ['col1'], @@ -97,17 +103,17 @@ describe('flora request processing', () => { floraRequest.queryAst.where = { type: 'binary_expr', operator: '<', - left: { type: 'column_ref', table: 't', column: 'id' }, + left: { type: 'column_ref', table, column: 'id' }, right: { type: 'number', value: 0 } }; await ds.process(floraRequest); - expect(querySpy).to.have.been.calledWith( - sinon.match.object, - sinon.match('WHERE "t"."id" < 0'), - sinon.match.any - ); - querySpy.restore(); + const [call] = ds._query.mock.calls; + const [, sql] = call.arguments; + + assert.equal(sql, `SELECT "${table}"."col1" FROM "${table}" WHERE "${table}"."id" < 0`); + + ds._query.mock.restore(); }); }); diff --git a/test/integration/init-queries.spec.js b/test/integration/init-queries.spec.js index 8c1e13e..90381d4 100644 --- a/test/integration/init-queries.spec.js +++ b/test/integration/init-queries.spec.js @@ -1,6 +1,7 @@ 'use strict'; -const { expect } = require('chai'); +const assert = require('node:assert/strict'); +const { afterEach, describe, it } = require('node:test'); const { FloraMysqlFactory } = require('../FloraMysqlFactory'); const ciCfg = require('./ci-config'); @@ -19,7 +20,7 @@ describe('init queries', () => { await ctx.query('SELECT 1 FROM dual'); const sqlMode = await ctx.queryOne('SELECT @@sql_mode'); - expect(sqlMode).to.match(/\bANSI\b/); + assert.match(sqlMode, /\bANSI\b/); }); it('should execute single init query', async () => { @@ -31,7 +32,7 @@ describe('init queries', () => { await ctx.query('SELECT 1 FROM dual'); const maxExecutionTime = await ctx.queryOne('SELECT @@max_execution_time'); - expect(maxExecutionTime).to.equal(1337); + assert.equal(maxExecutionTime, 1337); }); it('should execute multiple init queries', async () => { @@ -49,8 +50,8 @@ describe('init queries', () => { ctx.queryOne('SELECT @@max_execution_time') ]); - expect(sqlMode).to.contain('ANSI_QUOTES'); - expect(maxExecutionTime).to.equal(1337); + assert.match(sqlMode, /\bANSI_QUOTES\b/); + assert.equal(maxExecutionTime, 1337); }); it('should execute custom init function', async () => { @@ -70,7 +71,7 @@ describe('init queries', () => { await ctx.query('SELECT 1 FROM dual'); const maxExecutionTime = await ctx.queryOne('SELECT @@max_execution_time'); - expect(maxExecutionTime).to.equal(1337); + assert.equal(maxExecutionTime, 1337); }); it('should handle server specific init queries', async () => { @@ -89,23 +90,19 @@ describe('init queries', () => { ctx.queryOne('SELECT @@max_execution_time') ]); - expect(sqlMode).to.contain('ANSI_QUOTES'); - expect(maxExecutionTime).to.equal(1337); + assert.match(sqlMode, /\bANSI_QUOTES\b/); + assert.equal(maxExecutionTime, 1337); }); it('should handle errors', async () => { - const config = { ...ciCfg, onConnect: 'SELECT nonExistentAttr FROM t' }; + const config = { ...ciCfg, onConnect: 'SELECT nonExistentAttr FROM dual' }; ds = FloraMysqlFactory.create(config); const ctx = ds.getContext(ctxCfg); - try { - await ctx.query('SELECT 1 FROM dual'); - } catch (err) { - expect(err).to.include({ - code: 'ER_BAD_FIELD_ERROR', - message: `ER_BAD_FIELD_ERROR: Unknown column 'nonExistentAttr' in 'field list'` - }); - } + await assert.rejects(async () => await ctx.query('SELECT 1 FROM dual'), { + code: 'ER_BAD_FIELD_ERROR', + message: `ER_BAD_FIELD_ERROR: Unknown column 'nonExistentAttr' in 'field list'` + }); }); }); diff --git a/test/integration/query-method.spec.js b/test/integration/query-method.spec.js index b45d947..64ed58e 100644 --- a/test/integration/query-method.spec.js +++ b/test/integration/query-method.spec.js @@ -1,9 +1,8 @@ 'use strict'; -const chai = require('chai'); -const { expect } = chai; +const assert = require('node:assert/strict'); +const { after, describe, it, mock } = require('node:test'); const PoolConnection = require('../../node_modules/mysql/lib/PoolConnection'); -const sinon = require('sinon'); const { FloraMysqlFactory } = require('../FloraMysqlFactory'); const ciCfg = require('./ci-config'); @@ -17,11 +16,13 @@ describe('datasource-mysql', () => { describe('query method', () => { it('should release pool connections manually', async () => { - const releaseSpy = sinon.spy(PoolConnection.prototype, 'release'); + mock.method(PoolConnection.prototype, 'release'); await ctx.query('SELECT 1 FROM dual'); - expect(releaseSpy).to.have.been.calledOnce; - releaseSpy.restore(); + + assert.equal(PoolConnection.prototype.release.mock.callCount(), 1); + + PoolConnection.prototype.release.mock.restore(); }); }); }); diff --git a/test/integration/table-with-auto-increment.js b/test/integration/table-with-auto-increment.js deleted file mode 100644 index aed860d..0000000 --- a/test/integration/table-with-auto-increment.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -module.exports = async function tableWithAutoIncrement(ctx, tableName, callback) { - await ctx.exec(`DROP TABLE IF EXISTS ${tableName}`); - await ctx.exec(` - CREATE TABLE ${tableName} ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - col1 VARCHAR(50) DEFAULT NULL - ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - `); - - return callback(); -}; diff --git a/test/integration/tool/wait-for-mysql.js b/test/integration/tool/wait-for-mysql.js index 2bfd93f..f6ae09a 100644 --- a/test/integration/tool/wait-for-mysql.js +++ b/test/integration/tool/wait-for-mysql.js @@ -6,4 +6,4 @@ const [, , host, port, database] = process.argv; const connection = mysql.createConnection({ user: 'root', database, host, port }); connection.once('error', () => process.exit(1)); -connection.query('SELECT * FROM t LIMIT 1', (err) => process.exit(err ? 1 : 0)); +connection.query('SELECT 1 FROM dual', (err) => process.exit(err ? 1 : 0)); diff --git a/test/integration/transaction.spec.js b/test/integration/transaction.spec.js index 17bfbe4..3b2ff0a 100644 --- a/test/integration/transaction.spec.js +++ b/test/integration/transaction.spec.js @@ -1,13 +1,9 @@ 'use strict'; -const { expect } = require('chai'); -const sinon = require('sinon'); - -const PoolConnection = require('../../node_modules/mysql/lib/PoolConnection'); -const Transaction = require('../../lib/transaction'); +const assert = require('node:assert/strict'); +const { after, describe, it } = require('node:test'); const { FloraMysqlFactory } = require('../FloraMysqlFactory'); -const tableWithAutoIncrement = require('./table-with-auto-increment'); const ciCfg = require('./ci-config'); describe('transaction', () => { @@ -17,301 +13,322 @@ describe('transaction', () => { after(() => ds.close()); - it('should return a transaction', async () => { - const trx = await ctx.transaction(); - await trx.rollback(); - - expect(trx).to.be.instanceOf(Transaction); - }); - describe('transaction handling', () => { - let queryFnSpy; - - beforeEach(() => { - queryFnSpy = sinon.spy(PoolConnection.prototype, 'query'); - }); - - afterEach(() => queryFnSpy.restore()); + it('should send COMMIT on commit()', async () => { + const table = 'trx_commit'; - it('should acquire a connection and start the transaction', async () => { - const trx = await ctx.transaction(); + await ctx.exec(`CREATE TABLE ${table} (id INT)`); - expect(queryFnSpy).to.have.been.calledWith('START TRANSACTION'); - await trx.rollback(); - }); + const trx1 = await ctx.transaction(); + await trx1.insert(table, { id: 1 }); + const trxRunningCount = await ctx.queryOne(`SELECT COUNT(*) FROM ${table}`); + await trx1.commit(); - it('should send COMMIT on commit()', async () => { - const trx = await ctx.transaction(); + const trxFinishCount = await ctx.queryOne(`SELECT COUNT(*) FROM ${table}`); - await trx.commit(); - expect(queryFnSpy).to.have.been.calledWith('COMMIT'); + assert.equal(trxRunningCount, 0); + assert.equal(trxFinishCount, 1); }); it('should send ROLLBACK on rollback()', async () => { - const trx = await ctx.transaction(); + const table = 'trx_rollback'; + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + const trx = await ctx.transaction(); + await trx.insert(table, { id: 1 }); await trx.rollback(); - expect(queryFnSpy).to.have.been.calledWith('ROLLBACK'); + + const result = await ctx.queryOne(`SELECT COUNT(*) FROM ${table}`); + assert.equal(result, 0); }); }); describe('#insert', () => { it('should return last inserted id', async () => { - const { insertId } = await tableWithAutoIncrement(ctx, 't1', async () => { - const trx = await ctx.transaction(); - const result = await trx.insert('t1', { col1: 'foo' }); - await trx.rollback(); + const table = 'trx_insert_insert_id'; - return result; - }); + await ctx.exec(`CREATE TABLE ${table} (id INT PRIMARY KEY AUTO_INCREMENT, col1 VARCHAR(10))`); + const trx = await ctx.transaction(); + const { insertId } = await trx.insert(table, { col1: 'foo' }); + await trx.commit(); - expect(insertId).to.equal(1); + assert.equal(insertId, 1); }); it('should return number of affected rows', async () => { + const table = 'trx_insert_affected_rows'; + + await ctx.exec(`CREATE TABLE ${table} (id INT)`); const trx = await ctx.transaction(); - const { affectedRows } = await trx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - await trx.rollback(); + const { affectedRows } = await trx.insert(table, [{ id: 1 }, { id: 2 }]); + await trx.commit(); - expect(affectedRows).to.equal(2); + assert.equal(affectedRows, 2); }); }); describe('#update', () => { it('should return number of changed rows', async () => { + const table = 'trx_update_changed_rows'; + + await ctx.exec(`CREATE TABLE ${table} (id INT, col1 VARCHAR(10))`); const trx = await ctx.transaction(); - await trx.insert('t', [ + await trx.insert(table, [ { id: 1, col1: 'foo' }, { id: 2, col1: 'bar' } ]); - const { changedRows } = await trx.update('t', { col1: 'foobar' }, { id: 1 }); - await trx.rollback(); + const { changedRows } = await trx.update(table, { col1: 'foobar' }, { id: 1 }); + await trx.commit(); - expect(changedRows).to.equal(1); + assert.equal(changedRows, 1); }); it('should return number of affected rows', async () => { + const table = 'trx_update_affected_rows'; + + await ctx.exec(`CREATE TABLE ${table} (id INT, col1 VARCHAR(10))`); const trx = await ctx.transaction(); - await trx.insert('t', [ + await trx.insert(table, [ { id: 1, col1: 'foo' }, { id: 2, col1: 'bar' } ]); - const { affectedRows } = await trx.update('t', { col1: 'test' }, '1 = 1'); - await trx.rollback(); + const { affectedRows } = await trx.update(table, { col1: 'test' }, '1 = 1'); + await trx.commit(); - expect(affectedRows).to.equal(2); + assert.ok(affectedRows > 1); }); }); describe('#delete', () => { it('should return number of affected rows', async () => { + const table = 'trx_delete_affected_rows'; + + await ctx.exec(`CREATE TABLE ${table} (id INT)`); const trx = await ctx.transaction(); - await trx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - const { affectedRows } = await trx.delete('t', { id: 1 }); - await trx.rollback(); + await trx.insert(table, [{ id: 1 }, { id: 2 }]); + const { affectedRows } = await trx.delete(table, { id: 1 }); + await trx.commit(); - expect(affectedRows).to.equal(1); + assert.equal(affectedRows, 1); }); }); describe('#upsert', () => { it('should return number of affected rows', async () => { - const { affectedRows } = await tableWithAutoIncrement(ctx, 't1', async () => { - const trx = await ctx.transaction(); - const result = await trx.upsert('t1', { id: 1, col1: 'foobar' }, ['col1']); - await trx.rollback(); + const table = 'trx_upsert_affected_rows'; - return result; - }); + await ctx.exec(`CREATE TABLE ${table} (id INT, col1 VARCHAR(10))`); + const trx = await ctx.transaction(); + const { affectedRows } = await trx.upsert(table, { id: 1, col1: 'foobar' }, ['col1']); + await trx.commit(); - expect(affectedRows).to.equal(1); + assert.equal(affectedRows, 1); }); it('should return number of changed rows', async () => { - const { changedRows } = await tableWithAutoIncrement(ctx, 't1', async () => { - const trx = await ctx.transaction(); - const result = await trx.upsert('t1', { id: 1, col1: 'foobar' }, ['col1']); - await trx.rollback(); + const table = 'trx_upsert_changed_rows'; - return result; - }); + await ctx.exec(`CREATE TABLE ${table} (id INT, col1 VARCHAR(10))`); + const trx = await ctx.transaction(); + const { changedRows } = await trx.upsert(table, { id: 1, col1: 'foobar' }, ['col1']); + await trx.commit(); - expect(changedRows).to.equal(0); + assert.equal(changedRows, 0); }); it('should accept data as an object', async () => { - const trx = await ctx.transaction(); - const result = await trx.upsert('t', { id: 1, col1: 'foo' }, ['col1']); - await trx.rollback(); + const table = 'trx_upsert_data_object'; - expect(result).to.be.an('object'); + await ctx.exec(`CREATE TABLE ${table} (id INT, col1 VARCHAR(10))`); + await ctx.transaction(async (trx) => await trx.upsert(table, { id: 1, col1: 'foo' }, ['col1'])); + + const result = await ctx.query(`SELECT id, col1 FROM ${table}`); + assert.equal(result.length, 1); + assert.deepEqual({ ...result[0] }, { id: 1, col1: 'foo' }); }); it('should accept data as an array of objects', async () => { - const trx = await ctx.transaction(); - const result = await trx.upsert( - 't', - [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ], - ['col1'] + const table = 'trx_upsert_data_array'; + + await ctx.exec(`CREATE TABLE ${table} (id INT, col1 VARCHAR(10))`); + await ctx.transaction( + async (trx) => + await trx.upsert( + table, + [ + { id: 1, col1: 'foo' }, + { id: 2, col1: 'bar' } + ], + ['col1'] + ) ); - await trx.rollback(); - expect(result).to.be.an('object'); + const result = await ctx.query(`SELECT id, col1 FROM ${table}`); + assert.equal(result.length, 2); + assert.deepEqual({ ...result[0] }, { id: 1, col1: 'foo' }); + assert.deepEqual({ ...result[1] }, { id: 2, col1: 'bar' }); }); it('should accept updates as an object', async () => { - const trx = await ctx.transaction(); - const result = await trx.upsert('t', { id: 1, col1: 'foo' }, { col1: ctx.raw('MD5(col1)') }); - await trx.rollback(); + const table = 'trx_upsert_update_object'; + + await ctx.exec(`CREATE TABLE ${table} (id INT PRIMARY KEY, col1 VARCHAR(32))`); + await ctx.transaction(async (trx) => { + await trx.insert(table, { id: 1, col1: 'foo' }); + await trx.upsert(table, { id: 1, col1: 'bar' }, { col1: ctx.raw('MD5(col1)') }); + }); - expect(result).to.be.an('object'); + const hash = await ctx.queryOne(`SELECT col1 FROM ${table} WHERE id = 1`); + assert.equal(hash, 'acbd18db4cc2f85cedef654fccc4a4d8'); }); it('should accept updates as an array', async () => { - const trx = await ctx.transaction(); - const result = await trx.upsert('t', { id: 1, col1: 'foo' }, ['col1']); - await trx.rollback(); + const table = 'trx_upsert_update_array'; - expect(result).to.be.an('object'); + await ctx.exec(`CREATE TABLE ${table} (id INT PRIMARY KEY, col1 VARCHAR(10))`); + await ctx.transaction(async (trx) => { + await trx.insert(table, { id: 1, col1: 'foo' }); + await trx.upsert(table, { id: 1, col1: 'bar' }, ['col1']); + }); + + const result = await ctx.queryOne(`SELECT col1 FROM ${table} WHERE id = 1`); + assert.equal(result, 'bar'); }); }); describe('#query', () => { it('should support parameters as an array', async () => { - const trx = await ctx.transaction(); - await trx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - const result = await trx.query('SELECT "id", "col1" FROM "t" WHERE "id" = ?', [1]); - await trx.rollback(); + const table = 'trx_query_array'; - expect(result).to.eql([{ id: 1, col1: 'foo' }]); + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + const trx = await ctx.transaction(); + await trx.insert(table, [{ id: 1 }]); + await assert.doesNotReject(async () => await trx.query(`SELECT id FROM ${table} WHERE id = ?`, [1])); + await trx.commit(); }); it('should support named parameters', async () => { - const trx = await ctx.transaction(); - await trx.insert('t', [{ id: 1, col1: 'foo' }]); - const result = await trx.query('SELECT "id", "col1" FROM "t" WHERE "id" = :id', { id: 1 }); - await trx.rollback(); + const table = 'trx_query_object'; - expect(result).to.eql([{ id: 1, col1: 'foo' }]); + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + const trx = await ctx.transaction(); + await trx.insert(table, [{ id: 1 }]); + await assert.doesNotReject( + async () => await trx.query(`SELECT id FROM ${table} WHERE id = :id`, { id: 1 }) + ); + await trx.commit(); }); it('should return typecasted result', async () => { + const table = 'trx_query_typecast'; + + await ctx.exec(`CREATE TABLE ${table} (id INT)`); const trx = await ctx.transaction(); - await trx.insert('t', [{ id: 1, col1: 'foo' }]); - const [item] = await trx.query('SELECT "id" FROM "t" WHERE "id" = 1'); - await trx.rollback(); + await trx.insert(table, [{ id: 1 }]); + const [item] = await trx.query(`SELECT id FROM ${table} WHERE id = 1`); + await trx.commit(); - expect(item).to.have.property('id', 1); + assert.ok(Object.hasOwn(item, 'id')); + assert.equal(item.id, 1); }); }); describe('#queryRow', () => { it('should return an object', async () => { + const table = 'trx_queryrow_return_object'; + + await ctx.exec(`CREATE TABLE ${table} (id INT, col1 VARCHAR(10))`); const trx = await ctx.transaction(); - await trx.insert('t', [{ id: 1, col1: 'foo' }]); - const result = await trx.queryRow('SELECT "id", "col1" FROM "t" WHERE "id" = ?', [1]); - await trx.rollback(); + await trx.insert(table, [{ id: 1, col1: 'foo' }]); + const result = await trx.queryRow(`SELECT id, col1 FROM ${table} WHERE id = ?`, [1]); + await trx.commit(); - expect(result).to.eql({ id: 1, col1: 'foo' }); + assert.deepEqual({ ...result }, { id: 1, col1: 'foo' }); }); it('should return typecasted result', async () => { - const trx = await ctx.transaction(); - await trx.insert('t', [{ id: 1, col1: 'foo' }]); - const row = await trx.queryRow('SELECT "id", "col1" FROM "t" WHERE "id" = 1'); - await trx.rollback(); + const table = 'trx_queryrow_typecast'; + let row; + + await ctx.exec(`CREATE TABLE ${table} (id INT, col1 VARCHAR(10))`); + await ctx.transaction(async (trx) => { + await trx.insert(table, [{ id: 1, col1: 'foo' }]); + row = await trx.queryRow(`SELECT id, col1 FROM ${table} WHERE id = 1`); + }); - expect(row).to.have.property('id', 1); + assert.ok(Object.hasOwn(row, 'id')); + assert.equal(row.id, 1); }); }); describe('#queryOne', () => { - it('should return array of values', async () => { - const trx = await ctx.transaction(); - await trx.insert('t', [{ id: 1, col1: 'foo' }]); - const result = await trx.queryOne('SELECT "col1" FROM "t" WHERE "id" = ?', [1]); - await trx.rollback(); + it('should return single value', async () => { + const table = 'trx_queryone_return_value'; + let value; + + await ctx.exec(`CREATE TABLE ${table} (id INT, col1 VARCHAR(10))`); + await ctx.transaction(async (trx) => { + await trx.insert(table, [{ id: 1, col1: 'foo' }]); + value = await trx.queryOne(`SELECT col1 FROM ${table} WHERE id = ?`, [1]); + }); - expect(result).to.equal('foo'); + assert.equal(value, 'foo'); }); it('should return typecasted result', async () => { - const trx = await ctx.transaction(); - await trx.insert('t', [{ id: 1, col1: 'foo' }]); - const id = await trx.queryOne('SELECT "id" FROM "t" WHERE "id" = 1'); - await trx.rollback(); + const table = 'trx_queryone_return_typecast'; + let id; - expect(id).to.equal(1); + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.transaction(async (trx) => { + await trx.insert(table, [{ id: 1 }]); + id = await trx.queryOne(`SELECT id FROM ${table} WHERE id = 1`); + }); + + assert.equal(id, 1); }); }); describe('#queryCol', () => { - it('should return array of values', async () => { - const trx = await ctx.transaction(); - await trx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - const result = await trx.queryCol('SELECT "col1" FROM "t" WHERE "id" = ?', [1]); - await trx.rollback(); - - expect(result).to.eql(['foo']); - }); - - it('should return typecasted result', async () => { - const trx = await ctx.transaction(); - await trx.insert('t', [ - { id: 1, col1: 'foo' }, - { id: 2, col1: 'bar' } - ]); - const [id] = await trx.queryCol('SELECT "id" FROM "t"'); - await trx.rollback(); + it('should return array of (typecasted) values', async () => { + const table = 'trx_querycol_return_value'; + let result; + + await ctx.exec(`CREATE TABLE ${table} (id INT)`); + await ctx.transaction(async (trx) => { + await trx.insert(table, [{ id: 1 }, { id: 2 }]); + result = await trx.queryCol(`SELECT id FROM ${table}`); + }); - expect(id).to.equal(1); + assert.ok(Array.isArray(result)); + assert.deepEqual(result, [1, 2]); }); }); describe('#exec', () => { it('should support parameters', async () => { const trx = await ctx.transaction(); - await trx.insert('t', [{ id: 1, col1: 'foo' }]); - const result = await trx.query('SELECT "id", "col1" FROM "t" WHERE "id" = ?', [1]); + await assert.doesNotReject(async () => await trx.exec('SHOW TABLES LIKE ?', ['foo'])); await trx.rollback(); - - expect(result).to.eql([{ id: 1, col1: 'foo' }]); }); it('should support named parameters', async () => { const trx = await ctx.transaction(); - await trx.insert('t', [{ id: 1, col1: 'foo' }]); - const result = await trx.query('SELECT "id", "col1" FROM "t" WHERE "id" = :id', { id: 1 }); + await assert.doesNotReject(async () => await trx.exec('SHOW TABLES LIKE :name', { name: 'foo' })); await trx.rollback(); - - expect(result).to.eql([{ id: 1, col1: 'foo' }]); }); it(`should resolve/return with insertId property`, async () => { - const { insertId } = await tableWithAutoIncrement(ctx, 't1', async () => { - const trx = await ctx.transaction(); - const result = await trx.exec(`INSERT INTO t1 (col1) VALUES ('insertId')`); - await trx.rollback(); + const table = 'trx_exec_insert_id'; + let result; - return result; + await ctx.exec(`CREATE TABLE ${table} (id INT PRIMARY KEY AUTO_INCREMENT, col1 VARCHAR(10))`); + await ctx.transaction(async (trx) => { + result = await trx.exec(`INSERT INTO ${table} (col1) VALUES ('insertId')`); }); - expect(insertId).to.equal(1); + assert.ok(Object.hasOwn(result, 'insertId')); + assert.equal(result.insertId, 1); }); Object.entries({ @@ -320,12 +337,17 @@ describe('transaction', () => { insertId: 0 }).forEach(([property, value]) => { it(`should resolve/return with ${property} property`, async () => { - const trx = await ctx.transaction(); - await trx.insert('t', [{ id: 1, col1: 'foo' }]); - const result = await trx.exec(`UPDATE t SET col1 = 'affectedRows' WHERE id = 1`); - await trx.rollback(); + const table = `trx_return_property_${property.toLowerCase()}`; + let result; + + await ctx.exec(`CREATE TABLE ${table} (id INT PRIMARY KEY, col1 VARCHAR(10))`); + await ctx.transaction(async (trx) => { + await trx.insert(table, [{ id: 1, col1: 'foo' }]); + result = await trx.exec(`UPDATE ${table} SET col1 = 'affectedRows' WHERE id = 1`); + }); - expect(result).to.have.property(property, value); + assert.ok(Object.hasOwn(result, property)); + assert.equal(result[property], value); }); }); }); diff --git a/test/unit/connection-pooling.spec.js b/test/unit/connection-pooling.spec.js deleted file mode 100644 index a878e50..0000000 --- a/test/unit/connection-pooling.spec.js +++ /dev/null @@ -1,195 +0,0 @@ -'use strict'; - -const Connection = require('../../node_modules/mysql/lib/Connection'); -const PoolCluster = require('../../node_modules/mysql/lib/PoolCluster'); - -const chai = require('chai'); -const { expect } = chai; -const sinon = require('sinon'); - -const { FloraMysqlFactory, defaultCfg } = require('../FloraMysqlFactory'); -const testCfg = { - ...defaultCfg, - servers: { default: { user: 'foo', password: 'bar', masters: [{ host: 'mysql.example.com' }] } } -}; - -const sandbox = sinon.createSandbox(); - -chai.use(require('sinon-chai')); - -const PORT = process.env.MYSQL_PORT || 3306; - -describe('connection pooling', () => { - const ctxCfg = { db: 'test' }; - let poolSpy; - - beforeEach(() => { - sandbox.stub(Connection.prototype, 'connect').yields(null); - sandbox.stub(Connection.prototype, 'query').yields(null, []); - poolSpy = sandbox.spy(PoolCluster.prototype, 'add'); - }); - - afterEach(() => sandbox.restore()); - - describe('single server', () => { - describe('credentials', () => { - it('server specifc', async () => { - const ds = FloraMysqlFactory.create(testCfg); - const ctx = ds.getContext(ctxCfg); - - await ctx.exec('SELECT 1 FROM dual'); - - expect(poolSpy).to.have.been.calledWithMatch('mysql.example.com', { user: 'foo', password: 'bar' }); - }); - - it('global', async () => { - const cfg = { ...structuredClone(testCfg), user: 'root', password: 'secret' }; - delete cfg.servers.default.user; - delete cfg.servers.default.password; - - const ds = FloraMysqlFactory.create(cfg); - const ctx = ds.getContext(ctxCfg); - - await ctx.exec('SELECT 1 FROM dual'); - - expect(poolSpy).to.have.been.calledWithMatch('MASTER_mysql.example.com', { - user: 'root', - password: 'secret' - }); - }); - }); - - describe('port', () => { - it('default', async () => { - const cfg = structuredClone(testCfg); - cfg.servers.default.port = PORT; - const ds = FloraMysqlFactory.create(cfg); - const ctx = ds.getContext(ctxCfg); - - await ctx.exec('SELECT 1 FROM dual'); - - expect(poolSpy).to.have.been.calledWithMatch('MASTER_mysql.example.com', { port: PORT }); - }); - - it('custom', async () => { - const cfg = { ...structuredClone(testCfg) }; - cfg.servers.default.port = 1337; - const ds = FloraMysqlFactory.create(cfg); - const ctx = ds.getContext(ctxCfg); - - await ctx.exec('SELECT 1 FROM dual'); - - expect(poolSpy).to.have.been.calledWithMatch('MASTER_mysql.example.com', { port: 1337 }); - }); - }); - - describe('connect timeout', () => { - it('default', async () => { - const ds = FloraMysqlFactory.create(testCfg); - const ctx = ds.getContext(ctxCfg); - await ctx.exec('SELECT 1 FROM dual'); - - expect(poolSpy).to.have.been.calledWithMatch('MASTER_mysql.example.com', { connectTimeout: 3000 }); - }); - - it('server specific', async () => { - const cfg = structuredClone(testCfg); - cfg.servers.default.connectTimeout = 1000; - const ds = FloraMysqlFactory.create(cfg); - const ctx = ds.getContext(ctxCfg); - - await ctx.exec('SELECT 1 FROM dual'); - - expect(poolSpy).to.have.been.calledWithMatch('MASTER_mysql.example.com', { connectTimeout: 1000 }); - }); - - it('global', async () => { - const cfg = { ...structuredClone(testCfg), connectTimeout: 1500 }; - const ds = FloraMysqlFactory.create(cfg); - const ctx = ds.getContext(ctxCfg); - - await ctx.exec('SELECT 1 FROM dual'); - - expect(poolSpy).to.have.been.calledWithMatch('MASTER_mysql.example.com', { connectTimeout: 1500 }); - }); - }); - - describe('pool size', () => { - it('default', async () => { - const ds = FloraMysqlFactory.create(testCfg); - const ctx = ds.getContext(ctxCfg); - - await ctx.exec('SELECT 1 FROM dual'); - - expect(poolSpy).to.have.been.calledWithMatch('MASTER_mysql.example.com', { connectionLimit: 10 }); - }); - - it('server specific', async () => { - const cfg = structuredClone(testCfg); - cfg.servers.default.poolSize = 100; - const ds = FloraMysqlFactory.create(cfg); - const ctx = ds.getContext(ctxCfg); - - await ctx.exec('SELECT 1 FROM dual'); - - expect(poolSpy).to.have.been.calledWithMatch('MASTER_mysql.example.com', { connectionLimit: 100 }); - }); - - it('global', async () => { - const cfg = { ...structuredClone(testCfg), poolSize: 50 }; - const ds = FloraMysqlFactory.create(cfg); - const ctx = ds.getContext(ctxCfg); - - await ctx.exec('SELECT 1 FROM dual'); - - expect(poolSpy).to.have.been.calledWithMatch('MASTER_mysql.example.com', { connectionLimit: 50 }); - }); - }); - - it('should always use "master" server', async () => { - const ds = FloraMysqlFactory.create(testCfg); - const ctx = ds.getContext(ctxCfg); - const connectionSpy = sandbox.spy(PoolCluster.prototype, 'getConnection'); - - await ctx.exec('SELECT 1 FROM dual'); - - expect(connectionSpy).to.have.been.always.calledWith('MASTER*', sinon.match.any); - }); - }); - - describe('master/slave', () => { - xit('cluster config options'); - - it('should create pools for masters and slaves', async () => { - const user = 'root'; - const password = 'secret'; - const baseCfg = { user, password }; - const ds = FloraMysqlFactory.create({ - servers: { - default: { - user, - password, - masters: [{ host: 'mysql-master' }], - slaves: [ - { host: 'mysql-slave1', port: 1337 }, - { host: 'mysql-slave2', port: 4711 } - ] - } - } - }); - const ctx = ds.getContext(ctxCfg); - const sql = 'SELECT 1 FROM dual'; - - await Promise.all([ctx.exec(sql), ctx.query(sql)]); - - const masterCfg = { ...baseCfg, ...{ host: 'mysql-master', port: 3306 } }; - expect(poolSpy).to.have.been.calledWith('MASTER_mysql-master', sinon.match(masterCfg)); - - const slaveCfg1 = { ...baseCfg, ...{ host: 'mysql-slave1', port: 1337 } }; - expect(poolSpy).to.have.been.calledWith('SLAVE_mysql-slave1', sinon.match(slaveCfg1)); - - const slaveCfg2 = { ...baseCfg, ...{ host: 'mysql-slave2', port: 4711 } }; - expect(poolSpy).to.have.been.calledWith('SLAVE_mysql-slave2', sinon.match(slaveCfg2)); - }); - }); -}); diff --git a/test/unit/context.spec.js b/test/unit/context.spec.js index d84a0b1..88dd5ed 100644 --- a/test/unit/context.spec.js +++ b/test/unit/context.spec.js @@ -1,17 +1,9 @@ 'use strict'; -const chai = require('chai'); -const { expect } = chai; -const sinon = require('sinon'); -const sandbox = sinon.createSandbox(); - -const Connection = require('../../node_modules/mysql/lib/Connection'); -const Transaction = require('../../lib/transaction'); +const assert = require('node:assert/strict'); +const { afterEach, beforeEach, describe, it, mock } = require('node:test'); const { FloraMysqlFactory, defaultCfg } = require('../FloraMysqlFactory'); -const { ImplementationError } = require('@florajs/errors'); - -chai.use(require('sinon-chai')); describe('context', () => { const testCfg = { @@ -24,8 +16,6 @@ describe('context', () => { const db = 'db'; const ctx = ds.getContext({ db }); - afterEach(() => sandbox.restore()); - describe('interface', () => { [ 'delete', @@ -43,80 +33,96 @@ describe('context', () => { 'transaction' ].forEach((method) => { it(`should export "${method}" method`, () => { - expect(ctx[method]).to.be.a('function'); + assert.ok(typeof ctx[method] === 'function'); }); }); }); describe('#constructor', () => { it('should throw an error when database setting is missing', () => { - expect(() => ds.getContext({})).to.throw(ImplementationError, 'Context requires a db (database) property'); + assert.throws(() => ds.getContext({}), { + name: 'ImplementationError', + message: 'Context requires a db (database) property' + }); }); it('should throw an error when database setting is not a string', () => { - expect(() => ds.getContext({ db: undefined })).to.throw( - ImplementationError, - 'Invalid value for db (database) property' - ); + assert.throws(() => ds.getContext({ db: undefined }), { + name: 'ImplementationError', + message: 'Invalid value for db (database) property' + }); }); it('should throw an error when database setting is an empty string', () => { - expect(() => ds.getContext({ db: ' ' })).to.throw( - ImplementationError, - 'Invalid value for db (database) property' - ); + assert.throws(() => ds.getContext({ db: ' ' }), { + name: 'ImplementationError', + message: 'Invalid value for db (database) property' + }); }); }); describe('#query', () => { - let queryStub; - - beforeEach(() => { - queryStub = sandbox.stub(ds, '_query').resolves({ results: [] }); - }); + beforeEach(() => mock.method(ds, '_query', async () => Promise.resolve({ results: [] }))); + afterEach(() => ds._query.mock.restore()); it('should use a slave connection', async () => { const sql = 'SELECT 1 FROM dual'; await ctx.query(sql); - expect(queryStub).to.have.been.calledWith({ type: 'SLAVE', db, server: 'default' }, sql); + + const [call] = ds._query.mock.calls; + assert.deepEqual(call.arguments, [{ type: 'SLAVE', db, server: 'default' }, 'SELECT 1 FROM dual']); }); it('should handle placeholders', async () => { - const sql = `SELECT id FROM "t" WHERE col1 = 'val1'`; - await ctx.exec('SELECT id FROM "t" WHERE col1 = ?', ['val1']); - expect(queryStub).to.have.been.calledWith({ type: 'MASTER', db, server: 'default' }, sql); + await ctx.query('SELECT id FROM "t" WHERE col1 = ?', ['val1']); + + const [call] = ds._query.mock.calls; + assert.deepEqual(call.arguments, [ + { type: 'SLAVE', db, server: 'default' }, + `SELECT id FROM "t" WHERE col1 = 'val1'` + ]); }); it('should handle named placeholders', async () => { - const sql = `SELECT id FROM "t" WHERE col1 = 'val1'`; await ctx.query('SELECT id FROM "t" WHERE col1 = :col1', { col1: 'val1' }); - expect(queryStub).to.have.been.calledWith({ type: 'SLAVE', db, server: 'default' }, sql); + + const [call] = ds._query.mock.calls; + assert.deepEqual(call.arguments, [ + { type: 'SLAVE', db, server: 'default' }, + `SELECT id FROM "t" WHERE col1 = 'val1'` + ]); }); }); describe('#exec', () => { - let queryStub; - - beforeEach(() => { - queryStub = sandbox.stub(ds, '_query').resolves({ results: [] }); - }); + beforeEach(() => mock.method(ds, '_query', async () => Promise.resolve({ results: [] }))); + afterEach(() => ds._query.mock.restore()); it('should use a master connection', async () => { - const sql = 'SELECT 1 FROM dual'; - await ctx.exec(sql); - expect(queryStub).to.have.been.calledWith({ type: 'MASTER', db, server: 'default' }, sql); + await ctx.exec('SELECT 1 FROM dual'); + + const [call] = ds._query.mock.calls; + assert.deepEqual(call.arguments, [{ type: 'MASTER', db, server: 'default' }, 'SELECT 1 FROM dual']); }); it('should handle placeholders', async () => { - const sql = `SELECT id FROM "t" WHERE col1 = 'val1'`; await ctx.exec('SELECT id FROM "t" WHERE col1 = ?', ['val1']); - expect(queryStub).to.have.been.calledWith({ type: 'MASTER', db, server: 'default' }, sql); + + const [call] = ds._query.mock.calls; + assert.deepEqual(call.arguments, [ + { type: 'MASTER', db, server: 'default' }, + `SELECT id FROM "t" WHERE col1 = 'val1'` + ]); }); it('should handle named placeholders', async () => { - const sql = `SELECT id FROM "t" WHERE col1 = 'val1'`; await ctx.exec('SELECT id FROM "t" WHERE col1 = :col1', { col1: 'val1' }); - expect(queryStub).to.have.been.calledWith({ type: 'MASTER', db, server: 'default' }, sql); + + const [call] = ds._query.mock.calls; + assert.deepEqual(call.arguments, [ + { type: 'MASTER', db, server: 'default' }, + `SELECT id FROM "t" WHERE col1 = 'val1'` + ]); }); }); @@ -124,32 +130,27 @@ describe('context', () => { it('should pass through value', () => { const expr = ctx.raw('NOW()'); - expect(expr).to.be.an('object'); - expect(expr.toSqlString).to.be.a('function'); - expect(expr.toSqlString()).to.equal('NOW()'); + assert.ok(typeof expr === 'object'); + assert.ok(typeof expr.toSqlString === 'function'); + assert.equal(expr.toSqlString(), 'NOW()'); }); }); describe('#quote', () => { it('should quote values', () => { - expect(ctx.quote(`foo\\b'ar`)).to.equal(`'foo\\\\b\\'ar'`); + assert.equal(ctx.quote(`foo\\b'ar`), `'foo\\\\b\\'ar'`); }); }); describe('#quoteIdentifier', () => { it('should quote identifiers', () => { - expect(ctx.quoteIdentifier('table')).to.equal('`table`'); + assert.equal(ctx.quoteIdentifier('table'), '`table`'); }); }); describe('parameter placeholders', () => { - let queryStub; - - beforeEach(() => { - queryStub = sandbox.stub(ds, '_query').resolves({ results: [] }); - }); - - afterEach(() => sandbox.restore()); + beforeEach(() => mock.method(ds, '_query', async () => Promise.resolve({ results: [] }))); + afterEach(() => ds._query.mock.restore()); [ ['strings', 'val', 'SELECT "id" FROM "t" WHERE "col1" = ?', `SELECT "id" FROM "t" WHERE "col1" = 'val'`], @@ -177,52 +178,46 @@ describe('context', () => { ].forEach(([type, value, query, sql]) => { it(`should support ${type}`, async () => { await ctx.exec(query, [value]); - expect(queryStub).to.have.been.calledWith(sinon.match.object, sql); + + const [call] = ds._query.mock.calls; + assert.equal(call.arguments[1], sql); }); }); [ ['objects', {}, '"values" must not be an empty object'], ['arrays', [], '"values" must not be an empty array'] - ].forEach(([type, params, msg]) => { + ].forEach(([type, params, message]) => { it(`throw an error for empty ${type}`, async () => { - try { - await ctx.exec('SELECT "id" FROM "t" WHERE "col1" IN (:col1)', params); - } catch (e) { - expect(e).to.be.instanceOf(Error).with.property('message', msg); - expect(queryStub).to.not.have.been.called; - return; - } - - throw new Error('Expected an error'); + await assert.rejects( + async () => await ctx.exec('SELECT "id" FROM "t" WHERE "col1" IN (:col1)', params), + { + name: 'ImplementationError', + message + } + ); + assert.equal(ds._query.mock.callCount(), 0); }); }); it('throw an error for non-object or non-array values', async () => { - try { - await ctx.exec('SELECT "id" FROM "t" WHERE "col1" = ?', 'foo'); - } catch (e) { - expect(e).to.be.instanceOf(Error).with.property('message', '"values" must be an object or an array'); - expect(queryStub).to.not.have.been.called; - return; - } - - throw new Error('Expected an error'); + await assert.rejects(async () => await ctx.exec('SELECT "id" FROM "t" WHERE "col1" = ?', 'foo'), { + name: 'ImplementationError', + message: '"values" must be an object or an array' + }); + assert.equal(ds._query.mock.callCount(), 0); }); }); describe('#insert', () => { - let execStub; - - beforeEach(() => { - execStub = sandbox.stub(ctx, 'exec').resolves({ insertId: 1, affectedRow: 1 }); - }); + beforeEach(() => mock.method(ctx, 'exec', async () => Promise.resolve({ insertId: 1, affectedRow: 1 }))); + afterEach(() => ctx.exec.mock.restore()); it('should accept data as an object', async () => { await ctx.insert('t', { col1: 'val1', col2: 1, col3: ctx.raw('NOW()') }); - expect(execStub).to.have.been.calledWith( - "INSERT INTO `t` (`col1`, `col2`, `col3`) VALUES ('val1', 1, NOW())" - ); + + const [call] = ctx.exec.mock.calls; + assert.deepEqual(call.arguments, ["INSERT INTO `t` (`col1`, `col2`, `col3`) VALUES ('val1', 1, NOW())"]); }); it('should accept data as an array of objects', async () => { @@ -232,158 +227,128 @@ describe('context', () => { { col1: 'val1', col2: 1, col3: date }, { col1: 'val2', col2: 2, col3: date } ]); - expect(execStub).to.have.been.calledWith( + + const [call] = ctx.exec.mock.calls; + assert.deepEqual(call.arguments, [ `INSERT INTO \`t\` (\`col1\`, \`col2\`, \`col3\`) VALUES ('val1', 1, '${dateStr}'), ('val2', 2, '${dateStr}')` - ); + ]); }); it('should reject with an error if data is not set', async () => { - try { - await ctx.insert('t'); - } catch (e) { - expect(e).to.be.instanceOf(ImplementationError).with.property('message', 'data parameter is required'); - expect(execStub).to.not.have.been.called; - return; - } - - throw new Error('Expected promise to reject'); + await assert.rejects(async () => await ctx.insert('t'), { + name: 'ImplementationError', + message: 'data parameter is required' + }); }); it('should reject with an error if data neither an object nor an array', async () => { - try { - await ctx.insert('t', 'foo'); - } catch (e) { - expect(e) - .to.be.instanceOf(ImplementationError) - .with.property('message', 'data is neither an object nor an array of objects'); - expect(execStub).to.not.have.been.called; - return; - } - - throw new Error('Expected promise to reject'); + await assert.rejects(async () => await ctx.insert('t', 'foo'), { + name: 'ImplementationError', + message: 'data is neither an object nor an array of objects' + }); + assert.equal(ctx.exec.mock.callCount(), 0); }); }); describe('#update', () => { - let execStub; - - beforeEach(() => { - execStub = sandbox.stub(ctx, 'exec').resolves({ changedRows: 1 }); - }); + beforeEach(() => mock.method(ctx, 'exec', async () => Promise.resolve({ insertId: 1, affectedRow: 1 }))); + afterEach(() => ctx.exec.mock.restore()); it('should accept data as an object', async () => { await ctx.update('t', { col1: 'val1', col2: 1, col3: ctx.raw('NOW()') }, '1 = 1'); - expect(execStub).to.have.been.calledWith( + + const [call] = ctx.exec.mock.calls; + assert.deepEqual(call.arguments, [ `UPDATE \`t\` SET \`col1\` = 'val1', \`col2\` = 1, \`col3\` = NOW() WHERE 1 = 1` - ); + ]); }); it('should accept where as an object', async () => { await ctx.update('t', { col1: 'val1' }, { col2: 1, col3: ctx.raw('CURDATE()') }); - expect(execStub).to.have.been.calledWith( + + const [call] = ctx.exec.mock.calls; + assert.deepEqual(call.arguments, [ `UPDATE \`t\` SET \`col1\` = 'val1' WHERE \`col2\` = 1 AND \`col3\` = CURDATE()` - ); + ]); }); it('should reject with an error if data is not set', async () => { - try { - await ctx.update('t', {}, ''); - } catch (e) { - expect(e).to.be.instanceOf(ImplementationError).with.property('message', 'data is not set'); - expect(execStub).to.not.have.been.called; - return; - } - - throw new Error('Expected promise to reject'); + await assert.rejects(async () => await ctx.update('t', {}, ''), { + name: 'ImplementationError', + message: 'data is not set' + }); + assert.equal(ctx.exec.mock.callCount(), 0); }); it('should reject with an error if where expression is not set', async () => { - try { - await ctx.update('t', { col1: 'val1' }, ''); - } catch (e) { - expect(e).to.be.instanceOf(ImplementationError).with.property('message', 'where expression is not set'); - expect(execStub).to.not.have.been.called; - return; - } - - throw new Error('Expected promise to reject'); + await assert.rejects(async () => await ctx.update('t', { col1: 'val1' }, ''), { + name: 'ImplementationError', + message: 'where expression is not set' + }); + assert.equal(ctx.exec.mock.callCount(), 0); }); }); describe('#delete', () => { - let execStub; - - beforeEach(() => { - execStub = sandbox.stub(ctx, 'exec').resolves({ affectedRows: 1 }); - }); + beforeEach(() => mock.method(ctx, 'exec', async () => Promise.resolve({ affectedRows: 1 }))); + afterEach(() => ctx.exec.mock.restore()); it('should accept where as a string', async () => { await ctx.delete('t', '1 = 1'); - expect(execStub).to.have.been.calledWith(`DELETE FROM \`t\` WHERE 1 = 1`); + + const [call] = ctx.exec.mock.calls; + assert.deepEqual(call.arguments, [`DELETE FROM \`t\` WHERE 1 = 1`]); }); it('should accept where as an object', async () => { await ctx.delete('t', { col1: 'val1', col2: 1, col3: ctx.raw('CURDATE()') }); - expect(execStub).to.have.been.calledWith( + + const [call] = ctx.exec.mock.calls; + assert.deepEqual(call.arguments, [ `DELETE FROM \`t\` WHERE \`col1\` = 'val1' AND \`col2\` = 1 AND \`col3\` = CURDATE()` - ); + ]); }); it('should reject with an error if where expression is not set', async () => { - try { - await ctx.delete('t', ''); - } catch (e) { - expect(e).to.be.instanceOf(ImplementationError).with.property('message', 'where expression is not set'); - expect(execStub).to.not.have.been.called; - return; - } - - throw new Error('Expected promise to reject'); + await assert.rejects(async () => await ctx.delete('t', ''), { + name: 'ImplementationError', + message: 'where expression is not set' + }); + assert.equal(ctx.exec.mock.callCount(), 0); }); }); describe('#upsert', () => { - let execStub; - - beforeEach(() => { - execStub = sandbox.stub(ctx, 'exec').resolves({}); - }); + beforeEach(() => mock.method(ctx, 'exec', async () => Promise.resolve({ affectedRows: 1 }))); + afterEach(() => ctx.exec.mock.restore()); it('should throw an Implementation error if update parameter is missing', () => { - expect(() => { - ctx.upsert('t', { col1: 'val1', col2: 1, col3: ctx.raw('NOW()') }); - }).to.throw(ImplementationError, 'Update parameter must be either an object or an array of strings'); + assert.throws(() => ctx.upsert('t', { col1: 'val1', col2: 1, col3: ctx.raw('NOW()') }), { + name: 'ImplementationError', + message: 'Update parameter must be either an object or an array of strings' + }); }); it('should accept assignment list as an array of column names', async () => { - const sql = `INSERT INTO \`t\` (\`col1\`, \`col2\`, \`col3\`) VALUES ('val1', 1, NOW()) ON DUPLICATE KEY UPDATE \`col1\` = VALUES(\`col1\`), \`col2\` = VALUES(\`col2\`)`; await ctx.upsert('t', { col1: 'val1', col2: 1, col3: ctx.raw('NOW()') }, ['col1', 'col2']); - expect(execStub).to.have.been.calledWith(sql); + + const [call] = ctx.exec.mock.calls; + assert.deepEqual(call.arguments, [ + `INSERT INTO \`t\` (\`col1\`, \`col2\`, \`col3\`) VALUES ('val1', 1, NOW()) ON DUPLICATE KEY UPDATE \`col1\` = VALUES(\`col1\`), \`col2\` = VALUES(\`col2\`)` + ]); }); it('should accept assignment list as an object', async () => { - const sql = `INSERT INTO \`t\` (\`col1\`, \`col2\`, \`col3\`) VALUES ('val1', 1, NOW()) ON DUPLICATE KEY UPDATE \`col1\` = 'foo', \`col2\` = \`col2\` + 1`; await ctx.upsert( 't', { col1: 'val1', col2: 1, col3: ctx.raw('NOW()') }, { col1: 'foo', col2: ctx.raw(ctx.quoteIdentifier('col2') + ' + 1') } ); - expect(execStub).to.have.been.calledWith(sql); - }); - }); - describe('#transaction', () => { - it('should use master connection', async () => { - const connectionSpy = sandbox.spy(ds, '_getConnection'); - const queryStub = sandbox.stub(Connection.prototype, 'query').yields(null, []); - - sandbox.stub(Connection.prototype, 'connect').yields(null); - - const trx = await ctx.transaction(); - - expect(trx).to.be.instanceOf(Transaction); - expect(connectionSpy).to.have.been.calledWith({ type: 'MASTER', db, server: 'default' }); - expect(queryStub).to.have.been.calledWith('START TRANSACTION'); + const [call] = ctx.exec.mock.calls; + assert.deepEqual(call.arguments, [ + `INSERT INTO \`t\` (\`col1\`, \`col2\`, \`col3\`) VALUES ('val1', 1, NOW()) ON DUPLICATE KEY UPDATE \`col1\` = 'foo', \`col2\` = \`col2\` + 1` + ]); }); }); }); diff --git a/test/unit/flora-mysql.spec.js b/test/unit/flora-mysql.spec.js index d1f962a..63eaf4e 100644 --- a/test/unit/flora-mysql.spec.js +++ b/test/unit/flora-mysql.spec.js @@ -1,7 +1,7 @@ 'use strict'; -const { expect } = require('chai'); -const { ImplementationError } = require('@florajs/errors'); +const assert = require('node:assert/strict'); +const { describe, it } = require('node:test'); const { FloraMysqlFactory } = require('../FloraMysqlFactory'); const astTpl = require('../ast-tpl'); @@ -11,29 +11,33 @@ describe('mysql data source', () => { describe('interface', () => { it('should export a query function', () => { - expect(ds.process).to.be.a('function'); + assert.ok(typeof ds.process === 'function'); }); it('should export a prepare function', () => { - expect(ds.prepare).to.be.a('function'); + assert.ok(typeof ds.prepare === 'function'); }); it('should export a getContext function', () => { - expect(ds.getContext).to.be.a('function'); + assert.ok(typeof ds.getContext === 'function'); }); - it('should export a foo function', () => { - expect(ds.buildSqlAst).to.be.a('function'); + it('should export a buildSqlAst function', () => { + assert.ok(typeof ds.buildSqlAst === 'function'); }); }); describe('generate AST data source config', () => { it('should generate AST from SQL query', () => { - const resourceConfig = { database: 'test', query: 'SELECT t.id, t.col1, t.col2 FROM t' }; + const resourceConfig = { + database: 'test', + query: 'SELECT flora_request_processing.id, flora_request_processing.col1, flora_request_processing.col2 FROM flora_request_processing' + }; ds.prepare(resourceConfig, ['id', 'col1', 'col2']); - expect(resourceConfig).to.have.property('queryAstRaw').and.to.eql(astTpl); + assert.ok(Object.hasOwn(resourceConfig, 'queryAstRaw')); + assert.deepEqual(resourceConfig.queryAstRaw, astTpl); }); it('should prepare search attributes', () => { @@ -45,64 +49,68 @@ describe('mysql data source', () => { ds.prepare(resourceConfig, ['col1', 'col2']); - expect(resourceConfig.searchable).to.be.instanceof(Array).and.to.eql(['col1', 'col2']); + assert.ok(Array.isArray(resourceConfig.searchable)); + assert.deepEqual(resourceConfig.searchable, ['col1', 'col2']); }); describe('error handling', () => { it('should append query on a parse error', () => { const sql = 'SELECT col1 FRO t'; const resourceConfig = { database: 'test', query: sql }; - let exceptionThrown = false; - - try { - ds.prepare(resourceConfig, ['col1']); - } catch (e) { - expect(e).to.have.property('query'); - expect(e.query).to.equal(sql); - exceptionThrown = true; - } - expect(exceptionThrown).to.be.equal(true, 'Exception was not thrown'); + assert.throws( + () => ds.prepare(resourceConfig, ['col1']), + (err) => { + assert.ok(Object.hasOwn(err, 'query')); + assert.equal(err.query, sql); + return true; + } + ); }); it('should throw an error if database is not set', () => { const resourceConfig = { query: 'SELECT t.col1 FROM t' }; - expect(() => { - ds.prepare(resourceConfig, ['col1']); - }).to.throw(ImplementationError, 'Database must be specified'); + assert.throws(() => ds.prepare(resourceConfig, ['col1']), { + name: 'ImplementationError', + message: 'Database must be specified' + }); }); it('should throw an error if database is empty', () => { const resourceConfig = { database: '', query: 'SELECT t.col1 FROM t' }; - expect(() => { - ds.prepare(resourceConfig, ['col1']); - }).to.throw(ImplementationError, 'Database must not be empty'); + assert.throws(() => ds.prepare(resourceConfig, ['col1']), { + name: 'ImplementationError', + message: 'Database must not be empty' + }); }); it('should throw an error if neither query nor table is set', () => { const resourceConfig = { database: 'test' }; - expect(() => { - ds.prepare(resourceConfig, ['col1']); - }).to.throw(Error, 'Option "query" or "table" must be specified'); + assert.throws(() => ds.prepare(resourceConfig, ['col1']), { + name: 'ImplementationError', + message: 'Option "query" or "table" must be specified' + }); }); it('should throw an error if an attribute is not available in SQL query', () => { const resourceConfig = { database: 'test', query: 'SELECT t.col1 FROM t' }; - expect(() => { - ds.prepare(resourceConfig, ['col1', 'col2']); - }).to.throw(Error, 'Attribute "col2" is not provided by SQL query'); + assert.throws(() => ds.prepare(resourceConfig, ['col1', 'col2']), { + name: 'ImplementationError', + message: 'Attribute "col2" is not provided by SQL query' + }); }); it('should throw an error if an attribute is not available as column alias', () => { const resourceConfig = { database: 'test', query: 'SELECT t.someWeirdColumnName AS col1 FROM t' }; - expect(() => { - ds.prepare(resourceConfig, ['col1', 'col2']); - }).to.throw(Error, 'Attribute "col2" is not provided by SQL query'); + assert.throws(() => ds.prepare(resourceConfig, ['col1', 'col2']), { + name: 'ImplementationError', + message: 'Attribute "col2" is not provided by SQL query' + }); }); it('should throw an error if columns are not fully qualified', () => { @@ -111,17 +119,16 @@ describe('mysql data source', () => { query: 'SELECT t1.col1, attr AS col2 FROM t1 JOIN t2 ON t1.id = t2.id' }; - expect(() => { - ds.prepare(resourceConfig, ['col1', 'col2']); - }).to.throw(Error, 'Column "attr" must be fully qualified'); + assert.throws(() => ds.prepare(resourceConfig, ['col1', 'col2']), { + name: 'ImplementationError', + message: 'Column "attr" must be fully qualified' + }); }); it('should throw an error if columns are not unique', () => { const resourceConfig = { database: 'test', query: 'SELECT t.col1, someAttr AS col1 FROM t' }; - expect(() => { - ds.prepare(resourceConfig, ['col1', 'col2']); - }).to.throw(Error); + assert.throws(() => ds.prepare(resourceConfig, ['col1', 'col2']), { name: 'ImplementationError' }); }); it('should throw an error if search attribute is not available in AST', () => { @@ -131,19 +138,21 @@ describe('mysql data source', () => { query: 'SELECT t.col1 FROM t' }; - expect(() => { - ds.prepare(resourceConfig, ['col1']); - }).to.throw(ImplementationError, `Attribute "nonExistentAttr" is not available in AST`); + assert.throws(() => ds.prepare(resourceConfig, ['col1']), { + name: 'ImplementationError', + message: `Attribute "nonExistentAttr" is not available in AST` + }); }); }); it('should generate AST from data source config if no SQL query is available', () => { - const resourceConfig = { database: 'test', table: 't' }; + const resourceConfig = { database: 'test', table: 'flora_request_processing' }; const attributes = ['id', 'col1', 'col2']; ds.prepare(resourceConfig, attributes); - expect(resourceConfig).to.have.property('queryAstRaw').and.to.eql(astTpl); + assert.ok(Object.hasOwn(resourceConfig, 'queryAstRaw')); + assert.deepEqual(resourceConfig.queryAstRaw, astTpl); }); }); }); diff --git a/test/unit/sql-builder/fulltext-search.spec.js b/test/unit/sql-builder/fulltext-search.spec.js index 6914433..a7094c8 100644 --- a/test/unit/sql-builder/fulltext-search.spec.js +++ b/test/unit/sql-builder/fulltext-search.spec.js @@ -1,6 +1,7 @@ 'use strict'; -const { expect } = require('chai'); +const assert = require('node:assert/strict'); +const { beforeEach, describe, it } = require('node:test'); const queryBuilder = require('../../../lib/sql-query-builder'); const astFixture = require('./fixture'); @@ -8,13 +9,7 @@ const astFixture = require('./fixture'); describe('query-builder (fulltext search)', () => { let queryAst; - beforeEach(() => { - queryAst = JSON.parse(JSON.stringify(astFixture)); - }); - - afterEach(() => { - queryAst = null; - }); + beforeEach(() => (queryAst = structuredClone(astFixture))); it('should support single attribute', () => { const ast = queryBuilder({ @@ -23,7 +18,7 @@ describe('query-builder (fulltext search)', () => { search: 'foobar' }); - expect(ast.where).to.eql({ + assert.deepEqual(ast.where, { type: 'binary_expr', operator: 'LIKE', left: { type: 'column_ref', table: 't', column: 'col1' }, @@ -38,7 +33,7 @@ describe('query-builder (fulltext search)', () => { search: 'foobar' }); - expect(ast.where).to.eql({ + assert.deepEqual(ast.where, { type: 'binary_expr', operator: 'OR', left: { @@ -63,7 +58,7 @@ describe('query-builder (fulltext search)', () => { search: 'f%o_o%b_ar' }); - expect(ast.where).to.eql({ + assert.deepEqual(ast.where, { type: 'binary_expr', operator: 'LIKE', left: { type: 'column_ref', table: 't', column: 'col1' }, @@ -82,7 +77,7 @@ describe('query-builder (fulltext search)', () => { search: 'foobar' }); - expect(ast.where).to.eql({ + assert.deepEqual(ast.where, { type: 'binary_expr', operator: 'AND', left: { diff --git a/test/unit/sql-builder/limit-per.spec.js b/test/unit/sql-builder/limit-per.spec.js index 9faaeea..04e7857 100644 --- a/test/unit/sql-builder/limit-per.spec.js +++ b/test/unit/sql-builder/limit-per.spec.js @@ -1,20 +1,15 @@ 'use strict'; -const { expect } = require('chai'); +const assert = require('node:assert/strict'); +const { beforeEach, describe, it } = require('node:test'); const queryBuilder = require('../../../lib/sql-query-builder'); const astFixture = require('./fixture'); -xdescribe('query-builder (limit-per)', () => { +describe.skip('query-builder (limit-per)', () => { let queryAst; - beforeEach(() => { - queryAst = JSON.parse(JSON.stringify(astFixture)); - }); - - afterEach(() => { - queryAst = null; - }); + beforeEach(() => (queryAst = structuredClone(astFixture))); it('should use filtered ids in VALUES expression', () => { const ast = queryBuilder({ @@ -26,7 +21,7 @@ xdescribe('query-builder (limit-per)', () => { }); const [valuesExpr] = ast.from; - expect(valuesExpr).to.be.eql({ + assert.deepEqual(valuesExpr, { expr: { type: 'values', value: [ @@ -50,13 +45,14 @@ xdescribe('query-builder (limit-per)', () => { const [, lateralJoin] = ast.from; const { expr: originalQuery } = lateralJoin; - expect(originalQuery).to.have.property('type', 'select'); - expect(originalQuery).to.have.property('from').and.to.eql(queryAst.from); - expect(originalQuery).to.have.property('parentheses', true); - expect(lateralJoin).to.have.property('join', 'JOIN'); - expect(lateralJoin).to.have.property('lateral', true); - expect(lateralJoin).to.have.property('on').and.to.eql({ type: 'boolean', value: true }); - expect(lateralJoin).to.have.property('as', 'limitper'); + + assert.equal(originalQuery.type, 'select'); + assert.equal(originalQuery.from, queryAst.from); + assert.equal(originalQuery.parentheses, true); + assert.equal(lateralJoin.join, 'JOIN'); + assert.equal(lateralJoin.lateral, true); + assert.deepEqual(lateralJoin.on, { type: 'boolean', value: true }); + assert.equal(lateralJoin.as, 'limitper'); }); it('should select attributes from derived table alias', () => { @@ -68,7 +64,7 @@ xdescribe('query-builder (limit-per)', () => { filter: [[{ attribute: 'col1', operator: 'equal', valueFromParentKey: true, value: [1, 3] }]] }); - expect(ast.columns).to.eql([ + assert.deepEqual(ast.columns, [ { expr: { type: 'column_ref', table: 'limitper', column: 'col2' }, as: null @@ -87,14 +83,12 @@ xdescribe('query-builder (limit-per)', () => { }); const [, { expr: subSelect }] = ast.from; - expect(subSelect) - .to.have.property('where') - .and.to.eql({ - type: 'binary_expr', - operator: '=', - left: { type: 'column_ref', table: 'limitper_ids', column: 'id' }, - right: { type: 'column_ref', table: 't', column: 'col1' } - }); + assert.deepEqual(subSelect.where, { + type: 'binary_expr', + operator: '=', + left: { type: 'column_ref', table: 'limitper_ids', column: 'id' }, + right: { type: 'column_ref', table: 't', column: 'col1' } + }); }); it('should generate WHERE clause for correlated subquery for existing conditions', () => { @@ -115,26 +109,24 @@ xdescribe('query-builder (limit-per)', () => { }); const [, { expr: subSelect }] = ast.from; - expect(subSelect) - .to.have.property('where') - .and.to.eql({ + assert.deepEqual(subSelect.where, { + type: 'binary_expr', + operator: 'AND', + left: { type: 'binary_expr', - operator: 'AND', - left: { - type: 'binary_expr', - operator: 'IS', - left: { type: 'column_ref', table: 't', column: 'deleted_at' }, - right: { type: 'null', value: null }, - parentheses: true - }, - right: { - type: 'binary_expr', - operator: '=', - left: { type: 'column_ref', table: 'limitper_ids', column: 'id' }, - right: { type: 'column_ref', table: 't', column: 'col1' }, - parentheses: true - } - }); + operator: 'IS', + left: { type: 'column_ref', table: 't', column: 'deleted_at' }, + right: { type: 'null', value: null }, + parentheses: true + }, + right: { + type: 'binary_expr', + operator: '=', + left: { type: 'column_ref', table: 'limitper_ids', column: 'id' }, + right: { type: 'column_ref', table: 't', column: 'col1' }, + parentheses: true + } + }); }); it('should generate WHERE clause for correlated subquery for OR filters', () => { @@ -164,56 +156,54 @@ xdescribe('query-builder (limit-per)', () => { }); const [, { expr: subSelect }] = ast.from; - expect(subSelect) - .to.have.property('where') - .and.to.eql({ + assert.deepEqual(subSelect.where, { + type: 'binary_expr', + operator: 'AND', + left: { + type: 'binary_expr', + operator: 'IS', + left: { type: 'column_ref', table: 't', column: 'deleted_at' }, + right: { type: 'null', value: null }, + parentheses: true + }, + right: { type: 'binary_expr', - operator: 'AND', + operator: 'OR', left: { type: 'binary_expr', - operator: 'IS', - left: { type: 'column_ref', table: 't', column: 'deleted_at' }, - right: { type: 'null', value: null }, - parentheses: true + operator: 'AND', + left: { + type: 'binary_expr', + operator: '<=', + left: { column: 'col2', table: 't', type: 'column_ref' }, + right: { type: 'number', value: 10 } + }, + right: { + type: 'binary_expr', + operator: '=', + left: { column: 'id', table: 'limitper_ids', type: 'column_ref' }, + right: { column: 'col1', table: 't', type: 'column_ref' } + } }, right: { type: 'binary_expr', - operator: 'OR', + operator: 'AND', left: { type: 'binary_expr', - operator: 'AND', - left: { - type: 'binary_expr', - operator: '<=', - left: { column: 'col2', table: 't', type: 'column_ref' }, - right: { type: 'number', value: 10 } - }, - right: { - type: 'binary_expr', - operator: '=', - left: { column: 'id', table: 'limitper_ids', type: 'column_ref' }, - right: { column: 'col1', table: 't', type: 'column_ref' } - } + operator: '>=', + left: { column: 'col2', table: 't', type: 'column_ref' }, + right: { type: 'number', value: 5 } }, right: { type: 'binary_expr', - operator: 'AND', - left: { - type: 'binary_expr', - operator: '>=', - left: { column: 'col2', table: 't', type: 'column_ref' }, - right: { type: 'number', value: 5 } - }, - right: { - type: 'binary_expr', - operator: '=', - left: { column: 'id', table: 'limitper_ids', type: 'column_ref' }, - right: { column: 'col1', table: 't', type: 'column_ref' } - } - }, - parentheses: true - } - }); + operator: '=', + left: { column: 'id', table: 'limitper_ids', type: 'column_ref' }, + right: { column: 'col1', table: 't', type: 'column_ref' } + } + }, + parentheses: true + } + }); }); it('should support fulltext searches', () => { @@ -228,25 +218,24 @@ xdescribe('query-builder (limit-per)', () => { }); const [, { expr: subSelect }] = ast.from; - expect(subSelect) - .to.have.property('where') - .and.to.eql({ + assert.deepEqual(subSelect.where, { + type: 'binary_expr', + operator: 'AND', + left: { type: 'binary_expr', - operator: 'AND', - left: { - type: 'binary_expr', - operator: '=', - left: { type: 'column_ref', table: 'limitper_ids', column: 'id' }, - right: { type: 'column_ref', table: 't', column: 'col1' } - }, - right: { - type: 'binary_expr', - operator: 'LIKE', - left: { type: 'column_ref', table: 't', column: 'col2' }, - right: { type: 'string', value: '%te\\%st%' }, - parentheses: true - } - }); + operator: '=', + parentheses: true, + left: { type: 'column_ref', table: 'limitper_ids', column: 'id' }, + right: { type: 'column_ref', table: 't', column: 'col1' } + }, + right: { + type: 'binary_expr', + operator: 'LIKE', + left: { type: 'column_ref', table: 't', column: 'col2' }, + right: { type: 'string', value: '%te\\%st%' }, + parentheses: true + } + }); }); }); @@ -261,12 +250,10 @@ xdescribe('query-builder (limit-per)', () => { }); const [, { expr: subSelect }] = ast.from; - expect(subSelect) - .to.have.property('limit') - .and.to.eql([ - { type: 'number', value: 0 }, - { type: 'number', value: 5 } - ]); + assert.deepEqual(subSelect.limit, [ + { type: 'number', value: 0 }, + { type: 'number', value: 5 } + ]); }); it('should apply page to lateral join', () => { @@ -281,12 +268,10 @@ xdescribe('query-builder (limit-per)', () => { }); const [, { expr: subSelect }] = ast.from; - expect(subSelect) - .to.have.property('limit') - .and.to.eql([ - { type: 'number', value: 40 }, - { type: 'number', value: 10 } - ]); + assert.deepEqual(subSelect.limit, [ + { type: 'number', value: 40 }, + { type: 'number', value: 10 } + ]); }); }); @@ -300,8 +285,8 @@ xdescribe('query-builder (limit-per)', () => { }); const [, { expr: subSelect }] = ast.from; - expect(subSelect) - .to.have.property('orderby') - .and.to.eql([{ expr: { type: 'column_ref', table: 't', column: 'col2' }, type: 'DESC' }]); + assert.deepEqual(subSelect.orderby, [ + { expr: { type: 'column_ref', table: 't', column: 'col2' }, type: 'DESC' } + ]); }); }); diff --git a/test/unit/sql-builder/limit.spec.js b/test/unit/sql-builder/limit.spec.js index 2506ed5..1fa6cac 100644 --- a/test/unit/sql-builder/limit.spec.js +++ b/test/unit/sql-builder/limit.spec.js @@ -1,6 +1,7 @@ 'use strict'; -const { expect } = require('chai'); +const assert = require('node:assert/strict'); +const { beforeEach, describe, it } = require('node:test'); const queryBuilder = require('../../../lib/sql-query-builder'); const astFixture = require('./fixture'); @@ -8,13 +9,7 @@ const astFixture = require('./fixture'); describe('query-builder (limit)', () => { let queryAst; - beforeEach(() => { - queryAst = JSON.parse(JSON.stringify(astFixture)); - }); - - afterEach(() => { - queryAst = null; - }); + beforeEach(() => (queryAst = structuredClone(astFixture))); it('should set limit', () => { const ast = queryBuilder({ @@ -22,7 +17,7 @@ describe('query-builder (limit)', () => { limit: 17 }); - expect(ast.limit).to.eql([ + assert.deepEqual(ast.limit, [ { type: 'number', value: 0 }, { type: 'number', value: 17 } ]); @@ -35,7 +30,7 @@ describe('query-builder (limit)', () => { page: 3 }); - expect(ast.limit).to.eql([ + assert.deepEqual(ast.limit, [ { type: 'number', value: 20 }, { type: 'number', value: 10 } ]); diff --git a/test/unit/sql-builder/orderby.spec.js b/test/unit/sql-builder/orderby.spec.js index 2db753a..e84901d 100644 --- a/test/unit/sql-builder/orderby.spec.js +++ b/test/unit/sql-builder/orderby.spec.js @@ -1,6 +1,7 @@ 'use strict'; -const { expect } = require('chai'); +const assert = require('node:assert/strict'); +const { beforeEach, describe, it } = require('node:test'); const queryBuilder = require('../../../lib/sql-query-builder'); const astFixture = require('./fixture'); @@ -8,13 +9,7 @@ const astFixture = require('./fixture'); describe('query-builder (order)', () => { let queryAst; - beforeEach(() => { - queryAst = JSON.parse(JSON.stringify(astFixture)); - }); - - afterEach(() => { - queryAst = null; - }); + beforeEach(() => (queryAst = structuredClone(astFixture))); it('should order by one attribute', () => { const ast = queryBuilder({ @@ -22,7 +17,7 @@ describe('query-builder (order)', () => { order: [{ attribute: 'col1', direction: 'asc' }] }); - expect(ast.orderby).to.eql([ + assert.deepEqual(ast.orderby, [ { expr: { type: 'column_ref', table: 't', column: 'col1' }, type: 'ASC' @@ -36,7 +31,7 @@ describe('query-builder (order)', () => { order: [{ attribute: 'col1', direction: 'ASC' }] }); - expect(ast.orderby).to.eql([ + assert.deepEqual(ast.orderby, [ { expr: { type: 'column_ref', table: 't', column: 'col1' }, type: 'ASC' @@ -45,12 +40,17 @@ describe('query-builder (order)', () => { }); it('should throw on invalid direction', () => { - expect(() => { - queryBuilder({ - queryAst, - order: [{ attribute: 'col1', direction: 'invalid' }] - }); - }).to.throw(Error, /Invalid order direction/); + assert.throws( + () => + queryBuilder({ + queryAst, + order: [{ attribute: 'col1', direction: 'invalid' }] + }), + { + name: 'Error', + message: /Invalid order direction/ + } + ); }); it('should order by one attribute (random)', () => { @@ -59,7 +59,7 @@ describe('query-builder (order)', () => { order: [{ attribute: 'col1', direction: 'random' }] }); - expect(ast.orderby).to.eql([ + assert.deepEqual(ast.orderby, [ { expr: { type: 'function', name: 'RAND', args: { type: 'expr_list', value: [] } }, type: '' @@ -73,7 +73,7 @@ describe('query-builder (order)', () => { order: [{ attribute: 'col1', direction: 'rAnDoM' }] }); - expect(ast.orderby).to.eql([ + assert.deepEqual(ast.orderby, [ { expr: { type: 'function', name: 'RAND', args: { type: 'expr_list', value: [] } }, type: '' @@ -87,7 +87,7 @@ describe('query-builder (order)', () => { order: [{ attribute: 'columnAlias', direction: 'desc' }] }); - expect(ast.orderby).to.eql([ + assert.deepEqual(ast.orderby, [ { expr: { type: 'column_ref', table: 't', column: 'col3' }, type: 'DESC' @@ -104,7 +104,7 @@ describe('query-builder (order)', () => { ] }); - expect(ast.orderby).to.eql([ + assert.deepEqual(ast.orderby, [ { expr: { type: 'column_ref', table: 't', column: 'col1' }, type: 'ASC' }, { expr: { type: 'column_ref', table: 't', column: 'col2' }, type: 'DESC' } ]); diff --git a/test/unit/sql-builder/where.spec.js b/test/unit/sql-builder/where.spec.js index b8dae12..c0f8e95 100644 --- a/test/unit/sql-builder/where.spec.js +++ b/test/unit/sql-builder/where.spec.js @@ -1,6 +1,7 @@ 'use strict'; -const { expect } = require('chai'); +const assert = require('node:assert/strict'); +const { beforeEach, describe, it } = require('node:test'); const queryBuilder = require('../../../lib/sql-query-builder'); const astFixture = require('./fixture'); @@ -8,13 +9,7 @@ const astFixture = require('./fixture'); describe('query-builder (where)', () => { let queryAst; - beforeEach(() => { - queryAst = JSON.parse(JSON.stringify(astFixture)); - }); - - afterEach(() => { - queryAst = null; - }); + beforeEach(() => (queryAst = structuredClone(astFixture))); it('should add single "AND" condition', () => { const ast = queryBuilder({ @@ -22,7 +17,7 @@ describe('query-builder (where)', () => { filter: [[{ attribute: 'col1', operator: 'equal', value: 0 }]] }); - expect(ast.where).to.be.eql({ + assert.deepEqual(ast.where, { type: 'binary_expr', operator: '=', left: { type: 'column_ref', table: 't', column: 'col1' }, @@ -41,7 +36,7 @@ describe('query-builder (where)', () => { ] }); - expect(ast.where).to.be.eql({ + assert.deepEqual(ast.where, { type: 'binary_expr', operator: 'AND', left: { @@ -62,7 +57,7 @@ describe('query-builder (where)', () => { it('should remove empty "AND"/"OR" conditions', () => { const ast = queryBuilder({ queryAst, filter: [[]] }); - expect(ast.where).to.be.null; + assert.equal(ast.where, null); }); it('should add mulitple "OR" conditions', () => { @@ -74,7 +69,7 @@ describe('query-builder (where)', () => { ] }); - expect(ast.where).to.be.eql({ + assert.deepEqual(ast.where, { type: 'binary_expr', operator: 'OR', left: { @@ -110,7 +105,7 @@ describe('query-builder (where)', () => { filter: [[{ attribute: 'col2', operator: 'greater', value: 100 }]] }); - expect(ast.where).to.be.eql({ + assert.deepEqual(ast.where, { type: 'binary_expr', operator: 'AND', left: { @@ -151,7 +146,7 @@ describe('query-builder (where)', () => { ] }); - expect(ast.where).to.be.eql({ + assert.deepEqual(ast.where, { type: 'binary_expr', operator: 'AND', left: { @@ -199,8 +194,10 @@ describe('query-builder (where)', () => { filter: [[{ attribute: 'col2', operator: 'greater', value: 100 }]] }); - expect(ast.where.left).to.have.property('parentheses', true); - expect(ast.where.right).to.have.property('parentheses', true); + assert.ok(Object.hasOwn(ast.where.left, 'parentheses')); + assert.equal(ast.where.left.parentheses, true); + assert.ok(Object.hasOwn(ast.where.right, 'parentheses')); + assert.equal(ast.where.right.parentheses, true); }); it('should support arrays as attribute filters', () => { @@ -235,7 +232,7 @@ describe('query-builder (where)', () => { ] }); - expect(ast.where).to.eql({ + assert.deepEqual(ast.where, { type: 'binary_expr', operator: 'OR', left: { @@ -281,7 +278,7 @@ describe('query-builder (where)', () => { filter: [[{ attribute: 'col1', operator: filterOperator, value: null }]] }); - expect(ast.where).to.eql({ + assert.deepEqual(ast.where, { type: 'binary_expr', operator: sqlOperator, left: { type: 'column_ref', table: 't', column: 'col1' }, diff --git a/test/unit/sql-query-checker.spec.js b/test/unit/sql-query-checker.spec.js index a87007d..af0ddc2 100644 --- a/test/unit/sql-query-checker.spec.js +++ b/test/unit/sql-query-checker.spec.js @@ -1,6 +1,7 @@ 'use strict'; -const { expect } = require('chai'); +const assert = require('node:assert/strict'); +const { describe, it } = require('node:test'); const { Parser } = require('@florajs/sql-parser'); const check = require('../../lib/sql-query-checker'); @@ -10,24 +11,31 @@ describe('SQL query checker', () => { it('should not throw an error if all columns are fully qualified with table(s)', () => { ast = parser.parse('select t.col1, t.col2 from t'); - expect(() => check(ast)).to.not.throw(Error); + assert.doesNotThrow(() => check(ast)); }); it('should not throw an error if columns are not fully qualified and query contains only one table', () => { ast = parser.parse('select col1, col2 from t'); - expect(() => check(ast)).to.not.throw(Error); + assert.doesNotThrow(() => check(ast)); }); it('should throw an error if SELECT expression contains item(s) without fully qualified table(s)', () => { ast = parser.parse('select col1, t1.col2 from t join t2 on t1.id = t2.id'); - expect(() => check(ast)).to.throw(Error, 'Column "col1" must be fully qualified'); + assert.throws(() => check(ast), { + name: 'ImplementationError', + message: 'Column "col1" must be fully qualified' + }); }); it('should throw an error if JOINs contain columns without table', () => { const sql = 'select t1.col1, t2.col1 from t1 join t2 on t1.id = t1_pk_id'; ast = parser.parse(sql); - expect(() => check(ast)).to.throw(Error, 'Column "t1_pk_id" must be fully qualified'); + + assert.throws(() => check(ast), { + name: 'ImplementationError', + message: 'Column "t1_pk_id" must be fully qualified' + }); }); it('should resolve nested expressions', () => { @@ -36,7 +44,11 @@ describe('SQL query checker', () => { join t2 on t1.id = t2.id`; ast = parser.parse(sql); - expect(() => check(ast)).to.throw(Error, 'Column "attr" must be fully qualified'); + + assert.throws(() => check(ast), { + name: 'ImplementationError', + message: 'Column "attr" must be fully qualified' + }); }); it('should check where clause', () => { @@ -49,6 +61,10 @@ describe('SQL query checker', () => { WHERE locale = 'de'`; ast = parser.parse(sql); - expect(() => check(ast)).to.throw(Error, 'Column "locale" must be fully qualified'); + + assert.throws(() => check(ast), { + name: 'ImplementationError', + message: 'Column "locale" must be fully qualified' + }); }); }); diff --git a/test/unit/sql-query-optimizer.spec.js b/test/unit/sql-query-optimizer.spec.js index 2063ad7..9be8df9 100644 --- a/test/unit/sql-query-optimizer.spec.js +++ b/test/unit/sql-query-optimizer.spec.js @@ -1,6 +1,7 @@ 'use strict'; -const { expect } = require('chai'); +const assert = require('node:assert/strict'); +const { beforeEach, describe, it } = require('node:test'); const optimize = require('../../lib/sql-query-optimizer'); describe('SQL query optimizer', () => { @@ -11,7 +12,7 @@ describe('SQL query optimizer', () => { const initialAST = structuredClone(ast); optimize(ast, ['col1']); - expect(initialAST).to.eql(ast); + assert.deepEqual(ast, initialAST); }); it('should remove unused columns/attributes from AST', () => { @@ -31,7 +32,8 @@ describe('SQL query optimizer', () => { }; const optimizedAst = optimize(ast, ['col1', 'alias']); - expect(optimizedAst.columns).to.eql([ + + assert.deepEqual(optimizedAst.columns, [ // SELECT t.col1, t.col3 AS alias FROM t1 { expr: { type: 'column_ref', table: 't', column: 'col1' }, as: null }, { expr: { type: 'column_ref', table: 't', column: 'col3' }, as: 'alias' } @@ -69,7 +71,8 @@ describe('SQL query optimizer', () => { const initialAST = structuredClone(ast); const optimizedAst = optimize(ast, ['col1']); - expect(optimizedAst.from).to.eql(initialAST.from); + + assert.deepEqual(optimizedAst.from, initialAST.from); }); it('should remove unreferenced LEFT JOINs from AST', () => { @@ -102,7 +105,8 @@ describe('SQL query optimizer', () => { }; const optimizedAst = optimize(ast, ['col1']); - expect(optimizedAst.from).to.eql([{ db: null, table: 't', as: null }]); // SELECT t1.col1 FROM t + + assert.deepEqual(optimizedAst.from, [{ db: null, table: 't', as: null }]); }); it('should pay attention to table aliases', () => { @@ -154,7 +158,8 @@ describe('SQL query optimizer', () => { }; const optimizedAst = optimize(ast, ['col1', 'col3']); - expect(optimizedAst.from).to.eql([ + + assert.deepEqual(optimizedAst.from, [ // SELECT t1.col1, alias.col3 FROM t LEFT JOIN t3 AS alias ON t1.id = alias.id { db: null, table: 't', as: null }, { @@ -213,7 +218,8 @@ describe('SQL query optimizer', () => { }; const optimizedAst = optimize(ast, ['col1']); - expect(optimizedAst.from).to.eql(ast.from); + + assert.deepEqual(optimizedAst.from, ast.from); }); it('should not remove LEFT JOIN if table is referenced in GROUP BY clause', () => { @@ -247,7 +253,8 @@ describe('SQL query optimizer', () => { const initialAST = structuredClone(ast); const optimizedAst = optimize(ast, ['col1']); - expect(optimizedAst.from).to.eql(initialAST.from); + + assert.deepEqual(optimizedAst.from, initialAST.from); }); it('should not remove LEFT JOIN if table is referenced in ORDER BY clause', () => { @@ -279,7 +286,8 @@ describe('SQL query optimizer', () => { const initialAST = structuredClone(ast); const optimizedAst = optimize(ast, ['col1']); - expect(optimizedAst.from).to.eql(initialAST.from); + + assert.deepEqual(optimizedAst.from, initialAST.from); }); it('should not remove "parent" table/LEFT JOIN if "child" table/LEFT JOIN is needed', () => { @@ -371,7 +379,7 @@ describe('SQL query optimizer', () => { LEFT JOIN country AS c ON instrument.countryId = c.id LEFT JOIN country_translation AS ctde ON c.id = ctde.countryId AND ctde.lang = 'de' */ - expect(optimizedAst.from).to.eql([ + assert.deepEqual(optimizedAst.from, [ { db: null, table: 'instrument', as: null }, { db: null, @@ -487,7 +495,7 @@ describe('SQL query optimizer', () => { }; const optimizedAst = optimize(ast, ['id', 'name']); - expect(optimizedAst.from).to.eql([ + assert.deepEqual(optimizedAst.from, [ { db: null, table: 't1', as: null }, { db: null, @@ -547,7 +555,8 @@ describe('SQL query optimizer', () => { }; const optimizedAst = optimize(ast, ['id']); - expect(optimizedAst.columns).to.eql([ + + assert.deepEqual(optimizedAst.columns, [ { expr: { type: 'column_ref', table: 't', column: 'id' }, as: null }, { expr: { @@ -597,7 +606,8 @@ describe('SQL query optimizer', () => { }; const optimizedAst = optimize(ast, ['id']); - expect(optimizedAst.columns).to.eql([ + + assert.deepEqual(optimizedAst.columns, [ { expr: { type: 'column_ref', table: null, column: 'id' }, as: null }, { expr: { @@ -685,7 +695,9 @@ describe('SQL query optimizer', () => { const optimizedAst = optimize(ast, ['col1'], true); const [, { expr: subSelect }] = optimizedAst.from; - expect(subSelect.columns).to.eql([{ expr: { type: 'column_ref', table: 't', column: 'col1' }, as: null }]); + assert.deepEqual(subSelect.columns, [ + { expr: { type: 'column_ref', table: 't', column: 'col1' }, as: null } + ]); }); it('should optimize from clause', () => { @@ -706,7 +718,7 @@ describe('SQL query optimizer', () => { const optimizedAst = optimize(ast, ['col1'], true); const [, { expr: subSelect }] = optimizedAst.from; - expect(subSelect.from).to.eql([{ db: null, table: 't', as: null }]); + assert.deepEqual(subSelect.from, [{ db: null, table: 't', as: null }]); }); }); @@ -759,6 +771,7 @@ describe('SQL query optimizer', () => { const originalAst = structuredClone(ast); const optimizedAst = optimize(ast, ['id']); - expect(optimizedAst).to.eql(originalAst); + + assert.deepEqual(optimizedAst, originalAst); }); }); diff --git a/test/unit/transaction.spec.js b/test/unit/transaction.spec.js index f8c25e6..f41af6c 100644 --- a/test/unit/transaction.spec.js +++ b/test/unit/transaction.spec.js @@ -1,6 +1,7 @@ 'use strict'; -const { expect } = require('chai'); +const assert = require('node:assert/strict'); +const { describe, it } = require('node:test'); const Transaction = require('../../lib/transaction'); describe('transaction', () => { @@ -21,7 +22,7 @@ describe('transaction', () => { 'quoteIdentifier' ].forEach((method) => { it(`should have ${method} method`, () => { - expect(Transaction.prototype[method]).to.be.a('function'); + assert.ok(typeof Transaction.prototype[method] === 'function'); }); }); }); @@ -31,22 +32,24 @@ describe('transaction', () => { const trx = new Transaction({}); const expr = trx.raw('NOW()'); - expect(expr).to.be.an('object'); - expect(expr.toSqlString).to.be.a('function'); - expect(expr.toSqlString()).to.equal('NOW()'); + assert.ok(typeof expr === 'object'); + assert.ok(typeof expr.toSqlString === 'function'); + assert.equal(expr.toSqlString(), 'NOW()'); }); }); describe('#quote', () => { it('should quote values', () => { const trx = new Transaction({}); - expect(trx.quote(`foo\\b'ar`)).to.equal(`'foo\\\\b\\'ar'`); + + assert.equal(trx.quote(`foo\\b'ar`), `'foo\\\\b\\'ar'`); }); describe('#quoteIdentifier', () => { it('should quote identifiers', () => { const trx = new Transaction({}); - expect(trx.quoteIdentifier('table')).to.equal('`table`'); + + assert.equal(trx.quoteIdentifier('table'), '`table`'); }); }); });