1+ import copy
12import json
23import unittest
4+ from pathlib import Path
35from unittest .mock import MagicMock , patch
46
57from constants import IMMUNISATION_TYPE , SPEC_VERSION
6- from create_notification import calculate_age_at_vaccination , create_mns_notification
8+ from create_notification import (
9+ calculate_age_at_vaccination ,
10+ create_mns_notification ,
11+ get_practitioner_details_from_pds ,
12+ )
713
814
915class TestCalculateAgeAtVaccination (unittest .TestCase ):
1016 """Tests for age calculation at vaccination time."""
1117
1218 def test_age_calculation_yyyymmdd_format (self ):
13- """Test age calculation with YYYYMMDD format (actual format from payload) ."""
19+ """Test age calculation with YYYYMMDD format."""
1420 birth_date = "20040609"
1521 vaccination_date = "20260212"
1622
@@ -54,48 +60,131 @@ def test_age_calculation_infant(self):
5460
5561 self .assertEqual (age , 0 )
5662
63+ def test_age_calculation_leap_year_birthday (self ):
64+ """Test age calculation with leap year birthday."""
65+ birth_date = "20000229"
66+ vaccination_date = "20240228"
67+
68+ age = calculate_age_at_vaccination (birth_date , vaccination_date )
69+
70+ self .assertEqual (age , 23 )
71+
72+ def test_age_calculation_same_day_different_year (self ):
73+ """Test age calculation for same day in different year."""
74+ birth_date = "20000101"
75+ vaccination_date = "20250101"
76+
77+ age = calculate_age_at_vaccination (birth_date , vaccination_date )
78+
79+ self .assertEqual (age , 25 )
80+
81+
82+ class TestGetPractitionerDetailsFromPds (unittest .TestCase ):
83+ """Tests for get_practitioner_details_from_pds function."""
84+
85+ @patch ("create_notification.pds_get_patient_details" )
86+ @patch ("create_notification.logger" )
87+ def test_get_practitioner_success (self , mock_logger , mock_pds_get ):
88+ """Test successful retrieval of GP ODS code."""
89+ mock_pds_get .return_value = {"generalPractitioner" : {"value" : "Y12345" }}
90+
91+ result = get_practitioner_details_from_pds ("9481152782" )
92+
93+ self .assertEqual (result , "Y12345" )
94+ mock_pds_get .assert_called_once_with ("9481152782" )
95+ mock_logger .warning .assert_not_called ()
96+
97+ @patch ("create_notification.pds_get_patient_details" )
98+ @patch ("create_notification.logger" )
99+ def test_get_practitioner_no_gp_details (self , mock_logger , mock_pds_get ):
100+ """Test when generalPractitioner is missing."""
101+ mock_pds_get .return_value = {"name" : "John Doe" }
102+
103+ result = get_practitioner_details_from_pds ("9481152782" )
104+
105+ self .assertIsNone (result )
106+ mock_logger .warning .assert_called_once_with ("No patient details found for NHS number" )
107+
108+ @patch ("create_notification.pds_get_patient_details" )
109+ @patch ("create_notification.logger" )
110+ def test_get_practitioner_gp_is_none (self , mock_logger , mock_pds_get ):
111+ """Test when generalPractitioner is None."""
112+ mock_pds_get .return_value = {"generalPractitioner" : None }
113+
114+ result = get_practitioner_details_from_pds ("9481152782" )
115+
116+ self .assertIsNone (result )
117+ mock_logger .warning .assert_called_once ()
118+
119+ @patch ("create_notification.pds_get_patient_details" )
120+ @patch ("create_notification.logger" )
121+ def test_get_practitioner_no_value_field (self , mock_logger , mock_pds_get ):
122+ """Test when value field is missing from generalPractitioner."""
123+ mock_pds_get .return_value = {"generalPractitioner" : {"system" : "https://fhir.nhs.uk" }}
124+
125+ result = get_practitioner_details_from_pds ("9481152782" )
126+
127+ self .assertIsNone (result )
128+ mock_logger .warning .assert_called_with ("GP ODS code not found in practitioner details" )
129+
130+ @patch ("create_notification.pds_get_patient_details" )
131+ @patch ("create_notification.logger" )
132+ def test_get_practitioner_empty_value (self , mock_logger , mock_pds_get ):
133+ """Test when value is empty string."""
134+ mock_pds_get .return_value = {"generalPractitioner" : {"value" : "" }}
135+
136+ result = get_practitioner_details_from_pds ("9481152782" )
137+
138+ self .assertIsNone (result )
139+ mock_logger .warning .assert_called_with ("GP ODS code not found in practitioner details" )
140+
141+ @patch ("create_notification.pds_get_patient_details" )
142+ @patch ("create_notification.logger" )
143+ def test_get_practitioner_pds_exception (self , mock_logger , mock_pds_get ):
144+ """Test when PDS API raises exception."""
145+ mock_pds_get .side_effect = Exception ("PDS API error" )
146+
147+ with self .assertRaises (Exception ) as context :
148+ get_practitioner_details_from_pds ("9481152782" )
149+
150+ self .assertEqual (str (context .exception ), "PDS API error" )
151+ mock_logger .exception .assert_called_once ()
152+
153+ @patch ("create_notification.pds_get_patient_details" )
154+ @patch ("create_notification.logger" )
155+ def test_get_practitioner_patient_details_none (self , mock_logger , mock_pds_get ):
156+ """Test when pds_get_patient_details returns None."""
157+ mock_pds_get .return_value = None
158+
159+ with self .assertRaises (AttributeError ):
160+ get_practitioner_details_from_pds ("9481152782" )
161+
57162
58163class TestCreateMnsNotification (unittest .TestCase ):
59164 """Tests for MNS notification creation."""
60165
166+ @classmethod
167+ def setUpClass (cls ):
168+ """Load the sample SQS event once for all tests."""
169+ sample_event_path = Path (__file__ ).parent .parent / "tests" / "sqs_event.json"
170+ with open (sample_event_path , "r" ) as f :
171+ raw_event = json .load (f )
172+
173+ # Convert body from dict to JSON string (as it would be in real SQS)
174+ if isinstance (raw_event .get ("body" ), dict ):
175+ raw_event ["body" ] = json .dumps (raw_event ["body" ])
176+ cls .sample_sqs_event = raw_event
177+
61178 def setUp (self ):
62179 """Set up test fixtures."""
63- self .sample_sqs_event = {
64- "messageId" : "98ed30eb-829f-41df-8a73-57fef70cf161" ,
65- "body" : json .dumps (
66- {
67- "eventID" : "b1ba2a48eae68bf43a8cb49b400788c6" ,
68- "eventName" : "INSERT" ,
69- "dynamodb" : {
70- "NewImage" : {
71- "ImmsID" : {"S" : "d058014c-b0fd-4471-8db9-3316175eb825" },
72- "VaccineType" : {"S" : "hib" },
73- "SupplierSystem" : {"S" : "TPP" },
74- "DateTimeStamp" : {"S" : "2026-02-12T17:45:37+00:00" },
75- "Imms" : {
76- "M" : {
77- "NHS_NUMBER" : {"S" : "9481152782" },
78- "PERSON_DOB" : {"S" : "20040609" },
79- "DATE_AND_TIME" : {"S" : "20260212T174437" },
80- "VACCINE_TYPE" : {"S" : "hib" },
81- "SITE_CODE" : {"S" : "B0C4P" },
82- }
83- },
84- "Operation" : {"S" : "CREATE" },
85- }
86- },
87- }
88- ),
89- }
90-
91180 self .expected_gp_ods_code = "Y12345"
92181 self .expected_immunisation_url = "https://int.api.service.nhs.uk/immunisation-fhir-api"
93182
94183 @patch ("create_notification.get_practitioner_details_from_pds" )
95184 @patch ("create_notification.get_service_url" )
96185 @patch ("create_notification.uuid.uuid4" )
97- def test_create_mns_notification_success (self , mock_uuid , mock_get_service_url , mock_get_gp ):
98- """Test successful MNS notification creation."""
186+ def test_create_mns_notification_success_with_real_payload (self , mock_uuid , mock_get_service_url , mock_get_gp ):
187+ """Test successful MNS notification creation using real SQS event ."""
99188 mock_uuid .return_value = MagicMock (hex = "236a1d4a-5d69-4fa9-9c7f-e72bf505aa5b" )
100189 mock_get_service_url .return_value = self .expected_immunisation_url
101190 mock_get_gp .return_value = self .expected_gp_ods_code
@@ -113,8 +202,8 @@ def test_create_mns_notification_success(self, mock_uuid, mock_get_service_url,
113202
114203 @patch ("create_notification.get_practitioner_details_from_pds" )
115204 @patch ("create_notification.get_service_url" )
116- def test_create_mns_notification_dataref_format (self , mock_get_service_url , mock_get_gp ):
117- """Test dataref URL format is correct."""
205+ def test_create_mns_notification_dataref_format_real_payload (self , mock_get_service_url , mock_get_gp ):
206+ """Test dataref URL format is correct with real payload ."""
118207 mock_get_service_url .return_value = self .expected_immunisation_url
119208 mock_get_gp .return_value = self .expected_gp_ods_code
120209
@@ -125,8 +214,8 @@ def test_create_mns_notification_dataref_format(self, mock_get_service_url, mock
125214
126215 @patch ("create_notification.get_practitioner_details_from_pds" )
127216 @patch ("create_notification.get_service_url" )
128- def test_create_mns_notification_filtering_fields (self , mock_get_service_url , mock_get_gp ):
129- """Test all filtering fields are populated correctly."""
217+ def test_create_mns_notification_filtering_fields_real_payload (self , mock_get_service_url , mock_get_gp ):
218+ """Test all filtering fields are populated correctly with real payload ."""
130219 mock_get_service_url .return_value = self .expected_immunisation_url
131220 mock_get_gp .return_value = self .expected_gp_ods_code
132221
@@ -142,8 +231,8 @@ def test_create_mns_notification_filtering_fields(self, mock_get_service_url, mo
142231
143232 @patch ("create_notification.get_practitioner_details_from_pds" )
144233 @patch ("create_notification.get_service_url" )
145- def test_create_mns_notification_age_calculation (self , mock_get_service_url , mock_get_gp ):
146- """Test patient age is calculated correctly."""
234+ def test_create_mns_notification_age_calculation_real_payload (self , mock_get_service_url , mock_get_gp ):
235+ """Test patient age is calculated correctly with real payload ."""
147236 mock_get_service_url .return_value = self .expected_immunisation_url
148237 mock_get_gp .return_value = self .expected_gp_ods_code
149238
@@ -153,8 +242,8 @@ def test_create_mns_notification_age_calculation(self, mock_get_service_url, moc
153242
154243 @patch ("create_notification.get_practitioner_details_from_pds" )
155244 @patch ("create_notification.get_service_url" )
156- def test_create_mns_notification_calls_get_practitioner (self , mock_get_service_url , mock_get_gp ):
157- """Test get_practitioner_details_from_pds is called with correct NHS number."""
245+ def test_create_mns_notification_calls_get_practitioner_real_payload (self , mock_get_service_url , mock_get_gp ):
246+ """Test get_practitioner_details_from_pds is called with correct NHS number from real payload ."""
158247 mock_get_service_url .return_value = self .expected_immunisation_url
159248 mock_get_gp .return_value = self .expected_gp_ods_code
160249
@@ -219,6 +308,39 @@ def test_create_mns_notification_required_fields_present(self, mock_get_service_
219308 for field in required_fields :
220309 self .assertIn (field , result , f"Required field '{ field } ' missing" )
221310
311+ @patch ("create_notification.get_practitioner_details_from_pds" )
312+ @patch ("create_notification.get_service_url" )
313+ def test_create_mns_notification_missing_imms_data_field (self , mock_get_service_url , mock_get_gp ):
314+ """Test handling when a required field is missing from imms_data."""
315+ mock_get_service_url .return_value = self .expected_immunisation_url
316+ mock_get_gp .return_value = self .expected_gp_ods_code
317+
318+ incomplete_event = {
319+ "messageId" : "test-id" ,
320+ "body" : json .dumps ({"dynamodb" : {"NewImage" : {"ImmsID" : {"S" : "test-id" }}}}),
321+ }
322+
323+ with self .assertRaises ((KeyError , TypeError )):
324+ create_mns_notification (incomplete_event )
325+
326+
327+ @patch ("create_notification.get_practitioner_details_from_pds" )
328+ @patch ("create_notification.get_service_url" )
329+ def test_create_mns_notification_with_update_action (self , mock_get_service_url , mock_get_gp ):
330+ """Test notification creation with UPDATE action using real payload structure."""
331+ mock_get_service_url .return_value = self .expected_immunisation_url
332+ mock_get_gp .return_value = self .expected_gp_ods_code
333+
334+ update_event = copy .deepcopy (self .sample_sqs_event )
335+
336+ update_event ["body" ]["dynamodb" ]["NewImage" ]["Operation" ]["S" ] = "UPDATE"
337+
338+ result = create_mns_notification (update_event )
339+
340+ self .assertEqual (result ["filtering" ]["action" ], "UPDATE" )
341+ mock_get_service_url .assert_called ()
342+ mock_get_gp .assert_called ()
343+
222344
223345if __name__ == "__main__" :
224346 unittest .main ()
0 commit comments