Cloud connection via aiohttp (#9860)

* Cloud: connect to cloud

* Fix tests in py34

* Update warrant to 0.5.0

* Differentiate errors between unknown handler vs exception

* Lint

* Respond to cloud message to logout

* Refresh token exception handling

* Swap out bare exception for RuntimeError

* Add more tests

* Fix tests py34
This commit is contained in:
Paulus Schoutsen 2017-10-14 19:43:14 -07:00 committed by GitHub
parent 26cb67dec2
commit 0362a76cd6
12 changed files with 930 additions and 429 deletions

View file

@ -1,47 +1,147 @@
"""Component to integrate the Home Assistant cloud."""
import asyncio
import json
import logging
import os
import voluptuous as vol
from . import http_api, auth_api
from .const import DOMAIN
from homeassistant.const import EVENT_HOMEASSISTANT_START
from . import http_api, iot
from .const import CONFIG_DIR, DOMAIN, SERVERS
REQUIREMENTS = ['warrant==0.2.0']
REQUIREMENTS = ['warrant==0.5.0']
DEPENDENCIES = ['http']
CONF_MODE = 'mode'
CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
CONF_USER_POOL_ID = 'user_pool_id'
CONF_REGION = 'region'
CONF_RELAYER = 'relayer'
MODE_DEV = 'development'
MODE_STAGING = 'staging'
MODE_PRODUCTION = 'production'
DEFAULT_MODE = MODE_DEV
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
vol.In([MODE_DEV, MODE_STAGING, MODE_PRODUCTION]),
vol.In([MODE_DEV] + list(SERVERS)),
# Change to optional when we include real servers
vol.Required(CONF_COGNITO_CLIENT_ID): str,
vol.Required(CONF_USER_POOL_ID): str,
vol.Required(CONF_REGION): str,
vol.Required(CONF_RELAYER): str,
}),
}, extra=vol.ALLOW_EXTRA)
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def async_setup(hass, config):
"""Initialize the Home Assistant cloud."""
mode = MODE_PRODUCTION
if DOMAIN in config:
mode = config[DOMAIN].get(CONF_MODE)
kwargs = config[DOMAIN]
else:
kwargs = {CONF_MODE: DEFAULT_MODE}
if mode != 'development':
_LOGGER.error('Only development mode is currently allowed.')
return False
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
data = hass.data[DOMAIN] = {
'mode': mode
}
@asyncio.coroutine
def init_cloud(event):
"""Initialize connection."""
yield from cloud.initialize()
data['auth'] = yield from hass.async_add_job(auth_api.load_auth, hass)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, init_cloud)
yield from http_api.async_setup(hass)
return True
class Cloud:
"""Store the configuration of the cloud connection."""
def __init__(self, hass, mode, cognito_client_id=None, user_pool_id=None,
region=None, relayer=None):
"""Create an instance of Cloud."""
self.hass = hass
self.mode = mode
self.email = None
self.id_token = None
self.access_token = None
self.refresh_token = None
self.iot = iot.CloudIoT(self)
if mode == MODE_DEV:
self.cognito_client_id = cognito_client_id
self.user_pool_id = user_pool_id
self.region = region
self.relayer = relayer
else:
info = SERVERS[mode]
self.cognito_client_id = info['cognito_client_id']
self.user_pool_id = info['user_pool_id']
self.region = info['region']
self.relayer = info['relayer']
@property
def is_logged_in(self):
"""Get if cloud is logged in."""
return self.email is not None
@property
def user_info_path(self):
"""Get path to the stored auth."""
return self.path('{}_auth.json'.format(self.mode))
@asyncio.coroutine
def initialize(self):
"""Initialize and load cloud info."""
def load_config():
"""Load the configuration."""
# Ensure config dir exists
path = self.hass.config.path(CONFIG_DIR)
if not os.path.isdir(path):
os.mkdir(path)
user_info = self.user_info_path
if os.path.isfile(user_info):
with open(user_info, 'rt') as file:
info = json.loads(file.read())
self.email = info['email']
self.id_token = info['id_token']
self.access_token = info['access_token']
self.refresh_token = info['refresh_token']
yield from self.hass.async_add_job(load_config)
if self.email is not None:
yield from self.iot.connect()
def path(self, *parts):
"""Get config path inside cloud dir."""
return self.hass.config.path(CONFIG_DIR, *parts)
@asyncio.coroutine
def logout(self):
"""Close connection and remove all credentials."""
yield from self.iot.disconnect()
self.email = None
self.id_token = None
self.access_token = None
self.refresh_token = None
yield from self.hass.async_add_job(
lambda: os.remove(self.user_info_path))
def write_user_info(self):
"""Write user info to a file."""
with open(self.user_info_path, 'wt') as file:
file.write(json.dumps({
'email': self.email,
'id_token': self.id_token,
'access_token': self.access_token,
'refresh_token': self.refresh_token,
}, indent=4))

View file

@ -1,10 +1,7 @@
"""Package to offer tools to authenticate with the cloud."""
import json
"""Package to communicate with the authentication API."""
import hashlib
import logging
import os
from .const import AUTH_FILE, SERVERS
from .util import get_mode
_LOGGER = logging.getLogger(__name__)
@ -61,210 +58,120 @@ def _map_aws_exception(err):
return ex(err.response['Error']['Message'])
def load_auth(hass):
"""Load authentication from disk and verify it."""
info = _read_info(hass)
if info is None:
return Auth(hass)
auth = Auth(hass, _cognito(
hass,
id_token=info['id_token'],
access_token=info['access_token'],
refresh_token=info['refresh_token'],
))
if auth.validate_auth():
return auth
return Auth(hass)
def _generate_username(email):
"""Generate a username from an email address."""
return hashlib.sha512(email.encode('utf-8')).hexdigest()
def register(hass, email, password):
def register(cloud, email, password):
"""Register a new account."""
from botocore.exceptions import ClientError
cognito = _cognito(hass, username=email)
cognito = _cognito(cloud)
try:
cognito.register(email, password)
cognito.register(_generate_username(email), password, email=email)
except ClientError as err:
raise _map_aws_exception(err)
def confirm_register(hass, confirmation_code, email):
def confirm_register(cloud, confirmation_code, email):
"""Confirm confirmation code after registration."""
from botocore.exceptions import ClientError
cognito = _cognito(hass, username=email)
cognito = _cognito(cloud)
try:
cognito.confirm_sign_up(confirmation_code, email)
cognito.confirm_sign_up(confirmation_code, _generate_username(email))
except ClientError as err:
raise _map_aws_exception(err)
def forgot_password(hass, email):
def forgot_password(cloud, email):
"""Initiate forgotten password flow."""
from botocore.exceptions import ClientError
cognito = _cognito(hass, username=email)
cognito = _cognito(cloud, username=_generate_username(email))
try:
cognito.initiate_forgot_password()
except ClientError as err:
raise _map_aws_exception(err)
def confirm_forgot_password(hass, confirmation_code, email, new_password):
def confirm_forgot_password(cloud, confirmation_code, email, new_password):
"""Confirm forgotten password code and change password."""
from botocore.exceptions import ClientError
cognito = _cognito(hass, username=email)
cognito = _cognito(cloud, username=_generate_username(email))
try:
cognito.confirm_forgot_password(confirmation_code, new_password)
except ClientError as err:
raise _map_aws_exception(err)
class Auth(object):
"""Class that holds Cloud authentication."""
def __init__(self, hass, cognito=None):
"""Initialize Hass cloud info object."""
self.hass = hass
self.cognito = cognito
self.account = None
@property
def is_logged_in(self):
"""Return if user is logged in."""
return self.account is not None
def validate_auth(self):
"""Validate that the contained auth is valid."""
from botocore.exceptions import ClientError
try:
self._refresh_account_info()
except ClientError as err:
if err.response['Error']['Code'] != 'NotAuthorizedException':
_LOGGER.error('Unexpected error verifying auth: %s', err)
return False
try:
self.renew_access_token()
self._refresh_account_info()
except ClientError:
_LOGGER.error('Unable to refresh auth token: %s', err)
return False
return True
def login(self, username, password):
"""Login using a username and password."""
from botocore.exceptions import ClientError
from warrant.exceptions import ForceChangePasswordException
cognito = _cognito(self.hass, username=username)
try:
cognito.authenticate(password=password)
self.cognito = cognito
self._refresh_account_info()
_write_info(self.hass, self)
except ForceChangePasswordException as err:
raise PasswordChangeRequired
except ClientError as err:
raise _map_aws_exception(err)
def _refresh_account_info(self):
"""Refresh the account info.
Raises boto3 exceptions.
"""
self.account = self.cognito.get_user()
def renew_access_token(self):
"""Refresh token."""
from botocore.exceptions import ClientError
try:
self.cognito.renew_access_token()
_write_info(self.hass, self)
return True
except ClientError as err:
_LOGGER.error('Error refreshing token: %s', err)
return False
def logout(self):
"""Invalidate token."""
from botocore.exceptions import ClientError
try:
self.cognito.logout()
self.account = None
_write_info(self.hass, self)
except ClientError as err:
raise _map_aws_exception(err)
def login(cloud, email, password):
"""Log user in and fetch certificate."""
cognito = _authenticate(cloud, email, password)
cloud.id_token = cognito.id_token
cloud.access_token = cognito.access_token
cloud.refresh_token = cognito.refresh_token
cloud.email = email
cloud.write_user_info()
def _read_info(hass):
"""Read auth file."""
path = hass.config.path(AUTH_FILE)
def check_token(cloud):
"""Check that the token is valid and verify if needed."""
from botocore.exceptions import ClientError
if not os.path.isfile(path):
return None
cognito = _cognito(
cloud,
access_token=cloud.access_token,
refresh_token=cloud.refresh_token)
with open(path) as file:
return json.load(file).get(get_mode(hass))
try:
if cognito.check_token():
cloud.id_token = cognito.id_token
cloud.access_token = cognito.access_token
cloud.write_user_info()
except ClientError as err:
raise _map_aws_exception(err)
def _write_info(hass, auth):
"""Write auth info for specified mode.
def _authenticate(cloud, email, password):
"""Log in and return an authenticated Cognito instance."""
from botocore.exceptions import ClientError
from warrant.exceptions import ForceChangePasswordException
Pass in None for data to remove authentication for that mode.
"""
path = hass.config.path(AUTH_FILE)
mode = get_mode(hass)
assert not cloud.is_logged_in, 'Cannot login if already logged in.'
if os.path.isfile(path):
with open(path) as file:
content = json.load(file)
else:
content = {}
cognito = _cognito(cloud, username=email)
if auth.is_logged_in:
content[mode] = {
'id_token': auth.cognito.id_token,
'access_token': auth.cognito.access_token,
'refresh_token': auth.cognito.refresh_token,
}
else:
content.pop(mode, None)
try:
cognito.authenticate(password=password)
return cognito
with open(path, 'wt') as file:
file.write(json.dumps(content, indent=4, sort_keys=True))
except ForceChangePasswordException as err:
raise PasswordChangeRequired
except ClientError as err:
raise _map_aws_exception(err)
def _cognito(hass, **kwargs):
def _cognito(cloud, **kwargs):
"""Get the client credentials."""
import botocore
import boto3
from warrant import Cognito
mode = get_mode(hass)
info = SERVERS.get(mode)
if info is None:
raise ValueError('Mode {} is not supported.'.format(mode))
cognito = Cognito(
user_pool_id=info['identity_pool_id'],
client_id=info['client_id'],
user_pool_region=info['region'],
access_key=info['access_key_id'],
secret_key=info['secret_access_key'],
user_pool_id=cloud.user_pool_id,
client_id=cloud.cognito_client_id,
user_pool_region=cloud.region,
**kwargs
)
cognito.client = boto3.client(
'cognito-idp',
region_name=cloud.region,
config=botocore.config.Config(
signature_version=botocore.UNSIGNED
)
)
return cognito

View file

@ -1,14 +1,14 @@
"""Constants for the cloud component."""
DOMAIN = 'cloud'
CONFIG_DIR = '.cloud'
REQUEST_TIMEOUT = 10
AUTH_FILE = '.cloud'
SERVERS = {
'development': {
'client_id': '3k755iqfcgv8t12o4pl662mnos',
'identity_pool_id': 'us-west-2_vDOfweDJo',
'region': 'us-west-2',
'access_key_id': 'AKIAJGRK7MILPRJTT2ZQ',
'secret_access_key': 'lscdYBApxrLWL0HKuVqVXWv3ou8ZVXgG7rZBu/Sz'
}
# Example entry:
# 'production': {
# 'cognito_client_id': '',
# 'user_pool_id': '',
# 'region': '',
# 'relayer': ''
# }
}

View file

@ -10,7 +10,7 @@ from homeassistant.components.http import (
HomeAssistantView, RequestDataValidator)
from . import auth_api
from .const import REQUEST_TIMEOUT
from .const import DOMAIN, REQUEST_TIMEOUT
_LOGGER = logging.getLogger(__name__)
@ -74,13 +74,14 @@ class CloudLoginView(HomeAssistantView):
def post(self, request, data):
"""Handle login request."""
hass = request.app['hass']
auth = hass.data['cloud']['auth']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(auth.login, data['email'],
yield from hass.async_add_job(auth_api.login, cloud, data['email'],
data['password'])
hass.async_add_job(cloud.iot.connect)
return self.json(_auth_data(auth))
return self.json(_account_data(cloud))
class CloudLogoutView(HomeAssistantView):
@ -94,10 +95,10 @@ class CloudLogoutView(HomeAssistantView):
def post(self, request):
"""Handle logout request."""
hass = request.app['hass']
auth = hass.data['cloud']['auth']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(auth.logout)
yield from cloud.logout()
return self.json_message('ok')
@ -112,12 +113,12 @@ class CloudAccountView(HomeAssistantView):
def get(self, request):
"""Get account info."""
hass = request.app['hass']
auth = hass.data['cloud']['auth']
cloud = hass.data[DOMAIN]
if not auth.is_logged_in:
if not cloud.is_logged_in:
return self.json_message('Not logged in', 400)
return self.json(_auth_data(auth))
return self.json(_account_data(cloud))
class CloudRegisterView(HomeAssistantView):
@ -135,10 +136,11 @@ class CloudRegisterView(HomeAssistantView):
def post(self, request, data):
"""Handle registration request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(
auth_api.register, hass, data['email'], data['password'])
auth_api.register, cloud, data['email'], data['password'])
return self.json_message('ok')
@ -158,10 +160,11 @@ class CloudConfirmRegisterView(HomeAssistantView):
def post(self, request, data):
"""Handle registration confirmation request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(
auth_api.confirm_register, hass, data['confirmation_code'],
auth_api.confirm_register, cloud, data['confirmation_code'],
data['email'])
return self.json_message('ok')
@ -181,10 +184,11 @@ class CloudForgotPasswordView(HomeAssistantView):
def post(self, request, data):
"""Handle forgot password request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(
auth_api.forgot_password, hass, data['email'])
auth_api.forgot_password, cloud, data['email'])
return self.json_message('ok')
@ -205,18 +209,19 @@ class CloudConfirmForgotPasswordView(HomeAssistantView):
def post(self, request, data):
"""Handle forgot password confirm request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(
auth_api.confirm_forgot_password, hass,
auth_api.confirm_forgot_password, cloud,
data['confirmation_code'], data['email'],
data['new_password'])
return self.json_message('ok')
def _auth_data(auth):
def _account_data(cloud):
"""Generate the auth data JSON response."""
return {
'email': auth.account.email
'email': cloud.email
}

View file

@ -0,0 +1,195 @@
"""Module to handle messages from Home Assistant cloud."""
import asyncio
import logging
from aiohttp import hdrs, client_exceptions, WSMsgType
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.components.alexa import smart_home
from homeassistant.util.decorator import Registry
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import auth_api
HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__)
class UnknownHandler(Exception):
"""Exception raised when trying to handle unknown handler."""
class CloudIoT:
"""Class to manage the IoT connection."""
def __init__(self, cloud):
"""Initialize the CloudIoT class."""
self.cloud = cloud
self.client = None
self.close_requested = False
self.tries = 0
@property
def is_connected(self):
"""Return if connected to the cloud."""
return self.client is not None
@asyncio.coroutine
def connect(self):
"""Connect to the IoT broker."""
if self.client is not None:
raise RuntimeError('Cannot connect while already connected')
self.close_requested = False
hass = self.cloud.hass
remove_hass_stop_listener = None
session = async_get_clientsession(self.cloud.hass)
headers = {
hdrs.AUTHORIZATION: 'Bearer {}'.format(self.cloud.access_token)
}
@asyncio.coroutine
def _handle_hass_stop(event):
"""Handle Home Assistant shutting down."""
nonlocal remove_hass_stop_listener
remove_hass_stop_listener = None
yield from self.disconnect()
client = None
disconnect_warn = None
try:
yield from hass.async_add_job(auth_api.check_token, self.cloud)
self.client = client = yield from session.ws_connect(
'ws://{}/websocket'.format(self.cloud.relayer),
headers=headers)
self.tries = 0
remove_hass_stop_listener = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _handle_hass_stop)
_LOGGER.info('Connected')
while not client.closed:
msg = yield from client.receive()
if msg.type in (WSMsgType.ERROR, WSMsgType.CLOSED,
WSMsgType.CLOSING):
disconnect_warn = 'Closed by server'
break
elif msg.type != WSMsgType.TEXT:
disconnect_warn = 'Received non-Text message: {}'.format(
msg.type)
break
try:
msg = msg.json()
except ValueError:
disconnect_warn = 'Received invalid JSON.'
break
_LOGGER.debug('Received message: %s', msg)
response = {
'msgid': msg['msgid'],
}
try:
result = yield from async_handle_message(
hass, self.cloud, msg['handler'], msg['payload'])
# No response from handler
if result is None:
continue
response['payload'] = result
except UnknownHandler:
response['error'] = 'unknown-handler'
except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error handling message')
response['error'] = 'exception'
_LOGGER.debug('Publishing message: %s', response)
yield from client.send_json(response)
except auth_api.CloudError:
_LOGGER.warning('Unable to connect: Unable to refresh token.')
except client_exceptions.WSServerHandshakeError as err:
if err.code == 401:
disconnect_warn = 'Invalid auth.'
self.close_requested = True
# Should we notify user?
else:
_LOGGER.warning('Unable to connect: %s', err)
except client_exceptions.ClientError as err:
_LOGGER.warning('Unable to connect: %s', err)
except Exception: # pylint: disable=broad-except
if not self.close_requested:
_LOGGER.exception('Unexpected error')
finally:
if disconnect_warn is not None:
_LOGGER.warning('Connection closed: %s', disconnect_warn)
if remove_hass_stop_listener is not None:
remove_hass_stop_listener()
if client is not None:
self.client = None
yield from client.close()
if not self.close_requested:
self.tries += 1
# Sleep 0, 5, 10, 15 … up to 30 seconds between retries
yield from asyncio.sleep(
min(30, (self.tries - 1) * 5), loop=hass.loop)
hass.async_add_job(self.connect())
@asyncio.coroutine
def disconnect(self):
"""Disconnect the client."""
self.close_requested = True
yield from self.client.close()
@asyncio.coroutine
def async_handle_message(hass, cloud, handler_name, payload):
"""Handle incoming IoT message."""
handler = HANDLERS.get(handler_name)
if handler is None:
raise UnknownHandler()
return (yield from handler(hass, cloud, payload))
@HANDLERS.register('alexa')
@asyncio.coroutine
def async_handle_alexa(hass, cloud, payload):
"""Handle an incoming IoT message for Alexa."""
return (yield from smart_home.async_handle_message(hass, payload))
@HANDLERS.register('cloud')
@asyncio.coroutine
def async_handle_cloud(hass, cloud, payload):
"""Handle an incoming IoT message for cloud component."""
action = payload['action']
if action == 'logout':
yield from cloud.logout()
_LOGGER.error('You have been logged out from Home Assistant cloud: %s',
payload['reason'])
else:
_LOGGER.warning('Received unknown cloud action: %s', action)
return None

View file

@ -1,10 +0,0 @@
"""Utilities for the cloud integration."""
from .const import DOMAIN
def get_mode(hass):
"""Return the current mode of the cloud component.
Async friendly.
"""
return hass.data[DOMAIN]['mode']

View file

@ -1043,7 +1043,7 @@ wakeonlan==0.2.2
waqiasync==1.0.0
# homeassistant.components.cloud
warrant==0.2.0
warrant==0.5.0
# homeassistant.components.media_player.gpmdp
websocket-client==0.37.0

View file

@ -149,7 +149,7 @@ statsd==3.2.1
uvcclient==0.10.1
# homeassistant.components.cloud
warrant==0.2.0
warrant==0.5.0
# homeassistant.components.sensor.yahoo_finance
yahoo-finance==1.4.0

View file

@ -4,35 +4,7 @@ from unittest.mock import MagicMock, patch
from botocore.exceptions import ClientError
import pytest
from homeassistant.components.cloud import DOMAIN, auth_api
MOCK_AUTH = {
"id_token": "fake_id_token",
"access_token": "fake_access_token",
"refresh_token": "fake_refresh_token",
}
@pytest.fixture
def cloud_hass(hass):
"""Fixture to return a hass instance with cloud mode set."""
hass.data[DOMAIN] = {'mode': 'development'}
return hass
@pytest.fixture
def mock_write():
"""Mock reading authentication."""
with patch.object(auth_api, '_write_info') as mock:
yield mock
@pytest.fixture
def mock_read():
"""Mock writing authentication."""
with patch.object(auth_api, '_read_info') as mock:
yield mock
from homeassistant.components.cloud import auth_api
@pytest.fixture
@ -42,13 +14,6 @@ def mock_cognito():
yield mock_cog()
@pytest.fixture
def mock_auth():
"""Mock warrant."""
with patch('homeassistant.components.cloud.auth_api.Auth') as mock_auth:
yield mock_auth()
def aws_error(code, message='Unknown', operation_name='fake_operation_name'):
"""Generate AWS error response."""
response = {
@ -60,159 +25,64 @@ def aws_error(code, message='Unknown', operation_name='fake_operation_name'):
return ClientError(response, operation_name)
def test_load_auth_with_no_stored_auth(cloud_hass, mock_read):
"""Test loading authentication with no stored auth."""
mock_read.return_value = None
auth = auth_api.load_auth(cloud_hass)
assert auth.cognito is None
def test_load_auth_with_invalid_auth(cloud_hass, mock_read, mock_cognito):
"""Test calling load_auth when auth is no longer valid."""
mock_cognito.get_user.side_effect = aws_error('SomeError')
auth = auth_api.load_auth(cloud_hass)
assert auth.cognito is None
def test_load_auth_with_valid_auth(cloud_hass, mock_read, mock_cognito):
"""Test calling load_auth when valid auth."""
auth = auth_api.load_auth(cloud_hass)
assert auth.cognito is not None
def test_auth_properties():
"""Test Auth class properties."""
auth = auth_api.Auth(None, None)
assert not auth.is_logged_in
auth.account = {}
assert auth.is_logged_in
def test_auth_validate_auth_verification_fails(mock_cognito):
"""Test validate authentication with verify request failing."""
mock_cognito.get_user.side_effect = aws_error('UserNotFoundException')
auth = auth_api.Auth(None, mock_cognito)
assert auth.validate_auth() is False
def test_auth_validate_auth_token_refresh_needed_fails(mock_cognito):
"""Test validate authentication with refresh needed which gets 401."""
mock_cognito.get_user.side_effect = aws_error('NotAuthorizedException')
mock_cognito.renew_access_token.side_effect = \
aws_error('NotAuthorizedException')
auth = auth_api.Auth(None, mock_cognito)
assert auth.validate_auth() is False
def test_auth_validate_auth_token_refresh_needed_succeeds(mock_write,
mock_cognito):
"""Test validate authentication with refresh."""
mock_cognito.get_user.side_effect = [
aws_error('NotAuthorizedException'),
MagicMock(email='hello@home-assistant.io')
]
auth = auth_api.Auth(None, mock_cognito)
assert auth.validate_auth() is True
assert len(mock_write.mock_calls) == 1
def test_auth_login_invalid_auth(mock_cognito, mock_write):
def test_login_invalid_auth(mock_cognito):
"""Test trying to login with invalid credentials."""
cloud = MagicMock(is_logged_in=False)
mock_cognito.authenticate.side_effect = aws_error('NotAuthorizedException')
auth = auth_api.Auth(None, None)
with pytest.raises(auth_api.Unauthenticated):
auth.login('user', 'pass')
auth_api.login(cloud, 'user', 'pass')
assert not auth.is_logged_in
assert len(mock_cognito.get_user.mock_calls) == 0
assert len(mock_write.mock_calls) == 0
assert len(cloud.write_user_info.mock_calls) == 0
def test_auth_login_user_not_found(mock_cognito, mock_write):
def test_login_user_not_found(mock_cognito):
"""Test trying to login with invalid credentials."""
cloud = MagicMock(is_logged_in=False)
mock_cognito.authenticate.side_effect = aws_error('UserNotFoundException')
auth = auth_api.Auth(None, None)
with pytest.raises(auth_api.UserNotFound):
auth.login('user', 'pass')
auth_api.login(cloud, 'user', 'pass')
assert not auth.is_logged_in
assert len(mock_cognito.get_user.mock_calls) == 0
assert len(mock_write.mock_calls) == 0
assert len(cloud.write_user_info.mock_calls) == 0
def test_auth_login_user_not_confirmed(mock_cognito, mock_write):
def test_login_user_not_confirmed(mock_cognito):
"""Test trying to login without confirming account."""
cloud = MagicMock(is_logged_in=False)
mock_cognito.authenticate.side_effect = \
aws_error('UserNotConfirmedException')
auth = auth_api.Auth(None, None)
with pytest.raises(auth_api.UserNotConfirmed):
auth.login('user', 'pass')
auth_api.login(cloud, 'user', 'pass')
assert not auth.is_logged_in
assert len(mock_cognito.get_user.mock_calls) == 0
assert len(mock_write.mock_calls) == 0
assert len(cloud.write_user_info.mock_calls) == 0
def test_auth_login(cloud_hass, mock_cognito, mock_write):
def test_login(mock_cognito):
"""Test trying to login without confirming account."""
mock_cognito.get_user.return_value = \
MagicMock(email='hello@home-assistant.io')
auth = auth_api.Auth(cloud_hass, None)
auth.login('user', 'pass')
assert auth.is_logged_in
cloud = MagicMock(is_logged_in=False)
mock_cognito.id_token = 'test_id_token'
mock_cognito.access_token = 'test_access_token'
mock_cognito.refresh_token = 'test_refresh_token'
auth_api.login(cloud, 'user', 'pass')
assert len(mock_cognito.authenticate.mock_calls) == 1
assert len(mock_write.mock_calls) == 1
result_hass, result_auth = mock_write.mock_calls[0][1]
assert result_hass is cloud_hass
assert result_auth is auth
def test_auth_renew_access_token(mock_write, mock_cognito):
"""Test renewing an access token."""
auth = auth_api.Auth(None, mock_cognito)
assert auth.renew_access_token()
assert len(mock_write.mock_calls) == 1
def test_auth_renew_access_token_fails(mock_write, mock_cognito):
"""Test failing to renew an access token."""
mock_cognito.renew_access_token.side_effect = aws_error('SomeError')
auth = auth_api.Auth(None, mock_cognito)
assert not auth.renew_access_token()
assert len(mock_write.mock_calls) == 0
def test_auth_logout(mock_write, mock_cognito):
"""Test renewing an access token."""
auth = auth_api.Auth(None, mock_cognito)
auth.account = MagicMock()
auth.logout()
assert auth.account is None
assert len(mock_write.mock_calls) == 1
def test_auth_logout_fails(mock_write, mock_cognito):
"""Test error while logging out."""
mock_cognito.logout.side_effect = aws_error('SomeError')
auth = auth_api.Auth(None, mock_cognito)
auth.account = MagicMock()
with pytest.raises(auth_api.CloudError):
auth.logout()
assert auth.account is not None
assert len(mock_write.mock_calls) == 0
assert cloud.email == 'user'
assert cloud.id_token == 'test_id_token'
assert cloud.access_token == 'test_access_token'
assert cloud.refresh_token == 'test_refresh_token'
assert len(cloud.write_user_info.mock_calls) == 1
def test_register(mock_cognito):
"""Test registering an account."""
auth_api.register(None, 'email@home-assistant.io', 'password')
assert len(mock_cognito.register.mock_calls) == 1
result_email, result_password = mock_cognito.register.mock_calls[0][1]
assert result_email == 'email@home-assistant.io'
result_user, result_password = mock_cognito.register.mock_calls[0][1]
assert result_user == \
auth_api._generate_username('email@home-assistant.io')
assert result_password == 'password'
@ -227,8 +97,9 @@ def test_confirm_register(mock_cognito):
"""Test confirming a registration of an account."""
auth_api.confirm_register(None, '123456', 'email@home-assistant.io')
assert len(mock_cognito.confirm_sign_up.mock_calls) == 1
result_code, result_email = mock_cognito.confirm_sign_up.mock_calls[0][1]
assert result_email == 'email@home-assistant.io'
result_code, result_user = mock_cognito.confirm_sign_up.mock_calls[0][1]
assert result_user == \
auth_api._generate_username('email@home-assistant.io')
assert result_code == '123456'
@ -269,3 +140,45 @@ def test_confirm_forgot_password_fails(mock_cognito):
with pytest.raises(auth_api.CloudError):
auth_api.confirm_forgot_password(
None, '123456', 'email@home-assistant.io', 'new password')
def test_check_token_writes_new_token_on_refresh(mock_cognito):
"""Test check_token writes new token if refreshed."""
cloud = MagicMock()
mock_cognito.check_token.return_value = True
mock_cognito.id_token = 'new id token'
mock_cognito.access_token = 'new access token'
auth_api.check_token(cloud)
assert len(mock_cognito.check_token.mock_calls) == 1
assert cloud.id_token == 'new id token'
assert cloud.access_token == 'new access token'
assert len(cloud.write_user_info.mock_calls) == 1
def test_check_token_does_not_write_existing_token(mock_cognito):
"""Test check_token won't write new token if still valid."""
cloud = MagicMock()
mock_cognito.check_token.return_value = False
auth_api.check_token(cloud)
assert len(mock_cognito.check_token.mock_calls) == 1
assert cloud.id_token != mock_cognito.id_token
assert cloud.access_token != mock_cognito.access_token
assert len(cloud.write_user_info.mock_calls) == 0
def test_check_token_raises(mock_cognito):
"""Test we raise correct error."""
cloud = MagicMock()
mock_cognito.check_token.side_effect = aws_error('SomeError')
with pytest.raises(auth_api.CloudError):
auth_api.check_token(cloud)
assert len(mock_cognito.check_token.mock_calls) == 1
assert cloud.id_token != mock_cognito.id_token
assert cloud.access_token != mock_cognito.access_token
assert len(cloud.write_user_info.mock_calls) == 0

View file

@ -7,25 +7,25 @@ import pytest
from homeassistant.bootstrap import async_setup_component
from homeassistant.components.cloud import DOMAIN, auth_api
from tests.common import mock_coro
@pytest.fixture
def cloud_client(hass, test_client):
"""Fixture that can fetch from the cloud client."""
hass.loop.run_until_complete(async_setup_component(hass, 'cloud', {
'cloud': {
'mode': 'development'
}
}))
with patch('homeassistant.components.cloud.Cloud.initialize'):
hass.loop.run_until_complete(async_setup_component(hass, 'cloud', {
'cloud': {
'mode': 'development',
'cognito_client_id': 'cognito_client_id',
'user_pool_id': 'user_pool_id',
'region': 'region',
'relayer': 'relayer',
}
}))
return hass.loop.run_until_complete(test_client(hass.http.app))
@pytest.fixture
def mock_auth(cloud_client, hass):
"""Fixture to mock authentication."""
auth = hass.data[DOMAIN]['auth'] = MagicMock()
return auth
@pytest.fixture
def mock_cognito():
"""Mock warrant."""
@ -41,9 +41,9 @@ def test_account_view_no_account(cloud_client):
@asyncio.coroutine
def test_account_view(mock_auth, cloud_client):
def test_account_view(hass, cloud_client):
"""Test fetching account if no account available."""
mock_auth.account = MagicMock(email='hello@home-assistant.io')
hass.data[DOMAIN].email = 'hello@home-assistant.io'
req = yield from cloud_client.get('/api/cloud/account')
assert req.status == 200
result = yield from req.json()
@ -51,99 +51,112 @@ def test_account_view(mock_auth, cloud_client):
@asyncio.coroutine
def test_login_view(mock_auth, cloud_client):
def test_login_view(hass, cloud_client):
"""Test logging in."""
mock_auth.account = MagicMock(email='hello@home-assistant.io')
req = yield from cloud_client.post('/api/cloud/login', json={
'email': 'my_username',
'password': 'my_password'
})
hass.data[DOMAIN].email = 'hello@home-assistant.io'
with patch('homeassistant.components.cloud.iot.CloudIoT.connect'), \
patch('homeassistant.components.cloud.'
'auth_api.login') as mock_login:
req = yield from cloud_client.post('/api/cloud/login', json={
'email': 'my_username',
'password': 'my_password'
})
assert req.status == 200
result = yield from req.json()
assert result == {'email': 'hello@home-assistant.io'}
assert len(mock_auth.login.mock_calls) == 1
result_user, result_pass = mock_auth.login.mock_calls[0][1]
assert len(mock_login.mock_calls) == 1
cloud, result_user, result_pass = mock_login.mock_calls[0][1]
assert result_user == 'my_username'
assert result_pass == 'my_password'
@asyncio.coroutine
def test_login_view_invalid_json(mock_auth, cloud_client):
def test_login_view_invalid_json(cloud_client):
"""Try logging in with invalid JSON."""
req = yield from cloud_client.post('/api/cloud/login', data='Not JSON')
with patch('homeassistant.components.cloud.auth_api.login') as mock_login:
req = yield from cloud_client.post('/api/cloud/login', data='Not JSON')
assert req.status == 400
assert len(mock_auth.mock_calls) == 0
assert len(mock_login.mock_calls) == 0
@asyncio.coroutine
def test_login_view_invalid_schema(mock_auth, cloud_client):
def test_login_view_invalid_schema(cloud_client):
"""Try logging in with invalid schema."""
req = yield from cloud_client.post('/api/cloud/login', json={
'invalid': 'schema'
})
with patch('homeassistant.components.cloud.auth_api.login') as mock_login:
req = yield from cloud_client.post('/api/cloud/login', json={
'invalid': 'schema'
})
assert req.status == 400
assert len(mock_auth.mock_calls) == 0
assert len(mock_login.mock_calls) == 0
@asyncio.coroutine
def test_login_view_request_timeout(mock_auth, cloud_client):
def test_login_view_request_timeout(cloud_client):
"""Test request timeout while trying to log in."""
mock_auth.login.side_effect = asyncio.TimeoutError
req = yield from cloud_client.post('/api/cloud/login', json={
'email': 'my_username',
'password': 'my_password'
})
with patch('homeassistant.components.cloud.auth_api.login',
side_effect=asyncio.TimeoutError):
req = yield from cloud_client.post('/api/cloud/login', json={
'email': 'my_username',
'password': 'my_password'
})
assert req.status == 502
@asyncio.coroutine
def test_login_view_invalid_credentials(mock_auth, cloud_client):
def test_login_view_invalid_credentials(cloud_client):
"""Test logging in with invalid credentials."""
mock_auth.login.side_effect = auth_api.Unauthenticated
req = yield from cloud_client.post('/api/cloud/login', json={
'email': 'my_username',
'password': 'my_password'
})
with patch('homeassistant.components.cloud.auth_api.login',
side_effect=auth_api.Unauthenticated):
req = yield from cloud_client.post('/api/cloud/login', json={
'email': 'my_username',
'password': 'my_password'
})
assert req.status == 401
@asyncio.coroutine
def test_login_view_unknown_error(mock_auth, cloud_client):
def test_login_view_unknown_error(cloud_client):
"""Test unknown error while logging in."""
mock_auth.login.side_effect = auth_api.UnknownError
req = yield from cloud_client.post('/api/cloud/login', json={
'email': 'my_username',
'password': 'my_password'
})
with patch('homeassistant.components.cloud.auth_api.login',
side_effect=auth_api.UnknownError):
req = yield from cloud_client.post('/api/cloud/login', json={
'email': 'my_username',
'password': 'my_password'
})
assert req.status == 502
@asyncio.coroutine
def test_logout_view(mock_auth, cloud_client):
def test_logout_view(hass, cloud_client):
"""Test logging out."""
cloud = hass.data['cloud'] = MagicMock()
cloud.logout.return_value = mock_coro()
req = yield from cloud_client.post('/api/cloud/logout')
assert req.status == 200
data = yield from req.json()
assert data == {'message': 'ok'}
assert len(mock_auth.logout.mock_calls) == 1
assert len(cloud.logout.mock_calls) == 1
@asyncio.coroutine
def test_logout_view_request_timeout(mock_auth, cloud_client):
def test_logout_view_request_timeout(hass, cloud_client):
"""Test timeout while logging out."""
mock_auth.logout.side_effect = asyncio.TimeoutError
cloud = hass.data['cloud'] = MagicMock()
cloud.logout.side_effect = asyncio.TimeoutError
req = yield from cloud_client.post('/api/cloud/logout')
assert req.status == 502
@asyncio.coroutine
def test_logout_view_unknown_error(mock_auth, cloud_client):
def test_logout_view_unknown_error(hass, cloud_client):
"""Test unknown error while logging out."""
mock_auth.logout.side_effect = auth_api.UnknownError
cloud = hass.data['cloud'] = MagicMock()
cloud.logout.side_effect = auth_api.UnknownError
req = yield from cloud_client.post('/api/cloud/logout')
assert req.status == 502
@ -158,7 +171,7 @@ def test_register_view(mock_cognito, cloud_client):
assert req.status == 200
assert len(mock_cognito.register.mock_calls) == 1
result_email, result_pass = mock_cognito.register.mock_calls[0][1]
assert result_email == 'hello@bla.com'
assert result_email == auth_api._generate_username('hello@bla.com')
assert result_pass == 'falcon42'
@ -205,7 +218,7 @@ def test_confirm_register_view(mock_cognito, cloud_client):
assert req.status == 200
assert len(mock_cognito.confirm_sign_up.mock_calls) == 1
result_code, result_email = mock_cognito.confirm_sign_up.mock_calls[0][1]
assert result_email == 'hello@bla.com'
assert result_email == auth_api._generate_username('hello@bla.com')
assert result_code == '123456'

View file

@ -0,0 +1,135 @@
"""Test the cloud component."""
import asyncio
import json
from unittest.mock import patch, MagicMock, mock_open
import pytest
from homeassistant.components import cloud
from tests.common import mock_coro
@pytest.fixture
def mock_os():
"""Mock os module."""
with patch('homeassistant.components.cloud.os') as os:
os.path.isdir.return_value = True
yield os
@asyncio.coroutine
def test_constructor_loads_info_from_constant():
"""Test non-dev mode loads info from SERVERS constant."""
hass = MagicMock(data={})
with patch.dict(cloud.SERVERS, {
'beer': {
'cognito_client_id': 'test-cognito_client_id',
'user_pool_id': 'test-user_pool_id',
'region': 'test-region',
'relayer': 'test-relayer',
}
}):
result = yield from cloud.async_setup(hass, {
'cloud': {cloud.CONF_MODE: 'beer'}
})
assert result
cl = hass.data['cloud']
assert cl.mode == 'beer'
assert cl.cognito_client_id == 'test-cognito_client_id'
assert cl.user_pool_id == 'test-user_pool_id'
assert cl.region == 'test-region'
assert cl.relayer == 'test-relayer'
@asyncio.coroutine
def test_constructor_loads_info_from_config():
"""Test non-dev mode loads info from SERVERS constant."""
hass = MagicMock(data={})
result = yield from cloud.async_setup(hass, {
'cloud': {
cloud.CONF_MODE: cloud.MODE_DEV,
'cognito_client_id': 'test-cognito_client_id',
'user_pool_id': 'test-user_pool_id',
'region': 'test-region',
'relayer': 'test-relayer',
}
})
assert result
cl = hass.data['cloud']
assert cl.mode == cloud.MODE_DEV
assert cl.cognito_client_id == 'test-cognito_client_id'
assert cl.user_pool_id == 'test-user_pool_id'
assert cl.region == 'test-region'
assert cl.relayer == 'test-relayer'
@asyncio.coroutine
def test_initialize_loads_info(mock_os, hass):
"""Test initialize will load info from config file."""
mock_os.path.isfile.return_value = True
mopen = mock_open(read_data=json.dumps({
'email': 'test-email',
'id_token': 'test-id-token',
'access_token': 'test-access-token',
'refresh_token': 'test-refresh-token',
}))
cl = cloud.Cloud(hass, cloud.MODE_DEV)
cl.iot = MagicMock()
cl.iot.connect.return_value = mock_coro()
with patch('homeassistant.components.cloud.open', mopen, create=True):
yield from cl.initialize()
assert cl.email == 'test-email'
assert cl.id_token == 'test-id-token'
assert cl.access_token == 'test-access-token'
assert cl.refresh_token == 'test-refresh-token'
assert len(cl.iot.connect.mock_calls) == 1
@asyncio.coroutine
def test_logout_clears_info(mock_os, hass):
"""Test logging out disconnects and removes info."""
cl = cloud.Cloud(hass, cloud.MODE_DEV)
cl.iot = MagicMock()
cl.iot.disconnect.return_value = mock_coro()
yield from cl.logout()
assert len(cl.iot.disconnect.mock_calls) == 1
assert cl.email is None
assert cl.id_token is None
assert cl.access_token is None
assert cl.refresh_token is None
assert len(mock_os.remove.mock_calls) == 1
@asyncio.coroutine
def test_write_user_info():
"""Test writing user info works."""
mopen = mock_open()
cl = cloud.Cloud(MagicMock(), cloud.MODE_DEV)
cl.email = 'test-email'
cl.id_token = 'test-id-token'
cl.access_token = 'test-access-token'
cl.refresh_token = 'test-refresh-token'
with patch('homeassistant.components.cloud.open', mopen, create=True):
cl.write_user_info()
handle = mopen()
assert len(handle.write.mock_calls) == 1
data = json.loads(handle.write.mock_calls[0][1][0])
assert data == {
'access_token': 'test-access-token',
'email': 'test-email',
'id_token': 'test-id-token',
'refresh_token': 'test-refresh-token',
}

View file

@ -0,0 +1,243 @@
"""Test the cloud.iot module."""
import asyncio
from unittest.mock import patch, MagicMock, PropertyMock
from aiohttp import WSMsgType, client_exceptions
import pytest
from homeassistant.components.cloud import iot, auth_api
from tests.common import mock_coro
@pytest.fixture
def mock_client():
"""Mock the IoT client."""
client = MagicMock()
type(client).closed = PropertyMock(side_effect=[False, True])
with patch('asyncio.sleep'), \
patch('homeassistant.components.cloud.iot'
'.async_get_clientsession') as session:
session().ws_connect.return_value = mock_coro(client)
yield client
@pytest.fixture
def mock_handle_message():
"""Mock handle message."""
with patch('homeassistant.components.cloud.iot'
'.async_handle_message') as mock:
yield mock
@asyncio.coroutine
def test_cloud_calling_handler(mock_client, mock_handle_message):
"""Test we call handle message with correct info."""
cloud = MagicMock()
conn = iot.CloudIoT(cloud)
mock_client.receive.return_value = mock_coro(MagicMock(
type=WSMsgType.text,
json=MagicMock(return_value={
'msgid': 'test-msg-id',
'handler': 'test-handler',
'payload': 'test-payload'
})
))
mock_handle_message.return_value = mock_coro('response')
mock_client.send_json.return_value = mock_coro(None)
yield from conn.connect()
# Check that we sent message to handler correctly
assert len(mock_handle_message.mock_calls) == 1
p_hass, p_cloud, handler_name, payload = \
mock_handle_message.mock_calls[0][1]
assert p_hass is cloud.hass
assert p_cloud is cloud
assert handler_name == 'test-handler'
assert payload == 'test-payload'
# Check that we forwarded response from handler to cloud
assert len(mock_client.send_json.mock_calls) == 1
assert mock_client.send_json.mock_calls[0][1][0] == {
'msgid': 'test-msg-id',
'payload': 'response'
}
@asyncio.coroutine
def test_connection_msg_for_unknown_handler(mock_client):
"""Test a msg for an unknown handler."""
cloud = MagicMock()
conn = iot.CloudIoT(cloud)
mock_client.receive.return_value = mock_coro(MagicMock(
type=WSMsgType.text,
json=MagicMock(return_value={
'msgid': 'test-msg-id',
'handler': 'non-existing-handler',
'payload': 'test-payload'
})
))
mock_client.send_json.return_value = mock_coro(None)
yield from conn.connect()
# Check that we sent the correct error
assert len(mock_client.send_json.mock_calls) == 1
assert mock_client.send_json.mock_calls[0][1][0] == {
'msgid': 'test-msg-id',
'error': 'unknown-handler',
}
@asyncio.coroutine
def test_connection_msg_for_handler_raising(mock_client, mock_handle_message):
"""Test we sent error when handler raises exception."""
cloud = MagicMock()
conn = iot.CloudIoT(cloud)
mock_client.receive.return_value = mock_coro(MagicMock(
type=WSMsgType.text,
json=MagicMock(return_value={
'msgid': 'test-msg-id',
'handler': 'test-handler',
'payload': 'test-payload'
})
))
mock_handle_message.side_effect = Exception('Broken')
mock_client.send_json.return_value = mock_coro(None)
yield from conn.connect()
# Check that we sent the correct error
assert len(mock_client.send_json.mock_calls) == 1
assert mock_client.send_json.mock_calls[0][1][0] == {
'msgid': 'test-msg-id',
'error': 'exception',
}
@asyncio.coroutine
def test_handler_forwarding():
"""Test we forward messages to correct handler."""
handler = MagicMock()
handler.return_value = mock_coro()
hass = object()
cloud = object()
with patch.dict(iot.HANDLERS, {'test': handler}):
yield from iot.async_handle_message(
hass, cloud, 'test', 'payload')
assert len(handler.mock_calls) == 1
r_hass, r_cloud, payload = handler.mock_calls[0][1]
assert r_hass is hass
assert r_cloud is cloud
assert payload == 'payload'
@asyncio.coroutine
def test_handling_core_messages(hass):
"""Test handling core messages."""
cloud = MagicMock()
cloud.logout.return_value = mock_coro()
yield from iot.async_handle_cloud(hass, cloud, {
'action': 'logout',
'reason': 'Logged in at two places.'
})
assert len(cloud.logout.mock_calls) == 1
@asyncio.coroutine
def test_cloud_getting_disconnected_by_server(mock_client, caplog):
"""Test server disconnecting instance."""
cloud = MagicMock()
conn = iot.CloudIoT(cloud)
mock_client.receive.return_value = mock_coro(MagicMock(
type=WSMsgType.CLOSING,
))
yield from conn.connect()
assert 'Connection closed: Closed by server' in caplog.text
assert 'connect' in str(cloud.hass.async_add_job.mock_calls[-1][1][0])
@asyncio.coroutine
def test_cloud_receiving_bytes(mock_client, caplog):
"""Test server disconnecting instance."""
cloud = MagicMock()
conn = iot.CloudIoT(cloud)
mock_client.receive.return_value = mock_coro(MagicMock(
type=WSMsgType.BINARY,
))
yield from conn.connect()
assert 'Connection closed: Received non-Text message' in caplog.text
assert 'connect' in str(cloud.hass.async_add_job.mock_calls[-1][1][0])
@asyncio.coroutine
def test_cloud_sending_invalid_json(mock_client, caplog):
"""Test cloud sending invalid JSON."""
cloud = MagicMock()
conn = iot.CloudIoT(cloud)
mock_client.receive.return_value = mock_coro(MagicMock(
type=WSMsgType.TEXT,
json=MagicMock(side_effect=ValueError)
))
yield from conn.connect()
assert 'Connection closed: Received invalid JSON.' in caplog.text
assert 'connect' in str(cloud.hass.async_add_job.mock_calls[-1][1][0])
@asyncio.coroutine
def test_cloud_check_token_raising(mock_client, caplog):
"""Test cloud sending invalid JSON."""
cloud = MagicMock()
conn = iot.CloudIoT(cloud)
mock_client.receive.side_effect = auth_api.CloudError
yield from conn.connect()
assert 'Unable to connect: Unable to refresh token.' in caplog.text
assert 'connect' in str(cloud.hass.async_add_job.mock_calls[-1][1][0])
@asyncio.coroutine
def test_cloud_connect_invalid_auth(mock_client, caplog):
"""Test invalid auth detected by server."""
cloud = MagicMock()
conn = iot.CloudIoT(cloud)
mock_client.receive.side_effect = \
client_exceptions.WSServerHandshakeError(None, None, code=401)
yield from conn.connect()
assert 'Connection closed: Invalid auth.' in caplog.text
@asyncio.coroutine
def test_cloud_unable_to_connect(mock_client, caplog):
"""Test unable to connect error."""
cloud = MagicMock()
conn = iot.CloudIoT(cloud)
mock_client.receive.side_effect = client_exceptions.ClientError(None, None)
yield from conn.connect()
assert 'Unable to connect:' in caplog.text
@asyncio.coroutine
def test_cloud_random_exception(mock_client, caplog):
"""Test random exception."""
cloud = MagicMock()
conn = iot.CloudIoT(cloud)
mock_client.receive.side_effect = Exception
yield from conn.connect()
assert 'Unexpected error' in caplog.text