Skip to content

Commit a52e5a6

Browse files
committed
CCM-12613: use new types and validators
1 parent 3868b9a commit a52e5a6

File tree

9 files changed

+128
-85
lines changed

9 files changed

+128
-85
lines changed

lambdas/pdm-uploader-lambda/jest.config.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,4 @@ import { baseJestConfig } from '../../jest.config.base';
22

33
const config = baseJestConfig;
44

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-
155
export default config;

lambdas/pdm-uploader-lambda/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"dependencies": {
33
"@aws-sdk/lib-dynamodb": "^3.908.0",
44
"axios": "^1.13.2",
5+
"digital-letters-events": "^0.0.1",
56
"utils": "^0.0.1",
67
"zod": "^4.1.12"
78
},

lambdas/pdm-uploader-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,10 @@ const createValidSQSEvent = (overrides?: Partial<SQSEvent>): SQSEvent => ({
1414
messageId: 'msg-1',
1515
body: JSON.stringify({
1616
detail: {
17-
profileversion: '1.0.0',
18-
profilepublished: '2025-10',
1917
id: 'a449d419-e683-4ab4-9291-a0451b5cef8e',
2018
specversion: '1.0',
2119
source:
22-
'/nhs/england/notify/production/primary/data-plane/digital-letters',
20+
'/nhs/england/notify/production/primary/data-plane/digitalletters/mesh',
2321
subject:
2422
'customer/920fca11-596a-4eca-9c47-99f624614658/recipient/769acdd4-6a47-496f-999f-76a6fd2c3959',
2523
type: 'uk.nhs.notify.digital.letters.mesh.inbox.message.downloaded.v1',
@@ -30,11 +28,9 @@ const createValidSQSEvent = (overrides?: Partial<SQSEvent>): SQSEvent => ({
3028
'00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
3129
datacontenttype: 'application/json',
3230
dataschema:
33-
'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10/digital-letter-base-data.schema.json',
34-
dataschemaversion: '1.0',
31+
'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-mesh-inbox-message-downloaded-data.schema.json',
3532
severitytext: 'INFO',
3633
data: {
37-
'digital-letter-id': 'eb4c9442-6f74-437d-8548-76697868c807',
3834
messageReference: 'test-message-reference',
3935
senderId: 'test-sender-id',
4036
messageUri: 's3://bucket/key',
@@ -101,6 +97,7 @@ describe('sqs-trigger-lambda', () => {
10197
id: '3a2e9238-11f9-41ed-98e4-e519eafb1167',
10298
}),
10399
]),
100+
expect.anything(),
104101
);
105102
expect(mockLogger.info).toHaveBeenCalledWith(
106103
expect.objectContaining({
@@ -163,6 +160,7 @@ describe('sqs-trigger-lambda', () => {
163160
type: 'uk.nhs.notify.digital.letters.pdm.resource.submission.rejected.v1',
164161
}),
165162
]),
163+
expect.anything(),
166164
);
167165
expect(mockLogger.info).toHaveBeenCalledWith(
168166
expect.objectContaining({
@@ -217,6 +215,45 @@ describe('sqs-trigger-lambda', () => {
217215
}),
218216
);
219217
});
218+
219+
it('should handle unhandled promise rejection in processing', async () => {
220+
// It's an unlikely scenario due to the try/catch in processRecord, but added for 100% coverage.
221+
// Mock logger.error to throw an error, which will cause processRecord to reject
222+
const loggerErrorSpy = jest.spyOn(mockLogger, 'error');
223+
let callCount = 0;
224+
loggerErrorSpy.mockImplementation((() => {
225+
callCount += 1;
226+
if (callCount === 1) {
227+
// First call from processRecord's catch block - throw to cause rejection
228+
throw new Error('Logger error causing unhandled rejection');
229+
}
230+
return mockLogger;
231+
}) as any);
232+
233+
mockUploadToPdm.send.mockRejectedValue(new Error('Upload error'));
234+
const handler = createHandler({
235+
uploadToPdm: mockUploadToPdm,
236+
eventPublisher: mockEventPublisher,
237+
logger: mockLogger,
238+
});
239+
const sqsEvent = createValidSQSEvent();
240+
241+
const result = await handler(sqsEvent);
242+
243+
expect(result.batchItemFailures).toEqual([]);
244+
expect(mockLogger.error).toHaveBeenCalledWith(
245+
expect.objectContaining({
246+
err: expect.any(Error),
247+
}),
248+
);
249+
expect(mockLogger.info).toHaveBeenCalledWith(
250+
expect.objectContaining({
251+
retrieved: 1,
252+
sent: 0,
253+
failed: 1,
254+
}),
255+
);
256+
});
220257
});
221258

222259
describe('event publishing', () => {

lambdas/pdm-uploader-lambda/src/__tests__/app/upload-to-pdm.test.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Logger, TtlItemEvent, getS3ObjectFromUri } from 'utils';
21
import { UploadToPdm } from 'app/upload-to-pdm';
2+
import { MESHInboxMessageDownloaded } from 'digital-letters-events';
33
import { IPdmClient } from 'infra/pdm-api-client';
4+
import { Logger, getS3ObjectFromUri } from 'utils';
45

56
jest.mock('utils', () => ({
67
...jest.requireActual('utils'),
@@ -12,9 +13,7 @@ describe('UploadToPdm', () => {
1213
let mockLogger: jest.Mocked<Logger>;
1314
let uploadToPdm: UploadToPdm;
1415

15-
const mockEvent: TtlItemEvent = {
16-
profileversion: '1.0.0',
17-
profilepublished: '2025-10',
16+
const mockEvent: MESHInboxMessageDownloaded = {
1817
id: 'test-event-id',
1918
specversion: '1.0',
2019
source: '/nhs/england/notify/production/primary/data-plane/digital-letters',
@@ -27,11 +26,9 @@ describe('UploadToPdm', () => {
2726
traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
2827
datacontenttype: 'application/json',
2928
dataschema:
30-
'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10/digital-letter-base-data.schema.json',
31-
dataschemaversion: '1.0',
29+
'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-mesh-inbox-message-downloaded-data.schema.json',
3230
severitytext: 'INFO',
3331
data: {
34-
'digital-letter-id': 'test-letter-id',
3532
messageReference: 'test-message-reference',
3633
senderId: 'test-sender-id',
3734
messageUri: 's3://bucket/key',

lambdas/pdm-uploader-lambda/src/apis/sqs-trigger-lambda.ts

Lines changed: 66 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,15 @@ import type {
99
UploadToPdmOutcome,
1010
UploadToPdmResult,
1111
} from 'app/upload-to-pdm';
12-
import {
13-
$TtlItemBusEvent,
14-
EventPublisher,
15-
Logger,
16-
PdmResourceRejectedEvent,
17-
PdmResourceSubmittedEvent,
18-
} from 'utils';
12+
import messageDownloadedValidator from 'digital-letters-events/MESHInboxMessageDownloaded.js';
13+
import pdmResourceSubmittedValidator from 'digital-letters-events/PDMResourceSubmitted.js';
14+
import pdmResourceSubmissionRejectedValidator from 'digital-letters-events/PDMResourceSubmissionRejected.js';
15+
import { MESHInboxMessageDownloaded } from 'digital-letters-events';
16+
import { EventPublisher, Logger } from 'utils';
1917

2018
interface ProcessingResult {
2119
result: UploadToPdmResult;
22-
item?: PdmResourceSubmittedEvent | PdmResourceRejectedEvent;
20+
item?: MESHInboxMessageDownloaded;
2321
}
2422

2523
interface CreateHandlerDependencies {
@@ -35,29 +33,29 @@ async function processRecord(
3533
batchItemFailures: SQSBatchItemFailure[],
3634
): Promise<ProcessingResult> {
3735
try {
38-
const {
39-
data: item,
40-
error: parseError,
41-
success: parseSuccess,
42-
} = $TtlItemBusEvent.safeParse(JSON.parse(body));
36+
const sqsEventBody = JSON.parse(body);
37+
const sqsEventDetail = sqsEventBody.detail;
4338

44-
if (!parseSuccess) {
39+
const isEventValid = messageDownloadedValidator(sqsEventDetail);
40+
if (!isEventValid) {
4541
logger.error({
46-
err: parseError,
42+
err: messageDownloadedValidator.errors,
4743
description: 'Error parsing queue entry',
4844
});
4945
batchItemFailures.push({ itemIdentifier: messageId });
50-
return { result: { outcome: 'failed' } };
46+
return { result: { outcome: 'failed' }, item: sqsEventDetail };
5147
}
5248

53-
const result = await uploadToPdm.send(item.detail);
49+
const messageDownloadedEvent: MESHInboxMessageDownloaded = sqsEventDetail;
50+
51+
const result = await uploadToPdm.send(messageDownloadedEvent);
5452

5553
if (result.outcome === 'failed') {
5654
batchItemFailures.push({ itemIdentifier: messageId });
57-
return { result: { outcome: 'failed' }, item: item.detail };
55+
return { result: { outcome: 'failed' }, item: sqsEventDetail };
5856
}
5957

60-
return { result, item: item.detail };
58+
return { result, item: sqsEventDetail };
6159
} catch (error) {
6260
logger.error({
6361
err: error,
@@ -68,44 +66,41 @@ async function processRecord(
6866
}
6967
}
7068

69+
interface CategorizedResults {
70+
processed: Record<UploadToPdmOutcome | 'retrieved', number>;
71+
successfulItems: { event: MESHInboxMessageDownloaded; resourceId: string }[];
72+
failedItems: MESHInboxMessageDownloaded[];
73+
}
74+
7175
function categorizeResults(
7276
results: PromiseSettledResult<ProcessingResult>[],
73-
successfulEvents: PdmResourceSubmittedEvent[],
74-
failedEvents: PdmResourceRejectedEvent[],
7577
logger: Logger,
76-
): Record<UploadToPdmOutcome | 'retrieved', number> {
78+
): CategorizedResults {
7779
const processed: Record<UploadToPdmOutcome | 'retrieved', number> = {
7880
retrieved: results.length,
7981
sent: 0,
8082
failed: 0,
8183
};
8284

85+
const successfulItems: {
86+
event: MESHInboxMessageDownloaded;
87+
resourceId: string;
88+
}[] = [];
89+
const failedItems: MESHInboxMessageDownloaded[] = [];
90+
8391
for (const result of results) {
8492
if (result.status === 'fulfilled') {
8593
const { item, result: itemResult } = result.value;
8694
processed[itemResult.outcome] += 1;
8795

8896
if (item) {
8997
if (itemResult.outcome === 'sent' && itemResult.resourceId) {
90-
successfulEvents.push({
91-
...item,
92-
data: {
93-
'digital-letter-id': item.data['digital-letter-id'],
94-
messageReference: item.data.messageReference,
95-
senderId: item.data.senderId,
96-
resourceId: itemResult.resourceId,
97-
retryCount: 0,
98-
},
98+
successfulItems.push({
99+
event: item,
100+
resourceId: itemResult.resourceId,
99101
});
100102
} else {
101-
failedEvents.push({
102-
...item,
103-
data: {
104-
'digital-letter-id': item.data['digital-letter-id'],
105-
messageReference: item.data.messageReference,
106-
senderId: item.data.senderId,
107-
},
108-
});
103+
failedItems.push(item);
109104
}
110105
}
111106
} else {
@@ -114,71 +109,89 @@ function categorizeResults(
114109
}
115110
}
116111

117-
return processed;
112+
return { processed, successfulItems, failedItems };
118113
}
119114

120115
async function publishSuccessfulEvents(
121-
successfulEvents: PdmResourceSubmittedEvent[],
116+
successfulItems: { event: MESHInboxMessageDownloaded; resourceId: string }[],
122117
eventPublisher: EventPublisher,
123118
logger: Logger,
124119
): Promise<void> {
125-
if (successfulEvents.length === 0) return;
120+
if (successfulItems.length === 0) return;
126121

127122
try {
128123
const submittedFailedEvents = await eventPublisher.sendEvents(
129-
successfulEvents.map((event) => ({
124+
successfulItems.map(({ event, resourceId }) => ({
130125
...event,
131126
id: randomUUID(),
132127
time: new Date().toISOString(),
133128
recordedtime: new Date().toISOString(),
134129
type: 'uk.nhs.notify.digital.letters.pdm.resource.submitted.v1',
130+
dataschema:
131+
'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-pdm-resource-submitted-data.schema.json',
132+
source: event.source.replace(/\/mesh$/, '/pdm'),
133+
data: {
134+
messageReference: event.data.messageReference,
135+
senderId: event.data.senderId,
136+
resourceId,
137+
retryCount: -1, // Setting to -1 until this field is removed from pdm.resource.submitted.
138+
},
135139
})),
140+
pdmResourceSubmittedValidator,
136141
);
137142
if (submittedFailedEvents.length > 0) {
138143
logger.warn({
139144
description: 'Some successful events failed to publish',
140145
failedCount: submittedFailedEvents.length,
141-
totalAttempted: successfulEvents.length,
146+
totalAttempted: successfulItems.length,
142147
});
143148
}
144149
} catch (error) {
145150
logger.warn({
146151
err: error,
147152
description: 'Failed to send successful events to EventBridge',
148-
eventCount: successfulEvents.length,
153+
eventCount: successfulItems.length,
149154
});
150155
}
151156
}
152157

153158
async function publishFailedEvents(
154-
failedEvents: PdmResourceRejectedEvent[],
159+
failedItems: MESHInboxMessageDownloaded[],
155160
eventPublisher: EventPublisher,
156161
logger: Logger,
157162
): Promise<void> {
158-
if (failedEvents.length === 0) return;
163+
if (failedItems.length === 0) return;
159164

160165
try {
161166
const rejectedFailedEvents = await eventPublisher.sendEvents(
162-
failedEvents.map((event) => ({
167+
failedItems.map((event) => ({
163168
...event,
164169
id: randomUUID(),
165170
time: new Date().toISOString(),
166171
recordedtime: new Date().toISOString(),
167172
type: 'uk.nhs.notify.digital.letters.pdm.resource.submission.rejected.v1',
173+
dataschema:
174+
'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-pdm-resource-submission-rejected-data.schema.json',
175+
source: event.source.replace(/\/mesh$/, '/pdm'),
176+
data: {
177+
messageReference: event.data.messageReference,
178+
senderId: event.data.senderId,
179+
},
168180
})),
181+
pdmResourceSubmissionRejectedValidator,
169182
);
170183
if (rejectedFailedEvents.length > 0) {
171184
logger.warn({
172185
description: 'Some failed events failed to publish',
173186
failedCount: rejectedFailedEvents.length,
174-
totalAttempted: failedEvents.length,
187+
totalAttempted: failedItems.length,
175188
});
176189
}
177190
} catch (error) {
178191
logger.warn({
179192
err: error,
180193
description: 'Failed to send failed events to EventBridge',
181-
eventCount: failedEvents.length,
194+
eventCount: failedItems.length,
182195
});
183196
}
184197
}
@@ -190,23 +203,19 @@ export const createHandler = ({
190203
}: CreateHandlerDependencies) =>
191204
async function handler(sqsEvent: SQSEvent): Promise<SQSBatchResponse> {
192205
const batchItemFailures: SQSBatchItemFailure[] = [];
193-
const successfulEvents: PdmResourceSubmittedEvent[] = [];
194-
const failedEvents: PdmResourceRejectedEvent[] = [];
195206

196207
const promises = sqsEvent.Records.map((record) =>
197208
processRecord(record, uploadToPdm, logger, batchItemFailures),
198209
);
199210

200211
const results = await Promise.allSettled(promises);
201-
const processed = categorizeResults(
212+
const { failedItems, processed, successfulItems } = categorizeResults(
202213
results,
203-
successfulEvents,
204-
failedEvents,
205214
logger,
206215
);
207216

208-
await publishSuccessfulEvents(successfulEvents, eventPublisher, logger);
209-
await publishFailedEvents(failedEvents, eventPublisher, logger);
217+
await publishSuccessfulEvents(successfulItems, eventPublisher, logger);
218+
await publishFailedEvents(failedItems, eventPublisher, logger);
210219

211220
logger.info({
212221
description: 'Processed SQS Event.',

0 commit comments

Comments
 (0)