fix: blank inbox previews, two unauthenticated 500s, and the missing social tests
- Every conversation row in the inbox rendered an empty preview: the frontend read `last_body`, which the API has never sent. It sends `excerpt` plus `truncated`. - `GET /api/codex?limit=-1` passed a negative LIMIT to Postgres -> 500. Now bounded by Query(ge=1, le=200) -> 422. - Registering with a >255 char email hit the VARCHAR(255) column and surfaced as a 500. PATCH /api/profile already validated this; only /auth/register was open. The social layer had zero tests, despite routes/messages.py citing "tests/test_messages.py's cross-user leak tests" and routes/profile.py claiming "privacy rules that the tests pin". Neither file existed. Both now do: auth required on every route, email only on /api/profile/me, no id or password_hash on any public payload, third-party thread reads empty, payload-supplied sender inert, veiled profiles 404 and stay off the roster. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
131
backend/tests/test_messages.py
Normal file
131
backend/tests/test_messages.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""Cross-user leak and impersonation pins for hunter-to-hunter whispers.
|
||||
|
||||
routes/messages.py's docstring points at this file ("See
|
||||
tests/test_messages.py's cross-user leak tests") — it did not exist.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
async def _register(client, username, password="spookyspooky"):
|
||||
resp = await client.post(
|
||||
"/auth/register", json={"username": username, "password": password}
|
||||
)
|
||||
assert resp.status_code == 201, resp.text
|
||||
|
||||
|
||||
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
|
||||
@pytest.mark.parametrize(
|
||||
"method,path",
|
||||
[("get", "/api/messages"), ("get", "/api/messages/someone")],
|
||||
)
|
||||
async def test_message_reads_require_auth(client, method, path):
|
||||
assert (await getattr(client, method)(path)).status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_requires_auth(client):
|
||||
resp = await client.post("/api/messages", json={"to": "anyone", "body": "hi"})
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_third_party_cannot_read_a_thread(client):
|
||||
await _register(client, "alice")
|
||||
await _register(client, "bob")
|
||||
await _register(client, "mallory")
|
||||
|
||||
await _login(client, "alice")
|
||||
sent = await client.post("/api/messages", json={"to": "bob", "body": "the cellar is cold"})
|
||||
assert sent.status_code == 201
|
||||
|
||||
# Mallory asks for the thread with each participant: the query is pinned
|
||||
# to (caller, other) in both directions, so neither leg can match a
|
||||
# message between two other people.
|
||||
await _login(client, "mallory")
|
||||
for name in ("alice", "bob"):
|
||||
thread = await client.get(f"/api/messages/{name}")
|
||||
assert thread.status_code == 200
|
||||
assert thread.json()["messages"] == []
|
||||
assert (await client.get("/api/messages")).json()["conversations"] == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sender_is_always_the_session_user(client):
|
||||
await _register(client, "alice2")
|
||||
await _register(client, "bob2")
|
||||
await _register(client, "mallory2")
|
||||
|
||||
# An extra `from`/`sender` in the body must be inert — sender_id comes
|
||||
# from the cookie, never the payload.
|
||||
await _login(client, "mallory2")
|
||||
resp = await client.post(
|
||||
"/api/messages",
|
||||
json={"to": "bob2", "body": "trust me", "from": "alice2", "sender": "alice2"},
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
|
||||
await _login(client, "bob2")
|
||||
thread = (await client.get("/api/messages/alice2")).json()
|
||||
assert thread["messages"] == []
|
||||
thread = (await client.get("/api/messages/mallory2")).json()
|
||||
assert [m["body"] for m in thread["messages"]] == ["trust me"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_body_length_is_capped_and_empty_rejected(client):
|
||||
await _register(client, "alice3")
|
||||
await _register(client, "bob3")
|
||||
await _login(client, "alice3")
|
||||
|
||||
assert (
|
||||
await client.post("/api/messages", json={"to": "bob3", "body": " "})
|
||||
).status_code == 400
|
||||
assert (
|
||||
await client.post("/api/messages", json={"to": "bob3", "body": "x" * 1001})
|
||||
).status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_is_rate_limited(client, monkeypatch):
|
||||
from app.routes import messages as messages_module
|
||||
from app.rate_limit import RateLimiter
|
||||
|
||||
monkeypatch.setattr(
|
||||
messages_module, "send_limiter", RateLimiter(max_requests=2, window_seconds=3600)
|
||||
)
|
||||
await _register(client, "alice4")
|
||||
await _register(client, "bob4")
|
||||
await _login(client, "alice4")
|
||||
|
||||
for _ in range(2):
|
||||
assert (
|
||||
await client.post("/api/messages", json={"to": "bob4", "body": "again"})
|
||||
).status_code == 201
|
||||
assert (
|
||||
await client.post("/api/messages", json={"to": "bob4", "body": "again"})
|
||||
).status_code == 429
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_conversation_row_carries_an_excerpt(client):
|
||||
# The inbox preview key is `excerpt` (+ `truncated`); the frontend read a
|
||||
# nonexistent `last_body` and rendered blank previews. Pin the name.
|
||||
await _register(client, "alice5")
|
||||
await _register(client, "bob5")
|
||||
await _login(client, "alice5")
|
||||
await client.post("/api/messages", json={"to": "bob5", "body": "z" * 300})
|
||||
|
||||
row = (await client.get("/api/messages")).json()["conversations"][0]
|
||||
assert row["excerpt"] == "z" * 160
|
||||
assert row["truncated"] is True
|
||||
assert row["hunter"]["username"] == "bob5"
|
||||
assert "email" not in row["hunter"]
|
||||
assert "id" not in row["hunter"]
|
||||
Reference in New Issue
Block a user