189 lines
6.0 KiB
Python
189 lines
6.0 KiB
Python
"""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
|