Add re-auth flow to PurpleAir (#83445)

* Add re-auth flow to PurpleAir

* Code review

* Code review

* Code review
This commit is contained in:
Aaron Bach 2022-12-14 12:29:07 -07:00 committed by GitHub
parent 774ebc760c
commit 9f1c5d70bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 106 additions and 9 deletions

View File

@ -1,6 +1,7 @@
"""Config flow for PurpleAir integration.""" """Config flow for PurpleAir integration."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any from typing import Any
@ -160,7 +161,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
user_input[CONF_LONGITUDE], user_input[CONF_LONGITUDE],
user_input[CONF_DISTANCE], user_input[CONF_DISTANCE],
) )
if validation.errors: if validation.errors:
return self.async_show_form( return self.async_show_form(
step_id="by_coordinates", 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])]}, 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( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
@ -206,14 +241,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
api_key = user_input[CONF_API_KEY] api_key = user_input[CONF_API_KEY]
await self.async_set_unique_id(api_key) self._async_abort_entries_match({CONF_API_KEY: api_key})
self._abort_if_unique_id_configured()
validation = await async_validate_api_key(self.hass, api_key) validation = await async_validate_api_key(self.hass, api_key)
if validation.errors: if validation.errors:
return self.async_show_form( 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} self._flow_data = {CONF_API_KEY: api_key}

View File

@ -4,12 +4,13 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from aiopurpleair import API from aiopurpleair import API
from aiopurpleair.errors import PurpleAirError from aiopurpleair.errors import InvalidApiKeyError, PurpleAirError
from aiopurpleair.models.sensors import GetSensorsResponse from aiopurpleair.models.sensors import GetSensorsResponse
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -66,6 +67,8 @@ class PurpleAirDataUpdateCoordinator(DataUpdateCoordinator[GetSensorsResponse]):
SENSOR_FIELDS_TO_RETRIEVE, SENSOR_FIELDS_TO_RETRIEVE,
sensor_indices=self._entry.options[CONF_SENSOR_INDICES], sensor_indices=self._entry.options[CONF_SENSOR_INDICES],
) )
except InvalidApiKeyError as err:
raise ConfigEntryAuthFailed("Invalid API key") from err
except PurpleAirError as err: except PurpleAirError as err:
raise UpdateFailed(f"Error while fetching data: {err}") from err raise UpdateFailed(f"Error while fetching data: {err}") from err

View File

@ -23,6 +23,14 @@
"sensor_index": "The sensor to track" "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": { "user": {
"data": { "data": {
"api_key": "[%key:common::config_flow::data::api_key%]" "api_key": "[%key:common::config_flow::data::api_key%]"
@ -38,7 +46,8 @@
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
}, },
"abort": { "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%]"
} }
} }
} }

View File

@ -1,7 +1,8 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "Device is already configured" "already_configured": "Device is already configured",
"reauth_successful": "Re-authentication was successful"
}, },
"error": { "error": {
"invalid_api_key": "Invalid API key", "invalid_api_key": "Invalid API key",
@ -31,6 +32,14 @@
}, },
"description": "Which of the nearby sensors would you like to track?" "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": { "user": {
"data": { "data": {
"api_key": "API Key" "api_key": "API Key"

View File

@ -6,7 +6,7 @@ import pytest
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.components.purpleair import DOMAIN 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): async def test_duplicate_error(hass, config_entry, setup_purpleair):
@ -102,3 +102,44 @@ async def test_create_entry_by_coordinates(
assert result["options"] == { assert result["options"] == {
"sensor_indices": [123456], "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