""" FeatureFlagStore 单元测试 覆盖: - FeatureFlagConfig: to_dict / from_dict 序列化 - FeatureFlagConfig.is_active: 全局开关/白名单/百分比哈希 - InMemoryFeatureFlagStore: CRUD / is_active """ import pytest from packages.adapters.redis.feature_flag_store import ( FEATURE_FLAG_REDIS_PREFIX, FeatureFlagConfig, InMemoryFeatureFlagStore, ) # ============================================================ # 常量 # ============================================================ class TestConstants: """常量验证""" def test_redis_prefix(self): assert FEATURE_FLAG_REDIS_PREFIX == "feature_flag:" # ============================================================ # FeatureFlagConfig - 默认值 & 基础 # ============================================================ class TestFeatureFlagConfigDefaults: """FeatureFlagConfig 默认值""" def test_required_name(self): config = FeatureFlagConfig(name="test_flag") assert config.name == "test_flag" def test_default_disabled(self): config = FeatureFlagConfig(name="test_flag") assert config.enabled is False def test_default_percentage_zero(self): config = FeatureFlagConfig(name="test_flag") assert config.percentage == 0 def test_default_whitelist_empty(self): config = FeatureFlagConfig(name="test_flag") assert config.whitelist == set() def test_full_config(self): config = FeatureFlagConfig( name="full_flag", enabled=True, percentage=50, whitelist={"user1", "user2"}, ) assert config.name == "full_flag" assert config.enabled is True assert config.percentage == 50 assert config.whitelist == {"user1", "user2"} # ============================================================ # FeatureFlagConfig - 序列化 # ============================================================ class TestFeatureFlagConfigSerialization: """to_dict / from_dict 序列化""" def test_to_dict_defaults(self): config = FeatureFlagConfig(name="test") d = config.to_dict() assert d["name"] == "test" assert d["enabled"] is False assert d["percentage"] == 0 assert d["whitelist"] == [] def test_to_dict_with_values(self): config = FeatureFlagConfig( name="test", enabled=True, percentage=75, whitelist={"a", "b", "c"}, ) d = config.to_dict() assert d["name"] == "test" assert d["enabled"] is True assert d["percentage"] == 75 # whitelist 排序后输出 assert sorted(d["whitelist"]) == ["a", "b", "c"] def test_from_dict_minimal(self): d = {"name": "test"} config = FeatureFlagConfig.from_dict(d) assert config.name == "test" assert config.enabled is False assert config.percentage == 0 assert config.whitelist == set() def test_from_dict_full(self): d = { "name": "full", "enabled": True, "percentage": 30, "whitelist": ["u1", "u2"], } config = FeatureFlagConfig.from_dict(d) assert config.name == "full" assert config.enabled is True assert config.percentage == 30 assert config.whitelist == {"u1", "u2"} def test_round_trip(self): original = FeatureFlagConfig( name="round_trip", enabled=True, percentage=42, whitelist={"alice", "bob", "charlie"}, ) d = original.to_dict() restored = FeatureFlagConfig.from_dict(d) assert restored.name == original.name assert restored.enabled == original.enabled assert restored.percentage == original.percentage assert restored.whitelist == original.whitelist def test_from_dict_coerces_types(self): """from_dict 应该做类型转换""" d = { "name": "coerce", "enabled": 1, # int → bool "percentage": "50", # str → int "whitelist": ("a", "b"), # tuple → set } config = FeatureFlagConfig.from_dict(d) assert config.enabled is True assert config.percentage == 50 assert config.whitelist == {"a", "b"} # ============================================================ # FeatureFlagConfig.is_active - 全局开关 # ============================================================ class TestIsActiveGlobalSwitch: """is_active - 全局开关基础""" def test_disabled_returns_false(self): config = FeatureFlagConfig(name="test", enabled=False) assert config.is_active() is False def test_disabled_with_identifier_returns_false(self): config = FeatureFlagConfig(name="test", enabled=False) assert config.is_active(identifier="user1") is False def test_enabled_no_percentage_no_whitelist_returns_true(self): config = FeatureFlagConfig(name="test", enabled=True) # percentage=0, whitelist=空,但 enabled=True # 按逻辑:全局开了但百分比0且无白名单 → 其实应该是 False? # 让我看代码... # 代码里 percentage <= 0 时返回 False(没有白名单且百分比为0) assert config.is_active() is False def test_enabled_100_percent_returns_true(self): config = FeatureFlagConfig(name="test", enabled=True, percentage=100) assert config.is_active() is True # ============================================================ # FeatureFlagConfig.is_active - 白名单 # ============================================================ class TestIsActiveWhitelist: """is_active - 白名单优先级""" def test_whitelist_match_returns_true(self): config = FeatureFlagConfig( name="test", enabled=True, whitelist={"user1", "user2"}, ) assert config.is_active(identifier="user1") is True def test_whitelist_no_match_falls_through(self): config = FeatureFlagConfig( name="test", enabled=True, percentage=0, whitelist={"user1"}, ) # 不在白名单,且百分比为0 → False assert config.is_active(identifier="user3") is False def test_whitelist_overrides_percentage_zero(self): """白名单优先级最高,即使百分比为0也能启用""" config = FeatureFlagConfig( name="test", enabled=True, percentage=0, whitelist={"vip_user"}, ) assert config.is_active(identifier="vip_user") is True def test_whitelist_overrides_partial_percentage(self): """白名单用户即使在百分比外也能启用""" config = FeatureFlagConfig( name="test", enabled=True, percentage=1, # 只有1%的用户 whitelist={"important_user"}, ) # 白名单用户直接通过 assert config.is_active(identifier="important_user") is True def test_no_identifier_no_whitelist_check(self): """不传 identifier 时不做白名单检查""" config = FeatureFlagConfig( name="test", enabled=True, percentage=100, whitelist={"user1"}, ) # 无 identifier,直接看百分比(100%) assert config.is_active() is True # ============================================================ # FeatureFlagConfig.is_active - 百分比边界值 # ============================================================ class TestIsActivePercentageBoundaries: """is_active - 百分比边界值""" def test_percentage_0_returns_false(self): config = FeatureFlagConfig(name="test", enabled=True, percentage=0) assert config.is_active(identifier="any_user") is False def test_percentage_100_returns_true(self): config = FeatureFlagConfig(name="test", enabled=True, percentage=100) assert config.is_active(identifier="any_user") is True def test_percentage_negative_treated_as_0(self): """percentage < 0 应该按 0 处理""" config = FeatureFlagConfig(name="test", enabled=True, percentage=-5) assert config.is_active(identifier="any_user") is False def test_percentage_over_100_treated_as_100(self): """percentage > 100 应该按 100 处理""" config = FeatureFlagConfig(name="test", enabled=True, percentage=150) assert config.is_active(identifier="any_user") is True # ============================================================ # FeatureFlagConfig.is_active - 哈希一致性 # ============================================================ class TestIsActiveHashConsistency: """is_active - 哈希取模一致性验证""" def test_same_user_same_result_every_time(self): """同一用户多次调用结果一致(确定性哈希)""" config = FeatureFlagConfig(name="test", enabled=True, percentage=50) results = {config.is_active(identifier="user_xyz") for _ in range(100)} assert len(results) == 1 # 全部相同 def test_different_flags_same_user_can_differ(self): """不同 flag 对同一用户可以有不同结果(因为 flag name 参与哈希)""" config_a = FeatureFlagConfig(name="flag_a", enabled=True, percentage=50) config_b = FeatureFlagConfig(name="flag_b", enabled=True, percentage=50) # 不保证一定不同,但大部分情况下应该不同 # 这里只验证哈希输入包含了 flag name(通过机制保证) # 具体是否不同取决于哈希值 def test_percentage_coverage_roughly_correct(self): """大量用户中,命中比例大致接近百分比""" config = FeatureFlagConfig(name="coverage_test", enabled=True, percentage=30) users = [f"user_{i}" for i in range(1000)] active_count = sum(1 for u in users if config.is_active(identifier=u)) # 30% ± 10% 的容差 assert 200 <= active_count <= 400 def test_50_percent_roughly_half(self): config = FeatureFlagConfig(name="half_test", enabled=True, percentage=50) users = [f"user_{i}" for i in range(1000)] active_count = sum(1 for u in users if config.is_active(identifier=u)) # 50% ± 10% assert 400 <= active_count <= 600 def test_10_percent_roughly_tenth(self): config = FeatureFlagConfig(name="ten_pct", enabled=True, percentage=10) users = [f"user_{i}" for i in range(1000)] active_count = sum(1 for u in users if config.is_active(identifier=u)) assert 50 <= active_count <= 150 def test_empty_identifier_treated_as_no_identifier(self): """空字符串 identifier 应该如何处理?""" config = FeatureFlagConfig(name="test", enabled=True, percentage=50) # 空字符串是 falsy,走无 identifier 分支(随机) # 但白名单检查也会跳过 # 验证不会崩溃 result = config.is_active(identifier="") assert isinstance(result, bool) # ============================================================ # InMemoryFeatureFlagStore - CRUD # ============================================================ class TestInMemoryFeatureFlagStore: """InMemoryFeatureFlagStore 内存实现""" def test_get_nonexistent_returns_default_disabled(self): store = InMemoryFeatureFlagStore() config = store.get("nonexistent") assert config.name == "nonexistent" assert config.enabled is False assert config.percentage == 0 def test_set_and_get(self): store = InMemoryFeatureFlagStore() original = FeatureFlagConfig( name="my_flag", enabled=True, percentage=50, whitelist={"admin"}, ) store.set(original) retrieved = store.get("my_flag") assert retrieved.name == "my_flag" assert retrieved.enabled is True assert retrieved.percentage == 50 assert retrieved.whitelist == {"admin"} def test_set_overwrites_existing(self): store = InMemoryFeatureFlagStore() store.set(FeatureFlagConfig(name="flag", enabled=True, percentage=30)) store.set(FeatureFlagConfig(name="flag", enabled=False, percentage=70)) config = store.get("flag") assert config.enabled is False assert config.percentage == 70 def test_delete_existing_returns_true(self): store = InMemoryFeatureFlagStore() store.set(FeatureFlagConfig(name="delete_me")) result = store.delete("delete_me") assert result is True # 删除后获取返回默认配置 assert store.get("delete_me").enabled is False def test_delete_nonexistent_returns_false(self): store = InMemoryFeatureFlagStore() result = store.delete("no_such_flag") assert result is False def test_list_all_empty(self): store = InMemoryFeatureFlagStore() assert store.list_all() == {} def test_list_all_multiple(self): store = InMemoryFeatureFlagStore() store.set(FeatureFlagConfig(name="flag1", enabled=True)) store.set(FeatureFlagConfig(name="flag2", percentage=50)) store.set(FeatureFlagConfig(name="flag3")) all_flags = store.list_all() assert len(all_flags) == 3 assert "flag1" in all_flags assert "flag2" in all_flags assert "flag3" in all_flags assert all_flags["flag1"].enabled is True assert all_flags["flag2"].percentage == 50 def test_list_all_returns_copy(self): """返回的是副本,修改不影响内部状态""" store = InMemoryFeatureFlagStore() store.set(FeatureFlagConfig(name="flag1")) flags = store.list_all() flags["fake"] = FeatureFlagConfig(name="fake") assert "fake" not in store.list_all() # ============================================================ # InMemoryFeatureFlagStore - is_active # ============================================================ class TestInMemoryStoreIsActive: """store.is_active 便捷方法""" def test_is_active_enabled_flag(self): store = InMemoryFeatureFlagStore() store.set(FeatureFlagConfig(name="on", enabled=True, percentage=100)) assert store.is_active("on") is True def test_is_active_disabled_flag(self): store = InMemoryFeatureFlagStore() store.set(FeatureFlagConfig(name="off", enabled=False)) assert store.is_active("off") is False def test_is_active_nonexistent_flag(self): store = InMemoryFeatureFlagStore() assert store.is_active("unknown") is False def test_is_active_with_identifier_whitelist(self): store = InMemoryFeatureFlagStore() store.set( FeatureFlagConfig( name="beta", enabled=True, percentage=0, whitelist={"tester1"}, ) ) assert store.is_active("beta", identifier="tester1") is True assert store.is_active("beta", identifier="other_user") is False