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:
1 parent
8f40b6435c
commit
6087566336
4 files changed
+72
-2
No files matched your search
@@ -370,6 +370,19 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
return self.async_abort(reason="already_configured")
|
return self.async_abort(reason="already_configured")
|
||||||
self._abort_if_unique_id_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
|
# First-time setup: pre-filled user step
|
||||||
return await self.async_step_user()
|
return await self.async_step_user()
|
||||||
@@ -490,7 +503,7 @@ class NvidiaShieldConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
# duplicate guards
|
# duplicate guards
|
||||||
yhost = block.get(CONF_ADB_HOST) or block.get(CONF_PROTOCOL_HOST)
|
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(
|
ehost = existing.options.get(CONF_PROTOCOL_HOST) or existing.options.get(
|
||||||
CONF_ADB_HOST
|
CONF_ADB_HOST
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -144,6 +144,10 @@ def install() -> None:
|
|||||||
async_set_unique_id = _stub_set_unique_id
|
async_set_unique_id = _stub_set_unique_id
|
||||||
_abort_if_unique_id_configured = _noop_abort
|
_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):
|
def async_show_form(self, *, step_id, data_schema=None, errors=None):
|
||||||
return {
|
return {
|
||||||
"type": "form",
|
"type": "form",
|
||||||
|
|||||||
@@ -223,6 +223,9 @@ class TestImportStep:
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
}
|
}
|
||||||
flow.async_abort = lambda **kwargs: {"type": "abort", **kwargs}
|
flow.async_abort = lambda **kwargs: {"type": "abort", **kwargs}
|
||||||
|
flow._async_current_entries = lambda: list(
|
||||||
|
hass.config_entries.async_entries()
|
||||||
|
)
|
||||||
return flow
|
return flow
|
||||||
|
|
||||||
async def test_ui_configured_host_never_re_seeded(self):
|
async def test_ui_configured_host_never_re_seeded(self):
|
||||||
|
|||||||
@@ -95,6 +95,12 @@ def _install_stubs() -> None:
|
|||||||
def _async_current_entries(self):
|
def _async_current_entries(self):
|
||||||
return _zc_holder.entries
|
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:
|
class OptionsFlow:
|
||||||
def async_show_form(self, *, step_id, data_schema=None, errors=None):
|
def async_show_form(self, *, step_id, data_schema=None, errors=None):
|
||||||
return {
|
return {
|
||||||
@@ -128,7 +134,7 @@ def _install_stubs() -> None:
|
|||||||
|
|
||||||
# Shared holder so tests can swap the current-entry list through the
|
# Shared holder so tests can swap the current-entry list through the
|
||||||
# module object (the stub closure reads this same object).
|
# 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
|
sys.modules[__name__]._zc_holder = _zc_holder
|
||||||
|
|
||||||
class AbortIfConfiguredError(Exception):
|
class AbortIfConfiguredError(Exception):
|
||||||
@@ -835,6 +841,50 @@ class TestIpChangeSelfHealF1:
|
|||||||
assert factory() is None
|
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:
|
class TestZeroconfDiscovery:
|
||||||
"""P4-1a: _androidtvremote2._tcp discovery → pre-filled user step."""
|
"""P4-1a: _androidtvremote2._tcp discovery → pre-filled user step."""
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user