test(P3-1): 第59波 Schema验证+FeatureFlags单测(+72) #851
@@ -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
|
||||
|
||||
Executable
+251
@@ -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
|
||||
Reference in New Issue
Block a user