Merge pull request #18811 from home-assistant/rc

0.83.1
This commit is contained in:
Paulus Schoutsen 2018-11-29 23:18:13 +01:00 committed by GitHub
commit 3701c0f219
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 490 additions and 178 deletions

View File

@ -462,10 +462,11 @@ class AuthStore:
for group in self._groups.values(): for group in self._groups.values():
g_dict = { g_dict = {
'id': group.id, 'id': group.id,
# Name not read for sys groups. Kept here for backwards compat
'name': group.name
} # type: Dict[str, Any] } # type: Dict[str, Any]
if group.id not in (GROUP_ID_READ_ONLY, GROUP_ID_ADMIN): if group.id not in (GROUP_ID_READ_ONLY, GROUP_ID_ADMIN):
g_dict['name'] = group.name
g_dict['policy'] = group.policy g_dict['policy'] = group.policy
groups.append(g_dict) groups.append(g_dict)

View File

@ -4,16 +4,19 @@ Support Legacy API password auth provider.
It will be removed when auth system production ready It will be removed when auth system production ready
""" """
import hmac import hmac
from typing import Any, Dict, Optional, cast from typing import Any, Dict, Optional, cast, TYPE_CHECKING
import voluptuous as vol import voluptuous as vol
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401 from homeassistant.core import HomeAssistant, callback
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta from .. import AuthManager
from ..models import Credentials, UserMeta, User
if TYPE_CHECKING:
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401
USER_SCHEMA = vol.Schema({ USER_SCHEMA = vol.Schema({
@ -31,6 +34,24 @@ class InvalidAuthError(HomeAssistantError):
"""Raised when submitting invalid authentication.""" """Raised when submitting invalid authentication."""
async def async_get_user(hass: HomeAssistant) -> User:
"""Return the legacy API password user."""
auth = cast(AuthManager, hass.auth) # type: ignore
found = None
for prv in auth.auth_providers:
if prv.type == 'legacy_api_password':
found = prv
break
if found is None:
raise ValueError('Legacy API password provider not found')
return await auth.async_get_or_create_user(
await found.async_get_or_create_credentials({})
)
@AUTH_PROVIDERS.register('legacy_api_password') @AUTH_PROVIDERS.register('legacy_api_password')
class LegacyApiPasswordAuthProvider(AuthProvider): class LegacyApiPasswordAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords.""" """Example auth provider based on hardcoded usernames and passwords."""

View File

@ -74,7 +74,7 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
@callback @callback
def update(self): def update():
"""Update the state.""" """Update the state."""
self.async_schedule_update_ha_state(True) self.async_schedule_update_ha_state(True)

View File

@ -97,8 +97,8 @@ async def async_setup(hass, yaml_config):
app._on_startup.freeze() app._on_startup.freeze()
await app.startup() await app.startup()
handler = None runner = None
server = None site = None
DescriptionXmlView(config).register(app, app.router) DescriptionXmlView(config).register(app, app.router)
HueUsernameView().register(app, app.router) HueUsernameView().register(app, app.router)
@ -115,25 +115,24 @@ async def async_setup(hass, yaml_config):
async def stop_emulated_hue_bridge(event): async def stop_emulated_hue_bridge(event):
"""Stop the emulated hue bridge.""" """Stop the emulated hue bridge."""
upnp_listener.stop() upnp_listener.stop()
if server: if site:
server.close() await site.stop()
await server.wait_closed() if runner:
await app.shutdown() await runner.cleanup()
if handler:
await handler.shutdown(10)
await app.cleanup()
async def start_emulated_hue_bridge(event): async def start_emulated_hue_bridge(event):
"""Start the emulated hue bridge.""" """Start the emulated hue bridge."""
upnp_listener.start() upnp_listener.start()
nonlocal handler nonlocal site
nonlocal server nonlocal runner
handler = app.make_handler(loop=hass.loop) runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, config.host_ip_addr, config.listen_port)
try: try:
server = await hass.loop.create_server( await site.start()
handler, config.host_ip_addr, config.listen_port)
except OSError as error: except OSError as error:
_LOGGER.error("Failed to create HTTP server at port %d: %s", _LOGGER.error("Failed to create HTTP server at port %d: %s",
config.listen_port, error) config.listen_port, error)

View File

@ -103,29 +103,31 @@ class FibaroController():
"""Handle change report received from the HomeCenter.""" """Handle change report received from the HomeCenter."""
callback_set = set() callback_set = set()
for change in state.get('changes', []): for change in state.get('changes', []):
dev_id = change.pop('id') try:
for property_name, value in change.items(): dev_id = change.pop('id')
if property_name == 'log': if dev_id not in self._device_map.keys():
if value and value != "transfer OK":
_LOGGER.debug("LOG %s: %s",
self._device_map[dev_id].friendly_name,
value)
continue continue
if property_name == 'logTemp': device = self._device_map[dev_id]
continue for property_name, value in change.items():
if property_name in self._device_map[dev_id].properties: if property_name == 'log':
self._device_map[dev_id].properties[property_name] = \ if value and value != "transfer OK":
value _LOGGER.debug("LOG %s: %s",
_LOGGER.debug("<- %s.%s = %s", device.friendly_name, value)
self._device_map[dev_id].ha_id, continue
property_name, if property_name == 'logTemp':
str(value)) continue
else: if property_name in device.properties:
_LOGGER.warning("Error updating %s data of %s, not found", device.properties[property_name] = \
property_name, value
self._device_map[dev_id].ha_id) _LOGGER.debug("<- %s.%s = %s", device.ha_id,
if dev_id in self._callbacks: property_name, str(value))
callback_set.add(dev_id) else:
_LOGGER.warning("%s.%s not found", device.ha_id,
property_name)
if dev_id in self._callbacks:
callback_set.add(dev_id)
except (ValueError, KeyError):
pass
for item in callback_set: for item in callback_set:
self._callbacks[item]() self._callbacks[item]()
@ -137,8 +139,12 @@ class FibaroController():
def _map_device_to_type(device): def _map_device_to_type(device):
"""Map device to HA device type.""" """Map device to HA device type."""
# Use our lookup table to identify device type # Use our lookup table to identify device type
device_type = FIBARO_TYPEMAP.get( if 'type' in device:
device.type, FIBARO_TYPEMAP.get(device.baseType)) device_type = FIBARO_TYPEMAP.get(device.type)
elif 'baseType' in device:
device_type = FIBARO_TYPEMAP.get(device.baseType)
else:
device_type = None
# We can also identify device type by its capabilities # We can also identify device type by its capabilities
if device_type is None: if device_type is None:
@ -156,8 +162,7 @@ class FibaroController():
# Switches that control lights should show up as lights # Switches that control lights should show up as lights
if device_type == 'switch' and \ if device_type == 'switch' and \
'isLight' in device.properties and \ device.properties.get('isLight', 'false') == 'true':
device.properties.isLight == 'true':
device_type = 'light' device_type = 'light'
return device_type return device_type
@ -165,26 +170,31 @@ class FibaroController():
"""Read and process the device list.""" """Read and process the device list."""
devices = self._client.devices.list() devices = self._client.devices.list()
self._device_map = {} self._device_map = {}
for device in devices:
if device.roomID == 0:
room_name = 'Unknown'
else:
room_name = self._room_map[device.roomID].name
device.friendly_name = room_name + ' ' + device.name
device.ha_id = '{}_{}_{}'.format(
slugify(room_name), slugify(device.name), device.id)
self._device_map[device.id] = device
self.fibaro_devices = defaultdict(list) self.fibaro_devices = defaultdict(list)
for device in self._device_map.values(): for device in devices:
if device.enabled and \ try:
(not device.isPlugin or self._import_plugins): if device.roomID == 0:
device.mapped_type = self._map_device_to_type(device) room_name = 'Unknown'
else:
room_name = self._room_map[device.roomID].name
device.friendly_name = room_name + ' ' + device.name
device.ha_id = '{}_{}_{}'.format(
slugify(room_name), slugify(device.name), device.id)
if device.enabled and \
('isPlugin' not in device or
(not device.isPlugin or self._import_plugins)):
device.mapped_type = self._map_device_to_type(device)
else:
device.mapped_type = None
if device.mapped_type: if device.mapped_type:
self._device_map[device.id] = device
self.fibaro_devices[device.mapped_type].append(device) self.fibaro_devices[device.mapped_type].append(device)
else: else:
_LOGGER.debug("%s (%s, %s) not mapped", _LOGGER.debug("%s (%s, %s) not used",
device.ha_id, device.type, device.ha_id, device.type,
device.baseType) device.baseType)
except (KeyError, ValueError):
pass
def setup(hass, config): def setup(hass, config):

View File

@ -712,6 +712,8 @@ class FanSpeedTrait(_Trait):
modes = self.state.attributes.get(fan.ATTR_SPEED_LIST, []) modes = self.state.attributes.get(fan.ATTR_SPEED_LIST, [])
speeds = [] speeds = []
for mode in modes: for mode in modes:
if mode not in self.speed_synonyms:
continue
speed = { speed = {
"speed_name": mode, "speed_name": mode,
"speed_values": [{ "speed_values": [{

View File

@ -207,6 +207,13 @@ async def async_setup(hass, config):
DOMAIN, SERVICE_RELOAD, reload_service_handler, DOMAIN, SERVICE_RELOAD, reload_service_handler,
schema=RELOAD_SERVICE_SCHEMA) schema=RELOAD_SERVICE_SCHEMA)
service_lock = asyncio.Lock()
async def locked_service_handler(service):
"""Handle a service with an async lock."""
async with service_lock:
await groups_service_handler(service)
async def groups_service_handler(service): async def groups_service_handler(service):
"""Handle dynamic group service functions.""" """Handle dynamic group service functions."""
object_id = service.data[ATTR_OBJECT_ID] object_id = service.data[ATTR_OBJECT_ID]
@ -284,7 +291,7 @@ async def async_setup(hass, config):
await component.async_remove_entity(entity_id) await component.async_remove_entity(entity_id)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET, groups_service_handler, DOMAIN, SERVICE_SET, locked_service_handler,
schema=SET_SERVICE_SCHEMA) schema=SET_SERVICE_SCHEMA)
hass.services.async_register( hass.services.async_register(

View File

@ -302,12 +302,6 @@ class HomeAssistantHTTP:
async def start(self): async def start(self):
"""Start the aiohttp server.""" """Start the aiohttp server."""
# We misunderstood the startup signal. You're not allowed to change
# anything during startup. Temp workaround.
# pylint: disable=protected-access
self.app._on_startup.freeze()
await self.app.startup()
if self.ssl_certificate: if self.ssl_certificate:
try: try:
if self.ssl_profile == SSL_INTERMEDIATE: if self.ssl_profile == SSL_INTERMEDIATE:
@ -335,6 +329,7 @@ class HomeAssistantHTTP:
# However in Home Assistant components can be discovered after boot. # However in Home Assistant components can be discovered after boot.
# This will now raise a RunTimeError. # This will now raise a RunTimeError.
# To work around this we now prevent the router from getting frozen # To work around this we now prevent the router from getting frozen
# pylint: disable=protected-access
self.app._router.freeze = lambda: None self.app._router.freeze = lambda: None
self.runner = web.AppRunner(self.app) self.runner = web.AppRunner(self.app)

View File

@ -10,6 +10,7 @@ import jwt
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.const import HTTP_HEADER_HA_AUTH
from homeassistant.auth.providers import legacy_api_password
from homeassistant.auth.util import generate_secret from homeassistant.auth.util import generate_secret
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -78,12 +79,16 @@ def setup_auth(app, trusted_networks, use_auth,
request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))): request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))):
# A valid auth header has been set # A valid auth header has been set
authenticated = True authenticated = True
request['hass_user'] = await legacy_api_password.async_get_user(
app['hass'])
elif (legacy_auth and DATA_API_PASSWORD in request.query and elif (legacy_auth and DATA_API_PASSWORD in request.query and
hmac.compare_digest( hmac.compare_digest(
api_password.encode('utf-8'), api_password.encode('utf-8'),
request.query[DATA_API_PASSWORD].encode('utf-8'))): request.query[DATA_API_PASSWORD].encode('utf-8'))):
authenticated = True authenticated = True
request['hass_user'] = await legacy_api_password.async_get_user(
app['hass'])
elif _is_trusted_ip(request, trusted_networks): elif _is_trusted_ip(request, trusted_networks):
authenticated = True authenticated = True
@ -96,11 +101,7 @@ def setup_auth(app, trusted_networks, use_auth,
request[KEY_AUTHENTICATED] = authenticated request[KEY_AUTHENTICATED] = authenticated
return await handler(request) return await handler(request)
async def auth_startup(app): app.middlewares.append(auth_middleware)
"""Initialize auth middleware when app starts up."""
app.middlewares.append(auth_middleware)
app.on_startup.append(auth_startup)
def _is_trusted_ip(request, trusted_networks): def _is_trusted_ip(request, trusted_networks):

View File

@ -9,7 +9,7 @@ from aiohttp.web import middleware
from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback, HomeAssistant
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -36,13 +36,14 @@ SCHEMA_IP_BAN_ENTRY = vol.Schema({
@callback @callback
def setup_bans(hass, app, login_threshold): def setup_bans(hass, app, login_threshold):
"""Create IP Ban middleware for the app.""" """Create IP Ban middleware for the app."""
app.middlewares.append(ban_middleware)
app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int)
app[KEY_LOGIN_THRESHOLD] = login_threshold
async def ban_startup(app): async def ban_startup(app):
"""Initialize bans when app starts up.""" """Initialize bans when app starts up."""
app.middlewares.append(ban_middleware) app[KEY_BANNED_IPS] = await async_load_ip_bans_config(
app[KEY_BANNED_IPS] = await hass.async_add_job( hass, hass.config.path(IP_BANS_FILE))
load_ip_bans_config, hass.config.path(IP_BANS_FILE))
app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int)
app[KEY_LOGIN_THRESHOLD] = login_threshold
app.on_startup.append(ban_startup) app.on_startup.append(ban_startup)
@ -149,7 +150,7 @@ class IpBan:
self.banned_at = banned_at or datetime.utcnow() self.banned_at = banned_at or datetime.utcnow()
def load_ip_bans_config(path: str): async def async_load_ip_bans_config(hass: HomeAssistant, path: str):
"""Load list of banned IPs from config file.""" """Load list of banned IPs from config file."""
ip_list = [] ip_list = []
@ -157,7 +158,7 @@ def load_ip_bans_config(path: str):
return ip_list return ip_list
try: try:
list_ = load_yaml_config_file(path) list_ = await hass.async_add_executor_job(load_yaml_config_file, path)
except HomeAssistantError as err: except HomeAssistantError as err:
_LOGGER.error('Unable to load %s: %s', path, str(err)) _LOGGER.error('Unable to load %s: %s', path, str(err))
return ip_list return ip_list

View File

@ -33,8 +33,4 @@ def setup_real_ip(app, use_x_forwarded_for, trusted_proxies):
return await handler(request) return await handler(request)
async def app_startup(app): app.middlewares.append(real_ip_middleware)
"""Initialize bans when app starts up."""
app.middlewares.append(real_ip_middleware)
app.on_startup.append(app_startup)

View File

@ -445,6 +445,12 @@ def _exclude_events(events, entities_filter):
domain = event.data.get(ATTR_DOMAIN) domain = event.data.get(ATTR_DOMAIN)
entity_id = event.data.get(ATTR_ENTITY_ID) entity_id = event.data.get(ATTR_ENTITY_ID)
elif event.event_type == EVENT_ALEXA_SMART_HOME:
domain = 'alexa'
elif event.event_type == EVENT_HOMEKIT_CHANGED:
domain = DOMAIN_HOMEKIT
if not entity_id and domain: if not entity_id and domain:
entity_id = "%s." % (domain, ) entity_id = "%s." % (domain, )

View File

@ -40,8 +40,8 @@ class OwnTracksFlow(config_entries.ConfigFlow):
if supports_encryption(): if supports_encryption():
secret_desc = ( secret_desc = (
"The encryption key is {secret} " "The encryption key is {} "
"(on Android under preferences -> advanced)") "(on Android under preferences -> advanced)".format(secret))
else: else:
secret_desc = ( secret_desc = (
"Encryption is not supported because libsodium is not " "Encryption is not supported because libsodium is not "

View File

@ -77,7 +77,7 @@ class RainMachineSensor(RainMachineEntity):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
@callback @callback
def update(self): def update():
"""Update the state.""" """Update the state."""
self.async_schedule_update_ha_state(True) self.async_schedule_update_ha_state(True)

View File

@ -17,7 +17,7 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle, slugify from homeassistant.util import Throttle, slugify
REQUIREMENTS = ['py17track==2.0.2'] REQUIREMENTS = ['py17track==2.1.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_DESTINATION_COUNTRY = 'destination_country' ATTR_DESTINATION_COUNTRY = 'destination_country'

View File

@ -38,12 +38,28 @@ SERVICE_ITEM_SCHEMA = vol.Schema({
}) })
WS_TYPE_SHOPPING_LIST_ITEMS = 'shopping_list/items' WS_TYPE_SHOPPING_LIST_ITEMS = 'shopping_list/items'
WS_TYPE_SHOPPING_LIST_ADD_ITEM = 'shopping_list/items/add'
WS_TYPE_SHOPPING_LIST_UPDATE_ITEM = 'shopping_list/items/update'
SCHEMA_WEBSOCKET_ITEMS = \ SCHEMA_WEBSOCKET_ITEMS = \
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_SHOPPING_LIST_ITEMS vol.Required('type'): WS_TYPE_SHOPPING_LIST_ITEMS
}) })
SCHEMA_WEBSOCKET_ADD_ITEM = \
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_SHOPPING_LIST_ADD_ITEM,
vol.Required('name'): str
})
SCHEMA_WEBSOCKET_UPDATE_ITEM = \
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_SHOPPING_LIST_UPDATE_ITEM,
vol.Required('item_id'): str,
vol.Optional('name'): str,
vol.Optional('complete'): bool
})
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
@ -103,6 +119,14 @@ def async_setup(hass, config):
WS_TYPE_SHOPPING_LIST_ITEMS, WS_TYPE_SHOPPING_LIST_ITEMS,
websocket_handle_items, websocket_handle_items,
SCHEMA_WEBSOCKET_ITEMS) SCHEMA_WEBSOCKET_ITEMS)
hass.components.websocket_api.async_register_command(
WS_TYPE_SHOPPING_LIST_ADD_ITEM,
websocket_handle_add,
SCHEMA_WEBSOCKET_ADD_ITEM)
hass.components.websocket_api.async_register_command(
WS_TYPE_SHOPPING_LIST_UPDATE_ITEM,
websocket_handle_update,
SCHEMA_WEBSOCKET_UPDATE_ITEM)
return True return True
@ -276,3 +300,30 @@ def websocket_handle_items(hass, connection, msg):
"""Handle get shopping_list items.""" """Handle get shopping_list items."""
connection.send_message(websocket_api.result_message( connection.send_message(websocket_api.result_message(
msg['id'], hass.data[DOMAIN].items)) msg['id'], hass.data[DOMAIN].items))
@callback
def websocket_handle_add(hass, connection, msg):
"""Handle add item to shopping_list."""
item = hass.data[DOMAIN].async_add(msg['name'])
hass.bus.async_fire(EVENT)
connection.send_message(websocket_api.result_message(
msg['id'], item))
@websocket_api.async_response
async def websocket_handle_update(hass, connection, msg):
"""Handle update shopping_list item."""
msg_id = msg.pop('id')
item_id = msg.pop('item_id')
msg.pop('type')
data = msg
try:
item = hass.data[DOMAIN].async_update(item_id, data)
hass.bus.async_fire(EVENT)
connection.send_message(websocket_api.result_message(
msg_id, item))
except KeyError:
connection.send_message(websocket_api.error_message(
msg_id, 'item_not_found', 'Item not found'))

View File

@ -2,7 +2,7 @@
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 83 MINOR_VERSION = 83
PATCH_VERSION = '0' PATCH_VERSION = '1'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3) REQUIRED_PYTHON_VER = (3, 5, 3)

View File

@ -804,7 +804,7 @@ py-melissa-climate==2.0.0
py-synology==0.2.0 py-synology==0.2.0
# homeassistant.components.sensor.seventeentrack # homeassistant.components.sensor.seventeentrack
py17track==2.0.2 py17track==2.1.0
# homeassistant.components.hdmi_cec # homeassistant.components.hdmi_cec
pyCEC==0.4.13 pyCEC==0.4.13

View File

@ -199,13 +199,22 @@ async def test_loading_empty_data(hass, hass_storage):
assert len(users) == 0 assert len(users) == 0
async def test_system_groups_only_store_id(hass, hass_storage): async def test_system_groups_store_id_and_name(hass, hass_storage):
"""Test that for system groups we only store the ID.""" """Test that for system groups we store the ID and name.
Name is stored so that we remain backwards compat with < 0.82.
"""
store = auth_store.AuthStore(hass) store = auth_store.AuthStore(hass)
await store._async_load() await store._async_load()
data = store._data_to_save() data = store._data_to_save()
assert len(data['users']) == 0 assert len(data['users']) == 0
assert data['groups'] == [ assert data['groups'] == [
{'id': auth_store.GROUP_ID_ADMIN}, {
{'id': auth_store.GROUP_ID_READ_ONLY}, 'id': auth_store.GROUP_ID_ADMIN,
'name': auth_store.GROUP_NAME_ADMIN,
},
{
'id': auth_store.GROUP_ID_READ_ONLY,
'name': auth_store.GROUP_NAME_READ_ONLY,
},
] ]

View File

@ -23,7 +23,7 @@ NPR_NEWS_MP3_URL = "https://pd.npr.org/anon.npr-mp3/npr/news/newscast.mp3"
@pytest.fixture @pytest.fixture
def alexa_client(loop, hass, aiohttp_client): def alexa_client(loop, hass, hass_client):
"""Initialize a Home Assistant server for testing this module.""" """Initialize a Home Assistant server for testing this module."""
@callback @callback
def mock_service(call): def mock_service(call):
@ -95,7 +95,7 @@ def alexa_client(loop, hass, aiohttp_client):
}, },
} }
})) }))
return loop.run_until_complete(aiohttp_client(hass.http.app)) return loop.run_until_complete(hass_client())
def _intent_req(client, data=None): def _intent_req(client, data=None):

View File

@ -1437,10 +1437,10 @@ async def test_unsupported_domain(hass):
assert not msg['payload']['endpoints'] assert not msg['payload']['endpoints']
async def do_http_discovery(config, hass, aiohttp_client): async def do_http_discovery(config, hass, hass_client):
"""Submit a request to the Smart Home HTTP API.""" """Submit a request to the Smart Home HTTP API."""
await async_setup_component(hass, alexa.DOMAIN, config) await async_setup_component(hass, alexa.DOMAIN, config)
http_client = await aiohttp_client(hass.http.app) http_client = await hass_client()
request = get_new_request('Alexa.Discovery', 'Discover') request = get_new_request('Alexa.Discovery', 'Discover')
response = await http_client.post( response = await http_client.post(
@ -1450,7 +1450,7 @@ async def do_http_discovery(config, hass, aiohttp_client):
return response return response
async def test_http_api(hass, aiohttp_client): async def test_http_api(hass, hass_client):
"""With `smart_home:` HTTP API is exposed.""" """With `smart_home:` HTTP API is exposed."""
config = { config = {
'alexa': { 'alexa': {
@ -1458,7 +1458,7 @@ async def test_http_api(hass, aiohttp_client):
} }
} }
response = await do_http_discovery(config, hass, aiohttp_client) response = await do_http_discovery(config, hass, hass_client)
response_data = await response.json() response_data = await response.json()
# Here we're testing just the HTTP view glue -- details of discovery are # Here we're testing just the HTTP view glue -- details of discovery are
@ -1466,12 +1466,12 @@ async def test_http_api(hass, aiohttp_client):
assert response_data['event']['header']['name'] == 'Discover.Response' assert response_data['event']['header']['name'] == 'Discover.Response'
async def test_http_api_disabled(hass, aiohttp_client): async def test_http_api_disabled(hass, hass_client):
"""Without `smart_home:`, the HTTP API is disabled.""" """Without `smart_home:`, the HTTP API is disabled."""
config = { config = {
'alexa': {} 'alexa': {}
} }
response = await do_http_discovery(config, hass, aiohttp_client) response = await do_http_discovery(config, hass, hass_client)
assert response.status == 404 assert response.status == 404

View File

@ -4,12 +4,21 @@ from unittest.mock import patch
import pytest import pytest
from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
from homeassistant.auth.providers import legacy_api_password, homeassistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.components.websocket_api.http import URL from homeassistant.components.websocket_api.http import URL
from homeassistant.components.websocket_api.auth import ( from homeassistant.components.websocket_api.auth import (
TYPE_AUTH, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED) TYPE_AUTH, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED)
from tests.common import MockUser, CLIENT_ID from tests.common import MockUser, CLIENT_ID, mock_coro
@pytest.fixture(autouse=True)
def prevent_io():
"""Fixture to prevent certain I/O from happening."""
with patch('homeassistant.components.http.ban.async_load_ip_bans_config',
side_effect=lambda *args: mock_coro([])):
yield
@pytest.fixture @pytest.fixture
@ -80,7 +89,7 @@ def hass_access_token(hass, hass_admin_user):
@pytest.fixture @pytest.fixture
def hass_admin_user(hass): def hass_admin_user(hass, local_auth):
"""Return a Home Assistant admin user.""" """Return a Home Assistant admin user."""
admin_group = hass.loop.run_until_complete(hass.auth.async_get_group( admin_group = hass.loop.run_until_complete(hass.auth.async_get_group(
GROUP_ID_ADMIN)) GROUP_ID_ADMIN))
@ -88,8 +97,42 @@ def hass_admin_user(hass):
@pytest.fixture @pytest.fixture
def hass_read_only_user(hass): def hass_read_only_user(hass, local_auth):
"""Return a Home Assistant read only user.""" """Return a Home Assistant read only user."""
read_only_group = hass.loop.run_until_complete(hass.auth.async_get_group( read_only_group = hass.loop.run_until_complete(hass.auth.async_get_group(
GROUP_ID_READ_ONLY)) GROUP_ID_READ_ONLY))
return MockUser(groups=[read_only_group]).add_to_hass(hass) return MockUser(groups=[read_only_group]).add_to_hass(hass)
@pytest.fixture
def legacy_auth(hass):
"""Load legacy API password provider."""
prv = legacy_api_password.LegacyApiPasswordAuthProvider(
hass, hass.auth._store, {
'type': 'legacy_api_password'
}
)
hass.auth._providers[(prv.type, prv.id)] = prv
@pytest.fixture
def local_auth(hass):
"""Load local auth provider."""
prv = homeassistant.HassAuthProvider(
hass, hass.auth._store, {
'type': 'homeassistant'
}
)
hass.auth._providers[(prv.type, prv.id)] = prv
@pytest.fixture
def hass_client(hass, aiohttp_client, hass_access_token):
"""Return an authenticated HTTP client."""
async def auth_client():
"""Return an authenticated client."""
return await aiohttp_client(hass.http.app, headers={
'Authorization': "Bearer {}".format(hass_access_token)
})
return auth_client

View File

@ -27,7 +27,7 @@ def hassio_env():
@pytest.fixture @pytest.fixture
def hassio_client(hassio_env, hass, aiohttp_client): def hassio_client(hassio_env, hass, aiohttp_client, legacy_auth):
"""Create mock hassio http client.""" """Create mock hassio http client."""
with patch('homeassistant.components.hassio.HassIO.update_hass_api', with patch('homeassistant.components.hassio.HassIO.update_hass_api',
Mock(return_value=mock_coro({"result": "ok"}))), \ Mock(return_value=mock_coro({"result": "ok"}))), \

View File

@ -83,7 +83,8 @@ async def test_access_without_password(app, aiohttp_client):
assert resp.status == 200 assert resp.status == 200
async def test_access_with_password_in_header(app, aiohttp_client): async def test_access_with_password_in_header(app, aiohttp_client,
legacy_auth):
"""Test access with password in header.""" """Test access with password in header."""
setup_auth(app, [], False, api_password=API_PASSWORD) setup_auth(app, [], False, api_password=API_PASSWORD)
client = await aiohttp_client(app) client = await aiohttp_client(app)
@ -97,7 +98,7 @@ async def test_access_with_password_in_header(app, aiohttp_client):
assert req.status == 401 assert req.status == 401
async def test_access_with_password_in_query(app, aiohttp_client): async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth):
"""Test access with password in URL.""" """Test access with password in URL."""
setup_auth(app, [], False, api_password=API_PASSWORD) setup_auth(app, [], False, api_password=API_PASSWORD)
client = await aiohttp_client(app) client = await aiohttp_client(app)
@ -219,7 +220,8 @@ async def test_auth_active_access_with_trusted_ip(app2, aiohttp_client):
"{} should be trusted".format(remote_addr) "{} should be trusted".format(remote_addr)
async def test_auth_active_blocked_api_password_access(app, aiohttp_client): async def test_auth_active_blocked_api_password_access(
app, aiohttp_client, legacy_auth):
"""Test access using api_password should be blocked when auth.active.""" """Test access using api_password should be blocked when auth.active."""
setup_auth(app, [], True, api_password=API_PASSWORD) setup_auth(app, [], True, api_password=API_PASSWORD)
client = await aiohttp_client(app) client = await aiohttp_client(app)
@ -239,7 +241,8 @@ async def test_auth_active_blocked_api_password_access(app, aiohttp_client):
assert req.status == 401 assert req.status == 401
async def test_auth_legacy_support_api_password_access(app, aiohttp_client): async def test_auth_legacy_support_api_password_access(
app, aiohttp_client, legacy_auth):
"""Test access using api_password if auth.support_legacy.""" """Test access using api_password if auth.support_legacy."""
setup_auth(app, [], True, support_legacy=True, api_password=API_PASSWORD) setup_auth(app, [], True, support_legacy=True, api_password=API_PASSWORD)
client = await aiohttp_client(app) client = await aiohttp_client(app)

View File

@ -16,6 +16,9 @@ from homeassistant.components.http.ban import (
from . import mock_real_ip from . import mock_real_ip
from tests.common import mock_coro
BANNED_IPS = ['200.201.202.203', '100.64.0.2'] BANNED_IPS = ['200.201.202.203', '100.64.0.2']
@ -25,9 +28,9 @@ async def test_access_from_banned_ip(hass, aiohttp_client):
setup_bans(hass, app, 5) setup_bans(hass, app, 5)
set_real_ip = mock_real_ip(app) set_real_ip = mock_real_ip(app)
with patch('homeassistant.components.http.ban.load_ip_bans_config', with patch('homeassistant.components.http.ban.async_load_ip_bans_config',
return_value=[IpBan(banned_ip) for banned_ip return_value=mock_coro([IpBan(banned_ip) for banned_ip
in BANNED_IPS]): in BANNED_IPS])):
client = await aiohttp_client(app) client = await aiohttp_client(app)
for remote_addr in BANNED_IPS: for remote_addr in BANNED_IPS:
@ -71,9 +74,9 @@ async def test_ip_bans_file_creation(hass, aiohttp_client):
setup_bans(hass, app, 1) setup_bans(hass, app, 1)
mock_real_ip(app)("200.201.202.204") mock_real_ip(app)("200.201.202.204")
with patch('homeassistant.components.http.ban.load_ip_bans_config', with patch('homeassistant.components.http.ban.async_load_ip_bans_config',
return_value=[IpBan(banned_ip) for banned_ip return_value=mock_coro([IpBan(banned_ip) for banned_ip
in BANNED_IPS]): in BANNED_IPS])):
client = await aiohttp_client(app) client = await aiohttp_client(app)
m = mock_open() m = mock_open()

View File

@ -124,7 +124,7 @@ async def test_api_no_base_url(hass):
assert hass.config.api.base_url == 'http://127.0.0.1:8123' assert hass.config.api.base_url == 'http://127.0.0.1:8123'
async def test_not_log_password(hass, aiohttp_client, caplog): async def test_not_log_password(hass, aiohttp_client, caplog, legacy_auth):
"""Test access with password doesn't get logged.""" """Test access with password doesn't get logged."""
assert await async_setup_component(hass, 'api', { assert await async_setup_component(hass, 'api', {
'http': { 'http': {

View File

@ -16,12 +16,10 @@ from tests.common import async_mock_service
@pytest.fixture @pytest.fixture
def mock_api_client(hass, aiohttp_client, hass_access_token): def mock_api_client(hass, hass_client):
"""Start the Hass HTTP component and return admin API client.""" """Start the Hass HTTP component and return admin API client."""
hass.loop.run_until_complete(async_setup_component(hass, 'api', {})) hass.loop.run_until_complete(async_setup_component(hass, 'api', {}))
return hass.loop.run_until_complete(aiohttp_client(hass.http.app, headers={ return hass.loop.run_until_complete(hass_client())
'Authorization': 'Bearer {}'.format(hass_access_token)
}))
@asyncio.coroutine @asyncio.coroutine
@ -408,7 +406,7 @@ def _listen_count(hass):
async def test_api_error_log(hass, aiohttp_client, hass_access_token, async def test_api_error_log(hass, aiohttp_client, hass_access_token,
hass_admin_user): hass_admin_user, legacy_auth):
"""Test if we can fetch the error log.""" """Test if we can fetch the error log."""
hass.data[DATA_LOGGING] = '/some/path' hass.data[DATA_LOGGING] = '/some/path'
await async_setup_component(hass, 'api', { await async_setup_component(hass, 'api', {
@ -566,5 +564,17 @@ async def test_rendering_template_admin(hass, mock_api_client,
hass_admin_user): hass_admin_user):
"""Test rendering a template requires admin.""" """Test rendering a template requires admin."""
hass_admin_user.groups = [] hass_admin_user.groups = []
resp = await mock_api_client.post('/api/template') resp = await mock_api_client.post(const.URL_API_TEMPLATE)
assert resp.status == 401
async def test_rendering_template_legacy_user(
hass, mock_api_client, aiohttp_client, legacy_auth):
"""Test rendering a template with legacy API password."""
hass.states.async_set('sensor.temperature', 10)
client = await aiohttp_client(hass.http.app)
resp = await client.post(
const.URL_API_TEMPLATE,
json={"template": '{{ states.sensor.temperature.state }}'}
)
assert resp.status == 401 assert resp.status == 401

View File

@ -90,7 +90,7 @@ async def test_register_before_setup(hass):
assert intent.text_input == 'I would like the Grolsch beer' assert intent.text_input == 'I would like the Grolsch beer'
async def test_http_processing_intent(hass, aiohttp_client): async def test_http_processing_intent(hass, hass_client):
"""Test processing intent via HTTP API.""" """Test processing intent via HTTP API."""
class TestIntentHandler(intent.IntentHandler): class TestIntentHandler(intent.IntentHandler):
"""Test Intent Handler.""" """Test Intent Handler."""
@ -120,7 +120,7 @@ async def test_http_processing_intent(hass, aiohttp_client):
}) })
assert result assert result
client = await aiohttp_client(hass.http.app) client = await hass_client()
resp = await client.post('/api/conversation/process', json={ resp = await client.post('/api/conversation/process', json={
'text': 'I would like the Grolsch beer' 'text': 'I would like the Grolsch beer'
}) })
@ -244,7 +244,7 @@ async def test_toggle_intent(hass, sentence):
assert call.data == {'entity_id': 'light.kitchen'} assert call.data == {'entity_id': 'light.kitchen'}
async def test_http_api(hass, aiohttp_client): async def test_http_api(hass, hass_client):
"""Test the HTTP conversation API.""" """Test the HTTP conversation API."""
result = await component.async_setup(hass, {}) result = await component.async_setup(hass, {})
assert result assert result
@ -252,7 +252,7 @@ async def test_http_api(hass, aiohttp_client):
result = await async_setup_component(hass, 'conversation', {}) result = await async_setup_component(hass, 'conversation', {})
assert result assert result
client = await aiohttp_client(hass.http.app) client = await hass_client()
hass.states.async_set('light.kitchen', 'off') hass.states.async_set('light.kitchen', 'off')
calls = async_mock_service(hass, HASS_DOMAIN, 'turn_on') calls = async_mock_service(hass, HASS_DOMAIN, 'turn_on')
@ -268,7 +268,7 @@ async def test_http_api(hass, aiohttp_client):
assert call.data == {'entity_id': 'light.kitchen'} assert call.data == {'entity_id': 'light.kitchen'}
async def test_http_api_wrong_data(hass, aiohttp_client): async def test_http_api_wrong_data(hass, hass_client):
"""Test the HTTP conversation API.""" """Test the HTTP conversation API."""
result = await component.async_setup(hass, {}) result = await component.async_setup(hass, {})
assert result assert result
@ -276,7 +276,7 @@ async def test_http_api_wrong_data(hass, aiohttp_client):
result = await async_setup_component(hass, 'conversation', {}) result = await async_setup_component(hass, 'conversation', {})
assert result assert result
client = await aiohttp_client(hass.http.app) client = await hass_client()
resp = await client.post('/api/conversation/process', json={ resp = await client.post('/api/conversation/process', json={
'text': 123 'text': 123

View File

@ -515,13 +515,13 @@ class TestComponentHistory(unittest.TestCase):
return zero, four, states return zero, four, states
async def test_fetch_period_api(hass, aiohttp_client): async def test_fetch_period_api(hass, hass_client):
"""Test the fetch period view for history.""" """Test the fetch period view for history."""
await hass.async_add_job(init_recorder_component, hass) await hass.async_add_job(init_recorder_component, hass)
await async_setup_component(hass, 'history', {}) await async_setup_component(hass, 'history', {})
await hass.components.recorder.wait_connection_ready() await hass.components.recorder.wait_connection_ready()
await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
client = await aiohttp_client(hass.http.app) client = await hass_client()
response = await client.get( response = await client.get(
'/api/history/period/{}'.format(dt_util.utcnow().isoformat())) '/api/history/period/{}'.format(dt_util.utcnow().isoformat()))
assert response.status == 200 assert response.status == 200

View File

@ -242,9 +242,11 @@ class TestComponentLogbook(unittest.TestCase):
config = logbook.CONFIG_SCHEMA({ config = logbook.CONFIG_SCHEMA({
ha.DOMAIN: {}, ha.DOMAIN: {},
logbook.DOMAIN: {logbook.CONF_EXCLUDE: { logbook.DOMAIN: {logbook.CONF_EXCLUDE: {
logbook.CONF_DOMAINS: ['switch', ]}}}) logbook.CONF_DOMAINS: ['switch', 'alexa', DOMAIN_HOMEKIT]}}})
events = logbook._exclude_events( events = logbook._exclude_events(
(ha.Event(EVENT_HOMEASSISTANT_START), eventA, eventB), (ha.Event(EVENT_HOMEASSISTANT_START),
ha.Event(EVENT_ALEXA_SMART_HOME),
ha.Event(EVENT_HOMEKIT_CHANGED), eventA, eventB),
logbook._generate_filter_from_config(config[logbook.DOMAIN])) logbook._generate_filter_from_config(config[logbook.DOMAIN]))
entries = list(logbook.humanify(self.hass, events)) entries = list(logbook.humanify(self.hass, events))
@ -325,22 +327,35 @@ class TestComponentLogbook(unittest.TestCase):
pointA = dt_util.utcnow() pointA = dt_util.utcnow()
pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES) pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
event_alexa = ha.Event(EVENT_ALEXA_SMART_HOME, {'request': {
'namespace': 'Alexa.Discovery',
'name': 'Discover',
}})
event_homekit = ha.Event(EVENT_HOMEKIT_CHANGED, {
ATTR_ENTITY_ID: 'lock.front_door',
ATTR_DISPLAY_NAME: 'Front Door',
ATTR_SERVICE: 'lock',
})
eventA = self.create_state_changed_event(pointA, entity_id, 10) eventA = self.create_state_changed_event(pointA, entity_id, 10)
eventB = self.create_state_changed_event(pointB, entity_id2, 20) eventB = self.create_state_changed_event(pointB, entity_id2, 20)
config = logbook.CONFIG_SCHEMA({ config = logbook.CONFIG_SCHEMA({
ha.DOMAIN: {}, ha.DOMAIN: {},
logbook.DOMAIN: {logbook.CONF_INCLUDE: { logbook.DOMAIN: {logbook.CONF_INCLUDE: {
logbook.CONF_DOMAINS: ['sensor', ]}}}) logbook.CONF_DOMAINS: ['sensor', 'alexa', DOMAIN_HOMEKIT]}}})
events = logbook._exclude_events( events = logbook._exclude_events(
(ha.Event(EVENT_HOMEASSISTANT_START), eventA, eventB), (ha.Event(EVENT_HOMEASSISTANT_START),
event_alexa, event_homekit, eventA, eventB),
logbook._generate_filter_from_config(config[logbook.DOMAIN])) logbook._generate_filter_from_config(config[logbook.DOMAIN]))
entries = list(logbook.humanify(self.hass, events)) entries = list(logbook.humanify(self.hass, events))
assert 2 == len(entries) assert 4 == len(entries)
self.assert_entry(entries[0], name='Home Assistant', message='started', self.assert_entry(entries[0], name='Home Assistant', message='started',
domain=ha.DOMAIN) domain=ha.DOMAIN)
self.assert_entry(entries[1], pointB, 'blu', domain='sensor', self.assert_entry(entries[1], name='Amazon Alexa', domain='alexa')
self.assert_entry(entries[2], name='HomeKit', domain=DOMAIN_HOMEKIT)
self.assert_entry(entries[3], pointB, 'blu', domain='sensor',
entity_id=entity_id2) entity_id=entity_id2)
def test_include_exclude_events(self): def test_include_exclude_events(self):

View File

@ -55,7 +55,7 @@ def test_recent_items_intent(hass):
@asyncio.coroutine @asyncio.coroutine
def test_deprecated_api_get_all(hass, aiohttp_client): def test_deprecated_api_get_all(hass, hass_client):
"""Test the API.""" """Test the API."""
yield from async_setup_component(hass, 'shopping_list', {}) yield from async_setup_component(hass, 'shopping_list', {})
@ -66,7 +66,7 @@ def test_deprecated_api_get_all(hass, aiohttp_client):
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'wine'}} hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'wine'}}
) )
client = yield from aiohttp_client(hass.http.app) client = yield from hass_client()
resp = yield from client.get('/api/shopping_list') resp = yield from client.get('/api/shopping_list')
assert resp.status == 200 assert resp.status == 200
@ -110,7 +110,7 @@ async def test_ws_get_items(hass, hass_ws_client):
@asyncio.coroutine @asyncio.coroutine
def test_api_update(hass, aiohttp_client): def test_deprecated_api_update(hass, hass_client):
"""Test the API.""" """Test the API."""
yield from async_setup_component(hass, 'shopping_list', {}) yield from async_setup_component(hass, 'shopping_list', {})
@ -124,7 +124,7 @@ def test_api_update(hass, aiohttp_client):
beer_id = hass.data['shopping_list'].items[0]['id'] beer_id = hass.data['shopping_list'].items[0]['id']
wine_id = hass.data['shopping_list'].items[1]['id'] wine_id = hass.data['shopping_list'].items[1]['id']
client = yield from aiohttp_client(hass.http.app) client = yield from hass_client()
resp = yield from client.post( resp = yield from client.post(
'/api/shopping_list/item/{}'.format(beer_id), json={ '/api/shopping_list/item/{}'.format(beer_id), json={
'name': 'soda' 'name': 'soda'
@ -164,8 +164,63 @@ def test_api_update(hass, aiohttp_client):
} }
async def test_ws_update_item(hass, hass_ws_client):
"""Test update shopping_list item websocket command."""
await async_setup_component(hass, 'shopping_list', {})
await intent.async_handle(
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'beer'}}
)
await intent.async_handle(
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'wine'}}
)
beer_id = hass.data['shopping_list'].items[0]['id']
wine_id = hass.data['shopping_list'].items[1]['id']
client = await hass_ws_client(hass)
await client.send_json({
'id': 5,
'type': 'shopping_list/items/update',
'item_id': beer_id,
'name': 'soda'
})
msg = await client.receive_json()
assert msg['success'] is True
data = msg['result']
assert data == {
'id': beer_id,
'name': 'soda',
'complete': False
}
await client.send_json({
'id': 6,
'type': 'shopping_list/items/update',
'item_id': wine_id,
'complete': True
})
msg = await client.receive_json()
assert msg['success'] is True
data = msg['result']
assert data == {
'id': wine_id,
'name': 'wine',
'complete': True
}
beer, wine = hass.data['shopping_list'].items
assert beer == {
'id': beer_id,
'name': 'soda',
'complete': False
}
assert wine == {
'id': wine_id,
'name': 'wine',
'complete': True
}
@asyncio.coroutine @asyncio.coroutine
def test_api_update_fails(hass, aiohttp_client): def test_api_update_fails(hass, hass_client):
"""Test the API.""" """Test the API."""
yield from async_setup_component(hass, 'shopping_list', {}) yield from async_setup_component(hass, 'shopping_list', {})
@ -173,7 +228,7 @@ def test_api_update_fails(hass, aiohttp_client):
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'beer'}} hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'beer'}}
) )
client = yield from aiohttp_client(hass.http.app) client = yield from hass_client()
resp = yield from client.post( resp = yield from client.post(
'/api/shopping_list/non_existing', json={ '/api/shopping_list/non_existing', json={
'name': 'soda' 'name': 'soda'
@ -190,8 +245,37 @@ def test_api_update_fails(hass, aiohttp_client):
assert resp.status == 400 assert resp.status == 400
async def test_ws_update_item_fail(hass, hass_ws_client):
"""Test failure of update shopping_list item websocket command."""
await async_setup_component(hass, 'shopping_list', {})
await intent.async_handle(
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'beer'}}
)
client = await hass_ws_client(hass)
await client.send_json({
'id': 5,
'type': 'shopping_list/items/update',
'item_id': 'non_existing',
'name': 'soda'
})
msg = await client.receive_json()
assert msg['success'] is False
data = msg['error']
assert data == {
'code': 'item_not_found',
'message': 'Item not found'
}
await client.send_json({
'id': 6,
'type': 'shopping_list/items/update',
'name': 123,
})
msg = await client.receive_json()
assert msg['success'] is False
@asyncio.coroutine @asyncio.coroutine
def test_api_clear_completed(hass, aiohttp_client): def test_api_clear_completed(hass, hass_client):
"""Test the API.""" """Test the API."""
yield from async_setup_component(hass, 'shopping_list', {}) yield from async_setup_component(hass, 'shopping_list', {})
@ -205,7 +289,7 @@ def test_api_clear_completed(hass, aiohttp_client):
beer_id = hass.data['shopping_list'].items[0]['id'] beer_id = hass.data['shopping_list'].items[0]['id']
wine_id = hass.data['shopping_list'].items[1]['id'] wine_id = hass.data['shopping_list'].items[1]['id']
client = yield from aiohttp_client(hass.http.app) client = yield from hass_client()
# Mark beer as completed # Mark beer as completed
resp = yield from client.post( resp = yield from client.post(
@ -228,11 +312,11 @@ def test_api_clear_completed(hass, aiohttp_client):
@asyncio.coroutine @asyncio.coroutine
def test_api_create(hass, aiohttp_client): def test_deprecated_api_create(hass, hass_client):
"""Test the API.""" """Test the API."""
yield from async_setup_component(hass, 'shopping_list', {}) yield from async_setup_component(hass, 'shopping_list', {})
client = yield from aiohttp_client(hass.http.app) client = yield from hass_client()
resp = yield from client.post('/api/shopping_list/item', json={ resp = yield from client.post('/api/shopping_list/item', json={
'name': 'soda' 'name': 'soda'
}) })
@ -249,14 +333,48 @@ def test_api_create(hass, aiohttp_client):
@asyncio.coroutine @asyncio.coroutine
def test_api_create_fail(hass, aiohttp_client): def test_deprecated_api_create_fail(hass, hass_client):
"""Test the API.""" """Test the API."""
yield from async_setup_component(hass, 'shopping_list', {}) yield from async_setup_component(hass, 'shopping_list', {})
client = yield from aiohttp_client(hass.http.app) client = yield from hass_client()
resp = yield from client.post('/api/shopping_list/item', json={ resp = yield from client.post('/api/shopping_list/item', json={
'name': 1234 'name': 1234
}) })
assert resp.status == 400 assert resp.status == 400
assert len(hass.data['shopping_list'].items) == 0 assert len(hass.data['shopping_list'].items) == 0
async def test_ws_add_item(hass, hass_ws_client):
"""Test adding shopping_list item websocket command."""
await async_setup_component(hass, 'shopping_list', {})
client = await hass_ws_client(hass)
await client.send_json({
'id': 5,
'type': 'shopping_list/items/add',
'name': 'soda',
})
msg = await client.receive_json()
assert msg['success'] is True
data = msg['result']
assert data['name'] == 'soda'
assert data['complete'] is False
items = hass.data['shopping_list'].items
assert len(items) == 1
assert items[0]['name'] == 'soda'
assert items[0]['complete'] is False
async def test_ws_add_item_fail(hass, hass_ws_client):
"""Test adding shopping_list item failure websocket command."""
await async_setup_component(hass, 'shopping_list', {})
client = await hass_ws_client(hass)
await client.send_json({
'id': 5,
'type': 'shopping_list/items/add',
'name': 123,
})
msg = await client.receive_json()
assert msg['success'] is False
assert len(hass.data['shopping_list'].items) == 0

View File

@ -56,7 +56,7 @@ SENSOR_OUTPUT = {
@pytest.fixture @pytest.fixture
def mock_client(hass, aiohttp_client): def mock_client(hass, hass_client):
"""Start the Home Assistant HTTP component.""" """Start the Home Assistant HTTP component."""
with patch('homeassistant.components.spaceapi', with patch('homeassistant.components.spaceapi',
return_value=mock_coro(True)): return_value=mock_coro(True)):
@ -70,7 +70,7 @@ def mock_client(hass, aiohttp_client):
hass.states.async_set('test.hum1', 88, hass.states.async_set('test.hum1', 88,
attributes={'unit_of_measurement': '%'}) attributes={'unit_of_measurement': '%'})
return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) return hass.loop.run_until_complete(hass_client())
async def test_spaceapi_get(hass, mock_client): async def test_spaceapi_get(hass, mock_client):

View File

@ -14,9 +14,9 @@ BASIC_CONFIG = {
} }
async def get_error_log(hass, aiohttp_client, expected_count): async def get_error_log(hass, hass_client, expected_count):
"""Fetch all entries from system_log via the API.""" """Fetch all entries from system_log via the API."""
client = await aiohttp_client(hass.http.app) client = await hass_client()
resp = await client.get('/api/error/all') resp = await client.get('/api/error/all')
assert resp.status == 200 assert resp.status == 200
@ -45,37 +45,37 @@ def get_frame(name):
return (name, None, None, None) return (name, None, None, None)
async def test_normal_logs(hass, aiohttp_client): async def test_normal_logs(hass, hass_client):
"""Test that debug and info are not logged.""" """Test that debug and info are not logged."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.debug('debug') _LOGGER.debug('debug')
_LOGGER.info('info') _LOGGER.info('info')
# Assert done by get_error_log # Assert done by get_error_log
await get_error_log(hass, aiohttp_client, 0) await get_error_log(hass, hass_client, 0)
async def test_exception(hass, aiohttp_client): async def test_exception(hass, hass_client):
"""Test that exceptions are logged and retrieved correctly.""" """Test that exceptions are logged and retrieved correctly."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_generate_and_log_exception('exception message', 'log message') _generate_and_log_exception('exception message', 'log message')
log = (await get_error_log(hass, aiohttp_client, 1))[0] log = (await get_error_log(hass, hass_client, 1))[0]
assert_log(log, 'exception message', 'log message', 'ERROR') assert_log(log, 'exception message', 'log message', 'ERROR')
async def test_warning(hass, aiohttp_client): async def test_warning(hass, hass_client):
"""Test that warning are logged and retrieved correctly.""" """Test that warning are logged and retrieved correctly."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.warning('warning message') _LOGGER.warning('warning message')
log = (await get_error_log(hass, aiohttp_client, 1))[0] log = (await get_error_log(hass, hass_client, 1))[0]
assert_log(log, '', 'warning message', 'WARNING') assert_log(log, '', 'warning message', 'WARNING')
async def test_error(hass, aiohttp_client): async def test_error(hass, hass_client):
"""Test that errors are logged and retrieved correctly.""" """Test that errors are logged and retrieved correctly."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.error('error message') _LOGGER.error('error message')
log = (await get_error_log(hass, aiohttp_client, 1))[0] log = (await get_error_log(hass, hass_client, 1))[0]
assert_log(log, '', 'error message', 'ERROR') assert_log(log, '', 'error message', 'ERROR')
@ -121,26 +121,26 @@ async def test_error_posted_as_event(hass):
assert_log(events[0].data, '', 'error message', 'ERROR') assert_log(events[0].data, '', 'error message', 'ERROR')
async def test_critical(hass, aiohttp_client): async def test_critical(hass, hass_client):
"""Test that critical are logged and retrieved correctly.""" """Test that critical are logged and retrieved correctly."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.critical('critical message') _LOGGER.critical('critical message')
log = (await get_error_log(hass, aiohttp_client, 1))[0] log = (await get_error_log(hass, hass_client, 1))[0]
assert_log(log, '', 'critical message', 'CRITICAL') assert_log(log, '', 'critical message', 'CRITICAL')
async def test_remove_older_logs(hass, aiohttp_client): async def test_remove_older_logs(hass, hass_client):
"""Test that older logs are rotated out.""" """Test that older logs are rotated out."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.error('error message 1') _LOGGER.error('error message 1')
_LOGGER.error('error message 2') _LOGGER.error('error message 2')
_LOGGER.error('error message 3') _LOGGER.error('error message 3')
log = await get_error_log(hass, aiohttp_client, 2) log = await get_error_log(hass, hass_client, 2)
assert_log(log[0], '', 'error message 3', 'ERROR') assert_log(log[0], '', 'error message 3', 'ERROR')
assert_log(log[1], '', 'error message 2', 'ERROR') assert_log(log[1], '', 'error message 2', 'ERROR')
async def test_clear_logs(hass, aiohttp_client): async def test_clear_logs(hass, hass_client):
"""Test that the log can be cleared via a service call.""" """Test that the log can be cleared via a service call."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.error('error message') _LOGGER.error('error message')
@ -151,7 +151,7 @@ async def test_clear_logs(hass, aiohttp_client):
await hass.async_block_till_done() await hass.async_block_till_done()
# Assert done by get_error_log # Assert done by get_error_log
await get_error_log(hass, aiohttp_client, 0) await get_error_log(hass, hass_client, 0)
async def test_write_log(hass): async def test_write_log(hass):
@ -197,13 +197,13 @@ async def test_write_choose_level(hass):
assert logger.method_calls[0] == ('debug', ('test_message',)) assert logger.method_calls[0] == ('debug', ('test_message',))
async def test_unknown_path(hass, aiohttp_client): async def test_unknown_path(hass, hass_client):
"""Test error logged from unknown path.""" """Test error logged from unknown path."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
_LOGGER.findCaller = MagicMock( _LOGGER.findCaller = MagicMock(
return_value=('unknown_path', 0, None, None)) return_value=('unknown_path', 0, None, None))
_LOGGER.error('error message') _LOGGER.error('error message')
log = (await get_error_log(hass, aiohttp_client, 1))[0] log = (await get_error_log(hass, hass_client, 1))[0]
assert log['source'] == 'unknown_path' assert log['source'] == 'unknown_path'
@ -222,31 +222,31 @@ def log_error_from_test_path(path):
_LOGGER.error('error message') _LOGGER.error('error message')
async def test_homeassistant_path(hass, aiohttp_client): async def test_homeassistant_path(hass, hass_client):
"""Test error logged from homeassistant path.""" """Test error logged from homeassistant path."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
with patch('homeassistant.components.system_log.HOMEASSISTANT_PATH', with patch('homeassistant.components.system_log.HOMEASSISTANT_PATH',
new=['venv_path/homeassistant']): new=['venv_path/homeassistant']):
log_error_from_test_path( log_error_from_test_path(
'venv_path/homeassistant/component/component.py') 'venv_path/homeassistant/component/component.py')
log = (await get_error_log(hass, aiohttp_client, 1))[0] log = (await get_error_log(hass, hass_client, 1))[0]
assert log['source'] == 'component/component.py' assert log['source'] == 'component/component.py'
async def test_config_path(hass, aiohttp_client): async def test_config_path(hass, hass_client):
"""Test error logged from config path.""" """Test error logged from config path."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
with patch.object(hass.config, 'config_dir', new='config'): with patch.object(hass.config, 'config_dir', new='config'):
log_error_from_test_path('config/custom_component/test.py') log_error_from_test_path('config/custom_component/test.py')
log = (await get_error_log(hass, aiohttp_client, 1))[0] log = (await get_error_log(hass, hass_client, 1))[0]
assert log['source'] == 'custom_component/test.py' assert log['source'] == 'custom_component/test.py'
async def test_netdisco_path(hass, aiohttp_client): async def test_netdisco_path(hass, hass_client):
"""Test error logged from netdisco path.""" """Test error logged from netdisco path."""
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
with patch.dict('sys.modules', with patch.dict('sys.modules',
netdisco=MagicMock(__path__=['venv_path/netdisco'])): netdisco=MagicMock(__path__=['venv_path/netdisco'])):
log_error_from_test_path('venv_path/netdisco/disco_component.py') log_error_from_test_path('venv_path/netdisco/disco_component.py')
log = (await get_error_log(hass, aiohttp_client, 1))[0] log = (await get_error_log(hass, hass_client, 1))[0]
assert log['source'] == 'disco_component.py' assert log['source'] == 'disco_component.py'

View File

@ -7,10 +7,10 @@ from homeassistant.setup import async_setup_component
@pytest.fixture @pytest.fixture
def mock_client(hass, aiohttp_client): def mock_client(hass, hass_client):
"""Create http client for webhooks.""" """Create http client for webhooks."""
hass.loop.run_until_complete(async_setup_component(hass, 'webhook', {})) hass.loop.run_until_complete(async_setup_component(hass, 'webhook', {}))
return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) return hass.loop.run_until_complete(hass_client())
async def test_unregistering_webhook(hass, mock_client): async def test_unregistering_webhook(hass, mock_client):

View File

@ -431,3 +431,24 @@ async def test_update_entity(hass):
assert len(entity.async_update_ha_state.mock_calls) == 2 assert len(entity.async_update_ha_state.mock_calls) == 2
assert entity.async_update_ha_state.mock_calls[-1][1][0] is True assert entity.async_update_ha_state.mock_calls[-1][1][0] is True
async def test_set_service_race(hass):
"""Test race condition on setting service."""
exception = False
def async_loop_exception_handler(_, _2) -> None:
"""Handle all exception inside the core loop."""
nonlocal exception
exception = True
hass.loop.set_exception_handler(async_loop_exception_handler)
await async_setup_component(hass, 'group', {})
component = EntityComponent(_LOGGER, DOMAIN, hass, group_name='yo')
for i in range(2):
hass.async_create_task(component.async_add_entities([MockEntity()]))
await hass.async_block_till_done()
assert not exception