Skip to content

Commit 4afb122

Browse files
ELI-578 Unit tests
1 parent 8f4e28a commit 4afb122

File tree

3 files changed

+209
-0
lines changed

3 files changed

+209
-0
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import json
2+
import pytest
3+
from unittest.mock import MagicMock
4+
5+
from eligibility_signposting_api.model.consumer_mapping import ConsumerId
6+
from eligibility_signposting_api.repos.consumer_mapping_repo import ConsumerMappingRepo, BucketName
7+
8+
9+
class TestConsumerMappingRepo:
10+
@pytest.fixture
11+
def mock_s3_client(self):
12+
return MagicMock()
13+
14+
@pytest.fixture
15+
def repo(self, mock_s3_client):
16+
return ConsumerMappingRepo(
17+
s3_client=mock_s3_client,
18+
bucket_name=BucketName("test-bucket")
19+
)
20+
21+
def test_get_permitted_campaign_ids_success(self, repo, mock_s3_client):
22+
# Given
23+
consumer_id = "user-123"
24+
expected_campaigns = ["flu-2024", "covid-2024"]
25+
mapping_data = {
26+
consumer_id: expected_campaigns
27+
}
28+
29+
mock_s3_client.list_objects.return_value = {
30+
"Contents": [{"Key": "mappings.json"}]
31+
}
32+
33+
body_json = json.dumps(mapping_data).encode("utf-8")
34+
mock_s3_client.get_object.return_value = {
35+
"Body": MagicMock(read=lambda: body_json)
36+
}
37+
38+
# When
39+
result = repo.get_permitted_campaign_ids(ConsumerId(consumer_id))
40+
41+
# Then
42+
assert result == expected_campaigns
43+
mock_s3_client.list_objects.assert_called_once_with(Bucket="test-bucket")
44+
mock_s3_client.get_object.assert_called_once_with(
45+
Bucket="test-bucket",
46+
Key="mappings.json"
47+
)
48+
49+
def test_get_permitted_campaign_ids_returns_none_when_missing(self, repo, mock_s3_client):
50+
# Setup data where the consumer_id doesn't exist
51+
mock_s3_client.list_objects.return_value = {"Contents": [{"Key": "mappings.json"}]}
52+
body_json = json.dumps({"other-user": ["camp-1"]}).encode("utf-8")
53+
mock_s3_client.get_object.return_value = {"Body": MagicMock(read=lambda: body_json)}
54+
55+
result = repo.get_permitted_campaign_ids(ConsumerId("missing-user"))
56+
57+
assert result is None

tests/unit/services/test_eligibility_services.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,32 @@
33
import pytest
44
from hamcrest import assert_that, empty
55

6+
from eligibility_signposting_api.model.campaign_config import CampaignID, CampaignConfig
67
from eligibility_signposting_api.model.eligibility_status import NHSNumber
78
from eligibility_signposting_api.repos import CampaignRepo, NotFoundError, PersonRepo
89
from eligibility_signposting_api.repos.consumer_mapping_repo import ConsumerMappingRepo
910
from eligibility_signposting_api.services import EligibilityService, UnknownPersonError
1011
from eligibility_signposting_api.services.calculators.eligibility_calculator import EligibilityCalculatorFactory
12+
from eligibility_signposting_api.services.eligibility_services import NoPermittedCampaignsError
1113
from tests.fixtures.matchers.eligibility import is_eligibility_status
1214

15+
@pytest.fixture
16+
def mock_repos():
17+
return {
18+
"person": MagicMock(spec=PersonRepo),
19+
"campaign": MagicMock(spec=CampaignRepo),
20+
"consumer": MagicMock(spec=ConsumerMappingRepo),
21+
"factory": MagicMock(spec=EligibilityCalculatorFactory)
22+
}
23+
24+
@pytest.fixture
25+
def service(mock_repos):
26+
return EligibilityService(
27+
mock_repos["person"],
28+
mock_repos["campaign"],
29+
mock_repos["consumer"],
30+
mock_repos["factory"]
31+
)
1332

1433
def test_eligibility_service_returns_from_repo():
1534
# Given
@@ -45,3 +64,50 @@ def test_eligibility_service_for_nonexistent_nhs_number():
4564
category="ALL",
4665
consumer_id="test_consumer_id",
4766
)
67+
68+
69+
def test_get_eligibility_status_filters_permitted_campaigns(service, mock_repos):
70+
"""Tests that ONLY permitted campaigns reach the calculator factory."""
71+
# Given
72+
nhs_number = NHSNumber("1234567890")
73+
person_data = {"age": 65, "vulnerable": True}
74+
mock_repos["person"].get_eligibility_data.return_value = person_data
75+
76+
# Available campaigns in system
77+
camp_a = MagicMock(spec=CampaignConfig, id=CampaignID("CAMP_A"))
78+
camp_b = MagicMock(spec=CampaignConfig, id=CampaignID("CAMP_B"))
79+
mock_repos["campaign"].get_campaign_configs.return_value = [camp_a, camp_b]
80+
81+
# Consumer is only permitted to see CAMP_B
82+
mock_repos["consumer"].get_permitted_campaign_ids.return_value = [CampaignID("CAMP_B")]
83+
84+
# Mock calculator behavior
85+
mock_calc = MagicMock()
86+
mock_repos["factory"].get.return_value = mock_calc
87+
mock_calc.get_eligibility_status.return_value = "eligible_result"
88+
89+
# When
90+
result = service.get_eligibility_status(nhs_number, "Y", ["FLU"], "G1", "consumer_xyz")
91+
92+
# Then
93+
# Verify the factory was called ONLY with camp_b
94+
mock_repos["factory"].get.assert_called_once_with(person_data, [camp_b])
95+
assert result == "eligible_result"
96+
97+
def test_raises_no_permitted_campaigns_error(service, mock_repos):
98+
"""Tests the scenario where the consumer mapping exists but returns nothing."""
99+
mock_repos["person"].get_eligibility_data.return_value = {"data": "exists"}
100+
mock_repos["campaign"].get_campaign_configs.return_value = [MagicMock()]
101+
102+
# Consumer has no permitted IDs mapped
103+
mock_repos["consumer"].get_permitted_campaign_ids.return_value = []
104+
105+
with pytest.raises(NoPermittedCampaignsError):
106+
service.get_eligibility_status(NHSNumber("1"), "Y", [], "", "bad_consumer")
107+
108+
def test_raises_unknown_person_error_on_repo_not_found(service, mock_repos):
109+
"""Tests that NotFoundError from repo is translated to UnknownPersonError."""
110+
mock_repos["person"].get_eligibility_data.side_effect = NotFoundError
111+
112+
with pytest.raises(UnknownPersonError):
113+
service.get_eligibility_status(NHSNumber("999"), "Y", [], "", "any")

tests/unit/views/test_eligibility.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
UrlLink,
2929
)
3030
from eligibility_signposting_api.services import EligibilityService, UnknownPersonError
31+
from eligibility_signposting_api.services.eligibility_services import NoPermittedCampaignsError
3132
from eligibility_signposting_api.views.eligibility import (
3233
_get_or_default_query_params,
3334
build_actions,
@@ -93,6 +94,20 @@ def get_eligibility_status(
9394
) -> EligibilityStatus:
9495
raise ValueError
9596

97+
class FakeNoPermittedCampaignsService(EligibilityService):
98+
def __init__(self):
99+
pass
100+
101+
def get_eligibility_status(
102+
self,
103+
_nhs_number: NHSNumber,
104+
_include_actions: str,
105+
_conditions: list[str],
106+
_category: str,
107+
_consumer_id: str,
108+
) -> EligibilityStatus:
109+
# Simulate the new error scenario
110+
raise NoPermittedCampaignsError
96111

97112
def test_security_headers_present_on_successful_response(app: Flask, client: FlaskClient):
98113
"""Test that security headers are present on successful eligibility check response."""
@@ -583,3 +598,74 @@ def test_status_endpoint(app: Flask, client: FlaskClient):
583598
)
584599
),
585600
)
601+
602+
603+
def test_no_permitted_campaigns_for_consumer_error(app: Flask, client: FlaskClient):
604+
"""
605+
Tests that NoPermittedCampaignsError is caught and returns
606+
the correct FHIR OperationOutcome with FORBIDDEN status.
607+
"""
608+
# Given
609+
with (
610+
get_app_container(app).override.service(EligibilityService, new=FakeNoPermittedCampaignsService()),
611+
get_app_container(app).override.service(AuditService, new=FakeAuditService()),
612+
):
613+
headers = {
614+
"nhs-login-nhs-number": "9876543210",
615+
"Consumer-Id": "unrecognized_consumer"
616+
}
617+
618+
# When
619+
response = client.get("/patient-check/9876543210", headers=headers)
620+
621+
# Then
622+
assert_that(
623+
response,
624+
is_response()
625+
.with_status_code(HTTPStatus.FORBIDDEN)
626+
.with_headers(has_entries({"Content-Type": "application/fhir+json"}))
627+
.and_text(
628+
is_json_that(
629+
has_entries(
630+
resourceType="OperationOutcome",
631+
issue=contains_exactly(
632+
has_entries(
633+
severity="error",
634+
code="forbidden",
635+
diagnostics="Consumer ID 'unrecognized_consumer' was not recognised by the Eligibility Signposting API"
636+
)
637+
)
638+
)
639+
)
640+
)
641+
)
642+
643+
644+
def test_consumer_id_is_passed_to_service(app: Flask, client: FlaskClient):
645+
"""
646+
Verifies that the consumer ID from the header is actually passed
647+
to the eligibility service call.
648+
"""
649+
# Given
650+
mock_service = MagicMock(spec=EligibilityService)
651+
mock_service.get_eligibility_status.return_value = EligibilityStatusFactory.build()
652+
653+
with (
654+
get_app_container(app).override.service(EligibilityService, new=mock_service),
655+
get_app_container(app).override.service(AuditService, new=FakeAuditService()),
656+
):
657+
headers = {
658+
"nhs-login-nhs-number": "1234567890",
659+
"Consumer-Id": "specific_consumer_123"
660+
}
661+
662+
# When
663+
client.get("/patient-check/1234567890", headers=headers)
664+
665+
# Then
666+
# Verify the 5th positional argument or the keyword argument 'consumer_id'
667+
mock_service.get_eligibility_status.assert_called_once()
668+
args, kwargs = mock_service.get_eligibility_status.call_args
669+
670+
# Check that 'specific_consumer_123' was the consumer_id passed
671+
assert args[4] == "specific_consumer_123"

0 commit comments

Comments
 (0)