"""Privacy and authorization pins for the hunter profile layer. The module docstring of routes/profile.py promises three things that are easy to regress and were never actually tested: email appears on /api/profile/me and nowhere else, a veiled profile is indistinguishable from one that never existed, and editing is authenticated. """ import pytest async def _register(client, username, password="spookyspooky", **extra): resp = await client.post( "/auth/register", json={"username": username, "password": password, **extra} ) assert resp.status_code == 201, resp.text return resp.json() async def _login(client, username, password="spookyspooky"): resp = await client.post( "/auth/login", json={"username": username, "password": password} ) assert resp.status_code == 200, resp.text @pytest.mark.asyncio async def test_profile_me_requires_auth(client): assert (await client.get("/api/profile/me")).status_code == 401 @pytest.mark.asyncio async def test_profile_patch_requires_auth(client): resp = await client.patch("/api/profile", json={"display_name": "nobody"}) assert resp.status_code == 401 @pytest.mark.asyncio async def test_email_only_on_own_profile(client): await _register(client, "seer_a", email="seer@example.com") await _login(client, "seer_a") mine = (await client.get("/api/profile/me")).json() assert mine["email"] == "seer@example.com" public = (await client.get("/api/hunters/seer_a")).json() assert "email" not in public assert "password_hash" not in public assert "id" not in public roster = (await client.get("/api/hunters")).json()["hunters"] for hunter in roster: assert "email" not in hunter assert "password_hash" not in hunter assert "id" not in hunter @pytest.mark.asyncio async def test_patch_cannot_touch_another_hunter(client): await _register(client, "seer_b") await _register(client, "seer_c") await _login(client, "seer_b") # There is no user selector on PATCH at all — the payload is applied to # the session's own row, so a username in the body is inert. resp = await client.patch( "/api/profile", json={"username": "seer_c", "display_name": "impostor"} ) assert resp.status_code == 200 assert resp.json()["username"] == "seer_b" victim = (await client.get("/api/hunters/seer_c")).json() assert victim["display_name"] is None @pytest.mark.asyncio async def test_veiled_profile_is_404_and_absent_from_roster(client): await _register(client, "seer_d") await _login(client, "seer_d") assert (await client.patch("/api/profile", json={"profile_public": False})).status_code == 200 assert (await client.get("/api/hunters/seer_d")).status_code == 404 roster = (await client.get("/api/hunters")).json()["hunters"] assert all(h["username"] != "seer_d" for h in roster) @pytest.mark.asyncio async def test_text_fields_are_capped(client): await _register(client, "seer_e") await _login(client, "seer_e") assert ( await client.patch("/api/profile", json={"display_name": "x" * 49}) ).status_code == 422 assert (await client.patch("/api/profile", json={"bio": "y" * 281})).status_code == 422 assert (await client.patch("/api/profile", json={"avatar_hue": 400})).status_code == 422 assert (await client.patch("/api/profile", json={"gender": "wraith"})).status_code == 422 @pytest.mark.asyncio async def test_markup_in_profile_text_is_stored_verbatim_not_interpreted(client): # The API is a JSON API: it stores exactly what was sent. The XSS defence # is that every consumer renders it as a text node (see MessagesPage / # HunterPage). Pinned so nobody "helpfully" starts emitting HTML here. await _register(client, "seer_f") await _login(client, "seer_f") payload = "" resp = await client.patch("/api/profile", json={"bio": payload}) assert resp.json()["bio"] == payload assert (await client.get("/api/hunters/seer_f")).json()["bio"] == payload @pytest.mark.asyncio async def test_register_rejects_absurdly_long_email(client): # users.email is VARCHAR(255); without a schema cap this reached the # database and came back as a 500 instead of a validation error. resp = await client.post( "/auth/register", json={ "username": "seer_g", "password": "spookyspooky", "email": "a" * 300 + "@example.com", }, ) assert resp.status_code == 422