123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- from dataclasses import dataclass
- import jsonschema
- import orjson
- import pytest
- import yaml
- from flagpole import ContextBuilder, EvaluationContext, Feature, InvalidFeatureFlagConfiguration
- from flagpole.conditions import ConditionOperatorKind
- @dataclass
- class SimpleTestContextData:
- pass
- class TestParseFeatureConfig:
- def get_is_true_context_builder(self, is_true_value: bool):
- return ContextBuilder().add_context_transformer(lambda _data: dict(is_true=is_true_value))
- def test_feature_with_empty_segments(self):
- feature = Feature.from_feature_config_json(
- "foobar",
- """
- {
- "created_at": "2023-10-12T00:00:00.000Z",
- "owner": "test-owner",
- "segments": []
- }
- """,
- )
- assert feature.name == "foobar"
- assert feature.created_at == "2023-10-12T00:00:00.000Z"
- assert feature.owner == "test-owner"
- assert feature.segments == []
- assert not feature.match(EvaluationContext(dict()))
- def test_feature_with_default_rollout(self):
- feature = Feature.from_feature_config_json(
- "foo",
- """
- {
- "owner": "test-user",
- "created_at": "2023-10-12T00:00:00.000Z",
- "segments": [{
- "name": "always_pass_segment",
- "conditions": [{
- "name": "Always true",
- "property": "is_true",
- "operator": "equals",
- "value": true
- }]
- }]
- }
- """,
- )
- context_builder = self.get_is_true_context_builder(is_true_value=True)
- assert feature.segments[0].rollout == 100
- assert feature.match(context_builder.build(SimpleTestContextData()))
- def test_feature_with_rollout_zero(self):
- feature = Feature.from_feature_config_json(
- "foobar",
- """
- {
- "created_at": "2023-10-12T00:00:00.000Z",
- "owner": "test-owner",
- "segments": [
- {
- "name": "exclude",
- "rollout": 0,
- "conditions": [
- {
- "property": "user_email",
- "operator": "equals",
- "value": "nope@example.com"
- }
- ]
- },
- {
- "name": "friends",
- "rollout": 100,
- "conditions": [
- {
- "property": "organization_slug",
- "operator": "in",
- "value": ["acme", "sentry"]
- }
- ]
- }
- ]
- }
- """,
- )
- exclude_user = {"user_email": "nope@example.com", "organization_slug": "acme"}
- assert not feature.match(EvaluationContext(exclude_user))
- match_user = {"user_email": "yes@example.com", "organization_slug": "acme"}
- assert feature.match(EvaluationContext(match_user))
- def test_all_conditions_in_segment(self):
- feature = Feature.from_feature_config_json(
- "foobar",
- """
- {
- "created_at": "2023-10-12T00:00:00.000Z",
- "owner": "test-owner",
- "segments": [
- {
- "name": "multiple conditions",
- "rollout": 100,
- "conditions": [
- {
- "property": "user_email",
- "operator": "equals",
- "value": "yes@example.com"
- },
- {
- "property": "organization_slug",
- "operator": "in",
- "value": ["acme", "sentry"]
- }
- ]
- }
- ]
- }
- """,
- )
- exclude_user = {"user_email": "yes@example.com"}
- assert not feature.match(EvaluationContext(exclude_user))
- match_user = {"user_email": "yes@example.com", "organization_slug": "acme"}
- assert feature.match(EvaluationContext(match_user))
- def test_valid_with_all_nesting(self):
- feature = Feature.from_feature_config_json(
- "foobar",
- """
- {
- "created_at": "2023-10-12T00:00:00.000Z",
- "owner": "test-owner",
- "segments": [{
- "name": "segment1",
- "rollout": 100,
- "conditions": [{
- "property": "test_property",
- "operator": "in",
- "value": ["foobar"]
- }]
- }]
- }
- """,
- )
- assert feature.name == "foobar"
- assert len(feature.segments) == 1
- assert feature.segments[0].name == "segment1"
- assert feature.segments[0].rollout == 100
- assert len(feature.segments[0].conditions) == 1
- condition = feature.segments[0].conditions[0]
- assert condition.property == "test_property"
- assert condition.operator
- assert condition.operator == ConditionOperatorKind.IN
- assert condition.value == ["foobar"]
- assert feature.match(EvaluationContext(dict(test_property="foobar")))
- assert not feature.match(EvaluationContext(dict(test_property="barfoo")))
- def test_invalid_json(self):
- with pytest.raises(InvalidFeatureFlagConfiguration):
- Feature.from_feature_config_json("foobar", "{")
- def test_validate_invalid_schema(self):
- config = """
- {
- "owner": "sentry",
- "created_at": "2024-05-14",
- "segments": [
- {
- "name": "",
- "rollout": 1,
- "conditions": []
- }
- ]
- }
- """
- feature = Feature.from_feature_config_json("trash", config)
- with pytest.raises(jsonschema.ValidationError) as err:
- feature.validate()
- assert "is too short" in str(err)
- config = """
- {
- "owner": "sentry",
- "created_at": "2024-05-14",
- "segments": [
- {
- "name": "allowed orgs",
- "rollout": 1,
- "conditions": [
- {
- "property": "organization_slug",
- "operator": "contains",
- "value": ["derp"]
- }
- ]
- }
- ]
- }
- """
- feature = Feature.from_feature_config_json("trash", config)
- with pytest.raises(jsonschema.ValidationError) as err:
- feature.validate()
- assert "'contains'} is not valid" in str(err)
- def test_validate_valid(self):
- config = """
- {
- "owner": "sentry",
- "created_at": "2024-05-14",
- "segments": [
- {
- "name": "ga",
- "rollout": 100,
- "conditions": []
- }
- ]
- }
- """
- feature = Feature.from_feature_config_json("redpaint", config)
- assert feature.validate()
- def test_empty_string_name(self):
- with pytest.raises(InvalidFeatureFlagConfiguration) as exception:
- Feature.from_feature_config_json("", '{"segments":[]}')
- assert "Feature name is required" in str(exception)
- def test_missing_segments(self):
- with pytest.raises(InvalidFeatureFlagConfiguration) as exception:
- Feature.from_feature_config_json("foo", "{}")
- assert "Feature has no segments defined" in str(exception)
- def test_invalid_operator_condition(self):
- config = """
- {
- "owner": "sentry",
- "segments": [
- {
- "name": "derp",
- "conditions": [
- {"property": "user_email", "operator": "trash", "value": 1}
- ]
- }
- ]
- }
- """
- with pytest.raises(InvalidFeatureFlagConfiguration) as exception:
- Feature.from_feature_config_json("foo", config)
- assert "Provided config_dict is not a valid feature" in str(exception)
- def test_enabled_feature(self):
- feature = Feature.from_feature_config_json(
- "foo",
- """
- {
- "owner": "test-user",
- "created_at": "2023-10-12T00:00:00.000Z",
- "segments": [{
- "name": "always_pass_segment",
- "rollout": 100,
- "conditions": [{
- "name": "Always true",
- "property": "is_true",
- "operator": "equals",
- "value": true
- }]
- }]
- }
- """,
- )
- context_builder = self.get_is_true_context_builder(is_true_value=True)
- assert feature.match(context_builder.build(SimpleTestContextData()))
- def test_disabled_feature(self):
- feature = Feature.from_feature_config_json(
- "foo",
- """
- {
- "owner": "test-user",
- "enabled": false,
- "created_at": "2023-12-12T00:00:00.000Z",
- "segments": [{
- "name": "always_pass_segment",
- "rollout": 100,
- "conditions": [{
- "name": "Always true",
- "property": "is_true",
- "operator": "equals",
- "value": true
- }]
- }]
- }
- """,
- )
- context_builder = self.get_is_true_context_builder(is_true_value=True)
- assert not feature.match(context_builder.build(SimpleTestContextData()))
- def test_dump_yaml(self):
- feature = Feature.from_feature_config_json(
- "foo",
- """
- {
- "owner": "test-user",
- "created_at": "2023-12-12T00:00:00.000Z",
- "segments": [{
- "name": "always_pass_segment",
- "rollout": 100,
- "conditions": [{
- "name": "Always true",
- "property": "is_true",
- "operator": "equals",
- "value": true
- }]
- }]
- }
- """,
- )
- parsed_json = orjson.loads(feature.to_json_str())
- parsed_yaml = dict(yaml.safe_load(feature.to_yaml_str()))
- assert "foo" in parsed_yaml
- assert parsed_yaml == parsed_json
- features_from_yaml = Feature.from_bulk_yaml(feature.to_yaml_str())
- assert features_from_yaml == [feature]
|