From c479f5d379071bc4eb75eca599fdc2768bc1b9f5 Mon Sep 17 00:00:00 2001 From: axiosleo Date: Sat, 6 Dec 2025 18:25:39 +0800 Subject: [PATCH 1/2] feat(tests): add comprehensive test coverage for client, core, operator, query, and transaction functionalities --- tests/client.tests.js | 507 ++++++++++++++++++++++++++++++++++++- tests/core.tests.js | 217 ++++++++++++++++ tests/operator.tests.js | 359 +++++++++++++++++++++++++- tests/query.tests.js | 298 ++++++++++++++++++++++ tests/transaction.tests.js | 411 ++++++++++++++++++++++++++++++ 5 files changed, 1790 insertions(+), 2 deletions(-) create mode 100644 tests/core.tests.js create mode 100644 tests/transaction.tests.js diff --git a/tests/client.tests.js b/tests/client.tests.js index 8b6e79a..1019f23 100644 --- a/tests/client.tests.js +++ b/tests/client.tests.js @@ -1,10 +1,14 @@ 'use strict'; +const mm = require('mm'); /** * @type {Chai.ExpectStatic} */ let expect = null; -const { createPool, _clients } = require('../src/client'); +const mysql = require('mysql2'); +const mysqlPromise = require('mysql2/promise'); +const { createPool, createClient, createPromiseClient, getClient, MySQLClient, _clients } = require('../src/client'); +const { Query } = require('../src/query'); describe('client test case', () => { before(async function () { @@ -98,4 +102,505 @@ describe('client test case', () => { // 清理 delete _clients[testKey]; }); + + describe('createClient', () => { + beforeEach(() => { + // 清理 _clients + Object.keys(_clients).forEach(key => { + delete _clients[key]; + }); + }); + + it('should create new client', () => { + mm(mysql, 'createConnection', (options) => { + return { + connect: () => { }, + _closed: false, + _closing: false, + destroyed: false + }; + }); + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const client = createClient(options); + expect(client).to.not.be.null; + mm.restore(); + }); + + it('should return existing client when active', () => { + const mockConnection = { + connect: () => { }, + _closed: false, + _closing: false, + destroyed: false + }; + mm(mysql, 'createConnection', (options) => { + return mockConnection; + }); + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const client1 = createClient(options); + const client2 = createClient(options); + expect(client1).to.equal(client2); + mm.restore(); + }); + + it('should recreate client when closed', () => { + const mockConnection1 = { + connect: () => { }, + _closed: true, + _closing: false, + destroyed: false + }; + const mockConnection2 = { + connect: () => { }, + _closed: false, + _closing: false, + destroyed: false + }; + let callCount = 0; + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const key = `${options.host}:${options.port}:${options.user}:${options.password}:${options.database}`; + _clients[key] = mockConnection1; + mm(mysql, 'createConnection', (opts) => { + callCount++; + if (callCount === 1) { + return mockConnection2; + } + return mockConnection2; + }); + const client = createClient(options); + expect(client).to.equal(mockConnection2); + expect(client._closed).to.be.false; + mm.restore(); + }); + + it('should recreate client when closing', () => { + const mockConnection1 = { + connect: () => { }, + _closed: false, + _closing: true, + destroyed: false + }; + const mockConnection2 = { + connect: () => { }, + _closed: false, + _closing: false, + destroyed: false + }; + + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const key = `${options.host}:${options.port}:${options.user}:${options.password}:${options.database}`; + _clients[key] = mockConnection1; + let callCount = 0; + mm(mysql, 'createConnection', (opts) => { + callCount++; + return mockConnection2; + }); + const client = createClient(options); + expect(client).to.equal(mockConnection2); + expect(client._closing).to.be.false; + expect(callCount).to.be.equal(1); + mm.restore(); + }); + + it('should recreate client when destroyed', () => { + const mockConnection1 = { + connect: () => { }, + _closed: false, + _closing: false, + destroyed: true + }; + const mockConnection2 = { + connect: () => { }, + _closed: false, + _closing: false, + destroyed: false + }; + let callCount = 0; + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const key = `${options.host}:${options.port}:${options.user}:${options.password}:${options.database}`; + _clients[key] = mockConnection1; + mm(mysql, 'createConnection', (opts) => { + callCount++; + return mockConnection2; + }); + const client = createClient(options); + expect(client).to.equal(mockConnection2); + expect(client.destroyed).to.be.false; + expect(callCount).to.be.equal(1); + mm.restore(); + }); + + it('should use name parameter', () => { + mm(mysql, 'createConnection', (options) => { + return { + connect: () => { }, + _closed: false, + _closing: false, + destroyed: false + }; + }); + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const client1 = createClient(options, 'named_client'); + const client2 = createClient(options, 'named_client'); + expect(client1).to.equal(client2); + expect(_clients['named_client']).to.equal(client1); + mm.restore(); + }); + + it('should validate required options', () => { + expect(() => { + createClient({}); + }).to.throw(); + }); + }); + + describe('createPromiseClient', () => { + beforeEach(() => { + Object.keys(_clients).forEach(key => { + delete _clients[key]; + }); + }); + + it('should create new promise client', async () => { + const mockConnection = { + _closed: false, + _closing: false, + destroyed: false + }; + mm(mysqlPromise, 'createConnection', async (options) => { + return mockConnection; + }); + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const client = await createPromiseClient(options); + expect(client).to.not.be.null; + mm.restore(); + }); + + it('should return existing promise client when active', async () => { + const mockConnection = { + _closed: false, + _closing: false, + destroyed: false + }; + mm(mysqlPromise, 'createConnection', async (options) => { + return mockConnection; + }); + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const client1 = await createPromiseClient(options); + const client2 = await createPromiseClient(options); + expect(client1).to.equal(client2); + mm.restore(); + }); + + it('should recreate promise client when closed', async () => { + const mockConnection1 = { + _closed: true, + _closing: false, + destroyed: false + }; + const mockConnection2 = { + _closed: false, + _closing: false, + destroyed: false + }; + let callCount = 0; + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const key = `${options.host}:${options.port}:${options.user}:${options.password}:${options.database}`; + _clients[key] = mockConnection1; + mm(mysqlPromise, 'createConnection', async (opts) => { + callCount++; + return mockConnection2; + }); + const client = await createPromiseClient(options); + expect(client).to.equal(mockConnection2); + expect(client._closed).to.be.false; + expect(callCount).to.be.equal(1); + mm.restore(); + }); + + it('should use name parameter', async () => { + const mockConnection = { + _closed: false, + _closing: false, + destroyed: false + }; + mm(mysqlPromise, 'createConnection', async (options) => { + return mockConnection; + }); + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const client1 = await createPromiseClient(options, 'named_promise_client'); + const client2 = await createPromiseClient(options, 'named_promise_client'); + expect(client1).to.equal(client2); + expect(_clients['named_promise_client']).to.equal(client1); + mm.restore(); + }); + + it('should validate required options', async () => { + try { + await createPromiseClient({}); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err).to.exist; + } + }); + }); + + describe('getClient', () => { + beforeEach(() => { + Object.keys(_clients).forEach(key => { + delete _clients[key]; + }); + }); + + it('should get existing client', () => { + const mockClient = { test: 'client' }; + _clients['test_client'] = mockClient; + const client = getClient('test_client'); + expect(client).to.equal(mockClient); + }); + + it('should throw error when name is missing', () => { + expect(() => { + getClient(); + }).to.throw('name is required'); + }); + + it('should throw error when client not found', () => { + expect(() => { + getClient('non_existent_client'); + }).to.throw('client non_existent_client not found'); + }); + }); + + describe('MySQLClient', () => { + beforeEach(() => { + Object.keys(_clients).forEach(key => { + delete _clients[key]; + }); + }); + + it('should create MySQLClient with default type', () => { + mm(mysql, 'createConnection', (options) => { + return { + connect: () => { }, + _closed: false, + _closing: false, + destroyed: false + }; + }); + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const client = new MySQLClient(options); + expect(client).to.be.instanceOf(MySQLClient); + expect(client.database).to.be.equal('test_db'); + mm.restore(); + }); + + it('should create MySQLClient with pool type', () => { + mm(mysql, 'createPool', (options) => { + return { + _closed: false + }; + }); + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const client = new MySQLClient(options, 'pool_client', 'pool'); + expect(client).to.be.instanceOf(MySQLClient); + expect(client.database).to.be.equal('test_db'); + mm.restore(); + }); + + it('should throw error with invalid type', () => { + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + expect(() => { + new MySQLClient(options, 'test', 'invalid'); + }).to.throw('client type invalid not found'); + }); + + it('should execQuery with Query instance', async () => { + mm(mysql, 'createConnection', (options) => { + return { + connect: () => { }, + query: (opt, callback) => { + callback(null, [{ id: 1 }]); + }, + _closed: false, + _closing: false, + destroyed: false + }; + }); + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const client = new MySQLClient(options); + const query = new Query('select'); + query.table('users'); + const result = await client.execQuery(query, 'select'); + expect(result).to.be.an('array'); + mm.restore(); + }); + + it('should execQuery with operator parameter', async () => { + mm(mysql, 'createConnection', (options) => { + return { + connect: () => { }, + query: (opt, callback) => { + callback(null, [{ id: 1 }]); + }, + _closed: false, + _closing: false, + destroyed: false + }; + }); + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const client = new MySQLClient(options); + const query = new Query(); + query.table('users'); + const result = await client.execQuery(query, 'select'); + expect(result).to.be.an('array'); + mm.restore(); + }); + + it('should close connection', async () => { + let closed = false; + mm(mysql, 'createConnection', (options) => { + return { + connect: () => { }, + end: (callback) => { + closed = true; + callback(null); + }, + _closed: false, + _closing: false, + destroyed: false + }; + }); + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const client = new MySQLClient(options); + await client.close(); + expect(closed).to.be.true; + mm.restore(); + }); + + it('should handle close error', async () => { + mm(mysql, 'createConnection', (options) => { + return { + connect: () => { }, + end: (callback) => { + callback(new Error('Close error')); + }, + _closed: false, + _closing: false, + destroyed: false + }; + }); + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const client = new MySQLClient(options); + try { + await client.close(); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('Close error'); + } + mm.restore(); + }); + }); }); diff --git a/tests/core.tests.js b/tests/core.tests.js new file mode 100644 index 0000000..5115e4f --- /dev/null +++ b/tests/core.tests.js @@ -0,0 +1,217 @@ +'use strict'; + +let expect = null; +const { _query, _execSQL } = require('../src/core'); + +describe('core test case', () => { + before(async function () { + const chai = await import('chai'); + expect = chai.expect; + }); + + describe('_query function', () => { + it('should handle empty options with default driver', async () => { + const conn = { + query: (opt, callback) => { + callback(null, [{ id: 1 }]); + } + }; + const options = { + driver: 'mysql', + operator: 'select', + tables: [{ table: 'users' }], + conditions: [], + orders: [], + groupField: [], + having: [] + }; + const result = await _query(conn, options); + expect(result).to.be.an('array'); + expect(result[0].id).to.be.equal(1); + }); + + it('should handle transaction mode with execute', async () => { + const conn = { + execute: async (sql, values) => { + return [[{ id: 1 }]]; + } + }; + const options = { + driver: 'mysql', + transaction: true, + operator: 'select', + tables: [{ table: 'users' }], + conditions: [], + orders: [], + groupField: [], + having: [] + }; + const result = await _query(conn, options); + expect(result).to.be.an('array'); + expect(result[0].id).to.be.equal(1); + }); + + it('should handle non-transaction mode with query', async () => { + const conn = { + query: (opt, callback) => { + callback(null, [{ id: 1 }]); + } + }; + const options = { + driver: 'mysql', + transaction: false, + operator: 'select', + tables: [{ table: 'users' }], + conditions: [], + orders: [], + groupField: [], + having: [] + }; + const result = await _query(conn, options); + expect(result).to.be.an('array'); + expect(result[0].id).to.be.equal(1); + }); + + it('should handle custom driver with queryHandler', async () => { + const conn = {}; + const options = { + driver: 'custom', + queryHandler: (con, opts, opt) => { + return Promise.resolve([{ id: 1, name: 'test' }]); + }, + operator: 'select', + tables: [{ table: 'users' }], + conditions: [], + orders: [], + groupField: [], + having: [] + }; + const result = await _query(conn, options); + expect(result).to.be.an('array'); + expect(result[0].id).to.be.equal(1); + expect(result[0].name).to.be.equal('test'); + }); + + it('should throw error when queryHandler does not return Promise', async () => { + const conn = {}; + const options = { + driver: 'custom', + queryHandler: (con, opts, opt) => { + return 'not a promise'; + }, + operator: 'select', + tables: [{ table: 'users' }], + conditions: [], + orders: [], + groupField: [], + having: [] + }; + try { + await _query(conn, options); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('queryHandler must return a promise'); + } + }); + + it('should handle error in transaction mode', async () => { + const conn = { + execute: async (sql, values) => { + throw new Error('Database error'); + } + }; + const options = { + driver: 'mysql', + transaction: true, + operator: 'select', + tables: [{ table: 'users' }], + conditions: [], + orders: [], + groupField: [], + having: [] + }; + try { + await _query(conn, options); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('Database error'); + } + }); + + it('should handle error in non-transaction mode', async () => { + const conn = { + query: (opt, callback) => { + callback(new Error('Query error'), null); + } + }; + const options = { + driver: 'mysql', + transaction: false, + operator: 'select', + tables: [{ table: 'users' }], + conditions: [], + orders: [], + groupField: [], + having: [] + }; + try { + await _query(conn, options); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('Query error'); + } + }); + }); + + describe('_execSQL function', () => { + it('should use conn.query when available', async () => { + const conn = { + query: (opt, callback) => { + callback(null, { affectedRows: 1 }); + } + }; + const result = await _execSQL(conn, 'SELECT * FROM users', []); + expect(result.affectedRows).to.be.equal(1); + }); + + it('should use conn.execute when query is not a function', async () => { + const conn = { + execute: async (sql, values) => { + return [{ id: 1 }]; + } + }; + const result = await _execSQL(conn, 'SELECT * FROM users', []); + expect(result).to.be.an('array'); + expect(result[0].id).to.be.equal(1); + }); + + it('should handle error with conn.query', async () => { + const conn = { + query: (opt, callback) => { + callback(new Error('Query failed'), null); + } + }; + try { + await _execSQL(conn, 'SELECT * FROM users', []); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('Query failed'); + } + }); + + it('should handle error with conn.execute', async () => { + const conn = { + execute: async (sql, values) => { + throw new Error('Execute failed'); + } + }; + try { + await _execSQL(conn, 'SELECT * FROM users', []); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('Execute failed'); + } + }); + }); +}); + diff --git a/tests/operator.tests.js b/tests/operator.tests.js index 2b126f5..3ce5790 100644 --- a/tests/operator.tests.js +++ b/tests/operator.tests.js @@ -2,7 +2,7 @@ let expect = null; const { Builder } = require('../src/builder'); -const { QueryOperator } = require('../src/operator'); +const { QueryOperator, QueryHandler, QueryCondition } = require('../src/operator'); const { TransactionHandler } = require('../src/transaction'); describe('operator test case', () => { @@ -82,4 +82,361 @@ describe('operator test case', () => { throw e; } }); + + describe('QueryOperator methods', () => { + it('should use buildSql deprecated method', () => { + const conn = { + query: () => {} + }; + const operator = new QueryOperator(conn); + operator.table('users'); + const builder = operator.buildSql('select'); + expect(builder).to.be.instanceOf(Builder); + expect(builder.sql).to.be.equal('SELECT * FROM `users`'); + }); + + it('should use explain method', async () => { + const conn = { + query: (opt, callback) => { + callback(null, [{ id: 1 }]); + } + }; + const operator = new QueryOperator(conn); + operator.table('users'); + const result = await operator.explain('select'); + expect(result).to.be.an('array'); + }); + + it('should throw error when operator is invalid', async () => { + const conn = { + query: () => {} + }; + const operator = new QueryOperator(conn); + operator.table('users'); + operator.options.operator = null; + try { + await operator.exec(); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.include('Invalid operator'); + } + }); + + it('should throw error when connection is null', async () => { + const operator = new QueryOperator(null); + operator.table('users'); + operator.options.operator = 'select'; + try { + await operator.exec(); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('Connection is null'); + } + }); + + it('should handle select with attrs', async () => { + const conn = { + query: (opt, callback) => { + callback(null, [{ id: 1, name: 'test' }]); + } + }; + const operator = new QueryOperator(conn); + operator.table('users'); + const result = await operator.select('id', 'name'); + expect(result).to.be.an('array'); + }); + + it('should handle update with data', async () => { + const conn = { + query: (opt, callback) => { + callback(null, { affectedRows: 1 }); + } + }; + const operator = new QueryOperator(conn); + operator.table('users').where('id', 1); + const result = await operator.update({ name: 'test' }); + expect(result.affectedRows).to.be.equal(1); + }); + + it('should handle insert with keys', async () => { + const conn = { + query: (opt, callback) => { + callback(null, { affectedRows: 1 }); + } + }; + const operator = new QueryOperator(conn); + operator.table('users'); + const result = await operator.insert({ id: 1, name: 'test' }, ['id']); + expect(result.affectedRows).to.be.equal(1); + }); + + it('should throw error when insert data is not object', async () => { + const conn = { + query: () => {} + }; + const operator = new QueryOperator(conn); + operator.table('users'); + operator.options.data = null; + try { + await operator.insert(null); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('data must be an object'); + } + }); + + it('should handle insertAll', async () => { + const conn = { + query: (opt, callback) => { + callback(null, { affectedRows: 2 }); + } + }; + const operator = new QueryOperator(conn); + operator.table('users'); + const result = await operator.insertAll([ + { name: 'test1' }, + { name: 'test2' } + ]); + expect(result.affectedRows).to.be.equal(2); + }); + + it('should throw error when insertAll data is not array', async () => { + const conn = { + query: () => {} + }; + const operator = new QueryOperator(conn); + operator.table('users'); + operator.options.data = {}; + try { + await operator.insertAll(); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('data must be an array'); + } + }); + + it('should handle delete with id', async () => { + const conn = { + query: (opt, callback) => { + callback(null, { affectedRows: 1 }); + } + }; + const operator = new QueryOperator(conn); + operator.table('users'); + const result = await operator.delete(1); + expect(result.affectedRows).to.be.equal(1); + }); + + it('should handle delete with custom index field', async () => { + const conn = { + query: (opt, callback) => { + callback(null, { affectedRows: 1 }); + } + }; + const operator = new QueryOperator(conn); + operator.table('users'); + const result = await operator.delete(1, 'user_id'); + expect(result.affectedRows).to.be.equal(1); + }); + + it('should handle upsertRow with QueryCondition', async () => { + const conn = { + query: (opt, callback) => { + if (opt.sql && opt.sql.includes('COUNT(*)')) { + callback(null, [{ count: 1 }]); + } else { + callback(null, { affectedRows: 1 }); + } + } + }; + const operator = new QueryOperator(conn); + operator.table('users'); + const condition = new QueryCondition(); + condition.where('id', 1); + const result = await operator.upsertRow({ name: 'test' }, condition); + expect(result.affectedRows).to.be.equal(1); + }); + + it('should handle upsertRow with object condition', async () => { + const conn = { + query: (opt, callback) => { + if (opt.sql && opt.sql.includes('COUNT(*)')) { + callback(null, [{ count: 0 }]); + } else { + callback(null, { affectedRows: 1 }); + } + } + }; + const operator = new QueryOperator(conn); + operator.table('users'); + const result = await operator.upsertRow({ name: 'test' }, { id: 1 }); + expect(result.affectedRows).to.be.equal(1); + }); + + it('should throw error when upsertRow table is missing', async () => { + const conn = { + query: () => {} + }; + const operator = new QueryOperator(conn); + try { + await operator.upsertRow({ name: 'test' }, { id: 1 }); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('table is required'); + } + }); + + it('should handle notExec', async () => { + const conn = { + query: () => {} + }; + const operator = new QueryOperator(conn); + operator.table('users'); + operator.options.operator = 'select'; + operator.notExec(); + const builder = await operator.exec(); + expect(builder).to.be.instanceOf(Builder); + }); + + it('should throw error when custom driver without queryHandler', () => { + const conn = {}; + expect(() => { + new QueryOperator(conn, { driver: 'custom' }); + }).to.throw('queryHandler is required'); + }); + + it('should throw error when queryHandler is not a function', () => { + const conn = {}; + expect(() => { + new QueryOperator(conn, { + driver: 'custom', + queryHandler: 'not a function' + }); + }).to.throw('queryHandler must be a function'); + }); + }); + + describe('QueryHandler methods', () => { + it('should throw error when query opt is missing', async () => { + const conn = { + query: () => {} + }; + const handler = new QueryHandler(conn); + try { + await handler.query(); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('opt is required'); + } + }); + + it('should handle query', async () => { + const conn = { + query: (opt, callback) => { + callback(null, [{ id: 1 }]); + } + }; + const handler = new QueryHandler(conn); + const result = await handler.query({ + sql: 'SELECT * FROM users', + values: [] + }); + expect(result).to.be.an('array'); + }); + + it('should handle tables', () => { + const conn = { + query: () => {} + }; + const handler = new QueryHandler(conn); + const operator = handler.tables({ table: 'users' }, { table: 'posts' }); + expect(operator.options.tables.length).to.be.equal(2); + }); + + it('should handle upsert deprecated method', async () => { + const conn = { + query: (opt, callback) => { + if (opt.sql && opt.sql.includes('COUNT(*)')) { + callback(null, [{ count: 1 }]); + } else { + callback(null, { affectedRows: 1 }); + } + } + }; + const handler = new QueryHandler(conn); + handler.database = 'test_db'; + const result = await handler.upsert('users', { name: 'test' }, { id: 1 }); + expect(result.affectedRows).to.be.equal(1); + }); + + it('should handle existTable', async () => { + const conn = { + query: (opt, callback) => { + callback(null, [{ count: 1 }]); + } + }; + const handler = new QueryHandler(conn); + handler.database = 'test_db'; + const exists = await handler.existTable('users'); + expect(exists).to.be.true; + }); + + it('should handle existTable with database parameter', async () => { + const conn = { + query: (opt, callback) => { + callback(null, [{ count: 0 }]); + } + }; + const handler = new QueryHandler(conn); + handler.database = 'test_db'; + const exists = await handler.existTable('users', 'other_db'); + expect(exists).to.be.false; + }); + + it('should throw error when existTable table is missing', async () => { + const conn = { + query: () => {} + }; + const handler = new QueryHandler(conn); + try { + await handler.existTable(); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('table name is required'); + } + }); + + it('should handle existDatabase', async () => { + const conn = { + query: (opt, callback) => { + callback(null, [{ count: 1 }]); + } + }; + const handler = new QueryHandler(conn); + const exists = await handler.existDatabase('test_db'); + expect(exists).to.be.true; + }); + + it('should handle getTableFields without attrs', async () => { + const conn = { + query: (opt, callback) => { + callback(null, [{ column_name: 'id', data_type: 'int' }]); + } + }; + const handler = new QueryHandler(conn); + const fields = await handler.getTableFields('test_db', 'users'); + expect(fields).to.be.an('array'); + }); + + it('should handle getTableFields with attrs', async () => { + const conn = { + query: (opt, callback) => { + callback(null, [{ column_name: 'id' }]); + } + }; + const handler = new QueryHandler(conn); + const fields = await handler.getTableFields('test_db', 'users', 'column_name'); + expect(fields).to.be.an('array'); + }); + }); }); diff --git a/tests/query.tests.js b/tests/query.tests.js index ab121ec..10fdeb2 100644 --- a/tests/query.tests.js +++ b/tests/query.tests.js @@ -345,4 +345,302 @@ describe('query test case', () => { expect(res.sql).to.be.equal('SELECT * FROM `test` WHERE `id` = ? AND `company_id` = ? AND `type` = ? AND `disabled` = ? AND JSON_EXTRACT(`json`, \'$.time_end\') BETWEEN ? AND ?'); expect(JSON.stringify(res.values)).to.be.equal('[1,1,"company",0,"2024-12-09 00:00:00","2024-12-15 23:59:59"]'); }); + + describe('QueryCondition methods', () => { + it('should handle where with 4 arguments and OR flag', () => { + const condition = new QueryCondition(); + condition.where('id', '>', 1, true); + expect(condition.options.conditions.length).to.be.greaterThan(0); + }); + + it('should handle where with optType value swap', () => { + const condition = new QueryCondition(); + condition.where('id', 1, 'IN'); + expect(condition.options.conditions[0].opt).to.be.equal('IN'); + expect(condition.options.conditions[0].value).to.be.equal(1); + }); + + it('should handle whereAnd', () => { + const condition = new QueryCondition(); + condition.where('id', 1).whereAnd().where('name', 'test'); + expect(condition.options.conditions[1].opt).to.be.equal('AND'); + }); + + it('should handle whereOr', () => { + const condition = new QueryCondition(); + condition.where('id', 1).whereOr().where('name', 'test'); + expect(condition.options.conditions[1].opt).to.be.equal('OR'); + }); + + it('should handle whereIn', () => { + const condition = new QueryCondition(); + condition.whereIn('id', [1, 2, 3]); + expect(condition.options.conditions[0].opt).to.be.equal('IN'); + }); + + it('should handle whereNotIn', () => { + const condition = new QueryCondition(); + condition.whereNotIn('id', [1, 2, 3]); + expect(condition.options.conditions[0].opt).to.be.equal('NOT IN'); + }); + + it('should handle whereContain', () => { + const condition = new QueryCondition(); + condition.whereContain('name', 'test'); + expect(condition.options.conditions[0].opt).to.be.equal('CONTAIN'); + }); + + it('should handle whereNotContain', () => { + const condition = new QueryCondition(); + condition.whereNotContain('name', 'test'); + expect(condition.options.conditions[0].opt).to.be.equal('NOT CONTAIN'); + }); + + it('should handle whereBetween', () => { + const condition = new QueryCondition(); + condition.whereBetween('age', [18, 30]); + expect(condition.options.conditions[0].opt).to.be.equal('BETWEEN'); + }); + + it('should handle whereNotBetween', () => { + const condition = new QueryCondition(); + condition.whereNotBetween('age', [18, 30]); + expect(condition.options.conditions[0].opt).to.be.equal('NOT BETWEEN'); + }); + + it('should handle whereOverlaps', () => { + const condition = new QueryCondition(); + condition.whereOverlaps('tags', [1, 2]); + expect(condition.options.conditions[0].opt).to.be.equal('OVERLAPS'); + }); + + it('should handle whereNotOverlaps', () => { + const condition = new QueryCondition(); + condition.whereNotOverlaps('tags', [1, 2]); + expect(condition.options.conditions[0].opt).to.be.equal('NOT OVERLAPS'); + }); + + it('should handle whereLike with array value', () => { + const condition = new QueryCondition(); + condition.whereLike('name', ['%', 'test', '%']); + expect(condition.options.conditions[0].value).to.be.equal('%test%'); + }); + + it('should handle whereNotLike with array value', () => { + const condition = new QueryCondition(); + condition.whereNotLike('name', ['%', 'test', '%']); + expect(condition.options.conditions[0].value).to.be.equal('%test%'); + }); + + it('should handle whereCondition', () => { + const condition1 = new QueryCondition(); + condition1.where('id', 1); + const condition2 = new QueryCondition(); + condition2.whereCondition(condition1); + expect(condition2.options.conditions[0].opt).to.be.equal('GROUP'); + }); + + it('should handle whereObject', () => { + const condition = new QueryCondition(); + condition.whereObject({ id: 1, name: 'test' }); + // whereObject calls where for each key, and where adds AND between conditions + expect(condition.options.conditions.length).to.be.greaterThanOrEqual(2); + }); + + it('should throw error with invalid arguments', () => { + const condition = new QueryCondition(); + expect(() => { + condition.where(); + }).to.throw('Invalid arguments'); + }); + }); + + describe('Query methods', () => { + it('should handle tables method', () => { + const query = new Query('select'); + query.tables({ table: 'users' }, { table: 'posts' }); + expect(query.options.tables.length).to.be.equal(2); + }); + + it('should handle force method', () => { + const query = new Query('select'); + query.table('users').force('idx_users_id'); + expect(query.options.forceIndex).to.be.equal('idx_users_id'); + }); + + it('should handle keys method', () => { + const query = new Query('insert'); + query.keys('id', 'name'); + expect(query.options.keys.length).to.be.equal(2); + }); + + it('should handle limit with validation', () => { + const query = new Query('select'); + query.table('users'); + expect(() => { + query.limit('invalid'); + }).to.throw('limit must be an integer'); + }); + + it('should handle offset with validation', () => { + const query = new Query('select'); + query.table('users'); + expect(() => { + query.offset('invalid'); + }).to.throw('offset must be an integer'); + }); + + it('should handle attr with empty array', () => { + const query = new Query('select'); + query.table('users'); + query.attr(); + expect(query.options.attrs.length).to.be.equal(0); + }); + + it('should handle orderBy', () => { + const query = new Query('select'); + query.table('users').orderBy('id', 'desc'); + expect(query.options.orders[0].sortOrder).to.be.equal('DESC'); + }); + + it('should handle orderBy with lowercase asc', () => { + const query = new Query('select'); + query.table('users').orderBy('id', 'asc'); + expect(query.options.orders[0].sortOrder).to.be.equal('ASC'); + }); + + it('should handle groupBy', () => { + const query = new Query('select'); + query.table('users').groupBy('status', 'type'); + expect(query.options.groupField.length).to.be.equal(2); + }); + + it('should handle having with AND', () => { + const query = new Query('select'); + query.table('users').groupBy('status').having('count', '>', 1).having('sum', '>', 10); + expect(query.options.having.length).to.be.equal(3); // count condition + AND + sum condition + }); + + it('should handle having with OR', () => { + const query = new Query('select'); + query.table('users').groupBy('status').having('count', '>', 1).having('OR').having('sum', '>', 10); + // having adds AND between conditions, so: count condition + OR + sum condition = 3 + // But if OR is already there, it might add AND before OR, so could be more + expect(query.options.having.length).to.be.greaterThanOrEqual(3); + }); + + it('should handle page method', () => { + const query = new Query('select'); + query.table('users').page(10, 20); + expect(query.options.pageLimit).to.be.equal(10); + expect(query.options.pageOffset).to.be.equal(20); + }); + + it('should handle set with validation', () => { + const query = new Query('update'); + query.table('users'); + expect(() => { + query.set(null); + }).to.throw('data is required'); + }); + + it('should handle join with validation', () => { + const query = new Query('select'); + query.table('users'); + expect(() => { + query.join({}); + }).to.throw(); + }); + + it('should handle join with invalid join type', () => { + const query = new Query('select'); + query.table('users'); + expect(() => { + query.join({ + table: 'posts', + self_column: 'users.id', + foreign_column: 'posts.user_id', + join_type: 'invalid' + }); + }).to.throw('Invalid join type'); + }); + + it('should handle leftJoin', () => { + const query = new Query('select'); + query.table('users').leftJoin('posts', 'users.id = posts.user_id'); + expect(query.options.joins[0].join_type).to.be.equal('LEFT'); + }); + + it('should handle rightJoin', () => { + const query = new Query('select'); + query.table('users').rightJoin('posts', 'users.id = posts.user_id'); + expect(query.options.joins[0].join_type).to.be.equal('RIGHT'); + }); + + it('should handle innerJoin', () => { + const query = new Query('select'); + query.table('users').innerJoin('posts', 'users.id = posts.user_id'); + expect(query.options.joins[0].join_type).to.be.equal('INNER'); + }); + + it('should handle leftJoin error - table required', () => { + const query = new Query('select'); + query.table('users'); + expect(() => { + query.leftJoin(null, 'users.id = posts.user_id'); + }).to.throw('table is required'); + }); + + it('should handle leftJoin error - on required', () => { + const query = new Query('select'); + query.table('users'); + expect(() => { + query.leftJoin('posts', null); + }).to.throw('on is required'); + }); + + it('should handle whereConditions deprecated method', () => { + const query = new Query('select'); + query.table('users'); + query.whereConditions('AND', { id: 1 }); + expect(query.options.conditions.length).to.be.greaterThan(0); + }); + + it('should handle groupWhere deprecated method', () => { + const query = new Query('select'); + query.table('users'); + query.groupWhere('AND', { id: 1 }); + expect(query.options.conditions.length).to.be.greaterThan(0); + }); + + it('should handle orWhere deprecated method', () => { + const query = new Query('select'); + query.table('users').where('id', 1); + query.orWhere('name', '=', 'test'); + expect(query.options.conditions.length).to.be.equal(3); // id condition + OR + name condition + }); + + it('should handle orWhere error when no conditions', () => { + const query = new Query('select'); + query.table('users'); + expect(() => { + query.orWhere('name', '=', 'test'); + }).to.throw('At least one where condition is required'); + }); + + it('should handle andWhere deprecated method', () => { + const query = new Query('select'); + query.table('users').where('id', 1); + query.andWhere('name', '=', 'test'); + expect(query.options.conditions.length).to.be.equal(3); + }); + + it('should handle andWhere error when no conditions', () => { + const query = new Query('select'); + query.table('users'); + expect(() => { + query.andWhere('name', '=', 'test'); + }).to.throw('At least one where condition is required'); + }); + }); }); diff --git a/tests/transaction.tests.js b/tests/transaction.tests.js new file mode 100644 index 0000000..0fcd259 --- /dev/null +++ b/tests/transaction.tests.js @@ -0,0 +1,411 @@ +'use strict'; + +let expect = null; +const { TransactionOperator, TransactionHandler } = require('../src/transaction'); + +describe('transaction test case', () => { + before(async function () { + const chai = await import('chai'); + expect = chai.expect; + }); + + describe('TransactionOperator', () => { + it('should create TransactionOperator with default driver', () => { + const conn = { + query: () => { }, + execute: () => { } + }; + const options = { + driver: 'mysql' + }; + const operator = new TransactionOperator(conn, options); + expect(operator).to.be.instanceOf(TransactionOperator); + expect(operator.options.transaction).to.be.true; + expect(operator.options.driver).to.be.equal('mysql'); + }); + + it('should create TransactionOperator with custom driver and queryHandler', () => { + const conn = {}; + const options = { + driver: 'custom', + queryHandler: async () => { + return [{ id: 1 }]; + } + }; + const operator = new TransactionOperator(conn, options); + expect(operator).to.be.instanceOf(TransactionOperator); + expect(operator.options.driver).to.be.equal('custom'); + }); + + it('should throw error when custom driver without queryHandler', () => { + const conn = {}; + const options = { + driver: 'custom' + }; + expect(() => { + new TransactionOperator(conn, options); + }).to.throw('queryHandler is required'); + }); + + it('should throw error when queryHandler is not a function', () => { + const conn = {}; + const options = { + driver: 'custom', + queryHandler: 'not a function' + }; + expect(() => { + new TransactionOperator(conn, options); + }).to.throw('queryHandler must be a function'); + }); + + it('should append suffix', () => { + const conn = { + query: () => { }, + execute: () => { } + }; + const options = { + driver: 'mysql' + }; + const operator = new TransactionOperator(conn, options); + operator.table('users'); + operator.append('FOR UPDATE'); + expect(operator.options.suffix).to.be.equal('FOR UPDATE'); + }); + + it('should append null suffix', () => { + const conn = { + query: () => { }, + execute: () => { } + }; + const options = { + driver: 'mysql' + }; + const operator = new TransactionOperator(conn, options); + operator.table('users'); + operator.append(null); + expect(operator.options.suffix).to.be.null; + }); + }); + + describe('TransactionHandler', () => { + it('should create TransactionHandler with default level', () => { + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async () => { } + }; + const handler = new TransactionHandler(conn); + expect(handler.level).to.be.equal('SERIALIZABLE'); + expect(handler.isbegin).to.be.false; + }); + + it('should create TransactionHandler with full level name', () => { + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async () => { } + }; + const handler = new TransactionHandler(conn, { level: 'READ UNCOMMITTED' }); + expect(handler.level).to.be.equal('READ UNCOMMITTED'); + }); + + it('should create TransactionHandler with abbreviated level RU', () => { + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async () => { } + }; + const handler = new TransactionHandler(conn, { level: 'RU' }); + expect(handler.level).to.be.equal('READ UNCOMMITTED'); + }); + + it('should create TransactionHandler with abbreviated level RC', () => { + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async () => { } + }; + const handler = new TransactionHandler(conn, { level: 'RC' }); + expect(handler.level).to.be.equal('READ COMMITTED'); + }); + + it('should create TransactionHandler with abbreviated level RR', () => { + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async () => { } + }; + const handler = new TransactionHandler(conn, { level: 'RR' }); + expect(handler.level).to.be.equal('REPEATABLE READ'); + }); + + it('should create TransactionHandler with abbreviated level S', () => { + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async () => { } + }; + const handler = new TransactionHandler(conn, { level: 'S' }); + expect(handler.level).to.be.equal('SERIALIZABLE'); + }); + + it('should throw error with invalid level', () => { + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async () => { } + }; + expect(() => { + new TransactionHandler(conn, { level: 'INVALID' }); + }).to.throw('Invalid transaction level: INVALID'); + }); + + it('should execute query', async () => { + const conn = { + query: (opt, callback) => { + callback(null, [{ id: 1 }]); + }, + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async (sql, values) => { + return [[{ id: 1 }]]; + } + }; + const handler = new TransactionHandler(conn); + const result = await handler.query({ + sql: 'SELECT * FROM users', + values: [] + }); + expect(result).to.be.an('array'); + expect(result[0].id).to.be.equal(1); + }); + + it('should handle query error', async () => { + const conn = { + query: (opt, callback) => { + callback(new Error('Query error'), null); + }, + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async (sql, values) => { + throw new Error('Query error'); + } + }; + const handler = new TransactionHandler(conn); + try { + await handler.query({ + sql: 'SELECT * FROM users', + values: [] + }); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('Query error'); + } + }); + + it('should execute SQL with values', async () => { + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async (sql, values) => { + return [[{ id: 1, name: 'test' }]]; + } + }; + const handler = new TransactionHandler(conn); + const result = await handler.execute('SELECT * FROM users WHERE id = ?', [1]); + expect(result).to.be.an('array'); + expect(result[0][0].id).to.be.equal(1); + expect(result[0][0].name).to.be.equal('test'); + }); + + it('should get last insert id', async () => { + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async (sql, values) => { + return [[{ insert_id: 123 }]]; + } + }; + const handler = new TransactionHandler(conn); + const id = await handler.lastInsertId(); + expect(id).to.be.equal(123); + }); + + it('should get last insert id with custom alias', async () => { + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async (sql, values) => { + return [[{ custom_id: 456 }]]; + } + }; + const handler = new TransactionHandler(conn); + const id = await handler.lastInsertId('custom_id'); + expect(id).to.be.equal(456); + }); + + it('should return 0 when last insert id is null', async () => { + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async (sql, values) => { + return [[null]]; + } + }; + const handler = new TransactionHandler(conn); + const id = await handler.lastInsertId(); + expect(id).to.be.equal(0); + }); + + it('should begin transaction', async () => { + let isolationLevelSet = false; + let transactionBegun = false; + const conn = { + beginTransaction: async () => { + transactionBegun = true; + }, + commit: async () => { }, + rollback: async () => { }, + execute: async (sql, values) => { + if (sql.includes('SET TRANSACTION ISOLATION LEVEL')) { + isolationLevelSet = true; + } + return []; + } + }; + const handler = new TransactionHandler(conn, { level: 'READ COMMITTED' }); + await handler.begin(); + expect(handler.isbegin).to.be.true; + expect(isolationLevelSet).to.be.true; + expect(transactionBegun).to.be.true; + }); + + it('should throw error when table called before begin', () => { + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async () => { } + }; + const handler = new TransactionHandler(conn); + expect(() => { + handler.table('users'); + }).to.throw('Transaction is not begin'); + }); + + it('should upsert row', async () => { + let countCalled = false; + let updateCalled = false; + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async (sql, values) => { + if (sql.includes('COUNT(*)')) { + countCalled = true; + return [[{ count: 1 }]]; + } + if (sql.includes('UPDATE')) { + updateCalled = true; + return [{ affectedRows: 1 }]; + } + return []; + }, + query: (opt, callback) => { + if (opt.sql && opt.sql.includes('COUNT(*)')) { + callback(null, [{ count: 1 }]); + } else if (opt.sql && opt.sql.includes('UPDATE')) { + callback(null, { affectedRows: 1 }); + } else { + callback(null, []); + } + } + }; + const handler = new TransactionHandler(conn); + await handler.begin(); + const result = await handler.upsert('users', { name: 'test' }, { id: 1 }); + expect(countCalled).to.be.true; + expect(updateCalled).to.be.true; + expect(result).to.be.an('object'); + expect(result.affectedRows).to.be.equal(1); + }); + + it('should commit transaction', async () => { + let committed = false; + const conn = { + beginTransaction: async () => { }, + commit: async () => { + committed = true; + }, + rollback: async () => { }, + execute: async () => { } + }; + const handler = new TransactionHandler(conn); + await handler.begin(); + await handler.commit(); + expect(committed).to.be.true; + }); + + it('should throw error when commit called before begin', async () => { + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async () => { } + }; + const handler = new TransactionHandler(conn); + try { + await handler.commit(); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('Transaction is not begin'); + } + }); + + it('should rollback transaction', async () => { + let rolledBack = false; + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { + rolledBack = true; + }, + execute: async () => { } + }; + const handler = new TransactionHandler(conn); + await handler.begin(); + await handler.rollback(); + expect(rolledBack).to.be.true; + }); + + it('should throw error when rollback called before begin', async () => { + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async () => { } + }; + const handler = new TransactionHandler(conn); + try { + await handler.rollback(); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('Transaction is not begin'); + } + }); + }); +}); + From 33a2cafba5e6eebe9aa8346cf3c643b4f0a28908 Mon Sep 17 00:00:00 2001 From: axiosleo Date: Sat, 6 Dec 2025 20:18:05 +0800 Subject: [PATCH 2/2] feat(tests): expand test coverage for ManageSQLBuilder and enhance query handling in various scenarios --- tests/builder.tests.js | 331 +++++++++++++++++++++++++++++++++++++ tests/client.tests.js | 22 +++ tests/core.tests.js | 103 ++++++++++++ tests/hook.tests.js | 76 +++++++++ tests/operator.tests.js | 67 ++++++++ tests/query.tests.js | 155 +++++++++++++++++ tests/transaction.tests.js | 38 +++++ 7 files changed, 792 insertions(+) diff --git a/tests/builder.tests.js b/tests/builder.tests.js index 068b146..91ab6bb 100644 --- a/tests/builder.tests.js +++ b/tests/builder.tests.js @@ -635,4 +635,335 @@ describe('builder test case', () => { delete options.forceIndex; expect((new Builder(options)).sql).to.be.equal('UPDATE `table1` SET `name` = ? WHERE `id` = ?'); }); + + describe('ManageSQLBuilder renderSingleColumn', () => { + it('should handle VARCHAR type without length (default 255)', () => { + const options = { + operator: 'create', + target: 'column', + table: 'test_table', + name: 'description', + type: 'varchar' + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('`description` VARCHAR(255)'); + }); + + it('should handle UNSIGNED attribute', () => { + const options = { + operator: 'create', + target: 'column', + table: 'test_table', + name: 'age', + type: 'int', + unsigned: true + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('`age` INT(11) UNSIGNED'); + }); + + it('should throw error when PRIMARY KEY has default value', () => { + const options = { + operator: 'create', + target: 'column', + table: 'test_table', + name: 'id', + type: 'int', + primaryKey: true, + default: 1 + }; + expect(() => { + (new ManageSQLBuilder(options)).sql; + }).to.throw('Primary key can not have default value.'); + }); + + it('should handle default value as null', () => { + const options = { + operator: 'create', + target: 'column', + table: 'test_table', + name: 'status', + type: 'int', + default: null + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('`status` INT(11) DEFAULT NULL'); + }); + + it('should handle default value as timestamp', () => { + const options = { + operator: 'create', + target: 'column', + table: 'test_table', + name: 'created_at', + type: 'timestamp', + default: 'timestamp' + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('DEFAULT CURRENT_TIMESTAMP'); + }); + + it('should handle default value as TIMESTAMP', () => { + const options = { + operator: 'create', + target: 'column', + table: 'test_table', + name: 'created_at', + type: 'timestamp', + default: 'TIMESTAMP' + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('DEFAULT CURRENT_TIMESTAMP'); + }); + + it('should handle default value as CURRENT_TIMESTAMP', () => { + const options = { + operator: 'create', + target: 'column', + table: 'test_table', + name: 'updated_at', + type: 'timestamp', + default: 'CURRENT_TIMESTAMP' + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('DEFAULT CURRENT_TIMESTAMP'); + }); + + it('should handle default value as string', () => { + const options = { + operator: 'create', + target: 'column', + table: 'test_table', + name: 'name', + type: 'varchar', + default: 'default_name' + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('DEFAULT \'default_name\''); + }); + + it('should handle default value as number', () => { + const options = { + operator: 'create', + target: 'column', + table: 'test_table', + name: 'count', + type: 'int', + default: 0 + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('DEFAULT 0'); + }); + + it('should handle onUpdate attribute', () => { + const options = { + operator: 'create', + target: 'column', + table: 'test_table', + name: 'updated_at', + type: 'timestamp', + onUpdate: 'CURRENT_TIMESTAMP' + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('ON UPDATE CURRENT_TIMESTAMP'); + }); + + it('should handle after attribute', () => { + const options = { + operator: 'create', + target: 'column', + table: 'test_table', + name: 'description', + type: 'varchar', + after: 'name' + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('AFTER `name`'); + }); + + it('should handle INT type without length (default 11)', () => { + const options = { + operator: 'create', + target: 'column', + table: 'test_table', + name: 'id', + type: 'int' + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('`id` INT(11)'); + }); + + it('should handle TINYINT type without length (default 4)', () => { + const options = { + operator: 'create', + target: 'column', + table: 'test_table', + name: 'status', + type: 'tinyint' + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('`status` TINYINT(4)'); + }); + }); + + describe('ManageSQLBuilder other methods', () => { + it('should handle createColumn', () => { + const options = { + operator: 'create', + target: 'column', + table: 'test_table', + name: 'new_column', + type: 'varchar', + length: 100 + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('ALTER TABLE `test_table` ADD COLUMN'); + expect(sql).to.include('`new_column` VARCHAR(100)'); + }); + + it('should throw error when createColumn table is missing', () => { + const options = { + operator: 'create', + target: 'column', + name: 'new_column', + type: 'varchar' + }; + expect(() => { + (new ManageSQLBuilder(options)).sql; + }).to.throw('Table name is required'); + }); + + it('should handle createIndex with unique', () => { + const options = { + operator: 'create', + target: 'index', + table: 'test_table', + name: 'idx_unique_name', + columns: ['name'], + unique: true + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('CREATE UNIQUE INDEX'); + expect(sql).to.include('`idx_unique_name`'); + }); + + it('should handle createIndex with visible false', () => { + const options = { + operator: 'create', + target: 'index', + table: 'test_table', + name: 'idx_name', + columns: ['name'], + visible: false + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('INVISIBLE'); + }); + + it('should handle createIndex with order desc', () => { + const options = { + operator: 'create', + target: 'index', + table: 'test_table', + name: 'idx_name', + columns: ['name desc'] + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('`name` DESC'); + }); + + it('should handle dropIndex', () => { + const options = { + operator: 'drop', + target: 'index', + table: 'test_table', + name: 'idx_name' + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.be.equal('DROP INDEX `idx_name` ON `test_table`'); + }); + + it('should handle dropForeignKey', () => { + const options = { + operator: 'drop', + target: 'foreignKey', + table: 'test_table', + name: 'fk_test' + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.be.equal('ALTER TABLE `test_table` DROP FOREIGN KEY `fk_test`'); + }); + + it('should handle createColumns with uniqIndex', () => { + const options = { + operator: 'create', + target: 'table', + name: 'test_table', + columns: { + id: { + type: 'int', + primaryKey: true + }, + email: { + type: 'varchar', + length: 255, + uniqIndex: true + } + } + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('UNIQUE INDEX `email`'); + }); + + it('should handle createColumns with reference', () => { + const options = { + operator: 'create', + target: 'table', + name: 'test_table', + columns: { + id: { + type: 'int', + primaryKey: true + }, + user_id: { + type: 'int', + reference: { + table: 'users', + column: 'id', + onDelete: 'CASCADE', + onUpdate: 'RESTRICT' + } + } + } + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('FOREIGN KEY'); + expect(sql).to.include('ON DELETE CASCADE'); + expect(sql).to.include('ON UPDATE RESTRICT'); + }); + + it('should handle createColumns with reference default actions', () => { + const options = { + operator: 'create', + target: 'table', + name: 'test_table', + columns: { + id: { + type: 'int', + primaryKey: true + }, + user_id: { + type: 'int', + reference: { + table: 'users', + column: 'id' + } + } + } + }; + const sql = (new ManageSQLBuilder(options)).sql; + expect(sql).to.include('ON DELETE NO ACTION'); + expect(sql).to.include('ON UPDATE NO ACTION'); + }); + }); }); diff --git a/tests/client.tests.js b/tests/client.tests.js index 1019f23..691579c 100644 --- a/tests/client.tests.js +++ b/tests/client.tests.js @@ -547,6 +547,28 @@ describe('client test case', () => { mm.restore(); }); + it('should return undefined when execQuery receives non-Query instance', async () => { + mm(mysql, 'createConnection', (options) => { + return { + connect: () => { }, + _closed: false, + _closing: false, + destroyed: false + }; + }); + const options = { + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'test_db' + }; + const client = new MySQLClient(options); + const result = await client.execQuery({ notAQuery: true }); + expect(result).to.be.undefined; + mm.restore(); + }); + it('should close connection', async () => { let closed = false; mm(mysql, 'createConnection', (options) => { diff --git a/tests/core.tests.js b/tests/core.tests.js index 5115e4f..38f4fd0 100644 --- a/tests/core.tests.js +++ b/tests/core.tests.js @@ -161,6 +161,86 @@ describe('core test case', () => { expect(err.message).to.be.equal('Query error'); } }); + + it('should handle builder with empty values array', async () => { + const conn = { + query: (opt, callback) => { + expect(opt.values).to.be.an('array'); + callback(null, [{ id: 1 }]); + } + }; + const options = { + driver: 'mysql', + operator: 'select', + tables: [{ table: 'users' }], + conditions: [], + orders: [], + groupField: [], + having: [] + }; + const result = await _query(conn, options); + expect(result).to.be.an('array'); + }); + + it('should handle transaction mode with empty values', async () => { + const conn = { + execute: async (sql, values) => { + expect(values).to.be.an('array'); + return [[{ id: 1 }]]; + } + }; + const options = { + driver: 'mysql', + transaction: true, + operator: 'select', + tables: [{ table: 'users' }], + conditions: [], + orders: [], + groupField: [], + having: [] + }; + const result = await _query(conn, options); + expect(result).to.be.an('array'); + }); + + it('should throw error when queryHandler is not a function', async () => { + const conn = {}; + const options = { + driver: 'custom', + queryHandler: 'not a function', + operator: 'select', + tables: [{ table: 'users' }], + conditions: [], + orders: [], + groupField: [], + having: [] + }; + try { + await _query(conn, options); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('queryHandler must return a promise'); + } + }); + + it('should throw error when queryHandler is missing', async () => { + const conn = {}; + const options = { + driver: 'custom', + operator: 'select', + tables: [{ table: 'users' }], + conditions: [], + orders: [], + groupField: [], + having: [] + }; + try { + await _query(conn, options); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('queryHandler must return a promise'); + } + }); }); describe('_execSQL function', () => { @@ -185,6 +265,29 @@ describe('core test case', () => { expect(result[0].id).to.be.equal(1); }); + it('should use conn.execute when query property does not exist', async () => { + const conn = { + execute: async (sql, values) => { + return [{ id: 1 }]; + } + }; + const result = await _execSQL(conn, 'SELECT * FROM users', []); + expect(result).to.be.an('array'); + expect(result[0].id).to.be.equal(1); + }); + + it('should use conn.execute when query is null', async () => { + const conn = { + query: null, + execute: async (sql, values) => { + return [{ id: 1 }]; + } + }; + const result = await _execSQL(conn, 'SELECT * FROM users', []); + expect(result).to.be.an('array'); + expect(result[0].id).to.be.equal(1); + }); + it('should handle error with conn.query', async () => { const conn = { query: (opt, callback) => { diff --git a/tests/hook.tests.js b/tests/hook.tests.js index 051e779..53adb1b 100644 --- a/tests/hook.tests.js +++ b/tests/hook.tests.js @@ -27,4 +27,80 @@ describe('hook test case', () => { Hook.post(callback, { table: ['test1', 'test2'], opt: ['insert', 'find'] }); }); + + it('should handle register method', () => { + let called = false; + const callback = () => { + called = true; + }; + Hook.register(callback, 'custom', 'event', 'path'); + Hook.trigger(['custom', 'event', 'path']); + expect(called).to.be.true; + }); + + it('should handle special keys __proto__, constructor, prototype', () => { + let called = false; + const callback = () => { + called = true; + }; + Hook.register(callback, 'pre', '__proto__', 'test'); + Hook.trigger(['pre', '__proto__', 'test']); + expect(called).to.be.true; + + called = false; + Hook.register(callback, 'pre', 'constructor', 'test'); + Hook.trigger(['pre', 'constructor', 'test']); + expect(called).to.be.true; + + called = false; + Hook.register(callback, 'pre', 'prototype', 'test'); + Hook.trigger(['pre', 'prototype', 'test']); + expect(called).to.be.true; + }); + + it('should handle wildcard paths', () => { + let wildcardCalled = false; + let specificCalled = false; + + const wildcardCallback = () => { + wildcardCalled = true; + }; + const specificCallback = () => { + specificCalled = true; + }; + + Hook.register(wildcardCallback, 'pre', '*', 'test'); + Hook.register(specificCallback, 'pre', 'users', 'test'); + + Hook.trigger(['pre', 'users', 'test']); + expect(wildcardCalled).to.be.true; + expect(specificCalled).to.be.true; + }); + + it('should handle eventRecur termination', () => { + let called = false; + const callback = (...args) => { + called = true; + expect(args.length).to.be.equal(2); + expect(args[0]).to.be.equal('arg1'); + expect(args[1]).to.be.equal('arg2'); + }; + Hook.register(callback, 'test', 'event'); + Hook.trigger(['test', 'event'], 'arg1', 'arg2'); + expect(called).to.be.true; + }); + + it('should handle listen with trigger', () => { + let callCount = 0; + const callback = (...args) => { + callCount++; + if (callCount === 1) { + expect(args.length).to.be.equal(1); + expect(args[0]).to.be.equal('data'); + } + }; + Hook.pre(callback, { table: 'users', opt: 'select' }); + Hook.listen({ label: 'pre', table: 'users', opt: 'select' }, 'data'); + expect(callCount).to.be.greaterThanOrEqual(1); + }); }); diff --git a/tests/operator.tests.js b/tests/operator.tests.js index 3ce5790..3d4af17 100644 --- a/tests/operator.tests.js +++ b/tests/operator.tests.js @@ -21,6 +21,54 @@ describe('operator test case', () => { expect(res).to.be.an('array'); }); + it('should return first result for find operator', async () => { + const conn = { + query: (opt, callback) => { + callback(null, [{ id: 1, name: 'test1' }, { id: 2, name: 'test2' }]); + } + }; + const operator = new QueryOperator(conn); + operator.table('test'); + operator.options.operator = 'find'; + const res = await operator.exec(); + expect(res).to.be.an('object'); + expect(res.id).to.be.equal(1); + expect(res.name).to.be.equal('test1'); + }); + + it('should return undefined when find result is empty', async () => { + const conn = { + query: (opt, callback) => { + callback(null, []); + } + }; + const operator = new QueryOperator(conn); + operator.table('test'); + operator.options.operator = 'find'; + const res = await operator.exec(); + expect(res).to.be.undefined; + }); + + it('should handle error with sql property and stack trace', async () => { + const conn = { + query: (opt, callback) => { + const err = new Error('Database error'); + err.sql = 'SELECT * FROM test'; + callback(err, null); + } + }; + const operator = new QueryOperator(conn); + operator.table('test'); + operator.options.operator = 'select'; + try { + await operator.exec(); + expect.fail('Should have thrown an error'); + } catch (err) { + expect(err.message).to.be.equal('Database error'); + expect(err.sql).to.be.equal('SELECT * FROM test'); + } + }); + it('custom driver', async () => { const conn = { query: async (_, callback) => { @@ -105,6 +153,7 @@ describe('operator test case', () => { operator.table('users'); const result = await operator.explain('select'); expect(result).to.be.an('array'); + expect(result.length).to.be.greaterThanOrEqual(1); }); it('should throw error when operator is invalid', async () => { @@ -144,6 +193,7 @@ describe('operator test case', () => { operator.table('users'); const result = await operator.select('id', 'name'); expect(result).to.be.an('array'); + expect(result.length).to.be.greaterThanOrEqual(1); }); it('should handle update with data', async () => { @@ -369,6 +419,23 @@ describe('operator test case', () => { expect(result.affectedRows).to.be.equal(1); }); + it('should handle upsert deprecated method with insert path', async () => { + const conn = { + query: (opt, callback) => { + if (opt.sql && opt.sql.includes('COUNT(*)')) { + callback(null, [{ count: 0 }]); + } else { + callback(null, { affectedRows: 1, insertId: 123 }); + } + } + }; + const handler = new QueryHandler(conn); + handler.database = 'test_db'; + const result = await handler.upsert('users', { name: 'test' }, { id: 999 }); + expect(result.affectedRows).to.be.equal(1); + expect(result.insertId).to.be.equal(123); + }); + it('should handle existTable', async () => { const conn = { query: (opt, callback) => { diff --git a/tests/query.tests.js b/tests/query.tests.js index 10fdeb2..8f42b96 100644 --- a/tests/query.tests.js +++ b/tests/query.tests.js @@ -453,6 +453,15 @@ describe('query test case', () => { condition.where(); }).to.throw('Invalid arguments'); }); + + it('should handle where with single string argument (opt only)', () => { + const condition = new QueryCondition(); + condition.where('AND'); + expect(condition.options.conditions.length).to.be.equal(1); + expect(condition.options.conditions[0].opt).to.be.equal('AND'); + expect(condition.options.conditions[0].key).to.be.null; + expect(condition.options.conditions[0].value).to.be.null; + }); }); describe('Query methods', () => { @@ -529,6 +538,24 @@ describe('query test case', () => { expect(query.options.having.length).to.be.greaterThanOrEqual(3); }); + it('should handle having with AND directly', () => { + const query = new Query('select'); + query.table('users').groupBy('status'); + // having('AND') means having(null, 'AND', null) + query.having(null, 'AND', null); + expect(query.options.having.length).to.be.equal(1); + expect(query.options.having[0].opt).to.be.equal('AND'); + }); + + it('should handle having with OR directly', () => { + const query = new Query('select'); + query.table('users').groupBy('status'); + // having('OR') means having(null, 'OR', null) + query.having(null, 'OR', null); + expect(query.options.having.length).to.be.equal(1); + expect(query.options.having[0].opt).to.be.equal('OR'); + }); + it('should handle page method', () => { const query = new Query('select'); query.table('users').page(10, 20); @@ -606,6 +633,76 @@ describe('query test case', () => { expect(query.options.conditions.length).to.be.greaterThan(0); }); + it('should handle whereConditions with empty array', () => { + const query = new Query('select'); + query.table('users'); + const result = query.whereConditions(); + expect(result).to.equal(query); + expect(query.options.conditions.length).to.be.equal(0); + }); + + it('should handle whereConditions with first condition not string', () => { + const query = new Query('select'); + query.table('users').where('id', 1); + query.whereConditions({ id: 2 }); + expect(query.options.conditions.length).to.be.greaterThan(1); + expect(query.options.conditions[1].opt).to.be.equal('AND'); + }); + + it('should handle whereConditions with array length 2', () => { + const query = new Query('select'); + query.table('users'); + query.whereConditions(['id', 1]); + expect(query.options.conditions.length).to.be.equal(1); + expect(query.options.conditions[0].key).to.be.equal('id'); + expect(query.options.conditions[0].opt).to.be.equal('='); + expect(query.options.conditions[0].value).to.be.equal(1); + }); + + it('should handle whereConditions with array length 3', () => { + const query = new Query('select'); + query.table('users'); + query.whereConditions(['id', '>', 1]); + expect(query.options.conditions.length).to.be.equal(1); + expect(query.options.conditions[0].key).to.be.equal('id'); + expect(query.options.conditions[0].opt).to.be.equal('>'); + expect(query.options.conditions[0].value).to.be.equal(1); + }); + + it('should handle whereConditions with invalid array length', () => { + const query = new Query('select'); + query.table('users'); + expect(() => { + query.whereConditions(['id', '>', 1, 'extra']); + }).to.throw('Invalid condition'); + }); + + it('should handle whereConditions with object condition', () => { + const query = new Query('select'); + query.table('users'); + query.whereConditions({ key: 'id', opt: '=', value: 1 }); + expect(query.options.conditions.length).to.be.equal(1); + }); + + it('should handle whereConditions with other type condition', () => { + const query = new Query('select'); + query.table('users'); + const conditionObj = { key: 'id', opt: '=', value: 1 }; + query.whereConditions(conditionObj); + expect(query.options.conditions.length).to.be.equal(1); + }); + + it('should handle whereConditions with non-string, non-object, non-array condition', () => { + const query = new Query('select'); + query.table('users'); + const conditionObj = { key: 'id', opt: '=', value: 1 }; + query.whereConditions(conditionObj); + expect(query.options.conditions.length).to.be.equal(1); + expect(query.options.conditions[0].key).to.be.equal('id'); + expect(query.options.conditions[0].opt).to.be.equal('='); + expect(query.options.conditions[0].value).to.be.equal(1); + }); + it('should handle groupWhere deprecated method', () => { const query = new Query('select'); query.table('users'); @@ -613,6 +710,64 @@ describe('query test case', () => { expect(query.options.conditions.length).to.be.greaterThan(0); }); + it('should handle groupWhere with empty array', () => { + const query = new Query('select'); + query.table('users'); + const result = query.groupWhere(); + expect(result).to.equal(query); + expect(query.options.conditions.length).to.be.equal(0); + }); + + it('should handle groupWhere with array length 2', () => { + const query = new Query('select'); + query.table('users'); + query.groupWhere(['id', 1]); + expect(query.options.conditions.length).to.be.equal(1); + expect(query.options.conditions[0].opt).to.be.equal('group'); + expect(query.options.conditions[0].value.length).to.be.equal(1); + expect(query.options.conditions[0].value[0].key).to.be.equal('id'); + }); + + it('should handle groupWhere with array length 3', () => { + const query = new Query('select'); + query.table('users'); + query.groupWhere(['id', '>', 1]); + expect(query.options.conditions.length).to.be.equal(1); + expect(query.options.conditions[0].opt).to.be.equal('group'); + expect(query.options.conditions[0].value.length).to.be.equal(1); + expect(query.options.conditions[0].value[0].key).to.be.equal('id'); + expect(query.options.conditions[0].value[0].opt).to.be.equal('>'); + }); + + it('should handle groupWhere with invalid array length', () => { + const query = new Query('select'); + query.table('users'); + expect(() => { + query.groupWhere(['id', '>', 1, 'extra']); + }).to.throw('Invalid condition'); + }); + + it('should handle groupWhere with object condition', () => { + const query = new Query('select'); + query.table('users'); + query.groupWhere({ key: 'id', opt: '=', value: 1 }); + expect(query.options.conditions.length).to.be.equal(1); + expect(query.options.conditions[0].opt).to.be.equal('group'); + }); + + it('should handle groupWhere with other type condition', () => { + const query = new Query('select'); + query.table('users'); + const conditionObj = { key: 'id', opt: '=', value: 1 }; + query.groupWhere(conditionObj); + expect(query.options.conditions.length).to.be.equal(1); + expect(query.options.conditions[0].opt).to.be.equal('group'); + expect(query.options.conditions[0].value.length).to.be.equal(1); + expect(query.options.conditions[0].value[0].key).to.be.equal('id'); + expect(query.options.conditions[0].value[0].opt).to.be.equal('='); + expect(query.options.conditions[0].value[0].value).to.be.equal(1); + }); + it('should handle orWhere deprecated method', () => { const query = new Query('select'); query.table('users').where('id', 1); diff --git a/tests/transaction.tests.js b/tests/transaction.tests.js index 0fcd259..f4019bb 100644 --- a/tests/transaction.tests.js +++ b/tests/transaction.tests.js @@ -343,6 +343,44 @@ describe('transaction test case', () => { expect(result.affectedRows).to.be.equal(1); }); + it('should upsert row with insert path when count is 0', async () => { + let countCalled = false; + let insertCalled = false; + const conn = { + beginTransaction: async () => { }, + commit: async () => { }, + rollback: async () => { }, + execute: async (sql, values) => { + if (sql.includes('COUNT(*)')) { + countCalled = true; + return [[{ count: 0 }]]; + } + if (sql.includes('INSERT')) { + insertCalled = true; + return [{ affectedRows: 1, insertId: 123 }]; + } + return []; + }, + query: (opt, callback) => { + if (opt.sql && opt.sql.includes('COUNT(*)')) { + callback(null, [{ count: 0 }]); + } else if (opt.sql && opt.sql.includes('INSERT')) { + callback(null, { affectedRows: 1, insertId: 123 }); + } else { + callback(null, []); + } + } + }; + const handler = new TransactionHandler(conn); + await handler.begin(); + const result = await handler.upsert('users', { name: 'test' }, { id: 999 }); + expect(countCalled).to.be.true; + expect(insertCalled).to.be.true; + expect(result).to.be.an('object'); + expect(result.affectedRows).to.be.equal(1); + expect(result.insertId).to.be.equal(123); + }); + it('should commit transaction', async () => { let committed = false; const conn = {