Skip to content

Commit 8b8ed0f

Browse files
committed
eli-579 adding an integration tests to make sure add_days plus add_days with date formatting work
1 parent 50668e8 commit 8b8ed0f

File tree

2 files changed

+345
-0
lines changed

2 files changed

+345
-0
lines changed

tests/integration/conftest.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,16 @@
2121
from eligibility_signposting_api.model.campaign_config import (
2222
AvailableAction,
2323
CampaignConfig,
24+
CommsRouting,
2425
EndDate,
26+
RuleAttributeLevel,
27+
RuleAttributeName,
2528
RuleCode,
29+
RuleDescription,
2630
RuleEntry,
2731
RuleName,
32+
RuleOperator,
33+
RulePriority,
2834
RuleText,
2935
RuleType,
3036
StartDate,
@@ -1119,6 +1125,160 @@ def get_secret_previous(self, secret_name: str) -> dict[str, str]: # noqa: ARG0
11191125
return {}
11201126

11211127

1128+
@pytest.fixture
1129+
def person_with_covid_vaccination(
1130+
person_table: Any, faker: Faker, hashing_service: HashingService
1131+
) -> Generator[eligibility_status.NHSNumber]:
1132+
"""
1133+
Creates a person with a COVID vaccination record.
1134+
LAST_SUCCESSFUL_DATE is set to 2026-01-28 (20260128).
1135+
Used for testing derived values like NEXT_DOSE_DUE with ADD_DAYS function.
1136+
"""
1137+
nhs_num = faker.nhs_number()
1138+
nhs_number = eligibility_status.NHSNumber(nhs_num)
1139+
nhs_num_hash = hashing_service.hash_with_current_secret(nhs_num)
1140+
1141+
date_of_birth = eligibility_status.DateOfBirth(datetime.date(1960, 5, 15))
1142+
1143+
for row in (
1144+
rows := person_rows_builder(
1145+
nhs_number=nhs_num_hash,
1146+
date_of_birth=date_of_birth,
1147+
postcode="SW1A",
1148+
cohorts=["covid_eligible"],
1149+
vaccines={"COVID": {"LAST_SUCCESSFUL_DATE": "20260128", "CONDITION_NAME": "COVID"}},
1150+
).data
1151+
):
1152+
person_table.put_item(Item=row)
1153+
1154+
yield nhs_number
1155+
1156+
for row in rows:
1157+
person_table.delete_item(Key={"NHS_NUMBER": row["NHS_NUMBER"], "ATTRIBUTE_TYPE": row["ATTRIBUTE_TYPE"]})
1158+
1159+
1160+
@pytest.fixture(scope="class")
1161+
def campaign_config_with_derived_values(s3_client: BaseClient, rules_bucket: BucketName) -> Generator[CampaignConfig]:
1162+
"""
1163+
Creates a campaign config that uses the ADD_DAYS derived value function.
1164+
Contains actions for:
1165+
- DateOfLastVaccination: Shows the raw LAST_SUCCESSFUL_DATE
1166+
- DateOfNextEarliestVaccination: Shows NEXT_DOSE_DUE derived by adding 91 days
1167+
"""
1168+
campaign: CampaignConfig = rule.CampaignConfigFactory.build(
1169+
target="COVID",
1170+
iterations=[
1171+
rule.IterationFactory.build(
1172+
actions_mapper=rule.ActionsMapperFactory.build(
1173+
root={
1174+
"VACCINATION_DATES": AvailableAction(
1175+
ActionType="DataValue",
1176+
ExternalRoutingCode="DateOfLastVaccination",
1177+
ActionDescription="[[TARGET.COVID.LAST_SUCCESSFUL_DATE]]",
1178+
),
1179+
"NEXT_DOSE_DATE": AvailableAction(
1180+
ActionType="DataValue",
1181+
ExternalRoutingCode="DateOfNextEarliestVaccination",
1182+
ActionDescription="[[TARGET.COVID.NEXT_DOSE_DUE:ADD_DAYS(91)]]",
1183+
),
1184+
}
1185+
),
1186+
iteration_cohorts=[
1187+
rule.IterationCohortFactory.build(
1188+
cohort_label="covid_eligible",
1189+
cohort_group="covid_vaccination",
1190+
positive_description="Eligible for COVID vaccination",
1191+
negative_description="Not eligible for COVID vaccination",
1192+
),
1193+
],
1194+
iteration_rules=[
1195+
rule.IterationRuleFactory.build(
1196+
type=RuleType.redirect,
1197+
name=RuleName("Provide vaccination dates"),
1198+
description=RuleDescription("Provide vaccination dates to patient"),
1199+
priority=RulePriority(10),
1200+
operator=RuleOperator.is_not_null,
1201+
attribute_level=RuleAttributeLevel.TARGET,
1202+
attribute_target="COVID",
1203+
attribute_name=RuleAttributeName("LAST_SUCCESSFUL_DATE"),
1204+
comms_routing=CommsRouting("VACCINATION_DATES|NEXT_DOSE_DATE"),
1205+
),
1206+
],
1207+
default_comms_routing="VACCINATION_DATES",
1208+
default_not_eligible_routing="VACCINATION_DATES",
1209+
default_not_actionable_routing="VACCINATION_DATES",
1210+
)
1211+
],
1212+
)
1213+
campaign_data = {"CampaignConfig": campaign.model_dump(by_alias=True)}
1214+
s3_client.put_object(
1215+
Bucket=rules_bucket, Key=f"{campaign.name}.json", Body=json.dumps(campaign_data), ContentType="application/json"
1216+
)
1217+
yield campaign
1218+
s3_client.delete_object(Bucket=rules_bucket, Key=f"{campaign.name}.json")
1219+
1220+
1221+
@pytest.fixture(scope="class")
1222+
def campaign_config_with_derived_values_formatted(
1223+
s3_client: BaseClient, rules_bucket: BucketName
1224+
) -> Generator[CampaignConfig]:
1225+
"""
1226+
Creates a campaign config that uses ADD_DAYS with DATE formatting.
1227+
The NEXT_DOSE_DUE is formatted as "29 April 2026" instead of raw "20260429".
1228+
"""
1229+
campaign: CampaignConfig = rule.CampaignConfigFactory.build(
1230+
target="COVID",
1231+
iterations=[
1232+
rule.IterationFactory.build(
1233+
actions_mapper=rule.ActionsMapperFactory.build(
1234+
root={
1235+
"VACCINATION_DATES": AvailableAction(
1236+
ActionType="DataValue",
1237+
ExternalRoutingCode="DateOfLastVaccination",
1238+
ActionDescription="[[TARGET.COVID.LAST_SUCCESSFUL_DATE:DATE(%d %B %Y)]]",
1239+
),
1240+
"NEXT_DOSE_DATE": AvailableAction(
1241+
ActionType="DataValue",
1242+
ExternalRoutingCode="DateOfNextEarliestVaccination",
1243+
ActionDescription="[[TARGET.COVID.NEXT_DOSE_DUE:ADD_DAYS(91):DATE(%d %B %Y)]]",
1244+
),
1245+
}
1246+
),
1247+
iteration_cohorts=[
1248+
rule.IterationCohortFactory.build(
1249+
cohort_label="covid_eligible",
1250+
cohort_group="covid_vaccination",
1251+
positive_description="Eligible for COVID vaccination",
1252+
negative_description="Not eligible for COVID vaccination",
1253+
),
1254+
],
1255+
iteration_rules=[
1256+
rule.IterationRuleFactory.build(
1257+
type=RuleType.redirect,
1258+
name=RuleName("Provide vaccination dates"),
1259+
description=RuleDescription("Provide vaccination dates to patient"),
1260+
priority=RulePriority(10),
1261+
operator=RuleOperator.is_not_null,
1262+
attribute_level=RuleAttributeLevel.TARGET,
1263+
attribute_target="COVID",
1264+
attribute_name=RuleAttributeName("LAST_SUCCESSFUL_DATE"),
1265+
comms_routing=CommsRouting("VACCINATION_DATES|NEXT_DOSE_DATE"),
1266+
),
1267+
],
1268+
default_comms_routing="VACCINATION_DATES",
1269+
default_not_eligible_routing="VACCINATION_DATES",
1270+
default_not_actionable_routing="VACCINATION_DATES",
1271+
)
1272+
],
1273+
)
1274+
campaign_data = {"CampaignConfig": campaign.model_dump(by_alias=True)}
1275+
s3_client.put_object(
1276+
Bucket=rules_bucket, Key=f"{campaign.name}.json", Body=json.dumps(campaign_data), ContentType="application/json"
1277+
)
1278+
yield campaign
1279+
s3_client.delete_object(Bucket=rules_bucket, Key=f"{campaign.name}.json")
1280+
1281+
11221282
@pytest.fixture
11231283
def hashing_service() -> HashingService:
11241284
secret_repo = StubSecretRepo(
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"""
2+
Integration tests for derived values functionality.
3+
4+
These tests verify the end-to-end flow of the ADD_DAYS derived value function,
5+
demonstrating how NEXT_DOSE_DUE is calculated from LAST_SUCCESSFUL_DATE.
6+
7+
Example API response format:
8+
{
9+
"processedSuggestions": [
10+
{
11+
"actions": [
12+
{
13+
"actionType": "DataValue",
14+
"actionCode": "DateOfLastVaccination",
15+
"description": "20260128"
16+
},
17+
{
18+
"actionType": "DataValue",
19+
"actionCode": "DateOfNextEarliestVaccination",
20+
"description": "20260429"
21+
}
22+
]
23+
}
24+
]
25+
}
26+
"""
27+
28+
from http import HTTPStatus
29+
30+
from botocore.client import BaseClient
31+
from brunns.matchers.data import json_matching as is_json_that
32+
from brunns.matchers.werkzeug import is_werkzeug_response as is_response
33+
from flask.testing import FlaskClient
34+
from hamcrest import (
35+
assert_that,
36+
has_key,
37+
)
38+
39+
from eligibility_signposting_api.model.campaign_config import CampaignConfig
40+
from eligibility_signposting_api.model.eligibility_status import NHSNumber
41+
42+
43+
class TestDerivedValues:
44+
"""Integration tests for the ADD_DAYS derived value functionality."""
45+
46+
def test_add_days_calculates_next_dose_due_from_last_successful_date(
47+
self,
48+
client: FlaskClient,
49+
person_with_covid_vaccination: NHSNumber,
50+
campaign_config_with_derived_values: CampaignConfig, # noqa: ARG002
51+
secretsmanager_client: BaseClient, # noqa: ARG002
52+
):
53+
"""
54+
Test that the ADD_DAYS function correctly calculates the next dose date.
55+
56+
Given:
57+
- A person with COVID vaccination on 2026-01-28 (20260128)
58+
- A campaign config with actions using:
59+
- [[TARGET.COVID.LAST_SUCCESSFUL_DATE]] for DateOfLastVaccination
60+
- [[TARGET.COVID.NEXT_DOSE_DUE:ADD_DAYS(91)]] for DateOfNextEarliestVaccination
61+
62+
Expected:
63+
- DateOfLastVaccination shows "20260128"
64+
- DateOfNextEarliestVaccination shows "20260429" (2026-01-28 + 91 days = 2026-04-29)
65+
66+
This demonstrates the use case from the requirement:
67+
"actions": [
68+
{"actionType": "DataValue", "actionCode": "DateOfLastVaccination", "description": "20260128"},
69+
{"actionType": "DataValue", "actionCode": "DateOfNextEarliestVaccination", "description": "20260429"}
70+
]
71+
"""
72+
# Given
73+
headers = {"nhs-login-nhs-number": str(person_with_covid_vaccination)}
74+
75+
# When
76+
response = client.get(
77+
f"/patient-check/{person_with_covid_vaccination}?includeActions=Y",
78+
headers=headers,
79+
)
80+
81+
# Then - verify response is successful
82+
assert_that(
83+
response,
84+
is_response().with_status_code(HTTPStatus.OK).and_text(is_json_that(has_key("processedSuggestions"))),
85+
)
86+
87+
# Extract the processed suggestions
88+
body = response.get_json()
89+
assert body is not None
90+
processed_suggestions = body.get("processedSuggestions", [])
91+
92+
# Find the COVID condition
93+
covid_suggestion = next(
94+
(s for s in processed_suggestions if s.get("condition") == "COVID"),
95+
None,
96+
)
97+
assert covid_suggestion is not None, "Expected COVID condition in response"
98+
99+
# Extract actions
100+
actions = covid_suggestion.get("actions", [])
101+
expected_actions_count = 2
102+
assert len(actions) >= expected_actions_count, (
103+
f"Expected at least {expected_actions_count} actions, got {len(actions)}"
104+
)
105+
106+
# Find the vaccination date actions by action code
107+
date_of_last = next(
108+
(a for a in actions if a.get("actionCode") == "DateOfLastVaccination"),
109+
None,
110+
)
111+
date_of_next = next(
112+
(a for a in actions if a.get("actionCode") == "DateOfNextEarliestVaccination"),
113+
None,
114+
)
115+
116+
# Verify DateOfLastVaccination shows the raw date
117+
assert date_of_last is not None, "Expected DateOfLastVaccination action"
118+
assert date_of_last["description"] == "20260128", (
119+
f"Expected DateOfLastVaccination to be '20260128', got '{date_of_last['description']}'"
120+
)
121+
122+
# Verify DateOfNextEarliestVaccination shows the calculated date (2026-01-28 + 91 days = 2026-04-29)
123+
assert date_of_next is not None, "Expected DateOfNextEarliestVaccination action"
124+
assert date_of_next["description"] == "20260429", (
125+
f"Expected DateOfNextEarliestVaccination to be '20260429' (20260128 + 91 days), "
126+
f"got '{date_of_next['description']}'"
127+
)
128+
129+
# Verify action types are DataValue as per requirement
130+
assert date_of_last["actionType"] == "DataValue"
131+
assert date_of_next["actionType"] == "DataValue"
132+
133+
def test_add_days_with_formatted_date_output(
134+
self,
135+
client: FlaskClient,
136+
person_with_covid_vaccination: NHSNumber,
137+
campaign_config_with_derived_values_formatted: CampaignConfig, # noqa: ARG002
138+
secretsmanager_client: BaseClient, # noqa: ARG002
139+
):
140+
"""
141+
Test that ADD_DAYS can be combined with DATE formatting.
142+
143+
Given:
144+
- A person with COVID vaccination on 2026-01-28
145+
- A campaign config using [[TARGET.COVID.NEXT_DOSE_DUE:ADD_DAYS(91):DATE(%d %B %Y)]]
146+
147+
Expected:
148+
- DateOfNextEarliestVaccination shows "29 April 2026" (formatted output)
149+
"""
150+
# Given
151+
headers = {"nhs-login-nhs-number": str(person_with_covid_vaccination)}
152+
153+
# When
154+
response = client.get(
155+
f"/patient-check/{person_with_covid_vaccination}?includeActions=Y",
156+
headers=headers,
157+
)
158+
159+
# Then
160+
assert_that(
161+
response,
162+
is_response().with_status_code(HTTPStatus.OK).and_text(is_json_that(has_key("processedSuggestions"))),
163+
)
164+
165+
body = response.get_json()
166+
assert body is not None
167+
processed_suggestions = body.get("processedSuggestions", [])
168+
169+
covid_suggestion = next(
170+
(s for s in processed_suggestions if s.get("condition") == "COVID"),
171+
None,
172+
)
173+
assert covid_suggestion is not None
174+
175+
actions = covid_suggestion.get("actions", [])
176+
date_of_next = next(
177+
(a for a in actions if a.get("actionCode") == "DateOfNextEarliestVaccination"),
178+
None,
179+
)
180+
181+
# Verify the formatted date output
182+
assert date_of_next is not None, "Expected DateOfNextEarliestVaccination action"
183+
assert date_of_next["description"] == "29 April 2026", (
184+
f"Expected formatted date '29 April 2026', got '{date_of_next['description']}'"
185+
)

0 commit comments

Comments
 (0)