Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions infrastructure/terraform/components/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ No requirements.
| <a name="module_get_letter"></a> [get\_letter](#module\_get\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
| <a name="module_get_letter_data"></a> [get\_letter\_data](#module\_get\_letter\_data) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
| <a name="module_get_letters"></a> [get\_letters](#module\_get\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
| <a name="module_get_status"></a> [get\_status](#module\_get\_status) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
| <a name="module_kms"></a> [kms](#module\_kms) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-kms.zip | n/a |
| <a name="module_letter_status_update"></a> [letter\_status\_update](#module\_letter\_status\_update) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
| <a name="module_letter_status_updates_queue"></a> [letter\_status\_updates\_queue](#module\_letter\_status\_updates\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip | n/a |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ data "aws_iam_policy_document" "api_gateway_execution_policy" {
module.get_letters.function_arn,
module.patch_letter.function_arn,
module.post_letters.function_arn,
module.get_status.function_arn,
module.post_mi.function_arn
]
}
Expand Down
1 change: 1 addition & 0 deletions infrastructure/terraform/components/api/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ locals {
GET_LETTER_LAMBDA_ARN = module.get_letter.function_arn
GET_LETTERS_LAMBDA_ARN = module.get_letters.function_arn
GET_LETTER_DATA_LAMBDA_ARN = module.get_letter_data.function_arn
GET_STATUS_LAMBDA_ARN = module.get_status.function_arn
PATCH_LETTER_LAMBDA_ARN = module.patch_letter.function_arn
POST_LETTERS_LAMBDA_ARN = module.post_letters.function_arn
POST_MI_LAMBDA_ARN = module.post_mi.function_arn
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
module "get_status" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip"

function_name = "get_status"
description = "Healthcheck for service"

aws_account_id = var.aws_account_id
component = var.component
environment = var.environment
project = var.project
region = var.region
group = var.group

log_retention_in_days = var.log_retention_in_days
kms_key_arn = module.kms.key_arn

iam_policy_document = {
body = data.aws_iam_policy_document.get_status_lambda.json
}

function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"]
function_code_base_path = local.aws_lambda_functions_dir_path
function_code_dir = "api-handler/dist"
function_include_common = true
handler_function_name = "getStatus"
runtime = "nodejs22.x"
memory = 128
timeout = 5
log_level = var.log_level

force_lambda_code_deploy = var.force_lambda_code_deploy
enable_lambda_insights = false

send_to_firehose = true
log_destination_arn = local.destination_arn
log_subscription_role_arn = local.acct.log_subscription_role_arn

lambda_env_vars = merge(local.common_lambda_env_vars, {})
}

data "aws_iam_policy_document" "get_status_lambda" {
statement {
sid = "KMSPermissions"
effect = "Allow"

actions = [
"kms:Decrypt",
"kms:GenerateDataKey",
]

resources = [
module.kms.key_arn, ## Requires shared kms module
]
}

statement {
sid = "AllowDynamoDBAccess"
effect = "Allow"

actions = [
"dynamodb:DescribeTable"
]

resources = [
aws_dynamodb_table.letters.arn,
"${aws_dynamodb_table.letters.arn}/index/supplierStatus-index"
]
}


statement {
sid = "S3ListAllMyBuckets"
actions = ["s3:ListAllMyBuckets"]
resources = ["arn:aws:s3:::*"]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,34 @@
},
"openapi": "3.0.1",
"paths": {
"/_status": {
"get": {
"operationId": "getStatusId",
"responses": {
"200": {
"description": "OK"
},
"500": {
"description": "Server error"
}
},
"summary": "Healthcheck endpoint",
"x-amazon-apigateway-integration": {
"contentHandling": "CONVERT_TO_TEXT",
"credentials": "${APIG_EXECUTION_ROLE_ARN}",
"httpMethod": "POST",
"passthroughBehavior": "WHEN_NO_TEMPLATES",
"responses": {
".*": {
"statusCode": "200"
}
},
"timeoutInMillis": 29000,
"type": "AWS_PROXY",
"uri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${GET_STATUS_LAMBDA_ARN}/invocations"
}
}
},
"/letters": {
"get": {
"description": "Returns 200 OK with paginated letter ids.",
Expand Down
38 changes: 38 additions & 0 deletions internal/datastore/src/__test__/heathcheck.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { DBHealthcheck } from "../healthcheck";
import { createTables, DBContext, deleteTables, setupDynamoDBContainer } from "./db";

// Database tests can take longer, especially with setup and teardown
jest.setTimeout(30000);

describe('DBHealthcheck', () => {

let db: DBContext;

beforeAll(async () => {
db = await setupDynamoDBContainer();
});

beforeEach(async () => {
await createTables(db);
});

afterEach(async () => {
await deleteTables(db);
});

it('passes when the database is available', async () => {
const dbHealthCheck = new DBHealthcheck(db.docClient, db.config);
await dbHealthCheck.check();
});

it('fails when the database is unavailable', async () => {
const realFunction = db.docClient.send;
db.docClient.send = jest.fn().mockImplementation(() => { throw new Error('Failed to send')});

const dbHealthCheck = new DBHealthcheck(db.docClient, db.config);
await expect(dbHealthCheck.check()).rejects.toThrow();

db.docClient.send = realFunction;
});
});
13 changes: 13 additions & 0 deletions internal/datastore/src/healthcheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DescribeTableCommand } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { LetterRepositoryConfig } from "./letter-repository";

export class DBHealthcheck {
constructor(readonly ddbClient: DynamoDBDocumentClient,
readonly config: LetterRepositoryConfig) {}

async check(): Promise<void> {
await this.ddbClient.send(new DescribeTableCommand({
TableName: this.config.lettersTableName}));
}
}
1 change: 1 addition & 0 deletions internal/datastore/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './types';
export * from './mi-repository';
export * from './letter-repository';
export * from './supplier-repository';
export * from './healthcheck';
export * from './types';
2 changes: 1 addition & 1 deletion lambdas/api-handler/src/config/__tests__/deps.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import type { Deps } from '../deps';

describe('createDependenciesContainer', () => {
Expand Down Expand Up @@ -40,6 +39,7 @@ describe('createDependenciesContainer', () => {
jest.mock('@internal/datastore', () => ({
LetterRepository: jest.fn(),
MIRepository: jest.fn(),
DBHealthcheck: jest.fn()
}));

// Env
Expand Down
14 changes: 13 additions & 1 deletion lambdas/api-handler/src/config/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
import { SQSClient } from "@aws-sdk/client-sqs";
import pino from 'pino';
import { LetterRepository, MIRepository } from '../../../../internal/datastore';
import { LetterRepository, MIRepository, DBHealthcheck } from '@internal/datastore';
import { envVars, EnvVars } from "../config/env";

export type Deps = {
s3Client: S3Client;
sqsClient: SQSClient;
letterRepo: LetterRepository;
miRepo: MIRepository;
dbHealthcheck: DBHealthcheck;
logger: pino.Logger;
env: EnvVars
};
Expand All @@ -20,6 +21,7 @@ function createDocumentClient(): DynamoDBDocumentClient {
return DynamoDBDocumentClient.from(ddbClient);
}


function createLetterRepository(log: pino.Logger, envVars: EnvVars): LetterRepository {

const config = {
Expand All @@ -30,6 +32,15 @@ function createLetterRepository(log: pino.Logger, envVars: EnvVars): LetterRepos
return new LetterRepository(createDocumentClient(), log, config);
}

function createDBHealthcheck(envVars: EnvVars): DBHealthcheck {
const config = {
lettersTableName: envVars.LETTERS_TABLE_NAME,
lettersTtlHours: envVars.LETTER_TTL_HOURS
};

return new DBHealthcheck(createDocumentClient(), config);
}

function createMIRepository(log: pino.Logger, envVars: EnvVars): MIRepository {

const config = {
Expand All @@ -49,6 +60,7 @@ export function createDependenciesContainer(): Deps {
sqsClient: new SQSClient(),
letterRepo: createLetterRepository(log, envVars),
miRepo: createMIRepository(log, envVars),
dbHealthcheck: createDBHealthcheck(envVars),
logger: log,
env: envVars
};
Expand Down
73 changes: 73 additions & 0 deletions lambdas/api-handler/src/handlers/__tests__/get_status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { S3Client } from "@aws-sdk/client-s3";
import { DBHealthcheck } from "@internal/datastore/src";
import pino from "pino";
import { Deps } from "../../config/deps";
import { makeApiGwEvent } from "./utils/test-utils";
import { mockDeep } from "jest-mock-extended";
import { Context } from "aws-lambda";
import { createGetStatusHandler } from "../get-status";

describe('API Lambda handler', () => {
it('passes if S3 and DynamoDB are available', async() => {

const event = makeApiGwEvent({path: '/_status',
headers: undefined
});

const getLetterDataHandler = createGetStatusHandler(getMockedDeps());
const result = await getLetterDataHandler(event, mockDeep<Context>(), jest.fn());

expect(result).toEqual({
statusCode: 200,
body: JSON.stringify({ code: 200 }, null, 2)
});
});

it('fails if S3 is unavailable', async() => {
const mockedDeps = getMockedDeps();
mockedDeps.s3Client.send = jest.fn().mockRejectedValue(new Error('unexpected error'));

const event = makeApiGwEvent({path: '/_status',
headers: undefined
});

const getLetterDataHandler = createGetStatusHandler(mockedDeps);
const result = await getLetterDataHandler(event, mockDeep<Context>(), jest.fn());

expect(result).toEqual({
statusCode: 500,
body: JSON.stringify({ code: 500 }, null, 2)
});
});


it('fails if DynamoDB is unavailable', async() => {
const mockedDeps = getMockedDeps();
mockedDeps.dbHealthcheck.check = jest.fn().mockRejectedValue(new Error('unexpected error'));

const event = makeApiGwEvent({path: '/_status',
headers: {'Nhsd-Correlation-Id': 'correlationId'}
});

const getLetterDataHandler = createGetStatusHandler(mockedDeps);
const result = await getLetterDataHandler(event, mockDeep<Context>(), jest.fn());

expect(result).toEqual({
statusCode: 500,
body: JSON.stringify({ code: 500 }, null, 2)
});
});


function getMockedDeps(): jest.Mocked<Deps> {
return {
s3Client: { send: jest.fn()} as unknown as S3Client,
dbHealthcheck: {check: jest.fn()} as unknown as DBHealthcheck,
logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger,
env: {
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
APIM_CORRELATION_HEADER: 'nhsd-correlation-id'
}
} as Deps;
}
});
22 changes: 11 additions & 11 deletions lambdas/api-handler/src/handlers/__tests__/patch-letter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,17 @@ describe('patchLetter API Handler', () => {
});

const mockedDeps: jest.Mocked<Deps> = {
s3Client: {} as unknown as S3Client,
letterRepo: {} as unknown as LetterRepository,
logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger,
env: {
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
LETTERS_TABLE_NAME: 'LETTERS_TABLE_NAME',
LETTER_TTL_HOURS: 12960,
DOWNLOAD_URL_TTL_SECONDS: 60
} as unknown as EnvVars
} as Deps;
s3Client: {} as unknown as S3Client,
letterRepo: {} as unknown as LetterRepository,
logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger,
env: {
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
LETTERS_TABLE_NAME: 'LETTERS_TABLE_NAME',
LETTER_TTL_HOURS: 12960,
DOWNLOAD_URL_TTL_SECONDS: 60
} as unknown as EnvVars
} as Deps;

it('returns 202 Accepted', async () => {
const event = makeApiGwEvent({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { makeApiGwEvent } from "./utils/test-utils";
import { PostMIRequest, PostMIResponse } from "../../contracts/mi";
import * as miService from '../../services/mi-operations';
import pino from 'pino';
import { MIRepository } from "../../../../../internal/datastore/src";
import { MIRepository } from "@internal/datastore/src";
import { Deps } from "../../config/deps";
import { EnvVars } from "../../config/env";
import { createPostMIHandler } from "../post-mi";
Expand Down
Loading
Loading