"""Test config entries API.""" import asyncio from collections import OrderedDict from unittest.mock import patch import pytest import voluptuous as vol from homeassistant import config_entries as core_ce, data_entry_flow from homeassistant.config_entries import HANDLERS from homeassistant.core import callback from homeassistant.setup import async_setup_component from homeassistant.components.config import config_entries from homeassistant.loader import set_component from tests.common import MockConfigEntry, MockModule, mock_coro_func @pytest.fixture(autouse=True) def mock_test_component(hass): """Ensure a component called 'test' exists.""" set_component(hass, 'test', MockModule('test')) @pytest.fixture def client(hass, hass_client): """Fixture that can interact with the config manager API.""" hass.loop.run_until_complete(async_setup_component(hass, 'http', {})) hass.loop.run_until_complete(config_entries.async_setup(hass)) yield hass.loop.run_until_complete(hass_client()) async def test_get_entries(hass, client): """Test get entries.""" MockConfigEntry( domain='comp', title='Test 1', source='bla', connection_class=core_ce.CONN_CLASS_LOCAL_POLL, ).add_to_hass(hass) MockConfigEntry( domain='comp2', title='Test 2', source='bla2', state=core_ce.ENTRY_STATE_LOADED, connection_class=core_ce.CONN_CLASS_ASSUMED, ).add_to_hass(hass) class CompConfigFlow: @staticmethod @callback def async_get_options_flow(config, options): pass HANDLERS['comp'] = CompConfigFlow() class Comp2ConfigFlow: def __init__(self): pass HANDLERS['comp2'] = Comp2ConfigFlow() resp = await client.get('/api/config/config_entries/entry') assert resp.status == 200 data = await resp.json() for entry in data: entry.pop('entry_id') assert data == [ { 'domain': 'comp', 'title': 'Test 1', 'source': 'bla', 'state': 'not_loaded', 'connection_class': 'local_poll', 'supports_options': True, }, { 'domain': 'comp2', 'title': 'Test 2', 'source': 'bla2', 'state': 'loaded', 'connection_class': 'assumed', 'supports_options': False, }, ] @asyncio.coroutine def test_remove_entry(hass, client): """Test removing an entry via the API.""" entry = MockConfigEntry(domain='demo', state=core_ce.ENTRY_STATE_LOADED) entry.add_to_hass(hass) resp = yield from client.delete( '/api/config/config_entries/entry/{}'.format(entry.entry_id)) assert resp.status == 200 data = yield from resp.json() assert data == { 'require_restart': True } assert len(hass.config_entries.async_entries()) == 0 async def test_remove_entry_unauth(hass, client, hass_admin_user): """Test removing an entry via the API.""" hass_admin_user.groups = [] entry = MockConfigEntry(domain='demo', state=core_ce.ENTRY_STATE_LOADED) entry.add_to_hass(hass) resp = await client.delete( '/api/config/config_entries/entry/{}'.format(entry.entry_id)) assert resp.status == 401 assert len(hass.config_entries.async_entries()) == 1 @asyncio.coroutine def test_available_flows(hass, client): """Test querying the available flows.""" with patch.object(core_ce, 'FLOWS', ['hello', 'world']): resp = yield from client.get( '/api/config/config_entries/flow_handlers') assert resp.status == 200 data = yield from resp.json() assert data == ['hello', 'world'] ############################ # FLOW MANAGER API TESTS # ############################ @asyncio.coroutine def test_initialize_flow(hass, client): """Test we can initialize a flow.""" class TestFlow(core_ce.ConfigFlow): @asyncio.coroutine def async_step_user(self, user_input=None): schema = OrderedDict() schema[vol.Required('username')] = str schema[vol.Required('password')] = str return self.async_show_form( step_id='user', data_schema=schema, description_placeholders={ 'url': 'https://example.com', }, errors={ 'username': 'Should be unique.' } ) with patch.dict(HANDLERS, {'test': TestFlow}): resp = yield from client.post('/api/config/config_entries/flow', json={'handler': 'test'}) assert resp.status == 200 data = yield from resp.json() data.pop('flow_id') assert data == { 'type': 'form', 'handler': 'test', 'step_id': 'user', 'data_schema': [ { 'name': 'username', 'required': True, 'type': 'string' }, { 'name': 'password', 'required': True, 'type': 'string' } ], 'description_placeholders': { 'url': 'https://example.com', }, 'errors': { 'username': 'Should be unique.' } } async def test_initialize_flow_unauth(hass, client, hass_admin_user): """Test we can initialize a flow.""" hass_admin_user.groups = [] class TestFlow(core_ce.ConfigFlow): @asyncio.coroutine def async_step_user(self, user_input=None): schema = OrderedDict() schema[vol.Required('username')] = str schema[vol.Required('password')] = str return self.async_show_form( step_id='user', data_schema=schema, description_placeholders={ 'url': 'https://example.com', }, errors={ 'username': 'Should be unique.' } ) with patch.dict(HANDLERS, {'test': TestFlow}): resp = await client.post('/api/config/config_entries/flow', json={'handler': 'test'}) assert resp.status == 401 @asyncio.coroutine def test_abort(hass, client): """Test a flow that aborts.""" class TestFlow(core_ce.ConfigFlow): @asyncio.coroutine def async_step_user(self, user_input=None): return self.async_abort(reason='bla') with patch.dict(HANDLERS, {'test': TestFlow}): resp = yield from client.post('/api/config/config_entries/flow', json={'handler': 'test'}) assert resp.status == 200 data = yield from resp.json() data.pop('flow_id') assert data == { 'handler': 'test', 'reason': 'bla', 'type': 'abort' } @asyncio.coroutine def test_create_account(hass, client): """Test a flow that creates an account.""" set_component( hass, 'test', MockModule('test', async_setup_entry=mock_coro_func(True))) class TestFlow(core_ce.ConfigFlow): VERSION = 1 @asyncio.coroutine def async_step_user(self, user_input=None): return self.async_create_entry( title='Test Entry', data={'secret': 'account_token'} ) with patch.dict(HANDLERS, {'test': TestFlow}): resp = yield from client.post('/api/config/config_entries/flow', json={'handler': 'test'}) assert resp.status == 200 data = yield from resp.json() data.pop('flow_id') assert data == { 'handler': 'test', 'title': 'Test Entry', 'type': 'create_entry', 'version': 1, 'description': None, 'description_placeholders': None, } @asyncio.coroutine def test_two_step_flow(hass, client): """Test we can finish a two step flow.""" set_component( hass, 'test', MockModule('test', async_setup_entry=mock_coro_func(True))) class TestFlow(core_ce.ConfigFlow): VERSION = 1 @asyncio.coroutine def async_step_user(self, user_input=None): return self.async_show_form( step_id='account', data_schema=vol.Schema({ 'user_title': str })) @asyncio.coroutine def async_step_account(self, user_input=None): return self.async_create_entry( title=user_input['user_title'], data={'secret': 'account_token'} ) with patch.dict(HANDLERS, {'test': TestFlow}): resp = yield from client.post('/api/config/config_entries/flow', json={'handler': 'test'}) assert resp.status == 200 data = yield from resp.json() flow_id = data.pop('flow_id') assert data == { 'type': 'form', 'handler': 'test', 'step_id': 'account', 'data_schema': [ { 'name': 'user_title', 'type': 'string' } ], 'description_placeholders': None, 'errors': None } with patch.dict(HANDLERS, {'test': TestFlow}): resp = yield from client.post( '/api/config/config_entries/flow/{}'.format(flow_id), json={'user_title': 'user-title'}) assert resp.status == 200 data = yield from resp.json() data.pop('flow_id') assert data == { 'handler': 'test', 'type': 'create_entry', 'title': 'user-title', 'version': 1, 'description': None, 'description_placeholders': None, } async def test_continue_flow_unauth(hass, client, hass_admin_user): """Test we can't finish a two step flow.""" set_component( hass, 'test', MockModule('test', async_setup_entry=mock_coro_func(True))) class TestFlow(core_ce.ConfigFlow): VERSION = 1 @asyncio.coroutine def async_step_user(self, user_input=None): return self.async_show_form( step_id='account', data_schema=vol.Schema({ 'user_title': str })) @asyncio.coroutine def async_step_account(self, user_input=None): return self.async_create_entry( title=user_input['user_title'], data={'secret': 'account_token'}, ) with patch.dict(HANDLERS, {'test': TestFlow}): resp = await client.post('/api/config/config_entries/flow', json={'handler': 'test'}) assert resp.status == 200 data = await resp.json() flow_id = data.pop('flow_id') assert data == { 'type': 'form', 'handler': 'test', 'step_id': 'account', 'data_schema': [ { 'name': 'user_title', 'type': 'string' } ], 'description_placeholders': None, 'errors': None } hass_admin_user.groups = [] resp = await client.post( '/api/config/config_entries/flow/{}'.format(flow_id), json={'user_title': 'user-title'}) assert resp.status == 401 @asyncio.coroutine def test_get_progress_index(hass, client): """Test querying for the flows that are in progress.""" class TestFlow(core_ce.ConfigFlow): VERSION = 5 @asyncio.coroutine def async_step_hassio(self, info): return (yield from self.async_step_account()) @asyncio.coroutine def async_step_account(self, user_input=None): return self.async_show_form( step_id='account', ) with patch.dict(HANDLERS, {'test': TestFlow}): form = yield from hass.config_entries.flow.async_init( 'test', context={'source': 'hassio'}) resp = yield from client.get('/api/config/config_entries/flow') assert resp.status == 200 data = yield from resp.json() assert data == [ { 'flow_id': form['flow_id'], 'handler': 'test', 'context': {'source': 'hassio'} } ] async def test_get_progress_index_unauth(hass, client, hass_admin_user): """Test we can't get flows that are in progress.""" hass_admin_user.groups = [] resp = await client.get('/api/config/config_entries/flow') assert resp.status == 401 @asyncio.coroutine def test_get_progress_flow(hass, client): """Test we can query the API for same result as we get from init a flow.""" class TestFlow(core_ce.ConfigFlow): @asyncio.coroutine def async_step_user(self, user_input=None): schema = OrderedDict() schema[vol.Required('username')] = str schema[vol.Required('password')] = str return self.async_show_form( step_id='user', data_schema=schema, errors={ 'username': 'Should be unique.' } ) with patch.dict(HANDLERS, {'test': TestFlow}): resp = yield from client.post('/api/config/config_entries/flow', json={'handler': 'test'}) assert resp.status == 200 data = yield from resp.json() resp2 = yield from client.get( '/api/config/config_entries/flow/{}'.format(data['flow_id'])) assert resp2.status == 200 data2 = yield from resp2.json() assert data == data2 async def test_get_progress_flow_unauth(hass, client, hass_admin_user): """Test we can can't query the API for result of flow.""" class TestFlow(core_ce.ConfigFlow): async def async_step_user(self, user_input=None): schema = OrderedDict() schema[vol.Required('username')] = str schema[vol.Required('password')] = str return self.async_show_form( step_id='user', data_schema=schema, errors={ 'username': 'Should be unique.' } ) with patch.dict(HANDLERS, {'test': TestFlow}): resp = await client.post('/api/config/config_entries/flow', json={'handler': 'test'}) assert resp.status == 200 data = await resp.json() hass_admin_user.groups = [] resp2 = await client.get( '/api/config/config_entries/flow/{}'.format(data['flow_id'])) assert resp2.status == 401 async def test_options_flow(hass, client): """Test we can change options.""" class TestFlow(core_ce.ConfigFlow): @staticmethod @callback def async_get_options_flow(config, options): class OptionsFlowHandler(data_entry_flow.FlowHandler): def __init__(self, config, options): self.config = config self.options = options async def async_step_init(self, user_input=None): schema = OrderedDict() schema[vol.Required('enabled')] = bool return self.async_show_form( step_id='user', data_schema=schema, description_placeholders={ 'enabled': 'Set to true to be true', } ) return OptionsFlowHandler(config, options) MockConfigEntry( domain='test', entry_id='test1', source='bla', connection_class=core_ce.CONN_CLASS_LOCAL_POLL, ).add_to_hass(hass) entry = hass.config_entries._entries[0] with patch.dict(HANDLERS, {'test': TestFlow}): url = '/api/config/config_entries/entry/option/flow' resp = await client.post(url, json={'handler': entry.entry_id}) assert resp.status == 200 data = await resp.json() data.pop('flow_id') assert data == { 'type': 'form', 'handler': 'test1', 'step_id': 'user', 'data_schema': [ { 'name': 'enabled', 'required': True, 'type': 'boolean' }, ], 'description_placeholders': { 'enabled': 'Set to true to be true', }, 'errors': None } async def test_two_step_options_flow(hass, client): """Test we can finish a two step options flow.""" set_component( hass, 'test', MockModule('test', async_setup_entry=mock_coro_func(True))) class TestFlow(core_ce.ConfigFlow): @staticmethod @callback def async_get_options_flow(config, options): class OptionsFlowHandler(data_entry_flow.FlowHandler): def __init__(self, config, options): self.config = config self.options = options async def async_step_init(self, user_input=None): return self.async_show_form( step_id='finish', data_schema=vol.Schema({ 'enabled': bool }) ) async def async_step_finish(self, user_input=None): return self.async_create_entry( title='Enable disable', data=user_input ) return OptionsFlowHandler(config, options) MockConfigEntry( domain='test', entry_id='test1', source='bla', connection_class=core_ce.CONN_CLASS_LOCAL_POLL, ).add_to_hass(hass) entry = hass.config_entries._entries[0] with patch.dict(HANDLERS, {'test': TestFlow}): url = '/api/config/config_entries/entry/option/flow' resp = await client.post(url, json={'handler': entry.entry_id}) assert resp.status == 200 data = await resp.json() flow_id = data.pop('flow_id') assert data == { 'type': 'form', 'handler': 'test1', 'step_id': 'finish', 'data_schema': [ { 'name': 'enabled', 'type': 'boolean' } ], 'description_placeholders': None, 'errors': None } with patch.dict(HANDLERS, {'test': TestFlow}): resp = await client.post( '/api/config/config_entries/options/flow/{}'.format(flow_id), json={'enabled': True}) assert resp.status == 200 data = await resp.json() data.pop('flow_id') assert data == { 'handler': 'test1', 'type': 'create_entry', 'title': 'Enable disable', 'version': 1, 'description': None, 'description_placeholders': None, }