WIP checkpoint: Enforce 18+ age gate at account registration
This commit is contained in:
@@ -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.
|
||||||
today = datetime.now(timezone.utc).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
|
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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user