Files
core/tests/components/august/test_init.py

304 lines
11 KiB
Python

"""The tests for the august platform."""
from unittest.mock import Mock, patch
from aiohttp import ClientResponseError
import pytest
from yalexs.const import Brand
from yalexs.exceptions import AugustApiAIOHTTPError, InvalidAuth
from homeassistant.components.august.const import DOMAIN
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockState
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_LOCK,
SERVICE_OPEN,
SERVICE_UNLOCK,
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.setup import async_setup_component
from .mocks import (
_create_august_with_devices,
_mock_doorsense_enabled_august_lock_detail,
_mock_doorsense_missing_august_lock_detail,
_mock_inoperative_august_lock_detail,
_mock_lock_with_offline_key,
_mock_operative_august_lock_detail,
mock_august_config_entry,
mock_client_credentials,
)
from tests.common import MockConfigEntry
from tests.typing import WebSocketGenerator
async def test_august_api_is_failing(hass: HomeAssistant) -> None:
"""Config entry state is SETUP_RETRY when august api is failing."""
config_entry, _ = await _create_august_with_devices(
hass,
authenticate_side_effect=AugustApiAIOHTTPError(
"offline", ClientResponseError(None, None, status=500)
),
)
assert config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_august_is_offline(hass: HomeAssistant) -> None:
"""Config entry state is SETUP_RETRY when august is offline."""
config_entry, _ = await _create_august_with_devices(
hass, authenticate_side_effect=TimeoutError
)
assert config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_august_late_auth_failure(hass: HomeAssistant) -> None:
"""Test we can detect a late auth failure."""
config_entry, _ = await _create_august_with_devices(
hass,
authenticate_side_effect=InvalidAuth(
"authfailed", ClientResponseError(None, None, status=401)
),
)
assert config_entry.state is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert flows[0]["step_id"] == "pick_implementation"
async def test_unlock_throws_august_api_http_error(hass: HomeAssistant) -> None:
"""Test unlock throws correct error on http error."""
mocked_lock_detail = await _mock_operative_august_lock_detail(hass)
aiohttp_client_response_exception = ClientResponseError(None, None, status=400)
def _unlock_return_activities_side_effect(access_token, device_id):
raise AugustApiAIOHTTPError(
"This should bubble up as its user consumable",
aiohttp_client_response_exception,
)
await _create_august_with_devices(
hass,
[mocked_lock_detail],
api_call_side_effects={
"unlock_return_activities": _unlock_return_activities_side_effect
},
)
data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"}
with pytest.raises(
HomeAssistantError,
match=(
"A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user"
" consumable"
),
):
await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True)
async def test_lock_throws_august_api_http_error(hass: HomeAssistant) -> None:
"""Test lock throws correct error on http error."""
mocked_lock_detail = await _mock_operative_august_lock_detail(hass)
aiohttp_client_response_exception = ClientResponseError(None, None, status=400)
def _lock_return_activities_side_effect(access_token, device_id):
raise AugustApiAIOHTTPError(
"This should bubble up as its user consumable",
aiohttp_client_response_exception,
)
await _create_august_with_devices(
hass,
[mocked_lock_detail],
api_call_side_effects={
"lock_return_activities": _lock_return_activities_side_effect
},
)
data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"}
with pytest.raises(
HomeAssistantError,
match=(
"A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user"
" consumable"
),
):
await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True)
async def test_open_throws_hass_service_not_supported_error(
hass: HomeAssistant,
) -> None:
"""Test open throws correct error on entity does not support this service error."""
mocked_lock_detail = await _mock_operative_august_lock_detail(hass)
await _create_august_with_devices(hass, [mocked_lock_detail])
data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"}
with pytest.raises(HomeAssistantError):
await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True)
async def test_inoperative_locks_are_filtered_out(hass: HomeAssistant) -> None:
"""Ensure inoperative locks do not get setup."""
august_operative_lock = await _mock_operative_august_lock_detail(hass)
august_inoperative_lock = await _mock_inoperative_august_lock_detail(hass)
await _create_august_with_devices(
hass, [august_operative_lock, august_inoperative_lock]
)
lock_abc_name = hass.states.get("lock.abc_name")
assert lock_abc_name is None
lock_a6697750d607098bae8d6baa11ef8063_name = hass.states.get(
"lock.a6697750d607098bae8d6baa11ef8063_name"
)
assert lock_a6697750d607098bae8d6baa11ef8063_name.state == LockState.LOCKED
async def test_lock_has_doorsense(hass: HomeAssistant) -> None:
"""Check to see if a lock has doorsense."""
doorsenselock = await _mock_doorsense_enabled_august_lock_detail(hass)
nodoorsenselock = await _mock_doorsense_missing_august_lock_detail(hass)
await _create_august_with_devices(hass, [doorsenselock, nodoorsenselock])
binary_sensor_online_with_doorsense_name_open = hass.states.get(
"binary_sensor.online_with_doorsense_name_door"
)
assert binary_sensor_online_with_doorsense_name_open.state == STATE_ON
binary_sensor_missing_doorsense_id_name_open = hass.states.get(
"binary_sensor.missing_with_doorsense_name_door"
)
assert binary_sensor_missing_doorsense_id_name_open is None
async def test_load_unload(hass: HomeAssistant) -> None:
"""Config entry can be unloaded."""
august_operative_lock = await _mock_operative_august_lock_detail(hass)
august_inoperative_lock = await _mock_inoperative_august_lock_detail(hass)
config_entry, _ = await _create_august_with_devices(
hass, [august_operative_lock, august_inoperative_lock]
)
assert config_entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.NOT_LOADED
async def test_load_triggers_ble_discovery(
hass: HomeAssistant, mock_discovery: Mock
) -> None:
"""Test that loading a lock that supports offline ble operation passes the keys to yalexe_ble."""
august_lock_with_key = await _mock_lock_with_offline_key(hass)
august_lock_without_key = await _mock_operative_august_lock_detail(hass)
config_entry, _ = await _create_august_with_devices(
hass, [august_lock_with_key, august_lock_without_key]
)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert len(mock_discovery.mock_calls) == 1
assert mock_discovery.mock_calls[0].kwargs["data"] == {
"name": "Front Door Lock",
"address": None,
"serial": "X2FSW05DGA",
"key": "kkk01d4300c1dcxxx1c330f794941111",
"slot": 1,
}
async def test_device_remove_devices(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test we can only remove a device that no longer exists."""
assert await async_setup_component(hass, "config", {})
august_operative_lock = await _mock_operative_august_lock_detail(hass)
config_entry, _ = await _create_august_with_devices(hass, [august_operative_lock])
entity = entity_registry.entities["lock.a6697750d607098bae8d6baa11ef8063_name"]
device_entry = device_registry.async_get(entity.device_id)
client = await hass_ws_client(hass)
response = await client.remove_device(device_entry.id, config_entry.entry_id)
assert not response["success"]
dead_device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, "remove-device-id")},
)
response = await client.remove_device(dead_device_entry.id, config_entry.entry_id)
assert response["success"]
async def test_brand_migration_issue(hass: HomeAssistant) -> None:
"""Test removing the brand migration issue."""
august_operative_lock = await _mock_operative_august_lock_detail(hass)
config_entry, _ = await _create_august_with_devices(
hass, [august_operative_lock], brand=Brand.YALE_HOME
)
assert config_entry.state is ConfigEntryState.LOADED
issue_reg = ir.async_get(hass)
await hass.config_entries.async_remove(config_entry.entry_id)
assert not issue_reg.async_get_issue(DOMAIN, "yale_brand_migration")
async def test_oauth_migration_on_legacy_entry(hass: HomeAssistant) -> None:
"""Test that legacy config entry triggers OAuth migration."""
# Create a legacy config entry without auth_implementation
legacy_entry = MockConfigEntry(
domain=DOMAIN,
data={
"login_method": "email",
"username": "test@example.com",
"password": "test-password",
"install_id": None,
"timeout": 10,
"access_token_cache_file": ".test@example.com.august.conf",
},
unique_id="test@example.com",
)
legacy_entry.add_to_hass(hass)
# Try to setup the entry - should fail with auth error and trigger reauth
await hass.config_entries.async_setup(legacy_entry.entry_id)
await hass.async_block_till_done()
# Entry should be in setup_error state
assert legacy_entry.state is ConfigEntryState.SETUP_ERROR
# A reauth flow should be started
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "pick_implementation"
assert flows[0]["context"]["source"] == "reauth"
async def test_oauth_implementation_not_available(hass: HomeAssistant) -> None:
"""Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
await mock_client_credentials(hass)
entry = await mock_august_config_entry(hass)
with patch(
"homeassistant.components.august.config_entry_oauth2_flow.async_get_config_entry_implementation",
side_effect=ValueError("Implementation not available"),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY