Files
bot 988a30cbf7
Tests / pytest + ruff (3.12) (push) Has been cancelled
Validate / HACS validation (push) Has been cancelled
Validate / Hassfest validation (push) Has been cancelled
Initial commit: Main Gate Controller HACS integration
2026-09-07 23:19:12 +02:00

189 lines
6.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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