- 认证: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
59 lines
1.9 KiB
Python
59 lines
1.9 KiB
Python
"""pytest 夹具:统一连接 PostgreSQL 测试库(ADR-024,不用 SQLite 替代)。"""
|
|
|
|
import os
|
|
|
|
import pytest
|
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
|
|
from app import models # noqa: F401 注册模型
|
|
from app.db.base import Base
|
|
|
|
TEST_DATABASE_URL = os.environ.get(
|
|
"TEST_DATABASE_URL",
|
|
"postgresql+asyncpg://hunjing:hunjing_dev_password@127.0.0.1:5432/hunjing_test",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
async def db() -> async_sessionmaker[AsyncSession]:
|
|
"""每个测试重建 schema:隔离且可重复。"""
|
|
engine = create_async_engine(TEST_DATABASE_URL)
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.drop_all)
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
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"]}
|