Skip to content

Commit

Permalink
Handle UpdateFailed for YouTube (home-assistant#97233)
Browse files Browse the repository at this point in the history
  • Loading branch information
joostlek authored Jul 26, 2023
1 parent db491c8 commit d233438
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 22 deletions.
6 changes: 4 additions & 2 deletions homeassistant/components/youtube/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions homeassistant/components/youtube/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
)

Expand Down
9 changes: 7 additions & 2 deletions tests/components/youtube/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
class MockYouTube:
"""Service which returns mock objects."""

_authenticated = False
_thrown_error: Exception | None = None

def __init__(
self,
Expand All @@ -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."""
Expand All @@ -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)
Expand All @@ -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
12 changes: 6 additions & 6 deletions tests/components/youtube/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)

Expand All @@ -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
47 changes: 37 additions & 10 deletions tests/components/youtube/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand All @@ -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"

0 comments on commit d233438

Please sign in to comment.