video-compliance-ai/backend/tests/test_risk_exceptions_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

138 lines
5.3 KiB
Python

"""
特例审批 API 测试 (TDD - 红色阶段)
要求: 48 小时超时自动拒绝 + 必须留痕
"""
import pytest
from datetime import datetime, timedelta, timezone
from httpx import AsyncClient
from app.schemas.review import (
RiskExceptionRecord,
RiskExceptionStatus,
)
class TestRiskExceptionCRUD:
"""特例记录基础流程"""
@pytest.mark.asyncio
async def test_create_exception_returns_201(self, client: AsyncClient, tenant_id: str, applicant_id: str, approver_id: str):
"""创建特例返回 201"""
now = datetime.now(timezone.utc)
response = await client.post(
"/api/v1/risk-exceptions",
headers={"X-Tenant-ID": tenant_id},
json={
"applicant_id": applicant_id,
"target_type": "influencer",
"target_id": "influencer-001",
"risk_rule_id": "rule-absolute-word",
"reason_category": "业务强需",
"justification": "业务需要短期投放",
"attachment_url": "https://example.com/attach.png",
"current_approver_id": approver_id,
"valid_start_time": now.isoformat(),
"valid_end_time": (now + timedelta(days=7)).isoformat(),
}
)
assert response.status_code == 201
parsed = RiskExceptionRecord.model_validate(response.json())
assert parsed.status == RiskExceptionStatus.PENDING
assert parsed.current_approver_id == approver_id
@pytest.mark.asyncio
async def test_get_exception_returns_200(self, client: AsyncClient, tenant_id: str, applicant_id: str, approver_id: str):
"""查询特例记录返回 200"""
headers = {"X-Tenant-ID": tenant_id}
now = datetime.now(timezone.utc)
create_resp = await client.post(
"/api/v1/risk-exceptions",
headers=headers,
json={
"applicant_id": applicant_id,
"target_type": "content",
"target_id": "content-001",
"risk_rule_id": "rule-soft-risk",
"reason_category": "误判",
"justification": "内容无违规",
"current_approver_id": approver_id,
"valid_start_time": now.isoformat(),
"valid_end_time": (now + timedelta(days=3)).isoformat(),
}
)
record_id = create_resp.json()["record_id"]
response = await client.get(
f"/api/v1/risk-exceptions/{record_id}",
headers=headers,
)
assert response.status_code == 200
parsed = RiskExceptionRecord.model_validate(response.json())
assert parsed.record_id == record_id
@pytest.mark.asyncio
async def test_approve_exception_updates_status(self, client: AsyncClient, tenant_id: str, applicant_id: str, approver_id: str):
"""审批通过后状态更新为 approved"""
headers = {"X-Tenant-ID": tenant_id}
now = datetime.now(timezone.utc)
create_resp = await client.post(
"/api/v1/risk-exceptions",
headers=headers,
json={
"applicant_id": applicant_id,
"target_type": "order",
"target_id": "order-001",
"risk_rule_id": "rule-competitor",
"reason_category": "测试豁免",
"justification": "测试流程",
"current_approver_id": approver_id,
"valid_start_time": now.isoformat(),
"valid_end_time": (now + timedelta(days=1)).isoformat(),
}
)
record_id = create_resp.json()["record_id"]
response = await client.post(
f"/api/v1/risk-exceptions/{record_id}/approve",
headers=headers,
json={
"approver_id": approver_id,
"comment": "同意",
}
)
assert response.status_code == 200
parsed = RiskExceptionRecord.model_validate(response.json())
assert parsed.status == RiskExceptionStatus.APPROVED
@pytest.mark.asyncio
async def test_reject_exception_requires_reason(self, client: AsyncClient, tenant_id: str, applicant_id: str, approver_id: str):
"""驳回时需要理由"""
headers = {"X-Tenant-ID": tenant_id}
now = datetime.now(timezone.utc)
create_resp = await client.post(
"/api/v1/risk-exceptions",
headers=headers,
json={
"applicant_id": applicant_id,
"target_type": "influencer",
"target_id": "influencer-002",
"risk_rule_id": "rule-absolute-word",
"reason_category": "业务强需",
"justification": "需要豁免",
"current_approver_id": approver_id,
"valid_start_time": now.isoformat(),
"valid_end_time": (now + timedelta(days=2)).isoformat(),
}
)
record_id = create_resp.json()["record_id"]
response = await client.post(
f"/api/v1/risk-exceptions/{record_id}/reject",
headers=headers,
json={
"approver_id": approver_id,
"comment": "",
}
)
assert response.status_code == 422