Skip to content

Commit 3a6a304

Browse files
committed
Merge branch 'master' into VED-382-backend-configurable-disease-mapping
# Conflicts: # backend/poetry.lock # backend/src/fhir_controller.py # backend/src/fhir_repository.py # backend/src/models/utils/permission_checker.py # backend/tests/test_fhir_controller.py # backend/tests/test_fhir_service.py # terraform/endpoints.tf
2 parents 108e72b + aa9e0c9 commit 3a6a304

File tree

88 files changed

+4540
-216
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+4540
-216
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ See https://nhsd-confluence.digital.nhs.uk/display/APM/Glossary.
3939
| Folder | Description |
4040
|------------------------|-------------|
4141
| `infra` | Base infrastructure components. |
42+
| `infra_old` | Old infra code used to create INT to mimic prod. |
4243
| `grafana` | Terraform configuration for Grafana, built on top of core infra. |
4344
| `terraform` | Core Terraform infrastructure code. This is run in each PR and sets up lambdas associated with the PR.|
45+
| `terraform_old` | Old tf code used to create INT to mimic prod. |
4446
| `terraform_sandbox` | Sandbox environment for testing infrastructure changes. |
4547
| `terraform_aws_backup` | Streamlined backup processing with AWS. |
4648
| `mesh-infra` | Infrastructure setup for Imms batch MESH integration. |

backend/poetry.lock

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

backend/src/fhir_controller.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -431,9 +431,6 @@ def search_immunizations(self, aws_event: APIGatewayProxyEventV1) -> dict:
431431
for vaccine_type in search_params.immunization_targets
432432
if ApiOperationCode.SEARCH in expanded_permissions.get(vaccine_type.lower(), [])
433433
]
434-
# vax_type_perms = _expand_permissions(imms_vax_type_perms, ApiOperationCode.SEARCH)
435-
# vax_type_perm = [ vaccine_type for vaccine_type in search_params.immunization_targets
436-
# if f"{vaccine_type.lower()}.{ApiOperationCode.SEARCH}" in vax_type_perms ]
437434
if not vax_type_perm:
438435
raise UnauthorizedVaxError
439436
except UnauthorizedVaxError as unauthorized:
@@ -670,5 +667,5 @@ def create_response(status_code, body=None, headers=None):
670667
def _identify_supplier_system(aws_event):
671668
supplier_system = aws_event["headers"]["SupplierSystem"]
672669
if not supplier_system:
673-
raise UnauthorizedSystemError("SupplierSystem header is missing or empty.")
670+
return self.create_response(403, unauthorized.to_operation_outcome())
674671
return supplier_system

backend/src/fhir_repository.py

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -106,28 +106,23 @@ def get_immunization_by_identifier(
106106

107107
def get_immunization_by_id(self, imms_id: str, imms_vax_type_perms: str) -> Optional[dict]:
108108
response = self.table.get_item(Key={"PK": _make_immunization_pk(imms_id)})
109+
item = response.get("Item")
109110

110-
if "Item" in response:
111-
resp = dict()
112-
if "DeletedAt" in response["Item"]:
113-
if response["Item"]["DeletedAt"] == "reinstated":
114-
vaccine_type = self._vaccine_type(response["Item"]["PatientSK"])
115-
if not validate_permissions(imms_vax_type_perms,ApiOperationCode.READ, [vaccine_type]):
116-
raise UnauthorizedVaxError()
117-
resp["Resource"] = json.loads(response["Item"]["Resource"])
118-
resp["Version"] = response["Item"]["Version"]
119-
return resp
120-
else:
121-
return None
122-
else:
123-
vaccine_type = self._vaccine_type(response["Item"]["PatientSK"])
124-
if not validate_permissions(imms_vax_type_perms,ApiOperationCode.READ, [vaccine_type]):
125-
raise UnauthorizedVaxError()
126-
resp["Resource"] = json.loads(response["Item"]["Resource"])
127-
resp["Version"] = response["Item"]["Version"]
128-
return resp
129-
else:
111+
if not item:
130112
return None
113+
if item.get("DeletedAt") and item["DeletedAt"] != "reinstated":
114+
return None
115+
116+
# Get vaccine type + validate permissions
117+
vaccine_type = self._vaccine_type(item["PatientSK"])
118+
if not validate_permissions(imms_vax_type_perms, ApiOperationCode.READ, [vaccine_type]):
119+
raise UnauthorizedVaxError()
120+
121+
# Build response
122+
return {
123+
"Resource": json.loads(item["Resource"]),
124+
"Version": item["Version"]
125+
}
131126

132127
def get_immunization_by_id_all(self, imms_id: str, imms: dict) -> Optional[dict]:
133128
response = self.table.get_item(Key={"PK": _make_immunization_pk(imms_id)})
@@ -355,36 +350,39 @@ def _perform_dynamo_update(
355350
)
356351

357352
def delete_immunization(
358-
self, imms_id: str, imms_vax_type_perms: str, supplier_system: str
359-
) -> dict:
353+
self, imms_id: str, imms_vax_type_perms: str, supplier_system: str) -> dict:
360354
now_timestamp = int(time.time())
355+
361356
try:
362-
resp = self.table.get_item(Key={"PK": _make_immunization_pk(imms_id)})
363-
364-
if "Item" in resp:
365-
if "DeletedAt" in resp["Item"]:
366-
if resp["Item"]["DeletedAt"] == "reinstated":
367-
pass
368-
vaccine_type = self._vaccine_type(resp["Item"]["PatientSK"])
357+
item = self.table.get_item(Key={"PK": _make_immunization_pk(imms_id)}).get("Item")
358+
if not item:
359+
raise ResourceNotFoundError(resource_type="Immunization", resource_id=imms_id)
360+
361+
if not item.get("DeletedAt") or item.get("DeletedAt") == "reinstated":
362+
vaccine_type = self._vaccine_type(item["PatientSK"])
369363
if not validate_permissions(imms_vax_type_perms, ApiOperationCode.DELETE, [vaccine_type]):
370364
raise UnauthorizedVaxError()
371365

366+
# Proceed with delete update
372367
response = self.table.update_item(
373368
Key={"PK": _make_immunization_pk(imms_id)},
374-
UpdateExpression="SET DeletedAt = :timestamp, Operation = :operation, SupplierSystem = :supplier_system",
369+
UpdateExpression=(
370+
"SET DeletedAt = :timestamp, Operation = :operation, SupplierSystem = :supplier_system"
371+
),
375372
ExpressionAttributeValues={
376373
":timestamp": now_timestamp,
377374
":operation": "DELETE",
378375
":supplier_system": supplier_system,
379376
},
380377
ReturnValues="ALL_NEW",
381-
ConditionExpression=Attr("PK").eq(_make_immunization_pk(imms_id))
382-
& (Attr("DeletedAt").not_exists() | Attr("DeletedAt").eq("reinstated")),
378+
ConditionExpression=(
379+
Attr("PK").eq(_make_immunization_pk(imms_id)) &
380+
(Attr("DeletedAt").not_exists() | Attr("DeletedAt").eq("reinstated"))
381+
),
383382
)
383+
384384
return self._handle_dynamo_response(response)
385-
386385
except botocore.exceptions.ClientError as error:
387-
# Either resource didn't exist or it has already been deleted. See ConditionExpression in the request
388386
if error.response["Error"]["Code"] == "ConditionalCheckFailedException":
389387
raise ResourceNotFoundError(resource_type="Immunization", resource_id=imms_id)
390388
else:

backend/src/models/utils/permission_checker.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def _expand_permissions(permissions: list[str]) -> dict[str, list[ApiOperationCo
2323
def validate_permissions(permissions: list[str], operation: ApiOperationCode, vaccine_types: list[str]):
2424
expanded_permissions = _expand_permissions(permissions)
2525
print(f"operation: {operation}, expanded_permissions: {expanded_permissions}, vaccine_types: {vaccine_types}")
26-
return all([
26+
return all(
2727
operation in expanded_permissions.get(vaccine_type.lower(), [])
2828
for vaccine_type in vaccine_types
29-
])
29+
)

backend/tests/test_fhir_controller.py

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,17 @@
3232
from parameter_parser import patient_identifier_system, process_search_params
3333
from tests.utils.generic_utils import load_json_data
3434
from tests.utils.values_for_tests import ValidValues
35-
from utils.mock_redis import MOCK_REDIS_V2D_RESPONSE
3635

37-
class TestFhirControllerBase(unittest.TestCase):
38-
"""Base class for all tests to set up common fixtures"""
36+
"test"
3937

38+
39+
class TestFhirController(unittest.TestCase):
4040
def setUp(self):
41-
super().setUp()
42-
self.redis_patcher = patch("parameter_parser.redis_client")
43-
self.mock_redis_client = self.redis_patcher.start()
44-
self.logger_info_patcher = patch("logging.Logger.info")
45-
self.mock_logger_info = self.logger_info_patcher.start()
46-
47-
def tearDown(self):
48-
self.redis_patcher.stop()
49-
self.logger_info_patcher.stop()
50-
super().tearDown()
51-
52-
class TestFhirController(TestFhirControllerBase):
53-
def setUp(self):
54-
super().setUp()
5541
self.service = create_autospec(FhirService)
5642
self.repository = create_autospec(ImmunizationRepository)
5743
self.authorizer = create_autospec(Authorization)
5844
self.controller = FhirController(self.authorizer, self.service)
5945

60-
def tearDown(self):
61-
self.redis_patcher.stop()
62-
super().tearDown()
63-
6446
def test_create_response(self):
6547
"""it should return application/fhir+json with correct status code"""
6648
body = {"message": "a body"}
@@ -151,7 +133,7 @@ def test_get_imms_by_identifer_header_missing(self):
151133
response = self.controller.get_immunization_by_identifier(lambda_event)
152134

153135
self.assertEqual(response["statusCode"], 403)
154-
136+
155137
@patch("fhir_controller.get_supplier_permissions")
156138
def test_not_found_for_identifier(self, mock_get_permissions):
157139
"""it should return not-found OperationOutcome if it doesn't exist"""
@@ -183,7 +165,7 @@ def test_not_found_for_identifier(self, mock_get_permissions):
183165

184166
imms = identifier.replace("|", "#")
185167
# When
186-
168+
187169
response = self.controller.get_immunization_by_identifier(lambda_event)
188170

189171
# Then
@@ -218,7 +200,7 @@ def test_get_imms_by_identifer_patient_identifier_and_element_present(self, mock
218200
self.assertEqual(response["statusCode"], 400)
219201
body = json.loads(response["body"])
220202
self.assertEqual(body["resourceType"], "OperationOutcome")
221-
203+
222204
@patch("fhir_controller.get_supplier_permissions")
223205
def test_get_imms_by_identifer_both_body_and_query_params_present(self, mock_get_supplier_permissions):
224206
"""it should return Immunization Id if it exists"""
@@ -440,7 +422,7 @@ def test_validate_immunization_identifier_having_whitespace(self,mock_get_suppli
440422
self.assertEqual(response["statusCode"], 400)
441423
outcome = json.loads(response["body"])
442424
self.assertEqual(outcome["resourceType"], "OperationOutcome")
443-
425+
444426
@patch("fhir_controller.get_supplier_permissions")
445427
def test_validate_imms_id_invalid_vaccinetype(self, mock_get_permissions):
446428
"""it should validate lambda's Immunization id"""
@@ -756,7 +738,7 @@ def test_validate_immunization_identifier_having_whitespace(self,mock_get_suppli
756738
self.assertEqual(response["statusCode"], 400)
757739
outcome = json.loads(response["body"])
758740
self.assertEqual(outcome["resourceType"], "OperationOutcome")
759-
741+
760742
@patch("fhir_controller.get_supplier_permissions")
761743
def test_validate_imms_id_invalid_vaccinetype(self, mock_get_permissions):
762744
"""it should validate lambda's Immunization id"""
@@ -838,7 +820,7 @@ def test_get_imms_by_id_unauthorised_vax_error(self,mock_permissions):
838820
# Then
839821
mock_permissions.assert_called_once_with("test")
840822
self.assertEqual(response["statusCode"], 403)
841-
823+
842824
@patch("fhir_controller.get_supplier_permissions")
843825
def test_get_imms_by_id_no_vax_permission(self, mock_permissions):
844826
"""it should return Immunization Id if it exists"""
@@ -1160,7 +1142,7 @@ def test_update_immunization_UnauthorizedVaxError(self, mock_get_supplier_permis
11601142
response = self.controller.update_immunization(aws_event)
11611143
mock_get_supplier_permissions.assert_called_once_with("Test")
11621144
self.assertEqual(response["statusCode"], 403)
1163-
1145+
11641146
@patch("fhir_controller.get_supplier_permissions")
11651147
def test_update_immunization_UnauthorizedVaxError_check_for_non_batch(self, mock_get_supplier_permissions):
11661148
"""it should not update the Immunization record"""
@@ -1591,7 +1573,7 @@ def test_immunization_exception_not_found(self, mock_get_permissions):
15911573
body = json.loads(response["body"])
15921574
self.assertEqual(body["resourceType"], "OperationOutcome")
15931575
self.assertEqual(body["issue"][0]["code"], "not-found")
1594-
1576+
15951577
@patch("fhir_controller.get_supplier_permissions")
15961578
def test_immunization_unhandled_error(self, mock_get_supplier_permissions):
15971579
"""it should return server-error OperationOutcome if service throws UnhandledResponseError"""
@@ -1614,9 +1596,8 @@ def test_immunization_unhandled_error(self, mock_get_supplier_permissions):
16141596
self.assertEqual(body["resourceType"], "OperationOutcome")
16151597
self.assertEqual(body["issue"][0]["code"], "exception")
16161598

1617-
class TestSearchImmunizations(TestFhirControllerBase):
1599+
class TestSearchImmunizations(unittest.TestCase):
16181600
def setUp(self):
1619-
super().setUp()
16201601
self.service = create_autospec(FhirService)
16211602
self.authorizer = create_autospec(Authorization)
16221603
self.controller = FhirController(self.authorizer, self.service)
@@ -1626,15 +1607,11 @@ def setUp(self):
16261607
self.date_to_key = "-date.to"
16271608
self.nhs_number_valid_value = "9000000009"
16281609
self.patient_identifier_valid_value = f"{patient_identifier_system}|{self.nhs_number_valid_value}"
1629-
self.mock_redis_client.hkeys.return_value = MOCK_REDIS_V2D_RESPONSE
1630-
1631-
def tearDown(self):
1632-
return super().tearDown()
16331610

16341611
@patch("fhir_controller.get_supplier_permissions")
16351612
def test_get_search_immunizations(self, mock_get_supplier_permissions):
16361613
"""it should search based on patient_identifier and immunization_target"""
1637-
1614+
16381615
mock_get_supplier_permissions.return_value = ["COVID19.S"]
16391616
search_result = Bundle.construct()
16401617
self.service.search_immunizations.return_value = search_result
@@ -1818,7 +1795,7 @@ def test_post_search_immunizations(self,mock_get_supplier_permissions):
18181795
"headers": {"Content-Type": "application/x-www-form-urlencoded", "SupplierSystem": "Test"},
18191796
"body": base64_encoded_body,
18201797
}
1821-
1798+
18221799
# When
18231800
response = self.controller.search_immunizations(lambda_event)
18241801
# Then
@@ -1929,7 +1906,6 @@ def test_post_search_immunizations_for_unauthorized_vaccine_type_search_403(self
19291906

19301907
@patch("fhir_controller.process_search_params", wraps=process_search_params)
19311908
def test_uses_parameter_parser(self, process_search_params: Mock):
1932-
self.mock_redis_client.hkeys.return_value = MOCK_REDIS_V2D_RESPONSE
19331909
lambda_event = {
19341910
"multiValueQueryStringParameters": {
19351911
self.patient_identifier_key: ["https://fhir.nhs.uk/Id/nhs-number|9000000009"],

backend/tests/test_fhir_repository.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ def test_unauthorised_vax_delete(self):
576576
"Resource": json.dumps({"foo": "bar"}),
577577
"Version": 1,
578578
"PatientSK": "FLU#2516525251",
579+
"DeletedAt": "reinstated"
579580
}
580581
}
581582
)

0 commit comments

Comments
 (0)