diff --git a/fenn/remote/client.py b/fenn/remote/client.py index 99bbda0..6102399 100644 --- a/fenn/remote/client.py +++ b/fenn/remote/client.py @@ -121,10 +121,10 @@ def _request(self, method: str, path: str, **kwargs): kwargs.setdefault("timeout", (_CONNECT_TIMEOUT, _REQUEST_TIMEOUT)) try: response = self._session.request(method, self._url(path), **kwargs) - except requests.exceptions.SSLError: - raise - except requests.exceptions.ConnectionError: - raise + except requests.exceptions.SSLError as exc: + raise NetworkError(f"TLS error connecting to {self._host}: {exc}") from exc + except requests.exceptions.ConnectionError as exc: + raise NetworkError(f"Could not connect to {self._host}: {exc}") from exc except requests.exceptions.Timeout as exc: raise NetworkError(f"Request to {self._host} timed out: {exc}") from exc self._raise_for_status(response) diff --git a/tests/unit/remote/__init__.py b/tests/unit/remote/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/remote/test_client.py b/tests/unit/remote/test_client.py new file mode 100644 index 0000000..7429d3b --- /dev/null +++ b/tests/unit/remote/test_client.py @@ -0,0 +1,54 @@ +"""Tests for `fenn.remote.client.RemoteClient._request` error handling.""" + +from unittest.mock import Mock + +import pytest +import requests + +from fenn.exceptions import AuthError, InsufficientCreditsError, NetworkError +from fenn.remote.client import RemoteClient + + +def _client(response_or_exc) -> RemoteClient: + client = RemoteClient(host="https://example.test", api_key="fk_test") + if isinstance(response_or_exc, BaseException): + client._session.request = Mock(side_effect=response_or_exc) + else: + client._session.request = Mock(return_value=response_or_exc) + return client + + +@pytest.mark.parametrize( + "exc", + [ + requests.exceptions.ConnectionError("connection refused"), + requests.exceptions.SSLError("certificate verify failed"), + requests.exceptions.ConnectTimeout("connect timed out"), + requests.exceptions.ReadTimeout("read timed out"), + ], + ids=["connection", "ssl", "connect-timeout", "read-timeout"], +) +def test_request_wraps_transport_errors_as_network_error(exc): + client = _client(exc) + + with pytest.raises(NetworkError): + client.me() + + +def test_request_returns_response_on_success(): + ok = Mock(status_code=200) + ok.json.return_value = {"credits": 10} + + assert _client(ok).me() == {"credits": 10} + + +def test_request_maps_http_status_to_typed_errors(): + unauthorized = Mock(status_code=401) + unauthorized.json.return_value = {"detail": "revoked"} + with pytest.raises(AuthError): + _client(unauthorized).me() + + broke = Mock(status_code=402) + broke.json.return_value = {"detail": "no credits"} + with pytest.raises(InsufficientCreditsError): + _client(broke).me()