Skip to content

Commit e7d1a14

Browse files
authored
Merge branch 'master' into VED-746-E2E-Test
2 parents 5cf4669 + f737a78 commit e7d1a14

File tree

12 files changed

+101
-31
lines changed

12 files changed

+101
-31
lines changed

config/prod/permissions_config.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@
2424
"permissions": ["RSV.RS"],
2525
"ods_codes": ["X26", "X8E5B"]
2626
},
27+
{
28+
"supplier": "MAVIS",
29+
"permissions": [
30+
"FLU.CUD",
31+
"HPV.CUD"
32+
],
33+
"ods_codes": ["V0V8L"]
34+
},
2735
{
2836
"supplier": "EMIS",
2937
"permissions": [
@@ -108,9 +116,5 @@
108116
{
109117
"supplier": "SONAR",
110118
"ods_codes": ["8HK48"]
111-
},
112-
{
113-
"supplier": "MAVIS",
114-
"ods_codes": ["V0V8L"]
115119
}
116120
]

filenameprocessor/src/constants.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
InvalidSupplierError,
1010
UnhandledAuditTableError,
1111
DuplicateFileError,
12-
UnhandledSqsError,
12+
UnhandledSqsError, EmptyFileError,
1313
)
1414

1515
SOURCE_BUCKET_NAME = os.getenv("SOURCE_BUCKET_NAME")
@@ -24,13 +24,17 @@
2424
ERROR_TYPE_TO_STATUS_CODE_MAP = {
2525
VaccineTypePermissionsError: 403,
2626
InvalidFileKeyError: 400, # Includes invalid ODS code, therefore unable to identify supplier
27+
EmptyFileError: 400,
2728
InvalidSupplierError: 500, # Only raised if supplier variable is not correctly set
2829
UnhandledAuditTableError: 500,
2930
DuplicateFileError: 422,
3031
UnhandledSqsError: 500,
3132
Exception: 500,
3233
}
3334

35+
# The size in bytes of an empty batch file containing only the headers row
36+
EMPTY_BATCH_FILE_SIZE_IN_BYTES = 615
37+
3438

3539
class FileStatus(StrEnum):
3640
"""File status constants"""
@@ -39,6 +43,7 @@ class FileStatus(StrEnum):
3943
PROCESSING = "Processing"
4044
PROCESSED = "Processed"
4145
DUPLICATE = "Not processed - duplicate"
46+
EMPTY = "Not processed - empty file"
4247

4348

4449
class AuditTableKeys(StrEnum):

filenameprocessor/src/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ class DuplicateFileError(Exception):
55
"""A custom exception for when it is identified that the file is a duplicate."""
66

77

8+
class EmptyFileError(Exception):
9+
"""A custom exception for when the batch file contains only the header row or is completely empty"""
10+
11+
812
class UnhandledAuditTableError(Exception):
913
"""A custom exception for when an unexpected error occurs whilst adding the file to the audit table."""
1014

filenameprocessor/src/file_name_processor.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
import argparse
1010
from uuid import uuid4
1111
from utils_for_filenameprocessor import get_creation_and_expiry_times, move_file
12-
from file_key_validation import validate_file_key, is_file_in_directory_root
12+
from file_validation import validate_file_key, is_file_in_directory_root, validate_file_not_empty
1313
from send_sqs_message import make_and_send_sqs_message
1414
from make_and_upload_ack_file import make_and_upload_the_ack_file
1515
from audit_table import upsert_audit_table
16-
from clients import logger
16+
from clients import logger, s3_client
1717
from logging_decorator import logging_decorator
1818
from supplier_permissions import validate_vaccine_type_permissions
1919
from errors import (
@@ -22,6 +22,7 @@
2222
InvalidSupplierError,
2323
UnhandledAuditTableError,
2424
UnhandledSqsError,
25+
EmptyFileError
2526
)
2627
from constants import FileStatus, ERROR_TYPE_TO_STATUS_CODE_MAP, SOURCE_BUCKET_NAME
2728

@@ -63,9 +64,12 @@ def handle_record(record) -> dict:
6364

6465
try:
6566
message_id = str(uuid4())
66-
created_at_formatted_string, expiry_timestamp = get_creation_and_expiry_times(bucket_name, file_key)
67+
s3_response = s3_client.get_object(Bucket=bucket_name, Key=file_key)
68+
created_at_formatted_string, expiry_timestamp = get_creation_and_expiry_times(s3_response)
6769

6870
vaccine_type, supplier = validate_file_key(file_key)
71+
# VED-757: Known issue with suppliers sometimes sending empty files
72+
validate_file_not_empty(s3_response)
6973
permissions = validate_vaccine_type_permissions(vaccine_type=vaccine_type, supplier=supplier)
7074

7175
queue_name = f"{supplier}_{vaccine_type}"
@@ -90,6 +94,7 @@ def handle_record(record) -> dict:
9094

9195
except ( # pylint: disable=broad-exception-caught
9296
VaccineTypePermissionsError,
97+
EmptyFileError,
9398
InvalidFileKeyError,
9499
InvalidSupplierError,
95100
UnhandledAuditTableError,
@@ -99,8 +104,10 @@ def handle_record(record) -> dict:
99104
logger.error("Error processing file '%s': %s", file_key, str(error))
100105

101106
queue_name = f"{supplier}_{vaccine_type}"
107+
file_status = FileStatus.EMPTY if isinstance(error, EmptyFileError) else FileStatus.PROCESSED
108+
102109
upsert_audit_table(
103-
message_id, file_key, created_at_formatted_string, expiry_timestamp, queue_name, FileStatus.PROCESSED
110+
message_id, file_key, created_at_formatted_string, expiry_timestamp, queue_name, file_status
104111
)
105112

106113
# Create ack file

filenameprocessor/src/file_key_validation.py renamed to filenameprocessor/src/file_validation.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
from re import match
44
from datetime import datetime
5-
from constants import VALID_VERSIONS
5+
from constants import VALID_VERSIONS, EMPTY_BATCH_FILE_SIZE_IN_BYTES
66
from elasticache import get_valid_vaccine_types_from_cache, get_supplier_system_from_cache
7-
from errors import InvalidFileKeyError
7+
from errors import InvalidFileKeyError, EmptyFileError
88

99

1010
def is_file_in_directory_root(file_key: str) -> bool:
@@ -68,3 +68,9 @@ def validate_file_key(file_key: str) -> tuple[str, str]:
6868
raise InvalidFileKeyError("Initial file validation failed: invalid file key")
6969

7070
return vaccine_type, supplier
71+
72+
73+
def validate_file_not_empty(s3_response: dict) -> None:
74+
"""Checks that the batch file from S3 is not empty or containing only the header row"""
75+
if s3_response.get("ContentLength", 0) <= EMPTY_BATCH_FILE_SIZE_IN_BYTES:
76+
raise EmptyFileError("Initial file validation failed: batch file was empty")

filenameprocessor/src/utils_for_filenameprocessor.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
from constants import AUDIT_TABLE_TTL_DAYS
55

66

7-
def get_creation_and_expiry_times(bucket_name: str, file_key: str) -> (str, int):
7+
def get_creation_and_expiry_times(s3_response: dict) -> (str, int):
88
"""Get 'created_at_formatted_string' and 'expires_at' from the response"""
9-
response = s3_client.get_object(Bucket=bucket_name, Key=file_key)
10-
creation_datetime = response["LastModified"]
9+
creation_datetime = s3_response["LastModified"]
1110
expiry_datetime = creation_datetime + timedelta(days=int(AUDIT_TABLE_TTL_DAYS))
1211
expiry_timestamp = int(expiry_datetime.timestamp())
1312
return creation_datetime.strftime("%Y%m%dT%H%M%S00"), expiry_timestamp

filenameprocessor/tests/test_file_key_validation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
# Ensure environment variables are mocked before importing from src files
1111
with patch.dict("os.environ", MOCK_ENVIRONMENT_DICT):
12-
from file_key_validation import is_file_in_directory_root, is_valid_datetime, validate_file_key
12+
from file_validation import is_file_in_directory_root, is_valid_datetime, validate_file_key
1313
from errors import InvalidFileKeyError
1414

1515
VALID_FLU_EMIS_FILE_KEY = MockFileDetails.emis_flu.file_key

filenameprocessor/tests/test_lambda_handler.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
MOCK_ODS_CODE_TO_SUPPLIER
1919
)
2020
from tests.utils_for_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT, BucketNames, Sqs
21-
from tests.utils_for_tests.values_for_tests import MOCK_CREATED_AT_FORMATTED_STRING, MOCK_EXPIRES_AT, MockFileDetails
21+
from tests.utils_for_tests.values_for_tests import MOCK_CREATED_AT_FORMATTED_STRING, MockFileDetails, \
22+
MOCK_BATCH_FILE_CONTENT, MOCK_FILE_HEADERS, MOCK_EXPIRES_AT
2223

2324
# Ensure environment variables are mocked before importing from src files
2425
with patch.dict("os.environ", MOCK_ENVIRONMENT_DICT):
@@ -99,7 +100,7 @@ def make_record(file_key: str):
99100
@staticmethod
100101
def make_record_with_message_id(file_key: str, message_id: str):
101102
"""
102-
Makes a record which includes a message_id, with the s3 bucket name set to BucketNames.SOURCE and and
103+
Makes a record which includes a message_id, with the s3 bucket name set to BucketNames.SOURCE and
103104
s3 object key set to the file_key.
104105
"""
105106
return {"s3": {"bucket": {"name": BucketNames.SOURCE}, "object": {"key": file_key}}, "message_id": message_id}
@@ -184,7 +185,7 @@ def test_lambda_handler_new_file_success_and_first_in_queue(self):
184185
for file_details in test_cases:
185186
with self.subTest(file_details.name):
186187
# Set up the file in the source bucket
187-
s3_client.put_object(Bucket=BucketNames.SOURCE, Key=file_details.file_key)
188+
s3_client.put_object(Bucket=BucketNames.SOURCE, Key=file_details.file_key, Body=MOCK_BATCH_FILE_CONTENT)
188189

189190
with ( # noqa: E999
190191
patch("file_name_processor.uuid4", return_value=file_details.message_id), # noqa: E999
@@ -195,6 +196,29 @@ def test_lambda_handler_new_file_success_and_first_in_queue(self):
195196
self.assert_sqs_message(file_details)
196197
self.assert_no_ack_file(file_details)
197198

199+
def test_lambda_handler_correctly_flags_empty_file(self):
200+
"""
201+
VED-757 Tests that for an empty batch file:
202+
* The file status is updated to 'Not processed - empty file' in the audit table
203+
* The message is not sent to SQS
204+
* The failure inf_ack file is created
205+
"""
206+
file_details = MockFileDetails.ravs_rsv_1
207+
208+
s3_client.put_object(Bucket=BucketNames.SOURCE, Key=file_details.file_key, Body=MOCK_FILE_HEADERS)
209+
210+
with ( # noqa: E999
211+
patch("file_name_processor.uuid4", return_value=file_details.message_id), # noqa: E999
212+
): # noqa: E999
213+
lambda_handler(
214+
self.make_event([self.make_record_with_message_id(file_details.file_key, file_details.message_id)]),
215+
None,
216+
)
217+
218+
assert_audit_table_entry(file_details, FileStatus.EMPTY)
219+
self.assert_no_sqs_message()
220+
self.assert_ack_file_contents(file_details)
221+
198222
def test_lambda_handler_non_root_file(self):
199223
"""
200224
Tests that when the file is not in the root of the source bucket, no action is taken:
@@ -222,7 +246,7 @@ def test_lambda_invalid_file_key_no_other_files_in_queue(self):
222246
* The failure inf_ack file is created
223247
"""
224248
invalid_file_key = "InvalidVaccineType_Vaccinations_v5_YGM41_20240708T12130100.csv"
225-
s3_client.put_object(Bucket=BucketNames.SOURCE, Key=invalid_file_key)
249+
s3_client.put_object(Bucket=BucketNames.SOURCE, Key=invalid_file_key, Body=MOCK_BATCH_FILE_CONTENT)
226250
file_details = deepcopy(MockFileDetails.ravs_rsv_1)
227251
file_details.file_key = invalid_file_key
228252
file_details.ack_file_key = self.get_ack_file_key(invalid_file_key)
@@ -259,7 +283,7 @@ def test_lambda_invalid_permissions_other_files_in_queue(self):
259283
* The failure inf_ack file is created
260284
"""
261285
file_details = MockFileDetails.ravs_rsv_1
262-
s3_client.put_object(Bucket=BucketNames.SOURCE, Key=file_details.file_key)
286+
s3_client.put_object(Bucket=BucketNames.SOURCE, Key=file_details.file_key, Body=MOCK_BATCH_FILE_CONTENT)
263287

264288
queued_file_details = MockFileDetails.ravs_rsv_2
265289
add_entry_to_table(queued_file_details, FileStatus.QUEUED)

filenameprocessor/tests/test_logging_decorator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from tests.utils_for_tests.generic_setup_and_teardown import GenericSetUp, GenericTearDown
1212
from tests.utils_for_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT, BucketNames, Firehose
13-
from tests.utils_for_tests.values_for_tests import MockFileDetails, fixed_datetime
13+
from tests.utils_for_tests.values_for_tests import MockFileDetails, fixed_datetime, MOCK_BATCH_FILE_CONTENT
1414
from tests.utils_for_tests.utils_for_filenameprocessor_tests import create_mock_hget
1515

1616
# Ensure environment variables are mocked before importing from src files
@@ -41,7 +41,7 @@ class TestLoggingDecorator(unittest.TestCase):
4141
def setUp(self):
4242
"""Set up the mock AWS environment and upload a valid FLU/EMIS file example"""
4343
GenericSetUp(s3_client, firehose_client, sqs_client, dynamodb_client)
44-
s3_client.put_object(Bucket=BucketNames.SOURCE, Key=FILE_DETAILS.file_key)
44+
s3_client.put_object(Bucket=BucketNames.SOURCE, Key=FILE_DETAILS.file_key, Body=MOCK_BATCH_FILE_CONTENT)
4545

4646
def tearDown(self):
4747
"""Clean the mock AWS environment"""

filenameprocessor/tests/test_utils_for_filenameprocessor.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,14 @@ def tearDown(self):
3535

3636
def test_get_creation_and_expiry_times(self):
3737
"""Test that get_creation_and_expiry_times can correctly get the created_at_formatted_string"""
38-
bucket_name = BucketNames.SOURCE
39-
file_key = "test_file_key"
40-
41-
s3_client.put_object(Bucket=bucket_name, Key=file_key)
42-
4338
mock_last_modified_created_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
44-
mock_last_modified = {"LastModified": mock_last_modified_created_at}
39+
mock_last_modified_s3_response = {"LastModified": mock_last_modified_created_at}
40+
4541
expected_result_created_at = "20240101T12000000"
4642
expected_expiry_datetime = mock_last_modified_created_at + timedelta(days=int(AUDIT_TABLE_TTL_DAYS))
4743
expected_result_expires_at = int(expected_expiry_datetime.timestamp())
4844

49-
with patch("utils_for_filenameprocessor.s3_client.get_object", return_value=mock_last_modified):
50-
created_at_formatted_string, expires_at = get_creation_and_expiry_times(bucket_name, file_key)
45+
created_at_formatted_string, expires_at = get_creation_and_expiry_times(mock_last_modified_s3_response)
5146

5247
self.assertEqual(created_at_formatted_string, expected_result_created_at)
5348
self.assertEqual(expires_at, expected_result_expires_at)

0 commit comments

Comments
 (0)