"""Feature Flag 单元测试。 测试 FeatureFlagConfig、InMemoryFeatureFlagStore、RedisFeatureFlagStore 的核心逻辑。 """ from __future__ import annotations from unittest.mock import MagicMock from packages.adapters.redis.feature_flag_store import ( FeatureFlagConfig, InMemoryFeatureFlagStore, ) # ── FeatureFlagConfig 测试 ────────────────────────────────────────────────── class TestFeatureFlagConfig: """FeatureFlagConfig 核心逻辑测试。""" def test_default_disabled(self): """默认配置为关闭状态。""" config = FeatureFlagConfig(name="test_flag") assert config.enabled is False assert config.percentage == 0 assert config.whitelist == set() assert config.is_active() is False assert config.is_active("user1") is False def test_global_enabled_100_percent(self): """100% + 启用 = 全部命中。""" config = FeatureFlagConfig(name="test_flag", enabled=True, percentage=100) assert config.is_active() is True assert config.is_active("user1") is True assert config.is_active("any_user") is True def test_global_enabled_0_percent_no_whitelist(self): """启用但 0% 且无白名单 = 不命中。""" config = FeatureFlagConfig(name="test_flag", enabled=True, percentage=0) assert config.is_active() is False assert config.is_active("user1") is False def test_whitelist_takes_priority(self): """白名单优先级高于百分比。""" config = FeatureFlagConfig( name="test_flag", enabled=True, percentage=0, whitelist={"user1", "user2"}, ) assert config.is_active("user1") is True assert config.is_active("user2") is True assert config.is_active("user3") is False def test_whitelist_with_percentage(self): """白名单用户即使百分比为0也命中,非白名单按百分比。""" config = FeatureFlagConfig( name="test_flag", enabled=True, percentage=100, # 100% 所有人命中 whitelist={"user1"}, ) assert config.is_active("user1") is True assert config.is_active("user999") is True # 100% 命中 def test_percentage_consistency_same_user(self): """同一用户多次调用结果一致(哈希确定性)。""" config = FeatureFlagConfig(name="test_flag", enabled=True, percentage=50) results = [config.is_active("user_fixed") for _ in range(100)] assert all(r == results[0] for r in results) def test_percentage_different_users_distributed(self): """不同用户分布大致符合百分比(统计检验,宽松阈值)。""" config = FeatureFlagConfig(name="test_flag", enabled=True, percentage=50) active_count = sum(1 for i in range(1000) if config.is_active(f"user_{i}")) # 50% 上下浮动 10% 都算合理 assert 400 <= active_count <= 600, f"Expected ~500, got {active_count}" def test_percentage_boundary_0_and_100(self): """0% 和 100% 的边界情况。""" config_0 = FeatureFlagConfig(name="test", enabled=True, percentage=0) config_100 = FeatureFlagConfig(name="test", enabled=True, percentage=100) for i in range(100): assert config_0.is_active(f"user_{i}") is False assert config_100.is_active(f"user_{i}") is True def test_disabled_ignores_all_other_settings(self): """关闭时忽略白名单和百分比。""" config = FeatureFlagConfig( name="test_flag", enabled=False, percentage=100, whitelist={"user1"}, ) assert config.is_active("user1") is False assert config.is_active() is False def test_none_identifier_with_percentage(self): """无 identifier 时按随机比例(0% 和 100% 是确定的)。""" config_0 = FeatureFlagConfig(name="test", enabled=True, percentage=0) config_100 = FeatureFlagConfig(name="test", enabled=True, percentage=100) assert config_0.is_active(None) is False assert config_100.is_active(None) is True def test_to_dict_and_from_dict(self): """序列化和反序列化对称。""" original = FeatureFlagConfig( name="test_flag", enabled=True, percentage=30, whitelist={"user_a", "user_b", "user_c"}, ) data = original.to_dict() restored = FeatureFlagConfig.from_dict(data) assert restored.name == original.name assert restored.enabled == original.enabled assert restored.percentage == original.percentage assert restored.whitelist == original.whitelist def test_from_dict_with_missing_fields(self): """from_dict 缺失字段时使用默认值。""" config = FeatureFlagConfig.from_dict({"name": "minimal"}) assert config.name == "minimal" assert config.enabled is False assert config.percentage == 0 assert config.whitelist == set() # ── InMemoryFeatureFlagStore 测试 ─────────────────────────────────────────── class TestInMemoryFeatureFlagStore: """内存存储实现测试。""" def test_get_nonexistent_returns_default(self): """获取不存在的 flag 返回默认配置(关闭)。""" store = InMemoryFeatureFlagStore() config = store.get("nonexistent") assert config.name == "nonexistent" assert config.enabled is False def test_set_and_get(self): """设置后可以读取。""" store = InMemoryFeatureFlagStore() config = FeatureFlagConfig(name="test", enabled=True, percentage=50, whitelist={"u1"}) store.set(config) got = store.get("test") assert got.enabled is True assert got.percentage == 50 assert got.whitelist == {"u1"} def test_delete_existing(self): """删除存在的 flag 返回 True。""" store = InMemoryFeatureFlagStore() store.set(FeatureFlagConfig(name="test", enabled=True)) assert store.delete("test") is True assert store.get("test").enabled is False def test_delete_nonexistent(self): """删除不存在的 flag 返回 False。""" store = InMemoryFeatureFlagStore() assert store.delete("nonexistent") is False def test_list_all(self): """列出所有 flag。""" store = InMemoryFeatureFlagStore() store.set(FeatureFlagConfig(name="flag_a", enabled=True)) store.set(FeatureFlagConfig(name="flag_b", percentage=10)) all_flags = store.list_all() assert len(all_flags) == 2 assert "flag_a" in all_flags assert "flag_b" in all_flags assert all_flags["flag_a"].enabled is True def test_is_active_convenience(self): """is_active 便捷方法。""" store = InMemoryFeatureFlagStore() store.set(FeatureFlagConfig(name="render", enabled=True, percentage=0, whitelist={"vip_user"})) assert store.is_active("render", "vip_user") is True assert store.is_active("render", "normal_user") is False assert store.is_active("nonexistent") is False # ── RedisFeatureFlagStore 降级测试(无 Redis 环境) ─────────────────────── class TestRedisStoreDegradation: """Redis 不可用时的降级行为测试。""" def test_get_returns_default_when_redis_unavailable(self): """Redis 连接失败时返回默认关闭配置,不抛异常。""" from packages.adapters.redis import feature_flag_store as ff_module # 模拟 redis 模块不存在的场景不好做,这里直接测试异常捕获逻辑 store = ff_module.RedisFeatureFlagStore.__new__(ff_module.RedisFeatureFlagStore) store._redis = MagicMock() store._redis.hgetall.side_effect = ConnectionError("Redis down") store._key_prefix = ff_module.FEATURE_FLAG_REDIS_PREFIX store._cache = {} store._cache_ttl = 5.0 import threading store._lock = threading.Lock() config = store.get("render_engine") assert config.enabled is False assert config.name == "render_engine" def test_list_all_returns_empty_on_redis_error(self): """Redis 错误时 list_all 返回空字典。""" from packages.adapters.redis import feature_flag_store as ff_module store = ff_module.RedisFeatureFlagStore.__new__(ff_module.RedisFeatureFlagStore) store._redis = MagicMock() store._redis.scan.side_effect = ConnectionError("Redis down") store._key_prefix = ff_module.FEATURE_FLAG_REDIS_PREFIX store._cache = {} store._cache_ttl = 5.0 import threading store._lock = threading.Lock() result = store.list_all() assert result == {} class TestRedisStoreListAll: """RedisFeatureFlagStore list_all 正常路径测试。""" def _make_store(self): from packages.adapters.redis import feature_flag_store as ff_module store = ff_module.RedisFeatureFlagStore.__new__(ff_module.RedisFeatureFlagStore) store._redis = MagicMock() store._key_prefix = ff_module.FEATURE_FLAG_REDIS_PREFIX store._cache = {} store._cache_ttl = 5.0 import threading store._lock = threading.Lock() return store def test_list_all_scan_with_match_param(self): """list_all 调用 redis.scan 时使用正确的 match 参数名。""" store = self._make_store() prefix = store._key_prefix # 模拟 scan 返回 2 个 key,分 2 次游标 store._redis.scan.side_effect = [ (10, [f"{prefix}render_engine", f"{prefix}other_flag"]), (0, []), ] # 模拟 hgetall 返回配置 store._redis.hgetall.return_value = { b"enabled": b"true", b"percentage": b"50", b"whitelist": b'["user1","user2"]', } result = store.list_all() # 验证 scan 被调用了 2 次(游标遍历) assert store._redis.scan.call_count == 2 # 验证参数名是 match(不是 match_pattern) first_call_kwargs = store._redis.scan.call_args_list[0][1] assert "match" in first_call_kwargs assert "match_pattern" not in first_call_kwargs assert first_call_kwargs["match"] == f"{prefix}*" # 验证返回了 2 个 flag assert len(result) == 2 assert "render_engine" in result assert "other_flag" in result