Skip to content

Commit 425c342

Browse files
CCM-12858: fixing tests and tidy up
1 parent d639c7c commit 425c342

File tree

9 files changed

+287
-215
lines changed

9 files changed

+287
-215
lines changed
Lines changed: 89 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,48 @@
11
import type { SQSEvent, SQSRecord } from 'aws-lambda';
22
import { mock } from 'jest-mock-extended';
33
import { Logger, Sender } from 'utils';
4-
import { PDMResourceAvailable } from 'digital-letters-events';
54
import { NotifyMessageProcessor } from 'app/notify-message-processor';
6-
import { SenderManagement } from 'sender-management';
5+
import { ISenderManagement } from 'sender-management';
76
import { EventPublisherFacade } from 'infra/event-publisher-facade';
87
import { SqsHandlerDependencies, createHandler } from 'apis/sqs-handler';
98
import { parseSqsRecord } from 'app/parse-sqs-message';
109
import { InvalidPdmResourceAvailableEvent } from 'domain/invalid-pdm-resource-available-event';
1110
import { RequestNotifyError } from 'domain/request-notify-error';
11+
import { validPdmEvent, validSender } from '__tests__/constants';
1212

1313
jest.mock('app/parse-sqs-message');
1414

1515
const mockLogger = mock<Logger>();
1616
const mockNotifyMessageProcessor = mock<NotifyMessageProcessor>();
17-
const mockSenderManagement = mock<SenderManagement>();
17+
const mockSenderManagement = mock<ISenderManagement>();
1818
const mockEventPublisherFacade = mock<EventPublisherFacade>();
1919
const mockParseSqsRecord = jest.mocked(parseSqsRecord);
2020

21+
const createSqsRecord = (messageId: string): SQSRecord => ({
22+
messageId,
23+
receiptHandle: 'receipt-handle',
24+
body: JSON.stringify({
25+
detail: validPdmEvent,
26+
}),
27+
attributes: {
28+
ApproximateReceiveCount: '1',
29+
SentTimestamp: '1234567890',
30+
SenderId: 'sender-id',
31+
ApproximateFirstReceiveTimestamp: '1234567890',
32+
},
33+
messageAttributes: {},
34+
md5OfBody: 'md5',
35+
eventSource: 'aws:sqs',
36+
eventSourceARN: 'arn:aws:sqs:region:account:queue',
37+
awsRegion: 'eu-west-2',
38+
});
39+
40+
const createSqsEvent = (recordCount: number): SQSEvent => ({
41+
Records: Array.from({ length: recordCount }, (_, i) =>
42+
createSqsRecord(`message-id-${i + 1}`),
43+
),
44+
});
45+
2146
describe('createHandler', () => {
2247
const dependencies: SqsHandlerDependencies = {
2348
logger: mockLogger,
@@ -27,60 +52,9 @@ describe('createHandler', () => {
2752
};
2853

2954
const senderId = 'sender-123';
30-
const routingConfigId = 'routing-config-123';
3155
const messageReference = 'msg-ref-123';
3256
const notifyId = 'notify-id-123';
3357

34-
const mockSender: Sender = {
35-
senderId,
36-
routingConfigId,
37-
senderName: 'Test Sender',
38-
meshMailboxSenderId: 'meshMailBoxSender-123',
39-
meshMailboxReportsId: 'meshMailBoxReports-123',
40-
fallbackWaitTimeSeconds: 100,
41-
};
42-
43-
const mockPdmEvent: PDMResourceAvailable = {
44-
id: 'event-id-123',
45-
source: 'urn:nhs:names:services:notify:pdm',
46-
specversion: '1.0',
47-
type: 'uk.nhs.notify.digital.letters.pdm.resource.available.v1',
48-
time: '2025-12-15T10:00:00Z',
49-
datacontenttype: 'application/json',
50-
data: {
51-
senderId,
52-
messageReference,
53-
resourceId: 'ResourceId-123',
54-
nhsNumber: '9991234566',
55-
odsCode: 'A12345',
56-
},
57-
};
58-
59-
const createSqsRecord = (messageId: string): SQSRecord => ({
60-
messageId,
61-
receiptHandle: 'receipt-handle',
62-
body: JSON.stringify({
63-
detail: mockPdmEvent,
64-
}),
65-
attributes: {
66-
ApproximateReceiveCount: '1',
67-
SentTimestamp: '1234567890',
68-
SenderId: 'sender-id',
69-
ApproximateFirstReceiveTimestamp: '1234567890',
70-
},
71-
messageAttributes: {},
72-
md5OfBody: 'md5',
73-
eventSource: 'aws:sqs',
74-
eventSourceARN: 'arn:aws:sqs:region:account:queue',
75-
awsRegion: 'eu-west-2',
76-
});
77-
78-
const createSqsEvent = (recordCount: number): SQSEvent => ({
79-
Records: Array.from({ length: recordCount }, (_, i) =>
80-
createSqsRecord(`message-id-${i + 1}`),
81-
),
82-
});
83-
8458
beforeEach(() => {
8559
jest.clearAllMocks();
8660
});
@@ -90,8 +64,8 @@ describe('createHandler', () => {
9064
const sqsEvent = createSqsEvent(1);
9165
const handler = createHandler(dependencies);
9266

93-
mockParseSqsRecord.mockReturnValueOnce(mockPdmEvent);
94-
mockSenderManagement.getSender.mockReturnValueOnce(mockSender);
67+
mockParseSqsRecord.mockReturnValueOnce(validPdmEvent);
68+
mockSenderManagement.getSender.mockResolvedValue(validSender);
9569
mockNotifyMessageProcessor.process.mockResolvedValueOnce(notifyId);
9670

9771
const result = await handler(sqsEvent);
@@ -107,46 +81,63 @@ describe('createHandler', () => {
10781
sqsEvent.Records[0],
10882
mockLogger,
10983
);
110-
expect(mockSenderManagement.getSender).toHaveBeenCalledWith(senderId);
84+
expect(mockSenderManagement.getSender).toHaveBeenCalledWith({
85+
senderId,
86+
});
11187
expect(mockNotifyMessageProcessor.process).toHaveBeenCalledTimes(1);
11288
expect(
11389
mockEventPublisherFacade.publishMessageRequestSubmitted,
11490
).toHaveBeenCalledTimes(1);
11591
});
116-
});
11792

118-
describe('when sender has no routing config', () => {
11993
it('skips the message and publishes a skipped event', async () => {
12094
const sqsEvent = createSqsEvent(1);
12195
const handler = createHandler(dependencies);
12296
const senderWithoutRouting: Sender = {
123-
...mockSender,
97+
...validSender,
12498
routingConfigId: undefined,
12599
};
126100

127-
mockParseSqsRecord.mockReturnValueOnce(mockPdmEvent);
128-
mockSenderManagement.getSender.mockReturnValueOnce(senderWithoutRouting);
101+
mockParseSqsRecord.mockReturnValueOnce(validPdmEvent);
102+
mockSenderManagement.getSender.mockResolvedValue(senderWithoutRouting);
129103

130104
const result = await handler(sqsEvent);
131105

132106
expect(result).toEqual({ batchItemFailures: [] });
133-
expect(mockLogger.debug).toHaveBeenCalledWith(
134-
`No routing config for sender ${senderId}, skipping message`,
135-
);
136107
expect(mockNotifyMessageProcessor.process).not.toHaveBeenCalled();
137108
expect(
138109
mockEventPublisherFacade.publishMessageRequestSkipped,
139110
).toHaveBeenCalledTimes(1);
140111
});
112+
113+
it('throws an error when sender is not found', async () => {
114+
const sqsEvent = createSqsEvent(1);
115+
const handler = createHandler(dependencies);
116+
117+
mockParseSqsRecord.mockReturnValueOnce(validPdmEvent);
118+
mockSenderManagement.getSender.mockResolvedValue(null);
119+
120+
const result = await handler(sqsEvent);
121+
122+
expect(result).toEqual({
123+
batchItemFailures: [{ itemIdentifier: sqsEvent.Records[0].messageId }],
124+
});
125+
expect(mockLogger.warn).toHaveBeenCalledWith(
126+
expect.objectContaining({
127+
description: 'Failed processing message',
128+
messageId: sqsEvent.Records[0].messageId,
129+
}),
130+
);
131+
});
141132
});
142133

143134
describe('when processing multiple SQS records', () => {
144135
it('processes all records successfully', async () => {
145136
const sqsEvent = createSqsEvent(3);
146137
const handler = createHandler(dependencies);
147138

148-
mockParseSqsRecord.mockReturnValue(mockPdmEvent);
149-
mockSenderManagement.getSender.mockReturnValue(mockSender);
139+
mockParseSqsRecord.mockReturnValue(validPdmEvent);
140+
mockSenderManagement.getSender.mockResolvedValue(validSender);
150141
mockNotifyMessageProcessor.process.mockResolvedValue(notifyId);
151142

152143
const result = await handler(sqsEvent);
@@ -161,6 +152,30 @@ describe('createHandler', () => {
161152
expect(mockParseSqsRecord).toHaveBeenCalledTimes(3);
162153
expect(mockNotifyMessageProcessor.process).toHaveBeenCalledTimes(3);
163154
});
155+
156+
it('returns only failed message IDs', async () => {
157+
const sqsEvent = createSqsEvent(3);
158+
const handler = createHandler(dependencies);
159+
160+
mockParseSqsRecord
161+
.mockReturnValueOnce(validPdmEvent)
162+
.mockImplementationOnce(() => {
163+
throw new Error('Parse error');
164+
})
165+
.mockReturnValueOnce(validPdmEvent);
166+
167+
mockSenderManagement.getSender.mockResolvedValue(validSender);
168+
mockNotifyMessageProcessor.process.mockResolvedValue(notifyId);
169+
170+
const result = await handler(sqsEvent);
171+
172+
expect(result).toEqual({
173+
batchItemFailures: [{ itemIdentifier: 'message-id-2' }],
174+
});
175+
expect(mockLogger.info).toHaveBeenCalledWith(
176+
'2 of 3 records processed successfully',
177+
);
178+
});
164179
});
165180

166181
describe('when parseSqsRecord throws InvalidPdmResourceAvailableEvent', () => {
@@ -189,21 +204,15 @@ describe('createHandler', () => {
189204
});
190205
});
191206

192-
describe('when processing throws RequestNotifyError', () => {
193-
it('marks the message as failed for retry since error lacks messageReference', async () => {
207+
describe('when processing throws error', () => {
208+
it('marks the message as failed as is not a RequestNotifyError', async () => {
194209
const sqsEvent = createSqsEvent(1);
195210
const handler = createHandler(dependencies);
196211
const { messageId } = sqsEvent.Records[0];
197-
const errorCode = 'VALIDATION_ERROR';
198-
const correlationId = 'corr-123';
199-
const error = new RequestNotifyError(
200-
new Error('Validation failed'),
201-
correlationId,
202-
errorCode,
203-
);
212+
const error = new Error('Validation failed');
204213

205-
mockParseSqsRecord.mockReturnValueOnce(mockPdmEvent);
206-
mockSenderManagement.getSender.mockReturnValueOnce(mockSender);
214+
mockParseSqsRecord.mockReturnValueOnce(validPdmEvent);
215+
mockSenderManagement.getSender.mockResolvedValue(validSender);
207216
mockNotifyMessageProcessor.process.mockRejectedValueOnce(error);
208217

209218
const result = await handler(sqsEvent);
@@ -237,8 +246,8 @@ describe('createHandler', () => {
237246
// Add messageReference property dynamically to trigger the terminal error path
238247
(error as any).messageReference = messageReference;
239248

240-
mockParseSqsRecord.mockReturnValueOnce(mockPdmEvent);
241-
mockSenderManagement.getSender.mockReturnValueOnce(mockSender);
249+
mockParseSqsRecord.mockReturnValueOnce(validPdmEvent);
250+
mockSenderManagement.getSender.mockResolvedValue(validSender);
242251
mockNotifyMessageProcessor.process.mockRejectedValueOnce(error);
243252

244253
const result = await handler(sqsEvent);
@@ -266,75 +275,4 @@ describe('createHandler', () => {
266275
);
267276
});
268277
});
269-
270-
describe('when processing throws a generic error', () => {
271-
it('marks the message as failed for retry', async () => {
272-
const sqsEvent = createSqsEvent(1);
273-
const handler = createHandler(dependencies);
274-
const { messageId } = sqsEvent.Records[0];
275-
const error = new Error('Unexpected error');
276-
277-
mockParseSqsRecord.mockReturnValueOnce(mockPdmEvent);
278-
mockSenderManagement.getSender.mockReturnValueOnce(mockSender);
279-
mockNotifyMessageProcessor.process.mockRejectedValueOnce(error);
280-
281-
const result = await handler(sqsEvent);
282-
283-
expect(result).toEqual({
284-
batchItemFailures: [{ itemIdentifier: messageId }],
285-
});
286-
expect(mockLogger.warn).toHaveBeenCalledWith({
287-
error: error.message,
288-
description: 'Failed processing message',
289-
messageId,
290-
});
291-
expect(
292-
mockEventPublisherFacade.publishMessageRequestRejected,
293-
).not.toHaveBeenCalled();
294-
});
295-
});
296-
297-
describe('when processing mixed success and failure records', () => {
298-
it('returns only failed message IDs', async () => {
299-
const sqsEvent = createSqsEvent(3);
300-
const handler = createHandler(dependencies);
301-
302-
mockParseSqsRecord
303-
.mockReturnValueOnce(mockPdmEvent)
304-
.mockImplementationOnce(() => {
305-
throw new Error('Parse error');
306-
})
307-
.mockReturnValueOnce(mockPdmEvent);
308-
309-
mockSenderManagement.getSender.mockReturnValue(mockSender);
310-
mockNotifyMessageProcessor.process.mockResolvedValue(notifyId);
311-
312-
const result = await handler(sqsEvent);
313-
314-
expect(result).toEqual({
315-
batchItemFailures: [{ itemIdentifier: 'message-id-2' }],
316-
});
317-
expect(mockLogger.info).toHaveBeenCalledWith(
318-
'2 of 3 records processed successfully',
319-
);
320-
});
321-
});
322-
323-
describe('when notifyMessageProcessor returns undefined', () => {
324-
it('does not publish submitted event', async () => {
325-
const sqsEvent = createSqsEvent(1);
326-
const handler = createHandler(dependencies);
327-
328-
mockParseSqsRecord.mockReturnValueOnce(mockPdmEvent);
329-
mockSenderManagement.getSender.mockReturnValueOnce(mockSender);
330-
mockNotifyMessageProcessor.process.mockResolvedValueOnce();
331-
332-
const result = await handler(sqsEvent);
333-
334-
expect(result).toEqual({ batchItemFailures: [] });
335-
expect(
336-
mockEventPublisherFacade.publishMessageRequestSubmitted,
337-
).not.toHaveBeenCalled();
338-
});
339-
});
340278
});

lambdas/core-notifier-lambda/src/__tests__/app/notify-message-processor.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,17 @@ describe('NotifyMessageProcessor', () => {
6262
});
6363
});
6464

65-
it('does not re-throw when a RequestAlreadyReceivedError is thrown by the API client', async () => {
65+
it('re-throw when a RequestAlreadyReceivedError is thrown by the API client', async () => {
6666
const { messageReference } = mockRequest1.data.attributes;
6767
const err = new RequestAlreadyReceivedError(
6868
new Error('Request was already received!'),
6969
messageReference,
7070
);
7171
mockClient.sendRequest.mockRejectedValue(err);
7272

73-
expect(await notifyMessageProcessor.process(mockRequest1)).toBeUndefined();
73+
await expect(notifyMessageProcessor.process(mockRequest1)).rejects.toThrow(
74+
err,
75+
);
7476

7577
expect(mockLogger.info).toHaveBeenCalledWith(
7678
'Request has already been received by Notify',

lambdas/core-notifier-lambda/src/__tests__/app/parse-sqs-message.test.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { mock } from 'jest-mock-extended';
33
import { Logger } from 'utils';
44
import { parseSqsRecord } from 'app/parse-sqs-message';
55
import { InvalidPdmResourceAvailableEvent } from 'domain/invalid-pdm-resource-available-event';
6-
import { PDMResourceAvailable } from 'digital-letters-events';
6+
import { validPdmEvent } from '__tests__/constants';
77

88
// Import the mocked validator after the mock setup
99
import { messageDownloadedValidator } from 'digital-letters-events/PDMResourceAvailable.js';
@@ -16,21 +16,6 @@ const mockLogger = mock<Logger>();
1616

1717
describe('parseSqsRecord', () => {
1818
const messageId = 'test-message-id-123';
19-
const validPdmEvent: PDMResourceAvailable = {
20-
id: 'event-id-123',
21-
source: 'urn:nhs:names:services:notify:pdm',
22-
specversion: '1.0',
23-
type: 'uk.nhs.notify.digital.letters.pdm.resource.available.v1',
24-
time: '2025-12-15T10:00:00Z',
25-
datacontenttype: 'application/json',
26-
data: {
27-
senderId: 'sender-123',
28-
messageReference: 'ref-123',
29-
resourceId: 'ResourceId-123',
30-
nhsNumber: '9991234566',
31-
odsCode: 'A12345',
32-
},
33-
};
3419

3520
const createSqsRecord = (detail: any): SQSRecord => ({
3621
messageId,

0 commit comments

Comments
 (0)