From 57e755427c2d2f12d61420abdc282d651384d159 Mon Sep 17 00:00:00 2001 From: oliverbeumkes-nhs Date: Tue, 23 Dec 2025 16:28:40 +0000 Subject: [PATCH] Reverted health check removal --- .../__tests__/health-check.test.js | 50 +++++ .../src/api/health-check/health-check.js | 22 +++ services/ehr-out-service/src/app.js | 2 + .../get-health-check.integration.test.js | 23 +++ .../services/health-check/get-health-check.js | 19 ++ .../__tests__/health-check.test.js | 187 ++++++++++++++++++ .../src/api/health-check/health-check.js | 22 +++ services/ehr-repo/src/app.js | 3 +- .../__tests__/get-health-check.test.js | 67 +++++++ .../services/health-check/get-health-check.js | 22 +++ services/gp2gp-messenger/src/api/health.js | 15 ++ services/gp2gp-messenger/src/app.js | 2 + .../__tests__/get-health-check.test.js | 27 +++ .../services/health-check/get-health-check.js | 14 ++ 14 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 services/ehr-out-service/src/api/health-check/__tests__/health-check.test.js create mode 100644 services/ehr-out-service/src/api/health-check/health-check.js create mode 100644 services/ehr-out-service/src/services/health-check/__tests__/get-health-check.integration.test.js create mode 100644 services/ehr-out-service/src/services/health-check/get-health-check.js create mode 100644 services/ehr-repo/src/api/health-check/__tests__/health-check.test.js create mode 100644 services/ehr-repo/src/api/health-check/health-check.js create mode 100644 services/ehr-repo/src/services/health-check/__tests__/get-health-check.test.js create mode 100644 services/ehr-repo/src/services/health-check/get-health-check.js create mode 100644 services/gp2gp-messenger/src/api/health.js create mode 100644 services/gp2gp-messenger/src/services/health-check/__tests__/get-health-check.test.js create mode 100644 services/gp2gp-messenger/src/services/health-check/get-health-check.js diff --git a/services/ehr-out-service/src/api/health-check/__tests__/health-check.test.js b/services/ehr-out-service/src/api/health-check/__tests__/health-check.test.js new file mode 100644 index 00000000..55c03abc --- /dev/null +++ b/services/ehr-out-service/src/api/health-check/__tests__/health-check.test.js @@ -0,0 +1,50 @@ +import request from 'supertest'; +import { getHealthCheck } from '../../../services/health-check/get-health-check'; +import { logInfo, logError, logDebug } from '../../../middleware/logging'; +import { buildTestApp } from '../../../__builders__/test-app'; +import { healthCheck } from '../health-check'; + +jest.mock('../../../middleware/logging'); +jest.mock('../../../services/health-check/get-health-check'); + +describe('GET /health', () => { + const testApp = buildTestApp('/health', healthCheck); + + it('should return HTTP status code 200', async () => { + getHealthCheck.mockResolvedValue(expectedHealthCheckBase()); + const res = await request(testApp).get('/health'); + + expect(res.statusCode).toBe(200); + expect(res.body).toEqual(expectedHealthCheckBase()); + expect(logDebug).toHaveBeenCalledWith('Health check completed'); + }); + + it('should return 500 if getHealthCheck if it cannot provide a health check', async () => { + getHealthCheck.mockRejectedValue('some-error'); + const res = await request(testApp).get('/health'); + + expect(res.statusCode).toBe(500); + expect(logInfo).not.toHaveBeenCalled(); + expect(logError).toHaveBeenCalledWith('Health check error', 'some-error'); + }); +}); + +const expectedHealthCheckBase = (db_writable = true, db_connected = true) => ({ + details: { + database: getExpectedDatabase(db_writable, db_connected) + } +}); + +const getExpectedDatabase = (isWritable, isConnected) => { + const baseConf = { + connection: isConnected, + writable: isWritable + }; + + return !isWritable + ? { + ...baseConf, + error: 'some-error' + } + : baseConf; +}; diff --git a/services/ehr-out-service/src/api/health-check/health-check.js b/services/ehr-out-service/src/api/health-check/health-check.js new file mode 100644 index 00000000..82362cf1 --- /dev/null +++ b/services/ehr-out-service/src/api/health-check/health-check.js @@ -0,0 +1,22 @@ +import express from 'express'; +import { getHealthCheck } from '../../services/health-check/get-health-check'; +import { logError, logDebug } from '../../middleware/logging'; + +export const healthCheck = express.Router(); + +healthCheck.get('/', async (req, res, next) => { + try { + const status = await getHealthCheck(); + + if (status.details.database) { + logDebug('Health check completed'); + res.status(200).json(status); + } else { + logError('Health check failed', status); + res.status(503).json(status); + } + } catch (err) { + logError('Health check error', err); + next(err); + } +}); diff --git a/services/ehr-out-service/src/app.js b/services/ehr-out-service/src/app.js index 78e7166e..24248e5c 100644 --- a/services/ehr-out-service/src/app.js +++ b/services/ehr-out-service/src/app.js @@ -3,12 +3,14 @@ import swaggerUi from 'swagger-ui-express'; import { middleware } from './middleware/logging'; import { registrationRequests } from './api/registration-request'; +import { healthCheck } from './api/health-check/health-check'; import swaggerDocument from './swagger.json'; const app = express(); app.use(express.json()); app.use('/swagger', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); +app.use('/health', middleware, healthCheck); app.use('/registration-requests', middleware, registrationRequests); // eslint-disable-next-line no-unused-vars diff --git a/services/ehr-out-service/src/services/health-check/__tests__/get-health-check.integration.test.js b/services/ehr-out-service/src/services/health-check/__tests__/get-health-check.integration.test.js new file mode 100644 index 00000000..a0671c42 --- /dev/null +++ b/services/ehr-out-service/src/services/health-check/__tests__/get-health-check.integration.test.js @@ -0,0 +1,23 @@ +import { getHealthCheck } from '../get-health-check'; +import { config } from '../../../config'; + +describe('getHealthCheck', () => { + const { nhsEnvironment } = config(); + + it('should return static health check object', async () => { + const expected = { + version: '1', + description: 'Health of ehr-out-service', + nhsEnvironment: nhsEnvironment, + details: { + database: { + type: 'dynamodb' + } + } + }; + + const actual = await getHealthCheck(); + + expect(actual).toMatchObject(expected); + }); +}); diff --git a/services/ehr-out-service/src/services/health-check/get-health-check.js b/services/ehr-out-service/src/services/health-check/get-health-check.js new file mode 100644 index 00000000..9a52f2a4 --- /dev/null +++ b/services/ehr-out-service/src/services/health-check/get-health-check.js @@ -0,0 +1,19 @@ +import { config } from '../../config'; +import { EhrTransferTracker } from '../database/dynamodb/dynamo-ehr-transfer-tracker'; + +export const getHealthCheck = async () => { + const { nhsEnvironment } = config(); + const db = EhrTransferTracker.getInstance(); + + return { + version: '1', + description: 'Health of ehr-out-service', + nhsEnvironment: nhsEnvironment, + details: { + database: { + type: 'dynamodb', + status: `is tableName configured: ${db.tableName !== undefined}` + } + } + }; +}; diff --git a/services/ehr-repo/src/api/health-check/__tests__/health-check.test.js b/services/ehr-repo/src/api/health-check/__tests__/health-check.test.js new file mode 100644 index 00000000..250195c5 --- /dev/null +++ b/services/ehr-repo/src/api/health-check/__tests__/health-check.test.js @@ -0,0 +1,187 @@ +import app from '../../../app'; +import request from 'supertest'; +import { getHealthCheck } from '../../../services/health-check/get-health-check'; +import { logInfo, logError } from '../../../middleware/logging'; + +jest.mock('../../../config/logging'); +jest.mock('../../../services/health-check/get-health-check'); +jest.mock('../../../middleware/logging'); + +const mockErrorResponse = 'some-error'; + +describe('GET /health', () => { + describe('all dependencies are available', () => { + beforeEach(() => { + getHealthCheck.mockReturnValue(Promise.resolve(expectedHealthCheckBase())); + }); + + it('should return HTTP status code 200', (done) => { + request(app).get('/health').expect(200).end(done); + }); + + it('should return details of the response from getHealthCheck', (done) => { + request(app) + .get('/health') + .expect((res) => { + expect(res.body).toEqual(expectedHealthCheckBase()); + }) + .end(done); + }); + + it('should call health check service with no parameters', (done) => { + request(app) + .get('/health') + .expect(() => { + expect(getHealthCheck).toHaveBeenCalledTimes(1); + }) + .end(done); + }); + + it('should call logInfo with result when all dependencies are ok', (done) => { + request(app) + .get('/health') + .expect(() => { + expect(logInfo).toHaveBeenCalledWith('Health check successful'); + }) + .end(done); + }); + }); + + describe('S3 is not writable', () => { + beforeEach(() => { + getHealthCheck.mockReturnValue(Promise.resolve(expectedHealthCheckBase(false))); + }); + + it('should return 503 status if s3 writable is false', (done) => { + request(app).get('/health').expect(503).end(done); + }); + + it('should return details of the response from getHealthCheck when s3 writable is false', (done) => { + request(app) + .get('/health') + .expect((res) => { + expect(res.body).toEqual(expectedHealthCheckBase(false)); + }) + .end(done); + }); + + it('should call logError with the health check result if s3 writable is false', (done) => { + request(app) + .get('/health') + .expect(() => { + expect(logError).toHaveBeenCalledWith( + 'Health check failed', + expectedHealthCheckBase(false) + ); + }) + .end(done); + }); + }); + + describe('s3 is not available', () => { + beforeEach(() => { + getHealthCheck.mockReturnValue(Promise.resolve(expectedHealthCheckBase(true, false))); + }); + + it('should return 503 status if s3 available is false', (done) => { + request(app).get('/health').expect(503).end(done); + }); + + it('should return details of the response from getHealthCheck when the s3 available is false', (done) => { + request(app) + .get('/health') + .expect((res) => { + expect(res.body).toEqual(expectedHealthCheckBase(true, false)); + }) + .end(done); + }); + + it('should call logError with the health check result if s3 available is false', (done) => { + request(app) + .get('/health') + .expect(() => { + expect(logError).toHaveBeenCalledWith( + 'Health check failed', + expectedHealthCheckBase(true, false) + ); + }) + .end(done); + }); + }); + + describe('s3 is not available', () => { + beforeEach(() => { + getHealthCheck.mockReturnValue(Promise.resolve(expectedHealthCheckBase(false, false))); + }); + + it('should return 503 if s3 is not writable', (done) => { + request(app).get('/health').expect(503).end(done); + }); + + it('should call logError with the health check result if s3 is not writable', (done) => { + request(app) + .get('/health') + .expect(() => { + expect(logError).toHaveBeenCalledWith( + 'Health check failed', + expectedHealthCheckBase(false, false) + ); + }) + .end(done); + }); + }); + + describe('getHealthCheck throws error', () => { + beforeEach(() => { + getHealthCheck.mockRejectedValue(Error('some-error')); + }); + + it('should return 500 if getHealthCheck if it cannot provide a health check', (done) => { + request(app).get('/health').expect(500).end(done); + }); + + it('should logError if getHealthCheck throws an error', (done) => { + request(app) + .get('/health') + .expect(() => { + expect(logError).toHaveBeenCalledTimes(1); + expect(logError).toHaveBeenCalledWith('Health check error', expect.anything()); + }) + .end(done); + }); + + it('should update the log event for any unexpected error', (done) => { + getHealthCheck.mockReturnValue(Promise.resolve(expectedHealthCheckBase(false))); + + request(app) + .get('/health') + .expect(() => { + expect(logError).toHaveBeenCalledTimes(1); + expect(logError).toHaveBeenCalledWith( + 'Health check failed', + expectedHealthCheckBase(false) + ); + }) + .end(done); + }); + }); +}); + +const expectedS3Base = (isWritable, isConnected) => { + const s3Base = { + available: isConnected, + writable: isWritable + }; + return !isWritable + ? { + ...s3Base, + error: mockErrorResponse + } + : s3Base; +}; + +const expectedHealthCheckBase = (s3_writable = true, s3_connected = true) => ({ + details: { + filestore: expectedS3Base(s3_writable, s3_connected) + } +}); diff --git a/services/ehr-repo/src/api/health-check/health-check.js b/services/ehr-repo/src/api/health-check/health-check.js new file mode 100644 index 00000000..825e9993 --- /dev/null +++ b/services/ehr-repo/src/api/health-check/health-check.js @@ -0,0 +1,22 @@ +import express from 'express'; +import { logError, logInfo } from '../../middleware/logging'; +import { getHealthCheck } from '../../services/health-check/get-health-check'; + +export const healthCheck = express.Router(); + +healthCheck.get('/', (req, res, next) => { + getHealthCheck() + .then((status) => { + if (status.details.filestore.writable && status.details.filestore.available) { + logInfo('Health check successful'); + res.status(200).json(status); + } else { + logError('Health check failed', status); + res.status(503).json(status); + } + }) + .catch((err) => { + logError('Health check error', err); + next(err); + }); +}); diff --git a/services/ehr-repo/src/app.js b/services/ehr-repo/src/app.js index 945b5388..6fe8738e 100644 --- a/services/ehr-repo/src/app.js +++ b/services/ehr-repo/src/app.js @@ -9,6 +9,7 @@ import { options } from './config/logging'; import * as logging from './middleware/logging'; import swaggerDocument from './swagger.json'; import helmet from 'helmet'; +import { healthCheck } from './api/health-check/health-check'; httpContext.enable(); @@ -24,7 +25,7 @@ app.use( }) ); app.use(requestLogger(options)); - +app.use('/health', logging.middleware, healthCheck); app.use('/patients', logging.middleware, patients); app.use('/messages', logging.middleware, messages); app.use('/fragments', logging.middleware, fragments); diff --git a/services/ehr-repo/src/services/health-check/__tests__/get-health-check.test.js b/services/ehr-repo/src/services/health-check/__tests__/get-health-check.test.js new file mode 100644 index 00000000..38bf6e13 --- /dev/null +++ b/services/ehr-repo/src/services/health-check/__tests__/get-health-check.test.js @@ -0,0 +1,67 @@ +import { getHealthCheck } from '../get-health-check'; +import { S3 } from 'aws-sdk'; +import { initializeConfig } from '../../../config'; + +jest.mock('aws-sdk'); + +describe('getHealthCheck', () => { + const config = initializeConfig(); + const mockHeadBucket = jest.fn().mockImplementation((config, callback) => callback()); + const mockPutObjectPromise = jest.fn(); + const mockPutObject = jest.fn().mockImplementation(() => ({ promise: mockPutObjectPromise })); + const error = 'some-error'; + + beforeEach(() => { + S3.mockImplementation(() => ({ + putObject: mockPutObject, + headBucket: mockHeadBucket + })); + }); + + it('should return successful s3 health check if s3 succeeds', () => { + // when + mockPutObjectPromise.mockReturnValueOnce(Promise.resolve()); + return getHealthCheck().then((result) => { + const s3 = result.details.filestore; + // then + expect(s3).toEqual({ + type: 's3', + bucketName: config.awsS3BucketName, + available: true, + writable: true + }); + }); + }); + + it('should return failed s3 health check if s3 returns an error', () => { + // when + mockPutObjectPromise.mockRejectedValue(error); + return getHealthCheck().then((result) => { + const s3 = result.details.filestore; + // then + return expect(s3).toEqual({ + type: 's3', + bucketName: config.awsS3BucketName, + available: true, + writable: false, + error: error + }); + }); + }); + + it('should return available false if s3 can be connected ', () => { + mockHeadBucket.mockImplementation((config, callback) => callback(error)); + + return getHealthCheck().then((result) => { + const s3 = result.details.filestore; + + return expect(s3).toEqual({ + type: 's3', + bucketName: config.awsS3BucketName, + available: false, + writable: false, + error: error + }); + }); + }); +}); diff --git a/services/ehr-repo/src/services/health-check/get-health-check.js b/services/ehr-repo/src/services/health-check/get-health-check.js new file mode 100644 index 00000000..61d6f9db --- /dev/null +++ b/services/ehr-repo/src/services/health-check/get-health-check.js @@ -0,0 +1,22 @@ +import { S3Service } from '../storage'; +import { logInfo } from '../../middleware/logging'; +import { initializeConfig } from '../../config'; + +export function getHealthCheck() { + const config = initializeConfig(); + logInfo('Starting health check'); + + const s3Service = new S3Service(); + + return s3Service.checkS3Health().then((s3HealthCheckResult) => { + logInfo('Health check status', s3HealthCheckResult); + return { + version: '1', + description: 'Health of the EHR Repo S3 Bucket', + nhsEnvironment: config.nhsEnvironment, + details: { + filestore: s3HealthCheckResult + } + }; + }); +} diff --git a/services/gp2gp-messenger/src/api/health.js b/services/gp2gp-messenger/src/api/health.js new file mode 100644 index 00000000..695ba538 --- /dev/null +++ b/services/gp2gp-messenger/src/api/health.js @@ -0,0 +1,15 @@ +import express from 'express'; +import { logInfo } from '../middleware/logging'; +import { getHealthCheck } from '../services/health-check/get-health-check'; + +const router = express.Router(); + +// eslint-disable-next-line no-unused-vars +router.get('/', (req, res, next) => { + getHealthCheck().then(status => { + logInfo('Health check successful'); + res.status(200).send(status); + }); +}); + +export default router; diff --git a/services/gp2gp-messenger/src/app.js b/services/gp2gp-messenger/src/app.js index c7f6eb45..34e42fe2 100644 --- a/services/gp2gp-messenger/src/app.js +++ b/services/gp2gp-messenger/src/app.js @@ -9,6 +9,7 @@ import { options } from './config/logging'; import * as logging from './middleware/logging'; import swaggerDocument from './swagger.json'; import helmet from 'helmet'; +import healthCheck from './api/health'; const app = express(); @@ -21,6 +22,7 @@ app.use( maxAge: 31536000 }) ); +app.use('/health', logging.middleware, healthCheck); app.use('/error', logging.middleware, error); app.use('/swagger', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); app.use('/patient-demographics', logging.middleware, patientDemographicsRouter); diff --git a/services/gp2gp-messenger/src/services/health-check/__tests__/get-health-check.test.js b/services/gp2gp-messenger/src/services/health-check/__tests__/get-health-check.test.js new file mode 100644 index 00000000..12a36569 --- /dev/null +++ b/services/gp2gp-messenger/src/services/health-check/__tests__/get-health-check.test.js @@ -0,0 +1,27 @@ +import { getHealthCheck } from '../get-health-check'; + +jest.mock('../../../config/logging'); +jest.mock('../../../middleware/logging'); + +describe('get-health-check', () => { + describe('getHealthCheck', () => { + it('should resolve when both checks are ok', () => { + return getHealthCheck().then(result => { + return expect(result).toStrictEqual(expectedHealthCheckBase(true)); + }); + }); + + it('should resolve when MHS is not ok', () => { + return getHealthCheck().then(result => { + return expect(result).toStrictEqual(expectedHealthCheckBase(false)); + }); + }); + }); +}); + +const expectedHealthCheckBase = () => ({ + version: '1', + description: 'Health of GP2GP Messenger service', + node_env: process.env.NHS_ENVIRONMENT, + details: {} +}); diff --git a/services/gp2gp-messenger/src/services/health-check/get-health-check.js b/services/gp2gp-messenger/src/services/health-check/get-health-check.js new file mode 100644 index 00000000..5630543f --- /dev/null +++ b/services/gp2gp-messenger/src/services/health-check/get-health-check.js @@ -0,0 +1,14 @@ +import { logInfo } from '../../middleware/logging'; + +export function getHealthCheck() { + logInfo('Starting health check'); + + return Promise.resolve().then(() => { + return { + version: '1', + description: 'Health of GP2GP Messenger service', + node_env: process.env.NHS_ENVIRONMENT, + details: {} + }; + }); +}