From de3f0eab794f7c8ce4532817041a39039dd87762 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 7 Sep 2024 14:31:18 +0200 Subject: [PATCH] [OGUI-1550] Fix issues with status service when missing components (#2580) * renames StatusService to StatusController as it deals with HTTP requests * moves StatusController to controller folder * Improves StatusController by: * using naming conventions for private variables * initializing logger in constructor rather than on module import * Fixes bug in which if mysql configuration was missing, live mode would not work on front-end due to incorrect response on status --- InfoLogger/lib/api.js | 12 +- .../StatusController.js} | 92 ++++----- .../mocha-status-controller.test.js | 184 ++++++++++++++++++ .../test/lib/mocha-status-service.test.js | 144 -------------- 4 files changed, 232 insertions(+), 200 deletions(-) rename InfoLogger/lib/{StatusService.js => controller/StatusController.js} (63%) create mode 100644 InfoLogger/test/lib/controller/mocha-status-controller.test.js delete mode 100644 InfoLogger/test/lib/mocha-status-service.test.js diff --git a/InfoLogger/lib/api.js b/InfoLogger/lib/api.js index 2d50a1fd5..e6e704cc3 100644 --- a/InfoLogger/lib/api.js +++ b/InfoLogger/lib/api.js @@ -14,11 +14,11 @@ const { InfoLoggerReceiver, MySQL } = require('@aliceo2/web-ui'); +const { StatusController } = require('./controller/StatusController.js'); const { LiveService } = require('./services/LiveService.js'); const { QueryService } = require('./services/QueryService.js'); const ProfileService = require('./ProfileService.js'); const JsonFileConnector = require('./JSONFileConnector.js'); -const StatusService = require('./StatusService.js'); const { serviceAvailabilityCheck } = require('./middleware/serviceAvailabilityCheck.middleware.js'); @@ -44,9 +44,9 @@ module.exports.attachTo = async (http, ws) => { } const queryController = new QueryController(queryService); - const statusService = new StatusService(config, projPackage, ws); - statusService.setQuerySource(queryService); - statusService.liveSource = liveService; + const statusController = new StatusController(config, projPackage, ws); + statusController.querySource = queryService; + statusController.liveSource = liveService; const jsonDb = new JsonFileConnector(config.dbFile || `${__dirname}/../db.json`); const profileService = new ProfileService(jsonDb); @@ -64,8 +64,8 @@ module.exports.attachTo = async (http, ws) => { { public: true }, ); - http.get('/status/gui', statusService.getILGStatus.bind(statusService), { public: true }); - http.get('/getFrameworkInfo', statusService.frameworkInfo.bind(statusService)); + http.get('/status/gui', statusController.getILGStatus.bind(statusController), { public: true }); + http.get('/getFrameworkInfo', statusController.frameworkInfo.bind(statusController)); http.get('/getUserProfile', (req, res) => profileService.getUserProfile(req, res)); http.get('/getProfile', (req, res) => profileService.getProfile(req, res)); diff --git a/InfoLogger/lib/StatusService.js b/InfoLogger/lib/controller/StatusController.js similarity index 63% rename from InfoLogger/lib/StatusService.js rename to InfoLogger/lib/controller/StatusController.js index 3fec83003..81688f907 100644 --- a/InfoLogger/lib/StatusService.js +++ b/InfoLogger/lib/controller/StatusController.js @@ -12,26 +12,24 @@ * or submit itself to any jurisdiction. */ -const logger = require('@aliceo2/web-ui').LogManager - .getLogger(`${process.env.npm_config_log_label ?? 'ilg'}/status`); +const { LogManager } = require('@aliceo2/web-ui'); /** * Gateway for all calls with regards to the status * of the framework and its dependencies */ -class StatusService { +class StatusController { /** - * Setup StatusService - * @param {JSON} config - of the framework - * @param {JSON} projPackage - package json file + * Setup StatusController for the application which is to provide status of used services + * @param {object} config - of the framework + * @param {object} projPackage - package json file * @param {WebSocket} webSocketServer - instance of the web socket server used by the application */ constructor(config, projPackage, webSocketServer) { - if (!config) { - throw new Error('Empty Framework configuration'); - } - this.config = config; - this.projPackage = projPackage; + this._logger = LogManager.getLogger(`${process.env.npm_config_log_label ?? 'ilg'}/status`); + + this._config = config; + this._projPackage = projPackage; /** * @type {WebSocket} @@ -43,8 +41,8 @@ class StatusService { * Set source of data once enabled * @param {SQLDataSource} querySource - source of data */ - setQuerySource(querySource) { - this.querySource = querySource; + set querySource(querySource) { + this._querySource = querySource; } /** @@ -61,11 +59,10 @@ class StatusService { * @param {Response} res - HTTP Response object */ async getILGStatus(_, res) { - let result = {}; - if (this.projPackage && this.projPackage.version) { - result.version = this.projPackage.version; - } - if (this.config.http) { + let result = { + version: this?._projPackage?.version ?? 'unknown', + }; + if (this._config.http) { const ilg = { status: { ok: true } }; result = Object.assign(result, ilg); } @@ -80,15 +77,13 @@ class StatusService { * @param {Response} res - HTTP Response object */ async frameworkInfo(_, res) { - const result = {}; - result['infoLogger-gui'] = this.getProjectInfo(); + const { infoLoggerServer: ilgServerConfig, mysql: dataSourceConfig } = this._config; + const result = { + 'infoLogger-gui': this._getProjectInfo(), + infoLoggerServer: this._getLiveSourceStatus(ilgServerConfig ?? {}), + mysql: await this._getDataSourceStatus(dataSourceConfig ?? {}), + }; - if (this.config.infoLoggerServer) { - result.infoLoggerServer = this._getLiveSourceStatus(this.config.infoLoggerServer); - } - if (this.config.mysql) { - result.mysql = await this.getDataSourceStatus(this.config.mysql); - } res.status(200).json(result); } @@ -96,13 +91,12 @@ class StatusService { * Build an object containing InfoLogger GUI's information * @returns {object} - information about the application */ - getProjectInfo() { - let info = {}; - if (this.projPackage && this.projPackage.version) { - info.version = this.projPackage.version; - } - if (this.config.http) { - const { http } = this.config; + _getProjectInfo() { + let info = { + version: this?._projPackage?.version ?? 'unknown', + }; + if (this._config.http) { + const { http } = this._config; const ilg = { hostname: http.hostname, port: http.port, status: { ok: true }, name: http.name ?? '' }; info = Object.assign(info, ilg); } @@ -130,31 +124,29 @@ class StatusService { /** * Build object with information and status about data source * @param {object} config used for retrieving data form data source + * @param {string} config.host - host of the data source + * @param {number} config.port - port of the data source + * @param {string} config.database - database name * @returns {object} - information on statue of the data source */ - async getDataSourceStatus(config) { - const mysql = { - host: config.host, - port: config.port, - database: config.database, + async _getDataSourceStatus({ host, port, database }) { + const dataSourceStatus = { + host, + port, + database, }; - if (this.querySource) { + if (this._querySource) { try { - await this.querySource.isConnectionUpAndRunning(); - mysql.status = { ok: true }; + await this._querySource.isConnectionUpAndRunning(); + dataSourceStatus.status = { ok: true }; } catch (error) { - logger.error(error.message || error); - if (error.stack) { - logger.trace(error); - } - mysql.status = { ok: false, message: error.message || error }; + dataSourceStatus.status = { ok: false, message: error.message || error }; } } else { - logger.error('There was no data source set up'); - mysql.status = { ok: false, message: 'There was no data source set up' }; + dataSourceStatus.status = { ok: false, message: 'There was no data source set up' }; } - return mysql; + return dataSourceStatus; } } -module.exports = StatusService; +module.exports.StatusController = StatusController; diff --git a/InfoLogger/test/lib/controller/mocha-status-controller.test.js b/InfoLogger/test/lib/controller/mocha-status-controller.test.js new file mode 100644 index 000000000..96e8c4b9a --- /dev/null +++ b/InfoLogger/test/lib/controller/mocha-status-controller.test.js @@ -0,0 +1,184 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const sinon = require('sinon'); +const assert = require('assert'); +const config = require('./../../test-config.js'); + +const { StatusController } = require('./../../../lib/controller/StatusController.js'); + +describe('Status Service test suite', () => { + config.mysql = { + host: 'localhost', + port: 6103, + database: 'INFOLOGGER', + }; + describe('Creating a new StatusController instance', () => { + it('should successfully initialize StatusController', () => { + assert.doesNotThrow(() => new StatusController({ hostname: 'localhost', port: 8080 }, {})); + }); + }); + + describe('`_getProjectInfo()` tests', () => { + it('should successfully return ilg info even if version is missing', () => { + const statusController = new StatusController(config, undefined); + const info = { + hostname: 'localhost', + port: 8080, + status: { ok: true }, + name: 'TST', + clients: -1, + version: 'unknown', + }; + assert.deepStrictEqual(statusController._getProjectInfo(), info); + }); + + it('should successfully return ilg version even if http configuration is missing', () => { + const statusController = new StatusController({}, { version: '1.9.2' }); + const info = { version: '1.9.2', clients: -1 }; + assert.deepStrictEqual(statusController._getProjectInfo(), info); + }); + + it('should successfully add project version if package.json was provided', () => { + const statusController = new StatusController(config, { version: '1.9.2' }); + const info = { + hostname: 'localhost', port: 8080, status: { ok: true }, version: '1.9.2', name: 'TST', clients: -1, + }; + assert.deepStrictEqual(statusController._getProjectInfo(), info); + }); + }); + + describe('`_getLiveSourceStatus()` tests', () => { + it('should successfully return InfoLogger Server info with status ok false if live source is missing', () => { + const statusController = new StatusController(config, undefined); + const info = { + host: 'localhost', port: 6102, status: { ok: false, message: 'Unable to connect to InfoLogger Server' }, + }; + assert.deepStrictEqual(statusController._getLiveSourceStatus(config.infoLoggerServer), info); + }); + + it('should successfully return InfoLogger Server info with status ok when live source is present', () => { + const statusController = new StatusController(config, undefined); + statusController.liveSource = { isAvailable: true, onconnect: () => true }; + + const info = { host: 'localhost', port: 6102, status: { ok: true } }; + assert.deepStrictEqual(statusController._getLiveSourceStatus(config.infoLoggerServer), info); + }); + }); + + describe('`_getDataSourceStatus()` tests', () => { + it('should successfully return mysql info with status ok false if data source is missing', async () => { + const statusController = new StatusController(config, undefined); + const info = { + host: 'localhost', + port: 6103, + database: 'INFOLOGGER', + status: { + ok: false, message: 'There was no data source set up', + }, + }; + const mysql = await statusController._getDataSourceStatus(config.mysql); + assert.deepStrictEqual(mysql, info); + }); + + it( + 'should successfully return mysql info with status ok true when data source is present and connected', + async () => { + const statusController = new StatusController(config, undefined); + const info = { host: 'localhost', port: 6103, database: 'INFOLOGGER', status: { ok: true } }; + + const dataSource = { + isConnectionUpAndRunning: sinon.stub().resolves(), + }; + statusController.querySource = dataSource; + const mysql = await statusController._getDataSourceStatus(config.mysql); + assert.deepStrictEqual(mysql, info); + }, + ); + + it( + 'should successfully return mysql info with status ok false when data source is present but it is not connected', + async () => { + const statusController = new StatusController(config, undefined); + const info = { + host: 'localhost', + port: 6103, + database: 'INFOLOGGER', + status: { + ok: false, message: 'Could not connect', + }, + }; + + const dataSource = { + isConnectionUpAndRunning: sinon.stub().rejects(new Error('Could not connect')), + }; + statusController.querySource = dataSource; + const mysql = await statusController._getDataSourceStatus(config.mysql); + assert.deepStrictEqual(mysql, info); + }, + ); + }); + + describe('`frameworkInfo()` tests', () => { + it('should successfully send response with built JSON information', async () => { + const statusController = new StatusController(config, undefined); + const res = { + status: sinon.stub().returnsThis(), + json: sinon.stub(), + }; + await statusController.frameworkInfo(undefined, res); + + const info = { + 'infoLogger-gui': { + hostname: 'localhost', + port: 8080, + status: { ok: true }, + name: 'TST', + clients: -1, + version: 'unknown', + }, + mysql: { + host: 'localhost', + port: 6103, + database: 'INFOLOGGER', + status: { + ok: false, message: 'There was no data source set up', + }, + }, + infoLoggerServer: { + host: 'localhost', port: 6102, status: { ok: false, message: 'Unable to connect to InfoLogger Server' }, + }, + }; + + assert.ok(res.status.calledWith(200)); + assert.ok(res.json.calledWith(info)); + }); + }); + + describe('`getILGStatus()` tests', () => { + it('should successfully send response with JSON information about ILG', async () => { + const statusController = new StatusController(config, undefined); + const res = { + status: sinon.stub().returnsThis(), + json: sinon.stub(), + }; + await statusController.getILGStatus(undefined, res); + + const info = { status: { ok: true }, clients: -1, version: 'unknown' }; + + assert.ok(res.status.calledWith(200)); + assert.ok(res.json.calledWith(info)); + }); + }); +}); diff --git a/InfoLogger/test/lib/mocha-status-service.test.js b/InfoLogger/test/lib/mocha-status-service.test.js deleted file mode 100644 index 96756a264..000000000 --- a/InfoLogger/test/lib/mocha-status-service.test.js +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @license - * Copyright 2019-2020 CERN and copyright holders of ALICE O2. - * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. - * All rights not expressly granted are reserved. - * - * This software is distributed under the terms of the GNU General Public - * License v3 (GPL Version 3), copied verbatim in the file "COPYING". - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. -*/ -/* eslint-disable max-len */ - -const sinon = require('sinon'); -const assert = require('assert'); -const config = require('../test-config.js'); - -const StatusService = require('../../lib/StatusService.js'); - -describe('Status Service test suite', () => { - config.mysql = { - host: 'localhost', - port: 6103, - database: 'INFOLOGGER' - }; - describe('Creating a new StatusService instance', () => { - it('should throw an error if configuration object is not provided', () => { - assert.throws(() => new StatusService(), new Error('Empty Framework configuration')); - assert.throws(() => new StatusService(null), new Error('Empty Framework configuration')); - assert.throws(() => new StatusService(undefined), new Error('Empty Framework configuration')); - }); - - it('should successfully initialize StatusService', () => { - assert.doesNotThrow(() => new StatusService({hostname: 'localhost', port: 8080}, {})); - }); - }); - - describe('`getProjectInfo()` tests', () => { - it('should successfully return ilg info even if version is missing', () => { - const statusService = new StatusService(config, undefined); - const info = {hostname: 'localhost', port: 8080, status: {ok: true}, name: 'TST', clients: -1}; - assert.deepStrictEqual(statusService.getProjectInfo(), info); - }); - - it('should successfully return ilg version even if http configuration is missing', () => { - const statusService = new StatusService({}, {version: '1.9.2'}); - const info = {version: '1.9.2', clients: -1}; - assert.deepStrictEqual(statusService.getProjectInfo(), info); - }); - - it('should successfully add project version if package.json was provided', () => { - const statusService = new StatusService(config, {version: '1.9.2'}); - const info = {hostname: 'localhost', port: 8080, status: {ok: true}, version: '1.9.2', name: 'TST', clients: -1}; - assert.deepStrictEqual(statusService.getProjectInfo(), info); - }); - }); - - describe('`getLiveSourceStatus()` tests', () => { - it('should successfully return InfoLogger Server info with status ok false if live source is missing', () => { - const statusService = new StatusService(config, undefined); - const info = {host: 'localhost', port: 6102, status: {ok: false, message: 'Unable to connect to InfoLogger Server'}}; - assert.deepStrictEqual(statusService._getLiveSourceStatus(config.infoLoggerServer), info); - }); - - it('should successfully return InfoLogger Server info with status ok when live source is present', () => { - const statusService = new StatusService(config, undefined); - statusService.liveSource = {isAvailable: true, onconnect: () => true}; - - const info = {host: 'localhost', port: 6102, status: {ok: true}}; - assert.deepStrictEqual(statusService._getLiveSourceStatus(config.infoLoggerServer), info); - }); - }); - - describe('`getDataSourceStatus()` tests', () => { - it('should successfully return mysql info with status ok false if data source is missing', async () => { - const statusService = new StatusService(config, undefined); - const info = {host: 'localhost', port: 6103, database: 'INFOLOGGER', status: {ok: false, message: 'There was no data source set up'}}; - const mysql = await statusService.getDataSourceStatus(config.mysql); - assert.deepStrictEqual(mysql, info); - }); - - it('should successfully return mysql info with status ok true when data source is present and connected', async () => { - const statusService = new StatusService(config, undefined); - const info = {host: 'localhost', port: 6103, database: 'INFOLOGGER', status: {ok: true}}; - - const dataSource = { - isConnectionUpAndRunning: sinon.stub().resolves() - } - statusService.setQuerySource(dataSource); - const mysql = await statusService.getDataSourceStatus(config.mysql); - assert.deepStrictEqual(mysql, info); - }); - - it('should successfully return mysql info with status ok false when data source is present but it is not connected', async () => { - const statusService = new StatusService(config, undefined); - const info = {host: 'localhost', port: 6103, database: 'INFOLOGGER', status: {ok: false, message: 'Could not connect'}}; - - const dataSource = { - isConnectionUpAndRunning: sinon.stub().rejects(new Error('Could not connect')) - } - statusService.setQuerySource(dataSource); - const mysql = await statusService.getDataSourceStatus(config.mysql); - assert.deepStrictEqual(mysql, info); - }); - }); - - describe('`frameworkInfo()` tests', () => { - it('should successfully send response with built JSON information', async () => { - const statusService = new StatusService(config, undefined); - const res = { - status: sinon.stub().returnsThis(), - json: sinon.stub() - } - await statusService.frameworkInfo(undefined, res); - - const info = { - 'infoLogger-gui': {hostname: 'localhost', port: 8080, status: {ok: true}, name: 'TST', clients: -1}, - mysql: {host: 'localhost', port: 6103, database: 'INFOLOGGER', status: {ok: false, message: 'There was no data source set up'}}, - infoLoggerServer: {host: 'localhost', port: 6102, status: {ok: false, message: 'Unable to connect to InfoLogger Server'}} - }; - - assert.ok(res.status.calledWith(200)); - assert.ok(res.json.calledWith(info)); - }); - }); - - describe('`getILGStatus()` tests', () => { - it('should successfully send response with JSON information about ILG', async () => { - const statusService = new StatusService(config, undefined); - const res = { - status: sinon.stub().returnsThis(), - json: sinon.stub() - } - await statusService.getILGStatus(undefined, res); - - const info = {status: {ok: true}, clients: -1}; - - assert.ok(res.status.calledWith(200)); - assert.ok(res.json.calledWith(info)); - }); - }); -});