Skip to content

Commit e2e199b

Browse files
CCM-12875: Merging in mian
1 parent b7537f2 commit e2e199b

File tree

15 files changed

+1374
-0
lines changed

15 files changed

+1374
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { baseJestConfig } from '../../jest.config.base';
2+
3+
const config = baseJestConfig;
4+
5+
config.coveragePathIgnorePatterns = ['/__tests__/', 'cli.ts'];
6+
config.coverageThreshold = {
7+
global: {
8+
branches: 90,
9+
functions: 100,
10+
lines: 90,
11+
statements: -10,
12+
},
13+
};
14+
15+
export default config;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"dependencies": {
3+
"@aws-sdk/lib-dynamodb": "^3.908.0",
4+
"axios": "^1.13.2",
5+
"digital-letters-events": "^0.0.1",
6+
"utils": "^0.0.1",
7+
"zod": "^4.1.12"
8+
},
9+
"devDependencies": {
10+
"@tsconfig/node22": "^22.0.2",
11+
"@types/aws-lambda": "^8.10.155",
12+
"@types/jest": "^29.5.14",
13+
"jest": "^29.7.0",
14+
"typescript": "^5.9.3"
15+
},
16+
"name": "nhs-notify-digital-letters-pdm-upload-lambda",
17+
"private": true,
18+
"scripts": {
19+
"lambda-build": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts",
20+
"lint": "eslint .",
21+
"lint:fix": "eslint . --fix",
22+
"test:unit": "jest",
23+
"typecheck": "tsc --noEmit"
24+
},
25+
"version": "0.0.1"
26+
}
Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
import { randomUUID } from 'node:crypto';
2+
import type { SQSEvent } from 'aws-lambda';
3+
import type { EventPublisher, Logger } from 'utils';
4+
import { createHandler } from 'apis/sqs-trigger-lambda';
5+
import type { UploadToPdm } from 'app/upload-to-pdm';
6+
7+
jest.mock('node:crypto');
8+
9+
const mockRandomUUID = randomUUID as jest.MockedFunction<typeof randomUUID>;
10+
11+
const createValidSQSEvent = (overrides?: Partial<SQSEvent>): SQSEvent => ({
12+
Records: [
13+
{
14+
messageId: 'msg-1',
15+
body: JSON.stringify({
16+
detail: {
17+
id: 'a449d419-e683-4ab4-9291-a0451b5cef8e',
18+
specversion: '1.0',
19+
source:
20+
'/nhs/england/notify/production/primary/data-plane/digital-letters',
21+
subject:
22+
'customer/920fca11-596a-4eca-9c47-99f624614658/recipient/769acdd4-6a47-496f-999f-76a6fd2c3959',
23+
type: 'uk.nhs.notify.digital.letters.mesh.inbox.message.downloaded.v1',
24+
time: '2025-01-01T00:00:00Z',
25+
recordedtime: '2025-01-01T00:00:00Z',
26+
severitynumber: 2,
27+
traceparent:
28+
'00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
29+
datacontenttype: 'application/json',
30+
dataschema:
31+
'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-mesh-inbox-message-downloaded-data.schema.json',
32+
severitytext: 'INFO',
33+
data: {
34+
messageReference: 'test-message-reference',
35+
senderId: 'test-sender-id',
36+
messageUri: 's3://bucket/key',
37+
},
38+
},
39+
}),
40+
receiptHandle: 'receipt-1',
41+
attributes: {} as any,
42+
messageAttributes: {},
43+
md5OfBody: 'md5',
44+
eventSource: 'aws:sqs',
45+
eventSourceARN: 'arn:aws:sqs:region:account:queue',
46+
awsRegion: 'us-east-1',
47+
},
48+
],
49+
...overrides,
50+
});
51+
52+
describe('sqs-trigger-lambda', () => {
53+
let mockUploadToPdm: jest.Mocked<UploadToPdm>;
54+
let mockEventPublisher: jest.Mocked<EventPublisher>;
55+
let mockLogger: jest.Mocked<Logger>;
56+
57+
beforeEach(() => {
58+
jest.clearAllMocks();
59+
mockRandomUUID.mockReturnValue('3a2e9238-11f9-41ed-98e4-e519eafb1167');
60+
61+
mockUploadToPdm = {
62+
send: jest.fn(),
63+
} as unknown as jest.Mocked<UploadToPdm>;
64+
65+
mockEventPublisher = {
66+
sendEvents: jest.fn().mockResolvedValue([]),
67+
} as unknown as jest.Mocked<EventPublisher>;
68+
69+
mockLogger = {
70+
info: jest.fn(),
71+
error: jest.fn(),
72+
warn: jest.fn(),
73+
} as unknown as jest.Mocked<Logger>;
74+
});
75+
76+
describe('successful processing', () => {
77+
it('should process single message successfully', async () => {
78+
mockUploadToPdm.send.mockResolvedValue({
79+
outcome: 'sent',
80+
resourceId: 'resource-123',
81+
});
82+
const handler = createHandler({
83+
uploadToPdm: mockUploadToPdm,
84+
eventPublisher: mockEventPublisher,
85+
logger: mockLogger,
86+
});
87+
const sqsEvent = createValidSQSEvent();
88+
89+
const result = await handler(sqsEvent);
90+
91+
expect(result.batchItemFailures).toEqual([]);
92+
expect(mockUploadToPdm.send).toHaveBeenCalledTimes(1);
93+
expect(mockEventPublisher.sendEvents).toHaveBeenCalledWith(
94+
expect.arrayContaining([
95+
expect.objectContaining({
96+
type: 'uk.nhs.notify.digital.letters.pdm.resource.submitted.v1',
97+
id: '3a2e9238-11f9-41ed-98e4-e519eafb1167',
98+
}),
99+
]),
100+
);
101+
expect(mockLogger.info).toHaveBeenCalledWith(
102+
expect.objectContaining({
103+
description: 'Processed SQS Event.',
104+
retrieved: 1,
105+
sent: 1,
106+
failed: 0,
107+
}),
108+
);
109+
});
110+
111+
it('should process multiple messages successfully', async () => {
112+
mockUploadToPdm.send.mockResolvedValue({ outcome: 'sent' });
113+
const handler = createHandler({
114+
uploadToPdm: mockUploadToPdm,
115+
eventPublisher: mockEventPublisher,
116+
logger: mockLogger,
117+
});
118+
const sqsEvent = createValidSQSEvent({
119+
Records: [
120+
...createValidSQSEvent().Records,
121+
{
122+
...createValidSQSEvent().Records[0],
123+
messageId: 'msg-2',
124+
},
125+
],
126+
});
127+
128+
const result = await handler(sqsEvent);
129+
130+
expect(result.batchItemFailures).toEqual([]);
131+
expect(mockUploadToPdm.send).toHaveBeenCalledTimes(2);
132+
expect(mockEventPublisher.sendEvents).toHaveBeenCalledTimes(1);
133+
expect(mockLogger.info).toHaveBeenCalledWith(
134+
expect.objectContaining({
135+
retrieved: 2,
136+
sent: 2,
137+
failed: 0,
138+
}),
139+
);
140+
});
141+
});
142+
143+
describe('failed processing', () => {
144+
it('should handle upload failure', async () => {
145+
mockUploadToPdm.send.mockResolvedValue({ outcome: 'failed' });
146+
const handler = createHandler({
147+
uploadToPdm: mockUploadToPdm,
148+
eventPublisher: mockEventPublisher,
149+
logger: mockLogger,
150+
});
151+
const sqsEvent = createValidSQSEvent();
152+
153+
const result = await handler(sqsEvent);
154+
155+
expect(result.batchItemFailures).toEqual([{ itemIdentifier: 'msg-1' }]);
156+
expect(mockEventPublisher.sendEvents).toHaveBeenCalledWith(
157+
expect.arrayContaining([
158+
expect.objectContaining({
159+
type: 'uk.nhs.notify.digital.letters.pdm.resource.submission.rejected.v1',
160+
}),
161+
]),
162+
);
163+
expect(mockLogger.info).toHaveBeenCalledWith(
164+
expect.objectContaining({
165+
retrieved: 1,
166+
sent: 0,
167+
failed: 1,
168+
}),
169+
);
170+
});
171+
172+
it('should handle invalid message body', async () => {
173+
const handler = createHandler({
174+
uploadToPdm: mockUploadToPdm,
175+
eventPublisher: mockEventPublisher,
176+
logger: mockLogger,
177+
});
178+
const sqsEvent = createValidSQSEvent({
179+
Records: [
180+
{
181+
...createValidSQSEvent().Records[0],
182+
body: '{"invalidKey":"invalidValue"}',
183+
},
184+
],
185+
});
186+
187+
const result = await handler(sqsEvent);
188+
189+
expect(result.batchItemFailures).toEqual([{ itemIdentifier: 'msg-1' }]);
190+
expect(mockLogger.error).toHaveBeenCalledWith(
191+
expect.objectContaining({
192+
description: 'Error parsing queue entry',
193+
}),
194+
);
195+
});
196+
197+
it('should handle exception during upload', async () => {
198+
mockUploadToPdm.send.mockRejectedValue(new Error('Upload error'));
199+
const handler = createHandler({
200+
uploadToPdm: mockUploadToPdm,
201+
eventPublisher: mockEventPublisher,
202+
logger: mockLogger,
203+
});
204+
const sqsEvent = createValidSQSEvent();
205+
206+
const result = await handler(sqsEvent);
207+
208+
expect(result.batchItemFailures).toEqual([{ itemIdentifier: 'msg-1' }]);
209+
expect(mockLogger.error).toHaveBeenCalledWith(
210+
expect.objectContaining({
211+
description: 'Error during SQS trigger handler',
212+
err: expect.any(Error),
213+
}),
214+
);
215+
});
216+
});
217+
218+
describe('event publishing', () => {
219+
it('should handle partial event publishing failure for successful events', async () => {
220+
mockUploadToPdm.send.mockResolvedValue({
221+
outcome: 'sent',
222+
resourceId: 'resource-123',
223+
});
224+
mockEventPublisher.sendEvents.mockResolvedValue([
225+
{ itemIdentifier: 'failed-event' },
226+
] as any);
227+
const handler = createHandler({
228+
uploadToPdm: mockUploadToPdm,
229+
eventPublisher: mockEventPublisher,
230+
logger: mockLogger,
231+
});
232+
const sqsEvent = createValidSQSEvent();
233+
234+
await handler(sqsEvent);
235+
236+
expect(mockLogger.warn).toHaveBeenCalledWith(
237+
expect.objectContaining({
238+
description: 'Some successful events failed to publish',
239+
failedCount: 1,
240+
totalAttempted: 1,
241+
}),
242+
);
243+
});
244+
245+
it('should handle event publishing exception for successful events', async () => {
246+
mockUploadToPdm.send.mockResolvedValue({
247+
outcome: 'sent',
248+
resourceId: 'resource-123',
249+
});
250+
mockEventPublisher.sendEvents.mockRejectedValue(
251+
new Error('EventBridge error'),
252+
);
253+
const handler = createHandler({
254+
uploadToPdm: mockUploadToPdm,
255+
eventPublisher: mockEventPublisher,
256+
logger: mockLogger,
257+
});
258+
const sqsEvent = createValidSQSEvent();
259+
260+
await handler(sqsEvent);
261+
262+
expect(mockLogger.warn).toHaveBeenCalledWith(
263+
expect.objectContaining({
264+
description: 'Failed to send successful events to EventBridge',
265+
eventCount: 1,
266+
err: expect.any(Error),
267+
}),
268+
);
269+
});
270+
271+
it('should handle partial event publishing failure for failed events', async () => {
272+
mockUploadToPdm.send.mockResolvedValue({ outcome: 'failed' });
273+
mockEventPublisher.sendEvents.mockResolvedValue([
274+
{ itemIdentifier: 'failed-event' },
275+
] as any);
276+
const handler = createHandler({
277+
uploadToPdm: mockUploadToPdm,
278+
eventPublisher: mockEventPublisher,
279+
logger: mockLogger,
280+
});
281+
const sqsEvent = createValidSQSEvent();
282+
283+
await handler(sqsEvent);
284+
285+
expect(mockLogger.warn).toHaveBeenCalledWith(
286+
expect.objectContaining({
287+
description: 'Some failed events failed to publish',
288+
failedCount: 1,
289+
totalAttempted: 1,
290+
}),
291+
);
292+
});
293+
294+
it('should handle event publishing exception for failed events', async () => {
295+
mockUploadToPdm.send.mockResolvedValue({ outcome: 'failed' });
296+
mockEventPublisher.sendEvents.mockRejectedValue(
297+
new Error('EventBridge error'),
298+
);
299+
const handler = createHandler({
300+
uploadToPdm: mockUploadToPdm,
301+
eventPublisher: mockEventPublisher,
302+
logger: mockLogger,
303+
});
304+
const sqsEvent = createValidSQSEvent();
305+
306+
await handler(sqsEvent);
307+
308+
expect(mockLogger.warn).toHaveBeenCalledWith(
309+
expect.objectContaining({
310+
description: 'Failed to send failed events to EventBridge',
311+
eventCount: 1,
312+
err: expect.any(Error),
313+
}),
314+
);
315+
});
316+
});
317+
318+
describe('mixed outcomes', () => {
319+
it('should handle mix of successful and failed uploads', async () => {
320+
mockUploadToPdm.send
321+
.mockResolvedValueOnce({ outcome: 'sent', resourceId: 'resource-123' })
322+
.mockResolvedValueOnce({ outcome: 'failed' });
323+
const handler = createHandler({
324+
uploadToPdm: mockUploadToPdm,
325+
eventPublisher: mockEventPublisher,
326+
logger: mockLogger,
327+
});
328+
const sqsEvent = createValidSQSEvent({
329+
Records: [
330+
...createValidSQSEvent().Records,
331+
{
332+
...createValidSQSEvent().Records[0],
333+
messageId: 'msg-2',
334+
},
335+
],
336+
});
337+
338+
const result = await handler(sqsEvent);
339+
340+
expect(result.batchItemFailures).toEqual([{ itemIdentifier: 'msg-2' }]);
341+
expect(mockEventPublisher.sendEvents).toHaveBeenCalledTimes(2);
342+
expect(mockLogger.info).toHaveBeenCalledWith(
343+
expect.objectContaining({
344+
retrieved: 2,
345+
sent: 1,
346+
failed: 1,
347+
}),
348+
);
349+
});
350+
});
351+
});

0 commit comments

Comments
 (0)