"""Test Teltonika sensor platform.""" from datetime import timedelta from unittest.mock import AsyncMock, MagicMock from aiohttp import ClientResponseError from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from teltasync import TeltonikaAuthenticationError, TeltonikaConnectionError from teltasync.error_codes import TeltonikaErrorCode from homeassistant.components.teltonika.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform async def test_sensors( hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, init_integration: MockConfigEntry, ) -> None: """Test sensor entities match snapshot.""" await snapshot_platform(hass, entity_registry, snapshot, init_integration.entry_id) async def test_sensor_modem_removed( hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration: MockConfigEntry, mock_modems: MagicMock, freezer: FrozenDateTimeFactory, ) -> None: """Test sensor becomes unavailable when modem is removed.""" # Get initial sensor state state = hass.states.get("sensor.rutx50_test_internal_modem_rssi") assert state is not None # Update coordinator with empty modem data mock_response = MagicMock() mock_response.data = [] # No modems mock_modems.get_status.return_value = mock_response freezer.tick(timedelta(seconds=31)) async_fire_time_changed(hass) await hass.async_block_till_done() # Check that entity is marked as unavailable state = hass.states.get("sensor.rutx50_test_internal_modem_rssi") assert state is not None # When modem is removed, entity should be marked as unavailable # Verify through entity registry that entity exists but is unavailable entity_entry = entity_registry.async_get("sensor.rutx50_test_internal_modem_rssi") assert entity_entry is not None # State should show unavailable when modem is removed assert state.state == "unavailable" async def test_sensor_update_failure_and_recovery( hass: HomeAssistant, mock_modems: AsyncMock, init_integration: MockConfigEntry, freezer: FrozenDateTimeFactory, ) -> None: """Test sensor becomes unavailable on update failure and recovers.""" # Get initial sensor state, here it should be available state = hass.states.get("sensor.rutx50_test_internal_modem_rssi") assert state is not None assert state.state == "-63" mock_modems.get_status.side_effect = TeltonikaConnectionError("Connection lost") freezer.tick(timedelta(seconds=30)) async_fire_time_changed(hass) await hass.async_block_till_done() # Sensor should now be unavailable state = hass.states.get("sensor.rutx50_test_internal_modem_rssi") assert state is not None assert state.state == "unavailable" # Simulate recovery mock_modems.get_status.side_effect = None freezer.tick(timedelta(seconds=30)) async_fire_time_changed(hass) await hass.async_block_till_done() # Sensor should be available again with correct data state = hass.states.get("sensor.rutx50_test_internal_modem_rssi") assert state is not None assert state.state == "-63" @pytest.mark.parametrize( ("side_effect", "expect_reauth"), [ (TeltonikaAuthenticationError("Invalid credentials"), True), ( ClientResponseError( request_info=MagicMock(), history=(), status=401, message="Unauthorized", headers={}, ), True, ), ( ClientResponseError( request_info=MagicMock(), history=(), status=500, message="Server error", headers={}, ), False, ), ], ids=["auth_exception", "http_auth_error", "http_non_auth_error"], ) async def test_sensor_update_exception_paths( hass: HomeAssistant, mock_modems: AsyncMock, init_integration: MockConfigEntry, freezer: FrozenDateTimeFactory, side_effect: Exception, expect_reauth: bool, ) -> None: """Test auth and non-auth exceptions during updates.""" mock_modems.get_status.side_effect = side_effect freezer.tick(timedelta(seconds=31)) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.rutx50_test_internal_modem_rssi") assert state is not None assert state.state == "unavailable" has_reauth = any( flow["handler"] == DOMAIN and flow["context"]["source"] == SOURCE_REAUTH for flow in hass.config_entries.flow.async_progress() ) assert has_reauth is expect_reauth @pytest.mark.parametrize( ("error_code", "expect_reauth"), [ (TeltonikaErrorCode.UNAUTHORIZED_ACCESS, True), (999, False), ], ids=["api_auth_error", "api_non_auth_error"], ) async def test_sensor_update_unsuccessful_response_paths( hass: HomeAssistant, mock_modems: AsyncMock, init_integration: MockConfigEntry, freezer: FrozenDateTimeFactory, error_code: int, expect_reauth: bool, ) -> None: """Test unsuccessful API response handling.""" mock_modems.get_status.side_effect = None mock_modems.get_status.return_value = MagicMock( success=False, data=None, errors=[MagicMock(code=error_code, error="API error")], ) freezer.tick(timedelta(seconds=31)) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.rutx50_test_internal_modem_rssi") assert state is not None assert state.state == "unavailable" has_reauth = any( flow["handler"] == DOMAIN and flow["context"]["source"] == SOURCE_REAUTH for flow in hass.config_entries.flow.async_progress() ) assert has_reauth is expect_reauth