From 9f1c5d70bce107832bcf4bbc7452d054b134862b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 14 Dec 2022 12:29:07 -0700 Subject: [PATCH] Add re-auth flow to PurpleAir (#83445) * Add re-auth flow to PurpleAir * Code review * Code review * Code review --- .../components/purpleair/config_flow.py | 45 ++++++++++++++++--- .../components/purpleair/coordinator.py | 5 ++- .../components/purpleair/strings.json | 11 ++++- .../components/purpleair/translations/en.json | 11 ++++- .../components/purpleair/test_config_flow.py | 43 +++++++++++++++++- 5 files changed, 106 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/purpleair/config_flow.py b/homeassistant/components/purpleair/config_flow.py index fa0025f0ee8..bf84e1126dd 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -1,6 +1,7 @@ """Config flow for PurpleAir integration.""" from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass, field from typing import Any @@ -160,7 +161,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_LONGITUDE], user_input[CONF_DISTANCE], ) - if validation.errors: return self.async_show_form( step_id="by_coordinates", @@ -197,6 +197,41 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): options={CONF_SENSOR_INDICES: [int(user_input[CONF_SENSOR_INDEX])]}, ) + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the re-auth step.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", data_schema=API_KEY_SCHEMA + ) + + api_key = user_input[CONF_API_KEY] + + validation = await async_validate_api_key(self.hass, api_key) + if validation.errors: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=API_KEY_SCHEMA, + errors=validation.errors, + ) + + reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + assert reauth_entry + self.hass.config_entries.async_update_entry( + reauth_entry, data={CONF_API_KEY: api_key} + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -206,14 +241,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): api_key = user_input[CONF_API_KEY] - await self.async_set_unique_id(api_key) - self._abort_if_unique_id_configured() + self._async_abort_entries_match({CONF_API_KEY: api_key}) validation = await async_validate_api_key(self.hass, api_key) - if validation.errors: return self.async_show_form( - step_id="user", data_schema=API_KEY_SCHEMA, errors=validation.errors + step_id="user", + data_schema=API_KEY_SCHEMA, + errors=validation.errors, ) self._flow_data = {CONF_API_KEY: api_key} diff --git a/homeassistant/components/purpleair/coordinator.py b/homeassistant/components/purpleair/coordinator.py index d0e258a2d9c..9982db667f2 100644 --- a/homeassistant/components/purpleair/coordinator.py +++ b/homeassistant/components/purpleair/coordinator.py @@ -4,12 +4,13 @@ from __future__ import annotations from datetime import timedelta from aiopurpleair import API -from aiopurpleair.errors import PurpleAirError +from aiopurpleair.errors import InvalidApiKeyError, PurpleAirError from aiopurpleair.models.sensors import GetSensorsResponse from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -66,6 +67,8 @@ class PurpleAirDataUpdateCoordinator(DataUpdateCoordinator[GetSensorsResponse]): SENSOR_FIELDS_TO_RETRIEVE, sensor_indices=self._entry.options[CONF_SENSOR_INDICES], ) + except InvalidApiKeyError as err: + raise ConfigEntryAuthFailed("Invalid API key") from err except PurpleAirError as err: raise UpdateFailed(f"Error while fetching data: {err}") from err diff --git a/homeassistant/components/purpleair/strings.json b/homeassistant/components/purpleair/strings.json index 8fb867fa969..7077273ab76 100644 --- a/homeassistant/components/purpleair/strings.json +++ b/homeassistant/components/purpleair/strings.json @@ -23,6 +23,14 @@ "sensor_index": "The sensor to track" } }, + "reauth_confirm": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "api_key": "[%key:component::purpleair::config::step::user::data_description::api_key%]" + } + }, "user": { "data": { "api_key": "[%key:common::config_flow::data::api_key%]" @@ -38,7 +46,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/homeassistant/components/purpleair/translations/en.json b/homeassistant/components/purpleair/translations/en.json index d3e7ea6d63c..2934efa2fc3 100644 --- a/homeassistant/components/purpleair/translations/en.json +++ b/homeassistant/components/purpleair/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "invalid_api_key": "Invalid API key", @@ -31,6 +32,14 @@ }, "description": "Which of the nearby sensors would you like to track?" }, + "reauth_confirm": { + "data": { + "api_key": "API Key" + }, + "data_description": { + "api_key": "Your PurpleAir API key (if you have both read and write keys, use the read key)" + } + }, "user": { "data": { "api_key": "API Key" diff --git a/tests/components/purpleair/test_config_flow.py b/tests/components/purpleair/test_config_flow.py index 8fcd2a7c2bb..deccbb7d822 100644 --- a/tests/components/purpleair/test_config_flow.py +++ b/tests/components/purpleair/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import data_entry_flow from homeassistant.components.purpleair import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER async def test_duplicate_error(hass, config_entry, setup_purpleair): @@ -102,3 +102,44 @@ async def test_create_entry_by_coordinates( assert result["options"] == { "sensor_indices": [123456], } + + +@pytest.mark.parametrize( + "check_api_key_mock,check_api_key_errors", + [ + (AsyncMock(side_effect=Exception), {"base": "unknown"}), + (AsyncMock(side_effect=InvalidApiKeyError), {"base": "invalid_api_key"}), + (AsyncMock(side_effect=PurpleAirError), {"base": "unknown"}), + ], +) +async def test_reauth( + hass, api, check_api_key_errors, check_api_key_mock, config_entry, setup_purpleair +): + """Test re-auth (including errors).""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, + }, + data={"api_key": "abcde12345"}, + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + # Test errors that can arise when checking the API key: + with patch.object(api, "async_check_api_key", check_api_key_mock): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"api_key": "new_api_key"} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == check_api_key_errors + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"api_key": "new_api_key"}, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert len(hass.config_entries.async_entries()) == 1