Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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_logging_bucket"></a> [logging\_bucket](#module\_logging\_bucket) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-s3bucket.zip | n/a |
| <a name="module_patch_letter"></a> [patch\_letter](#module\_patch\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ data "aws_iam_policy_document" "api_gateway_execution_policy" {
module.get_letter.function_arn,
module.get_letters.function_arn,
module.patch_letter.function_arn,
module.get_letter_data.function_arn
module.get_letter_data.function_arn,
module.get_status.function_arn
]
}
}
1 change: 1 addition & 0 deletions infrastructure/terraform/components/api/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ locals {
GET_LETTERS_LAMBDA_ARN = module.get_letters.function_arn
GET_LETTER_DATA_LAMBDA_ARN = module.get_letter_data.function_arn
PATCH_LETTER_LAMBDA_ARN = module.patch_letter.function_arn
GET_STATUS_LAMBDA_ARN = module.get_status.function_arn
})

destination_arn = "arn:aws:logs:${var.region}:${var.shared_infra_account_id}:destination:nhs-main-obs-firehose-logs"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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, {
MAX_LIMIT = var.max_get_limit
Comment thread
stevebux marked this conversation as resolved.
Outdated
})
}

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,39 @@
},
"openapi": "3.0.1",
"paths": {
"/_status": {
"get": {
"operationId": "getStatusId",
"responses": {
"200": {
"description": "Empty body"
Comment thread
masl2 marked this conversation as resolved.
Outdated
Comment thread
francisco-videira-nhs marked this conversation as resolved.
Outdated
},
Comment thread
masl2 marked this conversation as resolved.
"500": {
"description": "Server error"
}
},
"security": [
{
"LambdaAuthorizer": []
}
],
Comment thread
stevebux marked this conversation as resolved.
Outdated
"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
@@ -1,3 +1,4 @@
export * from './types';
export * from './letter-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 All @@ -24,6 +23,7 @@ describe('createDependenciesContainer', () => {
// Repo client
jest.mock('../../../../../internal/datastore', () => ({
LetterRepository: jest.fn(),
DBHealthcheck: jest.fn()
}));

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

export type Deps = {
s3Client: S3Client;
letterRepo: LetterRepository;
logger: pino.Logger,
dbHealthcheck: DBHealthcheck;
logger: pino.Logger;
env: EnvVars
};

Expand All @@ -23,13 +24,25 @@ function createLetterRepository(log: pino.Logger, envVars: EnvVars): LetterRepos
return new LetterRepository(docClient, log, config);
}

function createDBHealthcheck(envVars: EnvVars): DBHealthcheck {
const ddbClient = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(ddbClient);
const config = {
lettersTableName: envVars.LETTERS_TABLE_NAME,
ttlHours: envVars.LETTER_TTL_HOURS
};

return new DBHealthcheck(docClient, config);
}

export function createDependenciesContainer(): Deps {

const log = pino();

return {
s3Client: new S3Client(),
letterRepo: createLetterRepository(log, envVars),
dbHealthcheck: createDBHealthcheck(envVars),
logger: log,
env: envVars
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { makeApiGwEvent } from './utils/test-utils';
import { ValidationError } from '../../errors';
import * as errors from '../../contracts/errors';
import { createGetLetterDataHandler } from '../get-letter-data';
import { S3Client } from '@aws-sdk/client-s3';
import { Destination, S3Client } from '@aws-sdk/client-s3';
Comment thread
stevebux marked this conversation as resolved.
Outdated
import pino from 'pino';
import { LetterRepository } from '../../../../../internal/datastore/src';
import { EnvVars } from '../../config/env';
Expand All @@ -37,7 +37,7 @@ describe('API Lambda handler', () => {
LETTER_TTL_HOURS: 12960,
DOWNLOAD_URL_TTL_SECONDS: 60
} as unknown as EnvVars
}
} as Deps;

beforeEach(() => {
jest.clearAllMocks();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('API Lambda handler', () => {
DOWNLOAD_URL_TTL_SECONDS: 60,
MAX_LIMIT: 2500
} as unknown as EnvVars
};
} as Deps;

beforeEach(() => {
jest.clearAllMocks();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('API Lambda handler', () => {
DOWNLOAD_URL_TTL_SECONDS: 60,
MAX_LIMIT: 2500
} as unknown as EnvVars
}
} as Deps;

beforeEach(() => {
jest.clearAllMocks();
Expand Down
83 changes: 83 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,83 @@
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: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId', 'x-request-id': 'requestId'}
});

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

expect(result).toEqual({
statusCode: 200,
body: JSON.stringify({}, 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: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId', 'x-request-id': 'requestId'}
});

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

expect(result).toEqual(expect.objectContaining({
statusCode: 500
}));
});


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-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId', 'x-request-id': 'requestId'}
});

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

expect(result).toEqual(expect.objectContaining({
statusCode: 500
}));
});

it('fails if request ID is absent', async() => {
const event = makeApiGwEvent({path: '/_status',
headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'}
});

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

expect(result).toEqual(expect.objectContaining({
statusCode: 500
}));
});

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;
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('patchLetter API Handler', () => {
LETTER_TTL_HOURS: 12960,
DOWNLOAD_URL_TTL_SECONDS: 60
} as unknown as EnvVars
}
} as Deps;

it('returns 200 OK with updated resource', async () => {
const event = makeApiGwEvent({
Expand Down
Loading
Loading