Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion homeassistant/components/unifi/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
from datetime import timedelta
from typing import TYPE_CHECKING, override

from aiounifi import EndpointNotFound
from aiounifi import EndpointNotFound, LoginRequired, Unauthorized
from aiounifi.interfaces.api_handlers import APIHandler, ItemEvent

from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import LOGGER
Expand Down Expand Up @@ -57,6 +58,8 @@ async def _async_update_data(self) -> None:
"""Update data from the API handler."""
try:
await self._handler.update()
except (Unauthorized, LoginRequired) as err:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might also happen on situations as when restarting the console so not sure this is the correct solution, it might introduce other issues

raise ConfigEntryAuthFailed(str(err)) from err
Comment on lines +61 to +62

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each coordinator stops itself: auth_failed prevents the reschedule, so a sibling that also gets a 401 makes one more failing call on its next tick and then stops. async_start_reauth_if_available keeps it to a single flow. That is bounded, not a loop, so I kept this change scoped to the coordinator.

except EndpointNotFound as err:
if (
self._disable_polling_on_endpoint_not_found
Expand Down
51 changes: 49 additions & 2 deletions tests/components/unifi/test_hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@
from unittest.mock import patch

import aiounifi
from aiounifi import EndpointNotFound
from aiounifi import EndpointNotFound, LoginRequired, Unauthorized
from aiounifi.interfaces.api_handlers import ItemEvent
from aiounifi.models.message import MessageKey
from freezegun.api import FrozenDateTimeFactory
import pytest

from homeassistant.components.unifi.const import CONF_BLOCK_CLIENT, DOMAIN
from homeassistant.components.unifi.coordinator import IDLE_POLL_INTERVAL, POLL_INTERVAL
from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect
from homeassistant.components.unifi.hub import get_unifi_api
from homeassistant.config_entries import ConfigEntryState
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import CONF_HOST, EVENT_STATE_REPORTED, Platform
from homeassistant.core import Event, EventStateReportedData, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
Expand Down Expand Up @@ -171,6 +172,52 @@ async def test_endpoint_not_found_disables_object_oriented_network_config_pollin
)


@pytest.mark.parametrize("error", [Unauthorized, LoginRequired])
async def test_authentication_error_triggers_reauth(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
config_entry_setup: MockConfigEntry,
error: type[Exception],
) -> None:
"""Ensure an authentication error starts reauth instead of looping."""
api = config_entry_setup.runtime_data.api

with patch.object(
api.traffic_rules,
"update",
side_effect=error(
"Call https://host:443/v2/api/site/default/trafficrules received 401 Unauthorized"
),
) as mock_update:
freezer.tick(IDLE_POLL_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()

assert mock_update.call_count == 1

# An authentication failure must stop the polling loop, otherwise the
# retries keep hitting the controller and can trip its login rate limit.
freezer.tick(IDLE_POLL_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()

assert mock_update.call_count == 1

coordinator = (
config_entry_setup.runtime_data.entity_loader.get_data_update_coordinator(
api.traffic_rules
)
)
assert coordinator.last_update_success is False
assert "Unexpected error fetching" not in caplog.text

flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN)
assert len(flows) == 1
assert flows[0]["context"]["source"] == SOURCE_REAUTH
assert flows[0]["context"]["entry_id"] == config_entry_setup.entry_id


@pytest.mark.parametrize(
"object_oriented_network_config_payload",
[
Expand Down