Some checks failed
CI / lint-and-test (push) Failing after 27s
- Added a global exception handler to log unhandled exceptions and return a generic 500 JSON response without exposing details to the client. - Updated the configuration to validate the `DATABASE_URL` format, ensuring it starts with `sqlite://` or `postgresql://`, and log warnings for invalid formats. - Introduced safe parsing for numeric environment variables (`HTTP_PORT`, `INIT_DATA_MAX_AGE_SECONDS`) with defaults on invalid values, including logging warnings for out-of-range values. - Enhanced the duty schedule parser to enforce limits on the number of schedule rows and the length of full names and duty strings, raising appropriate errors when exceeded. - Updated internationalization messages to include generic error responses for import failures and parsing issues, improving user experience. - Added unit tests to verify the new error handling and configuration validation behaviors.
126 lines
3.7 KiB
Python
126 lines
3.7 KiB
Python
"""Unit tests for utils (dates, user, handover)."""
|
|
|
|
from datetime import date, timedelta
|
|
|
|
import pytest
|
|
|
|
from duty_teller.utils.dates import (
|
|
DateRangeValidationError,
|
|
day_start_iso,
|
|
day_end_iso,
|
|
duty_to_iso,
|
|
parse_iso_date,
|
|
validate_date_range,
|
|
)
|
|
from duty_teller.utils.user import build_full_name
|
|
from duty_teller.utils.handover import parse_handover_time
|
|
|
|
|
|
# --- dates ---
|
|
|
|
|
|
def test_day_start_iso():
|
|
assert day_start_iso(date(2026, 2, 18)) == "2026-02-18T00:00:00Z"
|
|
|
|
|
|
def test_day_end_iso():
|
|
assert day_end_iso(date(2026, 2, 18)) == "2026-02-18T23:59:59Z"
|
|
|
|
|
|
def test_duty_to_iso():
|
|
assert duty_to_iso(date(2026, 2, 18), 6, 0) == "2026-02-18T06:00:00Z"
|
|
|
|
|
|
def test_parse_iso_date_valid():
|
|
assert parse_iso_date("2026-02-18") == date(2026, 2, 18)
|
|
assert parse_iso_date(" 2026-02-18 ") == date(2026, 2, 18)
|
|
|
|
|
|
def test_parse_iso_date_invalid():
|
|
assert parse_iso_date("") is None
|
|
assert parse_iso_date("2026-02-31") is None # invalid day
|
|
assert parse_iso_date("18-02-2026") is None
|
|
assert parse_iso_date("not-a-date") is None
|
|
|
|
|
|
def test_validate_date_range_ok():
|
|
validate_date_range("2025-01-01", "2025-01-31") # no raise
|
|
|
|
|
|
def test_validate_date_range_bad_format():
|
|
with pytest.raises(DateRangeValidationError) as exc_info:
|
|
validate_date_range("01-01-2025", "2025-01-31")
|
|
assert exc_info.value.kind == "bad_format"
|
|
with pytest.raises(DateRangeValidationError) as exc_info:
|
|
validate_date_range("2025-01-01", "invalid")
|
|
assert exc_info.value.kind == "bad_format"
|
|
|
|
|
|
def test_validate_date_range_from_after_to():
|
|
with pytest.raises(DateRangeValidationError) as exc_info:
|
|
validate_date_range("2025-02-01", "2025-01-01")
|
|
assert exc_info.value.kind == "from_after_to"
|
|
|
|
|
|
def test_validate_date_range_too_large():
|
|
"""Range longer than MAX_DATE_RANGE_DAYS raises range_too_large."""
|
|
from duty_teller.utils.dates import MAX_DATE_RANGE_DAYS
|
|
|
|
# from 2023-01-01 to 2025-06-01 is more than 731 days
|
|
with pytest.raises(DateRangeValidationError) as exc_info:
|
|
validate_date_range("2023-01-01", "2025-06-01")
|
|
assert exc_info.value.kind == "range_too_large"
|
|
|
|
# Exactly MAX_DATE_RANGE_DAYS + 1 day
|
|
from_d = date(2024, 1, 1)
|
|
to_d = from_d + timedelta(days=MAX_DATE_RANGE_DAYS + 1)
|
|
with pytest.raises(DateRangeValidationError) as exc_info:
|
|
validate_date_range(from_d.isoformat(), to_d.isoformat())
|
|
assert exc_info.value.kind == "range_too_large"
|
|
|
|
|
|
# --- user ---
|
|
|
|
|
|
def test_build_full_name_both():
|
|
assert build_full_name("John", "Doe") == "John Doe"
|
|
|
|
|
|
def test_build_full_name_first_only():
|
|
assert build_full_name("John", None) == "John"
|
|
|
|
|
|
def test_build_full_name_last_only():
|
|
assert build_full_name(None, "Doe") == "Doe"
|
|
|
|
|
|
def test_build_full_name_empty():
|
|
assert build_full_name("", "") == "User"
|
|
assert build_full_name(None, None) == "User"
|
|
|
|
|
|
# --- handover ---
|
|
|
|
|
|
def test_parse_handover_utc():
|
|
assert parse_handover_time("09:00") == (9, 0)
|
|
assert parse_handover_time("09:00 UTC") == (9, 0)
|
|
assert parse_handover_time(" 06:30 ") == (6, 30)
|
|
|
|
|
|
def test_parse_handover_with_tz():
|
|
# Europe/Moscow UTC+3 in winter: 09:00 Moscow = 06:00 UTC
|
|
assert parse_handover_time("09:00 Europe/Moscow") == (6, 0)
|
|
|
|
|
|
def test_parse_handover_invalid():
|
|
assert parse_handover_time("") is None
|
|
assert parse_handover_time("not a time") is None
|
|
# 25:00 is normalized to 1:00 by hour % 24; use non-matching string
|
|
assert parse_handover_time("12") is None
|
|
|
|
|
|
def test_parse_handover_invalid_timezone_returns_none():
|
|
"""Invalid IANA timezone string -> ZoneInfo raises, returns None."""
|
|
assert parse_handover_time("09:00 NotAReal/Timezone") is None
|