Skip to content

Commit fd3d546

Browse files
committed
AMB-1741 add controller->service->repository pattern
1 parent 8f658eb commit fd3d546

File tree

10 files changed

+199
-20
lines changed

10 files changed

+199
-20
lines changed

lambda_code/src/dynamodb.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ def __init__(self, table_name=None, endpoint_url=None):
1313
self.table = db.Table(table_name)
1414

1515
def get_event_by_id(self, event_id):
16-
# TODO: the main index doesn't need sort-key. You can use get_item instead of query
1716
response = self.table.get_item(Key={"PK": event_id})
1817

1918
if "Item" in response:

lambda_code/src/fhir_controller.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,34 @@
1-
from dataclasses import dataclass
1+
import json
2+
import re
3+
import uuid
24

3-
from fhir.resources.resource import Resource
4-
5-
6-
@dataclass
7-
class FhirResponse:
8-
headers: dict
9-
body: Resource
10-
status_code: int
5+
from fhir_service import FhirService
6+
from models.errors import ApiError, Severity, Code
117

128

139
class FhirController:
10+
immunisation_id_pattern = r"^[A-Za-z0-9\-.]{1,64}$"
1411

15-
def __init__(self, fhir_service):
12+
def __init__(self, fhir_service: FhirService):
1613
self.fhir_service = fhir_service
1714

18-
def get_immunisation_by_id(self, aws_event) -> FhirResponse:
19-
pass
15+
def get_immunisation_by_id(self, aws_event) -> dict:
16+
imms_id = aws_event["pathParameters"]["id"]
17+
18+
if not re.match(self.immunisation_id_pattern, imms_id):
19+
api_error = ApiError(id=str(uuid.uuid4()), severity=Severity.error, code=Code.invalid,
20+
diagnostics="the provided event ID is either missing or not in the expected format.")
21+
res_body = api_error.to_uk_core().dict()
22+
return FhirController._create_response(400, json.dumps(res_body))
23+
24+
resource = self.fhir_service.get_immunisation_by_id(imms_id)
25+
if resource:
26+
return FhirController._create_response(200, resource.json())
27+
else:
28+
api_error = ApiError(id=str(uuid.uuid4()), severity=Severity.error, code=Code.not_found,
29+
diagnostics="The requested resource was not found.")
30+
res_body = api_error.to_uk_core().dict()
31+
return FhirController._create_response(404, json.dumps(res_body))
2032

2133
@staticmethod
2234
def _create_response(status_code, body):

lambda_code/src/fhir_repository.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import json
2+
import os
3+
from typing import Optional
4+
5+
import boto3
6+
7+
8+
class FhirRepository:
9+
def __init__(self, table_name=None, endpoint_url=None):
10+
if not table_name:
11+
table_name = os.environ["DYNAMODB_TABLE_NAME"]
12+
db = boto3.resource("dynamodb", endpoint_url=endpoint_url, region_name='eu-west-2')
13+
self.table = db.Table(table_name)
14+
15+
16+
class ImmunisationRepository(FhirRepository):
17+
def get_immunisation_by_id(self, imms_id: str) -> Optional[dict]:
18+
response = self.table.get_item(Key={"PK": imms_id})
19+
20+
if "Item" in response:
21+
return json.loads(response["Item"]["Event"])
22+
else:
23+
return None

lambda_code/src/fhir_service.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,15 @@
1+
from typing import Optional
2+
3+
from fhir.resources.immunization import Immunization
4+
5+
from fhir_repository import ImmunisationRepository
6+
7+
18
class FhirService:
2-
pass
9+
10+
def __init__(self, imms_repo: ImmunisationRepository):
11+
self.immunisation_repo = imms_repo
12+
13+
def get_immunisation_by_id(self, imms_id: str) -> Optional[Immunization]:
14+
imms = self.immunisation_repo.get_immunisation_by_id(imms_id)
15+
return Immunization.parse_obj(imms)

lambda_code/src/get_imms_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def is_valid_id(_event_id):
4949
if not is_valid_id(event_id) or not event_id:
5050
body = json.dumps(
5151
create_operation_outcome(str(uuid.uuid4()),
52-
"he provided event ID is either missing or not in the expected format.",
52+
"the provided event ID is either missing or not in the expected format.",
5353
"invalid"))
5454
return FhirController._create_response(400, body)
5555

lambda_code/src/models/errors.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ class Severity(str, Enum):
99

1010

1111
class Code(str, Enum):
12-
not_found = "not_found"
12+
not_found = "not-found"
13+
invalid = "invalid"
1314

1415

1516
class ApiError(BaseModel, use_enum_values=True):

lambda_code/tests/test_api_errors.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
sys.path.append(f"{os.path.dirname(os.path.abspath(__file__))}/../src")
66

7-
from models.errors import ApiError, Severity, Code
7+
from models.errors import ApiError, Severity, Code
88

99

1010
class TestApiErrors(unittest.TestCase):
@@ -23,5 +23,3 @@ def test_error_to_uk_core(self):
2323
self.assertEqual(issue["severity"], "error")
2424
self.assertEqual(issue["diagnostics"], diag)
2525
self.assertEqual(issue["details"]["coding"][0]["code"], "NOT_FOUND")
26-
27-

lambda_code/tests/test_fhir_controller.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import json
12
import os
23
import sys
34
import unittest
45
from unittest.mock import create_autospec
56

7+
from fhir.resources.immunization import Immunization
8+
69
sys.path.append(f"{os.path.dirname(os.path.abspath(__file__))}/../src")
710

811
from fhir_controller import FhirController
@@ -23,4 +26,56 @@ def test_create_response(self):
2326
self.assertDictEqual(headers, {
2427
"Content-Type": "application/fhir+json",
2528
})
26-
self.assertEqual(res["body"], "a bodys")
29+
self.assertEqual(res["body"], "a body")
30+
31+
32+
class TestFhirControllerGetImmunisationById(unittest.TestCase):
33+
def setUp(self):
34+
self.service = create_autospec(FhirService)
35+
self.controller = FhirController(self.service)
36+
37+
def test_get_imms_by_id(self):
38+
"""it should return Immunization resource if it exists"""
39+
# Given
40+
imms_id = "a-id"
41+
self.service.get_immunisation_by_id.return_value = Immunization.construct()
42+
lambda_event = {"pathParameters": {"id": imms_id}}
43+
44+
# When
45+
response = self.controller.get_immunisation_by_id(lambda_event)
46+
47+
# Then
48+
self.service.get_immunisation_by_id.assert_called_once_with(imms_id)
49+
50+
self.assertEqual(response["statusCode"], 200)
51+
body = json.loads(response["body"])
52+
self.assertEqual(body["resourceType"], "Immunization")
53+
54+
def test_not_found(self):
55+
"""it should return not-found OperationOutcome if it doesn't exist"""
56+
# Given
57+
imms_id = "a-non-existing-id"
58+
self.service.get_immunisation_by_id.return_value = None
59+
lambda_event = {"pathParameters": {"id": imms_id}}
60+
61+
# When
62+
response = self.controller.get_immunisation_by_id(lambda_event)
63+
64+
# Then
65+
self.service.get_immunisation_by_id.assert_called_once_with(imms_id)
66+
67+
self.assertEqual(response["statusCode"], 404)
68+
body = json.loads(response["body"])
69+
self.assertEqual(body["resourceType"], "OperationOutcome")
70+
self.assertEqual(body["issue"][0]["code"], "not-found")
71+
72+
def test_validate_imms_id(self):
73+
"""it should validate lambda's immunisation id"""
74+
invalid_id = {"pathParameters": {"id": "invalid %$ id"}}
75+
76+
response = self.controller.get_immunisation_by_id(invalid_id)
77+
78+
self.assertEqual(self.service.get_immunisation_by_id.call_count, 0)
79+
self.assertEqual(response["statusCode"], 400)
80+
outcome = json.loads(response["body"])
81+
self.assertEqual(outcome["resourceType"], "OperationOutcome")
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import os
2+
import sys
3+
import unittest
4+
5+
sys.path.append(f"{os.path.dirname(os.path.abspath(__file__))}/../src")
6+
7+
from fhir_repository import ImmunisationRepository
8+
9+
10+
class TestImmunisationRepository(unittest.TestCase):
11+
def setUp(self):
12+
self.repository = ImmunisationRepository(table_name="a-table")
13+
14+
def test_get_immunisation_by_id(self):
15+
"""it should find an Immunization by id"""
16+
pass
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import os
2+
import sys
3+
import unittest
4+
from unittest.mock import create_autospec
5+
6+
from fhir.resources.immunization import Immunization
7+
8+
sys.path.append(f"{os.path.dirname(os.path.abspath(__file__))}/../src")
9+
10+
from fhir_repository import ImmunisationRepository
11+
from fhir_service import FhirService
12+
13+
14+
class TestFhirService(unittest.TestCase):
15+
def setUp(self):
16+
self.imms_repo = create_autospec(ImmunisationRepository)
17+
self.fhir_service = FhirService(self.imms_repo)
18+
19+
def test_get_immunisation_by_id(self):
20+
"""it should find an Immunization by id"""
21+
imms_id = "a-id"
22+
23+
self.imms_repo.get_immunisation_by_id.return_value = self._create_an_immunisation_obj(imms_id).dict()
24+
25+
# When
26+
act_imms = self.fhir_service.get_immunisation_by_id(imms_id)
27+
28+
# Then
29+
self.imms_repo.get_immunisation_by_id.assert_called_once_with(imms_id)
30+
self.assertEqual(act_imms.id, imms_id)
31+
32+
@staticmethod
33+
def _create_an_immunisation_obj(imms_id) -> Immunization:
34+
base_imms = {
35+
"resourceType": "Immunization",
36+
"id": imms_id,
37+
"identifier": [
38+
{
39+
"system": "https://supplierABC/ODSCode",
40+
"value": imms_id
41+
}
42+
],
43+
"status": "completed",
44+
"occurrenceDateTime": "2020-12-14T10:08:15+00:00",
45+
"patient": {
46+
"reference": "urn:uuid:124fcb63-669c-4a3c-af2b-caf55de167ec",
47+
"type": "Patient",
48+
"identifier": {
49+
"system": "https://fhir.nhs.uk/Id/nhs-number",
50+
"value": "9000000009"
51+
}
52+
},
53+
"vaccineCode": {
54+
"coding": [{
55+
"system": "http://snomed.info/sct",
56+
"code": "39114911000001105",
57+
"display": "some text"
58+
}]
59+
},
60+
}
61+
return Immunization.parse_obj(base_imms)
62+

0 commit comments

Comments
 (0)