11import { createHandler } from 'apis/sqs-trigger-lambda' ;
22import type { SQSEvent } from 'aws-lambda' ;
3- import { $TtlItem } from 'app/ttl-item-validator ' ;
3+ import { $TtlItemEvent , TtlItemEvent } from 'utils ' ;
44
55describe ( 'createHandler' , ( ) => {
66 let createTtl : any ;
7+ let eventPublisher : any ;
78 let logger : any ;
89 let handler : any ;
910
10- const validItem = {
11- id : '1' ,
12- source : 'src' ,
13- specversion : '1' ,
14- type : 't' ,
15- plane : 'p' ,
16- subject : 's' ,
17- time : 'now' ,
18- datacontenttype : 'json' ,
19- dataschema : 'sch' ,
20- dataschemaversion : '1' ,
21- data : { uri : 'uri' } ,
11+ const validItem : TtlItemEvent = {
12+ profileversion : '1.0.0' ,
13+ profilepublished : '2025-10' ,
14+ id : '550e8400-e29b-41d4-a716-446655440001' ,
15+ specversion : '1.0' ,
16+ source : '/nhs/england/notify/production/primary/data-plane/digital-letters' ,
17+ subject :
18+ 'customer/920fca11-596a-4eca-9c47-99f624614658/recipient/769acdd4-6a47-496f-999f-76a6fd2c3959' ,
19+ type : 'uk.nhs.notify.digital.letters.sent.v1' ,
20+ time : '2023-06-20T12:00:00Z' ,
21+ recordedtime : '2023-06-20T12:00:00.250Z' ,
22+ severitynumber : 2 ,
23+ traceparent : '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01' ,
24+ datacontenttype : 'application/json' ,
25+ dataschema :
26+ 'https://notify.nhs.uk/schemas/events/digital-letters/2025-10/digital-letters.schema.json' ,
27+ dataschemaversion : '1.0' ,
28+ severitytext : 'INFO' ,
29+ data : {
30+ uri : 'https://example.com/ttl/resource' ,
31+ 'digital-letter-id' : '123e4567-e89b-12d3-a456-426614174000' ,
32+ } ,
2233 } ;
2334
2435 beforeEach ( ( ) => {
2536 createTtl = { send : jest . fn ( ) } ;
26- logger = { error : jest . fn ( ) , info : jest . fn ( ) } ;
27- handler = createHandler ( { createTtl, logger } ) ;
37+ eventPublisher = { sendEvents : jest . fn ( ) . mockResolvedValue ( [ ] ) } ;
38+ logger = { error : jest . fn ( ) , info : jest . fn ( ) , warn : jest . fn ( ) } ;
39+ handler = createHandler ( { createTtl, eventPublisher, logger } ) ;
2840 } ) ;
2941
3042 it ( 'processes a valid SQS event and returns success' , async ( ) => {
3143 jest
32- . spyOn ( $TtlItem , 'safeParse' )
44+ . spyOn ( $TtlItemEvent , 'safeParse' )
3345 . mockReturnValue ( { success : true , data : validItem } ) ;
3446 createTtl . send . mockResolvedValue ( 'sent' ) ;
3547 const event : SQSEvent = {
@@ -40,6 +52,7 @@ describe('createHandler', () => {
4052
4153 expect ( res . batchItemFailures ) . toEqual ( [ ] ) ;
4254 expect ( createTtl . send ) . toHaveBeenCalledWith ( validItem ) ;
55+ expect ( eventPublisher . sendEvents ) . toHaveBeenCalledWith ( [ validItem ] ) ;
4356 expect ( logger . info ) . toHaveBeenCalledWith ( {
4457 description : 'Processed SQS Event.' ,
4558 failed : 0 ,
@@ -51,7 +64,7 @@ describe('createHandler', () => {
5164 it ( 'handles parse failure and logs error' , async ( ) => {
5265 const zodError = { errors : [ ] } as any ;
5366 jest
54- . spyOn ( $TtlItem , 'safeParse' )
67+ . spyOn ( $TtlItemEvent , 'safeParse' )
5568 . mockReturnValue ( { success : false , error : zodError } ) ;
5669 const event : SQSEvent = {
5770 Records : [ { body : '{}' , messageId : 'msg2' } ] ,
@@ -75,7 +88,7 @@ describe('createHandler', () => {
7588
7689 it ( 'handles createTtl.send failure' , async ( ) => {
7790 jest
78- . spyOn ( $TtlItem , 'safeParse' )
91+ . spyOn ( $TtlItemEvent , 'safeParse' )
7992 . mockReturnValue ( { success : true , data : validItem } ) ;
8093 createTtl . send . mockResolvedValue ( 'failed' ) ;
8194 const event : SQSEvent = {
@@ -94,7 +107,7 @@ describe('createHandler', () => {
94107 } ) ;
95108
96109 it ( 'handles thrown error and logs' , async ( ) => {
97- jest . spyOn ( $TtlItem , 'safeParse' ) . mockImplementation ( ( ) => {
110+ jest . spyOn ( $TtlItemEvent , 'safeParse' ) . mockImplementation ( ( ) => {
98111 throw new Error ( 'bad json' ) ;
99112 } ) ;
100113 const event : SQSEvent = {
@@ -143,4 +156,141 @@ describe('createHandler', () => {
143156
144157 Promise . allSettled = originalAllSettled ;
145158 } ) ;
159+
160+ it ( 'processes multiple successful events and sends them as a batch' , async ( ) => {
161+ jest
162+ . spyOn ( $TtlItemEvent , 'safeParse' )
163+ . mockReturnValue ( { success : true , data : validItem } ) ;
164+ createTtl . send . mockResolvedValue ( 'sent' ) ;
165+ const event : SQSEvent = {
166+ Records : [
167+ { body : JSON . stringify ( validItem ) , messageId : 'msg1' } ,
168+ { body : JSON . stringify ( validItem ) , messageId : 'msg2' } ,
169+ { body : JSON . stringify ( validItem ) , messageId : 'msg3' } ,
170+ ] ,
171+ } as any ;
172+
173+ const res = await handler ( event ) ;
174+
175+ expect ( res . batchItemFailures ) . toEqual ( [ ] ) ;
176+ expect ( createTtl . send ) . toHaveBeenCalledTimes ( 3 ) ;
177+ expect ( eventPublisher . sendEvents ) . toHaveBeenCalledWith ( [
178+ validItem ,
179+ validItem ,
180+ validItem ,
181+ ] ) ;
182+ expect ( logger . info ) . toHaveBeenCalledWith ( {
183+ description : 'Processed SQS Event.' ,
184+ failed : 0 ,
185+ retrieved : 3 ,
186+ sent : 3 ,
187+ } ) ;
188+ } ) ;
189+
190+ it ( 'handles partial event publishing failures and logs warning' , async ( ) => {
191+ jest
192+ . spyOn ( $TtlItemEvent , 'safeParse' )
193+ . mockReturnValue ( { success : true , data : validItem } ) ;
194+ createTtl . send . mockResolvedValue ( 'sent' ) ;
195+ const failedEvents = [ validItem ] ;
196+ eventPublisher . sendEvents . mockResolvedValue ( failedEvents ) ;
197+
198+ const event : SQSEvent = {
199+ Records : [
200+ { body : JSON . stringify ( validItem ) , messageId : 'msg1' } ,
201+ { body : JSON . stringify ( validItem ) , messageId : 'msg2' } ,
202+ ] ,
203+ } as any ;
204+
205+ const res = await handler ( event ) ;
206+
207+ expect ( res . batchItemFailures ) . toEqual ( [ ] ) ;
208+ expect ( eventPublisher . sendEvents ) . toHaveBeenCalledWith ( [
209+ validItem ,
210+ validItem ,
211+ ] ) ;
212+ expect ( logger . warn ) . toHaveBeenCalledWith ( {
213+ description : 'Some events failed to publish' ,
214+ failedCount : 1 ,
215+ totalAttempted : 2 ,
216+ } ) ;
217+ } ) ;
218+
219+ it ( 'handles event publishing exception and logs warning' , async ( ) => {
220+ jest
221+ . spyOn ( $TtlItemEvent , 'safeParse' )
222+ . mockReturnValue ( { success : true , data : validItem } ) ;
223+ createTtl . send . mockResolvedValue ( 'sent' ) ;
224+ const publishError = new Error ( 'EventBridge error' ) ;
225+ eventPublisher . sendEvents . mockRejectedValue ( publishError ) ;
226+
227+ const event : SQSEvent = {
228+ Records : [ { body : JSON . stringify ( validItem ) , messageId : 'msg1' } ] ,
229+ } as any ;
230+
231+ const res = await handler ( event ) ;
232+
233+ expect ( res . batchItemFailures ) . toEqual ( [ ] ) ;
234+ expect ( eventPublisher . sendEvents ) . toHaveBeenCalledWith ( [ validItem ] ) ;
235+ expect ( logger . warn ) . toHaveBeenCalledWith ( {
236+ err : publishError ,
237+ description : 'Failed to send events to EventBridge' ,
238+ eventCount : 1 ,
239+ } ) ;
240+ } ) ;
241+
242+ it ( 'does not call eventPublisher when no successful events' , async ( ) => {
243+ jest
244+ . spyOn ( $TtlItemEvent , 'safeParse' )
245+ . mockReturnValue ( { success : true , data : validItem } ) ;
246+ createTtl . send . mockResolvedValue ( 'failed' ) ;
247+
248+ const event : SQSEvent = {
249+ Records : [ { body : JSON . stringify ( validItem ) , messageId : 'msg1' } ] ,
250+ } as any ;
251+
252+ const res = await handler ( event ) ;
253+
254+ expect ( res . batchItemFailures ) . toEqual ( [ { itemIdentifier : 'msg1' } ] ) ;
255+ expect ( eventPublisher . sendEvents ) . not . toHaveBeenCalled ( ) ;
256+ expect ( logger . info ) . toHaveBeenCalledWith ( {
257+ description : 'Processed SQS Event.' ,
258+ failed : 1 ,
259+ retrieved : 1 ,
260+ sent : 0 ,
261+ } ) ;
262+ } ) ;
263+
264+ it ( 'handles mixed success and failure scenarios' , async ( ) => {
265+ jest
266+ . spyOn ( $TtlItemEvent , 'safeParse' )
267+ . mockReturnValueOnce ( { success : true , data : validItem } )
268+ . mockReturnValueOnce ( { success : false , error : { errors : [ ] } as any } )
269+ . mockReturnValueOnce ( { success : true , data : validItem } ) ;
270+ createTtl . send
271+ . mockResolvedValueOnce ( 'sent' )
272+ . mockResolvedValueOnce ( 'failed' ) ;
273+
274+ const event : SQSEvent = {
275+ Records : [
276+ { body : JSON . stringify ( validItem ) , messageId : 'msg1' } ,
277+ { body : '{}' , messageId : 'msg2' } ,
278+ { body : JSON . stringify ( validItem ) , messageId : 'msg3' } ,
279+ ] ,
280+ } as any ;
281+
282+ const res = await handler ( event ) ;
283+
284+ expect ( res . batchItemFailures ) . toEqual ( [
285+ { itemIdentifier : 'msg2' } ,
286+ { itemIdentifier : 'msg3' } ,
287+ ] ) ;
288+ expect ( eventPublisher . sendEvents ) . toHaveBeenCalledWith ( [ validItem ] ) ;
289+ expect ( logger . info ) . toHaveBeenCalledWith ( {
290+ description : 'Processed SQS Event.' ,
291+ failed : 2 ,
292+ retrieved : 3 ,
293+ sent : 1 ,
294+ } ) ;
295+ } ) ;
146296} ) ;
0 commit comments