mirror of
https://github.com/home-assistant/core.git
synced 2026-08-03 20:24:55 +02:00
Add 100% coverage to utils for Mikrotik (#177878)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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={})
|
||||
|
||||
Reference in New Issue
Block a user