From 3f3ed3a2c5927a5b1230f1333eac80cec2437a06 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Jul 2022 10:52:40 +0200 Subject: [PATCH] Add multi-factor authentication support to Verisure (#75113) Co-authored-by: Paulus Schoutsen --- homeassistant/components/verisure/__init__.py | 14 +- .../components/verisure/config_flow.py | 142 +++++++- .../components/verisure/coordinator.py | 4 +- .../components/verisure/manifest.json | 2 +- .../components/verisure/strings.json | 15 +- .../components/verisure/translations/en.json | 15 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/verisure/test_config_flow.py | 312 +++++++++++++++++- 9 files changed, 479 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 9a64061554d..9ad8db08d59 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -3,9 +3,10 @@ from __future__ import annotations from contextlib import suppress import os +from pathlib import Path from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.const import CONF_EMAIL, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv @@ -28,6 +29,8 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Verisure from a config entry.""" + await hass.async_add_executor_job(migrate_cookie_files, hass, entry) + coordinator = VerisureDataUpdateCoordinator(hass, entry=entry) if not await coordinator.async_login(): @@ -64,3 +67,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: del hass.data[DOMAIN] return True + + +def migrate_cookie_files(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Migrate old cookie file to new location.""" + cookie_file = Path(hass.config.path(STORAGE_DIR, f"verisure_{entry.unique_id}")) + if cookie_file.exists(): + cookie_file.rename( + hass.config.path(STORAGE_DIR, f"verisure_{entry.data[CONF_EMAIL]}") + ) diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index 119a9250736..d53c7c9ed66 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -13,9 +13,10 @@ from verisure import ( import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.const import CONF_CODE, CONF_EMAIL, CONF_PASSWORD from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.storage import STORAGE_DIR from .const import ( CONF_GIID, @@ -53,13 +54,34 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): self.email = user_input[CONF_EMAIL] self.password = user_input[CONF_PASSWORD] self.verisure = Verisure( - username=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD] + username=self.email, + password=self.password, + cookieFileName=self.hass.config.path( + STORAGE_DIR, f"verisure_{user_input[CONF_EMAIL]}" + ), ) + try: await self.hass.async_add_executor_job(self.verisure.login) except VerisureLoginError as ex: - LOGGER.debug("Could not log in to Verisure, %s", ex) - errors["base"] = "invalid_auth" + if "Multifactor authentication enabled" in str(ex): + try: + await self.hass.async_add_executor_job(self.verisure.login_mfa) + except ( + VerisureLoginError, + VerisureError, + VerisureResponseError, + ) as mfa_ex: + LOGGER.debug( + "Unexpected response from Verisure during MFA set up, %s", + mfa_ex, + ) + errors["base"] = "unknown_mfa" + else: + return await self.async_step_mfa() + else: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" except (VerisureError, VerisureResponseError) as ex: LOGGER.debug("Unexpected response from Verisure, %s", ex) errors["base"] = "unknown" @@ -77,6 +99,39 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_mfa( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle multifactor authentication step.""" + errors: dict[str, str] = {} + + if user_input is not None: + try: + await self.hass.async_add_executor_job( + self.verisure.mfa_validate, user_input[CONF_CODE], True + ) + await self.hass.async_add_executor_job(self.verisure.login) + except VerisureLoginError as ex: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" + except (VerisureError, VerisureResponseError) as ex: + LOGGER.debug("Unexpected response from Verisure, %s", ex) + errors["base"] = "unknown" + else: + return await self.async_step_installation() + + return self.async_show_form( + step_id="mfa", + data_schema=vol.Schema( + { + vol.Required(CONF_CODE): vol.All( + vol.Coerce(str), vol.Length(min=6, max=6) + ) + } + ), + errors=errors, + ) + async def async_step_installation( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -123,14 +178,38 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: - verisure = Verisure( - username=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD] + self.email = user_input[CONF_EMAIL] + self.password = user_input[CONF_PASSWORD] + + self.verisure = Verisure( + username=self.email, + password=self.password, + cookieFileName=self.hass.config.path( + STORAGE_DIR, f"verisure-{user_input[CONF_EMAIL]}" + ), ) + try: - await self.hass.async_add_executor_job(verisure.login) + await self.hass.async_add_executor_job(self.verisure.login) except VerisureLoginError as ex: - LOGGER.debug("Could not log in to Verisure, %s", ex) - errors["base"] = "invalid_auth" + if "Multifactor authentication enabled" in str(ex): + try: + await self.hass.async_add_executor_job(self.verisure.login_mfa) + except ( + VerisureLoginError, + VerisureError, + VerisureResponseError, + ) as mfa_ex: + LOGGER.debug( + "Unexpected response from Verisure during MFA set up, %s", + mfa_ex, + ) + errors["base"] = "unknown_mfa" + else: + return await self.async_step_reauth_mfa() + else: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" except (VerisureError, VerisureResponseError) as ex: LOGGER.debug("Unexpected response from Verisure, %s", ex) errors["base"] = "unknown" @@ -160,6 +239,51 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reauth_mfa( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle multifactor authentication step during re-authentication.""" + errors: dict[str, str] = {} + + if user_input is not None: + try: + await self.hass.async_add_executor_job( + self.verisure.mfa_validate, user_input[CONF_CODE], True + ) + await self.hass.async_add_executor_job(self.verisure.login) + except VerisureLoginError as ex: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" + except (VerisureError, VerisureResponseError) as ex: + LOGGER.debug("Unexpected response from Verisure, %s", ex) + errors["base"] = "unknown" + else: + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_EMAIL: self.email, + CONF_PASSWORD: self.password, + }, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_mfa", + data_schema=vol.Schema( + { + vol.Required(CONF_CODE): vol.All( + vol.Coerce(str), + vol.Length(min=6, max=6), + ) + } + ), + errors=errors, + ) + class VerisureOptionsFlowHandler(OptionsFlow): """Handle Verisure options.""" diff --git a/homeassistant/components/verisure/coordinator.py b/homeassistant/components/verisure/coordinator.py index 821e2830339..17cadb9598f 100644 --- a/homeassistant/components/verisure/coordinator.py +++ b/homeassistant/components/verisure/coordinator.py @@ -31,7 +31,9 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): self.verisure = Verisure( username=entry.data[CONF_EMAIL], password=entry.data[CONF_PASSWORD], - cookieFileName=hass.config.path(STORAGE_DIR, f"verisure_{entry.entry_id}"), + cookieFileName=hass.config.path( + STORAGE_DIR, f"verisure_{entry.data[CONF_EMAIL]}" + ), ) super().__init__( diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index c71be7ee4fc..820b8a20f14 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -2,7 +2,7 @@ "domain": "verisure", "name": "Verisure", "documentation": "https://www.home-assistant.io/integrations/verisure", - "requirements": ["vsure==1.7.3"], + "requirements": ["vsure==1.8.1"], "codeowners": ["@frenck"], "config_flow": true, "dhcp": [ diff --git a/homeassistant/components/verisure/strings.json b/homeassistant/components/verisure/strings.json index 5170bff5faa..c8326d73756 100644 --- a/homeassistant/components/verisure/strings.json +++ b/homeassistant/components/verisure/strings.json @@ -8,6 +8,12 @@ "password": "[%key:common::config_flow::data::password%]" } }, + "mfa": { + "data": { + "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you.", + "code": "Verification Code" + } + }, "installation": { "description": "Home Assistant found multiple Verisure installations in your My Pages account. Please, select the installation to add to Home Assistant.", "data": { @@ -20,11 +26,18 @@ "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_mfa": { + "data": { + "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you.", + "code": "Verification Code" + } } }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "unknown_mfa": "Unknown error occurred during MFA set up" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", diff --git a/homeassistant/components/verisure/translations/en.json b/homeassistant/components/verisure/translations/en.json index 57f73c3772b..34193ce0d09 100644 --- a/homeassistant/components/verisure/translations/en.json +++ b/homeassistant/components/verisure/translations/en.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "unknown_mfa": "Unknown error occurred during MFA set up" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant found multiple Verisure installations in your My Pages account. Please, select the installation to add to Home Assistant." }, + "mfa": { + "data": { + "code": "Verification Code", + "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you." + } + }, "reauth_confirm": { "data": { "description": "Re-authenticate with your Verisure My Pages account.", @@ -22,6 +29,12 @@ "password": "Password" } }, + "reauth_mfa": { + "data": { + "code": "Verification Code", + "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you." + } + }, "user": { "data": { "description": "Sign-in with your Verisure My Pages account.", diff --git a/requirements_all.txt b/requirements_all.txt index cf89a5622bd..0ef7f82524e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2402,7 +2402,7 @@ volkszaehler==0.3.2 volvooncall==0.10.0 # homeassistant.components.verisure -vsure==1.7.3 +vsure==1.8.1 # homeassistant.components.vasttrafik vtjp==0.1.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 009ccb04343..926e89c8f2a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1602,7 +1602,7 @@ venstarcolortouch==0.17 vilfo-api-client==0.3.2 # homeassistant.components.verisure -vsure==1.7.3 +vsure==1.8.1 # homeassistant.components.vulcan vulcan-api==2.1.1 diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index d957709c878..43adc91c38c 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -106,6 +106,129 @@ async def test_full_user_flow_multiple_installations( assert len(mock_setup_entry.mock_calls) == 1 +async def test_full_user_flow_single_installation_with_mfa( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, +) -> None: + """Test a full user initiated flow with a single installation and mfa.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("step_id") == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result + + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "mfa" + assert "flow_id" in result2 + + mock_verisure_config_flow.login.side_effect = None + mock_verisure_config_flow.installations = [ + mock_verisure_config_flow.installations[0] + ] + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "code": "123456", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "ascending (12345th street)" + assert result3.get("data") == { + CONF_GIID: "12345", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "SuperS3cr3t!", + } + + assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 1 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_full_user_flow_multiple_installations_with_mfa( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, +) -> None: + """Test a full user initiated configuration flow with a single installation.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("step_id") == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result + + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "mfa" + assert "flow_id" in result2 + + mock_verisure_config_flow.login.side_effect = None + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "code": "123456", + }, + ) + await hass.async_block_till_done() + + assert result3.get("step_id") == "installation" + assert result3.get("type") == FlowResultType.FORM + assert result3.get("errors") is None + assert "flow_id" in result2 + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], {"giid": "54321"} + ) + await hass.async_block_till_done() + + assert result4.get("type") == FlowResultType.CREATE_ENTRY + assert result4.get("title") == "descending (54321th street)" + assert result4.get("data") == { + CONF_GIID: "54321", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "SuperS3cr3t!", + } + + assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 1 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + @pytest.mark.parametrize( "side_effect,error", [ @@ -142,10 +265,10 @@ async def test_verisure_errors( assert result2.get("errors") == {"base": error} assert "flow_id" in result2 - mock_verisure_config_flow.login.side_effect = None - mock_verisure_config_flow.installations = [ - mock_verisure_config_flow.installations[0] - ] + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + mock_verisure_config_flow.login_mfa.side_effect = side_effect result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -156,15 +279,65 @@ async def test_verisure_errors( ) await hass.async_block_till_done() - assert result3.get("type") == FlowResultType.CREATE_ENTRY - assert result3.get("title") == "ascending (12345th street)" - assert result3.get("data") == { + mock_verisure_config_flow.login_mfa.side_effect = None + + assert result3.get("type") == FlowResultType.FORM + assert result3.get("step_id") == "user" + assert result3.get("errors") == {"base": "unknown_mfa"} + assert "flow_id" in result3 + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result4.get("type") == FlowResultType.FORM + assert result4.get("step_id") == "mfa" + assert "flow_id" in result4 + + mock_verisure_config_flow.mfa_validate.side_effect = side_effect + + result5 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + { + "code": "123456", + }, + ) + assert result5.get("type") == FlowResultType.FORM + assert result5.get("step_id") == "mfa" + assert result5.get("errors") == {"base": error} + assert "flow_id" in result5 + + mock_verisure_config_flow.installations = [ + mock_verisure_config_flow.installations[0] + ] + + mock_verisure_config_flow.mfa_validate.side_effect = None + mock_verisure_config_flow.login.side_effect = None + + result6 = await hass.config_entries.flow.async_configure( + result5["flow_id"], + { + "code": "654321", + }, + ) + await hass.async_block_till_done() + + assert result6.get("type") == FlowResultType.CREATE_ENTRY + assert result6.get("title") == "ascending (12345th street)" + assert result6.get("data") == { CONF_GIID: "12345", CONF_EMAIL: "verisure_my_pages@example.com", CONF_PASSWORD: "SuperS3cr3t!", } - assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login.mock_calls) == 4 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 2 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 2 assert len(mock_setup_entry.mock_calls) == 1 @@ -226,6 +399,70 @@ async def test_reauth_flow( assert len(mock_setup_entry.mock_calls) == 1 +async def test_reauth_flow_with_mfa( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test a reauthentication flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, + }, + data=mock_config_entry.data, + ) + assert result.get("step_id") == "reauth_confirm" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result + + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "correct horse battery staple!", + }, + ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "reauth_mfa" + assert "flow_id" in result2 + + mock_verisure_config_flow.login.side_effect = None + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "code": "123456", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == FlowResultType.ABORT + assert result3.get("reason") == "reauth_successful" + assert mock_config_entry.data == { + CONF_GIID: "12345", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "correct horse battery staple!", + } + + assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 1 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + @pytest.mark.parametrize( "side_effect,error", [ @@ -271,16 +508,63 @@ async def test_reauth_flow_errors( assert result2.get("errors") == {"base": error} assert "flow_id" in result2 + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + mock_verisure_config_flow.login_mfa.side_effect = side_effect + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == FlowResultType.FORM + assert result3.get("step_id") == "reauth_confirm" + assert result3.get("errors") == {"base": "unknown_mfa"} + assert "flow_id" in result3 + + mock_verisure_config_flow.login_mfa.side_effect = None + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result4.get("type") == FlowResultType.FORM + assert result4.get("step_id") == "reauth_mfa" + assert "flow_id" in result4 + + mock_verisure_config_flow.mfa_validate.side_effect = side_effect + + result5 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + { + "code": "123456", + }, + ) + assert result5.get("type") == FlowResultType.FORM + assert result5.get("step_id") == "reauth_mfa" + assert result5.get("errors") == {"base": error} + assert "flow_id" in result5 + + mock_verisure_config_flow.mfa_validate.side_effect = None mock_verisure_config_flow.login.side_effect = None mock_verisure_config_flow.installations = [ mock_verisure_config_flow.installations[0] ] await hass.config_entries.flow.async_configure( - result2["flow_id"], + result5["flow_id"], { - "email": "verisure_my_pages@example.com", - "password": "correct horse battery staple", + "code": "654321", }, ) await hass.async_block_till_done() @@ -288,10 +572,12 @@ async def test_reauth_flow_errors( assert mock_config_entry.data == { CONF_GIID: "12345", CONF_EMAIL: "verisure_my_pages@example.com", - CONF_PASSWORD: "correct horse battery staple", + CONF_PASSWORD: "SuperS3cr3t!", } - assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login.mock_calls) == 4 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 2 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 2 assert len(mock_setup_entry.mock_calls) == 1