"""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"]