Skip to content

Commit ec75255

Browse files
authored
VED-856: Improve Code Coverage in Codebase (#1319)
* test coverage on backend and common models
1 parent 4746d8b commit ec75255

7 files changed

Lines changed: 291 additions & 4 deletions

File tree

lambdas/backend/Makefile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ package: build
99

1010
test:
1111
$(TEST_ENV) python -m unittest
12-
1312
coverage-run:
1413
$(TEST_ENV) coverage run --source=src -m unittest discover
1514

lambdas/backend/src/get_status_handler.py

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import os
2+
import tempfile
3+
import unittest
4+
5+
from local_lambda import load_string
6+
7+
8+
class TestLoadString(unittest.TestCase):
9+
def test_reads_file_contents(self):
10+
fd, path = tempfile.mkstemp()
11+
self.addCleanup(os.unlink, path)
12+
with os.fdopen(fd, "w") as f:
13+
f.write("hello world")
14+
15+
self.assertEqual(load_string(path), "hello world")
16+
17+
def test_raises_if_file_not_found(self):
18+
with self.assertRaises(FileNotFoundError):
19+
load_string("/nonexistent/file/path.py")
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import json
2+
import unittest
3+
4+
from not_found_handler import ALLOWED_METHODS, not_found
5+
6+
7+
class TestNotFoundHandler(unittest.TestCase):
8+
"""Tests for the not_found_handler functionality"""
9+
10+
def test_method_not_allowed_unsupported_method(self):
11+
"""Test that unsupported HTTP methods return 405 Method Not Allowed"""
12+
event = {"httpMethod": "PATCH"}
13+
14+
response = not_found(event, None)
15+
16+
self.assertEqual(response["statusCode"], 405)
17+
self.assertEqual(response["headers"]["Content-Type"], "application/json")
18+
self.assertEqual(response["headers"]["Allow"], "GET, POST, DELETE, PUT")
19+
20+
# Verify the response body structure
21+
body = json.loads(response["body"])
22+
self.assertEqual(body["resourceType"], "OperationOutcome")
23+
self.assertEqual(body["issue"][0]["severity"], "error")
24+
self.assertEqual(body["issue"][0]["code"], "not-supported")
25+
self.assertEqual(body["issue"][0]["diagnostics"], "Method Not Allowed")
26+
27+
def test_method_not_allowed_other_unsupported_method(self):
28+
"""Test that other unsupported HTTP methods also return 405"""
29+
unsupported_methods = ["PUTT", "PATCH", "HEAD", "OPTIONS", "TRACE", "CONNECT"]
30+
31+
for method in unsupported_methods:
32+
with self.subTest(method=method):
33+
event = {"httpMethod": method}
34+
response = not_found(event, None)
35+
36+
self.assertEqual(response["statusCode"], 405)
37+
self.assertEqual(response["headers"]["Allow"], "GET, POST, DELETE, PUT")
38+
39+
def test_not_found_allowed_method(self):
40+
"""Test that allowed HTTP methods return 404 Not Found"""
41+
allowed_methods = ALLOWED_METHODS # ["GET", "POST", "DELETE", "PUT"]
42+
43+
for method in allowed_methods:
44+
with self.subTest(method=method):
45+
event = {"httpMethod": method}
46+
response = not_found(event, None)
47+
48+
self.assertEqual(response["statusCode"], 404)
49+
self.assertEqual(response["headers"]["Content-Type"], "application/json")
50+
self.assertNotIn("Allow", response["headers"]) # No Allow header for 404
51+
52+
# Verify the response body structure
53+
body = json.loads(response["body"])
54+
self.assertEqual(body["resourceType"], "OperationOutcome")
55+
self.assertEqual(body["issue"][0]["severity"], "error")
56+
self.assertEqual(body["issue"][0]["code"], "not-found")
57+
self.assertEqual(body["issue"][0]["diagnostics"], "The requested resource was not found.")
58+
59+
def test_not_found_missing_http_method(self):
60+
"""Test that missing httpMethod defaults to 405 Method Not Allowed"""
61+
event = {}
62+
63+
response = not_found(event, None)
64+
65+
self.assertEqual(response["statusCode"], 405)
66+
self.assertEqual(response["headers"]["Content-Type"], "application/json")
67+
self.assertEqual(response["headers"]["Allow"], "GET, POST, DELETE, PUT")
68+
69+
def test_not_found_none_http_method(self):
70+
"""Test that None httpMethod defaults to 405 Method Not Allowed"""
71+
event = {"httpMethod": None}
72+
73+
response = not_found(event, None)
74+
75+
self.assertEqual(response["statusCode"], 405)
76+
self.assertEqual(response["headers"]["Content-Type"], "application/json")
77+
self.assertEqual(response["headers"]["Allow"], "GET, POST, DELETE, PUT")
78+
79+
def test_allowed_methods_constant(self):
80+
"""Test that ALLOWED_METHODS contains the expected HTTP methods"""
81+
expected_methods = ["GET", "POST", "DELETE", "PUT"]
82+
self.assertEqual(ALLOWED_METHODS, expected_methods)
83+
84+
85+
if __name__ == "__main__":
86+
unittest.main()
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import unittest
2+
from unittest.mock import patch
3+
4+
from timer import timed
5+
6+
7+
class TestTimedDecorator(unittest.TestCase):
8+
@patch("timer.time")
9+
@patch("timer.logger")
10+
def test_timed_logs_correct_execution_time(self, mock_logger, mock_time):
11+
mock_time.time.side_effect = [1000.0, 1000.12345]
12+
13+
@timed
14+
def sample_function():
15+
return "success"
16+
17+
result = sample_function()
18+
19+
self.assertEqual(result, "success")
20+
mock_logger.info.assert_called_once_with({"time_taken": "sample_function ran in 0.12345s"})
21+
22+
@patch("timer.time")
23+
@patch("timer.logger")
24+
def test_timed_preserves_function_name(self, mock_logger, mock_time):
25+
mock_time.time.side_effect = [0.0, 0.0]
26+
27+
@timed
28+
def my_named_function():
29+
pass
30+
31+
my_named_function()
32+
33+
logged_payload = mock_logger.info.call_args[0][0]
34+
self.assertIn("my_named_function", logged_payload["time_taken"])

lambdas/shared/tests/test_common/test_generic_utils.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
"""Tests for generic utils"""
22

3+
import datetime
34
import unittest
45
from copy import deepcopy
56
from unittest.mock import Mock, patch
67

8+
from common.models.utils.generic_utils import (
9+
check_keys_in_sources,
10+
create_diagnostics,
11+
create_diagnostics_error,
12+
extract_file_key_elements,
13+
generate_field_location_for_name,
14+
get_contained_practitioner,
15+
get_nhs_number,
16+
get_occurrence_datetime,
17+
is_actor_referencing_contained_resource,
18+
)
719
from common.models.utils.validation_utils import (
820
convert_disease_codes_to_vaccine_type,
921
get_vaccine_type,
@@ -25,6 +37,127 @@ def tearDown(self):
2537
"""Tear down after each test. This runs after every test"""
2638
self.redis_getter_patcher.stop()
2739

40+
def test_get_nhs_number_success(self):
41+
"""Test get_nhs_number returns NHS number when present"""
42+
expected_nhs = "1234567890"
43+
imms = {"contained": [{"resourceType": "Patient", "identifier": [{"value": expected_nhs}]}]}
44+
result = get_nhs_number(imms)
45+
self.assertEqual(result, expected_nhs)
46+
47+
def test_get_nhs_number_missing_patient(self):
48+
"""Test get_nhs_number returns 'TBC' when patient not found"""
49+
imms = {"contained": []}
50+
result = get_nhs_number(imms)
51+
self.assertEqual(result, "TBC")
52+
53+
def test_get_nhs_number_missing_identifier(self):
54+
"""Test get_nhs_number returns 'TBC' when identifier not found"""
55+
imms = {"contained": [{"resourceType": "Patient"}]}
56+
result = get_nhs_number(imms)
57+
self.assertEqual(result, "TBC")
58+
59+
def test_get_contained_practitioner(self):
60+
"""Test get_contained_practitioner returns practitioner resource"""
61+
imms = {
62+
"contained": [
63+
{"resourceType": "Patient", "id": "patient1"},
64+
{"resourceType": "Practitioner", "id": "practitioner1"},
65+
]
66+
}
67+
result = get_contained_practitioner(imms)
68+
self.assertEqual(result["id"], "practitioner1")
69+
70+
def test_is_actor_referencing_contained_resource_true(self):
71+
"""Test is_actor_referencing_contained_resource returns True for matching reference"""
72+
element = {"actor": {"reference": "#patient1"}}
73+
result = is_actor_referencing_contained_resource(element, "patient1")
74+
self.assertTrue(result)
75+
76+
def test_is_actor_referencing_contained_resource_false(self):
77+
"""Test is_actor_referencing_contained_resource returns False for non-matching reference"""
78+
element = {"actor": {"reference": "#patient2"}}
79+
result = is_actor_referencing_contained_resource(element, "patient1")
80+
self.assertFalse(result)
81+
82+
def test_is_actor_referencing_contained_resource_missing_key(self):
83+
"""Test is_actor_referencing_contained_resource returns False when keys missing"""
84+
element = {}
85+
result = is_actor_referencing_contained_resource(element, "patient1")
86+
self.assertFalse(result)
87+
88+
def test_get_occurrence_datetime_valid(self):
89+
"""Test get_occurrence_datetime returns datetime for valid occurrenceDateTime"""
90+
immunization = {"occurrenceDateTime": "2023-01-15T10:30:00Z"}
91+
result = get_occurrence_datetime(immunization)
92+
# The result will be timezone-aware due to the 'Z' in the ISO string
93+
expected = datetime.datetime(2023, 1, 15, 10, 30, 0, tzinfo=datetime.UTC)
94+
self.assertEqual(result, expected)
95+
96+
def test_get_occurrence_datetime_none(self):
97+
"""Test get_occurrence_datetime returns None when occurrenceDateTime missing"""
98+
immunization = {}
99+
result = get_occurrence_datetime(immunization)
100+
self.assertIsNone(result)
101+
102+
def test_create_diagnostics(self):
103+
"""Test create_diagnostics returns expected error structure"""
104+
result = create_diagnostics()
105+
expected = {
106+
"diagnostics": "Validation errors: contained[?(@.resourceType=='Patient')].identifier[?(@.system=='https://fhir.nhs.uk/Id/nhs-number')].value does not exists."
107+
}
108+
self.assertEqual(result, expected)
109+
110+
def test_create_diagnostics_error_system(self):
111+
"""Test create_diagnostics_error for system mismatch"""
112+
result = create_diagnostics_error("system")
113+
expected = {"diagnostics": "Validation errors: identifier[0].system doesn't match with the stored content"}
114+
self.assertEqual(result, expected)
115+
116+
def test_create_diagnostics_error_value(self):
117+
"""Test create_diagnostics_error for value mismatch"""
118+
result = create_diagnostics_error("value")
119+
expected = {"diagnostics": "Validation errors: identifier[0].value doesn't match with the stored content"}
120+
self.assertEqual(result, expected)
121+
122+
def test_create_diagnostics_error_both(self):
123+
"""Test create_diagnostics_error for both system and value mismatch"""
124+
result = create_diagnostics_error("Both")
125+
expected = {
126+
"diagnostics": "Validation errors: identifier[0].system and identifier[0].value doesn't match with the stored content"
127+
}
128+
self.assertEqual(result, expected)
129+
130+
def test_check_keys_in_sources_query_params(self):
131+
"""Test check_keys_in_sources with queryStringParameters"""
132+
event = {"queryStringParameters": {"key1": "value1", "key2": "value2"}}
133+
not_required_keys = ["key1"]
134+
result = check_keys_in_sources(event, not_required_keys)
135+
self.assertEqual(result, ["key1"])
136+
137+
def test_check_keys_in_sources_body(self):
138+
"""Test check_keys_in_sources with body content"""
139+
import base64
140+
141+
body_data = "key1=value1&key2=value2"
142+
encoded_body = base64.b64encode(body_data.encode()).decode()
143+
event = {"body": encoded_body}
144+
not_required_keys = ["key1"]
145+
result = check_keys_in_sources(event, not_required_keys)
146+
self.assertEqual(result, ["key1"])
147+
148+
def test_generate_field_location_for_name(self):
149+
"""Test generate_field_location_for_name creates correct path"""
150+
result = generate_field_location_for_name("0", "given", "Patient")
151+
expected = "contained[?(@.resourceType=='Patient')].name[0].given"
152+
self.assertEqual(result, expected)
153+
154+
def test_extract_file_key_elements(self):
155+
"""Test extract_file_key_elements extracts vaccine type from file key"""
156+
file_key = "COVID_VACCINE_DATA.JSON"
157+
result = extract_file_key_elements(file_key)
158+
expected = {"vaccine_type": "COVID"}
159+
self.assertEqual(result, expected)
160+
28161
def test_convert_disease_codes_to_vaccine_type_returns_vaccine_type(self):
29162
"""
30163
If the mock returns a vaccine type, convert_disease_codes_to_vaccine_type returns that vaccine type.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import unittest
2+
3+
from fhir.resources.R4B.identifier import Identifier
4+
5+
from common.models.immunization_record_metadata import ImmunizationRecordMetadata
6+
7+
8+
class TestImmunizationRecordMetadata(unittest.TestCase):
9+
def test_initialization(self):
10+
identifier = Identifier.construct(value="12345")
11+
12+
metadata = ImmunizationRecordMetadata(
13+
identifier=identifier, resource_version=1, is_deleted=False, is_reinstated=False
14+
)
15+
16+
self.assertEqual(metadata.identifier.value, "12345")
17+
self.assertEqual(metadata.resource_version, 1)
18+
self.assertFalse(metadata.is_deleted)
19+
self.assertFalse(metadata.is_reinstated)

0 commit comments

Comments
 (0)