mirror of
https://github.com/home-assistant/core.git
synced 2026-05-08 17:49:37 +01:00
Add reauthentication and reconfiguration flows in Google Weather to reach platinum (#166106)
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -9,6 +10,9 @@ from google_weather_api import GoogleWeatherApi, GoogleWeatherApiError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_REAUTH,
|
||||
SOURCE_RECONFIGURE,
|
||||
SOURCE_USER,
|
||||
ConfigEntry,
|
||||
ConfigEntryState,
|
||||
ConfigFlow,
|
||||
@@ -81,11 +85,16 @@ def _get_location_schema(hass: HomeAssistant) -> vol.Schema:
|
||||
|
||||
|
||||
def _is_location_already_configured(
|
||||
hass: HomeAssistant, new_data: dict[str, float], epsilon: float = 1e-4
|
||||
hass: HomeAssistant,
|
||||
new_data: dict[str, float],
|
||||
epsilon: float = 1e-4,
|
||||
exclude_subentry_id: str | None = None,
|
||||
) -> bool:
|
||||
"""Check if the location is already configured."""
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
for subentry in entry.subentries.values():
|
||||
if exclude_subentry_id and subentry.subentry_id == exclude_subentry_id:
|
||||
continue
|
||||
# A more accurate way is to use the haversine formula, but for simplicity
|
||||
# we use a simple distance check. The epsilon value is small anyway.
|
||||
# This is mostly to capture cases where the user has slightly moved the location pin.
|
||||
@@ -106,7 +115,7 @@ class GoogleWeatherConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
"""Handle a flow initialized by the user, reauth or reconfigure."""
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders: dict[str, str] = {
|
||||
"api_key_url": "https://developers.google.com/maps/documentation/weather/get-api-key",
|
||||
@@ -116,21 +125,45 @@ class GoogleWeatherConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
api_key = user_input[CONF_API_KEY]
|
||||
referrer = user_input.get(SECTION_API_KEY_OPTIONS, {}).get(CONF_REFERRER)
|
||||
self._async_abort_entries_match({CONF_API_KEY: api_key})
|
||||
if _is_location_already_configured(self.hass, user_input[CONF_LOCATION]):
|
||||
return self.async_abort(reason="already_configured")
|
||||
if self.source in (SOURCE_REAUTH, SOURCE_RECONFIGURE):
|
||||
entry = (
|
||||
self._get_reauth_entry()
|
||||
if self.source == SOURCE_REAUTH
|
||||
else self._get_reconfigure_entry()
|
||||
)
|
||||
subentry = next(iter(entry.subentries.values()), None)
|
||||
if subentry:
|
||||
latitude = subentry.data[CONF_LATITUDE]
|
||||
longitude = subentry.data[CONF_LONGITUDE]
|
||||
else:
|
||||
latitude = self.hass.config.latitude
|
||||
longitude = self.hass.config.longitude
|
||||
validation_input = {
|
||||
CONF_LOCATION: {CONF_LATITUDE: latitude, CONF_LONGITUDE: longitude}
|
||||
}
|
||||
else:
|
||||
if _is_location_already_configured(
|
||||
self.hass, user_input[CONF_LOCATION]
|
||||
):
|
||||
return self.async_abort(reason="already_configured")
|
||||
validation_input = user_input
|
||||
|
||||
api = GoogleWeatherApi(
|
||||
session=async_get_clientsession(self.hass),
|
||||
api_key=api_key,
|
||||
referrer=referrer,
|
||||
language_code=self.hass.config.language,
|
||||
)
|
||||
if await _validate_input(user_input, api, errors, description_placeholders):
|
||||
if await _validate_input(
|
||||
validation_input, api, errors, description_placeholders
|
||||
):
|
||||
data = {CONF_API_KEY: api_key, CONF_REFERRER: referrer}
|
||||
if self.source in (SOURCE_REAUTH, SOURCE_RECONFIGURE):
|
||||
return self.async_update_reload_and_abort(entry, data=data)
|
||||
|
||||
return self.async_create_entry(
|
||||
title="Google Weather",
|
||||
data={
|
||||
CONF_API_KEY: api_key,
|
||||
CONF_REFERRER: referrer,
|
||||
},
|
||||
data=data,
|
||||
subentries=[
|
||||
{
|
||||
"subentry_type": "location",
|
||||
@@ -140,19 +173,47 @@ class GoogleWeatherConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
if self.source in (SOURCE_REAUTH, SOURCE_RECONFIGURE):
|
||||
entry = (
|
||||
self._get_reauth_entry()
|
||||
if self.source == SOURCE_REAUTH
|
||||
else self._get_reconfigure_entry()
|
||||
)
|
||||
if user_input is None:
|
||||
user_input = {
|
||||
CONF_API_KEY: entry.data.get(CONF_API_KEY),
|
||||
SECTION_API_KEY_OPTIONS: {
|
||||
CONF_REFERRER: entry.data.get(CONF_REFERRER)
|
||||
},
|
||||
}
|
||||
schema = STEP_USER_DATA_SCHEMA
|
||||
else:
|
||||
user_input = {}
|
||||
schema = STEP_USER_DATA_SCHEMA.schema.copy()
|
||||
schema.update(_get_location_schema(self.hass).schema)
|
||||
if user_input is None:
|
||||
user_input = {}
|
||||
schema_dict = STEP_USER_DATA_SCHEMA.schema.copy()
|
||||
schema_dict.update(_get_location_schema(self.hass).schema)
|
||||
schema = vol.Schema(schema_dict)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(schema), user_input
|
||||
),
|
||||
data_schema=self.add_suggested_values_to_schema(schema, user_input),
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth flow."""
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a reconfiguration flow."""
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
@classmethod
|
||||
@callback
|
||||
def async_get_supported_subentry_types(
|
||||
@@ -165,6 +226,11 @@ class GoogleWeatherConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class LocationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
"""Handle a subentry flow for location."""
|
||||
|
||||
@property
|
||||
def _is_new(self) -> bool:
|
||||
"""Return if this is a new subentry."""
|
||||
return self.source == SOURCE_USER
|
||||
|
||||
async def async_step_location(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
@@ -176,16 +242,35 @@ class LocationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
if _is_location_already_configured(self.hass, user_input[CONF_LOCATION]):
|
||||
exclude_id = (
|
||||
None if self._is_new else self._get_reconfigure_subentry().subentry_id
|
||||
)
|
||||
if _is_location_already_configured(
|
||||
self.hass, user_input[CONF_LOCATION], exclude_subentry_id=exclude_id
|
||||
):
|
||||
return self.async_abort(reason="already_configured")
|
||||
api: GoogleWeatherApi = self._get_entry().runtime_data.api
|
||||
if await _validate_input(user_input, api, errors, description_placeholders):
|
||||
return self.async_create_entry(
|
||||
if self._is_new:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_NAME],
|
||||
data=user_input[CONF_LOCATION],
|
||||
)
|
||||
return self.async_update_and_abort(
|
||||
self._get_entry(),
|
||||
self._get_reconfigure_subentry(),
|
||||
title=user_input[CONF_NAME],
|
||||
data=user_input[CONF_LOCATION],
|
||||
)
|
||||
else:
|
||||
elif self._is_new:
|
||||
user_input = {}
|
||||
else:
|
||||
subentry = self._get_reconfigure_subentry()
|
||||
user_input = {
|
||||
CONF_NAME: subentry.title,
|
||||
CONF_LOCATION: dict(subentry.data),
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="location",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
@@ -196,3 +281,4 @@ class LocationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
)
|
||||
|
||||
async_step_user = async_step_location
|
||||
async_step_reconfigure = async_step_location
|
||||
|
||||
@@ -12,6 +12,7 @@ from google_weather_api import (
|
||||
CurrentConditionsResponse,
|
||||
DailyForecastResponse,
|
||||
GoogleWeatherApi,
|
||||
GoogleWeatherApiAuthError,
|
||||
GoogleWeatherApiError,
|
||||
HourlyForecastResponse,
|
||||
)
|
||||
@@ -19,6 +20,7 @@ from google_weather_api import (
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
TimestampDataUpdateCoordinator,
|
||||
UpdateFailed,
|
||||
@@ -92,6 +94,14 @@ class GoogleWeatherBaseCoordinator(TimestampDataUpdateCoordinator[T]):
|
||||
self.subentry.data[CONF_LATITUDE],
|
||||
self.subentry.data[CONF_LONGITUDE],
|
||||
)
|
||||
except GoogleWeatherApiAuthError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_error",
|
||||
translation_placeholders={
|
||||
"error": str(err),
|
||||
},
|
||||
) from err
|
||||
except GoogleWeatherApiError as err:
|
||||
_LOGGER.error(
|
||||
"Error fetching %s for %s: %s",
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["google_weather_api"],
|
||||
"quality_scale": "bronze",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-google-weather-api==0.0.6"]
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ rules:
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
@@ -68,7 +68,7 @@ rules:
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
reconfiguration-flow: done
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: No repairs.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Unable to connect to the Google Weather API:\n\n{error_message}",
|
||||
@@ -38,7 +40,8 @@
|
||||
"location": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
|
||||
"entry_not_loaded": "Cannot add things while the configuration is disabled."
|
||||
"entry_not_loaded": "Cannot add things while the configuration is disabled.",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"entry_type": "Location",
|
||||
"error": {
|
||||
@@ -46,6 +49,7 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"initiate_flow": {
|
||||
"reconfigure": "Reconfigure location",
|
||||
"user": "Add location"
|
||||
},
|
||||
"step": {
|
||||
@@ -100,6 +104,9 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"auth_error": {
|
||||
"message": "Authentication failed: {error}"
|
||||
},
|
||||
"update_error": {
|
||||
"message": "Error fetching weather data: {error}"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from google_weather_api import GoogleWeatherApiError
|
||||
from google_weather_api import GoogleWeatherApiAuthError, GoogleWeatherApiError
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
@@ -359,6 +359,294 @@ async def test_subentry_flow_location_already_configured(
|
||||
assert len(entry.subentries) == 1
|
||||
|
||||
|
||||
async def test_reauth(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauth flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_API_KEY: "new-api-key",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert mock_config_entry.data[CONF_API_KEY] == "new-api-key"
|
||||
assert mock_config_entry.data.get(CONF_REFERRER) is None
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("api_exception", "expected_error", "expected_placeholders"),
|
||||
[
|
||||
(
|
||||
GoogleWeatherApiAuthError("Invalid API key"),
|
||||
"cannot_connect",
|
||||
{
|
||||
"api_key_url": "https://developers.google.com/maps/documentation/weather/get-api-key",
|
||||
"restricting_api_keys_url": "https://developers.google.com/maps/api-security-best-practices#restricting-api-keys",
|
||||
"error_message": "Invalid API key",
|
||||
"name": "Google Weather",
|
||||
},
|
||||
),
|
||||
(
|
||||
ValueError(),
|
||||
"unknown",
|
||||
{
|
||||
"api_key_url": "https://developers.google.com/maps/documentation/weather/get-api-key",
|
||||
"restricting_api_keys_url": "https://developers.google.com/maps/api-security-best-practices#restricting-api-keys",
|
||||
"name": "Google Weather",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_reauth_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
api_exception: Exception,
|
||||
expected_error: str,
|
||||
expected_placeholders: dict[str, str],
|
||||
) -> None:
|
||||
"""Test reauth flow with exceptions."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
mock_google_weather_api.async_get_current_conditions.side_effect = api_exception
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_API_KEY: "invalid-api-key",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": expected_error}
|
||||
assert result["description_placeholders"] == expected_placeholders
|
||||
|
||||
mock_google_weather_api.async_get_current_conditions.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_API_KEY: "valid-api-key",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
||||
|
||||
async def test_reauth_same_api_key_different_referrer(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauth flow with same API key but different referrer."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_API_KEY: "test-api-key",
|
||||
SECTION_API_KEY_OPTIONS: {
|
||||
CONF_REFERRER: "new-referrer",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert mock_config_entry.data[CONF_API_KEY] == "test-api-key"
|
||||
assert mock_config_entry.data.get(CONF_REFERRER) == "new-referrer"
|
||||
|
||||
|
||||
async def test_reconfigure(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reconfigure flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_API_KEY: "new-api-key",
|
||||
SECTION_API_KEY_OPTIONS: {
|
||||
CONF_REFERRER: "new-referrer",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert mock_config_entry.data[CONF_API_KEY] == "new-api-key"
|
||||
assert mock_config_entry.data.get(CONF_REFERRER) == "new-referrer"
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("api_exception", "expected_error", "expected_placeholders"),
|
||||
[
|
||||
(
|
||||
GoogleWeatherApiAuthError("Invalid API key"),
|
||||
"cannot_connect",
|
||||
{
|
||||
"api_key_url": "https://developers.google.com/maps/documentation/weather/get-api-key",
|
||||
"restricting_api_keys_url": "https://developers.google.com/maps/api-security-best-practices#restricting-api-keys",
|
||||
"error_message": "Invalid API key",
|
||||
},
|
||||
),
|
||||
(
|
||||
ValueError(),
|
||||
"unknown",
|
||||
{
|
||||
"api_key_url": "https://developers.google.com/maps/documentation/weather/get-api-key",
|
||||
"restricting_api_keys_url": "https://developers.google.com/maps/api-security-best-practices#restricting-api-keys",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_reconfigure_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
api_exception: Exception,
|
||||
expected_error: str,
|
||||
expected_placeholders: dict[str, str],
|
||||
) -> None:
|
||||
"""Test reconfigure flow with exceptions."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
mock_google_weather_api.async_get_current_conditions.side_effect = api_exception
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_API_KEY: "invalid-api-key",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": expected_error}
|
||||
assert result["description_placeholders"] == expected_placeholders
|
||||
|
||||
mock_google_weather_api.async_get_current_conditions.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_API_KEY: "valid-api-key",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_reconfigure_no_subentries(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reconfigure flow when there are no subentries."""
|
||||
mock_config_entry = MockConfigEntry(
|
||||
title="Google Weather",
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_API_KEY: "test-api-key",
|
||||
},
|
||||
)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_API_KEY: "new-api-key",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert mock_config_entry.data[CONF_API_KEY] == "new-api-key"
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_subentry_reconfigure(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reconfiguring a location subentry."""
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
subentry = next(iter(mock_config_entry.subentries.values()))
|
||||
result = await mock_config_entry.start_subentry_reconfigure_flow(
|
||||
hass, subentry.subentry_id
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "location"
|
||||
|
||||
result2 = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: "New Work",
|
||||
CONF_LOCATION: {
|
||||
CONF_LATITUDE: 30.1,
|
||||
CONF_LONGITUDE: 40.1,
|
||||
},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "reconfigure_successful"
|
||||
|
||||
# Reload the entry to see changes
|
||||
entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
||||
updated_subentry = entry.subentries[subentry.subentry_id]
|
||||
|
||||
assert updated_subentry.title == "New Work"
|
||||
assert updated_subentry.data == {
|
||||
CONF_LATITUDE: 30.1,
|
||||
CONF_LONGITUDE: 40.1,
|
||||
}
|
||||
|
||||
|
||||
async def test_subentry_flow_entry_not_loaded(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from google_weather_api import GoogleWeatherApiError
|
||||
from google_weather_api import GoogleWeatherApiAuthError, GoogleWeatherApiError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.google_weather.const import DOMAIN
|
||||
@@ -56,6 +56,25 @@ async def test_config_not_ready(
|
||||
assert "Error fetching weather data: API error" in caplog.text
|
||||
|
||||
|
||||
async def test_setup_auth_failed(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test auth failed during setup."""
|
||||
mock_google_weather_api.async_get_current_conditions.side_effect = (
|
||||
GoogleWeatherApiAuthError()
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
assert any(
|
||||
flow["step_id"] == "user"
|
||||
for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
||||
)
|
||||
|
||||
|
||||
async def test_unload_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
|
||||
@@ -4,14 +4,16 @@ from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from google_weather_api import GoogleWeatherApiError
|
||||
from google_weather_api import GoogleWeatherApiAuthError, GoogleWeatherApiError
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.google_weather.const import DOMAIN
|
||||
from homeassistant.components.homeassistant import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
@@ -170,3 +172,29 @@ async def test_state_update(
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "15.0"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_auth_failure_during_update(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Ensure that we start a reauth flow when auth fails during an update."""
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_google_weather_api.async_get_current_conditions.side_effect = (
|
||||
GoogleWeatherApiAuthError()
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(minutes=15))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert any(
|
||||
flow["step_id"] == "user"
|
||||
for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user