From 205508299366476b95d2fd6576763777489845d7 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Thu, 19 Feb 2026 17:14:14 +0000 Subject: [PATCH] Handle Mastodon auth fail in coordinator (#163234) --- .../components/mastodon/coordinator.py | 16 +++- tests/components/mastodon/test_init.py | 79 ++++++++++++++++++- 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mastodon/coordinator.py b/homeassistant/components/mastodon/coordinator.py index 99785eca80b9..5246bbd413af 100644 --- a/homeassistant/components/mastodon/coordinator.py +++ b/homeassistant/components/mastodon/coordinator.py @@ -6,13 +6,20 @@ from dataclasses import dataclass from datetime import timedelta from mastodon import Mastodon -from mastodon.Mastodon import Account, Instance, InstanceV2, MastodonError +from mastodon.Mastodon import ( + Account, + Instance, + InstanceV2, + MastodonError, + MastodonUnauthorizedError, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import LOGGER +from .const import DOMAIN, LOGGER @dataclass @@ -51,6 +58,11 @@ class MastodonCoordinator(DataUpdateCoordinator[Account]): account: Account = await self.hass.async_add_executor_job( self.client.account_verify_credentials ) + except MastodonUnauthorizedError as error: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="auth_failed", + ) from error except MastodonError as ex: raise UpdateFailed(ex) from ex diff --git a/tests/components/mastodon/test_init.py b/tests/components/mastodon/test_init.py index af6786a72883..f31235a5b796 100644 --- a/tests/components/mastodon/test_init.py +++ b/tests/components/mastodon/test_init.py @@ -1,21 +1,33 @@ """Tests for the Mastodon integration.""" +from datetime import timedelta from unittest.mock import AsyncMock -from mastodon.Mastodon import MastodonNotFoundError, MastodonUnauthorizedError +from freezegun.api import FrozenDateTimeFactory +from mastodon.Mastodon import ( + MastodonError, + MastodonNotFoundError, + MastodonUnauthorizedError, +) import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.mastodon.config_flow import MastodonConfigFlow from homeassistant.components.mastodon.const import CONF_BASE_URL, DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from . import setup_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_device_info( @@ -107,3 +119,62 @@ async def test_migrate( assert config_entry.version == MastodonConfigFlow.VERSION assert config_entry.minor_version == MastodonConfigFlow.MINOR_VERSION assert config_entry.unique_id == "trwnh_mastodon_social" + + +async def test_coordinator_general_error( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test general error during coordinator update makes entities unavailable.""" + await setup_integration(hass, mock_config_entry) + assert mock_config_entry.state is ConfigEntryState.LOADED + + state = hass.states.get("binary_sensor.mastodon_trwnh_mastodon_social_bot") + assert state is not None + assert state.state == STATE_ON + + mock_mastodon_client.account_verify_credentials.side_effect = MastodonError + + freezer.tick(timedelta(hours=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + state = hass.states.get("binary_sensor.mastodon_trwnh_mastodon_social_bot") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + # No reauth flow should be triggered (unlike auth errors) + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 0 + + +async def test_coordinator_auth_failure_triggers_reauth( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test auth failure during coordinator update triggers reauth flow.""" + await setup_integration(hass, mock_config_entry) + assert mock_config_entry.state is ConfigEntryState.LOADED + + mock_mastodon_client.account_verify_credentials.side_effect = ( + MastodonUnauthorizedError + ) + + freezer.tick(timedelta(hours=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["context"]["source"] == SOURCE_REAUTH + assert flow["context"]["entry_id"] == mock_config_entry.entry_id