From 0f7531008f0369bc2d3669df4b0a953ac0a77e98 Mon Sep 17 00:00:00 2001 From: coder-bot Date: Thu, 20 Aug 2026 14:58:15 +0000 Subject: [PATCH 1/3] WIP checkpoint: Registration: replace date-of-birth field with a simple 18+ confirmation --- backend/app/routers/auth.py | 48 ++++++------------------------------- 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 61e99ae..18f2c4f 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -1,13 +1,13 @@ """Authentication router - login, register, token refresh.""" import uuid -from datetime import date, datetime, timedelta, timezone +from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from jose import JWTError, jwt from passlib.context import CryptContext -from pydantic import BaseModel, EmailStr, field_validator +from pydantic import BaseModel, EmailStr from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -39,17 +39,7 @@ class RegisterRequest(BaseModel): username: str email: EmailStr password: str - date_of_birth: str # ISO format date string (YYYY-MM-DD) - - @field_validator("date_of_birth") - @classmethod - def validate_date_of_birth(cls, v: str) -> str: - """Validate that the date string is a valid date in YYYY-MM-DD format.""" - try: - datetime.strptime(v, "%Y-%m-%d") - except ValueError: - raise ValueError("date_of_birth must be in YYYY-MM-DD format") - return v + over_18: bool # Simple boolean confirmation instead of date_of_birth class UserResponse(BaseModel): @@ -115,26 +105,6 @@ def hash_password(password: str) -> str: return pwd_context.hash(password) -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): - age -= 1 - return age - - def _issue_refresh_token(user_id: int, db: AsyncSession) -> str: """Create and persist a refresh token for the given user.""" refresh_token_value = _create_refresh_token({"sub": str(user_id)}) @@ -155,16 +125,12 @@ def _issue_refresh_token(user_id: int, db: AsyncSession) -> str: async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)): """Register a new user and return access + refresh tokens. - Requires date_of_birth and rejects users under 18. + Requires over_18 boolean confirmation; rejects users who are not 18+. """ - # Parse and validate date_of_birth - dob = datetime.strptime(req.date_of_birth, "%Y-%m-%d").date() - age = calculate_age(dob) - - if age < 18: + if not req.over_18: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Must be 18 or older to register (provided date of birth: {req.date_of_birth})", + detail="Must be 18 or older to register", ) # Check if username already exists @@ -187,7 +153,7 @@ async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)): username=req.username, email=req.email, hashed_password=hash_password(req.password), - date_of_birth=dob, + date_of_birth=None, # No longer collecting DOB ) db.add(user) await db.flush() # flush to get user.id before creating refresh token -- 2.52.0 From 4ec71b87b91eda91c9bbe2b4a0f3213933cddb1d Mon Sep 17 00:00:00 2001 From: coder-bot Date: Thu, 20 Aug 2026 14:58:15 +0000 Subject: [PATCH 2/3] WIP checkpoint: Registration: replace date-of-birth field with a simple 18+ confirmation --- backend/app/routers/auth.py | 48 ++++++------------------------------- 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 61e99ae..18f2c4f 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -1,13 +1,13 @@ """Authentication router - login, register, token refresh.""" import uuid -from datetime import date, datetime, timedelta, timezone +from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from jose import JWTError, jwt from passlib.context import CryptContext -from pydantic import BaseModel, EmailStr, field_validator +from pydantic import BaseModel, EmailStr from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -39,17 +39,7 @@ class RegisterRequest(BaseModel): username: str email: EmailStr password: str - date_of_birth: str # ISO format date string (YYYY-MM-DD) - - @field_validator("date_of_birth") - @classmethod - def validate_date_of_birth(cls, v: str) -> str: - """Validate that the date string is a valid date in YYYY-MM-DD format.""" - try: - datetime.strptime(v, "%Y-%m-%d") - except ValueError: - raise ValueError("date_of_birth must be in YYYY-MM-DD format") - return v + over_18: bool # Simple boolean confirmation instead of date_of_birth class UserResponse(BaseModel): @@ -115,26 +105,6 @@ def hash_password(password: str) -> str: return pwd_context.hash(password) -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): - age -= 1 - return age - - def _issue_refresh_token(user_id: int, db: AsyncSession) -> str: """Create and persist a refresh token for the given user.""" refresh_token_value = _create_refresh_token({"sub": str(user_id)}) @@ -155,16 +125,12 @@ def _issue_refresh_token(user_id: int, db: AsyncSession) -> str: async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)): """Register a new user and return access + refresh tokens. - Requires date_of_birth and rejects users under 18. + Requires over_18 boolean confirmation; rejects users who are not 18+. """ - # Parse and validate date_of_birth - dob = datetime.strptime(req.date_of_birth, "%Y-%m-%d").date() - age = calculate_age(dob) - - if age < 18: + if not req.over_18: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Must be 18 or older to register (provided date of birth: {req.date_of_birth})", + detail="Must be 18 or older to register", ) # Check if username already exists @@ -187,7 +153,7 @@ async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)): username=req.username, email=req.email, hashed_password=hash_password(req.password), - date_of_birth=dob, + date_of_birth=None, # No longer collecting DOB ) db.add(user) await db.flush() # flush to get user.id before creating refresh token -- 2.52.0 From 0ecc16d963b49ad3df6d402c18ecf62b66db8966 Mon Sep 17 00:00:00 2001 From: claude-checkpoint-tooling Date: Thu, 20 Aug 2026 16:25:27 +0000 Subject: [PATCH 3/3] Registration: replace date-of-birth field with a simple 18+ confirmation The register form collected and validated an actual date of birth just to throw it away (backend never stored it beyond a nullable column). Per the roadmap note: don't collect/store DOB right now, just get a client-side "I am 18 or older" confirmation. RegisterRequest.date_of_birth (str) is now over_18 (bool); the endpoint rejects over_18=false with 400 same as before. calculate_age() is kept (unused by /register now) since real DOB collection is coming back in a future pass and deleting it broke test_auth_age_gate.py's import for no benefit. Frontend: DOBEdit LineEdit replaced with an Over18Check CheckBox; api_client.gd's register() now takes a bool instead of a date string. All Godot callers (smoke_test_auth/e2e/world_list/main_menu_ui/main_menu_full_flow) and backend test files updated to match. Full suite verified against a live backend: 61/61 pytest, 6/6 Godot smoke tests (33 individual assertions), all passing. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01M6rUEynmv2QmweocAQYARN --- backend/app/routers/auth.py | 32 ++++++++++++- backend/tests/test_auth_age_gate.py | 58 +++++++++++------------ backend/tests/test_auth_endpoints.py | 48 +++++++++---------- backend/tests/test_auth_expired_token.py | 2 +- backend/tests/test_refresh_endpoint.py | 4 +- docs/testing/test_inventory.md | 8 ++-- scenes/main_menu.tscn | 4 +- scripts/api_client.gd | 6 +-- scripts/main_menu.gd | 45 ++++-------------- scripts/smoke_test_auth.gd | 23 +++++---- scripts/smoke_test_e2e.gd | 3 +- scripts/smoke_test_main_menu_full_flow.gd | 4 +- scripts/smoke_test_main_menu_ui.gd | 8 ++-- scripts/smoke_test_world_list.gd | 2 +- 14 files changed, 122 insertions(+), 125 deletions(-) diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 18f2c4f..b1e1dd5 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -1,7 +1,7 @@ """Authentication router - login, register, token refresh.""" import uuid -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 @@ -105,6 +105,36 @@ def hash_password(password: str) -> str: return pwd_context.hash(password) +def calculate_age(birth_date: date, today: date | None = None) -> int: + """Calculate age in years from a birth date. + + Not called by /register anymore (that endpoint now just trusts the + client's over_18 boolean, see RegisterRequest) -- kept as a real, + tested utility since date-of-birth storage/verification is coming + 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: + 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): + age -= 1 + return age + + def _issue_refresh_token(user_id: int, db: AsyncSession) -> str: """Create and persist a refresh token for the given user.""" refresh_token_value = _create_refresh_token({"sub": str(user_id)}) diff --git a/backend/tests/test_auth_age_gate.py b/backend/tests/test_auth_age_gate.py index 22a1f2a..3b372b9 100644 --- a/backend/tests/test_auth_age_gate.py +++ b/backend/tests/test_auth_age_gate.py @@ -57,38 +57,38 @@ class TestCalculateAge: assert calculate_age(dob, today=date(2024, 12, 31)) == 24 -class TestRegisterRequestDateValidation: - """Tests for the RegisterRequest date_of_birth field validator.""" +class TestRegisterRequestOver18Field: + """Tests for RegisterRequest's over_18 field. - @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.""" + Replaces TestRegisterRequestDateValidation -- date_of_birth and its + field_validator were removed from RegisterRequest 2026-08-20 (interim + 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", date_of_birth=dob) - assert req.date_of_birth == dob + req = RegisterRequest(username="u", email="u@example.com", password="p", over_18=True) + assert req.over_18 is True - @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.""" + 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 - with pytest.raises(ValueError): - RegisterRequest(username="u", email="u@example.com", password="p", date_of_birth=dob) + 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") diff --git a/backend/tests/test_auth_endpoints.py b/backend/tests/test_auth_endpoints.py index 127494a..afe949e 100644 --- a/backend/tests/test_auth_endpoints.py +++ b/backend/tests/test_auth_endpoints.py @@ -73,7 +73,7 @@ class TestRegister: "username": "newuser", "email": "new@example.com", "password": "secret123", - "date_of_birth": "2000-01-01", + "over_18": True, }, ) assert resp.status_code == 201 @@ -81,8 +81,14 @@ class TestRegister: assert "access_token" in data assert data["token_type"] == "bearer" - def test_register_rejects_underage(self): - """Users under 18 get a 400 with a detail message.""" + def test_register_rejects_not_over_18(self): + """over_18=false gets a 400 with a detail message. + + The interim design (2026-08-20) trusts a client-side boolean + 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) resp = client.post( "/api/auth/register", @@ -90,7 +96,7 @@ class TestRegister: "username": "kid", "email": "kid@example.com", "password": "secret123", - "date_of_birth": "2010-06-15", # ~15 years old + "over_18": False, }, ) assert resp.status_code == 400 @@ -106,7 +112,7 @@ class TestRegister: "username": "duplicate", "email": "dup1@example.com", "password": "secret123", - "date_of_birth": "2000-01-01", + "over_18": True, }, ) # Second registration with same username, different email @@ -116,7 +122,7 @@ class TestRegister: "username": "duplicate", "email": "dup2@example.com", "password": "secret123", - "date_of_birth": "2000-01-01", + "over_18": True, }, ) assert resp.status_code == 409 @@ -131,7 +137,7 @@ class TestRegister: "username": "user1", "email": "same@example.com", "password": "secret123", - "date_of_birth": "2000-01-01", + "over_18": True, }, ) resp = client.post( @@ -140,14 +146,19 @@ class TestRegister: "username": "user2", "email": "same@example.com", "password": "secret123", - "date_of_birth": "2000-01-01", + "over_18": True, }, ) assert resp.status_code == 409 assert "Email already registered" in resp.json()["detail"] - def test_register_invalid_date_format(self): - """Invalid date format returns 422 validation error.""" + def test_register_missing_over_18(self): + """Missing the required over_18 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) resp = client.post( "/api/auth/register", @@ -155,25 +166,10 @@ class TestRegister: "username": "user", "email": "user@example.com", "password": "secret123", - "date_of_birth": "01-01-2000", # wrong format }, ) assert resp.status_code == 422 - def test_register_exactly_18(self): - """A user who just turned 18 can register.""" - client = TestClient(app) - resp = client.post( - "/api/auth/register", - json={ - "username": "just18", - "email": "just18@example.com", - "password": "secret123", - "date_of_birth": "2007-06-15", - }, - ) - assert resp.status_code == 201 - # --------------------------------------------------------------------------- # Login endpoint @@ -193,7 +189,7 @@ class TestLogin: "username": username, "email": email, "password": password, - "date_of_birth": "2000-01-01", + "over_18": True, }, ) assert resp.status_code == 201 diff --git a/backend/tests/test_auth_expired_token.py b/backend/tests/test_auth_expired_token.py index fdc26ad..960afb6 100644 --- a/backend/tests/test_auth_expired_token.py +++ b/backend/tests/test_auth_expired_token.py @@ -93,7 +93,7 @@ def _register_user(client: TestClient, username: str) -> str: "username": username, "email": f"{username}@example.com", "password": "secret123", - "date_of_birth": "2000-01-01", + "over_18": True, }, ) assert resp.status_code == 201 diff --git a/backend/tests/test_refresh_endpoint.py b/backend/tests/test_refresh_endpoint.py index 92e4322..3da8e47 100644 --- a/backend/tests/test_refresh_endpoint.py +++ b/backend/tests/test_refresh_endpoint.py @@ -59,7 +59,7 @@ class TestRefresh: "username": username, "email": email, "password": password, - "date_of_birth": "2000-01-01", + "over_18": True, }, ) assert resp.status_code == 201 @@ -143,7 +143,7 @@ class TestRefresh: "username": "refep_loginrefuser", "email": "loginref@example.com", "password": "secret123", - "date_of_birth": "2000-01-01", + "over_18": True, }, ) # Login to get a refresh token diff --git a/docs/testing/test_inventory.md b/docs/testing/test_inventory.md index a9f7f45..f5c7a3f 100644 --- a/docs/testing/test_inventory.md +++ b/docs/testing/test_inventory.md @@ -20,13 +20,13 @@ covered by the smoke tests below so they can't silently regress. Run: `cd backend && python -m pytest -v` (needs `DATABASE_URL` pointed at a reachable Postgres — see the runner script). -**Current total: 68 tests, all passing on a clean `main` checkout.** +**Current total: 61 tests, all passing on a clean `main` checkout.** | File | Tests | What it covers | |---|---|---| -| `test_auth_age_gate.py` | 17 | `calculate_age()` date-math correctness (exact boundary, leap years, birthday-not-yet-this-year) + `RegisterRequest` date-of-birth validation (parametrized valid/invalid date rejection). Backs the 18+ registration gate (PR #23). | +| `test_auth_age_gate.py` | 11 | `calculate_age()` date-math correctness (exact boundary, leap years, birthday-not-yet-this-year — kept as a tested utility for a future real-DOB pass even though `/register` no longer calls it) + `RegisterRequest.over_18` field tests (true accepted, false accepted, missing rejected). 2026-08-20: the register flow was simplified from collecting/validating a date of birth to a plain client-side "I am 18 or older" confirmation, so the old date-validation parametrized cases were replaced. Backs the 18+ registration gate (PR #23, revised PC1). | | `test_gltf_metadata.py` | 21 | glTF/GLB binary + JSON parsing: magic-byte detection, chunk-padding handling, mesh/animation/texture-reference metadata extraction, malformed-input rejection. Backs the asset-import pipeline (PR #27). | -| `test_auth_endpoints.py` | 11 | **Integration tests** for `POST /api/auth/register` and `POST /api/auth/login` against the real FastAPI app + real Postgres. Covers: successful register (201), duplicate username (409), duplicate email (409), underage rejection (400), invalid date format (422), exact-18 registration, successful login with correct credentials (200), wrong password (401), nonexistent user (401), form-encoded login (OAuth2), and JWT validity of returned tokens. | +| `test_auth_endpoints.py` | 10 | **Integration tests** for `POST /api/auth/register` and `POST /api/auth/login` against the real FastAPI app + real Postgres. Covers: successful register (201), duplicate username (409), duplicate email (409), `over_18=false` rejection (400), missing `over_18` field (422), successful login with correct credentials (200), wrong password (401), nonexistent user (401), form-encoded login (OAuth2), and JWT validity of returned tokens. | | `test_refresh_endpoint.py` | 8 | **Integration tests** for `POST /api/auth/refresh`. Covers: successful refresh (200, new access token, same refresh token returned), refresh produces different access tokens on repeated calls, invalid refresh token (401), empty body (422), missing field (422), refresh token from login also works, returned access token is valid JWT decodable with public key, `access_token_expires_minutes` in response. | | `test_refresh_token_jti.py` | 6 | Unit tests for refresh token uniqueness (jti claim) and model column length. Covers: two calls with same sub produce different tokens (jti uniqueness), jti is valid UUID, rapid repeated calls all produce unique jtis, jti survives decode, `RefreshToken.token` column is `String(512)`, column type is `String`. | | `test_auth_expired_token.py` | 5 | **Integration tests** for expired-token rejection across auth endpoints. Covers: expired access JWT rejected by `GET /api/auth/me` (401), garbage access token rejected by `/me` (401), missing bearer token returns 401, expired refresh token (DB `expires_at` in past) rejected by `/refresh` (401), expired refresh JWT (past `exp` claim) rejected before DB lookup (401). | @@ -60,7 +60,7 @@ these yet): | File | What it covers | |---|---| | `smoke_test_api.gd` | Exercises `ApiClient.health_check()` against a live backend. Verifies the real HTTP request path (`HTTPRequest` node lifecycle, JSON parse, result-dictionary contract). Exits 1 on failure. | -| `smoke_test_auth.gd` | Tests the full auth flow via `ApiClient` autoload: register unique user (201), duplicate username (409), under-18 registration (400), correct login (200), wrong password (401), nonexistent user (401). | +| `smoke_test_auth.gd` | Tests the full auth flow via `ApiClient` autoload: register unique user (201), duplicate username (409), `over_18=false` registration (400), correct login (200), wrong password (401), nonexistent user (401). | | `smoke_test_e2e.gd` | End-to-end flow: health check → register → login → worlds list. Verifies the full request chain with a fresh user, including token storage and `get_worlds()` with auth header. | | `smoke_test_world_list.gd` | Registers a user, **seeds a real world via `POST /api/worlds/`** (without this, `run_checkpoint_tests.py`'s always-fresh DB means the populated-card render path never actually runs in CI, only the empty-list path would), instantiates `world_list.tscn` as a real scene, waits for `_refresh_worlds()` to complete, confirms it reached the populated (not empty) state, and asserts **each rendered card's real computed width is >= 200px**. That last check is the one that matters: 2026-08-20, a real bug shipped where every card existed in the tree with a non-zero child count and no crash, but rendered at 1.0px wide (`ScrollContainer`'s `size_flags_horizontal`/`vertical` used the raw integer `4` = `Control.SIZE_SHRINK_CENTER` instead of `3` = `SIZE_EXPAND_FILL`) — invisible on screen, and a child-count-only check would never have caught it. Confirmed by deliberately reintroducing the bug: card width check correctly fails at 25px. | | `smoke_test_main_menu_ui.gd` | Instantiates `main_menu.tscn`, confirms the auth panel starts hidden, and confirms a **real signal emission** on LoginButton/RegisterButton (not calling the handler method directly) actually opens it. This is the check that would have caught 2026-08-20's real bug: the auth panel and its logic were fully correct, but nothing in the scene was ever wired to open it, so Login/Register were completely unreachable from the UI. | diff --git a/scenes/main_menu.tscn b/scenes/main_menu.tscn index b7371b7..a07c731 100644 --- a/scenes/main_menu.tscn +++ b/scenes/main_menu.tscn @@ -83,10 +83,10 @@ visible = false placeholder_text = "Email" size_flags_horizontal = 3 -[node name="DOBEdit" type="LineEdit" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"] +[node name="Over18Check" type="CheckBox" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"] layout_mode = 1 visible = false -placeholder_text = "Date of Birth (YYYY-MM-DD)" +text = "I am 18 or older" size_flags_horizontal = 3 [node name="AuthButton" type="Button" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"] diff --git a/scripts/api_client.gd b/scripts/api_client.gd index a483592..4905634 100644 --- a/scripts/api_client.gd +++ b/scripts/api_client.gd @@ -178,18 +178,18 @@ func login(username: String, password: String) -> Dictionary: ## Register a new user. On success stores the returned access token. -## @param date_of_birth ISO date string "YYYY-MM-DD". +## @param over_18 Must be true; the backend rejects registration otherwise. func register( username: String, email: String, password: String, - date_of_birth: String + over_18: bool ) -> Dictionary: var payload := { "username": username, "email": email, "password": password, - "date_of_birth": date_of_birth, + "over_18": over_18, } var result := await request( HTTPClient.METHOD_POST, diff --git a/scripts/main_menu.gd b/scripts/main_menu.gd index 06b2d94..c53f14e 100644 --- a/scripts/main_menu.gd +++ b/scripts/main_menu.gd @@ -17,7 +17,7 @@ var _auth_mode_label: Label var _username_edit: LineEdit var _password_edit: LineEdit var _email_edit: LineEdit -var _dob_edit: LineEdit +var _over_18_check: CheckBox var _auth_button: Button var _status_label: Label var _welcome_label: Label @@ -40,7 +40,7 @@ func _ready() -> void: _username_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit _password_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit _email_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit - _dob_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/DOBEdit + _over_18_check = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/Over18Check _auth_button = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton _status_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthStatusLabel _welcome_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel @@ -52,7 +52,8 @@ func _ready() -> void: _status_label.visible = false _welcome_label.visible = false _email_edit.visible = false - _dob_edit.visible = false + _over_18_check.visible = false + _over_18_check.button_pressed = false _play_button.disabled = true # Gate play behind successful auth. _update_auth_mode_ui() @@ -97,12 +98,12 @@ func _update_auth_mode_ui() -> void: _auth_mode_label.text = "Register" _auth_button.text = "Register" _email_edit.visible = true - _dob_edit.visible = true + _over_18_check.visible = true else: _auth_mode_label.text = "Login" _auth_button.text = "Login" _email_edit.visible = false - _dob_edit.visible = false + _over_18_check.visible = false # ─── Auth submission ────────────────────────────────────────────────────────── @@ -125,17 +126,12 @@ func _on_auth_button_pressed() -> void: if _is_register_mode: var email := _email_edit.text.strip_edges() - var dob := _dob_edit.text.strip_edges() if email == "": _show_status("Email is required.") return - if dob == "": - _show_status("Date of birth is required.") - return - # Basic YYYY-MM-DD format check. - if not _is_valid_date(dob): - _show_status("Date of birth must be in YYYY-MM-DD format.") + if not _over_18_check.button_pressed: + _show_status("You must confirm you are 18 or older to register.") return _auth_button.disabled = true @@ -143,7 +139,7 @@ func _on_auth_button_pressed() -> void: _is_authenticating = true _clear_status() - var result := await ApiClient.register(username, email, password, dob) + var result := await ApiClient.register(username, email, password, true) _on_auth_complete(result) else: _auth_button.disabled = true @@ -186,26 +182,3 @@ func _show_status(message: String) -> void: func _clear_status() -> void: _status_label.text = "" _status_label.visible = false - - -# ─── Validation ─────────────────────────────────────────────────────────────── - -func _is_valid_date(date_str: String) -> bool: - """Check if a string looks like a valid YYYY-MM-DD date.""" - if date_str.length() != 10: - return false - var parts := date_str.split("-") - if parts.size() != 3: - return false - var year := parts[0].to_int() - var month := parts[1].to_int() - var day := parts[2].to_int() - if month < 1 or month > 12: - return false - if day < 1 or day > 31: - return false - # Rough month-day validation. - var days_in_month := [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] - if day > days_in_month[month]: - return false - return true diff --git a/scripts/smoke_test_auth.gd b/scripts/smoke_test_auth.gd index 63dfc22..cc9182f 100644 --- a/scripts/smoke_test_auth.gd +++ b/scripts/smoke_test_auth.gd @@ -10,7 +10,7 @@ extends SceneTree ## Tests: ## 1. Register a unique user → 201 + token stored. ## 2. Register the same user again → 409 "Username already registered". -## 3. Register an under-18 user → 400 "Must be 18 or older". +## 3. Register with over_18=false → 400 "Must be 18 or older". ## 4. Login with correct credentials → 200 + token stored. ## 5. Login with wrong password → 401 "Incorrect username or password". ## 6. Login with non-existent user → 401 "Incorrect username or password". @@ -21,8 +21,6 @@ const API_BASE := "http://127.0.0.1:8000" const TEST_USERNAME := "smoke_test_user_%d" const TEST_EMAIL := "smoke_%d@test.example.com" const TEST_PASSWORD := "SmokeTest123!" -const TEST_DOB_ADULT := "2000-01-01" -const TEST_DOB_CHILD := "2020-01-01" # ~5 years old func _initialize() -> void: @@ -40,12 +38,13 @@ func _run() -> void: var failed: int = 0 # --- Test 1: Register a unique user --- - var unique_id := Time.get_ticks_msec() + randomize() + var unique_id: int = int(Time.get_unix_time_from_system() * 1000) + randi() % 100000 var username := "smoke_%d" % unique_id var email := "smoke_%d@test.example.com" % unique_id print("\n=== Test 1: Register unique user ===") - var result := await api.register(username, email, TEST_PASSWORD, TEST_DOB_ADULT) + var result := await api.register(username, email, TEST_PASSWORD, true) if _check("register returns ok", result.ok) \ and _check("register status is 201", result.status == 201) \ and _check("register stores token", api.has_token()): @@ -56,7 +55,7 @@ func _run() -> void: # --- Test 2: Register same user again (duplicate) --- print("\n=== Test 2: Duplicate username ===") - result = await api.register(username, email, TEST_PASSWORD, TEST_DOB_ADULT) + result = await api.register(username, email, TEST_PASSWORD, true) if _check("duplicate register not ok", not result.ok) \ and _check("duplicate status is 409", result.status == 409) \ and _check("duplicate detail mentions username", @@ -66,14 +65,14 @@ func _run() -> void: failed += 1 print(" detail: ", result.detail) - # --- Test 3: Register under-18 user --- - print("\n=== Test 3: Under-18 registration ===") + # --- Test 3: Register with over_18=false --- + print("\n=== Test 3: over_18=false registration ===") var child_username := "child_%d" % unique_id var child_email := "child_%d@test.example.com" % unique_id - result = await api.register(child_username, child_email, TEST_PASSWORD, TEST_DOB_CHILD) - if _check("under-18 register not ok", not result.ok) \ - and _check("under-18 status is 400", result.status == 400) \ - and _check("under-18 detail mentions age", + result = await api.register(child_username, child_email, TEST_PASSWORD, false) + if _check("over_18=false register not ok", not result.ok) \ + and _check("over_18=false status is 400", result.status == 400) \ + and _check("over_18=false detail mentions age", "18" in result.detail): passed += 1 else: diff --git a/scripts/smoke_test_e2e.gd b/scripts/smoke_test_e2e.gd index dacd41a..692aec9 100644 --- a/scripts/smoke_test_e2e.gd +++ b/scripts/smoke_test_e2e.gd @@ -23,7 +23,6 @@ extends SceneTree const TEST_USERNAME_BASE := "smoke_test_user" const TEST_EMAIL := "smoke@test.com" const TEST_PASSWORD := "TestPass123" -const TEST_DOB := "2000-01-01" func _initialize() -> void: @@ -70,7 +69,7 @@ func _run() -> void: # --- Step 2: Register ----------------------------------------------------- print("\n=== Step 2: Register (POST /api/auth/register) ===") - var reg: Dictionary = await api.register(username, email, TEST_PASSWORD, TEST_DOB) + var reg: Dictionary = await api.register(username, email, TEST_PASSWORD, true) var reg_token := _extract_token(reg) var reg_ok: bool = ( reg.ok diff --git a/scripts/smoke_test_main_menu_full_flow.gd b/scripts/smoke_test_main_menu_full_flow.gd index 7aa066c..c7dd9bd 100644 --- a/scripts/smoke_test_main_menu_full_flow.gd +++ b/scripts/smoke_test_main_menu_full_flow.gd @@ -26,7 +26,7 @@ func _run() -> void: 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 email_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit") - var dob_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/DOBEdit") + var over_18_check: CheckBox = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/Over18Check") var auth_button: Button = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton") var welcome_label: Label = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel") var play_button: Button = instance.get_node("CenterContainer/VBoxContainer/PlayButton") @@ -46,7 +46,7 @@ func _run() -> void: username_edit.text = "mainmenu_e2e_%d" % unique_id password_edit.text = "TestPass123!" email_edit.text = "mainmenu_e2e_%d@test.example.com" % unique_id - dob_edit.text = "2000-01-01" + over_18_check.button_pressed = true # Call the submit handler directly (awaited) rather than via # auth_button.pressed.emit() -- a synthetic .emit() in --script/ diff --git a/scripts/smoke_test_main_menu_ui.gd b/scripts/smoke_test_main_menu_ui.gd index 570e81c..f5d62ab 100644 --- a/scripts/smoke_test_main_menu_ui.gd +++ b/scripts/smoke_test_main_menu_ui.gd @@ -31,7 +31,7 @@ func _run() -> void: 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 email_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit") - var dob_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/DOBEdit") + var over_18_check: CheckBox = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/Over18Check") # --- Test 1: auth panel starts hidden --- if _check("auth panel starts hidden", not auth_panel.visible): @@ -58,7 +58,7 @@ func _run() -> void: else: failed += 1 - if _check("DOB field hidden in Login mode", not dob_edit.visible): + if _check("Over18 checkbox hidden in Login mode", not over_18_check.visible): passed += 1 else: failed += 1 @@ -82,7 +82,7 @@ func _run() -> void: else: failed += 1 - if _check("DOB field visible in Register mode", dob_edit.visible): + if _check("Over18 checkbox visible in Register mode", over_18_check.visible): passed += 1 else: failed += 1 @@ -101,7 +101,7 @@ func _run() -> void: else: failed += 1 - if _check("DOB field hidden after switching back to Login", not dob_edit.visible): + if _check("Over18 checkbox hidden after switching back to Login", not over_18_check.visible): passed += 1 else: failed += 1 diff --git a/scripts/smoke_test_world_list.gd b/scripts/smoke_test_world_list.gd index 5cb6a16..b0b6231 100644 --- a/scripts/smoke_test_world_list.gd +++ b/scripts/smoke_test_world_list.gd @@ -58,7 +58,7 @@ func _run() -> void: var unique_id: int = int(Time.get_unix_time_from_system() * 1000) + randi() % 100000 var username := "worldlist_smoke_%d" % unique_id var email := "worldlist_smoke_%d@test.example.com" % unique_id - var reg_result: Dictionary = await api.register(username, email, "SmokeTest123!", "2000-01-01") + var reg_result: Dictionary = await api.register(username, email, "SmokeTest123!", true) if not _check("setup: register succeeded", reg_result.ok): print(" detail: ", reg_result.detail) quit(1) -- 2.52.0