core/tests/components/cloud/test_init.py

378 lines
13 KiB
Python
Raw Normal View History

"""Test the cloud component."""
from collections.abc import Callable, Coroutine
from typing import Any
from unittest.mock import MagicMock, patch
from urllib.parse import quote_plus
2021-01-01 21:31:56 +00:00
from hass_nabucasa import Cloud
import pytest
from homeassistant.components import cloud
from homeassistant.components.cloud import (
CloudNotAvailable,
CloudNotConnected,
async_get_or_create_cloudhook,
)
from homeassistant.components.cloud.const import (
DOMAIN,
PREF_CLOUDHOOKS,
PREF_STRICT_CONNECTION,
)
from homeassistant.components.cloud.prefs import STORAGE_KEY
from homeassistant.components.http.const import StrictConnectionMode
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Context, HomeAssistant
from homeassistant.exceptions import ServiceValidationError, Unauthorized
from homeassistant.setup import async_setup_component
2024-01-12 08:47:08 +00:00
from tests.common import MockConfigEntry, MockUser
async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None:
"""Test non-dev mode loads info from SERVERS constant."""
with patch("hass_nabucasa.Cloud.initialize"):
2019-07-31 19:25:30 +00:00
result = await async_setup_component(
hass,
"cloud",
{
"http": {},
"cloud": {
cloud.CONF_MODE: cloud.MODE_DEV,
"cognito_client_id": "test-cognito_client_id",
"user_pool_id": "test-user_pool_id",
"region": "test-region",
"relayer_server": "test-relayer-server",
"accounts_server": "test-acounts-server",
"cloudhook_server": "test-cloudhook-server",
"alexa_server": "test-alexa-server",
"acme_server": "test-acme-server",
"remotestate_server": "test-remotestate-server",
2019-07-31 19:25:30 +00:00
},
},
)
assert result
2019-07-31 19:25:30 +00:00
cl = hass.data["cloud"]
assert cl.mode == cloud.MODE_DEV
2019-07-31 19:25:30 +00:00
assert cl.cognito_client_id == "test-cognito_client_id"
assert cl.user_pool_id == "test-user_pool_id"
assert cl.region == "test-region"
assert cl.relayer_server == "test-relayer-server"
assert cl.iot.ws_server_url == "wss://test-relayer-server/websocket"
assert cl.accounts_server == "test-acounts-server"
assert cl.cloudhook_server == "test-cloudhook-server"
assert cl.alexa_server == "test-alexa-server"
assert cl.acme_server == "test-acme-server"
assert cl.remotestate_server == "test-remotestate-server"
async def test_remote_services(
hass: HomeAssistant, mock_cloud_fixture, hass_read_only_user: MockUser
) -> None:
"""Setup cloud component and test services."""
cloud = hass.data[DOMAIN]
2019-07-31 19:25:30 +00:00
assert hass.services.has_service(DOMAIN, "remote_connect")
assert hass.services.has_service(DOMAIN, "remote_disconnect")
2020-04-30 23:31:00 +00:00
with patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect:
await hass.services.async_call(DOMAIN, "remote_connect", blocking=True)
await hass.async_block_till_done()
assert mock_connect.called
assert cloud.client.remote_autostart
2019-03-09 20:15:16 +00:00
2020-04-30 23:31:00 +00:00
with patch("hass_nabucasa.remote.RemoteUI.disconnect") as mock_disconnect:
2019-07-31 19:25:30 +00:00
await hass.services.async_call(DOMAIN, "remote_disconnect", blocking=True)
await hass.async_block_till_done()
2019-03-09 20:15:16 +00:00
assert mock_disconnect.called
assert not cloud.client.remote_autostart
2019-03-09 20:15:16 +00:00
# Test admin access required
non_admin_context = Context(user_id=hass_read_only_user.id)
with (
patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect,
pytest.raises(Unauthorized),
2020-04-30 23:31:00 +00:00
):
2019-07-31 19:25:30 +00:00
await hass.services.async_call(
DOMAIN, "remote_connect", blocking=True, context=non_admin_context
)
assert mock_connect.called is False
with (
patch("hass_nabucasa.remote.RemoteUI.disconnect") as mock_disconnect,
pytest.raises(Unauthorized),
):
await hass.services.async_call(
2019-07-31 19:25:30 +00:00
DOMAIN, "remote_disconnect", blocking=True, context=non_admin_context
)
assert mock_disconnect.called is False
2019-03-09 20:15:16 +00:00
async def test_shutdown_event(hass: HomeAssistant, mock_cloud_fixture) -> None:
"""Test if the cloud will stop on shutdown event."""
2020-04-30 23:31:00 +00:00
with patch("hass_nabucasa.Cloud.stop") as mock_stop:
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
2019-03-09 20:15:16 +00:00
assert mock_stop.called
async def test_setup_existing_cloud_user(
hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
"""Test setup with API push default data."""
2019-07-31 19:25:30 +00:00
user = await hass.auth.async_create_system_user("Cloud test")
hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": user.id}}
with patch("hass_nabucasa.Cloud.initialize"):
2019-07-31 19:25:30 +00:00
result = await async_setup_component(
hass,
"cloud",
{
"http": {},
"cloud": {
cloud.CONF_MODE: cloud.MODE_DEV,
"cognito_client_id": "test-cognito_client_id",
"user_pool_id": "test-user_pool_id",
"region": "test-region",
"relayer_server": "test-relayer-serer",
2019-07-31 19:25:30 +00:00
},
},
)
assert result
2019-07-31 19:25:30 +00:00
assert hass_storage[STORAGE_KEY]["data"]["cloud_user"] == user.id
async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None:
"""Test cloud on connect triggers."""
cl: Cloud[cloud.client.CloudClient] = hass.data["cloud"]
assert len(cl.iot._on_connect) == 3
2019-07-31 19:25:30 +00:00
assert len(hass.states.async_entity_ids("binary_sensor")) == 0
cloud_states = []
def handle_state(cloud_state):
nonlocal cloud_states
cloud_states.append(cloud_state)
cloud.async_listen_connection_change(hass, handle_state)
2019-07-31 19:25:30 +00:00
assert "async_setup" in str(cl.iot._on_connect[-1])
await cl.iot._on_connect[-1]()
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids("binary_sensor")) == 0
# The on_start callback discovers the binary sensor platform
assert "async_setup" in str(cl._on_start[-1])
await cl._on_start[-1]()
await hass.async_block_till_done()
2019-07-31 19:25:30 +00:00
assert len(hass.states.async_entity_ids("binary_sensor")) == 1
2020-04-30 23:31:00 +00:00
with patch("homeassistant.helpers.discovery.async_load_platform") as mock_load:
await cl._on_start[-1]()
await hass.async_block_till_done()
assert len(mock_load.mock_calls) == 0
assert len(cloud_states) == 1
assert cloud_states[-1] == cloud.CloudConnectionState.CLOUD_CONNECTED
await cl.iot._on_connect[-1]()
await hass.async_block_till_done()
assert len(cloud_states) == 2
assert cloud_states[-1] == cloud.CloudConnectionState.CLOUD_CONNECTED
assert len(cl.iot._on_disconnect) == 2
assert "async_setup" in str(cl.iot._on_disconnect[-1])
await cl.iot._on_disconnect[-1]()
await hass.async_block_till_done()
assert len(cloud_states) == 3
assert cloud_states[-1] == cloud.CloudConnectionState.CLOUD_DISCONNECTED
await cl.iot._on_disconnect[-1]()
await hass.async_block_till_done()
assert len(cloud_states) == 4
assert cloud_states[-1] == cloud.CloudConnectionState.CLOUD_DISCONNECTED
async def test_remote_ui_url(hass: HomeAssistant, mock_cloud_fixture) -> None:
"""Test getting remote ui url."""
cl = hass.data["cloud"]
# Not logged in
with pytest.raises(cloud.CloudNotAvailable):
cloud.async_remote_ui_url(hass)
with patch.object(cloud, "async_is_logged_in", return_value=True):
# Remote not enabled
with pytest.raises(cloud.CloudNotAvailable):
cloud.async_remote_ui_url(hass)
with patch.object(cl.remote, "connect"):
await cl.client.prefs.async_update(remote_enabled=True)
await hass.async_block_till_done()
# No instance domain
with pytest.raises(cloud.CloudNotAvailable):
cloud.async_remote_ui_url(hass)
# Remote finished initializing
cl.client.prefs._prefs["remote_domain"] = "example.com"
assert cloud.async_remote_ui_url(hass) == "https://example.com"
async def test_async_get_or_create_cloudhook(
hass: HomeAssistant,
cloud: MagicMock,
set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
) -> None:
"""Test async_get_or_create_cloudhook."""
assert await async_setup_component(hass, "cloud", {"cloud": {}})
await hass.async_block_till_done()
2024-01-12 08:47:08 +00:00
await cloud.login("test-user", "test-pass")
webhook_id = "mock-webhook-id"
cloudhook_url = "https://cloudhook.nabu.casa/abcdefg"
with patch(
"homeassistant.components.cloud.async_create_cloudhook",
return_value=cloudhook_url,
) as async_create_cloudhook_mock:
# create cloudhook as it does not exist
assert (await async_get_or_create_cloudhook(hass, webhook_id)) == cloudhook_url
async_create_cloudhook_mock.assert_called_once_with(hass, webhook_id)
await set_cloud_prefs(
{
PREF_CLOUDHOOKS: {
webhook_id: {
"webhook_id": webhook_id,
"cloudhook_id": "random-id",
"cloudhook_url": cloudhook_url,
"managed": True,
}
}
}
)
async_create_cloudhook_mock.reset_mock()
# get cloudhook as it exists
assert await async_get_or_create_cloudhook(hass, webhook_id) == cloudhook_url
async_create_cloudhook_mock.assert_not_called()
# Simulate logged out
2024-01-12 08:47:08 +00:00
await cloud.logout()
# Not logged in
with pytest.raises(CloudNotAvailable):
await async_get_or_create_cloudhook(hass, webhook_id)
# Simulate disconnected
cloud.iot.state = "disconnected"
# Not connected
with pytest.raises(CloudNotConnected):
await async_get_or_create_cloudhook(hass, webhook_id)
2024-01-12 08:47:08 +00:00
async def test_cloud_logout(
hass: HomeAssistant,
cloud: MagicMock,
) -> None:
"""Test cloud setup with existing config entry when user is logged out."""
assert cloud.is_logged_in is False
mock_config_entry = MockConfigEntry(domain=DOMAIN)
mock_config_entry.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {"cloud": {}})
await hass.async_block_till_done()
assert cloud.is_logged_in is False
async def test_service_create_temporary_strict_connection_url_strict_connection_disabled(
hass: HomeAssistant,
) -> None:
"""Test service create_temporary_strict_connection_url with strict_connection not enabled."""
mock_config_entry = MockConfigEntry(domain=DOMAIN)
mock_config_entry.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {"cloud": {}})
await hass.async_block_till_done()
with pytest.raises(
ServiceValidationError,
match="Strict connection is not enabled for cloud requests",
):
await hass.services.async_call(
cloud.DOMAIN,
"create_temporary_strict_connection_url",
blocking=True,
return_response=True,
)
@pytest.mark.parametrize(
("mode"),
[
StrictConnectionMode.DROP_CONNECTION,
StrictConnectionMode.GUARD_PAGE,
],
)
async def test_service_create_temporary_strict_connection(
hass: HomeAssistant,
set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
mode: StrictConnectionMode,
) -> None:
"""Test service create_temporary_strict_connection_url."""
mock_config_entry = MockConfigEntry(domain=DOMAIN)
mock_config_entry.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {"cloud": {}})
await hass.async_block_till_done()
await set_cloud_prefs(
{
PREF_STRICT_CONNECTION: mode,
}
)
# No cloud url set
with pytest.raises(ServiceValidationError, match="No cloud URL available"):
await hass.services.async_call(
cloud.DOMAIN,
"create_temporary_strict_connection_url",
blocking=True,
return_response=True,
)
# Patch cloud url
url = "https://example.com"
with patch(
"homeassistant.helpers.network._get_cloud_url",
return_value=url,
):
response = await hass.services.async_call(
cloud.DOMAIN,
"create_temporary_strict_connection_url",
blocking=True,
return_response=True,
)
assert isinstance(response, dict)
direct_url_prefix = f"{url}/auth/strict_connection/temp_token?authSig="
assert response.pop("direct_url").startswith(direct_url_prefix)
assert response.pop("url").startswith(
f"https://login.home-assistant.io?u={quote_plus(direct_url_prefix)}"
)
assert response == {} # No more keys in response