diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 380e29c..28f5848 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -1,6 +1,6 @@ """Authentication router - login, register, token refresh.""" -from datetime import datetime, timedelta, timezone +from datetime import date, datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm @@ -77,9 +77,19 @@ def hash_password(password: str) -> str: return pwd_context.hash(password) -def calculate_age(birth_date: datetime) -> int: - """Calculate age in years from a birth date.""" - today = datetime.now(timezone.utc).date() +def calculate_age(birth_date: date, today: date | None = None) -> int: + """Calculate age in years from a birth date. + + Args: + birth_date: The person's date of birth. + today: The reference date. Defaults to today's date (UTC). + + Returns: + Age in whole years as of the reference date. + """ + if today is None: + today = datetime.now(timezone.utc).date() + age = today.year - birth_date.year # Adjust if birthday hasn't occurred yet this year if (today.month, today.day) < (birth_date.month, birth_date.day): diff --git a/backend/tests/test_auth_age_gate.py b/backend/tests/test_auth_age_gate.py index 389eaf5..73a9f0a 100644 --- a/backend/tests/test_auth_age_gate.py +++ b/backend/tests/test_auth_age_gate.py @@ -1,7 +1,6 @@ """Tests for the 18+ age gate on user registration.""" -from datetime import date, timedelta, datetime -from unittest.mock import patch +from datetime import date, timedelta import pytest @@ -13,52 +12,83 @@ class TestCalculateAge: def test_exactly_18(self): """A person born exactly 18 years ago today is 18.""" - today = datetime.now().date() + today = date(2025, 6, 15) dob = today.replace(year=today.year - 18) - assert calculate_age(dob) == 18 + assert calculate_age(dob, today=today) == 18 def test_just_turned_18(self): """A person who turned 18 yesterday is 18.""" - today = datetime.now().date() + today = date(2025, 6, 15) dob = today.replace(year=today.year - 18) - timedelta(days=1) - assert calculate_age(dob) == 18 + assert calculate_age(dob, today=today) == 18 def test_one_day_short_of_18(self): """A person born one day after their 18th birthday is 17.""" - today = datetime.now().date() + today = date(2025, 6, 15) dob = today.replace(year=today.year - 18) + timedelta(days=1) - assert calculate_age(dob) == 17 + assert calculate_age(dob, today=today) == 17 def test_17_years_old(self): - """A person who is 17 years and 364 days old is 17.""" - today = datetime.now().date() - dob = today.replace(year=today.year - 17) + timedelta(days=1) - assert calculate_age(dob) == 17 + """A person who is 17 years old today is 17.""" + today = date(2025, 6, 15) + dob = date(2007, 6, 15) + assert calculate_age(dob, today=today) == 17 def test_65_years_old(self): """A person who is 65 is correctly calculated.""" - today = datetime.now().date() + today = date(2025, 6, 15) dob = today.replace(year=today.year - 65) - assert calculate_age(dob) == 65 + assert calculate_age(dob, today=today) == 65 def test_newborn(self): """A newborn is 0.""" - today = datetime.now().date() + today = date(2025, 6, 15) dob = today - assert calculate_age(dob) == 0 + assert calculate_age(dob, today=today) == 0 def test_birthday_not_yet_this_year(self): """Person born Dec 31, 2000 — on Jan 1, 2025 they are still 24.""" dob = date(2000, 12, 31) - with patch("app.routers.auth.datetime") as mock_dt: - mock_dt.now.return_value = datetime(2025, 1, 1) - mock_dt.now.return_value.date.return_value = date(2025, 1, 1) - assert calculate_age(dob) == 24 + assert calculate_age(dob, today=date(2025, 1, 1)) == 24 def test_birthday_today(self): """Person born Dec 31, 2000 — on Dec 31, 2024 they turn 24.""" dob = date(2000, 12, 31) - with patch("app.routers.auth.datetime") as mock_dt: - mock_dt.now.return_value = datetime(2024, 12, 31) - mock_dt.now.return_value.date.return_value = date(2024, 12, 31) - assert calculate_age(dob) == 24 + assert calculate_age(dob, today=date(2024, 12, 31)) == 24 + + +class TestRegisterRequestDateValidation: + """Tests for the RegisterRequest date_of_birth field validator.""" + + @pytest.mark.parametrize( + "dob", + [ + "2000-01-01", + "1990-12-31", + "2004-02-29", # leap day + ], + ) + def test_valid_dates(self, dob): + """Valid YYYY-MM-DD dates pass validation.""" + from app.routers.auth import RegisterRequest + + req = RegisterRequest(username="u", email="u@example.com", password="p", date_of_birth=dob) + assert req.date_of_birth == dob + + @pytest.mark.parametrize( + "dob", + [ + "01-01-2000", + "2000/01/01", + "2000-13-01", + "2000-00-10", + "not-a-date", + "", + ], + ) + def test_invalid_dates_rejected(self, dob): + """Invalid dates are rejected by the validator.""" + from app.routers.auth import RegisterRequest + + with pytest.raises(ValueError): + RegisterRequest(username="u", email="u@example.com", password="p", date_of_birth=dob)