Skip to content

Commit 54f63fa

Browse files
authored
VED-58 Batch processor error messages improved
Included supplier and vaccine type in error messages.
1 parent 73e5dcf commit 54f63fa

File tree

4 files changed

+216
-15
lines changed

4 files changed

+216
-15
lines changed

filenameprocessor/Makefile

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
build:
22
docker build -t imms-lambda-build -f Dockerfile .
33

4-
package:build
4+
package: build
55
mkdir -p build
66
docker run --rm -v $(shell pwd)/build:/build imms-lambda-build
77

88
test:
99
python -m unittest
1010

11-
coverage run:
12-
coverage run -m unittest discover
11+
coverage-run:
12+
coverage run -m unittest discover
1313

14-
coverage report:
14+
coverage-report:
1515
coverage report -m
1616

17-
coverage html:
18-
coverage html
19-
20-
.PHONY: build package test
17+
coverage-html:
18+
coverage html
19+
20+
.PHONY: build package test coverage-run coverage-report coverage-html

filenameprocessor/src/file_name_processor.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
(ODS code has multiple lengths)
77
"""
88

9+
import argparse
910
from uuid import uuid4
1011
from utils_for_filenameprocessor import get_created_at_formatted_string, move_file, invoke_filename_lambda
1112
from file_key_validation import validate_file_key
@@ -44,6 +45,9 @@ def handle_record(record) -> dict:
4445
logger.error("Error obtaining file_key: %s", error)
4546
return {"statusCode": 500, "message": "Failed to download file key", "error": str(error)}
4647

48+
vaccine_type = "unknown"
49+
supplier = "unknown"
50+
4751
if "data-sources" in bucket_name:
4852

4953
# The lambda is unintentionally invoked when a file is moved into a different folder in the source bucket.
@@ -56,8 +60,6 @@ def handle_record(record) -> dict:
5660
# Set default values for file-specific variables
5761
message_id = "Message id was not created"
5862
created_at_formatted_string = "created_at_time not identified"
59-
vaccine_type = "unknown"
60-
supplier = "unknown"
6163

6264
try:
6365
# If the record contains a message_id, then the lambda has been invoked by a file already in the queue
@@ -134,6 +136,8 @@ def handle_record(record) -> dict:
134136
"file_key": file_key,
135137
"message_id": message_id,
136138
"error": str(error),
139+
"vaccine_type": vaccine_type,
140+
"supplier": supplier
137141
}
138142

139143
elif "config" in bucket_name:
@@ -148,9 +152,21 @@ def handle_record(record) -> dict:
148152
return {"statusCode": 500, "message": message, "file_key": file_key, "error": str(error)}
149153

150154
else:
151-
logger.error("Unable to process file %s due to unexpected bucket name %s", file_key, bucket_name)
152-
message = f"Failed to process file due to unexpected bucket name {bucket_name}"
153-
return {"statusCode": 500, "message": message, "file_key": file_key}
155+
try:
156+
vaccine_type, supplier = validate_file_key(file_key)
157+
logger.error("Unable to process file %s due to unexpected bucket name %s", file_key, bucket_name)
158+
message = f"Failed to process file due to unexpected bucket name {bucket_name}"
159+
160+
return {"statusCode": 500, "message": message, "file_key": file_key,
161+
"vaccine_type": vaccine_type, "supplier": supplier}
162+
163+
except Exception as error:
164+
logger.error("Unable to process file due to unexpected bucket name %s and file key %s",
165+
bucket_name, file_key)
166+
message = f"Failed to process file due to unexpected bucket name {bucket_name} and file key {file_key}"
167+
168+
return {"statusCode": 500, "message": message, "file_key": file_key,
169+
"vaccine_type": vaccine_type, "supplier": supplier, "error": str(error)}
154170

155171

156172
def lambda_handler(event: dict, context) -> None: # pylint: disable=unused-argument
@@ -161,3 +177,27 @@ def lambda_handler(event: dict, context) -> None: # pylint: disable=unused-argu
161177
handle_record(record)
162178

163179
logger.info("Filename processor lambda task completed")
180+
181+
182+
def run_local():
183+
parser = argparse.ArgumentParser("file_name_processor")
184+
parser.add_argument("--bucket", required=True, help="Bucket name.", type=str)
185+
parser.add_argument("--key", required=True, help="Object key.", type=str)
186+
args = parser.parse_args()
187+
188+
event = {
189+
"Records": [
190+
{
191+
"s3": {
192+
"bucket": {"name": args.bucket},
193+
"object": {"key": args.key}
194+
}
195+
}
196+
]
197+
}
198+
print(event)
199+
print(lambda_handler(event=event, context={}))
200+
201+
202+
if __name__ == "__main__":
203+
run_local()

filenameprocessor/tests/test_lambda_handler.py

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Tests for lambda_handler"""
2-
2+
import sys
33
from unittest.mock import patch
44
from unittest import TestCase
55
from json import loads as json_loads
@@ -154,6 +154,14 @@ def get_audit_table_items():
154154
"""Return all items in the audit table"""
155155
return dynamodb_client.scan(TableName=AUDIT_TABLE_NAME).get("Items", [])
156156

157+
def test_lambda_handler_no_file_key_throws_exception(self):
158+
"""Tests if exception is thrown when file_key is not provided"""
159+
160+
broken_record = {"Records": [{"s3": {"bucket": {"name": "test"}}}]}
161+
with patch("file_name_processor.logger") as mock_logger:
162+
lambda_handler(broken_record, None)
163+
mock_logger.error.assert_called_once()
164+
157165
def test_lambda_handler_new_file_success_and_first_in_queue(self):
158166
"""
159167
Tests that for a new file, which passes validation and is the only file processing for the supplier_vaccineType
@@ -458,6 +466,29 @@ def setUp(self):
458466
def tearDown(self):
459467
GenericTearDown(s3_client, firehose_client, sqs_client, dynamodb_client)
460468

469+
def test_elasticcache_failure_handled(self):
470+
"Tests if elastic cache failure is handled when service fails to send message"
471+
event = {
472+
"s3": {
473+
"bucket": {"name": "my-config-bucket"}, # triggers 'config' branch
474+
"object": {"key": "testfile.csv"}
475+
}
476+
}
477+
478+
with patch("file_name_processor.upload_to_elasticache", side_effect=Exception("Upload failed")), \
479+
patch("file_name_processor.logger") as mock_logger:
480+
481+
result = handle_record(event)
482+
483+
self.assertEqual(result["statusCode"], 500)
484+
self.assertEqual(result["message"], "Failed to upload file content to cache")
485+
self.assertEqual(result["file_key"], "testfile.csv")
486+
self.assertIn("error", result)
487+
488+
mock_logger.error.assert_called_once()
489+
logged_msg = mock_logger.error.call_args[0][0]
490+
self.assertIn("Error uploading to cache", logged_msg)
491+
461492
def test_successful_processing_from_configs(self):
462493
"""Tests that the permissions config file content is uploaded to elasticache successfully"""
463494
fake_redis = fakeredis.FakeStrictRedis()
@@ -496,7 +527,7 @@ def test_successful_processing_from_configs(self):
496527
"file_key": ravs_rsv_file_details_1.file_key,
497528
"message_id": ravs_rsv_file_details_1.message_id,
498529
"vaccine_type": ravs_rsv_file_details_1.vaccine_type,
499-
"supplier": ravs_rsv_file_details_1.supplier,
530+
"supplier": ravs_rsv_file_details_1.supplier
500531
}
501532
self.assertEqual(result, expected_result)
502533

@@ -524,5 +555,104 @@ def test_successful_processing_from_configs(self):
524555
"file_key": ravs_rsv_file_details_2.file_key,
525556
"message_id": ravs_rsv_file_details_2.message_id,
526557
"error": "Initial file validation failed: RAVS does not have permissions for RSV",
558+
"vaccine_type": ravs_rsv_file_details_2.vaccine_type,
559+
"supplier": ravs_rsv_file_details_2.supplier
527560
}
528561
self.assertEqual(result, expected_result)
562+
563+
564+
@patch.dict("os.environ", MOCK_ENVIRONMENT_DICT)
565+
@mock_s3
566+
@mock_dynamodb
567+
@mock_sqs
568+
@mock_firehose
569+
class TestUnexpectedBucket(TestCase):
570+
"""Tests for lambda_handler when an unexpected bucket name is used"""
571+
572+
def setUp(self):
573+
GenericSetUp(s3_client, firehose_client, sqs_client, dynamodb_client)
574+
575+
def tearDown(self):
576+
GenericTearDown(s3_client, firehose_client, sqs_client, dynamodb_client)
577+
578+
def test_unexpected_bucket_name(self):
579+
"""Tests if unkown bucket name is handled in lambda_handler"""
580+
ravs_record = MockFileDetails.ravs_rsv_1
581+
record = {
582+
"s3": {
583+
"bucket": {"name": "unknown-bucket"},
584+
"object": {"key": ravs_record.file_key}
585+
}
586+
}
587+
588+
with patch("file_name_processor.logger") as mock_logger:
589+
result = handle_record(record)
590+
591+
self.assertEqual(result["statusCode"], 500)
592+
self.assertIn("unexpected bucket name", result["message"])
593+
self.assertEqual(result["file_key"], ravs_record.file_key)
594+
self.assertEqual(result["vaccine_type"], ravs_record.vaccine_type)
595+
self.assertEqual(result["supplier"], ravs_record.supplier)
596+
597+
mock_logger.error.assert_called_once()
598+
args = mock_logger.error.call_args[0]
599+
self.assertIn("Unable to process file", args[0])
600+
self.assertIn(ravs_record.file_key, args)
601+
self.assertIn("unknown-bucket", args)
602+
603+
def test_unexpected_bucket_name_and_filename_validation_fails(self):
604+
"""Tests if filename validation error is handled when bucket name is incorrect"""
605+
invalid_file_key = "InvalidVaccineType_Vaccinations_v5_YGM41_20240708T12130100.csv"
606+
record = {
607+
"s3": {
608+
"bucket": {"name": "unknown-bucket"},
609+
"object": {"key": invalid_file_key}
610+
}
611+
}
612+
613+
with patch("file_name_processor.logger") as mock_logger:
614+
result = handle_record(record)
615+
616+
self.assertEqual(result["statusCode"], 500)
617+
self.assertIn("unexpected bucket name", result["message"])
618+
self.assertEqual(result["file_key"], invalid_file_key)
619+
self.assertEqual(result["vaccine_type"], "unknown")
620+
self.assertEqual(result["supplier"], "unknown")
621+
622+
mock_logger.error.assert_called_once()
623+
args = mock_logger.error.call_args[0]
624+
self.assertIn("Unable to process file", args[0])
625+
self.assertIn(invalid_file_key, args)
626+
self.assertIn("unknown-bucket", args)
627+
628+
629+
class TestMainEntryPoint(TestCase):
630+
631+
def test_run_local_constructs_event_and_calls_lambda_handler(self):
632+
test_args = [
633+
"file_name_processor.py",
634+
"--bucket", "test-bucket",
635+
"--key", "some/path/file.csv"
636+
]
637+
638+
expected_event = {
639+
"Records": [
640+
{
641+
"s3": {
642+
"bucket": {"name": "test-bucket"},
643+
"object": {"key": "some/path/file.csv"}
644+
}
645+
}
646+
]
647+
}
648+
649+
with (
650+
patch.object(sys, "argv", test_args),
651+
patch("file_name_processor.lambda_handler") as mock_lambda_handler,
652+
patch("file_name_processor.print") as mock_print
653+
):
654+
import file_name_processor
655+
file_name_processor.run_local()
656+
657+
mock_lambda_handler.assert_called_once_with(event=expected_event, context={})
658+
mock_print.assert_called()

filenameprocessor/tests/test_logging_decorator.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from copy import deepcopy
77
from contextlib import ExitStack
88
from boto3 import client as boto3_client
9+
from botocore.exceptions import ClientError
910
from moto import mock_s3, mock_firehose, mock_sqs, mock_dynamodb
1011

1112
from tests.utils_for_tests.generic_setup_and_teardown import GenericSetUp, GenericTearDown
@@ -192,13 +193,43 @@ def test_logging_failed_validation(self):
192193
"file_key": FILE_DETAILS.file_key,
193194
"message_id": FILE_DETAILS.message_id,
194195
"error": "Initial file validation failed: EMIS does not have permissions for FLU",
196+
"vaccine_type": "FLU",
197+
"supplier": "EMIS"
195198
}
196199

197200
log_data = json.loads(mock_logger.info.call_args[0][0])
198201
self.assertEqual(log_data, expected_log_data)
199202

200203
mock_send_log_to_firehose.assert_called_once_with(log_data)
201204

205+
def test_logging_throws_exception(self):
206+
"""Tests that exception is caught when failing to send message to Firehose"""
207+
permissions_config_content = generate_permissions_config_content({"EMIS": ["COVID19_FULL"]})
208+
209+
firehose_exception = ClientError(
210+
error_response={"Error": {"Code": "ServiceUnavailable", "Message": "Service down"}},
211+
operation_name="PutRecord"
212+
)
213+
214+
with (
215+
patch("file_name_processor.uuid4", return_value=FILE_DETAILS.message_id),
216+
patch("elasticache.redis_client.get", return_value=permissions_config_content),
217+
patch("logging_decorator.firehose_client.put_record", side_effect=firehose_exception),
218+
patch("logging_decorator.logger") as mock_logger,
219+
):
220+
lambda_handler(MOCK_VACCINATION_EVENT, context=None)
221+
222+
# Assert logger.exception was called once
223+
mock_logger.exception.assert_called_once()
224+
225+
# Extract the call arguments
226+
exception_message = mock_logger.exception.call_args[0][0]
227+
exception_obj = mock_logger.exception.call_args[0][1]
228+
229+
# Check that the message format is correct
230+
self.assertIn("Error sending log to Firehose", exception_message)
231+
self.assertEqual(exception_obj, firehose_exception)
232+
202233
def test_logging_successful_config_upload(self):
203234
"""
204235
Tests that the correct logs are sent to cloudwatch and splunk when the config cache is successfully updated

0 commit comments

Comments
 (0)