Add webhook + IFTTT example (#16817)
* Add webhook + IFTTT example * Abort if not externally accessible * Abort on local url * Add description to create entry * Make body optional * Allow ifttt setup without config * Add tests * Lint * Fix Lint + Tests * Fix typing
This commit is contained in:
parent
06d959ed43
commit
f5632a5da5
13 changed files with 448 additions and 91 deletions
|
@ -1,24 +1,13 @@
|
|||
"""Helpers to resolve client ID/secret."""
|
||||
import asyncio
|
||||
from ipaddress import ip_address
|
||||
from html.parser import HTMLParser
|
||||
from ipaddress import ip_address, ip_network
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
|
||||
# IP addresses of loopback interfaces
|
||||
ALLOWED_IPS = (
|
||||
ip_address('127.0.0.1'),
|
||||
ip_address('::1'),
|
||||
)
|
||||
|
||||
# RFC1918 - Address allocation for Private Internets
|
||||
ALLOWED_NETWORKS = (
|
||||
ip_network('10.0.0.0/8'),
|
||||
ip_network('172.16.0.0/12'),
|
||||
ip_network('192.168.0.0/16'),
|
||||
)
|
||||
from homeassistant.util.network import is_local
|
||||
|
||||
|
||||
async def verify_redirect_uri(hass, client_id, redirect_uri):
|
||||
|
@ -185,9 +174,7 @@ def _parse_client_id(client_id):
|
|||
# Not an ip address
|
||||
pass
|
||||
|
||||
if (address is None or
|
||||
address in ALLOWED_IPS or
|
||||
any(address in network for network in ALLOWED_NETWORKS)):
|
||||
if address is None or is_local(address):
|
||||
return parts
|
||||
|
||||
raise ValueError('Hostname should be a domain name or local IP address')
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
"""
|
||||
Support to trigger Maker IFTTT recipes.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/ifttt/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyfttt==0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_EVENT = 'event'
|
||||
ATTR_VALUE1 = 'value1'
|
||||
ATTR_VALUE2 = 'value2'
|
||||
ATTR_VALUE3 = 'value3'
|
||||
|
||||
CONF_KEY = 'key'
|
||||
|
||||
DOMAIN = 'ifttt'
|
||||
|
||||
SERVICE_TRIGGER = 'trigger'
|
||||
|
||||
SERVICE_TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_EVENT): cv.string,
|
||||
vol.Optional(ATTR_VALUE1): cv.string,
|
||||
vol.Optional(ATTR_VALUE2): cv.string,
|
||||
vol.Optional(ATTR_VALUE3): cv.string,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_KEY): cv.string,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def trigger(hass, event, value1=None, value2=None, value3=None):
|
||||
"""Trigger a Maker IFTTT recipe."""
|
||||
data = {
|
||||
ATTR_EVENT: event,
|
||||
ATTR_VALUE1: value1,
|
||||
ATTR_VALUE2: value2,
|
||||
ATTR_VALUE3: value3,
|
||||
}
|
||||
hass.services.call(DOMAIN, SERVICE_TRIGGER, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the IFTTT service component."""
|
||||
key = config[DOMAIN][CONF_KEY]
|
||||
|
||||
def trigger_service(call):
|
||||
"""Handle IFTTT trigger service calls."""
|
||||
event = call.data[ATTR_EVENT]
|
||||
value1 = call.data.get(ATTR_VALUE1)
|
||||
value2 = call.data.get(ATTR_VALUE2)
|
||||
value3 = call.data.get(ATTR_VALUE3)
|
||||
|
||||
try:
|
||||
import pyfttt
|
||||
pyfttt.send_event(key, event, value1, value2, value3)
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.exception("Error communicating with IFTTT")
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service,
|
||||
schema=SERVICE_TRIGGER_SCHEMA)
|
||||
|
||||
return True
|
18
homeassistant/components/ifttt/.translations/en.json
Normal file
18
homeassistant/components/ifttt/.translations/en.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive IFTTT messages.",
|
||||
"one_instance_allowed": "Only a single instance is necessary."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "To send events to Home Assistant, you will need to use the \"Make a web request\" action from the [IFTTT Webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Are you sure you want to set up IFTTT?",
|
||||
"title": "Set up the IFTTT Webhook Applet"
|
||||
}
|
||||
},
|
||||
"title": "IFTTT"
|
||||
}
|
||||
}
|
135
homeassistant/components/ifttt/__init__.py
Normal file
135
homeassistant/components/ifttt/__init__.py
Normal file
|
@ -0,0 +1,135 @@
|
|||
"""
|
||||
Support to trigger Maker IFTTT recipes.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/ifttt/
|
||||
"""
|
||||
from ipaddress import ip_address
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.util.network import is_local
|
||||
|
||||
REQUIREMENTS = ['pyfttt==0.3']
|
||||
DEPENDENCIES = ['webhook']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
EVENT_RECEIVED = 'ifttt_webhook_received'
|
||||
|
||||
ATTR_EVENT = 'event'
|
||||
ATTR_VALUE1 = 'value1'
|
||||
ATTR_VALUE2 = 'value2'
|
||||
ATTR_VALUE3 = 'value3'
|
||||
|
||||
CONF_KEY = 'key'
|
||||
CONF_WEBHOOK_ID = 'webhook_id'
|
||||
|
||||
DOMAIN = 'ifttt'
|
||||
|
||||
SERVICE_TRIGGER = 'trigger'
|
||||
|
||||
SERVICE_TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_EVENT): cv.string,
|
||||
vol.Optional(ATTR_VALUE1): cv.string,
|
||||
vol.Optional(ATTR_VALUE2): cv.string,
|
||||
vol.Optional(ATTR_VALUE3): cv.string,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(DOMAIN): vol.Schema({
|
||||
vol.Required(CONF_KEY): cv.string,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the IFTTT service component."""
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
key = config[DOMAIN][CONF_KEY]
|
||||
|
||||
def trigger_service(call):
|
||||
"""Handle IFTTT trigger service calls."""
|
||||
event = call.data[ATTR_EVENT]
|
||||
value1 = call.data.get(ATTR_VALUE1)
|
||||
value2 = call.data.get(ATTR_VALUE2)
|
||||
value3 = call.data.get(ATTR_VALUE3)
|
||||
|
||||
try:
|
||||
import pyfttt
|
||||
pyfttt.send_event(key, event, value1, value2, value3)
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.exception("Error communicating with IFTTT")
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_TRIGGER, trigger_service,
|
||||
schema=SERVICE_TRIGGER_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def handle_webhook(hass, webhook_id, data):
|
||||
"""Handle webhook callback."""
|
||||
if isinstance(data, dict):
|
||||
data['webhook_id'] = webhook_id
|
||||
hass.bus.async_fire(EVENT_RECEIVED, data)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Configure based on config entry."""
|
||||
hass.components.webhook.async_register(
|
||||
entry.data['webhook_id'], handle_webhook)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload a config entry."""
|
||||
hass.components.webhook.async_unregister(entry.data['webhook_id'])
|
||||
return True
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class ConfigFlow(config_entries.ConfigFlow):
|
||||
"""Handle an IFTTT config flow."""
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a user initiated set up flow."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason='one_instance_allowed')
|
||||
|
||||
try:
|
||||
url_parts = urlparse(self.hass.config.api.base_url)
|
||||
|
||||
if is_local(ip_address(url_parts.hostname)):
|
||||
return self.async_abort(reason='not_internet_accessible')
|
||||
except ValueError:
|
||||
# If it's not an IP address, it's very likely publicly accessible
|
||||
pass
|
||||
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id='user',
|
||||
)
|
||||
|
||||
webhook_id = self.hass.components.webhook.async_generate_id()
|
||||
webhook_url = \
|
||||
self.hass.components.webhook.async_generate_url(webhook_id)
|
||||
|
||||
return self.async_create_entry(
|
||||
title='IFTTT Webhook',
|
||||
data={
|
||||
CONF_WEBHOOK_ID: webhook_id
|
||||
},
|
||||
description_placeholders={
|
||||
'applet_url': 'https://ifttt.com/maker_webhooks',
|
||||
'webhook_url': webhook_url,
|
||||
'docs_url':
|
||||
'https://www.home-assistant.io/components/ifttt/'
|
||||
}
|
||||
)
|
18
homeassistant/components/ifttt/strings.json
Normal file
18
homeassistant/components/ifttt/strings.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"config": {
|
||||
"title": "IFTTT",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up the IFTTT Webhook Applet",
|
||||
"description": "Are you sure you want to set up IFTTT?"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"one_instance_allowed": "Only a single instance is necessary.",
|
||||
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive IFTTT messages."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "To send events to Home Assistant, you will need to use the \"Make a web request\" action from the [IFTTT Webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
|
||||
}
|
||||
}
|
||||
}
|
94
homeassistant/components/webhook.py
Normal file
94
homeassistant/components/webhook.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
"""Webhooks for Home Assistant.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/webhook/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from aiohttp.web import Response
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.auth.util import generate_secret
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
|
||||
DOMAIN = 'webhook'
|
||||
DEPENDENCIES = ['http']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_register(hass, webhook_id, handler):
|
||||
"""Register a webhook."""
|
||||
handlers = hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
if webhook_id in handlers:
|
||||
raise ValueError('Handler is already defined!')
|
||||
|
||||
handlers[webhook_id] = handler
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_unregister(hass, webhook_id):
|
||||
"""Remove a webhook."""
|
||||
handlers = hass.data.setdefault(DOMAIN, {})
|
||||
handlers.pop(webhook_id, None)
|
||||
|
||||
|
||||
@callback
|
||||
def async_generate_id():
|
||||
"""Generate a webhook_id."""
|
||||
return generate_secret(entropy=32)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_generate_url(hass, webhook_id):
|
||||
"""Generate a webhook_id."""
|
||||
return "{}/api/webhook/{}".format(hass.config.api.base_url, webhook_id)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Initialize the webhook component."""
|
||||
hass.http.register_view(WebhookView)
|
||||
return True
|
||||
|
||||
|
||||
class WebhookView(HomeAssistantView):
|
||||
"""Handle incoming webhook requests."""
|
||||
|
||||
url = "/api/webhook/{webhook_id}"
|
||||
name = "api:webhook"
|
||||
requires_auth = False
|
||||
|
||||
async def post(self, request, webhook_id):
|
||||
"""Handle webhook call."""
|
||||
hass = request.app['hass']
|
||||
handlers = hass.data.setdefault(DOMAIN, {})
|
||||
handler = handlers.get(webhook_id)
|
||||
|
||||
# Always respond successfully to not give away if a hook exists or not.
|
||||
if handler is None:
|
||||
_LOGGER.warning(
|
||||
'Received message for unregistered webhook %s', webhook_id)
|
||||
return Response(status=200)
|
||||
|
||||
body = await request.text()
|
||||
try:
|
||||
data = json.loads(body) if body else {}
|
||||
except ValueError:
|
||||
_LOGGER.warning(
|
||||
'Received webhook %s with invalid JSON', webhook_id)
|
||||
return Response(status=200)
|
||||
|
||||
try:
|
||||
response = await handler(hass, webhook_id, data)
|
||||
if response is None:
|
||||
response = Response(status=200)
|
||||
return response
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Error processing webhook %s", webhook_id)
|
||||
return Response(status=200)
|
|
@ -141,6 +141,7 @@ FLOWS = [
|
|||
'deconz',
|
||||
'homematicip_cloud',
|
||||
'hue',
|
||||
'ifttt',
|
||||
'ios',
|
||||
'mqtt',
|
||||
'nest',
|
||||
|
|
|
@ -153,7 +153,10 @@ class FlowHandler:
|
|||
}
|
||||
|
||||
@callback
|
||||
def async_create_entry(self, *, title: str, data: Dict) -> Dict:
|
||||
def async_create_entry(self, *, title: str, data: Dict,
|
||||
description: Optional[str] = None,
|
||||
description_placeholders: Optional[Dict] = None) \
|
||||
-> Dict:
|
||||
"""Finish config flow and create a config entry."""
|
||||
return {
|
||||
'version': self.VERSION,
|
||||
|
@ -162,6 +165,8 @@ class FlowHandler:
|
|||
'handler': self.handler,
|
||||
'title': title,
|
||||
'data': data,
|
||||
'description': description,
|
||||
'description_placeholders': description_placeholders,
|
||||
}
|
||||
|
||||
@callback
|
||||
|
|
22
homeassistant/util/network.py
Normal file
22
homeassistant/util/network.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
"""Network utilities."""
|
||||
from ipaddress import IPv4Address, IPv6Address, ip_address, ip_network
|
||||
from typing import Union
|
||||
|
||||
# IP addresses of loopback interfaces
|
||||
LOCAL_IPS = (
|
||||
ip_address('127.0.0.1'),
|
||||
ip_address('::1'),
|
||||
)
|
||||
|
||||
# RFC1918 - Address allocation for Private Internets
|
||||
LOCAL_NETWORKS = (
|
||||
ip_network('10.0.0.0/8'),
|
||||
ip_network('172.16.0.0/12'),
|
||||
ip_network('192.168.0.0/16'),
|
||||
)
|
||||
|
||||
|
||||
def is_local(address: Union[IPv4Address, IPv6Address]) -> bool:
|
||||
"""Check if an address is local."""
|
||||
return address in LOCAL_IPS or \
|
||||
any(address in network for network in LOCAL_NETWORKS)
|
|
@ -206,6 +206,8 @@ def test_create_account(hass, client):
|
|||
'title': 'Test Entry',
|
||||
'type': 'create_entry',
|
||||
'version': 1,
|
||||
'description': None,
|
||||
'description_placeholders': None,
|
||||
}
|
||||
|
||||
|
||||
|
@ -266,6 +268,8 @@ def test_two_step_flow(hass, client):
|
|||
'type': 'create_entry',
|
||||
'title': 'user-title',
|
||||
'version': 1,
|
||||
'description': None,
|
||||
'description_placeholders': None,
|
||||
}
|
||||
|
||||
|
||||
|
|
1
tests/components/ifttt/__init__.py
Normal file
1
tests/components/ifttt/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the IFTTT component."""
|
48
tests/components/ifttt/test_init.py
Normal file
48
tests/components/ifttt/test_init.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
"""Test the init file of IFTTT."""
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import ifttt
|
||||
|
||||
|
||||
async def test_config_flow_registers_webhook(hass, aiohttp_client):
|
||||
"""Test setting up IFTTT and sending webhook."""
|
||||
with patch('homeassistant.util.get_local_ip', return_value='example.com'):
|
||||
result = await hass.config_entries.flow.async_init('ifttt', context={
|
||||
'source': 'user'
|
||||
})
|
||||
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM, result
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result['flow_id'], {})
|
||||
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
webhook_id = result['result'].data['webhook_id']
|
||||
|
||||
ifttt_events = []
|
||||
|
||||
@callback
|
||||
def handle_event(event):
|
||||
"""Handle IFTTT event."""
|
||||
ifttt_events.append(event)
|
||||
|
||||
hass.bus.async_listen(ifttt.EVENT_RECEIVED, handle_event)
|
||||
|
||||
client = await aiohttp_client(hass.http.app)
|
||||
await client.post('/api/webhook/{}'.format(webhook_id), json={
|
||||
'hello': 'ifttt'
|
||||
})
|
||||
|
||||
assert len(ifttt_events) == 1
|
||||
assert ifttt_events[0].data['webhook_id'] == webhook_id
|
||||
assert ifttt_events[0].data['hello'] == 'ifttt'
|
||||
|
||||
|
||||
async def test_config_flow_aborts_external_url(hass, aiohttp_client):
|
||||
"""Test setting up IFTTT and sending webhook."""
|
||||
hass.config.api = Mock(base_url='http://192.168.1.10')
|
||||
result = await hass.config_entries.flow.async_init('ifttt', context={
|
||||
'source': 'user'
|
||||
})
|
||||
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result['reason'] == 'not_internet_accessible'
|
98
tests/components/test_webhook.py
Normal file
98
tests/components/test_webhook.py
Normal file
|
@ -0,0 +1,98 @@
|
|||
"""Test the webhook component."""
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client(hass, aiohttp_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))
|
||||
|
||||
|
||||
async def test_unregistering_webhook(hass, mock_client):
|
||||
"""Test unregistering a webhook."""
|
||||
hooks = []
|
||||
webhook_id = hass.components.webhook.async_generate_id()
|
||||
|
||||
async def handle(*args):
|
||||
"""Handle webhook."""
|
||||
hooks.append(args)
|
||||
|
||||
hass.components.webhook.async_register(webhook_id, handle)
|
||||
|
||||
resp = await mock_client.post('/api/webhook/{}'.format(webhook_id))
|
||||
assert resp.status == 200
|
||||
assert len(hooks) == 1
|
||||
|
||||
hass.components.webhook.async_unregister(webhook_id)
|
||||
|
||||
resp = await mock_client.post('/api/webhook/{}'.format(webhook_id))
|
||||
assert resp.status == 200
|
||||
assert len(hooks) == 1
|
||||
|
||||
|
||||
async def test_generate_webhook_url(hass):
|
||||
"""Test we generate a webhook url correctly."""
|
||||
hass.config.api = Mock(base_url='https://example.com')
|
||||
url = hass.components.webhook.async_generate_url('some_id')
|
||||
|
||||
assert url == 'https://example.com/api/webhook/some_id'
|
||||
|
||||
|
||||
async def test_posting_webhook_nonexisting(hass, mock_client):
|
||||
"""Test posting to a nonexisting webhook."""
|
||||
resp = await mock_client.post('/api/webhook/non-existing')
|
||||
assert resp.status == 200
|
||||
|
||||
|
||||
async def test_posting_webhook_invalid_json(hass, mock_client):
|
||||
"""Test posting to a nonexisting webhook."""
|
||||
hass.components.webhook.async_register('hello', None)
|
||||
resp = await mock_client.post('/api/webhook/hello', data='not-json')
|
||||
assert resp.status == 200
|
||||
|
||||
|
||||
async def test_posting_webhook_json(hass, mock_client):
|
||||
"""Test posting a webhook with JSON data."""
|
||||
hooks = []
|
||||
webhook_id = hass.components.webhook.async_generate_id()
|
||||
|
||||
async def handle(*args):
|
||||
"""Handle webhook."""
|
||||
hooks.append(args)
|
||||
|
||||
hass.components.webhook.async_register(webhook_id, handle)
|
||||
|
||||
resp = await mock_client.post('/api/webhook/{}'.format(webhook_id), json={
|
||||
'data': True
|
||||
})
|
||||
assert resp.status == 200
|
||||
assert len(hooks) == 1
|
||||
assert hooks[0][0] is hass
|
||||
assert hooks[0][1] == webhook_id
|
||||
assert hooks[0][2] == {
|
||||
'data': True
|
||||
}
|
||||
|
||||
|
||||
async def test_posting_webhook_no_data(hass, mock_client):
|
||||
"""Test posting a webhook with no data."""
|
||||
hooks = []
|
||||
webhook_id = hass.components.webhook.async_generate_id()
|
||||
|
||||
async def handle(*args):
|
||||
"""Handle webhook."""
|
||||
hooks.append(args)
|
||||
|
||||
hass.components.webhook.async_register(webhook_id, handle)
|
||||
|
||||
resp = await mock_client.post('/api/webhook/{}'.format(webhook_id))
|
||||
assert resp.status == 200
|
||||
assert len(hooks) == 1
|
||||
assert hooks[0][0] is hass
|
||||
assert hooks[0][1] == webhook_id
|
||||
assert hooks[0][2] == {}
|
Loading…
Add table
Reference in a new issue