实现以下模块并通过全部测试 (150 passed, 92.65% coverage):
- validators.py: 数据验证器 (Brief/视频/审核决策/申诉/时间戳/UUID)
- timestamp_align.py: 多模态时间戳对齐 (ASR/OCR/CV 融合)
- rule_engine.py: 规则引擎 (违禁词检测/语境感知/规则版本管理)
- brief_parser.py: Brief 解析 (卖点/禁忌词/时序要求/品牌调性提取)
- video_auditor.py: 视频审核 (文件验证/ASR/OCR/Logo检测/合规检查)
验收标准达成:
- 违禁词召回率 ≥ 95%
- 误报率 ≤ 5%
- 时长统计误差 ≤ 0.5秒
- 语境感知检测 ("最开心的一天" 不误判)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
250 lines
7.9 KiB
Python
250 lines
7.9 KiB
Python
"""
|
||
数据验证器单元测试
|
||
|
||
TDD 测试用例 - 验证所有输入数据的格式和约束
|
||
"""
|
||
|
||
import pytest
|
||
from typing import Any
|
||
|
||
from app.utils.validators import (
|
||
BriefValidator,
|
||
VideoValidator,
|
||
ReviewDecisionValidator,
|
||
AppealValidator,
|
||
TimestampValidator,
|
||
UUIDValidator,
|
||
)
|
||
|
||
|
||
class TestBriefValidator:
|
||
"""Brief 数据验证测试"""
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.parametrize("platform,expected_valid", [
|
||
("douyin", True),
|
||
("xiaohongshu", True),
|
||
("bilibili", True),
|
||
("kuaishou", True),
|
||
("weibo", False), # 暂不支持
|
||
("unknown", False),
|
||
("", False),
|
||
(None, False),
|
||
])
|
||
def test_platform_validation(self, platform: str | None, expected_valid: bool) -> None:
|
||
"""测试平台验证"""
|
||
validator = BriefValidator()
|
||
result = validator.validate_platform(platform)
|
||
assert result.is_valid == expected_valid
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.parametrize("region,expected_valid", [
|
||
("mainland_china", True),
|
||
("hk_tw", True),
|
||
("overseas", True),
|
||
("unknown", False),
|
||
("", False),
|
||
])
|
||
def test_region_validation(self, region: str, expected_valid: bool) -> None:
|
||
"""测试区域验证"""
|
||
validator = BriefValidator()
|
||
result = validator.validate_region(region)
|
||
assert result.is_valid == expected_valid
|
||
|
||
@pytest.mark.unit
|
||
def test_selling_points_structure(self) -> None:
|
||
"""测试卖点结构验证"""
|
||
valid_selling_points = [
|
||
{"text": "24小时持妆", "priority": "high"},
|
||
{"text": "天然成分", "priority": "medium"},
|
||
]
|
||
|
||
invalid_selling_points = [
|
||
{"text": ""}, # 缺少 priority,文本为空
|
||
"just a string", # 格式错误
|
||
]
|
||
|
||
validator = BriefValidator()
|
||
|
||
assert validator.validate_selling_points(valid_selling_points).is_valid
|
||
assert not validator.validate_selling_points(invalid_selling_points).is_valid
|
||
|
||
|
||
class TestVideoValidator:
|
||
"""视频数据验证测试"""
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.parametrize("duration_seconds,expected_valid", [
|
||
(30, True),
|
||
(60, True),
|
||
(300, True), # 5 分钟
|
||
(1800, True), # 30 分钟 - 边界
|
||
(3600, False), # 1 小时 - 超过限制
|
||
(0, False),
|
||
(-1, False),
|
||
])
|
||
def test_duration_validation(self, duration_seconds: int, expected_valid: bool) -> None:
|
||
"""测试视频时长验证"""
|
||
validator = VideoValidator()
|
||
result = validator.validate_duration(duration_seconds)
|
||
assert result.is_valid == expected_valid
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.parametrize("resolution,expected_valid", [
|
||
("1920x1080", True), # 1080p
|
||
("1080x1920", True), # 竖屏 1080p
|
||
("3840x2160", True), # 4K
|
||
("1280x720", True), # 720p
|
||
("640x480", False), # 480p - 太低
|
||
("320x240", False),
|
||
])
|
||
def test_resolution_validation(self, resolution: str, expected_valid: bool) -> None:
|
||
"""测试分辨率验证"""
|
||
validator = VideoValidator()
|
||
result = validator.validate_resolution(resolution)
|
||
assert result.is_valid == expected_valid
|
||
|
||
|
||
class TestReviewDecisionValidator:
|
||
"""审核决策验证测试"""
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.parametrize("decision,expected_valid", [
|
||
("passed", True),
|
||
("rejected", True),
|
||
("force_passed", True),
|
||
("pending", False), # 无效决策
|
||
("unknown", False),
|
||
("", False),
|
||
])
|
||
def test_decision_type_validation(self, decision: str, expected_valid: bool) -> None:
|
||
"""测试决策类型验证"""
|
||
validator = ReviewDecisionValidator()
|
||
result = validator.validate_decision_type(decision)
|
||
assert result.is_valid == expected_valid
|
||
|
||
@pytest.mark.unit
|
||
def test_force_pass_requires_reason(self) -> None:
|
||
"""测试强制通过必须填写原因"""
|
||
# 强制通过但无原因
|
||
invalid_request = {
|
||
"decision": "force_passed",
|
||
"force_pass_reason": "",
|
||
}
|
||
|
||
# 强制通过有原因
|
||
valid_request = {
|
||
"decision": "force_passed",
|
||
"force_pass_reason": "达人玩的新梗,品牌方认可",
|
||
}
|
||
|
||
validator = ReviewDecisionValidator()
|
||
|
||
assert not validator.validate(invalid_request).is_valid
|
||
assert "原因" in validator.validate(invalid_request).error_message
|
||
|
||
assert validator.validate(valid_request).is_valid
|
||
|
||
@pytest.mark.unit
|
||
def test_rejection_requires_violations(self) -> None:
|
||
"""测试驳回必须选择违规项"""
|
||
# 驳回但无选择违规项
|
||
invalid_request = {
|
||
"decision": "rejected",
|
||
"selected_violations": [],
|
||
}
|
||
|
||
# 驳回并选择违规项
|
||
valid_request = {
|
||
"decision": "rejected",
|
||
"selected_violations": ["violation_001", "violation_002"],
|
||
}
|
||
|
||
validator = ReviewDecisionValidator()
|
||
|
||
assert not validator.validate(invalid_request).is_valid
|
||
assert validator.validate(valid_request).is_valid
|
||
|
||
|
||
class TestAppealValidator:
|
||
"""申诉验证测试"""
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.parametrize("reason_length,expected_valid", [
|
||
(5, False), # < 10 字
|
||
(9, False), # < 10 字
|
||
(10, True), # = 10 字
|
||
(50, True), # > 10 字
|
||
(500, True), # 长文本
|
||
])
|
||
def test_appeal_reason_length(self, reason_length: int, expected_valid: bool) -> None:
|
||
"""测试申诉理由长度 - 必须 ≥ 10 字"""
|
||
reason = "字" * reason_length
|
||
|
||
validator = AppealValidator()
|
||
result = validator.validate_reason(reason)
|
||
assert result.is_valid == expected_valid
|
||
|
||
@pytest.mark.unit
|
||
def test_appeal_token_check(self) -> None:
|
||
"""测试申诉令牌检查"""
|
||
validator = AppealValidator()
|
||
|
||
# 有令牌
|
||
result = validator.validate_token_available(user_id="user_001", token_count=3)
|
||
assert result.is_valid
|
||
|
||
# 无令牌
|
||
result = validator.validate_token_available(user_id="user_no_tokens", token_count=0)
|
||
assert not result.is_valid
|
||
|
||
|
||
class TestTimestampValidator:
|
||
"""时间戳验证测试"""
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.parametrize("timestamp_ms,video_duration_ms,expected_valid", [
|
||
(0, 60000, True), # 开始
|
||
(30000, 60000, True), # 中间
|
||
(60000, 60000, True), # 结束
|
||
(-1, 60000, False), # 负数
|
||
(70000, 60000, False), # 超出视频时长
|
||
])
|
||
def test_timestamp_range_validation(
|
||
self,
|
||
timestamp_ms: int,
|
||
video_duration_ms: int,
|
||
expected_valid: bool,
|
||
) -> None:
|
||
"""测试时间戳范围验证"""
|
||
validator = TimestampValidator()
|
||
result = validator.validate_range(timestamp_ms, video_duration_ms)
|
||
assert result.is_valid == expected_valid
|
||
|
||
@pytest.mark.unit
|
||
def test_timestamp_order_validation(self) -> None:
|
||
"""测试时间戳顺序验证 - start < end"""
|
||
validator = TimestampValidator()
|
||
|
||
assert validator.validate_order(start=1000, end=2000).is_valid
|
||
assert not validator.validate_order(start=2000, end=1000).is_valid
|
||
assert not validator.validate_order(start=1000, end=1000).is_valid
|
||
|
||
|
||
class TestUUIDValidator:
|
||
"""UUID 验证测试"""
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.parametrize("uuid_str,expected_valid", [
|
||
("550e8400-e29b-41d4-a716-446655440000", True),
|
||
("550E8400-E29B-41D4-A716-446655440000", True), # 大写
|
||
("not-a-uuid", False),
|
||
("", False),
|
||
("12345", False),
|
||
])
|
||
def test_uuid_format_validation(self, uuid_str: str, expected_valid: bool) -> None:
|
||
"""测试 UUID 格式验证"""
|
||
validator = UUIDValidator()
|
||
result = validator.validate(uuid_str)
|
||
assert result.is_valid == expected_valid
|