WIP checkpoint: Enforce 18+ age gate at account registration

This commit is contained in:
2026-08-18 15:32:16 +00:00
parent ef646a8e54
commit 62889f39f1
2 changed files with 68 additions and 28 deletions
+13 -3
View File
@@ -1,6 +1,6 @@
"""Authentication router - login, register, token refresh.""" """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 import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
@@ -77,9 +77,19 @@ def hash_password(password: str) -> str:
return pwd_context.hash(password) return pwd_context.hash(password)
def calculate_age(birth_date: datetime) -> int: def calculate_age(birth_date: date, today: date | None = None) -> int:
"""Calculate age in years from a birth date.""" """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() today = datetime.now(timezone.utc).date()
age = today.year - birth_date.year age = today.year - birth_date.year
# Adjust if birthday hasn't occurred yet this year # Adjust if birthday hasn't occurred yet this year
if (today.month, today.day) < (birth_date.month, birth_date.day): if (today.month, today.day) < (birth_date.month, birth_date.day):
+54 -24
View File
@@ -1,7 +1,6 @@
"""Tests for the 18+ age gate on user registration.""" """Tests for the 18+ age gate on user registration."""
from datetime import date, timedelta, datetime from datetime import date, timedelta
from unittest.mock import patch
import pytest import pytest
@@ -13,52 +12,83 @@ class TestCalculateAge:
def test_exactly_18(self): def test_exactly_18(self):
"""A person born exactly 18 years ago today is 18.""" """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) 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): def test_just_turned_18(self):
"""A person who turned 18 yesterday is 18.""" """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) 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): def test_one_day_short_of_18(self):
"""A person born one day after their 18th birthday is 17.""" """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) 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): def test_17_years_old(self):
"""A person who is 17 years and 364 days old is 17.""" """A person who is 17 years old today is 17."""
today = datetime.now().date() today = date(2025, 6, 15)
dob = today.replace(year=today.year - 17) + timedelta(days=1) dob = date(2007, 6, 15)
assert calculate_age(dob) == 17 assert calculate_age(dob, today=today) == 17
def test_65_years_old(self): def test_65_years_old(self):
"""A person who is 65 is correctly calculated.""" """A person who is 65 is correctly calculated."""
today = datetime.now().date() today = date(2025, 6, 15)
dob = today.replace(year=today.year - 65) dob = today.replace(year=today.year - 65)
assert calculate_age(dob) == 65 assert calculate_age(dob, today=today) == 65
def test_newborn(self): def test_newborn(self):
"""A newborn is 0.""" """A newborn is 0."""
today = datetime.now().date() today = date(2025, 6, 15)
dob = today dob = today
assert calculate_age(dob) == 0 assert calculate_age(dob, today=today) == 0
def test_birthday_not_yet_this_year(self): def test_birthday_not_yet_this_year(self):
"""Person born Dec 31, 2000 — on Jan 1, 2025 they are still 24.""" """Person born Dec 31, 2000 — on Jan 1, 2025 they are still 24."""
dob = date(2000, 12, 31) dob = date(2000, 12, 31)
with patch("app.routers.auth.datetime") as mock_dt: assert calculate_age(dob, today=date(2025, 1, 1)) == 24
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
def test_birthday_today(self): def test_birthday_today(self):
"""Person born Dec 31, 2000 — on Dec 31, 2024 they turn 24.""" """Person born Dec 31, 2000 — on Dec 31, 2024 they turn 24."""
dob = date(2000, 12, 31) dob = date(2000, 12, 31)
with patch("app.routers.auth.datetime") as mock_dt: assert calculate_age(dob, today=date(2024, 12, 31)) == 24
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 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)