"""Unit tests for the shared HH:MM dispatch time-list parser/normalizer.""" from __future__ import annotations import pytest from notify_bridge_server.services.time_list import ( MAX_DISPATCH_TIMES, TimeListError, normalize_time_list, parse_hhmm_list, ) class TestNormalizeTimeList: def test_single_time_passthrough(self): assert normalize_time_list("09:00") == "09:00" def test_zero_pads_short_parts(self): assert normalize_time_list("9:0") == "09:00" def test_trims_surrounding_and_inner_whitespace(self): assert normalize_time_list(" 09:00 , 18:30 ") == "09:00,18:30" def test_sorts_ascending(self): assert normalize_time_list("18:30,09:00,12:15") == "09:00,12:15,18:30" def test_deduplicates(self): # Duplicates collapse even when written with different padding. assert normalize_time_list("09:00,9:00,09:00") == "09:00" def test_empty_string_returns_empty(self): assert normalize_time_list("") == "" def test_whitespace_only_returns_empty(self): assert normalize_time_list(" ") == "" def test_trailing_comma_ignored(self): assert normalize_time_list("09:00,") == "09:00" def test_midnight_and_last_minute_are_valid(self): assert normalize_time_list("00:00,23:59") == "00:00,23:59" @pytest.mark.parametrize( "bad", ["24:00", "09:60", "noon", "9", "09-00", "-1:00", "09:5a", "1:2:3"], ) def test_invalid_entry_raises(self, bad): with pytest.raises(TimeListError): normalize_time_list(bad) @pytest.mark.parametrize( "bad", [ "+9:00", # sign not allowed "1_0:00", # PEP 515 underscore not allowed "09:0_0", # underscore in minutes "٩:00", # Arabic-Indic digit (non-ASCII) "٠٩:٠٠", # Arabic-Indic "09:00" "09: 00", # inner whitespace in part "9 :00", # inner whitespace in part "009:00", # 3-digit hour part ], ) def test_rejects_non_ascii_or_oddly_shaped_digits(self, bad): # int() alone would accept these; the parser must not. with pytest.raises(TimeListError): normalize_time_list(bad) def test_one_bad_entry_rejects_whole_list(self): # Strict on writes: a single bad slot fails the request rather than # being silently dropped. with pytest.raises(TimeListError): normalize_time_list("09:00,bad,18:30") def test_at_cap_is_allowed(self): times = ",".join(f"{h:02d}:00" for h in range(MAX_DISPATCH_TIMES)) assert normalize_time_list(times) == times def test_over_cap_raises(self): # 25 distinct times (minute-granular) exceeds the default cap of 24. times = ",".join(f"00:{m:02d}" for m in range(MAX_DISPATCH_TIMES + 1)) with pytest.raises(TimeListError): normalize_time_list(times) def test_custom_max_count(self): with pytest.raises(TimeListError): normalize_time_list("09:00,10:00,11:00", max_count=2) def test_duplicates_do_not_count_against_cap(self): # Three entries but one distinct → fits under a cap of 1. assert normalize_time_list("09:00,09:00,9:00", max_count=1) == "09:00" class TestParseHhmmList: def test_basic(self): assert parse_hhmm_list("09:00,18:30") == [(9, 0), (18, 30)] def test_preserves_order_and_duplicates(self): # Lenient parser keeps input order and does not collapse duplicates — # the scheduler keys jobs by time so a dup just replaces its own id. assert parse_hhmm_list("18:30,09:00,18:30") == [(18, 30), (9, 0), (18, 30)] def test_skips_invalid_entries(self): assert parse_hhmm_list("09:00,bad,18:30") == [(9, 0), (18, 30)] def test_empty_returns_empty_list(self): assert parse_hhmm_list("") == [] assert parse_hhmm_list(" ") == [] def test_skips_out_of_range(self): assert parse_hhmm_list("24:00,09:00") == [(9, 0)] def test_skips_non_ascii_and_oddly_shaped(self): # Lenient parser drops the odd shapes but keeps the valid neighbour. assert parse_hhmm_list("+9:00,1_0:00,٠٩:٠٠,18:30") == [(18, 30)]