diff --git a/homeassistant/components/mobile_app/.translations/en.json b/homeassistant/components/mobile_app/.translations/en.json new file mode 100644 index 00000000000..646151a5229 --- /dev/null +++ b/homeassistant/components/mobile_app/.translations/en.json @@ -0,0 +1,14 @@ +{ + "config": { + "title": "Mobile App", + "step": { + "confirm": { + "title": "Mobile App", + "description": "Do you want to set up the Mobile App component?" + } + }, + "abort": { + "install_app": "Open the mobile app to set up the integration with Home Assistant. See [the docs]({apps_url}) for a list of compatible apps." + } + } +} diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 0d95bfe6832..1c348ea0782 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,11 +1,18 @@ """Integrates Native Apps to Home Assistant.""" +from homeassistant import config_entries +from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.components.webhook import async_register as webhook_register +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .const import (DATA_DELETED_IDS, DATA_REGISTRATIONS, DATA_STORE, DOMAIN, - STORAGE_KEY, STORAGE_VERSION) +from .const import (ATTR_APP_COMPONENT, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, + ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, + DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DATA_DEVICES, + DATA_STORE, DOMAIN, STORAGE_KEY, STORAGE_VERSION) -from .http_api import register_http_handlers -from .webhook import register_deleted_webhooks, setup_registration +from .http_api import RegistrationsView +from .webhook import handle_webhook from .websocket_api import register_websocket_handlers DEPENDENCIES = ['device_tracker', 'http', 'webhook'] @@ -15,24 +22,88 @@ REQUIREMENTS = ['PyNaCl==1.3.0'] async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up the mobile app component.""" + hass.data[DOMAIN] = { + DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], DATA_DEVICES: {}, + } + store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) app_config = await store.async_load() if app_config is None: - app_config = {DATA_DELETED_IDS: [], DATA_REGISTRATIONS: {}} + app_config = { + DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], DATA_DEVICES: {}, + } - if hass.data.get(DOMAIN) is None: - hass.data[DOMAIN] = {DATA_DELETED_IDS: [], DATA_REGISTRATIONS: {}} - - hass.data[DOMAIN][DATA_DELETED_IDS] = app_config.get(DATA_DELETED_IDS, []) - hass.data[DOMAIN][DATA_REGISTRATIONS] = app_config.get(DATA_REGISTRATIONS, - {}) + hass.data[DOMAIN] = app_config hass.data[DOMAIN][DATA_STORE] = store - for registration in app_config[DATA_REGISTRATIONS].values(): - setup_registration(hass, store, registration) - - register_http_handlers(hass, store) + hass.http.register_view(RegistrationsView()) register_websocket_handlers(hass) - register_deleted_webhooks(hass, store) + + for deleted_id in hass.data[DOMAIN][DATA_DELETED_IDS]: + try: + webhook_register(hass, DOMAIN, "Deleted Webhook", deleted_id, + handle_webhook) + except ValueError: + pass return True + + +async def async_setup_entry(hass, entry): + """Set up a mobile_app entry.""" + registration = entry.data + + webhook_id = registration[CONF_WEBHOOK_ID] + + hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] = entry + + device_registry = await dr.async_get_registry(hass) + + identifiers = { + (ATTR_DEVICE_ID, registration[ATTR_DEVICE_ID]), + (CONF_WEBHOOK_ID, registration[CONF_WEBHOOK_ID]) + } + + device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers=identifiers, + manufacturer=registration[ATTR_MANUFACTURER], + model=registration[ATTR_MODEL], + name=registration[ATTR_DEVICE_NAME], + sw_version=registration[ATTR_OS_VERSION] + ) + + hass.data[DOMAIN][DATA_DEVICES][webhook_id] = device + + registration_name = 'Mobile App: {}'.format(registration[ATTR_DEVICE_NAME]) + webhook_register(hass, DOMAIN, registration_name, webhook_id, + handle_webhook) + + if ATTR_APP_COMPONENT in registration: + load_platform(hass, registration[ATTR_APP_COMPONENT], DOMAIN, {}, + {DOMAIN: {}}) + + return True + + +@config_entries.HANDLERS.register(DOMAIN) +class MobileAppFlowHandler(config_entries.ConfigFlow): + """Handle a Mobile App config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + placeholders = { + 'apps_url': + 'https://www.home-assistant.io/components/mobile_app/#apps' + } + + return self.async_abort(reason='install_app', + description_placeholders=placeholders) + + async def async_step_registration(self, user_input=None): + """Handle a flow initialized during registration.""" + return self.async_create_entry(title=user_input[ATTR_DEVICE_NAME], + data=user_input) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 7a497d76454..3ba029fec0e 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -17,8 +17,9 @@ CONF_CLOUDHOOK_URL = 'cloudhook_url' CONF_SECRET = 'secret' CONF_USER_ID = 'user_id' +DATA_CONFIG_ENTRIES = 'config_entries' DATA_DELETED_IDS = 'deleted_ids' -DATA_REGISTRATIONS = 'registrations' +DATA_DEVICES = 'devices' DATA_STORE = 'store' ATTR_APP_COMPONENT = 'app_component' @@ -26,6 +27,7 @@ ATTR_APP_DATA = 'app_data' ATTR_APP_ID = 'app_id' ATTR_APP_NAME = 'app_name' ATTR_APP_VERSION = 'app_version' +ATTR_CONFIG_ENTRY_ID = 'entry_id' ATTR_DEVICE_ID = 'device_id' ATTR_DEVICE_NAME = 'device_name' ATTR_MANUFACTURER = 'manufacturer' @@ -52,7 +54,6 @@ ATTR_WEBHOOK_TYPE = 'type' ERR_ENCRYPTION_REQUIRED = 'encryption_required' ERR_INVALID_COMPONENT = 'invalid_component' -ERR_SAVE_FAILURE = 'save_failure' WEBHOOK_TYPE_CALL_SERVICE = 'call_service' WEBHOOK_TYPE_FIRE_EVENT = 'fire_event' diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 1f67170a72c..5ec3b99b291 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -11,8 +11,7 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME, ATTR_APP_VERSION, ATTR_DEVICE_NAME, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, ATTR_SUPPORTS_ENCRYPTION, - CONF_SECRET, CONF_USER_ID, DATA_DELETED_IDS, - DATA_REGISTRATIONS, DOMAIN) + CONF_SECRET, CONF_USER_ID, DATA_DELETED_IDS, DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -125,7 +124,6 @@ def savable_state(hass: HomeAssistantType) -> Dict: """Return a clean object containing things that should be saved.""" return { DATA_DELETED_IDS: hass.data[DOMAIN][DATA_DELETED_IDS], - DATA_REGISTRATIONS: hass.data[DOMAIN][DATA_REGISTRATIONS] } diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 4948407b63b..8076d217cac 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -8,29 +8,16 @@ from homeassistant.auth.util import generate_secret from homeassistant.components.cloud import async_create_cloudhook from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.const import (HTTP_CREATED, HTTP_INTERNAL_SERVER_ERROR, - CONF_WEBHOOK_ID) +from homeassistant.const import (HTTP_CREATED, CONF_WEBHOOK_ID) -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import get_component from .const import (ATTR_APP_COMPONENT, ATTR_DEVICE_ID, ATTR_SUPPORTS_ENCRYPTION, CONF_CLOUDHOOK_URL, CONF_SECRET, - CONF_USER_ID, DATA_REGISTRATIONS, DOMAIN, - ERR_INVALID_COMPONENT, ERR_SAVE_FAILURE, + CONF_USER_ID, DOMAIN, ERR_INVALID_COMPONENT, REGISTRATION_SCHEMA) -from .helpers import error_response, supports_encryption, savable_state - -from .webhook import setup_registration - - -def register_http_handlers(hass: HomeAssistantType, store: Store) -> bool: - """Register the HTTP handlers/views.""" - hass.http.register_view(RegistrationsView(store)) - return True +from .helpers import error_response, supports_encryption class RegistrationsView(HomeAssistantView): @@ -39,10 +26,6 @@ class RegistrationsView(HomeAssistantView): url = '/api/mobile_app/registrations' name = 'api:mobile_app:register' - def __init__(self, store: Store) -> None: - """Initialize the view.""" - self._store = store - @RequestDataValidator(REGISTRATION_SCHEMA) async def post(self, request: Request, data: Dict) -> Response: """Handle the POST request for registration.""" @@ -79,16 +62,10 @@ class RegistrationsView(HomeAssistantView): data[CONF_USER_ID] = request['hass_user'].id - hass.data[DOMAIN][DATA_REGISTRATIONS][webhook_id] = data - - try: - await self._store.async_save(savable_state(hass)) - except HomeAssistantError: - return error_response(ERR_SAVE_FAILURE, - "Error saving registration", - status=HTTP_INTERNAL_SERVER_ERROR) - - setup_registration(hass, self._store, data) + ctx = {'source': 'registration'} + await hass.async_create_task( + hass.config_entries.flow.async_init(DOMAIN, context=ctx, + data=data)) return self.json({ CONF_CLOUDHOOK_URL: data.get(CONF_CLOUDHOOK_URL), diff --git a/homeassistant/components/mobile_app/strings.json b/homeassistant/components/mobile_app/strings.json new file mode 100644 index 00000000000..646151a5229 --- /dev/null +++ b/homeassistant/components/mobile_app/strings.json @@ -0,0 +1,14 @@ +{ + "config": { + "title": "Mobile App", + "step": { + "confirm": { + "title": "Mobile App", + "description": "Do you want to set up the Mobile App component?" + } + }, + "abort": { + "install_app": "Open the mobile app to set up the integration with Home Assistant. See [the docs]({apps_url}) for a list of compatible apps." + } + } +} diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 4d3e0aef4c6..1fab29160b7 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -1,7 +1,5 @@ """Webhook handlers for mobile_app.""" -from functools import partial import logging -from typing import Dict from aiohttp.web import HTTPBadRequest, Response, Request import voluptuous as vol @@ -10,27 +8,24 @@ from homeassistant.components.device_tracker import (ATTR_ATTRIBUTES, ATTR_DEV_ID, DOMAIN as DT_DOMAIN, SERVICE_SEE as DT_SEE) -from homeassistant.components.webhook import async_register as webhook_register from homeassistant.const import (ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, CONF_WEBHOOK_ID, HTTP_BAD_REQUEST) from homeassistant.core import EventOrigin -from homeassistant.exceptions import (HomeAssistantError, ServiceNotFound, - TemplateError) +from homeassistant.exceptions import (ServiceNotFound, TemplateError) +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.template import attach -from homeassistant.helpers.discovery import load_platform -from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import HomeAssistantType -from .const import (ATTR_ALTITUDE, ATTR_APP_COMPONENT, ATTR_BATTERY, - ATTR_COURSE, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, - ATTR_EVENT_DATA, ATTR_EVENT_TYPE, ATTR_GPS, - ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, ATTR_SPEED, +from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID, + ATTR_DEVICE_NAME, ATTR_EVENT_DATA, ATTR_EVENT_TYPE, + ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, + ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, ATTR_SPEED, ATTR_SUPPORTS_ENCRYPTION, ATTR_TEMPLATE, ATTR_TEMPLATE_VARIABLES, ATTR_VERTICAL_ACCURACY, ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED, ATTR_WEBHOOK_ENCRYPTED_DATA, ATTR_WEBHOOK_TYPE, - CONF_SECRET, DATA_DELETED_IDS, DATA_REGISTRATIONS, DOMAIN, + CONF_SECRET, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DOMAIN, ERR_ENCRYPTION_REQUIRED, WEBHOOK_PAYLOAD_SCHEMA, WEBHOOK_SCHEMAS, WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_RENDER_TEMPLATE, @@ -38,45 +33,24 @@ from .const import (ATTR_ALTITUDE, ATTR_APP_COMPONENT, ATTR_BATTERY, WEBHOOK_TYPE_UPDATE_REGISTRATION) from .helpers import (_decrypt_payload, empty_okay_response, error_response, - registration_context, safe_registration, savable_state, + registration_context, safe_registration, webhook_response) _LOGGER = logging.getLogger(__name__) -def register_deleted_webhooks(hass: HomeAssistantType, store: Store): - """Register previously deleted webhook IDs so we can return 410.""" - for deleted_id in hass.data[DOMAIN][DATA_DELETED_IDS]: - try: - webhook_register(hass, DOMAIN, "Deleted Webhook", deleted_id, - partial(handle_webhook, store)) - except ValueError: - pass - - -def setup_registration(hass: HomeAssistantType, store: Store, - registration: Dict) -> None: - """Register the webhook for a registration and loads the app component.""" - registration_name = 'Mobile App: {}'.format(registration[ATTR_DEVICE_NAME]) - webhook_id = registration[CONF_WEBHOOK_ID] - webhook_register(hass, DOMAIN, registration_name, webhook_id, - partial(handle_webhook, store)) - - if ATTR_APP_COMPONENT in registration: - load_platform(hass, registration[ATTR_APP_COMPONENT], DOMAIN, {}, - {DOMAIN: {}}) - - -async def handle_webhook(store: Store, hass: HomeAssistantType, - webhook_id: str, request: Request) -> Response: +async def handle_webhook(hass: HomeAssistantType, webhook_id: str, + request: Request) -> Response: """Handle webhook callback.""" if webhook_id in hass.data[DOMAIN][DATA_DELETED_IDS]: return Response(status=410) headers = {} - registration = hass.data[DOMAIN][DATA_REGISTRATIONS][webhook_id] + config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] + + registration = config_entry.data try: req_data = await request.json() @@ -179,13 +153,22 @@ async def handle_webhook(store: Store, hass: HomeAssistantType, if webhook_type == WEBHOOK_TYPE_UPDATE_REGISTRATION: new_registration = {**registration, **data} - hass.data[DOMAIN][DATA_REGISTRATIONS][webhook_id] = new_registration + device_registry = await dr.async_get_registry(hass) - try: - await store.async_save(savable_state(hass)) - except HomeAssistantError as ex: - _LOGGER.error("Error updating mobile_app registration: %s", ex) - return empty_okay_response() + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={ + (ATTR_DEVICE_ID, registration[ATTR_DEVICE_ID]), + (CONF_WEBHOOK_ID, registration[CONF_WEBHOOK_ID]) + }, + manufacturer=new_registration[ATTR_MANUFACTURER], + model=new_registration[ATTR_MODEL], + name=new_registration[ATTR_DEVICE_NAME], + sw_version=new_registration[ATTR_OS_VERSION] + ) + + hass.config_entries.async_update_entry(config_entry, + data=new_registration) return webhook_response(safe_registration(new_registration), registration=registration, headers=headers) diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py index 5f6a25cbcec..7bc1e59d623 100644 --- a/homeassistant/components/mobile_app/websocket_api.py +++ b/homeassistant/components/mobile_app/websocket_api.py @@ -17,16 +17,14 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import HomeAssistantType -from .const import (CONF_CLOUDHOOK_URL, CONF_USER_ID, DATA_DELETED_IDS, - DATA_REGISTRATIONS, DATA_STORE, DOMAIN) +from .const import (CONF_CLOUDHOOK_URL, CONF_USER_ID, DATA_CONFIG_ENTRIES, + DATA_DELETED_IDS, DATA_STORE, DOMAIN) from .helpers import safe_registration, savable_state def register_websocket_handlers(hass: HomeAssistantType) -> bool: """Register the websocket handlers.""" - async_register_command(hass, websocket_get_registration) - async_register_command(hass, websocket_get_user_registrations) async_register_command(hass, websocket_delete_registration) @@ -34,39 +32,6 @@ def register_websocket_handlers(hass: HomeAssistantType) -> bool: return True -@ws_require_user() -@async_response -@websocket_command({ - vol.Required('type'): 'mobile_app/get_registration', - vol.Required(CONF_WEBHOOK_ID): cv.string, -}) -async def websocket_get_registration( - hass: HomeAssistantType, connection: ActiveConnection, - msg: dict) -> None: - """Return the registration for the given webhook_id.""" - user = connection.user - - webhook_id = msg.get(CONF_WEBHOOK_ID) - if webhook_id is None: - connection.send_error(msg['id'], ERR_INVALID_FORMAT, - "Webhook ID not provided") - return - - registration = hass.data[DOMAIN][DATA_REGISTRATIONS].get(webhook_id) - - if registration is None: - connection.send_error(msg['id'], ERR_NOT_FOUND, - "Webhook ID not found in storage") - return - - if registration[CONF_USER_ID] != user.id and not user.is_admin: - return error_message( - msg['id'], ERR_UNAUTHORIZED, 'User is not registration owner') - - connection.send_message( - result_message(msg['id'], safe_registration(registration))) - - @ws_require_user() @async_response @websocket_command({ @@ -87,7 +52,8 @@ async def websocket_get_user_registrations( user_registrations = [] - for registration in hass.data[DOMAIN][DATA_REGISTRATIONS].values(): + for config_entry in hass.config_entries.async_entries(domain=DOMAIN): + registration = config_entry.data if connection.user.is_admin or registration[CONF_USER_ID] is user_id: user_registrations.append(safe_registration(registration)) @@ -113,7 +79,9 @@ async def websocket_delete_registration(hass: HomeAssistantType, "Webhook ID not provided") return - registration = hass.data[DOMAIN][DATA_REGISTRATIONS].get(webhook_id) + config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] + + registration = config_entry.data if registration is None: connection.send_error(msg['id'], ERR_NOT_FOUND, @@ -124,7 +92,7 @@ async def websocket_delete_registration(hass: HomeAssistantType, return error_message( msg['id'], ERR_UNAUTHORIZED, 'User is not registration owner') - del hass.data[DOMAIN][DATA_REGISTRATIONS][webhook_id] + await hass.config_entries.async_remove(config_entry.entry_id) hass.data[DOMAIN][DATA_DELETED_IDS].append(webhook_id) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1036c02fd0d..e00d7204a79 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -161,6 +161,7 @@ FLOWS = [ 'locative', 'luftdaten', 'mailgun', + 'mobile_app', 'mqtt', 'nest', 'openuv', diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 57265cf696d..acd0befda4e 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -170,11 +170,13 @@ class FlowHandler: } @callback - def async_abort(self, *, reason: str) -> Dict: + def async_abort(self, *, reason: str, + description_placeholders: Optional[Dict] = None) -> Dict: """Abort the config flow.""" return { 'type': RESULT_TYPE_ABORT, 'flow_id': self.flow_id, 'handler': self.handler, - 'reason': reason + 'reason': reason, + 'description_placeholders': description_placeholders, } diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 87ed83d9a7e..d5e4331f7b9 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -226,6 +226,7 @@ def test_abort(hass, client): data = yield from resp.json() data.pop('flow_id') assert data == { + 'description_placeholders': None, 'handler': 'test', 'reason': 'bla', 'type': 'abort' diff --git a/tests/components/mobile_app/__init__.py b/tests/components/mobile_app/__init__.py index 1f91eb7e442..bed275a534d 100644 --- a/tests/components/mobile_app/__init__.py +++ b/tests/components/mobile_app/__init__.py @@ -2,48 +2,59 @@ # pylint: disable=redefined-outer-name,unused-import import pytest +from tests.common import mock_device_registry + from homeassistant.setup import async_setup_component -from homeassistant.components.mobile_app.const import (DATA_DELETED_IDS, - DATA_REGISTRATIONS, - CONF_SECRET, - CONF_USER_ID, DOMAIN, +from homeassistant.components.mobile_app.const import (DATA_CONFIG_ENTRIES, + DATA_DELETED_IDS, + DATA_DEVICES, + DOMAIN, STORAGE_KEY, STORAGE_VERSION) -from homeassistant.const import CONF_WEBHOOK_ID + +from .const import REGISTER, REGISTER_CLEARTEXT @pytest.fixture -def webhook_client(hass, aiohttp_client, hass_storage, hass_admin_user): +def registry(hass): + """Return a configured device registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +async def create_registrations(authed_api_client): + """Return two new registrations.""" + enc_reg = await authed_api_client.post( + '/api/mobile_app/registrations', json=REGISTER + ) + + assert enc_reg.status == 201 + enc_reg_json = await enc_reg.json() + + clear_reg = await authed_api_client.post( + '/api/mobile_app/registrations', json=REGISTER_CLEARTEXT + ) + + assert clear_reg.status == 201 + clear_reg_json = await clear_reg.json() + + return (enc_reg_json, clear_reg_json) + + +@pytest.fixture +async def webhook_client(hass, aiohttp_client, hass_storage, hass_admin_user): """mobile_app mock client.""" hass_storage[STORAGE_KEY] = { 'version': STORAGE_VERSION, 'data': { - DATA_REGISTRATIONS: { - 'mobile_app_test': { - CONF_SECRET: '58eb127991594dad934d1584bdee5f27', - 'supports_encryption': True, - CONF_WEBHOOK_ID: 'mobile_app_test', - 'device_name': 'Test Device', - CONF_USER_ID: hass_admin_user.id, - }, - 'mobile_app_test_cleartext': { - 'supports_encryption': False, - CONF_WEBHOOK_ID: 'mobile_app_test_cleartext', - 'device_name': 'Test Device (Cleartext)', - CONF_USER_ID: hass_admin_user.id, - } - }, - DATA_DELETED_IDS: [], + DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], DATA_DEVICES: {}, } } - assert hass.loop.run_until_complete(async_setup_component( - hass, DOMAIN, { - DOMAIN: {} - })) + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) + return await aiohttp_client(hass.http.app) @pytest.fixture diff --git a/tests/components/mobile_app/test_http_api.py b/tests/components/mobile_app/test_http_api.py index 7861e63459a..eb9d1f54d93 100644 --- a/tests/components/mobile_app/test_http_api.py +++ b/tests/components/mobile_app/test_http_api.py @@ -2,14 +2,15 @@ # pylint: disable=redefined-outer-name,unused-import import pytest -from homeassistant.components.mobile_app.const import CONF_SECRET +from homeassistant.components.mobile_app.const import CONF_SECRET, DOMAIN from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.setup import async_setup_component from .const import REGISTER, RENDER_TEMPLATE from . import authed_api_client # noqa: F401 -async def test_registration(hass_client, authed_api_client): # noqa: F811 +async def test_registration(hass, hass_client): # noqa: F811 """Test that registrations happen.""" try: # pylint: disable=unused-import @@ -21,7 +22,11 @@ async def test_registration(hass_client, authed_api_client): # noqa: F811 import json - resp = await authed_api_client.post( + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + api_client = await hass_client() + + resp = await api_client.post( '/api/mobile_app/registrations', json=REGISTER ) @@ -30,6 +35,20 @@ async def test_registration(hass_client, authed_api_client): # noqa: F811 assert CONF_WEBHOOK_ID in register_json assert CONF_SECRET in register_json + entries = hass.config_entries.async_entries(DOMAIN) + + assert entries[0].data['app_data'] == REGISTER['app_data'] + assert entries[0].data['app_id'] == REGISTER['app_id'] + assert entries[0].data['app_name'] == REGISTER['app_name'] + assert entries[0].data['app_version'] == REGISTER['app_version'] + assert entries[0].data['device_name'] == REGISTER['device_name'] + assert entries[0].data['manufacturer'] == REGISTER['manufacturer'] + assert entries[0].data['model'] == REGISTER['model'] + assert entries[0].data['os_name'] == REGISTER['os_name'] + assert entries[0].data['os_version'] == REGISTER['os_version'] + assert entries[0].data['supports_encryption'] == \ + REGISTER['supports_encryption'] + keylen = SecretBox.KEY_SIZE key = register_json[CONF_SECRET].encode("utf-8") key = key[:keylen] @@ -46,9 +65,7 @@ async def test_registration(hass_client, authed_api_client): # noqa: F811 'encrypted_data': data, } - webhook_client = await hass_client() - - resp = await webhook_client.post( + resp = await api_client.post( '/api/webhook/{}'.format(register_json[CONF_WEBHOOK_ID]), json=container ) diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 75e8903c494..a70e8ba1275 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1,5 +1,6 @@ """Webhook tests for mobile_app.""" # pylint: disable=redefined-outer-name,unused-import +import logging import pytest from homeassistant.components.mobile_app.const import CONF_SECRET @@ -8,16 +9,20 @@ from homeassistant.core import callback from tests.common import async_mock_service -from . import authed_api_client, webhook_client # noqa: F401 +from . import (authed_api_client, create_registrations, # noqa: F401 + webhook_client) # noqa: F401 from .const import (CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE) +_LOGGER = logging.getLogger(__name__) -async def test_webhook_handle_render_template(webhook_client): # noqa: F811 + +async def test_webhook_handle_render_template(create_registrations, # noqa: F401, F811, E501 + webhook_client): # noqa: F811 """Test that we render templates properly.""" resp = await webhook_client.post( - '/api/webhook/mobile_app_test_cleartext', + '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), json=RENDER_TEMPLATE ) @@ -27,12 +32,13 @@ async def test_webhook_handle_render_template(webhook_client): # noqa: F811 assert json == {'one': 'Hello world'} -async def test_webhook_handle_call_services(hass, webhook_client): # noqa: E501 F811 +async def test_webhook_handle_call_services(hass, create_registrations, # noqa: F401, F811, E501 + webhook_client): # noqa: E501 F811 """Test that we call services properly.""" calls = async_mock_service(hass, 'test', 'mobile_app') resp = await webhook_client.post( - '/api/webhook/mobile_app_test_cleartext', + '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), json=CALL_SERVICE ) @@ -41,7 +47,8 @@ async def test_webhook_handle_call_services(hass, webhook_client): # noqa: E501 assert len(calls) == 1 -async def test_webhook_handle_fire_event(hass, webhook_client): # noqa: F811 +async def test_webhook_handle_fire_event(hass, create_registrations, # noqa: F401, F811, E501 + webhook_client): # noqa: F811 """Test that we can fire events.""" events = [] @@ -53,7 +60,7 @@ async def test_webhook_handle_fire_event(hass, webhook_client): # noqa: F811 hass.bus.async_listen('test_event', store_event) resp = await webhook_client.post( - '/api/webhook/mobile_app_test_cleartext', + '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), json=FIRE_EVENT ) @@ -93,10 +100,12 @@ async def test_webhook_update_registration(webhook_client, hass_client): # noqa assert CONF_SECRET not in update_json -async def test_webhook_returns_error_incorrect_json(webhook_client, caplog): # noqa: E501 F811 +async def test_webhook_returns_error_incorrect_json(webhook_client, # noqa: F401, F811, E501 + create_registrations, # noqa: F401, F811, E501 + caplog): # noqa: E501 F811 """Test that an error is returned when JSON is invalid.""" resp = await webhook_client.post( - '/api/webhook/mobile_app_test_cleartext', + '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), data='not json' ) @@ -106,7 +115,8 @@ async def test_webhook_returns_error_incorrect_json(webhook_client, caplog): # assert 'invalid JSON' in caplog.text -async def test_webhook_handle_decryption(webhook_client): # noqa: F811 +async def test_webhook_handle_decryption(webhook_client, # noqa: F811 + create_registrations): # noqa: F401, F811, E501 """Test that we can encrypt/decrypt properly.""" try: # pylint: disable=unused-import @@ -119,7 +129,7 @@ async def test_webhook_handle_decryption(webhook_client): # noqa: F811 import json keylen = SecretBox.KEY_SIZE - key = "58eb127991594dad934d1584bdee5f27".encode("utf-8") + key = create_registrations[0]['secret'].encode("utf-8") key = key[:keylen] key = key.ljust(keylen, b'\0') @@ -135,7 +145,7 @@ async def test_webhook_handle_decryption(webhook_client): # noqa: F811 } resp = await webhook_client.post( - '/api/webhook/mobile_app_test', + '/api/webhook/{}'.format(create_registrations[0]['webhook_id']), json=container ) @@ -151,10 +161,11 @@ async def test_webhook_handle_decryption(webhook_client): # noqa: F811 assert json.loads(decrypted_data) == {'one': 'Hello world'} -async def test_webhook_requires_encryption(webhook_client): # noqa: F811 +async def test_webhook_requires_encryption(webhook_client, # noqa: F811 + create_registrations): # noqa: F401, F811, E501 """Test that encrypted registrations only accept encrypted data.""" resp = await webhook_client.post( - '/api/webhook/mobile_app_test', + '/api/webhook/{}'.format(create_registrations[0]['webhook_id']), json=RENDER_TEMPLATE ) diff --git a/tests/components/mobile_app/test_websocket_api.py b/tests/components/mobile_app/test_websocket_api.py index 614fd33974b..ee656159d2e 100644 --- a/tests/components/mobile_app/test_websocket_api.py +++ b/tests/components/mobile_app/test_websocket_api.py @@ -9,33 +9,6 @@ from . import authed_api_client, setup_ws, webhook_client # noqa: F401 from .const import (CALL_SERVICE, REGISTER) -async def test_webocket_get_registration(hass, setup_ws, authed_api_client, # noqa: E501 F811 - hass_ws_client): - """Test get_registration websocket command.""" - register_resp = await authed_api_client.post( - '/api/mobile_app/registrations', json=REGISTER - ) - - assert register_resp.status == 201 - register_json = await register_resp.json() - assert CONF_WEBHOOK_ID in register_json - assert CONF_SECRET in register_json - - client = await hass_ws_client(hass) - await client.send_json({ - 'id': 5, - 'type': 'mobile_app/get_registration', - CONF_WEBHOOK_ID: register_json[CONF_WEBHOOK_ID], - }) - - msg = await client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == TYPE_RESULT - assert msg['success'] - assert msg['result']['app_id'] == 'io.homeassistant.mobile_app_test' - - async def test_webocket_get_user_registrations(hass, aiohttp_client, hass_ws_client, hass_read_only_access_token):