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
|
# (``NESTQUEST_PANEL_TOKEN`` on the API side). Both are stored on the
|
||||||
# entry like every other setting — options first, then the entry-data
|
# entry like every other setting — options first, then the entry-data
|
||||||
# copy, then defaults. The TOKEN IS A SECRET: it is never logged, never
|
# copy, then defaults. The TOKEN IS A SECRET: it is never logged, never
|
||||||
# echoed into error messages, and never committed. No invented default
|
# echoed into error messages, and never committed. The shipped default
|
||||||
# token ships (an empty stored token means "not configured yet", and the
|
# token IS the empty string (DEFAULT_PANEL_TOKEN) — an entry with no
|
||||||
# panel plane answers 401 until a real one is saved) — mirroring the API
|
# configured token is therefore NOT a working client: the options flow
|
||||||
# service's fail-closed stance (api/config.py: no default token exists).
|
# 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"
|
CONF_API_BASE_URL = "api_base_url"
|
||||||
DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"
|
DEFAULT_API_BASE_URL = "http://127.0.0.1:8000"
|
||||||
CONF_PANEL_TOKEN = "panel_token"
|
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"
|
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:
|
async def test_client_logs_nothing() -> None:
|
||||||
"""The client emits no log records at all (the token must never
|
"""The client emits no log records at all (the token must never
|
||||||
reach a log line), success or failure."""
|
reach a log line), success or failure."""
|
||||||
|
|||||||
Reference in New Issue
Block a user