Repository: fronzbot/blinkpy

Issue:
Don't refresh tokens on failed request
**Describe the bug**
Old versions of the library required auth tokens to be refreshed every 24hrs.  Logic was added to assume a failed request meant the token expired, so would log in again.  The move to 2FA made this an obsolete method and, in fact, may be causing many issues for users.

**Expected behavior**
A failed request should simply be marked as failed and the `available` property set to `False`.  Retry can be done at the `refresh()` method level or an  independent script can just check the property and call the `blink.auth.refresh_token()` method if needed.

**`blinkpy` version:** 0.15.0



Comments:

Patch:
diff --git a/blinkpy/auth.py b/blinkpy/auth.py
index 2f07048..b379406 100644
--- a/blinkpy/auth.py
+++ b/blinkpy/auth.py
@@ -5,7 +5,6 @@ from requests import Request, Session, exceptions
 from blinkpy import api
 from blinkpy.helpers import util
 from blinkpy.helpers.constants import BLINK_URL, LOGIN_ENDPOINT
-from blinkpy.helpers import errors as ERROR
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -95,7 +94,10 @@ class Auth:
             self.token = self.login_response["authtoken"]["authtoken"]
             self.client_id = self.login_response["client"]["id"]
             self.account_id = self.login_response["account"]["id"]
-        except KeyError:
+        except LoginError:
+            _LOGGER.error("Login endpoint failed. Try again later.")
+            raise TokenRefreshFailed
+        except (TypeError, KeyError):
             _LOGGER.error("Malformed login response: %s", self.login_response)
             raise TokenRefreshFailed
         return True
@@ -110,11 +112,12 @@ class Auth:
         """Check for valid response."""
         if not json_resp:
             return response
-
         try:
-            json_data = response.json()
-            if json_data["code"] in ERROR.BLINK_ERRORS:
+            if response.status_code in [101, 401]:
+                raise UnauthorizedError
+            if response.status_code == 404:
                 raise exceptions.ConnectionError
+            json_data = response.json()
         except KeyError:
             pass
         except (AttributeError, ValueError):
@@ -141,31 +144,42 @@ class Auth:
         :param reqtype: Can be 'get' or 'post' (default: 'get')
         :param stream: Stream response? True/FALSE
         :param json_resp: Return JSON response? TRUE/False
-        :param is_retry: Is this a retry attempt? True/FALSE
+        :param is_retry: Is this part of a re-auth attempt? True/FALSE
         """
         req = self.prepare_request(url, headers, data, reqtype)
         try:
             response = self.session.send(req, stream=stream)
             return self.validate_response(response, json_resp)
-
-        except (exceptions.ConnectionError, exceptions.Timeout, TokenRefreshFailed):
+        except (exceptions.ConnectionError, exceptions.Timeout):
+            _LOGGER.error(
+                "Connection error. Endpoint %s possibly down or throttled. %s: %s",
+                url,
+                response.status_code,
+                response.reason,
+            )
+        except BlinkBadResponse:
+            _LOGGER.error(
+                "Expected json response from %s, but received: %s: %s",
+                url,
+                response.status_code,
+                response.reason,
+            )
+        except UnauthorizedError:
             try:
                 if not is_retry:
                     self.refresh_token()
                     return self.query(
                         url=url,
                         data=data,
-                        headers=headers,
+                        headers=self.header,
                         reqtype=reqtype,
                         stream=stream,
                         json_resp=json_resp,
                         is_retry=True,
                     )
-            except (TokenRefreshFailed, LoginError):
-                _LOGGER.error("Endpoint %s failed. Unable to refresh login tokens", url)
-        except BlinkBadResponse:
-            _LOGGER.error("Expected json response, but received: %s", response)
-        _LOGGER.error("Endpoint %s failed", url)
+                _LOGGER.error("Unable to access %s after token refresh.", url)
+            except TokenRefreshFailed:
+                _LOGGER.error("Unable to refresh token.")
         return None
 
     def send_auth_key(self, blink, key):
@@ -200,3 +214,7 @@ class LoginError(Exception):
 
 class BlinkBadResponse(Exception):
     """Class to throw bad json response exception."""
+
+
+class UnauthorizedError(Exception):
+    """Class to throw an unauthorized access error."""
diff --git a/blinkpy/blinkpy.py b/blinkpy/blinkpy.py
index 671b380..1d6d0f5 100644
--- a/blinkpy/blinkpy.py
+++ b/blinkpy/blinkpy.py
@@ -78,7 +78,6 @@ class Blink:
         """
         if self.check_if_ok_to_update() or force:
             if not self.available:
-                self.auth.refresh_token()
                 self.setup_post_verify()
 
             for sync_name, sync_module in self.sync.items():
diff --git a/blinkpy/helpers/errors.py b/blinkpy/helpers/errors.py
index 3f39086..e701f2c 100644
--- a/blinkpy/helpers/errors.py
+++ b/blinkpy/helpers/errors.py
@@ -12,4 +12,4 @@ AUTH_TOKEN = (
 )
 REQUEST = (4, "Cannot perform request (get/post type incorrect)")
 
-BLINK_ERRORS = [101, 400, 404]
+BLINK_ERRORS = [400, 404]


Test patch:
diff --git a/tests/mock_responses.py b/tests/mock_responses.py
index 3981926..a789e80 100644
--- a/tests/mock_responses.py
+++ b/tests/mock_responses.py
@@ -9,6 +9,7 @@ class MockResponse:
         self.json_data = json_data
         self.status_code = status_code
         self.raw_data = raw_data
+        self.reason = "foobar"
 
     def json(self):
         """Return json data from get_request."""
diff --git a/tests/test_auth.py b/tests/test_auth.py
index 8086267..8ee18ce 100644
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -3,7 +3,13 @@
 import unittest
 from unittest import mock
 from requests import exceptions
-from blinkpy.auth import Auth, LoginError, TokenRefreshFailed, BlinkBadResponse
+from blinkpy.auth import (
+    Auth,
+    LoginError,
+    TokenRefreshFailed,
+    BlinkBadResponse,
+    UnauthorizedError,
+)
 import blinkpy.helpers.constants as const
 import tests.mock_responses as mresp
 
@@ -90,6 +96,10 @@ class TestAuth(unittest.TestCase):
         with self.assertRaises(exceptions.ConnectionError):
             self.auth.validate_response(fake_resp, True)
 
+        fake_resp = mresp.MockResponse({"code": 101}, 401)
+        with self.assertRaises(UnauthorizedError):
+            self.auth.validate_response(fake_resp, True)
+
     def test_good_response_code(self):
         """Check good response code from server."""
         fake_resp = mresp.MockResponse({"foo": "bar"}, 200)
@@ -133,6 +143,8 @@ class TestAuth(unittest.TestCase):
         mock_req.return_value = fake_resp
         with self.assertRaises(LoginError):
             self.auth.login()
+        with self.assertRaises(TokenRefreshFailed):
+            self.auth.refresh_token()
 
     @mock.patch("blinkpy.auth.Auth.login")
     def test_refresh_token(self, mock_login):
@@ -197,10 +209,22 @@ class TestAuth(unittest.TestCase):
     def test_query_retry(self, mock_refresh, mock_validate):
         """Check handling of request retry."""
         self.auth.session = MockSession()
-        mock_validate.side_effect = [TokenRefreshFailed, "foobar"]
+        mock_validate.side_effect = [UnauthorizedError, "foobar"]
         mock_refresh.return_value = True
         self.assertEqual(self.auth.query(url="http://example.com"), "foobar")
 
+    @mock.patch("blinkpy.auth.Auth.validate_response")
+    @mock.patch("blinkpy.auth.Auth.refresh_token")
+    def test_query_retry_failed(self, mock_refresh, mock_validate):
+        """Check handling of failed retry request."""
+        self.auth.seession = MockSession()
+        mock_validate.side_effect = [UnauthorizedError, BlinkBadResponse]
+        mock_refresh.return_value = True
+        self.assertEqual(self.auth.query(url="http://example.com"), None)
+
+        mock_validate.side_effect = [UnauthorizedError, TokenRefreshFailed]
+        self.assertEqual(self.auth.query(url="http://example.com"), None)
+
 
 class MockSession:
     """Object to mock a session."""