From 4ac6752fab04ca6e56bbef757ddb5f3aa8fcb61e Mon Sep 17 00:00:00 2001 From: George Raduta Date: Tue, 5 May 2026 11:26:46 +0200 Subject: [PATCH 1/4] Add conditional creation of pool in query service Co-authored-by: Copilot --- InfoLogger/lib/services/QueryService.js | 47 ++++++++++++++----- .../lib/services/mocha-query-service.test.js | 44 ++++++++++++++++- 2 files changed, 79 insertions(+), 12 deletions(-) diff --git a/InfoLogger/lib/services/QueryService.js b/InfoLogger/lib/services/QueryService.js index 9007265da..929e6b30a 100644 --- a/InfoLogger/lib/services/QueryService.js +++ b/InfoLogger/lib/services/QueryService.js @@ -13,7 +13,7 @@ */ const mariadb = require('mariadb'); -const { LogManager } = require('@aliceo2/web-ui'); +const { LogManager, InvalidInputError } = require('@aliceo2/web-ui'); const { fromSqlToNativeError } = require('../utils/fromSqlToNativeError'); const { processPreparedSQLStatement } = require('../utils/preparedStatementParser'); @@ -23,19 +23,27 @@ class QueryService { * @param {object} configMySql - mysql config */ constructor(configMySql = {}) { - configMySql.user = configMySql?.user ?? 'gui'; - configMySql.password = configMySql?.password ?? ''; - configMySql.host = configMySql?.host ?? 'localhost'; - configMySql.port = configMySql?.port ?? 3306; - configMySql.database = configMySql?.database ?? 'info_logger'; - configMySql.connectionLimit = configMySql?.connectionLimit ?? 25; this._timeout = configMySql?.timeout ?? 10000; - this._host = configMySql.host; - this._port = configMySql.port; - - this._pool = mariadb.createPool(configMySql); + this._host = configMySql?.host; + this._port = configMySql?.port; this._isAvailable = false; this._logger = LogManager.getLogger(`${process.env.npm_config_log_label ?? 'ilg'}/query-service`); + + // Only create a connection pool if configuration is provided + if (configMySql?.host && configMySql?.port) { + configMySql.user = configMySql.user ?? 'gui'; + configMySql.password = configMySql.password ?? ''; + configMySql.host = configMySql.host ?? 'localhost'; + configMySql.port = configMySql.port ?? 3306; + configMySql.database = configMySql.database ?? 'info_logger'; + configMySql.connectionLimit = configMySql.connectionLimit ?? 25; + this._host = configMySql.host; + this._port = configMySql.port; + + this._pool = mariadb.createPool(configMySql); + } else { + this._pool = null; + } } /** @@ -45,6 +53,17 @@ class QueryService { * @returns {Promise} - a promise that resolves if connection is successful */ async checkConnection(timeout = this._timeout, shouldThrow = true) { + if (!this._pool) { + this._isAvailable = false; + const error = new InvalidInputError('No database configuration provided'); + if (shouldThrow) { + throw error; + } else { + this._logger.errorMessage(error); + } + return; + } + try { await this._pool.query({ sql: 'SELECT 1', @@ -87,6 +106,9 @@ class QueryService { let rows = []; try { + if (!this._pool) { + throw new Error('No database connection available'); + } rows = await this._pool.query( { sql: requestRows, @@ -119,6 +141,9 @@ class QueryService { + 'in (\'D\', \'I\', \'W\', \'E\', \'F\') GROUP BY severity;'; let data = []; try { + if (!this._pool) { + throw new Error('No database connection available'); + } data = await this._pool.query({ sql: groupByStatement, timeout: this._timeout, diff --git a/InfoLogger/test/lib/services/mocha-query-service.test.js b/InfoLogger/test/lib/services/mocha-query-service.test.js index b901cff9e..24fd4ca85 100644 --- a/InfoLogger/test/lib/services/mocha-query-service.test.js +++ b/InfoLogger/test/lib/services/mocha-query-service.test.js @@ -16,7 +16,7 @@ const assert = require('assert'); const sinon = require('sinon'); const config = require('../../../config-default.js'); const { QueryService } = require('../../../lib/services/QueryService.js'); -const { UnauthorizedAccessError, TimeoutError } = require('@aliceo2/web-ui'); +const { UnauthorizedAccessError, TimeoutError, InvalidInputError } = require('@aliceo2/web-ui'); describe('\'QueryService\' test suite', () => { const filters = { @@ -102,6 +102,48 @@ describe('\'QueryService\' test suite', () => { await assert.doesNotReject(sqlDataSource.checkConnection()); assert.ok(sqlDataSource.isAvailable); }); + + it('should throw InvalidInputError when no pool is configured and shouldThrow is true', async () => { + const sqlDataSource = new QueryService(); + await assert.rejects( + sqlDataSource.checkConnection(), + new InvalidInputError('No database configuration provided'), + ); + assert.ok(sqlDataSource.isAvailable === false); + }); + + it('should not throw and log error when no pool is configured and shouldThrow is false', async () => { + const sqlDataSource = new QueryService(); + const logStub = sinon.stub(); + sqlDataSource._logger = { + errorMessage: logStub, + }; + + await assert.doesNotReject(sqlDataSource.checkConnection(10000, false)); + assert.ok(sqlDataSource.isAvailable === false); + assert.ok(logStub.calledOnce); + assert.ok(logStub.firstCall.args[0].message === 'No database configuration provided'); + }); + + it('should not throw and log error when connection fails and shouldThrow is false', async () => { + const sqlDataSource = new QueryService(config.mysql); + const logStub = sinon.stub(); + sqlDataSource._logger = { + errorMessage: logStub, + }; + sqlDataSource._pool = { + query: sinon.stub().rejects({ + code: 'ER_ACCESS_DENIED_ERROR', + errno: 1045, + sqlMessage: 'Access denied', + }), + }; + + await assert.doesNotReject(sqlDataSource.checkConnection(10000, false)); + assert.ok(sqlDataSource.isAvailable === false); + assert.ok(logStub.calledOnce); + assert.ok(logStub.firstCall.args[0].code === 'ER_ACCESS_DENIED_ERROR'); + }); }); describe('Filter to SQL Conditions', () => { From c8c0c95036fdf7caf0b8e967e2cd76cacb6b5cb0 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Tue, 5 May 2026 11:37:39 +0200 Subject: [PATCH 2/4] As this is a unit test with no mariadb instance, never create a pool Co-authored-by: Copilot --- .../lib/services/mocha-query-service.test.js | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/InfoLogger/test/lib/services/mocha-query-service.test.js b/InfoLogger/test/lib/services/mocha-query-service.test.js index 24fd4ca85..ff3470ddd 100644 --- a/InfoLogger/test/lib/services/mocha-query-service.test.js +++ b/InfoLogger/test/lib/services/mocha-query-service.test.js @@ -14,7 +14,6 @@ const assert = require('assert'); const sinon = require('sinon'); -const config = require('../../../config-default.js'); const { QueryService } = require('../../../lib/services/QueryService.js'); const { UnauthorizedAccessError, TimeoutError, InvalidInputError } = require('@aliceo2/web-ui'); @@ -71,11 +70,11 @@ describe('\'QueryService\' test suite', () => { $max: null, // 0, 1, 6, 11, 21 }, }; - const emptySqlDataSource = new QueryService(undefined, {}); + const emptySqlDataSource = new QueryService(); describe('\'checkConnection()\' - test suite', () => { it('should reject with error when simple query fails', async () => { - const sqlDataSource = new QueryService(config.mysql); + const sqlDataSource = new QueryService(); sqlDataSource._isAvailable = true; sqlDataSource._pool = { query: sinon.stub().rejects({ @@ -93,7 +92,7 @@ describe('\'QueryService\' test suite', () => { }); it('should do nothing when checking connection with mysql driver and driver returns resolved Promise', async () => { - const sqlDataSource = new QueryService(config.mysql); + const sqlDataSource = new QueryService(); sqlDataSource._isAvailable = false; sqlDataSource._pool = { query: sinon.stub().resolves(), @@ -126,7 +125,7 @@ describe('\'QueryService\' test suite', () => { }); it('should not throw and log error when connection fails and shouldThrow is false', async () => { - const sqlDataSource = new QueryService(config.mysql); + const sqlDataSource = new QueryService(); const logStub = sinon.stub(); sqlDataSource._logger = { errorMessage: logStub, @@ -233,7 +232,7 @@ describe('\'QueryService\' test suite', () => { describe('queryFromFilters() - test suite', () => { it('should throw an error when unable to query(API) due to rejected promise', async () => { - const sqlDataSource = new QueryService(config.mysql); + const sqlDataSource = new QueryService(); sqlDataSource._pool = { query: sinon.stub().rejects({ code: 'ER_ACCESS_DENIED_ERROR', @@ -251,7 +250,7 @@ describe('\'QueryService\' test suite', () => { const query = 'SELECT * FROM `messages` WHERE `timestamp`>=? AND `timestamp`<=? AND `hostname` = ? ' + 'AND NOT(`hostname` = ? AND `hostname` IS NOT NULL) AND `severity` IN (?) ORDER BY `TIMESTAMP` LIMIT 10'; - const sqlDataSource = new QueryService(config.mysql); + const sqlDataSource = new QueryService(); sqlDataSource._pool = { query: sinon.stub().resolves([ { hostname: 'test', severity: 'W' }, @@ -274,7 +273,7 @@ describe('\'QueryService\' test suite', () => { }); it('should log every executed sql query as debug', async () => { - const sqlDataSource = new QueryService(config.mysql); + const sqlDataSource = new QueryService(); sqlDataSource._logger = { debugMessage: sinon.stub(), }; @@ -292,7 +291,7 @@ describe('\'QueryService\' test suite', () => { describe('queryGroupCountLogsBySeverity() - test suite', () => { it(`should successfully return stats when queried for all known severities even if none is some are not returned by data service`, async () => { - const dataService = new QueryService(config.mysql); + const dataService = new QueryService(); dataService._pool = { query: sinon.stub().resolves([ { severity: 'E', 'COUNT(*)': 102 }, @@ -310,9 +309,8 @@ describe('\'QueryService\' test suite', () => { }); it('should throw error if data service throws SQL', async () => { - const dataService = new QueryService(config.mysql); - dataService._pool = - { + const dataService = new QueryService(); + dataService._pool = { query: sinon.stub().rejects({ code: 'ER_ACCESS_DENIED_ERROR', errno: 1045, From 8a4391afbd8fe897ece9b9b14bffe4346f873f73 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Tue, 5 May 2026 11:54:23 +0200 Subject: [PATCH 3/4] Make sure SQL config is not overwritten across test suite Co-authored-by: Copilot --- .../mocha-status-controller.test.js | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/InfoLogger/test/lib/controller/mocha-status-controller.test.js b/InfoLogger/test/lib/controller/mocha-status-controller.test.js index afc23b834..0b797eccc 100644 --- a/InfoLogger/test/lib/controller/mocha-status-controller.test.js +++ b/InfoLogger/test/lib/controller/mocha-status-controller.test.js @@ -19,11 +19,24 @@ const config = require('./../../test-config.js'); const { StatusController } = require('./../../../lib/controller/StatusController.js'); describe('Status Service test suite', () => { - config.mysql = { + const mysqlConfig = { host: 'localhost', port: 6103, database: 'INFOLOGGER', }; + + // Store original config.mysql value to restore later + const originalMysqlConfig = config.mysql; + + before(() => { + config.mysql = mysqlConfig; + }); + + after(() => { + // Restore original config to avoid affecting other tests + config.mysql = originalMysqlConfig; + }); + describe('Creating a new StatusController instance', () => { it('should successfully initialize StatusController', () => { assert.doesNotThrow(() => new StatusController({ hostname: 'localhost', port: 8080 }, {})); @@ -88,7 +101,7 @@ describe('Status Service test suite', () => { ok: false, message: 'Data source is not available', }, }; - const mysql = await statusController._getDataSourceStatus(config.mysql); + const mysql = await statusController._getDataSourceStatus(mysqlConfig); assert.deepStrictEqual(mysql, info); }); @@ -102,7 +115,7 @@ describe('Status Service test suite', () => { isAvailable: true, }; statusController.querySource = dataSource; - const mysql = await statusController._getDataSourceStatus(config.mysql); + const mysql = await statusController._getDataSourceStatus(mysqlConfig); assert.deepStrictEqual(mysql, info); }, ); @@ -124,7 +137,7 @@ describe('Status Service test suite', () => { isAvailable: false, }; statusController.querySource = dataSource; - const mysql = await statusController._getDataSourceStatus(config.mysql); + const mysql = await statusController._getDataSourceStatus(mysqlConfig); assert.deepStrictEqual(mysql, info); }, ); From 06dea50910b3577488871d0a86b2ac1518da0407 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Tue, 5 May 2026 12:09:54 +0200 Subject: [PATCH 4/4] Add logging for better tracing of connreset Co-authored-by: Copilot --- .../test/live-simulator/infoLoggerServer.js | 41 +++++++++++++++---- InfoLogger/test/mocha-index.js | 17 ++++++++ 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/InfoLogger/test/live-simulator/infoLoggerServer.js b/InfoLogger/test/live-simulator/infoLoggerServer.js index 1b736b888..37464e692 100644 --- a/InfoLogger/test/live-simulator/infoLoggerServer.js +++ b/InfoLogger/test/live-simulator/infoLoggerServer.js @@ -26,12 +26,20 @@ const createServer = () => { const port = 6102; // infoLoggerServer default port function connectionListener(client) { - console.log('Client connected'); + console.log('[InfoLogger Server] Client connected at:', new Date().toISOString()); let timer; let currentLogIndex = 0; client.on('close', onClientDisconnect); client.on('end', onClientDisconnect); + client.on('error', (error) => { + console.error('[InfoLogger Server] Client socket error:', error.code, error.message); + console.error('[InfoLogger Server] Error occurred at:', new Date().toISOString()); + if (error.stack) { + console.error('[InfoLogger Server] Stack trace:', error.stack); + } + clearTimeout(timer); + }); sendNextLog(); function sendNextLog() { @@ -84,27 +92,46 @@ const createServer = () => { } function onClientDisconnect() { - console.log('Client disconnected'); + console.log('[InfoLogger Server] Client disconnected at:', new Date().toISOString()); clearTimeout(timer); } } server.on('error', (error) => { - console.error('InfoLogger Server crashed due to:'); - console.trace(error); + console.error('[InfoLogger Server] Server error occurred at:', new Date().toISOString()); + console.error('[InfoLogger Server] Error code:', error.code); + console.error('[InfoLogger Server] Error message:', error.message); + if (error.stack) { + console.error('[InfoLogger Server] Stack trace:'); + console.trace(error); + } + }); + + server.on('close', () => { + console.log('[InfoLogger Server] Server closed at:', new Date().toISOString()); }); server.listen(port, () => { - console.log(`InfoLoggerServer is running on port ${port}`); + console.log(`[InfoLogger Server] InfoLoggerServer is running on port ${port}`); }); return server } const closeServer = (server) => { + console.log('[InfoLogger Server] Closing server at:', new Date().toISOString()); try { - server.close(); + if (server && server.listening) { + server.close((err) => { + if (err) { + console.error('[InfoLogger Server] Error closing server:', err.message); + } else { + console.log('[InfoLogger Server] Server closed successfully'); + } + }); + } } catch (err) { - console.error(err); + console.error('[InfoLogger Server] Exception while closing server:', err.message); + console.error('[InfoLogger Server] Stack:', err.stack); } } diff --git a/InfoLogger/test/mocha-index.js b/InfoLogger/test/mocha-index.js index 2f6a246ae..051a18aa1 100644 --- a/InfoLogger/test/mocha-index.js +++ b/InfoLogger/test/mocha-index.js @@ -41,6 +41,23 @@ describe('InfoLogger', function() { const baseUrl = `http://${config.http.hostname}:${config.http.port}/`; before(async () => { + // Add error handlers for uncaught errors + process.on('unhandledRejection', (error) => { + console.error('[Test Setup] Unhandled Promise Rejection at:', new Date().toISOString()); + console.error('[Test Setup] Error:', error); + if (error && error.stack) { + console.error('[Test Setup] Stack:', error.stack); + } + }); + + process.on('uncaughtException', (error) => { + console.error('[Test Setup] Uncaught Exception at:', new Date().toISOString()); + console.error('[Test Setup] Error:', error); + if (error && error.stack) { + console.error('[Test Setup] Stack:', error.stack); + } + }); + // Start infologger server simulator ilgServer = createServer();