PC3 Phase 3.2: real age verification (date of birth, not a checkbox)
POST /api/auth/register now requires a real date_of_birth (ISO YYYY-MM-DD). The server computes age (auth.calculate_age) and rejects under-18s with 403; a future/nonsensical date is 400. The DOB is stored and surfaced only to that user (/api/auth/me). Still self-attested (a user can enter a false DOB) -- but a concrete, stored, legally-meaningful data point and the seam for a future ID-check pass. Encryption-at-rest for the column and the Tier-2 hook are not done; the limitation is stated plainly in docs/architecture/content_moderation.md 5.2. - auth.py: RegisterRequest.date_of_birth (date); MIN_AGE=18; register() computes + enforces; UserResponse.date_of_birth typed `date | None` (the old `str | None` would have 500'd /me once a DOB was actually stored). - api_client.gd / main_menu.gd: the Over18 checkbox is now a "Date of birth (YYYY-MM-DD)" field with a client-side sanity check. - tests: test_auth_age_gate.py rewritten around the endpoint + age math (adult/minor/exactly-18/future-dob/stored); ~25 over_18 references across 6 backend test files + 6 game smoke tests updated. Backend 74 passed; game smokes green (auth 6/6, menu_full_flow 12/12, e2e 4/4, etc.); fresh-clone boot PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
+25
-15
@@ -21,6 +21,9 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
## Minimum age to register. This platform is adults-only.
|
||||||
|
MIN_AGE = 18
|
||||||
|
|
||||||
|
|
||||||
# --- Pydantic schemas ---
|
# --- Pydantic schemas ---
|
||||||
|
|
||||||
@@ -39,14 +42,19 @@ class RegisterRequest(BaseModel):
|
|||||||
username: str
|
username: str
|
||||||
email: EmailStr
|
email: EmailStr
|
||||||
password: str
|
password: str
|
||||||
over_18: bool # Simple boolean confirmation instead of date_of_birth
|
# Real date of birth -- the server computes age from it and rejects
|
||||||
|
# anyone under MIN_AGE. Still self-attested (no document check yet), but
|
||||||
|
# a concrete, stored, legally-meaningful data point rather than a
|
||||||
|
# throwaway checkbox, and the seam for a future hard ID-verification
|
||||||
|
# pass. See docs/architecture/content_moderation.md 5.2.
|
||||||
|
date_of_birth: date
|
||||||
|
|
||||||
|
|
||||||
class UserResponse(BaseModel):
|
class UserResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
username: str
|
username: str
|
||||||
email: str
|
email: str
|
||||||
date_of_birth: str | None = None
|
date_of_birth: date | None = None
|
||||||
is_active: bool
|
is_active: bool
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
|
|
||||||
@@ -108,15 +116,9 @@ def hash_password(password: str) -> str:
|
|||||||
def calculate_age(birth_date: date, today: date | None = None) -> 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.
|
||||||
|
|
||||||
Not called by /register anymore (that endpoint now just trusts the
|
Used by /register (PC3 Phase 3) to compute age from the submitted
|
||||||
client's over_18 boolean, see RegisterRequest) -- kept as a real,
|
date_of_birth and enforce the 18+ gate. See
|
||||||
tested utility since date-of-birth storage/verification is coming
|
docs/architecture/content_moderation.md 5.2.
|
||||||
back in a future pass (real age math, not just a client-side claim,
|
|
||||||
the user explicitly wants to revisit this design). Removing it broke
|
|
||||||
backend/tests/test_auth_age_gate.py's collection entirely (ImportError
|
|
||||||
on `from app.routers.auth import calculate_age`) for zero benefit --
|
|
||||||
restoring it is the lower-risk path over rewriting or deleting 17
|
|
||||||
tests for logic this codebase will need again.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
birth_date: The person's date of birth.
|
birth_date: The person's date of birth.
|
||||||
@@ -155,12 +157,20 @@ def _issue_refresh_token(user_id: int, db: AsyncSession) -> str:
|
|||||||
async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)):
|
async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)):
|
||||||
"""Register a new user and return access + refresh tokens.
|
"""Register a new user and return access + refresh tokens.
|
||||||
|
|
||||||
Requires over_18 boolean confirmation; rejects users who are not 18+.
|
Requires a date of birth; the server computes age and rejects anyone
|
||||||
|
under MIN_AGE (18). See docs/architecture/content_moderation.md for the
|
||||||
|
verification design and its self-attestation limitation.
|
||||||
"""
|
"""
|
||||||
if not req.over_18:
|
age = calculate_age(req.date_of_birth)
|
||||||
|
if age < 0 or req.date_of_birth > date.today():
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Must be 18 or older to register",
|
detail="Invalid date of birth",
|
||||||
|
)
|
||||||
|
if age < MIN_AGE:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail=f"You must be at least {MIN_AGE} to use this platform",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if username already exists
|
# Check if username already exists
|
||||||
@@ -183,7 +193,7 @@ async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)):
|
|||||||
username=req.username,
|
username=req.username,
|
||||||
email=req.email,
|
email=req.email,
|
||||||
hashed_password=hash_password(req.password),
|
hashed_password=hash_password(req.password),
|
||||||
date_of_birth=None, # No longer collecting DOB
|
date_of_birth=req.date_of_birth,
|
||||||
)
|
)
|
||||||
db.add(user)
|
db.add(user)
|
||||||
await db.flush() # flush to get user.id before creating refresh token
|
await db.flush() # flush to get user.id before creating refresh token
|
||||||
|
|||||||
@@ -3,92 +3,102 @@
|
|||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from app.routers.auth import calculate_age
|
from app.main import app
|
||||||
|
from app.routers.auth import calculate_age, MIN_AGE, RegisterRequest
|
||||||
|
|
||||||
|
|
||||||
class TestCalculateAge:
|
class TestCalculateAge:
|
||||||
"""Unit tests for the calculate_age helper."""
|
"""Unit tests for the calculate_age helper."""
|
||||||
|
|
||||||
def test_exactly_18(self):
|
def test_exactly_18(self):
|
||||||
"""A person born exactly 18 years ago today is 18."""
|
today = date(2026, 6, 15)
|
||||||
today = date(2025, 6, 15)
|
|
||||||
dob = today.replace(year=today.year - 18)
|
dob = today.replace(year=today.year - 18)
|
||||||
assert calculate_age(dob, today=today) == 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."""
|
today = date(2026, 6, 15)
|
||||||
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, today=today) == 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."""
|
today = date(2026, 6, 15)
|
||||||
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, today=today) == 17
|
assert calculate_age(dob, today=today) == 17
|
||||||
|
|
||||||
def test_17_years_old(self):
|
def test_leap_day_birthday_before(self):
|
||||||
"""A person who is 17 years old today is 17."""
|
assert calculate_age(date(2000, 2, 29), today=date(2024, 2, 28)) == 23
|
||||||
today = date(2025, 6, 15)
|
|
||||||
dob = date(2008, 6, 15)
|
|
||||||
assert calculate_age(dob, today=today) == 17
|
|
||||||
|
|
||||||
def test_65_years_old(self):
|
def test_leap_day_birthday_after(self):
|
||||||
"""A person who is 65 is correctly calculated."""
|
assert calculate_age(date(2000, 2, 29), today=date(2024, 3, 1)) == 24
|
||||||
today = date(2025, 6, 15)
|
|
||||||
dob = today.replace(year=today.year - 65)
|
|
||||||
assert calculate_age(dob, today=today) == 65
|
|
||||||
|
|
||||||
def test_newborn(self):
|
|
||||||
"""A newborn is 0."""
|
|
||||||
today = date(2025, 6, 15)
|
|
||||||
dob = today
|
|
||||||
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)
|
|
||||||
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)
|
|
||||||
assert calculate_age(dob, today=date(2024, 12, 31)) == 24
|
|
||||||
|
|
||||||
|
|
||||||
class TestRegisterRequestOver18Field:
|
class TestRegisterRequestDob:
|
||||||
"""Tests for RegisterRequest's over_18 field.
|
def test_dob_parsed_from_iso_string(self):
|
||||||
|
req = RegisterRequest(username="u", email="u@example.com", password="p",
|
||||||
|
date_of_birth="1990-01-01")
|
||||||
|
assert req.date_of_birth == date(1990, 1, 1)
|
||||||
|
|
||||||
Replaces TestRegisterRequestDateValidation -- date_of_birth and its
|
def test_dob_required(self):
|
||||||
field_validator were removed from RegisterRequest 2026-08-20 (interim
|
with pytest.raises(Exception):
|
||||||
simplification: a plain client-side over_18 confirmation instead of
|
|
||||||
collecting/validating an actual date of birth; see calculate_age's
|
|
||||||
own docstring in app/routers/auth.py for why that function is still
|
|
||||||
here even though this endpoint no longer calls it). There's no date
|
|
||||||
format left to validate, so the equivalent coverage is just: the
|
|
||||||
field exists, is a real bool, and is required."""
|
|
||||||
|
|
||||||
def test_over_18_true_accepted(self):
|
|
||||||
from app.routers.auth import RegisterRequest
|
|
||||||
|
|
||||||
req = RegisterRequest(username="u", email="u@example.com", password="p", over_18=True)
|
|
||||||
assert req.over_18 is True
|
|
||||||
|
|
||||||
def test_over_18_false_accepted(self):
|
|
||||||
"""False is a valid value at the schema level -- the endpoint,
|
|
||||||
not the schema, is what rejects it (see
|
|
||||||
TestRegister::test_register_rejects_not_over_18 in
|
|
||||||
test_auth_endpoints.py)."""
|
|
||||||
from app.routers.auth import RegisterRequest
|
|
||||||
|
|
||||||
req = RegisterRequest(username="u", email="u@example.com", password="p", over_18=False)
|
|
||||||
assert req.over_18 is False
|
|
||||||
|
|
||||||
def test_over_18_missing_rejected(self):
|
|
||||||
"""over_18 has no default -- omitting it entirely is a validation error."""
|
|
||||||
from app.routers.auth import RegisterRequest
|
|
||||||
from pydantic import ValidationError
|
|
||||||
|
|
||||||
with pytest.raises(ValidationError):
|
|
||||||
RegisterRequest(username="u", email="u@example.com", password="p")
|
RegisterRequest(username="u", email="u@example.com", password="p")
|
||||||
|
|
||||||
|
def test_bad_dob_string_rejected(self):
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
RegisterRequest(username="u", email="u@example.com", password="p",
|
||||||
|
date_of_birth="not-a-date")
|
||||||
|
|
||||||
|
|
||||||
|
class TestRegisterAgeGate:
|
||||||
|
"""End-to-end: the /register endpoint computes age from the DOB."""
|
||||||
|
|
||||||
|
def _dob_for_age(self, years: int) -> str:
|
||||||
|
return (date.today().replace(year=date.today().year - years)).isoformat()
|
||||||
|
|
||||||
|
def test_adult_accepted(self):
|
||||||
|
client = TestClient(app)
|
||||||
|
r = client.post("/api/auth/register", json={
|
||||||
|
"username": "gate_adult", "email": "gate_adult@e.com",
|
||||||
|
"password": "secret123", "date_of_birth": self._dob_for_age(25),
|
||||||
|
})
|
||||||
|
assert r.status_code == 201, r.text
|
||||||
|
|
||||||
|
def test_minor_rejected(self):
|
||||||
|
client = TestClient(app)
|
||||||
|
r = client.post("/api/auth/register", json={
|
||||||
|
"username": "gate_minor", "email": "gate_minor@e.com",
|
||||||
|
"password": "secret123", "date_of_birth": self._dob_for_age(15),
|
||||||
|
})
|
||||||
|
assert r.status_code == 403
|
||||||
|
assert str(MIN_AGE) in r.json()["detail"]
|
||||||
|
|
||||||
|
def test_exactly_18_accepted(self):
|
||||||
|
client = TestClient(app)
|
||||||
|
r = client.post("/api/auth/register", json={
|
||||||
|
"username": "gate_18", "email": "gate_18@e.com",
|
||||||
|
"password": "secret123",
|
||||||
|
"date_of_birth": (date.today().replace(year=date.today().year - 18) - timedelta(days=1)).isoformat(),
|
||||||
|
})
|
||||||
|
assert r.status_code == 201, r.text
|
||||||
|
|
||||||
|
def test_future_dob_rejected(self):
|
||||||
|
client = TestClient(app)
|
||||||
|
r = client.post("/api/auth/register", json={
|
||||||
|
"username": "gate_future", "email": "gate_future@e.com",
|
||||||
|
"password": "secret123", "date_of_birth": "2999-01-01",
|
||||||
|
})
|
||||||
|
assert r.status_code == 400
|
||||||
|
|
||||||
|
def test_dob_is_stored(self):
|
||||||
|
client = TestClient(app)
|
||||||
|
dob = self._dob_for_age(30)
|
||||||
|
reg = client.post("/api/auth/register", json={
|
||||||
|
"username": "gate_stored", "email": "gate_stored@e.com",
|
||||||
|
"password": "secret123", "date_of_birth": dob,
|
||||||
|
})
|
||||||
|
assert reg.status_code == 201
|
||||||
|
me = client.get("/api/auth/me", headers={
|
||||||
|
"Authorization": f"Bearer {reg.json()['access_token']}"
|
||||||
|
})
|
||||||
|
assert me.json()["date_of_birth"] == dob
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class TestRegister:
|
|||||||
"username": "newuser",
|
"username": "newuser",
|
||||||
"email": "new@example.com",
|
"email": "new@example.com",
|
||||||
"password": "secret123",
|
"password": "secret123",
|
||||||
"over_18": True,
|
"date_of_birth": "1990-01-01",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 201
|
assert resp.status_code == 201
|
||||||
@@ -81,26 +81,23 @@ class TestRegister:
|
|||||||
assert "access_token" in data
|
assert "access_token" in data
|
||||||
assert data["token_type"] == "bearer"
|
assert data["token_type"] == "bearer"
|
||||||
|
|
||||||
def test_register_rejects_not_over_18(self):
|
def test_register_rejects_under_18(self):
|
||||||
"""over_18=false gets a 400 with a detail message.
|
"""A DOB that computes to under 18 gets a 403 (see test_auth_age_gate
|
||||||
|
for the age-math coverage)."""
|
||||||
The interim design (2026-08-20) trusts a client-side boolean
|
from datetime import date
|
||||||
confirmation instead of collecting/validating an actual date of
|
|
||||||
birth -- this only checks that the flag is respected, not real
|
|
||||||
age math (there's no date to verify against anymore). Real DOB
|
|
||||||
collection/verification may come back in a future pass."""
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
minor_dob = date.today().replace(year=date.today().year - 14).isoformat()
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
"/api/auth/register",
|
"/api/auth/register",
|
||||||
json={
|
json={
|
||||||
"username": "kid",
|
"username": "kid",
|
||||||
"email": "kid@example.com",
|
"email": "kid@example.com",
|
||||||
"password": "secret123",
|
"password": "secret123",
|
||||||
"over_18": False,
|
"date_of_birth": minor_dob,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 400
|
assert resp.status_code == 403
|
||||||
assert "18 or older" in resp.json()["detail"]
|
assert "at least 18" in resp.json()["detail"]
|
||||||
|
|
||||||
def test_register_rejects_duplicate_username(self):
|
def test_register_rejects_duplicate_username(self):
|
||||||
"""Registering the same username twice returns 409."""
|
"""Registering the same username twice returns 409."""
|
||||||
@@ -112,7 +109,7 @@ class TestRegister:
|
|||||||
"username": "duplicate",
|
"username": "duplicate",
|
||||||
"email": "dup1@example.com",
|
"email": "dup1@example.com",
|
||||||
"password": "secret123",
|
"password": "secret123",
|
||||||
"over_18": True,
|
"date_of_birth": "1990-01-01",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
# Second registration with same username, different email
|
# Second registration with same username, different email
|
||||||
@@ -122,7 +119,7 @@ class TestRegister:
|
|||||||
"username": "duplicate",
|
"username": "duplicate",
|
||||||
"email": "dup2@example.com",
|
"email": "dup2@example.com",
|
||||||
"password": "secret123",
|
"password": "secret123",
|
||||||
"over_18": True,
|
"date_of_birth": "1990-01-01",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 409
|
assert resp.status_code == 409
|
||||||
@@ -137,7 +134,7 @@ class TestRegister:
|
|||||||
"username": "user1",
|
"username": "user1",
|
||||||
"email": "same@example.com",
|
"email": "same@example.com",
|
||||||
"password": "secret123",
|
"password": "secret123",
|
||||||
"over_18": True,
|
"date_of_birth": "1990-01-01",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
@@ -146,19 +143,14 @@ class TestRegister:
|
|||||||
"username": "user2",
|
"username": "user2",
|
||||||
"email": "same@example.com",
|
"email": "same@example.com",
|
||||||
"password": "secret123",
|
"password": "secret123",
|
||||||
"over_18": True,
|
"date_of_birth": "1990-01-01",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 409
|
assert resp.status_code == 409
|
||||||
assert "Email already registered" in resp.json()["detail"]
|
assert "Email already registered" in resp.json()["detail"]
|
||||||
|
|
||||||
def test_register_missing_over_18(self):
|
def test_register_missing_dob(self):
|
||||||
"""Missing the required over_18 field returns 422.
|
"""Missing the required date_of_birth field returns 422."""
|
||||||
|
|
||||||
Replaces the old test_register_invalid_date_format -- there's no
|
|
||||||
date format to get wrong anymore (no date field at all), so the
|
|
||||||
closest equivalent "malformed request" case is the required
|
|
||||||
over_18 boolean being absent entirely."""
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
"/api/auth/register",
|
"/api/auth/register",
|
||||||
@@ -189,7 +181,7 @@ class TestLogin:
|
|||||||
"username": username,
|
"username": username,
|
||||||
"email": email,
|
"email": email,
|
||||||
"password": password,
|
"password": password,
|
||||||
"over_18": True,
|
"date_of_birth": "1990-01-01",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 201
|
assert resp.status_code == 201
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ def _register_user(client: TestClient, username: str) -> str:
|
|||||||
"username": username,
|
"username": username,
|
||||||
"email": f"{username}@example.com",
|
"email": f"{username}@example.com",
|
||||||
"password": "secret123",
|
"password": "secret123",
|
||||||
"over_18": True,
|
"date_of_birth": "1990-01-01",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 201
|
assert resp.status_code == 201
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ def _auth_headers(client: TestClient, username: str) -> dict:
|
|||||||
"username": username,
|
"username": username,
|
||||||
"email": f"{username}@example.com",
|
"email": f"{username}@example.com",
|
||||||
"password": "secret123",
|
"password": "secret123",
|
||||||
"over_18": True,
|
"date_of_birth": "1990-01-01",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 201, resp.text
|
assert resp.status_code == 201, resp.text
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class TestRefresh:
|
|||||||
"username": username,
|
"username": username,
|
||||||
"email": email,
|
"email": email,
|
||||||
"password": password,
|
"password": password,
|
||||||
"over_18": True,
|
"date_of_birth": "1990-01-01",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 201
|
assert resp.status_code == 201
|
||||||
@@ -143,7 +143,7 @@ class TestRefresh:
|
|||||||
"username": "refep_loginrefuser",
|
"username": "refep_loginrefuser",
|
||||||
"email": "loginref@example.com",
|
"email": "loginref@example.com",
|
||||||
"password": "secret123",
|
"password": "secret123",
|
||||||
"over_18": True,
|
"date_of_birth": "1990-01-01",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
# Login to get a refresh token
|
# Login to get a refresh token
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from app.main import app
|
|||||||
|
|
||||||
def _auth(client: TestClient, name: str) -> tuple[dict, int]:
|
def _auth(client: TestClient, name: str) -> tuple[dict, int]:
|
||||||
r = client.post("/api/auth/register", json={
|
r = client.post("/api/auth/register", json={
|
||||||
"username": name, "email": f"{name}@e.com", "password": "secret123", "over_18": True,
|
"username": name, "email": f"{name}@e.com", "password": "secret123", "date_of_birth": "1990-01-01",
|
||||||
})
|
})
|
||||||
assert r.status_code == 201, r.text
|
assert r.status_code == 201, r.text
|
||||||
headers = {"Authorization": f"Bearer {r.json()['access_token']}"}
|
headers = {"Authorization": f"Bearer {r.json()['access_token']}"}
|
||||||
|
|||||||
@@ -227,18 +227,33 @@ An account has an **age verification status**:
|
|||||||
|
|
||||||
### 5.2 Tier 1 — Self-Declaration Flow
|
### 5.2 Tier 1 — Self-Declaration Flow
|
||||||
|
|
||||||
|
> **Implemented, PC3 Phase 3.** `POST /api/auth/register` now takes a real
|
||||||
|
> `date_of_birth` (ISO `YYYY-MM-DD`), not the throwaway `over_18` boolean it
|
||||||
|
> had before. The server computes age (`auth.calculate_age`) and rejects
|
||||||
|
> anyone under `auth.MIN_AGE` (18) with a **403** — this platform is
|
||||||
|
> adults-only, so there is no "minor, restricted" tier here (the 13+ rows
|
||||||
|
> below are the general design, not the current gate). A future-in-the-past
|
||||||
|
> or nonsensical date is a **400**. The DOB is stored on the user row and
|
||||||
|
> surfaced only back to that user (`/api/auth/me`), never to other players.
|
||||||
|
>
|
||||||
|
> **Limitation, stated plainly:** this is still self-attested — a determined
|
||||||
|
> user can enter a false DOB. It is a concrete, stored, legally-meaningful
|
||||||
|
> data point (matching how most platforms gate before adding hard checks)
|
||||||
|
> and the seam for Tier 2. Encryption-at-rest for the DOB column and the
|
||||||
|
> Tier 2 ID-check hook below are **not yet implemented**.
|
||||||
|
|
||||||
```
|
```
|
||||||
User submits { date_of_birth }
|
User submits { date_of_birth }
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Server validates format & plausibility (age 13–120, not in future)
|
Server validates format & plausibility (real date, not in future)
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Compute age from DOB
|
Compute age from DOB
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
age >= 18 ──► status = self_declared (adult)
|
age >= 18 ──► status = self_declared (adult) ← the current gate
|
||||||
age >= 13 ──► status = self_declared (minor, restricted)
|
age >= 13 ──► status = self_declared (minor, restricted) ← design only
|
||||||
age < 13 ──► status = rejected (below minimum platform age)
|
age < 13 ──► status = rejected (below minimum platform age)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -83,11 +83,11 @@ visible = false
|
|||||||
placeholder_text = "Email"
|
placeholder_text = "Email"
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
[node name="Over18Check" type="CheckBox" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
|
[node name="DobEdit" type="LineEdit" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
visible = false
|
visible = false
|
||||||
text = "I am 18 or older"
|
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
|
placeholder_text = "Date of birth (YYYY-MM-DD)"
|
||||||
|
|
||||||
[node name="AuthButton" type="Button" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
|
[node name="AuthButton" type="Button" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
|
|||||||
@@ -178,18 +178,19 @@ func login(username: String, password: String) -> Dictionary:
|
|||||||
|
|
||||||
|
|
||||||
## Register a new user. On success stores the returned access token.
|
## Register a new user. On success stores the returned access token.
|
||||||
## @param over_18 Must be true; the backend rejects registration otherwise.
|
## @param date_of_birth ISO "YYYY-MM-DD". The backend computes age from it
|
||||||
|
## and rejects under-18s (403).
|
||||||
func register(
|
func register(
|
||||||
username: String,
|
username: String,
|
||||||
email: String,
|
email: String,
|
||||||
password: String,
|
password: String,
|
||||||
over_18: bool
|
date_of_birth: String
|
||||||
) -> Dictionary:
|
) -> Dictionary:
|
||||||
var payload := {
|
var payload := {
|
||||||
"username": username,
|
"username": username,
|
||||||
"email": email,
|
"email": email,
|
||||||
"password": password,
|
"password": password,
|
||||||
"over_18": over_18,
|
"date_of_birth": date_of_birth,
|
||||||
}
|
}
|
||||||
var result := await request(
|
var result := await request(
|
||||||
HTTPClient.METHOD_POST,
|
HTTPClient.METHOD_POST,
|
||||||
|
|||||||
+25
-11
@@ -35,7 +35,7 @@ var _auth_mode_label: Label
|
|||||||
var _username_edit: LineEdit
|
var _username_edit: LineEdit
|
||||||
var _password_edit: LineEdit
|
var _password_edit: LineEdit
|
||||||
var _email_edit: LineEdit
|
var _email_edit: LineEdit
|
||||||
var _over_18_check: CheckBox
|
var _dob_edit: LineEdit
|
||||||
var _auth_button: Button
|
var _auth_button: Button
|
||||||
var _status_label: Label
|
var _status_label: Label
|
||||||
var _welcome_label: Label
|
var _welcome_label: Label
|
||||||
@@ -59,7 +59,7 @@ func _ready() -> void:
|
|||||||
_username_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit
|
_username_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit
|
||||||
_password_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit
|
_password_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit
|
||||||
_email_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit
|
_email_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit
|
||||||
_over_18_check = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/Over18Check
|
_dob_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/DobEdit
|
||||||
_auth_button = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton
|
_auth_button = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton
|
||||||
_status_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthStatusLabel
|
_status_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthStatusLabel
|
||||||
_welcome_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel
|
_welcome_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel
|
||||||
@@ -115,7 +115,7 @@ func _show_authenticated(username: String) -> void:
|
|||||||
_username_edit.visible = false
|
_username_edit.visible = false
|
||||||
_password_edit.visible = false
|
_password_edit.visible = false
|
||||||
_email_edit.visible = false
|
_email_edit.visible = false
|
||||||
_over_18_check.visible = false
|
_dob_edit.visible = false
|
||||||
_auth_button.visible = false
|
_auth_button.visible = false
|
||||||
_auth_mode_label.visible = false
|
_auth_mode_label.visible = false
|
||||||
_play_button.disabled = false
|
_play_button.disabled = false
|
||||||
@@ -131,8 +131,8 @@ func _reset_to_logged_out() -> void:
|
|||||||
_status_label.visible = false
|
_status_label.visible = false
|
||||||
_welcome_label.visible = false
|
_welcome_label.visible = false
|
||||||
_email_edit.visible = false
|
_email_edit.visible = false
|
||||||
_over_18_check.visible = false
|
_dob_edit.visible = false
|
||||||
_over_18_check.button_pressed = false
|
_dob_edit.text = ""
|
||||||
_play_button.disabled = true # Gate play behind successful auth.
|
_play_button.disabled = true # Gate play behind successful auth.
|
||||||
_customize_button.disabled = true
|
_customize_button.disabled = true
|
||||||
_update_auth_mode_ui()
|
_update_auth_mode_ui()
|
||||||
@@ -158,6 +158,19 @@ func _on_quit_button_pressed() -> void:
|
|||||||
get_tree().quit()
|
get_tree().quit()
|
||||||
|
|
||||||
|
|
||||||
|
## Loose client-side check -- YYYY-MM-DD, a real calendar date, not in the
|
||||||
|
## future. The backend does the authoritative age computation.
|
||||||
|
func _is_valid_dob(dob: String) -> bool:
|
||||||
|
var m := RegEx.new()
|
||||||
|
m.compile("^\\d{4}-\\d{2}-\\d{2}$")
|
||||||
|
if m.search(dob) == null:
|
||||||
|
return false
|
||||||
|
var parts := dob.split("-")
|
||||||
|
var d := {"year": int(parts[0]), "month": int(parts[1]), "day": int(parts[2]), "hour": 0, "minute": 0, "second": 0}
|
||||||
|
var unix := Time.get_unix_time_from_datetime_dict(d)
|
||||||
|
return unix > 0 and unix < Time.get_unix_time_from_system()
|
||||||
|
|
||||||
|
|
||||||
# ─── Auth panel toggle ────────────────────────────────────────────────────────
|
# ─── Auth panel toggle ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func _on_login_button_pressed() -> void:
|
func _on_login_button_pressed() -> void:
|
||||||
@@ -184,12 +197,12 @@ func _update_auth_mode_ui() -> void:
|
|||||||
_auth_mode_label.text = "Register"
|
_auth_mode_label.text = "Register"
|
||||||
_auth_button.text = "Register"
|
_auth_button.text = "Register"
|
||||||
_email_edit.visible = true
|
_email_edit.visible = true
|
||||||
_over_18_check.visible = true
|
_dob_edit.visible = true
|
||||||
else:
|
else:
|
||||||
_auth_mode_label.text = "Login"
|
_auth_mode_label.text = "Login"
|
||||||
_auth_button.text = "Login"
|
_auth_button.text = "Login"
|
||||||
_email_edit.visible = false
|
_email_edit.visible = false
|
||||||
_over_18_check.visible = false
|
_dob_edit.visible = false
|
||||||
|
|
||||||
|
|
||||||
# ─── Auth submission ──────────────────────────────────────────────────────────
|
# ─── Auth submission ──────────────────────────────────────────────────────────
|
||||||
@@ -216,8 +229,9 @@ func _on_auth_button_pressed() -> void:
|
|||||||
if email == "":
|
if email == "":
|
||||||
_show_status("Email is required.")
|
_show_status("Email is required.")
|
||||||
return
|
return
|
||||||
if not _over_18_check.button_pressed:
|
var dob := _dob_edit.text.strip_edges()
|
||||||
_show_status("You must confirm you are 18 or older to register.")
|
if not _is_valid_dob(dob):
|
||||||
|
_show_status("Enter your date of birth as YYYY-MM-DD.")
|
||||||
return
|
return
|
||||||
|
|
||||||
_auth_button.disabled = true
|
_auth_button.disabled = true
|
||||||
@@ -225,7 +239,7 @@ func _on_auth_button_pressed() -> void:
|
|||||||
_is_authenticating = true
|
_is_authenticating = true
|
||||||
_clear_status()
|
_clear_status()
|
||||||
|
|
||||||
var result := await ApiClient.register(username, email, password, true)
|
var result := await ApiClient.register(username, email, password, dob)
|
||||||
await _on_auth_complete(result)
|
await _on_auth_complete(result)
|
||||||
else:
|
else:
|
||||||
_auth_button.disabled = true
|
_auth_button.disabled = true
|
||||||
@@ -251,7 +265,7 @@ func _on_auth_complete(result: Dictionary) -> void:
|
|||||||
_username_edit.visible = false
|
_username_edit.visible = false
|
||||||
_password_edit.visible = false
|
_password_edit.visible = false
|
||||||
_email_edit.visible = false
|
_email_edit.visible = false
|
||||||
_over_18_check.visible = false
|
_dob_edit.visible = false
|
||||||
_auth_button.visible = false
|
_auth_button.visible = false
|
||||||
_auth_mode_label.visible = false
|
_auth_mode_label.visible = false
|
||||||
_status_label.visible = false
|
_status_label.visible = false
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func _run() -> void:
|
|||||||
var session := root.get_node("Session")
|
var session := root.get_node("Session")
|
||||||
|
|
||||||
var uname := "mpflow_%s_%d" % [role, int(Time.get_unix_time_from_system() * 1000) % 1000000]
|
var uname := "mpflow_%s_%d" % [role, int(Time.get_unix_time_from_system() * 1000) % 1000000]
|
||||||
var reg: Dictionary = await api.register(uname, uname + "@e.com", "secret123", true)
|
var reg: Dictionary = await api.register(uname, uname + "@e.com", "secret123", "1990-01-01")
|
||||||
if not reg.ok:
|
if not reg.ok:
|
||||||
print("RESULT %s register_failed %s" % [role, reg.detail])
|
print("RESULT %s register_failed %s" % [role, reg.detail])
|
||||||
quit(1); return
|
quit(1); return
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ extends SceneTree
|
|||||||
## Tests:
|
## Tests:
|
||||||
## 1. Register a unique user → 201 + token stored.
|
## 1. Register a unique user → 201 + token stored.
|
||||||
## 2. Register the same user again → 409 "Username already registered".
|
## 2. Register the same user again → 409 "Username already registered".
|
||||||
## 3. Register with over_18=false → 400 "Must be 18 or older".
|
## 3. Register with an under-18 DOB → 403 "at least 18".
|
||||||
## 4. Login with correct credentials → 200 + token stored.
|
## 4. Login with correct credentials → 200 + token stored.
|
||||||
## 5. Login with wrong password → 401 "Incorrect username or password".
|
## 5. Login with wrong password → 401 "Incorrect username or password".
|
||||||
## 6. Login with non-existent user → 401 "Incorrect username or password".
|
## 6. Login with non-existent user → 401 "Incorrect username or password".
|
||||||
@@ -44,7 +44,7 @@ func _run() -> void:
|
|||||||
var email := "smoke_%d@test.example.com" % unique_id
|
var email := "smoke_%d@test.example.com" % unique_id
|
||||||
|
|
||||||
print("\n=== Test 1: Register unique user ===")
|
print("\n=== Test 1: Register unique user ===")
|
||||||
var result := await api.register(username, email, TEST_PASSWORD, true)
|
var result := await api.register(username, email, TEST_PASSWORD, "1990-01-01")
|
||||||
if _check("register returns ok", result.ok) \
|
if _check("register returns ok", result.ok) \
|
||||||
and _check("register status is 201", result.status == 201) \
|
and _check("register status is 201", result.status == 201) \
|
||||||
and _check("register stores token", api.has_token()):
|
and _check("register stores token", api.has_token()):
|
||||||
@@ -55,7 +55,7 @@ func _run() -> void:
|
|||||||
|
|
||||||
# --- Test 2: Register same user again (duplicate) ---
|
# --- Test 2: Register same user again (duplicate) ---
|
||||||
print("\n=== Test 2: Duplicate username ===")
|
print("\n=== Test 2: Duplicate username ===")
|
||||||
result = await api.register(username, email, TEST_PASSWORD, true)
|
result = await api.register(username, email, TEST_PASSWORD, "1990-01-01")
|
||||||
if _check("duplicate register not ok", not result.ok) \
|
if _check("duplicate register not ok", not result.ok) \
|
||||||
and _check("duplicate status is 409", result.status == 409) \
|
and _check("duplicate status is 409", result.status == 409) \
|
||||||
and _check("duplicate detail mentions username",
|
and _check("duplicate detail mentions username",
|
||||||
@@ -65,14 +65,16 @@ func _run() -> void:
|
|||||||
failed += 1
|
failed += 1
|
||||||
print(" detail: ", result.detail)
|
print(" detail: ", result.detail)
|
||||||
|
|
||||||
# --- Test 3: Register with over_18=false ---
|
# --- Test 3: Register with an under-18 date of birth ---
|
||||||
print("\n=== Test 3: over_18=false registration ===")
|
print("\n=== Test 3: under-18 registration ===")
|
||||||
var child_username := "child_%d" % unique_id
|
var child_username := "child_%d" % unique_id
|
||||||
var child_email := "child_%d@test.example.com" % unique_id
|
var child_email := "child_%d@test.example.com" % unique_id
|
||||||
result = await api.register(child_username, child_email, TEST_PASSWORD, false)
|
var now := Time.get_datetime_dict_from_system()
|
||||||
if _check("over_18=false register not ok", not result.ok) \
|
var minor_dob := "%04d-01-01" % (int(now["year"]) - 14)
|
||||||
and _check("over_18=false status is 400", result.status == 400) \
|
result = await api.register(child_username, child_email, TEST_PASSWORD, minor_dob)
|
||||||
and _check("over_18=false detail mentions age",
|
if _check("under-18 register not ok", not result.ok) \
|
||||||
|
and _check("under-18 status is 403", result.status == 403) \
|
||||||
|
and _check("under-18 detail mentions age",
|
||||||
"18" in result.detail):
|
"18" in result.detail):
|
||||||
passed += 1
|
passed += 1
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ func _run() -> void:
|
|||||||
|
|
||||||
# --- Step 2: Register -----------------------------------------------------
|
# --- Step 2: Register -----------------------------------------------------
|
||||||
print("\n=== Step 2: Register (POST /api/auth/register) ===")
|
print("\n=== Step 2: Register (POST /api/auth/register) ===")
|
||||||
var reg: Dictionary = await api.register(username, email, TEST_PASSWORD, true)
|
var reg: Dictionary = await api.register(username, email, TEST_PASSWORD, "1990-01-01")
|
||||||
var reg_token := _extract_token(reg)
|
var reg_token := _extract_token(reg)
|
||||||
var reg_ok: bool = (
|
var reg_ok: bool = (
|
||||||
reg.ok
|
reg.ok
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func _run() -> void:
|
|||||||
var unique_id: int = int(Time.get_unix_time_from_system() * 1000) + randi() % 100000
|
var unique_id: int = int(Time.get_unix_time_from_system() * 1000) + randi() % 100000
|
||||||
var username := "enterworld_%d" % unique_id
|
var username := "enterworld_%d" % unique_id
|
||||||
var email := "enterworld_%d@test.example.com" % unique_id
|
var email := "enterworld_%d@test.example.com" % unique_id
|
||||||
var reg_result: Dictionary = await api.register(username, email, "SmokeTest123!", true)
|
var reg_result: Dictionary = await api.register(username, email, "SmokeTest123!", "1990-01-01")
|
||||||
if not _check("setup: register succeeded", reg_result.ok):
|
if not _check("setup: register succeeded", reg_result.ok):
|
||||||
print(" detail: ", reg_result.detail)
|
print(" detail: ", reg_result.detail)
|
||||||
_finish()
|
_finish()
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func _run() -> void:
|
|||||||
var username := USERNAME % unique_id
|
var username := USERNAME % unique_id
|
||||||
var email := EMAIL % unique_id
|
var email := EMAIL % unique_id
|
||||||
print("\n=== Setup: register ", username, " ===")
|
print("\n=== Setup: register ", username, " ===")
|
||||||
var register_result: Dictionary = await api.register(username, email, PASSWORD, true)
|
var register_result: Dictionary = await api.register(username, email, PASSWORD, "1990-01-01")
|
||||||
if not _check("register succeeds", register_result.ok):
|
if not _check("register succeeds", register_result.ok):
|
||||||
print(" detail: ", register_result.detail)
|
print(" detail: ", register_result.detail)
|
||||||
_finish()
|
_finish()
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func _run() -> void:
|
|||||||
var username_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit")
|
var username_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit")
|
||||||
var password_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit")
|
var password_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit")
|
||||||
var email_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit")
|
var email_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit")
|
||||||
var over_18_check: CheckBox = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/Over18Check")
|
var dob_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/DobEdit")
|
||||||
var auth_button: Button = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton")
|
var auth_button: Button = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton")
|
||||||
var auth_mode_label: Label = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthModeLabel")
|
var auth_mode_label: Label = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthModeLabel")
|
||||||
var welcome_label: Label = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel")
|
var welcome_label: Label = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel")
|
||||||
@@ -46,7 +46,7 @@ func _run() -> void:
|
|||||||
username_edit.text = "mainmenu_e2e_%d" % unique_id
|
username_edit.text = "mainmenu_e2e_%d" % unique_id
|
||||||
password_edit.text = "TestPass123!"
|
password_edit.text = "TestPass123!"
|
||||||
email_edit.text = "mainmenu_e2e_%d@test.example.com" % unique_id
|
email_edit.text = "mainmenu_e2e_%d@test.example.com" % unique_id
|
||||||
over_18_check.button_pressed = true
|
dob_edit.text = "1990-01-01"
|
||||||
|
|
||||||
# Call the submit handler directly (awaited) rather than via
|
# Call the submit handler directly (awaited) rather than via
|
||||||
# auth_button.pressed.emit() -- a synthetic .emit() in --script/
|
# auth_button.pressed.emit() -- a synthetic .emit() in --script/
|
||||||
@@ -68,7 +68,7 @@ func _run() -> void:
|
|||||||
_check("Username edit hidden after auth", not username_edit.visible)
|
_check("Username edit hidden after auth", not username_edit.visible)
|
||||||
_check("Password edit hidden after auth", not password_edit.visible)
|
_check("Password edit hidden after auth", not password_edit.visible)
|
||||||
_check("Email edit hidden after auth", not email_edit.visible)
|
_check("Email edit hidden after auth", not email_edit.visible)
|
||||||
_check("Over18 checkbox hidden after auth", not over_18_check.visible)
|
_check("DOB field hidden after auth", not dob_edit.visible)
|
||||||
_check("Auth button hidden after auth", not auth_button.visible)
|
_check("Auth button hidden after auth", not auth_button.visible)
|
||||||
_check("Auth mode label hidden after auth", not auth_mode_label.visible)
|
_check("Auth mode label hidden after auth", not auth_mode_label.visible)
|
||||||
_check("Welcome label contains username", welcome_label.text.begins_with("Welcome, mainmenu_e2e_"))
|
_check("Welcome label contains username", welcome_label.text.begins_with("Welcome, mainmenu_e2e_"))
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func _run() -> void:
|
|||||||
var auth_panel: PanelContainer = instance.get_node("CenterContainer/VBoxContainer/AuthPanel")
|
var auth_panel: PanelContainer = instance.get_node("CenterContainer/VBoxContainer/AuthPanel")
|
||||||
var auth_mode_label: Label = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthModeLabel")
|
var auth_mode_label: Label = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthModeLabel")
|
||||||
var email_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit")
|
var email_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit")
|
||||||
var over_18_check: CheckBox = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/Over18Check")
|
var dob_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/DobEdit")
|
||||||
|
|
||||||
# --- Test 1: auth panel starts hidden ---
|
# --- Test 1: auth panel starts hidden ---
|
||||||
if _check("auth panel starts hidden", not auth_panel.visible):
|
if _check("auth panel starts hidden", not auth_panel.visible):
|
||||||
@@ -58,7 +58,7 @@ func _run() -> void:
|
|||||||
else:
|
else:
|
||||||
failed += 1
|
failed += 1
|
||||||
|
|
||||||
if _check("Over18 checkbox hidden in Login mode", not over_18_check.visible):
|
if _check("DOB field hidden in Login mode", not dob_edit.visible):
|
||||||
passed += 1
|
passed += 1
|
||||||
else:
|
else:
|
||||||
failed += 1
|
failed += 1
|
||||||
@@ -82,7 +82,7 @@ func _run() -> void:
|
|||||||
else:
|
else:
|
||||||
failed += 1
|
failed += 1
|
||||||
|
|
||||||
if _check("Over18 checkbox visible in Register mode", over_18_check.visible):
|
if _check("DOB field visible in Register mode", dob_edit.visible):
|
||||||
passed += 1
|
passed += 1
|
||||||
else:
|
else:
|
||||||
failed += 1
|
failed += 1
|
||||||
@@ -101,7 +101,7 @@ func _run() -> void:
|
|||||||
else:
|
else:
|
||||||
failed += 1
|
failed += 1
|
||||||
|
|
||||||
if _check("Over18 checkbox hidden after switching back to Login", not over_18_check.visible):
|
if _check("DOB field hidden after switching back to Login", not dob_edit.visible):
|
||||||
passed += 1
|
passed += 1
|
||||||
else:
|
else:
|
||||||
failed += 1
|
failed += 1
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func _run() -> void:
|
|||||||
var unique_id: int = int(Time.get_unix_time_from_system() * 1000) + randi() % 100000
|
var unique_id: int = int(Time.get_unix_time_from_system() * 1000) + randi() % 100000
|
||||||
var username := "worldlist_smoke_%d" % unique_id
|
var username := "worldlist_smoke_%d" % unique_id
|
||||||
var email := "worldlist_smoke_%d@test.example.com" % unique_id
|
var email := "worldlist_smoke_%d@test.example.com" % unique_id
|
||||||
var reg_result: Dictionary = await api.register(username, email, "SmokeTest123!", true)
|
var reg_result: Dictionary = await api.register(username, email, "SmokeTest123!", "1990-01-01")
|
||||||
if not _check("setup: register succeeded", reg_result.ok):
|
if not _check("setup: register succeeded", reg_result.ok):
|
||||||
print(" detail: ", reg_result.detail)
|
print(" detail: ", reg_result.detail)
|
||||||
quit(1)
|
quit(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user