Skip to content

Commit ba443e5

Browse files
authored
Merge pull request #594 from splitio/FME-9614-fallback-config-validator
Updated Config and input validator
2 parents 5610069 + 52a2967 commit ba443e5

File tree

7 files changed

+234
-8
lines changed

7 files changed

+234
-8
lines changed

splitio/client/config.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from enum import Enum
55

66
from splitio.engine.impressions import ImpressionsMode
7-
from splitio.client.input_validator import validate_flag_sets
7+
from splitio.client.input_validator import validate_flag_sets, validate_fallback_treatment, validate_regex_name
8+
from splitio.models.fallback_config import FallbackTreatmentsConfiguration
89

910
_LOGGER = logging.getLogger(__name__)
1011
DEFAULT_DATA_SAMPLING = 1
@@ -69,7 +70,8 @@ class AuthenticateScheme(Enum):
6970
'flagSetsFilter': None,
7071
'httpAuthenticateScheme': AuthenticateScheme.NONE,
7172
'kerberosPrincipalUser': None,
72-
'kerberosPrincipalPassword': None
73+
'kerberosPrincipalPassword': None,
74+
'fallbackTreatments': FallbackTreatmentsConfiguration(None)
7375
}
7476

7577
def _parse_operation_mode(sdk_key, config):
@@ -168,4 +170,31 @@ def sanitize(sdk_key, config):
168170
' Defaulting to `none` mode.')
169171
processed["httpAuthenticateScheme"] = authenticate_scheme
170172

173+
processed = _sanitize_fallback_config(config, processed)
174+
171175
return processed
176+
177+
def _sanitize_fallback_config(config, processed):
178+
if config.get('fallbackTreatments') is not None:
179+
if not isinstance(config['fallbackTreatments'], FallbackTreatmentsConfiguration):
180+
_LOGGER.warning('Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.')
181+
processed['fallbackTreatments'] = None
182+
return processed
183+
184+
sanitized_global_fallback_treatment = config['fallbackTreatments'].global_fallback_treatment
185+
if config['fallbackTreatments'].global_fallback_treatment is not None and not validate_fallback_treatment(config['fallbackTreatments'].global_fallback_treatment):
186+
_LOGGER.warning('Config: global fallbacktreatment parameter is discarded.')
187+
sanitized_global_fallback_treatment = None
188+
189+
sanitized_flag_fallback_treatments = {}
190+
if config['fallbackTreatments'].by_flag_fallback_treatment is not None:
191+
for feature_name in config['fallbackTreatments'].by_flag_fallback_treatment.keys():
192+
if not validate_regex_name(feature_name) or not validate_fallback_treatment(config['fallbackTreatments'].by_flag_fallback_treatment[feature_name]):
193+
_LOGGER.warning('Config: fallback treatment parameter for feature flag %s is discarded.', feature_name)
194+
continue
195+
196+
sanitized_flag_fallback_treatments[feature_name] = config['fallbackTreatments'].by_flag_fallback_treatment[feature_name]
197+
198+
processed['fallbackTreatments'] = FallbackTreatmentsConfiguration(sanitized_global_fallback_treatment, sanitized_flag_fallback_treatments)
199+
200+
return processed

splitio/client/input_validator.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@
88
from splitio.client.key import Key
99
from splitio.client import client
1010
from splitio.engine.evaluator import CONTROL
11+
from splitio.models.fallback_treatment import FallbackTreatment
1112

1213

1314
_LOGGER = logging.getLogger(__name__)
1415
MAX_LENGTH = 250
1516
EVENT_TYPE_PATTERN = r'^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$'
1617
MAX_PROPERTIES_LENGTH_BYTES = 32768
1718
_FLAG_SETS_REGEX = '^[a-z0-9][_a-z0-9]{0,49}$'
18-
19+
_FALLBACK_TREATMENT_REGEX = '^[a-zA-Z][a-zA-Z0-9-_;]+$'
20+
_FALLBACK_TREATMENT_SIZE = 100
1921

2022
def _check_not_null(value, name, operation):
2123
"""
@@ -712,3 +714,24 @@ def validate_flag_sets(flag_sets, method_name):
712714
sanitized_flag_sets.add(flag_set)
713715

714716
return list(sanitized_flag_sets)
717+
718+
def validate_fallback_treatment(fallback_treatment):
719+
if not isinstance(fallback_treatment, FallbackTreatment):
720+
_LOGGER.warning("Config: Fallback treatment instance should be FallbackTreatment, input is discarded")
721+
return False
722+
723+
if not validate_regex_name(fallback_treatment.treatment):
724+
_LOGGER.warning("Config: Fallback treatment should match regex %s", _FALLBACK_TREATMENT_REGEX)
725+
return False
726+
727+
if len(fallback_treatment.treatment) > _FALLBACK_TREATMENT_SIZE:
728+
_LOGGER.warning("Config: Fallback treatment size should not exceed %s characters", _FALLBACK_TREATMENT_SIZE)
729+
return False
730+
731+
return True
732+
733+
def validate_regex_name(name):
734+
if re.match(_FALLBACK_TREATMENT_REGEX, name) == None:
735+
return False
736+
737+
return True

splitio/models/fallback_config.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Segment module."""
2+
3+
class FallbackTreatmentsConfiguration(object):
4+
"""FallbackTreatmentsConfiguration object class."""
5+
6+
def __init__(self, global_fallback_treatment=None, by_flag_fallback_treatment=None):
7+
"""
8+
Class constructor.
9+
10+
:param global_fallback_treatment: global FallbackTreatment.
11+
:type global_fallback_treatment: FallbackTreatment
12+
13+
:param by_flag_fallback_treatment: Dict of flags and their fallback treatment
14+
:type by_flag_fallback_treatment: {str: FallbackTreatment}
15+
"""
16+
self._global_fallback_treatment = global_fallback_treatment
17+
self._by_flag_fallback_treatment = by_flag_fallback_treatment
18+
19+
@property
20+
def global_fallback_treatment(self):
21+
"""Return global fallback treatment."""
22+
return self._global_fallback_treatment
23+
24+
@global_fallback_treatment.setter
25+
def global_fallback_treatment(self, new_value):
26+
"""Set global fallback treatment."""
27+
self._global_fallback_treatment = new_value
28+
29+
@property
30+
def by_flag_fallback_treatment(self):
31+
"""Return by flag fallback treatment."""
32+
return self._by_flag_fallback_treatment
33+
34+
@by_flag_fallback_treatment.setter
35+
def by_flag_fallback_treatment(self, new_value):
36+
"""Set global fallback treatment."""
37+
self.by_flag_fallback_treatment = new_value
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Segment module."""
2+
import json
3+
4+
class FallbackTreatment(object):
5+
"""FallbackTreatment object class."""
6+
7+
def __init__(self, treatment, config=None):
8+
"""
9+
Class constructor.
10+
11+
:param treatment: treatment.
12+
:type treatment: str
13+
14+
:param config: config.
15+
:type config: json
16+
"""
17+
self._treatment = treatment
18+
self._config = None
19+
if config != None:
20+
self._config = json.dumps(config)
21+
self._label_prefix = "fallback - "
22+
23+
@property
24+
def treatment(self):
25+
"""Return treatment."""
26+
return self._treatment
27+
28+
@property
29+
def config(self):
30+
"""Return config."""
31+
return self._config
32+
33+
@property
34+
def label_prefix(self):
35+
"""Return label prefix."""
36+
return self._label_prefix

tests/client/test_config.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import pytest
44
from splitio.client import config
55
from splitio.engine.impressions.impressions import ImpressionsMode
6-
6+
from splitio.models.fallback_treatment import FallbackTreatment
7+
from splitio.models.fallback_config import FallbackTreatmentsConfiguration
78

89
class ConfigSanitizationTests(object):
910
"""Inmemory storage-based integration tests."""
@@ -62,8 +63,10 @@ def test_sanitize_imp_mode(self):
6263
assert mode == ImpressionsMode.DEBUG
6364
assert rate == 60
6465

65-
def test_sanitize(self):
66+
def test_sanitize(self, mocker):
6667
"""Test sanitization."""
68+
_logger = mocker.Mock()
69+
mocker.patch('splitio.client.config._LOGGER', new=_logger)
6770
configs = {}
6871
processed = config.sanitize('some', configs)
6972
assert processed['redisLocalCacheEnabled'] # check default is True
@@ -87,3 +90,36 @@ def test_sanitize(self):
8790

8891
processed = config.sanitize('some', {'httpAuthenticateScheme': 'NONE'})
8992
assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.NONE
93+
94+
_logger.reset_mock()
95+
processed = config.sanitize('some', {'fallbackTreatments': 'NONE'})
96+
assert processed['fallbackTreatments'] == None
97+
assert _logger.warning.mock_calls[1] == mocker.call("Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.")
98+
99+
_logger.reset_mock()
100+
processed = config.sanitize('some', {'fallbackTreatments': FallbackTreatmentsConfiguration(123)})
101+
assert processed['fallbackTreatments'].global_fallback_treatment == None
102+
assert _logger.warning.mock_calls[1] == mocker.call("Config: global fallbacktreatment parameter is discarded.")
103+
104+
_logger.reset_mock()
105+
processed = config.sanitize('some', {'fallbackTreatments': FallbackTreatmentsConfiguration(FallbackTreatment("123"))})
106+
assert processed['fallbackTreatments'].global_fallback_treatment == None
107+
assert _logger.warning.mock_calls[1] == mocker.call("Config: global fallbacktreatment parameter is discarded.")
108+
109+
fb = FallbackTreatmentsConfiguration(FallbackTreatment('on'))
110+
processed = config.sanitize('some', {'fallbackTreatments': fb})
111+
assert processed['fallbackTreatments'].global_fallback_treatment.treatment == fb.global_fallback_treatment.treatment
112+
assert processed['fallbackTreatments'].global_fallback_treatment.label_prefix == "fallback - "
113+
114+
fb = FallbackTreatmentsConfiguration(FallbackTreatment('on'), {"flag": FallbackTreatment("off")})
115+
processed = config.sanitize('some', {'fallbackTreatments': fb})
116+
assert processed['fallbackTreatments'].global_fallback_treatment.treatment == fb.global_fallback_treatment.treatment
117+
assert processed['fallbackTreatments'].by_flag_fallback_treatment["flag"] == fb.by_flag_fallback_treatment["flag"]
118+
assert processed['fallbackTreatments'].by_flag_fallback_treatment["flag"].label_prefix == "fallback - "
119+
120+
_logger.reset_mock()
121+
fb = FallbackTreatmentsConfiguration(None, {"flag#%": FallbackTreatment("off"), "flag2": FallbackTreatment("on")})
122+
processed = config.sanitize('some', {'fallbackTreatments': fb})
123+
assert len(processed['fallbackTreatments'].by_flag_fallback_treatment) == 1
124+
assert processed['fallbackTreatments'].by_flag_fallback_treatment.get("flag2") == fb.by_flag_fallback_treatment["flag2"]
125+
assert _logger.warning.mock_calls[1] == mocker.call('Config: fallback treatment parameter for feature flag %s is discarded.', 'flag#%')

tests/client/test_input_validator.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
"""Unit tests for the input_validator module."""
2-
import logging
32
import pytest
43

54
from splitio.client.factory import SplitFactory, get_factory, SplitFactoryAsync, get_factory_async
65
from splitio.client.client import CONTROL, Client, _LOGGER as _logger, ClientAsync
7-
from splitio.client.manager import SplitManager, SplitManagerAsync
86
from splitio.client.key import Key
97
from splitio.storage import SplitStorage, EventStorage, ImpressionStorage, SegmentStorage, RuleBasedSegmentsStorage
108
from splitio.storage.inmemmory import InMemoryTelemetryStorage, InMemoryTelemetryStorageAsync, \
@@ -14,7 +12,7 @@
1412
from splitio.recorder.recorder import StandardRecorder, StandardRecorderAsync
1513
from splitio.engine.telemetry import TelemetryStorageProducer, TelemetryStorageProducerAsync
1614
from splitio.engine.impressions.impressions import Manager as ImpressionManager
17-
from splitio.engine.evaluator import EvaluationDataFactory
15+
from splitio.models.fallback_treatment import FallbackTreatment
1816

1917
class ClientInputValidationTests(object):
2018
"""Input validation test cases."""
@@ -1627,7 +1625,42 @@ def test_flag_sets_validation(self):
16271625
flag_sets = input_validator.validate_flag_sets([12, 33], 'method')
16281626
assert flag_sets == []
16291627

1628+
def test_fallback_treatments(self, mocker):
1629+
_logger = mocker.Mock()
1630+
mocker.patch('splitio.client.input_validator._LOGGER', new=_logger)
16301631

1632+
assert input_validator.validate_fallback_treatment(FallbackTreatment("on", {"prop":"val"}))
1633+
assert input_validator.validate_fallback_treatment(FallbackTreatment("on"))
1634+
1635+
_logger.reset_mock()
1636+
assert not input_validator.validate_fallback_treatment(FallbackTreatment("on" * 100))
1637+
assert _logger.warning.mock_calls == [
1638+
mocker.call("Config: Fallback treatment size should not exceed %s characters", 100)
1639+
]
1640+
1641+
assert input_validator.validate_fallback_treatment(FallbackTreatment("on", {"prop" * 500:"val" * 500}))
1642+
1643+
_logger.reset_mock()
1644+
assert not input_validator.validate_fallback_treatment(FallbackTreatment("on/c"))
1645+
assert _logger.warning.mock_calls == [
1646+
mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$")
1647+
]
1648+
1649+
_logger.reset_mock()
1650+
assert not input_validator.validate_fallback_treatment(FallbackTreatment("9on"))
1651+
assert _logger.warning.mock_calls == [
1652+
mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$")
1653+
]
1654+
1655+
_logger.reset_mock()
1656+
assert not input_validator.validate_fallback_treatment(FallbackTreatment("on$as"))
1657+
assert _logger.warning.mock_calls == [
1658+
mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$")
1659+
]
1660+
1661+
assert input_validator.validate_fallback_treatment(FallbackTreatment("on_c"))
1662+
assert input_validator.validate_fallback_treatment(FallbackTreatment("on_45-c"))
1663+
16311664
class ClientInputValidationAsyncTests(object):
16321665
"""Input validation test cases."""
16331666

tests/models/test_fallback.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from splitio.models.fallback_treatment import FallbackTreatment
2+
from splitio.models.fallback_config import FallbackTreatmentsConfiguration
3+
4+
class FallbackTreatmentModelTests(object):
5+
"""Fallback treatment model tests."""
6+
7+
def test_working(self):
8+
fallback_treatment = FallbackTreatment("on", {"prop": "val"})
9+
assert fallback_treatment.config == '{"prop": "val"}'
10+
assert fallback_treatment.treatment == 'on'
11+
12+
fallback_treatment = FallbackTreatment("off")
13+
assert fallback_treatment.config == None
14+
assert fallback_treatment.treatment == 'off'
15+
16+
class FallbackTreatmentsConfigModelTests(object):
17+
"""Fallback treatment model tests."""
18+
19+
def test_working(self):
20+
global_fb = FallbackTreatment("on")
21+
flag_fb = FallbackTreatment("off")
22+
fallback_config = FallbackTreatmentsConfiguration(global_fb, {"flag1": flag_fb})
23+
assert fallback_config.global_fallback_treatment == global_fb
24+
assert fallback_config.by_flag_fallback_treatment == {"flag1": flag_fb}
25+
26+
fallback_config.global_fallback_treatment = None
27+
assert fallback_config.global_fallback_treatment == None
28+
29+
fallback_config.by_flag_fallback_treatment["flag2"] = flag_fb
30+
assert fallback_config.by_flag_fallback_treatment == {"flag1": flag_fb, "flag2": flag_fb}
31+
32+

0 commit comments

Comments
 (0)