Adjust litterrobot tests and code to match guidelines (#47060)

* Use SwitchEntity instead of ToggleEntity and adjust test patches as recommended

* Move async_create_entry out of try block in config_flow

* Patch pypi package instead of HA code

* Bump pylitterbot to 2021.2.6, fix tests, and implement other code review suggestions

* Bump pylitterbot to 2021.2.8, remove sleep mode start/end time from vacuum, adjust and add sensors for sleep mode start/end time

* Move icon helper back to Litter-Robot component and isoformat times on time sensors
This commit is contained in:
Nathan Spencer
2021-03-08 23:21:08 +00:00
committed by Paulus Schoutsen
parent 939da2403f
commit 3b05a12e62
14 changed files with 277 additions and 142 deletions
+44 -28
View File
@@ -1,45 +1,59 @@
"""Configure pytest for Litter-Robot tests."""
from typing import Optional
from unittest.mock import AsyncMock, MagicMock, patch
import pylitterbot
from pylitterbot import Robot
import pytest
from homeassistant.components import litterrobot
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .common import CONFIG, ROBOT_DATA
from tests.common import MockConfigEntry
def create_mock_robot(hass):
def create_mock_robot(unit_status_code: Optional[str] = None):
"""Create a mock Litter-Robot device."""
robot = Robot(data=ROBOT_DATA)
robot.start_cleaning = AsyncMock()
robot.set_power_status = AsyncMock()
robot.reset_waste_drawer = AsyncMock()
robot.set_sleep_mode = AsyncMock()
robot.set_night_light = AsyncMock()
robot.set_panel_lockout = AsyncMock()
return robot
if not (
unit_status_code
and Robot.UnitStatus(unit_status_code) != Robot.UnitStatus.UNKNOWN
):
unit_status_code = ROBOT_DATA["unitStatus"]
with patch.dict(ROBOT_DATA, {"unitStatus": unit_status_code}):
robot = Robot(data=ROBOT_DATA)
robot.start_cleaning = AsyncMock()
robot.set_power_status = AsyncMock()
robot.reset_waste_drawer = AsyncMock()
robot.set_sleep_mode = AsyncMock()
robot.set_night_light = AsyncMock()
robot.set_panel_lockout = AsyncMock()
return robot
@pytest.fixture()
def mock_hub(hass):
"""Mock a Litter-Robot hub."""
hub = MagicMock(
hass=hass,
account=MagicMock(),
logged_in=True,
coordinator=MagicMock(spec=DataUpdateCoordinator),
spec=litterrobot.LitterRobotHub,
)
hub.coordinator.last_update_success = True
hub.account.robots = [create_mock_robot(hass)]
return hub
def create_mock_account(unit_status_code: Optional[str] = None):
"""Create a mock Litter-Robot account."""
account = MagicMock(spec=pylitterbot.Account)
account.connect = AsyncMock()
account.refresh_robots = AsyncMock()
account.robots = [create_mock_robot(unit_status_code)]
return account
async def setup_hub(hass, mock_hub, platform_domain):
@pytest.fixture
def mock_account():
"""Mock a Litter-Robot account."""
return create_mock_account()
@pytest.fixture
def mock_account_with_error():
"""Mock a Litter-Robot account with error."""
return create_mock_account("BR")
async def setup_integration(hass, mock_account, platform_domain=None):
"""Load a Litter-Robot platform with the provided hub."""
entry = MockConfigEntry(
domain=litterrobot.DOMAIN,
@@ -47,9 +61,11 @@ async def setup_hub(hass, mock_hub, platform_domain):
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.litterrobot.LitterRobotHub",
return_value=mock_hub,
with patch("pylitterbot.Account", return_value=mock_account), patch(
"homeassistant.components.litterrobot.PLATFORMS",
[platform_domain] if platform_domain else [],
):
await hass.config_entries.async_forward_entry_setup(entry, platform_domain)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry
@@ -4,11 +4,14 @@ from unittest.mock import patch
from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
from homeassistant import config_entries, setup
from homeassistant.components import litterrobot
from .common import CONF_USERNAME, CONFIG, DOMAIN
from tests.common import MockConfigEntry
async def test_form(hass):
async def test_form(hass, mock_account):
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
@@ -17,10 +20,7 @@ async def test_form(hass):
assert result["type"] == "form"
assert result["errors"] == {}
with patch(
"homeassistant.components.litterrobot.config_flow.LitterRobotHub.login",
return_value=True,
), patch(
with patch("pylitterbot.Account", return_value=mock_account), patch(
"homeassistant.components.litterrobot.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.litterrobot.async_setup_entry",
@@ -38,6 +38,23 @@ async def test_form(hass):
assert len(mock_setup_entry.mock_calls) == 1
async def test_already_configured(hass):
"""Test we handle already configured."""
MockConfigEntry(
domain=litterrobot.DOMAIN,
data=CONFIG[litterrobot.DOMAIN],
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data=CONFIG[litterrobot.DOMAIN],
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
async def test_form_invalid_auth(hass):
"""Test we handle invalid auth."""
result = await hass.config_entries.flow.async_init(
@@ -45,7 +62,7 @@ async def test_form_invalid_auth(hass):
)
with patch(
"homeassistant.components.litterrobot.config_flow.LitterRobotHub.login",
"pylitterbot.Account.connect",
side_effect=LitterRobotLoginException,
):
result2 = await hass.config_entries.flow.async_configure(
@@ -63,7 +80,7 @@ async def test_form_cannot_connect(hass):
)
with patch(
"homeassistant.components.litterrobot.config_flow.LitterRobotHub.login",
"pylitterbot.Account.connect",
side_effect=LitterRobotException,
):
result2 = await hass.config_entries.flow.async_configure(
@@ -81,7 +98,7 @@ async def test_form_unknown_error(hass):
)
with patch(
"homeassistant.components.litterrobot.config_flow.LitterRobotHub.login",
"pylitterbot.Account.connect",
side_effect=Exception,
):
result2 = await hass.config_entries.flow.async_configure(
+33 -5
View File
@@ -1,20 +1,48 @@
"""Test Litter-Robot setup process."""
from unittest.mock import patch
from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
import pytest
from homeassistant.components import litterrobot
from homeassistant.setup import async_setup_component
from homeassistant.config_entries import (
ENTRY_STATE_SETUP_ERROR,
ENTRY_STATE_SETUP_RETRY,
)
from .common import CONFIG
from .conftest import setup_integration
from tests.common import MockConfigEntry
async def test_unload_entry(hass):
async def test_unload_entry(hass, mock_account):
"""Test being able to unload an entry."""
entry = await setup_integration(hass, mock_account)
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert hass.data[litterrobot.DOMAIN] == {}
@pytest.mark.parametrize(
"side_effect,expected_state",
(
(LitterRobotLoginException, ENTRY_STATE_SETUP_ERROR),
(LitterRobotException, ENTRY_STATE_SETUP_RETRY),
),
)
async def test_entry_not_setup(hass, side_effect, expected_state):
"""Test being able to handle config entry not setup."""
entry = MockConfigEntry(
domain=litterrobot.DOMAIN,
data=CONFIG[litterrobot.DOMAIN],
)
entry.add_to_hass(hass)
assert await async_setup_component(hass, litterrobot.DOMAIN, {}) is True
assert await litterrobot.async_unload_entry(hass, entry)
assert hass.data[litterrobot.DOMAIN] == {}
with patch(
"pylitterbot.Account.connect",
side_effect=side_effect,
):
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state == expected_state
+47 -10
View File
@@ -1,20 +1,57 @@
"""Test the Litter-Robot sensor entity."""
from unittest.mock import Mock
from homeassistant.components.litterrobot.sensor import LitterRobotSleepTimeSensor
from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN
from homeassistant.const import PERCENTAGE
from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE
from .conftest import setup_hub
from .conftest import create_mock_robot, setup_integration
ENTITY_ID = "sensor.test_waste_drawer"
WASTE_DRAWER_ENTITY_ID = "sensor.test_waste_drawer"
async def test_sensor(hass, mock_hub):
"""Tests the sensor entity was set up."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN)
async def test_waste_drawer_sensor(hass, mock_account):
"""Tests the waste drawer sensor entity was set up."""
await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
sensor = hass.states.get(ENTITY_ID)
sensor = hass.states.get(WASTE_DRAWER_ENTITY_ID)
assert sensor
assert sensor.state == "50"
assert sensor.attributes["cycle_count"] == 15
assert sensor.attributes["cycle_capacity"] == 30
assert sensor.attributes["cycles_after_drawer_full"] == 0
assert sensor.attributes["unit_of_measurement"] == PERCENTAGE
async def test_sleep_time_sensor_with_none_state(hass):
"""Tests the sleep mode start time sensor where sleep mode is inactive."""
robot = create_mock_robot()
robot.sleep_mode_active = False
sensor = LitterRobotSleepTimeSensor(
robot, "Sleep Mode Start Time", Mock(), "sleep_mode_start_time"
)
assert sensor
assert sensor.state is None
assert sensor.device_class == DEVICE_CLASS_TIMESTAMP
async def test_gauge_icon():
"""Test icon generator for gauge sensor."""
from homeassistant.components.litterrobot.sensor import icon_for_gauge_level
GAUGE_EMPTY = "mdi:gauge-empty"
GAUGE_LOW = "mdi:gauge-low"
GAUGE = "mdi:gauge"
GAUGE_FULL = "mdi:gauge-full"
assert icon_for_gauge_level(None) == GAUGE_EMPTY
assert icon_for_gauge_level(0) == GAUGE_EMPTY
assert icon_for_gauge_level(5) == GAUGE_LOW
assert icon_for_gauge_level(40) == GAUGE
assert icon_for_gauge_level(80) == GAUGE_FULL
assert icon_for_gauge_level(100) == GAUGE_FULL
assert icon_for_gauge_level(None, 10) == GAUGE_EMPTY
assert icon_for_gauge_level(0, 10) == GAUGE_EMPTY
assert icon_for_gauge_level(5, 10) == GAUGE_EMPTY
assert icon_for_gauge_level(40, 10) == GAUGE_LOW
assert icon_for_gauge_level(80, 10) == GAUGE
assert icon_for_gauge_level(100, 10) == GAUGE_FULL
+8 -6
View File
@@ -12,7 +12,7 @@ from homeassistant.components.switch import (
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON
from homeassistant.util.dt import utcnow
from .conftest import setup_hub
from .conftest import setup_integration
from tests.common import async_fire_time_changed
@@ -20,9 +20,9 @@ NIGHT_LIGHT_MODE_ENTITY_ID = "switch.test_night_light_mode"
PANEL_LOCKOUT_ENTITY_ID = "switch.test_panel_lockout"
async def test_switch(hass, mock_hub):
async def test_switch(hass, mock_account):
"""Tests the switch entity was set up."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN)
await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
switch = hass.states.get(NIGHT_LIGHT_MODE_ENTITY_ID)
assert switch
@@ -36,9 +36,9 @@ async def test_switch(hass, mock_hub):
(PANEL_LOCKOUT_ENTITY_ID, "set_panel_lockout"),
],
)
async def test_on_off_commands(hass, mock_hub, entity_id, robot_command):
async def test_on_off_commands(hass, mock_account, entity_id, robot_command):
"""Test sending commands to the switch."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN)
await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
switch = hass.states.get(entity_id)
assert switch
@@ -48,12 +48,14 @@ async def test_on_off_commands(hass, mock_hub, entity_id, robot_command):
count = 0
for service in [SERVICE_TURN_ON, SERVICE_TURN_OFF]:
count += 1
await hass.services.async_call(
PLATFORM_DOMAIN,
service,
data,
blocking=True,
)
future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME)
async_fire_time_changed(hass, future)
assert getattr(mock_hub.account.robots[0], robot_command).call_count == count
assert getattr(mock_account.robots[0], robot_command).call_count == count
+25 -7
View File
@@ -12,20 +12,21 @@ from homeassistant.components.vacuum import (
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_DOCKED,
STATE_ERROR,
)
from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID
from homeassistant.util.dt import utcnow
from .conftest import setup_hub
from .conftest import setup_integration
from tests.common import async_fire_time_changed
ENTITY_ID = "vacuum.test_litter_box"
async def test_vacuum(hass, mock_hub):
async def test_vacuum(hass, mock_account):
"""Tests the vacuum entity was set up."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN)
await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
vacuum = hass.states.get(ENTITY_ID)
assert vacuum
@@ -33,6 +34,15 @@ async def test_vacuum(hass, mock_hub):
assert vacuum.attributes["is_sleeping"] is False
async def test_vacuum_with_error(hass, mock_account_with_error):
"""Tests a vacuum entity with an error."""
await setup_integration(hass, mock_account_with_error, PLATFORM_DOMAIN)
vacuum = hass.states.get(ENTITY_ID)
assert vacuum
assert vacuum.state == STATE_ERROR
@pytest.mark.parametrize(
"service,command,extra",
[
@@ -52,14 +62,22 @@ async def test_vacuum(hass, mock_hub):
ATTR_PARAMS: {"enabled": True, "sleep_time": "22:30"},
},
),
(
SERVICE_SEND_COMMAND,
"set_sleep_mode",
{
ATTR_COMMAND: "set_sleep_mode",
ATTR_PARAMS: {"enabled": True, "sleep_time": None},
},
),
],
)
async def test_commands(hass, mock_hub, service, command, extra):
async def test_commands(hass, mock_account, service, command, extra):
"""Test sending commands to the vacuum."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN)
await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
vacuum = hass.states.get(ENTITY_ID)
assert vacuum is not None
assert vacuum
assert vacuum.state == STATE_DOCKED
data = {ATTR_ENTITY_ID: ENTITY_ID}
@@ -74,4 +92,4 @@ async def test_commands(hass, mock_hub, service, command, extra):
)
future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME)
async_fire_time_changed(hass, future)
getattr(mock_hub.account.robots[0], command).assert_called_once()
getattr(mock_account.robots[0], command).assert_called_once()