Skip to content

Commit cdc3701

Browse files
committed
Review ready
1 parent 39f6d46 commit cdc3701

File tree

7 files changed

+272
-178
lines changed

7 files changed

+272
-178
lines changed

backend/poetry.lock

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

backend/src/parameter_parser.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import base64
22
import datetime
3+
import urllib.parse
34
from dataclasses import dataclass
45

56
from aws_lambda_typing.events import APIGatewayProxyEventV1
@@ -9,6 +10,8 @@
910
from clients import redis_client, logger
1011
from models.errors import ParameterException
1112
from models.constants import Constants
13+
from constants import SEARCH_IMMUNIZATION_BY_IDENTIFIER_PARAMETERS, SEARCH_IMMUNIZATIONS_PARAMETERS
14+
1215

1316
ParamValue = list[str]
1417
ParamContainer = dict[str, ParamValue]
@@ -162,3 +165,53 @@ def create_query_string(search_params: SearchParams) -> str:
162165
]
163166
search_params_qs = urlencode(sorted(params, key=lambda x: x[0]), safe=",")
164167
return search_params_qs
168+
169+
def body_to_dict(body):
170+
"""
171+
Converts a body array of {'key': ..., 'value': ...} to a dict.
172+
"""
173+
if isinstance(body, list):
174+
return {item['key']: item['value'] for item in body if 'key' in item and 'value' in item}
175+
return body if isinstance(body, dict) else {}
176+
177+
178+
def check_route_parameters(query_params, body, valid_params: list):
179+
try:
180+
181+
query_params = query_params or {}
182+
body_dict = body_to_dict(body)
183+
# merge query and body parameters
184+
all_params = {**query_params, **body_dict}
185+
186+
found = False
187+
for param in all_params:
188+
if param in valid_params:
189+
found = True
190+
break
191+
192+
# check if any params are not in the valid list
193+
for param in all_params:
194+
if param in valid_params:
195+
continue
196+
else:
197+
raise ValueError(f"Invalid body parameter: {param}")
198+
return found
199+
except Exception as e:
200+
raise ValueError(f"Error checking route parameters: {e}")
201+
202+
203+
def is_immunization_by_identifier(query_params, body):
204+
# check the parameters indicate search by identifier
205+
return check_route_parameters(query_params, body, SEARCH_IMMUNIZATION_BY_IDENTIFIER_PARAMETERS)
206+
207+
208+
def is_search_immunizations(query_params, body):
209+
# check the parameters indicate search for immunizations
210+
return check_route_parameters(query_params, body, SEARCH_IMMUNIZATIONS_PARAMETERS)
211+
212+
def get_parsed_body(body):
213+
if body:
214+
decoded_body = base64.b64decode(body).decode("utf-8")
215+
# Parse the URL encoded body
216+
return urllib.parse.parse_qs(decoded_body)
217+
return None

backend/src/search_imms_handler.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
from models.errors import Severity, Code, create_operation_outcome
1212
from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE
1313
from log_structure import function_info
14-
from search_parameter_validator import (
14+
from parameter_parser import (
1515
is_immunization_by_identifier,
16+
is_search_immunizations,
1617
get_parsed_body
1718
)
1819

@@ -32,8 +33,14 @@ def search_imms(event: events.APIGatewayProxyEventV1, controller: FhirController
3233
has_body = body is not None
3334
has_query_params = query_params is not None and query_params != {}
3435
if has_query_params or has_body:
35-
if is_immunization_by_identifier(query_params, get_parsed_body(body)):
36+
parsed_body = get_parsed_body(body)
37+
if is_immunization_by_identifier(query_params, parsed_body):
3638
return controller.get_immunization_by_identifier(event)
39+
elif is_search_immunizations(query_params, parsed_body):
40+
return controller.search_immunizations(event)
41+
else:
42+
raise ValueError("Missing search parameters")
43+
3744
response = controller.search_immunizations(event)
3845

3946
result_json = json.dumps(response)
@@ -50,7 +57,7 @@ def search_imms(event: events.APIGatewayProxyEventV1, controller: FhirController
5057
return response
5158

5259
except ValueError as ve:
53-
logger.exception("ValueError occurred")
60+
logger.exception("Invalid parameters")
5461
exp_error = create_operation_outcome(
5562
resource_id=str(uuid.uuid4()),
5663
severity=Severity.error,

backend/src/search_parameter_validator.py

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

backend/tests/test_parameter_parser.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import base64
22
import unittest
33
import datetime
4+
import urllib.parse
45
from unittest.mock import create_autospec, patch
56

67
from authorization import Authorization
@@ -13,7 +14,14 @@
1314
process_search_params,
1415
create_query_string,
1516
SearchParams,
17+
body_to_dict,
18+
check_route_parameters,
19+
is_immunization_by_identifier,
20+
is_search_immunizations,
21+
get_parsed_body,
1622
)
23+
from constants import SEARCH_IMMUNIZATION_BY_IDENTIFIER_PARAMETERS, SEARCH_IMMUNIZATIONS_PARAMETERS
24+
1725

1826
class TestParameterParser(unittest.TestCase):
1927
def setUp(self):
@@ -212,3 +220,81 @@ def test_create_query_string_with_multiple_immunization_targets_comma_separated(
212220
expected = "-immunization.target=b,c&patient.identifier=https%3A%2F%2Ffhir.nhs.uk%2FId%2Fnhs-number%7Ca"
213221

214222
self.assertEqual(expected, query_string)
223+
224+
225+
class TestSearchParameterValidator(unittest.TestCase):
226+
def test_body_to_dict_with_list(self):
227+
body = [{'key': 'identifier_1', 'value': ['id_1']}]
228+
result = body_to_dict(body)
229+
self.assertEqual(result, {'identifier_1': ['id_1']})
230+
231+
def test_body_to_dict_with_dict(self):
232+
body = {'identifier_2': ['id_2']}
233+
result = body_to_dict(body)
234+
self.assertEqual(result, {'identifier_2': ['id_2']})
235+
236+
def test_body_to_dict_with_none(self):
237+
body = None
238+
result = body_to_dict(body)
239+
self.assertEqual(result, {})
240+
241+
def test_check_route_parameters_valid(self):
242+
query_params = {'identifier_3': 'id_3'}
243+
body = None
244+
valid_params = ['identifier_3']
245+
self.assertTrue(check_route_parameters(query_params, body, valid_params))
246+
247+
def test_check_route_parameters_invalid(self):
248+
query_params = {'invalid_4': 'id_4'}
249+
body = None
250+
valid_params = ['identifier_4']
251+
with self.assertRaises(ValueError) as cm:
252+
check_route_parameters(query_params, body, valid_params)
253+
self.assertIn("Invalid body parameter: invalid_4", str(cm.exception))
254+
255+
def test_check_route_parameters_valid_body_list(self):
256+
query_params = {}
257+
body = [{'key': 'identifier_5', 'value': ['id_5']}]
258+
valid_params = ['identifier_5']
259+
self.assertTrue(check_route_parameters(query_params, body, valid_params))
260+
261+
def test_check_route_parameters_invalid_body_list(self):
262+
query_params = {}
263+
body = [{'key': 'badkey_6', 'value': ['id_6']}]
264+
valid_params = ['identifier_6']
265+
with self.assertRaises(ValueError) as cm:
266+
check_route_parameters(query_params, body, valid_params)
267+
self.assertIn("Invalid body parameter: badkey_6", str(cm.exception))
268+
269+
def test_is_immunization_by_identifier_true(self):
270+
query_params = {'identifier': 'id_7'}
271+
body = None
272+
self.assertTrue(is_immunization_by_identifier(query_params, body))
273+
274+
def test_is_immunization_by_identifier_false(self):
275+
query_params = {'badkey_8': 'id_8'}
276+
body = None
277+
with self.assertRaises(ValueError):
278+
is_immunization_by_identifier(query_params, body)
279+
280+
def test_is_search_immunizations_true(self):
281+
query_params = {'patient.identifier': 'id'}
282+
body = None
283+
self.assertTrue(is_search_immunizations(query_params, body))
284+
285+
def test_is_search_immunizations_false(self):
286+
query_params = {'badkey_10': 'bad_10'}
287+
body = None
288+
with self.assertRaises(ValueError):
289+
is_search_immunizations(query_params, body)
290+
291+
def test_get_parsed_body_none(self):
292+
self.assertIsNone(get_parsed_body(None))
293+
294+
def test_get_parsed_body_valid(self):
295+
data = {'identifier_11': ['id_11'], 'key_11': ['data_11']}
296+
encoded = urllib.parse.urlencode({k: v[0] for k, v in data.items()})
297+
b64 = base64.b64encode(encoded.encode("utf-8")).decode("utf-8")
298+
parsed = get_parsed_body(b64)
299+
self.assertEqual(parsed, data)
300+

backend/tests/test_search_imms.py

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from search_imms_handler import search_imms
88
from pathlib import Path
99
from constants import GENERIC_SERVER_ERROR_DIAGNOSTICS_MESSAGE
10+
from utils.generic_utils import encode_b64
1011

1112
script_location = Path(__file__).absolute().parent
1213

@@ -59,11 +60,13 @@ def test_search_immunizations_to_get_imms_id(self):
5960
self.controller.get_immunization_by_identifier.assert_called_once_with(lambda_event)
6061
self.assertDictEqual(exp_res, act_res)
6162

63+
64+
6265
def test_search_immunizations_get_id_from_body(self):
63-
"""it should return a list of Immunizations"""
66+
"""it should NOT return a list of Immunizations. ID is not a valid parameter """
6467
lambda_event = {
6568
"pathParameters": {"id": "an-id"},
66-
"body": "cGF0aWVudC5pZGVudGlmaWVyPWh0dHBzJTNBJTJGJTJGZmhpci5uaHMudWslMkZJZCUyRm5ocy1udW1iZXIlN0M5NjkzNjMyMTA5Ji1pbW11bml6YXRpb24udGFyZ2V0PUNPVklEMTkmX2luY2x1ZGU9SW1tdW5pemF0aW9uJTNBcGF0aWVudCZpZGVudGlmaWVyPWh0dHBzJTNBJTJGJTJGc3VwcGxpZXJBQkMlMkZpZGVudGlmaWVycyUyRnZhY2MlN0NmMTBiNTliMy1mYzczLTQ2MTYtOTljOS05ZTg4MmFiMzExODQmX2VsZW1lbnRzPWlkJTJDbWV0YSZpZD1z",
69+
"body": encode_b64('"id"="s"'),
6770
"queryStringParameters": None,
6871
}
6972
exp_res = {"a-key": "a-value"}
@@ -74,8 +77,13 @@ def test_search_immunizations_get_id_from_body(self):
7477
act_res = search_imms(lambda_event, self.controller)
7578

7679
# Then
77-
self.controller.get_immunization_by_identifier.assert_called_once_with(lambda_event)
78-
self.assertDictEqual(exp_res, act_res)
80+
# ID is not a valid parameter
81+
self.controller.get_immunization_by_identifier.assert_not_called()
82+
act_body = json.loads(act_res["body"])
83+
act_issue = act_body["issue"][0]
84+
self.assertEqual(act_issue["code"], Code.invalid)
85+
self.assertEqual(act_issue["severity"], Severity.error)
86+
self.assertEqual(act_issue["diagnostics"], 'Error checking route parameters: Invalid body parameter: "id"')
7987

8088
def test_search_immunizations_get_id_from_body_passing_none(self):
8189
"""it should enter search_immunizations as both the request params are none"""
@@ -166,10 +174,9 @@ def test_search_handle_exception(self):
166174
# Then
167175
act_body = json.loads(act_res["body"])
168176

169-
exp_issue = exp_error["issue"][0]
170177
act_issue = act_body["issue"][0]
171-
self.assertEqual(exp_issue["code"], act_issue["code"])
172-
self.assertEqual(exp_issue["severity"], act_issue["severity"])
178+
self.assertEqual(act_issue["code"], Code.server_error)
179+
self.assertEqual(act_issue["severity"], Severity.error)
173180
self.assertEqual(act_res["statusCode"], 500)
174181

175182
def test_search_immunizations_invalid_params(self):
@@ -186,22 +193,8 @@ def test_search_immunizations_invalid_params(self):
186193
act_res = search_imms(lambda_event, self.controller)
187194

188195
# Then
196+
act_body = json.loads(act_res["body"])
197+
act_issue = act_body["issue"][0]
189198
self.assertEqual(act_res["statusCode"], 400)
190-
191-
192-
def test_search_immunizations_invalid_params(self):
193-
"""it should return 400 if only invalid parameters are provided"""
194-
lambda_event = {
195-
"pathParameters": {"id": "an-id"},
196-
"queryStringParameters": {
197-
"_elephants": "id,meta",
198-
},
199-
"body": None,
200-
}
201-
202-
# When
203-
act_res = search_imms(lambda_event, self.controller)
204-
205-
# Then
206-
self.assertEqual(act_res["statusCode"], 400)
207-
199+
self.assertEqual(act_issue["severity"], Severity.error)
200+
self.assertEqual(act_issue["diagnostics"], 'Error checking route parameters: Invalid body parameter: _elephants')

backend/tests/utils/generic_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import os
55
import unittest
6+
import base64
67
from decimal import Decimal
78
from typing import Literal, Any
89
from jsonpath_ng.ext import parse
@@ -91,3 +92,12 @@ def update_contained_resource_field(
9192
{field_to_update: update_value}
9293
)
9394
return json_data
95+
96+
97+
def encode_b64(data):
98+
99+
if isinstance(data, str):
100+
data = data.encode('utf-8')
101+
102+
encoded = base64.b64encode(data)
103+
return encoded.decode('utf-8')

0 commit comments

Comments
 (0)