11import type { SQSEvent , SQSRecord } from 'aws-lambda' ;
22import { mock } from 'jest-mock-extended' ;
33import { Logger , Sender } from 'utils' ;
4- import { PDMResourceAvailable } from 'digital-letters-events' ;
54import { NotifyMessageProcessor } from 'app/notify-message-processor' ;
6- import { SenderManagement } from 'sender-management' ;
5+ import { ISenderManagement } from 'sender-management' ;
76import { EventPublisherFacade } from 'infra/event-publisher-facade' ;
87import { SqsHandlerDependencies , createHandler } from 'apis/sqs-handler' ;
98import { parseSqsRecord } from 'app/parse-sqs-message' ;
109import { InvalidPdmResourceAvailableEvent } from 'domain/invalid-pdm-resource-available-event' ;
1110import { RequestNotifyError } from 'domain/request-notify-error' ;
11+ import { validPdmEvent , validSender } from '__tests__/constants' ;
1212
1313jest . mock ( 'app/parse-sqs-message' ) ;
1414
1515const mockLogger = mock < Logger > ( ) ;
1616const mockNotifyMessageProcessor = mock < NotifyMessageProcessor > ( ) ;
17- const mockSenderManagement = mock < SenderManagement > ( ) ;
17+ const mockSenderManagement = mock < ISenderManagement > ( ) ;
1818const mockEventPublisherFacade = mock < EventPublisherFacade > ( ) ;
1919const 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+
2146describe ( '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} ) ;
0 commit comments