From 3248d8da74a6e29718bc9e0c60443d3cf74dfd4b Mon Sep 17 00:00:00 2001 From: Jason Claxton <30830544+Jozzey@users.noreply.github.com> Date: Mon, 4 Sep 2023 10:57:51 +0100 Subject: [PATCH] Add Redis info to `health/info` page (#402) https://eaflood.atlassian.net/browse/WATER-4099 Currently dummy info is being displayed for the Redis status. This PR will add the functionality to actually test the Redis status. --- .env.example | 5 ++ app/services/health/info.service.js | 18 ++++- config/redis.config.js | 18 +++++ package-lock.json | 50 +++++++++++++ package.json | 1 + test/services/health/info.service.test.js | 88 ++++++++++++++++------- 6 files changed, 153 insertions(+), 27 deletions(-) create mode 100644 config/redis.config.js diff --git a/.env.example b/.env.example index 9d19ab843a..904af0bea2 100644 --- a/.env.example +++ b/.env.example @@ -51,5 +51,10 @@ AWS_ACCESS_KEY_ID=uploadaccesskey AWS_SECRET_ACCESS_KEY=uploadsecretkey AWS_MAINTENANCE_BUCKET=upload-bucket-gov-uk +# Redis config +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= + # Feature flags ENABLE_REISSUING_BILLING_BATCHES=false diff --git a/app/services/health/info.service.js b/app/services/health/info.service.js index 61c6ea45e2..bbae91b4fb 100644 --- a/app/services/health/info.service.js +++ b/app/services/health/info.service.js @@ -9,11 +9,13 @@ const ChildProcess = require('child_process') const util = require('util') const exec = util.promisify(ChildProcess.exec) +const redis = require('@redis/client') const ChargingModuleRequestLib = require('../../lib/charging-module-request.lib.js') const RequestLib = require('../../lib/request.lib.js') const LegacyRequestLib = require('../../lib/legacy-request.lib.js') +const redisConfig = require('../../../config/redis.config.js') const servicesConfig = require('../../../config/services.config.js') /** @@ -62,10 +64,20 @@ async function _getVirusScannerData () { async function _getRedisConnectivityData () { try { - const { stdout, stderr } = await exec('redis-server --version') - return stderr ? `ERROR: ${stderr}` : stdout + const client = redis.createClient({ + socket: { + host: redisConfig.host, + port: redisConfig.port + }, + password: redisConfig.password + }) + + await client.connect() + await client.disconnect() + + return 'Up and running' } catch (error) { - return `ERROR: ${error.message}` + return 'Error connecting to Redis' } } diff --git a/config/redis.config.js b/config/redis.config.js new file mode 100644 index 0000000000..ff37e8df0a --- /dev/null +++ b/config/redis.config.js @@ -0,0 +1,18 @@ +'use strict' + +/** + * Config values used to connect to Redis + * @module RedisConfig + */ + +// We require dotenv directly in each config file to support unit tests that depend on this this subset of config. +// Requiring dotenv in multiple places has no effect on the app when running for real. +require('dotenv').config() + +const config = { + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT, + password: process.env.REDIS_PASSWORD +} + +module.exports = config diff --git a/package-lock.json b/package-lock.json index b2de2db797..72bd63ad77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@hapi/hapi": "^21.3.2", "@hapi/inert": "^7.1.0", "@hapi/vision": "^7.0.2", + "@redis/client": "^1.5.9", "@smithy/node-http-handler": "^2.0.4", "bcryptjs": "^2.4.3", "blipp": "^4.0.2", @@ -1908,6 +1909,19 @@ "node": ">= 8" } }, + "node_modules/@redis/client": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.9.tgz", + "integrity": "sha512-SffgN+P1zdWJWSXBvJeynvEnmnZrYmtKSRW00xl8pOPFOMJjxRR9u0frSxJpPR6Y4V+k54blJjGW7FgxbTI7bQ==", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -3264,6 +3278,14 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -4566,6 +4588,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "engines": { + "node": ">= 4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -9281,6 +9311,16 @@ "fastq": "^1.6.0" } }, + "@redis/client": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.9.tgz", + "integrity": "sha512-SffgN+P1zdWJWSXBvJeynvEnmnZrYmtKSRW00xl8pOPFOMJjxRR9u0frSxJpPR6Y4V+k54blJjGW7FgxbTI7bQ==", + "requires": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + } + }, "@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -10328,6 +10368,11 @@ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "optional": true }, + "cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==" + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -11280,6 +11325,11 @@ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true }, + "generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==" + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", diff --git a/package.json b/package.json index 698f152d55..e5796f4648 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@hapi/hapi": "^21.3.2", "@hapi/inert": "^7.1.0", "@hapi/vision": "^7.0.2", + "@redis/client": "^1.5.9", "@smithy/node-http-handler": "^2.0.4", "bcryptjs": "^2.4.3", "blipp": "^4.0.2", diff --git a/test/services/health/info.service.test.js b/test/services/health/info.service.test.js index f1979c6136..ea0954f88f 100644 --- a/test/services/health/info.service.test.js +++ b/test/services/health/info.service.test.js @@ -15,6 +15,7 @@ const servicesConfig = require('../../../config/services.config.js') // Things we need to stub const ChargingModuleRequestLib = require('../../../app/lib/charging-module-request.lib.js') const LegacyRequestLib = require('../../../app/lib/legacy-request.lib.js') +const redis = require('@redis/client') const RequestLib = require('../../../app/lib/request.lib.js') // Thing under test @@ -40,11 +41,13 @@ describe('Info service', () => { let chargingModuleRequestLibStub let legacyRequestLibStub let requestLibStub + let redisStub beforeEach(() => { chargingModuleRequestLibStub = Sinon.stub(ChargingModuleRequestLib, 'get') legacyRequestLibStub = Sinon.stub(LegacyRequestLib, 'get') requestLibStub = Sinon.stub(RequestLib, 'get') + redisStub = Sinon.stub(redis, 'createClient') // These requests will remain unchanged throughout the tests. We do alter the ones to the AddressFacade and the // water-api (foreground-service) though, which is why they are defined separately in each test. @@ -61,6 +64,8 @@ describe('Info service', () => { chargingModuleRequestLibStub .withArgs('status') .resolves(goodRequestResults.chargingModule) + + // redisStub.returns({ connect: Sinon.fake().resolves(), disconnect: Sinon.fake().resolves() }) }) afterEach(() => { @@ -88,14 +93,10 @@ describe('Info service', () => { stdout: 'ClamAV 9.99.9/26685/Mon Oct 10 08:00:01 2022\n', stderror: null }) - execStub - .withArgs('redis-server --version') - .resolves({ - stdout: 'Redis server v=9.99.9 sha=00000000:0 malloc=jemalloc-5.2.1 bits=64 build=66bd629f924ac924\n', - stderror: null - }) const utilStub = { promisify: Sinon.stub().callsFake(() => execStub) } InfoService = Proxyquire('../../../app/services/health/info.service', { util: utilStub }) + + redisStub.returns({ connect: Sinon.stub().resolves(), disconnect: Sinon.stub().resolves() }) }) it('returns details on each', async () => { @@ -106,16 +107,62 @@ describe('Info service', () => { ]) expect(result.appData).to.have.length(10) + + expect(result.virusScannerData).to.equal('ClamAV 9.99.9/26685/Mon Oct 10 08:00:01 2022\n') + expect(result.redisConnectivityData).to.equal('Up and running') }) }) - describe('when a service we check via the shell', () => { + describe('when Redis', () => { beforeEach(async () => { // In these scenarios everything is hunky-dory so we return 2xx responses from these services requestLibStub .withArgs(`${servicesConfig.addressFacade.url}/address-service/hola`) .resolves(goodRequestResults.addressFacade) legacyRequestLibStub.withArgs('water', 'health/info', false).resolves(goodRequestResults.app) + + const execStub = Sinon + .stub() + .withArgs('clamdscan --version') + .resolves({ + stdout: 'ClamAV 9.99.9/26685/Mon Oct 10 08:00:01 2022\n', + stderror: null + }) + const utilStub = { promisify: Sinon.stub().callsFake(() => execStub) } + InfoService = Proxyquire('../../../app/services/health/info.service', { util: utilStub }) + }) + + describe('is not running', () => { + beforeEach(async () => { + redisStub.returns({ + connect: Sinon.stub().throwsException(new Error('Redis check went boom')), + disconnect: Sinon.stub().resolves() + }) + }) + + it('handles the error and still returns a result for the other services', async () => { + const result = await InfoService.go() + + expect(result).to.include([ + 'virusScannerData', 'redisConnectivityData', 'addressFacadeData', 'chargingModuleData', 'appData' + ]) + expect(result.appData).to.have.length(10) + + expect(result.virusScannerData).to.equal('ClamAV 9.99.9/26685/Mon Oct 10 08:00:01 2022\n') + expect(result.redisConnectivityData).to.equal('Error connecting to Redis') + }) + }) + }) + + describe('when ClamAV', () => { + beforeEach(async () => { + // In these scenarios everything is hunky-dory so we return 2xx responses from these services + requestLibStub + .withArgs(`${servicesConfig.addressFacade.url}/address-service/hola`) + .resolves(goodRequestResults.addressFacade) + legacyRequestLibStub.withArgs('water', 'health/info', false).resolves(goodRequestResults.app) + + redisStub.returns({ connect: Sinon.stub().resolves(), disconnect: Sinon.stub().resolves() }) }) describe('is not running', () => { @@ -129,12 +176,6 @@ describe('Info service', () => { stdout: null, stderr: 'Could not connect to clamd' }) - execStub - .withArgs('redis-server --version') - .resolves({ - stdout: null, - stderr: 'Could not connect to Redis' - }) const utilStub = { promisify: Sinon.stub().callsFake(() => execStub) } InfoService = Proxyquire('../../../app/services/health/info.service', { util: utilStub }) }) @@ -148,7 +189,7 @@ describe('Info service', () => { expect(result.appData).to.have.length(10) expect(result.virusScannerData).to.startWith('ERROR:') - expect(result.redisConnectivityData).to.startWith('ERROR:') + expect(result.redisConnectivityData).to.equal('Up and running') }) }) @@ -160,9 +201,6 @@ describe('Info service', () => { .stub() .withArgs('clamdscan --version') .throwsException(new Error('ClamAV check went boom')) - execStub - .withArgs('redis-server --version') - .throwsException(new Error('Redis check went boom')) const utilStub = { promisify: Sinon.stub().callsFake(() => execStub) } InfoService = Proxyquire('../../../app/services/health/info.service', { util: utilStub }) }) @@ -176,7 +214,7 @@ describe('Info service', () => { expect(result.appData).to.have.length(10) expect(result.virusScannerData).to.startWith('ERROR:') - expect(result.redisConnectivityData).to.startWith('ERROR:') + expect(result.redisConnectivityData).to.equal('Up and running') }) }) }) @@ -191,14 +229,10 @@ describe('Info service', () => { stdout: 'ClamAV 9.99.9/26685/Mon Oct 10 08:00:01 2022\n', stderror: null }) - execStub - .withArgs('redis-server --version') - .resolves({ - stdout: 'Redis server v=9.99.9 sha=00000000:0 malloc=jemalloc-5.2.1 bits=64 build=66bd629f924ac924\n', - stderror: null - }) const utilStub = { promisify: Sinon.stub().callsFake(() => execStub) } InfoService = Proxyquire('../../../app/services/health/info.service', { util: utilStub }) + + redisStub.returns({ connect: Sinon.stub().resolves(), disconnect: Sinon.stub().resolves() }) }) describe('cannot be reached because of a network error', () => { @@ -221,6 +255,9 @@ describe('Info service', () => { expect(result.addressFacadeData).to.startWith('ERROR:') expect(result.appData[0].version).to.startWith('ERROR:') + + expect(result.virusScannerData).to.equal('ClamAV 9.99.9/26685/Mon Oct 10 08:00:01 2022\n') + expect(result.redisConnectivityData).to.equal('Up and running') }) }) @@ -244,6 +281,9 @@ describe('Info service', () => { expect(result.addressFacadeData).to.startWith('ERROR:') expect(result.appData[0].version).to.startWith('ERROR:') + + expect(result.virusScannerData).to.equal('ClamAV 9.99.9/26685/Mon Oct 10 08:00:01 2022\n') + expect(result.redisConnectivityData).to.equal('Up and running') }) }) })