diff --git a/README.md b/README.md index 5658ebd..063b1c6 100644 --- a/README.md +++ b/README.md @@ -310,20 +310,30 @@ Client.watch_snapshot(WatchSnapshotCallback( ## Testing & Development -### Built-in Mocking (Coming Soon) - -> 🚧 **Note**: The mocking features are currently under development +### Built-in Mocking The SDK will include powerful mocking capabilities for testing: ```python -# 🚧 TODO: Mock feature states for testing +# Mock feature states for testing Client.assume('FEATURE01').true() assert switcher.is_on('FEATURE01') == True -Client.forget('FEATURE01') # Reset to normal behavior +# Conditional mocking based on input criteria +Client.assume('FEATURE01').true() \ + # value can be either 'guest' or 'admin' + .when(StrategiesType.VALUE.value, ['guest', 'admin']) \ + .when(StrategiesType.NETWORK.value, '10.0.0.3') + +assert switcher \ + .check_value('guest') \ + .check_network('10.0.0.3') \ + .is_on('FEATURE01') == True + +# Reset to normal behavior +Client.forget('FEATURE01') -# 🚧 TODO: Mock with metadata +# Mock with metadata Client.assume('FEATURE01').false().with_metadata({ 'message': 'Feature is disabled' }) diff --git a/switcher_client/__init__.py b/switcher_client/__init__.py index e41eab8..ef1067e 100644 --- a/switcher_client/__init__.py +++ b/switcher_client/__init__.py @@ -2,10 +2,12 @@ from switcher_client.switcher import Switcher from switcher_client.lib.globals.global_context import ContextOptions from switcher_client.lib.snapshot_watcher import WatchSnapshotCallback +from switcher_client.lib.snapshot import StrategiesType __all__ = [ 'Client', 'Switcher', 'ContextOptions', 'WatchSnapshotCallback', + 'StrategiesType' ] diff --git a/switcher_client/lib/bypasser/key.py b/switcher_client/lib/bypasser/key.py index 62663af..d80f130 100644 --- a/switcher_client/lib/bypasser/key.py +++ b/switcher_client/lib/bypasser/key.py @@ -1,3 +1,5 @@ +from switcher_client.lib.compat import Self +from switcher_client.lib.snapshot import StrategiesType from switcher_client.lib.types import ResultDetail class Key: @@ -5,16 +7,53 @@ class Key: def __init__(self, key: str): self._key = key - self._result = None + self._result = False + self._reason = None + self._metadata: dict = {} + self._when: dict[str, list[str]] = {} - def true(self): + def true(self) -> Self: """ Force a switcher value to return true """ self._result = True + self._reason = "Forced to True" + return self - def get_response(self, input_list: list[str]) -> ResultDetail: - return ResultDetail.create(result=True, reason=f"Forced to '{self._result}' - input: {input_list}") + def false(self) -> Self: + """ Force a switcher value to return false """ + self._result = False + self._reason = "Forced to False" + return self + + def with_metadata(self, metadata: dict) -> Self: + """ Define metadata for the response """ + self._metadata = metadata + return self + + def when(self, strategy: str, input_strategy: str | list[str]) -> Self: + """ Conditionally set result based on strategy """ + if any(s.value == strategy for s in StrategiesType): + self._when[strategy] = input_strategy if isinstance(input_strategy, list) else [input_strategy] + return self + + def get_response(self, input_list: list[str] | None) -> ResultDetail: + """ Return key response """ + result = self._result + if self._when and input_list is not None: + result = self._get_result_based_on_when(input_list) + + return ResultDetail.create(result=result, reason=self._reason, metadata=self._metadata) @property def key(self): - """ Get the key of the switcher """ + """ Return selected switcher name """ return self._key + + def _get_result_based_on_when(self, input_list: list[str]) -> bool: + """ Evaluate the when conditions to determine the result """ + for strategy_when, input_when in self._when.items(): + entry = [e for e in input_list if e[0] == strategy_when] + if entry and entry[0][1] not in input_when: + self._reason = f"Forced to {not self._result} when: [{', '.join(input_when)}] - input: {entry[0][1]}" + return not self._result + + return self._result diff --git a/switcher_client/switcher.py b/switcher_client/switcher.py index 34436a6..fe195fd 100644 --- a/switcher_client/switcher.py +++ b/switcher_client/switcher.py @@ -68,6 +68,11 @@ def is_on_with_details(self, key: Optional[str] = None) -> ResultDetail: """ Execute criteria with details """ self._validate_args(key, details=True) + # verify if query from Bypasser + bypass_key = Bypasser.search_key(self._key) + if bypass_key is not None: + return bypass_key.get_response(self._input) + # try get cached result cached_result = self._try_cached_result() if cached_result is not None: diff --git a/tests/test_switcher_stub.py b/tests/test_switcher_stub.py index 99f1039..ddc3686 100644 --- a/tests/test_switcher_stub.py +++ b/tests/test_switcher_stub.py @@ -1,5 +1,6 @@ from tests.test_switcher_integration import given_context +from switcher_client.lib.snapshot import StrategiesType from switcher_client.client import Client, ContextOptions context_options_local = ContextOptions(snapshot_location='tests/snapshots', local=True, logger=True) @@ -24,5 +25,83 @@ def test_switcher_stub_result(self): # test Client.assume(self.key).true() + assert switcher.is_on() - assert switcher.is_on() \ No newline at end of file + Client.assume(self.key).false() + assert not switcher.is_on() + + def test_switcher_stub_result_details(self): + """ Should bypass Switcher evaluation and return the stubbed result with details """ + + # given + switcher = Client.get_switcher(self.key) + + # test + Client.assume(self.key).true() + result_detail = switcher.is_on_with_details(self.key) + assert result_detail.result + assert result_detail.reason == "Forced to True" + + def test_switcher_stub_result_with_metadata(self): + """ Should bypass Switcher evaluation and return the stubbed result with details and metadata """ + + # given + switcher = Client.get_switcher(self.key) + + # test + Client.assume(self.key).true().with_metadata({"env": "test", "version": "1.0.0"}) + + result_detail = switcher.is_on_with_details(self.key) + assert result_detail.result + assert result_detail.reason == "Forced to True" + assert result_detail.metadata == {"env": "test", "version": "1.0.0"} + + def test_switcher_stub_with_criteria(self): + """ Should bypass Switcher evaluation based on criteria conditions """ + + # given + switcher = Client.get_switcher(self.key) + + # test + Client.assume(self.key).true() \ + .when(StrategiesType.VALUE.value, "Canada") \ + .when(StrategiesType.NETWORK.value, "10.0.0.3") + + result_detail = switcher \ + .check_value("Canada") \ + .check_network('10.0.0.3') \ + .is_on_with_details() + + assert result_detail.result + assert result_detail.reason == "Forced to True" + + def test_switcher_stub_with_unrecheable_criteria(self): + """ Should bypass Switcher evaluation based on unrecheable criteria conditions """ + + # given + switcher = Client.get_switcher(self.key) + + # test + Client.assume(self.key).true() \ + .when(StrategiesType.VALUE.value, "Canada") + + result_detail = switcher \ + .check_value("Brazil") \ + .is_on_with_details() + + assert not result_detail.result + assert result_detail.reason == "Forced to False when: [Canada] - input: Brazil" + + def test_switcher_stub_with_multiple_criteria(self): + """ Should bypass Switcher evaluation based on multiple criteria conditions """ + + # given + switcher = Client.get_switcher(self.key) + + # test + Client.assume(self.key).true() \ + .when(StrategiesType.VALUE.value, ["Canada", "Brazil"]) + + assert switcher.check_value("Canada").is_on() + assert switcher.check_value("Brazil").is_on() + assert not switcher.check_value("USA").is_on() \ No newline at end of file