Skip to content

Commit 05053e1

Browse files
committed
Added unit tests and fixed TODOs
1 parent d4a17e9 commit 05053e1

6 files changed

Lines changed: 121 additions & 76 deletions

File tree

src/eligibility_signposting_api/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import wireup.integration.flask
55
from asgiref.wsgi import WsgiToAsgi
6-
from flask import Flask, request
6+
from flask import Flask
77
from mangum import Mangum
88
from mangum.types import LambdaContext, LambdaEvent
99

src/eligibility_signposting_api/audit_context.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
AuditCondition,
1111
AuditEligibilityCohortGroups,
1212
AuditEligibilityCohorts,
13+
AuditEvent,
1314
AuditFilterRule,
1415
AuditRedirectRule,
1516
AuditSuitabilityRule,
1617
RequestAuditData,
1718
RequestAuditHeader,
18-
RequestAuditQueryParams, AuditEvent,
19+
RequestAuditQueryParams,
1920
)
2021
from eligibility_signposting_api.model.eligibility import (
2122
CohortGroupResult,
@@ -57,7 +58,6 @@ def add_request_details(request: Request) -> None:
5758
),
5859
)
5960

60-
6161
@staticmethod
6262
def append_audit_condition(
6363
suggested_actions: SuggestedActions | None,
@@ -86,8 +86,7 @@ def append_audit_condition(
8686
if value.audit_rules:
8787
if best_candidate.status.name == Status.not_eligible.name:
8888
audit_filter_rule = AuditFilterRule(
89-
rule_priority=int(value.audit_rules[0].rule_priority),
90-
rule_name=value.audit_rules[0].rule_name
89+
rule_priority=int(value.audit_rules[0].rule_priority), rule_name=value.audit_rules[0].rule_name
9190
)
9291
if best_candidate.status.name == Status.not_actionable.name:
9392
audit_suitability_rule = AuditSuitabilityRule(
@@ -105,11 +104,13 @@ def append_audit_condition(
105104
elif len(suggested_actions.actions) > 0:
106105
for action in suggested_actions.actions:
107106
audit_actions.append(
108-
AuditAction(action_code=action.action_code,
109-
action_type=action.action_type,
110-
action_description=action.action_description,
111-
action_url=action.url_link,
112-
action_url_label=action.url_label)
107+
AuditAction(
108+
action_code=action.action_code,
109+
action_type=action.action_type,
110+
action_description=action.action_description,
111+
action_url=action.url_link,
112+
action_url_label=action.url_label,
113+
)
113114
)
114115

115116
audit_condition = AuditCondition(

src/eligibility_signposting_api/audit_models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import dataclass, field
2-
from datetime import datetime
2+
from datetime import datetime, UTC
33
from uuid import UUID
44

55

@@ -20,7 +20,7 @@ class RequestAuditQueryParams:
2020

2121
@dataclass
2222
class RequestAuditData:
23-
request_timestamp: datetime = field(default_factory=datetime.utcnow) # TODO: fix use of deprecated datetime.now()
23+
request_timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
2424
headers: RequestAuditHeader = field(default_factory=RequestAuditHeader)
2525
query_params: RequestAuditQueryParams = field(default_factory=RequestAuditQueryParams)
2626
nhs_number: str | None = None

tests/integration/conftest.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ def persisted_77yo_person(person_table: Any, faker: Faker) -> Generator[eligibil
349349
for row in rows:
350350
person_table.delete_item(Key={"NHS_NUMBER": row["NHS_NUMBER"], "ATTRIBUTE_TYPE": row["ATTRIBUTE_TYPE"]})
351351

352+
352353
@pytest.fixture
353354
def persisted_person_all_cohorts(person_table: Any, faker: Faker) -> Generator[eligibility.NHSNumber]:
354355
nhs_number = eligibility.NHSNumber(faker.nhs_number())
@@ -360,7 +361,7 @@ def persisted_person_all_cohorts(person_table: Any, faker: Faker) -> Generator[e
360361
date_of_birth=date_of_birth,
361362
postcode="hp1",
362363
cohorts=["cohort1", "cohort2", "cohort3"],
363-
icb="QE1"
364+
icb="QE1",
364365
)
365366
):
366367
person_table.put_item(Item=row)
@@ -462,6 +463,7 @@ def campaign_config(s3_client: BaseClient, rules_bucket: BucketName) -> Generato
462463
yield campaign
463464
s3_client.delete_object(Bucket=rules_bucket, Key=f"{campaign.name}.json")
464465

466+
465467
@pytest.fixture(scope="class")
466468
def multiple_campaign_configs(s3_client: BaseClient, rules_bucket: BucketName) -> Generator[list[rules.CampaignConfig]]:
467469
"""Create and upload multiple campaign configs to S3, then clean up after tests."""
@@ -483,10 +485,10 @@ def multiple_campaign_configs(s3_client: BaseClient, rules_bucket: BucketName) -
483485
iteration_rules=target_rules_map.get(targets[i]),
484486
iteration_cohorts=[
485487
rule.IterationCohortFactory.build(
486-
cohort_label=f"cohort1",
487-
cohort_group=f"cohort_group{i+1}",
488-
positive_description=f"positive_desc_{i+1}",
489-
negative_description=f"negative_desc_{i+1}",
488+
cohort_label="cohort1",
489+
cohort_group=f"cohort_group{i + 1}",
490+
positive_description=f"positive_desc_{i + 1}",
491+
negative_description=f"negative_desc_{i + 1}",
490492
)
491493
],
492494
)

tests/integration/lambda/test_app_running_as_lambda.py

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@
1313
from hamcrest import (
1414
assert_that,
1515
contains_exactly,
16+
contains_inanyorder,
1617
contains_string,
1718
equal_to,
1819
has_entries,
1920
has_item,
2021
has_key,
2122
is_not,
22-
contains_inanyorder
2323
)
2424
from yarl import URL
2525

@@ -162,6 +162,7 @@ def get_log_messages(flask_function: str, logs_client: BaseClient) -> list[str]:
162162
)
163163
return [e["message"] for e in log_events["events"]]
164164

165+
165166
def test_given_nhs_number_in_path_matches_with_nhs_number_in_headers_and_return_response( # noqa: PLR0913
166167
lambda_client: BaseClient, # noqa:ARG001
167168
persisted_person: NHSNumber,
@@ -264,6 +265,7 @@ def test_given_nhs_number_in_path_does_not_match_with_nhs_number_in_headers_resu
264265
is_response().with_status_code(HTTPStatus.FORBIDDEN).and_body("NHS number mismatch"),
265266
)
266267

268+
267269
def test_given_person_has_unique_status_for_different_conditions_with_audit( # noqa: PLR0913
268270
lambda_client: BaseClient, # noqa:ARG001
269271
persisted_person_all_cohorts: NHSNumber,
@@ -328,10 +330,7 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( #
328330
"cohort_status": "not_eligible",
329331
}
330332
],
331-
"filter_rules": {
332-
"rule_priority": 10,
333-
"rule_name": "Exclude too young less than 75"
334-
},
333+
"filter_rules": {"rule_priority": 10, "rule_name": "Exclude too young less than 75"},
335334
"suitability_rules": None,
336335
"action_rule": None,
337336
"actions": [],
@@ -356,7 +355,7 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( #
356355
"suitability_rules": {
357356
"rule_priority": 10,
358357
"rule_name": "Exclude too young less than 75",
359-
"rule_message": "Exclude too young less than 75"
358+
"rule_message": "Exclude too young less than 75",
360359
},
361360
"action_rule": None,
362361
"actions": [],
@@ -379,19 +378,18 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( #
379378
],
380379
"filter_rules": None,
381380
"suitability_rules": None,
382-
"action_rule": {
383-
"rule_priority": 20,
384-
"rule_name": "In QE1"
385-
},
386-
"actions": [{
387-
"internal_name": None, # TODO: FIX!
388-
"action_type": "defaultcomms",
389-
"action_code": "action_code",
390-
"action_description": None,
391-
"action_url": None,
392-
"action_url_label": None
393-
}],
394-
}
381+
"action_rule": {"rule_priority": 20, "rule_name": "In QE1"},
382+
"actions": [
383+
{
384+
"internal_name": None, # TODO: FIX!
385+
"action_type": "defaultcomms",
386+
"action_code": "action_code",
387+
"action_description": None,
388+
"action_url": None,
389+
"action_url_label": None,
390+
}
391+
],
392+
},
395393
]
396394

397395
assert_that(audit_data["request"]["request_timestamp"], is_not(equal_to("")))
@@ -401,5 +399,3 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( #
401399
assert_that(audit_data["response"]["response_id"], is_not(equal_to("")))
402400
assert_that(audit_data["response"]["last_updated"], is_not(equal_to("")))
403401
assert_that(audit_data["response"]["condition"], contains_inanyorder(*expected_conditions))
404-
405-

tests/unit/test_audit_context.py

Lines changed: 84 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,34 @@
11
import uuid
2-
import pytest
3-
from datetime import datetime
2+
from dataclasses import asdict
3+
from datetime import UTC, datetime
4+
from unittest.mock import Mock
45

5-
from flask import g, request, Flask
6+
import pytest
7+
from flask import Flask, g, request
68

79
from eligibility_signposting_api.audit_context import AuditContext
810
from eligibility_signposting_api.audit_models import AuditEvent
9-
from eligibility_signposting_api.model.eligibility import SuggestedActions, ConditionName, IterationResult, \
10-
CohortGroupResult, SuggestedAction, ActionCode, ActionType, ActionDescription, UrlLink, UrlLabel, Reason, RuleName, \
11-
RuleDescription, RulePriority, Status
12-
from eligibility_signposting_api.model.rules import Iteration, CampaignID, CampaignVersion, \
13-
RuleType
14-
from fixtures.builders.model.rule import IterationFactory
15-
from fixtures.builders.views.response_model.eligibility import EligibilityResponseFactory
11+
from eligibility_signposting_api.model.eligibility import (
12+
ActionCode,
13+
ActionDescription,
14+
ActionType,
15+
CohortGroupResult,
16+
ConditionName,
17+
IterationResult,
18+
Reason,
19+
RuleDescription,
20+
RuleName,
21+
RulePriority,
22+
Status,
23+
SuggestedAction,
24+
SuggestedActions,
25+
UrlLabel,
26+
UrlLink,
27+
)
28+
from eligibility_signposting_api.model.rules import CampaignID, CampaignVersion, Iteration, RuleType
29+
from eligibility_signposting_api.services.audit_service import AuditService
30+
from tests.fixtures.builders.model.rule import IterationFactory
31+
from tests.fixtures.builders.views.response_model.eligibility import EligibilityResponseFactory
1632

1733

1834
@pytest.fixture
@@ -33,7 +49,7 @@ def test_add_request_details_sets_audit_log_on_g(app):
3349
url = "/patient-check?includeActions=Y"
3450

3551
with app.test_request_context(url, headers=headers, method="GET"):
36-
request.view_args = {'nhs_number': nhs_number}
52+
request.view_args = {"nhs_number": nhs_number}
3753
AuditContext.add_request_details(request)
3854

3955
assert hasattr(g, "audit_log")
@@ -52,7 +68,7 @@ def test_add_request_details_when_headers_are_empty_sets_audit_log_on_g(app):
5268
url = "/patient-check?includeActions=Y"
5369

5470
with app.test_request_context(url, method="GET"):
55-
request.view_args = {'nhs_number': nhs_number}
71+
request.view_args = {"nhs_number": nhs_number}
5672
AuditContext.add_request_details(request)
5773

5874
assert hasattr(g, "audit_log")
@@ -66,44 +82,56 @@ def test_add_request_details_when_headers_are_empty_sets_audit_log_on_g(app):
6682
assert isinstance(audit_req.request_timestamp, datetime)
6783

6884

69-
def test_append_audit_condition_adds_condition_to_audit_log(app):
85+
def test_append_audit_condition_adds_condition_to_audit_log_on_g(app):
7086
suggested_actions: SuggestedActions | None
7187
condition_name: ConditionName
7288
best_results: tuple[Iteration, IterationResult, dict[str, CohortGroupResult]]
7389
campaign_details: tuple[CampaignID | None, CampaignVersion | None]
7490
redirect_rule_details: tuple[RulePriority | None, RuleName | None]
7591

76-
suggested_actions = SuggestedActions(actions=[SuggestedAction(
77-
action_code=ActionCode("ActionCode1"),
78-
action_type=ActionType("ActionType1"),
79-
action_description=ActionDescription("ActionDescription1"),
80-
url_link=UrlLink("https://www.example.com"),
81-
url_label=UrlLabel("ActionLabel1")
82-
)])
92+
suggested_actions = SuggestedActions(
93+
actions=[
94+
SuggestedAction(
95+
action_code=ActionCode("ActionCode1"),
96+
action_type=ActionType("ActionType1"),
97+
action_description=ActionDescription("ActionDescription1"),
98+
url_link=UrlLink("https://www.example.com"),
99+
url_label=UrlLabel("ActionLabel1"),
100+
)
101+
]
102+
)
83103

84104
condition_name = ConditionName("Condition1")
85105
iteration = IterationFactory.build()
86-
audit_rules = [Reason(
87-
rule_type=RuleType.filter,
88-
rule_name=RuleName("FilterRuleName1"),
89-
rule_description=RuleDescription("FilterRuleDescription1"),
90-
matcher_matched=True,
91-
rule_priority=RulePriority("1")
92-
)]
93-
cohort_group_result = CohortGroupResult(status=Status.actionable, cohort_code="CohortCode1",
94-
description="CohortDescription1", audit_rules=audit_rules,
95-
reasons=audit_rules)
96-
iteration_result = IterationResult(status=Status.actionable, cohort_results=[cohort_group_result],
97-
actions=suggested_actions)
106+
audit_rules = [
107+
Reason(
108+
rule_type=RuleType.filter,
109+
rule_name=RuleName("FilterRuleName1"),
110+
rule_description=RuleDescription("FilterRuleDescription1"),
111+
matcher_matched=True,
112+
rule_priority=RulePriority("1"),
113+
)
114+
]
115+
cohort_group_result = CohortGroupResult(
116+
status=Status.actionable,
117+
cohort_code="CohortCode1",
118+
description="CohortDescription1",
119+
audit_rules=audit_rules,
120+
reasons=audit_rules,
121+
)
122+
iteration_result = IterationResult(
123+
status=Status.actionable, cohort_results=[cohort_group_result], actions=suggested_actions
124+
)
98125
best_results = (iteration, iteration_result, {"CohortCode1": cohort_group_result})
99126
campaign_details = (CampaignID("CampaignID1"), CampaignVersion("CampaignVersion1"))
100127
redirect_rule_details = (RulePriority("1"), RuleName("RedirectRuleName1"))
101128

102129
with app.app_context():
103130
g.audit_log = AuditEvent()
104131

105-
AuditContext.append_audit_condition(suggested_actions, condition_name, best_results, campaign_details,
106-
redirect_rule_details)
132+
AuditContext.append_audit_condition(
133+
suggested_actions, condition_name, best_results, campaign_details, redirect_rule_details
134+
)
107135

108136
assert g.audit_log.response.condition, condition_name
109137
cond = g.audit_log.response.condition[0]
@@ -112,10 +140,11 @@ def test_append_audit_condition_adds_condition_to_audit_log(app):
112140
assert cond.status == best_results[1].status.name
113141
assert cond.status_text == best_results[1].status.name
114142

115-
def test_add_response_details_adds_to_audit_log(app):
116-
eligibility_response = EligibilityResponseFactory.build(response_id=uuid.uuid4(),
117-
meta={"last_updated": datetime(2023, 1, 1, 0, 0)},
118-
processed_suggestions=[])
143+
144+
def test_add_response_details_adds_to_audit_log_on_G(app):
145+
eligibility_response = EligibilityResponseFactory.build(
146+
response_id=uuid.uuid4(), meta={"last_updated": datetime(2023, 1, 1, 0, 0)}, processed_suggestions=[]
147+
)
119148

120149
with app.app_context():
121150
g.audit_log = AuditEvent()
@@ -126,3 +155,20 @@ def test_add_response_details_adds_to_audit_log(app):
126155
assert g.audit_log.response.last_updated is eligibility_response.meta.last_updated
127156

128157

158+
def test_write_to_firehose_calls_audit_service_with_correct_data_from_g(app):
159+
mock_audit_service = Mock(spec=AuditService)
160+
eligibility_response = EligibilityResponseFactory.build(
161+
response_id=(uuid.uuid4()),
162+
meta={"last_updated": (datetime(2023, 1, 1, 0, 0, tzinfo=UTC))},
163+
processed_suggestions=[],
164+
)
165+
166+
with app.app_context():
167+
g.audit_log = AuditEvent()
168+
169+
AuditContext.write_to_firehose(mock_audit_service, eligibility_response)
170+
171+
assert g.audit_log.response.response_id == eligibility_response.response_id
172+
assert g.audit_log.response.last_updated == eligibility_response.meta.last_updated
173+
174+
mock_audit_service.audit.assert_called_once_with(asdict(g.audit_log))

0 commit comments

Comments
 (0)