From c75222e63cda50a5b605a09bccbe6de38bfc801f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 25 Dec 2024 21:03:44 -0800 Subject: [PATCH] Bump python-google-nest-sdm to 7.0.0 (#134016) Update python-google-nest-sdm to 7.0.0 --- homeassistant/components/nest/__init__.py | 38 ++-- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nest/common.py | 78 ++------ tests/components/nest/conftest.py | 168 +++++++++++------- .../nest/snapshots/test_diagnostics.ambr | 6 + tests/components/nest/test_api.py | 32 ++-- tests/components/nest/test_camera.py | 7 +- tests/components/nest/test_climate.py | 11 +- tests/components/nest/test_config_flow.py | 116 ++++++------ tests/components/nest/test_device_trigger.py | 9 +- tests/components/nest/test_event.py | 25 +-- tests/components/nest/test_events.py | 77 ++++---- tests/components/nest/test_init.py | 97 +++++----- tests/components/nest/test_media_source.py | 27 +-- tests/components/nest/test_sensor.py | 9 +- 17 files changed, 329 insertions(+), 377 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 0bd2891914f..4349567b7f0 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -55,6 +55,7 @@ from homeassistant.helpers.typing import ConfigType from . import api from .const import ( + CONF_CLOUD_PROJECT_ID, CONF_PROJECT_ID, CONF_SUBSCRIBER_ID, CONF_SUBSCRIBER_ID_IMPORTED, @@ -214,33 +215,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: NestConfigEntry) -> bool update_callback = SignalUpdateCallback(hass, async_config_reload, entry) subscriber.set_update_callback(update_callback.async_handle_event) try: - await subscriber.start_async() + unsub = await subscriber.start_async() except AuthException as err: raise ConfigEntryAuthFailed( f"Subscriber authentication error: {err!s}" ) from err except ConfigurationException as err: _LOGGER.error("Configuration error: %s", err) - subscriber.stop_async() return False except SubscriberException as err: - subscriber.stop_async() raise ConfigEntryNotReady(f"Subscriber error: {err!s}") from err try: device_manager = await subscriber.async_get_device_manager() except ApiException as err: - subscriber.stop_async() + unsub() raise ConfigEntryNotReady(f"Device manager error: {err!s}") from err @callback def on_hass_stop(_: Event) -> None: """Close connection when hass stops.""" - subscriber.stop_async() + unsub() entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) ) + + entry.async_on_unload(unsub) entry.runtime_data = NestData( subscriber=subscriber, device_manager=device_manager, @@ -253,12 +254,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: NestConfigEntry) -> bool async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if DATA_SDM not in entry.data: - # Legacy API - return True - _LOGGER.debug("Stopping nest subscriber") - subscriber = entry.runtime_data.subscriber - subscriber.stop_async() return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @@ -272,24 +267,25 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: or CONF_SUBSCRIBER_ID_IMPORTED in entry.data ): return - - subscriber = await api.new_subscriber(hass, entry) - if not subscriber: - return - _LOGGER.debug("Deleting subscriber '%s'", subscriber.subscriber_id) + if (subscription_name := entry.data.get(CONF_SUBSCRIPTION_NAME)) is None: + subscription_name = entry.data[CONF_SUBSCRIBER_ID] + admin_client = api.new_pubsub_admin_client( + hass, + access_token=entry.data["token"]["access_token"], + cloud_project_id=entry.data[CONF_CLOUD_PROJECT_ID], + ) + _LOGGER.debug("Deleting subscription '%s'", subscription_name) try: - await subscriber.delete_subscription() - except (AuthException, SubscriberException) as err: + await admin_client.delete_subscription(subscription_name) + except ApiException as err: _LOGGER.warning( ( "Unable to delete subscription '%s'; Will be automatically cleaned up" " by cloud console: %s" ), - subscriber.subscriber_id, + subscription_name, err, ) - finally: - subscriber.stop_async() class NestEventViewBase(HomeAssistantView, ABC): diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 07c34c51568..e14474dc309 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -19,5 +19,5 @@ "documentation": "https://www.home-assistant.io/integrations/nest", "iot_class": "cloud_push", "loggers": ["google_nest_sdm"], - "requirements": ["google-nest-sdm==6.1.5"] + "requirements": ["google-nest-sdm==7.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index cbe8af9b71f..fa29d43faed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1024,7 +1024,7 @@ google-cloud-texttospeech==2.17.2 google-generativeai==0.8.2 # homeassistant.components.nest -google-nest-sdm==6.1.5 +google-nest-sdm==7.0.0 # homeassistant.components.google_photos google-photos-library-api==0.12.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 59aea0951ee..255100f2b31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -874,7 +874,7 @@ google-cloud-texttospeech==2.17.2 google-generativeai==0.8.2 # homeassistant.components.nest -google-nest-sdm==6.1.5 +google-nest-sdm==7.0.0 # homeassistant.components.google_photos google-photos-library-api==0.12.1 diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 8f1f0a2f074..803f6076ecc 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -5,17 +5,14 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Generator import copy from dataclasses import dataclass +import re from typing import Any -from google_nest_sdm.auth import AbstractAuth -from google_nest_sdm.device import Device -from google_nest_sdm.device_manager import DeviceManager -from google_nest_sdm.event import EventMessage -from google_nest_sdm.event_media import CachePolicy -from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber +from google_nest_sdm.streaming_manager import Message from homeassistant.components.application_credentials import ClientCredential from homeassistant.components.nest import DOMAIN +from homeassistant.components.nest.const import API_URL # Typing helpers type PlatformSetup = Callable[[], Awaitable[None]] @@ -66,68 +63,22 @@ TEST_CONFIG_NEW_SUBSCRIPTION = NestTestConfig( credential=ClientCredential(CLIENT_ID, CLIENT_SECRET), ) - -class FakeSubscriber(GoogleNestSubscriber): - """Fake subscriber that supplies a FakeDeviceManager.""" - - stop_calls = 0 - - def __init__(self) -> None: # pylint: disable=super-init-not-called - """Initialize Fake Subscriber.""" - self._device_manager = DeviceManager() - self._subscriber_name = "fake-name" - - def set_update_callback(self, target: Callable[[EventMessage], Awaitable[None]]): - """Capture the callback set by Home Assistant.""" - self._device_manager.set_update_callback(target) - - async def create_subscription(self): - """Create the subscription.""" - return - - async def delete_subscription(self): - """Delete the subscription.""" - return - - async def start_async(self): - """Return the fake device manager.""" - return self._device_manager - - async def async_get_device_manager(self) -> DeviceManager: - """Return the fake device manager.""" - return self._device_manager - - @property - def cache_policy(self) -> CachePolicy: - """Return the cache policy.""" - return self._device_manager.cache_policy - - def stop_async(self): - """No-op to stop the subscriber.""" - self.stop_calls += 1 - - async def async_receive_event(self, event_message: EventMessage): - """Simulate a received pubsub message, invoked by tests.""" - # Update device state, then invoke HomeAssistant to refresh - await self._device_manager.async_handle_event(event_message) - - -DEVICE_ID = "enterprise/project-id/devices/device-id" +DEVICE_ID = "enterprises/project-id/devices/device-id" DEVICE_COMMAND = f"{DEVICE_ID}:executeCommand" +DEVICE_URL_MATCH = re.compile( + f"{API_URL}/enterprises/project-id/devices/[^:]+:executeCommand" +) +TEST_IMAGE_URL = "https://domain/sdm_event_snapshot/dGTZwR3o4Y1..." +TEST_CLIP_URL = "https://domain/clip/XyZ.mp4" class CreateDevice: """Fixture used for creating devices.""" - def __init__( - self, - device_manager: DeviceManager, - auth: AbstractAuth, - ) -> None: + def __init__(self) -> None: """Initialize CreateDevice.""" - self.device_manager = device_manager - self.auth = auth self.data = {"traits": {}} + self.devices = [] def create( self, @@ -138,4 +89,9 @@ class CreateDevice: data = copy.deepcopy(self.data) data.update(raw_data if raw_data else {}) data["traits"].update(raw_traits if raw_traits else {}) - self.device_manager.add_device(Device.MakeDevice(data, auth=self.auth)) + self.devices.append(data) + + +def create_nest_event(data: dict[str, Any]) -> Message: + """Create a pub/sub event message for testing.""" + return Message.from_data(data) diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index 84f22e17e78..b5e3cd2b91c 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -2,7 +2,6 @@ from __future__ import annotations -from asyncio import AbstractEventLoop from collections.abc import Generator import copy import shutil @@ -13,100 +12,131 @@ import uuid import aiohttp from google_nest_sdm import diagnostics -from google_nest_sdm.auth import AbstractAuth -from google_nest_sdm.device_manager import DeviceManager +from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber +from google_nest_sdm.streaming_manager import StreamingManager import pytest +from yarl import URL from homeassistant.components.application_credentials import ( async_import_client_credential, ) from homeassistant.components.nest import DOMAIN -from homeassistant.components.nest.const import CONF_SUBSCRIBER_ID, SDM_SCOPES +from homeassistant.components.nest.const import API_URL, CONF_SUBSCRIBER_ID, SDM_SCOPES from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from .common import ( DEVICE_ID, + DEVICE_URL_MATCH, PROJECT_ID, SUBSCRIBER_ID, + TEST_CLIP_URL, TEST_CONFIG_APP_CREDS, + TEST_IMAGE_URL, CreateDevice, - FakeSubscriber, NestTestConfig, PlatformSetup, YieldFixture, ) from tests.common import MockConfigEntry -from tests.typing import ClientSessionGenerator +from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse FAKE_TOKEN = "some-token" FAKE_REFRESH_TOKEN = "some-refresh-token" -class FakeAuth(AbstractAuth): - """A fake implementation of the auth class that records requests. +class FakeAuth: + """A fixture for request handling that records requests. - This class captures the outgoing requests, and can also be used by - tests to set up fake responses. This class is registered as a response - handler for a fake aiohttp_server and can simulate successes or failures - from the API. + This class is used with AiohttpClientMocker to capture outgoing requests + and can also be used by tests to set up fake responses. """ - def __init__(self) -> None: + def __init__( + self, + aioclient_mock: AiohttpClientMocker, + device_factory: CreateDevice, + project_id: str, + ) -> None: """Initialize FakeAuth.""" - super().__init__(None, None) - # Tests can set fake responses here. - self.responses = [] + # Tests can factory fixture to create fake device responses. + self.device_factory = device_factory + # Tests can set fake structure responses here. + self.structures: list[dict[str, Any]] = [] + # Tests can set fake command responses here. + self.responses: list[aiohttp.web.Response] = [] + # The last request is recorded here. self.method = None self.url = None self.json = None self.headers = None self.captured_requests = [] - # Set up by fixture - self.client = None - async def async_get_access_token(self) -> str: - """Return a valid access token.""" - return "" + # API makes a call to request structures to initiate pubsub feed, but the + # integration does not use this. + aioclient_mock.get( + f"{API_URL}/enterprises/{project_id}/structures", + side_effect=self.request_structures, + ) + aioclient_mock.get( + f"{API_URL}/enterprises/{project_id}/devices", + side_effect=self.request_devices, + ) + aioclient_mock.post(DEVICE_URL_MATCH, side_effect=self.request) + aioclient_mock.get(TEST_IMAGE_URL, side_effect=self.request) + aioclient_mock.get(TEST_CLIP_URL, side_effect=self.request) - async def request(self, method, url, **kwargs): + async def request_structures( + self, method: str, url: str, data: dict[str, Any] + ) -> AiohttpClientMockResponse: + """Handle requests to create devices.""" + return AiohttpClientMockResponse( + method, url, json={"structures": self.structures} + ) + + async def request_devices( + self, method: str, url: str, data: dict[str, Any] + ) -> AiohttpClientMockResponse: + """Handle requests to create devices.""" + return AiohttpClientMockResponse( + method, url, json={"devices": self.device_factory.devices} + ) + + async def request( + self, method: str, url: URL, data: dict[str, Any] + ) -> AiohttpClientMockResponse: """Capure the request arguments for tests to assert on.""" self.method = method - self.url = url - self.json = kwargs.get("json") - self.headers = kwargs.get("headers") - self.captured_requests.append((method, url, self.json, self.headers)) - return await self.client.get("/") + str_url = str(url) + self.url = str_url[len(API_URL) + 1 :] + self.json = data + self.captured_requests.append((method, url, self.json)) - async def response_handler(self, request): - """Handle fake responess for aiohttp_server.""" if len(self.responses) > 0: - return self.responses.pop(0) - return aiohttp.web.json_response() + response = self.responses.pop(0) + return AiohttpClientMockResponse( + method, url, response=response.body, status=response.status + ) + return AiohttpClientMockResponse(method, url) + + +@pytest.fixture(name="device_access_project_id") +def mock_device_access_project_id() -> str: + """Fixture to configure the device access console project id used in tests.""" + return PROJECT_ID @pytest.fixture -def aiohttp_client( - event_loop: AbstractEventLoop, - aiohttp_client: ClientSessionGenerator, - socket_enabled: None, -) -> ClientSessionGenerator: - """Return aiohttp_client and allow opening sockets.""" - return aiohttp_client - - -@pytest.fixture -async def auth(aiohttp_client: ClientSessionGenerator) -> FakeAuth: +async def auth( + aioclient_mock: AiohttpClientMocker, + create_device: CreateDevice, + device_access_project_id: str, +) -> FakeAuth: """Fixture for an AbstractAuth.""" - auth = FakeAuth() - app = aiohttp.web.Application() - app.router.add_get("/", auth.response_handler) - app.router.add_post("/", auth.response_handler) - auth.client = await aiohttp_client(app) - return auth + return FakeAuth(aioclient_mock, create_device, device_access_project_id) @pytest.fixture(autouse=True) @@ -119,20 +149,32 @@ def cleanup_media_storage(hass: HomeAssistant) -> Generator[None]: @pytest.fixture -def subscriber() -> YieldFixture[FakeSubscriber]: - """Set up the FakeSusbcriber.""" - subscriber = FakeSubscriber() +def subscriber_side_effect() -> Any | None: + """Fixture to inject failures into FakeSubscriber start.""" + return None + + +@pytest.fixture(autouse=True) +def subscriber(subscriber_side_effect: Any | None) -> Generator[AsyncMock]: + """Fixture to allow tests to emulate the pub/sub subscriber receiving messages.""" with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=subscriber, - ): - yield subscriber + "google_nest_sdm.google_nest_subscriber.StreamingManager", spec=StreamingManager + ) as mock_manager: + # Use side_effect to capture the callback + def mock_init(**kwargs: Any) -> AsyncMock: + mock_manager.async_receive_event = kwargs["callback"] + if subscriber_side_effect is not None: + mock_manager.start.side_effect = subscriber_side_effect + return mock_manager + + mock_manager.side_effect = mock_init + yield mock_manager @pytest.fixture def mock_subscriber() -> YieldFixture[AsyncMock]: """Fixture for injecting errors into the subscriber.""" - mock_subscriber = AsyncMock(FakeSubscriber) + mock_subscriber = AsyncMock(GoogleNestSubscriber) with patch( "homeassistant.components.nest.api.GoogleNestSubscriber", return_value=mock_subscriber, @@ -140,12 +182,6 @@ def mock_subscriber() -> YieldFixture[AsyncMock]: yield mock_subscriber -@pytest.fixture -async def device_manager(subscriber: FakeSubscriber) -> DeviceManager: - """Set up the DeviceManager.""" - return await subscriber.async_get_device_manager() - - @pytest.fixture async def device_id() -> str: """Fixture to set default device id used when creating devices.""" @@ -166,14 +202,12 @@ async def device_traits() -> dict[str, Any]: @pytest.fixture async def create_device( - device_manager: DeviceManager, - auth: FakeAuth, device_id: str, device_type: str, device_traits: dict[str, Any], ) -> CreateDevice: """Fixture for creating devices.""" - factory = CreateDevice(device_manager, auth) + factory = CreateDevice() factory.data.update( { "name": device_id, @@ -262,6 +296,7 @@ async def setup_base_platform( hass: HomeAssistant, platforms: list[str], config_entry: MockConfigEntry | None, + auth: FakeAuth, ) -> YieldFixture[PlatformSetup]: """Fixture to setup the integration platform.""" config_entry.add_to_hass(hass) @@ -278,7 +313,8 @@ async def setup_base_platform( @pytest.fixture async def setup_platform( - setup_base_platform: PlatformSetup, subscriber: FakeSubscriber + setup_base_platform: PlatformSetup, + subscriber: AsyncMock, ) -> PlatformSetup: """Fixture to setup the integration platform and subscriber.""" return setup_base_platform diff --git a/tests/components/nest/snapshots/test_diagnostics.ambr b/tests/components/nest/snapshots/test_diagnostics.ambr index aa679b8821c..5e17156fd6a 100644 --- a/tests/components/nest/snapshots/test_diagnostics.ambr +++ b/tests/components/nest/snapshots/test_diagnostics.ambr @@ -31,6 +31,9 @@ }), }), ]), + 'subscriber': dict({ + 'start': 1, + }), }) # --- # name: test_device_diagnostics @@ -85,5 +88,8 @@ }), }), ]), + 'subscriber': dict({ + 'start': 1, + }), }) # --- diff --git a/tests/components/nest/test_api.py b/tests/components/nest/test_api.py index a13d4d3a337..98c3e06cfb8 100644 --- a/tests/components/nest/test_api.py +++ b/tests/components/nest/test_api.py @@ -9,16 +9,16 @@ The tests below exercise both cases during integration setup. """ import time -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch -from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber +from google.oauth2.credentials import Credentials import pytest from homeassistant.components.nest.const import API_URL, OAUTH2_TOKEN, SDM_SCOPES from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util -from .common import CLIENT_ID, CLIENT_SECRET, PROJECT_ID, FakeSubscriber, PlatformSetup +from .common import CLIENT_ID, CLIENT_SECRET, PROJECT_ID, PlatformSetup from .conftest import FAKE_REFRESH_TOKEN, FAKE_TOKEN from tests.test_util.aiohttp import AiohttpClientMocker @@ -27,7 +27,7 @@ FAKE_UPDATED_TOKEN = "fake-updated-token" @pytest.fixture -def subscriber() -> FakeSubscriber | None: +def subscriber() -> Mock | None: """Disable default subscriber since tests use their own patch.""" return None @@ -54,16 +54,16 @@ async def test_auth( # Prepare to capture credentials for Subscriber captured_creds = None - async def async_new_subscriber( - creds, subscription_name, event_loop, async_callback - ) -> GoogleNestSubscriber | None: + def async_new_subscriber( + credentials: Credentials, + ) -> Mock: """Capture credentials for tests.""" nonlocal captured_creds - captured_creds = creds - return None # GoogleNestSubscriber + captured_creds = credentials + return AsyncMock() with patch( - "google_nest_sdm.google_nest_subscriber.DefaultSubscriberFactory.async_new_subscriber", + "google_nest_sdm.subscriber_client.pubsub_v1.SubscriberAsyncClient", side_effect=async_new_subscriber, ) as new_subscriber_mock: await setup_platform() @@ -122,16 +122,16 @@ async def test_auth_expired_token( # Prepare to capture credentials for Subscriber captured_creds = None - async def async_new_subscriber( - creds, subscription_name, event_loop, async_callback - ) -> GoogleNestSubscriber | None: + def async_new_subscriber( + credentials: Credentials, + ) -> Mock: """Capture credentials for tests.""" nonlocal captured_creds - captured_creds = creds - return None # GoogleNestSubscriber + captured_creds = credentials + return AsyncMock() with patch( - "google_nest_sdm.google_nest_subscriber.DefaultSubscriberFactory.async_new_subscriber", + "google_nest_sdm.subscriber_client.pubsub_v1.SubscriberAsyncClient", side_effect=async_new_subscriber, ) as new_subscriber_mock: await setup_platform() diff --git a/tests/components/nest/test_camera.py b/tests/components/nest/test_camera.py index 698e9b7a274..3e7dbd3f223 100644 --- a/tests/components/nest/test_camera.py +++ b/tests/components/nest/test_camera.py @@ -24,7 +24,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from .common import DEVICE_ID, CreateDevice, FakeSubscriber, PlatformSetup +from .common import DEVICE_ID, CreateDevice, PlatformSetup from .conftest import FakeAuth from tests.common import async_fire_time_changed @@ -520,13 +520,10 @@ async def test_camera_removed( hass: HomeAssistant, auth: FakeAuth, camera_device: None, - subscriber: FakeSubscriber, setup_platform: PlatformSetup, ) -> None: """Test case where entities are removed and stream tokens revoked.""" await setup_platform() - # Simplify test setup - subscriber.cache_policy.fetch = False assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -780,7 +777,7 @@ async def test_camera_web_rtc_offer_failure( assert response["event"] == { "type": "error", "code": "webrtc_offer_failed", - "message": "Nest API error: Bad Request response from API (400)", + "message": "Nest API error: response from API (400)", } diff --git a/tests/components/nest/test_climate.py b/tests/components/nest/test_climate.py index 88847759a16..39b5bf5aed7 100644 --- a/tests/components/nest/test_climate.py +++ b/tests/components/nest/test_climate.py @@ -7,10 +7,9 @@ pubsub subscriber. from collections.abc import Awaitable, Callable from http import HTTPStatus from typing import Any +from unittest.mock import AsyncMock import aiohttp -from google_nest_sdm.auth import AbstractAuth -from google_nest_sdm.event import EventMessage import pytest from homeassistant.components.climate import ( @@ -45,8 +44,8 @@ from .common import ( DEVICE_COMMAND, DEVICE_ID, CreateDevice, - FakeSubscriber, PlatformSetup, + create_nest_event, ) from .conftest import FakeAuth @@ -72,14 +71,13 @@ def device_traits() -> dict[str, Any]: @pytest.fixture async def create_event( hass: HomeAssistant, - auth: AbstractAuth, - subscriber: FakeSubscriber, + subscriber: AsyncMock, ) -> CreateEvent: """Fixture to send a pub/sub event.""" async def create_event(traits: dict[str, Any]) -> None: await subscriber.async_receive_event( - EventMessage.create_event( + create_nest_event( { "eventId": EVENT_ID, "timestamp": "2019-01-01T00:00:01Z", @@ -88,7 +86,6 @@ async def create_event( "traits": traits, }, }, - auth=auth, ) ) await hass.async_block_till_done() diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index 807e299b79c..3d28c1abf23 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -7,7 +7,6 @@ from typing import Any from unittest.mock import patch from google_nest_sdm.exceptions import AuthException -from google_nest_sdm.structure import Structure import pytest from homeassistant import config_entries @@ -25,9 +24,9 @@ from .common import ( SUBSCRIBER_ID, TEST_CONFIG_APP_CREDS, TEST_CONFIGFLOW_APP_CREDS, - FakeSubscriber, NestTestConfig, ) +from .conftest import FakeAuth, PlatformSetup from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -58,6 +57,11 @@ def mock_rand_topic_name_fixture() -> None: yield +@pytest.fixture(autouse=True) +def mock_request_setup(auth: FakeAuth) -> None: + """Fixture to ensure fake requests are setup.""" + + class OAuthFixture: """Simulate the oauth flow used by the config flow.""" @@ -257,12 +261,6 @@ def mock_subscriptions() -> list[tuple[str, str]]: return [] -@pytest.fixture(name="device_access_project_id") -def mock_device_access_project_id() -> str: - """Fixture to configure the device access console project id used in tests.""" - return PROJECT_ID - - @pytest.fixture(name="cloud_project_id") def mock_cloud_project_id() -> str: """Fixture to configure the cloud console project id used in tests.""" @@ -350,8 +348,7 @@ def mock_pubsub_api_responses( @pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) async def test_app_credentials( hass: HomeAssistant, - oauth, - subscriber, + oauth: OAuthFixture, ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( @@ -388,7 +385,7 @@ async def test_app_credentials( ("sdm_managed_topic", "device_access_project_id", "cloud_project_id"), [(True, "new-project-id", "new-cloud-project-id")], ) -async def test_config_flow_restart(hass: HomeAssistant, oauth, subscriber) -> None: +async def test_config_flow_restart(hass: HomeAssistant, oauth: OAuthFixture) -> None: """Check with auth implementation is re-initialized when aborting the flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -443,8 +440,7 @@ async def test_config_flow_restart(hass: HomeAssistant, oauth, subscriber) -> No @pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) async def test_config_flow_wrong_project_id( hass: HomeAssistant, - oauth, - subscriber, + oauth: OAuthFixture, ) -> None: """Check the case where the wrong project ids are entered.""" result = await hass.config_entries.flow.async_init( @@ -500,8 +496,7 @@ async def test_config_flow_wrong_project_id( ) async def test_config_flow_pubsub_configuration_error( hass: HomeAssistant, - oauth, - mock_subscriber, + oauth: OAuthFixture, ) -> None: """Check full flow fails with configuration error.""" result = await hass.config_entries.flow.async_init( @@ -546,7 +541,8 @@ async def test_config_flow_pubsub_configuration_error( [(True, HTTPStatus.INTERNAL_SERVER_ERROR)], ) async def test_config_flow_pubsub_subscriber_error( - hass: HomeAssistant, oauth, mock_subscriber + hass: HomeAssistant, + oauth: OAuthFixture, ) -> None: """Check full flow with a subscriber error.""" result = await hass.config_entries.flow.async_init( @@ -697,7 +693,8 @@ async def test_reauth_multiple_config_entries( @pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) async def test_pubsub_subscription_strip_whitespace( - hass: HomeAssistant, oauth, subscriber + hass: HomeAssistant, + oauth: OAuthFixture, ) -> None: """Check that project id has whitespace stripped on entry.""" result = await hass.config_entries.flow.async_init( @@ -776,10 +773,9 @@ async def test_pubsub_subscription_auth_failure( ) async def test_pubsub_subscriber_config_entry_reauth( hass: HomeAssistant, - oauth, - setup_platform, - subscriber, - config_entry, + oauth: OAuthFixture, + setup_platform: PlatformSetup, + config_entry: MockConfigEntry, ) -> None: """Test the pubsub subscriber id is preserved during reauth.""" await setup_platform() @@ -805,22 +801,21 @@ async def test_pubsub_subscriber_config_entry_reauth( @pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) async def test_config_entry_title_from_home( - hass: HomeAssistant, oauth, subscriber + hass: HomeAssistant, + oauth: OAuthFixture, + auth: FakeAuth, ) -> None: """Test that the Google Home name is used for the config entry title.""" - device_manager = await subscriber.async_get_device_manager() - device_manager.add_structure( - Structure.MakeStructure( - { - "name": f"enterprise/{PROJECT_ID}/structures/some-structure-id", - "traits": { - "sdm.structures.traits.Info": { - "customName": "Example Home", - }, + auth.structures.append( + { + "name": f"enterprise/{PROJECT_ID}/structures/some-structure-id", + "traits": { + "sdm.structures.traits.Info": { + "customName": "Example Home", }, - } - ) + }, + } ) result = await hass.config_entries.flow.async_init( @@ -848,13 +843,13 @@ async def test_config_entry_title_from_home( @pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) async def test_config_entry_title_multiple_homes( - hass: HomeAssistant, oauth, subscriber + hass: HomeAssistant, + oauth: OAuthFixture, + auth: FakeAuth, ) -> None: """Test handling of multiple Google Homes authorized.""" - - device_manager = await subscriber.async_get_device_manager() - device_manager.add_structure( - Structure.MakeStructure( + auth.structures.extend( + [ { "name": f"enterprise/{PROJECT_ID}/structures/id-1", "traits": { @@ -862,11 +857,7 @@ async def test_config_entry_title_multiple_homes( "customName": "Example Home #1", }, }, - } - ) - ) - device_manager.add_structure( - Structure.MakeStructure( + }, { "name": f"enterprise/{PROJECT_ID}/structures/id-2", "traits": { @@ -874,8 +865,8 @@ async def test_config_entry_title_multiple_homes( "customName": "Example Home #2", }, }, - } - ) + }, + ] ) result = await hass.config_entries.flow.async_init( @@ -923,18 +914,17 @@ async def test_title_failure_fallback( @pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) -async def test_structure_missing_trait(hass: HomeAssistant, oauth, subscriber) -> None: +async def test_structure_missing_trait( + hass: HomeAssistant, oauth: OAuthFixture, auth: FakeAuth +) -> None: """Test handling the case where a structure has no name set.""" - device_manager = await subscriber.async_get_device_manager() - device_manager.add_structure( - Structure.MakeStructure( - { - "name": f"enterprise/{PROJECT_ID}/structures/id-1", - # Missing Info trait - "traits": {}, - } - ) + auth.structures.append( + { + "name": f"enterprise/{PROJECT_ID}/structures/id-1", + # Missing Info trait + "traits": {}, + } ) result = await hass.config_entries.flow.async_init( @@ -973,8 +963,7 @@ async def test_dhcp_discovery( @pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) async def test_dhcp_discovery_with_creds( hass: HomeAssistant, - oauth, - subscriber, + oauth: OAuthFixture, ) -> None: """Exercise discovery dhcp with no config present (can't run).""" result = await hass.config_entries.flow.async_init( @@ -1029,7 +1018,6 @@ async def test_dhcp_discovery_with_creds( async def test_token_error( hass: HomeAssistant, oauth: OAuthFixture, - subscriber: FakeSubscriber, status_code: HTTPStatus, error_reason: str, ) -> None: @@ -1064,8 +1052,7 @@ async def test_token_error( ) async def test_existing_topic_and_subscription( hass: HomeAssistant, - oauth, - subscriber, + oauth: OAuthFixture, ) -> None: """Test selecting existing user managed topic and subscription.""" result = await hass.config_entries.flow.async_init( @@ -1103,8 +1090,7 @@ async def test_existing_topic_and_subscription( async def test_no_eligible_topics( hass: HomeAssistant, - oauth, - subscriber, + oauth: OAuthFixture, ) -> None: """Test the case where there are no eligible pub/sub topics.""" result = await hass.config_entries.flow.async_init( @@ -1127,8 +1113,7 @@ async def test_no_eligible_topics( ) async def test_list_topics_failure( hass: HomeAssistant, - oauth, - subscriber, + oauth: OAuthFixture, ) -> None: """Test selecting existing user managed topic and subscription.""" result = await hass.config_entries.flow.async_init( @@ -1151,8 +1136,7 @@ async def test_list_topics_failure( ) async def test_list_subscriptions_failure( hass: HomeAssistant, - oauth, - subscriber, + oauth: OAuthFixture, ) -> None: """Test selecting existing user managed topic and subscription.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index cf0e1c5ecce..33f5c1637fa 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -1,8 +1,8 @@ """The tests for Nest device triggers.""" from typing import Any +from unittest.mock import AsyncMock -from google_nest_sdm.event import EventMessage import pytest from pytest_unordered import unordered @@ -18,7 +18,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from .common import DEVICE_ID, CreateDevice, FakeSubscriber, PlatformSetup +from .common import DEVICE_ID, CreateDevice, PlatformSetup, create_nest_event from tests.common import async_get_device_automations @@ -447,7 +447,7 @@ async def test_subscriber_automation( service_calls: list[ServiceCall], create_device: CreateDevice, setup_platform: PlatformSetup, - subscriber: FakeSubscriber, + subscriber: AsyncMock, ) -> None: """Test end to end subscriber triggers automation.""" create_device.create( @@ -465,7 +465,7 @@ async def test_subscriber_automation( assert await setup_automation(hass, device_entry.id, "camera_motion") # Simulate a pubsub message received by the subscriber with a motion event - event = EventMessage.create_event( + event = create_nest_event( { "eventId": "some-event-id", "timestamp": "2019-01-01T00:00:01Z", @@ -479,7 +479,6 @@ async def test_subscriber_automation( }, }, }, - auth=None, ) await subscriber.async_receive_event(event) await hass.async_block_till_done() diff --git a/tests/components/nest/test_event.py b/tests/components/nest/test_event.py index f45e6c1c6e6..942f19d6626 100644 --- a/tests/components/nest/test_event.py +++ b/tests/components/nest/test_event.py @@ -2,7 +2,7 @@ import datetime from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory from google_nest_sdm.event import EventMessage, EventType @@ -13,7 +13,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow -from .common import DEVICE_ID, CreateDevice, FakeSubscriber +from .common import DEVICE_ID, TEST_CLIP_URL, CreateDevice, create_nest_event from .conftest import PlatformSetup EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..." @@ -31,14 +31,6 @@ def platforms() -> list[Platform]: return [Platform.EVENT] -@pytest.fixture(autouse=True) -def enable_prefetch(subscriber: FakeSubscriber) -> None: - """Fixture to enable media fetching for tests to exercise.""" - subscriber.cache_policy.fetch = True - with patch("homeassistant.components.nest.EVENT_MEDIA_CACHE_SIZE", new=5): - yield - - @pytest.fixture def device_type() -> str: """Fixture for the type of device under test.""" @@ -80,7 +72,7 @@ def create_event_messages( events: dict[str, Any], parameters: dict[str, Any] | None = None ) -> EventMessage: """Create an EventMessage for events.""" - return EventMessage.create_event( + return create_nest_event( { "eventId": "some-event-id", "timestamp": utcnow().isoformat(timespec="seconds"), @@ -90,7 +82,6 @@ def create_event_messages( }, **(parameters if parameters else {}), }, - auth=None, ) @@ -152,7 +143,7 @@ def create_event_messages( ) async def test_receive_events( hass: HomeAssistant, - subscriber: FakeSubscriber, + subscriber: AsyncMock, setup_platform: PlatformSetup, create_device: CreateDevice, trait_types: list[TraitType], @@ -192,7 +183,7 @@ async def test_receive_events( @pytest.mark.parametrize(("trait_type"), [(TraitType.DOORBELL_CHIME)]) async def test_ignore_unrelated_event( hass: HomeAssistant, - subscriber: FakeSubscriber, + subscriber: AsyncMock, setup_platform: PlatformSetup, create_device: CreateDevice, trait_type: TraitType, @@ -222,7 +213,7 @@ async def test_ignore_unrelated_event( @pytest.mark.freeze_time("2024-08-24T12:00:00Z") async def test_event_threads( hass: HomeAssistant, - subscriber: FakeSubscriber, + subscriber: AsyncMock, setup_platform: PlatformSetup, create_device: CreateDevice, freezer: FrozenDateTimeFactory, @@ -275,7 +266,7 @@ async def test_event_threads( }, EventType.CAMERA_CLIP_PREVIEW: { "eventSessionId": EVENT_SESSION_ID, - "previewUrl": "http://example", + "previewUrl": TEST_CLIP_URL, }, }, parameters={"eventThreadState": "ENDED"}, @@ -306,7 +297,7 @@ async def test_event_threads( }, EventType.CAMERA_CLIP_PREVIEW: { "eventSessionId": EVENT_SESSION_ID2, - "previewUrl": "http://example", + "previewUrl": TEST_CLIP_URL, }, }, parameters={"eventThreadState": "ENDED"}, diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index e746e5f263f..d4ad81bd4e8 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -9,26 +9,38 @@ from __future__ import annotations from collections.abc import Mapping import datetime from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock -from google_nest_sdm.device import Device -from google_nest_sdm.event import EventMessage +import aiohttp import pytest from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.dt import utcnow -from .common import CreateDevice +from .common import ( + DEVICE_ID, + TEST_CLIP_URL, + TEST_IMAGE_URL, + CreateDevice, + PlatformSetup, + create_nest_event, +) from tests.common import async_capture_events DOMAIN = "nest" -DEVICE_ID = "some-device-id" PLATFORM = "camera" NEST_EVENT = "nest_event" EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..." EVENT_ID = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." +GENERATE_IMAGE_URL_RESPONSE = { + "results": { + "url": TEST_IMAGE_URL, + "token": "g.0.eventToken", + }, +} +IMAGE_BYTES_FROM_EVENT = b"test url image bytes" EVENT_KEYS = {"device_id", "type", "timestamp", "zones"} @@ -104,7 +116,7 @@ def create_events(events, device_id=DEVICE_ID, timestamp=None): """Create an EventMessage for events.""" if not timestamp: timestamp = utcnow() - return EventMessage.create_event( + return create_nest_event( { "eventId": "some-event-id", "timestamp": timestamp.isoformat(timespec="seconds"), @@ -113,7 +125,6 @@ def create_events(events, device_id=DEVICE_ID, timestamp=None): "events": events, }, }, - auth=None, ) @@ -167,7 +178,7 @@ async def test_event( entry = entity_registry.async_get("camera.front") assert entry is not None - assert entry.unique_id == "some-device-id-camera" + assert entry.unique_id == f"{DEVICE_ID}-camera" assert entry.domain == "camera" device = device_registry.async_get(entry.device_id) @@ -175,6 +186,11 @@ async def test_event( assert device.model == expected_model assert device.identifiers == {("nest", DEVICE_ID)} + auth.responses = [ + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + ] + timestamp = utcnow() await subscriber.async_receive_event(create_event(event_trait, timestamp=timestamp)) await hass.async_block_till_done() @@ -300,12 +316,11 @@ async def test_event_message_without_device_event( events = async_capture_events(hass, NEST_EVENT) await setup_platform() timestamp = utcnow() - event = EventMessage.create_event( + event = create_nest_event( { "eventId": "some-event-id", "timestamp": timestamp.isoformat(timespec="seconds"), }, - auth=None, ) await subscriber.async_receive_event(event) await hass.async_block_till_done() @@ -339,7 +354,7 @@ async def test_doorbell_event_thread( }, "sdm.devices.events.CameraClipPreview.ClipPreview": { "eventSessionId": EVENT_SESSION_ID, - "previewUrl": "image-url-1", + "previewUrl": TEST_CLIP_URL, }, }, }, @@ -356,9 +371,7 @@ async def test_doorbell_event_thread( "eventThreadState": "STARTED", } ) - await subscriber.async_receive_event( - EventMessage.create_event(message_data_1, auth=None) - ) + await subscriber.async_receive_event(create_nest_event(message_data_1)) # Publish message #2 that sends a no-op update to end the event thread timestamp2 = timestamp1 + datetime.timedelta(seconds=1) @@ -369,9 +382,7 @@ async def test_doorbell_event_thread( "eventThreadState": "ENDED", } ) - await subscriber.async_receive_event( - EventMessage.create_event(message_data_2, auth=None) - ) + await subscriber.async_receive_event(create_nest_event(message_data_2)) await hass.async_block_till_done() # The event is only published once @@ -415,7 +426,7 @@ async def test_doorbell_event_session_update( }, "sdm.devices.events.CameraClipPreview.ClipPreview": { "eventSessionId": EVENT_SESSION_ID, - "previewUrl": "image-url-1", + "previewUrl": TEST_CLIP_URL, }, }, timestamp=timestamp1, @@ -437,7 +448,7 @@ async def test_doorbell_event_session_update( }, "sdm.devices.events.CameraClipPreview.ClipPreview": { "eventSessionId": EVENT_SESSION_ID, - "previewUrl": "image-url-1", + "previewUrl": TEST_CLIP_URL, }, }, timestamp=timestamp2, @@ -459,7 +470,11 @@ async def test_doorbell_event_session_update( async def test_structure_update_event( - hass: HomeAssistant, entity_registry: er.EntityRegistry, subscriber, setup_platform + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + subscriber: AsyncMock, + setup_platform: PlatformSetup, + create_device: CreateDevice, ) -> None: """Test a pubsub message for a new device being added.""" events = async_capture_events(hass, NEST_EVENT) @@ -468,8 +483,8 @@ async def test_structure_update_event( # Entity for first device is registered assert entity_registry.async_get("camera.front") - new_device = Device.MakeDevice( - { + create_device.create( + raw_data={ "name": "device-id-2", "type": "sdm.devices.types.CAMERA", "traits": { @@ -479,16 +494,13 @@ async def test_structure_update_event( "sdm.devices.traits.CameraLiveStream": {}, }, }, - auth=None, ) - device_manager = await subscriber.async_get_device_manager() - device_manager.add_device(new_device) # Entity for new devie has not yet been loaded assert not entity_registry.async_get("camera.back") # Send a message that triggers the device to be loaded - message = EventMessage.create_event( + message = create_nest_event( { "eventId": "some-event-id", "timestamp": utcnow().isoformat(timespec="seconds"), @@ -498,17 +510,10 @@ async def test_structure_update_event( "object": "enterprise/example/devices/some-device-id2", }, }, - auth=None, ) - with ( - patch("homeassistant.components.nest.PLATFORMS", [PLATFORM]), - patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=subscriber, - ), - ): - await subscriber.async_receive_event(message) - await hass.async_block_till_done() + + await subscriber.async_receive_event(message) + await hass.async_block_till_done() # No home assistant events published assert not events diff --git a/tests/components/nest/test_init.py b/tests/components/nest/test_init.py index 17ddc485e85..7d04624dcc8 100644 --- a/tests/components/nest/test_init.py +++ b/tests/components/nest/test_init.py @@ -9,9 +9,9 @@ relevant modes. """ from collections.abc import Generator +from http import HTTPStatus import logging -from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from google_nest_sdm.exceptions import ( ApiException, @@ -29,11 +29,11 @@ from .common import ( PROJECT_ID, SUBSCRIBER_ID, TEST_CONFIG_NEW_SUBSCRIPTION, - FakeSubscriber, PlatformSetup, - YieldFixture, ) +from tests.test_util.aiohttp import AiohttpClientMocker + PLATFORM = "sensor" @@ -61,25 +61,6 @@ def warning_caplog( yield caplog -@pytest.fixture -def subscriber_side_effect() -> Any | None: - """Fixture to inject failures into FakeSubscriber start.""" - return None - - -@pytest.fixture -def failing_subscriber( - subscriber_side_effect: Any | None, -) -> YieldFixture[FakeSubscriber]: - """Fixture overriding default subscriber behavior to allow failure injection.""" - subscriber = FakeSubscriber() - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber.start_async", - side_effect=subscriber_side_effect, - ): - yield subscriber - - async def test_setup_success( hass: HomeAssistant, error_caplog: pytest.LogCaptureFixture, setup_platform ) -> None: @@ -125,10 +106,9 @@ async def test_setup_configuration_failure( @pytest.mark.parametrize("subscriber_side_effect", [SubscriberException()]) -async def test_setup_susbcriber_failure( +async def test_setup_subscriber_failure( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - failing_subscriber, setup_base_platform, ) -> None: """Test configuration error.""" @@ -145,7 +125,6 @@ async def test_setup_device_manager_failure( ) -> None: """Test device manager api failure.""" with ( - patch("homeassistant.components.nest.api.GoogleNestSubscriber.start_async"), patch( "homeassistant.components.nest.api.GoogleNestSubscriber.async_get_device_manager", side_effect=ApiException(), @@ -165,7 +144,6 @@ async def test_subscriber_auth_failure( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, setup_base_platform, - failing_subscriber, ) -> None: """Test subscriber throws an authentication error.""" await setup_base_platform() @@ -184,7 +162,6 @@ async def test_subscriber_configuration_failure( hass: HomeAssistant, error_caplog: pytest.LogCaptureFixture, setup_base_platform, - failing_subscriber, ) -> None: """Test configuration error.""" await setup_base_platform() @@ -210,14 +187,12 @@ async def test_unload_entry(hass: HomeAssistant, setup_platform) -> None: async def test_remove_entry( hass: HomeAssistant, - setup_base_platform, + setup_base_platform: PlatformSetup, + aioclient_mock: AiohttpClientMocker, + subscriber: AsyncMock, ) -> None: """Test successful unload of a ConfigEntry.""" - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=FakeSubscriber(), - ): - await setup_base_platform() + await setup_base_platform() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 @@ -227,14 +202,18 @@ async def test_remove_entry( assert entry.data.get("subscriber_id") == SUBSCRIBER_ID assert entry.data.get("project_id") == PROJECT_ID - with ( - patch("homeassistant.components.nest.api.GoogleNestSubscriber.subscriber_id"), - patch( - "homeassistant.components.nest.api.GoogleNestSubscriber.delete_subscription", - ) as delete, - ): - assert await hass.config_entries.async_remove(entry.entry_id) - assert delete.called + aioclient_mock.clear_requests() + aioclient_mock.delete( + f"https://pubsub.googleapis.com/v1/{SUBSCRIBER_ID}", + json={}, + ) + + assert not subscriber.stop.called + + assert await hass.config_entries.async_remove(entry.entry_id) + + assert aioclient_mock.call_count == 1 + assert subscriber.stop.called entries = hass.config_entries.async_entries(DOMAIN) assert not entries @@ -243,7 +222,7 @@ async def test_remove_entry( async def test_home_assistant_stop( hass: HomeAssistant, setup_platform: PlatformSetup, - subscriber: FakeSubscriber, + subscriber: AsyncMock, ) -> None: """Test successful subscriber shutdown when HomeAssistant stops.""" await setup_platform() @@ -253,31 +232,37 @@ async def test_home_assistant_stop( entry = entries[0] assert entry.state is ConfigEntryState.LOADED + assert not subscriber.stop.called await hass.async_stop() - assert subscriber.stop_calls == 1 + assert subscriber.stop.called async def test_remove_entry_delete_subscriber_failure( - hass: HomeAssistant, setup_base_platform + hass: HomeAssistant, + setup_base_platform: PlatformSetup, + aioclient_mock: AiohttpClientMocker, + subscriber: AsyncMock, ) -> None: """Test a failure when deleting the subscription.""" - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=FakeSubscriber(), - ): - await setup_base_platform() + await setup_base_platform() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 entry = entries[0] assert entry.state is ConfigEntryState.LOADED - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber.delete_subscription", - side_effect=SubscriberException(), - ) as delete: - assert await hass.config_entries.async_remove(entry.entry_id) - assert delete.called + aioclient_mock.clear_requests() + aioclient_mock.delete( + f"https://pubsub.googleapis.com/v1/{SUBSCRIBER_ID}", + status=HTTPStatus.NOT_FOUND, + ) + + assert not subscriber.stop.called + + assert await hass.config_entries.async_remove(entry.entry_id) + + assert aioclient_mock.call_count == 1 + assert subscriber.stop.called entries = hass.config_entries.async_entries(DOMAIN) assert not entries diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 2526bfdf975..276dd45d0ab 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -13,7 +13,6 @@ from unittest.mock import patch import aiohttp import av -from google_nest_sdm.event import EventMessage import numpy as np import pytest @@ -31,7 +30,14 @@ from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .common import DEVICE_ID, CreateDevice, FakeSubscriber +from .common import ( + DEVICE_ID, + TEST_CLIP_URL, + TEST_IMAGE_URL, + CreateDevice, + create_nest_event, +) +from .conftest import FakeAuth from tests.common import MockUser, async_capture_events from tests.typing import ClientSessionGenerator @@ -70,7 +76,6 @@ BATTERY_CAMERA_TRAITS = { PERSON_EVENT = "sdm.devices.events.CameraPerson.Person" MOTION_EVENT = "sdm.devices.events.CameraMotion.Motion" -TEST_IMAGE_URL = "https://domain/sdm_event_snapshot/dGTZwR3o4Y1..." GENERATE_IMAGE_URL_RESPONSE = { "results": { "url": TEST_IMAGE_URL, @@ -162,12 +167,6 @@ def mp4() -> io.BytesIO: return output -@pytest.fixture(autouse=True) -def enable_prefetch(subscriber: FakeSubscriber) -> None: - """Fixture to enable media fetching for tests to exercise.""" - subscriber.cache_policy.fetch = True - - @pytest.fixture def cache_size() -> int: """Fixture for overrideing cache size.""" @@ -200,7 +199,7 @@ def create_event_message(event_data, timestamp, device_id=None): """Create an EventMessage for a single event type.""" if device_id is None: device_id = DEVICE_ID - return EventMessage.create_event( + return create_nest_event( { "eventId": f"{EVENT_ID}-{timestamp}", "timestamp": timestamp.isoformat(timespec="seconds"), @@ -209,7 +208,6 @@ def create_event_message(event_data, timestamp, device_id=None): "events": event_data, }, }, - auth=None, ) @@ -224,7 +222,7 @@ def create_battery_event_data( }, "sdm.devices.events.CameraClipPreview.ClipPreview": { "eventSessionId": event_session_id, - "previewUrl": "https://127.0.0.1/example", + "previewUrl": TEST_CLIP_URL, }, } @@ -284,7 +282,9 @@ async def test_supported_device( assert len(browse.children) == 0 -async def test_integration_unloaded(hass: HomeAssistant, auth, setup_platform) -> None: +async def test_integration_unloaded( + hass: HomeAssistant, auth: FakeAuth, setup_platform +) -> None: """Test the media player loads, but has no devices, when config unloaded.""" await setup_platform() @@ -1257,6 +1257,7 @@ async def test_media_store_save_filesystem_error( assert response.status == HTTPStatus.NOT_FOUND, f"Response not matched: {response}" +@pytest.mark.parametrize("device_traits", [BATTERY_CAMERA_TRAITS]) async def test_media_store_load_filesystem_error( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/components/nest/test_sensor.py b/tests/components/nest/test_sensor.py index 2339d72ebc7..8bda1d387f8 100644 --- a/tests/components/nest/test_sensor.py +++ b/tests/components/nest/test_sensor.py @@ -5,8 +5,8 @@ pubsub subscriber. """ from typing import Any +from unittest.mock import AsyncMock -from google_nest_sdm.event import EventMessage import pytest from homeassistant.components.sensor import ( @@ -25,7 +25,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from .common import DEVICE_ID, CreateDevice, FakeSubscriber, PlatformSetup +from .common import DEVICE_ID, CreateDevice, PlatformSetup, create_nest_event @pytest.fixture @@ -198,7 +198,7 @@ async def test_device_name_from_structure( async def test_event_updates_sensor( hass: HomeAssistant, - subscriber: FakeSubscriber, + subscriber: AsyncMock, create_device: CreateDevice, setup_platform: PlatformSetup, ) -> None: @@ -217,7 +217,7 @@ async def test_event_updates_sensor( assert temperature.state == "25.1" # Simulate a pubsub message received by the subscriber with a trait update - event = EventMessage.create_event( + event = create_nest_event( { "eventId": "some-event-id", "timestamp": "2019-01-01T00:00:01Z", @@ -230,7 +230,6 @@ async def test_event_updates_sensor( }, }, }, - auth=None, ) await subscriber.async_receive_event(event) await hass.async_block_till_done() # Process dispatch/update signal