Skip to content

Commit 33b6f18

Browse files
test for campaign config rules
1 parent 9532d4d commit 33b6f18

6 files changed

Lines changed: 273 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.
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import pytest
2+
from pydantic import ValidationError
3+
4+
from rules_validation_api.validators.campaign_config_validator import CampaignConfigValidation
5+
6+
valid_base = {
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+
}
49+
50+
51+
class TestMandateFields:
52+
def test_valid_base_configuration(self):
53+
try:
54+
CampaignConfigValidation(**valid_base)
55+
except ValidationError as e:
56+
pytest.fail(f"Unexpected error during model instantiation: {e}")
57+
58+
@pytest.mark.parametrize(
59+
"mandate_field",
60+
[
61+
"ID",
62+
"Version",
63+
"Name",
64+
"Type",
65+
"Target",
66+
"IterationFrequency",
67+
"IterationType",
68+
"StartDate",
69+
"EndDate",
70+
"Iterations",
71+
],
72+
)
73+
def test_missing_mandate_fields(self, mandate_field):
74+
data = valid_base.copy()
75+
data.pop(mandate_field, None) # Simulate missing field
76+
with pytest.raises(ValidationError):
77+
CampaignConfigValidation(**data)
78+
assert mandate_field.lower()
79+
80+
# ID field
81+
82+
# ID
83+
@pytest.mark.parametrize("id_value", ["CAMP001", "12345", "X001"])
84+
def test_valid_id(self, id_value):
85+
data = {**valid_base, "ID": id_value}
86+
model = CampaignConfigValidation(**data)
87+
assert model.id == id_value
88+
89+
# Version
90+
@pytest.mark.parametrize("version_value", ["v1.0", "v2.1", "V3.0"])
91+
def test_valid_version(self, version_value):
92+
data = {**valid_base, "Version": version_value}
93+
model = CampaignConfigValidation(**data)
94+
assert model.version == version_value
95+
96+
# Name
97+
@pytest.mark.parametrize("name_value", ["Spring Campaign", "COVID-Alert", "Mass Outreach"])
98+
def test_valid_name(self, name_value):
99+
data = {**valid_base, "Name": name_value}
100+
model = CampaignConfigValidation(**data)
101+
assert model.name == name_value
102+
103+
# Type
104+
@pytest.mark.parametrize("type_value", ["V", "S"])
105+
def test_valid_type(self, type_value):
106+
data = {**valid_base, "Type": type_value}
107+
model = CampaignConfigValidation(**data)
108+
assert model.type == type_value
109+
110+
@pytest.mark.parametrize("type_value", ["X", "", None])
111+
def test_invalid_type(self, type_value):
112+
data = {**valid_base, "Type": type_value}
113+
with pytest.raises(ValidationError):
114+
CampaignConfigValidation(**data)
115+
116+
# Target
117+
@pytest.mark.parametrize("target_value", ["COVID", "FLU", "MMR", "RSV"])
118+
def test_valid_target(self, target_value):
119+
data = {**valid_base, "Target": target_value}
120+
model = CampaignConfigValidation(**data)
121+
assert model.target == target_value
122+
123+
@pytest.mark.parametrize("target_value", ["EBOLA", "HEP", "", None])
124+
def test_invalid_target(self, target_value):
125+
data = {**valid_base, "Target": target_value}
126+
with pytest.raises(ValidationError):
127+
CampaignConfigValidation(**data)
128+
129+
# IterationFrequency
130+
@pytest.mark.parametrize("freq_value", ["X", "D", "W", "M", "Q", "A"])
131+
def test_valid_iteration_frequency(self, freq_value):
132+
data = {**valid_base, "IterationFrequency": freq_value}
133+
model = CampaignConfigValidation(**data)
134+
assert model.iteration_frequency == freq_value
135+
136+
@pytest.mark.parametrize("freq_value", ["Z", "", None])
137+
def test_invalid_iteration_frequency(self, freq_value):
138+
data = {**valid_base, "IterationFrequency": freq_value}
139+
with pytest.raises(ValidationError):
140+
CampaignConfigValidation(**data)
141+
142+
# IterationType
143+
@pytest.mark.parametrize("iter_type", ["A", "M", "S", "O"])
144+
def test_valid_iteration_type(self, iter_type):
145+
data = {**valid_base, "IterationType": iter_type}
146+
model = CampaignConfigValidation(**data)
147+
assert model.iteration_type == iter_type
148+
149+
@pytest.mark.parametrize("iter_type", ["B", "", None])
150+
def test_invalid_iteration_type(self, iter_type):
151+
data = {**valid_base, "IterationType": iter_type}
152+
with pytest.raises(ValidationError):
153+
CampaignConfigValidation(**data)
154+
155+
# StartDate and EndDates
156+
@pytest.mark.parametrize(
157+
("start_date", "end_date"),
158+
[
159+
("20250101", "20250331"), # typical valid range
160+
("20250601", "20250630"), # short range
161+
("20250101", "20250101"), # same day
162+
],
163+
)
164+
def test_valid_start_end_dates(self, start_date, end_date):
165+
data = valid_base.copy()
166+
data["StartDate"] = start_date
167+
data["EndDate"] = end_date
168+
# If any error is raised, the test fails
169+
CampaignConfigValidation(**data)
170+
171+
@pytest.mark.parametrize(
172+
("start_date", "end_date"),
173+
[
174+
("20241231", "20250101"), # year transition
175+
("20250331", "20250101"), # end before start
176+
],
177+
)
178+
def test_invalid_start_end_dates(self, start_date, end_date):
179+
data = valid_base.copy()
180+
data["StartDate"] = start_date
181+
data["EndDate"] = end_date
182+
with pytest.raises(ValidationError):
183+
CampaignConfigValidation(**data)
184+
185+
@pytest.mark.parametrize(
186+
"start_date",
187+
[
188+
"", # empty string
189+
"invalid-date", # malformed value
190+
],
191+
)
192+
def test_invalid_start_date_only(self, start_date):
193+
data = valid_base.copy()
194+
data["StartDate"] = start_date
195+
data["EndDate"] = "20250101" # valid end date
196+
197+
with pytest.raises(ValidationError) as exc_info:
198+
CampaignConfigValidation(**data)
199+
200+
errors = exc_info.value.errors()
201+
for error in errors:
202+
assert error["loc"][0] == "StartDate"
203+
204+
@pytest.mark.parametrize(
205+
"end_date",
206+
[
207+
"", # empty string
208+
"31032025", # malformed value
209+
],
210+
)
211+
def test_invalid_end_date_only(self, end_date):
212+
data = valid_base.copy()
213+
data["StartDate"] = "20250101" # valid start date
214+
data["EndDate"] = end_date
215+
216+
with pytest.raises(ValidationError) as exc_info:
217+
CampaignConfigValidation(**data)
218+
219+
errors = exc_info.value.errors()
220+
for error in errors:
221+
assert error["loc"][0] == "EndDate"
222+
223+
224+
class TestOptionalFields:
225+
@pytest.mark.parametrize("manager", ["alice", "bob", "carol"])
226+
def test_manager_field(self, manager):
227+
data = {**valid_base, "Manager": manager}
228+
model = CampaignConfigValidation(**data)
229+
assert model.manager == manager
230+
231+
@pytest.mark.parametrize("approver", ["bob", "dave", "rachel"])
232+
def test_approver_field(self, approver):
233+
data = {**valid_base, "Approver": approver}
234+
model = CampaignConfigValidation(**data)
235+
assert model.approver == approver
236+
237+
@pytest.mark.parametrize("reviewer", ["carol", "eve", "zane"])
238+
def test_reviewer_field(self, reviewer):
239+
data = {**valid_base, "Reviewer": reviewer}
240+
model = CampaignConfigValidation(**data)
241+
assert model.reviewer == reviewer
242+
243+
@pytest.mark.parametrize("iteration_time", ["14:00", "09:30", "18:45"])
244+
def test_iteration_time_field(self, iteration_time):
245+
data = {**valid_base, "IterationTime": iteration_time}
246+
model = CampaignConfigValidation(**data)
247+
assert model.iteration_time == iteration_time
248+
249+
@pytest.mark.parametrize("routing", ["email", "sms", "push"])
250+
def test_default_comms_routing_field(self, routing):
251+
data = {**valid_base, "DefaultCommsRouting": routing}
252+
model = CampaignConfigValidation(**data)
253+
assert model.default_comms_routing == routing
254+
255+
@pytest.mark.parametrize("min_approval", [0, 1, 2])
256+
def test_approval_minimum_field(self, min_approval):
257+
data = {**valid_base, "ApprovalMinimum": min_approval}
258+
model = CampaignConfigValidation(**data)
259+
assert model.approval_minimum == min_approval
260+
261+
@pytest.mark.parametrize("max_approval", [5, 10, 15])
262+
def test_approval_maximum_field(self, max_approval):
263+
data = {**valid_base, "ApprovalMaximum": max_approval}
264+
model = CampaignConfigValidation(**data)
265+
assert model.approval_maximum == max_approval

0 commit comments

Comments
 (0)