Implement bridge-backed Home Assistant integration
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
"""Tests for MB-Secure snapshot and event coordination."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import UpdateFailed
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from custom_components.mb_secure.api import BridgeAuthenticationError
|
||||
from custom_components.mb_secure.const import CONF_TOKEN, DOMAIN
|
||||
from custom_components.mb_secure.coordinator import MBSecureCoordinator
|
||||
from custom_components.mb_secure.models import BridgeInfo, BridgeSnapshot, DomainEvent
|
||||
|
||||
|
||||
def snapshot(state: str = "disarmed", revision: int = 1) -> BridgeSnapshot:
|
||||
"""Return a minimal area snapshot."""
|
||||
return BridgeSnapshot.from_dict(
|
||||
{
|
||||
"revision": revision,
|
||||
"controllers": [
|
||||
{
|
||||
"id": "controller-1",
|
||||
"name": "Controller",
|
||||
"state": "online",
|
||||
"capabilities": [],
|
||||
}
|
||||
],
|
||||
"areas": [
|
||||
{
|
||||
"id": "area-1",
|
||||
"controller_id": "controller-1",
|
||||
"name": "Area",
|
||||
"state": state,
|
||||
"capabilities": ["arm_away"],
|
||||
}
|
||||
],
|
||||
"points": [],
|
||||
"modules": [],
|
||||
"outputs": [],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def coordinator(
|
||||
hass: HomeAssistant, client: MagicMock
|
||||
) -> tuple[MBSecureCoordinator, MockConfigEntry]:
|
||||
"""Create a coordinator and attached mock config entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={"host": "bridge.local", "port": 8099, CONF_TOKEN: "x" * 32},
|
||||
unique_id="bridge-installation-1",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
return MBSecureCoordinator(hass, entry, client), entry
|
||||
|
||||
|
||||
async def test_snapshot_authentication_failure_starts_reauth(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Rejected bridge credentials are converted to Home Assistant reauth."""
|
||||
client = MagicMock()
|
||||
client.async_get_info = AsyncMock(side_effect=BridgeAuthenticationError)
|
||||
instance, _ = coordinator(hass, client)
|
||||
|
||||
with pytest.raises(ConfigEntryAuthFailed):
|
||||
await instance._async_update_data()
|
||||
|
||||
|
||||
async def test_snapshot_rejects_changed_bridge_identity(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""An endpoint cannot silently replace the configured bridge instance."""
|
||||
client = MagicMock()
|
||||
client.async_get_info = AsyncMock(
|
||||
return_value=BridgeInfo(
|
||||
bridge_id="different-installation",
|
||||
bridge_version="0.1.0",
|
||||
api_version=1,
|
||||
connected=True,
|
||||
capabilities=frozenset(),
|
||||
)
|
||||
)
|
||||
client.async_get_snapshot = AsyncMock(return_value=snapshot())
|
||||
instance, _ = coordinator(hass, client)
|
||||
|
||||
with pytest.raises(UpdateFailed, match="bridge installation ID has changed"):
|
||||
await instance._async_update_data()
|
||||
|
||||
client.async_get_snapshot.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_contiguous_event_updates_coordinator_data(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""A contiguous event updates state without waiting for the next poll."""
|
||||
client = MagicMock()
|
||||
|
||||
async def events():
|
||||
yield DomainEvent(
|
||||
revision=2,
|
||||
event_type="state_changed",
|
||||
object_type="area",
|
||||
object_id="area-1",
|
||||
state="armed_away",
|
||||
)
|
||||
raise BridgeAuthenticationError
|
||||
|
||||
client.async_events = events
|
||||
instance, entry = coordinator(hass, client)
|
||||
instance.async_set_updated_data(snapshot())
|
||||
entry.async_start_reauth = MagicMock()
|
||||
|
||||
await instance._async_event_loop()
|
||||
|
||||
assert instance.data.revision == 2
|
||||
assert instance.data.areas["area-1"].state == "armed_away"
|
||||
entry.async_start_reauth.assert_called_once_with(hass)
|
||||
|
||||
|
||||
async def test_event_gap_requests_snapshot_reconciliation(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""A revision gap schedules a full snapshot refresh."""
|
||||
client = MagicMock()
|
||||
|
||||
async def events():
|
||||
yield DomainEvent(
|
||||
revision=3,
|
||||
event_type="state_changed",
|
||||
object_type="area",
|
||||
object_id="area-1",
|
||||
state="armed_away",
|
||||
)
|
||||
raise BridgeAuthenticationError
|
||||
|
||||
client.async_events = events
|
||||
instance, entry = coordinator(hass, client)
|
||||
instance.async_set_updated_data(snapshot())
|
||||
instance.async_request_refresh = AsyncMock()
|
||||
entry.async_start_reauth = MagicMock()
|
||||
|
||||
await instance._async_event_loop()
|
||||
|
||||
instance.async_request_refresh.assert_awaited_once()
|
||||
assert instance.data.revision == 1
|
||||
Reference in New Issue
Block a user