206 lines
7.5 KiB
Python
206 lines
7.5 KiB
Python
"""Tests for date utility functions."""
|
|
|
|
import datetime
|
|
|
|
import pytest
|
|
|
|
from app.utils.dates import add_months, age_in_months, fourth_birthday
|
|
|
|
|
|
class TestAddMonths:
|
|
"""Test add_months function."""
|
|
|
|
def test_add_positive_months(self) -> None:
|
|
"""Adding positive months increments the month correctly."""
|
|
date = datetime.date(2026, 1, 15)
|
|
result = add_months(date, 1)
|
|
assert result == datetime.date(2026, 2, 15)
|
|
|
|
def test_add_multiple_months(self) -> None:
|
|
"""Adding multiple months increments year when needed."""
|
|
date = datetime.date(2026, 11, 15)
|
|
result = add_months(date, 2)
|
|
assert result == datetime.date(2027, 1, 15)
|
|
|
|
def test_add_12_months(self) -> None:
|
|
"""Adding exactly 12 months increments year only."""
|
|
date = datetime.date(2026, 3, 15)
|
|
result = add_months(date, 12)
|
|
assert result == datetime.date(2027, 3, 15)
|
|
|
|
def test_add_many_months(self) -> None:
|
|
"""Adding many months works correctly."""
|
|
date = datetime.date(2026, 1, 15)
|
|
result = add_months(date, 48) # 4 years
|
|
assert result == datetime.date(2030, 1, 15)
|
|
|
|
def test_subtract_months(self) -> None:
|
|
"""Subtracting months (negative n) works correctly."""
|
|
date = datetime.date(2026, 3, 15)
|
|
result = add_months(date, -1)
|
|
assert result == datetime.date(2026, 2, 15)
|
|
|
|
def test_subtract_year(self) -> None:
|
|
"""Subtracting 12 months decrements year."""
|
|
date = datetime.date(2026, 3, 15)
|
|
result = add_months(date, -12)
|
|
assert result == datetime.date(2025, 3, 15)
|
|
|
|
def test_subtract_across_year_boundary(self) -> None:
|
|
"""Subtracting months crossing year boundary works."""
|
|
date = datetime.date(2026, 1, 15)
|
|
result = add_months(date, -2)
|
|
assert result == datetime.date(2025, 11, 15)
|
|
|
|
def test_month_end_handling_jan_to_feb_leap_year(self) -> None:
|
|
"""Jan 31 + 1 month = Feb 29 in leap year."""
|
|
date = datetime.date(2024, 1, 31) # 2024 is a leap year
|
|
result = add_months(date, 1)
|
|
assert result == datetime.date(2024, 2, 29)
|
|
|
|
def test_month_end_handling_jan_to_feb_non_leap_year(self) -> None:
|
|
"""Jan 31 + 1 month = Feb 28 in non-leap year."""
|
|
date = datetime.date(2025, 1, 31) # 2025 is not a leap year
|
|
result = add_months(date, 1)
|
|
assert result == datetime.date(2025, 2, 28)
|
|
|
|
def test_month_end_handling_mar_to_feb(self) -> None:
|
|
"""Mar 31 - 1 month = Feb 28/29."""
|
|
date = datetime.date(2025, 3, 31)
|
|
result = add_months(date, -1)
|
|
assert result == datetime.date(2025, 2, 28)
|
|
|
|
def test_month_end_handling_may_to_apr(self) -> None:
|
|
"""May 31 - 1 month = Apr 30."""
|
|
date = datetime.date(2026, 5, 31)
|
|
result = add_months(date, -1)
|
|
assert result == datetime.date(2026, 4, 30)
|
|
|
|
def test_add_zero_months(self) -> None:
|
|
"""Adding 0 months returns the same date."""
|
|
date = datetime.date(2026, 6, 15)
|
|
result = add_months(date, 0)
|
|
assert result == date
|
|
|
|
def test_leap_year_edge_case(self) -> None:
|
|
"""Feb 29 in leap year + 1 year = Feb 28 in non-leap year."""
|
|
date = datetime.date(2024, 2, 29)
|
|
result = add_months(date, 12)
|
|
assert result == datetime.date(2025, 2, 28)
|
|
|
|
def test_leap_year_century_2000_is_leap(self) -> None:
|
|
"""Year 2000 is a leap year (divisible by 400)."""
|
|
date = datetime.date(2000, 1, 31)
|
|
result = add_months(date, 1)
|
|
assert result == datetime.date(2000, 2, 29)
|
|
|
|
def test_leap_year_century_1900_not_leap(self) -> None:
|
|
"""Year 1900 is not a leap year (divisible by 100 but not 400)."""
|
|
date = datetime.date(1900, 1, 31)
|
|
result = add_months(date, 1)
|
|
assert result == datetime.date(1900, 2, 28)
|
|
|
|
|
|
class TestAgeInMonths:
|
|
"""Test age_in_months function."""
|
|
|
|
def test_same_day(self) -> None:
|
|
"""Same birth and reference date = 0 months."""
|
|
birth = datetime.date(2026, 1, 15)
|
|
on = datetime.date(2026, 1, 15)
|
|
assert age_in_months(birth, on) == 0
|
|
|
|
def test_one_month_later(self) -> None:
|
|
"""One month after birth date."""
|
|
birth = datetime.date(2026, 1, 15)
|
|
on = datetime.date(2026, 2, 15)
|
|
assert age_in_months(birth, on) == 1
|
|
|
|
def test_one_month_not_complete(self) -> None:
|
|
"""Day before one-month anniversary."""
|
|
birth = datetime.date(2026, 1, 15)
|
|
on = datetime.date(2026, 2, 14)
|
|
assert age_in_months(birth, on) == 0
|
|
|
|
def test_twelve_months(self) -> None:
|
|
"""Exactly one year."""
|
|
birth = datetime.date(2026, 1, 15)
|
|
on = datetime.date(2027, 1, 15)
|
|
assert age_in_months(birth, on) == 12
|
|
|
|
def test_48_months(self) -> None:
|
|
"""Exactly 4 years (48 months)."""
|
|
birth = datetime.date(2026, 1, 15)
|
|
on = datetime.date(2030, 1, 15)
|
|
assert age_in_months(birth, on) == 48
|
|
|
|
def test_month_boundary_not_reached(self) -> None:
|
|
"""Before the day of month anniversary."""
|
|
birth = datetime.date(2026, 1, 31)
|
|
on = datetime.date(2026, 2, 28)
|
|
assert age_in_months(birth, on) == 0
|
|
|
|
def test_month_boundary_reached(self) -> None:
|
|
"""On or after the day of month anniversary."""
|
|
birth = datetime.date(2026, 1, 31)
|
|
on = datetime.date(2026, 2, 28)
|
|
assert age_in_months(birth, on) == 0
|
|
|
|
def test_month_boundary_on_31st_to_mar(self) -> None:
|
|
"""Jan 31 to Mar 31."""
|
|
birth = datetime.date(2026, 1, 31)
|
|
on = datetime.date(2026, 3, 31)
|
|
assert age_in_months(birth, on) == 2
|
|
|
|
def test_negative_age(self) -> None:
|
|
"""Reference date before birth date (not yet born)."""
|
|
birth = datetime.date(2026, 6, 15)
|
|
on = datetime.date(2026, 1, 15)
|
|
assert age_in_months(birth, on) == -5
|
|
|
|
def test_leap_year_boundary(self) -> None:
|
|
"""Born on Feb 29 (leap year)."""
|
|
birth = datetime.date(2024, 2, 29)
|
|
on = datetime.date(2025, 2, 28)
|
|
assert age_in_months(birth, on) == 11
|
|
|
|
def test_leap_year_boundary_anniversary(self) -> None:
|
|
"""Born on Feb 29; checking on Mar 1 (anniversary passed)."""
|
|
birth = datetime.date(2024, 2, 29)
|
|
on = datetime.date(2025, 3, 1)
|
|
assert age_in_months(birth, on) == 12
|
|
|
|
|
|
class TestFourthBirthday:
|
|
"""Test fourth_birthday function."""
|
|
|
|
def test_regular_date(self) -> None:
|
|
"""Fourth birthday calculation."""
|
|
birth = datetime.date(2026, 6, 15)
|
|
result = fourth_birthday(birth)
|
|
assert result == datetime.date(2030, 6, 15)
|
|
|
|
def test_leap_year_feb_29(self) -> None:
|
|
"""Born on Feb 29; fourth birthday in non-leap year."""
|
|
birth = datetime.date(2024, 2, 29)
|
|
result = fourth_birthday(birth)
|
|
assert result == datetime.date(2028, 2, 29)
|
|
|
|
def test_jan_31(self) -> None:
|
|
"""Born on Jan 31; fourth birthday."""
|
|
birth = datetime.date(2022, 1, 31)
|
|
result = fourth_birthday(birth)
|
|
assert result == datetime.date(2026, 1, 31)
|
|
|
|
def test_dec_31(self) -> None:
|
|
"""Born on Dec 31; fourth birthday crosses year."""
|
|
birth = datetime.date(2022, 12, 31)
|
|
result = fourth_birthday(birth)
|
|
assert result == datetime.date(2026, 12, 31)
|
|
|
|
def test_fourth_birthday_is_48_months_later(self) -> None:
|
|
"""Verify fourth birthday is exactly 48 months after birth."""
|
|
birth = datetime.date(2020, 5, 10)
|
|
result = fourth_birthday(birth)
|
|
assert age_in_months(birth, result) == 48
|