Skip to content

Commit

Permalink
Expose create/delete cloudhook (#21606)
Browse files Browse the repository at this point in the history
* Expose create/delete cloudhook

* Make sure we dont publish cloudhooks when not connected
  • Loading branch information
balloob committed Mar 4, 2019
1 parent c25cbcc commit f5ed643
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 6 deletions.
48 changes: 47 additions & 1 deletion homeassistant/components/cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@

import voluptuous as vol

from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, CLOUD_NEVER_EXPOSED_ENTITIES, CONF_REGION,
CONF_MODE, CONF_NAME)
from homeassistant.helpers import entityfilter, config_validation as cv
from homeassistant.loader import bind_hass
from homeassistant.util import dt as dt_util
from homeassistant.util.aiohttp import MockRequest
from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import helpers as ga_h
from homeassistant.components.google_assistant import const as ga_c

from . import http_api, iot, auth_api, prefs, cloudhooks
from .const import CONFIG_DIR, DOMAIN, SERVERS
from .const import CONFIG_DIR, DOMAIN, SERVERS, STATE_CONNECTED

REQUIREMENTS = ['warrant==0.6.1']

Expand Down Expand Up @@ -81,6 +85,43 @@
}, extra=vol.ALLOW_EXTRA)


class CloudNotAvailable(HomeAssistantError):
"""Raised when an action requires the cloud but it's not available."""


@bind_hass
@callback
def async_is_logged_in(hass):
"""Test if user is logged in."""
return DOMAIN in hass.data and hass.data[DOMAIN].is_logged_in


@bind_hass
async def async_create_cloudhook(hass, webhook_id):
"""Create a cloudhook."""
if not async_is_logged_in(hass):
raise CloudNotAvailable

return await hass.data[DOMAIN].cloudhooks.async_create(webhook_id)


@bind_hass
async def async_delete_cloudhook(hass, webhook_id):
"""Delete a cloudhook."""
if not async_is_logged_in(hass):
raise CloudNotAvailable

return await hass.data[DOMAIN].cloudhooks.async_delete(webhook_id)


def is_cloudhook_request(request):
"""Test if a request came from a cloudhook.
Async friendly.
"""
return isinstance(request, MockRequest)


async def async_setup(hass, config):
"""Initialize the Home Assistant cloud."""
if DOMAIN in config:
Expand Down Expand Up @@ -152,6 +193,11 @@ def is_logged_in(self):
"""Get if cloud is logged in."""
return self.id_token is not None

@property
def is_connected(self):
"""Get if cloud is connected."""
return self.iot.state == STATE_CONNECTED

@property
def subscription_expired(self):
"""Return a boolean if the subscription has expired."""
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/cloud/cloudhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ def __init__(self, cloud):

async def async_publish_cloudhooks(self):
"""Inform the Relayer of the cloudhooks that we support."""
if not self.cloud.is_connected:
return

cloudhooks = self.cloud.prefs.cloudhooks
await self.cloud.iot.async_send_message('webhook-register', {
'cloudhook_ids': [info['cloudhook_id'] for info
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/cloud/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,7 @@
to verify your credentials. Please [log in](/config/cloud) again to continue
using the service.
"""

STATE_CONNECTING = 'connecting'
STATE_CONNECTED = 'connected'
STATE_DISCONNECTED = 'disconnected'
9 changes: 4 additions & 5 deletions homeassistant/components/cloud/iot.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import auth_api
from . import utils
from .const import MESSAGE_EXPIRATION, MESSAGE_AUTH_FAIL
from .const import (
MESSAGE_EXPIRATION, MESSAGE_AUTH_FAIL, STATE_CONNECTED, STATE_CONNECTING,
STATE_DISCONNECTED
)

HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__)

STATE_CONNECTING = 'connecting'
STATE_CONNECTED = 'connected'
STATE_DISCONNECTED = 'disconnected'


class UnknownHandler(Exception):
"""Exception raised when trying to handle unknown handler."""
Expand Down
26 changes: 26 additions & 0 deletions tests/components/cloud/test_cloudhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,29 @@ async def test_disable(mock_cloudhooks):
assert publish_calls[0][1][1] == {
'cloudhook_ids': []
}


async def test_create_without_connected(mock_cloudhooks, aioclient_mock):
"""Test we don't publish a hook if not connected."""
mock_cloudhooks.cloud.is_connected = False
# Make sure we fail test when we send a message.
mock_cloudhooks.cloud.iot.async_send_message.side_effect = ValueError

aioclient_mock.post('https://webhook-create.url', json={
'cloudhook_id': 'mock-cloud-id',
'url': 'https://hooks.nabu.casa/ZXCZCXZ',
})

hook = {
'webhook_id': 'mock-webhook-id',
'cloudhook_id': 'mock-cloud-id',
'cloudhook_url': 'https://hooks.nabu.casa/ZXCZCXZ',
}

assert hook == await mock_cloudhooks.async_create('mock-webhook-id')

assert mock_cloudhooks.cloud.prefs.cloudhooks == {
'mock-webhook-id': hook
}

assert len(mock_cloudhooks.cloud.iot.async_send_message.mock_calls) == 0
69 changes: 69 additions & 0 deletions tests/components/cloud/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import pytest

from homeassistant.setup import async_setup_component
from homeassistant.components import cloud
from homeassistant.util.dt import utcnow

Expand Down Expand Up @@ -175,3 +176,71 @@ def test_subscription_not_expired(hass):
patch('homeassistant.util.dt.utcnow',
return_value=utcnow().replace(year=2017, month=11, day=9)):
assert not cl.subscription_expired


async def test_create_cloudhook_no_login(hass):
"""Test create cloudhook when not logged in."""
assert await async_setup_component(hass, 'cloud', {})
coro = mock_coro({'yo': 'hey'})
with patch('homeassistant.components.cloud.cloudhooks.'
'Cloudhooks.async_create', return_value=coro) as mock_create, \
pytest.raises(cloud.CloudNotAvailable):
await hass.components.cloud.async_create_cloudhook('hello')

assert len(mock_create.mock_calls) == 0


async def test_delete_cloudhook_no_login(hass):
"""Test delete cloudhook when not logged in."""
assert await async_setup_component(hass, 'cloud', {})
coro = mock_coro({'yo': 'hey'})
with patch('homeassistant.components.cloud.cloudhooks.'
'Cloudhooks.async_delete', return_value=coro) as mock_delete, \
pytest.raises(cloud.CloudNotAvailable):
await hass.components.cloud.async_delete_cloudhook('hello')

assert len(mock_delete.mock_calls) == 0


async def test_create_cloudhook(hass):
"""Test create cloudhook."""
assert await async_setup_component(hass, 'cloud', {})
coro = mock_coro({'yo': 'hey'})
with patch('homeassistant.components.cloud.cloudhooks.'
'Cloudhooks.async_create', return_value=coro) as mock_create, \
patch('homeassistant.components.cloud.async_is_logged_in',
return_value=True):
result = await hass.components.cloud.async_create_cloudhook('hello')

assert result == {'yo': 'hey'}
assert len(mock_create.mock_calls) == 1


async def test_delete_cloudhook(hass):
"""Test delete cloudhook."""
assert await async_setup_component(hass, 'cloud', {})
coro = mock_coro({'yo': 'hey'})
with patch('homeassistant.components.cloud.cloudhooks.'
'Cloudhooks.async_delete', return_value=coro) as mock_delete, \
patch('homeassistant.components.cloud.async_is_logged_in',
return_value=True):
result = await hass.components.cloud.async_delete_cloudhook('hello')

assert result == {'yo': 'hey'}
assert len(mock_delete.mock_calls) == 1


async def test_async_logged_in(hass):
"""Test if is_logged_in works."""
# Cloud not loaded
assert hass.components.cloud.async_is_logged_in() is False

assert await async_setup_component(hass, 'cloud', {})

# Cloud loaded, not logged in
assert hass.components.cloud.async_is_logged_in() is False

hass.data['cloud'].id_token = "some token"

# Cloud loaded, logged in
assert hass.components.cloud.async_is_logged_in() is True

0 comments on commit f5ed643

Please sign in to comment.