11from unittest .mock import MagicMock
22
33import pytest
4+ from hamcrest import assert_that , empty , equal_to , is_
45
56from eligibility_signposting_api .services .processors .derived_values import (
67 AddDaysHandler ,
@@ -26,7 +27,7 @@ def test_calculate_adds_default_days_to_date(self):
2627 result = handler .calculate (context )
2728
2829 # 2025-01-01 + 91 days = 2025-04-02
29- assert result == "20250402"
30+ assert_that ( result , is_ ( equal_to ( "20250402" )))
3031
3132 def test_calculate_with_function_args_override (self ):
3233 """Test that function args override default days."""
@@ -42,7 +43,7 @@ def test_calculate_with_function_args_override(self):
4243 result = handler .calculate (context )
4344
4445 # 2025-01-01 + 30 days = 2025-01-31
45- assert result == "20250131"
46+ assert_that ( result , is_ ( equal_to ( "20250131" )))
4647
4748 def test_calculate_with_vaccine_specific_days (self ):
4849 """Test that vaccine-specific days are used when configured."""
@@ -61,7 +62,7 @@ def test_calculate_with_vaccine_specific_days(self):
6162 result = handler .calculate (context )
6263
6364 # 2025-01-01 + 365 days = 2026-01-01
64- assert result == "20260101"
65+ assert_that ( result , is_ ( equal_to ( "20260101" )))
6566
6667 def test_calculate_with_date_format (self ):
6768 """Test that date format is applied to output."""
@@ -76,7 +77,7 @@ def test_calculate_with_date_format(self):
7677
7778 result = handler .calculate (context )
7879
79- assert result == "02 April 2025"
80+ assert_that ( result , is_ ( equal_to ( "02 April 2025" )))
8081
8182 def test_calculate_returns_empty_when_source_not_found (self ):
8283 """Test that empty string is returned when source date not found."""
@@ -91,7 +92,7 @@ def test_calculate_returns_empty_when_source_not_found(self):
9192
9293 result = handler .calculate (context )
9394
94- assert result == ""
95+ assert_that ( result , is_ ( empty ()))
9596
9697 def test_calculate_returns_empty_when_vaccine_not_found (self ):
9798 """Test that empty string is returned when vaccine type not found."""
@@ -106,7 +107,7 @@ def test_calculate_returns_empty_when_vaccine_not_found(self):
106107
107108 result = handler .calculate (context )
108109
109- assert result == ""
110+ assert_that ( result , is_ ( empty ()))
110111
111112 def test_calculate_with_invalid_date_raises_error (self ):
112113 """Test that invalid date format raises ValueError."""
@@ -126,13 +127,13 @@ def test_get_source_attribute_maps_derived_to_source(self):
126127 """Test that get_source_attribute maps derived attributes correctly."""
127128 handler = AddDaysHandler ()
128129
129- assert handler .get_source_attribute ("NEXT_DOSE_DUE" ) == "LAST_SUCCESSFUL_DATE"
130+ assert_that ( handler .get_source_attribute ("NEXT_DOSE_DUE" ), is_ ( equal_to ( "LAST_SUCCESSFUL_DATE" )))
130131
131132 def test_get_source_attribute_returns_original_if_not_mapped (self ):
132133 """Test that unmapped attributes return themselves."""
133134 handler = AddDaysHandler ()
134135
135- assert handler .get_source_attribute ("UNKNOWN_ATTR" ) == "UNKNOWN_ATTR"
136+ assert_that ( handler .get_source_attribute ("UNKNOWN_ATTR" ), is_ ( equal_to ( "UNKNOWN_ATTR" )))
136137
137138 def test_function_args_priority_over_vaccine_config (self ):
138139 """Test that function args take priority over vaccine-specific config."""
@@ -151,7 +152,86 @@ def test_function_args_priority_over_vaccine_config(self):
151152 result = handler .calculate (context )
152153
153154 # 2025-01-01 + 30 days = 2025-01-31
154- assert result == "20250131"
155+ assert_that (result , is_ (equal_to ("20250131" )))
156+
157+ def test_calculate_with_negative_days (self ):
158+ """Test that negative days subtracts from date."""
159+ handler = AddDaysHandler (default_days = 91 )
160+ context = DerivedValueContext (
161+ person_data = [{"ATTRIBUTE_TYPE" : "COVID" , "LAST_SUCCESSFUL_DATE" : "20250110" }],
162+ attribute_name = "COVID" ,
163+ source_attribute = "LAST_SUCCESSFUL_DATE" ,
164+ function_args = "-7" ,
165+ date_format = None ,
166+ )
167+
168+ result = handler .calculate (context )
169+
170+ # 2025-01-10 - 7 days = 2025-01-03
171+ assert_that (result , is_ (equal_to ("20250103" )))
172+
173+ def test_calculate_with_zero_days (self ):
174+ """Test that zero days returns the same date."""
175+ handler = AddDaysHandler (default_days = 91 )
176+ context = DerivedValueContext (
177+ person_data = [{"ATTRIBUTE_TYPE" : "COVID" , "LAST_SUCCESSFUL_DATE" : "20250115" }],
178+ attribute_name = "COVID" ,
179+ source_attribute = "LAST_SUCCESSFUL_DATE" ,
180+ function_args = "0" ,
181+ date_format = None ,
182+ )
183+
184+ result = handler .calculate (context )
185+
186+ assert_that (result , is_ (equal_to ("20250115" )))
187+
188+ def test_calculate_with_invalid_args_falls_back_to_default (self ):
189+ """Test that invalid (non-numeric) function args falls back to default days."""
190+ handler = AddDaysHandler (default_days = 91 )
191+ context = DerivedValueContext (
192+ person_data = [{"ATTRIBUTE_TYPE" : "COVID" , "LAST_SUCCESSFUL_DATE" : "20250101" }],
193+ attribute_name = "COVID" ,
194+ source_attribute = "LAST_SUCCESSFUL_DATE" ,
195+ function_args = "invalid" , # Non-numeric, should fall back to default
196+ date_format = None ,
197+ )
198+
199+ result = handler .calculate (context )
200+
201+ # 2025-01-01 + 91 days (default) = 2025-04-02
202+ assert_that (result , is_ (equal_to ("20250402" )))
203+
204+ def test_calculate_crossing_year_boundary (self ):
205+ """Test adding days that crosses year boundary."""
206+ handler = AddDaysHandler (default_days = 91 )
207+ context = DerivedValueContext (
208+ person_data = [{"ATTRIBUTE_TYPE" : "COVID" , "LAST_SUCCESSFUL_DATE" : "20251230" }],
209+ attribute_name = "COVID" ,
210+ source_attribute = "LAST_SUCCESSFUL_DATE" ,
211+ function_args = "5" ,
212+ date_format = None ,
213+ )
214+
215+ result = handler .calculate (context )
216+
217+ # 2025-12-30 + 5 days = 2026-01-04
218+ assert_that (result , is_ (equal_to ("20260104" )))
219+
220+ def test_calculate_leap_year_handling (self ):
221+ """Test adding days from leap year date."""
222+ handler = AddDaysHandler (default_days = 91 )
223+ context = DerivedValueContext (
224+ person_data = [{"ATTRIBUTE_TYPE" : "COVID" , "LAST_SUCCESSFUL_DATE" : "20240229" }],
225+ attribute_name = "COVID" ,
226+ source_attribute = "LAST_SUCCESSFUL_DATE" ,
227+ function_args = "365" ,
228+ date_format = None ,
229+ )
230+
231+ result = handler .calculate (context )
232+
233+ # 2024-02-29 + 365 days = 2025-02-28 (no Feb 29 in 2025)
234+ assert_that (result , is_ (equal_to ("20250228" )))
155235
156236
157237class TestDerivedValueRegistry :
@@ -165,29 +245,29 @@ def test_register_and_get_handler(self):
165245
166246 retrieved = registry .get_handler ("ADD_DAYS" )
167247
168- assert retrieved is handler
248+ assert_that ( retrieved , is_ ( handler ))
169249
170250 def test_get_handler_case_insensitive (self ):
171251 """Test that handler lookup is case insensitive."""
172252 registry = DerivedValueRegistry ()
173253 handler = AddDaysHandler ()
174254 registry .register (handler )
175255
176- assert registry .get_handler ("add_days" ) is handler
177- assert registry .get_handler ("Add_Days" ) is handler
256+ assert_that ( registry .get_handler ("add_days" ), is_ ( handler ))
257+ assert_that ( registry .get_handler ("Add_Days" ), is_ ( handler ))
178258
179259 def test_has_handler_returns_true_when_exists (self ):
180260 """Test has_handler returns True for registered handlers."""
181261 registry = DerivedValueRegistry ()
182262 registry .register (AddDaysHandler ())
183263
184- assert registry .has_handler ("ADD_DAYS" ) is True
264+ assert_that ( registry .has_handler ("ADD_DAYS" ), is_ ( True ))
185265
186266 def test_has_handler_returns_false_when_not_exists (self ):
187267 """Test has_handler returns False for unregistered handlers."""
188268 registry = DerivedValueRegistry ()
189269
190- assert registry .has_handler ("UNKNOWN" ) is False
270+ assert_that ( registry .has_handler ("UNKNOWN" ), is_ ( False ))
191271
192272 def test_calculate_delegates_to_correct_handler (self ):
193273 """Test that calculate delegates to the correct handler."""
@@ -215,7 +295,7 @@ def test_calculate_delegates_to_correct_handler(self):
215295
216296 # Verify the mock handler was called with the context
217297 mock_handler .calculate .assert_called_once_with (context )
218- assert result == "mock_result"
298+ assert_that ( result , is_ ( equal_to ( "mock_result" )))
219299
220300 def test_calculate_raises_for_unknown_function (self ):
221301 """Test that calculate raises ValueError for unknown functions."""
@@ -240,21 +320,21 @@ def test_is_derived_attribute_returns_true_for_derived(self):
240320 registry = DerivedValueRegistry ()
241321 registry .register (AddDaysHandler ())
242322
243- assert registry .is_derived_attribute ("NEXT_DOSE_DUE" ) is True
323+ assert_that ( registry .is_derived_attribute ("NEXT_DOSE_DUE" ), is_ ( True ))
244324
245325 def test_is_derived_attribute_returns_false_for_non_derived (self ):
246326 """Test is_derived_attribute for non-derived attributes."""
247327 registry = DerivedValueRegistry ()
248328 registry .register (AddDaysHandler ())
249329
250- assert registry .is_derived_attribute ("LAST_SUCCESSFUL_DATE" ) is False
330+ assert_that ( registry .is_derived_attribute ("LAST_SUCCESSFUL_DATE" ), is_ ( False ))
251331
252332 def test_default_handlers_are_registered (self ):
253333 """Test that default handlers from the module are registered."""
254334 registry = DerivedValueRegistry ()
255335
256336 # The default ADD_DAYS handler should be registered via __init__.py
257- assert registry .has_handler ("ADD_DAYS" )
337+ assert_that ( registry .has_handler ("ADD_DAYS" ), is_ ( True ) )
258338
259339 def test_clear_defaults_removes_default_handlers (self ):
260340 """Test that clear_defaults removes all default handlers."""
@@ -266,7 +346,7 @@ def test_clear_defaults_removes_default_handlers(self):
266346
267347 # New registry should have no handlers
268348 registry = DerivedValueRegistry ()
269- assert not registry .has_handler ("ADD_DAYS" )
349+ assert_that ( registry .has_handler ("ADD_DAYS" ), is_ ( False ) )
270350 finally :
271351 # Restore defaults for other tests using public method
272352 DerivedValueRegistry .set_default_handlers (saved_defaults )
0 commit comments