Skip to content

Commit 832691b

Browse files
committed
fix(feature_flags): handle same-hour midnight rollover and reject malformed HH:MM
SCHEDULE_BETWEEN_TIME_RANGE decided whether a range crossed midnight by comparing hours only, so a range like 23:30 -> 23:00 (equal hours) took the same-day branch and could never match. Compare minutes since midnight instead. TIME_RANGE_PATTERN was unanchored and used with re.match, so values such as "10:00abc" passed schema validation and then failed silently at evaluation time. Anchor the pattern so only exact HH:MM strings validate. Fixes #8423
1 parent af87f36 commit 832691b

4 files changed

Lines changed: 57 additions & 3 deletions

File tree

‎aws_lambda_powertools/utilities/feature_flags/comparators.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,15 @@ def compare_time_range(context_value: Any, condition_value: dict) -> bool:
5555
start_time = current_time.replace(hour=int(start_hour), minute=int(start_min))
5656
end_time = current_time.replace(hour=int(end_hour), minute=int(end_min))
5757

58-
if int(end_hour) < int(start_hour):
58+
# Compare full minutes-since-midnight so that ranges within the same hour (e.g. 23:30 -> 23:00)
59+
# are correctly detected as crossing midnight.
60+
start_minutes = int(start_hour) * 60 + int(start_min)
61+
end_minutes = int(end_hour) * 60 + int(end_min)
62+
63+
if end_minutes < start_minutes:
5964
# In normal circumstances, we need to assert **both** conditions
6065
"""
61-
# When the end hour is smaller than start hour, it means we are crossing a day's boundary.
66+
# When the end time is earlier than start time, it means we are crossing a day's boundary.
6267
# In this case we need to assert that current_time is **either** on one side or the other side of the boundary
6368
#
6469
# ┌─────┐ ┌─────┐ ┌─────┐

‎aws_lambda_powertools/utilities/feature_flags/constants.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@
99
CONDITION_ACTION = "action"
1010
FEATURE_DEFAULT_VAL_TYPE_KEY = "boolean_type"
1111
TIME_RANGE_FORMAT = "%H:%M" # hour:min 24 hours clock
12-
TIME_RANGE_PATTERN = re.compile(r"2[0-3]:[0-5]\d|[0-1]\d:[0-5]\d") # 24 hour clock
12+
TIME_RANGE_PATTERN = re.compile(r"^(?:2[0-3]|[01]\d):[0-5]\d$") # 24 hour clock, exactly HH:MM
1313
HOUR_MIN_SEPARATOR = ":"

‎tests/functional/feature_flags/_boto3/test_schema_validation.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,9 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva
523523
[
524524
{TimeValues.START.value: "11-11", TimeValues.END.value: "23:59"},
525525
{TimeValues.START.value: "24:99", TimeValues.END.value: "23:59"},
526+
{TimeValues.START.value: "10:00abc", TimeValues.END.value: "23:59"}, # trailing garbage
527+
{TimeValues.START.value: "abc10:00", TimeValues.END.value: "23:59"}, # leading garbage
528+
{TimeValues.START.value: "110:00", TimeValues.END.value: "23:59"}, # extra leading digit
526529
],
527530
)
528531
def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_start_time_value(cond_value):
@@ -549,6 +552,8 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva
549552
[
550553
{TimeValues.START.value: "10:11", TimeValues.END.value: "11-11"},
551554
{TimeValues.START.value: "10:11", TimeValues.END.value: "999:59"},
555+
{TimeValues.START.value: "10:11", TimeValues.END.value: "12:00abc"}, # trailing garbage
556+
{TimeValues.START.value: "10:11", TimeValues.END.value: "12:000"}, # extra trailing digit
552557
],
553558
)
554559
def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_end_time_value(cond_value):

‎tests/functional/feature_flags/_boto3/test_time_based_actions.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,50 @@ def test_time_based_utc_in_between_time_range_between_days_rule_no_match(mocker)
175175
)
176176

177177

178+
def test_time_based_utc_in_between_time_range_same_hour_between_days_rule_match(mocker):
179+
# GIVEN a range whose END is earlier than START but within the same hour (23:30 -> 23:00)
180+
# WHEN the current time is 10:00, which lies inside the range after crossing midnight
181+
# THEN the rule matches
182+
assert evaluate_mocked_schema(
183+
mocker=mocker,
184+
rules={
185+
"lambda time is between UTC 23:30-23:00": {
186+
RULE_MATCH_VALUE: True,
187+
CONDITIONS_KEY: [
188+
{
189+
CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value,
190+
CONDITION_KEY: TimeKeys.CURRENT_TIME.value,
191+
CONDITION_VALUE: {TimeValues.START.value: "23:30", TimeValues.END.value: "23:00"},
192+
},
193+
],
194+
},
195+
},
196+
mocked_time=(2022, 2, 15, 10, 0, 0, datetime.timezone.utc), # rule match 10:00 am
197+
)
198+
199+
200+
def test_time_based_utc_in_between_time_range_same_hour_between_days_rule_no_match(mocker):
201+
# GIVEN a range whose END is earlier than START but within the same hour (23:30 -> 23:00)
202+
# WHEN the current time is 23:15, the only 30-minute gap not covered by the range
203+
# THEN the rule does not match
204+
assert not evaluate_mocked_schema(
205+
mocker=mocker,
206+
rules={
207+
"lambda time is between UTC 23:30-23:00": {
208+
RULE_MATCH_VALUE: True,
209+
CONDITIONS_KEY: [
210+
{
211+
CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value,
212+
CONDITION_KEY: TimeKeys.CURRENT_TIME.value,
213+
CONDITION_VALUE: {TimeValues.START.value: "23:30", TimeValues.END.value: "23:00"},
214+
},
215+
],
216+
},
217+
},
218+
mocked_time=(2022, 2, 15, 23, 15, 0, datetime.timezone.utc), # rule no match 23:15
219+
)
220+
221+
178222
def test_time_based_between_time_range_rule_timezone_match(mocker):
179223
timezone_name = "Europe/Copenhagen"
180224

0 commit comments

Comments
 (0)