Add 100% coverage to utils for Mikrotik (#177878)

This commit is contained in:
Simone Chemelli
2026-08-03 11:11:05 +02:00
committed by GitHub
parent e01e308b15
commit 4dd9fef63d
2 changed files with 163 additions and 14 deletions
+6 -2
View File
@@ -1,15 +1,19 @@
"""Mikrotik test configuration."""
from collections.abc import Generator
from collections.abc import Callable, Generator
from unittest.mock import MagicMock, patch
import pytest
from . import create_mock_config_entry
from tests.common import MockConfigEntry
type MockConfigEntryFactory = Callable[..., MockConfigEntry]
@pytest.fixture
def mock_config_entry():
def mock_config_entry() -> MockConfigEntryFactory:
"""Create Mikrotik config entries with optional overrides."""
return create_mock_config_entry
+157 -12
View File
@@ -1,57 +1,181 @@
"""Test Mikrotik setup process."""
from collections.abc import Callable
from datetime import timedelta
from typing import Any
from unittest.mock import MagicMock, patch
from librouteros.exceptions import ConnectionClosed, LibRouterosError
import pytest
from homeassistant.config_entries import ConfigEntryState
from homeassistant.components.mikrotik.const import (
IDENTITY,
MIKROTIK_SERVICES,
ROUTERBOARD,
)
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import UpdateFailed
from homeassistant.util import dt as dt_util
from . import setup_integration
from .conftest import MockConfigEntryFactory
from tests.common import async_fire_time_changed
_BASE_COMMAND_RESPONSES: dict[str, list[dict[str, Any]]] = {
MIKROTIK_SERVICES[IDENTITY]: [{"name": "Mikrotik"}]
}
async def test_successful_config_entry(hass: HomeAssistant, mock_config_entry) -> None:
def _command_side_effect(
error_cmd: str, error: Exception
) -> Callable[..., list[dict[str, Any]]]:
"""Return minimal hub responses, except for one command that raises."""
def side_effect(cmd: str, **params: Any) -> list[dict[str, Any]]:
if cmd == error_cmd:
raise error
return _BASE_COMMAND_RESPONSES.get(cmd, [])
return side_effect
async def test_successful_config_entry(
hass: HomeAssistant, mock_config_entry: MockConfigEntryFactory
) -> None:
"""Test config entry successful setup."""
entry = mock_config_entry()
await setup_integration(hass, entry, command_responses={})
assert entry.state is ConfigEntryState.LOADED
@pytest.mark.parametrize(
"error",
[ConnectionClosed(), OSError(), TimeoutError()],
ids=["connection_closed", "os_error", "timeout_error"],
)
async def test_hub_connection_error(
hass: HomeAssistant, mock_api: MagicMock, mock_config_entry
hass: HomeAssistant,
mock_api: MagicMock,
mock_config_entry: MockConfigEntryFactory,
error: Exception,
) -> None:
"""Test setup fails due to connection error."""
"""Test setup retries when the hub can't be reached after connecting."""
entry = mock_config_entry()
entry.add_to_hass(hass)
mock_api.side_effect = ConnectionClosed
mock_api.side_effect = error
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_hub_authentication_error(
hass: HomeAssistant, mock_api: MagicMock, mock_config_entry
@pytest.mark.parametrize(
"error",
[
LibRouterosError("no route to host"),
OSError(),
TimeoutError(),
ConnectionClosed(),
],
ids=["cannot_connect", "os_error", "timeout_error", "connection_closed"],
)
async def test_hub_connect_error_retries_setup(
hass: HomeAssistant,
mock_config_entry: MockConfigEntryFactory,
error: Exception,
) -> None:
"""Test setup fails due to authentication error."""
"""Test setup retries when the initial connection to the hub fails."""
entry = mock_config_entry()
entry.add_to_hass(hass)
mock_api.side_effect = LibRouterosError("invalid user name or password")
with patch("librouteros.connect", side_effect=error):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_hub_login_error_starts_reauth(
hass: HomeAssistant, mock_config_entry: MockConfigEntryFactory
) -> None:
"""Test setup starts a reauth flow when the hub rejects the credentials."""
entry = mock_config_entry()
entry.add_to_hass(hass)
with patch(
"librouteros.connect",
side_effect=LibRouterosError("invalid user name or password"),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_ERROR
assert any(entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
async def test_optional_command_error_is_suppressed(
hass: HomeAssistant, mock_api: MagicMock, mock_config_entry: MockConfigEntryFactory
) -> None:
"""Test setup succeeds when an optional command isn't supported by the hub."""
entry = mock_config_entry()
entry.add_to_hass(hass)
mock_api.side_effect = _command_side_effect(
MIKROTIK_SERVICES[ROUTERBOARD], LibRouterosError("no such command prefix")
)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.LOADED
async def test_optional_command_unexpected_error_fails_setup(
hass: HomeAssistant, mock_api: MagicMock, mock_config_entry: MockConfigEntryFactory
) -> None:
"""Test setup fails when an optional command raises an unexpected error."""
entry = mock_config_entry()
entry.add_to_hass(hass)
mock_api.side_effect = _command_side_effect(
MIKROTIK_SERVICES[ROUTERBOARD], LibRouterosError("permission denied")
)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_ERROR
async def test_required_command_error_fails_setup(
hass: HomeAssistant, mock_api: MagicMock, mock_config_entry: MockConfigEntryFactory
) -> None:
"""Test setup fails on a required command even with a suppressible message."""
entry = mock_config_entry()
entry.add_to_hass(hass)
mock_api.side_effect = _command_side_effect(
MIKROTIK_SERVICES[IDENTITY], LibRouterosError("no such command prefix")
)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_ERROR
@pytest.mark.parametrize(
"error",
[OSError(), TimeoutError(), ConnectionClosed()],
ids=["os_error", "timeout_error", "connection_closed"],
)
async def test_connection_lost_during_refresh_raises_update_failed(
hass: HomeAssistant, mock_config_entry
hass: HomeAssistant,
mock_config_entry: MockConfigEntryFactory,
error: Exception,
) -> None:
"""Test a lost connection during a scheduled refresh is treated as UpdateFailed.
@@ -68,7 +192,7 @@ async def test_connection_lost_during_refresh_raises_update_failed(
with patch.object(
entry.runtime_data.api,
"command",
side_effect=OSError(113, "Host is unreachable"),
side_effect=error,
):
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
await hass.async_block_till_done(wait_background_tasks=True)
@@ -78,7 +202,28 @@ async def test_connection_lost_during_refresh_raises_update_failed(
assert isinstance(coordinator.last_exception, UpdateFailed)
async def test_unload_entry(hass: HomeAssistant, mock_config_entry) -> None:
async def test_hub_reconnect_error_during_refresh_raises_update_failed(
hass: HomeAssistant, mock_config_entry: MockConfigEntryFactory
) -> None:
"""Test a failed reconnect during a scheduled refresh is treated as UpdateFailed."""
entry = mock_config_entry()
await setup_integration(hass, entry, command_responses={})
assert entry.state is ConfigEntryState.LOADED
coordinator = entry.runtime_data
with patch("librouteros.connect", side_effect=OSError()):
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
await hass.async_block_till_done(wait_background_tasks=True)
assert entry.state is ConfigEntryState.LOADED
assert coordinator.last_update_success is False
assert isinstance(coordinator.last_exception, UpdateFailed)
async def test_unload_entry(
hass: HomeAssistant, mock_config_entry: MockConfigEntryFactory
) -> None:
"""Test unloading an entry."""
entry = mock_config_entry()
await setup_integration(hass, entry, command_responses={})