core/tests/components/smartthings/test_config_flow.py

442 lines
16 KiB
Python

"""Tests for the SmartThings config flow module."""
from uuid import uuid4
from aiohttp import ClientResponseError
from asynctest import Mock, patch
from pysmartthings import APIResponseError
from pysmartthings.installedapp import format_install_url
from homeassistant import data_entry_flow
from homeassistant.components.smartthings import smartapp
from homeassistant.components.smartthings.config_flow import SmartThingsFlowHandler
from homeassistant.components.smartthings.const import (
CONF_APP_ID,
CONF_INSTALLED_APP_ID,
CONF_LOCATION_ID,
CONF_OAUTH_CLIENT_ID,
CONF_OAUTH_CLIENT_SECRET,
CONF_REFRESH_TOKEN,
DOMAIN,
)
from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_FORBIDDEN, HTTP_NOT_FOUND
from tests.common import MockConfigEntry, mock_coro
async def test_step_import(hass):
"""Test import returns user."""
flow = SmartThingsFlowHandler()
flow.hass = hass
result = await flow.async_step_import()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["description_placeholders"][
"webhook_url"
] == smartapp.get_webhook_url(hass)
async def test_step_user(hass):
"""Test the webhook confirmation is shown."""
flow = SmartThingsFlowHandler()
flow.hass = hass
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["description_placeholders"][
"webhook_url"
] == smartapp.get_webhook_url(hass)
async def test_step_user_aborts_invalid_webhook(hass):
"""Test flow aborts if webhook is invalid."""
hass.config.api.base_url = "http://0.0.0.0"
flow = SmartThingsFlowHandler()
flow.hass = hass
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "invalid_webhook_url"
assert result["description_placeholders"][
"webhook_url"
] == smartapp.get_webhook_url(hass)
assert "component_url" in result["description_placeholders"]
async def test_step_user_advances_to_pat(hass):
"""Test user step advances to the pat step."""
flow = SmartThingsFlowHandler()
flow.hass = hass
result = await flow.async_step_user({})
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "pat"
async def test_step_pat(hass):
"""Test pat step shows the input form."""
flow = SmartThingsFlowHandler()
flow.hass = hass
result = await flow.async_step_pat()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "pat"
assert result["errors"] == {}
assert result["data_schema"]({CONF_ACCESS_TOKEN: ""}) == {CONF_ACCESS_TOKEN: ""}
assert "token_url" in result["description_placeholders"]
assert "component_url" in result["description_placeholders"]
async def test_step_pat_defaults_token(hass):
"""Test pat form defaults the token from another entry."""
token = str(uuid4())
entry = MockConfigEntry(domain=DOMAIN, data={CONF_ACCESS_TOKEN: token})
entry.add_to_hass(hass)
flow = SmartThingsFlowHandler()
flow.hass = hass
result = await flow.async_step_pat()
assert flow.access_token == token
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "pat"
assert result["errors"] == {}
assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token}
assert "token_url" in result["description_placeholders"]
assert "component_url" in result["description_placeholders"]
async def test_step_pat_invalid_token(hass):
"""Test an error is shown for invalid token formats."""
flow = SmartThingsFlowHandler()
flow.hass = hass
token = "123456789"
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "pat"
assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token}
assert result["errors"] == {"access_token": "token_invalid_format"}
assert "token_url" in result["description_placeholders"]
assert "component_url" in result["description_placeholders"]
async def test_step_pat_unauthorized(hass, smartthings_mock):
"""Test an error is shown when the token is not authorized."""
flow = SmartThingsFlowHandler()
flow.hass = hass
request_info = Mock(real_url="http://example.com")
smartthings_mock.apps.side_effect = ClientResponseError(
request_info=request_info, history=None, status=401
)
token = str(uuid4())
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "pat"
assert result["errors"] == {CONF_ACCESS_TOKEN: "token_unauthorized"}
assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token}
async def test_step_pat_forbidden(hass, smartthings_mock):
"""Test an error is shown when the token is forbidden."""
flow = SmartThingsFlowHandler()
flow.hass = hass
request_info = Mock(real_url="http://example.com")
smartthings_mock.apps.side_effect = ClientResponseError(
request_info=request_info, history=None, status=HTTP_FORBIDDEN
)
token = str(uuid4())
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "pat"
assert result["errors"] == {CONF_ACCESS_TOKEN: "token_forbidden"}
assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token}
async def test_step_pat_webhook_error(hass, smartthings_mock):
"""Test an error is shown when there's an problem with the webhook endpoint."""
flow = SmartThingsFlowHandler()
flow.hass = hass
data = {"error": {}}
request_info = Mock(real_url="http://example.com")
error = APIResponseError(
request_info=request_info, history=None, data=data, status=422
)
error.is_target_error = Mock(return_value=True)
smartthings_mock.apps.side_effect = error
token = str(uuid4())
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "pat"
assert result["errors"] == {"base": "webhook_error"}
assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token}
async def test_step_pat_api_error(hass, smartthings_mock):
"""Test an error is shown when other API errors occur."""
flow = SmartThingsFlowHandler()
flow.hass = hass
data = {"error": {}}
request_info = Mock(real_url="http://example.com")
error = APIResponseError(
request_info=request_info, history=None, data=data, status=400
)
smartthings_mock.apps.side_effect = error
token = str(uuid4())
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "pat"
assert result["errors"] == {"base": "app_setup_error"}
assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token}
async def test_step_pat_unknown_api_error(hass, smartthings_mock):
"""Test an error is shown when there is an unknown API error."""
flow = SmartThingsFlowHandler()
flow.hass = hass
request_info = Mock(real_url="http://example.com")
smartthings_mock.apps.side_effect = ClientResponseError(
request_info=request_info, history=None, status=HTTP_NOT_FOUND
)
token = str(uuid4())
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "pat"
assert result["errors"] == {"base": "app_setup_error"}
assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token}
async def test_step_pat_unknown_error(hass, smartthings_mock):
"""Test an error is shown when there is an unknown API error."""
flow = SmartThingsFlowHandler()
flow.hass = hass
smartthings_mock.apps.side_effect = Exception("Unknown error")
token = str(uuid4())
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "pat"
assert result["errors"] == {"base": "app_setup_error"}
assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token}
async def test_step_pat_app_created_webhook(
hass, app, app_oauth_client, location, smartthings_mock
):
"""Test SmartApp is created when one does not exist and shows location form."""
flow = SmartThingsFlowHandler()
flow.hass = hass
smartthings_mock.apps.return_value = []
smartthings_mock.create_app.return_value = (app, app_oauth_client)
smartthings_mock.locations.return_value = [location]
token = str(uuid4())
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
assert flow.access_token == token
assert flow.app_id == app.app_id
assert flow.oauth_client_secret == app_oauth_client.client_secret
assert flow.oauth_client_id == app_oauth_client.client_id
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "select_location"
async def test_step_pat_app_created_cloudhook(
hass, app, app_oauth_client, location, smartthings_mock
):
"""Test SmartApp is created with a cloudhook and shows location form."""
hass.config.components.add("cloud")
# Unload the endpoint so we can reload it under the cloud.
await smartapp.unload_smartapp_endpoint(hass)
with patch.object(
hass.components.cloud, "async_active_subscription", return_value=True
), patch.object(
hass.components.cloud,
"async_create_cloudhook",
return_value=mock_coro("http://cloud.test"),
) as mock_create_cloudhook:
await smartapp.setup_smartapp_endpoint(hass)
flow = SmartThingsFlowHandler()
flow.hass = hass
smartthings_mock.apps.return_value = []
smartthings_mock.create_app.return_value = (app, app_oauth_client)
smartthings_mock.locations.return_value = [location]
token = str(uuid4())
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
assert flow.access_token == token
assert flow.app_id == app.app_id
assert flow.oauth_client_secret == app_oauth_client.client_secret
assert flow.oauth_client_id == app_oauth_client.client_id
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "select_location"
assert mock_create_cloudhook.call_count == 1
async def test_step_pat_app_updated_webhook(
hass, app, app_oauth_client, location, smartthings_mock
):
"""Test SmartApp is updated then show location form."""
flow = SmartThingsFlowHandler()
flow.hass = hass
smartthings_mock.apps.return_value = [app]
smartthings_mock.generate_app_oauth.return_value = app_oauth_client
smartthings_mock.locations.return_value = [location]
token = str(uuid4())
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
assert flow.access_token == token
assert flow.app_id == app.app_id
assert flow.oauth_client_secret == app_oauth_client.client_secret
assert flow.oauth_client_id == app_oauth_client.client_id
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "select_location"
async def test_step_pat_app_updated_webhook_from_existing_oauth_client(
hass, app, location, smartthings_mock
):
"""Test SmartApp is updated from existing then show location form."""
oauth_client_id = str(uuid4())
oauth_client_secret = str(uuid4())
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_APP_ID: app.app_id,
CONF_OAUTH_CLIENT_ID: oauth_client_id,
CONF_OAUTH_CLIENT_SECRET: oauth_client_secret,
CONF_LOCATION_ID: str(uuid4()),
},
)
entry.add_to_hass(hass)
flow = SmartThingsFlowHandler()
flow.hass = hass
smartthings_mock.apps.return_value = [app]
smartthings_mock.locations.return_value = [location]
token = str(uuid4())
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
assert flow.access_token == token
assert flow.app_id == app.app_id
assert flow.oauth_client_secret == oauth_client_secret
assert flow.oauth_client_id == oauth_client_id
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "select_location"
async def test_step_select_location(hass, location, smartthings_mock):
"""Test select location shows form with available locations."""
smartthings_mock.locations.return_value = [location]
flow = SmartThingsFlowHandler()
flow.hass = hass
flow.api = smartthings_mock
result = await flow.async_step_select_location()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "select_location"
assert result["data_schema"]({CONF_LOCATION_ID: location.location_id}) == {
CONF_LOCATION_ID: location.location_id
}
async def test_step_select_location_aborts(hass, location, smartthings_mock):
"""Test select location aborts if no available locations."""
smartthings_mock.locations.return_value = [location]
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_LOCATION_ID: location.location_id}
)
entry.add_to_hass(hass)
flow = SmartThingsFlowHandler()
flow.hass = hass
flow.api = smartthings_mock
result = await flow.async_step_select_location()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "no_available_locations"
async def test_step_select_location_advances(hass):
"""Test select location aborts if no available locations."""
location_id = str(uuid4())
app_id = str(uuid4())
flow = SmartThingsFlowHandler()
flow.hass = hass
flow.app_id = app_id
result = await flow.async_step_select_location({CONF_LOCATION_ID: location_id})
assert flow.location_id == location_id
assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
assert result["step_id"] == "authorize"
assert result["url"] == format_install_url(app_id, location_id)
async def test_step_authorize_advances(hass):
"""Test authorize step advances when completed."""
installed_app_id = str(uuid4())
refresh_token = str(uuid4())
flow = SmartThingsFlowHandler()
flow.hass = hass
result = await flow.async_step_authorize(
{CONF_INSTALLED_APP_ID: installed_app_id, CONF_REFRESH_TOKEN: refresh_token}
)
assert flow.installed_app_id == installed_app_id
assert flow.refresh_token == refresh_token
assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP_DONE
assert result["step_id"] == "install"
async def test_step_install_creates_entry(hass, location, smartthings_mock):
"""Test a config entry is created once the app is installed."""
flow = SmartThingsFlowHandler()
flow.hass = hass
flow.api = smartthings_mock
flow.access_token = str(uuid4())
flow.app_id = str(uuid4())
flow.installed_app_id = str(uuid4())
flow.location_id = location.location_id
flow.oauth_client_id = str(uuid4())
flow.oauth_client_secret = str(uuid4())
flow.refresh_token = str(uuid4())
result = await flow.async_step_install()
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"]["app_id"] == flow.app_id
assert result["data"]["installed_app_id"] == flow.installed_app_id
assert result["data"]["location_id"] == flow.location_id
assert result["data"]["access_token"] == flow.access_token
assert result["data"]["refresh_token"] == flow.refresh_token
assert result["data"]["client_secret"] == flow.oauth_client_secret
assert result["data"]["client_id"] == flow.oauth_client_id
assert result["title"] == location.name