Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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 .gitleaksignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ b1f85a7faf54eaf66074d7a6daa093aefe6b3ebe:sdk/python/pyproject.toml:ipv4:25
93e54b6baa390529aab08d9fd956837f7bb3f30:src/src.sln:ipv4:3
493e54b6baa390529aab08d9fd956837f7bb3f30:src/src.sln:ipv4:3
d8aaf7e033bf78fff491caa148897be266b60f67:src/src.sln:ipv4:3
e12407e09151898bfd8d049d57eee9db9977d56b:.github/copilot-instructions.md:generic-api-key:213
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 @@ -53,6 +53,8 @@ data "aws_iam_policy_document" "api_gateway_execution_policy" {
module.get_letter_data.function_arn,
module.get_letters.function_arn,
module.patch_letter.function_arn,
module.get_letter_data.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_MI_LAMBDA_ARN = module.post_mi.function_arn
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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
}

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,4 +1,5 @@
export * from './types';
export * from './mi-repository';
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 Down Expand Up @@ -36,6 +35,7 @@ describe('createDependenciesContainer', () => {
jest.mock('@internal/datastore', () => ({
LetterRepository: jest.fn(),
MIRepository: jest.fn(),
DBHealthcheck: jest.fn()
}));

// Env
Expand Down
18 changes: 14 additions & 4 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, MIRepository } from '../../../../internal/datastore';
import { LetterRepository, MIRepository, DBHealthcheck } from '@internal/datastore';
import { envVars, EnvVars } from "../config/env";

export type Deps = {
s3Client: S3Client;
letterRepo: LetterRepository;
miRepo: MIRepository;
dbHealthcheck: DBHealthcheck;
logger: pino.Logger;
env: EnvVars
};
Expand All @@ -18,15 +19,23 @@ function createDocumentClient(): DynamoDBDocumentClient {
return DynamoDBDocumentClient.from(ddbClient);
}


function createLetterRepository(documentClient: DynamoDBDocumentClient, log: pino.Logger, envVars: EnvVars): LetterRepository {
const ddbClient = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(ddbClient);
const config = {
lettersTableName: envVars.LETTERS_TABLE_NAME,
lettersTtlHours: envVars.LETTER_TTL_HOURS
};

return new LetterRepository(docClient, log, config);
return new LetterRepository(documentClient, log, config);
}

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

return new DBHealthcheck(documentClient, config);
}

function createMIRepository(documentClient: DynamoDBDocumentClient, log: pino.Logger, envVars: EnvVars): MIRepository {
Expand All @@ -49,6 +58,7 @@ export function createDependenciesContainer(): Deps {
s3Client: new S3Client(),
letterRepo: createLetterRepository(documentClient, log, envVars),
miRepo: createMIRepository(documentClient, log, envVars),
dbHealthcheck: createDBHealthcheck(documentClient, 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 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: '{}',
});
});

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;
}
});
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 200 OK with updated resource', 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