Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
e7ea643
Change env var name
francisco-videira-nhs Sep 26, 2025
6f006a7
fix env var name
francisco-videira-nhs Sep 29, 2025
6e5bf07
Merge remote-tracking branch 'origin/main' into feature/CCM-11188_2
francisco-videira-nhs Sep 29, 2025
c94892f
Refactor contracts and mappers
francisco-videira-nhs Oct 2, 2025
094289f
Merge remote-tracking branch 'origin/main' into feature/CCM-11188_2
francisco-videira-nhs Oct 2, 2025
989792f
Fix invalid error message
francisco-videira-nhs Oct 2, 2025
681ee2b
Clean up updateLetterStatus
francisco-videira-nhs Oct 2, 2025
8a8bcc4
Merge remote-tracking branch 'origin/main' into feature/CCM-11188_2
francisco-videira-nhs Oct 3, 2025
3a5da13
patchLetters to patchLetter
francisco-videira-nhs Oct 3, 2025
e2ae5fb
Fix sonar issues
francisco-videira-nhs Oct 3, 2025
520be45
Read headers in lower case
francisco-videira-nhs Oct 6, 2025
813c409
wip
francisco-videira-nhs Oct 6, 2025
0d5b6d6
wip
francisco-videira-nhs Oct 8, 2025
ee5c3ea
Merge remote-tracking branch 'origin/main' into feature/CCM-11603
francisco-videira-nhs Oct 8, 2025
933524f
Add tf and tests
francisco-videira-nhs Oct 8, 2025
a39a042
Merge remote-tracking branch 'origin/main' into feature/CCM-11603
francisco-videira-nhs Oct 8, 2025
40f7580
Fix tf
francisco-videira-nhs Oct 8, 2025
a38c8f8
Fix spec and return code
francisco-videira-nhs Oct 9, 2025
d670dbb
Fix spec
francisco-videira-nhs Oct 9, 2025
b879ed4
Fix spec
francisco-videira-nhs Oct 9, 2025
f4ed8d2
Fix tf
francisco-videira-nhs Oct 9, 2025
f2cf557
Fix export
francisco-videira-nhs Oct 9, 2025
95bc137
Fix location header
francisco-videira-nhs Oct 9, 2025
d1e5da3
Fix tests and tf
francisco-videira-nhs Oct 9, 2025
5bfa8dd
Add aws cli in devcontainer
francisco-videira-nhs Oct 9, 2025
0c80eed
Merge remote-tracking branch 'origin/main' into feature/CCM-11603
francisco-videira-nhs Oct 9, 2025
d93c073
Improve dependency injection
francisco-videira-nhs Oct 13, 2025
957c732
Fix sonar attempt
francisco-videira-nhs Oct 13, 2025
aa36690
Add tests for sonar coverage
francisco-videira-nhs Oct 14, 2025
8fcb28f
Add url ttl as config
francisco-videira-nhs Oct 15, 2025
b7e6fc6
Add peer review suggestions
francisco-videira-nhs Oct 16, 2025
f8881cb
Merge remote-tracking branch 'origin/main' into feature/CCM-11603
francisco-videira-nhs Oct 16, 2025
bfdc5fd
Remove non null assertion from env
francisco-videira-nhs Oct 16, 2025
fbb28c0
Fix lambdas ddb permissions
francisco-videira-nhs Oct 16, 2025
a4c7f26
Add dependency container
francisco-videira-nhs Oct 19, 2025
d37e735
Bump shared lambda module
francisco-videira-nhs Oct 19, 2025
7d9a3f2
Refactor header validation
francisco-videira-nhs Oct 19, 2025
1d0a6a5
Merge remote-tracking branch 'origin/main' into feature/CCM-11603
francisco-videira-nhs Oct 21, 2025
b91e208
Last peer review actions
francisco-videira-nhs Oct 22, 2025
f2928fd
Merge remote-tracking branch 'origin/main' into feature/CCM-11603
francisco-videira-nhs Oct 22, 2025
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
7 changes: 4 additions & 3 deletions infrastructure/terraform/components/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ No requirements.

| Name | Source | Version |
|------|--------|---------|
| <a name="module_authorizer_lambda"></a> [authorizer\_lambda](#module\_authorizer\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip | n/a |
| <a name="module_authorizer_lambda"></a> [authorizer\_lambda](#module\_authorizer\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
| <a name="module_domain_truststore"></a> [domain\_truststore](#module\_domain\_truststore) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-s3bucket.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.20/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_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.20/terraform-lambda.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 |
| <a name="module_s3bucket_test_letters"></a> [s3bucket\_test\_letters](#module\_s3bucket\_test\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-s3bucket.zip | n/a |
| <a name="module_supplier_ssl"></a> [supplier\_ssl](#module\_supplier\_ssl) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-ssl.zip | n/a |
## Outputs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ data "aws_iam_policy_document" "api_gateway_execution_policy" {
resources = [
module.authorizer_lambda.function_arn,
module.get_letters.function_arn,
module.patch_letter.function_arn
module.patch_letter.function_arn,
module.get_letter_data.function_arn
]
}
}
8 changes: 5 additions & 3 deletions infrastructure/terraform/components/api/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ locals {
AWS_REGION = var.region
AUTHORIZER_LAMBDA_ARN = module.authorizer_lambda.function_arn
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
})

destination_arn = "arn:aws:logs:${var.region}:${var.shared_infra_account_id}:destination:nhs-main-obs-firehose-logs"

common_lambda_env_vars = {
LETTERS_TABLE_NAME = aws_dynamodb_table.letters.name,
LETTER_TTL_HOURS = 24,
SUPPLIER_ID_HEADER = "nhsd-supplier-id"
APIM_CORRELATION_HEADER = "nhsd-correlation-id"
LETTER_TTL_HOURS = 12960, # 18 months * 30 days * 24 hours
SUPPLIER_ID_HEADER = "nhsd-supplier-id",
APIM_CORRELATION_HEADER = "nhsd-correlation-id",
DOWNLOAD_URL_TTL_SECONDS = 60
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module "authorizer_lambda" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip"
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip"

aws_account_id = var.aws_account_id
component = var.component
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
module "get_letter_data" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip"

function_name = "get_letter_data"
description = "Get the letter data"

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_letter_data_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 = "getLetterData"
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_letter_data_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:GetItem",
"dynamodb:Query"
]

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

statement {
sid = "S3GetObjectForPresign"
actions = [
"s3:GetObject",
"s3:ListBucket"] # allows 404 response instead of 403 if object missing
resources = ["${module.s3bucket_test_letters.arn}/*"]
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module "get_letters" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip"
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip"

function_name = "get_letters"
description = "Get paginated letter ids"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module "patch_letter" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip"
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip"

function_name = "patch_letter"
description = "Update the status of a letter"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,66 @@
"uri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${PATCH_LETTER_LAMBDA_ARN}/invocations"
}
}
},
"/letters/{id}/data": {
"get": {
"operationId": "getDataId",
"responses": {
"303": {
"description": "See Other",
"headers": {
"Location": {
"description": "The signed S3 URL of the data file to download",
"example": "https://examples3bucket.com/filelocation",
"schema": {
"type": "string"
}
}
}
},
"404": {
"description": "Resource not found"
},
"429": {
"description": "Too many requests"
},
"500": {
"description": "Server error"
}
},
"security": [
{
"LambdaAuthorizer": []
}
],
"summary": "Fetch a data file",
"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_LETTER_DATA_LAMBDA_ARN}/invocations"
}
},
"parameters": [
{
"description": "Unique identifier of this resource",
"in": "path",
"name": "id",
"required": true,
"schema": {
"example": "24L5eYSWGzCHlGmzNxuqVusPxDg",
"type": "string"
}
}
]
}
}
}
1 change: 1 addition & 0 deletions lambdas/api-handler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"esbuild": "^0.24.0"
},
"devDependencies": {
"@aws-sdk/s3-request-presigner": "^3.901.0",
"@tsconfig/node22": "^22.0.2",
"@types/aws-lambda": "^8.10.148",
"@types/jest": "^29.5.14",
Expand Down
68 changes: 68 additions & 0 deletions lambdas/api-handler/src/config/__tests__/deps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@

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

describe('createDependenciesContainer', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();

// pino
jest.mock('pino', () => ({
__esModule: true,
default: jest.fn(() => ({
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
})),
}));

jest.mock('@aws-sdk/client-s3', () => ({
S3Client: jest.fn(),
}));

// Repo client
jest.mock('../../../../../internal/datastore', () => ({
LetterRepository: jest.fn(),
}));

// Env
jest.mock('../env', () => ({
envVars: {
LETTERS_TABLE_NAME: 'LettersTable',
LETTER_TTL_HOURS: 12960,
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
DOWNLOAD_URL_TTL_SECONDS: 60
},
}));
});

test('constructs deps and wires repository config correctly', async () => {
// get current mock instances
const { S3Client } = jest.requireMock('@aws-sdk/client-s3') as { S3Client: jest.Mock };
const pinoMock = jest.requireMock('pino') as { default: jest.Mock };
const { LetterRepository } = jest.requireMock('../../../../../internal/datastore') as { LetterRepository: jest.Mock };

const { createDependenciesContainer } = require('../deps');
const deps: Deps = createDependenciesContainer();

expect(S3Client).toHaveBeenCalledTimes(1);
expect(pinoMock.default).toHaveBeenCalledTimes(1);

expect(LetterRepository).toHaveBeenCalledTimes(1);
const repoCtorArgs = (LetterRepository as jest.Mock).mock.calls[0];
expect(repoCtorArgs[2]).toEqual({
lettersTableName: 'LettersTable',
ttlHours: 12960
});

expect(deps.env).toEqual({
LETTERS_TABLE_NAME: 'LettersTable',
LETTER_TTL_HOURS: 12960,
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
DOWNLOAD_URL_TTL_SECONDS: 60
});
});
});
63 changes: 63 additions & 0 deletions lambdas/api-handler/src/config/__tests__/env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ZodError } from 'zod';

describe('lambdaEnv', () => {
const OLD_ENV = process.env;

beforeEach(() => {
jest.resetModules(); // Clears cached modules
process.env = { ...OLD_ENV }; // Clone original env
});

afterAll(() => {
process.env = OLD_ENV; // Restore
});

it('should load all environment variables successfully', () => {
process.env.SUPPLIER_ID_HEADER = 'nhsd-supplier-id';
process.env.APIM_CORRELATION_HEADER = 'nhsd-correlation-id';
process.env.LETTERS_TABLE_NAME = 'letters-table';
process.env.LETTER_TTL_HOURS = '12960';
process.env.DOWNLOAD_URL_TTL_SECONDS = '60';
process.env.MAX_LIMIT = '2500';

const { envVars } = require('../env');

expect(envVars).toEqual({
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
LETTERS_TABLE_NAME: 'letters-table',
LETTER_TTL_HOURS: 12960,
DOWNLOAD_URL_TTL_SECONDS: 60,
MAX_LIMIT: 2500,
});
});

it('should throw if a required env var is missing', () => {
process.env.SUPPLIER_ID_HEADER = 'nhsd-supplier-id';
process.env.APIM_CORRELATION_HEADER = 'nhsd-correlation-id';
process.env.LETTERS_TABLE_NAME = undefined; // simulate missing var
process.env.LETTER_TTL_HOURS = '12960';
process.env.DOWNLOAD_URL_TTL_SECONDS = '60';

expect(() => require('../env')).toThrow(ZodError);
});

it('should not throw if optional are not set', () => {
process.env.SUPPLIER_ID_HEADER = 'nhsd-supplier-id';
process.env.APIM_CORRELATION_HEADER = 'nhsd-correlation-id';
process.env.LETTERS_TABLE_NAME = 'letters-table';
process.env.LETTER_TTL_HOURS = '12960';
process.env.DOWNLOAD_URL_TTL_SECONDS = '60';

const { envVars } = require('../env');

expect(envVars).toEqual({
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
LETTERS_TABLE_NAME: 'letters-table',
LETTER_TTL_HOURS: 12960,
DOWNLOAD_URL_TTL_SECONDS: 60,
MAX_LIMIT: undefined
});
});
});
36 changes: 36 additions & 0 deletions lambdas/api-handler/src/config/deps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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 { envVars, EnvVars } from "../config/env";

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

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

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

export function createDependenciesContainer(): Deps {

const log = pino();

return {
s3Client: new S3Client(),
letterRepo: createLetterRepository(log, envVars),
logger: log,
env: envVars
};
}
Loading
Loading