Mobile App: Register devices into the registry (#21856)

* Register devices into the registry

* Switch to device ID instead of webhook ID

* Rearchitect mobile_app to support config entries

* Kill DATA_REGISTRATIONS by migrating registrations into config entries

* Fix tests

* Improve how we get the config_entry_id

* Remove single_instance_allowed

* Simplify setup_registration

* Move webhook registering functions into __init__.py since they are only ever used once

* Kill get_registration websocket command

* Support description_placeholders in async_abort

* Add link to mobile_app implementing apps in abort dialog

* Store config entry and device registry entry in hass.data instead of looking it up

* Add testing to ensure that the config entry is created at registration

* Fix busted async_abort test

* Remove unnecessary check for entry is None
pull/22049/head
Robbie Trencheny 2019-03-14 12:57:50 -07:00 committed by GitHub
parent 62f12d242a
commit 3769f5893a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 254 additions and 212 deletions

View File

@ -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."
}
}
}

View File

@ -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)

View File

@ -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'

View File

@ -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]
}

View File

@ -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),

View File

@ -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."
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -161,6 +161,7 @@ FLOWS = [
'locative',
'luftdaten',
'mailgun',
'mobile_app',
'mqtt',
'nest',
'openuv',

View File

@ -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,
}

View File

@ -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'

View File

@ -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

View File

@ -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
)

View File

@ -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
)

View File

@ -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):