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:
@@ -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"
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user