diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index c742b69b7..534217a7b 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -42,27 +42,16 @@ SERVICE_TRIGGER_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ + vol.Optional(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 async_setup(hass, config): +async def async_setup(hass, config): """Set up the IFTTT service component.""" if DOMAIN not in config: - return + return True key = config[DOMAIN][CONF_KEY] @@ -87,11 +76,12 @@ def async_setup(hass, config): async def handle_webhook(hass, webhook_id, data): """Handle webhook callback.""" - data['webhook_id'] = webhook_id + if isinstance(data, dict): + data['webhook_id'] = webhook_id hass.bus.async_fire(EVENT_RECEIVED, data) -def async_setup_entry(hass, entry): +async def async_setup_entry(hass, entry): """Configure based on config entry.""" hass.components.webhook.async_register( entry.data['webhook_id'], handle_webhook) @@ -108,8 +98,6 @@ async def async_unload_entry(hass, entry): class ConfigFlow(config_entries.ConfigFlow): """Handle an IFTTT config flow.""" - webhook_id = None - async def async_step_user(self, user_input=None): """Handle a user initiated set up flow.""" if self._async_current_entries(): @@ -121,25 +109,22 @@ class ConfigFlow(config_entries.ConfigFlow): if is_local(ip_address(url_parts.hostname)): return self.async_abort(reason='not_internet_accessible') except ValueError: - return self.async_abort(reason='not_internet_accessible') - - if self.webhook_id is None: - self.webhook_id = \ - self.hass.components.webhook.async_generate_webhook_id() + # 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_webhook_url( - self.webhook_id) + self.hass.components.webhook.async_generate_url(webhook_id) return self.async_create_entry( title='IFTTT Webhook', data={ - CONF_WEBHOOK_ID: self.webhook_id + CONF_WEBHOOK_ID: webhook_id }, description_placeholders={ 'applet_url': 'https://ifttt.com/maker_webhooks', diff --git a/homeassistant/components/webhook.py b/homeassistant/components/webhook.py index 990fdaca9..14699e7d7 100644 --- a/homeassistant/components/webhook.py +++ b/homeassistant/components/webhook.py @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) @bind_hass def async_register(hass, webhook_id, handler): """Register a webhook.""" - handlers = hass.data.setdefault(webhook_id, {}) + handlers = hass.data.setdefault(DOMAIN, {}) if webhook_id in handlers: raise ValueError('Handler is already defined!') @@ -34,19 +34,19 @@ def async_register(hass, webhook_id, handler): @bind_hass def async_unregister(hass, webhook_id): """Remove a webhook.""" - handlers = hass.data.setdefault(webhook_id, {}) + handlers = hass.data.setdefault(DOMAIN, {}) handlers.pop(webhook_id, None) @callback -def async_generate_webhook_id(): +def async_generate_id(): """Generate a webhook_id.""" return generate_secret(entropy=32) @callback @bind_hass -def async_generate_webhook_url(hass, webhook_id): +def async_generate_url(hass, webhook_id): """Generate a webhook_id.""" return "{}/api/webhook/{}".format(hass.config.api.base_url, webhook_id) @@ -78,7 +78,7 @@ class WebhookView(HomeAssistantView): body = await request.text() try: - data = json.load(body) if body else {} + data = json.loads(body) if body else {} except ValueError: _LOGGER.warning( 'Received webhook %s with invalid JSON', webhook_id) diff --git a/tests/components/ifttt/__init__.py b/tests/components/ifttt/__init__.py new file mode 100644 index 000000000..2fe2f4027 --- /dev/null +++ b/tests/components/ifttt/__init__.py @@ -0,0 +1 @@ +"""Tests for the IFTTT component.""" diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py new file mode 100644 index 000000000..236ed57e7 --- /dev/null +++ b/tests/components/ifttt/test_init.py @@ -0,0 +1,54 @@ +"""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 + +# @pytest.fixture +# def mock_client(hass, aiohttp_client): +# """Create http client for webhooks.""" +# hass.loop.run_until_complete(async_setup_component(hass, 'http', {})) +# return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) + + + +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' diff --git a/tests/components/test_webhook.py b/tests/components/test_webhook.py new file mode 100644 index 000000000..c87687292 --- /dev/null +++ b/tests/components/test_webhook.py @@ -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] == {}