Your Name e4959d584f feat: 完善代理商端业务逻辑与前后端框架
主要更新:
- 更新代理商端文档,明确项目由品牌方分配流程
- 新增Brief配置详情页(已配置)设计稿
- 完善工作台紧急待办中品牌新任务功能
- 整理Pencil设计文件中代理商端页面顺序
- 新增后端FastAPI框架及核心API
- 新增前端Next.js页面和组件库
- 添加.gitignore排除构建和缓存文件

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 19:27:31 +08:00

465 lines
11 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 配置和 fixtures
测试覆盖: 数据库会话、HTTP 客户端、Mock 数据
使用 app.dependency_overrides 实现测试隔离(支持并行测试)
"""
import pytest
import asyncio
import uuid
from typing import AsyncGenerator
from unittest.mock import AsyncMock, MagicMock
from httpx import AsyncClient, ASGITransport
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.config import settings
from app.database import get_db
from app.models.base import Base
from app.services.health import (
MockHealthChecker,
get_health_checker,
)
@pytest.fixture(scope="session")
def event_loop():
"""创建事件循环session 级别)"""
policy = asyncio.get_event_loop_policy()
loop = policy.new_event_loop()
yield loop
loop.close()
# ==================== 数据库测试 Fixtures ====================
@pytest.fixture(scope="function")
async def test_db_engine():
"""创建测试数据库引擎(使用 SQLite 内存数据库)"""
engine = create_async_engine(
"sqlite+aiosqlite:///:memory:",
echo=False,
future=True,
)
# 创建所有表
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
# 清理
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await engine.dispose()
@pytest.fixture(scope="function")
async def test_db_session(test_db_engine):
"""创建测试数据库会话"""
async_session_factory = sessionmaker(
test_db_engine,
class_=AsyncSession,
expire_on_commit=False,
)
async with async_session_factory() as session:
yield session
@pytest.fixture
async def client(test_db_session) -> AsyncGenerator[AsyncClient, None]:
"""
创建异步测试客户端(使用测试数据库)
Yields:
AsyncClient: httpx 异步客户端
"""
# 覆盖数据库依赖
async def override_get_db():
yield test_db_session
app.dependency_overrides[get_db] = override_get_db
transport = ASGITransport(app=app, raise_app_exceptions=False)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
# 每个测试结束后清理 dependency_overrides
app.dependency_overrides.clear()
@pytest.fixture
async def client_no_db() -> AsyncGenerator[AsyncClient, None]:
"""
创建异步测试客户端(不使用数据库,用于简单测试)
Yields:
AsyncClient: httpx 异步客户端
"""
transport = ASGITransport(app=app, raise_app_exceptions=False)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
app.dependency_overrides.clear()
@pytest.fixture
def mock_health_checker(client: AsyncClient):
"""
创建 Mock 健康检查器(所有依赖健康)
使用 FastAPI dependency_overrides 实现隔离
Yields:
MockHealthChecker: mock 实例
"""
checker = MockHealthChecker(database_healthy=True, redis_healthy=True)
app.dependency_overrides[get_health_checker] = lambda: checker
yield checker
# 清理由 client fixture 统一处理
@pytest.fixture
def mock_unhealthy_db_checker(client: AsyncClient):
"""
创建 Mock 健康检查器(数据库不健康)
Yields:
MockHealthChecker: mock 实例
"""
checker = MockHealthChecker(database_healthy=False, redis_healthy=True)
app.dependency_overrides[get_health_checker] = lambda: checker
yield checker
@pytest.fixture
def mock_unhealthy_redis_checker(client: AsyncClient):
"""
创建 Mock 健康检查器Redis 不健康)
Yields:
MockHealthChecker: mock 实例
"""
checker = MockHealthChecker(database_healthy=True, redis_healthy=False)
app.dependency_overrides[get_health_checker] = lambda: checker
yield checker
@pytest.fixture
def mock_all_unhealthy_checker(client: AsyncClient):
"""
创建 Mock 健康检查器(所有依赖不健康)
Yields:
MockHealthChecker: mock 实例
"""
checker = MockHealthChecker(database_healthy=False, redis_healthy=False)
app.dependency_overrides[get_health_checker] = lambda: checker
yield checker
@pytest.fixture
def app_settings():
"""
获取应用配置(用于测试断言)
Returns:
Settings: 应用配置实例
"""
return settings
# ==================== 通用测试数据 Fixtures ====================
def _unique(prefix: str) -> str:
return f"{prefix}-{uuid.uuid4().hex[:8]}"
@pytest.fixture
def tenant_id() -> str:
return _unique("tenant")
@pytest.fixture
def brand_id() -> str:
return _unique("brand")
@pytest.fixture
def other_brand_id() -> str:
return _unique("brand")
@pytest.fixture
def creator_id() -> str:
return _unique("creator")
@pytest.fixture
def influencer_id() -> str:
return _unique("influencer")
@pytest.fixture
def applicant_id() -> str:
return _unique("applicant")
@pytest.fixture
def approver_id() -> str:
return _unique("approver")
@pytest.fixture
def video_url() -> str:
return f"https://example.com/video-{uuid.uuid4().hex[:8]}.mp4"
@pytest.fixture
def forbidden_word() -> str:
return f"测试违禁词-{uuid.uuid4().hex[:6]}"
@pytest.fixture
def whitelist_term() -> str:
return f"品牌专属词-{uuid.uuid4().hex[:6]}"
@pytest.fixture
def competitor_name() -> str:
return f"竞品-{uuid.uuid4().hex[:6]}"
# ==================== 集成测试 Fixtures ====================
# 使用 testcontainers 运行真实依赖,标记为 integration
def _is_docker_available() -> bool:
"""检查 Docker 是否可用"""
import subprocess
try:
result = subprocess.run(
["docker", "info"],
capture_output=True,
timeout=5,
)
return result.returncode == 0
except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
return False
# 在模块加载时检查一次 Docker 可用性
_docker_available = None
def docker_available() -> bool:
"""获取 Docker 可用性(缓存结果)"""
global _docker_available
if _docker_available is None:
_docker_available = _is_docker_available()
return _docker_available
@pytest.fixture(scope="session")
def postgres_container():
"""
启动 PostgreSQL 容器(集成测试用)
需要 Docker 运行
Yields:
PostgresContainer: 容器实例
"""
pytest.importorskip("testcontainers")
if not docker_available():
pytest.skip("Docker is not available")
from testcontainers.postgres import PostgresContainer
with PostgresContainer("postgres:15-alpine") as postgres:
yield postgres
@pytest.fixture(scope="session")
def redis_container():
"""
启动 Redis 容器(集成测试用)
需要 Docker 运行
Yields:
RedisContainer: 容器实例
"""
pytest.importorskip("testcontainers")
if not docker_available():
pytest.skip("Docker is not available")
from testcontainers.redis import RedisContainer
with RedisContainer("redis:7-alpine") as redis:
yield redis
# ==================== Mock 数据 Fixtures ====================
@pytest.fixture
def mock_ai_response():
"""
AI 审核响应 mock 数据
Returns:
dict: 模拟的 AI 审核结果
"""
return {
"violations": [],
"score": 95,
"summary": "内容合规",
"details": {
"forbidden_words": [],
"logo_detected": True,
"duration_valid": True,
}
}
@pytest.fixture
def mock_ai_violation_response():
"""
AI 审核违规响应 mock 数据
Returns:
dict: 模拟的违规审核结果
"""
return {
"violations": [
{
"type": "forbidden_word",
"content": "最好",
"position": {"start": 10, "end": 12},
"severity": "medium",
"suggestion": "建议删除或替换为其他词汇",
}
],
"score": 65,
"summary": "发现1处违规",
"details": {
"forbidden_words": ["最好"],
"logo_detected": True,
"duration_valid": True,
}
}
@pytest.fixture
def sample_video_metadata():
"""
示例视频元数据
Returns:
dict: 视频元数据
"""
return {
"id": "video-001",
"title": "测试视频",
"duration": 30,
"resolution": "1080p",
"creator_id": "creator-001",
"platform": "douyin",
}
@pytest.fixture
def sample_task_data():
"""
示例审核任务数据
Returns:
dict: 任务数据
"""
return {
"video_url": "https://example.com/video.mp4",
"platform": "douyin",
"creator_id": "creator-001",
"priority": "normal",
"rules": ["ad_law", "platform_rules"],
}
# ==================== AI 配置相关 Fixtures ====================
@pytest.fixture
def mock_ai_models_response():
"""Mock 模型列表响应"""
return {
"success": True,
"models": {
"text": [
{"id": "gpt-4o", "name": "GPT-4o"},
{"id": "claude-3-opus", "name": "Claude 3 Opus"},
],
"vision": [
{"id": "gpt-4o", "name": "GPT-4o"},
{"id": "qwen-vl-max", "name": "Qwen VL Max"},
],
"audio": [
{"id": "whisper-1", "name": "Whisper"},
{"id": "whisper-large-v3", "name": "Whisper Large V3"},
],
},
}
@pytest.fixture
def mock_connection_test_success():
"""Mock 连接测试成功响应"""
return {
"success": True,
"results": {
"text": {"success": True, "latency_ms": 342, "model": "gpt-4o"},
"vision": {"success": True, "latency_ms": 528, "model": "gpt-4o"},
"audio": {"success": True, "latency_ms": 215, "model": "whisper-1"},
},
"message": "所有模型连接成功",
}
@pytest.fixture
def mock_connection_test_partial_fail():
"""Mock 连接测试部分失败响应"""
return {
"success": False,
"results": {
"text": {"success": True, "latency_ms": 342, "model": "gpt-4o"},
"vision": {"success": True, "latency_ms": 528, "model": "gpt-4o"},
"audio": {"success": False, "error": "Model not found", "model": "invalid-model"},
},
"message": "1 个模型连接失败,请检查模型名称或 API 权限",
}
# ==================== AI 客户端 Mock Fixtures ====================
@pytest.fixture
def mock_ai_client():
"""创建 Mock AI 客户端"""
client = MagicMock()
client.chat_completion = AsyncMock(return_value=MagicMock(
content="[]",
model="gpt-4o",
usage={"prompt_tokens": 100, "completion_tokens": 50, "total_tokens": 150},
finish_reason="stop",
))
client.vision_analysis = AsyncMock(return_value=MagicMock(
content="无竞品 Logo",
model="gpt-4o",
usage={"prompt_tokens": 200, "completion_tokens": 50, "total_tokens": 250},
finish_reason="stop",
))
client.test_connection = AsyncMock(return_value=MagicMock(
success=True,
latency_ms=100,
error=None,
))
client.close = AsyncMock()
return client