基于项目需求文档(PRD.md, FeatureSummary.md, DevelopmentPlan.md, UIDesign.md, User_Role_Interfaces.md)编写的 TDD 测试用例。 后端测试 (Python/pytest): - 单元测试: rule_engine, brief_parser, timestamp_alignment, video_auditor, validators - 集成测试: API Brief, Video, Review 端点 - AI 模块测试: ASR, OCR, Logo 检测服务 - 全局 fixtures 和 pytest 配置 前端测试 (TypeScript/Vitest): - 工具函数测试: utils.test.ts - 组件测试: Button, VideoPlayer, ViolationList - Hooks 测试: useVideoAudit, useVideoPlayer, useAppeal - MSW mock handlers 配置 E2E 测试 (Playwright): - 认证流程测试 - 视频上传流程测试 - 视频审核流程测试 - 申诉流程测试 所有测试当前使用 pytest.skip() / it.skip() 作为占位符, 遵循 TDD 红灯阶段 - 等待实现代码后运行。 验收标准覆盖: - ASR WER ≤ 10% - OCR 准确率 ≥ 95% - Logo F1 ≥ 0.85 - 时间戳误差 ≤ 0.5s - 频次统计准确率 ≥ 95% Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
488 lines
18 KiB
Python
488 lines
18 KiB
Python
"""
|
||
审核决策 API 集成测试
|
||
|
||
TDD 测试用例 - 测试审核员操作相关 API 接口
|
||
|
||
接口规范参考:DevelopmentPlan.md 第 7 章
|
||
用户角色参考:User_Role_Interfaces.md
|
||
"""
|
||
|
||
import pytest
|
||
from typing import Any
|
||
|
||
# 导入待实现的模块(TDD 红灯阶段)
|
||
# from httpx import AsyncClient
|
||
# from app.main import app
|
||
|
||
|
||
class TestReviewDecisionAPI:
|
||
"""审核决策 API 测试"""
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_submit_pass_decision(self) -> None:
|
||
"""测试提交通过决策"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# # 以审核员身份登录
|
||
# login_response = await client.post("/api/v1/auth/login", json={
|
||
# "email": "reviewer@test.com",
|
||
# "password": "password"
|
||
# })
|
||
# token = login_response.json()["access_token"]
|
||
# headers = {"Authorization": f"Bearer {token}"}
|
||
#
|
||
# # 提交通过决策
|
||
# response = await client.post(
|
||
# "/api/v1/reviews/video_001/decision",
|
||
# json={
|
||
# "decision": "passed",
|
||
# "comment": "内容符合要求"
|
||
# },
|
||
# headers=headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 200
|
||
# data = response.json()
|
||
# assert data["status"] == "passed"
|
||
# assert "review_id" in data
|
||
pytest.skip("待实现:通过决策 API")
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_submit_reject_decision_with_violations(self) -> None:
|
||
"""测试提交驳回决策 - 必须选择违规项"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# response = await client.post(
|
||
# "/api/v1/reviews/video_001/decision",
|
||
# json={
|
||
# "decision": "rejected",
|
||
# "selected_violations": ["vio_001", "vio_002"],
|
||
# "comment": "存在违规内容"
|
||
# },
|
||
# headers=headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 200
|
||
# data = response.json()
|
||
# assert data["status"] == "rejected"
|
||
# assert len(data["selected_violations"]) == 2
|
||
pytest.skip("待实现:驳回决策 API")
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_reject_without_violations_returns_400(self) -> None:
|
||
"""测试驳回无违规项返回 400"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# response = await client.post(
|
||
# "/api/v1/reviews/video_001/decision",
|
||
# json={
|
||
# "decision": "rejected",
|
||
# "selected_violations": [], # 空违规列表
|
||
# "comment": "驳回"
|
||
# },
|
||
# headers=headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 400
|
||
# assert "违规项" in response.json()["error"]
|
||
pytest.skip("待实现:驳回无违规项测试")
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_submit_force_pass_with_reason(self) -> None:
|
||
"""测试强制通过 - 必须填写原因"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# response = await client.post(
|
||
# "/api/v1/reviews/video_001/decision",
|
||
# json={
|
||
# "decision": "force_passed",
|
||
# "force_pass_reason": "达人玩的新梗,品牌方认可",
|
||
# "comment": "特殊情况强制通过"
|
||
# },
|
||
# headers=headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 200
|
||
# data = response.json()
|
||
# assert data["status"] == "force_passed"
|
||
# assert data["force_pass_reason"] is not None
|
||
pytest.skip("待实现:强制通过 API")
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_force_pass_without_reason_returns_400(self) -> None:
|
||
"""测试强制通过无原因返回 400"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# response = await client.post(
|
||
# "/api/v1/reviews/video_001/decision",
|
||
# json={
|
||
# "decision": "force_passed",
|
||
# "force_pass_reason": "", # 空原因
|
||
# },
|
||
# headers=headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 400
|
||
# assert "原因" in response.json()["error"]
|
||
pytest.skip("待实现:强制通过无原因测试")
|
||
|
||
|
||
class TestViolationEditAPI:
|
||
"""违规项编辑 API 测试"""
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_add_manual_violation(self) -> None:
|
||
"""测试手动添加违规项"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# response = await client.post(
|
||
# "/api/v1/reviews/video_001/violations",
|
||
# json={
|
||
# "type": "other",
|
||
# "content": "手动发现的问题",
|
||
# "timestamp_start": 10.5,
|
||
# "timestamp_end": 15.0,
|
||
# "severity": "medium"
|
||
# },
|
||
# headers=headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 201
|
||
# data = response.json()
|
||
# assert "violation_id" in data
|
||
# assert data["source"] == "manual"
|
||
pytest.skip("待实现:添加手动违规项")
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_delete_ai_violation(self) -> None:
|
||
"""测试删除 AI 检测的违规项"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# response = await client.delete(
|
||
# "/api/v1/reviews/video_001/violations/vio_001",
|
||
# json={
|
||
# "delete_reason": "误检"
|
||
# },
|
||
# headers=headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 200
|
||
# data = response.json()
|
||
# assert data["status"] == "deleted"
|
||
pytest.skip("待实现:删除违规项")
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_modify_violation_severity(self) -> None:
|
||
"""测试修改违规项严重程度"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# response = await client.patch(
|
||
# "/api/v1/reviews/video_001/violations/vio_001",
|
||
# json={
|
||
# "severity": "low",
|
||
# "modify_reason": "风险较低"
|
||
# },
|
||
# headers=headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 200
|
||
# data = response.json()
|
||
# assert data["severity"] == "low"
|
||
pytest.skip("待实现:修改违规严重程度")
|
||
|
||
|
||
class TestAppealAPI:
|
||
"""申诉 API 测试"""
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_submit_appeal_success(self) -> None:
|
||
"""测试提交申诉成功"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# # 以达人身份登录
|
||
# login_response = await client.post("/api/v1/auth/login", json={
|
||
# "email": "creator@test.com",
|
||
# "password": "password"
|
||
# })
|
||
# token = login_response.json()["access_token"]
|
||
# headers = {"Authorization": f"Bearer {token}"}
|
||
#
|
||
# response = await client.post(
|
||
# "/api/v1/reviews/video_001/appeal",
|
||
# json={
|
||
# "violation_ids": ["vio_001"],
|
||
# "reason": "这个词语在此语境下是正常使用,不应被判定为违规"
|
||
# },
|
||
# headers=headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 201
|
||
# data = response.json()
|
||
# assert "appeal_id" in data
|
||
# assert data["status"] == "pending"
|
||
pytest.skip("待实现:提交申诉 API")
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_appeal_reason_too_short_returns_400(self) -> None:
|
||
"""测试申诉理由过短返回 400 - 必须 ≥ 10 字"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# response = await client.post(
|
||
# "/api/v1/reviews/video_001/appeal",
|
||
# json={
|
||
# "violation_ids": ["vio_001"],
|
||
# "reason": "太短了" # < 10 字
|
||
# },
|
||
# headers=creator_headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 400
|
||
# assert "10" in response.json()["error"]
|
||
pytest.skip("待实现:申诉理由过短测试")
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_appeal_token_deduction(self) -> None:
|
||
"""测试申诉扣除令牌"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# # 获取当前令牌数
|
||
# profile_response = await client.get(
|
||
# "/api/v1/users/me",
|
||
# headers=creator_headers
|
||
# )
|
||
# initial_tokens = profile_response.json()["appeal_tokens"]
|
||
#
|
||
# # 提交申诉
|
||
# await client.post(
|
||
# "/api/v1/reviews/video_001/appeal",
|
||
# json={
|
||
# "violation_ids": ["vio_001"],
|
||
# "reason": "这个词语在此语境下是正常使用,不应被判定为违规"
|
||
# },
|
||
# headers=creator_headers
|
||
# )
|
||
#
|
||
# # 验证令牌扣除
|
||
# profile_response = await client.get(
|
||
# "/api/v1/users/me",
|
||
# headers=creator_headers
|
||
# )
|
||
# assert profile_response.json()["appeal_tokens"] == initial_tokens - 1
|
||
pytest.skip("待实现:申诉令牌扣除")
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_appeal_no_token_returns_403(self) -> None:
|
||
"""测试无令牌申诉返回 403"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# # 使用无令牌的用户
|
||
# response = await client.post(
|
||
# "/api/v1/reviews/video_001/appeal",
|
||
# json={
|
||
# "violation_ids": ["vio_001"],
|
||
# "reason": "这个词语在此语境下是正常使用,不应被判定为违规"
|
||
# },
|
||
# headers=no_token_user_headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 403
|
||
# assert "令牌" in response.json()["error"]
|
||
pytest.skip("待实现:无令牌申诉测试")
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_process_appeal_success(self) -> None:
|
||
"""测试处理申诉 - 申诉成功"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# response = await client.post(
|
||
# "/api/v1/reviews/appeals/appeal_001/process",
|
||
# json={
|
||
# "decision": "approved",
|
||
# "comment": "申诉理由成立"
|
||
# },
|
||
# headers=reviewer_headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 200
|
||
# data = response.json()
|
||
# assert data["status"] == "approved"
|
||
pytest.skip("待实现:处理申诉 API")
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_appeal_success_restores_token(self) -> None:
|
||
"""测试申诉成功返还令牌"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# # 获取申诉前令牌数
|
||
# profile_response = await client.get(
|
||
# "/api/v1/users/creator_001",
|
||
# headers=admin_headers
|
||
# )
|
||
# tokens_before = profile_response.json()["appeal_tokens"]
|
||
#
|
||
# # 处理申诉为成功
|
||
# await client.post(
|
||
# "/api/v1/reviews/appeals/appeal_001/process",
|
||
# json={"decision": "approved", "comment": "申诉成立"},
|
||
# headers=reviewer_headers
|
||
# )
|
||
#
|
||
# # 验证令牌返还
|
||
# profile_response = await client.get(
|
||
# "/api/v1/users/creator_001",
|
||
# headers=admin_headers
|
||
# )
|
||
# assert profile_response.json()["appeal_tokens"] == tokens_before + 1
|
||
pytest.skip("待实现:申诉成功返还令牌")
|
||
|
||
|
||
class TestReviewHistoryAPI:
|
||
"""审核历史 API 测试"""
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_get_review_history(self) -> None:
|
||
"""测试获取审核历史"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# response = await client.get(
|
||
# "/api/v1/reviews/video_001/history",
|
||
# headers=headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 200
|
||
# data = response.json()
|
||
#
|
||
# assert "history" in data
|
||
# for entry in data["history"]:
|
||
# assert "timestamp" in entry
|
||
# assert "action" in entry
|
||
# assert "actor" in entry
|
||
pytest.skip("待实现:审核历史 API")
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_review_history_includes_all_actions(self) -> None:
|
||
"""测试审核历史包含所有操作"""
|
||
# TODO: 实现 API 测试
|
||
# 应包含:AI 审核、人工审核、申诉、重新提交等
|
||
pytest.skip("待实现:审核历史完整性")
|
||
|
||
|
||
class TestBatchReviewAPI:
|
||
"""批量审核 API 测试"""
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_batch_pass_videos(self) -> None:
|
||
"""测试批量通过视频"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# response = await client.post(
|
||
# "/api/v1/reviews/batch/decision",
|
||
# json={
|
||
# "video_ids": ["video_001", "video_002", "video_003"],
|
||
# "decision": "passed",
|
||
# "comment": "批量通过"
|
||
# },
|
||
# headers=headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 200
|
||
# data = response.json()
|
||
# assert data["processed_count"] == 3
|
||
# assert data["success_count"] == 3
|
||
pytest.skip("待实现:批量通过 API")
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_batch_review_partial_failure(self) -> None:
|
||
"""测试批量审核部分失败"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# response = await client.post(
|
||
# "/api/v1/reviews/batch/decision",
|
||
# json={
|
||
# "video_ids": ["video_001", "nonexistent_video"],
|
||
# "decision": "passed"
|
||
# },
|
||
# headers=headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 207 # Multi-Status
|
||
# data = response.json()
|
||
# assert data["success_count"] == 1
|
||
# assert data["failure_count"] == 1
|
||
# assert "failures" in data
|
||
pytest.skip("待实现:批量审核部分失败")
|
||
|
||
|
||
class TestReviewPermissionAPI:
|
||
"""审核权限 API 测试"""
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_creator_cannot_review_own_video(self) -> None:
|
||
"""测试达人不能审核自己的视频"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# response = await client.post(
|
||
# "/api/v1/reviews/video_own/decision",
|
||
# json={"decision": "passed"},
|
||
# headers=creator_headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 403
|
||
pytest.skip("待实现:达人审核权限限制")
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_agency_can_review_assigned_videos(self) -> None:
|
||
"""测试 Agency 可以审核分配的视频"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# response = await client.post(
|
||
# "/api/v1/reviews/video_assigned/decision",
|
||
# json={"decision": "passed"},
|
||
# headers=agency_headers
|
||
# )
|
||
#
|
||
# assert response.status_code == 200
|
||
pytest.skip("待实现:Agency 审核权限")
|
||
|
||
@pytest.mark.integration
|
||
@pytest.mark.asyncio
|
||
async def test_brand_can_view_but_not_decide(self) -> None:
|
||
"""测试品牌方可以查看但不能决策"""
|
||
# TODO: 实现 API 测试
|
||
# async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# # 可以查看
|
||
# view_response = await client.get(
|
||
# "/api/v1/reviews/video_001",
|
||
# headers=brand_headers
|
||
# )
|
||
# assert view_response.status_code == 200
|
||
#
|
||
# # 不能决策
|
||
# decision_response = await client.post(
|
||
# "/api/v1/reviews/video_001/decision",
|
||
# json={"decision": "passed"},
|
||
# headers=brand_headers
|
||
# )
|
||
# assert decision_response.status_code == 403
|
||
pytest.skip("待实现:品牌方权限限制")
|