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