Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions splitio/client/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from enum import Enum

from splitio.engine.impressions import ImpressionsMode
from splitio.client.input_validator import validate_flag_sets
from splitio.client.input_validator import validate_flag_sets, validate_fallback_treatment, validate_regex_name
from splitio.models.fallback_config import FallbackTreatmentsConfiguration

_LOGGER = logging.getLogger(__name__)
DEFAULT_DATA_SAMPLING = 1
Expand Down Expand Up @@ -69,7 +70,8 @@ class AuthenticateScheme(Enum):
'flagSetsFilter': None,
'httpAuthenticateScheme': AuthenticateScheme.NONE,
'kerberosPrincipalUser': None,
'kerberosPrincipalPassword': None
'kerberosPrincipalPassword': None,
'fallbackTreatments': FallbackTreatmentsConfiguration(None)
}

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

processed = _sanitize_fallback_config(config, processed)

return processed

def _sanitize_fallback_config(config, processed):
if config.get('fallbackTreatments') is not None:
if not isinstance(config['fallbackTreatments'], FallbackTreatmentsConfiguration):
_LOGGER.warning('Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.')
processed['fallbackTreatments'] = None
return processed

sanitized_global_fallback_treatment = config['fallbackTreatments'].global_fallback_treatment
if config['fallbackTreatments'].global_fallback_treatment is not None and not validate_fallback_treatment(config['fallbackTreatments'].global_fallback_treatment):
_LOGGER.warning('Config: global fallbacktreatment parameter is discarded.')
sanitized_global_fallback_treatment = None

sanitized_flag_fallback_treatments = {}
if config['fallbackTreatments'].by_flag_fallback_treatment is not None:
for feature_name in config['fallbackTreatments'].by_flag_fallback_treatment.keys():
if not validate_regex_name(feature_name) or not validate_fallback_treatment(config['fallbackTreatments'].by_flag_fallback_treatment[feature_name]):
_LOGGER.warning('Config: fallback treatment parameter for feature flag %s is discarded.', feature_name)
continue

sanitized_flag_fallback_treatments[feature_name] = config['fallbackTreatments'].by_flag_fallback_treatment[feature_name]

processed['fallbackTreatments'] = FallbackTreatmentsConfiguration(sanitized_global_fallback_treatment, sanitized_flag_fallback_treatments)

return processed
25 changes: 24 additions & 1 deletion splitio/client/input_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@
from splitio.client.key import Key
from splitio.client import client
from splitio.engine.evaluator import CONTROL
from splitio.models.fallback_treatment import FallbackTreatment


_LOGGER = logging.getLogger(__name__)
MAX_LENGTH = 250
EVENT_TYPE_PATTERN = r'^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$'
MAX_PROPERTIES_LENGTH_BYTES = 32768
_FLAG_SETS_REGEX = '^[a-z0-9][_a-z0-9]{0,49}$'

_FALLBACK_TREATMENT_REGEX = '^[a-zA-Z][a-zA-Z0-9-_;]+$'
_FALLBACK_TREATMENT_SIZE = 100

def _check_not_null(value, name, operation):
"""
Expand Down Expand Up @@ -712,3 +714,24 @@ def validate_flag_sets(flag_sets, method_name):
sanitized_flag_sets.add(flag_set)

return list(sanitized_flag_sets)

def validate_fallback_treatment(fallback_treatment):
if not isinstance(fallback_treatment, FallbackTreatment):
_LOGGER.warning("Config: Fallback treatment instance should be FallbackTreatment, input is discarded")
return False

if not validate_regex_name(fallback_treatment.treatment):
_LOGGER.warning("Config: Fallback treatment should match regex %s", _FALLBACK_TREATMENT_REGEX)
return False

if len(fallback_treatment.treatment) > _FALLBACK_TREATMENT_SIZE:
_LOGGER.warning("Config: Fallback treatment size should not exceed %s characters", _FALLBACK_TREATMENT_SIZE)
return False

return True

def validate_regex_name(name):
if re.match(_FALLBACK_TREATMENT_REGEX, name) == None:
return False

return True
37 changes: 37 additions & 0 deletions splitio/models/fallback_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Segment module."""

class FallbackTreatmentsConfiguration(object):
"""FallbackTreatmentsConfiguration object class."""

def __init__(self, global_fallback_treatment=None, by_flag_fallback_treatment=None):
"""
Class constructor.

:param global_fallback_treatment: global FallbackTreatment.
:type global_fallback_treatment: FallbackTreatment

:param by_flag_fallback_treatment: Dict of flags and their fallback treatment
:type by_flag_fallback_treatment: {str: FallbackTreatment}
"""
self._global_fallback_treatment = global_fallback_treatment
self._by_flag_fallback_treatment = by_flag_fallback_treatment

@property
def global_fallback_treatment(self):
"""Return global fallback treatment."""
return self._global_fallback_treatment

@global_fallback_treatment.setter
def global_fallback_treatment(self, new_value):
"""Set global fallback treatment."""
self._global_fallback_treatment = new_value

@property
def by_flag_fallback_treatment(self):
"""Return by flag fallback treatment."""
return self._by_flag_fallback_treatment

@by_flag_fallback_treatment.setter
def by_flag_fallback_treatment(self, new_value):
"""Set global fallback treatment."""
self.by_flag_fallback_treatment = new_value
36 changes: 36 additions & 0 deletions splitio/models/fallback_treatment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Segment module."""
import json

class FallbackTreatment(object):
"""FallbackTreatment object class."""

def __init__(self, treatment, config=None):
"""
Class constructor.

:param treatment: treatment.
:type treatment: str

:param config: config.
:type config: json
"""
self._treatment = treatment
self._config = None
if config != None:
self._config = json.dumps(config)
self._label_prefix = "fallback - "

@property
def treatment(self):
"""Return treatment."""
return self._treatment

@property
def config(self):
"""Return config."""
return self._config

@property
def label_prefix(self):
"""Return label prefix."""
return self._label_prefix
40 changes: 38 additions & 2 deletions tests/client/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import pytest
from splitio.client import config
from splitio.engine.impressions.impressions import ImpressionsMode

from splitio.models.fallback_treatment import FallbackTreatment
from splitio.models.fallback_config import FallbackTreatmentsConfiguration

class ConfigSanitizationTests(object):
"""Inmemory storage-based integration tests."""
Expand Down Expand Up @@ -62,8 +63,10 @@ def test_sanitize_imp_mode(self):
assert mode == ImpressionsMode.DEBUG
assert rate == 60

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

processed = config.sanitize('some', {'httpAuthenticateScheme': 'NONE'})
assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.NONE

_logger.reset_mock()
processed = config.sanitize('some', {'fallbackTreatments': 'NONE'})
assert processed['fallbackTreatments'] == None
assert _logger.warning.mock_calls[1] == mocker.call("Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.")

_logger.reset_mock()
processed = config.sanitize('some', {'fallbackTreatments': FallbackTreatmentsConfiguration(123)})
assert processed['fallbackTreatments'].global_fallback_treatment == None
assert _logger.warning.mock_calls[1] == mocker.call("Config: global fallbacktreatment parameter is discarded.")

_logger.reset_mock()
processed = config.sanitize('some', {'fallbackTreatments': FallbackTreatmentsConfiguration(FallbackTreatment("123"))})
assert processed['fallbackTreatments'].global_fallback_treatment == None
assert _logger.warning.mock_calls[1] == mocker.call("Config: global fallbacktreatment parameter is discarded.")

fb = FallbackTreatmentsConfiguration(FallbackTreatment('on'))
processed = config.sanitize('some', {'fallbackTreatments': fb})
assert processed['fallbackTreatments'].global_fallback_treatment.treatment == fb.global_fallback_treatment.treatment
assert processed['fallbackTreatments'].global_fallback_treatment.label_prefix == "fallback - "

fb = FallbackTreatmentsConfiguration(FallbackTreatment('on'), {"flag": FallbackTreatment("off")})
processed = config.sanitize('some', {'fallbackTreatments': fb})
assert processed['fallbackTreatments'].global_fallback_treatment.treatment == fb.global_fallback_treatment.treatment
assert processed['fallbackTreatments'].by_flag_fallback_treatment["flag"] == fb.by_flag_fallback_treatment["flag"]
assert processed['fallbackTreatments'].by_flag_fallback_treatment["flag"].label_prefix == "fallback - "

_logger.reset_mock()
fb = FallbackTreatmentsConfiguration(None, {"flag#%": FallbackTreatment("off"), "flag2": FallbackTreatment("on")})
processed = config.sanitize('some', {'fallbackTreatments': fb})
assert len(processed['fallbackTreatments'].by_flag_fallback_treatment) == 1
assert processed['fallbackTreatments'].by_flag_fallback_treatment.get("flag2") == fb.by_flag_fallback_treatment["flag2"]
assert _logger.warning.mock_calls[1] == mocker.call('Config: fallback treatment parameter for feature flag %s is discarded.', 'flag#%')
39 changes: 36 additions & 3 deletions tests/client/test_input_validator.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
"""Unit tests for the input_validator module."""
import logging
import pytest

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

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

def test_fallback_treatments(self, mocker):
_logger = mocker.Mock()
mocker.patch('splitio.client.input_validator._LOGGER', new=_logger)

assert input_validator.validate_fallback_treatment(FallbackTreatment("on", {"prop":"val"}))
assert input_validator.validate_fallback_treatment(FallbackTreatment("on"))

_logger.reset_mock()
assert not input_validator.validate_fallback_treatment(FallbackTreatment("on" * 100))
assert _logger.warning.mock_calls == [
mocker.call("Config: Fallback treatment size should not exceed %s characters", 100)
]

assert input_validator.validate_fallback_treatment(FallbackTreatment("on", {"prop" * 500:"val" * 500}))

_logger.reset_mock()
assert not input_validator.validate_fallback_treatment(FallbackTreatment("on/c"))
assert _logger.warning.mock_calls == [
mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$")
]

_logger.reset_mock()
assert not input_validator.validate_fallback_treatment(FallbackTreatment("9on"))
assert _logger.warning.mock_calls == [
mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$")
]

_logger.reset_mock()
assert not input_validator.validate_fallback_treatment(FallbackTreatment("on$as"))
assert _logger.warning.mock_calls == [
mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$")
]

assert input_validator.validate_fallback_treatment(FallbackTreatment("on_c"))
assert input_validator.validate_fallback_treatment(FallbackTreatment("on_45-c"))

class ClientInputValidationAsyncTests(object):
"""Input validation test cases."""

Expand Down
32 changes: 32 additions & 0 deletions tests/models/test_fallback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from splitio.models.fallback_treatment import FallbackTreatment
from splitio.models.fallback_config import FallbackTreatmentsConfiguration

class FallbackTreatmentModelTests(object):
"""Fallback treatment model tests."""

def test_working(self):
fallback_treatment = FallbackTreatment("on", {"prop": "val"})
assert fallback_treatment.config == '{"prop": "val"}'
assert fallback_treatment.treatment == 'on'

fallback_treatment = FallbackTreatment("off")
assert fallback_treatment.config == None
assert fallback_treatment.treatment == 'off'

class FallbackTreatmentsConfigModelTests(object):
"""Fallback treatment model tests."""

def test_working(self):
global_fb = FallbackTreatment("on")
flag_fb = FallbackTreatment("off")
fallback_config = FallbackTreatmentsConfiguration(global_fb, {"flag1": flag_fb})
assert fallback_config.global_fallback_treatment == global_fb
assert fallback_config.by_flag_fallback_treatment == {"flag1": flag_fb}

fallback_config.global_fallback_treatment = None
assert fallback_config.global_fallback_treatment == None

fallback_config.by_flag_fallback_treatment["flag2"] = flag_fb
assert fallback_config.by_flag_fallback_treatment == {"flag1": flag_fb, "flag2": flag_fb}