Fix (P2): document the real unconfigured-token contract and pin it with a test

The const.py comment claimed an empty stored token yields a client that
answers 401 until configured, but NestQuestApiClient raises ValueError
at construction for an empty/whitespace token. Keep the fail-fast guard,
correct the documentation to state the actual contract (default token is
the empty string; options flow validates non-empty; the coordinator must
configure a token first or handle the construction error), and add a
test pinning that client_from_entry with an unconfigured entry raises at
construction instead of building a client that can only 401.
This commit is contained in:
2026-09-23 05:03:29 -04:00
parent 2b6b7431b5
commit b6b1ab1934
2 changed files with 39 additions and 4 deletions
+11 -4
View File
@@ -109,10 +109,17 @@ DEFAULT_AUTOMATION_ENABLED = True
# (``NESTQUEST_PANEL_TOKEN`` on the API side). Both are stored on the
# entry like every other setting — options first, then the entry-data
# copy, then defaults. The TOKEN IS A SECRET: it is never logged, never
# echoed into error messages, and never committed. No invented default
# token ships (an empty stored token means "not configured yet", and the
# panel plane answers 401 until a real one is saved) — mirroring the API
# service's fail-closed stance (api/config.py: no default token exists).
# echoed into error messages, and never committed. The shipped default
# token IS the empty string (DEFAULT_PANEL_TOKEN) — an entry with no
# configured token is therefore NOT a working client: the options flow
# validates that a saved token is non-empty (the user-facing guard), and
# NestQuestApiClient fails fast at construction on an empty/whitespace
# token (api_client.py). So a caller building the client from a bare
# entry must either ensure a token is configured first or handle the
# construction ValueError and surface an "API not configured" state —
# an unconfigured entry never yields a client that can only 401.
# This mirrors the API service's fail-closed stance (api/config.py: no
# default token exists).
CONF_API_BASE_URL = "api_base_url"
DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"
CONF_PANEL_TOKEN = "panel_token"
+28
View File
@@ -399,6 +399,34 @@ def test_client_from_entry_reads_config_and_shared_session(monkeypatch) -> None:
assert client._panel_token == "entry-token"
def test_client_from_entry_unconfigured_token_raises_at_construction(
monkeypatch,
) -> None:
"""An entry with NO configured token (the shipped default) raises
ValueError AT CONSTRUCTION — the client's fail-fast guard — rather
than silently building a client that can only 401 (the documented
contract: the options flow's non-empty validation is the user-facing
guard; the coordinator must ensure a token is configured first or
handle the construction error as "API not configured")."""
fake_module = types.ModuleType("homeassistant.helpers.aiohttp_client")
fake_module.async_get_clientsession = lambda hass: object()
monkeypatch.setitem(
sys.modules, "homeassistant.helpers.aiohttp_client", fake_module
)
entry = make_config_entry(
options={CONF_API_BASE_URL: DEFAULT_API_BASE_URL},
)
assert resolve_api_config(entry)[1] == DEFAULT_PANEL_TOKEN == ""
with pytest.raises(ValueError, match="panel_token must be a non-empty"):
api_client.client_from_entry(object(), entry)
# Same for a whitespace-only stored token: whitespace is not a token.
entry_ws = make_config_entry(options={CONF_PANEL_TOKEN: " "})
with pytest.raises(ValueError, match="panel_token must be a non-empty"):
api_client.client_from_entry(object(), entry_ws)
async def test_client_logs_nothing() -> None:
"""The client emits no log records at all (the token must never
reach a log line), success or failure."""