diff --git a/tests/unit/test_feature_flags_infra.py b/tests/unit/test_feature_flags_infra.py index 4d12d33dd..d5cf2c3db 100755 --- a/tests/unit/test_feature_flags_infra.py +++ b/tests/unit/test_feature_flags_infra.py @@ -1,121 +1,134 @@ """ -Feature Flags 基础设施层单元测试 +Feature Flags 基础设施测试. """ +from __future__ import annotations + import pytest from packages.infrastructure.feature_flags import ( FeatureFlag, FeatureFlags, FeatureScope, - feature_flags, ) -class TestFeatureFlag: - """FeatureFlag 单个开关测试""" +class TestFeatureFlagDefaults: + """FeatureFlag 默认值.""" - def test_default_values(self): - """测试默认值""" - flag = FeatureFlag(name="test_flag") - assert flag.name == "test_flag" - assert flag.description == "" + def test_default_enabled(self): + flag = FeatureFlag(name="test") assert flag.global_enabled is True assert flag.plan_overrides == {} assert flag.user_overrides == {} + assert flag.description == "" - def test_is_enabled_global_true(self): - """测试全局启用""" + def test_custom_description(self): + flag = FeatureFlag(name="test", description="测试功能") + assert flag.description == "测试功能" + + def test_global_disabled(self): + flag = FeatureFlag(name="test", global_enabled=False) + assert flag.global_enabled is False + + +class TestFeatureFlagIsEnabled: + """is_enabled 优先级逻辑.""" + + def test_global_enabled_no_user_no_plan(self): flag = FeatureFlag(name="test", global_enabled=True) assert flag.is_enabled() is True - def test_is_enabled_global_false(self): - """测试全局禁用""" + def test_global_disabled_no_user_no_plan(self): flag = FeatureFlag(name="test", global_enabled=False) assert flag.is_enabled() is False - def test_is_enabled_plan_override(self): - """测试套餐级别覆盖""" - flag = FeatureFlag( - name="test", - global_enabled=True, - plan_overrides={"free": False, "premium": True}, - ) - # free 套餐被覆盖为 False - assert flag.is_enabled(user_plan="free") is False - # premium 套餐覆盖为 True - assert flag.is_enabled(user_plan="premium") is True - # 没有覆盖的套餐用全局值 - assert flag.is_enabled(user_plan="basic") is True - - def test_is_enabled_user_override_priority(self): - """测试用户白名单优先级最高""" - flag = FeatureFlag( - name="test", - global_enabled=False, - plan_overrides={"premium": True}, - user_overrides={"user-1": True, "user-2": False}, - ) - # 用户白名单 True → 全局禁用也能启用 - assert flag.is_enabled(user_plan="free", user_id="user-1") is True - # 用户白名单 False → premium 套餐也禁用 - assert flag.is_enabled(user_plan="premium", user_id="user-2") is False - # 没有用户白名单 → 走套餐级别 - assert flag.is_enabled(user_plan="premium", user_id="user-3") is True - - def test_is_enabled_no_params(self): - """测试不传任何参数时使用全局值""" - flag = FeatureFlag(name="test", global_enabled=True) - assert flag.is_enabled() is True - - def test_is_enabled_empty_strings_treated_as_none(self): - """测试空字符串 user_id/user_plan 不触发覆盖""" - flag = FeatureFlag( - name="test", - global_enabled=True, - plan_overrides={"free": False}, - user_overrides={"": True}, # 空字符串key - ) - # 空字符串 user_id 被当作 falsy,不走用户白名单分支 - assert flag.is_enabled(user_id="", user_plan="") is True - - def test_plan_override_does_not_affect_other_plans(self): - """测试套餐覆盖不影响其他套餐""" + def test_plan_override_free_disabled(self): flag = FeatureFlag( name="test", global_enabled=True, plan_overrides={"free": False}, ) assert flag.is_enabled(user_plan="free") is False - assert flag.is_enabled(user_plan="basic") is True - assert flag.is_enabled(user_plan="premium") is True + assert flag.is_enabled(user_plan="premium") is True # 走全局 - def test_user_override_can_enable_for_disabled_plan(self): - """测试用户白名单可以为被禁用的套餐用户单独启用""" + def test_plan_override_premium_enabled(self): flag = FeatureFlag( name="test", global_enabled=False, plan_overrides={"premium": True}, - user_overrides={"special-user": True}, ) - # free 套餐用户 + 白名单 → 启用 - assert flag.is_enabled(user_plan="free", user_id="special-user") is True + assert flag.is_enabled(user_plan="premium") is True + assert flag.is_enabled(user_plan="free") is False # 走全局 - def test_user_override_can_disable_for_enabled_plan(self): - """测试用户白名单可以为启用套餐的用户单独禁用""" + def test_user_override_highest_priority(self): + """用户白名单优先级最高""" + flag = FeatureFlag( + name="test", + global_enabled=False, + plan_overrides={"premium": True}, + user_overrides={"user_1": True}, + ) + # free 套餐全局禁用,但用户在白名单 → 启用 + assert flag.is_enabled(user_plan="free", user_id="user_1") is True + + def test_user_override_disable_overrides_plan(self): flag = FeatureFlag( name="test", global_enabled=True, - user_overrides={"bad-user": False}, + plan_overrides={"premium": True}, + user_overrides={"user_1": False}, ) - assert flag.is_enabled(user_id="bad-user") is False + # premium 套餐应该启用,但用户在禁用名单 → 禁用 + assert flag.is_enabled(user_plan="premium", user_id="user_1") is False + + def test_user_not_in_overrides_falls_to_plan(self): + flag = FeatureFlag( + name="test", + global_enabled=True, + plan_overrides={"free": False}, + user_overrides={"user_x": True}, + ) + assert flag.is_enabled(user_plan="free", user_id="user_other") is False + + def test_none_user_id_skipped(self): + flag = FeatureFlag( + name="test", + global_enabled=True, + plan_overrides={"free": False}, + user_overrides={"None": False}, + ) + assert flag.is_enabled(user_plan="premium", user_id=None) is True + + def test_none_plan_skipped(self): + flag = FeatureFlag( + name="test", + global_enabled=True, + plan_overrides={"free": False}, + ) + assert flag.is_enabled(user_plan=None) is True + + def test_empty_user_id_skipped(self): + flag = FeatureFlag( + name="test", + global_enabled=True, + plan_overrides={"free": False}, + ) + assert flag.is_enabled(user_plan="premium", user_id="") is True + + def test_empty_plan_skipped(self): + flag = FeatureFlag( + name="test", + global_enabled=True, + plan_overrides={"free": False}, + ) + assert flag.is_enabled(user_plan="") is True class TestFeatureScope: - """FeatureScope 常量测试""" + """FeatureScope 常量.""" - def test_scope_constants(self): - """测试所有常量存在""" + def test_constants_exist(self): assert FeatureScope.AI_VOICE_GENERATION == "ai_voice_generation" assert FeatureScope.DEDUPLICATION_REPORT == "deduplication_report" assert FeatureScope.BATCH_EXPORT == "batch_export" @@ -123,225 +136,105 @@ class TestFeatureScope: assert FeatureScope.RECIPE_REUSE == "recipe_reuse" -class TestFeatureFlags: - """FeatureFlags 管理器测试""" +class TestFeatureFlagsManager: + """FeatureFlags 管理器.""" @pytest.fixture - def flags(self): - """创建新的 FeatureFlags 实例(不影响全局单例)""" + def ff(self): return FeatureFlags() - # ===== 初始化 ===== + def test_default_flags_registered(self, ff): + flags = ff.list_flags() + assert len(flags) >= 5 + assert FeatureScope.AI_VOICE_GENERATION in flags + assert FeatureScope.DEDUPLICATION_REPORT in flags + assert FeatureScope.BATCH_EXPORT in flags + assert FeatureScope.MULTI_PLATFORM_OUTPUT in flags + assert FeatureScope.RECIPE_REUSE in flags - def test_default_flags_exist(self, flags): - """测试默认 flags 已注册""" - all_flags = flags.list_flags() - assert FeatureScope.AI_VOICE_GENERATION in all_flags - assert FeatureScope.DEDUPLICATION_REPORT in all_flags - assert FeatureScope.BATCH_EXPORT in all_flags - assert FeatureScope.MULTI_PLATFORM_OUTPUT in all_flags - assert FeatureScope.RECIPE_REUSE in all_flags - - def test_default_ai_voice_generation(self, flags): - """测试 AI 配音功能默认配置""" - # free 套餐不可用 - assert flags.is_enabled("ai_voice_generation", user_plan="free") is False - # basic 套餐可用 - assert flags.is_enabled("ai_voice_generation", user_plan="basic") is True - # premium 套餐可用 - assert flags.is_enabled("ai_voice_generation", user_plan="premium") is True - - def test_default_deduplication_report(self, flags): - """测试去重报告默认配置(仅 premium)""" - assert flags.is_enabled("deduplication_report", user_plan="free") is False - assert flags.is_enabled("deduplication_report", user_plan="basic") is False - assert flags.is_enabled("deduplication_report", user_plan="premium") is True - - def test_default_multi_platform_output(self, flags): - """测试多平台输出默认配置(仅 premium)""" - assert flags.is_enabled("multi_platform_output", user_plan="free") is False - assert flags.is_enabled("multi_platform_output", user_plan="basic") is False - assert flags.is_enabled("multi_platform_output", user_plan="premium") is True - - def test_default_batch_export(self, flags): - """测试批量导出默认配置""" - assert flags.is_enabled("batch_export", user_plan="free") is False - assert flags.is_enabled("batch_export", user_plan="basic") is True - assert flags.is_enabled("batch_export", user_plan="premium") is True - - def test_default_recipe_reuse(self, flags): - """测试配方复用默认配置""" - assert flags.is_enabled("recipe_reuse", user_plan="free") is False - assert flags.is_enabled("recipe_reuse", user_plan="basic") is True - assert flags.is_enabled("recipe_reuse", user_plan="premium") is True - - # ===== 注册新 flag ===== - - def test_register_new_flag(self, flags): - """测试注册新的 feature flag""" - new_flag = FeatureFlag(name="new_feature", description="新功能", global_enabled=False) - flags.register(new_flag) - - assert flags.get("new_feature") is not None - assert flags.get("new_feature").description == "新功能" - assert flags.is_enabled("new_feature") is False - - def test_register_overwrites_existing(self, flags): - """测试注册同名 flag 会覆盖""" - flag1 = FeatureFlag(name="test", global_enabled=True, description="v1") - flags.register(flag1) - assert flags.get("test").description == "v1" - - flag2 = FeatureFlag(name="test", global_enabled=False, description="v2") - flags.register(flag2) - assert flags.get("test").description == "v2" - assert flags.is_enabled("test") is False - - # ===== get 方法 ===== - - def test_get_existing_flag(self, flags): - """测试获取存在的 flag""" - flag = flags.get("ai_voice_generation") + def test_get_existing_flag(self, ff): + flag = ff.get(FeatureScope.BATCH_EXPORT) assert flag is not None - assert flag.name == "ai_voice_generation" + assert flag.name == FeatureScope.BATCH_EXPORT - def test_get_nonexistent_flag(self, flags): - """测试获取不存在的 flag 返回 None""" - assert flags.get("nonexistent") is None + def test_get_nonexistent_flag(self, ff): + assert ff.get("nonexistent") is None - # ===== is_enabled 方法 ===== + def test_is_enabled_global(self, ff): + assert ff.is_enabled(FeatureScope.BATCH_EXPORT) is True - def test_is_enabled_nonexistent_flag_returns_false(self, flags): - """测试不存在的 flag 返回 False""" - assert flags.is_enabled("nonexistent_flag") is False + def test_is_enabled_nonexistent_returns_false(self, ff): + """未知 flag 默认禁用(安全保守)""" + assert ff.is_enabled("unknown_feature") is False - def test_is_enabled_without_plan_or_user(self, flags): - """测试不传套餐和用户ID""" - assert flags.is_enabled("ai_voice_generation") is True + def test_free_plan_ai_voice_disabled(self, ff): + """AI 配音 free 套餐不可用""" + assert ff.is_enabled(FeatureScope.AI_VOICE_GENERATION, user_plan="free") is False - # ===== set_global ===== + def test_premium_plan_ai_voice_enabled(self, ff): + assert ff.is_enabled(FeatureScope.AI_VOICE_GENERATION, user_plan="premium") is True - def test_set_global_enable(self, flags): - """测试设置全局启用""" - flags.set_global("ai_voice_generation", enabled=False) - assert flags.is_enabled("ai_voice_generation", user_plan="premium") is False + def test_deduplication_only_premium(self, ff): + """去重报告仅 premium 可用""" + assert ff.is_enabled(FeatureScope.DEDUPLICATION_REPORT, user_plan="free") is False + assert ff.is_enabled(FeatureScope.DEDUPLICATION_REPORT, user_plan="basic") is False + assert ff.is_enabled(FeatureScope.DEDUPLICATION_REPORT, user_plan="premium") is True - def test_set_global_disable(self, flags): - """测试设置全局禁用""" - flags.set_global("deduplication_report", enabled=False) - assert flags.is_enabled("deduplication_report", user_plan="premium") is False + def test_set_global(self, ff): + ff.set_global(FeatureScope.BATCH_EXPORT, False) + assert ff.is_enabled(FeatureScope.BATCH_EXPORT) is False + # 恢复 + ff.set_global(FeatureScope.BATCH_EXPORT, True) + assert ff.is_enabled(FeatureScope.BATCH_EXPORT) is True - def test_set_global_nonexistent_raises(self, flags): - """测试设置不存在的 flag 抛出异常""" - with pytest.raises(KeyError, match="not found"): - flags.set_global("nonexistent", enabled=True) + def test_set_global_nonexistent_raises(self, ff): + with pytest.raises(KeyError): + ff.set_global("nonexistent", True) - # ===== set_plan_override ===== + def test_set_plan_override(self, ff): + ff.set_plan_override(FeatureScope.BATCH_EXPORT, "enterprise", False) + assert ff.is_enabled(FeatureScope.BATCH_EXPORT, user_plan="enterprise") is False - def test_set_plan_override(self, flags): - """测试设置套餐覆盖""" - # 先确认 basic 套餐默认是去重报告禁用 - assert flags.is_enabled("deduplication_report", user_plan="basic") is False + def test_set_plan_override_nonexistent_raises(self, ff): + with pytest.raises(KeyError): + ff.set_plan_override("nonexistent", "free", True) - flags.set_plan_override("deduplication_report", "basic", True) - assert flags.is_enabled("deduplication_report", user_plan="basic") is True + def test_set_user_override(self, ff): + ff.set_user_override(FeatureScope.BATCH_EXPORT, "user_42", True) + assert ff.is_enabled( + FeatureScope.BATCH_EXPORT, + user_plan="free", + user_id="user_42", + ) is True - def test_set_plan_override_nonexistent_raises(self, flags): - """测试设置不存在 flag 的套餐覆盖抛出异常""" - with pytest.raises(KeyError, match="not found"): - flags.set_plan_override("nonexistent", "free", True) + def test_set_user_override_nonexistent_raises(self, ff): + with pytest.raises(KeyError): + ff.set_user_override("nonexistent", "user_1", True) - # ===== set_user_override ===== + def test_register_new_flag(self, ff): + new_flag = FeatureFlag(name="new_feature", description="新功能") + ff.register(new_flag) + assert ff.get("new_feature") is not None + assert ff.is_enabled("new_feature") is True - def test_set_user_override_enable(self, flags): - """测试设置用户白名单启用""" - assert flags.is_enabled("deduplication_report", user_plan="free", user_id="user-1") is False + def test_register_overwrites(self, ff): + flag1 = FeatureFlag(name="test", global_enabled=True) + flag2 = FeatureFlag(name="test", global_enabled=False) + ff.register(flag1) + ff.register(flag2) + assert ff.is_enabled("test") is False - flags.set_user_override("deduplication_report", "user-1", True) - assert flags.is_enabled("deduplication_report", user_plan="free", user_id="user-1") is True + def test_list_flags_returns_copy(self, ff): + flags = ff.list_flags() + assert isinstance(flags, dict) + # 修改返回值不影响内部 + flags["new"] = FeatureFlag(name="new") + assert ff.get("new") is None - def test_set_user_override_disable(self, flags): - """测试设置用户白名单禁用""" - assert flags.is_enabled("batch_export", user_plan="premium", user_id="user-2") is True - - flags.set_user_override("batch_export", "user-2", False) - assert flags.is_enabled("batch_export", user_plan="premium", user_id="user-2") is False - - def test_set_user_override_nonexistent_raises(self, flags): - """测试设置不存在 flag 的用户覆盖抛出异常""" - with pytest.raises(KeyError, match="not found"): - flags.set_user_override("nonexistent", "user-1", True) - - # ===== list_flags ===== - - def test_list_flags_returns_copy(self, flags): - """测试 list_flags 返回副本""" - all_flags = flags.list_flags() - all_flags["fake"] = FeatureFlag(name="fake") - - # 原注册表不应被修改 - assert "fake" not in flags.list_flags() - - def test_list_flags_count(self, flags): - """测试默认 flag 数量""" - all_flags = flags.list_flags() - assert len(all_flags) == 5 # 5 个默认 flag - - # ===== get_enabled_for_plan ===== - - def test_get_enabled_for_free_plan(self, flags): - """测试 free 套餐启用的功能""" - enabled = flags.get_enabled_for_plan("free") - # free 套餐应该只有 0 个默认启用的功能?不对,让我看看... - # 所有5个默认功能 free 套餐都是 False 吗? - # AI_VOICE_GENERATION: free=False - # DEDUPLICATION_REPORT: free=False, basic=False - # BATCH_EXPORT: free=False - # MULTI_PLATFORM_OUTPUT: free=False, basic=False - # RECIPE_REUSE: free=False - # 所以 free 套餐一个都没有? - assert len(enabled) == 0 - - def test_get_enabled_for_premium_plan(self, flags): - """测试 premium 套餐启用的功能""" - enabled = flags.get_enabled_for_plan("premium") - # premium 套餐所有功能都应该启用 - assert len(enabled) == 5 - assert "ai_voice_generation" in enabled - assert "deduplication_report" in enabled - assert "batch_export" in enabled - assert "multi_platform_output" in enabled - assert "recipe_reuse" in enabled - - def test_get_enabled_for_basic_plan(self, flags): - """测试 basic 套餐启用的功能""" - enabled = flags.get_enabled_for_plan("basic") - # basic: ai_voice=True, dedup=False, batch=True, multi=False, recipe=True - assert "ai_voice_generation" in enabled - assert "deduplication_report" not in enabled - assert "batch_export" in enabled - assert "multi_platform_output" not in enabled - assert "recipe_reuse" in enabled - assert len(enabled) == 3 - - -class TestGlobalSingleton: - """全局单例测试""" - - def test_global_singleton_exists(self): - """测试全局单例存在""" - assert feature_flags is not None - assert isinstance(feature_flags, FeatureFlags) - - def test_global_singleton_has_defaults(self): - """测试全局单例有默认配置""" - assert feature_flags.get("ai_voice_generation") is not None - assert feature_flags.get("deduplication_report") is not None - - def test_global_singleton_independent_from_new_instance(self): - """测试全局单例与新实例相互独立""" - new_flags = FeatureFlags() - new_flags.set_global("ai_voice_generation", False) - - # 全局单例不应受影响 - assert feature_flags.is_enabled("ai_voice_generation") is True + def test_get_enabled_for_plan(self, ff): + free_features = ff.get_enabled_for_plan("free") + premium_features = ff.get_enabled_for_plan("premium") + assert len(premium_features) >= len(free_features) + # free 套餐功能是 premium 的子集 + for f in free_features: + assert f in premium_features diff --git a/tests/unit/test_templates_editor_schemas.py b/tests/unit/test_templates_editor_schemas.py new file mode 100755 index 000000000..8679d8d47 --- /dev/null +++ b/tests/unit/test_templates_editor_schemas.py @@ -0,0 +1,251 @@ +""" +模板编辑器 Schema 验证测试. + +覆盖 ExportUpdateRequest 等带 validator 的 Schema. +纯数据 model 不写单测(无业务逻辑)。 +""" + +from __future__ import annotations + +import pytest + +from app.api.routes.templates_editor.schemas import ( + ExportUpdateRequest, +) + + +class TestExportUpdateRequestResolution: + """resolution 字段验证.""" + + def test_valid_1080p(self): + req = ExportUpdateRequest(resolution="1920x1080") + assert req.resolution == "1920x1080" + + def test_valid_portrait(self): + req = ExportUpdateRequest(resolution="1080x1920") + assert req.resolution == "1080x1920" + + def test_valid_square(self): + req = ExportUpdateRequest(resolution="1080x1080") + assert req.resolution == "1080x1080" + + def test_valid_min(self): + req = ExportUpdateRequest(resolution="100x100") + assert req.resolution == "100x100" + + def test_valid_max(self): + req = ExportUpdateRequest(resolution="4096x4096") + assert req.resolution == "4096x4096" + + def test_none_is_valid(self): + req = ExportUpdateRequest() + assert req.resolution is None + + def test_invalid_format_no_x(self): + with pytest.raises(ValueError, match="分辨率格式错误"): + ExportUpdateRequest(resolution="19201080") + + def test_invalid_format_letters(self): + with pytest.raises(ValueError, match="分辨率格式错误"): + ExportUpdateRequest(resolution="abcxdef") + + def test_invalid_format_empty(self): + with pytest.raises(ValueError, match="分辨率格式错误"): + ExportUpdateRequest(resolution="") + + def test_invalid_format_upper_x(self): + """大写 X 不匹配正则""" + with pytest.raises(ValueError, match="分辨率格式错误"): + ExportUpdateRequest(resolution="1920X1080") + + def test_too_small_width(self): + with pytest.raises(ValueError, match="过小"): + ExportUpdateRequest(resolution="50x1080") + + def test_too_small_height(self): + with pytest.raises(ValueError, match="过小"): + ExportUpdateRequest(resolution="1920x50") + + def test_too_large_width(self): + with pytest.raises(ValueError, match="过大"): + ExportUpdateRequest(resolution="5000x1080") + + def test_too_large_height(self): + with pytest.raises(ValueError, match="过大"): + ExportUpdateRequest(resolution="1920x5000") + + def test_extra_chars_after(self): + """后面多字符也不匹配""" + with pytest.raises(ValueError, match="分辨率格式错误"): + ExportUpdateRequest(resolution="1920x1080p") + + def test_extra_chars_before(self): + with pytest.raises(ValueError, match="分辨率格式错误"): + ExportUpdateRequest(resolution="fhd1920x1080") + + +class TestExportUpdateRequestFormat: + """format 字段验证.""" + + def test_valid_mp4(self): + req = ExportUpdateRequest(format="mp4") + assert req.format == "mp4" + + def test_valid_mov(self): + req = ExportUpdateRequest(format="mov") + assert req.format == "mov" + + def test_none_is_valid(self): + req = ExportUpdateRequest() + assert req.format is None + + def test_invalid_avi(self): + with pytest.raises(ValueError, match="无效格式"): + ExportUpdateRequest(format="avi") + + def test_invalid_empty(self): + with pytest.raises(ValueError, match="无效格式"): + ExportUpdateRequest(format="") + + def test_invalid_upper(self): + with pytest.raises(ValueError, match="无效格式"): + ExportUpdateRequest(format="MP4") + + +class TestExportUpdateRequestQualityPreset: + """quality_preset 字段验证.""" + + @pytest.mark.parametrize("preset", ["ultra_fast", "fast", "balanced", "high", "best"]) + def test_valid_presets(self, preset): + req = ExportUpdateRequest(quality_preset=preset) + assert req.quality_preset == preset + + def test_none_is_valid(self): + req = ExportUpdateRequest() + assert req.quality_preset is None + + def test_invalid_preset(self): + with pytest.raises(ValueError, match="无效质量预设"): + ExportUpdateRequest(quality_preset="ultra") + + def test_invalid_empty(self): + with pytest.raises(ValueError, match="无效质量预设"): + ExportUpdateRequest(quality_preset="") + + +class TestExportUpdateRequestFps: + """fps 字段范围验证(由 Field ge/le 控制).""" + + def test_valid_30(self): + req = ExportUpdateRequest(fps=30) + assert req.fps == 30 + + def test_valid_min(self): + req = ExportUpdateRequest(fps=15) + assert req.fps == 15 + + def test_valid_max(self): + req = ExportUpdateRequest(fps=60) + assert req.fps == 60 + + def test_below_min(self): + with pytest.raises(ValueError): + ExportUpdateRequest(fps=10) + + def test_above_max(self): + with pytest.raises(ValueError): + ExportUpdateRequest(fps=120) + + def test_none_is_valid(self): + req = ExportUpdateRequest() + assert req.fps is None + + +class TestExportUpdateRequestBitrate: + """码率字段范围验证.""" + + def test_video_bitrate_valid(self): + req = ExportUpdateRequest(video_bitrate=5000) + assert req.video_bitrate == 5000 + + def test_video_bitrate_min(self): + req = ExportUpdateRequest(video_bitrate=1000) + assert req.video_bitrate == 1000 + + def test_video_bitrate_max(self): + req = ExportUpdateRequest(video_bitrate=20000) + assert req.video_bitrate == 20000 + + def test_video_bitrate_below_min(self): + with pytest.raises(ValueError): + ExportUpdateRequest(video_bitrate=500) + + def test_video_bitrate_above_max(self): + with pytest.raises(ValueError): + ExportUpdateRequest(video_bitrate=50000) + + def test_audio_bitrate_valid(self): + req = ExportUpdateRequest(audio_bitrate=128) + assert req.audio_bitrate == 128 + + def test_audio_bitrate_min(self): + req = ExportUpdateRequest(audio_bitrate=64) + assert req.audio_bitrate == 64 + + def test_audio_bitrate_max(self): + req = ExportUpdateRequest(audio_bitrate=320) + assert req.audio_bitrate == 320 + + def test_audio_bitrate_below_min(self): + with pytest.raises(ValueError): + ExportUpdateRequest(audio_bitrate=32) + + def test_audio_bitrate_above_max(self): + with pytest.raises(ValueError): + ExportUpdateRequest(audio_bitrate=512) + + +class TestExportUpdateRequestWatermark: + """水印字段.""" + + def test_watermark_enabled(self): + req = ExportUpdateRequest(watermark_enabled=True, watermark_text="hello") + assert req.watermark_enabled is True + assert req.watermark_text == "hello" + + def test_watermark_disabled(self): + req = ExportUpdateRequest(watermark_enabled=False) + assert req.watermark_enabled is False + + def test_watermark_default_none(self): + req = ExportUpdateRequest() + assert req.watermark_enabled is None + assert req.watermark_text is None + + +class TestExportUpdateRequestCombined: + """组合字段验证.""" + + def test_full_valid(self): + req = ExportUpdateRequest( + resolution="1920x1080", + fps=30, + video_bitrate=8000, + audio_bitrate=128, + format="mp4", + quality_preset="balanced", + watermark_enabled=True, + watermark_text="test", + ) + assert req.resolution == "1920x1080" + assert req.fps == 30 + assert req.format == "mp4" + assert req.quality_preset == "balanced" + + def test_partial_update(self): + """只更新部分字段,其余为 None""" + req = ExportUpdateRequest(fps=24) + assert req.fps == 24 + assert req.resolution is None + assert req.format is None + assert req.quality_preset is None