From 246a96c00685d1c96e1a64e7076400d5b7aaf61a Mon Sep 17 00:00:00 2001 From: Ivan Lee Date: Fri, 4 Feb 2022 02:21:27 -0500 Subject: [PATCH 01/10] Add (and load) new Constraint configuration. --- UnleashClient/constraints/Constraint.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/UnleashClient/constraints/Constraint.py b/UnleashClient/constraints/Constraint.py index 50f37424..aaaf1742 100644 --- a/UnleashClient/constraints/Constraint.py +++ b/UnleashClient/constraints/Constraint.py @@ -9,9 +9,14 @@ def __init__(self, constraint_dict: dict) -> None: :param constraint_dict: From the strategy document. """ - self.context_name = constraint_dict['contextName'] - self.operator = constraint_dict['operator'] - self.values = constraint_dict['values'] + self.context_name: str = constraint_dict['contextName'] + self.operator: str = constraint_dict['operator'] + self.values = constraint_dict['values'] if 'values' in constraint_dict.keys() else [] + self.value = constraint_dict['value'] if 'value' in constraint_dict.keys() else [] + + self.case_insensitive = constraint_dict['caseInsensitive'] if 'caseInsensitive' in constraint_dict.keys() else False + self.inverted = constraint_dict['inverted'] if 'inverted' in constraint_dict.keys() else False + def apply(self, context: dict = None) -> bool: """ From 9dd50acde63d15c8490a378a6a4040e01bb54390 Mon Sep 17 00:00:00 2001 From: Ivan Lee Date: Fri, 4 Feb 2022 02:47:30 -0500 Subject: [PATCH 02/10] Refactor constraint object to sparate logic. --- UnleashClient/constraints/Constraint.py | 38 ++++++++++++++++++++----- tests/unit_tests/test_constraints.py | 35 +++++++---------------- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/UnleashClient/constraints/Constraint.py b/UnleashClient/constraints/Constraint.py index aaaf1742..ca0cc95a 100644 --- a/UnleashClient/constraints/Constraint.py +++ b/UnleashClient/constraints/Constraint.py @@ -1,7 +1,14 @@ # pylint: disable=invalid-name, too-few-public-methods +from typing import List +from enum import Enum from UnleashClient.utils import LOGGER, get_identifier +class ConstraintOperators(Enum): + IN = "IN" + NOT_IN = "NOT_IN" + + class Constraint: def __init__(self, constraint_dict: dict) -> None: """ @@ -10,7 +17,7 @@ def __init__(self, constraint_dict: dict) -> None: :param constraint_dict: From the strategy document. """ self.context_name: str = constraint_dict['contextName'] - self.operator: str = constraint_dict['operator'] + self.operator: str = ConstraintOperators(constraint_dict['operator'].upper()) self.values = constraint_dict['values'] if 'values' in constraint_dict.keys() else [] self.value = constraint_dict['value'] if 'value' in constraint_dict.keys() else [] @@ -18,6 +25,25 @@ def __init__(self, constraint_dict: dict) -> None: self.inverted = constraint_dict['inverted'] if 'inverted' in constraint_dict.keys() else False + @staticmethod + def check_in(context_value: str, values: List[str]) -> bool: + return context_value in values + + + @staticmethod + def check_not_in(context_value: str, values: List[str]) -> bool: + return context_value not in values + + + def check_list_operators(self, context_value: str) -> bool: + if self.operator == ConstraintOperators.IN: + return self.check_in(context_value, self.values) + elif self.operator == ConstraintOperators.NOT_IN: + return self.check_not_in(context_value, self.values) + else: + return False + + def apply(self, context: dict = None) -> bool: """ Returns true/false depending on constraint provisioning and context. @@ -28,13 +54,11 @@ def apply(self, context: dict = None) -> bool: constraint_check = False try: - value = get_identifier(self.context_name, context) + context_value = get_identifier(self.context_name, context) - if value: - if self.operator.upper() == "IN": - constraint_check = value in self.values - elif self.operator.upper() == "NOT_IN": - constraint_check = value not in self.values + if context_value: + if self.operator in [ConstraintOperators.IN, ConstraintOperators.NOT_IN]: + constraint_check = self.check_list_operators(context_value=context_value) except Exception as excep: # pylint: disable=broad-except LOGGER.info("Could not evaluate context %s! Error: %s", self.context_name, excep) diff --git a/tests/unit_tests/test_constraints.py b/tests/unit_tests/test_constraints.py index 16715290..e0a6b6a1 100644 --- a/tests/unit_tests/test_constraints.py +++ b/tests/unit_tests/test_constraints.py @@ -1,37 +1,17 @@ import pytest from UnleashClient.constraints import Constraint - - -CONSTRAINT_DICT_IN = \ - { - "contextName": "appName", - "operator": "IN", - "values": [ - "test", - "test2" - ] - } - - -CONSTRAINT_DICT_NOTIN = \ - { - "contextName": "appName", - "operator": "NOT_IN", - "values": [ - "test", - "test2" - ] - } +from tests.utilities.mocks import mock_constraints @pytest.fixture() def constraint_IN(): - yield Constraint(CONSTRAINT_DICT_IN) + yield Constraint(mock_constraints.CONSTRAINT_DICT_IN) @pytest.fixture() def constraint_NOTIN(): - yield Constraint(CONSTRAINT_DICT_NOTIN) + yield Constraint(mock_constraints.CONSTRAINT_DICT_NOTIN) + def test_constraint_IN_match(constraint_IN): @@ -73,3 +53,10 @@ def test_constraint_NOTIN_not_match(constraint_NOTIN): } assert constraint.apply(context) + + +def test_constraint_STR_ENDS_WITH_not_insensitive(): + constraint_case_insensitive = Constraint(constraint_dict=mock_constraints.CONSTRAINT_DICT_STR_ENDS_WITH) + + assert constraint_case_insensitive.apply({'customField': "dot"}) + assert not constraint_case_insensitive.apply({'customField': "hamster"}) From f8ba79501dc78672e748d8476baf7e60ae9378c5 Mon Sep 17 00:00:00 2001 From: Ivan Lee Date: Fri, 4 Feb 2022 03:31:12 -0500 Subject: [PATCH 03/10] Add string operators and inverted support. --- UnleashClient/constraints/Constraint.py | 50 ++++++++----- tests/unit_tests/test_constraints.py | 46 ++++++++++-- tests/utilities/mocks/mock_constraints.py | 88 +++++++++++++++++++++++ 3 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 tests/utilities/mocks/mock_constraints.py diff --git a/UnleashClient/constraints/Constraint.py b/UnleashClient/constraints/Constraint.py index ca0cc95a..73e3be4f 100644 --- a/UnleashClient/constraints/Constraint.py +++ b/UnleashClient/constraints/Constraint.py @@ -1,5 +1,4 @@ -# pylint: disable=invalid-name, too-few-public-methods -from typing import List +# pylint: disable=invalid-name, too-few-public-methods, use-a-generator from enum import Enum from UnleashClient.utils import LOGGER, get_identifier @@ -7,7 +6,9 @@ class ConstraintOperators(Enum): IN = "IN" NOT_IN = "NOT_IN" - + STR_ENDS_WITH = "STR_ENDS_WITH" + STR_STARTS_WITH = "STR_STARTS_WITH" + STR_CONTAINS = "STR_CONTAINS" class Constraint: def __init__(self, constraint_dict: dict) -> None: @@ -17,7 +18,7 @@ def __init__(self, constraint_dict: dict) -> None: :param constraint_dict: From the strategy document. """ self.context_name: str = constraint_dict['contextName'] - self.operator: str = ConstraintOperators(constraint_dict['operator'].upper()) + self.operator: ConstraintOperators = ConstraintOperators(constraint_dict['operator'].upper()) self.values = constraint_dict['values'] if 'values' in constraint_dict.keys() else [] self.value = constraint_dict['value'] if 'value' in constraint_dict.keys() else [] @@ -25,24 +26,35 @@ def __init__(self, constraint_dict: dict) -> None: self.inverted = constraint_dict['inverted'] if 'inverted' in constraint_dict.keys() else False - @staticmethod - def check_in(context_value: str, values: List[str]) -> bool: - return context_value in values - - - @staticmethod - def check_not_in(context_value: str, values: List[str]) -> bool: - return context_value not in values - - + # Methods to handle each operator type. def check_list_operators(self, context_value: str) -> bool: + return_value = False + if self.operator == ConstraintOperators.IN: - return self.check_in(context_value, self.values) + return_value = context_value in self.values elif self.operator == ConstraintOperators.NOT_IN: - return self.check_not_in(context_value, self.values) + return_value = context_value not in self.values + + return return_value + + def check_string_operators(self, context_value: str) -> bool: + if self.case_insensitive: + normalized_values = [x.upper() for x in self.values] + normalized_context_value = context_value.upper() else: - return False + normalized_values = self.values + normalized_context_value = context_value + + return_value = False + + if self.operator == ConstraintOperators.STR_CONTAINS: + return_value = any([x in normalized_context_value for x in normalized_values]) + elif self.operator == ConstraintOperators.STR_ENDS_WITH: + return_value = any([normalized_context_value.endswith(x) for x in normalized_values]) + elif self.operator == ConstraintOperators.STR_STARTS_WITH: + return_value = any([normalized_context_value.startswith(x) for x in normalized_values]) + return return_value def apply(self, context: dict = None) -> bool: """ @@ -59,7 +71,9 @@ def apply(self, context: dict = None) -> bool: if context_value: if self.operator in [ConstraintOperators.IN, ConstraintOperators.NOT_IN]: constraint_check = self.check_list_operators(context_value=context_value) + if self.operator in [ConstraintOperators.STR_CONTAINS, ConstraintOperators.STR_ENDS_WITH, ConstraintOperators.STR_STARTS_WITH]: + constraint_check = self.check_string_operators(context_value=context_value) except Exception as excep: # pylint: disable=broad-except LOGGER.info("Could not evaluate context %s! Error: %s", self.context_name, excep) - return constraint_check + return not constraint_check if self.inverted else constraint_check diff --git a/tests/unit_tests/test_constraints.py b/tests/unit_tests/test_constraints.py index e0a6b6a1..cbff2ac6 100644 --- a/tests/unit_tests/test_constraints.py +++ b/tests/unit_tests/test_constraints.py @@ -55,8 +55,46 @@ def test_constraint_NOTIN_not_match(constraint_NOTIN): assert constraint.apply(context) -def test_constraint_STR_ENDS_WITH_not_insensitive(): - constraint_case_insensitive = Constraint(constraint_dict=mock_constraints.CONSTRAINT_DICT_STR_ENDS_WITH) +def test_constraint_inversion(): + constraint_ci = Constraint(constraint_dict=mock_constraints.CONSTRAINT_DICT_STR_INVERT) - assert constraint_case_insensitive.apply({'customField': "dot"}) - assert not constraint_case_insensitive.apply({'customField': "hamster"}) + assert not constraint_ci.apply({'customField': "adogb"}) + + +def test_constraint_STR_CONTAINS(): + constraint_not_ci = Constraint(constraint_dict=mock_constraints.CONSTRAINT_DICT_STR_CONTAINS_NOT_CI) + constraint_ci = Constraint(constraint_dict=mock_constraints.CONSTRAINT_DICT_STR_CONTAINS_CI) + + assert constraint_ci.apply({'customField': "adogb"}) + assert not constraint_ci.apply({'customField': "aparrotb"}) + assert constraint_ci.apply({'customField': "ahamsterb"}) + + assert constraint_not_ci.apply({'customField': "adogb"}) + assert not constraint_ci.apply({'customField': "aparrotb"}) + assert not constraint_not_ci.apply({'customField': "ahamsterb"}) + + +def test_constraint_STR_ENDS_WITH(): + constraint_not_ci = Constraint(constraint_dict=mock_constraints.CONSTRAINT_DICT_STR_ENDS_WITH_NOT_CI) + constraint_ci = Constraint(constraint_dict=mock_constraints.CONSTRAINT_DICT_STR_ENDS_WITH_CI) + + assert constraint_ci.apply({'customField': "adog"}) + assert not constraint_ci.apply({'customField': "aparrot"}) + assert constraint_ci.apply({'customField': "ahamster"}) + + assert constraint_not_ci.apply({'customField': "adog"}) + assert not constraint_not_ci.apply({'customField': "aparrot"}) + assert not constraint_not_ci.apply({'customField': "ahamster"}) + + +def test_constraint_STR_STARTS_WITH(): + constraint_not_ci = Constraint(constraint_dict=mock_constraints.CONSTRAINT_DICT_STR_STARTS_WITH_NOT_CI) + constraint_ci = Constraint(constraint_dict=mock_constraints.CONSTRAINT_DICT_STR_STARTS_WITH_CI) + + assert constraint_ci.apply({'customField': "dogb"}) + assert not constraint_ci.apply({'customField': "parrotb"}) + assert constraint_ci.apply({'customField': "hamsterb"}) + + assert constraint_not_ci.apply({'customField': "dogb"}) + assert not constraint_not_ci.apply({'customField': "parrotb"}) + assert not constraint_not_ci.apply({'customField': "hamsterb"}) diff --git a/tests/utilities/mocks/mock_constraints.py b/tests/utilities/mocks/mock_constraints.py new file mode 100644 index 00000000..a6669473 --- /dev/null +++ b/tests/utilities/mocks/mock_constraints.py @@ -0,0 +1,88 @@ +CONSTRAINT_DICT_IN = \ + { + "contextName": "appName", + "operator": "IN", + "values": [ + "test", + "test2" + ] + } + + +CONSTRAINT_DICT_NOTIN = \ + { + "contextName": "appName", + "operator": "NOT_IN", + "values": [ + "test", + "test2" + ] + } + + +CONSTRAINT_DICT_STR_INVERT = \ + { + "contextName": "customField", + "operator": "STR_CONTAINS", + "values": ["dog", "cat", "hAmStEr"], + "caseInsensitive": True, + "inverted": True + } + +CONSTRAINT_DICT_STR_CONTAINS_CI = \ + { + "contextName": "customField", + "operator": "STR_CONTAINS", + "values": ["dog", "cat", "hAmStEr"], + "caseInsensitive": True, + "inverted": False + } + + +CONSTRAINT_DICT_STR_CONTAINS_NOT_CI = \ + { + "contextName": "customField", + "operator": "STR_CONTAINS", + "values": ["dog", "cat", "hAmStEr"], + "caseInsensitive": False, + "inverted": False + } + + +CONSTRAINT_DICT_STR_ENDS_WITH_CI = \ + { + "contextName": "customField", + "operator": "STR_ENDS_WITH", + "values": ["dog", "cat", "hAmStEr"], + "caseInsensitive": True, + "inverted": False + } + +CONSTRAINT_DICT_STR_ENDS_WITH_NOT_CI = \ + { + "contextName": "customField", + "operator": "STR_ENDS_WITH", + "values": ["dog", "cat", "hAmStEr"], + "caseInsensitive": False, + "inverted": False + } + + +CONSTRAINT_DICT_STR_STARTS_WITH_CI = \ + { + "contextName": "customField", + "operator": "STR_STARTS_WITH", + "values": ["dog", "cat", "hAmStEr"], + "caseInsensitive": True, + "inverted": False + } + + +CONSTRAINT_DICT_STR_STARTS_WITH_NOT_CI = \ + { + "contextName": "customField", + "operator": "STR_STARTS_WITH", + "values": ["dog", "cat", "hAmStEr"], + "caseInsensitive": False, + "inverted": False + } From c8f6269a268276245387269379d7bc19d44fc413 Mon Sep 17 00:00:00 2001 From: Ivan Lee Date: Fri, 4 Feb 2022 03:48:21 -0500 Subject: [PATCH 04/10] Add numeric operators. --- UnleashClient/constraints/Constraint.py | 28 ++++++++++++++ UnleashClient/utils.py | 3 +- tests/unit_tests/test_constraints.py | 40 ++++++++++++++++++++ tests/utilities/mocks/mock_constraints.py | 45 +++++++++++++++++++++++ 4 files changed, 115 insertions(+), 1 deletion(-) diff --git a/UnleashClient/constraints/Constraint.py b/UnleashClient/constraints/Constraint.py index 73e3be4f..d6d737d8 100644 --- a/UnleashClient/constraints/Constraint.py +++ b/UnleashClient/constraints/Constraint.py @@ -4,12 +4,22 @@ class ConstraintOperators(Enum): + # Logical operators IN = "IN" NOT_IN = "NOT_IN" + + # String operators STR_ENDS_WITH = "STR_ENDS_WITH" STR_STARTS_WITH = "STR_STARTS_WITH" STR_CONTAINS = "STR_CONTAINS" + # Numeric oeprators + NUM_EQ = "NUM_EQ" + NUM_GT = "NUM_GT" + NUM_GTE = "NUM_GTE" + NUM_LT = "NUM_LT" + NUM_LTE = "NUM_LTE" + class Constraint: def __init__(self, constraint_dict: dict) -> None: """ @@ -56,6 +66,22 @@ def check_string_operators(self, context_value: str) -> bool: return return_value + def check_numeric_operators(self, context_value: int) -> bool: + return_value = False + + if self.operator == ConstraintOperators.NUM_EQ: + return_value = context_value == self.value + elif self.operator == ConstraintOperators.NUM_GT: + return_value = context_value > self.value + elif self.operator == ConstraintOperators.NUM_GTE: + return_value = context_value >= self.value + elif self.operator == ConstraintOperators.NUM_LT: + return_value = context_value < self.value + elif self.operator == ConstraintOperators.NUM_LTE: + return_value = context_value <= self.value + + return return_value + def apply(self, context: dict = None) -> bool: """ Returns true/false depending on constraint provisioning and context. @@ -73,6 +99,8 @@ def apply(self, context: dict = None) -> bool: constraint_check = self.check_list_operators(context_value=context_value) if self.operator in [ConstraintOperators.STR_CONTAINS, ConstraintOperators.STR_ENDS_WITH, ConstraintOperators.STR_STARTS_WITH]: constraint_check = self.check_string_operators(context_value=context_value) + if self.operator in [ConstraintOperators.NUM_EQ, ConstraintOperators.NUM_GT, ConstraintOperators.NUM_GTE, ConstraintOperators.NUM_LT, ConstraintOperators.NUM_LTE]: + constraint_check = self.check_numeric_operators(context_value=context_value) except Exception as excep: # pylint: disable=broad-except LOGGER.info("Could not evaluate context %s! Error: %s", self.context_name, excep) diff --git a/UnleashClient/utils.py b/UnleashClient/utils.py index 33a913fd..50d33991 100644 --- a/UnleashClient/utils.py +++ b/UnleashClient/utils.py @@ -1,4 +1,5 @@ import logging +from typing import Any import mmh3 # pylint: disable=import-error from requests import Response @@ -14,7 +15,7 @@ def normalized_hash(identifier: str, return mmh3.hash(f"{activation_group}:{identifier}", signed=False) % normalizer + 1 -def get_identifier(context_key_name: str, context: dict) -> str: +def get_identifier(context_key_name: str, context: dict) -> Any: if context_key_name in context.keys(): value = context[context_key_name] elif 'properties' in context.keys() and context_key_name in context['properties'].keys(): diff --git a/tests/unit_tests/test_constraints.py b/tests/unit_tests/test_constraints.py index cbff2ac6..b4138da9 100644 --- a/tests/unit_tests/test_constraints.py +++ b/tests/unit_tests/test_constraints.py @@ -98,3 +98,43 @@ def test_constraint_STR_STARTS_WITH(): assert constraint_not_ci.apply({'customField': "dogb"}) assert not constraint_not_ci.apply({'customField': "parrotb"}) assert not constraint_not_ci.apply({'customField': "hamsterb"}) + + +def test_constraints_NUM_EQ(): + constraint = Constraint(constraint_dict=mock_constraints.CONSTRAINT_NUM_EQ) + + assert not constraint.apply({'customField': 4}) + assert constraint.apply({'customField': 5}) + assert not constraint.apply({'customField': 6}) + + +def test_constraints_NUM_GT(): + constraint = Constraint(constraint_dict=mock_constraints.CONSTRAINT_NUM_GT) + + assert not constraint.apply({'customField': 4}) + assert not constraint.apply({'customField': 5}) + assert constraint.apply({'customField': 6}) + + +def test_constraints_NUM_GTE(): + constraint = Constraint(constraint_dict=mock_constraints.CONSTRAINT_NUM_GTE) + + assert not constraint.apply({'customField': 4}) + assert constraint.apply({'customField': 5}) + assert constraint.apply({'customField': 6}) + + +def test_constraints_NUM_LT(): + constraint = Constraint(constraint_dict=mock_constraints.CONSTRAINT_NUM_LT) + + assert constraint.apply({'customField': 4}) + assert not constraint.apply({'customField': 5}) + assert not constraint.apply({'customField': 6}) + + +def test_constraints_NUM_LTE(): + constraint = Constraint(constraint_dict=mock_constraints.CONSTRAINT_NUM_LTE) + + assert constraint.apply({'customField': 4}) + assert constraint.apply({'customField': 5}) + assert not constraint.apply({'customField': 6}) diff --git a/tests/utilities/mocks/mock_constraints.py b/tests/utilities/mocks/mock_constraints.py index a6669473..7bf55a82 100644 --- a/tests/utilities/mocks/mock_constraints.py +++ b/tests/utilities/mocks/mock_constraints.py @@ -86,3 +86,48 @@ "caseInsensitive": False, "inverted": False } + + +CONSTRAINT_NUM_EQ = \ + { + "contextName": "customField", + "operator": "NUM_EQ", + "value": 5, + "inverted": False + } + + +CONSTRAINT_NUM_GT = \ + { + "contextName": "customField", + "operator": "NUM_GT", + "value": 5, + "inverted": False + } + + +CONSTRAINT_NUM_GTE = \ + { + "contextName": "customField", + "operator": "NUM_GTE", + "value": 5, + "inverted": False + } + + +CONSTRAINT_NUM_LT = \ + { + "contextName": "customField", + "operator": "NUM_LT", + "value": 5, + "inverted": False + } + + +CONSTRAINT_NUM_LTE = \ + { + "contextName": "customField", + "operator": "NUM_LTE", + "value": 5, + "inverted": False + } From ba28699cc0a7d66d00667bbe4846348482756f73 Mon Sep 17 00:00:00 2001 From: Ivan Lee Date: Fri, 4 Feb 2022 04:43:45 -0500 Subject: [PATCH 05/10] Support date operators and currentTime context value. --- UnleashClient/__init__.py | 9 +++++-- UnleashClient/constraints/Constraint.py | 30 +++++++++++++++++++++-- pyproject.toml | 3 ++- requirements.txt | 1 + setup.py | 9 ++++++- tests/unit_tests/test_client.py | 5 ++++ tests/unit_tests/test_constraints.py | 23 +++++++++++++++++ tests/utilities/mocks/mock_constraints.py | 27 ++++++++++++++++++++ tests/utilities/mocks/mock_features.py | 20 +++++++++++++++ 9 files changed, 121 insertions(+), 6 deletions(-) diff --git a/UnleashClient/__init__.py b/UnleashClient/__init__.py index e8038a24..61e41a7c 100644 --- a/UnleashClient/__init__.py +++ b/UnleashClient/__init__.py @@ -1,8 +1,7 @@ # pylint: disable=invalid-name import warnings from datetime import datetime, timezone -from typing import Dict, Callable, Any, Optional -import copy +from typing import Callable, Optional from fcache.cache import FileCache from apscheduler.job import Job from apscheduler.schedulers.background import BackgroundScheduler @@ -227,8 +226,14 @@ def is_enabled(self, :return: Feature flag result """ context = context or {} + + # Update context with static values context.update(self.unleash_static_context) + # Update context with optional values + if 'currentTime' not in context.keys(): + context.update({'currentTime': datetime.now()}) + if self.is_initialized: try: return self.features[feature_name].is_enabled(context) diff --git a/UnleashClient/constraints/Constraint.py b/UnleashClient/constraints/Constraint.py index d6d737d8..1a05dfb3 100644 --- a/UnleashClient/constraints/Constraint.py +++ b/UnleashClient/constraints/Constraint.py @@ -1,5 +1,7 @@ # pylint: disable=invalid-name, too-few-public-methods, use-a-generator +from datetime import datetime from enum import Enum +from dateutil.parser import parse, ParserError from UnleashClient.utils import LOGGER, get_identifier @@ -20,6 +22,10 @@ class ConstraintOperators(Enum): NUM_LT = "NUM_LT" NUM_LTE = "NUM_LTE" + # Date operators + DATE_AFTER = "DATE_AFTER" + DATE_BEFORE = "DATE_BEFORE" + class Constraint: def __init__(self, constraint_dict: dict) -> None: """ @@ -82,6 +88,23 @@ def check_numeric_operators(self, context_value: int) -> bool: return return_value + + def check_date_operators(self, context_value: datetime) -> bool: + return_value = False + + try: + parsed_date = parse(self.value, ignoretz=True) + except ParserError: + LOGGER.error(f"Unable to parse date: {self.value}") + + if self.operator == ConstraintOperators.DATE_AFTER: + return_value = context_value >= parsed_date + elif self.operator == ConstraintOperators.DATE_BEFORE: + return_value = context_value <= parsed_date + + return return_value + + def apply(self, context: dict = None) -> bool: """ Returns true/false depending on constraint provisioning and context. @@ -97,10 +120,13 @@ def apply(self, context: dict = None) -> bool: if context_value: if self.operator in [ConstraintOperators.IN, ConstraintOperators.NOT_IN]: constraint_check = self.check_list_operators(context_value=context_value) - if self.operator in [ConstraintOperators.STR_CONTAINS, ConstraintOperators.STR_ENDS_WITH, ConstraintOperators.STR_STARTS_WITH]: + elif self.operator in [ConstraintOperators.STR_CONTAINS, ConstraintOperators.STR_ENDS_WITH, ConstraintOperators.STR_STARTS_WITH]: constraint_check = self.check_string_operators(context_value=context_value) - if self.operator in [ConstraintOperators.NUM_EQ, ConstraintOperators.NUM_GT, ConstraintOperators.NUM_GTE, ConstraintOperators.NUM_LT, ConstraintOperators.NUM_LTE]: + elif self.operator in [ConstraintOperators.NUM_EQ, ConstraintOperators.NUM_GT, ConstraintOperators.NUM_GTE, ConstraintOperators.NUM_LT, ConstraintOperators.NUM_LTE]: constraint_check = self.check_numeric_operators(context_value=context_value) + elif self.operator in [ConstraintOperators.DATE_AFTER, ConstraintOperators.DATE_BEFORE]: + constraint_check = self.check_date_operators(context_value=context_value) + except Exception as excep: # pylint: disable=broad-except LOGGER.info("Could not evaluate context %s! Error: %s", self.context_name, excep) diff --git a/pyproject.toml b/pyproject.toml index cec3eadc..ca83e7f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,8 @@ disable = [ "line-too-long", "missing-class-docstring", "missing-module-docstring", - "missing-function-docstring" + "missing-function-docstring", + "logging-fstring-interpolation" ] max-attributes = 25 max-args = 25 diff --git a/requirements.txt b/requirements.txt index 4e033917..c32acbf1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ requests fcache mmh3 APScheduler +python-dateutil # Development packages bumpversion diff --git a/setup.py b/setup.py index 9ef9c705..e0b68e23 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,14 @@ def readme(): url='https://github.com/Unleash/unleash-client-python', packages=find_packages(exclude=["tests*"]), package_data={"UnleashClient": ["py.typed"]}, - install_requires=["requests", "fcache", "mmh3", "apscheduler", "importlib_metadata"], + install_requires=[ + "requests", + "fcache", + "mmh3", + "apscheduler", + "importlib_metadata", + "python-dateutil" + ], setup_requires=['setuptools_scm'], zip_safe=False, include_package_data=True, diff --git a/tests/unit_tests/test_client.py b/tests/unit_tests/test_client.py index 2ce44e29..62e536a1 100644 --- a/tests/unit_tests/test_client.py +++ b/tests/unit_tests/test_client.py @@ -1,6 +1,7 @@ import time import json import warnings +from datetime import datetime import pytest import responses @@ -159,6 +160,10 @@ def test_uc_is_enabled(unleash_client): time.sleep(1) assert unleash_client.is_enabled("testFlag") + # Test default context values. + assert not unleash_client.is_enabled("testConstraintFlag") + assert unleash_client.is_enabled("testConstraintFlag", context={"currentTime": datetime(2022, 1, 21)}) + @responses.activate def test_uc_project(unleash_client_project): diff --git a/tests/unit_tests/test_constraints.py b/tests/unit_tests/test_constraints.py index b4138da9..295b7c6c 100644 --- a/tests/unit_tests/test_constraints.py +++ b/tests/unit_tests/test_constraints.py @@ -1,3 +1,5 @@ +from datetime import datetime +from sqlite3 import Date import pytest from UnleashClient.constraints import Constraint from tests.utilities.mocks import mock_constraints @@ -138,3 +140,24 @@ def test_constraints_NUM_LTE(): assert constraint.apply({'customField': 4}) assert constraint.apply({'customField': 5}) assert not constraint.apply({'customField': 6}) + + +def test_constraints_DATE_AFTER(): + constraint = Constraint(constraint_dict=mock_constraints.CONSTRAINT_DATE_AFTER) + + assert constraint.apply({'currentTime': datetime(2022, 1, 23)}) + assert constraint.apply({'currentTime': datetime(2022, 1, 22)}) + assert not constraint.apply({'currentTime': datetime(2022, 1, 21)}) + + +def test_constraints_DATE_BEFORE(): + constraint = Constraint(constraint_dict=mock_constraints.CONSTRAINT_DATE_BEFORE) + + assert not constraint.apply({'currentTime': datetime(2022, 1, 23)}) + assert constraint.apply({'currentTime': datetime(2022, 1, 22)}) + assert constraint.apply({'currentTime': datetime(2022, 1, 21)}) + + +def test_constraints_date_error(): + constraint = Constraint(constraint_dict=mock_constraints.CONSTRAINT_DATE_ERROR) + assert not constraint.apply({'currentTime': datetime(2022, 1, 23)}) diff --git a/tests/utilities/mocks/mock_constraints.py b/tests/utilities/mocks/mock_constraints.py index 7bf55a82..30ccde8f 100644 --- a/tests/utilities/mocks/mock_constraints.py +++ b/tests/utilities/mocks/mock_constraints.py @@ -131,3 +131,30 @@ "value": 5, "inverted": False } + + +CONSTRAINT_DATE_AFTER = \ + { + "contextName": "currentTime", + "operator": "DATE_AFTER", + "value": "2022-01-22T00:00:00.000Z", + "inverted": False + } + + +CONSTRAINT_DATE_BEFORE = \ + { + "contextName": "currentTime", + "operator": "DATE_BEFORE", + "value": "2022-01-22T00:00:00.000Z", + "inverted": False + } + + +CONSTRAINT_DATE_ERROR = \ + { + "contextName": "currentTime", + "operator": "DATE_AFTER", + "value": "abcd", + "inverted": False + } diff --git a/tests/utilities/mocks/mock_features.py b/tests/utilities/mocks/mock_features.py index deac2549..526c56c9 100644 --- a/tests/utilities/mocks/mock_features.py +++ b/tests/utilities/mocks/mock_features.py @@ -41,6 +41,26 @@ ], "createdAt": "2018-10-04T01:27:28.477Z" }, + { + "name": "testConstraintFlag", + "description": "This is a flag with a constraint!", + "enabled": True, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "currentTime", + "operator": "DATE_BEFORE", + "value": "2022-01-22T00:00:00.000Z", + "inverted": False + } + ], + }, + ], + "createdAt": "2018-10-04T01:27:28.477Z" + }, { "name": "testVariations", "description": "Test variation", From 15d787d72f59767814ee6cdaa196522478697941 Mon Sep 17 00:00:00 2001 From: Ivan Lee Date: Fri, 4 Feb 2022 05:39:24 -0500 Subject: [PATCH 06/10] Add semver operator. --- UnleashClient/constraints/Constraint.py | 47 +++++++++++++++++++++-- requirements.txt | 1 + setup.py | 3 +- tests/unit_tests/test_constraints.py | 30 +++++++++++++++ tests/utilities/mocks/mock_constraints.py | 27 +++++++++++++ 5 files changed, 103 insertions(+), 5 deletions(-) diff --git a/UnleashClient/constraints/Constraint.py b/UnleashClient/constraints/Constraint.py index 1a05dfb3..3ba8c50e 100644 --- a/UnleashClient/constraints/Constraint.py +++ b/UnleashClient/constraints/Constraint.py @@ -2,6 +2,7 @@ from datetime import datetime from enum import Enum from dateutil.parser import parse, ParserError +import semver from UnleashClient.utils import LOGGER, get_identifier @@ -26,6 +27,12 @@ class ConstraintOperators(Enum): DATE_AFTER = "DATE_AFTER" DATE_BEFORE = "DATE_BEFORE" + # Semver operators + SEMVER_EQ = "SEMVER_EQ" + SEMVER_GT = "SEMVER_GT" + SEMVER_LT = "SEMVER_LT" + + class Constraint: def __init__(self, constraint_dict: dict) -> None: """ @@ -91,16 +98,46 @@ def check_numeric_operators(self, context_value: int) -> bool: def check_date_operators(self, context_value: datetime) -> bool: return_value = False + parsing_exception = False try: parsed_date = parse(self.value, ignoretz=True) except ParserError: LOGGER.error(f"Unable to parse date: {self.value}") + parsing_exception = True + + if not parsing_exception: + if self.operator == ConstraintOperators.DATE_AFTER: + return_value = context_value >= parsed_date + elif self.operator == ConstraintOperators.DATE_BEFORE: + return_value = context_value <= parsed_date + + return return_value + - if self.operator == ConstraintOperators.DATE_AFTER: - return_value = context_value >= parsed_date - elif self.operator == ConstraintOperators.DATE_BEFORE: - return_value = context_value <= parsed_date + def check_semver_operators(self, context_value: str) -> bool: + return_value = False + parsing_exception = False + + try: + target_version = semver.VersionInfo.parse(self.value) + except ValueError: + LOGGER.error(f"Unable to parse server semver: {self.value}") + parsing_exception = True + + try: + context_version = semver.VersionInfo.parse(context_value) + except ValueError: + LOGGER.error(f"Unable to parse context semver: {context_value}") + parsing_exception = True + + if not parsing_exception: + if self.operator == ConstraintOperators.SEMVER_EQ: + return_value = context_version == target_version + elif self.operator == ConstraintOperators.SEMVER_GT: + return_value = context_version > target_version + elif self.operator == ConstraintOperators.SEMVER_LT: + return_value = context_version < target_version return return_value @@ -126,6 +163,8 @@ def apply(self, context: dict = None) -> bool: constraint_check = self.check_numeric_operators(context_value=context_value) elif self.operator in [ConstraintOperators.DATE_AFTER, ConstraintOperators.DATE_BEFORE]: constraint_check = self.check_date_operators(context_value=context_value) + elif self.operator in [ConstraintOperators.SEMVER_EQ, ConstraintOperators.SEMVER_GT, ConstraintOperators.SEMVER_LT]: + constraint_check = self.check_semver_operators(context_value=context_value) except Exception as excep: # pylint: disable=broad-except LOGGER.info("Could not evaluate context %s! Error: %s", self.context_name, excep) diff --git a/requirements.txt b/requirements.txt index c32acbf1..9fb99cc7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ fcache mmh3 APScheduler python-dateutil +semver # Development packages bumpversion diff --git a/setup.py b/setup.py index e0b68e23..0ef84adc 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,8 @@ def readme(): "mmh3", "apscheduler", "importlib_metadata", - "python-dateutil" + "python-dateutil", + "semver < 3.0.0" ], setup_requires=['setuptools_scm'], zip_safe=False, diff --git a/tests/unit_tests/test_constraints.py b/tests/unit_tests/test_constraints.py index 295b7c6c..60d4f008 100644 --- a/tests/unit_tests/test_constraints.py +++ b/tests/unit_tests/test_constraints.py @@ -161,3 +161,33 @@ def test_constraints_DATE_BEFORE(): def test_constraints_date_error(): constraint = Constraint(constraint_dict=mock_constraints.CONSTRAINT_DATE_ERROR) assert not constraint.apply({'currentTime': datetime(2022, 1, 23)}) + + +def test_constraints_SEMVER_EQ(): + constraint = Constraint(constraint_dict=mock_constraints.CONSTRAINT_SEMVER_EQ) + + assert not constraint.apply({'customField': '1.2.1'}) + assert constraint.apply({'customField': '1.2.2'}) + assert not constraint.apply({'customField': '1.2.3'}) + + +def test_constraints_SEMVER_GT(): + constraint = Constraint(constraint_dict=mock_constraints.CONSTRAINT_SEMVER_GT) + + assert not constraint.apply({'customField': '1.2.1'}) + assert not constraint.apply({'customField': '1.2.2'}) + assert constraint.apply({'customField': '1.2.3'}) + + +def test_constraints_SEMVER_LT(): + constraint = Constraint(constraint_dict=mock_constraints.CONSTRAINT_SEMVER_LT) + + assert constraint.apply({'customField': '1.2.1'}) + assert not constraint.apply({'customField': '1.2.2'}) + assert not constraint.apply({'customField': '1.2.3'}) + + +def test_constraints_semverexception(): + constraint = Constraint(constraint_dict=mock_constraints.CONSTRAINT_SEMVER_EQ) + + assert not constraint.apply({'customField': 'hamstershamsterhamsters'}) diff --git a/tests/utilities/mocks/mock_constraints.py b/tests/utilities/mocks/mock_constraints.py index 30ccde8f..41d4d7ac 100644 --- a/tests/utilities/mocks/mock_constraints.py +++ b/tests/utilities/mocks/mock_constraints.py @@ -158,3 +158,30 @@ "value": "abcd", "inverted": False } + + +CONSTRAINT_SEMVER_EQ = \ + { + "contextName": "customField", + "operator": "SEMVER_EQ", + "value": "1.2.2", + "inverted": False + } + + +CONSTRAINT_SEMVER_GT = \ + { + "contextName": "customField", + "operator": "SEMVER_GT", + "value": "1.2.2", + "inverted": False + } + + +CONSTRAINT_SEMVER_LT = \ + { + "contextName": "customField", + "operator": "SEMVER_LT", + "value": "1.2.2", + "inverted": False + } From f15b66f78b3047df843860eca8e5f62656fda4a3 Mon Sep 17 00:00:00 2001 From: Ivan Lee Date: Fri, 4 Feb 2022 06:00:53 -0500 Subject: [PATCH 07/10] Add spec tests. --- CHANGELOG.md | 1 + UnleashClient/constraints/Constraint.py | 2 +- .../test_13_constraint_operators.py | 920 ++++++++++++++++++ 3 files changed, 922 insertions(+), 1 deletion(-) create mode 100644 tests/specification_tests/test_13_constraint_operators.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0078adc2..89786a6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## Next version +* (Major) Support new constraint operators. * (Minor) Refactor `unleash-client-python` to modernize tooling (`setuptools_scm` and centralizing tool config in `pyproject.toml`). * (Minor) Migrate documentation to Sphinx. diff --git a/UnleashClient/constraints/Constraint.py b/UnleashClient/constraints/Constraint.py index 3ba8c50e..ba7798b1 100644 --- a/UnleashClient/constraints/Constraint.py +++ b/UnleashClient/constraints/Constraint.py @@ -154,7 +154,7 @@ def apply(self, context: dict = None) -> bool: try: context_value = get_identifier(self.context_name, context) - if context_value: + if context_value is not None: if self.operator in [ConstraintOperators.IN, ConstraintOperators.NOT_IN]: constraint_check = self.check_list_operators(context_value=context_value) elif self.operator in [ConstraintOperators.STR_CONTAINS, ConstraintOperators.STR_ENDS_WITH, ConstraintOperators.STR_STARTS_WITH]: diff --git a/tests/specification_tests/test_13_constraint_operators.py b/tests/specification_tests/test_13_constraint_operators.py new file mode 100644 index 00000000..9aa09e4c --- /dev/null +++ b/tests/specification_tests/test_13_constraint_operators.py @@ -0,0 +1,920 @@ +import uuid +from datetime import datetime +import json +import pytest +import responses +from UnleashClient import UnleashClient +from UnleashClient.constants import FEATURES_URL, METRICS_URL, REGISTER_URL +from tests.utilities.testing_constants import URL, APP_NAME + + +MOCK_JSON = """ + { + "version": 1, + "features": [ + { + "name": "F1.startsWith", + "description": "startsWith", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "customField", + "operator": "STR_STARTS_WITH", + "values": [ + "some-string" + ] + } + ] + } + ] + }, + { + "name": "F2.startsWith.multiple", + "description": "endsWith", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "customField", + "operator": "STR_STARTS_WITH", + "values": [ + "e1", + "e2" + ] + } + ] + } + ] + }, + { + "name": "F3.endsWith", + "description": "endsWith", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "email", + "operator": "STR_ENDS_WITH", + "values": [ + "@some-email.com" + ] + } + ] + } + ] + }, + { + "name": "F3.endsWith.ignoringCase", + "description": "endsWith", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "email", + "operator": "STR_ENDS_WITH", + "values": [ + "@some-email.com" + ], + "caseInsensitive": true + } + ] + } + ] + }, + { + "name": "F4.contains", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "email", + "operator": "STR_CONTAINS", + "values": [ + "email" + ] + } + ] + } + ] + }, + { + "name": "F4.contains.inverted", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "email", + "operator": "STR_CONTAINS", + "values": [ + "email" + ], + "inverted": true + } + ] + } + ] + }, + { + "name": "F5.numEq", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "someValue", + "operator": "NUM_EQ", + "value": 12 + } + ] + } + ] + }, + { + "name": "F5.numEq.inverted", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "someValue", + "operator": "NUM_EQ", + "value": 12, + "inverted": true + } + ] + } + ] + }, + { + "name": "F5.numGT", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "someValue", + "operator": "NUM_GT", + "value": 12 + } + ] + } + ] + }, + { + "name": "F5.numGTE", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "someValue", + "operator": "NUM_GTE", + "value": 12 + } + ] + } + ] + }, + { + "name": "F5.numLT", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "someValue", + "operator": "NUM_LT", + "value": 12 + } + ] + } + ] + }, + { + "name": "F5.numLTE", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "someValue", + "operator": "NUM_LTE", + "value": 12 + } + ] + } + ] + }, + { + "name": "F6.number-range", + "description": "range of numbers", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "someValue", + "operator": "NUM_GT", + "value": 12 + }, + { + "contextName": "someValue", + "operator": "NUM_LT", + "value": 16 + } + ] + } + ] + }, + { + "name": "F7.dateAfter", + "description": "dates", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "currentTime", + "operator": "DATE_AFTER", + "value": "2022-01-29T13:00:00.000Z" + } + ] + } + ] + }, + { + "name": "F7.dateBefore", + "description": "dates", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "currentTime", + "operator": "DATE_BEFORE", + "value": "2022-01-29T13:00:00.000Z" + } + ] + } + ] + }, + { + "name": "F7.data-range", + "description": "dates", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "currentTime", + "operator": "DATE_AFTER", + "value": "2022-01-22T13:00:00.000Z" + }, + { + "contextName": "currentTime", + "operator": "DATE_BEFORE", + "value": "2022-01-29T13:00:00.000Z" + } + ] + } + ] + }, + { + "name": "F8.semverEQ", + "description": "semver", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "version", + "operator": "SEMVER_EQ", + "value": "1.2.2" + } + ] + } + ] + }, + { + "name": "F8.semverGT", + "description": "semver", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "version", + "operator": "SEMVER_GT", + "value": "1.2.2" + } + ] + } + ] + }, + { + "name": "F8.semverLT", + "description": "semver", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "version", + "operator": "SEMVER_LT", + "value": "1.2.2" + } + ] + } + ] + } + ] +} +""" + + +@pytest.fixture() +def unleash_client(): + unleash_client = UnleashClient(url=URL, + app_name=APP_NAME, + instance_id='pytest_%s' % uuid.uuid4()) + yield unleash_client + unleash_client.destroy() + + +@responses.activate +def test_feature_constraintoperators_startswith_enabled(unleash_client): + """ + F1.startsWith should be enabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F1.startsWith", {'customField': 'some-string-is-cool'}) + + +@responses.activate +def test_feature_constraintoperators_startswith_disabled(unleash_client): + """ + F1.startsWith should be disabled" + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F1.startsWith", {'customField': 'some2-string-is-cool'}) + + +@responses.activate +def test_feature_constraintoperators_startswith_enabled_multiple(unleash_client): + """ + F2.startsWith.multiple should be enabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F2.startsWith.multiple", {'customField': 'e2 cool'}) + + +@responses.activate +def test_feature_constraintoperators_startswith_disabled_multiple(unleash_client): + """ + F2.startsWith.multiple should be enabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F2.startsWith.multiple", {'customField': 'cool e2'}) + + +@responses.activate +def test_feature_constraintoperators_endswith_enabled(unleash_client): + """ + F3.endsWith should be enabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F3.endsWith", {'email': '@some-email.com'}) + + +@responses.activate +def test_feature_constraintoperators_endswith_incorrectcasing(unleash_client): + """ + F3.endsWith should be disabled when casing is incorrect + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F3.endsWith", {'email': '@some-EMAIL.com'}) + + +@responses.activate +def test_feature_constraintoperators_endswith_ignorecasing(unleash_client): + """ + F3.endsWith should be disabled when casing is incorrect + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F3.endsWith.ignoringCase", {'email': '@SOME-EMAIL.com'}) + + +@responses.activate +def test_feature_constraintoperators_endswith_disabled(unleash_client): + """ + F3.endsWith should be disabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F3.endsWith", {'email': '@another-email.com'}) + + +@responses.activate +def test_feature_constraintoperators_contains_enabled(unleash_client): + """ + F4.contains should be enabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F4.contains", {'email': '@some-email.com'}) + + +@responses.activate +def test_feature_constraintoperators_contains_disabled(unleash_client): + """ + F4.contains should be disabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F4.contains", {'email': '@another.com'}) + + +@responses.activate +def test_feature_constraintoperators_contains_inverted(unleash_client): + """ + F4.contains.inverted should be enabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F4.contains.inverted", {'email': '@another.com'}) + + +@responses.activate +def test_feature_constraintoperators_numeq_enabled(unleash_client): + """ + F5.numEq + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F5.numEq", {'someValue': 12}) + + +@responses.activate +def test_feature_constraintoperators_numeq_inverted(unleash_client): + """ + F5.numEq.inverted should be true + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F5.numEq.inverted", {'someValue': 12}) + + +@responses.activate +def test_feature_constraintoperators_numgt_enabled(unleash_client): + """ + F5.numGT should be true + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F5.numGT", {'someValue': 13}) + + +@responses.activate +def test_feature_constraintoperators_numgt_disabled(unleash_client): + """ + F5.numGT should be false + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F5.numGT", {'someValue': 12}) + + +@responses.activate +def test_feature_constraintoperators_numgte_enabled_equal(unleash_client): + """ + F5.numGTE should be true when equal + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F5.numGTE", {'someValue': 12}) + + +@responses.activate +def test_feature_constraintoperators_numgte_enabled_greater(unleash_client): + """ + F5.numGTE should be true when larger + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F5.numGTE", {'someValue': 13}) + + +@responses.activate +def test_feature_constraintoperators_numgte_disabled(unleash_client): + """ + F5.numGTE should be false when lower + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F5.numGTE", {'someValue': 11}) + + +@responses.activate +def test_feature_constraintoperators_numlte_enabled_equal(unleash_client): + """ + F5.numLTE should be true when equal + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F5.numLTE", {'someValue': 12}) + + +@responses.activate +def test_feature_constraintoperators_numlte_enabled_less(unleash_client): + """ + F5.numLTE should be true when lower + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F5.numLTE", {'someValue': 0}) + + +@responses.activate +def test_feature_constraintoperators_numlt_enabled_less(unleash_client): + """ + F5.numLT should be true when lower + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F5.numLT", {'someValue': 0}) + + +@responses.activate +def test_feature_constraintoperators_numlt_disabled_equal(unleash_client): + """ + F5.numLT should be false when equal + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F5.numLT", {'someValue': 12}) + + +@responses.activate +def test_feature_constraintoperators_numberranger_disabled(unleash_client): + """ + F6.number-range should be false when not in range + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F6.number-range", {'someValue': 11}) + + +@responses.activate +def test_feature_constraintoperators_numberranger_enabled(unleash_client): + """ + F6.number-range should be true when in range + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F6.number-range", {'someValue': 14}) + + +@responses.activate +def test_feature_constraintoperators_dateafter_enabled(unleash_client): + """ + F7.dateAfter should be enabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F7.dateAfter", {'currentTime': datetime(2022, 1, 30, 13)}) + + +@responses.activate +def test_feature_constraintoperators_dateafter_disabled(unleash_client): + """ + F7.dateAfter should be disabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F7.dateAfter", {'currentTime': datetime(2022, 1, 28, 13)}) + + +@responses.activate +def test_feature_constraintoperators_datebefore_enabled(unleash_client): + """ + F7.dateBefore should be enabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F7.dateBefore", {'currentTime': datetime(2022, 1, 28, 13)}) + + +@responses.activate +def test_feature_constraintoperators_datebefore_disabled(unleash_client): + """ + F7.dateBefore should be disabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F7.dateBefore", {'currentTime': datetime(2022, 1, 30, 13)}) + + +@responses.activate +def test_feature_constraintoperators_daterange(unleash_client): + """ + "F7.data-range should be enabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F7.date-range", {'currentTime': datetime(2022, 1, 25, 13)}) + + +@responses.activate +def test_feature_constraintoperators_semvereq_enabled(unleash_client): + """ + F8.semverEQ should be enabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F8.semverEQ", {'version': '1.2.2'}) + + +@responses.activate +def test_feature_constraintoperators_semvereq_disabled(unleash_client): + """ + F8.semverEQ should be disabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F8.semverEQ", {'version': '1.2.0'}) + + +@responses.activate +def test_feature_constraintoperators_semvergt_enabled(unleash_client): + """ + F8.semverGT should be enabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F8.semverGT", {'version': '1.2.3'}) + + +@responses.activate +def test_feature_constraintoperators_semvergt_disabled(unleash_client): + """ + F8.semverGT should be disabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F8.semverGT", {'version': '1.2.0'}) + + +@responses.activate +def test_feature_constraintoperators_semverlt_enabled(unleash_client): + """ + F8.semverLT should be enabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F8.semverLT", {'version': '1.2.1'}) + + +@responses.activate +def test_feature_constraintoperators_semverlt_disabled(unleash_client): + """ + F8.semverLT should be disabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F8.semverLT", {'version': '1.2.3'}) + \ No newline at end of file From a2e0ac2e173ff5ab17629bd4fcfbe8d66022a526 Mon Sep 17 00:00:00 2001 From: Ivan Lee Date: Fri, 4 Feb 2022 06:05:04 -0500 Subject: [PATCH 08/10] Install types. --- .github/workflows/pull_request.yml | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 87cc8019..b3709879 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -20,7 +20,7 @@ jobs: python setup.py install - name: Linting run: | - mypy UnleashClient + mypy UnleashClient --install-types --non-interactive pylint UnleashClient - name: Unit tests run: | diff --git a/tox.ini b/tox.ini index 3f29f9cd..cf238180 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = py36,py37,py38,py39,py310 [testenv] deps = -rrequirements.txt commands = - mypy UnleashClient + mypy UnleashClient --install-types --non-interactive pylint UnleashClient py.test tests/unit_tests py.test tests/specification_tests From f3a285dd974999311f832b6a822d5bec299cf881 Mon Sep 17 00:00:00 2001 From: Ivan Lee Date: Fri, 4 Feb 2022 06:23:48 -0500 Subject: [PATCH 09/10] Initialize semver vars. --- UnleashClient/constraints/Constraint.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/UnleashClient/constraints/Constraint.py b/UnleashClient/constraints/Constraint.py index ba7798b1..d2823719 100644 --- a/UnleashClient/constraints/Constraint.py +++ b/UnleashClient/constraints/Constraint.py @@ -1,4 +1,5 @@ # pylint: disable=invalid-name, too-few-public-methods, use-a-generator +from typing import Optional from datetime import datetime from enum import Enum from dateutil.parser import parse, ParserError @@ -118,6 +119,8 @@ def check_date_operators(self, context_value: datetime) -> bool: def check_semver_operators(self, context_value: str) -> bool: return_value = False parsing_exception = False + target_version: Optional[semver.VersionInfo] = None + context_version: Optional[semver.VersionInfo] = None try: target_version = semver.VersionInfo.parse(self.value) From abd47fb53763c19c021ff5e7bb6b97b372afa4d6 Mon Sep 17 00:00:00 2001 From: Ivan Lee Date: Wed, 9 Feb 2022 07:08:12 -0500 Subject: [PATCH 10/10] Round 2 of spec changes. --- UnleashClient/constraints/Constraint.py | 19 +- .../test_13_constraint_operators.py | 806 ++++++++---------- .../test_14_constraint_semver_operators.py | 420 +++++++++ tests/unit_tests/test_constraints.py | 12 +- tests/utilities/mocks/mock_constraints.py | 17 +- 5 files changed, 819 insertions(+), 455 deletions(-) create mode 100644 tests/specification_tests/test_14_constraint_semver_operators.py diff --git a/UnleashClient/constraints/Constraint.py b/UnleashClient/constraints/Constraint.py index d2823719..1816401d 100644 --- a/UnleashClient/constraints/Constraint.py +++ b/UnleashClient/constraints/Constraint.py @@ -1,5 +1,5 @@ # pylint: disable=invalid-name, too-few-public-methods, use-a-generator -from typing import Optional +from typing import Optional, Union from datetime import datetime from enum import Enum from dateutil.parser import parse, ParserError @@ -80,19 +80,20 @@ def check_string_operators(self, context_value: str) -> bool: return return_value - def check_numeric_operators(self, context_value: int) -> bool: + def check_numeric_operators(self, context_value: Union[float, int]) -> bool: return_value = False + parsed_value = float(self.value) if self.operator == ConstraintOperators.NUM_EQ: - return_value = context_value == self.value + return_value = context_value == parsed_value elif self.operator == ConstraintOperators.NUM_GT: - return_value = context_value > self.value + return_value = context_value > parsed_value elif self.operator == ConstraintOperators.NUM_GTE: - return_value = context_value >= self.value + return_value = context_value >= parsed_value elif self.operator == ConstraintOperators.NUM_LT: - return_value = context_value < self.value + return_value = context_value < parsed_value elif self.operator == ConstraintOperators.NUM_LTE: - return_value = context_value <= self.value + return_value = context_value <= parsed_value return return_value @@ -109,9 +110,9 @@ def check_date_operators(self, context_value: datetime) -> bool: if not parsing_exception: if self.operator == ConstraintOperators.DATE_AFTER: - return_value = context_value >= parsed_date + return_value = context_value > parsed_date elif self.operator == ConstraintOperators.DATE_BEFORE: - return_value = context_value <= parsed_date + return_value = context_value < parsed_date return return_value diff --git a/tests/specification_tests/test_13_constraint_operators.py b/tests/specification_tests/test_13_constraint_operators.py index 9aa09e4c..9b74356f 100644 --- a/tests/specification_tests/test_13_constraint_operators.py +++ b/tests/specification_tests/test_13_constraint_operators.py @@ -9,379 +9,335 @@ MOCK_JSON = """ - { +{ "version": 1, "features": [ - { - "name": "F1.startsWith", - "description": "startsWith", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "customField", - "operator": "STR_STARTS_WITH", - "values": [ - "some-string" - ] - } + { + "name": "F1.startsWith", + "description": "startsWith", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "customField", + "operator": "STR_STARTS_WITH", + "values": ["some-string"] + + } + ] + } ] - } - ] - }, - { - "name": "F2.startsWith.multiple", - "description": "endsWith", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "customField", - "operator": "STR_STARTS_WITH", - "values": [ - "e1", - "e2" - ] - } + }, + { + "name": "F2.startsWith.multiple", + "description": "endsWith", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "customField", + "operator": "STR_STARTS_WITH", + "values": ["e1", "e2"] + + } + ] + } ] - } - ] - }, - { - "name": "F3.endsWith", - "description": "endsWith", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "email", - "operator": "STR_ENDS_WITH", - "values": [ - "@some-email.com" - ] - } + }, + { + "name": "F3.endsWith", + "description": "endsWith", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "email", + "operator": "STR_ENDS_WITH", + "values": ["@some-email.com"] + } + ] + } ] - } - ] - }, - { - "name": "F3.endsWith.ignoringCase", - "description": "endsWith", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "email", - "operator": "STR_ENDS_WITH", - "values": [ - "@some-email.com" - ], - "caseInsensitive": true - } + }, + { + "name": "F3.endsWith.ignoringCase", + "description": "endsWith", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "email", + "operator": "STR_ENDS_WITH", + "values": ["@some-email.com"], + "caseInsensitive": true + } + ] + } ] - } - ] - }, - { - "name": "F4.contains", - "description": "contains", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "email", - "operator": "STR_CONTAINS", - "values": [ - "email" - ] - } + }, + { + "name": "F4.contains", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "email", + "operator": "STR_CONTAINS", + "values": ["email"] + + } + ] + } ] - } - ] - }, - { - "name": "F4.contains.inverted", - "description": "contains", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "email", - "operator": "STR_CONTAINS", - "values": [ - "email" - ], - "inverted": true - } + }, + { + "name": "F4.contains.inverted", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "email", + "operator": "STR_CONTAINS", + "values": ["email"], + "inverted": true + + } + ] + } ] - } - ] - }, - { - "name": "F5.numEq", - "description": "contains", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "someValue", - "operator": "NUM_EQ", - "value": 12 - } + }, + { + "name": "F5.numEq", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "someValue", + "operator": "NUM_EQ", + "value": "12" + } + ] + } ] - } - ] - }, - { - "name": "F5.numEq.inverted", - "description": "contains", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "someValue", - "operator": "NUM_EQ", - "value": 12, - "inverted": true - } + }, + { + "name": "F5.numEq.float", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "someValue", + "operator": "NUM_EQ", + "value": "12.0" + } + ] + } ] - } - ] - }, - { - "name": "F5.numGT", - "description": "contains", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "someValue", - "operator": "NUM_GT", - "value": 12 - } + }, + { + "name": "F5.numEq.inverted", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "someValue", + "operator": "NUM_EQ", + "value": "12", + "inverted": true + } + ] + } ] - } - ] - }, - { - "name": "F5.numGTE", - "description": "contains", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "someValue", - "operator": "NUM_GTE", - "value": 12 - } + }, + { + "name": "F5.numGT", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "someValue", + "operator": "NUM_GT", + "value": "12" + } + ] + } ] - } - ] - }, - { - "name": "F5.numLT", - "description": "contains", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "someValue", - "operator": "NUM_LT", - "value": 12 - } + }, + { + "name": "F5.numGTE", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "someValue", + "operator": "NUM_GTE", + "value": "12" + } + ] + } ] - } - ] - }, - { - "name": "F5.numLTE", - "description": "contains", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "someValue", - "operator": "NUM_LTE", - "value": 12 - } + }, + { + "name": "F5.numLT", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "someValue", + "operator": "NUM_LT", + "value": "12" + } + ] + } ] - } - ] - }, - { - "name": "F6.number-range", - "description": "range of numbers", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "someValue", - "operator": "NUM_GT", - "value": 12 - }, - { - "contextName": "someValue", - "operator": "NUM_LT", - "value": 16 - } + }, + { + "name": "F5.numLTE", + "description": "contains", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "someValue", + "operator": "NUM_LTE", + "value": "12" + } + ] + } ] - } - ] - }, - { - "name": "F7.dateAfter", - "description": "dates", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "currentTime", - "operator": "DATE_AFTER", - "value": "2022-01-29T13:00:00.000Z" - } + }, + { + "name": "F6.number-range", + "description": "range of numbers", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "someValue", + "operator": "NUM_GT", + "value": "12" + }, + { + "contextName": "someValue", + "operator": "NUM_LT", + "value": "16" + } + ] + } ] - } - ] - }, - { - "name": "F7.dateBefore", - "description": "dates", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "currentTime", - "operator": "DATE_BEFORE", - "value": "2022-01-29T13:00:00.000Z" - } + }, + { + "name": "F7.dateAfter", + "description": "dates", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "currentTime", + "operator": "DATE_AFTER", + "value": "2022-01-29T13:00:00.000Z" + } + ] + } ] - } - ] - }, - { - "name": "F7.data-range", - "description": "dates", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "currentTime", - "operator": "DATE_AFTER", - "value": "2022-01-22T13:00:00.000Z" - }, - { - "contextName": "currentTime", - "operator": "DATE_BEFORE", - "value": "2022-01-29T13:00:00.000Z" - } + }, + { + "name": "F7.dateBefore", + "description": "dates", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "currentTime", + "operator": "DATE_BEFORE", + "value": "2022-01-29T13:00:00.000Z" + } + ] + } ] - } - ] - }, - { - "name": "F8.semverEQ", - "description": "semver", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "version", - "operator": "SEMVER_EQ", - "value": "1.2.2" - } + }, + { + "name": "F7.date-range", + "description": "dates", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "currentTime", + "operator": "DATE_AFTER", + "value": "2022-01-22T13:00:00.000Z" + }, + { + "contextName": "currentTime", + "operator": "DATE_BEFORE", + "value": "2022-01-29T13:00:00.000Z" + } + ] + } ] - } - ] - }, - { - "name": "F8.semverGT", - "description": "semver", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "version", - "operator": "SEMVER_GT", - "value": "1.2.2" - } - ] - } - ] - }, - { - "name": "F8.semverLT", - "description": "semver", - "enabled": true, - "strategies": [ - { - "name": "default", - "parameters": {}, - "constraints": [ - { - "contextName": "version", - "operator": "SEMVER_LT", - "value": "1.2.2" - } - ] - } - ] - } + } + ] -} + } """ @@ -574,6 +530,21 @@ def test_feature_constraintoperators_numeq_enabled(unleash_client): assert unleash_client.is_enabled("F5.numEq", {'someValue': 12}) +@responses.activate +def test_feature_constraintoperators_numeq_enabled_floats(unleash_client): + """ + F5.numEq works for floats + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F5.numEq", {'someValue': 12.0}) + + @responses.activate def test_feature_constraintoperators_numeq_inverted(unleash_client): """ @@ -589,6 +560,36 @@ def test_feature_constraintoperators_numeq_inverted(unleash_client): assert not unleash_client.is_enabled("F5.numEq.inverted", {'someValue': 12}) +@responses.activate +def test_feature_constraintoperators_numeqfloat_enabled_floats(unleash_client): + """ + F5.numEq.float works for floats + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F5.numEq.float", {'someValue': 12.0}) + + +@responses.activate +def test_feature_constraintoperators_numeqfloat_enabled_int(unleash_client): + """ + F5.numEq.float works for integers + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F5.numEq.float", {'someValue': 12}) + + @responses.activate def test_feature_constraintoperators_numgt_enabled(unleash_client): """ @@ -785,84 +786,9 @@ def test_feature_constraintoperators_dateafter_disabled(unleash_client): @responses.activate -def test_feature_constraintoperators_datebefore_enabled(unleash_client): - """ - F7.dateBefore should be enabled - """ - # Set up API - responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) - responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) - responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) - - # Tests - unleash_client.initialize_client() - assert unleash_client.is_enabled("F7.dateBefore", {'currentTime': datetime(2022, 1, 28, 13)}) - - -@responses.activate -def test_feature_constraintoperators_datebefore_disabled(unleash_client): - """ - F7.dateBefore should be disabled - """ - # Set up API - responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) - responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) - responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) - - # Tests - unleash_client.initialize_client() - assert not unleash_client.is_enabled("F7.dateBefore", {'currentTime': datetime(2022, 1, 30, 13)}) - - -@responses.activate -def test_feature_constraintoperators_daterange(unleash_client): - """ - "F7.data-range should be enabled - """ - # Set up API - responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) - responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) - responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) - - # Tests - unleash_client.initialize_client() - assert not unleash_client.is_enabled("F7.date-range", {'currentTime': datetime(2022, 1, 25, 13)}) - - -@responses.activate -def test_feature_constraintoperators_semvereq_enabled(unleash_client): - """ - F8.semverEQ should be enabled - """ - # Set up API - responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) - responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) - responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) - - # Tests - unleash_client.initialize_client() - assert unleash_client.is_enabled("F8.semverEQ", {'version': '1.2.2'}) - - -@responses.activate -def test_feature_constraintoperators_semvereq_disabled(unleash_client): +def test_feature_constraintoperators_dateafter_exclusive(unleash_client): """ - F8.semverEQ should be disabled - """ - # Set up API - responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) - responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) - responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) - - # Tests - unleash_client.initialize_client() - assert not unleash_client.is_enabled("F8.semverEQ", {'version': '1.2.0'}) - - -@responses.activate -def test_feature_constraintoperators_semvergt_enabled(unleash_client): - """ - F8.semverGT should be enabled + F7.dateAfter should be disabled """ # Set up API responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) @@ -871,13 +797,13 @@ def test_feature_constraintoperators_semvergt_enabled(unleash_client): # Tests unleash_client.initialize_client() - assert unleash_client.is_enabled("F8.semverGT", {'version': '1.2.3'}) + assert not unleash_client.is_enabled("F7.dateAfter", {'currentTime': datetime(2022, 1, 29, 13)}) @responses.activate -def test_feature_constraintoperators_semvergt_disabled(unleash_client): +def test_feature_constraintoperators_datebefore_enabled(unleash_client): """ - F8.semverGT should be disabled + F7.dateBefore should be enabled """ # Set up API responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) @@ -886,13 +812,13 @@ def test_feature_constraintoperators_semvergt_disabled(unleash_client): # Tests unleash_client.initialize_client() - assert not unleash_client.is_enabled("F8.semverGT", {'version': '1.2.0'}) + assert unleash_client.is_enabled("F7.dateBefore", {'currentTime': datetime(2022, 1, 28, 13)}) @responses.activate -def test_feature_constraintoperators_semverlt_enabled(unleash_client): +def test_feature_constraintoperators_datebefore_disabled(unleash_client): """ - F8.semverLT should be enabled + F7.dateBefore should be disabled """ # Set up API responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) @@ -901,13 +827,13 @@ def test_feature_constraintoperators_semverlt_enabled(unleash_client): # Tests unleash_client.initialize_client() - assert unleash_client.is_enabled("F8.semverLT", {'version': '1.2.1'}) + assert not unleash_client.is_enabled("F7.dateBefore", {'currentTime': datetime(2022, 1, 30, 13)}) @responses.activate -def test_feature_constraintoperators_semverlt_disabled(unleash_client): +def test_feature_constraintoperators_daterange(unleash_client): """ - F8.semverLT should be disabled + "F7.data-range should be enabled """ # Set up API responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) @@ -916,5 +842,5 @@ def test_feature_constraintoperators_semverlt_disabled(unleash_client): # Tests unleash_client.initialize_client() - assert not unleash_client.is_enabled("F8.semverLT", {'version': '1.2.3'}) + assert unleash_client.is_enabled("F7.date-range", {'currentTime': datetime(2022, 1, 25, 13)}) \ No newline at end of file diff --git a/tests/specification_tests/test_14_constraint_semver_operators.py b/tests/specification_tests/test_14_constraint_semver_operators.py new file mode 100644 index 00000000..bd1ad1a6 --- /dev/null +++ b/tests/specification_tests/test_14_constraint_semver_operators.py @@ -0,0 +1,420 @@ +import uuid +import json +import pytest +import responses +from UnleashClient import UnleashClient +from UnleashClient.constants import FEATURES_URL, METRICS_URL, REGISTER_URL +from tests.utilities.testing_constants import URL, APP_NAME + + +MOCK_JSON = """ +{ + "version": 2, + "features": [ + { + "name": "F8.semverEQ", + "description": "semver", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "version", + "operator": "SEMVER_EQ", + "value": "1.2.2" + } + ] + } + ] + }, + { + "name": "F8.semverGT", + "description": "semver", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "version", + "operator": "SEMVER_GT", + "value": "1.2.2" + } + ] + } + ] + }, + { + "name": "F8.semverLT", + "description": "semver", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "version", + "operator": "SEMVER_LT", + "value": "1.2.2" + } + ] + } + ] + }, + { + "name": "F8.semverAlphaGT", + "description": "semver", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "version", + "operator": "SEMVER_GT", + "value": "2.0.0-alpha.1" + } + ] + } + ] + }, + { + "name": "F8.semverAlphaLT", + "description": "semver", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "version", + "operator": "SEMVER_LT", + "value": "2.0.0-alpha.1" + } + ] + } + ] + }, + { + "name": "F8.semverAlphaVersioning", + "description": "semver", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "version", + "operator": "SEMVER_LT", + "value": "2.0.0-alpha.3" + } + ] + } + ] + }, + { + "name": "F8.alphaUnnumbered", + "description": "semver", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "version", + "operator": "SEMVER_GT", + "value": "2.0.0-alpha" + } + ] + } + ] + }, + { + "name": "F8.releaseCandidate", + "description": "semver", + "enabled": true, + "strategies": [ + { + "name": "default", + "parameters": {}, + "constraints": [ + { + "contextName": "version", + "operator": "SEMVER_GT", + "value": "2.0.0-rc" + } + ] + } + ] + } + ] +} +""" + + +@pytest.fixture() +def unleash_client(): + unleash_client = UnleashClient(url=URL, + app_name=APP_NAME, + instance_id='pytest_%s' % uuid.uuid4()) + yield unleash_client + unleash_client.destroy() + + +@responses.activate +def test_feature_constraintoperators_semvereq_enabled(unleash_client): + """ + F8.semverEQ should be enabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F8.semverEQ", {'version': '1.2.2'}) + + +@responses.activate +def test_feature_constraintoperators_semvereq_disabled(unleash_client): + """ + F8.semverEQ should be disabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F8.semverEQ", {'version': '1.2.0'}) + + +@responses.activate +def test_feature_constraintoperators_semvergt_enabled(unleash_client): + """ + F8.semverGT should be enabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F8.semverGT", {'version': '1.2.3'}) + + +@responses.activate +def test_feature_constraintoperators_semvergt_disabled(unleash_client): + """ + F8.semverGT should be disabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F8.semverGT", {'version': '1.2.0'}) + + +@responses.activate +def test_feature_constraintoperators_semverlt_enabled(unleash_client): + """ + F8.semverLT should be enabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F8.semverLT", {'version': '1.2.1'}) + + +@responses.activate +def test_feature_constraintoperators_semverlt_disabled(unleash_client): + """ + F8.semverLT should be disabled + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F8.semverLT", {'version': '1.2.3'}) + + +@responses.activate +def test_feature_constraintoperators_semveralphagt_beta(unleash_client): + """ + F8.semverAlphaGT is less than beta + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F8.semverAlphaGT", {'version': '2.0.0-beta.1'}) + + +@responses.activate +def test_feature_constraintoperators_semveralphagt_release(unleash_client): + """ + F8.semverAlphaGT is less than release + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F8.semverAlphaGT", {'version': '2.0.0'}) + + +@responses.activate +def test_feature_constraintoperators_semveralphalt_beta(unleash_client): + """ + F8.semverAlphaLT is less than beta + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F8.semverAlphaLT", {'version': '2.0.0-beta.1'}) + + +@responses.activate +def test_feature_constraintoperators_semveralphalt_oldbeta(unleash_client): + """ + F8.semverAlphaLT is greater than old beta + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F8.semverAlphaLT", {'version': '1.9.1-beta.1'}) + + +@responses.activate +def test_feature_constraintoperators_semveralphalt_release(unleash_client): + """ + F8.semverAlpha is less than release + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F8.semverAlphaLT", {'version': '2.0.0'}) + + +@responses.activate +def test_feature_constraintoperators_semveralphalt_alphas_lt(unleash_client): + """ + F8.semverAlphaVersioning alpha.1 is less than alpha.3 + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F8.semverAlphaVersioning", {'version': '2.0.0-alpha.1'}) + + +@responses.activate +def test_feature_constraintoperators_semveralphalt_dif_alphas_gt(unleash_client): + """ + F8.semverAlphaVersioning alpha.4 is greater than alpha.3 + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F8.semverAlphaVersioning", {'version': '2.0.0-alpha.4'}) + + +@responses.activate +def test_feature_constraintoperators_semverunnumbered_lt(unleash_client): + """ + "F8.alphaUnnumbered - unnumbered is LT than numbered + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert unleash_client.is_enabled("F8.alphaUnnumbered", {'version': '2.0.0-alpha.1'}) + + +def test_feature_constraintoperators_semverrc_alpha(unleash_client): + """ + F8.releaseCandidate - alpha is not greater than rc + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F8.releaseCandidate", {'version': '2.0.0-alpha.1'}) + +def test_feature_constraintoperators_semverc_beta(unleash_client): + """ + F8.releaseCandidate - beta is not greater tha rc + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F8.releaseCandidate", {'version': '2.0.0-beta.1'}) + + +def test_feature_constraintoperators_semverc_release(unleash_client): + """ + F8.releaseCandidate - release is greater than rc + """ + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Tests + unleash_client.initialize_client() + assert not unleash_client.is_enabled("F8.releaseCandidate", {'version': '2.0.0'}) diff --git a/tests/unit_tests/test_constraints.py b/tests/unit_tests/test_constraints.py index 60d4f008..0a730abe 100644 --- a/tests/unit_tests/test_constraints.py +++ b/tests/unit_tests/test_constraints.py @@ -142,11 +142,19 @@ def test_constraints_NUM_LTE(): assert not constraint.apply({'customField': 6}) +def test_constraints_NUM_FLOAT(): + constraint = Constraint(constraint_dict=mock_constraints.CONSTRAINT_NUM_FLOAT) + + assert constraint.apply({'customField': 5}) + assert constraint.apply({'customField': 5.1}) + assert not constraint.apply({'customField': 5.2}) + + def test_constraints_DATE_AFTER(): constraint = Constraint(constraint_dict=mock_constraints.CONSTRAINT_DATE_AFTER) assert constraint.apply({'currentTime': datetime(2022, 1, 23)}) - assert constraint.apply({'currentTime': datetime(2022, 1, 22)}) + assert not constraint.apply({'currentTime': datetime(2022, 1, 22)}) assert not constraint.apply({'currentTime': datetime(2022, 1, 21)}) @@ -154,7 +162,7 @@ def test_constraints_DATE_BEFORE(): constraint = Constraint(constraint_dict=mock_constraints.CONSTRAINT_DATE_BEFORE) assert not constraint.apply({'currentTime': datetime(2022, 1, 23)}) - assert constraint.apply({'currentTime': datetime(2022, 1, 22)}) + assert not constraint.apply({'currentTime': datetime(2022, 1, 22)}) assert constraint.apply({'currentTime': datetime(2022, 1, 21)}) diff --git a/tests/utilities/mocks/mock_constraints.py b/tests/utilities/mocks/mock_constraints.py index 41d4d7ac..6437a23a 100644 --- a/tests/utilities/mocks/mock_constraints.py +++ b/tests/utilities/mocks/mock_constraints.py @@ -92,7 +92,7 @@ { "contextName": "customField", "operator": "NUM_EQ", - "value": 5, + "value": "5", "inverted": False } @@ -101,7 +101,7 @@ { "contextName": "customField", "operator": "NUM_GT", - "value": 5, + "value": "5", "inverted": False } @@ -119,7 +119,7 @@ { "contextName": "customField", "operator": "NUM_LT", - "value": 5, + "value": "5", "inverted": False } @@ -128,7 +128,16 @@ { "contextName": "customField", "operator": "NUM_LTE", - "value": 5, + "value": "5", + "inverted": False + } + + +CONSTRAINT_NUM_FLOAT = \ + { + "contextName": "customField", + "operator": "NUM_LTE", + "value": "5.1", "inverted": False }