"""Tests covering error paths in the controller.""" from __future__ import annotations import logging from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.main_gate_controller.const import ( DOMAIN, GATE_STATE_CLOSED, NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE, ) from tests.conftest import ServiceCalls, make_entry_data def _make_failing_handler(fail_on_domain: str, fail_on_service: str, message: str): async def _handler(call) -> None: # type: ignore[no-untyped-def] if call.domain == fail_on_domain and call.service == fail_on_service: raise HomeAssistantError(message) # Recording handled elsewhere – this helper is only for failure simulation. return _handler async def test_notification_failure_continues_cycle( hass: HomeAssistant, service_calls: ServiceCalls ): """If a notify.send_message call fails, the cycle still finishes normally.""" switch = "switch.main_gate_relay" hass.states.async_set(switch, "off") hass.states.async_set("notify.mobile_app_phone", "unknown") entry = MockConfigEntry( domain=DOMAIN, title="Main Gate", data=make_entry_data( switch_entity_id=switch, name="Main Gate", open_duration=1, pulse_duration=0.1, notify_enabled=True, notify_entities=["notify.mobile_app_phone"], ), unique_id=switch, ) entry.add_to_hass(hass) # Replace the recording notify handler with one that fails. hass.services.async_remove(NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE) hass.services.async_register( NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE, _make_failing_handler( NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE, "deliberate failure" ), ) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() controller = entry.runtime_data assert controller.async_trigger() is True await controller.async_wait_until_idle() assert controller.status == GATE_STATE_CLOSED assert controller.is_running is False async def test_notification_log_warning_emitted( hass: HomeAssistant, service_calls: ServiceCalls, caplog ): """The notify failure path emits a warning via the module logger.""" switch = "switch.main_gate_relay" hass.states.async_set(switch, "off") hass.states.async_set("notify.mobile_app_phone", "unknown") entry = MockConfigEntry( domain=DOMAIN, title="Main Gate", data=make_entry_data( switch_entity_id=switch, name="Main Gate", open_duration=0.5, pulse_duration=0.1, notify_enabled=True, notify_entities=["notify.mobile_app_phone"], ), unique_id=switch, ) entry.add_to_hass(hass) hass.services.async_remove(NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE) async def _failing_notify(call) -> None: raise HomeAssistantError("boom") hass.services.async_register(NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE, _failing_notify) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() controller = entry.runtime_data with caplog.at_level(logging.WARNING): controller.async_trigger() await controller.async_wait_until_idle() assert any( "notification" in r.message.lower() and "boom" in r.message for r in caplog.records ) async def test_switch_failure_aborts_cycle( hass: HomeAssistant, service_calls: ServiceCalls, caplog ): """A switch.turn_on failure aborts the cycle and resets state to closed.""" switch = "switch.main_gate_relay" hass.states.async_set(switch, "off") entry = MockConfigEntry( domain=DOMAIN, title="Main Gate", data=make_entry_data( switch_entity_id=switch, name="Main Gate", open_duration=20, pulse_duration=0.1, notify_enabled=False, notify_entities=[], ), unique_id=switch, ) entry.add_to_hass(hass) # Replace the recording handler for switch.turn_off with a failing one. hass.services.async_remove("switch", "turn_off") async def _failing_turn_off(call) -> None: raise HomeAssistantError("relay offline") hass.services.async_register("switch", "turn_off", _failing_turn_off) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() controller = entry.runtime_data with caplog.at_level(logging.ERROR): controller.async_trigger() await controller.async_wait_until_idle() assert controller.status == GATE_STATE_CLOSED assert controller.is_running is False assert any( "aborted" in r.message.lower() or "cycle aborted" in r.message.lower() for r in caplog.records ) async def test_unload_cancels_running_cycle( hass: HomeAssistant, service_calls: ServiceCalls ): """Unloading an entry while a cycle runs cancels the background task.""" switch = "switch.main_gate_relay" hass.states.async_set(switch, "off") entry = MockConfigEntry( domain=DOMAIN, title="Main Gate", data=make_entry_data( switch_entity_id=switch, name="Main Gate", open_duration=3, pulse_duration=0.2, notify_enabled=False, notify_entities=[], ), unique_id=switch, ) entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() controller = entry.runtime_data controller.async_trigger() # Immediately start unloading. assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert controller.is_running is False assert controller.status == GATE_STATE_CLOSED