mirror of
https://github.com/home-assistant/core.git
synced 2026-09-28 02:18:10 -04:00
Add NASweb integration (#98118)
* Add NASweb integration * Fix DeviceInfo import * Remove commented out code * Change class name for uniquness * Drop CoordinatorEntity inheritance * Rename class Output to more descriptive: RelaySwitch * Update required webio-api version * Implement on-the-fly addition/removal of entities * Set coordinator name matching device name * Set entities with too old status as unavailable * Drop Optional in favor of modern typing * Fix spelling of a variable * Rename commons to more fitting name: helper * Remove redundant code * Let unload fail when there is no coordinator * Fix bad docstring * Rename cord to coordinator for clarity * Remove default value for pop and let it raise exception * Drop workaround and use get_url from helper.network * Use webhook to send data from device * Deinitialize coordinator when no longer needed * Use Python formattable string * Use dataclass to store integration data in hass.data * Raise ConfigEntryNotReady when appropriate * Refactor NASwebData class * Move RelaySwitch to switch.py * Fix ConfigFlow tests * Create issues when entry fails to load * Respond when correctly received status update * Depend on webhook instead of http * Create issue when status is not received during entry set up * Make issue_id unique across integration entries * Remove unnecessary initializations * Inherit CoordinatorEntity to avoid code duplication * Optimize property access via assignment in __init__ * Use preexisting mechanism to fill schema with user input * Fix translation strings * Handle unavailable or unreachable internal url * Implement custom coordinator for push driven data updates * Move module-specific constants to respective modules * Fix requirements_all.txt * Fix CODEOWNERS file * Raise ConfigEntryError instead of issue creation * Fix entity registry import * Use HassKey as key in hass.data * Use typed ConfigEntry * Store runtime data in config entry * Rewrite to be more Pythonic * Move add/remove of switch entities to switch.py * Skip unnecessary check * Remove unnecessary type hints * Remove unnecessary nonlocal * Use a more descriptive docstring * Add docstrings to NASwebCoordinator * Fix formatting * Use correct return type * Fix tests to align with changed code * Remove commented code * Use serial number as config entry id * Catch AbortFlow exception * Update tests to check ConfigEntry Unique ID * Remove unnecessary form abort
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Tests for the NASweb integration."""
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Common fixtures for the NASweb tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.nasweb.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
BASE_CONFIG_FLOW = "homeassistant.components.nasweb.config_flow."
|
||||
BASE_NASWEB_DATA = "homeassistant.components.nasweb.nasweb_data."
|
||||
BASE_COORDINATOR = "homeassistant.components.nasweb.coordinator."
|
||||
TEST_SERIAL_NUMBER = "0011223344556677"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def validate_input_all_ok() -> Generator[dict[str, AsyncMock | MagicMock]]:
|
||||
"""Yield dictionary of mocked functions required for successful test_form execution."""
|
||||
with (
|
||||
patch(
|
||||
BASE_CONFIG_FLOW + "WebioAPI.check_connection",
|
||||
return_value=True,
|
||||
) as check_connection,
|
||||
patch(
|
||||
BASE_CONFIG_FLOW + "WebioAPI.refresh_device_info",
|
||||
return_value=True,
|
||||
) as refresh_device_info,
|
||||
patch(
|
||||
BASE_NASWEB_DATA + "NASwebData.get_webhook_url",
|
||||
return_value="http://127.0.0.1:8123/api/webhook/de705e77291402afa0dd961426e9f19bb53631a9f2a106c52cfd2d2266913c04",
|
||||
) as get_webhook_url,
|
||||
patch(
|
||||
BASE_CONFIG_FLOW + "WebioAPI.get_serial_number",
|
||||
return_value=TEST_SERIAL_NUMBER,
|
||||
) as get_serial,
|
||||
patch(
|
||||
BASE_CONFIG_FLOW + "WebioAPI.status_subscription",
|
||||
return_value=True,
|
||||
) as status_subscription,
|
||||
patch(
|
||||
BASE_NASWEB_DATA + "NotificationCoordinator.check_connection",
|
||||
return_value=True,
|
||||
) as check_status_confirmation,
|
||||
):
|
||||
yield {
|
||||
BASE_CONFIG_FLOW + "WebioAPI.check_connection": check_connection,
|
||||
BASE_CONFIG_FLOW + "WebioAPI.refresh_device_info": refresh_device_info,
|
||||
BASE_NASWEB_DATA + "NASwebData.get_webhook_url": get_webhook_url,
|
||||
BASE_CONFIG_FLOW + "WebioAPI.get_serial_number": get_serial,
|
||||
BASE_CONFIG_FLOW + "WebioAPI.status_subscription": status_subscription,
|
||||
BASE_NASWEB_DATA
|
||||
+ "NotificationCoordinator.check_connection": check_status_confirmation,
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
"""Test the NASweb config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from webio_api.api_client import AuthError
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.nasweb.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.network import NoURLAvailableError
|
||||
|
||||
from .conftest import (
|
||||
BASE_CONFIG_FLOW,
|
||||
BASE_COORDINATOR,
|
||||
BASE_NASWEB_DATA,
|
||||
TEST_SERIAL_NUMBER,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
||||
|
||||
|
||||
TEST_USER_INPUT = {
|
||||
CONF_HOST: "1.1.1.1",
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password",
|
||||
}
|
||||
|
||||
|
||||
async def _add_test_config_entry(hass: HomeAssistant) -> ConfigFlowResult:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result.get("type") == FlowResultType.FORM
|
||||
assert not result.get("errors")
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_USER_INPUT
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
return result2
|
||||
|
||||
|
||||
async def test_form(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
validate_input_all_ok: dict[str, AsyncMock | MagicMock],
|
||||
) -> None:
|
||||
"""Test the form."""
|
||||
result = await _add_test_config_entry(hass)
|
||||
|
||||
assert result.get("type") == FlowResultType.CREATE_ENTRY
|
||||
assert result.get("title") == "1.1.1.1"
|
||||
assert result.get("data") == TEST_USER_INPUT
|
||||
|
||||
config_entry = result.get("result")
|
||||
assert config_entry is not None
|
||||
assert config_entry.unique_id == TEST_SERIAL_NUMBER
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_cannot_connect(
|
||||
hass: HomeAssistant,
|
||||
validate_input_all_ok: dict[str, AsyncMock | MagicMock],
|
||||
) -> None:
|
||||
"""Test cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(BASE_CONFIG_FLOW + "WebioAPI.check_connection", return_value=False):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_USER_INPUT
|
||||
)
|
||||
|
||||
assert result2.get("type") == FlowResultType.FORM
|
||||
assert result2.get("errors") == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_invalid_auth(
|
||||
hass: HomeAssistant,
|
||||
validate_input_all_ok: dict[str, AsyncMock | MagicMock],
|
||||
) -> None:
|
||||
"""Test invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
BASE_CONFIG_FLOW + "WebioAPI.refresh_device_info",
|
||||
side_effect=AuthError,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_USER_INPUT
|
||||
)
|
||||
|
||||
assert result2.get("type") == FlowResultType.FORM
|
||||
assert result2.get("errors") == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_form_missing_internal_url(
|
||||
hass: HomeAssistant,
|
||||
validate_input_all_ok: dict[str, AsyncMock | MagicMock],
|
||||
) -> None:
|
||||
"""Test missing internal url."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
BASE_NASWEB_DATA + "NASwebData.get_webhook_url", side_effect=NoURLAvailableError
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_USER_INPUT
|
||||
)
|
||||
assert result2.get("type") == FlowResultType.FORM
|
||||
assert result2.get("errors") == {"base": "missing_internal_url"}
|
||||
|
||||
|
||||
async def test_form_missing_nasweb_data(
|
||||
hass: HomeAssistant,
|
||||
validate_input_all_ok: dict[str, AsyncMock | MagicMock],
|
||||
) -> None:
|
||||
"""Test invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
BASE_CONFIG_FLOW + "WebioAPI.get_serial_number",
|
||||
return_value=None,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_USER_INPUT
|
||||
)
|
||||
assert result2.get("type") == FlowResultType.FORM
|
||||
assert result2.get("errors") == {"base": "missing_nasweb_data"}
|
||||
with patch(BASE_CONFIG_FLOW + "WebioAPI.status_subscription", return_value=False):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_USER_INPUT
|
||||
)
|
||||
assert result2.get("type") == FlowResultType.FORM
|
||||
assert result2.get("errors") == {"base": "missing_nasweb_data"}
|
||||
|
||||
|
||||
async def test_missing_status(
|
||||
hass: HomeAssistant,
|
||||
validate_input_all_ok: dict[str, AsyncMock | MagicMock],
|
||||
) -> None:
|
||||
"""Test missing status update."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
BASE_COORDINATOR + "NotificationCoordinator.check_connection",
|
||||
return_value=False,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_USER_INPUT
|
||||
)
|
||||
assert result2.get("type") == FlowResultType.FORM
|
||||
assert result2.get("errors") == {"base": "missing_status"}
|
||||
|
||||
|
||||
async def test_form_exception(
|
||||
hass: HomeAssistant,
|
||||
validate_input_all_ok: dict[str, AsyncMock | MagicMock],
|
||||
) -> None:
|
||||
"""Test other exceptions."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.nasweb.config_flow.validate_input",
|
||||
side_effect=Exception,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_USER_INPUT
|
||||
)
|
||||
assert result2.get("type") == FlowResultType.FORM
|
||||
assert result2.get("errors") == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_form_already_configured(
|
||||
hass: HomeAssistant,
|
||||
validate_input_all_ok: dict[str, AsyncMock | MagicMock],
|
||||
) -> None:
|
||||
"""Test already configured device."""
|
||||
result = await _add_test_config_entry(hass)
|
||||
config_entry = result.get("result")
|
||||
assert config_entry is not None
|
||||
assert config_entry.unique_id == TEST_SERIAL_NUMBER
|
||||
|
||||
result2_1 = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
result2_2 = await hass.config_entries.flow.async_configure(
|
||||
result2_1["flow_id"], TEST_USER_INPUT
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2_2.get("type") == FlowResultType.ABORT
|
||||
assert result2_2.get("reason") == "already_configured"
|
||||
Reference in New Issue
Block a user