Files
fgl-aircon/tests/components/fglair/test_config_flow.py
T
petr.polezhaev da01c607a6 ha(H2): исправления по ревью — несколько устройств, suggested values, YAML-import, типы ошибок
- critical: listen_port по умолчанию 0 (эфемерный) — несколько ConfigEntry
  не конфликтуют; тест двух записей на дефолтном порту
- manifest: добавлен обязательный ключ codeowners
- unload: set_log_handler(None) вынесен в executor (не блокирует loop);
  рефкаунт лог-пампа без гонки (старт после успешного setup)
- config flow: возврат введённого в форму при ошибке
  (add_suggested_values_to_schema; секреты исключены), YAML-import подключён
  через async_setup + SOURCE_IMPORT, ProvisionError.kind вместо разбора
  русского текста, валидация device_port/key_id из JSON
- trial: требует получения всех базовых свойств шаблона (не пустой ok),
  безопасное создание Session
- py-ci.sh: отсутствие .venv-ha — ошибка (PY_CI_SKIP_HA=1 для пропуска);
  pytest.ini testpaths
2026-09-29 12:20:37 +03:00

298 lines
10 KiB
Python

"""Config flow fglair: меню, ручной ввод, импорт, облако (discover замокан)."""
from __future__ import annotations
import json
from unittest.mock import AsyncMock, patch
from helpers import DSN, KEY_ID, LANIP_KEY
from homeassistant import config_entries
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from pytest_homeassistant_custom_component.common import MockConfigEntry
import pyfglair
from custom_components.fglair.const import DOMAIN
from custom_components.fglair.trial import TrialResult
from pyfglair.provision import Device
MANUAL_INPUT = {
"host": "192.168.1.50",
"device_port": 80,
"dsn": DSN,
"lanip_key": LANIP_KEY,
"lanip_key_id": KEY_ID,
"name": "Bedroom",
"model": "AP-WC1E",
}
TRIAL_OK = TrialResult(ok=True)
TRIAL_NETWORK_FAIL = TrialResult(ok=False, reason="unreachable")
async def _menu(hass: HomeAssistant):
return await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
async def _select(hass: HomeAssistant, flow_id: str, option: str):
return await hass.config_entries.flow.async_configure(
flow_id, {"next_step_id": option}
)
def _suggested(schema, field: str):
for key in schema.schema:
if getattr(key, "schema", None) == field:
description = key.description
if isinstance(description, dict):
return description.get("suggested_value")
return None
async def test_user_menu(hass: HomeAssistant):
result = await _menu(hass)
assert result["type"] == "menu"
assert set(result["menu_options"]) == {"cloud", "manual", "import_json"}
async def test_manual_success(hass: HomeAssistant):
with patch(
"custom_components.fglair.config_flow._run_trial",
return_value=TRIAL_OK,
) as trial:
result = await _menu(hass)
result = await _select(hass, result["flow_id"], "manual")
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], MANUAL_INPUT
)
assert result["type"] == "create_entry"
assert result["title"] == "Bedroom"
assert result["result"].unique_id == DSN
data = result["data"]
assert data["lanip_key_id"] == KEY_ID
assert data["template"] == "A"
trial.assert_called_once()
assert trial.call_args.args[0]["host"] == "192.168.1.50"
async def test_manual_connect_error(hass: HomeAssistant):
with patch(
"custom_components.fglair.config_flow._run_trial",
return_value=TRIAL_NETWORK_FAIL,
):
result = await _menu(hass)
result = await _select(hass, result["flow_id"], "manual")
result = await hass.config_entries.flow.async_configure(
result["flow_id"], MANUAL_INPUT
)
assert result["type"] == "form"
assert result["errors"] == {"base": "cannot_connect"}
async def test_manual_key_mismatch_error(hass: HomeAssistant):
with patch(
"custom_components.fglair.config_flow._run_trial",
return_value=TrialResult(ok=False, reason="key_mismatch"),
):
result = await _menu(hass)
result = await _select(hass, result["flow_id"], "manual")
result = await hass.config_entries.flow.async_configure(
result["flow_id"], MANUAL_INPUT
)
assert result["type"] == "form"
assert result["errors"] == {"base": "key_mismatch"}
async def test_manual_already_configured(hass: HomeAssistant):
existing = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DSN)
existing.add_to_hass(hass)
with patch(
"custom_components.fglair.config_flow._run_trial",
return_value=TRIAL_OK,
):
result = await _menu(hass)
result = await _select(hass, result["flow_id"], "manual")
result = await hass.config_entries.flow.async_configure(
result["flow_id"], MANUAL_INPUT
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
async def test_import_json_success(hass: HomeAssistant):
payload = json.dumps(
{
"ip_address": "192.168.1.60",
"dsn": DSN,
"lanip_key": LANIP_KEY,
"lanip_key_id": KEY_ID,
"model": "AP-WB2E",
"name": "Kitchen",
}
)
with patch(
"custom_components.fglair.config_flow._run_trial",
return_value=TRIAL_OK,
):
result = await _menu(hass)
result = await _select(hass, result["flow_id"], "import_json")
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"config_json": payload}
)
assert result["type"] == "create_entry"
assert result["data"]["host"] == "192.168.1.60"
assert result["data"]["template"] == "B"
async def test_import_json_invalid(hass: HomeAssistant):
result = await _menu(hass)
result = await _select(hass, result["flow_id"], "import_json")
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"config_json": "not json"}
)
assert result["type"] == "form"
assert result["errors"] == {"base": "invalid_import"}
async def test_cloud_flow_success(hass: HomeAssistant):
device = Device(
name="Bedroom",
model="AP-WC1E",
dsn=DSN,
lan_ip="192.168.1.50",
lanip_key=LANIP_KEY,
lanip_key_id=KEY_ID,
region="eu",
)
with patch(
"pyfglair.discover", AsyncMock(return_value=[device])
), patch(
"custom_components.fglair.config_flow._run_trial",
return_value=TRIAL_OK,
):
result = await _menu(hass)
result = await _select(hass, result["flow_id"], "cloud")
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"email": "user@example.com", "password": "secret", "region": "eu"},
)
assert result["type"] == "form"
assert result["step_id"] == "cloud_device"
with patch(
"custom_components.fglair.config_flow._run_trial",
return_value=TRIAL_OK,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"dsn": DSN}
)
assert result["type"] == "create_entry"
assert result["data"]["host"] == "192.168.1.50"
assert result["data"]["lanip_key_id"] == KEY_ID
async def test_cloud_auth_error(hass: HomeAssistant):
with patch(
"pyfglair.discover",
AsyncMock(
side_effect=pyfglair.ProvisionError("401 unauthorized", "auth")
),
):
result = await _menu(hass)
result = await _select(hass, result["flow_id"], "cloud")
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"email": "user@example.com", "password": "bad", "region": "eu"},
)
assert result["type"] == "form"
assert result["errors"] == {"base": "invalid_auth"}
async def test_manual_error_keeps_input(hass: HomeAssistant):
with patch(
"custom_components.fglair.config_flow._run_trial",
return_value=TRIAL_NETWORK_FAIL,
):
result = await _menu(hass)
result = await _select(hass, result["flow_id"], "manual")
result = await hass.config_entries.flow.async_configure(
result["flow_id"], MANUAL_INPUT
)
assert result["errors"] == {"base": "cannot_connect"}
assert _suggested(result["data_schema"], "host") == MANUAL_INPUT["host"]
assert _suggested(result["data_schema"], "dsn") == DSN
assert _suggested(result["data_schema"], "lanip_key") is None
async def test_cloud_no_devices(hass: HomeAssistant):
with patch(
"pyfglair.discover",
AsyncMock(
side_effect=pyfglair.ProvisionError(
"У аккаунта нет устройств", "no_devices"
)
),
):
result = await _menu(hass)
result = await _select(hass, result["flow_id"], "cloud")
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"email": "user@example.com", "password": "secret", "region": "eu"},
)
assert result["type"] == "form"
assert result["errors"] == {"base": "no_devices"}
async def test_yaml_import(hass: HomeAssistant):
payload = {
"ip_address": "192.168.1.70",
"dsn": DSN,
"lanip_key": LANIP_KEY,
"lanip_key_id": KEY_ID,
"model": "AP-WF1E",
}
with patch(
"custom_components.fglair.config_flow._run_trial",
return_value=TRIAL_OK,
):
assert await async_setup_component(hass, DOMAIN, {DOMAIN: payload})
await hass.async_block_till_done()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].data["template"] == "F"
assert entries[0].data["host"] == "192.168.1.70"
async def test_yaml_import_invalid(hass: HomeAssistant):
assert await async_setup_component(hass, DOMAIN, {DOMAIN: "not-a-dict"})
await hass.async_block_till_done()
assert hass.config_entries.async_entries(DOMAIN) == []
assert not hass.config_entries.flow.async_progress()
async def test_import_json_null_device_port(hass: HomeAssistant):
payload = json.dumps(
{
"ip_address": "192.168.1.71",
"dsn": DSN,
"lanip_key": LANIP_KEY,
"lanip_key_id": str(KEY_ID),
"device_port": None,
}
)
with patch(
"custom_components.fglair.config_flow._run_trial",
return_value=TRIAL_OK,
) as trial:
result = await _menu(hass)
result = await _select(hass, result["flow_id"], "import_json")
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"config_json": payload}
)
assert result["type"] == "create_entry"
assert result["data"]["device_port"] == 80
assert trial.call_args.args[0]["device_port"] == 80