Handle UpdateFailed for YouTube (#97233)

This commit is contained in:
Joost Lekkerkerker 2023-07-26 15:09:15 +02:00 committed by GitHub
parent db491c86c3
commit d233438e1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 56 additions and 22 deletions

View File

@ -5,13 +5,13 @@ from datetime import timedelta
from typing import Any from typing import Any
from youtubeaio.helper import first from youtubeaio.helper import first
from youtubeaio.types import UnauthorizedError from youtubeaio.types import UnauthorizedError, YouTubeBackendError
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ICON, ATTR_ID from homeassistant.const import ATTR_ICON, ATTR_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from . import AsyncConfigEntryAuth from . import AsyncConfigEntryAuth
from .const import ( from .const import (
@ -70,4 +70,6 @@ class YouTubeDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
} }
except UnauthorizedError as err: except UnauthorizedError as err:
raise ConfigEntryAuthFailed from err raise ConfigEntryAuthFailed from err
except YouTubeBackendError as err:
raise UpdateFailed("Couldn't connect to YouTube") from err
return res return res

View File

@ -87,9 +87,9 @@ class YouTubeSensor(YouTubeChannelEntity, SensorEntity):
entity_description: YouTubeSensorEntityDescription entity_description: YouTubeSensorEntityDescription
@property @property
def available(self): def available(self) -> bool:
"""Return if the entity is available.""" """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] self.coordinator.data[self._channel_id]
) )

View File

@ -11,7 +11,7 @@ from tests.common import load_fixture
class MockYouTube: class MockYouTube:
"""Service which returns mock objects.""" """Service which returns mock objects."""
_authenticated = False _thrown_error: Exception | None = None
def __init__( def __init__(
self, self,
@ -28,7 +28,6 @@ class MockYouTube:
self, token: str, scopes: list[AuthScope] self, token: str, scopes: list[AuthScope]
) -> None: ) -> None:
"""Authenticate the user.""" """Authenticate the user."""
self._authenticated = True
async def get_user_channels(self) -> AsyncGenerator[YouTubeChannel, None]: async def get_user_channels(self) -> AsyncGenerator[YouTubeChannel, None]:
"""Get channels for authenticated user.""" """Get channels for authenticated user."""
@ -40,6 +39,8 @@ class MockYouTube:
self, channel_ids: list[str] self, channel_ids: list[str]
) -> AsyncGenerator[YouTubeChannel, None]: ) -> AsyncGenerator[YouTubeChannel, None]:
"""Get channels.""" """Get channels."""
if self._thrown_error is not None:
raise self._thrown_error
channels = json.loads(load_fixture(self._channel_fixture)) channels = json.loads(load_fixture(self._channel_fixture))
for item in channels["items"]: for item in channels["items"]:
yield YouTubeChannel(**item) yield YouTubeChannel(**item)
@ -57,3 +58,7 @@ class MockYouTube:
channels = json.loads(load_fixture(self._subscriptions_fixture)) channels = json.loads(load_fixture(self._subscriptions_fixture))
for item in channels["items"]: for item in channels["items"]:
yield YouTubeSubscription(**item) yield YouTubeSubscription(**item)
def set_thrown_exception(self, exception: Exception) -> None:
"""Set thrown exception for testing purposes."""
self._thrown_error = exception

View File

@ -18,7 +18,7 @@ from tests.common import MockConfigEntry
from tests.components.youtube import MockYouTube from tests.components.youtube import MockYouTube
from tests.test_util.aiohttp import AiohttpClientMocker from tests.test_util.aiohttp import AiohttpClientMocker
ComponentSetup = Callable[[], Awaitable[None]] ComponentSetup = Callable[[], Awaitable[MockYouTube]]
CLIENT_ID = "1234" CLIENT_ID = "1234"
CLIENT_SECRET = "5678" CLIENT_SECRET = "5678"
@ -92,7 +92,7 @@ def mock_connection(aioclient_mock: AiohttpClientMocker) -> None:
@pytest.fixture(name="setup_integration") @pytest.fixture(name="setup_integration")
async def mock_setup_integration( async def mock_setup_integration(
hass: HomeAssistant, config_entry: MockConfigEntry hass: HomeAssistant, config_entry: MockConfigEntry
) -> Callable[[], Coroutine[Any, Any, None]]: ) -> Callable[[], Coroutine[Any, Any, MockYouTube]]:
"""Fixture for setting up the component.""" """Fixture for setting up the component."""
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
@ -104,11 +104,11 @@ async def mock_setup_integration(
DOMAIN, DOMAIN,
) )
async def func() -> None: async def func() -> MockYouTube:
with patch( mock = MockYouTube()
"homeassistant.components.youtube.api.YouTube", return_value=MockYouTube() with patch("homeassistant.components.youtube.api.YouTube", return_value=mock):
):
assert await async_setup_component(hass, DOMAIN, {}) assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done() await hass.async_block_till_done()
return mock
return func return func

View File

@ -3,12 +3,11 @@ from datetime import timedelta
from unittest.mock import patch from unittest.mock import patch
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
from youtubeaio.types import UnauthorizedError from youtubeaio.types import UnauthorizedError, YouTubeBackendError
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.youtube.const import DOMAIN from homeassistant.components.youtube.const import DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import MockYouTube from . import MockYouTube
@ -87,11 +86,15 @@ async def test_sensor_reauth_trigger(
hass: HomeAssistant, setup_integration: ComponentSetup hass: HomeAssistant, setup_integration: ComponentSetup
) -> None: ) -> None:
"""Test reauth is triggered after a refresh error.""" """Test reauth is triggered after a refresh error."""
with patch( mock = await setup_integration()
"youtubeaio.youtube.YouTube.get_channels", side_effect=UnauthorizedError
): state = hass.states.get("sensor.google_for_developers_latest_upload")
assert await async_setup_component(hass, DOMAIN, {}) assert state.state == "What's new in Google Home in less than 1 minute"
await hass.async_block_till_done()
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) future = dt_util.utcnow() + timedelta(minutes=15)
async_fire_time_changed(hass, future) async_fire_time_changed(hass, future)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -103,3 +106,27 @@ async def test_sensor_reauth_trigger(
assert flow["step_id"] == "reauth_confirm" assert flow["step_id"] == "reauth_confirm"
assert flow["handler"] == DOMAIN assert flow["handler"] == DOMAIN
assert flow["context"]["source"] == config_entries.SOURCE_REAUTH 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"