Skip to content

Commit 53866ca

Browse files
Merge pull request #335 from NHSDigital/AMB_2288-Upgrade-test-suite-filenameprocessor
Add unittests for cache and logging functions and move cache function…
2 parents d00157a + 40480f2 commit 53866ca

22 files changed

+858
-487
lines changed

.github/workflows/sonarcloud.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
id: filenameprocessor
3535
continue-on-error: true
3636
run: |
37-
pip install poetry==1.8.4 moto==4.2.11 coverage redis botocore==1.35.49 simplejson pandas freezegun responses structlog fhir.resources jsonpath_ng pydantic==1.10.13 requests aws-lambda-typing cffi pyjwt boto3-stubs-lite[dynamodb]~=1.26.90 python-stdnum==1.20
37+
pip install poetry==1.8.4 moto==4.2.11 coverage redis botocore==1.35.49 simplejson pandas freezegun responses structlog fhir.resources jsonpath_ng pydantic==1.10.13 requests aws-lambda-typing cffi pyjwt boto3-stubs-lite[dynamodb]~=1.26.90 python-stdnum==1.20 fakeredis
3838
poetry run coverage run --source=filenameprocessor -m unittest discover -s filenameprocessor || echo "filenameprocessor tests failed" >> failed_tests.txt
3939
poetry run coverage xml -o sonarcloud-coverage-filenameprocessor-coverage.xml
4040

filenameprocessor/poetry.lock

Lines changed: 202 additions & 174 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

filenameprocessor/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ structlog = "^24.1.0"
2525
redis = "^5.1.1"
2626
coverage = "7.6.1"
2727
freezegun = "^1.5.1"
28+
fakeredis = "^2.0.0"
2829

2930
[build-system]
3031
requires = ["poetry-core >= 1.5.0"]

filenameprocessor/src/clients.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
"""Initialise clients, resources and logger. Note that all clients, resources and logger for the filenameprocessor
2-
lambda should be initialised ONCE ONLY (in this file) and then imported into the files where they are needed."""
1+
"""
2+
Initialise clients, resources and logger. Note that all clients, resources and logger for the filenameprocessor
3+
lambda should be initialised ONCE ONLY (in this file) and then imported into the files where they are needed.
4+
"""
35

46
import os
57
import logging
68
import redis
79
from boto3 import client as boto3_client, resource as boto3_resource
810

9-
# AWS Clients and Resources
11+
# AWS Clients and Resources.
1012
REGION_NAME = "eu-west-2"
1113

1214
s3_client = boto3_client("s3", region_name=REGION_NAME)
1315
sqs_client = boto3_client("sqs", region_name=REGION_NAME)
1416
dynamodb_client = boto3_client("dynamodb", region_name=REGION_NAME)
1517
firehose_client = boto3_client("firehose", region_name=REGION_NAME)
16-
lambda_client = boto3_client('lambda', region_name=REGION_NAME)
18+
lambda_client = boto3_client("lambda", region_name=REGION_NAME)
1719

1820
dynamodb_resource = boto3_resource("dynamodb", region_name=REGION_NAME)
1921

filenameprocessor/src/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
AUDIT_TABLE_QUEUE_NAME_GSI = "queue_name_index"
1717
AUDIT_TABLE_FILENAME_GSI = "filename_index"
1818

19+
PERMISSIONS_CONFIG_FILE_KEY = "permissions_config.json"
20+
1921
ERROR_TYPE_TO_STATUS_CODE_MAP = {
2022
VaccineTypePermissionsError: 403,
2123
InvalidFileKeyError: 400, # Includes invalid ODS code, therefore unable to identify supplier
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"Upload the content from a config file in S3 to ElastiCache (Redis)"
2+
3+
import json
24
from clients import s3_client, redis_client
5+
from constants import PERMISSIONS_CONFIG_FILE_KEY
36

47

58
def upload_to_elasticache(file_key: str, bucket_name: str) -> None:
@@ -8,3 +11,8 @@ def upload_to_elasticache(file_key: str, bucket_name: str) -> None:
811
config_file_content = config_file["Body"].read().decode("utf-8")
912
# Use the file_key as the Redis key and file content as the value
1013
redis_client.set(file_key, config_file_content)
14+
15+
16+
def get_permissions_config_json_from_cache() -> dict:
17+
"""Gets and returns the permissions config file content from ElastiCache (Redis)."""
18+
return json.loads(redis_client.get(PERMISSIONS_CONFIG_FILE_KEY))

filenameprocessor/src/file_name_processor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from make_and_upload_ack_file import make_and_upload_the_ack_file
1414
from audit_table import upsert_audit_table, get_next_queued_file_details, ensure_file_is_not_a_duplicate
1515
from clients import logger
16-
from elasticcache import upload_to_elasticache
16+
from elasticache import upload_to_elasticache
1717
from logging_decorator import logging_decorator
1818
from supplier_permissions import validate_vaccine_type_permissions
1919
from errors import (

filenameprocessor/src/supplier_permissions.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
"""Functions for fetching supplier permissions"""
22

3-
import json
4-
from clients import redis_client, logger
3+
from clients import logger
54
from errors import VaccineTypePermissionsError
6-
7-
PERMISSIONS_CONFIG_FILE_KEY = "permissions_config.json"
8-
9-
10-
def get_permissions_config_json_from_cache() -> dict:
11-
"""Gets and returns the permissions config file content from ElastiCache."""
12-
return json.loads(redis_client.get(PERMISSIONS_CONFIG_FILE_KEY))
5+
from elasticache import get_permissions_config_json_from_cache
136

147

158
def get_supplier_permissions(supplier: str) -> list:

filenameprocessor/src/utils_for_filenameprocessor.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""Utils for filenameprocessor lambda"""
22

33
import json
4-
from csv import DictReader
5-
from io import StringIO
64
from constants import Constants, SOURCE_BUCKET_NAME, FILE_NAME_PROC_LAMBDA_NAME
75
from clients import s3_client, logger, lambda_client
86

@@ -13,13 +11,6 @@ def get_created_at_formatted_string(bucket_name: str, file_key: str) -> str:
1311
return response["LastModified"].strftime("%Y%m%dT%H%M%S00")
1412

1513

16-
def get_csv_content_dict_reader(bucket_name: str, file_key: str) -> DictReader:
17-
"""Downloads the csv data and returns a csv_reader with the content of the csv"""
18-
csv_obj = s3_client.get_object(Bucket=bucket_name, Key=file_key)
19-
csv_content_string = csv_obj["Body"].read().decode("utf-8")
20-
return DictReader(StringIO(csv_content_string), delimiter="|")
21-
22-
2314
def identify_supplier(ods_code: str) -> str:
2415
"""
2516
Identifies the supplier from the ods code using the mapping.

filenameprocessor/tests/test_audit_table.py

Lines changed: 44 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
22

33
from unittest import TestCase
44
from unittest.mock import patch
5-
from boto3 import resource as boto3_resource
5+
from boto3 import client as boto3_client
66
from moto import mock_dynamodb
7-
from errors import UnhandledAuditTableError
8-
from clients import REGION_NAME
9-
from tests.utils_for_tests.values_for_tests import MOCK_ENVIRONMENT_DICT
107

11-
# Some environment variables are evaluated when constants and upsert_audit_table are imported,
12-
# so environment dictionary must be mocked first
8+
from tests.utils_for_tests.mock_environment_variables import MOCK_ENVIRONMENT_DICT
9+
from tests.utils_for_tests.generic_setup_and_teardown import GenericSetUp, GenericTearDown
10+
11+
# Ensure environment variables are mocked before importing from src files
1312
with patch.dict("os.environ", MOCK_ENVIRONMENT_DICT):
14-
from constants import AuditTableKeys, AUDIT_TABLE_NAME, AUDIT_TABLE_QUEUE_NAME_GSI, AUDIT_TABLE_FILENAME_GSI
13+
from constants import AUDIT_TABLE_NAME
1514
from audit_table import upsert_audit_table
15+
from errors import UnhandledAuditTableError
16+
from clients import REGION_NAME
17+
18+
dynamodb_client = boto3_client("dynamodb", region_name=REGION_NAME)
1619

1720

1821
@mock_dynamodb
@@ -22,43 +25,16 @@ class TestAuditTable(TestCase):
2225

2326
def setUp(self):
2427
"""Set up test values to be used for the tests"""
25-
self.dynamodb_resource = boto3_resource("dynamodb", region_name=REGION_NAME)
26-
self.table = self.dynamodb_resource.create_table(
27-
TableName=AUDIT_TABLE_NAME,
28-
KeySchema=[
29-
{"AttributeName": AuditTableKeys.MESSAGE_ID, "KeyType": "HASH"},
30-
# {"AttributeName": "placeholder", "KeyType": "RANGE"},
31-
],
32-
AttributeDefinitions=[
33-
{"AttributeName": AuditTableKeys.MESSAGE_ID, "AttributeType": "S"},
34-
{"AttributeName": AuditTableKeys.FILENAME, "AttributeType": "S"},
35-
{"AttributeName": AuditTableKeys.QUEUE_NAME, "AttributeType": "S"},
36-
{"AttributeName": AuditTableKeys.STATUS, "AttributeType": "S"},
37-
],
38-
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
39-
GlobalSecondaryIndexes=[
40-
{
41-
"IndexName": AUDIT_TABLE_FILENAME_GSI,
42-
"KeySchema": [{"AttributeName": AuditTableKeys.FILENAME, "KeyType": "HASH"}],
43-
"Projection": {"ProjectionType": "KEYS_ONLY"},
44-
"ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
45-
},
46-
{
47-
"IndexName": AUDIT_TABLE_QUEUE_NAME_GSI,
48-
"KeySchema": [
49-
{"AttributeName": AuditTableKeys.QUEUE_NAME, "KeyType": "HASH"},
50-
{"AttributeName": AuditTableKeys.STATUS, "KeyType": "RANGE"},
51-
],
52-
"Projection": {"ProjectionType": "ALL"},
53-
"ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
54-
},
55-
],
56-
)
28+
GenericSetUp(dynamodb_client=dynamodb_client)
29+
30+
def tearDown(self):
31+
"""Tear down the test values"""
32+
GenericTearDown(dynamodb_client=dynamodb_client)
5733

58-
assert self.table.table_status == "ACTIVE"
59-
assert len(self.table.global_secondary_indexes) == 2
60-
assert self.table.global_secondary_indexes[0]["IndexName"] == AUDIT_TABLE_FILENAME_GSI
61-
assert self.table.global_secondary_indexes[1]["IndexName"] == AUDIT_TABLE_QUEUE_NAME_GSI
34+
@staticmethod
35+
def get_table_items():
36+
"""Return all items in the audit table"""
37+
return dynamodb_client.scan(TableName=AUDIT_TABLE_NAME).get("Items", [])
6238

6339
def test_upsert_audit_table(self):
6440
"""Test that the upsert_audit_table function works as expected for the following:
@@ -79,42 +55,42 @@ def test_upsert_audit_table(self):
7955

8056
# Test case 1: file_key_1 should be added to the audit table with status set to "Processing"
8157
expected_table_item_1 = {
82-
"message_id": message_id_1,
83-
"filename": file_key_1,
84-
"queue_name": "test_queue",
85-
"status": "Processing",
86-
"timestamp": created_at_formatted_string_1,
58+
"message_id": {"S": message_id_1},
59+
"filename": {"S": file_key_1},
60+
"queue_name": {"S": "test_queue"},
61+
"status": {"S": "Processing"},
62+
"timestamp": {"S": created_at_formatted_string_1},
8763
}
8864

8965
# Test case 2: file_key_2 should be added to the audit table with status set to "Queued"
9066
expected_table_item_2 = {
91-
"message_id": message_id_2,
92-
"filename": file_key_2,
93-
"queue_name": "test_queue",
94-
"status": "Queued",
95-
"timestamp": created_at_formatted_string_2,
67+
"message_id": {"S": message_id_2},
68+
"filename": {"S": file_key_2},
69+
"queue_name": {"S": "test_queue"},
70+
"status": {"S": "Queued"},
71+
"timestamp": {"S": created_at_formatted_string_2},
9672
}
9773

9874
# Test case 3: file_key_1 should be added to the audit table again,
9975
# with status set to "Not processed - duplicate"
10076
expected_table_item_3 = {
101-
"message_id": message_id_3,
102-
"filename": file_key_1,
103-
"queue_name": "test_queue",
104-
"status": "Not processed - duplicate",
105-
"timestamp": created_at_formatted_string_3,
77+
"message_id": {"S": message_id_3},
78+
"filename": {"S": file_key_1},
79+
"queue_name": {"S": "test_queue"},
80+
"status": {"S": "Not processed - duplicate"},
81+
"timestamp": {"S": created_at_formatted_string_3},
10682
}
10783

10884
# Test case 4: file_key_4 should not be added to the audit table because the message_id is a duplicate.
10985
# Note that this scenario should never occur as a new unique message_id is generated for each file upload,
11086
# even if the file name is a duplicate, but this test is included for safety because default behaviour of
11187
# dynamodb is to overwrite the existing item if the message_id is the same.
112-
expected_table_item_4 = { # This item should not be added to the table
113-
"message_id": message_id_3,
114-
"filename": file_key_4,
115-
"queue_name": "test_queue",
116-
"status": "Processed",
117-
"timestamp": created_at_formatted_string_4,
88+
expected_table_item_4 = {
89+
"message_id": {"S": message_id_3},
90+
"filename": {"S": file_key_4},
91+
"queue_name": {"S": "test_queue"},
92+
"status": {"S": "Processed"},
93+
"timestamp": {"S": created_at_formatted_string_4},
11894
}
11995

12096
# Add a file to the audit table
@@ -128,7 +104,7 @@ def test_upsert_audit_table(self):
128104
is_existing_file=False,
129105
)
130106
)
131-
table_items = self.table.scan()["Items"]
107+
table_items = self.get_table_items()
132108
assert len(table_items) == 1
133109
assert expected_table_item_1 in table_items
134110

@@ -143,7 +119,7 @@ def test_upsert_audit_table(self):
143119
is_existing_file=False,
144120
)
145121
)
146-
table_items = self.table.scan()["Items"]
122+
table_items = self.get_table_items()
147123
assert len(table_items) == 2
148124
assert expected_table_item_1 in table_items
149125
assert expected_table_item_2 in table_items
@@ -162,7 +138,7 @@ def test_upsert_audit_table(self):
162138

163139
# Check that the file has been added to the audit table again,
164140
# with the different message_id and created_at_formatted_string
165-
table_items = self.table.scan()["Items"]
141+
table_items = self.get_table_items()
166142
assert len(table_items) == 3
167143
assert expected_table_item_1 in table_items
168144
assert expected_table_item_2 in table_items
@@ -180,7 +156,7 @@ def test_upsert_audit_table(self):
180156
)
181157

182158
# Check that the file has not been added to the audit table
183-
table_items = self.table.scan()["Items"]
159+
table_items = self.get_table_items()
184160
assert len(table_items) == 3
185161
assert expected_table_item_1 in table_items
186162
assert expected_table_item_2 in table_items

0 commit comments

Comments
 (0)