1+ import json
12import unittest
23from unittest .mock import MagicMock , patch
34
78from steps .parsing import parse , parse_event_type
89
910
11+ class Context :
12+ function_version = "$LATEST"
13+ invoked_function_arn = (
14+ "arn:aws:lambda:us-east-1:123456789012:function:datadog-forwarder"
15+ )
16+ function_name = "datadog-forwarder"
17+ memory_limit_in_mb = "128"
18+
19+
20+ def _make_s3_event (bucket , key ):
21+ return {"Records" : [{"s3" : {"bucket" : {"name" : bucket }, "object" : {"key" : key }}}]}
22+
23+
24+ def _make_sqs_record (body , message_id = "msg-1" ):
25+ return {
26+ "messageId" : message_id ,
27+ "body" : body if isinstance (body , str ) else json .dumps (body ),
28+ "eventSource" : "aws:sqs" ,
29+ "eventSourceARN" : "arn:aws:sqs:us-east-1:123456789012:q" ,
30+ }
31+
32+
1033class TestParseEventSource (unittest .TestCase ):
1134 def test_aws_source_if_none_found (self ):
1235 self .assertEqual (parse_event_source ({}, "asdfalsfhalskjdfhalsjdf" ), "aws" )
@@ -187,7 +210,7 @@ def test_get_service_from_tags_removing_duplicates(self):
187210
188211class TestParseEventType (unittest .TestCase ):
189212 def test_parse_eventbridge_s3_event_type (self ):
190- """Test that EventBridge S3 events are correctly identified as EventBridge S3 type """
213+ """EventBridge S3 events are correctly identified"""
191214 eventbridge_s3_event = {
192215 "version" : "0" ,
193216 "id" : "test-event-id" ,
@@ -202,53 +225,147 @@ def test_parse_eventbridge_s3_event_type(self):
202225 "object" : {"key" : "my-key.log" },
203226 },
204227 }
205-
206- event_type = parse_event_type (eventbridge_s3_event )
207- self .assertEqual (event_type , AwsEventType .EVENTBRIDGE_S3 )
228+ self .assertEqual (parse_event_type (eventbridge_s3_event ), AwsEventType .EVENTBRIDGE_S3 )
208229
209230 def test_parse_direct_s3_event_type (self ):
210- """Test that direct S3 events are still correctly identified as S3 type"""
211- direct_s3_event = {
212- "Records" : [
213- {
214- "s3" : {
215- "bucket" : {"name" : "my-bucket" },
216- "object" : {"key" : "my-key" },
217- }
218- }
219- ]
220- }
221-
222- event_type = parse_event_type (direct_s3_event )
223- self .assertEqual (event_type , AwsEventType .S3 )
231+ """Direct S3 events are correctly identified"""
232+ self .assertEqual (
233+ parse_event_type (_make_s3_event ("my-bucket" , "my-key" )), AwsEventType .S3
234+ )
224235
225236 def test_parse_non_s3_eventbridge_event_type (self ):
226- """Test that non -S3 EventBridge events are identified as EVENTS type"""
237+ """Non -S3 EventBridge events are identified as EVENTS type"""
227238 eventbridge_other_event = {
228239 "version" : "0" ,
229240 "detail-type" : "EC2 Instance State-change Notification" ,
230241 "source" : "aws.ec2" ,
231242 "detail" : {"instance-id" : "i-1234567890abcdef0" , "state" : "terminated" },
232243 }
244+ self .assertEqual (parse_event_type (eventbridge_other_event ), AwsEventType .EVENTS )
233245
234- event_type = parse_event_type (eventbridge_other_event )
235- self .assertEqual (event_type , AwsEventType .EVENTS )
246+ def test_parse_sqs_event_type (self ):
247+ """SQS events are correctly identified"""
248+ sqs_event = {"Records" : [_make_sqs_record (_make_s3_event ("b" , "k" ))]}
249+ self .assertEqual (parse_event_type (sqs_event ), AwsEventType .SQS )
236250
251+ def test_direct_s3_event_not_detected_as_sqs (self ):
252+ """Direct S3 events must still be detected as S3, not SQS"""
253+ self .assertEqual (
254+ parse_event_type (_make_s3_event ("my-bucket" , "my-key" )), AwsEventType .S3
255+ )
237256
238- class TestEventBridgeS3Parsing (unittest .TestCase ):
239- class Context :
240- function_version = "$LATEST"
241- invoked_function_arn = (
242- "arn:aws:lambda:us-east-1:123456789012:function:datadog-forwarder"
257+ def test_sns_event_not_detected_as_sqs (self ):
258+ """SNS events must still be detected as SNS, not SQS"""
259+ sns_event = {"Records" : [{"Sns" : {"Message" : "hello" }}]}
260+ self .assertEqual (parse_event_type (sns_event ), AwsEventType .SNS )
261+
262+ def test_kinesis_event_not_detected_as_sqs (self ):
263+ """Kinesis events must still be detected as Kinesis, not SQS"""
264+ kinesis_event = {"Records" : [{"kinesis" : {"data" : "base64data" }}]}
265+ self .assertEqual (parse_event_type (kinesis_event ), AwsEventType .KINESIS )
266+
267+
268+ class TestSQSEventParsing (unittest .TestCase ):
269+ @patch ("steps.parsing.S3EventHandler" )
270+ def test_parse_sqs_s3_event (self , mock_s3_handler_cls ):
271+ """S3 event delivered via SQS is unwrapped and forwarded to S3EventHandler"""
272+ mock_s3_handler = mock_s3_handler_cls .return_value
273+ mock_s3_handler .handle .return_value = iter ([{"message" : "log line" }])
274+
275+ sqs_event = {
276+ "Records" : [_make_sqs_record (_make_s3_event ("my-bucket" , "my-key.log" ))]
277+ }
278+
279+ result = parse (sqs_event , Context (), MagicMock ())
280+
281+ mock_s3_handler .handle .assert_called_once ()
282+ inner_event = mock_s3_handler .handle .call_args .args [0 ]
283+ self .assertEqual (inner_event ["Records" ][0 ]["s3" ]["bucket" ]["name" ], "my-bucket" )
284+ self .assertEqual (len (result ), 1 )
285+ self .assertIn ("ddsourcecategory" , result [0 ])
286+ self .assertIn ("aws" , result [0 ])
287+ self .assertIn ("invoked_function_arn" , result [0 ]["aws" ])
288+
289+ @patch ("steps.parsing.S3EventHandler" )
290+ def test_parse_sqs_sns_s3_event (self , mock_s3_handler_cls ):
291+ """S3 event delivered via SNS -> SQS is unwrapped and forwarded to S3EventHandler"""
292+ mock_s3_handler = mock_s3_handler_cls .return_value
293+ mock_s3_handler .handle .return_value = iter ([{"message" : "log line" }])
294+
295+ sns_body = {
296+ "Type" : "Notification" ,
297+ "MessageId" : "a1b2c3d4" ,
298+ "TopicArn" : "arn:aws:sns:us-east-1:123456789012:my-topic" ,
299+ "Message" : json .dumps (_make_s3_event ("sns-bucket" , "sns-key.log" )),
300+ }
301+ sqs_event = {"Records" : [_make_sqs_record (sns_body )]}
302+
303+ result = parse (sqs_event , Context (), MagicMock ())
304+
305+ mock_s3_handler .handle .assert_called_once ()
306+ inner_event = mock_s3_handler .handle .call_args .args [0 ]
307+ self .assertEqual (
308+ inner_event ["Records" ][0 ]["s3" ]["bucket" ]["name" ], "sns-bucket"
243309 )
244- function_name = "datadog-forwarder"
245- memory_limit_in_mb = "128"
310+ self .assertEqual (len (result ), 1 )
311+
312+ @patch ("steps.parsing.S3EventHandler" )
313+ def test_parse_sqs_batch_multiple_records (self , mock_s3_handler_cls ):
314+ """Multiple SQS records in a single batch are all processed"""
315+ mock_s3_handler = mock_s3_handler_cls .return_value
316+ mock_s3_handler .handle .side_effect = [
317+ iter ([{"message" : "line1" }]),
318+ iter ([{"message" : "line2" }]),
319+ ]
320+
321+ sqs_event = {
322+ "Records" : [
323+ _make_sqs_record (_make_s3_event ("b1" , "k1" ), message_id = "msg-1" ),
324+ _make_sqs_record (_make_s3_event ("b2" , "k2" ), message_id = "msg-2" ),
325+ ]
326+ }
327+
328+ result = parse (sqs_event , Context (), MagicMock ())
329+
330+ self .assertEqual (mock_s3_handler .handle .call_count , 2 )
331+ self .assertEqual (len (result ), 2 )
332+
333+ @patch ("steps.parsing.S3EventHandler" )
334+ def test_parse_sqs_malformed_body_skipped (self , mock_s3_handler_cls ):
335+ """SQS records with malformed body are skipped without crashing"""
336+ mock_s3_handler = mock_s3_handler_cls .return_value
337+ mock_s3_handler .handle .return_value = iter ([{"message" : "ok" }])
246338
339+ sqs_event = {
340+ "Records" : [
341+ _make_sqs_record ("not valid json" , message_id = "bad" ),
342+ _make_sqs_record (_make_s3_event ("b" , "k" ), message_id = "good" ),
343+ ]
344+ }
345+
346+ result = parse (sqs_event , Context (), MagicMock ())
347+
348+ mock_s3_handler .handle .assert_called_once ()
349+ self .assertEqual (len (result ), 1 )
350+
351+ @patch ("steps.parsing.S3EventHandler" )
352+ def test_parse_sqs_unrecognized_body_skipped (self , mock_s3_handler_cls ):
353+ """SQS records with valid JSON but unrecognized content are skipped"""
354+ mock_s3_handler = mock_s3_handler_cls .return_value
355+
356+ sqs_event = {"Records" : [_make_sqs_record ({"foo" : "bar" })]}
357+
358+ result = parse (sqs_event , Context (), MagicMock ())
359+
360+ mock_s3_handler .handle .assert_not_called ()
361+ self .assertEqual (len (result ), 0 )
362+
363+
364+ class TestEventBridgeS3Parsing (unittest .TestCase ):
247365 @patch ("steps.parsing.S3EventHandler" )
248366 def test_parse_normalizes_eventbridge_s3_event_before_s3_handler (
249367 self , mock_s3_handler_cls
250368 ):
251- # Arrange: handler yields one log line; we only care about the input event it received
252369 mock_s3_handler = mock_s3_handler_cls .return_value
253370 mock_s3_handler .handle .return_value = iter ([{"message" : "ok" }])
254371
@@ -262,12 +379,8 @@ def test_parse_normalizes_eventbridge_s3_event_before_s3_handler(
262379 },
263380 }
264381
265- cache_layer = MagicMock ()
266-
267- # Act
268- _ = parse (eventbridge_event , self .Context (), cache_layer )
382+ parse (eventbridge_event , Context (), MagicMock ())
269383
270- # Assert: parse() passed a canonical S3-shaped event into the S3 handler
271384 mock_s3_handler .handle .assert_called_once ()
272385 (normalized_event ,) = mock_s3_handler .handle .call_args .args
273386
0 commit comments