Skip to content

Commit c30564a

Browse files
authored
VED-902 Fix timestamp validation for EA and batch (#1072)
1 parent 6ce912d commit c30564a

File tree

5 files changed

+36
-63
lines changed

5 files changed

+36
-63
lines changed

lambdas/filenameprocessor/src/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
Exception: 500,
3939
}
4040

41+
# Filename timestamp constants
42+
VALID_TIMESTAMP_LENGTH = 17
43+
VALID_TIMEZONE_OFFSETS = {"00", "01"}
44+
4145

4246
class FileStatus(StrEnum):
4347
"""File status constants"""

lambdas/filenameprocessor/src/file_name_processor.py

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

9-
import argparse
109
from uuid import uuid4
1110

1211
from audit_table import upsert_audit_table
@@ -264,7 +263,6 @@ def handle_extended_attributes_file(
264263
FileStatus.PROCESSING,
265264
)
266265

267-
# TODO: agree the prefix with DPS
268266
dest_file_key = f"{DPS_DESTINATION_PREFIX}/{file_key}"
269267
copy_file_to_external_bucket(
270268
bucket_name,
@@ -339,18 +337,3 @@ def lambda_handler(event: dict, context) -> None: # pylint: disable=unused-argu
339337
handle_record(record)
340338

341339
logger.info("Filename processor lambda task completed")
342-
343-
344-
def run_local():
345-
parser = argparse.ArgumentParser("file_name_processor")
346-
parser.add_argument("--bucket", required=True, help="Bucket name.", type=str)
347-
parser.add_argument("--key", required=True, help="Object key.", type=str)
348-
args = parser.parse_args()
349-
350-
event = {"Records": [{"s3": {"bucket": {"name": args.bucket}, "object": {"key": args.key}}}]}
351-
print(event)
352-
print(lambda_handler(event=event, context={}))
353-
354-
355-
if __name__ == "__main__":
356-
run_local()

lambdas/filenameprocessor/src/file_validation.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
from datetime import datetime
44
from re import match
55

6-
from constants import EXTENDED_ATTRIBUTES_FILE_PREFIX, EXTENDED_ATTRIBUTES_VACC_TYPE, VALID_EA_VERSIONS, VALID_VERSIONS
6+
from constants import (
7+
EXTENDED_ATTRIBUTES_FILE_PREFIX,
8+
EXTENDED_ATTRIBUTES_VACC_TYPE,
9+
VALID_EA_VERSIONS,
10+
VALID_TIMESTAMP_LENGTH,
11+
VALID_TIMEZONE_OFFSETS,
12+
VALID_VERSIONS,
13+
)
714
from elasticache import (
815
get_supplier_system_from_cache,
916
get_valid_vaccine_types_from_cache,
@@ -20,20 +27,32 @@ def is_file_in_directory_root(file_key: str) -> bool:
2027

2128
def is_valid_datetime(timestamp: str) -> bool:
2229
"""
23-
Returns a bool to indicate whether the timestamp is a valid datetime in the format 'YYYYmmddTHHMMSSzz'
24-
where 'zz' is a two digit number indicating the timezone
30+
Returns a bool to indicate whether the file timestamp matches the expected NHS file submission format:
31+
- YYYY = Year
32+
- MM = Month (01-12)
33+
- DD = Date (01-31)
34+
- T = fixed value of “T”
35+
- hh = Hours (00-23)
36+
- mm = Minutes (00-59)
37+
- ss = Seconds (00-59)
38+
- time zone offset = 00 for GMT, 01 for BST
39+
40+
e.g. 20220514T10081501
2541
"""
2642
# Check that datetime (excluding timezone) is a valid datetime in the expected format.
27-
if len(timestamp) < 15:
43+
if len(timestamp) != VALID_TIMESTAMP_LENGTH:
2844
return False
2945

30-
# Note that any digits after the seconds (i.e. from the 16th character onwards, usually expected to represent
31-
# timezone), do not need to be validated
3246
try:
3347
datetime.strptime(timestamp[:15], "%Y%m%dT%H%M%S")
3448
except ValueError:
3549
return False
3650

51+
time_zone_offset = timestamp[-2:]
52+
53+
if time_zone_offset not in VALID_TIMEZONE_OFFSETS:
54+
return False
55+
3756
return True
3857

3958

lambdas/filenameprocessor/tests/test_file_key_validation.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,20 @@ def test_is_valid_datetime(self, _):
4545
"""Tests that is_valid_datetime returns True for valid datetimes, and false otherwise"""
4646
# Test case tuples are structured as (date_time_string, expected_result)
4747
test_cases = [
48-
("20200101T12345600", True), # Valid datetime string with timezone
49-
("20200101T123456", True), # Valid datetime string without timezone
48+
("20200101T12345600", True), # Valid datetime string with timezone offset "00" GMT
49+
("20200101T12345601", True), # Valid datetime string with timezone offset "01" BST
50+
("20200101T123456", False), # Invalid datetime string without timezone
5051
(
5152
"20200101T123456extracharacters",
52-
True,
53-
), # Valid datetime string with additional characters
53+
False,
54+
), # Invalid datetime string with additional characters
5455
("20201301T12345600", False), # Invalid month
5556
("20200100T12345600", False), # Invalid day
5657
("20200230T12345600", False), # Invalid combination of month and day
5758
("20200101T24345600", False), # Invalid hours
5859
("20200101T12605600", False), # Invalid minutes
5960
("20200101T12346000", False), # Invalid seconds
61+
("20200101T12345609", False), # Invalid timezone offset
6062
("2020010112345600", False), # Invalid missing the 'T'
6163
("20200101T12345", False), # Invalid string too short
6264
]
@@ -124,7 +126,7 @@ def test_validate_extended_attributes_file_key(self, mock_get_redis_client):
124126
),
125127
# Valid extended attributes file key with different organization code
126128
(
127-
"Vaccination_Extended_Attributes_v1_5_YGM41_20221231T23595999.csv",
129+
"Vaccination_Extended_Attributes_v1_5_YGM41_20221231T23595900.csv",
128130
"YGM41",
129131
),
130132
]

lambdas/filenameprocessor/tests/test_lambda_handler.py

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Tests for lambda_handler"""
22

33
import json
4-
import sys
54
from contextlib import ExitStack
65
from copy import deepcopy
76
from json import loads as json_loads
@@ -778,37 +777,3 @@ def test_unexpected_bucket_name_and_filename_validation_fails(self):
778777
self.assertIn("Unable to process file", args[0])
779778
self.assertIn(invalid_file_key, args)
780779
self.assertIn("unknown-bucket", args)
781-
782-
783-
class TestMainEntryPoint(TestCase):
784-
def test_run_local_constructs_event_and_calls_lambda_handler(self):
785-
test_args = [
786-
"file_name_processor.py",
787-
"--bucket",
788-
"test-bucket",
789-
"--key",
790-
"some/path/file.csv",
791-
]
792-
793-
expected_event = {
794-
"Records": [
795-
{
796-
"s3": {
797-
"bucket": {"name": "test-bucket"},
798-
"object": {"key": "some/path/file.csv"},
799-
}
800-
}
801-
]
802-
}
803-
804-
with (
805-
patch.object(sys, "argv", test_args),
806-
patch("file_name_processor.lambda_handler") as mock_lambda_handler,
807-
patch("file_name_processor.print") as mock_print,
808-
):
809-
import file_name_processor
810-
811-
file_name_processor.run_local()
812-
813-
mock_lambda_handler.assert_called_once_with(event=expected_event, context={})
814-
mock_print.assert_called()

0 commit comments

Comments
 (0)