Skip to content

Commit a4c7f26

Browse files
Add dependency container
1 parent fbb28c0 commit a4c7f26

File tree

13 files changed

+302
-306
lines changed

13 files changed

+302
-306
lines changed

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

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

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

4-
describe('getDeps()', () => {
4+
describe('createDependenciesContainer', () => {
55
beforeEach(() => {
66
jest.clearAllMocks();
77
jest.resetModules();
@@ -28,12 +28,12 @@ describe('getDeps()', () => {
2828

2929
// Env
3030
jest.mock('../env', () => ({
31-
lambdaEnv: {
31+
envVars: {
3232
LETTERS_TABLE_NAME: 'LettersTable',
33-
LETTER_TTL_HOURS: '24',
33+
LETTER_TTL_HOURS: 24,
3434
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
3535
APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
36-
DOWNLOAD_URL_TTL_SECONDS: '3600'
36+
DOWNLOAD_URL_TTL_SECONDS: 3600
3737
},
3838
}));
3939
});
@@ -44,8 +44,8 @@ describe('getDeps()', () => {
4444
const pinoMock = jest.requireMock('pino') as { default: jest.Mock };
4545
const { LetterRepository } = jest.requireMock('../../../../../internal/datastore') as { LetterRepository: jest.Mock };
4646

47-
const { getDeps } = require('../deps');
48-
const deps: Deps = getDeps();
47+
const { createDependenciesContainer } = require('../deps');
48+
const deps: Deps = createDependenciesContainer();
4949

5050
expect(S3Client).toHaveBeenCalledTimes(1);
5151
expect(pinoMock.default).toHaveBeenCalledTimes(1);
@@ -59,27 +59,10 @@ describe('getDeps()', () => {
5959

6060
expect(deps.env).toEqual({
6161
LETTERS_TABLE_NAME: 'LettersTable',
62-
LETTER_TTL_HOURS: '24',
62+
LETTER_TTL_HOURS: 24,
6363
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
6464
APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
65-
DOWNLOAD_URL_TTL_SECONDS: '3600'
65+
DOWNLOAD_URL_TTL_SECONDS: 3600
6666
});
6767
});
68-
69-
test('is a singleton (second call returns the same object; constructors not re-run)', async () => {
70-
// get current mock instances
71-
const { S3Client } = jest.requireMock('@aws-sdk/client-s3') as { S3Client: jest.Mock };
72-
const pinoMock = jest.requireMock('pino') as { default: jest.Mock };
73-
const { LetterRepository } = jest.requireMock('../../../../../internal/datastore') as { LetterRepository: jest.Mock };
74-
75-
const { getDeps } = require('../deps');
76-
77-
const first = getDeps();
78-
const second = getDeps();
79-
80-
expect(first).toBe(second);
81-
expect(S3Client).toHaveBeenCalledTimes(1);
82-
expect(LetterRepository).toHaveBeenCalledTimes(1);
83-
expect(pinoMock.default).toHaveBeenCalledTimes(1);
84-
});
8568
});

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { ZodError } from 'zod';
2+
13
describe('lambdaEnv', () => {
24
const OLD_ENV = process.env;
35

@@ -17,14 +19,14 @@ describe('lambdaEnv', () => {
1719
process.env.LETTER_TTL_HOURS = '24';
1820
process.env.DOWNLOAD_URL_TTL_SECONDS = '3600';
1921

20-
const { lambdaEnv } = require('../env');
22+
const { envVars } = require('../env');
2123

22-
expect(lambdaEnv).toEqual({
24+
expect(envVars).toEqual({
2325
SUPPLIER_ID_HEADER: 'x-supplier-id',
2426
APIM_CORRELATION_HEADER: 'x-correlation-id',
2527
LETTERS_TABLE_NAME: 'letters-table',
26-
LETTER_TTL_HOURS: '24',
27-
DOWNLOAD_URL_TTL_SECONDS: '3600'
28+
LETTER_TTL_HOURS: 24,
29+
DOWNLOAD_URL_TTL_SECONDS: 3600
2830
});
2931
});
3032

@@ -35,8 +37,6 @@ describe('lambdaEnv', () => {
3537
process.env.LETTER_TTL_HOURS = '24';
3638
process.env.DOWNLOAD_URL_TTL_SECONDS = '3600';
3739

38-
expect(() => require('../env')).toThrow(
39-
'Missing required env var: LETTERS_TABLE_NAME'
40-
);
40+
expect(() => require('../env')).toThrow(ZodError);
4141
});
4242
});

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

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,34 @@ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
33
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
44
import pino from 'pino';
55
import { LetterRepository } from '../../../../internal/datastore';
6-
import { lambdaEnv, LambdaEnv } from "../config/env";
7-
8-
let singletonDeps: Deps | null = null;
6+
import { envVars, EnvVars } from "../config/env";
97

108
export type Deps = {
119
s3Client: S3Client;
1210
letterRepo: LetterRepository;
1311
logger: pino.Logger,
14-
env: LambdaEnv
12+
env: EnvVars
1513
};
1614

17-
function createLetterRepository(log: pino.Logger, lambdaEnv: LambdaEnv): LetterRepository {
15+
function createLetterRepository(log: pino.Logger, envVars: EnvVars): LetterRepository {
1816
const ddbClient = new DynamoDBClient({});
1917
const docClient = DynamoDBDocumentClient.from(ddbClient);
2018
const config = {
21-
lettersTableName: lambdaEnv.LETTERS_TABLE_NAME,
22-
ttlHours: Number.parseInt(lambdaEnv.LETTER_TTL_HOURS)
19+
lettersTableName: envVars.LETTERS_TABLE_NAME,
20+
ttlHours: envVars.LETTER_TTL_HOURS
2321
};
2422

2523
return new LetterRepository(docClient, log, config);
2624
}
2725

28-
export function getDeps(): Deps {
29-
30-
if (singletonDeps) return singletonDeps;
26+
export function createDependenciesContainer(): Deps {
3127

3228
const log = pino();
3329

34-
singletonDeps = {
30+
return {
3531
s3Client: new S3Client(),
36-
letterRepo: createLetterRepository(log, lambdaEnv),
32+
letterRepo: createLetterRepository(log, envVars),
3733
logger: log,
38-
env: lambdaEnv
34+
env: envVars
3935
};
40-
41-
return singletonDeps;
4236
}
Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,13 @@
1-
export interface LambdaEnv {
2-
SUPPLIER_ID_HEADER: string;
3-
APIM_CORRELATION_HEADER: string;
4-
LETTERS_TABLE_NAME: string;
5-
LETTER_TTL_HOURS: string;
6-
DOWNLOAD_URL_TTL_SECONDS: string;
7-
}
1+
import {z} from 'zod';
82

9-
export const lambdaEnv: LambdaEnv = {
10-
SUPPLIER_ID_HEADER: getEnv('SUPPLIER_ID_HEADER'),
11-
APIM_CORRELATION_HEADER: getEnv('APIM_CORRELATION_HEADER'),
12-
LETTERS_TABLE_NAME: getEnv('LETTERS_TABLE_NAME'),
13-
LETTER_TTL_HOURS: getEnv('LETTER_TTL_HOURS'),
14-
DOWNLOAD_URL_TTL_SECONDS: getEnv('DOWNLOAD_URL_TTL_SECONDS')
15-
};
3+
const EnvVarsSchema = z.object({
4+
SUPPLIER_ID_HEADER: z.string(),
5+
APIM_CORRELATION_HEADER: z.string(),
6+
LETTERS_TABLE_NAME: z.string(),
7+
LETTER_TTL_HOURS: z.coerce.number().int(),
8+
DOWNLOAD_URL_TTL_SECONDS: z.coerce.number().int()
9+
});
1610

17-
function getEnv(name: string): string {
18-
const value = process.env[name];
19-
if (!value) {
20-
throw new Error(`Missing required env var: ${name}`);
21-
}
22-
return value;
23-
}
11+
export type EnvVars = z.infer<typeof EnvVarsSchema>;
12+
13+
export const envVars = EnvVarsSchema.parse(process.env);

lambdas/api-handler/src/handlers/__tests__/get-letter-data.test.ts

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,37 +12,33 @@ mockedMapErrorToResponse.mockReturnValue(expectedErrorResponse);
1212
jest.mock('../../services/letter-operations');
1313
import * as letterService from '../../services/letter-operations';
1414

15-
// mock dependencies
16-
jest.mock("../../config/deps", () => ({ getDeps: jest.fn() }));
17-
import { Deps, getDeps } from "../../config/deps";
18-
const mockedGetDeps = getDeps as jest.Mock<Deps>;
19-
const fakeDeps: jest.Mocked<Deps> = {
20-
s3Client: {} as unknown as S3Client,
21-
letterRepo: {} as unknown as LetterRepository,
22-
logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger,
23-
env: {
24-
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
25-
APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
26-
LETTERS_TABLE_NAME: 'LETTERS_TABLE_NAME',
27-
LETTER_TTL_HOURS: 'LETTER_TTL_HOURS',
28-
DOWNLOAD_URL_TTL_SECONDS: 'DOWNLOAD_URL_TTL_SECONDS'
29-
} as unknown as LambdaEnv
30-
}
31-
mockedGetDeps.mockReturnValue(fakeDeps);
32-
3315
import type { APIGatewayProxyResult, Context } from 'aws-lambda';
3416
import { mockDeep } from 'jest-mock-extended';
3517
import { makeApiGwEvent } from './utils/test-utils';
3618
import { ValidationError } from '../../errors';
3719
import * as errors from '../../contracts/errors';
38-
import { getLetterData } from '../get-letter-data';
20+
import { createGetLetterDataHandler } from '../get-letter-data';
3921
import { S3Client } from '@aws-sdk/client-s3';
4022
import pino from 'pino';
4123
import { LetterRepository } from '../../../../../internal/datastore/src';
42-
import { LambdaEnv } from '../../config/env';
24+
import { EnvVars } from '../../config/env';
25+
import { Deps } from "../../config/deps";
4326

4427
describe('API Lambda handler', () => {
4528

29+
const mockedDeps: jest.Mocked<Deps> = {
30+
s3Client: {} as unknown as S3Client,
31+
letterRepo: {} as unknown as LetterRepository,
32+
logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger,
33+
env: {
34+
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
35+
APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
36+
LETTERS_TABLE_NAME: 'LETTERS_TABLE_NAME',
37+
LETTER_TTL_HOURS: 1,
38+
DOWNLOAD_URL_TTL_SECONDS: 1
39+
} as unknown as EnvVars
40+
}
41+
4642
beforeEach(() => {
4743
jest.clearAllMocks();
4844
});
@@ -59,7 +55,8 @@ describe('API Lambda handler', () => {
5955
const context = mockDeep<Context>();
6056
const callback = jest.fn();
6157

62-
const result = await getLetterData(event, context, callback);
58+
const getLetterDataHandler = createGetLetterDataHandler(mockedDeps);
59+
const result = await getLetterDataHandler(event, context, callback);
6360

6461
expect(result).toEqual({
6562
statusCode: 303,
@@ -77,9 +74,10 @@ describe('API Lambda handler', () => {
7774
const context = mockDeep<Context>();
7875
const callback = jest.fn();
7976

80-
const result = await getLetterData(event, context, callback);
77+
const getLetterDataHandler = createGetLetterDataHandler(mockedDeps);
78+
const result = await getLetterDataHandler(event, context, callback);
8179

82-
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error('The request headers are empty'), undefined, mockedGetDeps().logger);
80+
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error('The request headers are empty'), undefined, mockedDeps.logger);
8381
expect(result).toEqual(expectedErrorResponse);
8482
});
8583

@@ -93,9 +91,10 @@ describe('API Lambda handler', () => {
9391
const context = mockDeep<Context>();
9492
const callback = jest.fn();
9593

96-
const result = await getLetterData(event, context, callback);
94+
const getLetterDataHandler = createGetLetterDataHandler(mockedDeps);
95+
const result = await getLetterDataHandler(event, context, callback);
9796

98-
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error("The request headers don't contain the APIM correlation id"), undefined, mockedGetDeps().logger);
97+
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error("The request headers don't contain the APIM correlation id"), undefined, mockedDeps.logger);
9998
expect(result).toEqual(expectedErrorResponse);
10099
});
101100

@@ -107,9 +106,10 @@ describe('API Lambda handler', () => {
107106
const context = mockDeep<Context>();
108107
const callback = jest.fn();
109108

110-
const result = await getLetterData(event, context, callback);
109+
const getLetterDataHandler = createGetLetterDataHandler(mockedDeps);
110+
const result = await getLetterDataHandler(event, context, callback);
111111

112-
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter), 'correlationId', mockedGetDeps().logger);
112+
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter), 'correlationId', mockedDeps.logger);
113113
expect(result).toEqual(expectedErrorResponse);
114114
});
115115
});

0 commit comments

Comments
 (0)