core/tests/components/cloud/test_client.py

505 lines
16 KiB
Python

"""Test the cloud.iot module."""
from datetime import timedelta
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 (
PREF_ALEXA_REPORT_STATE,
PREF_ENABLE_ALEXA,
PREF_ENABLE_GOOGLE,
)
from homeassistant.components.homeassistant.exposed_entities import (
DATA_EXPOSED_ENTITIES,
ExposedEntities,
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
from homeassistant.helpers.issue_registry import IssueRegistry
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():
"""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["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) -> None:
"""Test handler Alexa when user has disabled it."""
mock_cloud_fixture._prefs[PREF_ENABLE_ALEXA] = False
cloud = hass.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["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, intent, response_payload
) -> 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["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_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["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, webhook_id, request):
"""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
async def test_google_config_expose_entity(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_cloud_setup,
mock_cloud_login,
) -> None:
"""Test Google config exposing entity method uses latest config."""
# Enable exposing new entities to Google
exposed_entities: ExposedEntities = 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[DOMAIN].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)
async def test_google_config_should_2fa(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_cloud_setup,
mock_cloud_login,
) -> 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[DOMAIN].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["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["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: 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: 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()
# Simulate logged out
await cloud.logout()
await hass.async_block_till_done()
# Check we clean up Alexa and Google
assert cloud.client._alexa_config is None
assert cloud.client._google_config 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()