diff --git a/homeassistant/components/youtube/coordinator.py b/homeassistant/components/youtube/coordinator.py index cb9d1e8214e249..07420233baf81d 100644 --- a/homeassistant/components/youtube/coordinator.py +++ b/homeassistant/components/youtube/coordinator.py @@ -5,13 +5,13 @@ from typing import Any from youtubeaio.helper import first -from youtubeaio.types import UnauthorizedError +from youtubeaio.types import UnauthorizedError, YouTubeBackendError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ICON, ATTR_ID from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from . import AsyncConfigEntryAuth from .const import ( @@ -70,4 +70,6 @@ async def _async_update_data(self) -> dict[str, Any]: } except UnauthorizedError as err: raise ConfigEntryAuthFailed from err + except YouTubeBackendError as err: + raise UpdateFailed("Couldn't connect to YouTube") from err return res diff --git a/homeassistant/components/youtube/sensor.py b/homeassistant/components/youtube/sensor.py index a63b8fb0c0ba34..99cd3ecf095836 100644 --- a/homeassistant/components/youtube/sensor.py +++ b/homeassistant/components/youtube/sensor.py @@ -87,9 +87,9 @@ class YouTubeSensor(YouTubeChannelEntity, SensorEntity): entity_description: YouTubeSensorEntityDescription @property - def available(self): + def available(self) -> bool: """Return if the entity is available.""" - return self.entity_description.available_fn( + return super().available and self.entity_description.available_fn( self.coordinator.data[self._channel_id] ) diff --git a/tests/components/youtube/__init__.py b/tests/components/youtube/__init__.py index 3c46ff92661c8b..665f5f3a76274b 100644 --- a/tests/components/youtube/__init__.py +++ b/tests/components/youtube/__init__.py @@ -11,7 +11,7 @@ class MockYouTube: """Service which returns mock objects.""" - _authenticated = False + _thrown_error: Exception | None = None def __init__( self, @@ -28,7 +28,6 @@ async def set_user_authentication( self, token: str, scopes: list[AuthScope] ) -> None: """Authenticate the user.""" - self._authenticated = True async def get_user_channels(self) -> AsyncGenerator[YouTubeChannel, None]: """Get channels for authenticated user.""" @@ -40,6 +39,8 @@ async def get_channels( self, channel_ids: list[str] ) -> AsyncGenerator[YouTubeChannel, None]: """Get channels.""" + if self._thrown_error is not None: + raise self._thrown_error channels = json.loads(load_fixture(self._channel_fixture)) for item in channels["items"]: yield YouTubeChannel(**item) @@ -57,3 +58,7 @@ async def get_user_subscriptions(self) -> AsyncGenerator[YouTubeSubscription, No channels = json.loads(load_fixture(self._subscriptions_fixture)) for item in channels["items"]: yield YouTubeSubscription(**item) + + def set_thrown_exception(self, exception: Exception) -> None: + """Set thrown exception for testing purposes.""" + self._thrown_error = exception diff --git a/tests/components/youtube/conftest.py b/tests/components/youtube/conftest.py index a8a333190eecd6..8b6ce5d00a23a4 100644 --- a/tests/components/youtube/conftest.py +++ b/tests/components/youtube/conftest.py @@ -18,7 +18,7 @@ from tests.components.youtube import MockYouTube from tests.test_util.aiohttp import AiohttpClientMocker -ComponentSetup = Callable[[], Awaitable[None]] +ComponentSetup = Callable[[], Awaitable[MockYouTube]] CLIENT_ID = "1234" CLIENT_SECRET = "5678" @@ -92,7 +92,7 @@ def mock_connection(aioclient_mock: AiohttpClientMocker) -> None: @pytest.fixture(name="setup_integration") async def mock_setup_integration( hass: HomeAssistant, config_entry: MockConfigEntry -) -> Callable[[], Coroutine[Any, Any, None]]: +) -> Callable[[], Coroutine[Any, Any, MockYouTube]]: """Fixture for setting up the component.""" config_entry.add_to_hass(hass) @@ -104,11 +104,11 @@ async def mock_setup_integration( DOMAIN, ) - async def func() -> None: - with patch( - "homeassistant.components.youtube.api.YouTube", return_value=MockYouTube() - ): + async def func() -> MockYouTube: + mock = MockYouTube() + with patch("homeassistant.components.youtube.api.YouTube", return_value=mock): assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() + return mock return func diff --git a/tests/components/youtube/test_sensor.py b/tests/components/youtube/test_sensor.py index 7dc368a5860c25..9f0b63bc062c3f 100644 --- a/tests/components/youtube/test_sensor.py +++ b/tests/components/youtube/test_sensor.py @@ -3,12 +3,11 @@ from unittest.mock import patch from syrupy import SnapshotAssertion -from youtubeaio.types import UnauthorizedError +from youtubeaio.types import UnauthorizedError, YouTubeBackendError from homeassistant import config_entries from homeassistant.components.youtube.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from . import MockYouTube @@ -87,14 +86,18 @@ async def test_sensor_reauth_trigger( hass: HomeAssistant, setup_integration: ComponentSetup ) -> None: """Test reauth is triggered after a refresh error.""" - with patch( - "youtubeaio.youtube.YouTube.get_channels", side_effect=UnauthorizedError - ): - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - future = dt_util.utcnow() + timedelta(minutes=15) - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + mock = await setup_integration() + + state = hass.states.get("sensor.google_for_developers_latest_upload") + assert state.state == "What's new in Google Home in less than 1 minute" + + state = hass.states.get("sensor.google_for_developers_subscribers") + assert state.state == "2290000" + + mock.set_thrown_exception(UnauthorizedError()) + future = dt_util.utcnow() + timedelta(minutes=15) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() @@ -103,3 +106,27 @@ async def test_sensor_reauth_trigger( assert flow["step_id"] == "reauth_confirm" assert flow["handler"] == DOMAIN assert flow["context"]["source"] == config_entries.SOURCE_REAUTH + + +async def test_sensor_unavailable( + hass: HomeAssistant, setup_integration: ComponentSetup +) -> None: + """Test update failed.""" + mock = await setup_integration() + + state = hass.states.get("sensor.google_for_developers_latest_upload") + assert state.state == "What's new in Google Home in less than 1 minute" + + state = hass.states.get("sensor.google_for_developers_subscribers") + assert state.state == "2290000" + + mock.set_thrown_exception(YouTubeBackendError()) + future = dt_util.utcnow() + timedelta(minutes=15) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.google_for_developers_latest_upload") + assert state.state == "unavailable" + + state = hass.states.get("sensor.google_for_developers_subscribers") + assert state.state == "unavailable"