From 76fe02c5b18cab82d1336d4b2333f249317243b9 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 4 Jan 2024 08:20:12 +0000 Subject: [PATCH 1/8] Guard with a lock mobile_app cloud hook creation --- homeassistant/components/mobile_app/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index cb5c0ae5c3ddd2..a43273a1f98a7e 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,4 +1,5 @@ """Integrates Native Apps to Home Assistant.""" +import asyncio from contextlib import suppress from typing import Any @@ -103,12 +104,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: registration_name = f"Mobile App: {registration[ATTR_DEVICE_NAME]}" webhook_register(hass, DOMAIN, registration_name, webhook_id, handle_webhook) + cloud_hook_lock = asyncio.Lock() + async def create_cloud_hook() -> None: """Create a cloud hook.""" - hook = await cloud.async_create_cloudhook(hass, webhook_id) - hass.config_entries.async_update_entry( - entry, data={**entry.data, CONF_CLOUDHOOK_URL: hook} - ) + async with cloud_hook_lock: + if CONF_CLOUDHOOK_URL in entry.data: + return # We already have a cloudhook + hook = await cloud.async_create_cloudhook(hass, webhook_id) + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_CLOUDHOOK_URL: hook} + ) async def manage_cloudhook(state: cloud.CloudConnectionState) -> None: if ( @@ -123,6 +129,7 @@ async def manage_cloudhook(state: cloud.CloudConnectionState) -> None: and cloud.async_is_connected(hass) ): await create_cloud_hook() + entry.async_on_unload(cloud.async_listen_connection_change(hass, manage_cloudhook)) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) From d7ea05eaddfe70393092d62e04f428c89111fc47 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 4 Jan 2024 16:02:44 +0000 Subject: [PATCH 2/8] Add async_get_or_create_cloudhook --- homeassistant/components/cloud/__init__.py | 18 +++++++ .../components/mobile_app/__init__.py | 4 +- tests/components/cloud/test_init.py | 49 ++++++++++++++++++- 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index d7d57835e3ade1..7f814cf0aa4db0 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -5,6 +5,7 @@ from collections.abc import Awaitable, Callable from datetime import datetime, timedelta from enum import Enum +from typing import cast from hass_nabucasa import Cloud import voluptuous as vol @@ -176,6 +177,23 @@ def async_active_subscription(hass: HomeAssistant) -> bool: return async_is_logged_in(hass) and not hass.data[DOMAIN].subscription_expired +@bind_hass +async def async_get_or_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str: + """Get or create a cloudhook.""" + if not async_is_connected(hass): + raise CloudNotConnected + + if not async_is_logged_in(hass): + raise CloudNotAvailable + + cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloudhooks = cloud.client.cloudhooks + if hook := cloudhooks.get(webhook_id): + return cast(str, hook["cloudhook_url"]) + + return await async_create_cloudhook(hass, webhook_id) + + @bind_hass async def async_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str: """Create a cloudhook.""" diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index a43273a1f98a7e..1acb428067dd23 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -109,9 +109,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def create_cloud_hook() -> None: """Create a cloud hook.""" async with cloud_hook_lock: - if CONF_CLOUDHOOK_URL in entry.data: - return # We already have a cloudhook - hook = await cloud.async_create_cloudhook(hass, webhook_id) + hook = await cloud.async_get_or_create_cloudhook(hass, webhook_id) hass.config_entries.async_update_entry( entry, data={**entry.data, CONF_CLOUDHOOK_URL: hook} ) diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index e12775d5a4a5f4..c12f9314bce52e 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -6,8 +6,9 @@ import pytest from homeassistant.components import cloud +from homeassistant.components.cloud.client import CloudClient from homeassistant.components.cloud.const import DOMAIN -from homeassistant.components.cloud.prefs import STORAGE_KEY +from homeassistant.components.cloud.prefs import STORAGE_KEY, CloudPreferences from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import Unauthorized @@ -214,3 +215,49 @@ async def test_remote_ui_url(hass: HomeAssistant, mock_cloud_fixture) -> None: 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, mock_cloud_fixture: CloudPreferences +) -> None: + """Test async_get_or_create_cloudhook.""" + cl: Cloud[CloudClient] = hass.data[DOMAIN] + webhook_id = "mock-webhook-id" + + # Not connected + with pytest.raises(cloud.CloudNotConnected): + await cloud.async_get_or_create_cloudhook(hass, webhook_id) + + # Simulate connected + cl.iot.state = "connected" + + # Not logged in + with pytest.raises(cloud.CloudNotAvailable): + await cloud.async_get_or_create_cloudhook(hass, webhook_id) + + cloudhook_url = "https://cloudhook.nabu.casa/abcdefg" + + with patch.object(cloud, "async_is_logged_in", return_value=True), patch.object( + cloud, "async_create_cloudhook", return_value=cloudhook_url + ) as async_create_cloudhook_mock: + # create cloudhook as it does not exist + assert ( + await cloud.async_get_or_create_cloudhook(hass, webhook_id) + ) == cloudhook_url + async_create_cloudhook_mock.assert_called_once_with(hass, webhook_id) + + mock_cloud_fixture._prefs[cloud.const.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 cloud.async_get_or_create_cloudhook(hass, webhook_id) == cloudhook_url + ) + async_create_cloudhook_mock.assert_not_called() From 93a3193f8e07790d9669f85f2fdf4fc9b04d4de3 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 4 Jan 2024 16:25:16 +0000 Subject: [PATCH 3/8] Fix tests --- tests/components/mobile_app/test_init.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/components/mobile_app/test_init.py b/tests/components/mobile_app/test_init.py index d504703c222339..6a365e84fb0f1f 100644 --- a/tests/components/mobile_app/test_init.py +++ b/tests/components/mobile_app/test_init.py @@ -88,15 +88,17 @@ async def _test_create_cloud_hook( ), patch( "homeassistant.components.cloud.async_is_connected", return_value=True ), patch( - "homeassistant.components.cloud.async_create_cloudhook", autospec=True - ) as mock_create_cloudhook: + "homeassistant.components.cloud.async_get_or_create_cloudhook", autospec=True + ) as mock_async_get_or_create_cloudhook: cloud_hook = "https://hook-url" - mock_create_cloudhook.return_value = cloud_hook + mock_async_get_or_create_cloudhook.return_value = cloud_hook assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - await additional_steps(config_entry, mock_create_cloudhook, cloud_hook) + await additional_steps( + config_entry, mock_async_get_or_create_cloudhook, cloud_hook + ) async def test_create_cloud_hook_on_setup( From 0d4d10945e1bb0e56adcfed0c5cf4b5a947b9cb6 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 4 Jan 2024 17:00:41 +0000 Subject: [PATCH 4/8] Implement suggestions --- homeassistant/components/cloud/__init__.py | 1 - .../components/mobile_app/__init__.py | 29 +++++++++++-------- .../components/mobile_app/http_api.py | 5 ++-- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 7f814cf0aa4db0..6e5cddd0f28764 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -177,7 +177,6 @@ def async_active_subscription(hass: HomeAssistant) -> bool: return async_is_logged_in(hass) and not hass.data[DOMAIN].subscription_expired -@bind_hass async def async_get_or_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str: """Get or create a cloudhook.""" if not async_is_connected(hass): diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 1acb428067dd23..e02a8b32763010 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -43,6 +43,21 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) +_CLOUD_HOOK_LOCK = asyncio.Lock() + + +async def create_cloud_hook( + hass: HomeAssistant, webhook_id: str, entry: ConfigEntry | None +) -> str: + """Create a cloud hook.""" + async with _CLOUD_HOOK_LOCK: + hook = await cloud.async_get_or_create_cloudhook(hass, webhook_id) + if entry: + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_CLOUDHOOK_URL: hook} + ) + return hook + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the mobile app component.""" @@ -104,29 +119,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: registration_name = f"Mobile App: {registration[ATTR_DEVICE_NAME]}" webhook_register(hass, DOMAIN, registration_name, webhook_id, handle_webhook) - cloud_hook_lock = asyncio.Lock() - - async def create_cloud_hook() -> None: - """Create a cloud hook.""" - async with cloud_hook_lock: - hook = await cloud.async_get_or_create_cloudhook(hass, webhook_id) - hass.config_entries.async_update_entry( - entry, data={**entry.data, CONF_CLOUDHOOK_URL: hook} - ) - async def manage_cloudhook(state: cloud.CloudConnectionState) -> None: if ( state is cloud.CloudConnectionState.CLOUD_CONNECTED and CONF_CLOUDHOOK_URL not in entry.data ): - await create_cloud_hook() + await create_cloud_hook(hass, webhook_id, entry) if ( CONF_CLOUDHOOK_URL not in entry.data and cloud.async_active_subscription(hass) and cloud.async_is_connected(hass) ): - await create_cloud_hook() + await create_cloud_hook(hass, webhook_id, entry) entry.async_on_unload(cloud.async_listen_connection_change(hass, manage_cloudhook)) diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 3c34a291df15e9..c506f85089eaeb 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -16,6 +16,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.util import slugify +from . import create_cloud_hook from .const import ( ATTR_APP_DATA, ATTR_APP_ID, @@ -69,9 +70,7 @@ async def post(self, request: Request, data: dict) -> Response: webhook_id = secrets.token_hex() if cloud.async_active_subscription(hass): - data[CONF_CLOUDHOOK_URL] = await cloud.async_create_cloudhook( - hass, webhook_id - ) + data[CONF_CLOUDHOOK_URL] = await create_cloud_hook(hass, webhook_id, None) data[CONF_WEBHOOK_ID] = webhook_id From 4b2d9731e930a2c07cd95c129a953382327b98e8 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 4 Jan 2024 17:19:21 +0000 Subject: [PATCH 5/8] Fix import cycle --- .../components/mobile_app/__init__.py | 17 +-------------- .../components/mobile_app/http_api.py | 2 +- homeassistant/components/mobile_app/util.py | 21 +++++++++++++++++++ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index e02a8b32763010..9350cf4bb042d8 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,5 +1,4 @@ """Integrates Native Apps to Home Assistant.""" -import asyncio from contextlib import suppress from typing import Any @@ -37,27 +36,13 @@ ) from .helpers import savable_state from .http_api import RegistrationsView +from .util import create_cloud_hook from .webhook import handle_webhook PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER] CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) -_CLOUD_HOOK_LOCK = asyncio.Lock() - - -async def create_cloud_hook( - hass: HomeAssistant, webhook_id: str, entry: ConfigEntry | None -) -> str: - """Create a cloud hook.""" - async with _CLOUD_HOOK_LOCK: - hook = await cloud.async_get_or_create_cloudhook(hass, webhook_id) - if entry: - hass.config_entries.async_update_entry( - entry, data={**entry.data, CONF_CLOUDHOOK_URL: hook} - ) - return hook - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the mobile app component.""" diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index c506f85089eaeb..6d075ba54bbdd0 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -16,7 +16,6 @@ from homeassistant.helpers import config_validation as cv from homeassistant.util import slugify -from . import create_cloud_hook from .const import ( ATTR_APP_DATA, ATTR_APP_ID, @@ -36,6 +35,7 @@ SCHEMA_APP_DATA, ) from .helpers import supports_encryption +from .util import create_cloud_hook class RegistrationsView(HomeAssistantView): diff --git a/homeassistant/components/mobile_app/util.py b/homeassistant/components/mobile_app/util.py index 45641861e5cd61..f8786da9040f4d 100644 --- a/homeassistant/components/mobile_app/util.py +++ b/homeassistant/components/mobile_app/util.py @@ -1,8 +1,11 @@ """Mobile app utility functions.""" from __future__ import annotations +import asyncio from typing import TYPE_CHECKING +from homeassistant.components import cloud +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from .const import ( @@ -10,6 +13,7 @@ ATTR_PUSH_TOKEN, ATTR_PUSH_URL, ATTR_PUSH_WEBSOCKET_CHANNEL, + CONF_CLOUDHOOK_URL, DATA_CONFIG_ENTRIES, DATA_DEVICES, DATA_NOTIFY, @@ -53,3 +57,20 @@ def get_notify_service(hass: HomeAssistant, webhook_id: str) -> str | None: return target_service return None + + +_CLOUD_HOOK_LOCK = asyncio.Lock() + + +@callback +async def create_cloud_hook( + hass: HomeAssistant, webhook_id: str, entry: ConfigEntry | None +) -> str: + """Create a cloud hook.""" + async with _CLOUD_HOOK_LOCK: + hook = await cloud.async_get_or_create_cloudhook(hass, webhook_id) + if entry: + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_CLOUDHOOK_URL: hook} + ) + return hook From 11b1b80f2c44d3b642febb9c6e2a2ce37731aa5d Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 5 Jan 2024 08:43:45 +0000 Subject: [PATCH 6/8] Implement suggestions --- homeassistant/components/mobile_app/__init__.py | 6 +++--- homeassistant/components/mobile_app/http_api.py | 6 ++++-- homeassistant/components/mobile_app/util.py | 3 +-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 9350cf4bb042d8..124ef750baa128 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -36,7 +36,7 @@ ) from .helpers import savable_state from .http_api import RegistrationsView -from .util import create_cloud_hook +from .util import async_create_cloud_hook from .webhook import handle_webhook PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER] @@ -109,14 +109,14 @@ async def manage_cloudhook(state: cloud.CloudConnectionState) -> None: state is cloud.CloudConnectionState.CLOUD_CONNECTED and CONF_CLOUDHOOK_URL not in entry.data ): - await create_cloud_hook(hass, webhook_id, entry) + await async_create_cloud_hook(hass, webhook_id, entry) if ( CONF_CLOUDHOOK_URL not in entry.data and cloud.async_active_subscription(hass) and cloud.async_is_connected(hass) ): - await create_cloud_hook(hass, webhook_id, entry) + await async_create_cloud_hook(hass, webhook_id, entry) entry.async_on_unload(cloud.async_listen_connection_change(hass, manage_cloudhook)) diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 6d075ba54bbdd0..92bb473d51acdb 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -35,7 +35,7 @@ SCHEMA_APP_DATA, ) from .helpers import supports_encryption -from .util import create_cloud_hook +from .util import async_create_cloud_hook class RegistrationsView(HomeAssistantView): @@ -70,7 +70,9 @@ async def post(self, request: Request, data: dict) -> Response: webhook_id = secrets.token_hex() if cloud.async_active_subscription(hass): - data[CONF_CLOUDHOOK_URL] = await create_cloud_hook(hass, webhook_id, None) + data[CONF_CLOUDHOOK_URL] = await async_create_cloud_hook( + hass, webhook_id, None + ) data[CONF_WEBHOOK_ID] = webhook_id diff --git a/homeassistant/components/mobile_app/util.py b/homeassistant/components/mobile_app/util.py index f8786da9040f4d..a7871d935edf37 100644 --- a/homeassistant/components/mobile_app/util.py +++ b/homeassistant/components/mobile_app/util.py @@ -62,8 +62,7 @@ def get_notify_service(hass: HomeAssistant, webhook_id: str) -> str | None: _CLOUD_HOOK_LOCK = asyncio.Lock() -@callback -async def create_cloud_hook( +async def async_create_cloud_hook( hass: HomeAssistant, webhook_id: str, entry: ConfigEntry | None ) -> str: """Create a cloud hook.""" From fe141c7d42b7416d31670f9948088b835b404c44 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 5 Jan 2024 09:23:16 +0000 Subject: [PATCH 7/8] use cloud fixture --- tests/components/cloud/conftest.py | 1 + tests/components/cloud/test_init.py | 67 ++++++++++++++++++----------- 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index ef8cb037cdbf72..42852b15206110 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -109,6 +109,7 @@ def mock_is_connected() -> bool: is_connected = PropertyMock(side_effect=mock_is_connected) type(mock_cloud).is_connected = is_connected + type(mock_cloud.iot).connected = is_connected # Properties that we mock as attributes. mock_cloud.expiration_date = utcnow() diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index c12f9314bce52e..8fa237fda17ec7 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -1,14 +1,14 @@ """Test the cloud component.""" +from collections.abc import Callable, Coroutine from typing import Any -from unittest.mock import patch +from unittest.mock import MagicMock, patch from hass_nabucasa import Cloud import pytest from homeassistant.components import cloud -from homeassistant.components.cloud.client import CloudClient from homeassistant.components.cloud.const import DOMAIN -from homeassistant.components.cloud.prefs import STORAGE_KEY, CloudPreferences +from homeassistant.components.cloud.prefs import STORAGE_KEY from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import Unauthorized @@ -217,27 +217,25 @@ async def test_remote_ui_url(hass: HomeAssistant, mock_cloud_fixture) -> None: assert cloud.async_remote_ui_url(hass) == "https://example.com" +@pytest.fixture +def cloud_mock(cloud: MagicMock) -> MagicMock: + """Rename cloud mock.""" + return cloud + + async def test_async_get_or_create_cloudhook( - hass: HomeAssistant, mock_cloud_fixture: CloudPreferences + hass: HomeAssistant, + cloud_mock: MagicMock, + set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]], ) -> None: """Test async_get_or_create_cloudhook.""" - cl: Cloud[CloudClient] = hass.data[DOMAIN] - webhook_id = "mock-webhook-id" - - # Not connected - with pytest.raises(cloud.CloudNotConnected): - await cloud.async_get_or_create_cloudhook(hass, webhook_id) - - # Simulate connected - cl.iot.state = "connected" - - # Not logged in - with pytest.raises(cloud.CloudNotAvailable): - await cloud.async_get_or_create_cloudhook(hass, webhook_id) + assert await async_setup_component(hass, "cloud", {"cloud": {}}) + await hass.async_block_till_done() + webhook_id = "mock-webhook-id" cloudhook_url = "https://cloudhook.nabu.casa/abcdefg" - with patch.object(cloud, "async_is_logged_in", return_value=True), patch.object( + with patch.object( cloud, "async_create_cloudhook", return_value=cloudhook_url ) as async_create_cloudhook_mock: # create cloudhook as it does not exist @@ -246,14 +244,19 @@ async def test_async_get_or_create_cloudhook( ) == cloudhook_url async_create_cloudhook_mock.assert_called_once_with(hass, webhook_id) - mock_cloud_fixture._prefs[cloud.const.PREF_CLOUDHOOKS] = { - webhook_id: { - "webhook_id": webhook_id, - "cloudhook_id": "random-id", - "cloudhook_url": cloudhook_url, - "managed": True, + await set_cloud_prefs( + { + cloud.const.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 @@ -261,3 +264,17 @@ async def test_async_get_or_create_cloudhook( await cloud.async_get_or_create_cloudhook(hass, webhook_id) == cloudhook_url ) async_create_cloudhook_mock.assert_not_called() + + # Simulate logged out + cloud_mock.id_token = None + + # Not logged in + with pytest.raises(cloud.CloudNotAvailable): + await cloud.async_get_or_create_cloudhook(hass, webhook_id) + + # Simulate disconnected + cloud_mock.iot.state = "disconnected" + + # Not connected + with pytest.raises(cloud.CloudNotConnected): + await cloud.async_get_or_create_cloudhook(hass, webhook_id) From 267b5bdad2a8ae104ea47ff2ab0a39fd9ddebdaa Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 5 Jan 2024 09:26:25 +0000 Subject: [PATCH 8/8] Remove rename cloud mock fixture --- tests/components/cloud/test_init.py | 42 +++++++++++++---------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 8fa237fda17ec7..850f8e12e02085 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -7,7 +7,12 @@ import pytest from homeassistant.components import cloud -from homeassistant.components.cloud.const import DOMAIN +from homeassistant.components.cloud import ( + CloudNotAvailable, + CloudNotConnected, + async_get_or_create_cloudhook, +) +from homeassistant.components.cloud.const import DOMAIN, PREF_CLOUDHOOKS from homeassistant.components.cloud.prefs import STORAGE_KEY from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Context, HomeAssistant @@ -217,15 +222,9 @@ async def test_remote_ui_url(hass: HomeAssistant, mock_cloud_fixture) -> None: assert cloud.async_remote_ui_url(hass) == "https://example.com" -@pytest.fixture -def cloud_mock(cloud: MagicMock) -> MagicMock: - """Rename cloud mock.""" - return cloud - - async def test_async_get_or_create_cloudhook( hass: HomeAssistant, - cloud_mock: MagicMock, + cloud: MagicMock, set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]], ) -> None: """Test async_get_or_create_cloudhook.""" @@ -235,18 +234,17 @@ async def test_async_get_or_create_cloudhook( webhook_id = "mock-webhook-id" cloudhook_url = "https://cloudhook.nabu.casa/abcdefg" - with patch.object( - cloud, "async_create_cloudhook", return_value=cloudhook_url + 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 cloud.async_get_or_create_cloudhook(hass, webhook_id) - ) == cloudhook_url + 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( { - cloud.const.PREF_CLOUDHOOKS: { + PREF_CLOUDHOOKS: { webhook_id: { "webhook_id": webhook_id, "cloudhook_id": "random-id", @@ -260,21 +258,19 @@ async def test_async_get_or_create_cloudhook( async_create_cloudhook_mock.reset_mock() # get cloudhook as it exists - assert ( - await cloud.async_get_or_create_cloudhook(hass, webhook_id) == cloudhook_url - ) + assert await async_get_or_create_cloudhook(hass, webhook_id) == cloudhook_url async_create_cloudhook_mock.assert_not_called() # Simulate logged out - cloud_mock.id_token = None + cloud.id_token = None # Not logged in - with pytest.raises(cloud.CloudNotAvailable): - await cloud.async_get_or_create_cloudhook(hass, webhook_id) + with pytest.raises(CloudNotAvailable): + await async_get_or_create_cloudhook(hass, webhook_id) # Simulate disconnected - cloud_mock.iot.state = "disconnected" + cloud.iot.state = "disconnected" # Not connected - with pytest.raises(cloud.CloudNotConnected): - await cloud.async_get_or_create_cloudhook(hass, webhook_id) + with pytest.raises(CloudNotConnected): + await async_get_or_create_cloudhook(hass, webhook_id)