From ee1b0b46ce53b049a293889c4eb0fa15d6fbd11e Mon Sep 17 00:00:00 2001 From: Floris272 <60342568+Floris272@users.noreply.github.com> Date: Sat, 30 Dec 2023 13:53:35 +0100 Subject: [PATCH] Add reauth to Blue Current integration (#106658) * Add reauth to Blue Current integration. * Apply feedback * Fix failing codecov check * Fix patches * Add wrong_account to strings.json --- .../components/blue_current/__init__.py | 7 +- .../components/blue_current/config_flow.py | 28 ++++++- .../components/blue_current/manifest.json | 1 - .../components/blue_current/strings.json | 4 +- .../blue_current/test_config_flow.py | 72 ++++++++++++++++-- tests/components/blue_current/test_init.py | 74 ++++++++++++++----- 6 files changed, 152 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/blue_current/__init__.py b/homeassistant/components/blue_current/__init__.py index 0dfa67f097d..604f251bfeb 100644 --- a/homeassistant/components/blue_current/__init__.py +++ b/homeassistant/components/blue_current/__init__.py @@ -16,7 +16,7 @@ from bluecurrent_api.exceptions import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME, CONF_API_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later @@ -42,9 +42,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: await connector.connect(api_token) - except InvalidApiToken: - LOGGER.error("Invalid Api token") - return False + except InvalidApiToken as err: + raise ConfigEntryAuthFailed("Invalid API token.") from err except BlueCurrentException as err: raise ConfigEntryNotReady from err diff --git a/homeassistant/components/blue_current/config_flow.py b/homeassistant/components/blue_current/config_flow.py index 32a6c177b49..68a30fcdf7f 100644 --- a/homeassistant/components/blue_current/config_flow.py +++ b/homeassistant/components/blue_current/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Blue Current integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from bluecurrent_api import Client @@ -25,6 +26,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the config flow for Blue Current.""" VERSION = 1 + _reauth_entry: config_entries.ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -51,11 +53,31 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" else: - await self.async_set_unique_id(customer_id) - self._abort_if_unique_id_configured() + if not self._reauth_entry: + await self.async_set_unique_id(customer_id) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=email, data=user_input) - return self.async_create_entry(title=email, data=user_input) + if self._reauth_entry.unique_id == customer_id: + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=user_input + ) + await self.hass.config_entries.async_reload( + self._reauth_entry.entry_id + ) + return self.async_abort(reason="reauth_successful") + return self.async_abort( + reason="wrong_account", + description_placeholders={"email": email}, + ) return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle a reauthorization flow request.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_user() diff --git a/homeassistant/components/blue_current/manifest.json b/homeassistant/components/blue_current/manifest.json index bff8a057f08..cadaac30d68 100644 --- a/homeassistant/components/blue_current/manifest.json +++ b/homeassistant/components/blue_current/manifest.json @@ -5,6 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blue_current", "iot_class": "cloud_push", - "issue_tracker": "https://github.com/bluecurrent/ha-bluecurrent/issues", "requirements": ["bluecurrent-api==1.0.6"] } diff --git a/homeassistant/components/blue_current/strings.json b/homeassistant/components/blue_current/strings.json index 10c114e5f1c..293d0cd6ab7 100644 --- a/homeassistant/components/blue_current/strings.json +++ b/homeassistant/components/blue_current/strings.json @@ -18,7 +18,9 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "wrong_account": "Wrong account: Please authenticate with the api key for {email}." } }, "entity": { diff --git a/tests/components/blue_current/test_config_flow.py b/tests/components/blue_current/test_config_flow.py index c510aeada4f..057701235ad 100644 --- a/tests/components/blue_current/test_config_flow.py +++ b/tests/components/blue_current/test_config_flow.py @@ -12,6 +12,9 @@ from homeassistant.components.blue_current.config_flow import ( WebsocketError, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry async def test_form(hass: HomeAssistant) -> None: @@ -30,8 +33,12 @@ async def test_user(hass: HomeAssistant) -> None: ) assert result["errors"] == {} - with patch("bluecurrent_api.Client.validate_api_token", return_value=True), patch( - "bluecurrent_api.Client.get_email", return_value="test@email.com" + with patch( + "homeassistant.components.blue_current.config_flow.Client.validate_api_token", + return_value="1234", + ), patch( + "homeassistant.components.blue_current.config_flow.Client.get_email", + return_value="test@email.com", ), patch( "homeassistant.components.blue_current.async_setup_entry", return_value=True, @@ -59,9 +66,9 @@ async def test_user(hass: HomeAssistant) -> None: ], ) async def test_flow_fails(hass: HomeAssistant, error: Exception, message: str) -> None: - """Test user initialized flow with invalid username.""" + """Test bluecurrent api errors during configuration flow.""" with patch( - "bluecurrent_api.Client.validate_api_token", + "homeassistant.components.blue_current.config_flow.Client.validate_api_token", side_effect=error, ): result = await hass.config_entries.flow.async_init( @@ -71,8 +78,12 @@ async def test_flow_fails(hass: HomeAssistant, error: Exception, message: str) - ) assert result["errors"]["base"] == message - with patch("bluecurrent_api.Client.validate_api_token", return_value=True), patch( - "bluecurrent_api.Client.get_email", return_value="test@email.com" + with patch( + "homeassistant.components.blue_current.config_flow.Client.validate_api_token", + return_value="1234", + ), patch( + "homeassistant.components.blue_current.config_flow.Client.get_email", + return_value="test@email.com", ), patch( "homeassistant.components.blue_current.async_setup_entry", return_value=True, @@ -87,3 +98,52 @@ async def test_flow_fails(hass: HomeAssistant, error: Exception, message: str) - assert result2["title"] == "test@email.com" assert result2["data"] == {"api_token": "123"} + + +@pytest.mark.parametrize( + ("customer_id", "reason", "expected_api_token"), + [ + ("1234", "reauth_successful", "1234567890"), + ("6666", "wrong_account", "123"), + ], +) +async def test_reauth( + hass: HomeAssistant, customer_id: str, reason: str, expected_api_token: str +) -> None: + """Test reauth flow.""" + with patch( + "homeassistant.components.blue_current.config_flow.Client.validate_api_token", + return_value=customer_id, + ), patch( + "homeassistant.components.blue_current.config_flow.Client.get_email", + return_value="test@email.com", + ): + entry = MockConfigEntry( + domain=DOMAIN, + entry_id="uuid", + unique_id="1234", + data={"api_token": "123"}, + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + data={"api_token": "123"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"api_token": "1234567890"}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == reason + assert entry.data == {"api_token": expected_api_token} + + await hass.async_block_till_done() diff --git a/tests/components/blue_current/test_init.py b/tests/components/blue_current/test_init.py index fe40f58077f..14bd055cd45 100644 --- a/tests/components/blue_current/test_init.py +++ b/tests/components/blue_current/test_init.py @@ -4,13 +4,22 @@ from datetime import timedelta from unittest.mock import patch from bluecurrent_api.client import Client -from bluecurrent_api.exceptions import RequestLimitReached, WebsocketError +from bluecurrent_api.exceptions import ( + BlueCurrentException, + InvalidApiToken, + RequestLimitReached, + WebsocketError, +) import pytest from homeassistant.components.blue_current import DOMAIN, Connector, async_setup_entry from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + IntegrationError, +) from . import init_integration @@ -29,12 +38,21 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None: assert hass.data[DOMAIN] == {} -async def test_config_not_ready(hass: HomeAssistant) -> None: - """Tests if ConfigEntryNotReady is raised when connect raises a WebsocketError.""" +@pytest.mark.parametrize( + ("api_error", "config_error"), + [ + (InvalidApiToken, ConfigEntryAuthFailed), + (BlueCurrentException, ConfigEntryNotReady), + ], +) +async def test_config_exceptions( + hass: HomeAssistant, api_error: BlueCurrentException, config_error: IntegrationError +) -> None: + """Tests if the correct config error is raised when connecting to the api fails.""" with patch( - "bluecurrent_api.Client.connect", - side_effect=WebsocketError, - ), pytest.raises(ConfigEntryNotReady): + "homeassistant.components.blue_current.Client.connect", + side_effect=api_error, + ), pytest.raises(config_error): config_entry = MockConfigEntry( domain=DOMAIN, entry_id="uuid", @@ -143,14 +161,15 @@ async def test_start_loop(hass: HomeAssistant) -> None: connector = Connector(hass, config_entry, Client) with patch( - "bluecurrent_api.Client.start_loop", + "homeassistant.components.blue_current.Client.start_loop", side_effect=WebsocketError("unknown command"), ): await connector.start_loop() test_async_call_later.assert_called_with(hass, 1, connector.reconnect) with patch( - "bluecurrent_api.Client.start_loop", side_effect=RequestLimitReached + "homeassistant.components.blue_current.Client.start_loop", + side_effect=RequestLimitReached, ): await connector.start_loop() test_async_call_later.assert_called_with(hass, 1, connector.reconnect) @@ -159,11 +178,7 @@ async def test_start_loop(hass: HomeAssistant) -> None: async def test_reconnect(hass: HomeAssistant) -> None: """Tests reconnect.""" - with patch("bluecurrent_api.Client.connect"), patch( - "bluecurrent_api.Client.connect", side_effect=WebsocketError - ), patch( - "bluecurrent_api.Client.get_next_reset_delta", return_value=timedelta(hours=1) - ), patch( + with patch( "homeassistant.components.blue_current.async_call_later" ) as test_async_call_later: config_entry = MockConfigEntry( @@ -174,12 +189,33 @@ async def test_reconnect(hass: HomeAssistant) -> None: ) connector = Connector(hass, config_entry, Client) - await connector.reconnect() + + with patch( + "homeassistant.components.blue_current.Client.connect", + side_effect=WebsocketError, + ): + await connector.reconnect() test_async_call_later.assert_called_with(hass, 20, connector.reconnect) - with patch("bluecurrent_api.Client.connect", side_effect=RequestLimitReached): + with patch( + "homeassistant.components.blue_current.Client.connect", + side_effect=RequestLimitReached, + ), patch( + "homeassistant.components.blue_current.Client.get_next_reset_delta", + return_value=timedelta(hours=1), + ): await connector.reconnect() - test_async_call_later.assert_called_with( - hass, timedelta(hours=1), connector.reconnect - ) + + test_async_call_later.assert_called_with( + hass, timedelta(hours=1), connector.reconnect + ) + + with patch("homeassistant.components.blue_current.Client.connect"), patch( + "homeassistant.components.blue_current.Connector.start_loop" + ) as test_start_loop, patch( + "homeassistant.components.blue_current.Client.get_charge_points" + ) as test_get_charge_points: + await connector.reconnect() + test_start_loop.assert_called_once() + test_get_charge_points.assert_called_once()