11import json
2+ from json import JSONDecodeError
23
34import boto3
45import copy
56from unittest import TestCase
6- from unittest .mock import patch
7+ from unittest .mock import patch , Mock , ANY
78
89import botocore
910from moto import mock_aws
@@ -77,6 +78,8 @@ def setUp(self):
7778
7879 self .logger_patcher = patch ("batch_processor_filter_service.logger" )
7980 self .mock_logger = self .logger_patcher .start ()
81+ self .exception_decorator_logger_patcher = patch ("exception_decorator.logger" )
82+ self .mock_exception_decorator_logger = self .exception_decorator_logger_patcher .start ()
8083 self .firehose_log_patcher = patch ("batch_processor_filter_service.send_log_to_firehose" )
8184 self .mock_firehose_send_log = self .firehose_log_patcher .start ()
8285
@@ -90,8 +93,7 @@ def tearDown(self):
9093 s3_client .delete_object (Bucket = bucket_name , Key = obj ["Key" ])
9194 s3_client .delete_bucket (Bucket = bucket_name )
9295
93- self .logger_patcher .stop ()
94- self .firehose_log_patcher .stop ()
96+ patch .stopall ()
9597
9698 def _assert_source_file_moved (self , filename : str ):
9799 """Check used in the duplicate scenario to validate that the original uploaded file is moved"""
@@ -110,7 +112,7 @@ def _assert_ack_file_created(self, ack_file_key: str):
110112
111113 def test_lambda_handler_raises_error_when_empty_batch_received (self ):
112114 with self .assertRaises (InvalidBatchSizeError ) as exc :
113- lambda_handler ({"Records" : []}, {} )
115+ lambda_handler ({"Records" : []}, Mock () )
114116
115117 self .assertEqual (str (exc .exception ), "Received 0 records, expected 1" )
116118
@@ -119,10 +121,24 @@ def test_lambda_handler_raises_error_when_more_than_one_record_in_batch(self):
119121 lambda_handler ({"Records" : [
120122 make_sqs_record (self .default_batch_file_event ),
121123 make_sqs_record (self .default_batch_file_event ),
122- ]}, {} )
124+ ]}, Mock () )
123125
124126 self .assertEqual (str (exc .exception ), "Received 2 records, expected 1" )
125127
128+ def test_lambda_handler_decorator_logs_unhandled_exceptions (self ):
129+ """The exception decorator should log the error when an unhandled exception occurs"""
130+ with self .assertRaises (JSONDecodeError ):
131+ lambda_handler ({"Records" : [
132+ {
133+ "body" : "{'malformed}"
134+ }
135+ ]}, Mock ())
136+
137+ self .mock_exception_decorator_logger .error .assert_called_once_with (
138+ "An unhandled exception occurred in the batch processor filter Lambda" ,
139+ exc_info = ANY
140+ )
141+
126142 def test_lambda_handler_handles_duplicate_file_scenario (self ):
127143 """Should update the audit table status to duplicate for the incoming record"""
128144 # Add the duplicate entry that has already been processed
@@ -137,61 +153,69 @@ def test_lambda_handler_handles_duplicate_file_scenario(self):
137153 # Create the source file in S3
138154 s3_client .put_object (Bucket = self .mock_source_bucket , Key = test_file_name )
139155
140- lambda_handler ({"Records" : [make_sqs_record (duplicate_file_event )]}, {} )
156+ lambda_handler ({"Records" : [make_sqs_record (duplicate_file_event )]}, Mock () )
141157
142158 status = get_audit_entry_status_by_id (dynamodb_client , AUDIT_TABLE_NAME , duplicate_file_event ["message_id" ])
143- self .assertEqual (status , "Not processed - duplicate " )
159+ self .assertEqual (status , "Not processed - Duplicate " )
144160
145161 sqs_messages = sqs_client .receive_message (QueueUrl = self .mock_queue_url )
146162 self .assertEqual (sqs_messages .get ("Messages" , []), [])
147163 self ._assert_source_file_moved (test_file_name )
148164 self ._assert_ack_file_created ("Menacwy_Vaccinations_v5_TEST_20250820T10210000_InfAck_20250826T14372600.csv" )
149165
150- self .mock_logger .info .assert_called_once_with (
166+ self .mock_logger .error .assert_called_once_with (
151167 "A duplicate file has already been processed. Filename: %s" ,
152168 test_file_name
153169 )
154170
155171 def test_lambda_handler_raises_error_when_event_already_processing_for_supplier_and_vacc_type (self ):
156172 """Should raise exception so that the event is returned to the originating queue to be retried later"""
157- # Add an audit entry for a batch event that is already processing
158- add_entry_to_mock_table (dynamodb_client , AUDIT_TABLE_NAME , self .default_batch_file_event , FileStatus .PROCESSING )
159-
160- test_event : BatchFileCreatedEvent = BatchFileCreatedEvent (
161- message_id = "3b60c4f7-ef67-43c7-8f0d-4faee04d7d0e" ,
162- vaccine_type = "MENACWY" , # Same vacc type
163- supplier = "TESTSUPPLIER" , # Same supplier
164- permission = ["some-permissions" ],
165- filename = "Menacwy_Vaccinations_v5_TEST_20250826T15003000.csv" , # Different timestamp
166- created_at_formatted_string = "20250826T15003000"
167- )
168- # Add the audit record for the incoming event
169- add_entry_to_mock_table (dynamodb_client , AUDIT_TABLE_NAME , test_event , FileStatus .QUEUED )
170-
171- with self .assertRaises (EventAlreadyProcessingForSupplierAndVaccTypeError ) as exc :
172- lambda_handler ({"Records" : [make_sqs_record (test_event )]}, {})
173-
174- self .assertEqual (
175- str (exc .exception ),
176- "Batch event already processing for supplier: TESTSUPPLIER and vacc type: MENACWY"
177- )
178-
179- status = get_audit_entry_status_by_id (dynamodb_client , AUDIT_TABLE_NAME , test_event ["message_id" ])
180- self .assertEqual (status , "Queued" )
181-
182- sqs_messages = sqs_client .receive_message (QueueUrl = self .mock_queue_url )
183- self .assertEqual (sqs_messages .get ("Messages" , []), [])
184-
185- self .mock_logger .info .assert_called_once_with (
186- "Batch event already being processed for supplier and vacc type. Filename: %s" ,
187- "Menacwy_Vaccinations_v5_TEST_20250826T15003000.csv"
188- )
173+ test_cases = {
174+ ("Event is already being processed for supplier + vacc type queue" , FileStatus .PROCESSING ),
175+ ("There is a failed event to be checked in supplier + vacc type queue" , FileStatus .FAILED )
176+ }
177+
178+ for msg , file_status in test_cases :
179+ self .mock_logger .reset_mock ()
180+ with self .subTest (msg = msg ):
181+ # Add an audit entry for a batch event that is already processing or failed
182+ add_entry_to_mock_table (dynamodb_client , AUDIT_TABLE_NAME , self .default_batch_file_event , file_status )
183+
184+ test_event : BatchFileCreatedEvent = BatchFileCreatedEvent (
185+ message_id = "3b60c4f7-ef67-43c7-8f0d-4faee04d7d0e" ,
186+ vaccine_type = "MENACWY" , # Same vacc type
187+ supplier = "TESTSUPPLIER" , # Same supplier
188+ permission = ["some-permissions" ],
189+ filename = "Menacwy_Vaccinations_v5_TEST_20250826T15003000.csv" , # Different timestamp
190+ created_at_formatted_string = "20250826T15003000"
191+ )
192+ # Add the audit record for the incoming event
193+ add_entry_to_mock_table (dynamodb_client , AUDIT_TABLE_NAME , test_event , FileStatus .QUEUED )
194+
195+ with self .assertRaises (EventAlreadyProcessingForSupplierAndVaccTypeError ) as exc :
196+ lambda_handler ({"Records" : [make_sqs_record (test_event )]}, Mock ())
197+
198+ self .assertEqual (
199+ str (exc .exception ),
200+ "Batch event already processing for supplier: TESTSUPPLIER and vacc type: MENACWY"
201+ )
202+
203+ status = get_audit_entry_status_by_id (dynamodb_client , AUDIT_TABLE_NAME , test_event ["message_id" ])
204+ self .assertEqual (status , "Queued" )
205+
206+ sqs_messages = sqs_client .receive_message (QueueUrl = self .mock_queue_url )
207+ self .assertEqual (sqs_messages .get ("Messages" , []), [])
208+
209+ self .mock_logger .info .assert_called_once_with (
210+ "Batch event already processing for supplier and vacc type. Filename: %s" ,
211+ "Menacwy_Vaccinations_v5_TEST_20250826T15003000.csv"
212+ )
189213
190214 def test_lambda_handler_processes_event_successfully (self ):
191215 """Should update the audit entry status to Processing and forward to SQS"""
192216 add_entry_to_mock_table (dynamodb_client , AUDIT_TABLE_NAME , self .default_batch_file_event , FileStatus .QUEUED )
193217
194- lambda_handler ({"Records" : [make_sqs_record (self .default_batch_file_event )]}, {} )
218+ lambda_handler ({"Records" : [make_sqs_record (self .default_batch_file_event )]}, Mock () )
195219
196220 status = get_audit_entry_status_by_id (dynamodb_client , AUDIT_TABLE_NAME ,
197221 self .default_batch_file_event ["message_id" ])
@@ -223,7 +247,7 @@ def test_lambda_handler_processes_event_successfully_when_event_for_same_supplie
223247 )
224248 add_entry_to_mock_table (dynamodb_client , AUDIT_TABLE_NAME , test_event , FileStatus .QUEUED )
225249
226- lambda_handler ({"Records" : [make_sqs_record (test_event )]}, {} )
250+ lambda_handler ({"Records" : [make_sqs_record (test_event )]}, Mock () )
227251
228252 status = get_audit_entry_status_by_id (dynamodb_client , AUDIT_TABLE_NAME , test_event ["message_id" ])
229253 self .assertEqual (status , "Processing" )
0 commit comments