Skip to content

Commit d00157a

Browse files
authored
Merge pull request #339 from NHSDigital/AMB--2361
AMB-2361 : Recordforwarder Endpoint tests Part - 2
2 parents a80c0a3 + c561caa commit d00157a

File tree

1 file changed

+357
-0
lines changed

1 file changed

+357
-0
lines changed
Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
import os
2+
import unittest
3+
from unittest.mock import MagicMock, ANY, patch
4+
import boto3
5+
import simplejson as json
6+
import botocore.exceptions
7+
from moto import mock_dynamodb
8+
from uuid import uuid4
9+
from models.errors import IdentifierDuplicationError, ResourceNotFoundError, UnhandledResponseError, ResourceFoundError
10+
from fhir_batch_repository import ImmunizationBatchRepository, create_table
11+
from tests.utils.immunization_utils import create_covid_19_immunization_dict
12+
imms_id = str(uuid4())
13+
14+
15+
def _make_immunization_pk(_id):
16+
return f"Immunization#{_id}"
17+
18+
@mock_dynamodb
19+
class TestImmunizationBatchRepository(unittest.TestCase):
20+
21+
def setUp(self):
22+
os.environ["DYNAMODB_TABLE_NAME"] = "test-immunization-table"
23+
self.dynamodb = boto3.resource("dynamodb", region_name="eu-west-2")
24+
self.table = MagicMock()
25+
self.table.wait_until_exists()
26+
self.repository = ImmunizationBatchRepository()
27+
self.table.put_item = MagicMock(return_value={"ResponseMetadata": {"HTTPStatusCode": 200}})
28+
self.table.query = MagicMock(return_value={})
29+
self.immunization = create_covid_19_immunization_dict(imms_id)
30+
self.table.update_item = MagicMock(return_value = {"ResponseMetadata": {"HTTPStatusCode": 200}})
31+
32+
class TestCreateImmunization(TestImmunizationBatchRepository):
33+
34+
def modify_immunization(self, remove_nhs):
35+
"""Modify the immunization object by removing NHS number if required"""
36+
if remove_nhs:
37+
for i, x in enumerate(self.immunization["contained"]):
38+
if x["resourceType"] == "Patient":
39+
del self.immunization["contained"][i]
40+
break
41+
42+
def create_immunization_test_logic(self, is_present, remove_nhs):
43+
"""Common logic for testing immunization creation."""
44+
self.modify_immunization(remove_nhs)
45+
46+
self.repository.create_immunization(
47+
self.immunization, "supplier", "vax-type", self.table, is_present
48+
)
49+
item = self.table.put_item.call_args.kwargs["Item"]
50+
51+
self.table.put_item.assert_called_with(
52+
Item={
53+
"PK": ANY,
54+
"PatientPK": ANY,
55+
"PatientSK": ANY,
56+
"Resource": json.dumps(self.immunization, use_decimal=True),
57+
"IdentifierPK": ANY,
58+
"Operation": "CREATE",
59+
"Version": 1,
60+
"SupplierSystem": "supplier",
61+
},
62+
ConditionExpression=ANY
63+
)
64+
self.assertEqual(item["PK"], f'Immunization#{self.immunization["id"]}')
65+
66+
def test_create_immunization_with_nhs_number(self):
67+
"""Test creating Immunization with NHS number."""
68+
69+
self.create_immunization_test_logic(is_present=True, remove_nhs=False)
70+
71+
def test_create_immunization_without_nhs_number(self):
72+
"""Test creating Immunization without NHS number."""
73+
74+
self.create_immunization_test_logic(is_present=False, remove_nhs=True)
75+
76+
77+
def test_create_immunization_duplicate(self):
78+
"""it should not create Immunization since the request is duplicate"""
79+
80+
self.table.query = MagicMock(return_value={
81+
"id": imms_id,
82+
"identifier": [{"system": "test-system", "value": "12345"}],
83+
"contained": [{"resourceType": "Patient", "identifier": [{"value": "98765"}]}],
84+
"Count": 1
85+
})
86+
with self.assertRaises(IdentifierDuplicationError):
87+
self.repository.create_immunization(self.immunization, "supplier", "vax-type", self.table, False)
88+
self.table.put_item.assert_not_called()
89+
90+
def test_create_should_catch_dynamo_error(self):
91+
"""it should throw UnhandledResponse when the response from dynamodb can't be handled"""
92+
93+
bad_request = 400
94+
response = {"ResponseMetadata": {"HTTPStatusCode": bad_request}}
95+
self.table.put_item = MagicMock(return_value=response)
96+
with self.assertRaises(UnhandledResponseError) as e:
97+
self.repository.create_immunization(self.immunization, "supplier", "vax-type", self.table, False)
98+
self.assertDictEqual(e.exception.response, response)
99+
100+
101+
def test_create_immunization_unhandled_error(self):
102+
"""it should throw UnhandledResponse when the response from dynamodb can't be handled"""
103+
104+
response = {'Error': {'Code': 'InternalServerError'}}
105+
with unittest.mock.patch.object(self.table, 'put_item', side_effect=botocore.exceptions.ClientError({"Error": {"Code": "InternalServerError"}}, "PutItem")):
106+
with self.assertRaises(UnhandledResponseError) as e:
107+
self.repository.create_immunization(self.immunization, "supplier", "vax-type", self.table, False)
108+
self.assertDictEqual(e.exception.response, response)
109+
110+
def test_create_immunization_conditionalcheckfailedexception_error(self):
111+
"""it should throw UnhandledResponse when the response from dynamodb can't be handled"""
112+
113+
with unittest.mock.patch.object(self.table, 'put_item', side_effect=botocore.exceptions.ClientError({"Error": {"Code": "ConditionalCheckFailedException"}}, "PutItem")):
114+
with self.assertRaises(ResourceFoundError):
115+
self.repository.create_immunization(self.immunization, "supplier", "vax-type", self.table, False)
116+
117+
118+
class TestUpdateImmunization(TestImmunizationBatchRepository):
119+
def test_update_immunization(self):
120+
"""it should update Immunization record"""
121+
122+
test_cases = [
123+
# Update scenario
124+
{
125+
"query_response": {
126+
"Count": 1,
127+
"Items": [{
128+
"PK": _make_immunization_pk(imms_id),
129+
"Resource": json.dumps(self.immunization),
130+
"Version": 1
131+
}]
132+
},
133+
"expected_extra_values": {} # No extra assertion values
134+
},
135+
# Reinstated scenario
136+
{
137+
"query_response": {
138+
"Count": 1,
139+
"Items": [{
140+
"PK": _make_immunization_pk(imms_id),
141+
"Resource": json.dumps(self.immunization),
142+
"Version": 1,
143+
"DeletedAt": "20210101"
144+
}]
145+
},
146+
"expected_extra_values": {":respawn": "reinstated"}
147+
},
148+
# Update reinstated scenario
149+
{
150+
"query_response": {
151+
"Count": 1,
152+
"Items": [{
153+
"PK": _make_immunization_pk(imms_id),
154+
"Resource": json.dumps(self.immunization),
155+
"Version": 1,
156+
"DeletedAt": "reinstated"
157+
}]
158+
},
159+
"expected_extra_values": {}
160+
}
161+
]
162+
for is_present in [True, False]:
163+
for case in test_cases:
164+
with self.subTest(is_present=is_present, case=case):
165+
self.table.query = MagicMock(return_value=case["query_response"])
166+
response = self.repository.update_immunization(
167+
self.immunization, "supplier", "vax-type", self.table, is_present
168+
)
169+
expected_values = {
170+
":timestamp": ANY,
171+
":patient_pk": ANY,
172+
":patient_sk": ANY,
173+
":imms_resource_val": json.dumps(self.immunization),
174+
":operation": "UPDATE",
175+
":version": 2,
176+
":supplier_system": "supplier"
177+
}
178+
expected_values.update(case["expected_extra_values"])
179+
180+
self.table.update_item.assert_called_with(
181+
Key={"PK": _make_immunization_pk(imms_id)},
182+
UpdateExpression=ANY,
183+
ExpressionAttributeNames={"#imms_resource": "Resource"},
184+
ExpressionAttributeValues=expected_values,
185+
ReturnValues=ANY,
186+
ConditionExpression=ANY,
187+
)
188+
self.assertEqual(response, f'Immunization#{self.immunization["id"]}')
189+
190+
def test_update_immunization_not_found(self):
191+
"""it should not update Immunization since the imms id not found"""
192+
193+
with self.assertRaises(ResourceNotFoundError):
194+
self.repository.update_immunization(self.immunization, "supplier", "vax-type", self.table, False)
195+
self.table.update_item.assert_not_called()
196+
197+
def test_update_should_catch_dynamo_error(self):
198+
"""it should throw UnhandledResponse when the response from dynamodb can't be handled"""
199+
200+
bad_request = 400
201+
response = {"ResponseMetadata": {"HTTPStatusCode": bad_request}}
202+
self.table.update_item = MagicMock(return_value=response)
203+
self.table.query = MagicMock(return_value={
204+
"Count": 1,
205+
"Items": [{
206+
"PK": _make_immunization_pk(imms_id),
207+
"Resource": json.dumps(self.immunization),
208+
"Version": 1
209+
}]
210+
}
211+
)
212+
with self.assertRaises(UnhandledResponseError) as e:
213+
self.repository.update_immunization(self.immunization, "supplier", "vax-type", self.table, False)
214+
self.assertDictEqual(e.exception.response, response)
215+
216+
def test_update_immunization_unhandled_error(self):
217+
"""it should throw UnhandledResponse when the response from dynamodb can't be handled"""
218+
219+
response = {'Error': {'Code': 'InternalServerError'}}
220+
with unittest.mock.patch.object(self.table, 'update_item', side_effect=botocore.exceptions.ClientError({"Error": {"Code": "InternalServerError"}}, "UpdateItem")):
221+
with self.assertRaises(UnhandledResponseError) as e:
222+
self.table.query = MagicMock(return_value={
223+
"Count": 1,
224+
"Items": [{
225+
"PK": _make_immunization_pk(imms_id),
226+
"Resource": json.dumps(self.immunization),
227+
"Version": 1
228+
}]
229+
}
230+
)
231+
self.repository.update_immunization(self.immunization, "supplier", "vax-type", self.table, False)
232+
self.assertDictEqual(e.exception.response, response)
233+
234+
def test_update_immunization_conditionalcheckfailedexception_error(self):
235+
"""it should throw UnhandledResponse when the response from dynamodb can't be handled"""
236+
237+
with unittest.mock.patch.object(self.table, 'update_item', side_effect=botocore.exceptions.ClientError({"Error": {"Code": "ConditionalCheckFailedException"}}, "UpdateItem")):
238+
with self.assertRaises(ResourceNotFoundError) as e:
239+
self.table.query = MagicMock(return_value={
240+
"Count": 1,
241+
"Items": [{
242+
"PK": _make_immunization_pk(imms_id),
243+
"Resource": json.dumps(self.immunization),
244+
"Version": 1
245+
}]
246+
}
247+
)
248+
self.repository.update_immunization(self.immunization, "supplier", "vax-type", self.table, False)
249+
250+
class TestDeleteImmunization(TestImmunizationBatchRepository):
251+
def test_delete_immunization(self):
252+
"""it should delete Immunization record"""
253+
254+
self.table.query = MagicMock(return_value={
255+
"Count": 1,
256+
"Items": [{
257+
"PK": _make_immunization_pk(imms_id),
258+
"Resource": json.dumps(self.immunization),
259+
"Version": 1
260+
}]
261+
}
262+
)
263+
for is_present in [True, False]:
264+
response = self.repository.delete_immunization(self.immunization, "supplier", "vax-type", self.table, is_present)
265+
self.table.update_item.assert_called_with(
266+
Key={"PK": _make_immunization_pk(imms_id)},
267+
UpdateExpression="SET DeletedAt = :timestamp, Operation = :operation, SupplierSystem = :supplier_system",
268+
ExpressionAttributeValues={":timestamp": ANY, ":operation": "DELETE", ":supplier_system": "supplier"},
269+
ReturnValues=ANY,
270+
ConditionExpression=ANY,
271+
)
272+
self.assertEqual(response, f'Immunization#{self.immunization ["id"]}')
273+
274+
def test_delete_immunization_not_found(self):
275+
"""it should not delete Immunization since the imms id not found"""
276+
277+
with self.assertRaises(ResourceNotFoundError):
278+
self.repository.delete_immunization(self.immunization, "supplier", "vax-type", self.table, False)
279+
self.table.update_item.assert_not_called()
280+
281+
def test_delete_should_catch_dynamo_error(self):
282+
"""it should throw UnhandledResponse when the response from dynamodb can't be handled"""
283+
284+
bad_request = 400
285+
response = {"ResponseMetadata": {"HTTPStatusCode": bad_request}}
286+
self.table.update_item = MagicMock(return_value=response)
287+
self.table.query = MagicMock(return_value={
288+
"Count": 1,
289+
"Items": [{
290+
"PK": _make_immunization_pk(imms_id),
291+
"Resource": json.dumps(self.immunization),
292+
"Version": 1
293+
}]
294+
}
295+
)
296+
with self.assertRaises(UnhandledResponseError) as e:
297+
self.repository.delete_immunization(self.immunization, "supplier", "vax-type", self.table, False)
298+
self.assertDictEqual(e.exception.response, response)
299+
300+
def test_delete_immunization_unhandled_error(self):
301+
"""it should throw UnhandledResponse when the response from dynamodb can't be handled"""
302+
303+
response = {'Error': {'Code': 'InternalServerError'}}
304+
with unittest.mock.patch.object(self.table, 'update_item', side_effect=botocore.exceptions.ClientError({"Error": {"Code": "InternalServerError"}}, "UpdateItem")):
305+
with self.assertRaises(UnhandledResponseError) as e:
306+
self.table.query = MagicMock(return_value={
307+
"Count": 1,
308+
"Items": [{
309+
"PK": _make_immunization_pk(imms_id),
310+
"Resource": json.dumps(self.immunization),
311+
"Version": 1
312+
}]
313+
}
314+
)
315+
self.repository.delete_immunization(self.immunization, "supplier", "vax-type", self.table, False)
316+
self.assertDictEqual(e.exception.response, response)
317+
318+
def test_delete_immunization_conditionalcheckfailedexception_error(self):
319+
"""it should throw UnhandledResponse when the response from dynamodb can't be handled"""
320+
321+
with unittest.mock.patch.object(self.table, 'update_item', side_effect=botocore.exceptions.ClientError({"Error": {"Code": "ConditionalCheckFailedException"}}, "UpdateItem")):
322+
with self.assertRaises(ResourceNotFoundError) as e:
323+
self.table.query = MagicMock(return_value={
324+
"Count": 1,
325+
"Items": [{
326+
"PK": _make_immunization_pk(imms_id),
327+
"Resource": json.dumps(self.immunization),
328+
"Version": 1
329+
}]
330+
}
331+
)
332+
self.repository.delete_immunization(self.immunization, "supplier", "vax-type", self.table, False)
333+
334+
@mock_dynamodb
335+
@patch.dict(os.environ, {"DYNAMODB_TABLE_NAME": "TestTable"})
336+
class TestCreateTable(TestImmunizationBatchRepository):
337+
338+
def test_create_table_success(self):
339+
"""Test if create_table returns a DynamoDB Table instance with the correct name"""
340+
341+
# Create a mock DynamoDB table
342+
dynamodb = boto3.resource("dynamodb", region_name="eu-west-2")
343+
table_name = os.environ["DYNAMODB_TABLE_NAME"]
344+
345+
# Define table schema
346+
dynamodb.create_table(
347+
TableName=table_name,
348+
KeySchema=[{"AttributeName": "PK", "KeyType": "HASH"}],
349+
AttributeDefinitions=[{"AttributeName": "PK", "AttributeType": "S"}],
350+
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
351+
)
352+
353+
# Call the function
354+
table = create_table(region_name="eu-west-2")
355+
356+
# Assertions
357+
self.assertEqual(table.table_name, table_name)

0 commit comments

Comments
 (0)