Files
hunjing/backend/tests/conftest.py
T
chenxin 701c20c783 ✨ feat: WP-08 媒体基础与 CI(上传/文件/任务/声契 + 三任务流水线)
- 迁移 15:upload(三态)、file_asset(sha256/存储键/pending-ready-quarantined)、media_job(四类五态+进度+取消)、voice_consent(用途/期限/撤回)
- 上传三步流(契约第 9 节开发档):POST uploads(MIME 白名单+大小约束)→ PUT data(真实写 storage/ 落盘、sha256 校验)→ completions 转 ready——未完成文件不可被业务引用
- 文件内容取回(开发档本地;正式部署换短期签名 URL,存储键不变)
- 媒体任务:ASR 输入必须 ready 音频;后台执行经注入 Provider(测试 mock 确定性转写);无 Provider 显式失败(fail-closed);取消幂等
- 声契:TTS/克隆需有效声契(未撤回未过期);撤回后新任务 403(ADR 媒体域“撤回停止新生成”)
- CI(WP-14 起步):三任务流水线——backend(pgvector 服务容器 + ruff/mypy/pytest)、engine(CMake 警告即错误 + ctest)、openapi(Redocly lint)
- 测试 91 项全绿(+5)
2026-09-12 10:50:37 +08:00

128 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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
# 后台 Run 任务使用测试库会话工厂与 mock 流(生产为独立工厂与真实传输)
app.state.session_factory = db
app.state.gw_transport = _mock_openai_stream()
app.state.media_provider = _mock_media_provider()
import os as _os
_os.environ.setdefault("HJ_MOCK_KEY", "test-key")
from app.models import LogicalModel, ProviderEndpoint
async with db() as session:
session.add(LogicalModel(id="default-chat", capabilities=["chat"]))
session.add(
ProviderEndpoint(
logical_model_id="default-chat",
provider="openai_compatible",
name="mock-endpoint",
base_url="http://mock.internal",
model_id="mock-chat",
api_key_env="HJ_MOCK_KEY",
priority=1,
)
)
await session.commit()
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 支持多用户交替)。
同时保留 cookie jar 自动存储:不带 headers 的旧式请求仍走 jar。
显式 Cookie header 优先于 jar(httpx 行为),交替测试不串会话。
"""
resp = await client.post("/v1/auth/sessions", json={"email": email})
assert resp.status_code == 201, resp.text
from app.auth.service import SESSION_COOKIE
token = resp.cookies.get(SESSION_COOKIE)
assert token, "登录未签发会话 Cookie"
return {
"Hunjing-CSRF-Token": resp.json()["csrf_token"],
"Cookie": f"{SESSION_COOKIE}={token}",
}
def _mock_openai_stream():
"""OpenAI-compatible 流式 mock:真实 SSE 字节流(data: {...} 分块 + usage)。"""
import httpx
sse = (
b'data: {"choices":[{"delta":{"content":"\\u4f60\\u597d"}}]}\n\n'
b'data: {"choices":[{"delta":{"content":","}}]}\n\n'
b'data: {"choices":[{"delta":{"content":"\\u9b42\\u9559"}}]}\n\n'
b'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\n'
b'data: {"choices":[],"usage":{"prompt_tokens":12,'
b'"completion_tokens":6,"total_tokens":18}}\n\n'
b'data: [DONE]\n\n'
)
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(
200, headers={"content-type": "text/event-stream"}, content=sse
)
return httpx.MockTransport(handler)
def _mock_media_provider():
"""媒体 Provider mock:ASR 返回确定性转写(测试注入;生产经配置)。"""
async def provider(kind: str, input_: dict) -> dict:
if kind == "asr":
return {"transcript": "(开发档转写)你好,魂镜", "language": "zh"}
if kind == "image_gen":
return {"file_id": None, "description": "generated"}
raise RuntimeError(f"unsupported kind {kind}")
return provider