video-compliance-ai/backend/tests/test_tasks_api.py
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

429 lines
15 KiB
Python

"""
审核任务 API 测试 (TDD - 红色阶段)
测试覆盖: 创建任务、查询任务、更新任务状态
"""
import pytest
from httpx import AsyncClient
from app.schemas.review import TaskResponse, TaskListResponse, TaskStatus
class TestCreateTask:
"""创建审核任务"""
@pytest.mark.asyncio
async def test_create_task_returns_201(self, client: AsyncClient, tenant_id: str, video_url: str, creator_id: str):
"""创建任务返回 201"""
response = await client.post(
"/api/v1/tasks",
headers={"X-Tenant-ID": tenant_id},
json={
"platform": "douyin",
"creator_id": creator_id,
"video_url": video_url,
}
)
assert response.status_code == 201
@pytest.mark.asyncio
async def test_create_task_returns_task_id(self, client: AsyncClient, tenant_id: str, video_url: str, creator_id: str):
"""创建任务返回任务 ID"""
response = await client.post(
"/api/v1/tasks",
headers={"X-Tenant-ID": tenant_id},
json={
"platform": "douyin",
"creator_id": creator_id,
"video_url": video_url,
}
)
data = response.json()
parsed = TaskResponse.model_validate(data)
assert parsed.task_id
@pytest.mark.asyncio
async def test_create_task_initial_status_pending(self, client: AsyncClient, tenant_id: str, video_url: str, creator_id: str):
"""创建任务初始状态为 pending"""
response = await client.post(
"/api/v1/tasks",
headers={"X-Tenant-ID": tenant_id},
json={
"platform": "douyin",
"creator_id": creator_id,
"video_url": video_url,
}
)
data = response.json()
parsed = TaskResponse.model_validate(data)
assert parsed.status == TaskStatus.PENDING
@pytest.mark.asyncio
async def test_create_task_validates_platform(self, client: AsyncClient, tenant_id: str, video_url: str, creator_id: str):
"""创建任务校验平台参数"""
response = await client.post(
"/api/v1/tasks",
headers={"X-Tenant-ID": tenant_id},
json={
"platform": "invalid_platform",
"creator_id": creator_id,
"video_url": video_url,
}
)
assert response.status_code == 422
@pytest.mark.asyncio
async def test_create_task_validates_video_url(self, client: AsyncClient, tenant_id: str, creator_id: str):
"""创建任务校验视频 URL"""
response = await client.post(
"/api/v1/tasks",
headers={"X-Tenant-ID": tenant_id},
json={
"video_url": "not-a-url",
"platform": "douyin",
"creator_id": creator_id,
}
)
assert response.status_code == 422
@pytest.mark.asyncio
async def test_create_task_allows_missing_video(self, client: AsyncClient, tenant_id: str, creator_id: str):
"""创建任务允许暂不上传视频"""
response = await client.post(
"/api/v1/tasks",
headers={"X-Tenant-ID": tenant_id},
json={
"platform": "douyin",
"creator_id": creator_id,
}
)
data = response.json()
parsed = TaskResponse.model_validate(data)
assert parsed.has_video is False
@pytest.mark.asyncio
async def test_create_task_with_script_content(self, client: AsyncClient, tenant_id: str, creator_id: str):
"""创建任务可携带脚本内容"""
response = await client.post(
"/api/v1/tasks",
headers={"X-Tenant-ID": tenant_id},
json={
"platform": "douyin",
"creator_id": creator_id,
"script_content": "脚本内容示例",
}
)
data = response.json()
parsed = TaskResponse.model_validate(data)
assert parsed.has_script is True
assert parsed.script_content == "脚本内容示例"
class TestGetTask:
"""查询审核任务"""
@pytest.mark.asyncio
async def test_get_task_returns_200(self, client: AsyncClient, tenant_id: str, video_url: str, creator_id: str):
"""查询存在的任务返回 200"""
headers = {"X-Tenant-ID": tenant_id}
# 先创建任务
create_resp = await client.post(
"/api/v1/tasks",
headers=headers,
json={
"platform": "douyin",
"creator_id": creator_id,
"video_url": video_url,
}
)
task_id = create_resp.json()["task_id"]
# 查询任务
response = await client.get(f"/api/v1/tasks/{task_id}", headers=headers)
assert response.status_code == 200
@pytest.mark.asyncio
async def test_get_task_returns_task_details(self, client: AsyncClient, tenant_id: str, video_url: str, creator_id: str):
"""查询任务返回完整信息"""
headers = {"X-Tenant-ID": tenant_id}
create_resp = await client.post(
"/api/v1/tasks",
headers=headers,
json={
"video_url": video_url,
"platform": "douyin",
"creator_id": creator_id,
}
)
task_id = create_resp.json()["task_id"]
response = await client.get(f"/api/v1/tasks/{task_id}", headers=headers)
data = response.json()
parsed = TaskResponse.model_validate(data)
assert parsed.task_id == task_id
assert parsed.video_url == video_url
assert parsed.platform.value == "douyin"
assert parsed.creator_id == creator_id
assert parsed.has_video is True
assert parsed.created_at
@pytest.mark.asyncio
async def test_get_nonexistent_task_returns_404(self, client: AsyncClient, tenant_id: str):
"""查询不存在的任务返回 404"""
response = await client.get(
"/api/v1/tasks/nonexistent-task-id",
headers={"X-Tenant-ID": tenant_id},
)
assert response.status_code == 404
class TestListTasks:
"""任务列表查询"""
@pytest.mark.asyncio
async def test_list_tasks_returns_200(self, client: AsyncClient, tenant_id: str):
"""查询任务列表返回 200"""
response = await client.get(
"/api/v1/tasks",
headers={"X-Tenant-ID": tenant_id},
)
assert response.status_code == 200
@pytest.mark.asyncio
async def test_list_tasks_returns_array(self, client: AsyncClient, tenant_id: str):
"""查询任务列表返回数组"""
response = await client.get(
"/api/v1/tasks",
headers={"X-Tenant-ID": tenant_id},
)
data = response.json()
parsed = TaskListResponse.model_validate(data)
assert isinstance(parsed.items, list)
@pytest.mark.asyncio
async def test_list_tasks_pagination(self, client: AsyncClient, tenant_id: str):
"""任务列表支持分页"""
response = await client.get(
"/api/v1/tasks?page=1&page_size=10",
headers={"X-Tenant-ID": tenant_id},
)
data = response.json()
parsed = TaskListResponse.model_validate(data)
assert parsed.page == 1
assert parsed.page_size == 10
@pytest.mark.asyncio
async def test_list_tasks_filter_by_status(self, client: AsyncClient, tenant_id: str, video_url: str, creator_id: str):
"""任务列表支持按状态筛选"""
headers = {"X-Tenant-ID": tenant_id}
create_resp = await client.post(
"/api/v1/tasks",
headers=headers,
json={
"video_url": video_url,
"platform": "douyin",
"creator_id": creator_id,
}
)
task_id = create_resp.json()["task_id"]
response = await client.get("/api/v1/tasks?status=pending", headers=headers)
assert response.status_code == 200
data = response.json()
parsed = TaskListResponse.model_validate(data)
assert any(item.task_id == task_id for item in parsed.items)
@pytest.mark.asyncio
async def test_list_tasks_filter_by_platform(self, client: AsyncClient, tenant_id: str, video_url: str, creator_id: str):
"""任务列表支持按平台筛选"""
headers = {"X-Tenant-ID": tenant_id}
create_resp = await client.post(
"/api/v1/tasks",
headers=headers,
json={
"video_url": video_url,
"platform": "douyin",
"creator_id": creator_id,
}
)
task_id = create_resp.json()["task_id"]
response = await client.get("/api/v1/tasks?platform=douyin", headers=headers)
assert response.status_code == 200
data = response.json()
parsed = TaskListResponse.model_validate(data)
assert any(item.task_id == task_id for item in parsed.items)
class TestUploadTaskAssets:
"""任务脚本/视频上传"""
@pytest.mark.asyncio
async def test_upload_script_requires_payload(self, client: AsyncClient, tenant_id: str, creator_id: str):
"""上传脚本必须提供内容或文件 URL"""
headers = {"X-Tenant-ID": tenant_id}
create_resp = await client.post(
"/api/v1/tasks",
headers=headers,
json={
"platform": "douyin",
"creator_id": creator_id,
}
)
task_id = create_resp.json()["task_id"]
response = await client.post(
f"/api/v1/tasks/{task_id}/script",
headers=headers,
json={},
)
assert response.status_code == 422
@pytest.mark.asyncio
async def test_upload_script_updates_task(self, client: AsyncClient, tenant_id: str, creator_id: str):
"""上传脚本更新任务内容"""
headers = {"X-Tenant-ID": tenant_id}
create_resp = await client.post(
"/api/v1/tasks",
headers=headers,
json={
"platform": "douyin",
"creator_id": creator_id,
}
)
task_id = create_resp.json()["task_id"]
response = await client.post(
f"/api/v1/tasks/{task_id}/script",
headers=headers,
json={"script_content": "更新后的脚本"},
)
data = response.json()
parsed = TaskResponse.model_validate(data)
assert parsed.has_script is True
assert parsed.script_content == "更新后的脚本"
@pytest.mark.asyncio
async def test_upload_video_updates_task(self, client: AsyncClient, tenant_id: str, creator_id: str, video_url: str):
"""上传视频更新任务视频 URL"""
headers = {"X-Tenant-ID": tenant_id}
create_resp = await client.post(
"/api/v1/tasks",
headers=headers,
json={
"platform": "douyin",
"creator_id": creator_id,
}
)
task_id = create_resp.json()["task_id"]
response = await client.post(
f"/api/v1/tasks/{task_id}/video",
headers=headers,
json={"video_url": video_url},
)
data = response.json()
parsed = TaskResponse.model_validate(data)
assert parsed.has_video is True
assert parsed.video_url == video_url
class TestUpdateTaskStatus:
"""更新任务状态"""
@pytest.mark.asyncio
async def test_approve_task_returns_200(self, client: AsyncClient, tenant_id: str, video_url: str, creator_id: str):
"""通过任务返回 200"""
headers = {"X-Tenant-ID": tenant_id}
# 创建任务
create_resp = await client.post(
"/api/v1/tasks",
headers=headers,
json={
"video_url": video_url,
"platform": "douyin",
"creator_id": creator_id,
}
)
task_id = create_resp.json()["task_id"]
# 通过任务
response = await client.post(
f"/api/v1/tasks/{task_id}/approve",
headers=headers,
json={"comment": "审核通过"}
)
assert response.status_code == 200
@pytest.mark.asyncio
async def test_approve_task_updates_status(self, client: AsyncClient, tenant_id: str, video_url: str, creator_id: str):
"""通过任务更新状态为 approved"""
headers = {"X-Tenant-ID": tenant_id}
create_resp = await client.post(
"/api/v1/tasks",
headers=headers,
json={
"video_url": video_url,
"platform": "douyin",
"creator_id": creator_id,
}
)
task_id = create_resp.json()["task_id"]
await client.post(
f"/api/v1/tasks/{task_id}/approve",
headers=headers,
json={"comment": "审核通过"}
)
# 验证状态
get_resp = await client.get(f"/api/v1/tasks/{task_id}", headers=headers)
parsed = TaskResponse.model_validate(get_resp.json())
assert parsed.status == TaskStatus.APPROVED
@pytest.mark.asyncio
async def test_reject_task_returns_200(self, client: AsyncClient, tenant_id: str, video_url: str, creator_id: str):
"""驳回任务返回 200"""
headers = {"X-Tenant-ID": tenant_id}
create_resp = await client.post(
"/api/v1/tasks",
headers=headers,
json={
"video_url": video_url,
"platform": "douyin",
"creator_id": creator_id,
}
)
task_id = create_resp.json()["task_id"]
response = await client.post(
f"/api/v1/tasks/{task_id}/reject",
headers=headers,
json={"reason": "违规内容", "violations": ["forbidden_word"]}
)
assert response.status_code == 200
get_resp = await client.get(f"/api/v1/tasks/{task_id}", headers=headers)
parsed = TaskResponse.model_validate(get_resp.json())
assert parsed.status == TaskStatus.REJECTED
@pytest.mark.asyncio
async def test_reject_task_requires_reason(self, client: AsyncClient, tenant_id: str, video_url: str, creator_id: str):
"""驳回任务必须提供原因"""
headers = {"X-Tenant-ID": tenant_id}
create_resp = await client.post(
"/api/v1/tasks",
headers=headers,
json={
"video_url": video_url,
"platform": "douyin",
"creator_id": creator_id,
}
)
task_id = create_resp.json()["task_id"]
response = await client.post(
f"/api/v1/tasks/{task_id}/reject",
headers=headers,
json={}
)
assert response.status_code == 422