diff --git a/tests/conftest.py b/tests/conftest.py index 13c0928e..aa348cad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import pytest +from faker import Faker from flask import Flask from flask.testing import FlaskClient @@ -13,3 +14,8 @@ def app() -> Flask: @pytest.fixture(scope="session") def client(app) -> FlaskClient: return app.test_client() + + +@pytest.fixture(scope="session") +def faker() -> Faker: + return Faker("en_UK") diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 250d8f62..ecf778ad 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -30,11 +30,6 @@ AWS_REGION = "eu-west-1" -@pytest.fixture(scope="session") -def faker() -> Faker: - return Faker("en_UK") - - @pytest.fixture(scope="session") def localstack(request: pytest.FixtureRequest) -> URL: if url := os.getenv("RUNNING_LOCALSTACK_URL", None): diff --git a/tests/unit/services/calculators/test_eligibility_calculator.py b/tests/unit/services/calculators/test_eligibility_calculator.py index 89540c09..627de8c7 100644 --- a/tests/unit/services/calculators/test_eligibility_calculator.py +++ b/tests/unit/services/calculators/test_eligibility_calculator.py @@ -1,7 +1,12 @@ +import datetime + +import pytest from faker import Faker -from hamcrest import assert_that, has_item +from freezegun import freeze_time +from hamcrest import assert_that, contains_exactly, empty, has_item, has_items -from eligibility_signposting_api.model.eligibility import ConditionName, NHSNumber, Status +from eligibility_signposting_api.model import rules as rules_model +from eligibility_signposting_api.model.eligibility import ConditionName, DateOfBirth, NHSNumber, Postcode, Status from eligibility_signposting_api.services.calculators.eligibility_calculator import EligibilityCalculator from tests.fixtures.builders.model import rule as rule_builder from tests.fixtures.builders.repos.person import person_rows_builder @@ -38,3 +43,476 @@ def test_not_base_eligible(faker: Faker): has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.not_eligible)) ), ) + + +@freeze_time("2025-04-25") +def test_only_live_campaigns_considered(faker: Faker): + # Given + nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") + + person_rows = person_rows_builder(nhs_number, cohorts=["cohort1"]) + campaign_configs = [ + rule_builder.CampaignConfigFactory.build( + name="Live", + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort2")], + ) + ], + start_date=datetime.date(2025, 4, 20), + end_date=datetime.date(2025, 4, 30), + ), + rule_builder.CampaignConfigFactory.build( + name="No longer live", + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_cohorts=[ + rule_builder.IterationCohortFactory.build(cohort_label="cohort1"), + rule_builder.IterationCohortFactory.build(cohort_label="cohort2"), + ], + ) + ], + start_date=datetime.date(2025, 4, 1), + end_date=datetime.date(2025, 4, 24), + ), + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + actual = calculator.evaluate_eligibility() + + # Then + assert_that( + actual, + is_eligibility_status().with_conditions( + has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.not_eligible)) + ), + ) + + +def test_base_eligible_and_simple_rule_includes(faker: Faker): + # Given + nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") + date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=76, maximum_age=79)) + + person_rows = person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"]) + campaign_configs = [ + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build()], + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + ) + ], + ) + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + actual = calculator.evaluate_eligibility() + + # Then + assert_that( + actual, + is_eligibility_status().with_conditions( + has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.actionable)) + ), + ) + + +def test_base_eligible_but_simple_rule_excludes(faker: Faker): + # Given + nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") + date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=18, maximum_age=74)) + + person_rows = person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"]) + campaign_configs = [ + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build()], + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + ) + ], + ) + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + actual = calculator.evaluate_eligibility() + + # Then + assert_that( + actual, + is_eligibility_status().with_conditions( + has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.not_actionable)) + ), + ) + + +@freeze_time("2025-04-25") +def test_simple_rule_only_excludes_from_live_iteration(faker: Faker): + # Given + nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") + date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=66, maximum_age=74)) + + person_rows = person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"]) + campaign_configs = [ + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + name="old iteration - would not exclude 74 year old", + iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build(comparator="-65")], + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + iteration_date=datetime.date(2025, 4, 10), + ), + rule_builder.IterationFactory.build( + name="current - would exclude 74 year old", + iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build()], + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + iteration_date=datetime.date(2025, 4, 20), + ), + rule_builder.IterationFactory.build( + name="next iteration - would not exclude 74 year old", + iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build(comparator="-65")], + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + iteration_date=datetime.date(2025, 4, 30), + ), + ], + ) + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + actual = calculator.evaluate_eligibility() + + # Then + assert_that( + actual, + is_eligibility_status().with_conditions( + has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.not_actionable)) + ), + ) + + +@freeze_time("2025-04-25") +def test_campaign_with_no_active_iteration_not_considered(faker: Faker): + # Given + nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") + + person_rows = person_rows_builder(nhs_number) + campaign_configs = [ + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_date=rules_model.IterationDate(datetime.date(2025, 4, 26)), + ) + ], + ) + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + actual = calculator.evaluate_eligibility() + + # Then + assert_that(actual, is_eligibility_status().with_conditions(empty())) + + +@pytest.mark.parametrize( + ("rule_type", "expected_status"), + [ + (rules_model.RuleType.suppression, Status.not_actionable), + (rules_model.RuleType.filter, Status.not_eligible), + (rules_model.RuleType.redirect, Status.actionable), + ], +) +def test_rule_types_cause_correct_statuses(rule_type: rules_model.RuleType, expected_status: Status, faker: Faker): + # Given + nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") + date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=18, maximum_age=74)) + + person_rows = person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"]) + campaign_configs = [ + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build(type=rule_type)], + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + ) + ], + ) + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + actual = calculator.evaluate_eligibility() + + # Then + assert_that( + actual, + is_eligibility_status().with_conditions( + has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(expected_status)) + ), + ) + + +def test_multiple_rule_types_cause_correct_status(faker: Faker): + # Given + nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") + date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=18, maximum_age=74)) + + person_rows = person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"]) + campaign_configs = [ + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_rules=[ + rule_builder.PersonAgeSuppressionRuleFactory.build( + priority=rules_model.RulePriority(5), type=rules_model.RuleType.suppression + ), + rule_builder.PersonAgeSuppressionRuleFactory.build( + priority=rules_model.RulePriority(10), type=rules_model.RuleType.filter + ), + rule_builder.PersonAgeSuppressionRuleFactory.build( + priority=rules_model.RulePriority(15), type=rules_model.RuleType.suppression + ), + ], + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + ) + ], + ) + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + actual = calculator.evaluate_eligibility() + + # Then + assert_that( + actual, + is_eligibility_status().with_conditions( + has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.not_eligible)) + ), + ) + + +@pytest.mark.parametrize( + ("test_comment", "rule1", "rule2", "expected_status"), + [ + ( + "two rules, both exclude, same priority, should exclude", + rule_builder.PersonAgeSuppressionRuleFactory.build(priority=rules_model.RulePriority(5)), + rule_builder.PostcodeSuppressionRuleFactory.build(priority=rules_model.RulePriority(5)), + Status.not_actionable, + ), + ( + "two rules, rule 1 excludes, same priority, should allow", + rule_builder.PersonAgeSuppressionRuleFactory.build(priority=rules_model.RulePriority(5)), + rule_builder.PostcodeSuppressionRuleFactory.build( + priority=rules_model.RulePriority(5), comparator=rules_model.RuleComparator("NW1") + ), + Status.actionable, + ), + ( + "two rules, rule 2 excludes, same priority, should allow", + rule_builder.PersonAgeSuppressionRuleFactory.build( + priority=rules_model.RulePriority(5), comparator=rules_model.RuleComparator("-65") + ), + rule_builder.PostcodeSuppressionRuleFactory.build(priority=rules_model.RulePriority(5)), + Status.actionable, + ), + ( + "two rules, rule 1 excludes, different priority, should exclude", + rule_builder.PersonAgeSuppressionRuleFactory.build(priority=rules_model.RulePriority(5)), + rule_builder.PostcodeSuppressionRuleFactory.build( + priority=rules_model.RulePriority(10), comparator=rules_model.RuleComparator("NW1") + ), + Status.not_actionable, + ), + ( + "two rules, rule 2 excludes, different priority, should exclude", + rule_builder.PersonAgeSuppressionRuleFactory.build( + priority=rules_model.RulePriority(5), comparator=rules_model.RuleComparator("-65") + ), + rule_builder.PostcodeSuppressionRuleFactory.build(priority=rules_model.RulePriority(10)), + Status.not_actionable, + ), + ( + "two rules, both excludes, different priority, should exclude", + rule_builder.PersonAgeSuppressionRuleFactory.build(priority=rules_model.RulePriority(5)), + rule_builder.PostcodeSuppressionRuleFactory.build(priority=rules_model.RulePriority(10)), + Status.not_actionable, + ), + ], +) +def test_rules_with_same_priority_must_all_match_to_exclude( + test_comment: str, + rule1: rules_model.IterationRule, + rule2: rules_model.IterationRule, + expected_status: Status, + faker: Faker, +): + # Given + nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") + date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=66, maximum_age=74)) + + person_rows = person_rows_builder( + nhs_number, date_of_birth=date_of_birth, postcode=Postcode("SW19 2BH"), cohorts=["cohort1"] + ) + campaign_configs = [ + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_rules=[rule1, rule2], + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + ) + ], + ) + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + actual = calculator.evaluate_eligibility() + + # Then + assert_that( + actual, + is_eligibility_status().with_conditions( + has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(expected_status)) + ), + test_comment, + ) + + +def test_multiple_conditions(faker: Faker): + # Given + nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") + date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=76, maximum_age=78)) + + person_rows = person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"]) + campaign_configs = [ + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build()], + ) + ], + ), + rule_builder.CampaignConfigFactory.build( + target="COVID", + iterations=[ + rule_builder.IterationFactory.build( + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build()], + ) + ], + ), + ] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + actual = calculator.evaluate_eligibility() + + # Then + assert_that( + actual, + is_eligibility_status().with_conditions( + has_items( + is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.actionable), + is_condition().with_condition_name(ConditionName("COVID")).and_status(Status.actionable), + ) + ), + ) + + +@pytest.mark.parametrize( + ("test_comment", "campaign1", "campaign2"), + [ + ( + "1st campaign allows, 2nd excludes", + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build()], + ) + ], + ), + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build(comparator="-85")], + ) + ], + ), + ), + ( + "1st campaign excludes, 2nd allows", + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build(comparator="-85")], + ) + ], + ), + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build()], + ) + ], + ), + ), + ], +) +def test_multiple_campaigns_for_single_condition( + test_comment: str, campaign1: rules_model.CampaignConfig, campaign2: rules_model.CampaignConfig, faker: Faker +): + # Given + nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") + date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=76, maximum_age=78)) + + person_rows = person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"]) + campaign_configs = [campaign1, campaign2] + + calculator = EligibilityCalculator(person_rows, campaign_configs) + + # When + actual = calculator.evaluate_eligibility() + + # Then + assert_that( + actual, + is_eligibility_status().with_conditions( + contains_exactly(is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.actionable)) + ), + test_comment, + ) diff --git a/tests/unit/services/test_eligibility_services.py b/tests/unit/services/test_eligibility_services.py index 4137a839..c99f3b73 100644 --- a/tests/unit/services/test_eligibility_services.py +++ b/tests/unit/services/test_eligibility_services.py @@ -1,24 +1,13 @@ -import datetime from unittest.mock import MagicMock import pytest -from faker import Faker -from freezegun import freeze_time -from hamcrest import assert_that, contains_exactly, empty, has_item, has_items +from hamcrest import assert_that, empty -from eligibility_signposting_api.model import rules as rules_model -from eligibility_signposting_api.model.eligibility import ConditionName, DateOfBirth, NHSNumber, Postcode, Status +from eligibility_signposting_api.model.eligibility import NHSNumber from eligibility_signposting_api.repos import CampaignRepo, NotFoundError, PersonRepo from eligibility_signposting_api.services import EligibilityService, UnknownPersonError from eligibility_signposting_api.services.calculators.eligibility_calculator import EligibilityCalculatorFactory -from tests.fixtures.builders.model import rule as rule_builder -from tests.fixtures.builders.repos.person import person_rows_builder -from tests.fixtures.matchers.eligibility import is_condition, is_eligibility_status - - -@pytest.fixture(scope="session") -def faker() -> Faker: - return Faker("en_UK") +from tests.fixtures.matchers.eligibility import is_eligibility_status def test_eligibility_service_returns_from_repo(): @@ -45,567 +34,3 @@ def test_eligibility_service_for_nonexistent_nhs_number(): # When with pytest.raises(UnknownPersonError): service.get_eligibility_status(NHSNumber("1234567890")) - - -def test_not_base_eligible(faker: Faker): - # Given - nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") - - person_repo = MagicMock(spec=PersonRepo) - campaign_repo = MagicMock(spec=CampaignRepo) - - person_repo.get_eligibility_data = MagicMock(return_value=person_rows_builder(nhs_number, cohorts=["cohort1"])) - campaign_repo.get_campaign_configs = MagicMock( - return_value=[ - rule_builder.CampaignConfigFactory.build( - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort2")] - ) - ], - ) - ] - ) - - service = EligibilityService(person_repo, campaign_repo, EligibilityCalculatorFactory()) - - # When - actual = service.get_eligibility_status(NHSNumber(nhs_number)) - - # Then - assert_that( - actual, - is_eligibility_status().with_conditions( - has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.not_eligible)) - ), - ) - - -@freeze_time("2025-04-25") -def test_only_live_campaigns_considered(faker: Faker): - # Given - nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") - - person_repo = MagicMock(spec=PersonRepo) - campaign_repo = MagicMock(spec=CampaignRepo) - person_repo.get_eligibility_data = MagicMock(return_value=person_rows_builder(nhs_number, cohorts=["cohort1"])) - campaign_repo.get_campaign_configs = MagicMock( - return_value=[ - rule_builder.CampaignConfigFactory.build( - name="Live", - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort2")], - ) - ], - start_date=datetime.date(2025, 4, 20), - end_date=datetime.date(2025, 4, 30), - ), - rule_builder.CampaignConfigFactory.build( - name="No longer live", - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - iteration_cohorts=[ - rule_builder.IterationCohortFactory.build(cohort_label="cohort1"), - rule_builder.IterationCohortFactory.build(cohort_label="cohort2"), - ], - ) - ], - start_date=datetime.date(2025, 4, 1), - end_date=datetime.date(2025, 4, 24), - ), - ] - ) - - service = EligibilityService(person_repo, campaign_repo, EligibilityCalculatorFactory()) - - # When - actual = service.get_eligibility_status(NHSNumber(nhs_number)) - - # Then - assert_that( - actual, - is_eligibility_status().with_conditions( - has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.not_eligible)) - ), - ) - - -def test_base_eligible_and_simple_rule_includes(faker: Faker): - # Given - nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") - date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=76, maximum_age=79)) - - person_repo = MagicMock(spec=PersonRepo) - campaign_repo = MagicMock(spec=CampaignRepo) - person_repo.get_eligibility_data = MagicMock( - return_value=person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"]) - ) - campaign_repo.get_campaign_configs = MagicMock( - return_value=[ - rule_builder.CampaignConfigFactory.build( - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build()], - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - ) - ], - ) - ] - ) - - service = EligibilityService(person_repo, campaign_repo, EligibilityCalculatorFactory()) - - # When - actual = service.get_eligibility_status(NHSNumber(nhs_number)) - - # Then - assert_that( - actual, - is_eligibility_status().with_conditions( - has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.actionable)) - ), - ) - - -def test_base_eligible_but_simple_rule_excludes(faker: Faker): - # Given - nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") - date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=18, maximum_age=74)) - - person_repo = MagicMock(spec=PersonRepo) - campaign_repo = MagicMock(spec=CampaignRepo) - person_repo.get_eligibility_data = MagicMock( - return_value=person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"]) - ) - campaign_repo.get_campaign_configs = MagicMock( - return_value=[ - rule_builder.CampaignConfigFactory.build( - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build()], - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - ) - ], - ) - ] - ) - - service = EligibilityService(person_repo, campaign_repo, EligibilityCalculatorFactory()) - - # When - actual = service.get_eligibility_status(NHSNumber(nhs_number)) - - # Then - assert_that( - actual, - is_eligibility_status().with_conditions( - has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.not_actionable)) - ), - ) - - -@freeze_time("2025-04-25") -def test_simple_rule_only_excludes_from_live_iteration(faker: Faker): - # Given - nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") - date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=66, maximum_age=74)) - - person_repo = MagicMock(spec=PersonRepo) - campaign_repo = MagicMock(spec=CampaignRepo) - person_repo.get_eligibility_data = MagicMock( - return_value=person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"]) - ) - campaign_repo.get_campaign_configs = MagicMock( - return_value=[ - rule_builder.CampaignConfigFactory.build( - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - name="old iteration - would not exclude 74 year old", - iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build(comparator="-65")], - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - iteration_date=datetime.date(2025, 4, 10), - ), - rule_builder.IterationFactory.build( - name="current - would exclude 74 year old", - iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build()], - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - iteration_date=datetime.date(2025, 4, 20), - ), - rule_builder.IterationFactory.build( - name="next iteration - would not exclude 74 year old", - iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build(comparator="-65")], - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - iteration_date=datetime.date(2025, 4, 30), - ), - ], - ) - ] - ) - - service = EligibilityService(person_repo, campaign_repo, EligibilityCalculatorFactory()) - - # When - actual = service.get_eligibility_status(NHSNumber(nhs_number)) - - # Then - assert_that( - actual, - is_eligibility_status().with_conditions( - has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.not_actionable)) - ), - ) - - -@freeze_time("2025-04-25") -def test_campaign_with_no_active_iteration_not_considered(faker: Faker): - # Given - nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") - - person_repo = MagicMock(spec=PersonRepo) - campaign_repo = MagicMock(spec=CampaignRepo) - person_repo.get_eligibility_data = MagicMock(return_value=person_rows_builder(nhs_number)) - campaign_repo.get_campaign_configs = MagicMock( - return_value=[ - rule_builder.CampaignConfigFactory.build( - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - iteration_date=rules_model.IterationDate(datetime.date(2025, 4, 26)), - ) - ], - ) - ] - ) - - service = EligibilityService(person_repo, campaign_repo, EligibilityCalculatorFactory()) - - # When - actual = service.get_eligibility_status(NHSNumber(nhs_number)) - - # Then - assert_that(actual, is_eligibility_status().with_conditions(empty())) - - -@pytest.mark.parametrize( - ("rule_type", "expected_status"), - [ - (rules_model.RuleType.suppression, Status.not_actionable), - (rules_model.RuleType.filter, Status.not_eligible), - (rules_model.RuleType.redirect, Status.actionable), - ], -) -def test_rule_types_cause_correct_statuses(rule_type: rules_model.RuleType, expected_status: Status, faker: Faker): - # Given - nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") - date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=18, maximum_age=74)) - - person_repo = MagicMock(spec=PersonRepo) - campaign_repo = MagicMock(spec=CampaignRepo) - person_repo.get_eligibility_data = MagicMock( - return_value=person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"]) - ) - campaign_repo.get_campaign_configs = MagicMock( - return_value=[ - rule_builder.CampaignConfigFactory.build( - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build(type=rule_type)], - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - ) - ], - ) - ] - ) - - service = EligibilityService(person_repo, campaign_repo, EligibilityCalculatorFactory()) - - # When - actual = service.get_eligibility_status(NHSNumber(nhs_number)) - - # Then - assert_that( - actual, - is_eligibility_status().with_conditions( - has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(expected_status)) - ), - ) - - -def test_multiple_rule_types_cause_correct_status(faker: Faker): - # Given - nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") - date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=18, maximum_age=74)) - - person_repo = MagicMock(spec=PersonRepo) - campaign_repo = MagicMock(spec=CampaignRepo) - person_repo.get_eligibility_data = MagicMock( - return_value=person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"]) - ) - campaign_repo.get_campaign_configs = MagicMock( - return_value=[ - rule_builder.CampaignConfigFactory.build( - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - iteration_rules=[ - rule_builder.PersonAgeSuppressionRuleFactory.build( - priority=rules_model.RulePriority(5), type=rules_model.RuleType.suppression - ), - rule_builder.PersonAgeSuppressionRuleFactory.build( - priority=rules_model.RulePriority(10), type=rules_model.RuleType.filter - ), - rule_builder.PersonAgeSuppressionRuleFactory.build( - priority=rules_model.RulePriority(15), type=rules_model.RuleType.suppression - ), - ], - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - ) - ], - ) - ] - ) - - service = EligibilityService(person_repo, campaign_repo, EligibilityCalculatorFactory()) - - # When - actual = service.get_eligibility_status(NHSNumber(nhs_number)) - - # Then - assert_that( - actual, - is_eligibility_status().with_conditions( - has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.not_eligible)) - ), - ) - - -@pytest.mark.parametrize( - ("test_comment", "rule1", "rule2", "expected_status"), - [ - ( - "two rules, both exclude, same priority, should exclude", - rule_builder.PersonAgeSuppressionRuleFactory.build(priority=rules_model.RulePriority(5)), - rule_builder.PostcodeSuppressionRuleFactory.build(priority=rules_model.RulePriority(5)), - Status.not_actionable, - ), - ( - "two rules, rule 1 excludes, same priority, should allow", - rule_builder.PersonAgeSuppressionRuleFactory.build(priority=rules_model.RulePriority(5)), - rule_builder.PostcodeSuppressionRuleFactory.build( - priority=rules_model.RulePriority(5), comparator=rules_model.RuleComparator("NW1") - ), - Status.actionable, - ), - ( - "two rules, rule 2 excludes, same priority, should allow", - rule_builder.PersonAgeSuppressionRuleFactory.build( - priority=rules_model.RulePriority(5), comparator=rules_model.RuleComparator("-65") - ), - rule_builder.PostcodeSuppressionRuleFactory.build(priority=rules_model.RulePriority(5)), - Status.actionable, - ), - ( - "two rules, rule 1 excludes, different priority, should exclude", - rule_builder.PersonAgeSuppressionRuleFactory.build(priority=rules_model.RulePriority(5)), - rule_builder.PostcodeSuppressionRuleFactory.build( - priority=rules_model.RulePriority(10), comparator=rules_model.RuleComparator("NW1") - ), - Status.not_actionable, - ), - ( - "two rules, rule 2 excludes, different priority, should exclude", - rule_builder.PersonAgeSuppressionRuleFactory.build( - priority=rules_model.RulePriority(5), comparator=rules_model.RuleComparator("-65") - ), - rule_builder.PostcodeSuppressionRuleFactory.build(priority=rules_model.RulePriority(10)), - Status.not_actionable, - ), - ( - "two rules, both excludes, different priority, should exclude", - rule_builder.PersonAgeSuppressionRuleFactory.build(priority=rules_model.RulePriority(5)), - rule_builder.PostcodeSuppressionRuleFactory.build(priority=rules_model.RulePriority(10)), - Status.not_actionable, - ), - ], -) -def test_rules_with_same_priority_must_all_match_to_exclude( - test_comment: str, - rule1: rules_model.IterationRule, - rule2: rules_model.IterationRule, - expected_status: Status, - faker: Faker, -): - # Given - nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") - date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=66, maximum_age=74)) - - person_repo = MagicMock(spec=PersonRepo) - campaign_repo = MagicMock(spec=CampaignRepo) - person_repo.get_eligibility_data = MagicMock( - return_value=person_rows_builder( - nhs_number, date_of_birth=date_of_birth, postcode=Postcode("SW19 2BH"), cohorts=["cohort1"] - ) - ) - campaign_repo.get_campaign_configs = MagicMock( - return_value=[ - rule_builder.CampaignConfigFactory.build( - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - iteration_rules=[rule1, rule2], - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - ) - ], - ) - ] - ) - - service = EligibilityService(person_repo, campaign_repo, EligibilityCalculatorFactory()) - - # When - actual = service.get_eligibility_status(NHSNumber(nhs_number)) - - # Then - assert_that( - actual, - is_eligibility_status().with_conditions( - has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(expected_status)) - ), - test_comment, - ) - - -def test_multiple_conditions(faker: Faker): - # Given - nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") - date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=76, maximum_age=78)) - - person_repo = MagicMock(spec=PersonRepo) - campaign_repo = MagicMock(spec=CampaignRepo) - - person_repo.get_eligibility_data = MagicMock( - return_value=person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"]) - ) - campaign_repo.get_campaign_configs = MagicMock( - return_value=[ - rule_builder.CampaignConfigFactory.build( - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build()], - ) - ], - ), - rule_builder.CampaignConfigFactory.build( - target="COVID", - iterations=[ - rule_builder.IterationFactory.build( - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build()], - ) - ], - ), - ] - ) - - service = EligibilityService(person_repo, campaign_repo, EligibilityCalculatorFactory()) - - # When - actual = service.get_eligibility_status(NHSNumber(nhs_number)) - - # Then - assert_that( - actual, - is_eligibility_status().with_conditions( - has_items( - is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.actionable), - is_condition().with_condition_name(ConditionName("COVID")).and_status(Status.actionable), - ) - ), - ) - - -@pytest.mark.parametrize( - ("test_comment", "campaign1", "campaign2"), - [ - ( - "1st campaign allows, 2nd excludes", - rule_builder.CampaignConfigFactory.build( - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build()], - ) - ], - ), - rule_builder.CampaignConfigFactory.build( - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build(comparator="-85")], - ) - ], - ), - ), - ( - "1st campaign excludes, 2nd allows", - rule_builder.CampaignConfigFactory.build( - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build(comparator="-85")], - ) - ], - ), - rule_builder.CampaignConfigFactory.build( - target="RSV", - iterations=[ - rule_builder.IterationFactory.build( - iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], - iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build()], - ) - ], - ), - ), - ], -) -def test_multiple_campaigns_for_single_condition( - test_comment: str, campaign1: rules_model.CampaignConfig, campaign2: rules_model.CampaignConfig, faker: Faker -): - # Given - nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") - date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=76, maximum_age=78)) - - person_repo = MagicMock(spec=PersonRepo) - campaign_repo = MagicMock(spec=CampaignRepo) - - person_repo.get_eligibility_data = MagicMock( - return_value=person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"]) - ) - campaign_repo.get_campaign_configs = MagicMock(return_value=[campaign1, campaign2]) - - service = EligibilityService(person_repo, campaign_repo, EligibilityCalculatorFactory()) - - # When - actual = service.get_eligibility_status(NHSNumber(nhs_number)) - - # Then - assert_that( - actual, - is_eligibility_status().with_conditions( - contains_exactly(is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.actionable)) - ), - test_comment, - )