From faf3f5a6ad3924beccafae9259d1807a34004477 Mon Sep 17 00:00:00 2001 From: Adrian Czerwiec Date: Fri, 22 May 2026 15:52:15 +0200 Subject: [PATCH 1/8] add creds valitdation --- fishjam/__init__.py | 3 ++ fishjam/_ws_notifier.py | 3 +- fishjam/api/_client.py | 3 +- fishjam/api/_fishjam_client.py | 44 +++++++++++++++++++ fishjam/errors.py | 10 +++++ fishjam/utils.py | 9 ++++ tests/test_config_validation.py | 75 +++++++++++++++++++++++++++++++++ 7 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 tests/test_config_validation.py diff --git a/fishjam/__init__.py b/fishjam/__init__.py index 15195a4..d7be1f0 100644 --- a/fishjam/__init__.py +++ b/fishjam/__init__.py @@ -24,6 +24,7 @@ Room, RoomOptions, ) +from fishjam.errors import MissingFishjamIdError, MissingManagementTokenError __version__ = version.__version__ @@ -39,6 +40,8 @@ "AgentOutputOptions", "Room", "Peer", + "MissingFishjamIdError", + "MissingManagementTokenError", "events", "errors", "room", diff --git a/fishjam/_ws_notifier.py b/fishjam/_ws_notifier.py index bfbd2da..bc2fd7c 100644 --- a/fishjam/_ws_notifier.py +++ b/fishjam/_ws_notifier.py @@ -21,7 +21,7 @@ ALLOWED_NOTIFICATIONS, AllowedNotification, ) -from fishjam.utils import get_fishjam_url +from fishjam.utils import get_fishjam_url, validate_fishjam_config NotificationHandler = ( Callable[[AllowedNotification], None] @@ -38,6 +38,7 @@ def __init__( management_token: str, ): """Create a FishjamNotifier instance with an ID and management token.""" + validate_fishjam_config(fishjam_id, management_token) websocket_url = get_fishjam_url(fishjam_id).replace("http", "ws") self._fishjam_url = f"{websocket_url}/socket/server/websocket" self._management_token: str = management_token diff --git a/fishjam/api/_client.py b/fishjam/api/_client.py index 4a30e7a..8c1a5a2 100644 --- a/fishjam/api/_client.py +++ b/fishjam/api/_client.py @@ -6,12 +6,13 @@ from fishjam._openapi_client.models import Error from fishjam._openapi_client.types import Response from fishjam.errors import HTTPError -from fishjam.utils import get_fishjam_url +from fishjam.utils import get_fishjam_url, validate_fishjam_config from fishjam.version import get_version class Client: def __init__(self, fishjam_id: str, management_token: str): + validate_fishjam_config(fishjam_id, management_token) self._fishjam_url = get_fishjam_url(fishjam_id) self.client = AuthenticatedClient( self._fishjam_url, diff --git a/fishjam/api/_fishjam_client.py b/fishjam/api/_fishjam_client.py index af0a0bf..1592afb 100644 --- a/fishjam/api/_fishjam_client.py +++ b/fishjam/api/_fishjam_client.py @@ -154,12 +154,56 @@ def __init__( ): """Create a FishjamClient instance. + Performs only required-field shape validation on the provided + credentials. The constructor does NOT contact the Fishjam backend. + Use :meth:`create_and_verify` or :meth:`check_credentials` to verify + the credentials against the backend. + Args: fishjam_id: The unique identifier for the Fishjam instance. management_token: The token used for authenticating management operations. + + Raises: + MissingFishjamIdError: If ``fishjam_id`` is empty. + MissingManagementTokenError: If ``management_token`` is empty. """ super().__init__(fishjam_id=fishjam_id, management_token=management_token) + @classmethod + def create_and_verify( + cls, *, fishjam_id: str, management_token: str + ) -> "FishjamClient": + """Construct a FishjamClient and verify credentials against the backend. + + Args: + fishjam_id: The unique identifier for the Fishjam instance. + management_token: The token used for authenticating management operations. + + Returns: + FishjamClient: A client whose credentials have been verified. + + Raises: + MissingFishjamIdError: If ``fishjam_id`` is empty. + MissingManagementTokenError: If ``management_token`` is empty. + UnauthorizedError: If the credentials are rejected by the backend. + NotFoundError: If ``fishjam_id`` does not refer to a known Fishjam. + """ + client = cls(fishjam_id=fishjam_id, management_token=management_token) + client.check_credentials() + return client + + def check_credentials(self) -> None: + """Verify configured credentials by pinging the backend. + + Performs a single lightweight call (``get_all_rooms``) and lets the + normal error translation surface any HTTP errors. + + Raises: + UnauthorizedError: On 401. + NotFoundError: On 404. + """ + self.get_all_rooms() + def create_peer( self, room_id: str, diff --git a/fishjam/errors.py b/fishjam/errors.py index 32d5dee..7ecc349 100644 --- a/fishjam/errors.py +++ b/fishjam/errors.py @@ -4,6 +4,16 @@ from fishjam._openapi_client.types import Response +class MissingFishjamIdError(ValueError): + def __init__(self) -> None: + super().__init__("Fishjam ID is required") + + +class MissingManagementTokenError(ValueError): + def __init__(self) -> None: + super().__init__("Management Token is required") + + class HTTPError(Exception): """""" diff --git a/fishjam/utils.py b/fishjam/utils.py index cf470b4..509ed2b 100644 --- a/fishjam/utils.py +++ b/fishjam/utils.py @@ -1,5 +1,14 @@ from urllib.parse import urlparse +from fishjam.errors import MissingFishjamIdError, MissingManagementTokenError + + +def validate_fishjam_config(fishjam_id: str, management_token: str) -> None: + if not fishjam_id: + raise MissingFishjamIdError() + if not management_token: + raise MissingManagementTokenError() + def validate_url(url: str) -> bool: try: diff --git a/tests/test_config_validation.py b/tests/test_config_validation.py new file mode 100644 index 0000000..4998d56 --- /dev/null +++ b/tests/test_config_validation.py @@ -0,0 +1,75 @@ +# pylint: disable=missing-class-docstring, missing-function-docstring, missing-module-docstring + +from unittest.mock import patch + +import pytest + +from fishjam import FishjamClient +from fishjam.errors import ( + MissingFishjamIdError, + MissingManagementTokenError, + UnauthorizedError, +) + +VALID_FISHJAM_ID = "fjm_test" +VALID_MANAGEMENT_TOKEN = "tok_test" + + +class TestSyncValidation: + def test_empty_fishjam_id_raises(self): + with pytest.raises(MissingFishjamIdError): + FishjamClient(fishjam_id="", management_token=VALID_MANAGEMENT_TOKEN) + + def test_empty_management_token_raises(self): + with pytest.raises(MissingManagementTokenError): + FishjamClient(fishjam_id=VALID_FISHJAM_ID, management_token="") + + def test_both_provided_does_not_raise(self): + FishjamClient( + fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN + ) + + +class TestLiveCheck: + def test_create_and_verify_raises_unauthorized_on_401(self): + with patch.object( + FishjamClient, + "get_all_rooms", + side_effect=UnauthorizedError("Invalid token"), + ): + with pytest.raises(UnauthorizedError): + FishjamClient.create_and_verify( + fishjam_id=VALID_FISHJAM_ID, + management_token=VALID_MANAGEMENT_TOKEN, + ) + + def test_create_and_verify_returns_client_and_pings_once(self): + with patch.object( + FishjamClient, "get_all_rooms", return_value=[] + ) as mock_get_all: + client = FishjamClient.create_and_verify( + fishjam_id=VALID_FISHJAM_ID, + management_token=VALID_MANAGEMENT_TOKEN, + ) + + assert isinstance(client, FishjamClient) + assert mock_get_all.call_count == 1 + + def test_check_credentials_raises_unauthorized_on_401(self): + client = FishjamClient( + fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN + ) + with patch.object( + FishjamClient, + "get_all_rooms", + side_effect=UnauthorizedError("Invalid token"), + ): + with pytest.raises(UnauthorizedError): + client.check_credentials() + + def test_check_credentials_returns_none_on_success(self): + client = FishjamClient( + fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN + ) + with patch.object(FishjamClient, "get_all_rooms", return_value=[]): + assert client.check_credentials() is None From a22c39c212567bb318c221156f696beff7dccc77 Mon Sep 17 00:00:00 2001 From: Adrian Czerwiec Date: Fri, 22 May 2026 16:30:48 +0200 Subject: [PATCH 2/8] bump examples --- examples/multimodal/multimodal/room.py | 4 +++- examples/poet_chat/poet_chat/config.py | 4 +++- examples/room_manager/room_service.py | 2 +- .../selective_subscription/room_service.py | 2 +- examples/transcription/transcription/room.py | 4 +++- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/examples/multimodal/multimodal/room.py b/examples/multimodal/multimodal/room.py index 6049327..233fc0b 100644 --- a/examples/multimodal/multimodal/room.py +++ b/examples/multimodal/multimodal/room.py @@ -6,7 +6,9 @@ from .config import FISHJAM_ID, FISHJAM_TOKEN from .worker import BackgroundWorker -fishjam = FishjamClient(FISHJAM_ID, FISHJAM_TOKEN) +fishjam = FishjamClient.create_and_verify( + fishjam_id=FISHJAM_ID, management_token=FISHJAM_TOKEN +) class RoomService: diff --git a/examples/poet_chat/poet_chat/config.py b/examples/poet_chat/poet_chat/config.py index bb89c0a..a99e11c 100644 --- a/examples/poet_chat/poet_chat/config.py +++ b/examples/poet_chat/poet_chat/config.py @@ -25,4 +25,6 @@ with open(GREET_PATH) as prompt: OPENAI_GREET = prompt.read() -fishjam_client = FishjamClient(FISHJAM_ID, FISHJAM_TOKEN) +fishjam_client = FishjamClient.create_and_verify( + fishjam_id=FISHJAM_ID, management_token=FISHJAM_TOKEN +) diff --git a/examples/room_manager/room_service.py b/examples/room_manager/room_service.py index 92c8be5..1cb66dc 100644 --- a/examples/room_manager/room_service.py +++ b/examples/room_manager/room_service.py @@ -28,7 +28,7 @@ class PeerAccess: class RoomService: def __init__(self, args: Namespace, logger: Logger): - self.fishjam_client = FishjamClient( + self.fishjam_client = FishjamClient.create_and_verify( fishjam_id=args.fishjam_id, management_token=args.management_token, ) diff --git a/examples/selective_subscription/selective_subscription/room_service.py b/examples/selective_subscription/selective_subscription/room_service.py index bab8410..042ef2a 100644 --- a/examples/selective_subscription/selective_subscription/room_service.py +++ b/examples/selective_subscription/selective_subscription/room_service.py @@ -8,7 +8,7 @@ class RoomService: def __init__(self): - self.fishjam = FishjamClient( + self.fishjam = FishjamClient.create_and_verify( fishjam_id=FISHJAM_ID, management_token=FISHJAM_TOKEN ) self.room = self.fishjam.create_room( diff --git a/examples/transcription/transcription/room.py b/examples/transcription/transcription/room.py index d5fd571..0250fab 100644 --- a/examples/transcription/transcription/room.py +++ b/examples/transcription/transcription/room.py @@ -6,7 +6,9 @@ from .agent import TranscriptionAgent from .config import FISHJAM_ID, FISHJAM_TOKEN -fishjam = FishjamClient(FISHJAM_ID, FISHJAM_TOKEN) +fishjam = FishjamClient.create_and_verify( + fishjam_id=FISHJAM_ID, management_token=FISHJAM_TOKEN +) class RoomService: From f3134fea01140f20edaf20390d371c3c12092982 Mon Sep 17 00:00:00 2001 From: Adrian Czerwiec Date: Tue, 2 Jun 2026 11:24:23 +0200 Subject: [PATCH 3/8] simpllify validation --- fishjam/__init__.py | 4 ++-- fishjam/_ws_notifier.py | 3 +-- fishjam/api/_client.py | 3 +-- fishjam/api/_fishjam_client.py | 36 ++++++++++++++---------------- fishjam/errors.py | 11 +++++----- fishjam/utils.py | 12 ++++------ tests/test_config_validation.py | 39 +++++++++++++++++++++++++-------- 7 files changed, 61 insertions(+), 47 deletions(-) diff --git a/fishjam/__init__.py b/fishjam/__init__.py index d7be1f0..221e611 100644 --- a/fishjam/__init__.py +++ b/fishjam/__init__.py @@ -24,7 +24,7 @@ Room, RoomOptions, ) -from fishjam.errors import MissingFishjamIdError, MissingManagementTokenError +from fishjam.errors import InvalidFishjamCredentialsError, MissingFishjamIdError __version__ = version.__version__ @@ -41,7 +41,7 @@ "Room", "Peer", "MissingFishjamIdError", - "MissingManagementTokenError", + "InvalidFishjamCredentialsError", "events", "errors", "room", diff --git a/fishjam/_ws_notifier.py b/fishjam/_ws_notifier.py index bc2fd7c..bfbd2da 100644 --- a/fishjam/_ws_notifier.py +++ b/fishjam/_ws_notifier.py @@ -21,7 +21,7 @@ ALLOWED_NOTIFICATIONS, AllowedNotification, ) -from fishjam.utils import get_fishjam_url, validate_fishjam_config +from fishjam.utils import get_fishjam_url NotificationHandler = ( Callable[[AllowedNotification], None] @@ -38,7 +38,6 @@ def __init__( management_token: str, ): """Create a FishjamNotifier instance with an ID and management token.""" - validate_fishjam_config(fishjam_id, management_token) websocket_url = get_fishjam_url(fishjam_id).replace("http", "ws") self._fishjam_url = f"{websocket_url}/socket/server/websocket" self._management_token: str = management_token diff --git a/fishjam/api/_client.py b/fishjam/api/_client.py index 8c1a5a2..4a30e7a 100644 --- a/fishjam/api/_client.py +++ b/fishjam/api/_client.py @@ -6,13 +6,12 @@ from fishjam._openapi_client.models import Error from fishjam._openapi_client.types import Response from fishjam.errors import HTTPError -from fishjam.utils import get_fishjam_url, validate_fishjam_config +from fishjam.utils import get_fishjam_url from fishjam.version import get_version class Client: def __init__(self, fishjam_id: str, management_token: str): - validate_fishjam_config(fishjam_id, management_token) self._fishjam_url = get_fishjam_url(fishjam_id) self.client = AuthenticatedClient( self._fishjam_url, diff --git a/fishjam/api/_fishjam_client.py b/fishjam/api/_fishjam_client.py index 1592afb..6ac6587 100644 --- a/fishjam/api/_fishjam_client.py +++ b/fishjam/api/_fishjam_client.py @@ -50,6 +50,11 @@ from fishjam._openapi_client.types import UNSET from fishjam.agent import Agent from fishjam.api._client import Client +from fishjam.errors import ( + InvalidFishjamCredentialsError, + NotFoundError, + UnauthorizedError, +) @dataclass @@ -154,18 +159,12 @@ def __init__( ): """Create a FishjamClient instance. - Performs only required-field shape validation on the provided - credentials. The constructor does NOT contact the Fishjam backend. - Use :meth:`create_and_verify` or :meth:`check_credentials` to verify - the credentials against the backend. + Does not contact the Fishjam backend — use :meth:`create_and_verify` + or :meth:`check_credentials` to verify credentials live. Args: fishjam_id: The unique identifier for the Fishjam instance. management_token: The token used for authenticating management operations. - - Raises: - MissingFishjamIdError: If ``fishjam_id`` is empty. - MissingManagementTokenError: If ``management_token`` is empty. """ super().__init__(fishjam_id=fishjam_id, management_token=management_token) @@ -183,10 +182,8 @@ def create_and_verify( FishjamClient: A client whose credentials have been verified. Raises: - MissingFishjamIdError: If ``fishjam_id`` is empty. - MissingManagementTokenError: If ``management_token`` is empty. - UnauthorizedError: If the credentials are rejected by the backend. - NotFoundError: If ``fishjam_id`` does not refer to a known Fishjam. + InvalidFishjamCredentialsError: If the ``fishjam_id`` / + ``management_token`` pair is rejected by the backend. """ client = cls(fishjam_id=fishjam_id, management_token=management_token) client.check_credentials() @@ -195,14 +192,15 @@ def create_and_verify( def check_credentials(self) -> None: """Verify configured credentials by pinging the backend. - Performs a single lightweight call (``get_all_rooms``) and lets the - normal error translation surface any HTTP errors. - - Raises: - UnauthorizedError: On 401. - NotFoundError: On 404. + Performs a single lightweight ``get_all_rooms`` call. A 401 or 404 + from the backend is translated into + :class:`InvalidFishjamCredentialsError`; other HTTP errors propagate + as their normal mapped types. """ - self.get_all_rooms() + try: + self.get_all_rooms() + except (UnauthorizedError, NotFoundError) as exc: + raise InvalidFishjamCredentialsError(*exc.args) from exc def create_peer( self, diff --git a/fishjam/errors.py b/fishjam/errors.py index 7ecc349..620b4ac 100644 --- a/fishjam/errors.py +++ b/fishjam/errors.py @@ -9,11 +9,6 @@ def __init__(self) -> None: super().__init__("Fishjam ID is required") -class MissingManagementTokenError(ValueError): - def __init__(self) -> None: - super().__init__("Management Token is required") - - class HTTPError(Exception): """""" @@ -79,3 +74,9 @@ class ConflictError(HTTPError): def __init__(self, errors): """@private""" super().__init__(errors) + + +class InvalidFishjamCredentialsError(HTTPError): + def __init__(self, errors): + """@private""" + super().__init__(errors) diff --git a/fishjam/utils.py b/fishjam/utils.py index 509ed2b..2434966 100644 --- a/fishjam/utils.py +++ b/fishjam/utils.py @@ -1,13 +1,6 @@ from urllib.parse import urlparse -from fishjam.errors import MissingFishjamIdError, MissingManagementTokenError - - -def validate_fishjam_config(fishjam_id: str, management_token: str) -> None: - if not fishjam_id: - raise MissingFishjamIdError() - if not management_token: - raise MissingManagementTokenError() +from fishjam.errors import MissingFishjamIdError def validate_url(url: str) -> bool: @@ -19,6 +12,9 @@ def validate_url(url: str) -> bool: def get_fishjam_url(fishjam_id: str) -> str: + if not fishjam_id: + raise MissingFishjamIdError() + if not validate_url(fishjam_id): return f"https://fishjam.io/api/v1/connect/{fishjam_id}" diff --git a/tests/test_config_validation.py b/tests/test_config_validation.py index 4998d56..c861d72 100644 --- a/tests/test_config_validation.py +++ b/tests/test_config_validation.py @@ -6,8 +6,9 @@ from fishjam import FishjamClient from fishjam.errors import ( + InvalidFishjamCredentialsError, MissingFishjamIdError, - MissingManagementTokenError, + NotFoundError, UnauthorizedError, ) @@ -20,10 +21,6 @@ def test_empty_fishjam_id_raises(self): with pytest.raises(MissingFishjamIdError): FishjamClient(fishjam_id="", management_token=VALID_MANAGEMENT_TOKEN) - def test_empty_management_token_raises(self): - with pytest.raises(MissingManagementTokenError): - FishjamClient(fishjam_id=VALID_FISHJAM_ID, management_token="") - def test_both_provided_does_not_raise(self): FishjamClient( fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN @@ -31,13 +28,25 @@ def test_both_provided_does_not_raise(self): class TestLiveCheck: - def test_create_and_verify_raises_unauthorized_on_401(self): + def test_create_and_verify_raises_invalid_credentials_on_401(self): with patch.object( FishjamClient, "get_all_rooms", side_effect=UnauthorizedError("Invalid token"), ): - with pytest.raises(UnauthorizedError): + with pytest.raises(InvalidFishjamCredentialsError): + FishjamClient.create_and_verify( + fishjam_id=VALID_FISHJAM_ID, + management_token=VALID_MANAGEMENT_TOKEN, + ) + + def test_create_and_verify_raises_invalid_credentials_on_404(self): + with patch.object( + FishjamClient, + "get_all_rooms", + side_effect=NotFoundError("Fishjam not found"), + ): + with pytest.raises(InvalidFishjamCredentialsError): FishjamClient.create_and_verify( fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN, @@ -55,7 +64,7 @@ def test_create_and_verify_returns_client_and_pings_once(self): assert isinstance(client, FishjamClient) assert mock_get_all.call_count == 1 - def test_check_credentials_raises_unauthorized_on_401(self): + def test_check_credentials_raises_invalid_credentials_on_401(self): client = FishjamClient( fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN ) @@ -64,7 +73,19 @@ def test_check_credentials_raises_unauthorized_on_401(self): "get_all_rooms", side_effect=UnauthorizedError("Invalid token"), ): - with pytest.raises(UnauthorizedError): + with pytest.raises(InvalidFishjamCredentialsError): + client.check_credentials() + + def test_check_credentials_raises_invalid_credentials_on_404(self): + client = FishjamClient( + fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN + ) + with patch.object( + FishjamClient, + "get_all_rooms", + side_effect=NotFoundError("Fishjam not found"), + ): + with pytest.raises(InvalidFishjamCredentialsError): client.check_credentials() def test_check_credentials_returns_none_on_success(self): From 68a843a399690b496b850cd4d2e822c7ba83427c Mon Sep 17 00:00:00 2001 From: Adrian Czerwiec Date: Tue, 2 Jun 2026 15:51:29 +0200 Subject: [PATCH 4/8] lint --- fishjam/api/_fishjam_client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fishjam/api/_fishjam_client.py b/fishjam/api/_fishjam_client.py index 6ac6587..626fa3b 100644 --- a/fishjam/api/_fishjam_client.py +++ b/fishjam/api/_fishjam_client.py @@ -196,6 +196,10 @@ def check_credentials(self) -> None: from the backend is translated into :class:`InvalidFishjamCredentialsError`; other HTTP errors propagate as their normal mapped types. + + Raises: + InvalidFishjamCredentialsError: If the ``fishjam_id`` / + ``management_token`` pair is rejected by the backend. """ try: self.get_all_rooms() From 26e653dd9b390a5f5f1f9d0cb3d2caa750578389 Mon Sep 17 00:00:00 2001 From: Adrian Czerwiec Date: Wed, 10 Jun 2026 14:00:40 +0200 Subject: [PATCH 5/8] validate credentials using validate --- .../api/credentials/__init__.py | 1 + .../api/credentials/validate_credentials.py | 91 +++++++++++++++++++ fishjam/api/_fishjam_client.py | 8 +- 3 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 fishjam/_openapi_client/api/credentials/__init__.py create mode 100644 fishjam/_openapi_client/api/credentials/validate_credentials.py diff --git a/fishjam/_openapi_client/api/credentials/__init__.py b/fishjam/_openapi_client/api/credentials/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/fishjam/_openapi_client/api/credentials/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/fishjam/_openapi_client/api/credentials/validate_credentials.py b/fishjam/_openapi_client/api/credentials/validate_credentials.py new file mode 100644 index 0000000..dff8756 --- /dev/null +++ b/fishjam/_openapi_client/api/credentials/validate_credentials.py @@ -0,0 +1,91 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...types import Response + + +def _get_kwargs() -> dict[str, Any]: + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/validate", + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Any | None: + if response.status_code == 200: + return None + + if response.status_code == 404: + return None + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[Any]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, +) -> Response[Any]: + """Validate Fishjam Management Token + + Returns 200 if the provided Fishjam Management Token is valid, 404 otherwise. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs() + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, +) -> Response[Any]: + """Validate Fishjam Management Token + + Returns 200 if the provided Fishjam Management Token is valid, 404 otherwise. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs() + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) diff --git a/fishjam/api/_fishjam_client.py b/fishjam/api/_fishjam_client.py index 626fa3b..0151402 100644 --- a/fishjam/api/_fishjam_client.py +++ b/fishjam/api/_fishjam_client.py @@ -3,6 +3,9 @@ from dataclasses import dataclass, field from typing import Any, Literal, cast +from fishjam._openapi_client.api.credentials import ( + validate_credentials as credentials_validate_credentials, +) from fishjam._openapi_client.api.mo_q import ( create_moq_token as moq_create_token, ) @@ -53,7 +56,6 @@ from fishjam.errors import ( InvalidFishjamCredentialsError, NotFoundError, - UnauthorizedError, ) @@ -202,8 +204,8 @@ def check_credentials(self) -> None: ``management_token`` pair is rejected by the backend. """ try: - self.get_all_rooms() - except (UnauthorizedError, NotFoundError) as exc: + self._request(credentials_validate_credentials) + except NotFoundError as exc: raise InvalidFishjamCredentialsError(*exc.args) from exc def create_peer( From 4d185b2e85896537c205b6d771fa11c62562306d Mon Sep 17 00:00:00 2001 From: Adrian Czerwiec Date: Thu, 11 Jun 2026 10:19:55 +0200 Subject: [PATCH 6/8] just mock the request --- tests/test_config_validation.py | 37 ++++++--------------------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/tests/test_config_validation.py b/tests/test_config_validation.py index c861d72..fe25ea3 100644 --- a/tests/test_config_validation.py +++ b/tests/test_config_validation.py @@ -9,7 +9,6 @@ InvalidFishjamCredentialsError, MissingFishjamIdError, NotFoundError, - UnauthorizedError, ) VALID_FISHJAM_ID = "fjm_test" @@ -28,22 +27,10 @@ def test_both_provided_does_not_raise(self): class TestLiveCheck: - def test_create_and_verify_raises_invalid_credentials_on_401(self): - with patch.object( - FishjamClient, - "get_all_rooms", - side_effect=UnauthorizedError("Invalid token"), - ): - with pytest.raises(InvalidFishjamCredentialsError): - FishjamClient.create_and_verify( - fishjam_id=VALID_FISHJAM_ID, - management_token=VALID_MANAGEMENT_TOKEN, - ) - def test_create_and_verify_raises_invalid_credentials_on_404(self): with patch.object( FishjamClient, - "get_all_rooms", + "_request", side_effect=NotFoundError("Fishjam not found"), ): with pytest.raises(InvalidFishjamCredentialsError): @@ -54,27 +41,15 @@ def test_create_and_verify_raises_invalid_credentials_on_404(self): def test_create_and_verify_returns_client_and_pings_once(self): with patch.object( - FishjamClient, "get_all_rooms", return_value=[] - ) as mock_get_all: + FishjamClient, "_request", return_value=None + ) as mock_request: client = FishjamClient.create_and_verify( fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN, ) assert isinstance(client, FishjamClient) - assert mock_get_all.call_count == 1 - - def test_check_credentials_raises_invalid_credentials_on_401(self): - client = FishjamClient( - fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN - ) - with patch.object( - FishjamClient, - "get_all_rooms", - side_effect=UnauthorizedError("Invalid token"), - ): - with pytest.raises(InvalidFishjamCredentialsError): - client.check_credentials() + assert mock_request.call_count == 1 def test_check_credentials_raises_invalid_credentials_on_404(self): client = FishjamClient( @@ -82,7 +57,7 @@ def test_check_credentials_raises_invalid_credentials_on_404(self): ) with patch.object( FishjamClient, - "get_all_rooms", + "_request", side_effect=NotFoundError("Fishjam not found"), ): with pytest.raises(InvalidFishjamCredentialsError): @@ -92,5 +67,5 @@ def test_check_credentials_returns_none_on_success(self): client = FishjamClient( fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN ) - with patch.object(FishjamClient, "get_all_rooms", return_value=[]): + with patch.object(FishjamClient, "_request", return_value=None): assert client.check_credentials() is None From c0a452d991b6694df360def9209e29fb594656df Mon Sep 17 00:00:00 2001 From: Adrian Czerwiec Date: Thu, 11 Jun 2026 10:22:22 +0200 Subject: [PATCH 7/8] format --- tests/test_config_validation.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_config_validation.py b/tests/test_config_validation.py index fe25ea3..1256e51 100644 --- a/tests/test_config_validation.py +++ b/tests/test_config_validation.py @@ -40,9 +40,7 @@ def test_create_and_verify_raises_invalid_credentials_on_404(self): ) def test_create_and_verify_returns_client_and_pings_once(self): - with patch.object( - FishjamClient, "_request", return_value=None - ) as mock_request: + with patch.object(FishjamClient, "_request", return_value=None) as mock_request: client = FishjamClient.create_and_verify( fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN, From 2be64e04c5ffe9b9245f080af6978824a8c2d89c Mon Sep 17 00:00:00 2001 From: Adrian Czerwiec Date: Thu, 11 Jun 2026 10:43:16 +0200 Subject: [PATCH 8/8] fix tests and simplify docstrings --- fishjam/api/_fishjam_client.py | 26 ++++++++++---------------- tests/test_config_validation.py | 28 +++++++++++++--------------- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/fishjam/api/_fishjam_client.py b/fishjam/api/_fishjam_client.py index 0151402..0fe0f2b 100644 --- a/fishjam/api/_fishjam_client.py +++ b/fishjam/api/_fishjam_client.py @@ -1,6 +1,7 @@ """Fishjam client used to manage rooms.""" from dataclasses import dataclass, field +from http import HTTPStatus from typing import Any, Literal, cast from fishjam._openapi_client.api.credentials import ( @@ -55,7 +56,6 @@ from fishjam.api._client import Client from fishjam.errors import ( InvalidFishjamCredentialsError, - NotFoundError, ) @@ -174,7 +174,7 @@ def __init__( def create_and_verify( cls, *, fishjam_id: str, management_token: str ) -> "FishjamClient": - """Construct a FishjamClient and verify credentials against the backend. + """Construct a FishjamClient and verify its credentials against the backend. Args: fishjam_id: The unique identifier for the Fishjam instance. @@ -184,29 +184,23 @@ def create_and_verify( FishjamClient: A client whose credentials have been verified. Raises: - InvalidFishjamCredentialsError: If the ``fishjam_id`` / - ``management_token`` pair is rejected by the backend. + InvalidFishjamCredentialsError: If the token is rejected. """ client = cls(fishjam_id=fishjam_id, management_token=management_token) client.check_credentials() return client def check_credentials(self) -> None: - """Verify configured credentials by pinging the backend. - - Performs a single lightweight ``get_all_rooms`` call. A 401 or 404 - from the backend is translated into - :class:`InvalidFishjamCredentialsError`; other HTTP errors propagate - as their normal mapped types. + """Verify the management token via a single ``/validate`` call. Raises: - InvalidFishjamCredentialsError: If the ``fishjam_id`` / - ``management_token`` pair is rejected by the backend. + InvalidFishjamCredentialsError: If the token is rejected. """ - try: - self._request(credentials_validate_credentials) - except NotFoundError as exc: - raise InvalidFishjamCredentialsError(*exc.args) from exc + response = credentials_validate_credentials.sync_detailed(client=self.client) + self._handle_deprecation_header(response.headers) + + if response.status_code == HTTPStatus.NOT_FOUND: + raise InvalidFishjamCredentialsError("Invalid Fishjam credentials") def create_peer( self, diff --git a/tests/test_config_validation.py b/tests/test_config_validation.py index 1256e51..df9fdf8 100644 --- a/tests/test_config_validation.py +++ b/tests/test_config_validation.py @@ -1,6 +1,7 @@ # pylint: disable=missing-class-docstring, missing-function-docstring, missing-module-docstring -from unittest.mock import patch +from http import HTTPStatus +from unittest.mock import Mock, patch import pytest @@ -8,12 +9,17 @@ from fishjam.errors import ( InvalidFishjamCredentialsError, MissingFishjamIdError, - NotFoundError, ) VALID_FISHJAM_ID = "fjm_test" VALID_MANAGEMENT_TOKEN = "tok_test" +VALIDATE = "fishjam.api._fishjam_client.credentials_validate_credentials.sync_detailed" + + +def _response(status: HTTPStatus): + return Mock(status_code=status, headers={}) + class TestSyncValidation: def test_empty_fishjam_id_raises(self): @@ -28,11 +34,7 @@ def test_both_provided_does_not_raise(self): class TestLiveCheck: def test_create_and_verify_raises_invalid_credentials_on_404(self): - with patch.object( - FishjamClient, - "_request", - side_effect=NotFoundError("Fishjam not found"), - ): + with patch(VALIDATE, return_value=_response(HTTPStatus.NOT_FOUND)): with pytest.raises(InvalidFishjamCredentialsError): FishjamClient.create_and_verify( fishjam_id=VALID_FISHJAM_ID, @@ -40,24 +42,20 @@ def test_create_and_verify_raises_invalid_credentials_on_404(self): ) def test_create_and_verify_returns_client_and_pings_once(self): - with patch.object(FishjamClient, "_request", return_value=None) as mock_request: + with patch(VALIDATE, return_value=_response(HTTPStatus.OK)) as mock_validate: client = FishjamClient.create_and_verify( fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN, ) assert isinstance(client, FishjamClient) - assert mock_request.call_count == 1 + assert mock_validate.call_count == 1 def test_check_credentials_raises_invalid_credentials_on_404(self): client = FishjamClient( fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN ) - with patch.object( - FishjamClient, - "_request", - side_effect=NotFoundError("Fishjam not found"), - ): + with patch(VALIDATE, return_value=_response(HTTPStatus.NOT_FOUND)): with pytest.raises(InvalidFishjamCredentialsError): client.check_credentials() @@ -65,5 +63,5 @@ def test_check_credentials_returns_none_on_success(self): client = FishjamClient( fishjam_id=VALID_FISHJAM_ID, management_token=VALID_MANAGEMENT_TOKEN ) - with patch.object(FishjamClient, "_request", return_value=None): + with patch(VALIDATE, return_value=_response(HTTPStatus.OK)): assert client.check_credentials() is None