Skip to content

Commit ea7dc5c

Browse files
committed
CCM-12649 Get status healthcheck endpoint
1 parent 64285f2 commit ea7dc5c

File tree

9 files changed

+204
-32
lines changed

9 files changed

+204
-32
lines changed

infrastructure/terraform/components/api/iam_role_api_gateway_execution_role.tf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ data "aws_iam_policy_document" "api_gateway_execution_policy" {
5151
module.authorizer_lambda.function_arn,
5252
module.get_letters.function_arn,
5353
module.patch_letter.function_arn,
54-
module.get_letter_data.function_arn
54+
module.get_letter_data.function_arn,
55+
module.get_status_data.function_arn
5556
]
5657
}
5758
}

infrastructure/terraform/components/api/locals.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ locals {
1111
GET_LETTERS_LAMBDA_ARN = module.get_letters.function_arn
1212
GET_LETTER_DATA_LAMBDA_ARN = module.get_letter_data.function_arn
1313
PATCH_LETTER_LAMBDA_ARN = module.patch_letter.function_arn
14+
GET_STATUS_LAMBDA_ARN = module.get_status.function_arn
1415
})
1516

1617
destination_arn = "arn:aws:logs:${var.region}:${var.shared_infra_account_id}:destination:nhs-main-obs-firehose-logs"
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
module "get_letters" {
2+
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip"
3+
4+
function_name = "get_status"
5+
description = "Healthcheck for service"
6+
7+
aws_account_id = var.aws_account_id
8+
component = var.component
9+
environment = var.environment
10+
project = var.project
11+
region = var.region
12+
group = var.group
13+
14+
log_retention_in_days = var.log_retention_in_days
15+
kms_key_arn = module.kms.key_arn
16+
17+
iam_policy_document = {
18+
body = data.aws_iam_policy_document.get_status_lambda.json
19+
}
20+
21+
function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"]
22+
function_code_base_path = local.aws_lambda_functions_dir_path
23+
function_code_dir = "api-handler/dist"
24+
function_include_common = true
25+
handler_function_name = "getStatus"
26+
runtime = "nodejs22.x"
27+
memory = 128
28+
timeout = 5
29+
log_level = var.log_level
30+
31+
force_lambda_code_deploy = var.force_lambda_code_deploy
32+
enable_lambda_insights = false
33+
34+
send_to_firehose = true
35+
log_destination_arn = local.destination_arn
36+
log_subscription_role_arn = local.acct.log_subscription_role_arn
37+
38+
lambda_env_vars = merge(local.common_lambda_env_vars, {
39+
MAX_LIMIT = var.max_get_limit
40+
})
41+
}
42+
43+
data "aws_iam_policy_document" "get_status_lambda" {
44+
statement {
45+
sid = "KMSPermissions"
46+
effect = "Allow"
47+
48+
actions = [
49+
"kms:Decrypt",
50+
"kms:GenerateDataKey",
51+
]
52+
53+
resources = [
54+
module.kms.key_arn, ## Requires shared kms module
55+
]
56+
}
57+
58+
statement {
59+
sid = "AllowDynamoDBAccess"
60+
effect = "Allow"
61+
62+
actions = [
63+
"dynamodb:DescribeTable"
64+
]
65+
66+
resources = [
67+
aws_dynamodb_table.letters.arn,
68+
"${aws_dynamodb_table.letters.arn}/index/supplierStatus-index"
69+
]
70+
}
71+
72+
73+
statement {
74+
sid = "S3ListBuckets"
75+
actions = ["s3:ListBuckets"]
76+
resources = ["${module.s3bucket_test_letters.arn}/*"]
77+
}
78+
}

infrastructure/terraform/components/api/resources/spec.tmpl.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,39 @@
2323
},
2424
"openapi": "3.0.1",
2525
"paths": {
26+
"/_status": {
27+
"get": {
28+
"operationId": "getStatusId",
29+
"responses": {
30+
"200": {
31+
"description": "Empty body"
32+
},
33+
"500": {
34+
"description": "Server error"
35+
}
36+
},
37+
"security": [
38+
{
39+
"LambdaAuthorizer": []
40+
}
41+
],
42+
"summary": "Healthcheck endpoint",
43+
"x-amazon-apigateway-integration": {
44+
"contentHandling": "CONVERT_TO_TEXT",
45+
"credentials": "${APIG_EXECUTION_ROLE_ARN}",
46+
"httpMethod": "POST",
47+
"passthroughBehavior": "WHEN_NO_TEMPLATES",
48+
"responses": {
49+
".*": {
50+
"statusCode": "200"
51+
}
52+
},
53+
"timeoutInMillis": 29000,
54+
"type": "AWS_PROXY",
55+
"uri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${GET_STATUS_LAMBDA_ARN}/invocations"
56+
}
57+
}
58+
},
2659
"/letters": {
2760
"get": {
2861
"description": "Returns 200 OK with paginated letter ids.",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
2+
import { DBHealthcheck } from "../healthcheck";
3+
import { createTables, DBContext, deleteTables, setupDynamoDBContainer } from "./db";
4+
5+
// Database tests can take longer, especially with setup and teardown
6+
jest.setTimeout(30000);
7+
8+
describe('DBHealthcheck', () => {
9+
10+
let db: DBContext;
11+
12+
beforeAll(async () => {
13+
db = await setupDynamoDBContainer();
14+
});
15+
16+
beforeEach(async () => {
17+
await createTables(db);
18+
});
19+
20+
afterEach(async () => {
21+
await deleteTables(db);
22+
});
23+
24+
it('passes when the database is available', async () => {
25+
const dbHealthCheck = new DBHealthcheck(db.docClient, db.config);
26+
await dbHealthCheck.check();
27+
});
28+
29+
it('fails when the database is unavailable', async () => {
30+
const realFunction = db.docClient.send;
31+
db.docClient.send = jest.fn().mockImplementation(() => { throw new Error('Failed to send')});
32+
33+
const dbHealthCheck = new DBHealthcheck(db.docClient, db.config);
34+
await expect(dbHealthCheck.check()).rejects.toThrow();
35+
36+
db.docClient.send = realFunction;
37+
});
38+
});

internal/datastore/src/healthcheck.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export class DBHealthcheck {
77
readonly config: LetterRepositoryConfig) {}
88

99
async check(): Promise<void> {
10-
this.ddbClient.send(new DescribeTableCommand({
10+
await this.ddbClient.send(new DescribeTableCommand({
1111
TableName: this.config.lettersTableName}));
1212
}
1313
}

lambdas/api-handler/src/config/__tests__/deps.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import type { Deps } from '../deps';
32

43
describe('createDependenciesContainer', () => {
@@ -24,6 +23,7 @@ describe('createDependenciesContainer', () => {
2423
// Repo client
2524
jest.mock('../../../../../internal/datastore', () => ({
2625
LetterRepository: jest.fn(),
26+
DBHealthcheck: jest.fn()
2727
}));
2828

2929
// Env
Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { S3, S3Client } from "@aws-sdk/client-s3";
1+
import { S3Client } from "@aws-sdk/client-s3";
22
import { DBHealthcheck } from "../../../../../internal/datastore/src";
33
import pino from "pino";
44
import { Deps } from "../../config/deps";
@@ -8,30 +8,13 @@ import { Context } from "aws-lambda";
88
import { createGetStatusHandler } from "../get-status";
99

1010
describe('API Lambda handler', () => {
11-
12-
const mockedDeps: jest.Mocked<Deps> = {
13-
s3Client: { send: jest.fn()} as unknown as S3Client,
14-
dbHealthcheck: {check: jest.fn()} as unknown as DBHealthcheck,
15-
logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger,
16-
env: {
17-
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
18-
APIM_CORRELATION_HEADER: 'nhsd-correlation-id'
19-
}
20-
} as Deps;
21-
22-
beforeEach(() => {
23-
jest.clearAllMocks();
24-
jest.resetModules();
25-
});
26-
2711
it('passes if S3 and DynamoDB are available', async() => {
2812

2913
const event = makeApiGwEvent({path: '/_status',
30-
headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'},
31-
pathParameters: {id: 'id1'}
14+
headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId', 'x-request-id': 'requestId'}
3215
});
3316

34-
const getLetterDataHandler = createGetStatusHandler(mockedDeps);
17+
const getLetterDataHandler = createGetStatusHandler(getMockedDeps());
3518
const result = await getLetterDataHandler(event, mockDeep<Context>(), jest.fn());
3619

3720
expect(result).toEqual({
@@ -41,15 +24,29 @@ describe('API Lambda handler', () => {
4124
});
4225

4326
it('fails if S3 is unavailable', async() => {
44-
mockedDeps.s3Client = {
45-
send: jest.fn().mockRejectedValue(new Error('unexpected error'))
46-
} as unknown as S3Client;
27+
const mockedDeps = getMockedDeps();
28+
mockedDeps.s3Client.send = jest.fn().mockRejectedValue(new Error('unexpected error'));
4729

4830
const event = makeApiGwEvent({path: '/_status',
49-
headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'},
50-
pathParameters: {id: 'id1'}
31+
headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId', 'x-request-id': 'requestId'}
5132
});
5233

34+
const getLetterDataHandler = createGetStatusHandler(mockedDeps);
35+
const result = await getLetterDataHandler(event, mockDeep<Context>(), jest.fn());
36+
37+
expect(result).toEqual(expect.objectContaining({
38+
statusCode: 500
39+
}));
40+
});
41+
42+
43+
it('fails if DynamoDB is unavailable', async() => {
44+
const mockedDeps = getMockedDeps();
45+
mockedDeps.dbHealthcheck.check = jest.fn().mockRejectedValue(new Error('unexpected error'));
46+
47+
const event = makeApiGwEvent({path: '/_status',
48+
headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId', 'x-request-id': 'requestId'}
49+
});
5350

5451
const getLetterDataHandler = createGetStatusHandler(mockedDeps);
5552
const result = await getLetterDataHandler(event, mockDeep<Context>(), jest.fn());
@@ -59,4 +56,28 @@ describe('API Lambda handler', () => {
5956
}));
6057
});
6158

59+
it('fails if request ID is absent', async() => {
60+
const event = makeApiGwEvent({path: '/_status',
61+
headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'}
62+
});
63+
64+
const getLetterDataHandler = createGetStatusHandler(getMockedDeps());
65+
const result = await getLetterDataHandler(event, mockDeep<Context>(), jest.fn());
66+
67+
expect(result).toEqual(expect.objectContaining({
68+
statusCode: 500
69+
}));
70+
});
71+
72+
function getMockedDeps(): jest.Mocked<Deps> {
73+
return {
74+
s3Client: { send: jest.fn()} as unknown as S3Client,
75+
dbHealthcheck: {check: jest.fn()} as unknown as DBHealthcheck,
76+
logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger,
77+
env: {
78+
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
79+
APIM_CORRELATION_HEADER: 'nhsd-correlation-id'
80+
}
81+
} as Deps;
82+
}
6283
});

lambdas/api-handler/src/handlers/get-status.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export function createGetStatusHandler(deps: Deps): APIGatewayProxyHandler {
1515
}
1616

1717
try {
18-
deps.dbHealthcheck.check();
19-
s3HealthCheck(deps.s3Client);
18+
await deps.dbHealthcheck.check();
19+
await s3HealthCheck(deps.s3Client);
2020

2121
deps.logger.info({
2222
description: 'Healthcheck passed',
@@ -34,9 +34,9 @@ export function createGetStatusHandler(deps: Deps): APIGatewayProxyHandler {
3434
}
3535

3636

37-
function s3HealthCheck(s3Client: S3Client) {
37+
async function s3HealthCheck(s3Client: S3Client) {
3838
const command: ListBucketsCommand = new ListBucketsCommand({
3939
MaxBuckets: 1
4040
});
41-
s3Client.send(command);
41+
await s3Client.send(command);
4242
}

0 commit comments

Comments
 (0)