Skip to content

Commit c9ce52b

Browse files
committed
eli-579 safer handling of non-numeric values if passed to ADD_DAYS
1 parent c39bc7d commit c9ce52b

2 files changed

Lines changed: 48 additions & 30 deletions

File tree

src/eligibility_signposting_api/services/processors/derived_values/add_days_handler.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,13 @@ def _get_days_to_add(self, context: DerivedValueContext) -> int:
118118
Returns:
119119
Number of days to add
120120
"""
121-
# Priority 1: Token argument
121+
# Priority 1: Token argument (if non-empty)
122122
if context.function_args:
123123
try:
124124
return int(context.function_args)
125-
except ValueError:
126-
pass
125+
except ValueError as e:
126+
message = f"Invalid days argument '{context.function_args}' for ADD_DAYS function. Expected an integer."
127+
raise ValueError(message) from e
127128

128129
# Priority 2: Vaccine-specific configuration
129130
if context.attribute_name in self.vaccine_type_days:

tests/unit/services/processors/test_derived_values.py

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from unittest.mock import MagicMock
22

3-
import pytest
3+
from hamcrest import assert_that, calling, equal_to, is_, raises, same_instance
44

55
from eligibility_signposting_api.services.processors.derived_values import (
66
AddDaysHandler,
@@ -26,7 +26,7 @@ def test_calculate_adds_default_days_to_date(self):
2626
result = handler.calculate(context)
2727

2828
# 2025-01-01 + 91 days = 2025-04-02
29-
assert result == "20250402"
29+
assert_that(result, is_(equal_to("20250402")))
3030

3131
def test_calculate_with_function_args_override(self):
3232
"""Test that function args override default days."""
@@ -42,7 +42,7 @@ def test_calculate_with_function_args_override(self):
4242
result = handler.calculate(context)
4343

4444
# 2025-01-01 + 30 days = 2025-01-31
45-
assert result == "20250131"
45+
assert_that(result, is_(equal_to("20250131")))
4646

4747
def test_calculate_with_vaccine_specific_days(self):
4848
"""Test that vaccine-specific days are used when configured."""
@@ -61,7 +61,7 @@ def test_calculate_with_vaccine_specific_days(self):
6161
result = handler.calculate(context)
6262

6363
# 2025-01-01 + 365 days = 2026-01-01
64-
assert result == "20260101"
64+
assert_that(result, is_(equal_to("20260101")))
6565

6666
def test_calculate_with_date_format(self):
6767
"""Test that date format is applied to output."""
@@ -76,7 +76,7 @@ def test_calculate_with_date_format(self):
7676

7777
result = handler.calculate(context)
7878

79-
assert result == "02 April 2025"
79+
assert_that(result, is_(equal_to("02 April 2025")))
8080

8181
def test_calculate_returns_empty_when_source_not_found(self):
8282
"""Test that empty string is returned when source date not found."""
@@ -91,7 +91,7 @@ def test_calculate_returns_empty_when_source_not_found(self):
9191

9292
result = handler.calculate(context)
9393

94-
assert result == ""
94+
assert_that(result, is_(equal_to("")))
9595

9696
def test_calculate_returns_empty_when_vaccine_not_found(self):
9797
"""Test that empty string is returned when vaccine type not found."""
@@ -106,7 +106,7 @@ def test_calculate_returns_empty_when_vaccine_not_found(self):
106106

107107
result = handler.calculate(context)
108108

109-
assert result == ""
109+
assert_that(result, is_(equal_to("")))
110110

111111
def test_calculate_with_invalid_date_raises_error(self):
112112
"""Test that invalid date format raises ValueError."""
@@ -119,20 +119,38 @@ def test_calculate_with_invalid_date_raises_error(self):
119119
date_format=None,
120120
)
121121

122-
with pytest.raises(ValueError, match="Invalid date format"):
123-
handler.calculate(context)
122+
assert_that(
123+
calling(handler.calculate).with_args(context),
124+
raises(ValueError, pattern="Invalid date format"),
125+
)
126+
127+
def test_calculate_with_invalid_function_args_raises_error(self):
128+
"""Test that non-integer function args raises ValueError."""
129+
handler = AddDaysHandler(default_days=91)
130+
context = DerivedValueContext(
131+
person_data=[{"ATTRIBUTE_TYPE": "COVID", "LAST_SUCCESSFUL_DATE": "20250101"}],
132+
attribute_name="COVID",
133+
source_attribute="LAST_SUCCESSFUL_DATE",
134+
function_args="abc", # Invalid: not an integer
135+
date_format=None,
136+
)
137+
138+
assert_that(
139+
calling(handler.calculate).with_args(context),
140+
raises(ValueError, pattern="Invalid days argument 'abc' for ADD_DAYS function"),
141+
)
124142

125143
def test_get_source_attribute_maps_derived_to_source(self):
126144
"""Test that get_source_attribute maps derived attributes correctly."""
127145
handler = AddDaysHandler()
128146

129-
assert handler.get_source_attribute("NEXT_DOSE_DUE") == "LAST_SUCCESSFUL_DATE"
147+
assert_that(handler.get_source_attribute("NEXT_DOSE_DUE"), is_(equal_to("LAST_SUCCESSFUL_DATE")))
130148

131149
def test_get_source_attribute_returns_original_if_not_mapped(self):
132150
"""Test that unmapped attributes return themselves."""
133151
handler = AddDaysHandler()
134152

135-
assert handler.get_source_attribute("UNKNOWN_ATTR") == "UNKNOWN_ATTR"
153+
assert_that(handler.get_source_attribute("UNKNOWN_ATTR"), is_(equal_to("UNKNOWN_ATTR")))
136154

137155
def test_function_args_priority_over_vaccine_config(self):
138156
"""Test that function args take priority over vaccine-specific config."""
@@ -151,7 +169,7 @@ def test_function_args_priority_over_vaccine_config(self):
151169
result = handler.calculate(context)
152170

153171
# 2025-01-01 + 30 days = 2025-01-31
154-
assert result == "20250131"
172+
assert_that(result, is_(equal_to("20250131")))
155173

156174

157175
class TestDerivedValueRegistry:
@@ -165,29 +183,29 @@ def test_register_and_get_handler(self):
165183

166184
retrieved = registry.get_handler("ADD_DAYS")
167185

168-
assert retrieved is handler
186+
assert_that(retrieved, same_instance(handler)) # type: ignore[call-overload]
169187

170188
def test_get_handler_case_insensitive(self):
171189
"""Test that handler lookup is case insensitive."""
172190
registry = DerivedValueRegistry()
173191
handler = AddDaysHandler()
174192
registry.register(handler)
175193

176-
assert registry.get_handler("add_days") is handler
177-
assert registry.get_handler("Add_Days") is handler
194+
assert_that(registry.get_handler("add_days"), same_instance(handler)) # type: ignore[call-overload]
195+
assert_that(registry.get_handler("Add_Days"), same_instance(handler)) # type: ignore[call-overload]
178196

179197
def test_has_handler_returns_true_when_exists(self):
180198
"""Test has_handler returns True for registered handlers."""
181199
registry = DerivedValueRegistry()
182200
registry.register(AddDaysHandler())
183201

184-
assert registry.has_handler("ADD_DAYS") is True
202+
assert_that(registry.has_handler("ADD_DAYS"), is_(True))
185203

186204
def test_has_handler_returns_false_when_not_exists(self):
187205
"""Test has_handler returns False for unregistered handlers."""
188206
registry = DerivedValueRegistry()
189207

190-
assert registry.has_handler("UNKNOWN") is False
208+
assert_that(registry.has_handler("UNKNOWN"), is_(False))
191209

192210
def test_calculate_delegates_to_correct_handler(self):
193211
"""Test that calculate delegates to the correct handler."""
@@ -215,7 +233,7 @@ def test_calculate_delegates_to_correct_handler(self):
215233

216234
# Verify the mock handler was called with the context
217235
mock_handler.calculate.assert_called_once_with(context)
218-
assert result == "mock_result"
236+
assert_that(result, is_(equal_to("mock_result")))
219237

220238
def test_calculate_raises_for_unknown_function(self):
221239
"""Test that calculate raises ValueError for unknown functions."""
@@ -229,32 +247,31 @@ def test_calculate_raises_for_unknown_function(self):
229247
date_format=None,
230248
)
231249

232-
with pytest.raises(ValueError, match="No handler registered"):
233-
registry.calculate(
234-
function_name="UNKNOWN",
235-
context=context,
236-
)
250+
assert_that(
251+
calling(registry.calculate).with_args(function_name="UNKNOWN", context=context),
252+
raises(ValueError, pattern="No handler registered"),
253+
)
237254

238255
def test_is_derived_attribute_returns_true_for_derived(self):
239256
"""Test is_derived_attribute for known derived attributes."""
240257
registry = DerivedValueRegistry()
241258
registry.register(AddDaysHandler())
242259

243-
assert registry.is_derived_attribute("NEXT_DOSE_DUE") is True
260+
assert_that(registry.is_derived_attribute("NEXT_DOSE_DUE"), is_(True))
244261

245262
def test_is_derived_attribute_returns_false_for_non_derived(self):
246263
"""Test is_derived_attribute for non-derived attributes."""
247264
registry = DerivedValueRegistry()
248265
registry.register(AddDaysHandler())
249266

250-
assert registry.is_derived_attribute("LAST_SUCCESSFUL_DATE") is False
267+
assert_that(registry.is_derived_attribute("LAST_SUCCESSFUL_DATE"), is_(False))
251268

252269
def test_default_handlers_are_registered(self):
253270
"""Test that default handlers from the module are registered."""
254271
registry = DerivedValueRegistry()
255272

256273
# The default ADD_DAYS handler should be registered via __init__.py
257-
assert registry.has_handler("ADD_DAYS")
274+
assert_that(registry.has_handler("ADD_DAYS"), is_(True))
258275

259276
def test_clear_defaults_removes_default_handlers(self):
260277
"""Test that clear_defaults removes all default handlers."""
@@ -266,7 +283,7 @@ def test_clear_defaults_removes_default_handlers(self):
266283

267284
# New registry should have no handlers
268285
registry = DerivedValueRegistry()
269-
assert not registry.has_handler("ADD_DAYS")
286+
assert_that(registry.has_handler("ADD_DAYS"), is_(False))
270287
finally:
271288
# Restore defaults for other tests using public method
272289
DerivedValueRegistry.set_default_handlers(saved_defaults)

0 commit comments

Comments
 (0)