From c7a55aaa7506fde3e929afe7e9cb4663c62ff952 Mon Sep 17 00:00:00 2001 From: William Di Pasquale Date: Wed, 10 Feb 2021 12:42:37 +0000 Subject: [PATCH 1/2] feat: Implement HealthCheck functionality It is a common task to health check an application at start up and force an early failure. The health check logic should be based on LambdaWrapper and extended in each application. See: - [ENG-210] --- src/Service/BaseConfig.service.js | 44 +++++++++++- tests/unit/Service/BaseConfig.service.test.js | 70 ++++++++++++++++++- .../BaseConfig.service.test.js.snap | 24 +++++++ 3 files changed, 136 insertions(+), 2 deletions(-) diff --git a/src/Service/BaseConfig.service.js b/src/Service/BaseConfig.service.js index 5ba377c5..83af0065 100644 --- a/src/Service/BaseConfig.service.js +++ b/src/Service/BaseConfig.service.js @@ -1,7 +1,7 @@ import { S3 } from 'aws-sdk'; import DependencyAwareClass from '../DependencyInjection/DependencyAware.class'; - +import LambdaTermination from '../Wrapper/LambdaTermination'; /** * Error.code for S3 404 errors */ @@ -16,6 +16,15 @@ export const ServiceStates = { INDEFINITELY_PAUSED: 'UNDEFINITELY_PAUSED', }; +/** + * Maps service states to HTTP codes + */ +export const ServiceStatesHttpCodes = { + [ServiceStates.OK]: 200, + [ServiceStates.TEMPORARY_PAUSED]: 409, + [ServiceStates.INDEFINITELY_PAUSED]: 409, +}; + /** * BaseConfigService class * @@ -159,4 +168,37 @@ export default class BaseConfigService extends DependencyAwareClass { ...partialConfig, }); } + + /** + * Performs an health check + * given the currentConfig. + * + * If currentConfig is not supplied + * it uses `getOrCreate` to fetch it. + * + * @param currentConfig + */ + async healthCheck(currentConfig = null) { + const config = currentConfig || await this.getOrCreate(); + + return ServiceStatesHttpCodes[config.state] || 500; + } + + /** + * Ensures that the application is healthy + * or throws a LambdaTermination + * + * @param currentConfig + */ + async ensureHealthy(currentConfig = null) { + const statusCode = await this.healthCheck(currentConfig); + + if (statusCode < 400) { + return statusCode; + } + + const message = 'Application is not healthy.'; + + throw new LambdaTermination(message, statusCode, message, message); + } } diff --git a/tests/unit/Service/BaseConfig.service.test.js b/tests/unit/Service/BaseConfig.service.test.js index 2b79caaf..0f5a8da5 100644 --- a/tests/unit/Service/BaseConfig.service.test.js +++ b/tests/unit/Service/BaseConfig.service.test.js @@ -1,7 +1,7 @@ import { S3 } from 'aws-sdk'; import DependencyInjection from '../../../src/DependencyInjection/DependencyInjection.class'; -import BaseConfigService, { S3_NO_SUCH_KEY_ERROR_CODE } from '../../../src/Service/BaseConfig.service'; +import BaseConfigService, { S3_NO_SUCH_KEY_ERROR_CODE, ServiceStates, ServiceStatesHttpCodes } from '../../../src/Service/BaseConfig.service'; const createAsyncMock = (returnValue) => { const mockedValue = returnValue instanceof Error @@ -182,6 +182,74 @@ const BaseConfigUnitTests = (serviceGenerator: (...args) => BaseConfigService) = await expect(service.patch({ b: 1 })).rejects.toThrowErrorMatchingSnapshot(); }); }); + + describe('healthCheck', () => { + Object.values(ServiceStates).forEach((state) => { + describe(state, () => { + it('Returns the expected HTTP code with the given config', async () => { + const config = { state }; + const service = serviceGenerator(); + const statusCode = await service.healthCheck(config); + const expected = ServiceStatesHttpCodes[state]; + + expect(statusCode).toEqual(expected); + }); + + it('Returns the expected HTTP code with the existing config', async () => { + const config = { state }; + const service = serviceGenerator({ getObject: { Body: JSON.stringify(config) } }); + const statusCode = await service.healthCheck(); + const expected = ServiceStatesHttpCodes[state]; + + expect(statusCode).toEqual(expected); + }); + }); + }); + + describe('Unknown state', () => { + it('Returns 500 with the given config', async () => { + const config = { state: 'Unknown' }; + const service = serviceGenerator(); + const statusCode = await service.healthCheck(config); + const expected = 500; + + expect(statusCode).toEqual(expected); + }); + + it('Returns 500 with the existing config', async () => { + const config = { state: 'Unknown' }; + const service = serviceGenerator({ getObject: { Body: JSON.stringify(config) } }); + const statusCode = await service.healthCheck(); + const expected = 500; + + expect(statusCode).toEqual(expected); + }); + }); + }); + + describe('ensureHealthy', () => { + [200, 201, 202, 204, 300, 301, 399].forEach((statusCode) => { + describe(statusCode, () => { + it('is healthy', async () => { + const service = serviceGenerator(); + jest.spyOn(service, 'healthCheck').mockImplementation(() => Promise.resolve(statusCode)); + + await expect(service.ensureHealthy()).resolves.toEqual(statusCode); + }); + }); + }); + + [400, 401, 403, 404, 409, 499, 500, 501, 502, 503, 504, 'Dante Alighieri'].forEach((statusCode) => { + describe(statusCode, () => { + it('throws a LambdaTermination', async () => { + const service = serviceGenerator(); + jest.spyOn(service, 'healthCheck').mockImplementation(() => Promise.resolve(statusCode)); + + await expect(service.ensureHealthy()).rejects.toThrowErrorMatchingSnapshot(); + }); + }); + }); + }); }; describe('Service/BaseConfigService', () => { diff --git a/tests/unit/Service/__snapshots__/BaseConfig.service.test.js.snap b/tests/unit/Service/__snapshots__/BaseConfig.service.test.js.snap index 2e345a5a..37742866 100644 --- a/tests/unit/Service/__snapshots__/BaseConfig.service.test.js.snap +++ b/tests/unit/Service/__snapshots__/BaseConfig.service.test.js.snap @@ -1,5 +1,29 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Service/BaseConfigService ensureHealthy 400 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`Service/BaseConfigService ensureHealthy 401 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`Service/BaseConfigService ensureHealthy 403 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`Service/BaseConfigService ensureHealthy 404 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`Service/BaseConfigService ensureHealthy 409 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`Service/BaseConfigService ensureHealthy 499 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`Service/BaseConfigService ensureHealthy 500 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`Service/BaseConfigService ensureHealthy 501 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`Service/BaseConfigService ensureHealthy 502 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`Service/BaseConfigService ensureHealthy 503 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`Service/BaseConfigService ensureHealthy 504 throws a LambdaTermination 1`] = `"Application is not healthy."`; + +exports[`Service/BaseConfigService ensureHealthy Dante Alighieri throws a LambdaTermination 1`] = `"Application is not healthy."`; + exports[`Service/BaseConfigService get propagates the 404 1`] = `"404"`; exports[`Service/BaseConfigService get refuses empty configurations 1`] = `"Configuration file is empty"`; From ab2701d25cf3f37dcc16cfdcfa076516ae80a237 Mon Sep 17 00:00:00 2001 From: William Di Pasquale Date: Wed, 10 Feb 2021 14:00:06 +0000 Subject: [PATCH 2/2] Update src/Service/BaseConfig.service.js Co-authored-by: Mohamed Labib --- src/Service/BaseConfig.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/BaseConfig.service.js b/src/Service/BaseConfig.service.js index 83af0065..093db6f9 100644 --- a/src/Service/BaseConfig.service.js +++ b/src/Service/BaseConfig.service.js @@ -170,7 +170,7 @@ export default class BaseConfigService extends DependencyAwareClass { } /** - * Performs an health check + * Performs a health check * given the currentConfig. * * If currentConfig is not supplied