diff --git a/switcher_client/client.py b/switcher_client/client.py index 3f61aac..5b8e19f 100644 --- a/switcher_client/client.py +++ b/switcher_client/client.py @@ -1,5 +1,6 @@ from typing import Optional, Callable +from switcher_client.lib.bypasser import Bypasser, Key from switcher_client.lib.globals.global_auth import GlobalAuth from switcher_client.lib.globals.global_snapshot import GlobalSnapshot, LoadSnapshotOptions from switcher_client.lib.globals.global_context import Context, ContextOptions, DEFAULT_ENVIRONMENT @@ -249,6 +250,16 @@ def subscribe_notify_error(callback: Callable[[Exception], None]) -> None: """ ExecutionLogger.subscribe_notify_error(callback) + @staticmethod + def assume(key: str) -> Key: + """ Force a switcher value to return a given value by calling one of both methods - true() false() """ + return Bypasser.assume(key) + + @staticmethod + def forget(key: str) -> None: + """ Remove forced value from a switcher """ + Bypasser.forget(key) + @staticmethod def _is_check_snapshot_available(fetch_remote = False) -> bool: return Client.snapshot_version() == 0 and (fetch_remote or not Client._context.options.local) diff --git a/switcher_client/lib/bypasser/__init__.py b/switcher_client/lib/bypasser/__init__.py new file mode 100644 index 0000000..3ce8829 --- /dev/null +++ b/switcher_client/lib/bypasser/__init__.py @@ -0,0 +1,7 @@ +from switcher_client.lib.bypasser.bypasser import Bypasser +from switcher_client.lib.bypasser.key import Key + +__all__ = [ + 'Bypasser', + 'Key', +] diff --git a/switcher_client/lib/bypasser/bypasser.py b/switcher_client/lib/bypasser/bypasser.py new file mode 100644 index 0000000..2022e54 --- /dev/null +++ b/switcher_client/lib/bypasser/bypasser.py @@ -0,0 +1,33 @@ +from switcher_client.lib.bypasser.key import Key + +class Bypasser: + """ + Bypasser allows to force a switcher value to return a given value by calling one of both methods - true() false() + """ + + _bypassed_keys = [] + + @staticmethod + def assume(key: str) -> Key: + # Remove previous forced value if exists to avoid conflicts + Bypasser.forget(key) + + new_key = Key(key) + Bypasser._bypassed_keys.append(new_key) + return new_key + + @staticmethod + def forget(key: str) -> None: + """ Remove forced value from a switcher """ + key_stored = Bypasser.search_key(key) + if key_stored is not None: + Bypasser._bypassed_keys.remove(key_stored) + + @staticmethod + def search_key(key: str) -> Key | None: + """ Search for key registered via 'assume' """ + for bypassed_key in Bypasser._bypassed_keys: + if bypassed_key.key == key: + return bypassed_key + + return None diff --git a/switcher_client/lib/bypasser/key.py b/switcher_client/lib/bypasser/key.py new file mode 100644 index 0000000..62663af --- /dev/null +++ b/switcher_client/lib/bypasser/key.py @@ -0,0 +1,20 @@ +from switcher_client.lib.types import ResultDetail + +class Key: + """ Key record used to store key response when bypassing criteria execution """ + + def __init__(self, key: str): + self._key = key + self._result = None + + def true(self): + """ Force a switcher value to return true """ + self._result = True + + def get_response(self, input_list: list[str]) -> ResultDetail: + return ResultDetail.create(result=True, reason=f"Forced to '{self._result}' - input: {input_list}") + + @property + def key(self): + """ Get the key of the switcher """ + return self._key diff --git a/switcher_client/switcher.py b/switcher_client/switcher.py index e2a9fb2..34436a6 100644 --- a/switcher_client/switcher.py +++ b/switcher_client/switcher.py @@ -2,6 +2,7 @@ from datetime import datetime from typing import Optional +from switcher_client.lib.bypasser import Bypasser from switcher_client.lib.globals.global_context import Context from switcher_client.lib.globals.global_snapshot import GlobalSnapshot from switcher_client.lib.remote_auth import RemoteAuth @@ -51,6 +52,11 @@ def is_on(self, key: Optional[str] = None) -> bool: self._show_details = False self._validate_args(key, details=False) + # 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).result + # 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 new file mode 100644 index 0000000..99f1039 --- /dev/null +++ b/tests/test_switcher_stub.py @@ -0,0 +1,28 @@ +from tests.test_switcher_integration import given_context + +from switcher_client.client import Client, ContextOptions + +context_options_local = ContextOptions(snapshot_location='tests/snapshots', local=True, logger=True) + +class TestSwitcherStub: + """ Test suite for SwitcherStub (Bypasser) - Client.assume() """ + + key = 'FF2FOR2020' + + def setup_method(self): + given_context(options=context_options_local) + Client.load_snapshot() + + def teardown_method(self): + Client.forget(self.key) + + def test_switcher_stub_result(self): + """ Should bypass Switcher evaluation and return the stubbed result """ + + # given + switcher = Client.get_switcher(self.key) + + # test + Client.assume(self.key).true() + + assert switcher.is_on() \ No newline at end of file