Skip to content

Commit d93c073

Browse files
Improve dependency injection
1 parent 0c80eed commit d93c073

File tree

15 files changed

+272
-195
lines changed

15 files changed

+272
-195
lines changed

lambdas/api-handler/src/__tests__/index.test.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { S3Client } from "@aws-sdk/client-s3";
2+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
4+
import pino from 'pino';
5+
import { LetterRepository } from '../../../../internal/datastore';
6+
import { lambdaEnv, LambdaEnv } from "../config/env";
7+
8+
const BASE_TEN = 10;
9+
10+
let singletonDeps: Deps | null = null;
11+
12+
export type Deps = {
13+
s3Client: S3Client;
14+
letterRepo: LetterRepository;
15+
logger: pino.Logger,
16+
env: LambdaEnv
17+
};
18+
19+
function createLetterRepository(log: pino.Logger, lambdaEnv: LambdaEnv): LetterRepository {
20+
const ddbClient = new DynamoDBClient({});
21+
const docClient = DynamoDBDocumentClient.from(ddbClient);
22+
const config = {
23+
lettersTableName: lambdaEnv.LETTERS_TABLE_NAME,
24+
ttlHours: parseInt(lambdaEnv.LETTER_TTL_HOURS, BASE_TEN),
25+
};
26+
27+
return new LetterRepository(docClient, log, config);
28+
}
29+
30+
export function getDeps(): Deps {
31+
32+
if (singletonDeps) return singletonDeps;
33+
34+
const log = pino();
35+
36+
singletonDeps = {
37+
s3Client: new S3Client(),
38+
letterRepo: createLetterRepository(log, lambdaEnv),
39+
logger: log,
40+
env: lambdaEnv
41+
};
42+
43+
return singletonDeps;
44+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
}
7+
8+
export const lambdaEnv: LambdaEnv = {
9+
SUPPLIER_ID_HEADER: getEnv('SUPPLIER_ID_HEADER')!,
10+
APIM_CORRELATION_HEADER: getEnv('APIM_CORRELATION_HEADER')!,
11+
LETTERS_TABLE_NAME: getEnv('LETTERS_TABLE_NAME')!,
12+
LETTER_TTL_HOURS: getEnv('LETTER_TTL_HOURS')!
13+
};
14+
15+
function getEnv(name: string, required = true): string | undefined {
16+
const value = process.env[name];
17+
if (!value && required) {
18+
throw new Error(`Missing required env var: ${name}`);
19+
}
20+
return value;
21+
}

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

Lines changed: 0 additions & 17 deletions
This file was deleted.

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

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,44 @@
1-
import type { APIGatewayProxyResult, Context } from 'aws-lambda';
2-
import { mockDeep } from 'jest-mock-extended';
3-
import { makeApiGwEvent } from './utils/test-utils';
4-
import * as letterService from '../../services/letter-operations';
5-
import { mapErrorToResponse } from '../../mappers/error-mapper';
6-
import { ValidationError } from '../../errors';
7-
import * as errors from '../../contracts/errors';
8-
import { getLetterData } from '../get-letter-data';
9-
1+
// mock error mapper
102
jest.mock('../../mappers/error-mapper');
3+
import { mapErrorToResponse } from '../../mappers/error-mapper';
114
const mockedMapErrorToResponse = jest.mocked(mapErrorToResponse);
125
const expectedErrorResponse: APIGatewayProxyResult = {
136
statusCode: 400,
147
body: 'Error'
158
};
169
mockedMapErrorToResponse.mockReturnValue(expectedErrorResponse);
1710

11+
// mock letterService
1812
jest.mock('../../services/letter-operations');
13+
import * as letterService from '../../services/letter-operations';
1914

20-
jest.mock('../../config/lambda-config', () => ({
21-
lambdaConfig: {
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: {
2224
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
23-
APIM_CORRELATION_HEADER: 'nhsd-correlation-id'
24-
}
25-
}));
25+
APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
26+
LETTERS_TABLE_NAME: 'LETTERS_TABLE_NAME',
27+
LETTER_TTL_HOURS: 'LETTER_TTL_HOURS'
28+
} as unknown as LambdaEnv
29+
}
30+
mockedGetDeps.mockReturnValue(fakeDeps);
31+
32+
import type { APIGatewayProxyResult, Context } from 'aws-lambda';
33+
import { mockDeep } from 'jest-mock-extended';
34+
import { makeApiGwEvent } from './utils/test-utils';
35+
import { ValidationError } from '../../errors';
36+
import * as errors from '../../contracts/errors';
37+
import { getLetterData } from '../get-letter-data';
38+
import { S3Client } from '@aws-sdk/client-s3';
39+
import pino from 'pino';
40+
import { LetterRepository } from '../../../../../internal/datastore/src';
41+
import { LambdaEnv } from '../../config/env';
2642

2743
describe('API Lambda handler', () => {
2844

@@ -63,7 +79,7 @@ describe('API Lambda handler', () => {
6379

6480
const result = await getLetterData(event, context, callback);
6581

66-
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error('The request headers are empty'), undefined);
82+
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error('The request headers are empty'), undefined, mockedGetDeps().logger);
6783
expect(result).toEqual(expectedErrorResponse);
6884
});
6985

@@ -79,7 +95,7 @@ describe('API Lambda handler', () => {
7995

8096
const result = await getLetterData(event, context, callback);
8197

82-
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error("The request headers don't contain the APIM correlation id"), undefined);
98+
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error("The request headers don't contain the APIM correlation id"), undefined, mockedGetDeps().logger);
8399
expect(result).toEqual(expectedErrorResponse);
84100
});
85101

@@ -93,7 +109,7 @@ describe('API Lambda handler', () => {
93109

94110
const result = await getLetterData(event, context, callback);
95111

96-
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter), 'correlationId');
112+
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter), 'correlationId', mockedGetDeps().logger);
97113
expect(result).toEqual(expectedErrorResponse);
98114
});
99115
});

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

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,45 @@
1-
import { getLetters } from '../../index';
2-
import type { APIGatewayProxyResult, Context } from 'aws-lambda';
3-
import { mockDeep } from 'jest-mock-extended';
4-
import { makeApiGwEvent } from './utils/test-utils';
5-
import * as letterService from '../../services/letter-operations';
6-
import { mapErrorToResponse } from '../../mappers/error-mapper';
7-
import { getEnvars } from '../get-letters';
8-
import { ValidationError } from '../../errors';
9-
import * as errors from '../../contracts/errors';
10-
1+
// mock dependencies
2+
jest.mock("../../config/deps", () => ({ getDeps: jest.fn() }));
3+
import { Deps, getDeps } from "../../config/deps";
4+
const mockedGetDeps = getDeps as jest.Mock<Deps>;
5+
const fakeDeps: jest.Mocked<Deps> = {
6+
s3Client: {} as unknown as S3Client,
7+
letterRepo: {} as unknown as LetterRepository,
8+
logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger,
9+
env: {
10+
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
11+
APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
12+
LETTERS_TABLE_NAME: 'LETTERS_TABLE_NAME',
13+
LETTER_TTL_HOURS: 'LETTER_TTL_HOURS'
14+
} as unknown as LambdaEnv
15+
}
16+
mockedGetDeps.mockReturnValue(fakeDeps);
17+
18+
// mock error mapper
1119
jest.mock('../../mappers/error-mapper');
20+
import { mapErrorToResponse } from '../../mappers/error-mapper';
1221
const mockedMapErrorToResponse = jest.mocked(mapErrorToResponse);
1322
const expectedErrorResponse: APIGatewayProxyResult = {
1423
statusCode: 400,
1524
body: 'Error'
1625
};
1726
mockedMapErrorToResponse.mockReturnValue(expectedErrorResponse);
1827

28+
//mock letter service
1929
jest.mock('../../services/letter-operations');
30+
import * as letterService from '../../services/letter-operations';
2031

21-
jest.mock('../../config/lambda-config', () => ({
22-
lambdaConfig: {
23-
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
24-
APIM_CORRELATION_HEADER: 'nhsd-correlation-id'
25-
}
26-
}));
32+
import type { APIGatewayProxyResult, Context } from 'aws-lambda';
33+
import { mockDeep } from 'jest-mock-extended';
34+
import { makeApiGwEvent } from './utils/test-utils';
35+
import { getMaxLimit } from '../get-letters';
36+
import { ValidationError } from '../../errors';
37+
import * as errors from '../../contracts/errors';
38+
import { S3Client } from '@aws-sdk/client-s3';
39+
import pino from 'pino';
40+
import { LetterRepository } from '../../../../../internal/datastore/src';
41+
import { LambdaEnv } from '../../config/env';
42+
import { getLetters } from "../get-letters";
2743

2844
describe('API Lambda handler', () => {
2945

@@ -32,6 +48,7 @@ describe('API Lambda handler', () => {
3248
beforeEach(() => {
3349
jest.clearAllMocks();
3450
jest.resetModules();
51+
3552
process.env = { ...originalEnv };
3653
process.env.MAX_LIMIT = '2500';
3754
});
@@ -41,7 +58,7 @@ describe('API Lambda handler', () => {
4158
});
4259

4360
it('uses process.env.MAX_LIMIT for max limit set', async () => {
44-
expect(getEnvars().maxLimit).toBe(2500);
61+
expect(getMaxLimit().maxLimit).toBe(2500);
4562
});
4663

4764
it('returns 200 OK with basic paginated resources', async () => {
@@ -74,6 +91,7 @@ describe('API Lambda handler', () => {
7491
headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'}});
7592
const context = mockDeep<Context>();
7693
const callback = jest.fn();
94+
7795
const result = await getLetters(event, context, callback);
7896

7997
const expected = {
@@ -103,6 +121,7 @@ describe('API Lambda handler', () => {
103121
});
104122

105123
it("returns 400 if the limit parameter is not a number", async () => {
124+
106125
const event = makeApiGwEvent({
107126
path: "/letters",
108127
queryStringParameters: { limit: "1%" },
@@ -113,7 +132,7 @@ describe('API Lambda handler', () => {
113132
const callback = jest.fn();
114133
const result = await getLetters(event, context, callback);
115134

116-
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotANumber), 'correlationId');
135+
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotANumber), 'correlationId', mockedGetDeps().logger);
117136
expect(result).toEqual(expectedErrorResponse);
118137
});
119138

@@ -129,7 +148,7 @@ describe('API Lambda handler', () => {
129148
const result = await getLetters(event, context, callback);
130149

131150
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(
132-
new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [getEnvars().maxLimit] }), 'correlationId');
151+
new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [getMaxLimit().maxLimit] }), 'correlationId', mockedGetDeps().logger);
133152
expect(result).toEqual(expectedErrorResponse);
134153
});
135154

@@ -144,7 +163,7 @@ describe('API Lambda handler', () => {
144163
const result = await getLetters(event, context, callback);
145164

146165
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(
147-
new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [getEnvars().maxLimit] }), 'correlationId');
166+
new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [getMaxLimit().maxLimit] }), 'correlationId', mockedGetDeps().logger);
148167
expect(result).toEqual(expectedErrorResponse);
149168
});
150169

@@ -159,7 +178,7 @@ describe('API Lambda handler', () => {
159178
const result = await getLetters(event, context, callback);
160179

161180
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(
162-
new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [getEnvars().maxLimit] }), 'correlationId');
181+
new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [getMaxLimit().maxLimit] }), 'correlationId', mockedGetDeps().logger);
163182
expect(result).toEqual(expectedErrorResponse);
164183
});
165184

@@ -173,7 +192,7 @@ describe('API Lambda handler', () => {
173192
const callback = jest.fn();
174193
const result = await getLetters(event, context, callback);
175194

176-
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitOnly), 'correlationId');
195+
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitOnly), 'correlationId', mockedGetDeps().logger);
177196
expect(result).toEqual(expectedErrorResponse);
178197
});
179198

@@ -183,7 +202,7 @@ describe('API Lambda handler', () => {
183202
const callback = jest.fn();
184203
const result = await getLetters(event, context, callback);
185204

186-
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error('The request headers are empty'), undefined);
205+
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error('The request headers are empty'), undefined, mockedGetDeps().logger);
187206
expect(result).toEqual(expectedErrorResponse);
188207
});
189208

@@ -197,7 +216,7 @@ describe('API Lambda handler', () => {
197216
const callback = jest.fn();
198217
const result = await getLetters(event, context, callback);
199218

200-
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error("The request headers don't contain the APIM correlation id"), undefined);
219+
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error("The request headers don't contain the APIM correlation id"), undefined, mockedGetDeps().logger);
201220
expect(result).toEqual(expectedErrorResponse);
202221
});
203222
});

0 commit comments

Comments
 (0)