Do not probe Pi-hole v6 API version with a wrong password (#178143)

This commit is contained in:
Nicolas Chambers
2026-08-05 13:42:37 +02:00
committed by GitHub
parent eda8d3b5c0
commit 796e41d28c
3 changed files with 58 additions and 8 deletions
+19 -8
View File
@@ -146,7 +146,11 @@ async def determine_api_version(
debugging.
"""
hole_v6 = api_by_version(hass, entry, 6, password="wrong_password")
# Pi-hole v6 rate-limits login attempts, so probing with a deliberately
# wrong password makes the real login that follows shortly after fail as
# well. Probe with the configured password instead: a 401 identifies v6
# just as well as a success does.
hole_v6 = api_by_version(hass, entry, 6)
try:
await hole_v6.authenticate()
except HoleConnectionError as err:
@@ -160,7 +164,8 @@ async def determine_api_version(
except HoleError as ex_v6:
if str(ex_v6) == "Authentication failed: Invalid password":
_LOGGER.debug(
"Success connecting to Pi-hole at %s without auth, API version is : %s",
"Pi-hole at %s answered the v6 auth endpoint (password"
" rejected), API version is : %s",
hole_v6.base_url,
6,
)
@@ -169,13 +174,19 @@ async def determine_api_version(
"Connection to %s failed: %s, trying API version 5", hole_v6.base_url, ex_v6
)
else:
# It seems that occasionally the auth can succeed
# unexpectedly when there is a valid session
_LOGGER.warning(
"Authenticated with %s through v6 API, but"
" succeeded with an incorrect password."
" This is a known bug",
# Release the probe session again: the operational client
# authenticates separately and Pi-hole allows only a limited
# number of concurrent sessions.
try:
await hole_v6.logout()
except HoleError as err:
_LOGGER.debug(
"Could not release the probe session at %s: %s", hole_v6.base_url, err
)
_LOGGER.debug(
"Authenticated with %s through v6 API, API version is : %s",
hole_v6.base_url,
6,
)
return 6
hole_v5 = api_by_version(hass, entry, 5, password="wrong_token")
+4
View File
@@ -202,6 +202,7 @@ def _create_mocked_hole(
incorrect_app_password: bool = False,
wrong_host: bool = False,
ftl_error: bool = False,
logout_error: bool = False,
) -> MagicMock:
"""Return a mocked Hole API object with side effects based on constructor args."""
@@ -250,6 +251,9 @@ def _create_mocked_hole(
mocked_hole.data = FTL_ERROR
mocked_hole.authenticate = AsyncMock(side_effect=authenticate_side_effect)
mocked_hole.logout = AsyncMock(
side_effect=HoleError("Logout failed") if logout_error else None
)
mocked_hole.get_data = AsyncMock(side_effect=get_data_side_effect)
if ftl_error:
+35
View File
@@ -62,6 +62,41 @@ async def test_setup_api_v6(
)
async def test_version_probe_uses_configured_password(hass: HomeAssistant) -> None:
"""Test the v6 version probe never authenticates with a sentinel password.
Pi-hole v6 rate-limits login attempts, so a deliberately failed probe makes
the real login that follows shortly after fail as well, and the config entry
then never recovers on its own.
"""
mocked_hole = _create_mocked_hole(api_version=6)
entry = MockConfigEntry(domain=pi_hole.DOMAIN, data={**CONFIG_DATA_DEFAULTS})
entry.add_to_hass(hass)
with _patch_init_hole(mocked_hole):
assert await hass.config_entries.async_setup(entry.entry_id)
assert mocked_hole.instances
assert all(instance.password == API_KEY for instance in mocked_hole.instances)
# The probe releases its session again so it does not use up one of the
# limited number of Pi-hole sessions.
mocked_hole.instances[0].logout.assert_awaited_once()
async def test_version_probe_survives_failing_logout(hass: HomeAssistant) -> None:
"""Test setup still succeeds when releasing the probe session fails.
Releasing the session is a courtesy, not a requirement, so a failure there
must not turn a working configuration into a failed setup.
"""
mocked_hole = _create_mocked_hole(api_version=6, logout_error=True)
entry = MockConfigEntry(domain=pi_hole.DOMAIN, data={**CONFIG_DATA_DEFAULTS})
entry.add_to_hass(hass)
with _patch_init_hole(mocked_hole):
assert await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
@pytest.mark.parametrize(
("config_entry_data", "expected_api_token"),
[({**CONFIG_DATA_DEFAULTS}, API_KEY)],