Files
core/tests/components/vegehub/test_config_flow.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

386 lines
12 KiB
Python
Raw Normal View History

New core integration for VegeHub (#129598) * Initial commit for VegeHub integration * Moved several pieces to library, continuing. * All device contact moved to library * Updated documentation link * Fixed an error in strings.json * Removed commented out code and unused file * Removed unneeded info logging, and a few missed lines of commented code * Added/removed comments for clarity * Converted integration to use webhooks. * Update __init__.py to remove unnecessary code. Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Remove unnecessary code from config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Simplify unique_id assertion. * Switch to CONF_ constant for user input * Added explanation for passing exception. * Got rid of try-except, since I don't really handle the exceptions her anyway. * Moved data transform to vegehub library * Changed references to use HA constants. * Fixed assigning and returning _attr properties. * Moved temperature sensor transform to the library. * Moved sensor names to strings.json * Made webhook names unique to avoid collisions when multiple devices are added. * Converted to using entry.runtime_data * Removed options flow for first PR * Removed switch support to limit PR to one platform * Removed/updated outdated tests * Update homeassistant/components/vegehub/__init__.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Got rid of strings in favor of constants. * Got rid of unnecessary check * Imported constant directly. * Added custom type for entry * Expanded CONF_ constants into sensor.py * Get rid of extra `str` and `get` Co-authored-by: Josef Zweck <josef@zweck.dev> * Added type to errors * Added try/except to MAC address retrieval * Moved functionality out of ConfigFlow that shouldn't have been there * Removed IP:MAC tracking from ConfigFlow * Added retries to VegeHub PyPI package, and implemented them in integration * Removed different sensor types for now * Fixed typo * Changed abort to error * Fixed error reporting in config flow * Further simplify sensor.py to handle all sensors the same * Added comment to clarify * Got rid of unused constants * Removed unused strings in strings.json * Added quality_scale.yaml * Fixed problems in sensor init * Moved config url and sw version storage into vegehub package * Get rid of extra declaration Co-authored-by: Josef Zweck <josef@zweck.dev> * Removed unnecessary task * Fix type for entry * Added a test before setup * Fixed tests and got test coverage of config flow to 100% * Fixed test descriptions * Implemented a coordinator * Removed unused property * Fixed a few minor issues with the coordinator implementation * Removed unused function * Fixed some tests * Trying to fix a problem with re-initialization when server reboots. Mostly working. * Moved hub.setup from async_setup_entry to config flow to avoid running it on system reboot * Delete tests/testing_config/.storage/http.auth * Fixed errors in coordinator.py * Added IP validation for manual input IP addresses * Moved data into self._discovered to simplify * Removed redundant typing * Shortened sensor unique ID and added coordinator handler * Added call to super()._handle_coordinator_update() so state gets handled correctly * Fixed == and is * Got rid of "slot" and moved functionality to lib * Got rid of mocked aiohttp calls in favor of just mocking the vegehub library * Rewrote config flow to make more sense. * Changed order of data and data_description * Changes to sensor.py * Got rid of async_update_data in coordinator and moved async_set_updated_data into webhook callback * Changed sensor updates so that they keep using last known values if update doesn't contain data for them * Changed config flow to use homeassistant.helpers.service_info zeroconf instead of homeassistant.components zeroconf * Added types to test parameters * Changes and notes in config_flow.py * Minor fix to get existing tests working before making changes to tests * Removed unused data and simplified data passing * Fixed tests, removed unused data, moved sensor tests to snapshots * Mocked async_setup_entry and async_unload_entry * Eliminated retry step so that retries just happen in the user flow or zeroconf_confirm * Bumped the library version * Bumped library version again * Changed test-before-setup test * Improved use of coordinator * Almost done reworking tests. A few more changes still needed. * Added via device to sensor.py and key reference to strings.json * Webhook tests are almost, but not quite, working * Fully functional again * Change error to assert * made identifiers and via_device the same * made the via_device just be the mac * Fixed strings.json and updated translations * Fixed test_sensor.py * Cleaned up tests and added autouse to several fixtures to simplify * Switched from error to assert, and added exemption to quality scale. * Cleaned up some tests and added update of IP if unique ID of discovered device is the same. * Improved zeroconfig to update IP and hostname, and added a test to make sure those work. * Fixed a comment. * Improved ip/hostname update test. * Changed Hub to VegeHub in strings.json for clarity. * Switched to using a base entity to simplify and make adding platforms in the future easier. * Moved the vegehub object into the coordinator to simplify. * Removed actuators from sensors, and added unique name for battery sensor * Changed coordinator to manage its own data, changed sensors to use descriptions and return their value as a property * Updated data retrieval keys * Minor updates to several files * Fixed a few things for pytest * Reverted to explicit check for None for pytest * Fixed a comment and a variable name * Fixed a comment * Fix * Bumped depenency version to eliminate pytest from dependencies. --------- Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> Co-authored-by: Josef Zweck <josef@zweck.dev> Co-authored-by: Joostlek <joostlek@outlook.com>
2025-06-23 22:55:34 -06:00
"""Tests for VegeHub config flow."""
from collections.abc import Generator
from ipaddress import ip_address
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.components.vegehub.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
from homeassistant.const import (
CONF_DEVICE,
CONF_HOST,
CONF_IP_ADDRESS,
CONF_MAC,
CONF_WEBHOOK_ID,
)
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from .conftest import TEST_HOSTNAME, TEST_IP, TEST_SIMPLE_MAC
from tests.common import MockConfigEntry
DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo(
ip_address=ip_address(TEST_IP),
ip_addresses=[ip_address(TEST_IP)],
port=80,
hostname=f"{TEST_HOSTNAME}.local.",
type="mock_type",
name="myVege",
properties={
zeroconf.ATTR_PROPERTIES_ID: TEST_HOSTNAME,
"version": "5.1.1",
},
)
@pytest.fixture(autouse=True)
def mock_setup_entry() -> Generator[Any, Any, Any]:
"""Prevent the actual integration from being set up."""
with (
patch("homeassistant.components.vegehub.async_setup_entry", return_value=True),
):
yield
# Tests for flows where the user manually inputs an IP address
async def test_user_flow_success(hass: HomeAssistant) -> None:
"""Test the user flow with successful configuration."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: TEST_IP}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TEST_IP
assert result["data"][CONF_MAC] == TEST_SIMPLE_MAC
assert result["data"][CONF_IP_ADDRESS] == TEST_IP
assert result["data"][CONF_DEVICE] is not None
assert result["data"][CONF_WEBHOOK_ID] is not None
# Since this is user flow, there is no hostname, so hostname should be the IP address
assert result["data"][CONF_HOST] == TEST_IP
assert result["result"].unique_id == TEST_SIMPLE_MAC
# Confirm that the entry was created
entries = hass.config_entries.async_entries(domain=DOMAIN)
assert len(entries) == 1
async def test_user_flow_cannot_connect(
hass: HomeAssistant,
mock_vegehub: MagicMock,
) -> None:
"""Test the user flow with bad data."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
mock_vegehub.mac_address = ""
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: TEST_IP}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"]["base"] == "cannot_connect"
mock_vegehub.mac_address = TEST_SIMPLE_MAC
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: TEST_IP}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
@pytest.mark.parametrize(
("side_effect", "expected_error"),
[
(TimeoutError, "timeout_connect"),
(ConnectionError, "cannot_connect"),
],
)
async def test_user_flow_device_bad_connection_then_success(
hass: HomeAssistant,
mock_vegehub: MagicMock,
side_effect: Exception,
expected_error: str,
) -> None:
"""Test the user flow with a timeout."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
mock_vegehub.setup.side_effect = side_effect
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: TEST_IP}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert "errors" in result
assert result["errors"] == {"base": expected_error}
mock_vegehub.setup.side_effect = None # Clear the error
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: TEST_IP}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TEST_IP
assert result["data"][CONF_IP_ADDRESS] == TEST_IP
assert result["data"][CONF_MAC] == TEST_SIMPLE_MAC
async def test_user_flow_no_ip_entered(hass: HomeAssistant) -> None:
"""Test the user flow with blank IP."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: ""}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"]["base"] == "invalid_ip"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: TEST_IP}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_user_flow_bad_ip_entered(hass: HomeAssistant) -> None:
"""Test the user flow with badly formed IP."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: "192.168.0"}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"]["base"] == "invalid_ip"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: TEST_IP}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_user_flow_duplicate_device(
hass: HomeAssistant, mocked_config_entry: MockConfigEntry
) -> None:
"""Test when user flow gets the same device twice."""
mocked_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_IP_ADDRESS: TEST_IP}
)
assert result["type"] is FlowResultType.ABORT
# Tests for flows that start in zeroconf
async def test_zeroconf_flow_success(hass: HomeAssistant) -> None:
"""Test the zeroconf discovery flow with successful configuration."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "zeroconf_confirm"
# Display the confirmation form
result = await hass.config_entries.flow.async_configure(result["flow_id"], None)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "zeroconf_confirm"
# Proceed to creating the entry
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TEST_HOSTNAME
assert result["data"][CONF_HOST] == TEST_HOSTNAME
assert result["data"][CONF_MAC] == TEST_SIMPLE_MAC
assert result["result"].unique_id == TEST_SIMPLE_MAC
async def test_zeroconf_flow_abort_device_asleep(
hass: HomeAssistant,
mock_vegehub: MagicMock,
) -> None:
"""Test when zeroconf tries to contact a device that is asleep."""
mock_vegehub.retrieve_mac_address.side_effect = TimeoutError
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "timeout_connect"
async def test_zeroconf_flow_abort_same_id(
hass: HomeAssistant,
mocked_config_entry: MockConfigEntry,
) -> None:
"""Test when zeroconf gets the same device twice."""
mocked_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO
)
assert result["type"] is FlowResultType.ABORT
async def test_zeroconf_flow_abort_cannot_connect(
hass: HomeAssistant,
mock_vegehub: MagicMock,
) -> None:
"""Test when zeroconf gets bad data."""
mock_vegehub.mac_address = ""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
async def test_zeroconf_flow_abort_cannot_connect_404(
hass: HomeAssistant,
mock_vegehub: MagicMock,
) -> None:
"""Test when zeroconf gets bad responses."""
mock_vegehub.retrieve_mac_address.side_effect = ConnectionError
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
@pytest.mark.parametrize(
("side_effect", "expected_error"),
[
(TimeoutError, "timeout_connect"),
(ConnectionError, "cannot_connect"),
],
)
async def test_zeroconf_flow_device_error_response(
hass: HomeAssistant,
mock_vegehub: MagicMock,
side_effect: Exception,
expected_error: str,
) -> None:
"""Test when zeroconf detects the device, but the communication fails at setup."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "zeroconf_confirm"
# Part way through the process, we simulate getting bad responses
mock_vegehub.setup.side_effect = side_effect
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.FORM
assert result["errors"]["base"] == expected_error
mock_vegehub.setup.side_effect = None
# Proceed to creating the entry
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_zeroconf_flow_update_ip_hostname(
hass: HomeAssistant,
mocked_config_entry: MockConfigEntry,
) -> None:
"""Test when zeroconf gets the same device with a new IP and hostname."""
mocked_config_entry.add_to_hass(hass)
# Use the same discovery info, but change the IP and hostname
new_ip = "192.168.0.99"
new_hostname = "new_hostname"
new_discovery_info = zeroconf.ZeroconfServiceInfo(
ip_address=ip_address(new_ip),
ip_addresses=[ip_address(new_ip)],
port=DISCOVERY_INFO.port,
hostname=f"{new_hostname}.local.",
type=DISCOVERY_INFO.type,
name=DISCOVERY_INFO.name,
properties=DISCOVERY_INFO.properties,
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=new_discovery_info,
)
assert result["type"] is FlowResultType.ABORT
# Check if the original config entry has been updated
entries = hass.config_entries.async_entries(domain=DOMAIN)
assert len(entries) == 1
assert mocked_config_entry.data[CONF_IP_ADDRESS] == new_ip
assert mocked_config_entry.data[CONF_HOST] == new_hostname