fix(config_flow): F2 — MAC-less zeroconf discovery calls the audited _async_handle_discovery_without_unique_id helper and guards against a same-host duplicate prefill, so re-discovery can't stack entries; the import guard reads entries via the base _async_current_entries helper; TestMacLessDiscoveryF2 pins helper call + one-entry outcome (t32)

This commit is contained in:
James Mackay committed 2026-10-09 08:04:05 +01:00
1 parent 8f40b6435c
commit 6087566336
4 files changed
+72 -2

No files matched your search

+14 -1
View File
@@ -370,6 +370,19 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
)
return self.async_abort(reason="already_configured")
self._abort_if_unique_id_configured()
else:
# t32 F2 (review rider): MAC-less discovery cannot carry a
# unique_id — HA's helper records the uniqueness-less discovery
# (the same discovery context can't stack duplicates) and the
# host guard keeps a second discovery of an already-configured
# host from stacking another prefill.
self._async_handle_discovery_without_unique_id()
for entry in self._async_current_entries():
existing_host = entry.options.get(
CONF_PROTOCOL_HOST
) or entry.options.get(CONF_ADB_HOST)
if existing_host and existing_host == host:
return self.async_abort(reason="already_configured")
# First-time setup: pre-filled user step
return await self.async_step_user()
@@ -490,7 +503,7 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
# duplicate guards
yhost = block.get(CONF_ADB_HOST) or block.get(CONF_PROTOCOL_HOST)
for existing in self.hass.config_entries.async_entries():
for existing in self._async_current_entries():
ehost = existing.options.get(CONF_PROTOCOL_HOST) or existing.options.get(
CONF_ADB_HOST
)
+4
View File
@@ -144,6 +144,10 @@ def install() -> None:
async_set_unique_id = _stub_set_unique_id
_abort_if_unique_id_configured = _noop_abort
def _async_handle_discovery_without_unique_id(self):
# t32 F2 (audited helper): recording no-op at hub scope
pass
def async_show_form(self, *, step_id, data_schema=None, errors=None):
return {
"type": "form",
+3
View File
@@ -223,6 +223,9 @@ class TestImportStep:
**kwargs,
}
flow.async_abort = lambda **kwargs: {"type": "abort", **kwargs}
flow._async_current_entries = lambda: list(
hass.config_entries.async_entries()
)
return flow
async def test_ui_configured_host_never_re_seeded(self):
+51 -1
View File
@@ -95,6 +95,12 @@ def _install_stubs() -> None:
def _async_current_entries(self):
return _zc_holder.entries
# t32 F2: the audited helper for uniqueness-less discovery — a
# recording no-op here; the flow's own host guard does the stacking
# prevention the acceptance pins.
def _async_handle_discovery_without_unique_id(self):
_zc_holder.discovery_without_unique_id_calls += 1
class OptionsFlow:
def async_show_form(self, *, step_id, data_schema=None, errors=None):
return {
@@ -128,7 +134,7 @@ def _install_stubs() -> None:
# Shared holder so tests can swap the current-entry list through the
# module object (the stub closure reads this same object).
_zc_holder = types.SimpleNamespace(entries=[])
_zc_holder = types.SimpleNamespace(entries=[], discovery_without_unique_id_calls=0)
sys.modules[__name__]._zc_holder = _zc_holder
class AbortIfConfiguredError(Exception):
@@ -835,6 +841,50 @@ class TestIpChangeSelfHealF1:
assert factory() is None
class TestMacLessDiscoveryF2:
"""t32 F2 (rider): MAC-less zeroconf discovery records the
uniqueness-less discovery and can't stack a duplicate prefill."""
def _discovery(self, host="192.168.1.50", props=None):
return SimpleNamespace(
ip_address=host, host=host, properties=props or {}
)
async def test_mac_less_records_the_helper(self, tmp_path):
module = _form_module()
flow = module.NvidiaShieldConfigFlow()
flow.hass = FakeHass(str(tmp_path))
sys.modules[__name__]._zc_holder.discovery_without_unique_id_calls = 0
result = await flow.async_step_zeroconf(self._discovery())
assert result["type"] == "form" # prefill still offered once
assert (
sys.modules[__name__]._zc_holder.discovery_without_unique_id_calls
== 1
)
async def test_mac_less_rediscovery_does_not_stack(self, tmp_path):
"""A second MAC-less discovery of an already-configured host
aborts — one-entry outcome."""
module = _form_module()
flow = module.NvidiaShieldConfigFlow()
flow.hass = FakeHass(str(tmp_path))
entry = SimpleNamespace(
entry_id="e1",
unique_id=None, # MAC-less entry
options={CONF_PROTOCOL_HOST: "192.168.1.50"},
title="Shield",
)
sys.modules[__name__]._zc_holder.entries[:] = [entry]
result = await flow.async_step_zeroconf(self._discovery())
assert result == {"type": "abort", "reason": "already_configured"}
# the entries list is untouched: one-entry outcome
assert len(sys.modules[__name__]._zc_holder.entries) == 1
class TestZeroconfDiscovery:
"""P4-1a: _androidtvremote2._tcp discovery → pre-filled user step."""