4
0
Эх сурвалжийг харах

Implement tests for config_flow.py

Test coverage has slipped, especially with the rewrite of config flow, which never had tests, but has grown to be more significant.

Also some minor improvements that help testability, and add aborting the flow
when no matching device is found.
Jason Rumney 4 жил өмнө
parent
commit
319c71986a

+ 4 - 0
custom_components/tuya_local/config_flow.py

@@ -17,6 +17,8 @@ _LOGGER = logging.getLogger(__name__)
 class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
     VERSION = 2
     CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+    device = None
+    data = {}
 
     async def async_step_user(self, user_input=None):
         errors = {}
@@ -106,6 +108,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
             vol.Required(CONF_HOST): str,
         }
         cfg = config_for_legacy_use(config[CONF_TYPE])
+        if cfg is None:
+            return self.async_abort(reason="not_supported")
         e = cfg.primary_entity
         schema[vol.Optional(e.entity, default=True)] = bool
         for e in cfg.secondary_entities():

+ 2 - 2
custom_components/tuya_local/translations/en.json

@@ -34,7 +34,7 @@
     },
     "abort": {
 	"already_configured": "A device with that ID has already been added.",
-	"imported_connection": "Unable to connect to your device with the configured details."
+	"not_supported": "Sorry, there is no support for this device."
     },
     "error": {
 	"connection": "Unable to connect to your device with those details. It could be an intermittent issue, or they may be incorrect."
@@ -61,7 +61,7 @@
 	"connection": "Unable to connect to your device with those details. It could be an intermittent issue, or they may be incorrect."
     },
     "abort": {
-	"not_supported": "No configuration matching this device was found"
+	"not_supported": "Sorry, there is no support for this device."
     }
   }
 }

+ 350 - 0
tests/test_config_flow.py

@@ -0,0 +1,350 @@
+"""Tests for the config flow."""
+from unittest.mock import ANY, AsyncMock, MagicMock, patch
+
+from homeassistant.const import CONF_HOST, CONF_NAME
+import pytest
+from pytest_homeassistant_custom_component.common import MockConfigEntry
+import voluptuous as vol
+
+from custom_components.tuya_local import config_flow
+from custom_components.tuya_local.const import (
+    CONF_CLIMATE,
+    CONF_DEVICE_ID,
+    CONF_LOCAL_KEY,
+    CONF_LOCK,
+    CONF_SWITCH,
+    CONF_TYPE,
+    DOMAIN,
+)
+
+
+@pytest.fixture(autouse=True)
+def auto_enable_custom_integrations(enable_custom_integrations):
+    yield
+
+
+async def test_init_entry(hass):
+    """Test initialisation of the config flow."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        version=2,
+        title="test",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: "localkey",
+            CONF_TYPE: "kogan_heater",
+            CONF_CLIMATE: True,
+            CONF_LOCK: True,
+        },
+    )
+    entry.add_to_hass(hass)
+    await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+    state = hass.states.get("climate.test")
+    assert state
+
+
+async def test_flow_user_init(hass):
+    """Test the initialisation of the form in the first step of the config flow."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": "user"}
+    )
+    expected = {
+        "data_schema": vol.Schema(config_flow.individual_config_schema()),
+        "description_placeholders": None,
+        "errors": {},
+        "flow_id": ANY,
+        "handler": DOMAIN,
+        "step_id": "user",
+        "type": "form",
+        "last_step": ANY,
+    }
+    assert expected == result
+
+
+@patch("custom_components.tuya_local.config_flow.TuyaLocalDevice")
+async def test_async_test_connection_valid(mock_device, hass):
+    """Test that device is returned when connection is valid."""
+    mock_instance = AsyncMock()
+    mock_instance.has_returned_state = True
+    mock_device.return_value = mock_instance
+    device = await config_flow.async_test_connection(
+        {
+            CONF_DEVICE_ID: "deviceid",
+            CONF_LOCAL_KEY: "localkey",
+            CONF_HOST: "hostname",
+        },
+        hass,
+    )
+    assert device == mock_instance
+
+
+@patch("custom_components.tuya_local.config_flow.TuyaLocalDevice")
+async def test_async_test_connection_invalid(mock_device, hass):
+    """Test that None is returned when connection is invalid."""
+    mock_instance = AsyncMock()
+    mock_instance.has_returned_state = False
+    mock_device.return_value = mock_instance
+    device = await config_flow.async_test_connection(
+        {
+            CONF_DEVICE_ID: "deviceid",
+            CONF_LOCAL_KEY: "localkey",
+            CONF_HOST: "hostname",
+        },
+        hass,
+    )
+    assert device is None
+
+
+@patch("custom_components.tuya_local.config_flow.async_test_connection")
+async def test_flow_user_init_invalid_config(mock_test, hass):
+    """Test errors populated when config is invalid."""
+    mock_test.return_value = None
+    flow = await hass.config_entries.flow.async_init(DOMAIN, context={"source": "user"})
+    result = await hass.config_entries.flow.async_configure(
+        flow["flow_id"],
+        user_input={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: "badkey",
+        },
+    )
+    assert {"base": "connection"} == result["errors"]
+
+
+def setup_device_mock(mock, failure=False, type="test"):
+    mock_type = MagicMock()
+    mock_type.legacy_type = type
+    mock_iter = MagicMock()
+    mock_iter.__aiter__.return_value = [mock_type] if not failure else []
+    mock.async_possible_types = MagicMock(return_value=mock_iter)
+
+
+@patch("custom_components.tuya_local.config_flow.async_test_connection")
+async def test_flow_user_init_data_valid(mock_test, hass):
+    """Test we advance to the next step when connection config is valid."""
+    mock_device = MagicMock()
+    setup_device_mock(mock_device)
+    mock_test.return_value = mock_device
+
+    flow = await hass.config_entries.flow.async_init(DOMAIN, context={"source": "user"})
+    result = await hass.config_entries.flow.async_configure(
+        flow["flow_id"],
+        user_input={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: "localkey",
+        },
+    )
+    assert "form" == result["type"]
+    assert "select_type" == result["step_id"]
+
+
+@patch.object(config_flow.ConfigFlowHandler, "device")
+async def test_flow_select_type_init(mock_device, hass):
+    """Test the initialisation of the form in the 2nd step of the config flow."""
+    setup_device_mock(mock_device)
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": "select_type"}
+    )
+    expected = {
+        "data_schema": ANY,
+        "description_placeholders": None,
+        "errors": None,
+        "flow_id": ANY,
+        "handler": DOMAIN,
+        "step_id": "select_type",
+        "type": "form",
+        "last_step": ANY,
+    }
+    assert expected == result
+    # Check the schema.  Simple comparison does not work since they are not
+    # the same object
+    try:
+        result["data_schema"]({CONF_TYPE: "test"})
+    except vol.MultipleInvalid:
+        assert False
+    try:
+        result["data_schema"]({CONF_TYPE: "not_test"})
+        assert False
+    except vol.MultipleInvalid:
+        pass
+
+
+@patch.object(config_flow.ConfigFlowHandler, "device")
+async def test_flow_select_type_aborts_when_no_match(mock_device, hass):
+    """Test the flow aborts when an unsupported device is used."""
+    setup_device_mock(mock_device, failure=True)
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": "select_type"}
+    )
+
+    assert result["type"] == "abort"
+    assert result["reason"] == "not_supported"
+
+
+@patch.object(config_flow.ConfigFlowHandler, "device")
+async def test_flow_select_type_data_valid(mock_device, hass):
+    """Test the flow continues when valid data is supplied."""
+    setup_device_mock(mock_device, type="kogan_switch")
+
+    flow = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": "select_type"}
+    )
+    result = await hass.config_entries.flow.async_configure(
+        flow["flow_id"],
+        user_input={CONF_TYPE: "kogan_switch"},
+    )
+    assert "form" == result["type"]
+    assert "choose_entities" == result["step_id"]
+
+
+async def test_flow_choose_entities_init(hass):
+    """Test the initialisation of the form in the 3rd step of the config flow."""
+
+    with patch.dict(config_flow.ConfigFlowHandler.data, {CONF_TYPE: "kogan_switch"}):
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN, context={"source": "choose_entities"}
+        )
+
+    expected = {
+        "data_schema": ANY,
+        "description_placeholders": None,
+        "errors": None,
+        "flow_id": ANY,
+        "handler": DOMAIN,
+        "step_id": "choose_entities",
+        "type": "form",
+        "last_step": ANY,
+    }
+    assert expected == result
+    # Check the schema.  Simple comparison does not work since they are not
+    # the same object
+    try:
+        result["data_schema"]({CONF_NAME: "test", CONF_SWITCH: True})
+    except vol.MultipleInvalid:
+        assert False
+    try:
+        result["data_schema"]({CONF_CLIMATE: True})
+        assert False
+    except vol.MultipleInvalid:
+        pass
+
+
+async def test_flow_choose_entities_creates_config_entry(hass):
+    """Test the flow ends when data is valid."""
+
+    with patch.dict(
+        config_flow.ConfigFlowHandler.data,
+        {
+            CONF_DEVICE_ID: "deviceid",
+            CONF_LOCAL_KEY: "localkey",
+            CONF_HOST: "hostname",
+            CONF_TYPE: "kogan_switch",
+        },
+    ):
+        flow = await hass.config_entries.flow.async_init(
+            DOMAIN, context={"source": "choose_entities"}
+        )
+        result = await hass.config_entries.flow.async_configure(
+            flow["flow_id"],
+            user_input={CONF_NAME: "test", CONF_SWITCH: True},
+        )
+        expected = {
+            "version": 2,
+            "type": "create_entry",
+            "flow_id": ANY,
+            "handler": DOMAIN,
+            "title": "test",
+            "description": None,
+            "description_placeholders": None,
+            "result": ANY,
+            "options": {},
+            "data": {
+                CONF_DEVICE_ID: "deviceid",
+                CONF_HOST: "hostname",
+                CONF_LOCAL_KEY: "localkey",
+                CONF_SWITCH: True,
+                CONF_TYPE: "kogan_switch",
+            },
+        }
+        assert expected == result
+
+
+async def test_options_flow_init(hass):
+    """Test config flow options."""
+    config_entry = MockConfigEntry(
+        domain=DOMAIN,
+        unique_id="uniqueid",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: "localkey",
+            CONF_NAME: "test",
+            CONF_SWITCH: True,
+            CONF_TYPE: "kogan_switch",
+        },
+    )
+    config_entry.add_to_hass(hass)
+
+    assert await hass.config_entries.async_setup(config_entry.entry_id)
+    await hass.async_block_till_done()
+
+    # show initial form
+    result = await hass.config_entries.options.async_init(config_entry.entry_id)
+    assert "form" == result["type"]
+    assert "user" == result["step_id"]
+    assert {} == result["errors"]
+    assert result["data_schema"](
+        {
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: "localkey",
+            CONF_SWITCH: True,
+        }
+    )
+
+
+@patch("custom_components.tuya_local.config_flow.async_test_connection")
+async def test_options_flow_modifies_config(mock_test, hass):
+    mock_device = MagicMock()
+    mock_test.return_value = mock_device
+
+    config_entry = MockConfigEntry(
+        domain=DOMAIN,
+        unique_id="uniqueid",
+        data={
+            CONF_DEVICE_ID: "deviceid",
+            CONF_HOST: "hostname",
+            CONF_LOCAL_KEY: "localkey",
+            CONF_NAME: "test",
+            CONF_SWITCH: True,
+            CONF_TYPE: "kogan_switch",
+        },
+    )
+    config_entry.add_to_hass(hass)
+
+    assert await hass.config_entries.async_setup(config_entry.entry_id)
+    await hass.async_block_till_done()
+    # show initial form
+    form = await hass.config_entries.options.async_init(config_entry.entry_id)
+    # submit updated config
+    result = await hass.config_entries.options.async_configure(
+        form["flow_id"],
+        user_input={
+            CONF_HOST: "new_hostname",
+            CONF_LOCAL_KEY: "new_key",
+            CONF_SWITCH: False,
+        },
+    )
+    expected = {
+        CONF_HOST: "new_hostname",
+        CONF_LOCAL_KEY: "new_key",
+        CONF_SWITCH: False,
+    }
+    assert "create_entry" == result["type"]
+    assert "" == result["title"]
+    assert result["result"] is True
+    assert expected == result["data"]