videos1.0/backend/tests/unit/test_validators.py
Your Name e77af7f8f0 feat: 实现 TDD 绿色阶段核心模块
实现以下模块并通过全部测试 (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>
2026-02-02 17:41:37 +08:00

250 lines
7.9 KiB
Python
Raw Permalink 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.

"""
数据验证器单元测试
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