Compare commits

...
13 Commits
Author SHA1 Message Date
Kevin FronczakandGitHub 42e444c0e0 Merge pull request #275 from fronzbot/add-errored-property
Add is_errored property to Auth class
2020-05-27 19:30:14 -04:00
Kevin FronczakandGitHub 2baae9ea45 Merge branch 'dev' into add-errored-property 2020-05-27 19:28:02 -04:00
Kevin FronczakandGitHub 789724a6a3 Merge pull request #276 from fronzbot/dev-bump
Bump dev version
2020-05-27 19:26:51 -04:00
Kevin Fronczak e53439141a Bump dev version 2020-05-27 23:23:50 +00:00
Kevin Fronczak d5ec39113e Add is_errored property to Auth class 2020-05-27 23:19:59 +00:00
Kevin FronczakandGitHub 58117b3daf Merge pull request #274 from fronzbot/remove-auto-refresh
Only refresh token on unauthorized response from request
2020-05-27 19:19:33 -04:00
Kevin FronczakandGitHub 558c190512 Merge branch 'dev' into remove-auto-refresh 2020-05-27 19:17:00 -04:00
Kevin Fronczak 9e124ce5cc Only refresh token on unauthorized response from request 2020-05-27 23:05:51 +00:00
Kevin FronczakandGitHub 396824d38c Merge pull request #273 from fronzbot/add-camera-arm-property
Add arm property to camera, deprecate motion enable method
2020-05-27 17:55:28 -04:00
Kevin FronczakandGitHub cc860c3d03 Merge branch 'dev' into add-camera-arm-property 2020-05-27 17:49:29 -04:00
Kevin FronczakandGitHub 6a5256e9b1 Merge pull request #272 from fronzbot/catch-none-response
Catch all None responses from http requests
2020-05-27 17:48:32 -04:00
Kevin Fronczak 00c2c2cc29 Add arm property to camera, deprecate motion enable method 2020-05-27 21:44:07 +00:00
Kevin Fronczak 4775bda51c Catch all None responses from http requests 2020-05-27 21:27:08 +00:00
11 changed files with 141 additions and 30 deletions
+38 -14
View File
@@ -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__)
@@ -33,6 +32,7 @@ class Auth:
self.client_id = login_data.get("client_id", None)
self.account_id = login_data.get("account_id", None)
self.login_response = None
self.is_errored = False
self.no_prompt = no_prompt
self.session = self.create_session()
@@ -87,6 +87,7 @@ class Auth:
def refresh_token(self):
"""Refresh auth token."""
self.is_errored = True
try:
_LOGGER.info("Token expired, attempting automatic refresh.")
self.login_response = self.login()
@@ -95,7 +96,11 @@ 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:
self.is_errored = False
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
@@ -109,17 +114,21 @@ class Auth:
def validate_response(self, response, json_resp):
"""Check for valid response."""
if not json_resp:
self.is_errored = False
return response
self.is_errored = True
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):
raise BlinkBadResponse
self.is_errored = False
return json_data
def query(
@@ -141,31 +150,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 +220,7 @@ class LoginError(Exception):
class BlinkBadResponse(Exception):
"""Class to throw bad json response exception."""
class UnauthorizedError(Exception):
"""Class to throw an unauthorized access error."""
+4 -5
View File
@@ -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():
@@ -152,7 +151,7 @@ class Blink:
{"name": camera["name"], "id": camera["id"]}
)
return all_cameras
except KeyError:
except (KeyError, TypeError):
_LOGGER.error("Unable to retrieve cameras from response %s", response)
raise BlinkSetupError
@@ -176,7 +175,7 @@ class Blink:
response = api.request_networks(self)
try:
self.networks = response["summary"]
except KeyError:
except (KeyError, TypeError):
raise BlinkSetupError
def setup_network_ids(self):
@@ -250,8 +249,8 @@ class Blink:
try:
result = response["media"]
if not result:
raise IndexError
except (KeyError, IndexError):
raise KeyError
except (KeyError, TypeError):
_LOGGER.info("No videos found on page %s. Exiting.", page)
break
+19
View File
@@ -77,12 +77,31 @@ class BlinkCamera:
return self._cached_video
return None
@property
def arm(self):
"""Return arm status of camera."""
return self.motion_enabled
@arm.setter
def arm(self, value):
"""Set camera arm status."""
if value:
return api.request_motion_detection_enable(
self.sync.blink, self.network_id, self.camera_id
)
return api.request_motion_detection_disable(
self.sync.blink, self.network_id, self.camera_id
)
def snap_picture(self):
"""Take a picture with camera to create a new thumbnail."""
return api.request_new_image(self.sync.blink, self.network_id, self.camera_id)
def set_motion_detect(self, enable):
"""Set motion detection."""
_LOGGER.warning(
"Method is deprecated as of v0.16.0 and will be removed in a future version. Please use the BlinkCamera.arm property instead."
)
if enable:
return api.request_motion_detection_enable(
self.sync.blink, self.network_id, self.camera_id
+1 -1
View File
@@ -4,7 +4,7 @@ import os
MAJOR_VERSION = 0
MINOR_VERSION = 16
PATCH_VERSION = "0-rc1"
PATCH_VERSION = "0-rc2"
__version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}"
+1 -1
View File
@@ -12,4 +12,4 @@ AUTH_TOKEN = (
)
REQUEST = (4, "Cannot perform request (get/post type incorrect)")
BLINK_ERRORS = [101, 400, 404]
BLINK_ERRORS = [400, 404]
+3 -6
View File
@@ -149,14 +149,11 @@ class BlinkSyncModule:
def get_network_info(self):
"""Retrieve network status."""
is_errored = False
self.network_info = api.request_network_status(self.blink, self.network_id)
try:
is_errored = self.network_info["network"]["sync_module_error"]
except KeyError:
is_errored = True
if is_errored:
if self.network_info["network"]["sync_module_error"]:
raise KeyError
except (TypeError, KeyError):
self.available = False
return False
return True
+1
View File
@@ -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."""
+40 -2
View File
@@ -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
@@ -86,24 +92,38 @@ class TestAuth(unittest.TestCase):
def test_bad_response_code(self):
"""Check bad response code from server."""
self.auth.is_errored = False
fake_resp = mresp.MockResponse({"code": 404}, 404)
with self.assertRaises(exceptions.ConnectionError):
self.auth.validate_response(fake_resp, True)
self.assertTrue(self.auth.is_errored)
self.auth.is_errored = False
fake_resp = mresp.MockResponse({"code": 101}, 401)
with self.assertRaises(UnauthorizedError):
self.auth.validate_response(fake_resp, True)
self.assertTrue(self.auth.is_errored)
def test_good_response_code(self):
"""Check good response code from server."""
fake_resp = mresp.MockResponse({"foo": "bar"}, 200)
self.auth.is_errored = True
self.assertEqual(self.auth.validate_response(fake_resp, True), {"foo": "bar"})
self.assertFalse(self.auth.is_errored)
def test_response_not_json(self):
"""Check response when not json."""
fake_resp = "foobar"
self.auth.is_errored = True
self.assertEqual(self.auth.validate_response(fake_resp, False), "foobar")
self.assertFalse(self.auth.is_errored)
def test_response_bad_json(self):
"""Check response when not json but expecting json."""
self.auth.is_errored = False
with self.assertRaises(BlinkBadResponse):
self.auth.validate_response(None, True)
self.assertTrue(self.auth.is_errored)
def test_header(self):
"""Test header data."""
@@ -131,8 +151,12 @@ class TestAuth(unittest.TestCase):
"""Test login handling when bad response."""
fake_resp = mresp.MockResponse({"foo": "bar"}, 404)
mock_req.return_value = fake_resp
self.auth.is_errored = False
with self.assertRaises(LoginError):
self.auth.login()
with self.assertRaises(TokenRefreshFailed):
self.auth.refresh_token()
self.assertTrue(self.auth.is_errored)
@mock.patch("blinkpy.auth.Auth.login")
def test_refresh_token(self, mock_login):
@@ -153,8 +177,10 @@ class TestAuth(unittest.TestCase):
def test_refresh_token_failed(self, mock_login):
"""Test refresh token failed."""
mock_login.return_value = {}
self.auth.is_errored = False
with self.assertRaises(TokenRefreshFailed):
self.auth.refresh_token()
self.assertTrue(self.auth.is_errored)
def test_check_key_required(self):
"""Check key required method."""
@@ -197,10 +223,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."""
+6
View File
@@ -106,6 +106,9 @@ class TestBlinkSetup(unittest.TestCase):
mock_home.return_value = {}
with self.assertRaises(BlinkSetupError):
self.blink.setup_camera_list()
mock_home.return_value = None
with self.assertRaises(BlinkSetupError):
self.blink.setup_camera_list()
def test_setup_urls(self):
"""Check setup of URLS."""
@@ -132,6 +135,9 @@ class TestBlinkSetup(unittest.TestCase):
mock_networks.return_value = {}
with self.assertRaises(BlinkSetupError):
self.blink.setup_networks()
mock_networks.return_value = None
with self.assertRaises(BlinkSetupError):
self.blink.setup_networks()
@mock.patch("blinkpy.blinkpy.Auth.send_auth_key")
def test_setup_prompt_2fa(self, mock_key):
+9
View File
@@ -144,6 +144,15 @@ class TestBlinkCameraSetup(unittest.TestCase):
self.assertEqual(self.camera.clip, None)
self.assertEqual(self.camera.video_from_cache, None)
def test_camera_arm_status(self, mock_resp):
"""Test arming and disarming camera."""
self.camera.motion_enabled = None
self.assertFalse(self.camera.arm)
self.camera.motion_enabled = False
self.assertFalse(self.camera.arm)
self.camera.motion_enabled = True
self.assertTrue(self.camera.arm)
@mock.patch("blinkpy.camera.api.request_motion_detection_enable")
@mock.patch("blinkpy.camera.api.request_motion_detection_disable")
def test_motion_detection_enable_disable(self, mock_dis, mock_en, mock_rep):
+19 -1
View File
@@ -78,7 +78,7 @@ class TestBlinkSyncModule(unittest.TestCase):
self.assertEqual(self.blink.sync["test"].get_camera_info("1234"), "foobar")
def test_get_camera_info_fail(self, mock_resp):
"""Test hadnling of failed get camera info function."""
"""Test handling of failed get camera info function."""
mock_resp.return_value = None
self.assertEqual(self.blink.sync["test"].get_camera_info("1"), [])
mock_resp.return_value = {}
@@ -86,6 +86,24 @@ class TestBlinkSyncModule(unittest.TestCase):
mock_resp.return_value = {"camera": None}
self.assertEqual(self.blink.sync["test"].get_camera_info("1"), [])
def test_get_network_info(self, mock_resp):
"""Test network retrieval."""
mock_resp.return_value = {"network": {"sync_module_error": False}}
self.assertTrue(self.blink.sync["test"].get_network_info())
mock_resp.return_value = {"network": {"sync_module_error": True}}
self.assertFalse(self.blink.sync["test"].get_network_info())
def test_get_network_info_failure(self, mock_resp):
"""Test failed network retrieval."""
mock_resp.return_value = {}
self.blink.sync["test"].available = True
self.assertFalse(self.blink.sync["test"].get_network_info())
self.assertFalse(self.blink.sync["test"].available)
self.blink.sync["test"].available = True
mock_resp.return_value = None
self.assertFalse(self.blink.sync["test"].get_network_info())
self.assertFalse(self.blink.sync["test"].available)
def test_check_new_videos_startup(self, mock_resp):
"""Test that check_new_videos does not block startup."""
sync_module = self.blink.sync["test"]