1- import { Context , SNSEvent , SNSEventRecord } from "aws-lambda" ;
1+ import { Context , SQSEvent , SQSRecord } from "aws-lambda" ;
22import { FirehoseClient , PutRecordCommand } from "@aws-sdk/client-firehose" ;
33import { mockDeep } from "jest-mock-extended" ;
44import pino from "pino" ;
55import createForwarder from "../forwarder" ;
66import { Deps } from "../deps" ;
77
8- function createSNSEvent ( records : SNSEventRecord [ ] ) : SNSEvent {
8+ function createSQSEvent ( records : SQSRecord [ ] ) : SQSEvent {
99 return {
1010 Records : records ,
1111 } ;
1212}
1313
14- function createSNSEventRecord (
15- message : string ,
16- overrides : Partial < SNSEventRecord [ "Sns" ] > = { } ,
17- ) : SNSEventRecord {
14+ /**
15+ * Creates an SQS record with a body containing the SNS notification wrapper.
16+ * This simulates what SNS delivers to SQS when raw_message_delivery is false.
17+ */
18+ function createSQSRecord (
19+ body : string ,
20+ messageId = "test-sqs-msg-id" ,
21+ ) : SQSRecord {
1822 return {
19- EventVersion : "1.0" ,
20- EventSubscriptionArn :
21- "arn:aws:sns:eu-west-2:123456789012:topic:subscription" ,
22- EventSource : "aws:sns" ,
23- Sns : {
24- SignatureVersion : "1" ,
25- Timestamp : "2026-01-09T12:00:00.000Z" ,
26- Signature : "test-signature" ,
27- SigningCertUrl : "https://sns.eu-west-2.amazonaws.com/cert.pem" ,
28- MessageId : "test-message-id" ,
29- Message : message ,
30- MessageAttributes : { } ,
31- Type : "Notification" ,
32- UnsubscribeUrl : "https://sns.eu-west-2.amazonaws.com/unsubscribe" ,
33- TopicArn : "arn:aws:sns:eu-west-2:123456789012:test-topic" ,
34- Subject : "Test Subject" ,
35- ...overrides ,
23+ messageId,
24+ receiptHandle : "test-receipt-handle" ,
25+ body,
26+ attributes : {
27+ ApproximateReceiveCount : "1" ,
28+ SentTimestamp : "1704801600000" ,
29+ SenderId : "123456789012" ,
30+ ApproximateFirstReceiveTimestamp : "1704801600000" ,
3631 } ,
32+ messageAttributes : { } ,
33+ md5OfBody : "test-md5" ,
34+ eventSource : "aws:sqs" ,
35+ eventSourceARN : "arn:aws:sqs:eu-west-2:123456789012:test-queue.fifo" ,
36+ awsRegion : "eu-west-2" ,
3737 } ;
3838}
3939
40- function buildExpectedSnsWrapper ( record : SNSEventRecord ) : object {
41- return {
40+ /**
41+ * Creates an SNS notification wrapper as it would appear in the SQS message body
42+ * when raw_message_delivery is false.
43+ */
44+ function createSnsNotificationWrapper (
45+ message : string ,
46+ overrides : Partial < {
47+ MessageId : string ;
48+ TopicArn : string ;
49+ Subject : string ;
50+ } > = { } ,
51+ ) : string {
52+ return JSON . stringify ( {
4253 Type : "Notification" ,
43- MessageId : record . Sns . MessageId ,
44- TopicArn : record . Sns . TopicArn ,
45- Subject : record . Sns . Subject ,
46- Message : record . Sns . Message ,
47- Timestamp : record . Sns . Timestamp ,
48- SignatureVersion : record . Sns . SignatureVersion ,
49- Signature : record . Sns . Signature ,
50- SigningCertUrl : record . Sns . SigningCertUrl ,
51- UnsubscribeUrl : record . Sns . UnsubscribeUrl ,
52- MessageAttributes : record . Sns . MessageAttributes ,
53- } ;
54+ MessageId : overrides . MessageId ?? "test-sns-message-id" ,
55+ TopicArn :
56+ overrides . TopicArn ??
57+ "arn:aws:sns:eu-west-2:123456789012:test-topic.fifo" ,
58+ Subject : overrides . Subject ?? "Test Subject" ,
59+ Message : message ,
60+ Timestamp : "2026-01-09T12:00:00.000Z" ,
61+ SignatureVersion : "1" ,
62+ Signature : "test-signature" ,
63+ SigningCertUrl : "https://sns.eu-west-2.amazonaws.com/cert.pem" ,
64+ UnsubscribeUrl : "https://sns.eu-west-2.amazonaws.com/unsubscribe" ,
65+ MessageAttributes : { } ,
66+ } ) ;
5467}
5568
5669describe ( "forwarder" , ( ) => {
@@ -74,13 +87,14 @@ describe("forwarder", () => {
7487 } ) ;
7588
7689 describe ( "createForwarder" , ( ) => {
77- it ( "should process a single SNS record and send to Firehose" , async ( ) => {
90+ it ( "should process a single SQS record and send to Firehose" , async ( ) => {
7891 const message = JSON . stringify ( { eventType : "test" , data : "value" } ) ;
79- const snsRecord = createSNSEventRecord ( message ) ;
80- const snsEvent = createSNSEvent ( [ snsRecord ] ) ;
92+ const snsWrapper = createSnsNotificationWrapper ( message ) ;
93+ const sqsRecord = createSQSRecord ( snsWrapper ) ;
94+ const sqsEvent = createSQSEvent ( [ sqsRecord ] ) ;
8195
8296 const handler = createForwarder ( mockDeps ) ;
83- await handler ( snsEvent , mockDeep < Context > ( ) , jest . fn ( ) ) ;
97+ await handler ( sqsEvent , mockDeep < Context > ( ) , jest . fn ( ) ) ;
8498
8599 expect ( mockFirehoseClient . send ) . toHaveBeenCalledTimes ( 1 ) ;
86100 expect ( mockFirehoseClient . send ) . toHaveBeenCalledWith (
@@ -89,89 +103,87 @@ describe("forwarder", () => {
89103
90104 const sentCommand = mockFirehoseClient . send . mock
91105 . calls [ 0 ] [ 0 ] as PutRecordCommand ;
92- const expectedWrapper = buildExpectedSnsWrapper ( snsRecord ) ;
93106 expect ( sentCommand . input ) . toEqual ( {
94107 DeliveryStreamName : mockDeliveryStreamName ,
95108 Record : {
96- Data : Buffer . from ( `${ JSON . stringify ( expectedWrapper ) } \n` ) ,
109+ Data : Buffer . from ( `${ snsWrapper } \n` ) ,
97110 } ,
98111 } ) ;
99112 } ) ;
100113
101- it ( "should process multiple SNS records and send to Firehose" , async ( ) => {
114+ it ( "should process multiple SQS records and send to Firehose" , async ( ) => {
102115 const message1 = JSON . stringify ( { eventType : "test1" } ) ;
103116 const message2 = JSON . stringify ( { eventType : "test2" } ) ;
104117 const message3 = JSON . stringify ( { eventType : "test3" } ) ;
105118
106- const snsRecord1 = createSNSEventRecord ( message1 , {
119+ const snsWrapper1 = createSnsNotificationWrapper ( message1 , {
107120 MessageId : "msg-1" ,
108121 } ) ;
109- const snsRecord2 = createSNSEventRecord ( message2 , {
122+ const snsWrapper2 = createSnsNotificationWrapper ( message2 , {
110123 MessageId : "msg-2" ,
111124 } ) ;
112- const snsRecord3 = createSNSEventRecord ( message3 , {
125+ const snsWrapper3 = createSnsNotificationWrapper ( message3 , {
113126 MessageId : "msg-3" ,
114127 } ) ;
115128
116- const snsEvent = createSNSEvent ( [ snsRecord1 , snsRecord2 , snsRecord3 ] ) ;
129+ const sqsEvent = createSQSEvent ( [
130+ createSQSRecord ( snsWrapper1 , "sqs-1" ) ,
131+ createSQSRecord ( snsWrapper2 , "sqs-2" ) ,
132+ createSQSRecord ( snsWrapper3 , "sqs-3" ) ,
133+ ] ) ;
117134
118135 const handler = createForwarder ( mockDeps ) ;
119- await handler ( snsEvent , mockDeep < Context > ( ) , jest . fn ( ) ) ;
136+ await handler ( sqsEvent , mockDeep < Context > ( ) , jest . fn ( ) ) ;
120137
121138 expect ( mockFirehoseClient . send ) . toHaveBeenCalledTimes ( 3 ) ;
122139
123140 const sentCommands = mockFirehoseClient . send . mock . calls . map (
124- ( call : [ PutRecordCommand ] ) => call [ 0 ] ,
141+ ( call ) => call [ 0 ] as PutRecordCommand ,
125142 ) ;
126143
127144 expect ( sentCommands [ 0 ] . input ) . toEqual ( {
128145 DeliveryStreamName : mockDeliveryStreamName ,
129146 Record : {
130- Data : Buffer . from (
131- `${ JSON . stringify ( buildExpectedSnsWrapper ( snsRecord1 ) ) } \n` ,
132- ) ,
147+ Data : Buffer . from ( `${ snsWrapper1 } \n` ) ,
133148 } ,
134149 } ) ;
135150
136151 expect ( sentCommands [ 1 ] . input ) . toEqual ( {
137152 DeliveryStreamName : mockDeliveryStreamName ,
138153 Record : {
139- Data : Buffer . from (
140- `${ JSON . stringify ( buildExpectedSnsWrapper ( snsRecord2 ) ) } \n` ,
141- ) ,
154+ Data : Buffer . from ( `${ snsWrapper2 } \n` ) ,
142155 } ,
143156 } ) ;
144157
145158 expect ( sentCommands [ 2 ] . input ) . toEqual ( {
146159 DeliveryStreamName : mockDeliveryStreamName ,
147160 Record : {
148- Data : Buffer . from (
149- `${ JSON . stringify ( buildExpectedSnsWrapper ( snsRecord3 ) ) } \n` ,
150- ) ,
161+ Data : Buffer . from ( `${ snsWrapper3 } \n` ) ,
151162 } ,
152163 } ) ;
153164 } ) ;
154165
155- it ( "should handle empty SNS event with no records" , async ( ) => {
156- const snsEvent = createSNSEvent ( [ ] ) ;
166+ it ( "should handle empty SQS event with no records" , async ( ) => {
167+ const sqsEvent = createSQSEvent ( [ ] ) ;
157168
158169 const handler = createForwarder ( mockDeps ) ;
159- await handler ( snsEvent , mockDeep < Context > ( ) , jest . fn ( ) ) ;
170+ await handler ( sqsEvent , mockDeep < Context > ( ) , jest . fn ( ) ) ;
160171
161172 expect ( mockFirehoseClient . send ) . not . toHaveBeenCalled ( ) ;
162173 } ) ;
163174
164- it ( "should wrap message in SNS notification format " , async ( ) => {
175+ it ( "should forward the SNS notification wrapper from SQS body to Firehose " , async ( ) => {
165176 const message = JSON . stringify ( { key : "value" } ) ;
166- const snsRecord = createSNSEventRecord ( message , {
177+ const snsWrapper = createSnsNotificationWrapper ( message , {
167178 MessageId : "unique-msg-id" ,
168- TopicArn : "arn:aws:sns:eu-west-2:123456789012:my-topic" ,
179+ TopicArn : "arn:aws:sns:eu-west-2:123456789012:my-topic.fifo " ,
169180 Subject : "My Subject" ,
170181 } ) ;
171- const snsEvent = createSNSEvent ( [ snsRecord ] ) ;
182+ const sqsRecord = createSQSRecord ( snsWrapper ) ;
183+ const sqsEvent = createSQSEvent ( [ sqsRecord ] ) ;
172184
173185 const handler = createForwarder ( mockDeps ) ;
174- await handler ( snsEvent , mockDeep < Context > ( ) , jest . fn ( ) ) ;
186+ await handler ( sqsEvent , mockDeep < Context > ( ) , jest . fn ( ) ) ;
175187
176188 const sentCommand = mockFirehoseClient . send . mock
177189 . calls [ 0 ] [ 0 ] as PutRecordCommand ;
@@ -181,7 +193,7 @@ describe("forwarder", () => {
181193 expect ( parsedData ) . toEqual ( {
182194 Type : "Notification" ,
183195 MessageId : "unique-msg-id" ,
184- TopicArn : "arn:aws:sns:eu-west-2:123456789012:my-topic" ,
196+ TopicArn : "arn:aws:sns:eu-west-2:123456789012:my-topic.fifo " ,
185197 Subject : "My Subject" ,
186198 Message : message ,
187199 Timestamp : "2026-01-09T12:00:00.000Z" ,
@@ -193,13 +205,14 @@ describe("forwarder", () => {
193205 } ) ;
194206 } ) ;
195207
196- it ( "should append newline to wrapped message for JSON Lines format" , async ( ) => {
208+ it ( "should append newline to message for JSON Lines format" , async ( ) => {
197209 const message = JSON . stringify ( { key : "value" } ) ;
198- const snsRecord = createSNSEventRecord ( message ) ;
199- const snsEvent = createSNSEvent ( [ snsRecord ] ) ;
210+ const snsWrapper = createSnsNotificationWrapper ( message ) ;
211+ const sqsRecord = createSQSRecord ( snsWrapper ) ;
212+ const sqsEvent = createSQSEvent ( [ sqsRecord ] ) ;
200213
201214 const handler = createForwarder ( mockDeps ) ;
202- await handler ( snsEvent , mockDeep < Context > ( ) , jest . fn ( ) ) ;
215+ await handler ( sqsEvent , mockDeep < Context > ( ) , jest . fn ( ) ) ;
203216
204217 const sentCommand = mockFirehoseClient . send . mock
205218 . calls [ 0 ] [ 0 ] as PutRecordCommand ;
0 commit comments