Review fixes: persisted flow-user admin seed and real cv.multi_select

Review rounds 1-2 (Codex gpt-5.6-terra) found two HIGHs against real HA:
- entry.context is not a durable record — the config flow now persists
  the completing HA user into entry data (CONF_ADMIN_USER_IDS) and the
  seed precedence becomes: options copy (the current list) first, then
  the config flow's entry-data copy, then the context user as a last
  resort. Config-flow tests cover the persisted user and the
  context-free path; a seed-precedence test covers options > data.
- the hand-rolled closure could not serialize as an HA multi-select.
  The harness now mocks homeassistant.helpers.config_validation with a
  faithful multi_select, and the flow uses cv.multi_select exactly as
  real HA code does. Suite: 904 passed.
This commit is contained in:
Joshua King
2026-09-15 17:13:32 -04:00
parent 0d61ea2c88
commit d90c170460
6 changed files with 114 additions and 35 deletions
+28
View File
@@ -42,6 +42,8 @@ _HA_MODULES = (
"homeassistant.config_entries",
"homeassistant.data_entry_flow",
"homeassistant.exceptions",
"homeassistant.helpers",
"homeassistant.helpers.config_validation",
)
def _find_spec(name: str):
@@ -79,6 +81,8 @@ for _parent, _child in (
("homeassistant", "data_entry_flow"),
("homeassistant", "core"),
("homeassistant", "exceptions"),
("homeassistant", "helpers"),
("homeassistant.helpers", "config_validation"),
):
setattr(sys.modules[_parent], _child, sys.modules[f"{_parent}.{_child}"])
@@ -181,6 +185,30 @@ class ConfigEntryNotReady(Exception):
_exceptions_mock.ConfigEntryNotReady = ConfigEntryNotReady
import voluptuous as vol
def _multi_select(choices: dict):
"""Mirror homeassistant.helpers.config_validation.multi_select.
The options flow's admin picker uses HA's supported multi-select
validator (schema-serializable by HA's frontend); the mock provides
the same contract: a list whose members are keys of ``choices``.
"""
def _validate(selected):
if not isinstance(selected, list):
raise vol.Invalid("Not a list")
for value in selected:
if value not in choices:
raise vol.Invalid(f"{value} is not a valid option")
return selected
return _validate
_ha_mock("homeassistant.helpers.config_validation").multi_select = _multi_select
# ---------------------------------------------------------------------------
# Shared helpers for the behavior tests.
+24
View File
@@ -278,3 +278,27 @@ async def test_setup_does_not_reseed_narrowed_allowlist(
finally:
await database.close()
assert await async_unload_entry(hass, entry_reloaded) is True
async def test_setup_seed_prefers_options_over_entry_data(
hass, make_entry
) -> None:
"""Seed precedence on a fresh database: the options flow's saved
list (the current one) beats the config flow's original copy."""
from custom_components.nestquest.const import CONF_ADMIN_USER_IDS
entry = await _setup_entry(
hass,
make_entry(
options={CONF_ADMIN_USER_IDS: ["user-current"]},
data={CONF_ADMIN_USER_IDS: ["ha-owner"]},
context={"user_id": "ha-owner"},
),
hass.registry,
)
database = entry.runtime_data.database
try:
assert await list_admin_ids(database) == ["user-current"]
finally:
await database.close()
assert await async_unload_entry(hass, entry) is True
+22 -1
View File
@@ -154,4 +154,25 @@ def test_no_hardcoded_domain_or_db_filename_in_new_modules() -> None:
)
assert node.value != "nestquest.db", (
f"Found hard-coded database filename in {py_file.name}:{node.lineno}"
)
)
async def test_flow_persists_flow_user_as_initial_admin(hass, make_flow) -> None:
"""The HA user completing the flow lands in entry data as the
initial admin copy: entry.context is not persisted by real HA, so
the durable record must carry the owner."""
from custom_components.nestquest.const import CONF_ADMIN_USER_IDS
flow = make_flow(hass)
flow.context = {"user_id": "ha-owner"}
result = await flow.async_step_user({})
assert result["type"] == "create_entry"
assert result["data"] == {CONF_ADMIN_USER_IDS: ["ha-owner"]}
async def test_flow_without_context_user_keeps_data_untouched(
hass, make_flow
) -> None:
flow = make_flow(hass)
result = await flow.async_step_user({"horizon_days": 7})
assert result["type"] == "create_entry"
assert result["data"] == {"horizon_days": 7}