diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 1c55d932425357..6c279b6b32c346 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -7,6 +7,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -46,8 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b login = await hass.async_add_executor_job(manager.login) if not login: - _LOGGER.error("Unable to login to the VeSync server") - return False + raise ConfigEntryAuthFailed hass.data[DOMAIN] = {} hass.data[DOMAIN][VS_MANAGER] = manager diff --git a/homeassistant/components/vesync/config_flow.py b/homeassistant/components/vesync/config_flow.py index 07543440e9163e..b37e2416c6f344 100644 --- a/homeassistant/components/vesync/config_flow.py +++ b/homeassistant/components/vesync/config_flow.py @@ -1,5 +1,6 @@ """Config flow utilities.""" +from collections.abc import Mapping from typing import Any from pyvesync import VeSync @@ -57,3 +58,35 @@ async def async_step_user( title=username, data={CONF_USERNAME: username, CONF_PASSWORD: password}, ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle re-authentication with vesync.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm re-authentication with vesync.""" + + if user_input: + reauth_entry = self._get_reauth_entry() + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + data = { + **reauth_entry.data, + CONF_USERNAME: username, + CONF_PASSWORD: password, + } + + manager = VeSync(username, password) + login = await self.hass.async_add_executor_job(manager.login) + if login: + return self.async_update_reload_and_abort(reauth_entry, data=data) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=DATA_SCHEMA, + description_placeholders={"name": "VeSync"}, + ) diff --git a/homeassistant/components/vesync/strings.json b/homeassistant/components/vesync/strings.json index 3eb2a0c3fd584d..bf7f30e694220e 100644 --- a/homeassistant/components/vesync/strings.json +++ b/homeassistant/components/vesync/strings.json @@ -7,13 +7,22 @@ "username": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The vesync integration needs to re-authenticate your account", + "data": { + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "entity": { diff --git a/tests/components/vesync/test_config_flow.py b/tests/components/vesync/test_config_flow.py index 22a93e1ba56142..d1b79b9ec873a8 100644 --- a/tests/components/vesync/test_config_flow.py +++ b/tests/components/vesync/test_config_flow.py @@ -48,3 +48,51 @@ async def test_config_flow_user_input(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == "user" assert result["data"][CONF_PASSWORD] == "pass" + + +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test a successful reauth flow.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + + result = await mock_entry.start_reauth_flow(hass) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] is FlowResultType.FORM + with patch("pyvesync.vesync.VeSync.login", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password"}, + ) + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert mock_entry.data == { + CONF_USERNAME: "new-username", + CONF_PASSWORD: "new-password", + } + + +async def test_reauth_flow_invalid_auth(hass: HomeAssistant) -> None: + """Test an authorization error reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + + result = await mock_entry.start_reauth_flow(hass) + assert result["step_id"] == "reauth_confirm" + assert result["type"] is FlowResultType.FORM + + with patch("pyvesync.vesync.VeSync.login", return_value=False): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password"}, + ) + + assert result2["type"] is FlowResultType.FORM diff --git a/tests/components/vesync/test_init.py b/tests/components/vesync/test_init.py index f1fb3931bf975a..ba9f2086d0c2a2 100644 --- a/tests/components/vesync/test_init.py +++ b/tests/components/vesync/test_init.py @@ -2,7 +2,6 @@ from unittest.mock import Mock, patch -import pytest from pyvesync import VeSync from homeassistant.components.vesync import SERVICE_UPDATE_DEVS, async_setup_entry @@ -19,25 +18,17 @@ async def test_async_setup_entry__not_login( hass: HomeAssistant, config_entry: ConfigEntry, manager: VeSync, - caplog: pytest.LogCaptureFixture, ) -> None: """Test setup does not create config entry when not logged in.""" manager.login = Mock(return_value=False) - with ( - patch.object(hass.config_entries, "async_forward_entry_setups") as setups_mock, - patch( - "homeassistant.components.vesync.async_generate_device_list" - ) as process_mock, - ): - assert not await async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - assert setups_mock.call_count == 0 - assert process_mock.call_count == 0 + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() assert manager.login.call_count == 1 - assert DOMAIN not in hass.data - assert "Unable to login to the VeSync server" in caplog.text + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.SETUP_ERROR + assert not hass.data.get(DOMAIN) async def test_async_setup_entry__no_devices(