Files
DadaoTask/backend/tests/conftest.py
T
Steven 0c53aa7062 feat(m0): 后端骨架 + Web/安卓前端骨架 + 设计文档
- backend: FastAPI 应用工厂、统一错误信封、JWT、短信登录、工作区/成员/邀请、幂等中间件
- alembic 0001: workspace/member/invitation/audit_log/app_user + RLS 策略 + 双角色 GRANT
- tests: 认证/工作区/租户隔离/幂等 四组用例(尚未在本机执行)
- web: React18+Vite 骨架(登录、工作区、邀请落地页)
- android: Compose 骨架(登录、工作区列表)
- 2.Design: PRODUCT-PLAN / ROADMAP-M0-M1 / NAMING / DEVELOPMENT
2026-10-28 02:38:53 +08:00

99 lines
3.0 KiB
Python

"""测试夹具。
约定:
- 一律走真实 HTTP 栈(ASGITransport),不做 handler 级 mock —— 中间件/依赖/RLS 都是被测对象。
- 每个用例前清库,避免串味;清库走**迁移角色**(app 角色没有 TRUNCATE 权限,这是设计使然)。
"""
from __future__ import annotations
import os
os.environ["ENV"] = "test"
os.environ.setdefault("SMS_DEV_CODE", "123456")
import uuid
from collections.abc import AsyncIterator
from typing import Any
import pytest
from alembic import command
from alembic.config import Config
from httpx import ASGITransport, AsyncClient
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.pool import NullPool
from app.config import get_settings
from app.db import engine
from app.main import app
from app.stores import reset_store_for_tests
settings = get_settings()
DEV_CODE = settings.sms_dev_code
_TABLES = "audit_log, invitation, member, workspace, app_user"
def _run_migrations() -> None:
cfg = Config(str(os.path.join(os.path.dirname(__file__), "..", "alembic.ini")))
command.upgrade(cfg, "head")
@pytest.fixture(scope="session", autouse=True)
def migrated_database() -> None:
_run_migrations()
@pytest.fixture(autouse=True)
async def clean_database(migrated_database: None) -> AsyncIterator[None]:
owner_engine = create_async_engine(settings.migration_database_url, poolclass=NullPool)
async with owner_engine.begin() as conn:
await conn.execute(text(f"TRUNCATE {_TABLES} CASCADE"))
await owner_engine.dispose()
reset_store_for_tests()
yield
@pytest.fixture(autouse=True)
async def dispose_app_engine() -> AsyncIterator[None]:
yield
await engine.dispose()
@pytest.fixture
async def client() -> AsyncIterator[AsyncClient]:
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
def auth(token: str) -> dict[str, str]:
return {"Authorization": f"Bearer {token}"}
def unwrap(response: Any) -> Any:
body = response.json()
assert body.get("code") == 0, f"期望成功,实际 {body}"
return body["data"]
async def login(client: AsyncClient, phone: str | None = None) -> dict[str, Any]:
"""发码 + 登录,返回 {token, refresh_token, user_id, phone}。"""
phone = phone or f"138{uuid.uuid4().int % 100_000_000:08d}"
await client.post("/api/v1/auth/sms/send", json={"phone": phone})
data = unwrap(
await client.post("/api/v1/auth/login", json={"phone": phone, "code": DEV_CODE})
)
return {
"token": data["tokens"]["access_token"],
"refresh_token": data["tokens"]["refresh_token"],
"user_id": data["user"]["id"],
"phone": phone,
"headers": auth(data["tokens"]["access_token"]),
}
async def create_workspace(client: AsyncClient, user: dict[str, Any], name: str) -> dict[str, Any]:
return unwrap(await client.post("/api/v1/workspaces", json={"name": name}, headers=user["headers"]))