33import unittest
44import json
55from decimal import Decimal
6+ from json import JSONDecodeError
67from unittest .mock import patch
78from datetime import datetime , timedelta , timezone
8- from moto import mock_s3 , mock_kinesis , mock_firehose
9+ from moto import mock_s3 , mock_kinesis , mock_firehose , mock_dynamodb
910from boto3 import client as boto3_client
1011
1112from tests .utils_for_recordprocessor_tests .utils_for_recordprocessor_tests import (
1213 GenericSetUp ,
1314 GenericTearDown ,
15+ add_entry_to_table ,
16+ assert_audit_table_entry
1417)
1518from tests .utils_for_recordprocessor_tests .values_for_recordprocessor_tests import (
1619 MockFileDetails ,
2528from tests .utils_for_recordprocessor_tests .mock_environment_variables import MOCK_ENVIRONMENT_DICT , BucketNames , Kinesis
2629
2730with patch ("os.environ" , MOCK_ENVIRONMENT_DICT ):
28- from constants import Diagnostics
31+ from constants import Diagnostics , FileStatus , FileNotProcessedReason , AUDIT_TABLE_NAME , AuditTableKeys
2932 from batch_processor import main
3033
3134s3_client = boto3_client ("s3" , region_name = REGION_NAME )
3235kinesis_client = boto3_client ("kinesis" , region_name = REGION_NAME )
3336firehose_client = boto3_client ("firehose" , region_name = REGION_NAME )
37+ dynamo_db_client = boto3_client ("dynamodb" , region_name = REGION_NAME )
3438yesterday = datetime .now (timezone .utc ) - timedelta (days = 1 )
3539mock_rsv_emis_file = MockFileDetails .rsv_emis
3640
3741
3842@patch .dict ("os.environ" , MOCK_ENVIRONMENT_DICT )
43+ @mock_dynamodb
3944@mock_s3
4045@mock_kinesis
4146@mock_firehose
4247class TestRecordProcessor (unittest .TestCase ):
4348 """Tests for main function for RecordProcessor"""
4449
4550 def setUp (self ) -> None :
46- GenericSetUp (s3_client , firehose_client , kinesis_client )
51+ GenericSetUp (s3_client , firehose_client , kinesis_client , dynamo_db_client )
4752
4853 redis_patcher = patch ("mappings.redis_client" )
54+ batch_processor_logger_patcher = patch ("batch_processor.logger" )
4955 self .addCleanup (redis_patcher .stop )
56+ self .mock_batch_processor_logger = batch_processor_logger_patcher .start ()
5057 mock_redis_client = redis_patcher .start ()
5158 mock_redis_client .hget .return_value = json .dumps ([{
5259 "code" : "55735004" ,
5360 "term" : "Respiratory syncytial virus infection (disorder)"
5461 }])
5562
5663 def tearDown (self ) -> None :
57- GenericTearDown (s3_client , firehose_client , kinesis_client )
64+ GenericTearDown (s3_client , firehose_client , kinesis_client , dynamo_db_client )
65+ patch .stopall ()
5866
5967 @staticmethod
6068 def upload_source_files (source_file_content ): # pylint: disable=dangerous-default-value
@@ -148,9 +156,11 @@ def test_e2e_full_permissions(self):
148156 Tests that file containing CREATE, UPDATE and DELETE is successfully processed when the supplier has
149157 full permissions.
150158 """
159+ test_file = mock_rsv_emis_file
151160 self .upload_source_files (ValidMockFileContent .with_new_and_update_and_delete )
161+ add_entry_to_table (test_file , FileStatus .PROCESSING )
152162
153- main (mock_rsv_emis_file .event_full_permissions )
163+ main (test_file .event_full_permissions )
154164
155165 # Assertion case tuples are stuctured as
156166 # (test_name, index, expected_kinesis_data_ignoring_fhir_json,expect_success)
@@ -176,15 +186,18 @@ def test_e2e_full_permissions(self):
176186 ]
177187 self .make_inf_ack_assertions (file_details = mock_rsv_emis_file , passed_validation = True )
178188 self .make_kinesis_assertions (assertion_cases )
189+ assert_audit_table_entry (test_file , FileStatus .PREPROCESSED )
179190
180191 def test_e2e_partial_permissions (self ):
181192 """
182193 Tests that file containing CREATE, UPDATE and DELETE is successfully processed when the supplier only has CREATE
183194 permissions.
184195 """
196+ test_file = mock_rsv_emis_file
197+ add_entry_to_table (test_file , FileStatus .PROCESSING )
185198 self .upload_source_files (ValidMockFileContent .with_new_and_update_and_delete )
186199
187- main (mock_rsv_emis_file .event_create_permissions_only )
200+ main (test_file .event_create_permissions_only )
188201
189202 # Assertion case tuples are stuctured as
190203 # (test_name, index, expected_kinesis_data_ignoring_fhir_json,expect_success)
@@ -226,15 +239,18 @@ def test_e2e_partial_permissions(self):
226239 ]
227240 self .make_inf_ack_assertions (file_details = mock_rsv_emis_file , passed_validation = True )
228241 self .make_kinesis_assertions (assertion_cases )
242+ assert_audit_table_entry (test_file , FileStatus .PREPROCESSED )
229243
230244 def test_e2e_no_required_permissions (self ):
231245 """
232246 Tests that file containing UPDATE and DELETE is successfully processed when the supplier has CREATE permissions
233247 only.
234248 """
249+ test_file = mock_rsv_emis_file
250+ add_entry_to_table (test_file , FileStatus .PROCESSING )
235251 self .upload_source_files (ValidMockFileContent .with_update_and_delete )
236252
237- main (mock_rsv_emis_file .event_create_permissions_only )
253+ main (test_file .event_create_permissions_only )
238254
239255 kinesis_records = kinesis_client .get_records (ShardIterator = self .get_shard_iterator (), Limit = 10 )["Records" ]
240256 self .assertEqual (len (kinesis_records ), 2 )
@@ -244,27 +260,39 @@ def test_e2e_no_required_permissions(self):
244260 self .assertIn ("diagnostics" , data_dict )
245261 self .assertNotIn ("fhir_json" , data_dict )
246262 self .make_inf_ack_assertions (file_details = mock_rsv_emis_file , passed_validation = True )
263+ assert_audit_table_entry (test_file , FileStatus .PREPROCESSED )
247264
248265 def test_e2e_no_permissions (self ):
249266 """
250267 Tests that file containing UPDATE and DELETE is successfully processed when the supplier has no permissions.
251268 """
269+ test_file = mock_rsv_emis_file
270+ add_entry_to_table (test_file , FileStatus .PROCESSING )
252271 self .upload_source_files (ValidMockFileContent .with_update_and_delete )
253272
254- main (mock_rsv_emis_file .event_no_permissions )
273+ main (test_file .event_no_permissions )
255274
256275 kinesis_records = kinesis_client .get_records (ShardIterator = self .get_shard_iterator (), Limit = 10 )["Records" ]
276+ table_entry = dynamo_db_client .get_item (
277+ TableName = AUDIT_TABLE_NAME , Key = {AuditTableKeys .MESSAGE_ID : {"S" : test_file .message_id }}
278+ ).get ("Item" )
257279 self .assertEqual (len (kinesis_records ), 0 )
258280 self .make_inf_ack_assertions (file_details = mock_rsv_emis_file , passed_validation = False )
281+ self .assertDictEqual (table_entry , {
282+ ** test_file .audit_table_entry ,
283+ "status" : {"S" : f"{ FileStatus .NOT_PROCESSED } - { FileNotProcessedReason .UNAUTHORISED } " },
284+ "error_details" : {"S" : "EMIS does not have permissions to perform any of the requested actions." }
285+ })
259286
260287 def test_e2e_invalid_action_flags (self ):
261288 """Tests that file is successfully processed when the ACTION_FLAG field is empty or invalid."""
262-
289+ test_file = mock_rsv_emis_file
290+ add_entry_to_table (test_file , FileStatus .PROCESSING )
263291 self .upload_source_files (
264292 ValidMockFileContent .with_update_and_delete .replace ("update" , "" ).replace ("delete" , "INVALID" )
265293 )
266294
267- main (mock_rsv_emis_file .event_full_permissions )
295+ main (test_file .event_full_permissions )
268296
269297 expected_kinesis_data = {
270298 "diagnostics" : {
@@ -288,14 +316,16 @@ def test_e2e_invalid_action_flags(self):
288316 def test_e2e_differing_amounts_of_data (self ):
289317 """Tests that file containing rows with differing amounts of data present is processed as expected"""
290318 # Create file content with different amounts of data present in each row
319+ test_file = mock_rsv_emis_file
320+ add_entry_to_table (test_file , FileStatus .PROCESSING )
291321 headers = "|" .join (MockFieldDictionaries .all_fields .keys ())
292322 all_fields_values = "|" .join (f'"{ v } "' for v in MockFieldDictionaries .all_fields .values ())
293323 mandatory_fields_only_values = "|" .join (f'"{ v } "' for v in MockFieldDictionaries .mandatory_fields_only .values ())
294324 critical_fields_only_values = "|" .join (f'"{ v } "' for v in MockFieldDictionaries .critical_fields_only .values ())
295325 file_content = f"{ headers } \n { all_fields_values } \n { mandatory_fields_only_values } \n { critical_fields_only_values } "
296326 self .upload_source_files (file_content )
297327
298- main (mock_rsv_emis_file .event_full_permissions )
328+ main (test_file .event_full_permissions )
299329
300330 all_fields_row_expected_kinesis_data = {
301331 "operation_requested" : "UPDATE" ,
@@ -329,6 +359,8 @@ def test_e2e_kinesis_failed(self):
329359 Tests that, for a file with valid content and supplier with full permissions, when the kinesis send fails, the
330360 ack file is created and documents an error.
331361 """
362+ test_file = mock_rsv_emis_file
363+ add_entry_to_table (test_file , FileStatus .PROCESSING )
332364 self .upload_source_files (ValidMockFileContent .with_new_and_update )
333365 # Delete the kinesis stream, to cause kinesis send to fail
334366 kinesis_client .delete_stream (StreamName = Kinesis .STREAM_NAME , EnforceConsumerDeletion = True )
@@ -340,11 +372,14 @@ def test_e2e_kinesis_failed(self):
340372 ): # noqa: E999
341373 mock_time .time .side_effect = [1672531200 , 1672531200.123456 ]
342374 mock_datetime .now .return_value = datetime (2024 , 1 , 1 , 12 , 0 , 0 )
343- main (mock_rsv_emis_file .event_full_permissions )
375+ main (test_file .event_full_permissions )
344376
345377 # Since the failure occured at row level, not file level, the ack file should still be created
346378 # and firehose logs should indicate a successful file level validation
347- self .make_inf_ack_assertions (file_details = mock_rsv_emis_file , passed_validation = True )
379+ table_entry = dynamo_db_client .get_item (
380+ TableName = AUDIT_TABLE_NAME , Key = {AuditTableKeys .MESSAGE_ID : {"S" : test_file .message_id }}
381+ ).get ("Item" )
382+ self .make_inf_ack_assertions (file_details = test_file , passed_validation = True )
348383 expected_log_data = {
349384 "function_name" : "record_processor_file_level_validation" ,
350385 "date_time" : "2024-01-01 12:00:00" ,
@@ -357,6 +392,25 @@ def test_e2e_kinesis_failed(self):
357392 "message" : "Successfully sent for record processing" ,
358393 }
359394 mock_send_log_to_firehose .assert_called_with (expected_log_data )
395+ self .assertDictEqual (table_entry , {
396+ ** test_file .audit_table_entry ,
397+ "status" : {"S" : FileStatus .FAILED },
398+ "error_details" : {"S" : "An error occurred (ResourceNotFoundException) when calling the PutRecord operation"
399+ ": Stream imms-batch-internal-dev-processingdata-stream under account 123456789012"
400+ " not found." }
401+ })
402+
403+ def test_e2e_error_is_logged_if_invalid_json_provided (self ):
404+ """This scenario should not happen. If it does, it means our batch processing system config is broken and we
405+ have received malformed content from SQS -> EventBridge. In this case we log the error so we will be alerted.
406+ However, we cannot do anything with the AuditDB record as we cannot retrieve information from the event"""
407+ malformed_event = '{"test": {}'
408+ main (malformed_event )
409+
410+ logged_message = self .mock_batch_processor_logger .error .call_args [0 ][0 ]
411+ exception = self .mock_batch_processor_logger .error .call_args [0 ][1 ]
412+ self .assertEqual (logged_message , "Error decoding incoming message: %s" )
413+ self .assertIsInstance (exception , JSONDecodeError )
360414
361415
362416if __name__ == "__main__" :
0 commit comments