MySensors: Add lots of unit tests

This commit is contained in:
functionpointer
2021-01-25 22:39:03 +01:00
parent c18d19f599
commit 67d4d68019
6 changed files with 609 additions and 62 deletions

View File

@@ -568,6 +568,12 @@ omit =
homeassistant/components/mychevy/* homeassistant/components/mychevy/*
homeassistant/components/mycroft/* homeassistant/components/mycroft/*
homeassistant/components/mycroft/notify.py homeassistant/components/mycroft/notify.py
homeassistant/components/mysensors/__init__.py
homeassistant/components/mysensors/const.py
homeassistant/components/mysensors/device.py
homeassistant/components/mysensors/handler.py
homeassistant/components/mysensors/helpers.py
homeassistant/components/mysensors/notify.py
homeassistant/components/mysensors/binary_sensor.py homeassistant/components/mysensors/binary_sensor.py
homeassistant/components/mysensors/light.py homeassistant/components/mysensors/light.py
homeassistant/components/mysensors/climate.py homeassistant/components/mysensors/climate.py

View File

@@ -1,4 +1,5 @@
"""Connect to a MySensors gateway via pymysensors API.""" """Connect to a MySensors gateway via pymysensors API."""
import asyncio
import logging import logging
from typing import Callable, Dict, List, Optional, Tuple, Type, Union from typing import Callable, Dict, List, Optional, Tuple, Type, Union
@@ -199,8 +200,16 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo
_LOGGER.error("Can't unload configentry %s, no gateway found", entry.entry_id) _LOGGER.error("Can't unload configentry %s, no gateway found", entry.entry_id)
return False return False
for platform in SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT: unload_ok = all(
await hass.config_entries.async_forward_entry_unload(entry, platform) await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, platform)
for platform in SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT
]
)
)
if not unload_ok:
return False
key = MYSENSORS_ON_UNLOAD.format(entry.entry_id) key = MYSENSORS_ON_UNLOAD.format(entry.entry_id)
if key in hass.data[DOMAIN]: if key in hass.data[DOMAIN]:

View File

@@ -9,7 +9,8 @@
"mqtt" "mqtt"
], ],
"codeowners": [ "codeowners": [
"@MartinHjelmare", "@functionpointer" "@MartinHjelmare",
"@functionpointer"
], ],
"config_flow": true "config_flow": true
} }

View File

@@ -2,13 +2,21 @@
"title": "MySensors", "title": "MySensors",
"config": { "config": {
"step": { "step": {
"confirm": { "user": {
"description": "[%key:common::config_flow::description::confirm_setup%]" "data": {
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
} }
}, },
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": { "abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
} }
} }
} }

View File

@@ -1,58 +1,22 @@
{ {
"config": { "config": {
"abort": { "abort": {
"single_instance_allowed": "Already configured. Only a single configuration possible." "already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_subscribe_topic": "Invalid subscribe topic",
"invalid_publish_topic": "Invalid publish topic",
"invalid_port": "Invalid port number",
"invalid_persistence_file": "Invalid persistence file",
"invalid_ip": "Invalid IP address",
"invalid_serial": "Invalid serial port",
"invalid_device": "Invalid device",
"invalid_version": "Invalid MySensors version",
"not_a_number": "Please enter a number",
"port_out_of_range": "Port number must be at least 1 and at most 65535"
},
"step": {
"user": {
"data": {
"optimistic": "optimistic",
"persistence": "persistence",
"gateway_type_mqtt": "MQTT"
}, },
"description": "Choose connection method to the gateway" "error": {
}, "cannot_connect": "Failed to connect",
"gw_tcp": { "invalid_auth": "Invalid authentication",
"description": "Ethernet gateway setup", "unknown": "Unexpected error"
"data": { },
"device": "IP address of the gateway", "step": {
"tcp_port": "port", "user": {
"version": "MySensors version", "data": {
"persistence_file": "persistence file (leave empty to auto-generate)" "host": "Host",
"password": "Password",
"username": "Username"
}
}
} }
}, },
"gw_serial": { "title": "MySensors"
"description": "Serial gateway setup",
"data": {
"device": "Serial port",
"baud_rate": "baud rate",
"version": "MySensors version",
"persistence_file": "persistence file (leave empty to auto-generate)"
}
},
"gw_mqtt": {
"description": "MQTT gateway setup",
"data": {
"retain": "mqtt retain",
"topic_in_prefix": "prefix for input topics (topic_in_prefix)",
"topic_out_prefix": "prefix for output topics (topic_out_prefix)",
"version": "MySensors version",
"persistence_file": "persistence file (leave empty to auto-generate)"
}
}
}
}
} }

View File

@@ -0,0 +1,559 @@
"""Test the MySensors config flow."""
from typing import Dict
from unittest.mock import patch
import voluptuous as vol
from homeassistant import config_entries, setup
from homeassistant.components.mysensors import async_setup
from homeassistant.components.mysensors.config_flow import MySensorsConfigFlowHandler
from homeassistant.components.mysensors.const import (
CONF_BAUD_RATE,
CONF_DEVICE,
CONF_GATEWAY_TYPE,
CONF_GATEWAY_TYPE_MQTT,
CONF_GATEWAY_TYPE_SERIAL,
CONF_GATEWAY_TYPE_TCP,
CONF_GATEWAY_TYPE_TYPE,
CONF_GATEWAYS,
CONF_PERSISTENCE,
CONF_PERSISTENCE_FILE,
CONF_RETAIN,
CONF_TCP_PORT,
CONF_TOPIC_IN_PREFIX,
CONF_TOPIC_OUT_PREFIX,
CONF_VERSION,
DOMAIN,
)
from homeassistant.components.mysensors.gateway import is_serial_port
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
async def get_form(
hass: HomeAssistantType, gatway_type: CONF_GATEWAY_TYPE_TYPE, expected_step_id: str
):
"""Get a form for the given gateway type."""
await setup.async_setup_component(hass, "persistent_notification", {})
stepuser = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert stepuser["type"] == "form"
assert not stepuser["errors"]
actualstep = await hass.config_entries.flow.async_configure(
stepuser["flow_id"],
{CONF_GATEWAY_TYPE: gatway_type},
)
await hass.async_block_till_done()
assert actualstep["type"] == "form"
assert actualstep["step_id"] == expected_step_id
return actualstep
async def test_config_mqtt(hass: HomeAssistantType):
"""Test configuring a mqtt gateway."""
step = await get_form(hass, CONF_GATEWAY_TYPE_MQTT, "gw_mqtt")
flowid = step["flow_id"]
with patch(
"homeassistant.components.mysensors.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.mysensors.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
flowid,
{
CONF_RETAIN: True,
CONF_TOPIC_IN_PREFIX: "bla",
CONF_TOPIC_OUT_PREFIX: "blub",
CONF_VERSION: "2.4",
},
)
await hass.async_block_till_done()
if "errors" in result2:
assert not result2["errors"]
assert result2["type"] == "create_entry"
assert result2["title"] == "mqtt"
assert result2["data"] == {
CONF_DEVICE: "mqtt",
CONF_RETAIN: True,
CONF_TOPIC_IN_PREFIX: "bla",
CONF_TOPIC_OUT_PREFIX: "blub",
CONF_VERSION: "2.4",
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
def test_is_serial_port_windows(hass: HomeAssistantType):
"""Test windows serial ports."""
tests = {
"COM5": True,
"asdf": False,
"COM17": True,
"COM": False,
"/dev/ttyACM0": False,
}
def testport(port, result):
try:
is_serial_port(port)
except vol.Invalid:
success = False
else:
success = True
assert success == result
with patch("sys.platform", "win32"):
for test, result in tests.items():
testport(test, result)
async def test_config_serial(hass: HomeAssistantType):
"""Test configuring a gateway via serial."""
step = await get_form(hass, CONF_GATEWAY_TYPE_SERIAL, "gw_serial")
flowid = step["flow_id"]
with patch( # mock is_serial_port because otherwise the test will be platform dependent (/dev/ttyACMx vs COMx)
"homeassistant.components.mysensors.config_flow.is_serial_port",
return_value=True,
), patch(
"homeassistant.components.mysensors.config_flow.try_connect", return_value=True
), patch(
"homeassistant.components.mysensors.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.mysensors.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
flowid,
{
CONF_BAUD_RATE: 115200,
CONF_DEVICE: "/dev/ttyACM0",
CONF_VERSION: "2.4",
},
)
await hass.async_block_till_done()
if "errors" in result2:
assert not result2["errors"]
assert result2["type"] == "create_entry"
assert result2["title"] == "/dev/ttyACM0"
assert result2["data"] == {
CONF_DEVICE: "/dev/ttyACM0",
CONF_BAUD_RATE: 115200,
CONF_VERSION: "2.4",
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_config_tcp(hass: HomeAssistantType):
"""Test configuring a gateway via tcp."""
step = await get_form(hass, CONF_GATEWAY_TYPE_TCP, "gw_tcp")
flowid = step["flow_id"]
with patch(
"homeassistant.components.mysensors.config_flow.try_connect", return_value=True
), patch(
"homeassistant.components.mysensors.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.mysensors.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
flowid,
{
CONF_TCP_PORT: 5003,
CONF_DEVICE: "127.0.0.1",
CONF_VERSION: "2.4",
},
)
await hass.async_block_till_done()
if "errors" in result2:
assert not result2["errors"]
assert result2["type"] == "create_entry"
assert result2["title"] == "127.0.0.1"
assert result2["data"] == {
CONF_DEVICE: "127.0.0.1",
CONF_TCP_PORT: 5003,
CONF_VERSION: "2.4",
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_fail_to_connect(hass: HomeAssistantType):
"""Test configuring a gateway via tcp."""
step = await get_form(hass, CONF_GATEWAY_TYPE_TCP, "gw_tcp")
flowid = step["flow_id"]
with patch(
"homeassistant.components.mysensors.config_flow.try_connect", return_value=False
), patch(
"homeassistant.components.mysensors.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.mysensors.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
flowid,
{
CONF_TCP_PORT: 5003,
CONF_DEVICE: "127.0.0.1",
CONF_VERSION: "2.4",
},
)
await hass.async_block_till_done()
assert result2["type"] == "form"
assert "errors" in result2
assert "base" in result2["errors"]
assert result2["errors"]["base"] == "cannot_connect"
assert len(mock_setup.mock_calls) == 0
assert len(mock_setup_entry.mock_calls) == 0
async def config_invalid(
hass: HomeAssistantType,
gatway_type: CONF_GATEWAY_TYPE_TYPE,
expected_step_id: str,
user_input: Dict[str, any],
err_field,
err_string,
):
"""Perform a test that is expected to generate an error."""
step = await get_form(hass, gatway_type, expected_step_id)
flowid = step["flow_id"]
with patch(
"homeassistant.components.mysensors.config_flow.try_connect", return_value=True
), patch(
"homeassistant.components.mysensors.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.mysensors.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
flowid,
user_input,
)
await hass.async_block_till_done()
assert result2["type"] == "form"
assert "errors" in result2
assert err_field in result2["errors"]
assert result2["errors"][err_field] == err_string
assert len(mock_setup.mock_calls) == 0
assert len(mock_setup_entry.mock_calls) == 0
async def test_config_tcp_invalid_port(hass: HomeAssistantType):
"""Test invalid port on a tcp gateway."""
await config_invalid(
hass,
CONF_GATEWAY_TYPE_TCP,
"gw_tcp",
{
CONF_TCP_PORT: 60000000,
CONF_DEVICE: "127.0.0.1",
CONF_VERSION: "2.4",
},
CONF_TCP_PORT,
"port_out_of_range",
)
async def test_config_tcp_invalid_port_too_small(hass: HomeAssistantType):
"""Test invalid port on a tcp gateway."""
await config_invalid(
hass,
CONF_GATEWAY_TYPE_TCP,
"gw_tcp",
{
CONF_TCP_PORT: 0,
CONF_DEVICE: "127.0.0.1",
CONF_VERSION: "2.4",
},
CONF_TCP_PORT,
"port_out_of_range",
)
async def test_config_tcp_invalid_version(hass: HomeAssistantType):
"""Test tcp gateway with invalid version."""
await config_invalid(
hass,
CONF_GATEWAY_TYPE_TCP,
"gw_tcp",
{
CONF_TCP_PORT: 5003,
CONF_DEVICE: "127.0.0.1",
CONF_VERSION: "a",
},
CONF_VERSION,
"invalid_version",
)
async def test_config_tcp_invalid_version2(hass: HomeAssistantType):
"""Test tcp gateway with invalid version."""
await config_invalid(
hass,
CONF_GATEWAY_TYPE_TCP,
"gw_tcp",
{
CONF_TCP_PORT: 5003,
CONF_DEVICE: "127.0.0.1",
CONF_VERSION: "a.b",
},
CONF_VERSION,
"invalid_version",
)
async def test_config_tcp_no_version(hass: HomeAssistantType):
"""Test tcp gateway with invalid version."""
await config_invalid(
hass,
CONF_GATEWAY_TYPE_TCP,
"gw_tcp",
{
CONF_TCP_PORT: 5003,
CONF_DEVICE: "127.0.0.1",
},
CONF_VERSION,
"invalid_version",
)
async def test_config_tcp_invalid_address(hass: HomeAssistantType):
"""Test tcp gateway with invalid ip address."""
await config_invalid(
hass,
CONF_GATEWAY_TYPE_TCP,
"gw_tcp",
{
CONF_TCP_PORT: 5003,
CONF_DEVICE: "127.0.0.",
CONF_VERSION: "2.4",
},
CONF_DEVICE,
"invalid_ip",
)
async def test_config_mqtt_invalid_persistence_file(hass: HomeAssistantType):
"""Test mqtt gateway with invalid input topic."""
await config_invalid(
hass,
CONF_GATEWAY_TYPE_MQTT,
"gw_mqtt",
{
CONF_RETAIN: True,
CONF_TOPIC_IN_PREFIX: "bla",
CONF_TOPIC_OUT_PREFIX: "blub",
CONF_PERSISTENCE_FILE: "asdf.zip",
CONF_VERSION: "2.4",
},
CONF_PERSISTENCE_FILE,
"invalid_persistence_file",
)
async def test_config_mqtt_invalid_in_topic(hass: HomeAssistantType):
"""Test mqtt gateway with invalid input topic."""
await config_invalid(
hass,
CONF_GATEWAY_TYPE_MQTT,
"gw_mqtt",
{
CONF_RETAIN: True,
CONF_TOPIC_IN_PREFIX: "/#/#",
CONF_TOPIC_OUT_PREFIX: "blub",
CONF_VERSION: "2.4",
},
CONF_TOPIC_IN_PREFIX,
"invalid_subscribe_topic",
)
async def test_config_mqtt_invalid_out_topic(hass: HomeAssistantType):
"""Test mqtt gateway with invalid output topic."""
await config_invalid(
hass,
CONF_GATEWAY_TYPE_MQTT,
"gw_mqtt",
{
CONF_RETAIN: True,
CONF_TOPIC_IN_PREFIX: "asdf",
CONF_TOPIC_OUT_PREFIX: "/#/#",
CONF_VERSION: "2.4",
},
CONF_TOPIC_OUT_PREFIX,
"invalid_publish_topic",
)
async def attempt_import(hass: HomeAssistantType, config: ConfigType, expected_calls=1):
"""Test importing a gateway."""
with patch("sys.platform", "win32"), patch(
"homeassistant.components.mysensors.config_flow.try_connect", return_value=True
), patch(
"homeassistant.components.mysensors.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await async_setup(hass, config)
assert result
await hass.async_block_till_done()
assert len(mock_setup_entry.mock_calls) == expected_calls
return mock_setup_entry.call_args
async def test_import_serial(hass: HomeAssistantType):
"""Test importing a gateway via serial."""
args, _ = await attempt_import(
hass,
{
DOMAIN: {
CONF_GATEWAYS: [
{
CONF_DEVICE: "COM5",
CONF_PERSISTENCE_FILE: "bla.json",
CONF_BAUD_RATE: 57600,
CONF_TCP_PORT: 5003,
}
],
CONF_VERSION: "2.3",
CONF_PERSISTENCE: False,
CONF_RETAIN: True,
}
},
1,
)
# check result
# we check in this weird way bc there may be some extra keys that we don't care about
wanted = {
CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL,
CONF_DEVICE: "COM5",
CONF_PERSISTENCE_FILE: "bla.json",
CONF_BAUD_RATE: 57600,
CONF_VERSION: "2.3",
}
for key, value in wanted.items():
assert key in args[1].data
assert args[1].data[key] == value
async def test_import_tcp(hass: HomeAssistantType):
"""Test configuring a gateway via serial."""
args, _ = await attempt_import(
hass,
{
DOMAIN: {
CONF_GATEWAYS: [
{
CONF_DEVICE: "127.0.0.1",
CONF_PERSISTENCE_FILE: "blub.pickle",
CONF_BAUD_RATE: 115200,
CONF_TCP_PORT: 343,
}
],
CONF_VERSION: "2.4",
CONF_PERSISTENCE: False,
CONF_RETAIN: False,
}
},
1,
)
# check result
# we check in this weird way bc there may be some extra keys that we don't care about
wanted = {
CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP,
CONF_DEVICE: "127.0.0.1",
CONF_PERSISTENCE_FILE: "blub.pickle",
CONF_TCP_PORT: 343,
CONF_VERSION: "2.4",
}
for key, value in wanted.items():
assert key in args[1].data
assert args[1].data[key] == value
async def test_import_mqtt(hass: HomeAssistantType):
"""Test configuring a gateway via serial."""
args, _ = await attempt_import(
hass,
{
DOMAIN: {
CONF_GATEWAYS: [
{
CONF_DEVICE: "mqtt",
CONF_BAUD_RATE: 115200,
CONF_TCP_PORT: 5003,
CONF_TOPIC_IN_PREFIX: "intopic",
CONF_TOPIC_OUT_PREFIX: "outtopic",
}
],
CONF_VERSION: "2.4",
CONF_PERSISTENCE: False,
CONF_RETAIN: False,
}
},
1,
)
# check result
# we check in this weird way bc there may be some extra keys that we don't care about
wanted = {
CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT,
CONF_DEVICE: "mqtt",
CONF_VERSION: "2.4",
CONF_TOPIC_OUT_PREFIX: "outtopic",
CONF_TOPIC_IN_PREFIX: "intopic",
}
for key, value in wanted.items():
assert key in args[1].data
assert args[1].data[key] == value
async def test_import_two(hass: HomeAssistantType):
"""Test configuring a gateway via serial."""
await attempt_import(
hass,
{
DOMAIN: {
CONF_GATEWAYS: [
{
CONF_DEVICE: "mqtt",
CONF_PERSISTENCE_FILE: "bla.json",
CONF_BAUD_RATE: 115200,
CONF_TCP_PORT: 5003,
},
{
CONF_DEVICE: "COM6",
CONF_PERSISTENCE_FILE: "bla.json",
CONF_BAUD_RATE: 115200,
CONF_TCP_PORT: 5003,
},
],
CONF_VERSION: "2.4",
CONF_PERSISTENCE: False,
CONF_RETAIN: False,
}
},
2,
)
async def test_validate_common_none(hass: HomeAssistantType):
"""Test validate common with None."""
with patch("sys.platform", "win32"), patch(
"homeassistant.components.mysensors.config_flow.try_connect", return_value=True
):
handler = MySensorsConfigFlowHandler()
assert await handler.validate_common(CONF_GATEWAY_TYPE_MQTT) == {}