11import re
22
33import pytest
4+ from hamcrest import assert_that , calling , equal_to , is_ , raises
45
56from eligibility_signposting_api .model import eligibility_status
67from eligibility_signposting_api .model .eligibility_status import (
@@ -43,7 +44,7 @@ def test_simple_token_replacement(self):
4344
4445 actual = TokenProcessor .find_and_replace_tokens (person , condition )
4546
46- assert actual == expected
47+ assert_that ( actual , is_ ( equal_to ( expected )))
4748
4849 def test_deep_nesting_token_replacement (self ):
4950 person = Person ([{"ATTRIBUTE_TYPE" : "PERSON" , "AGE" : "30" , "DEGREE" : "DOCTOR" , "QUALITY" : "NICE" }])
@@ -84,9 +85,9 @@ def test_deep_nesting_token_replacement(self):
8485
8586 actual = TokenProcessor .find_and_replace_tokens (person , condition )
8687
87- assert actual .cohort_results [0 ].description == "Results for cohort 30."
88- assert actual .cohort_results [0 ].reasons [1 ].rule_text == "Rule 30 here."
89- assert actual .status_text == StatusText ("Everything is NICE." )
88+ assert_that ( actual .cohort_results [0 ].description , is_ ( equal_to ( "Results for cohort 30." )))
89+ assert_that ( actual .cohort_results [0 ].reasons [1 ].rule_text , is_ ( equal_to ( "Rule 30 here." )))
90+ assert_that ( actual .status_text , is_ ( equal_to ( StatusText ("Everything is NICE." ))) )
9091
9192 def test_invalid_token_on_person_attribute_should_raise_error (self ):
9293 person = Person ([{"ATTRIBUTE_TYPE" : "PERSON" , "AGE" : "30" }])
@@ -102,8 +103,10 @@ def test_invalid_token_on_person_attribute_should_raise_error(self):
102103
103104 expected_error = re .escape ("Invalid attribute name 'ICECREAM' in token '[[PERSON.ICECREAM]]'." )
104105
105- with pytest .raises (ValueError , match = expected_error ):
106- TokenProcessor .find_and_replace_tokens (person , condition )
106+ assert_that (
107+ calling (TokenProcessor .find_and_replace_tokens ).with_args (person , condition ),
108+ raises (ValueError , pattern = expected_error ),
109+ )
107110
108111 def test_invalid_token_should_raise_error (self ):
109112 person = Person ([{"ATTRIBUTE_TYPE" : "PERSON" , "AGE" : "30" }])
@@ -118,8 +121,10 @@ def test_invalid_token_should_raise_error(self):
118121 )
119122
120123 expected_error = re .escape ("Invalid attribute level 'ICECREAM' in token '[[ICECREAM.FLAVOR]]'." )
121- with pytest .raises (ValueError , match = expected_error ):
122- TokenProcessor .find_and_replace_tokens (person , condition )
124+ assert_that (
125+ calling (TokenProcessor .find_and_replace_tokens ).with_args (person , condition ),
126+ raises (ValueError , pattern = expected_error ),
127+ )
123128
124129 def test_invalid_token_on_target_attribute_should_raise_error (self ):
125130 person = Person ([{"ATTRIBUTE_TYPE" : "RSV" , "LAST_SUCCESSFUL_DATE" : "20250101" }])
@@ -134,8 +139,10 @@ def test_invalid_token_on_target_attribute_should_raise_error(self):
134139 )
135140
136141 expected_error = re .escape ("Invalid attribute name 'ICECREAM' in token '[[TARGET.RSV.ICECREAM]]'." )
137- with pytest .raises (ValueError , match = expected_error ):
138- TokenProcessor .find_and_replace_tokens (person , condition )
142+ assert_that (
143+ calling (TokenProcessor .find_and_replace_tokens ).with_args (person , condition ),
144+ raises (ValueError , pattern = expected_error ),
145+ )
139146
140147 def test_missing_target_attribute_and_invalid_token_should_raise_error (self ):
141148 person = Person ([{"ATTRIBUTE_TYPE" : "PERSON" , "AGE" : "30" }])
@@ -150,8 +157,10 @@ def test_missing_target_attribute_and_invalid_token_should_raise_error(self):
150157 )
151158
152159 expected_error = re .escape ("Invalid attribute name 'ICECREAM' in token '[[TARGET.RSV.ICECREAM]]'." )
153- with pytest .raises (ValueError , match = expected_error ):
154- TokenProcessor .find_and_replace_tokens (person , condition )
160+ assert_that (
161+ calling (TokenProcessor .find_and_replace_tokens ).with_args (person , condition ),
162+ raises (ValueError , pattern = expected_error ),
163+ )
155164
156165 def test_missing_patient_vaccine_data_on_target_attribute_should_replace_with_empty (self ):
157166 person = Person ([{"ATTRIBUTE_TYPE" : "PERSON" , "AGE" : "30" }])
@@ -167,7 +176,7 @@ def test_missing_patient_vaccine_data_on_target_attribute_should_replace_with_em
167176
168177 actual = TokenProcessor .find_and_replace_tokens (person , condition )
169178
170- assert actual .condition_name == "Last successful date: "
179+ assert_that ( actual .condition_name , is_ ( equal_to ( "Last successful date: " )))
171180
172181 def test_not_allowed_target_conditions_token_should_raise_error (self ):
173182 person = Person (
@@ -189,8 +198,10 @@ def test_not_allowed_target_conditions_token_should_raise_error(self):
189198 expected_error = re .escape (
190199 "Invalid attribute name 'LAST_SUCCESSFUL_DATE' in token '[[TARGET.YELLOW_FEVER.LAST_SUCCESSFUL_DATE]]'."
191200 )
192- with pytest .raises (ValueError , match = expected_error ):
193- TokenProcessor .find_and_replace_tokens (person , condition )
201+ assert_that (
202+ calling (TokenProcessor .find_and_replace_tokens ).with_args (person , condition ),
203+ raises (ValueError , pattern = expected_error ),
204+ )
194205
195206 def test_valid_token_but_missing_attribute_data_to_replace (self ):
196207 person = Person (
@@ -223,8 +234,8 @@ def test_valid_token_but_missing_attribute_data_to_replace(self):
223234
224235 actual = TokenProcessor .find_and_replace_tokens (person , condition )
225236
226- assert actual .status_text == expected .status_text
227- assert actual .condition_name == expected .condition_name
237+ assert_that ( actual .status_text , is_ ( equal_to ( expected .status_text )))
238+ assert_that ( actual .condition_name , is_ ( equal_to ( expected .condition_name )))
228239
229240 def test_valid_token_but_missing_attribute_in_multiple_vacc_data_to_replace (self ):
230241 person = Person (
@@ -259,8 +270,8 @@ def test_valid_token_but_missing_attribute_in_multiple_vacc_data_to_replace(self
259270
260271 actual = TokenProcessor .find_and_replace_tokens (person , condition )
261272
262- assert actual .status_text == expected .status_text
263- assert actual .condition_name == expected .condition_name
273+ assert_that ( actual .status_text , is_ ( equal_to ( expected .status_text )))
274+ assert_that ( actual .condition_name , is_ ( equal_to ( expected .condition_name )))
264275
265276 def test_simple_string_with_multiple_tokens (self ):
266277 person = Person (
@@ -292,7 +303,7 @@ def test_simple_string_with_multiple_tokens(self):
292303
293304 actual = TokenProcessor .find_and_replace_tokens (person , condition )
294305
295- assert actual == expected
306+ assert_that ( actual , is_ ( equal_to ( expected )))
296307
297308 def test_valid_token_valid_format_should_replace_with_date_formatting (self ):
298309 person = Person (
@@ -325,8 +336,8 @@ def test_valid_token_valid_format_should_replace_with_date_formatting(self):
325336
326337 actual = TokenProcessor .find_and_replace_tokens (person , condition )
327338
328- assert actual .condition_name == expected .condition_name
329- assert actual .status_text == expected .status_text
339+ assert_that ( actual .condition_name , is_ ( equal_to ( expected .condition_name )))
340+ assert_that ( actual .status_text , is_ ( equal_to ( expected .status_text )))
330341
331342 @pytest .mark .parametrize (
332343 "token_format" ,
@@ -348,8 +359,10 @@ def test_valid_token_invalid_format_should_raise_error(self, token_format: str):
348359 actions = [],
349360 )
350361
351- with pytest .raises (ValueError , match = r"Invalid token format\." ):
352- TokenProcessor .find_and_replace_tokens (person , condition )
362+ assert_that (
363+ calling (TokenProcessor .find_and_replace_tokens ).with_args (person , condition ),
364+ raises (ValueError , pattern = r"Invalid token format\." ),
365+ )
353366
354367 @pytest .mark .parametrize (
355368 ("token_format" , "func_name" ),
@@ -373,8 +386,10 @@ def test_unknown_function_raises_error(self, token_format: str, func_name: str):
373386 actions = [],
374387 )
375388
376- with pytest .raises (ValueError , match = f"Unknown function '{ func_name } '" ):
377- TokenProcessor .find_and_replace_tokens (person , condition )
389+ assert_that (
390+ calling (TokenProcessor .find_and_replace_tokens ).with_args (person , condition ),
391+ raises (ValueError , pattern = f"Unknown function '{ func_name } '" ),
392+ )
378393
379394 @pytest .mark .parametrize (
380395 ("token_format" , "expected" ),
@@ -409,7 +424,7 @@ def test_valid_date_format(self, token_format: str, expected: str):
409424
410425 actual = TokenProcessor .find_and_replace_tokens (person , condition )
411426
412- assert actual .condition_name == f"Date: { expected } "
427+ assert_that ( actual .condition_name , is_ ( equal_to ( f"Date: { expected } " )))
413428
414429 @pytest .mark .parametrize (
415430 ("token" , "expected" ),
@@ -443,8 +458,8 @@ def test_token_replace_is_case_insensitive(self, token: str, expected: str):
443458
444459 result = TokenProcessor .find_and_replace_tokens (person , condition )
445460
446- assert result .status_text == f"Your DOB is: { expected } ."
447- assert result .condition_name == f"FLU vaccine on: { expected } ."
461+ assert_that ( result .status_text , is_ ( equal_to ( f"Your DOB is: { expected } ." )))
462+ assert_that ( result .condition_name , is_ ( equal_to ( f"FLU vaccine on: { expected } ." )))
448463
449464
450465class TestCustomTargetAttributeNames :
@@ -472,7 +487,7 @@ def test_custom_target_attribute_with_add_days_when_data_present(self):
472487 result = TokenProcessor .find_and_replace_tokens (person , condition )
473488
474489 # 2026-01-28 + 71 days = 2026-04-09
475- assert result .status_text == "Next booking: 20260409"
490+ assert_that ( result .status_text , is_ ( equal_to ( "Next booking: 20260409" )))
476491
477492 def test_custom_target_attribute_with_add_days_and_formatting (self ):
478493 """Test that custom target attributes work with both ADD_DAYS and DATE formatting."""
@@ -496,7 +511,7 @@ def test_custom_target_attribute_with_add_days_and_formatting(self):
496511 result = TokenProcessor .find_and_replace_tokens (person , condition )
497512
498513 # 2026-01-28 + 71 days = 2026-04-09, formatted as "09 April 2026"
499- assert result .status_text == "Date: 09 April 2026"
514+ assert_that ( result .status_text , is_ ( equal_to ( "Date: 09 April 2026" )))
500515
501516 def test_custom_target_attribute_returns_empty_when_condition_not_present (self ):
502517 """Test that custom target attributes return empty string when condition data not present."""
@@ -521,7 +536,7 @@ def test_custom_target_attribute_returns_empty_when_condition_not_present(self):
521536 result = TokenProcessor .find_and_replace_tokens (person , condition )
522537
523538 # Should return empty string when condition data is not present
524- assert result .status_text == "Next booking: "
539+ assert_that ( result .status_text , is_ ( equal_to ( "Next booking: " )))
525540
526541 def test_multiple_custom_target_attributes_with_different_functions (self ):
527542 """Test multiple custom target attributes with different parameters."""
@@ -546,7 +561,7 @@ def test_multiple_custom_target_attributes_with_different_functions(self):
546561 result = TokenProcessor .find_and_replace_tokens (person , condition )
547562
548563 # 2026-01-28 + 30 = 2026-02-27, + 60 = 2026-03-29
549- assert result .status_text == "First: 20260227 Second: 20260329"
564+ assert_that ( result .status_text , is_ ( equal_to ( "First: 20260227 Second: 20260329" )))
550565
551566 def test_custom_target_attribute_raises_error_for_invalid_condition (self ):
552567 """Test that invalid condition names still raise errors even with custom target attributes."""
@@ -565,8 +580,10 @@ def test_custom_target_attribute_raises_error_for_invalid_condition(self):
565580 actions = [],
566581 )
567582
568- with pytest .raises (ValueError , match = "Invalid attribute name 'CUSTOM_FIELD'" ):
569- TokenProcessor .find_and_replace_tokens (person , condition )
583+ assert_that (
584+ calling (TokenProcessor .find_and_replace_tokens ).with_args (person , condition ),
585+ raises (ValueError , pattern = "Invalid attribute name 'CUSTOM_FIELD'" ),
586+ )
570587
571588 def test_non_derived_token_with_invalid_target_attribute_raises_error (self ):
572589 """Test that non-derived tokens (without functions) validate target attributes strictly."""
@@ -586,8 +603,10 @@ def test_non_derived_token_with_invalid_target_attribute_raises_error(self):
586603 )
587604
588605 # Non-derived tokens should only allow ALLOWED_TARGET_ATTRIBUTES
589- with pytest .raises (ValueError , match = "Invalid attribute name 'CUSTOM_INVALID_FIELD'" ):
590- TokenProcessor .find_and_replace_tokens (person , condition )
606+ assert_that (
607+ calling (TokenProcessor .find_and_replace_tokens ).with_args (person , condition ),
608+ raises (ValueError , pattern = "Invalid attribute name 'CUSTOM_INVALID_FIELD'" ),
609+ )
591610
592611 def test_non_derived_token_with_valid_target_attribute_works (self ):
593612 """Test that non-derived tokens with valid target attributes work correctly."""
@@ -608,7 +627,7 @@ def test_non_derived_token_with_valid_target_attribute_works(self):
608627
609628 result = TokenProcessor .find_and_replace_tokens (person , condition )
610629
611- assert result .status_text == "Last date: 20260128"
630+ assert_that ( result .status_text , is_ ( equal_to ( "Last date: 20260128" )))
612631
613632 def test_person_level_attribute_with_add_days_without_explicit_source (self ):
614633 """Test that ADD_DAYS works on PERSON-level attributes without explicit source."""
@@ -630,7 +649,7 @@ def test_person_level_attribute_with_add_days_without_explicit_source(self):
630649 result = TokenProcessor .find_and_replace_tokens (person , condition )
631650
632651 # 1990-03-27 + 91 days = 1990-06-26
633- assert result .status_text == "Future date: 19900626"
652+ assert_that ( result .status_text , is_ ( equal_to ( "Future date: 19900626" )))
634653
635654 def test_person_level_attribute_with_add_days_explicit_source (self ):
636655 """Test that ADD_DAYS works on PERSON-level attributes with explicit source."""
@@ -652,7 +671,7 @@ def test_person_level_attribute_with_add_days_explicit_source(self):
652671 result = TokenProcessor .find_and_replace_tokens (person , condition )
653672
654673 # 1990-03-27 + 91 days = 1990-06-26
655- assert result .status_text == "Future date: 19900626"
674+ assert_that ( result .status_text , is_ ( equal_to ( "Future date: 19900626" )))
656675
657676 def test_derived_value_with_no_function_name_raises_error (self ):
658677 """Test that derived tokens without function name raise ValueError."""
@@ -666,13 +685,15 @@ def test_derived_value_with_no_function_name_raises_error(self):
666685 format = None ,
667686 )
668687
669- with pytest . raises ( ValueError , match = "No function specified in token" ):
670- TokenProcessor .get_derived_value (
688+ assert_that (
689+ calling ( TokenProcessor .get_derived_value ). with_args (
671690 parsed_token = parsed_token ,
672691 person_data = [{"ATTRIBUTE_TYPE" : "COVID" , "LAST_SUCCESSFUL_DATE" : "20250101" }],
673692 present_attributes = {"COVID" },
674693 token = "[[TARGET.COVID.NEXT_DOSE_DUE:]]" , # Malformed token
675- )
694+ ),
695+ raises (ValueError , pattern = "No function specified in token" ),
696+ )
676697
677698 def test_derived_value_with_unknown_function_raises_error (self ):
678699 """Test that derived tokens with unknown function raise ValueError."""
@@ -685,13 +706,15 @@ def test_derived_value_with_unknown_function_raises_error(self):
685706 format = None ,
686707 )
687708
688- with pytest . raises ( ValueError , match = "Unknown function 'UNKNOWN_FUNCTION' in token" ):
689- TokenProcessor .get_derived_value (
709+ assert_that (
710+ calling ( TokenProcessor .get_derived_value ). with_args (
690711 parsed_token = parsed_token ,
691712 person_data = [{"ATTRIBUTE_TYPE" : "COVID" , "LAST_SUCCESSFUL_DATE" : "20250101" }],
692713 present_attributes = {"COVID" },
693714 token = "[[TARGET.COVID.NEXT_DOSE_DUE:UNKNOWN_FUNCTION(30)]]" ,
694- )
715+ ),
716+ raises (ValueError , pattern = "Unknown function 'UNKNOWN_FUNCTION' in token" ),
717+ )
695718
696719 def test_derived_value_handler_exception_gets_wrapped (self ):
697720 """Test that exceptions from derived value handlers are wrapped with context."""
@@ -704,10 +727,12 @@ def test_derived_value_handler_exception_gets_wrapped(self):
704727 format = None ,
705728 )
706729
707- with pytest . raises ( ValueError , match = r"Error calculating derived value for token.*Invalid days argument" ):
708- TokenProcessor .get_derived_value (
730+ assert_that (
731+ calling ( TokenProcessor .get_derived_value ). with_args (
709732 parsed_token = parsed_token ,
710733 person_data = [{"ATTRIBUTE_TYPE" : "COVID" , "LAST_SUCCESSFUL_DATE" : "20250101" }],
711734 present_attributes = {"COVID" },
712735 token = "[[TARGET.COVID.NEXT_DOSE_DUE:ADD_DAYS(invalid_arg)]]" ,
713- )
736+ ),
737+ raises (ValueError , pattern = r"Error calculating derived value for token.*Invalid days argument" ),
738+ )
0 commit comments