test: wire pytest test harness with standard hass fixture (#9)

This commit was merged in pull request #9.
This commit is contained in:
2026-09-13 19:18:10 -04:00
parent c75de71f37
commit b854013f1a
11 changed files with 686 additions and 467 deletions
+31 -99
View File
@@ -4,58 +4,6 @@ from __future__ import annotations
import ast
import json
from pathlib import Path
import sys
from types import SimpleNamespace
from unittest.mock import MagicMock
# Mock homeassistant modules if not installed in environment
for _mod in (
"homeassistant",
"homeassistant.core",
"homeassistant.config_entries",
"homeassistant.data_entry_flow",
):
sys.modules.setdefault(_mod, MagicMock())
# Wire parent package attributes to the actual module mocks so that
# "from homeassistant import config_entries" resolves to the mocked module.
sys.modules["homeassistant"].config_entries = sys.modules["homeassistant.config_entries"]
sys.modules["homeassistant"].data_entry_flow = sys.modules["homeassistant.data_entry_flow"]
class _ConfigFlowBase:
"""Minimal stand-in for homeassistant ConfigFlow."""
VERSION = 1
def __init_subclass__(cls, domain=None, **kwargs):
super().__init_subclass__(**kwargs)
cls._domain = domain
def __init__(self):
self.context = {}
def async_show_form(self, *, step_id, **kwargs):
return {"type": "form", "step_id": step_id}
def async_abort(self, *, reason, **kwargs):
return {"type": "abort", "reason": reason}
def async_create_entry(self, *, title, data, **kwargs):
return {"type": "create_entry", "title": title, "data": data}
async def async_set_unique_id(self, unique_id, raise_on_progress=True):
self.context["unique_id"] = unique_id
return None
def _async_current_entries(self):
return self.hass.config_entries.async_entries(self._domain)
def _async_in_progress(self):
return []
sys.modules["homeassistant.config_entries"].ConfigFlow = _ConfigFlowBase
import pytest
@@ -67,79 +15,63 @@ STRINGS_PATH = PKG_DIR / "strings.json"
TRANSLATIONS_EN_PATH = PKG_DIR / "translations" / "en.json"
def _run(coro):
import asyncio
return asyncio.new_event_loop().run_until_complete(coro)
def _make_flow(existing_entries: list | None = None, in_progress: list | None = None):
"""Create a config flow instance with a stubbed hass."""
flow = config_flow.NestQuestConfigFlow()
flow.hass = SimpleNamespace(
config_entries=SimpleNamespace(
async_entries=lambda domain: [
entry
for entry in (existing_entries or [])
if entry.domain == domain
]
)
)
if in_progress:
flow._async_in_progress = lambda: in_progress
return flow
def _make_entry(domain: str):
"""Create a stub config entry with the given domain."""
return SimpleNamespace(domain=domain)
def test_flow_shows_form_initially() -> None:
async def test_flow_shows_form_initially(hass, make_flow) -> None:
"""The initial user step shows a form."""
flow = _make_flow()
result = _run(flow.async_step_user(None))
flow = make_flow(hass)
result = await flow.async_step_user(None)
assert result["type"] == "form"
assert result["step_id"] == "user"
def test_flow_creates_entry_on_submit() -> None:
"""Submitting the user step creates a config entry."""
flow = _make_flow()
result = _run(flow.async_step_user({}))
async def test_flow_creates_entry_on_submit(hass, make_flow) -> None:
"""Submitting the user step returns a create-entry result with the payload.
Real HA's ConfigFlow.async_create_entry only returns the flow result; the
flow manager persists the entry afterward. The manager surface therefore
stays empty here — persistence is the manager's job, not the flow's.
"""
flow = make_flow(hass)
assert hass.config_entries.async_entries(DOMAIN) == []
result = await flow.async_step_user({})
assert result["type"] == "create_entry"
assert result["title"] == "NestQuest"
assert result["data"] == {}
assert hass.config_entries.async_entries(DOMAIN) == []
def test_second_flow_aborts_single_instance() -> None:
async def test_second_flow_aborts_single_instance(
hass, make_flow, make_config_flow_entry
) -> None:
"""A second flow aborts with single_instance_allowed."""
flow = _make_flow([_make_entry(DOMAIN)])
result = _run(flow.async_step_user(None))
existing = make_config_flow_entry(DOMAIN)
flow = make_flow(hass, existing_entries=[existing])
assert hass.config_entries.async_entries(DOMAIN) == [existing]
result = await flow.async_step_user(None)
assert result["type"] == "abort"
assert result["reason"] == "single_instance_allowed"
assert hass.config_entries.async_entries(DOMAIN) == [existing]
def test_flow_aborts_when_in_progress() -> None:
async def test_flow_aborts_when_in_progress(hass, make_flow) -> None:
"""A concurrent flow in progress aborts with single_instance_allowed."""
flow = _make_flow(in_progress=[{"flow_id": "other"}])
result = _run(flow.async_step_user(None))
flow = make_flow(hass, in_progress=[{"flow_id": "other"}])
result = await flow.async_step_user(None)
assert result["type"] == "abort"
assert result["reason"] == "single_instance_allowed"
def test_flow_sets_stable_unique_id() -> None:
async def test_flow_sets_stable_unique_id(hass, make_flow) -> None:
"""The flow assigns a stable unique id equal to the domain."""
flow = _make_flow()
flow = make_flow(hass)
assert flow.context.get("unique_id") is None
_run(flow.async_step_user(None))
await flow.async_step_user(None)
assert flow.context["unique_id"] == DOMAIN
def test_flow_ignores_other_domain_entries() -> None:
async def test_flow_ignores_other_domain_entries(hass, make_flow, make_config_flow_entry) -> None:
"""Entries of other domains do not trigger the abort."""
flow = _make_flow([_make_entry("other_domain")])
result = _run(flow.async_step_user(None))
flow = make_flow(hass, existing_entries=[make_config_flow_entry("other_domain")])
result = await flow.async_step_user(None)
assert result["type"] == "form"
assert result["step_id"] == "user"