From 78d7c42eb836cccac1ee28718799d56db13ed454 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 14:31:05 +0000 Subject: [PATCH 1/4] Initial plan From 15586d35daebc4adb32c2097940722df298684ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 15:39:56 +0000 Subject: [PATCH 2/4] Harden OAuthRefreshException with robust parsing and clearer str output - Add class docstring and message attribute - Expand constructor to accept error, error_description, and cause - Add _load_data() helper that safely parses response JSON (handles missing response, missing .json(), invalid JSON, non-dict payloads) - Populate _error and _error_description from explicit args first, falling back to parsed response data - Implement __str__ with full, error-only, and fallback formatting --- trakt/errors.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/trakt/errors.py b/trakt/errors.py index 190271c..ee652b3 100644 --- a/trakt/errors.py +++ b/trakt/errors.py @@ -69,17 +69,42 @@ class OAuthException(TraktException): class OAuthRefreshException(OAuthException): - def __init__(self, response=None): + """Raised when an OAuth access token could not be refreshed.""" + + message = 'Unauthorized - OAuth token refresh failed' + + def __init__(self, response=None, error=None, error_description=None, cause=None): super().__init__(response) - self.data = self.response.json() + self.cause = cause + data = self._load_data() + self._error = error if error is not None else data.get("error") + self._error_description = error_description if error_description is not None else data.get("error_description") + + def _load_data(self): + if self.response is None: + return {} + try: + data = self.response.json() + except Exception: + return {} + if not isinstance(data, dict): + return {} + return data @property def error(self): - return self.data["error"] + return self._error @property def error_description(self): - return self.data["error_description"] + return self._error_description + + def __str__(self): + if self._error and self._error_description: + return f'{self.message}: {self._error} - {self._error_description}' + if self._error: + return f'{self.message}: {self._error}' + return self.message class ForbiddenException(TraktException): From b59b7f97ef2b18a4c7d5ee70d5d3d793b5b599cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 15:40:40 +0000 Subject: [PATCH 3/4] Add unit tests for OAuthRefreshException Cover: - default str form when no response/error details are present - parsing error and error_description from response JSON payload - explicit constructor args overriding parsed response data - graceful behavior when response is None or .json() raises - str formatting for error-only and error+description cases - cause attribute preservation --- tests/test_errors.py | 53 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/tests/test_errors.py b/tests/test_errors.py index b491825..efa6789 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- """unit tests to define behavior of custom exception types""" +from unittest.mock import MagicMock + from trakt.errors import (BadRequestException, ConflictException, ForbiddenException, NotFoundException, - OAuthException, ProcessException, RateLimitException, + OAuthException, OAuthRefreshException, + ProcessException, RateLimitException, TraktException, TraktInternalException, TraktUnavailable) @@ -74,3 +77,51 @@ def test_503_exception(): assert texc.http_code == 503 assert texc.message == 'Trakt Unavailable - server overloaded' assert str(texc) == texc.message + + +def test_oauth_refresh_exception_default_str(): + texc = OAuthRefreshException() + assert str(texc) == 'Unauthorized - OAuth token refresh failed' + + +def test_oauth_refresh_exception_from_response_json(): + response = MagicMock() + response.json.return_value = {'error': 'invalid_grant', 'error_description': 'Token has expired'} + texc = OAuthRefreshException(response=response) + assert texc.error == 'invalid_grant' + assert texc.error_description == 'Token has expired' + assert str(texc) == 'Unauthorized - OAuth token refresh failed: invalid_grant - Token has expired' + + +def test_oauth_refresh_exception_explicit_args_override_response(): + response = MagicMock() + response.json.return_value = {'error': 'from_response', 'error_description': 'from_response_desc'} + texc = OAuthRefreshException(response=response, error='explicit_error', error_description='explicit_desc') + assert texc.error == 'explicit_error' + assert texc.error_description == 'explicit_desc' + + +def test_oauth_refresh_exception_no_response(): + texc = OAuthRefreshException(response=None) + assert texc.error is None + assert texc.error_description is None + assert str(texc) == 'Unauthorized - OAuth token refresh failed' + + +def test_oauth_refresh_exception_json_raises(): + response = MagicMock() + response.json.side_effect = ValueError('not json') + texc = OAuthRefreshException(response=response) + assert texc.error is None + assert texc.error_description is None + + +def test_oauth_refresh_exception_error_only_str(): + texc = OAuthRefreshException(error='invalid_client') + assert str(texc) == 'Unauthorized - OAuth token refresh failed: invalid_client' + + +def test_oauth_refresh_exception_cause(): + cause = RuntimeError('original') + texc = OAuthRefreshException(cause=cause) + assert texc.cause is cause From c711ec33c55c6d8af8f493f47cec07b002066662 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 15:41:45 +0000 Subject: [PATCH 4/4] Narrow exception catch in _load_data to specific exceptions --- trakt/errors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/trakt/errors.py b/trakt/errors.py index ee652b3..956ce9f 100644 --- a/trakt/errors.py +++ b/trakt/errors.py @@ -81,11 +81,12 @@ def __init__(self, response=None, error=None, error_description=None, cause=None self._error_description = error_description if error_description is not None else data.get("error_description") def _load_data(self): + from json import JSONDecodeError if self.response is None: return {} try: data = self.response.json() - except Exception: + except (ValueError, JSONDecodeError, AttributeError): return {} if not isinstance(data, dict): return {}