✨ feat: 阶段 0 收尾——认证边界、请求上下文与垂直切片(WP-01 第 5-7 步)

- 认证:Web Session(__Host- Cookie、HttpOnly/Secure/SameSite=Lax、CSPRNG opaque token 数据库只存哈希)、CSRF 双提交、Origin 校验、dev 登录/登出端点;未认证 401、缺 CSRF 403
- 中间件:Hunjing-Request-ID 回显/生成(request_ UUIDv4)+ W3C traceparent 校验透传;JSON 结构化日志与敏感头脱敏
- 迁移 2:conversation/branch/message/run/idempotency_record 五表(含检查约束与复合索引);Session 补 token/csrf 哈希列
- 垂直切片:POST /v1/conversations/{id}/messages 按 Idempotency-Key 幂等(同键同体 200 重放、异体 409),同事务原子写 Message+Run+Audit+Outbox+幂等记录;ContentBlock text/markdown 判别联合(未知类型 422)
- SSE:GET /v1/runs/{id}/events 返回快照与终止状态事件 envelope
- 测试 26 项全绿:IDOR 跨域 404 fail-closed、失败注入回滚无孤儿(Run 崩溃后 message/run/audit/outbox/幂等全零)、SSE 事件序、auth 边界;ruff/mypy strict 干净
- 修复真实问题:asyncpg 跨事件循环(anyio/pytest-asyncio 双 loop、模块级 engine 改为 app 实例持有)、httpx 拒存 http 下 Secure Cookie
This commit is contained in:
chenxin committed 2026-09-10 10:13:45 +08:00
1 parent 28127949db
commit ba502297ac
16 files changed
+1165

No files matched your search

+32
View File
@@ -24,3 +24,35 @@ async def db() -> async_sessionmaker[AsyncSession]:
factory = async_sessionmaker(engine, expire_on_commit=False)
yield factory
await engine.dispose()
@pytest.fixture
async def client(db):
"""连接测试库的应用客户端(覆盖 get_db)。"""
from httpx import ASGITransport, AsyncClient
from app.db import session as db_session
from app.main import create_app
app = create_app()
async def override():
async with db() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
app.dependency_overrides[db_session.get_db] = override
transport = ASGITransport(app=app, raise_app_exceptions=False)
async with AsyncClient(transport=transport, base_url="https://test") as c:
yield c
async def login(client, email: str) -> dict[str, str]:
"""开发档登录:返回需附加的请求头(Cookie 由客户端自动携带)。"""
resp = await client.post("/v1/auth/sessions", json={"email": email})
assert resp.status_code == 201, resp.text
return {"Hunjing-CSRF-Token": resp.json()["csrf_token"]}