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:
Indiana
2026-08-01 01:45:04 +00:00
parent a727858a62
commit 605f32f20e
5 changed files with 270 additions and 5 deletions

View File

@@ -3,7 +3,7 @@ contacted, shared across all users (spec §4)."""
import uuid
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -126,7 +126,9 @@ def _entity_card(entity: Entity, discoverer: str | None) -> dict:
async def list_codex(
rarity: str | None = None,
sort: str = "recent",
limit: int = 60,
# Bounded at the edge: an unvalidated `?limit=-1` reached Postgres as a
# negative LIMIT and returned a 500 to an unauthenticated caller.
limit: int = Query(default=60, ge=1, le=200),
db: AsyncSession = Depends(get_db),
):
query = select(Entity)

View File

@@ -7,7 +7,9 @@ from pydantic import BaseModel, ConfigDict, Field
class RegisterRequest(BaseModel):
username: str = Field(min_length=3, max_length=32)
password: str = Field(min_length=8, max_length=128)
email: str | None = None
# users.email is VARCHAR(255): without a cap here an over-long address
# reached Postgres and surfaced as a 500 instead of a validation error.
email: str | None = Field(default=None, max_length=255)
class UserOut(BaseModel):