Skip to content

Commit 023942a

Browse files
committed
remove dynamo utils and add tests
1 parent f13b423 commit 023942a

8 files changed

Lines changed: 109 additions & 196 deletions

File tree

lambdas/mns_publisher/poetry.lock

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

lambdas/mns_publisher/pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,8 @@ cache = "^1.0.3"
2323
[build-system]
2424
requires = ["poetry-core >= 1.5.0"]
2525
build-backend = "poetry.core.masonry.api"
26+
27+
[dependency-groups]
28+
dev = [
29+
"responses (>=0.26.0,<0.27.0)"
30+
]

lambdas/mns_publisher/src/create_notification.py

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
import json
12
import os
23
import uuid
34
from datetime import datetime
5+
from typing import Any
46

57
from aws_lambda_typing.events.sqs import SQSMessage
68

79
from common.api_clients.get_pds_details import pds_get_patient_details
810
from common.clients import logger
911
from common.get_service_url import get_service_url
10-
from constants import IMMUNISATION_TYPE, SPEC_VERSION, MnsNotificationPayload
11-
from sqs_dynamo_utils import extract_sqs_imms_data
12+
from constants import DYNAMO_DB_TYPE_DESCRIPTORS, IMMUNISATION_TYPE, SPEC_VERSION, MnsNotificationPayload
1213

1314
IMMUNIZATION_ENV = os.getenv("IMMUNIZATION_ENV")
1415
IMMUNIZATION_BASE_PATH = os.getenv("IMMUNIZATION_BASE_PATH")
@@ -18,28 +19,37 @@ def create_mns_notification(sqs_event: SQSMessage) -> MnsNotificationPayload:
1819
"""Create a notification payload for MNS."""
1920
immunisation_url = get_service_url(IMMUNIZATION_ENV, IMMUNIZATION_BASE_PATH)
2021

21-
# Simple, direct extraction
22-
imms_data = extract_sqs_imms_data(sqs_event)
22+
body = json.loads(sqs_event.get("body", "{}"))
23+
new_image = body.get("dynamodb", {}).get("NewImage", {})
24+
imms_id = _unwrap_dynamodb_value(new_image.get("ImmsID", {}))
25+
supplier_system = _unwrap_dynamodb_value(new_image.get("SupplierSystem", {}))
26+
vaccine_type = _unwrap_dynamodb_value(new_image.get("VaccineType", {}))
27+
operation = _unwrap_dynamodb_value(new_image.get("Operation", {}))
2328

24-
patient_age = calculate_age_at_vaccination(imms_data["person_dob"], imms_data["date_and_time"])
29+
imms_map = new_image.get("Imms", {}).get("M", {})
30+
nhs_number = _unwrap_dynamodb_value(imms_map.get("NHS_NUMBER", {}))
31+
person_dob = _unwrap_dynamodb_value(imms_map.get("PERSON_DOB", {}))
32+
date_and_time = _unwrap_dynamodb_value(imms_map.get("DATE_AND_TIME", {}))
33+
site_code = _unwrap_dynamodb_value(imms_map.get("SITE_CODE", {}))
2534

26-
gp_ods_code = get_practitioner_details_from_pds(imms_data["nhs_number"])
35+
patient_age = calculate_age_at_vaccination(person_dob, date_and_time)
36+
gp_ods_code = get_practitioner_details_from_pds(nhs_number)
2737

2838
return {
2939
"specversion": SPEC_VERSION,
3040
"id": str(uuid.uuid4()),
3141
"source": immunisation_url,
3242
"type": IMMUNISATION_TYPE,
33-
"time": imms_data["date_and_time"],
34-
"subject": imms_data["nhs_number"],
35-
"dataref": f"{immunisation_url}/Immunization/{imms_data['imms_id']}",
43+
"time": date_and_time,
44+
"subject": nhs_number,
45+
"dataref": f"{immunisation_url}/Immunization/{imms_id}",
3646
"filtering": {
3747
"generalpractitioner": gp_ods_code,
38-
"sourceorganisation": imms_data["site_code"],
39-
"sourceapplication": imms_data["supplier_system"],
48+
"sourceorganisation": site_code,
49+
"sourceapplication": supplier_system,
4050
"subjectage": str(patient_age),
41-
"immunisationtype": imms_data["vaccine_type"],
42-
"action": imms_data["operation"],
51+
"immunisationtype": vaccine_type,
52+
"action": operation,
4353
},
4454
}
4555

@@ -95,3 +105,24 @@ def get_practitioner_details_from_pds(nhs_number: str) -> str | None:
95105
return None
96106

97107
return gp_ods_code
108+
109+
110+
def _unwrap_dynamodb_value(value: dict) -> Any:
111+
"""
112+
Unwrap DynamoDB type descriptor to get the actual value.
113+
DynamoDB types: S (String), N (Number), BOOL, M (Map), L (List), NULL
114+
"""
115+
if not isinstance(value, dict):
116+
return value
117+
118+
# DynamoDB type descriptors
119+
if "NULL" in value:
120+
return None
121+
122+
# Check other DynamoDB types
123+
for key in DYNAMO_DB_TYPE_DESCRIPTORS:
124+
if key in value:
125+
return value[key]
126+
127+
# Not a DynamoDB type, return as-is
128+
return value

lambdas/mns_publisher/src/process_records.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def process_records(records: list[SQSMessage]) -> dict[str, list]:
2727
except Exception:
2828
message_id = record.get("messageId", "unknown")
2929
batch_item_failures.append({"itemIdentifier": message_id})
30-
logger.exception("Failed to process record", trace_id={"message_id": message_id})
30+
logger.exception("Failed to process record", extra={"message_id": message_id})
3131

3232
if batch_item_failures:
3333
logger.warning(f"Batch completed with {len(batch_item_failures)} failures")
@@ -37,7 +37,7 @@ def process_records(records: list[SQSMessage]) -> dict[str, list]:
3737
return {"batchItemFailures": batch_item_failures}
3838

3939

40-
def process_record(record: SQSMessage, mns_service: MnsService) -> dict | None:
40+
def process_record(record: SQSMessage, mns_service: MnsService) -> None:
4141
"""
4242
Process a single SQS record.
4343
Args:
@@ -48,13 +48,12 @@ def process_record(record: SQSMessage, mns_service: MnsService) -> dict | None:
4848
message_id, immunisation_id = extract_trace_ids(record)
4949
notification_id = None
5050

51-
# Create notification payload
5251
mns_notification_payload = create_mns_notification(record)
5352
notification_id = mns_notification_payload.get("id")
5453
action_flag = mns_notification_payload.get("filtering", {}).get("action")
5554
logger.info(
5655
"Processing message",
57-
trace_ids={
56+
extra={
5857
"notification_id": notification_id,
5958
"message_id": message_id,
6059
"immunisation_id": immunisation_id,
@@ -63,7 +62,7 @@ def process_record(record: SQSMessage, mns_service: MnsService) -> dict | None:
6362
)
6463

6564
mns_service.publish_notification(mns_notification_payload)
66-
logger.info("Successfully created MNS notification", trace_ids={"mns_notification_id": notification_id})
65+
logger.info("Successfully created MNS notification", extra={"mns_notification_id": notification_id})
6766

6867
return None
6968

lambdas/mns_publisher/src/sqs_dynamo_utils.py

Lines changed: 0 additions & 56 deletions
This file was deleted.

lambdas/mns_publisher/tests/test_create_notification.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from constants import IMMUNISATION_TYPE, SPEC_VERSION
77
from create_notification import (
8+
_unwrap_dynamodb_value,
89
calculate_age_at_vaccination,
910
create_mns_notification,
1011
get_practitioner_details_from_pds,
@@ -346,5 +347,45 @@ def test_get_practitioner_pds_exception(self, mock_logger, mock_pds_get):
346347
self.assertEqual(str(context.exception), "PDS API error")
347348

348349

350+
class TestUnwrapDynamodbValue(unittest.TestCase):
351+
"""Tests for _unwrap_dynamodb_value helper function."""
352+
353+
def test_unwrap_string_type(self):
354+
"""Test unwrapping DynamoDB String type."""
355+
value = {"S": "test-value"}
356+
result = _unwrap_dynamodb_value(value)
357+
self.assertEqual(result, "test-value")
358+
359+
def test_unwrap_number_type(self):
360+
"""Test unwrapping DynamoDB Number type."""
361+
value = {"N": "123"}
362+
result = _unwrap_dynamodb_value(value)
363+
self.assertEqual(result, "123")
364+
365+
def test_unwrap_boolean_type(self):
366+
"""Test unwrapping DynamoDB Boolean type."""
367+
value = {"BOOL": True}
368+
result = _unwrap_dynamodb_value(value)
369+
self.assertTrue(result)
370+
371+
def test_unwrap_null_type(self):
372+
"""Test unwrapping DynamoDB NULL type."""
373+
value = {"NULL": True}
374+
result = _unwrap_dynamodb_value(value)
375+
self.assertIsNone(result)
376+
377+
def test_unwrap_map_type(self):
378+
"""Test unwrapping DynamoDB Map type."""
379+
value = {"M": {"key": {"S": "value"}}}
380+
result = _unwrap_dynamodb_value(value)
381+
self.assertEqual(result, {"key": {"S": "value"}})
382+
383+
def test_unwrap_list_type(self):
384+
"""Test unwrapping DynamoDB List type."""
385+
value = {"L": [{"S": "item1"}, {"S": "item2"}]}
386+
result = _unwrap_dynamodb_value(value)
387+
self.assertEqual(result, [{"S": "item1"}, {"S": "item2"}])
388+
389+
349390
if __name__ == "__main__":
350391
unittest.main()

lambdas/mns_publisher/tests/test_lambda_handler.py

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from lambda_handler import lambda_handler
77
from process_records import extract_trace_ids, process_record, process_records
8+
from test_utils import load_sample_sqs_event
89

910

1011
class TestExtractTraceIds(unittest.TestCase):
@@ -13,14 +14,7 @@ class TestExtractTraceIds(unittest.TestCase):
1314
@classmethod
1415
def setUpClass(cls):
1516
"""Load the sample SQS event once for all tests."""
16-
sample_event_path = Path(__file__).parent.parent / "tests/sqs_event.json"
17-
with open(sample_event_path, "r") as f:
18-
raw_event = json.load(f)
19-
20-
if isinstance(raw_event.get("body"), dict):
21-
raw_event["body"] = json.dumps(raw_event["body"])
22-
23-
cls.sample_sqs_event = raw_event
17+
cls.sample_sqs_event = load_sample_sqs_event()
2418

2519
def test_extract_trace_ids_success_from_real_payload(self):
2620
"""Test successful extraction using real SQS event structure."""
@@ -72,14 +66,7 @@ class TestProcessRecord(unittest.TestCase):
7266
@classmethod
7367
def setUpClass(cls):
7468
"""Load the sample SQS event once for all tests."""
75-
sample_event_path = Path(__file__).parent.parent / "tests/sqs_event.json"
76-
with open(sample_event_path, "r") as f:
77-
raw_event = json.load(f)
78-
79-
if isinstance(raw_event.get("body"), dict):
80-
raw_event["body"] = json.dumps(raw_event["body"])
81-
82-
cls.sample_sqs_record = raw_event
69+
cls.sample_sqs_record = load_sample_sqs_event()
8370

8471
def setUp(self):
8572
"""Set up test fixtures."""
@@ -181,7 +168,7 @@ def test_process_records_partial_failure(self, mock_process_record, mock_get_mns
181168

182169
self.assertEqual(len(result["batchItemFailures"]), 1)
183170
self.assertEqual(result["batchItemFailures"][0]["itemIdentifier"], "msg-456")
184-
mock_logger.warning.assert_called_with("Batch completed with 1 failures")
171+
mock_logger.exception.assert_called_once()
185172

186173
@patch("process_records.logger")
187174
@patch("process_records.get_mns_service")
@@ -219,14 +206,7 @@ class TestLambdaHandler(unittest.TestCase):
219206
@classmethod
220207
def setUpClass(cls):
221208
"""Load the sample SQS event once for all tests."""
222-
sample_event_path = Path(__file__).parent.parent / "tests/sqs_event.json"
223-
with open(sample_event_path, "r") as f:
224-
raw_event = json.load(f)
225-
226-
if isinstance(raw_event.get("body"), dict):
227-
raw_event["body"] = json.dumps(raw_event["body"])
228-
229-
cls.sample_sqs_record = raw_event
209+
cls.sample_sqs_record = load_sample_sqs_event()
230210

231211
@patch("lambda_handler.process_records")
232212
def test_lambda_handler_all_success(self, mock_process_records):

0 commit comments

Comments
 (0)