diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index cf82c40a4d3..bad1bdcf913 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -462,10 +462,11 @@ class AuthStore: for group in self._groups.values(): g_dict = { 'id': group.id, + # Name not read for sys groups. Kept here for backwards compat + 'name': group.name } # type: Dict[str, Any] if group.id not in (GROUP_ID_READ_ONLY, GROUP_ID_ADMIN): - g_dict['name'] = group.name g_dict['policy'] = group.policy groups.append(g_dict) diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index 111b9e7d39f..6cdb12b7157 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -4,16 +4,19 @@ Support Legacy API password auth provider. It will be removed when auth system production ready """ import hmac -from typing import Any, Dict, Optional, cast +from typing import Any, Dict, Optional, cast, TYPE_CHECKING import voluptuous as vol -from homeassistant.components.http import HomeAssistantHTTP # noqa: F401 -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError 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({ @@ -31,6 +34,24 @@ class InvalidAuthError(HomeAssistantError): """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') class LegacyApiPasswordAuthProvider(AuthProvider): """Example auth provider based on hardcoded usernames and passwords.""" diff --git a/homeassistant/components/binary_sensor/rainmachine.py b/homeassistant/components/binary_sensor/rainmachine.py index 4a671fc9512..efae9330365 100644 --- a/homeassistant/components/binary_sensor/rainmachine.py +++ b/homeassistant/components/binary_sensor/rainmachine.py @@ -74,7 +74,7 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): async def async_added_to_hass(self): """Register callbacks.""" @callback - def update(self): + def update(): """Update the state.""" self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 5f1d61dd602..9c0df0f9f03 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -97,8 +97,8 @@ async def async_setup(hass, yaml_config): app._on_startup.freeze() await app.startup() - handler = None - server = None + runner = None + site = None DescriptionXmlView(config).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): """Stop the emulated hue bridge.""" upnp_listener.stop() - if server: - server.close() - await server.wait_closed() - await app.shutdown() - if handler: - await handler.shutdown(10) - await app.cleanup() + if site: + await site.stop() + if runner: + await runner.cleanup() async def start_emulated_hue_bridge(event): """Start the emulated hue bridge.""" upnp_listener.start() - nonlocal handler - nonlocal server + nonlocal site + 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: - server = await hass.loop.create_server( - handler, config.host_ip_addr, config.listen_port) + await site.start() except OSError as error: _LOGGER.error("Failed to create HTTP server at port %d: %s", config.listen_port, error) diff --git a/homeassistant/components/fibaro.py b/homeassistant/components/fibaro.py index c9dd19b4bc8..85bd5c3c018 100644 --- a/homeassistant/components/fibaro.py +++ b/homeassistant/components/fibaro.py @@ -103,29 +103,31 @@ class FibaroController(): """Handle change report received from the HomeCenter.""" callback_set = set() for change in state.get('changes', []): - dev_id = change.pop('id') - for property_name, value in change.items(): - if property_name == 'log': - if value and value != "transfer OK": - _LOGGER.debug("LOG %s: %s", - self._device_map[dev_id].friendly_name, - value) + try: + dev_id = change.pop('id') + if dev_id not in self._device_map.keys(): continue - if property_name == 'logTemp': - continue - if property_name in self._device_map[dev_id].properties: - self._device_map[dev_id].properties[property_name] = \ - value - _LOGGER.debug("<- %s.%s = %s", - self._device_map[dev_id].ha_id, - property_name, - str(value)) - else: - _LOGGER.warning("Error updating %s data of %s, not found", - property_name, - self._device_map[dev_id].ha_id) - if dev_id in self._callbacks: - callback_set.add(dev_id) + device = self._device_map[dev_id] + for property_name, value in change.items(): + if property_name == 'log': + if value and value != "transfer OK": + _LOGGER.debug("LOG %s: %s", + device.friendly_name, value) + continue + if property_name == 'logTemp': + continue + if property_name in device.properties: + device.properties[property_name] = \ + value + _LOGGER.debug("<- %s.%s = %s", device.ha_id, + property_name, str(value)) + 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: self._callbacks[item]() @@ -137,8 +139,12 @@ class FibaroController(): def _map_device_to_type(device): """Map device to HA device type.""" # Use our lookup table to identify device type - device_type = FIBARO_TYPEMAP.get( - device.type, FIBARO_TYPEMAP.get(device.baseType)) + if 'type' in device: + 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 if device_type is None: @@ -156,8 +162,7 @@ class FibaroController(): # Switches that control lights should show up as lights if device_type == 'switch' and \ - 'isLight' in device.properties and \ - device.properties.isLight == 'true': + device.properties.get('isLight', 'false') == 'true': device_type = 'light' return device_type @@ -165,26 +170,31 @@ class FibaroController(): """Read and process the device list.""" devices = self._client.devices.list() 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) - for device in self._device_map.values(): - if device.enabled and \ - (not device.isPlugin or self._import_plugins): - device.mapped_type = self._map_device_to_type(device) + for device in devices: + try: + 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) + 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: + self._device_map[device.id] = device self.fibaro_devices[device.mapped_type].append(device) else: - _LOGGER.debug("%s (%s, %s) not mapped", + _LOGGER.debug("%s (%s, %s) not used", device.ha_id, device.type, device.baseType) + except (KeyError, ValueError): + pass def setup(hass, config): diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index d32dd91a3c1..61231a7894d 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -712,6 +712,8 @@ class FanSpeedTrait(_Trait): modes = self.state.attributes.get(fan.ATTR_SPEED_LIST, []) speeds = [] for mode in modes: + if mode not in self.speed_synonyms: + continue speed = { "speed_name": mode, "speed_values": [{ diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 4dd3571e69c..15a3816c559 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -207,6 +207,13 @@ async def async_setup(hass, config): DOMAIN, SERVICE_RELOAD, reload_service_handler, 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): """Handle dynamic group service functions.""" object_id = service.data[ATTR_OBJECT_ID] @@ -284,7 +291,7 @@ async def async_setup(hass, config): await component.async_remove_entity(entity_id) hass.services.async_register( - DOMAIN, SERVICE_SET, groups_service_handler, + DOMAIN, SERVICE_SET, locked_service_handler, schema=SET_SERVICE_SCHEMA) hass.services.async_register( diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 1b22f8e62d4..7180002430a 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -302,12 +302,6 @@ class HomeAssistantHTTP: async def start(self): """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: try: if self.ssl_profile == SSL_INTERMEDIATE: @@ -335,6 +329,7 @@ class HomeAssistantHTTP: # However in Home Assistant components can be discovered after boot. # This will now raise a RunTimeError. # To work around this we now prevent the router from getting frozen + # pylint: disable=protected-access self.app._router.freeze = lambda: None self.runner = web.AppRunner(self.app) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 64ee7fb8a3f..0e943b33fb8 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -10,6 +10,7 @@ import jwt from homeassistant.core import callback 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.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'))): # A valid auth header has been set 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 hmac.compare_digest( api_password.encode('utf-8'), request.query[DATA_API_PASSWORD].encode('utf-8'))): authenticated = True + request['hass_user'] = await legacy_api_password.async_get_user( + app['hass']) elif _is_trusted_ip(request, trusted_networks): authenticated = True @@ -96,11 +101,7 @@ def setup_auth(app, trusted_networks, use_auth, request[KEY_AUTHENTICATED] = authenticated return await handler(request) - async def auth_startup(app): - """Initialize auth middleware when app starts up.""" - app.middlewares.append(auth_middleware) - - app.on_startup.append(auth_startup) + app.middlewares.append(auth_middleware) def _is_trusted_ip(request, trusted_networks): diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 2a25de96edc..d6d7168ce6d 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -9,7 +9,7 @@ from aiohttp.web import middleware from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized 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.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -36,13 +36,14 @@ SCHEMA_IP_BAN_ENTRY = vol.Schema({ @callback def setup_bans(hass, app, login_threshold): """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): """Initialize bans when app starts up.""" - app.middlewares.append(ban_middleware) - app[KEY_BANNED_IPS] = await hass.async_add_job( - load_ip_bans_config, hass.config.path(IP_BANS_FILE)) - app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int) - app[KEY_LOGIN_THRESHOLD] = login_threshold + app[KEY_BANNED_IPS] = await async_load_ip_bans_config( + hass, hass.config.path(IP_BANS_FILE)) app.on_startup.append(ban_startup) @@ -149,7 +150,7 @@ class IpBan: 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.""" ip_list = [] @@ -157,7 +158,7 @@ def load_ip_bans_config(path: str): return ip_list try: - list_ = load_yaml_config_file(path) + list_ = await hass.async_add_executor_job(load_yaml_config_file, path) except HomeAssistantError as err: _LOGGER.error('Unable to load %s: %s', path, str(err)) return ip_list diff --git a/homeassistant/components/http/real_ip.py b/homeassistant/components/http/real_ip.py index f8adc815fde..27a8550ab8c 100644 --- a/homeassistant/components/http/real_ip.py +++ b/homeassistant/components/http/real_ip.py @@ -33,8 +33,4 @@ def setup_real_ip(app, use_x_forwarded_for, trusted_proxies): return await handler(request) - async def app_startup(app): - """Initialize bans when app starts up.""" - app.middlewares.append(real_ip_middleware) - - app.on_startup.append(app_startup) + app.middlewares.append(real_ip_middleware) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index c7a37411f1e..b6f434a82ad 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -445,6 +445,12 @@ def _exclude_events(events, entities_filter): domain = event.data.get(ATTR_DOMAIN) 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: entity_id = "%s." % (domain, ) diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 88362946428..8cf19e84bcd 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -40,8 +40,8 @@ class OwnTracksFlow(config_entries.ConfigFlow): if supports_encryption(): secret_desc = ( - "The encryption key is {secret} " - "(on Android under preferences -> advanced)") + "The encryption key is {} " + "(on Android under preferences -> advanced)".format(secret)) else: secret_desc = ( "Encryption is not supported because libsodium is not " diff --git a/homeassistant/components/sensor/rainmachine.py b/homeassistant/components/sensor/rainmachine.py index 5131b25510a..86a97bc291c 100644 --- a/homeassistant/components/sensor/rainmachine.py +++ b/homeassistant/components/sensor/rainmachine.py @@ -77,7 +77,7 @@ class RainMachineSensor(RainMachineEntity): async def async_added_to_hass(self): """Register callbacks.""" @callback - def update(self): + def update(): """Update the state.""" self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/sensor/seventeentrack.py b/homeassistant/components/sensor/seventeentrack.py index 7ad0e453760..b4c869e7267 100644 --- a/homeassistant/components/sensor/seventeentrack.py +++ b/homeassistant/components/sensor/seventeentrack.py @@ -17,7 +17,7 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle, slugify -REQUIREMENTS = ['py17track==2.0.2'] +REQUIREMENTS = ['py17track==2.1.0'] _LOGGER = logging.getLogger(__name__) ATTR_DESTINATION_COUNTRY = 'destination_country' diff --git a/homeassistant/components/shopping_list.py b/homeassistant/components/shopping_list.py index 45650ece621..ad4680982b4 100644 --- a/homeassistant/components/shopping_list.py +++ b/homeassistant/components/shopping_list.py @@ -38,12 +38,28 @@ SERVICE_ITEM_SCHEMA = vol.Schema({ }) 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 = \ websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ 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 def async_setup(hass, config): @@ -103,6 +119,14 @@ def async_setup(hass, config): WS_TYPE_SHOPPING_LIST_ITEMS, websocket_handle_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 @@ -276,3 +300,30 @@ def websocket_handle_items(hass, connection, msg): """Handle get shopping_list items.""" connection.send_message(websocket_api.result_message( 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')) diff --git a/homeassistant/const.py b/homeassistant/const.py index dc00267cdf8..6b69609be22 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 83 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) diff --git a/requirements_all.txt b/requirements_all.txt index 197f9be02d0..dc0f7e8679f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -804,7 +804,7 @@ py-melissa-climate==2.0.0 py-synology==0.2.0 # homeassistant.components.sensor.seventeentrack -py17track==2.0.2 +py17track==2.1.0 # homeassistant.components.hdmi_cec pyCEC==0.4.13 diff --git a/tests/auth/test_auth_store.py b/tests/auth/test_auth_store.py index b76d68fbeac..7e9df869a04 100644 --- a/tests/auth/test_auth_store.py +++ b/tests/auth/test_auth_store.py @@ -199,13 +199,22 @@ async def test_loading_empty_data(hass, hass_storage): assert len(users) == 0 -async def test_system_groups_only_store_id(hass, hass_storage): - """Test that for system groups we only store the ID.""" +async def test_system_groups_store_id_and_name(hass, hass_storage): + """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) await store._async_load() data = store._data_to_save() assert len(data['users']) == 0 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, + }, ] diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index d15c7ccbb34..ab84dd2a3bc 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -23,7 +23,7 @@ NPR_NEWS_MP3_URL = "https://pd.npr.org/anon.npr-mp3/npr/news/newscast.mp3" @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.""" @callback 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): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 766075f8eb5..3cfb8068177 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1437,10 +1437,10 @@ async def test_unsupported_domain(hass): 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.""" 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') response = await http_client.post( @@ -1450,7 +1450,7 @@ async def do_http_discovery(config, hass, aiohttp_client): 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.""" config = { '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() # 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' -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.""" config = { 'alexa': {} } - response = await do_http_discovery(config, hass, aiohttp_client) + response = await do_http_discovery(config, hass, hass_client) assert response.status == 404 diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 97f2044baea..110ba8d5ad6 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -4,12 +4,21 @@ from unittest.mock import patch import pytest 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.components.websocket_api.http import URL from homeassistant.components.websocket_api.auth import ( 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 @@ -80,7 +89,7 @@ def hass_access_token(hass, hass_admin_user): @pytest.fixture -def hass_admin_user(hass): +def hass_admin_user(hass, local_auth): """Return a Home Assistant admin user.""" admin_group = hass.loop.run_until_complete(hass.auth.async_get_group( GROUP_ID_ADMIN)) @@ -88,8 +97,42 @@ def hass_admin_user(hass): @pytest.fixture -def hass_read_only_user(hass): +def hass_read_only_user(hass, local_auth): """Return a Home Assistant read only user.""" read_only_group = hass.loop.run_until_complete(hass.auth.async_get_group( GROUP_ID_READ_ONLY)) 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 diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index f9ad1c578de..435de6d1edf 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -27,7 +27,7 @@ def hassio_env(): @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.""" with patch('homeassistant.components.hassio.HassIO.update_hass_api', Mock(return_value=mock_coro({"result": "ok"}))), \ diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 2746abcf15c..979bfc28689 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -83,7 +83,8 @@ async def test_access_without_password(app, aiohttp_client): 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.""" setup_auth(app, [], False, api_password=API_PASSWORD) client = await aiohttp_client(app) @@ -97,7 +98,7 @@ async def test_access_with_password_in_header(app, aiohttp_client): 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.""" setup_auth(app, [], False, api_password=API_PASSWORD) 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) -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.""" setup_auth(app, [], True, api_password=API_PASSWORD) 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 -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.""" setup_auth(app, [], True, support_legacy=True, api_password=API_PASSWORD) client = await aiohttp_client(app) diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index a6a07928113..6624937da8d 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -16,6 +16,9 @@ from homeassistant.components.http.ban import ( from . import mock_real_ip +from tests.common import mock_coro + + 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) set_real_ip = mock_real_ip(app) - with patch('homeassistant.components.http.ban.load_ip_bans_config', - return_value=[IpBan(banned_ip) for banned_ip - in BANNED_IPS]): + with patch('homeassistant.components.http.ban.async_load_ip_bans_config', + return_value=mock_coro([IpBan(banned_ip) for banned_ip + in BANNED_IPS])): client = await aiohttp_client(app) 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) mock_real_ip(app)("200.201.202.204") - with patch('homeassistant.components.http.ban.load_ip_bans_config', - return_value=[IpBan(banned_ip) for banned_ip - in BANNED_IPS]): + with patch('homeassistant.components.http.ban.async_load_ip_bans_config', + return_value=mock_coro([IpBan(banned_ip) for banned_ip + in BANNED_IPS])): client = await aiohttp_client(app) m = mock_open() diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 9f6441c5238..1c1afe711c6 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -124,7 +124,7 @@ async def test_api_no_base_url(hass): 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.""" assert await async_setup_component(hass, 'api', { 'http': { diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 3ebfa05a3d3..0bc89292855 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -16,12 +16,10 @@ from tests.common import async_mock_service @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.""" hass.loop.run_until_complete(async_setup_component(hass, 'api', {})) - return hass.loop.run_until_complete(aiohttp_client(hass.http.app, headers={ - 'Authorization': 'Bearer {}'.format(hass_access_token) - })) + return hass.loop.run_until_complete(hass_client()) @asyncio.coroutine @@ -408,7 +406,7 @@ def _listen_count(hass): 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.""" hass.data[DATA_LOGGING] = '/some/path' await async_setup_component(hass, 'api', { @@ -566,5 +564,17 @@ async def test_rendering_template_admin(hass, mock_api_client, hass_admin_user): """Test rendering a template requires admin.""" 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 diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index 7934e016281..2aa1f499a76 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -90,7 +90,7 @@ async def test_register_before_setup(hass): 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.""" class TestIntentHandler(intent.IntentHandler): """Test Intent Handler.""" @@ -120,7 +120,7 @@ async def test_http_processing_intent(hass, aiohttp_client): }) assert result - client = await aiohttp_client(hass.http.app) + client = await hass_client() resp = await client.post('/api/conversation/process', json={ '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'} -async def test_http_api(hass, aiohttp_client): +async def test_http_api(hass, hass_client): """Test the HTTP conversation API.""" result = await component.async_setup(hass, {}) assert result @@ -252,7 +252,7 @@ async def test_http_api(hass, aiohttp_client): result = await async_setup_component(hass, 'conversation', {}) assert result - client = await aiohttp_client(hass.http.app) + client = await hass_client() hass.states.async_set('light.kitchen', 'off') 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'} -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.""" result = await component.async_setup(hass, {}) assert result @@ -276,7 +276,7 @@ async def test_http_api_wrong_data(hass, aiohttp_client): result = await async_setup_component(hass, 'conversation', {}) assert result - client = await aiohttp_client(hass.http.app) + client = await hass_client() resp = await client.post('/api/conversation/process', json={ 'text': 123 diff --git a/tests/components/test_history.py b/tests/components/test_history.py index 9764af1592c..641dff3b4e6 100644 --- a/tests/components/test_history.py +++ b/tests/components/test_history.py @@ -515,13 +515,13 @@ class TestComponentHistory(unittest.TestCase): 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.""" await hass.async_add_job(init_recorder_component, hass) await async_setup_component(hass, 'history', {}) await hass.components.recorder.wait_connection_ready() 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( '/api/history/period/{}'.format(dt_util.utcnow().isoformat())) assert response.status == 200 diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index ae1e3d1d51a..0d204773241 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -242,9 +242,11 @@ class TestComponentLogbook(unittest.TestCase): config = logbook.CONFIG_SCHEMA({ ha.DOMAIN: {}, logbook.DOMAIN: {logbook.CONF_EXCLUDE: { - logbook.CONF_DOMAINS: ['switch', ]}}}) + logbook.CONF_DOMAINS: ['switch', 'alexa', DOMAIN_HOMEKIT]}}}) 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])) entries = list(logbook.humanify(self.hass, events)) @@ -325,22 +327,35 @@ class TestComponentLogbook(unittest.TestCase): pointA = dt_util.utcnow() 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) eventB = self.create_state_changed_event(pointB, entity_id2, 20) config = logbook.CONFIG_SCHEMA({ ha.DOMAIN: {}, logbook.DOMAIN: {logbook.CONF_INCLUDE: { - logbook.CONF_DOMAINS: ['sensor', ]}}}) + logbook.CONF_DOMAINS: ['sensor', 'alexa', DOMAIN_HOMEKIT]}}}) 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])) 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', 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) def test_include_exclude_events(self): diff --git a/tests/components/test_shopping_list.py b/tests/components/test_shopping_list.py index e64b9a5ae26..1e89287bcc1 100644 --- a/tests/components/test_shopping_list.py +++ b/tests/components/test_shopping_list.py @@ -55,7 +55,7 @@ def test_recent_items_intent(hass): @asyncio.coroutine -def test_deprecated_api_get_all(hass, aiohttp_client): +def test_deprecated_api_get_all(hass, hass_client): """Test the API.""" 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'}} ) - client = yield from aiohttp_client(hass.http.app) + client = yield from hass_client() resp = yield from client.get('/api/shopping_list') assert resp.status == 200 @@ -110,7 +110,7 @@ async def test_ws_get_items(hass, hass_ws_client): @asyncio.coroutine -def test_api_update(hass, aiohttp_client): +def test_deprecated_api_update(hass, hass_client): """Test the API.""" 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'] 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( '/api/shopping_list/item/{}'.format(beer_id), json={ '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 -def test_api_update_fails(hass, aiohttp_client): +def test_api_update_fails(hass, hass_client): """Test the API.""" 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'}} ) - client = yield from aiohttp_client(hass.http.app) + client = yield from hass_client() resp = yield from client.post( '/api/shopping_list/non_existing', json={ 'name': 'soda' @@ -190,8 +245,37 @@ def test_api_update_fails(hass, aiohttp_client): 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 -def test_api_clear_completed(hass, aiohttp_client): +def test_api_clear_completed(hass, hass_client): """Test the API.""" 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'] 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 resp = yield from client.post( @@ -228,11 +312,11 @@ def test_api_clear_completed(hass, aiohttp_client): @asyncio.coroutine -def test_api_create(hass, aiohttp_client): +def test_deprecated_api_create(hass, hass_client): """Test the API.""" 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={ 'name': 'soda' }) @@ -249,14 +333,48 @@ def test_api_create(hass, aiohttp_client): @asyncio.coroutine -def test_api_create_fail(hass, aiohttp_client): +def test_deprecated_api_create_fail(hass, hass_client): """Test the API.""" 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={ 'name': 1234 }) assert resp.status == 400 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 diff --git a/tests/components/test_spaceapi.py b/tests/components/test_spaceapi.py index e7e7d158a31..61bb009ff8f 100644 --- a/tests/components/test_spaceapi.py +++ b/tests/components/test_spaceapi.py @@ -56,7 +56,7 @@ SENSOR_OUTPUT = { @pytest.fixture -def mock_client(hass, aiohttp_client): +def mock_client(hass, hass_client): """Start the Home Assistant HTTP component.""" with patch('homeassistant.components.spaceapi', return_value=mock_coro(True)): @@ -70,7 +70,7 @@ def mock_client(hass, aiohttp_client): hass.states.async_set('test.hum1', 88, 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): diff --git a/tests/components/test_system_log.py b/tests/components/test_system_log.py index 5d48fd88127..6afd792be9c 100644 --- a/tests/components/test_system_log.py +++ b/tests/components/test_system_log.py @@ -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.""" - client = await aiohttp_client(hass.http.app) + client = await hass_client() resp = await client.get('/api/error/all') assert resp.status == 200 @@ -45,37 +45,37 @@ def get_frame(name): 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.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.debug('debug') _LOGGER.info('info') # 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.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _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') -async def test_warning(hass, aiohttp_client): +async def test_warning(hass, hass_client): """Test that warning are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _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') -async def test_error(hass, aiohttp_client): +async def test_error(hass, hass_client): """Test that errors are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _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') @@ -121,26 +121,26 @@ async def test_error_posted_as_event(hass): 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.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _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') -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.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.error('error message 1') _LOGGER.error('error message 2') _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[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.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.error('error message') @@ -151,7 +151,7 @@ async def test_clear_logs(hass, aiohttp_client): await hass.async_block_till_done() # 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): @@ -197,13 +197,13 @@ async def test_write_choose_level(hass): 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.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.findCaller = MagicMock( return_value=('unknown_path', 0, None, None)) _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' @@ -222,31 +222,31 @@ def log_error_from_test_path(path): _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.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch('homeassistant.components.system_log.HOMEASSISTANT_PATH', new=['venv_path/homeassistant']): log_error_from_test_path( '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' -async def test_config_path(hass, aiohttp_client): +async def test_config_path(hass, hass_client): """Test error logged from config path.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch.object(hass.config, 'config_dir', new='config'): 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' -async def test_netdisco_path(hass, aiohttp_client): +async def test_netdisco_path(hass, hass_client): """Test error logged from netdisco path.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch.dict('sys.modules', netdisco=MagicMock(__path__=['venv_path/netdisco'])): 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' diff --git a/tests/components/test_webhook.py b/tests/components/test_webhook.py index c16fef3e059..e67cf7481cc 100644 --- a/tests/components/test_webhook.py +++ b/tests/components/test_webhook.py @@ -7,10 +7,10 @@ from homeassistant.setup import async_setup_component @pytest.fixture -def mock_client(hass, aiohttp_client): +def mock_client(hass, hass_client): """Create http client for webhooks.""" 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): diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 2bef8c0b53e..7562a38d268 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -431,3 +431,24 @@ async def test_update_entity(hass): assert len(entity.async_update_ha_state.mock_calls) == 2 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