Add test coverage to Twente Milieu (#59640)

This commit is contained in:
Franck Nijhof 2021-11-14 11:11:29 +01:00 committed by GitHub
parent da8bfed793
commit 458bc92124
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 351 additions and 97 deletions

View File

@ -1139,9 +1139,6 @@ omit =
homeassistant/components/tuya/switch.py
homeassistant/components/tuya/util.py
homeassistant/components/tuya/vacuum.py
homeassistant/components/twentemilieu/__init__.py
homeassistant/components/twentemilieu/const.py
homeassistant/components/twentemilieu/sensor.py
homeassistant/components/twilio_call/notify.py
homeassistant/components/twilio_sms/notify.py
homeassistant/components/twitter/notify.py

View File

@ -46,7 +46,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# For backwards compat, set unique ID
if entry.unique_id is None:
hass.config_entries.async_update_entry(entry, unique_id=entry.data[CONF_ID])
hass.config_entries.async_update_entry(
entry, unique_id=str(entry.data[CONF_ID])
)
hass.data.setdefault(DOMAIN, {})[entry.data[CONF_ID]] = coordinator
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
@ -58,5 +60,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Twente Milieu config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
del hass.data[DOMAIN][entry.entry_id]
del hass.data[DOMAIN][entry.data[CONF_ID]]
return unload_ok

View File

@ -11,3 +11,5 @@ SCAN_INTERVAL = timedelta(hours=1)
CONF_POST_CODE = "post_code"
CONF_HOUSE_NUMBER = "house_number"
CONF_HOUSE_LETTER = "house_letter"
ENTRY_TYPE_SERVICE: Final = "service"

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/twentemilieu",
"requirements": ["twentemilieu==0.4.2"],
"codeowners": ["@frenck"],
"quality_scale": "platinum",
"iot_class": "cloud_polling"
}

View File

@ -18,7 +18,7 @@ from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
)
from .const import DOMAIN
from .const import DOMAIN, ENTRY_TYPE_SERVICE
@dataclass
@ -97,7 +97,8 @@ class TwenteMilieuSensor(CoordinatorEntity, SensorEntity):
self._attr_unique_id = f"{DOMAIN}_{entry.data[CONF_ID]}_{description.key}"
self._attr_device_info = DeviceInfo(
configuration_url="https://www.twentemilieu.nl",
identifiers={(DOMAIN, entry.data[CONF_ID])},
entry_type=ENTRY_TYPE_SERVICE,
identifiers={(DOMAIN, str(entry.data[CONF_ID]))},
manufacturer="Twente Milieu",
name="Twente Milieu",
)

View File

@ -0,0 +1,88 @@
"""Fixtures for the Twente Milieu integration tests."""
from __future__ import annotations
from collections.abc import Generator
from datetime import date
from unittest.mock import MagicMock, patch
import pytest
from twentemilieu import WasteType
from homeassistant.components.twentemilieu.const import (
CONF_HOUSE_LETTER,
CONF_HOUSE_NUMBER,
CONF_POST_CODE,
DOMAIN,
)
from homeassistant.const import CONF_ID
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
title="1234AB 1",
domain=DOMAIN,
data={
CONF_ID: 12345,
CONF_POST_CODE: "1234AB",
CONF_HOUSE_NUMBER: "1",
CONF_HOUSE_LETTER: "A",
},
unique_id="12345",
)
@pytest.fixture
def mock_setup_entry() -> Generator[None, None, None]:
"""Mock setting up a config entry."""
with patch(
"homeassistant.components.twentemilieu.async_setup_entry", return_value=True
):
yield
@pytest.fixture
def mock_twentemilieu_config_flow() -> Generator[None, MagicMock, None]:
"""Return a mocked Twente Milieu client."""
with patch(
"homeassistant.components.twentemilieu.config_flow.TwenteMilieu", autospec=True
) as twentemilieu_mock:
twentemilieu = twentemilieu_mock.return_value
twentemilieu.unique_id.return_value = 12345
yield twentemilieu
@pytest.fixture
def mock_twentemilieu() -> Generator[None, MagicMock, None]:
"""Return a mocked Twente Milieu client."""
with patch(
"homeassistant.components.twentemilieu.TwenteMilieu", autospec=True
) as twentemilieu_mock:
twentemilieu = twentemilieu_mock.return_value
twentemilieu.unique_id.return_value = 12345
twentemilieu.update.return_value = {
WasteType.NON_RECYCLABLE: date(2021, 11, 1),
WasteType.ORGANIC: date(2021, 11, 2),
WasteType.PACKAGES: date(2021, 11, 3),
WasteType.PAPER: None,
}
yield twentemilieu
@pytest.fixture
async def init_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_twentemilieu: MagicMock,
) -> MockConfigEntry:
"""Set up the TwenteMilieu integration for testing."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry

View File

@ -1,7 +1,9 @@
"""Tests for the Twente Milieu config flow."""
import aiohttp
from unittest.mock import MagicMock
from homeassistant import config_entries, data_entry_flow
from twentemilieu import TwenteMilieuAddressError, TwenteMilieuConnectionError
from homeassistant import config_entries
from homeassistant.components.twentemilieu import config_flow
from homeassistant.components.twentemilieu.const import (
CONF_HOUSE_LETTER,
@ -10,116 +12,139 @@ from homeassistant.components.twentemilieu.const import (
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_ID, CONTENT_TYPE_JSON
from homeassistant.const import CONF_ID
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker
FIXTURE_USER_INPUT = {
CONF_POST_CODE: "1234AB",
CONF_HOUSE_NUMBER: "1",
CONF_HOUSE_LETTER: "A",
}
async def test_show_set_form(hass: HomeAssistant) -> None:
"""Test that the setup form is served."""
async def test_full_user_flow(
hass: HomeAssistant,
mock_twentemilieu_config_flow: MagicMock,
mock_setup_entry: MagicMock,
) -> None:
"""Test registering an integration and finishing flow works."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result.get("type") == RESULT_TYPE_FORM
assert result.get("step_id") == SOURCE_USER
assert "flow_id" in result
async def test_connection_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we show user form on Twente Milieu connection error."""
aioclient_mock.post(
"https://twentemilieuapi.ximmio.com/api/FetchAdress", exc=aiohttp.ClientError
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_POST_CODE: "1234AB",
CONF_HOUSE_NUMBER: "1",
CONF_HOUSE_LETTER: "A",
},
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=FIXTURE_USER_INPUT
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "cannot_connect"}
assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
assert result2.get("title") == "12345"
assert result2.get("data") == {
CONF_ID: 12345,
CONF_POST_CODE: "1234AB",
CONF_HOUSE_NUMBER: "1",
CONF_HOUSE_LETTER: "A",
}
async def test_invalid_address(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
hass: HomeAssistant,
mock_twentemilieu_config_flow: MagicMock,
mock_setup_entry: MagicMock,
) -> None:
"""Test we show user form on Twente Milieu invalid address error."""
aioclient_mock.post(
"https://twentemilieuapi.ximmio.com/api/FetchAdress",
json={"dataList": []},
headers={"Content-Type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=FIXTURE_USER_INPUT
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "invalid_address"}
async def test_address_already_set_up(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test we abort if address has already been set up."""
MockConfigEntry(
domain=DOMAIN,
data={**FIXTURE_USER_INPUT, CONF_ID: "12345"},
title="12345",
unique_id="12345",
).add_to_hass(hass)
aioclient_mock.post(
"https://twentemilieuapi.ximmio.com/api/FetchAdress",
json={"dataList": [{"UniqueId": "12345"}]},
headers={"Content-Type": CONTENT_TYPE_JSON},
)
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN,
context={"source": config_entries.SOURCE_USER},
data=FIXTURE_USER_INPUT,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_full_flow_implementation(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test registering an integration and finishing flow works."""
aioclient_mock.post(
"https://twentemilieuapi.ximmio.com/api/FetchAdress",
json={"dataList": [{"UniqueId": "12345"}]},
headers={"Content-Type": CONTENT_TYPE_JSON},
)
"""Test full user flow when the user enters an incorrect address.
This tests also tests if the user recovers from it by entering a valid
address in the second attempt.
"""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result.get("type") == RESULT_TYPE_FORM
assert result.get("step_id") == SOURCE_USER
assert "flow_id" in result
result = await hass.config_entries.flow.async_configure(
mock_twentemilieu_config_flow.unique_id.side_effect = TwenteMilieuAddressError
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
FIXTURE_USER_INPUT,
user_input={
CONF_POST_CODE: "1234",
CONF_HOUSE_NUMBER: "1",
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "12345"
assert result["data"][CONF_POST_CODE] == FIXTURE_USER_INPUT[CONF_POST_CODE]
assert result["data"][CONF_HOUSE_NUMBER] == FIXTURE_USER_INPUT[CONF_HOUSE_NUMBER]
assert result["data"][CONF_HOUSE_LETTER] == FIXTURE_USER_INPUT[CONF_HOUSE_LETTER]
assert result2.get("type") == RESULT_TYPE_FORM
assert result2.get("step_id") == SOURCE_USER
assert result2.get("errors") == {"base": "invalid_address"}
assert "flow_id" in result2
mock_twentemilieu_config_flow.unique_id.side_effect = None
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_POST_CODE: "1234AB",
CONF_HOUSE_NUMBER: "1",
},
)
assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY
assert result3.get("title") == "12345"
assert result3.get("data") == {
CONF_ID: 12345,
CONF_POST_CODE: "1234AB",
CONF_HOUSE_NUMBER: "1",
CONF_HOUSE_LETTER: None,
}
async def test_connection_error(
hass: HomeAssistant,
mock_twentemilieu_config_flow: MagicMock,
) -> None:
"""Test we show user form on Twente Milieu connection error."""
mock_twentemilieu_config_flow.unique_id.side_effect = TwenteMilieuConnectionError
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
data={
CONF_POST_CODE: "1234AB",
CONF_HOUSE_NUMBER: "1",
CONF_HOUSE_LETTER: "A",
},
)
assert result.get("type") == RESULT_TYPE_FORM
assert result.get("step_id") == SOURCE_USER
assert result.get("errors") == {"base": "cannot_connect"}
async def test_address_already_set_up(
hass: HomeAssistant,
mock_twentemilieu_config_flow: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test we abort if address has already been set up."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN,
context={"source": config_entries.SOURCE_USER},
data={
CONF_POST_CODE: "1234AB",
CONF_HOUSE_NUMBER: "1",
CONF_HOUSE_LETTER: "A",
},
)
assert result.get("type") == RESULT_TYPE_ABORT
assert result.get("reason") == "already_configured"

View File

@ -0,0 +1,60 @@
"""Tests for the Twente Milieu integration."""
from unittest.mock import AsyncMock, MagicMock, patch
from homeassistant.components.twentemilieu.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def test_load_unload_config_entry(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_twentemilieu: AsyncMock,
) -> None:
"""Test the Twente Milieu configuration entry loading/unloading."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert not hass.data.get(DOMAIN)
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
@patch(
"homeassistant.components.twentemilieu.TwenteMilieu.update",
side_effect=RuntimeError,
)
async def test_config_entry_not_ready(
mock_request: MagicMock,
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the Twente Milieu configuration entry not ready."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_request.call_count == 1
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_update_config_entry_unique_id(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_twentemilieu: AsyncMock,
) -> None:
"""Test the we update old config entries with an unique ID."""
mock_config_entry.unique_id = None
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.unique_id == "12345"

View File

@ -0,0 +1,78 @@
"""Tests for the Twente Milieu sensors."""
from homeassistant.components.twentemilieu.const import DOMAIN, ENTRY_TYPE_SERVICE
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_FRIENDLY_NAME,
ATTR_ICON,
ATTR_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_DATE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.common import MockConfigEntry
async def test_waste_pickup_sensors(
hass: HomeAssistant,
init_integration: MockConfigEntry,
) -> None:
"""Test the Twente Milieu waste pickup sensors."""
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
state = hass.states.get("sensor.non_recyclable_waste_pickup")
entry = entity_registry.async_get("sensor.non_recyclable_waste_pickup")
assert entry
assert state
assert entry.unique_id == "twentemilieu_12345_Non-recyclable"
assert state.state == "2021-11-01"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Non-recyclable Waste Pickup"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE
assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty"
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
state = hass.states.get("sensor.organic_waste_pickup")
entry = entity_registry.async_get("sensor.organic_waste_pickup")
assert entry
assert state
assert entry.unique_id == "twentemilieu_12345_Organic"
assert state.state == "2021-11-02"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Organic Waste Pickup"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE
assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty"
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
state = hass.states.get("sensor.packages_waste_pickup")
entry = entity_registry.async_get("sensor.packages_waste_pickup")
assert entry
assert state
assert entry.unique_id == "twentemilieu_12345_Plastic"
assert state.state == "2021-11-03"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Packages Waste Pickup"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE
assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty"
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
state = hass.states.get("sensor.paper_waste_pickup")
entry = entity_registry.async_get("sensor.paper_waste_pickup")
assert entry
assert state
assert entry.unique_id == "twentemilieu_12345_Paper"
assert state.state == STATE_UNKNOWN
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Paper Waste Pickup"
assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE
assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty"
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
assert entry.device_id
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.identifiers == {(DOMAIN, "12345")}
assert device_entry.manufacturer == "Twente Milieu"
assert device_entry.name == "Twente Milieu"
assert device_entry.entry_type == ENTRY_TYPE_SERVICE
assert device_entry.configuration_url == "https://www.twentemilieu.nl"
assert not device_entry.model
assert not device_entry.sw_version