what_to_wear/tests/test_config_flow.py
Nora 95a077f0c1 feat(1.8): Config-Flow (Test-Abruf, 4 Fehler) + Entry-Lebenszyklus
Story 1.8 (TDD, phcc, Suite gruen 105/105):
- config_flow.py: EntitySelector + Offset, sofortiger Test-Abruf, 4 unterscheidbare
  uebersetzte Fehler (no_weather_entity/no_forecast/forecast_too_short/temporary).
- __init__.py: async_setup/unload_entry, Coordinator + Zeit-/Entity-Listener leak-frei
  via async_on_unload, update_listener->reload, PLATFORMS=[] (Sensor kommt 1.9).
- translations/{en,de}.json (inkrementell, AD-13); requirements-test.txt (yanked-Pin
  + frontend); conftest hass_storage gegen Lingering-Timer.
- luna-pro-Story-Review: 2/3 Findings uebernommen, 1 verworfen (Evidenz im Ledger).

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

142 lines
5.8 KiB
Python

"""Story 1.8 — config flow (test fetch, 4 errors) and entry lifecycle."""
from __future__ import annotations
from datetime import timedelta
import pytest
from homeassistant.components.weather import WeatherEntityFeature
from homeassistant.config_entries import SOURCE_USER
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.exceptions import HomeAssistantError
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
ENTITY = "weather.home"
def _set_weather(hass, features=WeatherEntityFeature.FORECAST_DAILY, state="cloudy") -> None:
hass.states.async_set(
ENTITY,
state,
{
"temperature_unit": "°C",
"wind_speed_unit": "km/h",
"precipitation_unit": "mm",
"supported_features": features,
},
)
def _register_forecast(hass, entries=None, raises=False) -> None:
async def handler(call) -> ServiceResponse:
if raises:
raise HomeAssistantError("unavailable")
base = dt_util.now()
default = [
{"datetime": (base + timedelta(days=off)).replace(hour=12), "temperature": 10.0,
"templow": 5.0}
for off in range(-1, 4)
]
return {call.data["entity_id"]: {"forecast": entries if entries is not None else default}}
hass.services.async_register(
"weather", "get_forecasts", handler, supports_response=SupportsResponse.ONLY
)
async def test_flow_success_creates_entry(hass: HomeAssistant) -> None:
_set_weather(hass)
_register_forecast(hass)
result = await hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_USER})
assert result["type"] == FlowResultType.FORM
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_WEATHER_ENTITY: ENTITY, "cold_sensitivity_offset": 1}
)
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["data"] == {CONF_WEATHER_ENTITY: ENTITY}
assert result2["options"]["cold_sensitivity_offset"] == 1
async def test_flow_error_no_weather_entity(hass: HomeAssistant) -> None:
_register_forecast(hass)
result = await hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_USER})
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_WEATHER_ENTITY: "weather.ghost"}
)
assert result2["type"] == FlowResultType.FORM
assert result2["errors"] == {"base": "no_weather_entity"}
async def test_flow_error_no_forecast(hass: HomeAssistant) -> None:
_set_weather(hass, features=WeatherEntityFeature.FORECAST_TWICE_DAILY)
result = await hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_USER})
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_WEATHER_ENTITY: ENTITY}
)
assert result2["errors"] == {"base": "no_forecast"}
async def test_flow_error_forecast_too_short(hass: HomeAssistant) -> None:
_set_weather(hass)
# forecast only for a far-away day -> does not reach the target date
far = [{"datetime": (dt_util.now() + timedelta(days=20)).replace(hour=12), "temperature": 10.0}]
_register_forecast(hass, entries=far)
result = await hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_USER})
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_WEATHER_ENTITY: ENTITY}
)
assert result2["errors"] == {"base": "forecast_too_short"}
async def test_flow_error_temporary_when_unavailable(hass: HomeAssistant) -> None:
_set_weather(hass, state="unavailable")
_register_forecast(hass)
result = await hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_USER})
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_WEATHER_ENTITY: ENTITY}
)
assert result2["errors"] == {"base": "temporary"}
async def test_flow_error_temporary_on_service_error(hass: HomeAssistant) -> None:
# any fetch failure (not just HomeAssistantError) maps to temporary (F1).
_set_weather(hass)
_register_forecast(hass, raises=True)
result = await hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_USER})
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_WEATHER_ENTITY: ENTITY}
)
assert result2["errors"] == {"base": "temporary"}
async def test_setup_unload_setup_no_leak(hass: HomeAssistant) -> None:
_set_weather(hass)
_register_forecast(hass)
entry = MockConfigEntry(domain=DOMAIN, data={CONF_WEATHER_ENTITY: ENTITY}, options={})
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.entry_id in hass.data[DOMAIN]
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert entry.entry_id not in hass.data.get(DOMAIN, {})
# setting up again must not raise (listeners were cancelled leak-free)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.entry_id in hass.data[DOMAIN]
async def test_single_config_entry(hass: HomeAssistant) -> None:
_set_weather(hass)
_register_forecast(hass)
entry = MockConfigEntry(domain=DOMAIN, data={CONF_WEATHER_ENTITY: ENTITY})
entry.add_to_hass(hass)
# a second user flow must abort (single_config_entry manifest flag)
result = await hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_USER})
assert result["type"] == FlowResultType.ABORT