7bd8124199
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
92 lines
3.4 KiB
Python
92 lines
3.4 KiB
Python
"""Integration tests for GET/PUT /api/avatar/loadout.
|
|
|
|
Hits the real app against the configured Postgres (same contract as the
|
|
other integration tests -- migrations applied first).
|
|
"""
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.main import app
|
|
from app.models.avatar_loadout import DEFAULT_LOADOUT
|
|
|
|
|
|
def _auth_headers(client: TestClient, username: str) -> dict:
|
|
resp = client.post(
|
|
"/api/auth/register",
|
|
json={
|
|
"username": username,
|
|
"email": f"{username}@example.com",
|
|
"password": "secret123",
|
|
"date_of_birth": "1990-01-01",
|
|
},
|
|
)
|
|
assert resp.status_code == 201, resp.text
|
|
return {"Authorization": f"Bearer {resp.json()['access_token']}"}
|
|
|
|
|
|
class TestGetLoadout:
|
|
def test_new_user_gets_the_default(self):
|
|
client = TestClient(app)
|
|
headers = _auth_headers(client, "av_default")
|
|
resp = client.get("/api/avatar/loadout", headers=headers)
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["is_default"] is True
|
|
assert body["data"] == DEFAULT_LOADOUT
|
|
|
|
def test_requires_auth(self):
|
|
client = TestClient(app)
|
|
assert client.get("/api/avatar/loadout").status_code == 401
|
|
|
|
|
|
class TestPutLoadout:
|
|
def test_save_then_read_back(self):
|
|
client = TestClient(app)
|
|
headers = _auth_headers(client, "av_save")
|
|
loadout = {
|
|
"base_body": "base_human_male_01",
|
|
"slots": {"hair": "hair_mohawk_01", "accessory": ["accessory_belt_01"]},
|
|
"skin_tone": {"r": 0.8, "g": 0.6, "b": 0.5, "a": 1.0},
|
|
}
|
|
put = client.put("/api/avatar/loadout", headers=headers, json={"data": loadout})
|
|
assert put.status_code == 200, put.text
|
|
assert put.json()["is_default"] is False
|
|
|
|
got = client.get("/api/avatar/loadout", headers=headers)
|
|
assert got.status_code == 200
|
|
assert got.json()["is_default"] is False
|
|
assert got.json()["data"] == loadout
|
|
|
|
def test_put_is_idempotent_replace(self):
|
|
client = TestClient(app)
|
|
headers = _auth_headers(client, "av_replace")
|
|
client.put(
|
|
"/api/avatar/loadout", headers=headers,
|
|
json={"data": {"base_body": "base_human_male_01", "slots": {"hair": "hair_mohawk_01"}}},
|
|
)
|
|
second = {"base_body": "base_human_male_01", "slots": {"chest": "chest_bandit_01"}}
|
|
client.put("/api/avatar/loadout", headers=headers, json={"data": second})
|
|
got = client.get("/api/avatar/loadout", headers=headers)
|
|
assert got.json()["data"] == second
|
|
|
|
def test_rejects_missing_base_body(self):
|
|
client = TestClient(app)
|
|
headers = _auth_headers(client, "av_nobase")
|
|
resp = client.put("/api/avatar/loadout", headers=headers, json={"data": {"slots": {}}})
|
|
assert resp.status_code == 422
|
|
|
|
def test_rejects_non_object_slots(self):
|
|
client = TestClient(app)
|
|
headers = _auth_headers(client, "av_badslots")
|
|
resp = client.put(
|
|
"/api/avatar/loadout", headers=headers,
|
|
json={"data": {"base_body": "base_human_male_01", "slots": "nope"}},
|
|
)
|
|
assert resp.status_code == 422
|
|
|
|
def test_requires_auth(self):
|
|
client = TestClient(app)
|
|
resp = client.put("/api/avatar/loadout", json={"data": {"base_body": "x", "slots": {}}})
|
|
assert resp.status_code == 401
|