what_to_wear/tests/test_diagnostics.py
Nora 34d028c01a feat(4.4): Diagnostics-Whitelist — Epic 4 komplett
Story 4.4 (TDD, phcc, Suite gruen 167/167):
- diagnostics.py: Whitelist statt redigiertem Dump — Options ohne Secrets, KEIN
  entry.data-Dump (Entity-ID), Stueck-ANZAHL je Kategorie (gegen Enum normalisiert,
  nie Namen), keine source/Namen; Substring-Test beweist kein Leak (AD-5/FR-6.4).
- luna-pro-Security-Review: 2/5 Findings uebernommen, 3 verworfen (Evidenz im Ledger).

Epic 4 (optionaler LLM-Ton) komplett: Optionen+Disclosure, Phraser, Client, Diagnostics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:34:17 +00:00

83 lines
3.3 KiB
Python

"""Story 4.4 — diagnostics whitelist + redaction (AD-5/NFR-6)."""
from __future__ import annotations
import json
from datetime import timedelta
from homeassistant.components.weather import WeatherEntityFeature
from homeassistant.config_entries import ConfigSubentryData
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
from homeassistant.util import dt as dt_util
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.what_to_wear.const import CONF_WEATHER_ENTITY, DOMAIN, default_options
from custom_components.what_to_wear.diagnostics import async_get_config_entry_diagnostics
ENTITY = "weather.home_muellerstrasse"
SECRET_KEY = "sk-supersecret-12345"
ITEM_NAME = "Michaels Arbeitshose"
def _set_weather(hass) -> None:
hass.states.async_set(
ENTITY, "cloudy",
{"temperature_unit": "°C", "wind_speed_unit": "km/h", "precipitation_unit": "mm",
"supported_features": WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY},
)
def _register_forecast(hass) -> None:
async def handler(call) -> ServiceResponse:
base = dt_util.now()
entries = [{"datetime": (base + timedelta(days=o)).replace(hour=12), "temperature": 8.0,
"templow": 5.0} for o in range(-1, 4)]
return {call.data["entity_id"]: {"forecast": entries}}
hass.services.async_register("weather", "get_forecasts", handler,
supports_response=SupportsResponse.ONLY)
def _item(name, cat):
return ConfigSubentryData(data={"name": name, "category": cat, "warmth": 3},
subentry_type="item", title=name, unique_id=None)
async def _setup(hass):
_set_weather(hass)
_register_forecast(hass)
opts = default_options()
opts.update({"llm_enabled": True, "llm_api_key": SECRET_KEY})
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_WEATHER_ENTITY: ENTITY}, options=opts,
subentries_data=[_item(ITEM_NAME, "bottom"), _item("Shirt", "top"), _item("Boots", "shoes")],
)
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry
async def test_diagnostics_never_leak_key_entity_or_names(hass: HomeAssistant) -> None:
entry = await _setup(hass)
diag = await async_get_config_entry_diagnostics(hass, entry)
blob = json.dumps(diag, default=str)
# the API key value must never appear
assert SECRET_KEY not in blob
# the weather entity id (a location hint) must never appear
assert ENTITY not in blob
# item names must never appear
assert ITEM_NAME not in blob
async def test_diagnostics_contains_useful_whitelist(hass: HomeAssistant) -> None:
entry = await _setup(hass)
diag = await async_get_config_entry_diagnostics(hass, entry)
# per-category counts (not names)
assert diag["item_counts"] == {"bottom": 1, "top": 1, "shoes": 1}
assert "recommendation" in diag
assert diag["recommendation"]["status"] == "ok"
assert "metrics" in diag["recommendation"]
# options present as a whitelist without the secret key
assert "llm_api_key" not in diag["options"]
assert "data" not in diag # entry.data (weather entity id) not dumped at all
assert "source" not in diag["recommendation"] # entity id never included