core/tests/components/rainbird/test_init.py

437 lines
13 KiB
Python

"""Tests for rainbird initialization."""
from __future__ import annotations
from http import HTTPStatus
from typing import Any
import pytest
from homeassistant.components.rainbird.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_MAC
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .conftest import (
CONFIG_ENTRY_DATA,
CONFIG_ENTRY_DATA_OLD_FORMAT,
MAC_ADDRESS,
MAC_ADDRESS_UNIQUE_ID,
MODEL_AND_VERSION_RESPONSE,
SERIAL_NUMBER,
WIFI_PARAMS_RESPONSE,
mock_json_response,
mock_response,
mock_response_error,
)
from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMockResponse
async def test_init_success(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test successful setup and unload."""
await hass.config_entries.async_setup(config_entry.entry_id)
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
@pytest.mark.parametrize(
("config_entry_data", "responses", "config_entry_state"),
[
(
CONFIG_ENTRY_DATA,
[mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)],
ConfigEntryState.SETUP_RETRY,
),
(
CONFIG_ENTRY_DATA,
[mock_response_error(HTTPStatus.INTERNAL_SERVER_ERROR)],
ConfigEntryState.SETUP_RETRY,
),
(
CONFIG_ENTRY_DATA,
[
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE),
],
ConfigEntryState.SETUP_RETRY,
),
(
CONFIG_ENTRY_DATA,
[
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response_error(HTTPStatus.INTERNAL_SERVER_ERROR),
],
ConfigEntryState.SETUP_RETRY,
),
],
ids=[
"unavailable",
"server-error",
"coordinator-unavailable",
"coordinator-server-error",
],
)
async def test_communication_failure(
hass: HomeAssistant,
config_entry: MockConfigEntry,
config_entry_state: list[ConfigEntryState],
) -> None:
"""Test unable to talk to device on startup, which fails setup."""
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state == config_entry_state
@pytest.mark.parametrize(
("config_entry_unique_id", "config_entry_data"),
[
(
None,
{**CONFIG_ENTRY_DATA, "mac": None},
),
],
ids=["config_entry"],
)
async def test_fix_unique_id(
hass: HomeAssistant,
responses: list[AiohttpClientMockResponse],
config_entry: MockConfigEntry,
) -> None:
"""Test fix of a config entry with no unique id."""
responses.insert(0, mock_json_response(WIFI_PARAMS_RESPONSE))
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state is ConfigEntryState.NOT_LOADED
assert entries[0].unique_id is None
assert entries[0].data.get(CONF_MAC) is None
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.LOADED
# Verify config entry now has a unique id
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state is ConfigEntryState.LOADED
assert entries[0].unique_id == MAC_ADDRESS_UNIQUE_ID
assert entries[0].data.get(CONF_MAC) == MAC_ADDRESS
@pytest.mark.parametrize(
(
"config_entry_unique_id",
"config_entry_data",
"initial_response",
"expected_warning",
),
[
(
None,
CONFIG_ENTRY_DATA_OLD_FORMAT,
mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE),
"Unable to fix missing unique id:",
),
(
None,
CONFIG_ENTRY_DATA_OLD_FORMAT,
mock_response_error(HTTPStatus.NOT_FOUND),
"Unable to fix missing unique id:",
),
(
None,
CONFIG_ENTRY_DATA_OLD_FORMAT,
mock_response("bogus"),
"Unable to fix missing unique id (mac address was None)",
),
],
ids=["service_unavailable", "not_found", "unexpected_response_format"],
)
async def test_fix_unique_id_failure(
hass: HomeAssistant,
initial_response: AiohttpClientMockResponse,
responses: list[AiohttpClientMockResponse],
expected_warning: str,
caplog: pytest.LogCaptureFixture,
config_entry: MockConfigEntry,
) -> None:
"""Test a failure during fix of a config entry with no unique id."""
responses.insert(0, initial_response)
await hass.config_entries.async_setup(config_entry.entry_id)
# Config entry is loaded, but not updated
assert config_entry.state is ConfigEntryState.LOADED
assert config_entry.unique_id is None
assert expected_warning in caplog.text
@pytest.mark.parametrize(
("config_entry_unique_id"),
[(MAC_ADDRESS_UNIQUE_ID)],
)
async def test_fix_unique_id_duplicate(
hass: HomeAssistant,
config_entry: MockConfigEntry,
responses: list[AiohttpClientMockResponse],
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that a config entry unique id already exists during fix."""
# Add a second config entry that has no unique id, but has the same
# mac address. When fixing the unique id, it can't use the mac address
# since it already exists.
other_entry = MockConfigEntry(
unique_id=None,
domain=DOMAIN,
data=CONFIG_ENTRY_DATA_OLD_FORMAT,
)
other_entry.add_to_hass(hass)
# Responses for the second config entry. This first fetches wifi params
# to repair the unique id.
responses_copy = [*responses]
responses.append(mock_json_response(WIFI_PARAMS_RESPONSE))
responses.extend(responses_copy)
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.LOADED
assert config_entry.unique_id == MAC_ADDRESS_UNIQUE_ID
assert "Unable to fix missing unique id (already exists)" in caplog.text
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
@pytest.mark.parametrize(
(
"config_entry_unique_id",
"serial_number",
"entity_unique_id",
"device_identifier",
"expected_unique_id",
"expected_device_identifier",
),
[
(
SERIAL_NUMBER,
SERIAL_NUMBER,
SERIAL_NUMBER,
str(SERIAL_NUMBER),
MAC_ADDRESS_UNIQUE_ID,
MAC_ADDRESS_UNIQUE_ID,
),
(
SERIAL_NUMBER,
SERIAL_NUMBER,
f"{SERIAL_NUMBER}-rain-delay",
f"{SERIAL_NUMBER}-1",
f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay",
f"{MAC_ADDRESS_UNIQUE_ID}-1",
),
(
SERIAL_NUMBER,
SERIAL_NUMBER,
SERIAL_NUMBER,
SERIAL_NUMBER,
MAC_ADDRESS_UNIQUE_ID,
MAC_ADDRESS_UNIQUE_ID,
),
("0", 0, "0", "0", MAC_ADDRESS_UNIQUE_ID, MAC_ADDRESS_UNIQUE_ID),
(
"0",
0,
"0-rain-delay",
"0-1",
f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay",
f"{MAC_ADDRESS_UNIQUE_ID}-1",
),
(
MAC_ADDRESS_UNIQUE_ID,
SERIAL_NUMBER,
MAC_ADDRESS_UNIQUE_ID,
MAC_ADDRESS_UNIQUE_ID,
MAC_ADDRESS_UNIQUE_ID,
MAC_ADDRESS_UNIQUE_ID,
),
(
MAC_ADDRESS_UNIQUE_ID,
SERIAL_NUMBER,
f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay",
f"{MAC_ADDRESS_UNIQUE_ID}-1",
f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay",
f"{MAC_ADDRESS_UNIQUE_ID}-1",
),
],
ids=(
"serial-number",
"serial-number-with-suffix",
"serial-number-int",
"zero-serial",
"zero-serial-suffix",
"new-format",
"new-format-suffx",
),
)
async def test_fix_entity_unique_ids(
hass: HomeAssistant,
config_entry: MockConfigEntry,
entity_unique_id: str,
device_identifier: str,
expected_unique_id: str,
expected_device_identifier: str,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test fixing entity unique ids from old unique id formats."""
entity_entry = entity_registry.async_get_or_create(
DOMAIN, "number", unique_id=entity_unique_id, config_entry=config_entry
)
device_entry = device_registry.async_get_or_create(
identifiers={(DOMAIN, device_identifier)},
config_entry_id=config_entry.entry_id,
serial_number=config_entry.data["serial_number"],
)
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.LOADED
entity_entry = entity_registry.async_get(entity_entry.id)
assert entity_entry
assert entity_entry.unique_id == expected_unique_id
device_entry = device_registry.async_get_device(
{(DOMAIN, expected_device_identifier)}
)
assert device_entry
assert device_entry.identifiers == {(DOMAIN, expected_device_identifier)}
@pytest.mark.parametrize(
(
"entry1_updates",
"entry2_updates",
"expected_device_name",
"expected_disabled_by",
),
[
({}, {}, None, None),
(
{
"name_by_user": "Front Sprinkler",
},
{},
"Front Sprinkler",
None,
),
(
{},
{
"name_by_user": "Front Sprinkler",
},
"Front Sprinkler",
None,
),
(
{
"name_by_user": "Sprinkler 1",
},
{
"name_by_user": "Sprinkler 2",
},
"Sprinkler 2",
None,
),
(
{
"disabled_by": dr.DeviceEntryDisabler.USER,
},
{},
None,
None,
),
(
{},
{
"disabled_by": dr.DeviceEntryDisabler.USER,
},
None,
None,
),
(
{
"disabled_by": dr.DeviceEntryDisabler.USER,
},
{
"disabled_by": dr.DeviceEntryDisabler.USER,
},
None,
dr.DeviceEntryDisabler.USER,
),
],
ids=[
"duplicates",
"prefer-old-name",
"prefer-new-name",
"both-names-prefers-new",
"old-disabled-prefer-new",
"new-disabled-prefer-old",
"both-disabled",
],
)
async def test_fix_duplicate_device_ids(
hass: HomeAssistant,
config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry,
entry1_updates: dict[str, Any],
entry2_updates: dict[str, Any],
expected_device_name: str | None,
expected_disabled_by: dr.DeviceEntryDisabler | None,
) -> None:
"""Test fixing duplicate device ids."""
entry1 = device_registry.async_get_or_create(
identifiers={(DOMAIN, str(SERIAL_NUMBER))},
config_entry_id=config_entry.entry_id,
serial_number=config_entry.data["serial_number"],
)
device_registry.async_update_device(entry1.id, **entry1_updates)
entry2 = device_registry.async_get_or_create(
identifiers={(DOMAIN, MAC_ADDRESS_UNIQUE_ID)},
config_entry_id=config_entry.entry_id,
serial_number=config_entry.data["serial_number"],
)
device_registry.async_update_device(entry2.id, **entry2_updates)
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
)
assert len(device_entries) == 2
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.LOADED
# Only the device with the new format exists
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
)
assert len(device_entries) == 1
device_entry = device_registry.async_get_device({(DOMAIN, MAC_ADDRESS_UNIQUE_ID)})
assert device_entry
assert device_entry.identifiers == {(DOMAIN, MAC_ADDRESS_UNIQUE_ID)}
assert device_entry.name_by_user == expected_device_name
assert device_entry.disabled_by == expected_disabled_by