diff --git a/switcher_client/client.py b/switcher_client/client.py index fe217d8..9139ada 100644 --- a/switcher_client/client.py +++ b/switcher_client/client.py @@ -1,8 +1,10 @@ from typing import Optional, Callable +from .lib.globals.global_auth import GlobalAuth from .lib.globals.global_snapshot import GlobalSnapshot, LoadSnapshotOptions from .lib.globals.global_context import Context, ContextOptions, DEFAULT_ENVIRONMENT from .lib.remote_auth import RemoteAuth +from .lib.remote import Remote from .lib.snapshot_auto_updater import SnapshotAutoUpdater from .lib.snapshot_loader import load_domain, validate_snapshot, save_snapshot from .lib.utils.execution_logger import ExecutionLogger @@ -165,6 +167,11 @@ def snapshot_version() -> int: return 0 return snapshot.domain.version + + @staticmethod + def check_switchers(switcher_keys: list[str]) -> None: + """ Verifies if switchers are properly configured """ + Client._check_switchers_remote(switcher_keys) @staticmethod def get_execution(switcher: Switcher) -> ExecutionLogger: @@ -194,4 +201,12 @@ def subscribe_notify_error(callback: Callable[[Exception], None]) -> None: @staticmethod def _is_check_snapshot_available(fetch_remote = False) -> bool: - return Client.snapshot_version() == 0 and (fetch_remote or not Client._context.options.local) \ No newline at end of file + return Client.snapshot_version() == 0 and (fetch_remote or not Client._context.options.local) + + @staticmethod + def _check_switchers_remote(switcher_keys: list[str]) -> None: + RemoteAuth.auth() + Remote.check_switchers( + token=GlobalAuth.get_token(), + switcher_keys=switcher_keys, + context=Client._context) \ No newline at end of file diff --git a/switcher_client/errors/__init__.py b/switcher_client/errors/__init__.py index 2f70b77..07ca1f5 100644 --- a/switcher_client/errors/__init__.py +++ b/switcher_client/errors/__init__.py @@ -11,6 +11,10 @@ class RemoteCriteriaError(RemoteError): def __init__(self, message): super().__init__(message) +class RemoteSwitcherError(RemoteError): + def __init__(self, not_found: list): + super().__init__(f'{", ".join(not_found)} not found') + class LocalCriteriaError(Exception): def __init__(self, message): self.message = message @@ -20,5 +24,6 @@ def __init__(self, message): 'RemoteError', 'RemoteAuthError', 'RemoteCriteriaError', + 'RemoteSwitcherError', 'LocalCriteriaError', ] \ No newline at end of file diff --git a/switcher_client/lib/remote.py b/switcher_client/lib/remote.py index ed4266a..71ad8a5 100644 --- a/switcher_client/lib/remote.py +++ b/switcher_client/lib/remote.py @@ -4,7 +4,7 @@ from typing import Optional -from ..errors import RemoteAuthError, RemoteError, RemoteCriteriaError +from ..errors import RemoteAuthError, RemoteError, RemoteCriteriaError, RemoteSwitcherError from ..lib.globals.global_context import DEFAULT_ENVIRONMENT, Context from ..lib.types import ResultDetail from ..lib.utils import get, get_entry @@ -53,6 +53,18 @@ def check_criteria(token: Optional[str], context: Context, switcher: SwitcherDat raise RemoteCriteriaError(f'[check_criteria] failed with status: {response.status_code}') + @staticmethod + def check_switchers(token: Optional[str], switcher_keys: list[str], context: Context) -> None: + url = f'{context.url}/criteria/switchers_check' + response = Remote._do_post(context, url, { 'switchers': switcher_keys }, Remote._get_header(token)) + + if response.status_code != 200: + raise RemoteError(f'[check_switchers] failed with status: {response.status_code}') + + not_found = response.json().get('not_found', []) + if len(not_found) > 0: + raise RemoteSwitcherError(not_found) + @staticmethod def check_snapshot_version(token: Optional[str], context: Context, snapshot_version: int) -> bool: url = f'{context.url}/criteria/snapshot_check/{snapshot_version}' diff --git a/tests/playground/index.py b/tests/playground/index.py index 91f070b..ebe7d63 100644 --- a/tests/playground/index.py +++ b/tests/playground/index.py @@ -77,6 +77,18 @@ def auto_update_snapshot(): ) ) +def check_switchers(): + """ Use case: Check switchers """ + global LOOP + LOOP = False + + setup_context() + + try: + Client.check_switchers([SWITCHER_KEY, 'NON_EXISTENT_SWITCHER']) + except Exception as e: + print(f"❌ Configuration error: {e}") + try: # Replace with use case simple_api_call() diff --git a/tests/test_client_check_switchers.py b/tests/test_client_check_switchers.py new file mode 100644 index 0000000..4de89d3 --- /dev/null +++ b/tests/test_client_check_switchers.py @@ -0,0 +1,77 @@ +import time +from typing import Optional + +from pytest_httpx import HTTPXMock + +from switcher_client.client import Client +from switcher_client.errors import RemoteSwitcherError +from switcher_client.lib.globals.global_context import ContextOptions + + +def test_check_remote_switchers(httpx_mock): + """ Should check remote switchers with success """ + + # given + given_auth(httpx_mock) + given_check_switchers(httpx_mock) + given_context() + + # test + Client.check_switchers(['MY_SWITCHER', 'ANOTHER_SWITCHER']) + +def test_check_remote_switchers_not_found(httpx_mock): + """ Should check remote switchers and raise RemoteSwitcherError with not found switchers """ + + # given + given_auth(httpx_mock) + given_check_switchers(httpx_mock, not_found=['MY_SWITCHER']) + given_context() + + # test + try: + Client.check_switchers(['MY_SWITCHER', 'ANOTHER_SWITCHER']) + assert False, 'Expected RemoteSwitcherError to be raised' + except RemoteSwitcherError as e: + assert str(e) == 'MY_SWITCHER not found' + +def test_check_remote_switchers_api_error(httpx_mock): + """ Should check remote switchers and raise RemoteError when API returns an error """ + + # given + given_auth(httpx_mock) + given_check_switchers(httpx_mock, status=500) + given_context() + + # test + try: + Client.check_switchers(['MY_SWITCHER', 'ANOTHER_SWITCHER']) + assert False, 'Expected RemoteError to be raised' + except Exception as e: + assert str(e) == '[check_switchers] failed with status: 500' + +# Helpers + +def given_context(url='https://api.switcherapi.com', api_key='[API_KEY]', options = ContextOptions()): + Client.build_context( + url=url, + api_key=api_key, + domain='Playground', + component='switcher-playground', + options=options + ) + +def given_auth(httpx_mock: HTTPXMock, status=200, token: Optional[str]='[token]', exp=int(round(time.time() * 1000))): + httpx_mock.add_response( + url='https://api.switcherapi.com/criteria/auth', + method='POST', + status_code=status, + json={'token': token, 'exp': exp} + ) + +def given_check_switchers(httpx_mock: HTTPXMock, status=200, not_found: Optional[list[str]]=None): + httpx_mock.add_response( + url='https://api.switcherapi.com/criteria/switchers_check', + method='POST', + status_code=status, + json={'not_found': not_found or []} + ) \ No newline at end of file