mirror of https://github.com/home-assistant/core
568 lines
18 KiB
Python
568 lines
18 KiB
Python
"""Test the cloud.iot module."""
|
|
|
|
from collections.abc import Callable, Coroutine
|
|
from datetime import timedelta
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch
|
|
|
|
import aiohttp
|
|
from aiohttp import web
|
|
from hass_nabucasa.client import RemoteActivationNotAllowed
|
|
import pytest
|
|
|
|
from homeassistant.components import webhook
|
|
from homeassistant.components.cloud import DOMAIN
|
|
from homeassistant.components.cloud.client import (
|
|
VALID_REPAIR_TRANSLATION_KEYS,
|
|
CloudClient,
|
|
)
|
|
from homeassistant.components.cloud.const import (
|
|
DATA_CLOUD,
|
|
PREF_ALEXA_REPORT_STATE,
|
|
PREF_ENABLE_ALEXA,
|
|
PREF_ENABLE_GOOGLE,
|
|
)
|
|
from homeassistant.components.cloud.prefs import CloudPreferences
|
|
from homeassistant.components.homeassistant.exposed_entities import (
|
|
DATA_EXPOSED_ENTITIES,
|
|
async_expose_entity,
|
|
)
|
|
from homeassistant.const import CONTENT_TYPE_JSON, __version__ as HA_VERSION
|
|
from homeassistant.core import HomeAssistant, State
|
|
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
|
from homeassistant.setup import async_setup_component
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
from . import mock_cloud, mock_cloud_prefs
|
|
|
|
from tests.common import async_fire_time_changed
|
|
from tests.components.alexa import test_smart_home as test_alexa
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_cloud_inst() -> MagicMock:
|
|
"""Mock cloud class."""
|
|
return MagicMock(subscription_expired=False)
|
|
|
|
|
|
async def test_handler_alexa(hass: HomeAssistant) -> None:
|
|
"""Test handler Alexa."""
|
|
hass.states.async_set("switch.test", "on", {"friendly_name": "Test switch"})
|
|
hass.states.async_set("switch.test2", "on", {"friendly_name": "Test switch 2"})
|
|
|
|
await mock_cloud(
|
|
hass,
|
|
{
|
|
"alexa": {
|
|
"filter": {"exclude_entities": "switch.test2"},
|
|
"entity_config": {
|
|
"switch.test": {
|
|
"name": "Config name",
|
|
"description": "Config description",
|
|
"display_categories": "LIGHT",
|
|
}
|
|
},
|
|
}
|
|
},
|
|
)
|
|
|
|
mock_cloud_prefs(hass, {PREF_ALEXA_REPORT_STATE: False})
|
|
cloud = hass.data[DATA_CLOUD]
|
|
|
|
resp = await cloud.client.async_alexa_message(
|
|
test_alexa.get_new_request("Alexa.Discovery", "Discover")
|
|
)
|
|
|
|
endpoints = resp["event"]["payload"]["endpoints"]
|
|
|
|
assert len(endpoints) == 1
|
|
device = endpoints[0]
|
|
|
|
assert device["description"] == "Config description via Home Assistant"
|
|
assert device["friendlyName"] == "Config name"
|
|
assert device["displayCategories"] == ["LIGHT"]
|
|
assert device["manufacturerName"] == "Home Assistant"
|
|
|
|
|
|
async def test_handler_alexa_disabled(
|
|
hass: HomeAssistant, mock_cloud_fixture: CloudPreferences
|
|
) -> None:
|
|
"""Test handler Alexa when user has disabled it."""
|
|
mock_cloud_fixture._prefs[PREF_ENABLE_ALEXA] = False
|
|
cloud = hass.data[DATA_CLOUD]
|
|
|
|
resp = await cloud.client.async_alexa_message(
|
|
test_alexa.get_new_request("Alexa.Discovery", "Discover")
|
|
)
|
|
|
|
assert resp["event"]["header"]["namespace"] == "Alexa"
|
|
assert resp["event"]["header"]["name"] == "ErrorResponse"
|
|
assert resp["event"]["payload"]["type"] == "BRIDGE_UNREACHABLE"
|
|
|
|
|
|
async def test_handler_google_actions(hass: HomeAssistant) -> None:
|
|
"""Test handler Google Actions."""
|
|
hass.states.async_set("switch.test", "on", {"friendly_name": "Test switch"})
|
|
hass.states.async_set("switch.test2", "on", {"friendly_name": "Test switch 2"})
|
|
hass.states.async_set("group.all_locks", "on", {"friendly_name": "Evil locks"})
|
|
|
|
await mock_cloud(
|
|
hass,
|
|
{
|
|
"google_actions": {
|
|
"filter": {"exclude_entities": "switch.test2"},
|
|
"entity_config": {
|
|
"switch.test": {
|
|
"name": "Config name",
|
|
"aliases": "Config alias",
|
|
"room": "living room",
|
|
}
|
|
},
|
|
}
|
|
},
|
|
)
|
|
|
|
mock_cloud_prefs(hass, {})
|
|
cloud = hass.data[DATA_CLOUD]
|
|
|
|
reqid = "5711642932632160983"
|
|
data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]}
|
|
|
|
with patch(
|
|
"hass_nabucasa.Cloud._decode_claims",
|
|
return_value={"cognito:username": "myUserName"},
|
|
):
|
|
await cloud.client.get_google_config()
|
|
resp = await cloud.client.async_google_message(data)
|
|
|
|
assert resp["requestId"] == reqid
|
|
payload = resp["payload"]
|
|
|
|
assert payload["agentUserId"] == "myUserName"
|
|
|
|
devices = payload["devices"]
|
|
assert len(devices) == 1
|
|
|
|
device = devices[0]
|
|
assert device["id"] == "switch.test"
|
|
assert device["name"]["name"] == "Config name"
|
|
assert device["name"]["nicknames"] == ["Config name", "Config alias"]
|
|
assert device["type"] == "action.devices.types.SWITCH"
|
|
assert device["roomHint"] == "living room"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("intent", "response_payload"),
|
|
[
|
|
("action.devices.SYNC", {"agentUserId": "myUserName", "devices": []}),
|
|
("action.devices.QUERY", {"errorCode": "deviceTurnedOff"}),
|
|
],
|
|
)
|
|
async def test_handler_google_actions_disabled(
|
|
hass: HomeAssistant,
|
|
mock_cloud_fixture: CloudPreferences,
|
|
intent: str,
|
|
response_payload: dict[str, Any],
|
|
) -> None:
|
|
"""Test handler Google Actions when user has disabled it."""
|
|
mock_cloud_fixture._prefs[PREF_ENABLE_GOOGLE] = False
|
|
|
|
with patch("hass_nabucasa.Cloud.initialize"):
|
|
assert await async_setup_component(hass, "cloud", {})
|
|
|
|
reqid = "5711642932632160983"
|
|
data = {"requestId": reqid, "inputs": [{"intent": intent}]}
|
|
|
|
cloud = hass.data[DATA_CLOUD]
|
|
with patch(
|
|
"hass_nabucasa.Cloud._decode_claims",
|
|
return_value={"cognito:username": "myUserName"},
|
|
):
|
|
resp = await cloud.client.async_google_message(data)
|
|
|
|
assert resp["requestId"] == reqid
|
|
assert resp["payload"] == response_payload
|
|
|
|
|
|
async def test_handler_ice_servers(
|
|
hass: HomeAssistant,
|
|
cloud: MagicMock,
|
|
set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
|
|
) -> None:
|
|
"""Test handler ICE servers."""
|
|
assert await async_setup_component(hass, "cloud", {"cloud": {}})
|
|
await hass.async_block_till_done()
|
|
# make sure that preferences will not be reset
|
|
await cloud.client.prefs.async_set_username(cloud.username)
|
|
await set_cloud_prefs(
|
|
{
|
|
"alexa_enabled": False,
|
|
"google_enabled": False,
|
|
}
|
|
)
|
|
|
|
await cloud.login("test-user", "test-pass")
|
|
await cloud.client.cloud_connected()
|
|
|
|
assert cloud.client._cloud_ice_servers_listener is not None
|
|
assert cloud.client._cloud_ice_servers_listener() == "mock-unregister"
|
|
|
|
|
|
async def test_handler_ice_servers_disabled(
|
|
hass: HomeAssistant,
|
|
cloud: MagicMock,
|
|
set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
|
|
) -> None:
|
|
"""Test handler ICE servers when user has disabled it."""
|
|
assert await async_setup_component(hass, "cloud", {"cloud": {}})
|
|
await hass.async_block_till_done()
|
|
# make sure that preferences will not be reset
|
|
await cloud.client.prefs.async_set_username(cloud.username)
|
|
await set_cloud_prefs(
|
|
{
|
|
"alexa_enabled": False,
|
|
"google_enabled": False,
|
|
}
|
|
)
|
|
|
|
await cloud.login("test-user", "test-pass")
|
|
await cloud.client.cloud_connected()
|
|
|
|
await set_cloud_prefs(
|
|
{
|
|
"cloud_ice_servers_enabled": False,
|
|
}
|
|
)
|
|
|
|
assert cloud.client._cloud_ice_servers_listener is None
|
|
|
|
|
|
async def test_webhook_msg(
|
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""Test webhook msg."""
|
|
with patch("hass_nabucasa.Cloud.initialize"):
|
|
setup = await async_setup_component(hass, "cloud", {"cloud": {}})
|
|
assert setup
|
|
cloud = hass.data[DATA_CLOUD]
|
|
|
|
await cloud.client.prefs.async_initialize()
|
|
await cloud.client.prefs.async_update(
|
|
cloudhooks={
|
|
"mock-webhook-id": {
|
|
"webhook_id": "mock-webhook-id",
|
|
"cloudhook_id": "mock-cloud-id",
|
|
},
|
|
"no-longere-existing": {
|
|
"webhook_id": "no-longere-existing",
|
|
"cloudhook_id": "mock-nonexisting-id",
|
|
},
|
|
}
|
|
)
|
|
|
|
received = []
|
|
|
|
async def handler(
|
|
hass: HomeAssistant, webhook_id: str, request: web.Request
|
|
) -> web.Response:
|
|
"""Handle a webhook."""
|
|
received.append(request)
|
|
return web.json_response({"from": "handler"})
|
|
|
|
webhook.async_register(hass, "test", "Test", "mock-webhook-id", handler)
|
|
|
|
response = await cloud.client.async_webhook_message(
|
|
{
|
|
"cloudhook_id": "mock-cloud-id",
|
|
"body": '{"hello": "world"}',
|
|
"headers": {"content-type": CONTENT_TYPE_JSON},
|
|
"method": "POST",
|
|
"query": None,
|
|
}
|
|
)
|
|
|
|
assert response == {
|
|
"status": 200,
|
|
"body": '{"from": "handler"}',
|
|
"headers": {"Content-Type": CONTENT_TYPE_JSON},
|
|
}
|
|
|
|
assert len(received) == 1
|
|
assert await received[0].json() == {"hello": "world"}
|
|
|
|
# Non existing webhook
|
|
caplog.clear()
|
|
|
|
response = await cloud.client.async_webhook_message(
|
|
{
|
|
"cloudhook_id": "mock-nonexisting-id",
|
|
"body": '{"nonexisting": "payload"}',
|
|
"headers": {"content-type": CONTENT_TYPE_JSON},
|
|
"method": "POST",
|
|
"query": None,
|
|
}
|
|
)
|
|
|
|
assert response == {
|
|
"status": 200,
|
|
"body": None,
|
|
"headers": {"Content-Type": "application/octet-stream"},
|
|
}
|
|
|
|
assert (
|
|
"Received message for unregistered webhook no-longere-existing from cloud"
|
|
in caplog.text
|
|
)
|
|
assert '{"nonexisting": "payload"}' in caplog.text
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_cloud_setup", "mock_cloud_login")
|
|
async def test_google_config_expose_entity(
|
|
hass: HomeAssistant,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Test Google config exposing entity method uses latest config."""
|
|
|
|
# Enable exposing new entities to Google
|
|
exposed_entities = hass.data[DATA_EXPOSED_ENTITIES]
|
|
exposed_entities.async_set_expose_new_entities("cloud.google_assistant", True)
|
|
|
|
# Register a light entity
|
|
entity_entry = entity_registry.async_get_or_create(
|
|
"light", "test", "unique", suggested_object_id="kitchen"
|
|
)
|
|
|
|
cloud_client = hass.data[DATA_CLOUD].client
|
|
state = State(entity_entry.entity_id, "on")
|
|
gconf = await cloud_client.get_google_config()
|
|
|
|
assert gconf.should_expose(state)
|
|
|
|
async_expose_entity(hass, "cloud.google_assistant", entity_entry.entity_id, False)
|
|
|
|
assert not gconf.should_expose(state)
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_cloud_setup", "mock_cloud_login")
|
|
async def test_google_config_should_2fa(
|
|
hass: HomeAssistant,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Test Google config disabling 2FA method uses latest config."""
|
|
|
|
# Register a light entity
|
|
entity_entry = entity_registry.async_get_or_create(
|
|
"light", "test", "unique", suggested_object_id="kitchen"
|
|
)
|
|
|
|
cloud_client = hass.data[DATA_CLOUD].client
|
|
gconf = await cloud_client.get_google_config()
|
|
state = State(entity_entry.entity_id, "on")
|
|
|
|
assert gconf.should_2fa(state)
|
|
|
|
entity_registry.async_update_entity_options(
|
|
entity_entry.entity_id, "cloud.google_assistant", {"disable_2fa": True}
|
|
)
|
|
|
|
assert not gconf.should_2fa(state)
|
|
|
|
|
|
async def test_set_username(hass: HomeAssistant) -> None:
|
|
"""Test we set username during login."""
|
|
prefs = MagicMock(
|
|
alexa_enabled=False,
|
|
google_enabled=False,
|
|
async_set_username=AsyncMock(return_value=None),
|
|
)
|
|
client = CloudClient(hass, prefs, None, {}, {})
|
|
client.cloud = MagicMock(is_logged_in=True, username="mock-username")
|
|
await client.cloud_connected()
|
|
|
|
assert len(prefs.async_set_username.mock_calls) == 1
|
|
assert prefs.async_set_username.mock_calls[0][1][0] == "mock-username"
|
|
|
|
|
|
async def test_login_recovers_bad_internet(
|
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""Test Alexa can recover bad auth."""
|
|
prefs = Mock(
|
|
alexa_enabled=True,
|
|
google_enabled=False,
|
|
async_set_username=AsyncMock(return_value=None),
|
|
)
|
|
client = CloudClient(hass, prefs, None, {}, {})
|
|
client.cloud = Mock()
|
|
client._alexa_config = Mock(
|
|
async_enable_proactive_mode=Mock(side_effect=aiohttp.ClientError)
|
|
)
|
|
await client.cloud_connected()
|
|
assert len(client._alexa_config.async_enable_proactive_mode.mock_calls) == 1
|
|
assert "Unable to activate Alexa Report State" in caplog.text
|
|
|
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30))
|
|
await hass.async_block_till_done()
|
|
|
|
assert len(client._alexa_config.async_enable_proactive_mode.mock_calls) == 2
|
|
|
|
|
|
async def test_system_msg(hass: HomeAssistant) -> None:
|
|
"""Test system msg."""
|
|
with patch("hass_nabucasa.Cloud.initialize"):
|
|
setup = await async_setup_component(hass, "cloud", {"cloud": {}})
|
|
assert setup
|
|
cloud = hass.data[DATA_CLOUD]
|
|
|
|
assert cloud.client.relayer_region is None
|
|
|
|
response = await cloud.client.async_system_message(
|
|
{
|
|
"region": "xx-earth-616",
|
|
}
|
|
)
|
|
|
|
assert response is None
|
|
assert cloud.client.relayer_region == "xx-earth-616"
|
|
|
|
|
|
async def test_cloud_connection_info(hass: HomeAssistant) -> None:
|
|
"""Test connection info msg."""
|
|
with (
|
|
patch("hass_nabucasa.Cloud.initialize"),
|
|
patch("uuid.UUID.hex", new_callable=PropertyMock) as hexmock,
|
|
):
|
|
hexmock.return_value = "12345678901234567890"
|
|
setup = await async_setup_component(hass, "cloud", {"cloud": {}})
|
|
assert setup
|
|
cloud = hass.data[DATA_CLOUD]
|
|
|
|
response = await cloud.client.async_cloud_connection_info({})
|
|
|
|
assert response == {
|
|
"instance_id": "12345678901234567890",
|
|
"remote": {
|
|
"alias": None,
|
|
"can_enable": True,
|
|
"connected": False,
|
|
"enabled": False,
|
|
"instance_domain": None,
|
|
},
|
|
"version": HA_VERSION,
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"translation_key",
|
|
sorted(VALID_REPAIR_TRANSLATION_KEYS),
|
|
)
|
|
async def test_async_create_repair_issue_known(
|
|
cloud: MagicMock,
|
|
mock_cloud_setup: None,
|
|
issue_registry: ir.IssueRegistry,
|
|
translation_key: str,
|
|
) -> None:
|
|
"""Test create repair issue for known repairs."""
|
|
identifier = f"test_identifier_{translation_key}"
|
|
await cloud.client.async_create_repair_issue(
|
|
identifier=identifier,
|
|
translation_key=translation_key,
|
|
placeholders={"custom_domains": "example.com"},
|
|
severity="warning",
|
|
)
|
|
issue = issue_registry.async_get_issue(domain=DOMAIN, issue_id=identifier)
|
|
assert issue is not None
|
|
|
|
|
|
async def test_async_create_repair_issue_unknown(
|
|
cloud: MagicMock,
|
|
mock_cloud_setup: None,
|
|
issue_registry: ir.IssueRegistry,
|
|
) -> None:
|
|
"""Test not creating repair issue for unknown repairs."""
|
|
identifier = "abc123"
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="Invalid translation key unknown_translation_key",
|
|
):
|
|
await cloud.client.async_create_repair_issue(
|
|
identifier=identifier,
|
|
translation_key="unknown_translation_key",
|
|
placeholders={"custom_domains": "example.com"},
|
|
severity="error",
|
|
)
|
|
issue = issue_registry.async_get_issue(domain=DOMAIN, issue_id=identifier)
|
|
assert issue is None
|
|
|
|
|
|
async def test_disconnected(hass: HomeAssistant) -> None:
|
|
"""Test cleanup when disconnected from the cloud."""
|
|
prefs = MagicMock(
|
|
alexa_enabled=False,
|
|
google_enabled=True,
|
|
async_set_username=AsyncMock(return_value=None),
|
|
)
|
|
client = CloudClient(hass, prefs, None, {}, {})
|
|
client.cloud = MagicMock(is_logged_in=True, username="mock-username")
|
|
client._google_config = Mock()
|
|
client._google_config.async_disable_local_sdk.assert_not_called()
|
|
|
|
await client.cloud_disconnected()
|
|
client._google_config.async_disable_local_sdk.assert_called_once_with()
|
|
|
|
|
|
async def test_logged_out(
|
|
hass: HomeAssistant,
|
|
cloud: MagicMock,
|
|
) -> None:
|
|
"""Test cleanup when logged out from the cloud."""
|
|
|
|
assert await async_setup_component(hass, "cloud", {"cloud": {}})
|
|
await hass.async_block_till_done()
|
|
await cloud.login("test-user", "test-pass")
|
|
|
|
alexa_config_mock = Mock(async_enable_proactive_mode=AsyncMock())
|
|
google_config_mock = Mock(async_sync_entities=AsyncMock())
|
|
cloud.client._alexa_config = alexa_config_mock
|
|
cloud.client._google_config = google_config_mock
|
|
|
|
await cloud.client.cloud_connected()
|
|
await hass.async_block_till_done()
|
|
|
|
assert cloud.client._cloud_ice_servers_listener is not None
|
|
|
|
# Simulate logged out
|
|
await cloud.logout()
|
|
await hass.async_block_till_done()
|
|
|
|
# Check we clean up Alexa, Google and ICE servers
|
|
assert cloud.client._alexa_config is None
|
|
assert cloud.client._google_config is None
|
|
assert cloud.client._cloud_ice_servers_listener is None
|
|
google_config_mock.async_deinitialize.assert_called_once_with()
|
|
alexa_config_mock.async_deinitialize.assert_called_once_with()
|
|
|
|
|
|
async def test_remote_enable(hass: HomeAssistant) -> None:
|
|
"""Test enabling remote UI."""
|
|
prefs = MagicMock(async_update=AsyncMock(return_value=None))
|
|
client = CloudClient(hass, prefs, None, {}, {})
|
|
client.cloud = MagicMock(is_logged_in=True, username="mock-username")
|
|
|
|
await client.async_cloud_connect_update(True)
|
|
prefs.async_update.assert_called_once_with(remote_enabled=True)
|
|
|
|
|
|
async def test_remote_enable_not_allowed(hass: HomeAssistant) -> None:
|
|
"""Test enabling remote UI."""
|
|
prefs = MagicMock(
|
|
async_update=AsyncMock(return_value=None),
|
|
remote_allow_remote_enable=False,
|
|
)
|
|
client = CloudClient(hass, prefs, None, {}, {})
|
|
client.cloud = MagicMock(is_logged_in=True, username="mock-username")
|
|
|
|
with pytest.raises(RemoteActivationNotAllowed):
|
|
await client.async_cloud_connect_update(True)
|
|
prefs.async_update.assert_not_called()
|