Skip to content

Commit 1d71677

Browse files
campaign config validation
1 parent 9532d4d commit 1d71677

8 files changed

Lines changed: 427 additions & 16 deletions

File tree

src/rules_validation_api/app.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
def main() -> None:
99
print("Starting rules validation")
10-
with open('campaign_config.json', 'r') as file:
11-
json_data = json.load(file) # this validates json
10+
with open("campaign_config.json") as file:
11+
json_data = json.load(file) # this validates json
1212

1313
try:
1414
user = CampaignConfigValidation(**json_data["CampaignConfig"])
@@ -17,6 +17,5 @@ def main() -> None:
1717
print(e)
1818

1919

20-
2120
if __name__ == "__main__":
2221
main()
Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
from typing import List
1+
from pydantic import Field, field_validator
22

3-
from pydantic import field_validator, Field
4-
5-
from eligibility_signposting_api.model.campaign_config import CampaignConfig, Iteration
3+
from eligibility_signposting_api.model.campaign_config import CampaignConfig
64
from rules_validation_api.validators.iteration_validator import IterationValidation
75

86

97
class CampaignConfigValidation(CampaignConfig):
10-
iterations: List[IterationValidation] = Field(..., min_length=1, alias="Iterations")
8+
iterations: list[IterationValidation] = Field(..., min_length=1, alias="Iterations")
119

1210
@field_validator("id")
1311
def validate_name(cls, value: str) -> str:
@@ -21,4 +19,3 @@ def validate_type(cls, value: str) -> str:
2119
if value not in allowed_values:
2220
raise ValueError(f"type must be one of {allowed_values}")
2321
return value
24-

src/rules_validation_api/validators/iteration_rules_validator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from pydantic import field_validator
22

3-
from eligibility_signposting_api.model.campaign_config import ActionsMapper, IterationRule
3+
from eligibility_signposting_api.model.campaign_config import IterationRule
44

55

66
class IterationRuleValidation(IterationRule):

src/rules_validation_api/validators/iteration_validator.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
from typing import List
2-
3-
from pydantic import field_validator, BaseModel, Field
1+
from pydantic import Field, field_validator
42

53
from eligibility_signposting_api.model.campaign_config import Iteration
64
from rules_validation_api.validators.iteration_rules_validator import IterationRuleValidation
75

86

97
class IterationValidation(Iteration):
10-
iteration_rules: List[IterationRuleValidation] = Field(..., min_length=1, alias="IterationRules")
8+
iteration_rules: list[IterationRuleValidation] = Field(..., min_length=1, alias="IterationRules")
119

1210
@field_validator("id")
1311
@classmethod
@@ -22,5 +20,3 @@ def validate_name(cls, value: str) -> str:
2220
if not value.strip():
2321
raise ValueError("ID must not be empty")
2422
return value
25-
26-

tests/unit/validation/__init__.py

Whitespace-only changes.

tests/unit/validation/conftest.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import pytest
2+
3+
4+
@pytest.fixture
5+
def valid_campaign_config_with_only_mandate_fields():
6+
return {
7+
"ID": "CAMP001",
8+
"Version": "v1.0",
9+
"Name": "Spring Campaign",
10+
"Type": "V",
11+
"Target": "COVID",
12+
"IterationFrequency": "M",
13+
"IterationType": "A",
14+
"StartDate": "20250101",
15+
"EndDate": "20250331",
16+
"Iterations": [
17+
{
18+
"ID": "ITER001",
19+
"Version": "v1.0",
20+
"Name": "Mid-January Push",
21+
"IterationDate": "20250101",
22+
"IterationNumber": 1,
23+
"ApprovalMinimum": 10,
24+
"ApprovalMaximum": 100,
25+
"Type": "A",
26+
"DefaultCommsRouting": "RouteA",
27+
"DefaultNotEligibleRouting": "RouteB",
28+
"DefaultNotActionableRouting": "RouteC",
29+
"IterationCohorts": [],
30+
"IterationRules": [
31+
{
32+
"Type": "F",
33+
"Name": "Assure only already vaccinated taken from magic cohort",
34+
"Description": "Exclude anyone who has NOT been given a dose of RSV "
35+
"Vaccination from the magic cohort",
36+
"Operator": "is_empty",
37+
"Comparator": "",
38+
"AttributeTarget": "RSV",
39+
"AttributeLevel": "TARGET",
40+
"AttributeName": "LAST_SUCCESSFUL_DATE",
41+
"CohortLabel": "elid_all_people",
42+
"Priority": 100,
43+
}
44+
],
45+
"ActionsMapper": {},
46+
}
47+
],
48+
}
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import pytest
2+
from pydantic import ValidationError
3+
4+
from rules_validation_api.validators.campaign_config_validator import CampaignConfigValidation
5+
6+
7+
class TestMandateFieldsSchemaValidations:
8+
def test_campaign_config_with_only_mandate_fields_configuration(self,
9+
valid_campaign_config_with_only_mandate_fields):
10+
try:
11+
CampaignConfigValidation(**valid_campaign_config_with_only_mandate_fields)
12+
except ValidationError as e:
13+
pytest.fail(f"Unexpected error during model instantiation: {e}")
14+
15+
@pytest.mark.parametrize(
16+
"mandate_field",
17+
[
18+
"ID",
19+
"Version",
20+
"Name",
21+
"Type",
22+
"Target",
23+
"IterationFrequency",
24+
"IterationType",
25+
"StartDate",
26+
"EndDate",
27+
"Iterations",
28+
],
29+
)
30+
def test_missing_mandate_fields(self, mandate_field, valid_campaign_config_with_only_mandate_fields):
31+
data = valid_campaign_config_with_only_mandate_fields.copy()
32+
data.pop(mandate_field, None) # Simulate missing field
33+
with pytest.raises(ValidationError):
34+
CampaignConfigValidation(**data)
35+
assert mandate_field.lower()
36+
37+
# ID field
38+
39+
# ID
40+
@pytest.mark.parametrize("id_value", ["CAMP001", "12345", "X001"])
41+
def test_valid_id(self, id_value, valid_campaign_config_with_only_mandate_fields):
42+
data = {**valid_campaign_config_with_only_mandate_fields, "ID": id_value}
43+
model = CampaignConfigValidation(**data)
44+
assert model.id == id_value
45+
46+
# Version
47+
@pytest.mark.parametrize("version_value", ["v1.0", "v2.1", "V3.0"])
48+
def test_valid_version(self, version_value, valid_campaign_config_with_only_mandate_fields):
49+
data = {**valid_campaign_config_with_only_mandate_fields, "Version": version_value}
50+
model = CampaignConfigValidation(**data)
51+
assert model.version == version_value
52+
53+
# Name
54+
@pytest.mark.parametrize("name_value", ["Spring Campaign", "COVID-Alert", "Mass Outreach"])
55+
def test_valid_name(self, name_value, valid_campaign_config_with_only_mandate_fields):
56+
data = {**valid_campaign_config_with_only_mandate_fields, "Name": name_value}
57+
model = CampaignConfigValidation(**data)
58+
assert model.name == name_value
59+
60+
# Type
61+
@pytest.mark.parametrize("type_value", ["V", "S"])
62+
def test_valid_type(self, type_value, valid_campaign_config_with_only_mandate_fields):
63+
data = {**valid_campaign_config_with_only_mandate_fields, "Type": type_value}
64+
model = CampaignConfigValidation(**data)
65+
assert model.type == type_value
66+
67+
@pytest.mark.parametrize("type_value", ["X", "", None])
68+
def test_invalid_type(self, type_value, valid_campaign_config_with_only_mandate_fields):
69+
data = {**valid_campaign_config_with_only_mandate_fields, "Type": type_value}
70+
with pytest.raises(ValidationError):
71+
CampaignConfigValidation(**data)
72+
73+
# Target
74+
@pytest.mark.parametrize("target_value", ["COVID", "FLU", "MMR", "RSV"])
75+
def test_valid_target(self, target_value, valid_campaign_config_with_only_mandate_fields):
76+
data = {**valid_campaign_config_with_only_mandate_fields, "Target": target_value}
77+
model = CampaignConfigValidation(**data)
78+
assert model.target == target_value
79+
80+
@pytest.mark.parametrize("target_value", ["EBOLA", "HEP", "", None])
81+
def test_invalid_target(self, target_value, valid_campaign_config_with_only_mandate_fields):
82+
data = {**valid_campaign_config_with_only_mandate_fields, "Target": target_value}
83+
with pytest.raises(ValidationError):
84+
CampaignConfigValidation(**data)
85+
86+
# IterationFrequency
87+
@pytest.mark.parametrize("freq_value", ["X", "D", "W", "M", "Q", "A"])
88+
def test_valid_iteration_frequency(self, freq_value, valid_campaign_config_with_only_mandate_fields):
89+
data = {**valid_campaign_config_with_only_mandate_fields, "IterationFrequency": freq_value}
90+
model = CampaignConfigValidation(**data)
91+
assert model.iteration_frequency == freq_value
92+
93+
@pytest.mark.parametrize("freq_value", ["Z", "", None])
94+
def test_invalid_iteration_frequency(self, freq_value, valid_campaign_config_with_only_mandate_fields):
95+
data = {**valid_campaign_config_with_only_mandate_fields, "IterationFrequency": freq_value}
96+
with pytest.raises(ValidationError):
97+
CampaignConfigValidation(**data)
98+
99+
# IterationType
100+
@pytest.mark.parametrize("iter_type", ["A", "M", "S", "O"])
101+
def test_valid_iteration_type(self, iter_type, valid_campaign_config_with_only_mandate_fields):
102+
data = {**valid_campaign_config_with_only_mandate_fields, "IterationType": iter_type}
103+
model = CampaignConfigValidation(**data)
104+
assert model.iteration_type == iter_type
105+
106+
@pytest.mark.parametrize("iter_type", ["B", "", None])
107+
def test_invalid_iteration_type(self, iter_type, valid_campaign_config_with_only_mandate_fields):
108+
data = {**valid_campaign_config_with_only_mandate_fields, "IterationType": iter_type}
109+
with pytest.raises(ValidationError):
110+
CampaignConfigValidation(**data)
111+
112+
# StartDate
113+
@pytest.mark.parametrize(
114+
"start_date",
115+
[
116+
"", # empty string
117+
"invalid-date", # malformed value
118+
],
119+
)
120+
def test_invalid_start_date(self, start_date, valid_campaign_config_with_only_mandate_fields):
121+
data = valid_campaign_config_with_only_mandate_fields.copy()
122+
data["StartDate"] = start_date
123+
124+
with pytest.raises(ValidationError) as exc_info:
125+
CampaignConfigValidation(**data)
126+
127+
errors = exc_info.value.errors()
128+
for error in errors:
129+
assert error["loc"][0] == "StartDate"
130+
131+
# EndDates
132+
@pytest.mark.parametrize(
133+
"end_date",
134+
[
135+
"", # empty string
136+
"31032025", # malformed value
137+
],
138+
)
139+
def test_invalid_end_date(self, end_date, valid_campaign_config_with_only_mandate_fields):
140+
data = valid_campaign_config_with_only_mandate_fields.copy()
141+
data["EndDate"] = end_date
142+
143+
with pytest.raises(ValidationError) as exc_info:
144+
CampaignConfigValidation(**data)
145+
146+
errors = exc_info.value.errors()
147+
for error in errors:
148+
assert error["loc"][0] == "EndDate"
149+
150+
151+
class TestOptionalFieldsSchemaValidations:
152+
@pytest.mark.parametrize("manager", ["alice", "bob", "carol"])
153+
def test_manager_field(self, manager, valid_campaign_config_with_only_mandate_fields):
154+
data = {**valid_campaign_config_with_only_mandate_fields, "Manager": manager}
155+
model = CampaignConfigValidation(**data)
156+
assert model.manager == manager
157+
158+
@pytest.mark.parametrize("approver", ["bob", "dave", "rachel"])
159+
def test_approver_field(self, approver, valid_campaign_config_with_only_mandate_fields):
160+
data = {**valid_campaign_config_with_only_mandate_fields, "Approver": approver}
161+
model = CampaignConfigValidation(**data)
162+
assert model.approver == approver
163+
164+
@pytest.mark.parametrize("reviewer", ["carol", "eve", "zane"])
165+
def test_reviewer_field(self, reviewer, valid_campaign_config_with_only_mandate_fields):
166+
data = {**valid_campaign_config_with_only_mandate_fields, "Reviewer": reviewer}
167+
model = CampaignConfigValidation(**data)
168+
assert model.reviewer == reviewer
169+
170+
@pytest.mark.parametrize("iteration_time", ["14:00", "09:30", "18:45"])
171+
def test_iteration_time_field(self, iteration_time, valid_campaign_config_with_only_mandate_fields):
172+
data = {**valid_campaign_config_with_only_mandate_fields, "IterationTime": iteration_time}
173+
model = CampaignConfigValidation(**data)
174+
assert model.iteration_time == iteration_time
175+
176+
@pytest.mark.parametrize("routing", ["email", "sms", "push"])
177+
def test_default_comms_routing_field(self, routing, valid_campaign_config_with_only_mandate_fields):
178+
data = {**valid_campaign_config_with_only_mandate_fields, "DefaultCommsRouting": routing}
179+
model = CampaignConfigValidation(**data)
180+
assert model.default_comms_routing == routing
181+
182+
@pytest.mark.parametrize("min_approval", [0, 1, 2])
183+
def test_approval_minimum_field(self, min_approval, valid_campaign_config_with_only_mandate_fields):
184+
data = {**valid_campaign_config_with_only_mandate_fields, "ApprovalMinimum": min_approval}
185+
model = CampaignConfigValidation(**data)
186+
assert model.approval_minimum == min_approval
187+
188+
@pytest.mark.parametrize("max_approval", [5, 10, 15])
189+
def test_approval_maximum_field(self, max_approval, valid_campaign_config_with_only_mandate_fields):
190+
data = {**valid_campaign_config_with_only_mandate_fields, "ApprovalMaximum": max_approval}
191+
model = CampaignConfigValidation(**data)
192+
assert model.approval_maximum == max_approval
193+
194+
195+
class TestBUCValidations:
196+
197+
# StartDate and EndDates
198+
@pytest.mark.parametrize(
199+
("start_date", "end_date"),
200+
[
201+
("20250101", "20250331"), # typical valid range
202+
("20250601", "20250630"), # short range
203+
("20250101", "20250101"), # same day
204+
],
205+
)
206+
def test_valid_start_and_end_dates_relationship_with_iteration_dates(self, start_date, end_date, valid_campaign_config_with_only_mandate_fields):
207+
data = valid_campaign_config_with_only_mandate_fields.copy()
208+
data["StartDate"] = start_date
209+
data["EndDate"] = end_date
210+
# If any error is raised, the test fails
211+
CampaignConfigValidation(**data)
212+
213+
@pytest.mark.parametrize(
214+
("start_date", "end_date"),
215+
[
216+
("20241231", "20250101"), # year transition
217+
("20250331", "20250101"), # end before start
218+
],
219+
)
220+
def test_invalid_start_and_end_dates_relationship_with_iteration_dates(self, start_date, end_date, valid_campaign_config_with_only_mandate_fields):
221+
data = valid_campaign_config_with_only_mandate_fields.copy()
222+
data["StartDate"] = start_date
223+
data["EndDate"] = end_date
224+
with pytest.raises(ValidationError):
225+
CampaignConfigValidation(**data)
226+
227+
# Iteration
228+
def test_validate_iterations_non_empty(self, valid_campaign_config_with_only_mandate_fields):
229+
data = {**valid_campaign_config_with_only_mandate_fields, "Iterations": []}
230+
with pytest.raises(ValidationError) as error:
231+
CampaignConfigValidation(**data)
232+
# Inspect errors and check for specific field
233+
errors = error.value.errors()
234+
assert any(
235+
e["loc"][-1] == "Iterations" for e in errors), "Expected validation error on 'Iterations'"

0 commit comments

Comments
 (0)