mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 15:15:58 +01:00
183 lines
6.1 KiB
Python
183 lines
6.1 KiB
Python
"""Test the Traccar Server integration setup and subscription lifecycle."""
|
|
|
|
import asyncio
|
|
from collections.abc import Awaitable, Callable
|
|
from datetime import timedelta
|
|
import logging
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from freezegun.api import FrozenDateTimeFactory
|
|
import pytest
|
|
from pytraccar import SubscriptionData, TraccarAuthenticationException, TraccarException
|
|
|
|
from homeassistant.config_entries import ConfigEntryState
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
|
|
|
from .common import setup_integration
|
|
|
|
from tests.common import MockConfigEntry, async_capture_events, async_fire_time_changed
|
|
|
|
|
|
async def test_update_data_auth_failure_triggers_reauth(
|
|
hass: HomeAssistant,
|
|
mock_traccar_api_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""An auth failure during update should fail setup and prompt reauth."""
|
|
mock_traccar_api_client.get_devices.side_effect = TraccarAuthenticationException(
|
|
"Unauthorized"
|
|
)
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
|
assert any(
|
|
flow["context"]["source"] == "reauth"
|
|
for flow in hass.config_entries.flow.async_progress()
|
|
)
|
|
|
|
|
|
async def test_update_data_traccar_exception_retries_setup(
|
|
hass: HomeAssistant,
|
|
mock_traccar_api_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""A non-auth error during update should leave setup pending retry."""
|
|
mock_traccar_api_client.get_positions.side_effect = TraccarException(
|
|
"Simulated server error"
|
|
)
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
|
|
|
|
|
async def test_handle_subscription_data_logs_restored_after_failures(
|
|
hass: HomeAssistant,
|
|
mock_traccar_api_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Receiving data after failures logs a restored message and resets the counter."""
|
|
calls = 0
|
|
|
|
async def _fail_three_times_then_succeed(
|
|
callback: Callable[[SubscriptionData], Awaitable[None]],
|
|
) -> None:
|
|
nonlocal calls
|
|
calls += 1
|
|
if calls <= 3:
|
|
raise TraccarException("Simulated dropped connection")
|
|
await callback({"devices": None, "events": None, "positions": None})
|
|
raise asyncio.CancelledError
|
|
|
|
mock_traccar_api_client.subscribe = AsyncMock(
|
|
side_effect=_fail_three_times_then_succeed
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"homeassistant.components.traccar_server.coordinator._SUBSCRIPTION_RECONNECT_DELAY",
|
|
0,
|
|
),
|
|
caplog.at_level(logging.INFO, logger="homeassistant.components.traccar_server"),
|
|
):
|
|
await setup_integration(hass, mock_config_entry)
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
info_records = [r for r in caplog.records if r.levelno == logging.INFO]
|
|
assert any(
|
|
"connection restored after 3 failed attempt(s)" in r.message
|
|
for r in info_records
|
|
)
|
|
|
|
|
|
async def test_import_events_fires_hass_events(
|
|
hass: HomeAssistant,
|
|
mock_traccar_api_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
freezer: FrozenDateTimeFactory,
|
|
) -> None:
|
|
"""Events returned by the Traccar API are imported on schedule and fired on the HA bus."""
|
|
events = async_capture_events(hass, "traccar_device_moving")
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
freezer.tick(timedelta(seconds=30))
|
|
async_fire_time_changed(hass)
|
|
await hass.async_block_till_done()
|
|
|
|
assert len(events) == 1
|
|
assert events[0].data["device_traccar_id"] == 0
|
|
assert events[0].data["device_name"] == "X-Wing"
|
|
assert events[0].data["type"] == "deviceMoving"
|
|
|
|
|
|
async def test_subscribe_raises_config_entry_auth_failed(
|
|
hass: HomeAssistant,
|
|
mock_traccar_api_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""An authentication failure must stop retrying, not loop forever."""
|
|
ready_to_raise = asyncio.Event()
|
|
|
|
async def _raise_auth_failure_when_ready(_callback: object) -> None:
|
|
await ready_to_raise.wait()
|
|
raise TraccarAuthenticationException("Unauthorized")
|
|
|
|
mock_traccar_api_client.subscribe = AsyncMock(
|
|
side_effect=_raise_auth_failure_when_ready
|
|
)
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
# The background task the integration created is still pending here
|
|
# (it's blocked on ready_to_raise), so it's still tracked on the entry.
|
|
background_tasks = list(mock_config_entry._background_tasks)
|
|
assert len(background_tasks) == 1
|
|
|
|
ready_to_raise.set()
|
|
with pytest.raises(ConfigEntryAuthFailed):
|
|
await background_tasks[0]
|
|
|
|
# A retryable failure would call subscribe() again after the reconnect
|
|
# delay; an auth failure must not retry at all.
|
|
assert mock_traccar_api_client.subscribe.call_count == 1
|
|
|
|
|
|
async def test_subscribe_retries_on_unexpected_exception(
|
|
hass: HomeAssistant,
|
|
mock_traccar_api_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""An exception that isn't a TraccarException must still be retried.
|
|
|
|
pytraccar's own exceptions all subclass TraccarException, but an
|
|
unrecognized failure could still slip through as something else -
|
|
that must not be allowed to escape the retry loop.
|
|
"""
|
|
calls = 0
|
|
|
|
async def _weird_failure_then_cancel(_callback: object) -> None:
|
|
nonlocal calls
|
|
calls += 1
|
|
if calls >= 3:
|
|
raise asyncio.CancelledError
|
|
# Something that is NOT a TraccarException - e.g. a raw error
|
|
# that slipped through pytraccar's own exception wrapping.
|
|
raise ValueError("Simulated unexpected failure")
|
|
|
|
mock_traccar_api_client.subscribe = AsyncMock(
|
|
side_effect=_weird_failure_then_cancel
|
|
)
|
|
|
|
with patch(
|
|
"homeassistant.components.traccar_server.coordinator._SUBSCRIPTION_RECONNECT_DELAY",
|
|
0,
|
|
):
|
|
await setup_integration(hass, mock_config_entry)
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
assert calls == 3
|