Cloud Updates (#11404)
* Verify stored keys on startup * Handle Google Assistant messages * Fix tests * Don't verify expiration when getting claims * Remove email based check * Lint * Lint * Lint
This commit is contained in:
parent
86e1d0f952
commit
f314b6cb6c
7 changed files with 178 additions and 108 deletions
|
@ -5,13 +5,17 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import async_timeout
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE)
|
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE)
|
||||||
from homeassistant.helpers import entityfilter
|
from homeassistant.helpers import entityfilter
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.components.alexa import smart_home
|
from homeassistant.components.alexa import smart_home as alexa_sh
|
||||||
|
from homeassistant.components.google_assistant import smart_home as ga_sh
|
||||||
|
|
||||||
from . import http_api, iot
|
from . import http_api, iot
|
||||||
from .const import CONFIG_DIR, DOMAIN, SERVERS
|
from .const import CONFIG_DIR, DOMAIN, SERVERS
|
||||||
|
@ -21,7 +25,8 @@ REQUIREMENTS = ['warrant==0.6.1']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_ALEXA = 'alexa'
|
CONF_ALEXA = 'alexa'
|
||||||
CONF_ALEXA_FILTER = 'filter'
|
CONF_GOOGLE_ASSISTANT = 'google_assistant'
|
||||||
|
CONF_FILTER = 'filter'
|
||||||
CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
|
CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
|
||||||
CONF_RELAYER = 'relayer'
|
CONF_RELAYER = 'relayer'
|
||||||
CONF_USER_POOL_ID = 'user_pool_id'
|
CONF_USER_POOL_ID = 'user_pool_id'
|
||||||
|
@ -30,9 +35,9 @@ MODE_DEV = 'development'
|
||||||
DEFAULT_MODE = 'production'
|
DEFAULT_MODE = 'production'
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
|
|
||||||
ALEXA_SCHEMA = vol.Schema({
|
ASSISTANT_SCHEMA = vol.Schema({
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_ALEXA_FILTER,
|
CONF_FILTER,
|
||||||
default=lambda: entityfilter.generate_filter([], [], [], [])
|
default=lambda: entityfilter.generate_filter([], [], [], [])
|
||||||
): entityfilter.FILTER_SCHEMA,
|
): entityfilter.FILTER_SCHEMA,
|
||||||
})
|
})
|
||||||
|
@ -46,7 +51,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||||
vol.Optional(CONF_USER_POOL_ID): str,
|
vol.Optional(CONF_USER_POOL_ID): str,
|
||||||
vol.Optional(CONF_REGION): str,
|
vol.Optional(CONF_REGION): str,
|
||||||
vol.Optional(CONF_RELAYER): str,
|
vol.Optional(CONF_RELAYER): str,
|
||||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA
|
vol.Optional(CONF_ALEXA): ASSISTANT_SCHEMA,
|
||||||
|
vol.Optional(CONF_GOOGLE_ASSISTANT): ASSISTANT_SCHEMA,
|
||||||
}),
|
}),
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
@ -60,17 +66,19 @@ def async_setup(hass, config):
|
||||||
kwargs = {CONF_MODE: DEFAULT_MODE}
|
kwargs = {CONF_MODE: DEFAULT_MODE}
|
||||||
|
|
||||||
if CONF_ALEXA not in kwargs:
|
if CONF_ALEXA not in kwargs:
|
||||||
kwargs[CONF_ALEXA] = ALEXA_SCHEMA({})
|
kwargs[CONF_ALEXA] = ASSISTANT_SCHEMA({})
|
||||||
|
|
||||||
kwargs[CONF_ALEXA] = smart_home.Config(**kwargs[CONF_ALEXA])
|
if CONF_GOOGLE_ASSISTANT not in kwargs:
|
||||||
|
kwargs[CONF_GOOGLE_ASSISTANT] = ASSISTANT_SCHEMA({})
|
||||||
|
|
||||||
|
kwargs[CONF_ALEXA] = alexa_sh.Config(**kwargs[CONF_ALEXA])
|
||||||
|
kwargs['gass_should_expose'] = kwargs.pop(CONF_GOOGLE_ASSISTANT)
|
||||||
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
|
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
|
||||||
|
|
||||||
@asyncio.coroutine
|
success = yield from cloud.initialize()
|
||||||
def init_cloud(event):
|
|
||||||
"""Initialize connection."""
|
|
||||||
yield from cloud.initialize()
|
|
||||||
|
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, init_cloud)
|
if not success:
|
||||||
|
return False
|
||||||
|
|
||||||
yield from http_api.async_setup(hass)
|
yield from http_api.async_setup(hass)
|
||||||
return True
|
return True
|
||||||
|
@ -79,12 +87,16 @@ def async_setup(hass, config):
|
||||||
class Cloud:
|
class Cloud:
|
||||||
"""Store the configuration of the cloud connection."""
|
"""Store the configuration of the cloud connection."""
|
||||||
|
|
||||||
def __init__(self, hass, mode, cognito_client_id=None, user_pool_id=None,
|
def __init__(self, hass, mode, alexa, gass_should_expose,
|
||||||
region=None, relayer=None, alexa=None):
|
cognito_client_id=None, user_pool_id=None, region=None,
|
||||||
|
relayer=None):
|
||||||
"""Create an instance of Cloud."""
|
"""Create an instance of Cloud."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.alexa_config = alexa
|
self.alexa_config = alexa
|
||||||
|
self._gass_should_expose = gass_should_expose
|
||||||
|
self._gass_config = None
|
||||||
|
self.jwt_keyset = None
|
||||||
self.id_token = None
|
self.id_token = None
|
||||||
self.access_token = None
|
self.access_token = None
|
||||||
self.refresh_token = None
|
self.refresh_token = None
|
||||||
|
@ -104,11 +116,6 @@ class Cloud:
|
||||||
self.region = info['region']
|
self.region = info['region']
|
||||||
self.relayer = info['relayer']
|
self.relayer = info['relayer']
|
||||||
|
|
||||||
@property
|
|
||||||
def cognito_email_based(self):
|
|
||||||
"""Return if cognito is email based."""
|
|
||||||
return not self.user_pool_id.endswith('GmV')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_logged_in(self):
|
def is_logged_in(self):
|
||||||
"""Get if cloud is logged in."""
|
"""Get if cloud is logged in."""
|
||||||
|
@ -128,37 +135,37 @@ class Cloud:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def claims(self):
|
def claims(self):
|
||||||
"""Get the claims from the id token."""
|
"""Return the claims from the id token."""
|
||||||
from jose import jwt
|
return self._decode_claims(self.id_token)
|
||||||
return jwt.get_unverified_claims(self.id_token)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user_info_path(self):
|
def user_info_path(self):
|
||||||
"""Get path to the stored auth."""
|
"""Get path to the stored auth."""
|
||||||
return self.path('{}_auth.json'.format(self.mode))
|
return self.path('{}_auth.json'.format(self.mode))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gass_config(self):
|
||||||
|
"""Return the Google Assistant config."""
|
||||||
|
if self._gass_config is None:
|
||||||
|
self._gass_config = ga_sh.Config(
|
||||||
|
should_expose=self._gass_should_expose,
|
||||||
|
agent_user_id=self.claims['cognito:username']
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._gass_config
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
"""Initialize and load cloud info."""
|
"""Initialize and load cloud info."""
|
||||||
def load_config():
|
jwt_success = yield from self._fetch_jwt_keyset()
|
||||||
"""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 not jwt_success:
|
||||||
if os.path.isfile(user_info):
|
return False
|
||||||
with open(user_info, 'rt') as file:
|
|
||||||
info = json.loads(file.read())
|
|
||||||
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)
|
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START,
|
||||||
|
self._start_cloud)
|
||||||
|
|
||||||
if self.id_token is not None:
|
return True
|
||||||
yield from self.iot.connect()
|
|
||||||
|
|
||||||
def path(self, *parts):
|
def path(self, *parts):
|
||||||
"""Get config path inside cloud dir.
|
"""Get config path inside cloud dir.
|
||||||
|
@ -175,6 +182,7 @@ class Cloud:
|
||||||
self.id_token = None
|
self.id_token = None
|
||||||
self.access_token = None
|
self.access_token = None
|
||||||
self.refresh_token = None
|
self.refresh_token = None
|
||||||
|
self._gass_config = None
|
||||||
|
|
||||||
yield from self.hass.async_add_job(
|
yield from self.hass.async_add_job(
|
||||||
lambda: os.remove(self.user_info_path))
|
lambda: os.remove(self.user_info_path))
|
||||||
|
@ -187,3 +195,79 @@ class Cloud:
|
||||||
'access_token': self.access_token,
|
'access_token': self.access_token,
|
||||||
'refresh_token': self.refresh_token,
|
'refresh_token': self.refresh_token,
|
||||||
}, indent=4))
|
}, indent=4))
|
||||||
|
|
||||||
|
def _start_cloud(self, event):
|
||||||
|
"""Start the cloud component."""
|
||||||
|
# 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 not os.path.isfile(user_info):
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(user_info, 'rt') as file:
|
||||||
|
info = json.loads(file.read())
|
||||||
|
|
||||||
|
# Validate tokens
|
||||||
|
try:
|
||||||
|
for token in 'id_token', 'access_token':
|
||||||
|
self._decode_claims(info[token])
|
||||||
|
except ValueError as err: # Raised when token is invalid
|
||||||
|
_LOGGER.warning('Found invalid token %s: %s', token, err)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.id_token = info['id_token']
|
||||||
|
self.access_token = info['access_token']
|
||||||
|
self.refresh_token = info['refresh_token']
|
||||||
|
|
||||||
|
self.hass.add_job(self.iot.connect())
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def _fetch_jwt_keyset(self):
|
||||||
|
"""Fetch the JWT keyset for the Cognito instance."""
|
||||||
|
session = async_get_clientsession(self.hass)
|
||||||
|
url = ("https://cognito-idp.us-east-1.amazonaws.com/"
|
||||||
|
"{}/.well-known/jwks.json".format(self.user_pool_id))
|
||||||
|
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||||
|
req = yield from session.get(url)
|
||||||
|
self.jwt_keyset = yield from req.json()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
|
||||||
|
_LOGGER.error("Error fetching Cognito keyset: %s", err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _decode_claims(self, token):
|
||||||
|
"""Decode the claims in a token."""
|
||||||
|
from jose import jwt, exceptions as jose_exceptions
|
||||||
|
try:
|
||||||
|
header = jwt.get_unverified_header(token)
|
||||||
|
except jose_exceptions.JWTError as err:
|
||||||
|
raise ValueError(str(err)) from None
|
||||||
|
kid = header.get("kid")
|
||||||
|
|
||||||
|
if kid is None:
|
||||||
|
raise ValueError('No kid in header')
|
||||||
|
|
||||||
|
# Locate the key for this kid
|
||||||
|
key = None
|
||||||
|
for key_dict in self.jwt_keyset["keys"]:
|
||||||
|
if key_dict["kid"] == kid:
|
||||||
|
key = key_dict
|
||||||
|
break
|
||||||
|
if not key:
|
||||||
|
raise ValueError(
|
||||||
|
"Unable to locate kid ({}) in keyset".format(kid))
|
||||||
|
|
||||||
|
try:
|
||||||
|
return jwt.decode(
|
||||||
|
token, key, audience=self.cognito_client_id, options={
|
||||||
|
'verify_exp': False,
|
||||||
|
})
|
||||||
|
except jose_exceptions.JWTError as err:
|
||||||
|
raise ValueError(str(err)) from None
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""Package to communicate with the authentication API."""
|
"""Package to communicate with the authentication API."""
|
||||||
import hashlib
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
@ -58,11 +57,6 @@ def _map_aws_exception(err):
|
||||||
return ex(err.response['Error']['Message'])
|
return ex(err.response['Error']['Message'])
|
||||||
|
|
||||||
|
|
||||||
def _generate_username(email):
|
|
||||||
"""Generate a username from an email address."""
|
|
||||||
return hashlib.sha512(email.encode('utf-8')).hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def register(cloud, email, password):
|
def register(cloud, email, password):
|
||||||
"""Register a new account."""
|
"""Register a new account."""
|
||||||
from botocore.exceptions import ClientError
|
from botocore.exceptions import ClientError
|
||||||
|
@ -72,10 +66,7 @@ def register(cloud, email, password):
|
||||||
# https://github.com/capless/warrant/pull/82
|
# https://github.com/capless/warrant/pull/82
|
||||||
cognito.add_base_attributes()
|
cognito.add_base_attributes()
|
||||||
try:
|
try:
|
||||||
if cloud.cognito_email_based:
|
|
||||||
cognito.register(email, password)
|
cognito.register(email, password)
|
||||||
else:
|
|
||||||
cognito.register(_generate_username(email), password)
|
|
||||||
except ClientError as err:
|
except ClientError as err:
|
||||||
raise _map_aws_exception(err)
|
raise _map_aws_exception(err)
|
||||||
|
|
||||||
|
@ -86,11 +77,7 @@ def confirm_register(cloud, confirmation_code, email):
|
||||||
|
|
||||||
cognito = _cognito(cloud)
|
cognito = _cognito(cloud)
|
||||||
try:
|
try:
|
||||||
if cloud.cognito_email_based:
|
|
||||||
cognito.confirm_sign_up(confirmation_code, email)
|
cognito.confirm_sign_up(confirmation_code, email)
|
||||||
else:
|
|
||||||
cognito.confirm_sign_up(confirmation_code,
|
|
||||||
_generate_username(email))
|
|
||||||
except ClientError as err:
|
except ClientError as err:
|
||||||
raise _map_aws_exception(err)
|
raise _map_aws_exception(err)
|
||||||
|
|
||||||
|
@ -114,10 +101,7 @@ def forgot_password(cloud, email):
|
||||||
"""Initiate forgotten password flow."""
|
"""Initiate forgotten password flow."""
|
||||||
from botocore.exceptions import ClientError
|
from botocore.exceptions import ClientError
|
||||||
|
|
||||||
if cloud.cognito_email_based:
|
|
||||||
cognito = _cognito(cloud, username=email)
|
cognito = _cognito(cloud, username=email)
|
||||||
else:
|
|
||||||
cognito = _cognito(cloud, username=_generate_username(email))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cognito.initiate_forgot_password()
|
cognito.initiate_forgot_password()
|
||||||
|
@ -129,10 +113,7 @@ def confirm_forgot_password(cloud, confirmation_code, email, new_password):
|
||||||
"""Confirm forgotten password code and change password."""
|
"""Confirm forgotten password code and change password."""
|
||||||
from botocore.exceptions import ClientError
|
from botocore.exceptions import ClientError
|
||||||
|
|
||||||
if cloud.cognito_email_based:
|
|
||||||
cognito = _cognito(cloud, username=email)
|
cognito = _cognito(cloud, username=email)
|
||||||
else:
|
|
||||||
cognito = _cognito(cloud, username=_generate_username(email))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cognito.confirm_forgot_password(confirmation_code, new_password)
|
cognito.confirm_forgot_password(confirmation_code, new_password)
|
||||||
|
|
|
@ -252,6 +252,6 @@ def _account_data(cloud):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'email': claims['email'],
|
'email': claims['email'],
|
||||||
'sub_exp': claims.get('custom:sub-exp'),
|
'sub_exp': claims['custom:sub-exp'],
|
||||||
'cloud': cloud.iot.state,
|
'cloud': cloud.iot.state,
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,8 @@ import logging
|
||||||
from aiohttp import hdrs, client_exceptions, WSMsgType
|
from aiohttp import hdrs, client_exceptions, WSMsgType
|
||||||
|
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.components.alexa import smart_home
|
from homeassistant.components.alexa import smart_home as alexa
|
||||||
|
from homeassistant.components.google_assistant import smart_home as ga
|
||||||
from homeassistant.util.decorator import Registry
|
from homeassistant.util.decorator import Registry
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from . import auth_api
|
from . import auth_api
|
||||||
|
@ -204,9 +205,18 @@ def async_handle_message(hass, cloud, handler_name, payload):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_handle_alexa(hass, cloud, payload):
|
def async_handle_alexa(hass, cloud, payload):
|
||||||
"""Handle an incoming IoT message for Alexa."""
|
"""Handle an incoming IoT message for Alexa."""
|
||||||
return (yield from smart_home.async_handle_message(hass,
|
result = yield from alexa.async_handle_message(hass, cloud.alexa_config,
|
||||||
cloud.alexa_config,
|
payload)
|
||||||
payload))
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register('google_assistant')
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_handle_google_assistant(hass, cloud, payload):
|
||||||
|
"""Handle an incoming IoT message for Google Assistant."""
|
||||||
|
result = yield from ga.async_handle_message(hass, cloud.gass_config,
|
||||||
|
payload)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register('cloud')
|
@HANDLERS.register('cloud')
|
||||||
|
|
|
@ -78,21 +78,17 @@ def test_login(mock_cognito):
|
||||||
def test_register(mock_cognito):
|
def test_register(mock_cognito):
|
||||||
"""Test registering an account."""
|
"""Test registering an account."""
|
||||||
cloud = MagicMock()
|
cloud = MagicMock()
|
||||||
cloud.cognito_email_based = False
|
|
||||||
cloud = MagicMock()
|
cloud = MagicMock()
|
||||||
cloud.cognito_email_based = False
|
|
||||||
auth_api.register(cloud, 'email@home-assistant.io', 'password')
|
auth_api.register(cloud, 'email@home-assistant.io', 'password')
|
||||||
assert len(mock_cognito.register.mock_calls) == 1
|
assert len(mock_cognito.register.mock_calls) == 1
|
||||||
result_user, result_password = mock_cognito.register.mock_calls[0][1]
|
result_user, result_password = mock_cognito.register.mock_calls[0][1]
|
||||||
assert result_user == \
|
assert result_user == 'email@home-assistant.io'
|
||||||
auth_api._generate_username('email@home-assistant.io')
|
|
||||||
assert result_password == 'password'
|
assert result_password == 'password'
|
||||||
|
|
||||||
|
|
||||||
def test_register_fails(mock_cognito):
|
def test_register_fails(mock_cognito):
|
||||||
"""Test registering an account."""
|
"""Test registering an account."""
|
||||||
cloud = MagicMock()
|
cloud = MagicMock()
|
||||||
cloud.cognito_email_based = False
|
|
||||||
mock_cognito.register.side_effect = aws_error('SomeError')
|
mock_cognito.register.side_effect = aws_error('SomeError')
|
||||||
with pytest.raises(auth_api.CloudError):
|
with pytest.raises(auth_api.CloudError):
|
||||||
auth_api.register(cloud, 'email@home-assistant.io', 'password')
|
auth_api.register(cloud, 'email@home-assistant.io', 'password')
|
||||||
|
@ -101,19 +97,16 @@ def test_register_fails(mock_cognito):
|
||||||
def test_confirm_register(mock_cognito):
|
def test_confirm_register(mock_cognito):
|
||||||
"""Test confirming a registration of an account."""
|
"""Test confirming a registration of an account."""
|
||||||
cloud = MagicMock()
|
cloud = MagicMock()
|
||||||
cloud.cognito_email_based = False
|
|
||||||
auth_api.confirm_register(cloud, '123456', 'email@home-assistant.io')
|
auth_api.confirm_register(cloud, '123456', 'email@home-assistant.io')
|
||||||
assert len(mock_cognito.confirm_sign_up.mock_calls) == 1
|
assert len(mock_cognito.confirm_sign_up.mock_calls) == 1
|
||||||
result_code, result_user = mock_cognito.confirm_sign_up.mock_calls[0][1]
|
result_code, result_user = mock_cognito.confirm_sign_up.mock_calls[0][1]
|
||||||
assert result_user == \
|
assert result_user == 'email@home-assistant.io'
|
||||||
auth_api._generate_username('email@home-assistant.io')
|
|
||||||
assert result_code == '123456'
|
assert result_code == '123456'
|
||||||
|
|
||||||
|
|
||||||
def test_confirm_register_fails(mock_cognito):
|
def test_confirm_register_fails(mock_cognito):
|
||||||
"""Test an error during confirmation of an account."""
|
"""Test an error during confirmation of an account."""
|
||||||
cloud = MagicMock()
|
cloud = MagicMock()
|
||||||
cloud.cognito_email_based = False
|
|
||||||
mock_cognito.confirm_sign_up.side_effect = aws_error('SomeError')
|
mock_cognito.confirm_sign_up.side_effect = aws_error('SomeError')
|
||||||
with pytest.raises(auth_api.CloudError):
|
with pytest.raises(auth_api.CloudError):
|
||||||
auth_api.confirm_register(cloud, '123456', 'email@home-assistant.io')
|
auth_api.confirm_register(cloud, '123456', 'email@home-assistant.io')
|
||||||
|
@ -138,7 +131,6 @@ def test_resend_email_confirm_fails(mock_cognito):
|
||||||
def test_forgot_password(mock_cognito):
|
def test_forgot_password(mock_cognito):
|
||||||
"""Test starting forgot password flow."""
|
"""Test starting forgot password flow."""
|
||||||
cloud = MagicMock()
|
cloud = MagicMock()
|
||||||
cloud.cognito_email_based = False
|
|
||||||
auth_api.forgot_password(cloud, 'email@home-assistant.io')
|
auth_api.forgot_password(cloud, 'email@home-assistant.io')
|
||||||
assert len(mock_cognito.initiate_forgot_password.mock_calls) == 1
|
assert len(mock_cognito.initiate_forgot_password.mock_calls) == 1
|
||||||
|
|
||||||
|
@ -146,7 +138,6 @@ def test_forgot_password(mock_cognito):
|
||||||
def test_forgot_password_fails(mock_cognito):
|
def test_forgot_password_fails(mock_cognito):
|
||||||
"""Test failure when starting forgot password flow."""
|
"""Test failure when starting forgot password flow."""
|
||||||
cloud = MagicMock()
|
cloud = MagicMock()
|
||||||
cloud.cognito_email_based = False
|
|
||||||
mock_cognito.initiate_forgot_password.side_effect = aws_error('SomeError')
|
mock_cognito.initiate_forgot_password.side_effect = aws_error('SomeError')
|
||||||
with pytest.raises(auth_api.CloudError):
|
with pytest.raises(auth_api.CloudError):
|
||||||
auth_api.forgot_password(cloud, 'email@home-assistant.io')
|
auth_api.forgot_password(cloud, 'email@home-assistant.io')
|
||||||
|
@ -155,7 +146,6 @@ def test_forgot_password_fails(mock_cognito):
|
||||||
def test_confirm_forgot_password(mock_cognito):
|
def test_confirm_forgot_password(mock_cognito):
|
||||||
"""Test confirming forgot password."""
|
"""Test confirming forgot password."""
|
||||||
cloud = MagicMock()
|
cloud = MagicMock()
|
||||||
cloud.cognito_email_based = False
|
|
||||||
auth_api.confirm_forgot_password(
|
auth_api.confirm_forgot_password(
|
||||||
cloud, '123456', 'email@home-assistant.io', 'new password')
|
cloud, '123456', 'email@home-assistant.io', 'new password')
|
||||||
assert len(mock_cognito.confirm_forgot_password.mock_calls) == 1
|
assert len(mock_cognito.confirm_forgot_password.mock_calls) == 1
|
||||||
|
@ -168,7 +158,6 @@ def test_confirm_forgot_password(mock_cognito):
|
||||||
def test_confirm_forgot_password_fails(mock_cognito):
|
def test_confirm_forgot_password_fails(mock_cognito):
|
||||||
"""Test failure when confirming forgot password."""
|
"""Test failure when confirming forgot password."""
|
||||||
cloud = MagicMock()
|
cloud = MagicMock()
|
||||||
cloud.cognito_email_based = False
|
|
||||||
mock_cognito.confirm_forgot_password.side_effect = aws_error('SomeError')
|
mock_cognito.confirm_forgot_password.side_effect = aws_error('SomeError')
|
||||||
with pytest.raises(auth_api.CloudError):
|
with pytest.raises(auth_api.CloudError):
|
||||||
auth_api.confirm_forgot_password(
|
auth_api.confirm_forgot_password(
|
||||||
|
|
|
@ -14,7 +14,8 @@ from tests.common import mock_coro
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def cloud_client(hass, test_client):
|
def cloud_client(hass, test_client):
|
||||||
"""Fixture that can fetch from the cloud client."""
|
"""Fixture that can fetch from the cloud client."""
|
||||||
with patch('homeassistant.components.cloud.Cloud.initialize'):
|
with patch('homeassistant.components.cloud.Cloud.initialize',
|
||||||
|
return_value=mock_coro(True)):
|
||||||
hass.loop.run_until_complete(async_setup_component(hass, 'cloud', {
|
hass.loop.run_until_complete(async_setup_component(hass, 'cloud', {
|
||||||
'cloud': {
|
'cloud': {
|
||||||
'mode': 'development',
|
'mode': 'development',
|
||||||
|
@ -24,6 +25,8 @@ def cloud_client(hass, test_client):
|
||||||
'relayer': 'relayer',
|
'relayer': 'relayer',
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
hass.data['cloud']._decode_claims = \
|
||||||
|
lambda token: jwt.get_unverified_claims(token)
|
||||||
with patch('homeassistant.components.cloud.Cloud.write_user_info'):
|
with patch('homeassistant.components.cloud.Cloud.write_user_info'):
|
||||||
yield hass.loop.run_until_complete(test_client(hass.http.app))
|
yield hass.loop.run_until_complete(test_client(hass.http.app))
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import asyncio
|
||||||
import json
|
import json
|
||||||
from unittest.mock import patch, MagicMock, mock_open
|
from unittest.mock import patch, MagicMock, mock_open
|
||||||
|
|
||||||
from jose import jwt
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components import cloud
|
from homeassistant.components import cloud
|
||||||
|
@ -31,7 +30,8 @@ def test_constructor_loads_info_from_constant():
|
||||||
'region': 'test-region',
|
'region': 'test-region',
|
||||||
'relayer': 'test-relayer',
|
'relayer': 'test-relayer',
|
||||||
}
|
}
|
||||||
}):
|
}), patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset',
|
||||||
|
return_value=mock_coro(True)):
|
||||||
result = yield from cloud.async_setup(hass, {
|
result = yield from cloud.async_setup(hass, {
|
||||||
'cloud': {cloud.CONF_MODE: 'beer'}
|
'cloud': {cloud.CONF_MODE: 'beer'}
|
||||||
})
|
})
|
||||||
|
@ -50,6 +50,8 @@ def test_constructor_loads_info_from_config():
|
||||||
"""Test non-dev mode loads info from SERVERS constant."""
|
"""Test non-dev mode loads info from SERVERS constant."""
|
||||||
hass = MagicMock(data={})
|
hass = MagicMock(data={})
|
||||||
|
|
||||||
|
with patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset',
|
||||||
|
return_value=mock_coro(True)):
|
||||||
result = yield from cloud.async_setup(hass, {
|
result = yield from cloud.async_setup(hass, {
|
||||||
'cloud': {
|
'cloud': {
|
||||||
cloud.CONF_MODE: cloud.MODE_DEV,
|
cloud.CONF_MODE: cloud.MODE_DEV,
|
||||||
|
@ -79,12 +81,13 @@ def test_initialize_loads_info(mock_os, hass):
|
||||||
'refresh_token': 'test-refresh-token',
|
'refresh_token': 'test-refresh-token',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
cl = cloud.Cloud(hass, cloud.MODE_DEV)
|
cl = cloud.Cloud(hass, cloud.MODE_DEV, None, None)
|
||||||
cl.iot = MagicMock()
|
cl.iot = MagicMock()
|
||||||
cl.iot.connect.return_value = mock_coro()
|
cl.iot.connect.return_value = mock_coro()
|
||||||
|
|
||||||
with patch('homeassistant.components.cloud.open', mopen, create=True):
|
with patch('homeassistant.components.cloud.open', mopen, create=True), \
|
||||||
yield from cl.initialize()
|
patch('homeassistant.components.cloud.Cloud._decode_claims'):
|
||||||
|
cl._start_cloud(None)
|
||||||
|
|
||||||
assert cl.id_token == 'test-id-token'
|
assert cl.id_token == 'test-id-token'
|
||||||
assert cl.access_token == 'test-access-token'
|
assert cl.access_token == 'test-access-token'
|
||||||
|
@ -95,7 +98,7 @@ def test_initialize_loads_info(mock_os, hass):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def test_logout_clears_info(mock_os, hass):
|
def test_logout_clears_info(mock_os, hass):
|
||||||
"""Test logging out disconnects and removes info."""
|
"""Test logging out disconnects and removes info."""
|
||||||
cl = cloud.Cloud(hass, cloud.MODE_DEV)
|
cl = cloud.Cloud(hass, cloud.MODE_DEV, None, None)
|
||||||
cl.iot = MagicMock()
|
cl.iot = MagicMock()
|
||||||
cl.iot.disconnect.return_value = mock_coro()
|
cl.iot.disconnect.return_value = mock_coro()
|
||||||
|
|
||||||
|
@ -113,7 +116,7 @@ def test_write_user_info():
|
||||||
"""Test writing user info works."""
|
"""Test writing user info works."""
|
||||||
mopen = mock_open()
|
mopen = mock_open()
|
||||||
|
|
||||||
cl = cloud.Cloud(MagicMock(), cloud.MODE_DEV)
|
cl = cloud.Cloud(MagicMock(), cloud.MODE_DEV, None, None)
|
||||||
cl.id_token = 'test-id-token'
|
cl.id_token = 'test-id-token'
|
||||||
cl.access_token = 'test-access-token'
|
cl.access_token = 'test-access-token'
|
||||||
cl.refresh_token = 'test-refresh-token'
|
cl.refresh_token = 'test-refresh-token'
|
||||||
|
@ -135,12 +138,12 @@ def test_write_user_info():
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def test_subscription_expired():
|
def test_subscription_expired():
|
||||||
"""Test subscription being expired."""
|
"""Test subscription being expired."""
|
||||||
cl = cloud.Cloud(None, cloud.MODE_DEV)
|
cl = cloud.Cloud(None, cloud.MODE_DEV, None, None)
|
||||||
cl.id_token = jwt.encode({
|
token_val = {
|
||||||
'custom:sub-exp': '2017-11-13'
|
'custom:sub-exp': '2017-11-13'
|
||||||
}, 'test')
|
}
|
||||||
|
with patch.object(cl, '_decode_claims', return_value=token_val), \
|
||||||
with patch('homeassistant.util.dt.utcnow',
|
patch('homeassistant.util.dt.utcnow',
|
||||||
return_value=utcnow().replace(year=2018)):
|
return_value=utcnow().replace(year=2018)):
|
||||||
assert cl.subscription_expired
|
assert cl.subscription_expired
|
||||||
|
|
||||||
|
@ -148,11 +151,11 @@ def test_subscription_expired():
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def test_subscription_not_expired():
|
def test_subscription_not_expired():
|
||||||
"""Test subscription not being expired."""
|
"""Test subscription not being expired."""
|
||||||
cl = cloud.Cloud(None, cloud.MODE_DEV)
|
cl = cloud.Cloud(None, cloud.MODE_DEV, None, None)
|
||||||
cl.id_token = jwt.encode({
|
token_val = {
|
||||||
'custom:sub-exp': '2017-11-13'
|
'custom:sub-exp': '2017-11-13'
|
||||||
}, 'test')
|
}
|
||||||
|
with patch.object(cl, '_decode_claims', return_value=token_val), \
|
||||||
with patch('homeassistant.util.dt.utcnow',
|
patch('homeassistant.util.dt.utcnow',
|
||||||
return_value=utcnow().replace(year=2017, month=11, day=9)):
|
return_value=utcnow().replace(year=2017, month=11, day=9)):
|
||||||
assert not cl.subscription_expired
|
assert not cl.subscription_expired
|
||||||
|
|
Loading…
Add table
Reference in a new issue