442 lines
16 KiB
Python
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
|