mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Add 2FA support for Subaru integration setup (#68753)
* Add 2FA support for Subaru integration setup * Update config flow to abort with 2FA request fail
This commit is contained in:
parent
bb7593351b
commit
ab0abdc988
@ -19,6 +19,8 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv
|
|||||||
from .const import CONF_COUNTRY, CONF_UPDATE_ENABLED, DOMAIN
|
from .const import CONF_COUNTRY, CONF_UPDATE_ENABLED, DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
CONF_CONTACT_METHOD = "contact_method"
|
||||||
|
CONF_VALIDATION_CODE = "validation_code"
|
||||||
PIN_SCHEMA = vol.Schema({vol.Required(CONF_PIN): str})
|
PIN_SCHEMA = vol.Schema({vol.Required(CONF_PIN): str})
|
||||||
|
|
||||||
|
|
||||||
@ -47,6 +49,9 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
_LOGGER.error("Unable to communicate with Subaru API: %s", ex.message)
|
_LOGGER.error("Unable to communicate with Subaru API: %s", ex.message)
|
||||||
return self.async_abort(reason="cannot_connect")
|
return self.async_abort(reason="cannot_connect")
|
||||||
else:
|
else:
|
||||||
|
if not self.controller.device_registered:
|
||||||
|
_LOGGER.debug("2FA validation is required")
|
||||||
|
return await self.async_step_two_factor()
|
||||||
if self.controller.is_pin_required():
|
if self.controller.is_pin_required():
|
||||||
return await self.async_step_pin()
|
return await self.async_step_pin()
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
@ -103,13 +108,60 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
device_name=device_name,
|
device_name=device_name,
|
||||||
country=data[CONF_COUNTRY],
|
country=data[CONF_COUNTRY],
|
||||||
)
|
)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug("Setting up first time connection to Subaru API")
|
||||||
"Setting up first time connection to Subaru API. This may take up to 20 seconds"
|
|
||||||
)
|
|
||||||
if await self.controller.connect():
|
if await self.controller.connect():
|
||||||
_LOGGER.debug("Successfully authenticated and authorized with Subaru API")
|
_LOGGER.debug("Successfully authenticated with Subaru API")
|
||||||
self.config_data.update(data)
|
self.config_data.update(data)
|
||||||
|
|
||||||
|
async def async_step_two_factor(self, user_input=None):
|
||||||
|
"""Select contact method and request 2FA code from Subaru."""
|
||||||
|
error = None
|
||||||
|
if user_input:
|
||||||
|
# self.controller.contact_methods is a dict:
|
||||||
|
# {"phone":"555-555-5555", "userName":"my@email.com"}
|
||||||
|
selected_method = next(
|
||||||
|
k
|
||||||
|
for k, v in self.controller.contact_methods.items()
|
||||||
|
if v == user_input[CONF_CONTACT_METHOD]
|
||||||
|
)
|
||||||
|
if await self.controller.request_auth_code(selected_method):
|
||||||
|
return await self.async_step_two_factor_validate()
|
||||||
|
return self.async_abort(reason="two_factor_request_failed")
|
||||||
|
|
||||||
|
data_schema = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_CONTACT_METHOD): vol.In(
|
||||||
|
list(self.controller.contact_methods.values())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="two_factor", data_schema=data_schema, errors=error
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_two_factor_validate(self, user_input=None):
|
||||||
|
"""Validate received 2FA code with Subaru."""
|
||||||
|
error = None
|
||||||
|
if user_input:
|
||||||
|
try:
|
||||||
|
vol.Match(r"^[0-9]{6}$")(user_input[CONF_VALIDATION_CODE])
|
||||||
|
if await self.controller.submit_auth_code(
|
||||||
|
user_input[CONF_VALIDATION_CODE]
|
||||||
|
):
|
||||||
|
if self.controller.is_pin_required():
|
||||||
|
return await self.async_step_pin()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self.config_data[CONF_USERNAME], data=self.config_data
|
||||||
|
)
|
||||||
|
error = {"base": "incorrect_validation_code"}
|
||||||
|
except vol.Invalid:
|
||||||
|
error = {"base": "bad_validation_code_format"}
|
||||||
|
|
||||||
|
data_schema = vol.Schema({vol.Required(CONF_VALIDATION_CODE): str})
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="two_factor_validate", data_schema=data_schema, errors=error
|
||||||
|
)
|
||||||
|
|
||||||
async def async_step_pin(self, user_input=None):
|
async def async_step_pin(self, user_input=None):
|
||||||
"""Handle second part of config flow, if required."""
|
"""Handle second part of config flow, if required."""
|
||||||
error = None
|
error = None
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Subaru",
|
"name": "Subaru",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/subaru",
|
"documentation": "https://www.home-assistant.io/integrations/subaru",
|
||||||
"requirements": ["subarulink==0.4.2"],
|
"requirements": ["subarulink==0.5.0"],
|
||||||
"codeowners": ["@G-Two"],
|
"codeowners": ["@G-Two"],
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["stdiomask", "subarulink"]
|
"loggers": ["stdiomask", "subarulink"]
|
||||||
|
@ -10,6 +10,20 @@
|
|||||||
"country": "Select country"
|
"country": "Select country"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"two_factor": {
|
||||||
|
"title": "Subaru Starlink Configuration",
|
||||||
|
"description": "Two factor authentication required",
|
||||||
|
"data": {
|
||||||
|
"contact_method": "Please select a contact method:"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"two_factor_validate": {
|
||||||
|
"title": "Subaru Starlink Configuration",
|
||||||
|
"description": "Please enter validation code received",
|
||||||
|
"data": {
|
||||||
|
"validation_code": "Validation code"
|
||||||
|
}
|
||||||
|
},
|
||||||
"pin": {
|
"pin": {
|
||||||
"title": "Subaru Starlink Configuration",
|
"title": "Subaru Starlink Configuration",
|
||||||
"description": "Please enter your MySubaru PIN\nNOTE: All vehicles in account must have the same PIN",
|
"description": "Please enter your MySubaru PIN\nNOTE: All vehicles in account must have the same PIN",
|
||||||
@ -22,7 +36,10 @@
|
|||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
"incorrect_pin": "Incorrect PIN",
|
"incorrect_pin": "Incorrect PIN",
|
||||||
"bad_pin_format": "PIN should be 4 digits"
|
"bad_pin_format": "PIN should be 4 digits",
|
||||||
|
"two_factor_request_failed": "Request for 2FA code failed, please try again",
|
||||||
|
"bad_validation_code_format": "Validation code should be 6 digits",
|
||||||
|
"incorrect_validation_code": "Incorrect validation code"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||||
|
@ -2224,7 +2224,7 @@ streamlabswater==1.0.1
|
|||||||
stringcase==1.2.0
|
stringcase==1.2.0
|
||||||
|
|
||||||
# homeassistant.components.subaru
|
# homeassistant.components.subaru
|
||||||
subarulink==0.4.2
|
subarulink==0.5.0
|
||||||
|
|
||||||
# homeassistant.components.ecovacs
|
# homeassistant.components.ecovacs
|
||||||
sucks==0.9.4
|
sucks==0.9.4
|
||||||
|
@ -1440,7 +1440,7 @@ stookalert==0.1.4
|
|||||||
stringcase==1.2.0
|
stringcase==1.2.0
|
||||||
|
|
||||||
# homeassistant.components.subaru
|
# homeassistant.components.subaru
|
||||||
subarulink==0.4.2
|
subarulink==0.5.0
|
||||||
|
|
||||||
# homeassistant.components.solarlog
|
# homeassistant.components.solarlog
|
||||||
sunwatcher==0.2.1
|
sunwatcher==0.2.1
|
||||||
|
@ -28,6 +28,10 @@ from .api_responses import TEST_VIN_2_EV, VEHICLE_DATA, VEHICLE_STATUS_EV
|
|||||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
MOCK_API = "homeassistant.components.subaru.SubaruAPI."
|
MOCK_API = "homeassistant.components.subaru.SubaruAPI."
|
||||||
|
MOCK_API_DEVICE_REGISTERED = f"{MOCK_API}device_registered"
|
||||||
|
MOCK_API_2FA_CONTACTS = f"{MOCK_API}contact_methods"
|
||||||
|
MOCK_API_2FA_REQUEST = f"{MOCK_API}request_auth_code"
|
||||||
|
MOCK_API_2FA_VERIFY = f"{MOCK_API}submit_auth_code"
|
||||||
MOCK_API_CONNECT = f"{MOCK_API}connect"
|
MOCK_API_CONNECT = f"{MOCK_API}connect"
|
||||||
MOCK_API_IS_PIN_REQUIRED = f"{MOCK_API}is_pin_required"
|
MOCK_API_IS_PIN_REQUIRED = f"{MOCK_API}is_pin_required"
|
||||||
MOCK_API_TEST_PIN = f"{MOCK_API}test_pin"
|
MOCK_API_TEST_PIN = f"{MOCK_API}test_pin"
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
# pylint: disable=redefined-outer-name
|
# pylint: disable=redefined-outer-name
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from unittest.mock import patch
|
from unittest.mock import PropertyMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from subarulink.exceptions import InvalidCredentials, InvalidPIN, SubaruException
|
from subarulink.exceptions import InvalidCredentials, InvalidPIN, SubaruException
|
||||||
@ -14,7 +14,11 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_PIN
|
|||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from .conftest import (
|
from .conftest import (
|
||||||
|
MOCK_API_2FA_CONTACTS,
|
||||||
|
MOCK_API_2FA_REQUEST,
|
||||||
|
MOCK_API_2FA_VERIFY,
|
||||||
MOCK_API_CONNECT,
|
MOCK_API_CONNECT,
|
||||||
|
MOCK_API_DEVICE_REGISTERED,
|
||||||
MOCK_API_IS_PIN_REQUIRED,
|
MOCK_API_IS_PIN_REQUIRED,
|
||||||
MOCK_API_TEST_PIN,
|
MOCK_API_TEST_PIN,
|
||||||
MOCK_API_UPDATE_SAVED_PIN,
|
MOCK_API_UPDATE_SAVED_PIN,
|
||||||
@ -28,6 +32,10 @@ from .conftest import (
|
|||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
ASYNC_SETUP_ENTRY = "homeassistant.components.subaru.async_setup_entry"
|
ASYNC_SETUP_ENTRY = "homeassistant.components.subaru.async_setup_entry"
|
||||||
|
MOCK_2FA_CONTACTS = {
|
||||||
|
"phone": "123-123-1234",
|
||||||
|
"userName": "email@addr.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_user_form_init(user_form):
|
async def test_user_form_init(user_form):
|
||||||
@ -89,19 +97,22 @@ async def test_user_form_invalid_auth(hass, user_form):
|
|||||||
assert result["errors"] == {"base": "invalid_auth"}
|
assert result["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
async def test_user_form_pin_not_required(hass, user_form):
|
async def test_user_form_pin_not_required(hass, two_factor_verify_form):
|
||||||
"""Test successful login when no PIN is required."""
|
"""Test successful login when no PIN is required."""
|
||||||
with patch(MOCK_API_CONNECT, return_value=True,) as mock_connect, patch(
|
with patch(
|
||||||
|
MOCK_API_2FA_VERIFY,
|
||||||
|
return_value=True,
|
||||||
|
) as mock_two_factor_verify, patch(
|
||||||
MOCK_API_IS_PIN_REQUIRED,
|
MOCK_API_IS_PIN_REQUIRED,
|
||||||
return_value=False,
|
return_value=False,
|
||||||
) as mock_is_pin_required, patch(
|
) as mock_is_pin_required, patch(
|
||||||
ASYNC_SETUP_ENTRY, return_value=True
|
ASYNC_SETUP_ENTRY, return_value=True
|
||||||
) as mock_setup_entry:
|
) as mock_setup_entry:
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
user_form["flow_id"],
|
two_factor_verify_form["flow_id"],
|
||||||
TEST_CREDS,
|
user_input={config_flow.CONF_VALIDATION_CODE: "123456"},
|
||||||
)
|
)
|
||||||
assert len(mock_connect.mock_calls) == 1
|
assert len(mock_two_factor_verify.mock_calls) == 1
|
||||||
assert len(mock_is_pin_required.mock_calls) == 1
|
assert len(mock_is_pin_required.mock_calls) == 1
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
@ -117,11 +128,118 @@ async def test_user_form_pin_not_required(hass, user_form):
|
|||||||
"data": deepcopy(TEST_CONFIG),
|
"data": deepcopy(TEST_CONFIG),
|
||||||
"options": {},
|
"options": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
expected["data"][CONF_PIN] = None
|
expected["data"][CONF_PIN] = None
|
||||||
result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID
|
result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID
|
||||||
assert result == expected
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
async def test_registered_pin_required(hass, user_form):
|
||||||
|
"""Test if the device is already registered and PIN required."""
|
||||||
|
with patch(MOCK_API_CONNECT, return_value=True), patch(
|
||||||
|
MOCK_API_DEVICE_REGISTERED, new_callable=PropertyMock
|
||||||
|
) as mock_device_registered, patch(MOCK_API_IS_PIN_REQUIRED, return_value=True):
|
||||||
|
mock_device_registered.return_value = True
|
||||||
|
await hass.config_entries.flow.async_configure(
|
||||||
|
user_form["flow_id"], user_input=TEST_CREDS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_registered_no_pin_required(hass, user_form):
|
||||||
|
"""Test if the device is already registered and PIN not required."""
|
||||||
|
with patch(MOCK_API_CONNECT, return_value=True), patch(
|
||||||
|
MOCK_API_DEVICE_REGISTERED, new_callable=PropertyMock
|
||||||
|
) as mock_device_registered, patch(MOCK_API_IS_PIN_REQUIRED, return_value=False):
|
||||||
|
mock_device_registered.return_value = True
|
||||||
|
await hass.config_entries.flow.async_configure(
|
||||||
|
user_form["flow_id"], user_input=TEST_CREDS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_two_factor_request_success(hass, two_factor_start_form):
|
||||||
|
"""Test two factor contact method selection."""
|
||||||
|
with patch(
|
||||||
|
MOCK_API_2FA_REQUEST,
|
||||||
|
return_value=True,
|
||||||
|
) as mock_two_factor_request, patch(
|
||||||
|
MOCK_API_2FA_CONTACTS, new_callable=PropertyMock
|
||||||
|
) as mock_contacts:
|
||||||
|
mock_contacts.return_value = MOCK_2FA_CONTACTS
|
||||||
|
await hass.config_entries.flow.async_configure(
|
||||||
|
two_factor_start_form["flow_id"],
|
||||||
|
user_input={config_flow.CONF_CONTACT_METHOD: "email@addr.com"},
|
||||||
|
)
|
||||||
|
assert len(mock_two_factor_request.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_two_factor_request_fail(hass, two_factor_start_form):
|
||||||
|
"""Test two factor auth request failure."""
|
||||||
|
with patch(
|
||||||
|
MOCK_API_2FA_REQUEST,
|
||||||
|
return_value=False,
|
||||||
|
) as mock_two_factor_request, patch(
|
||||||
|
MOCK_API_2FA_CONTACTS, new_callable=PropertyMock
|
||||||
|
) as mock_contacts:
|
||||||
|
mock_contacts.return_value = MOCK_2FA_CONTACTS
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
two_factor_start_form["flow_id"],
|
||||||
|
user_input={config_flow.CONF_CONTACT_METHOD: "email@addr.com"},
|
||||||
|
)
|
||||||
|
assert len(mock_two_factor_request.mock_calls) == 1
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "two_factor_request_failed"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_two_factor_verify_success(hass, two_factor_verify_form):
|
||||||
|
"""Test two factor verification."""
|
||||||
|
with patch(
|
||||||
|
MOCK_API_2FA_VERIFY,
|
||||||
|
return_value=True,
|
||||||
|
) as mock_two_factor_verify, patch(
|
||||||
|
MOCK_API_IS_PIN_REQUIRED, return_value=True
|
||||||
|
) as mock_is_in_required:
|
||||||
|
await hass.config_entries.flow.async_configure(
|
||||||
|
two_factor_verify_form["flow_id"],
|
||||||
|
user_input={config_flow.CONF_VALIDATION_CODE: "123456"},
|
||||||
|
)
|
||||||
|
assert len(mock_two_factor_verify.mock_calls) == 1
|
||||||
|
assert len(mock_is_in_required.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_two_factor_verify_bad_format(hass, two_factor_verify_form):
|
||||||
|
"""Test two factor verification bad format."""
|
||||||
|
with patch(
|
||||||
|
MOCK_API_2FA_VERIFY,
|
||||||
|
return_value=False,
|
||||||
|
) as mock_two_factor_verify, patch(
|
||||||
|
MOCK_API_IS_PIN_REQUIRED, return_value=True
|
||||||
|
) as mock_is_pin_required:
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
two_factor_verify_form["flow_id"],
|
||||||
|
user_input={config_flow.CONF_VALIDATION_CODE: "1234567"},
|
||||||
|
)
|
||||||
|
assert len(mock_two_factor_verify.mock_calls) == 0
|
||||||
|
assert len(mock_is_pin_required.mock_calls) == 0
|
||||||
|
assert result["errors"] == {"base": "bad_validation_code_format"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_two_factor_verify_fail(hass, two_factor_verify_form):
|
||||||
|
"""Test two factor verification failure."""
|
||||||
|
with patch(
|
||||||
|
MOCK_API_2FA_VERIFY,
|
||||||
|
return_value=False,
|
||||||
|
) as mock_two_factor_verify, patch(
|
||||||
|
MOCK_API_IS_PIN_REQUIRED, return_value=True
|
||||||
|
) as mock_is_pin_required:
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
two_factor_verify_form["flow_id"],
|
||||||
|
user_input={config_flow.CONF_VALIDATION_CODE: "123456"},
|
||||||
|
)
|
||||||
|
assert len(mock_two_factor_verify.mock_calls) == 1
|
||||||
|
assert len(mock_is_pin_required.mock_calls) == 0
|
||||||
|
assert result["errors"] == {"base": "incorrect_validation_code"}
|
||||||
|
|
||||||
|
|
||||||
async def test_pin_form_init(pin_form):
|
async def test_pin_form_init(pin_form):
|
||||||
"""Test the pin entry form for second step of the config flow."""
|
"""Test the pin entry form for second step of the config flow."""
|
||||||
expected = {
|
expected = {
|
||||||
@ -232,17 +350,44 @@ async def user_form(hass):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def pin_form(hass, user_form):
|
async def two_factor_start_form(hass, user_form):
|
||||||
"""Return second form (PIN input) for Subaru config flow."""
|
"""Return two factor form for Subaru config flow."""
|
||||||
with patch(MOCK_API_CONNECT, return_value=True,), patch(
|
with patch(MOCK_API_CONNECT, return_value=True), patch(
|
||||||
MOCK_API_IS_PIN_REQUIRED,
|
MOCK_API_2FA_CONTACTS, new_callable=PropertyMock
|
||||||
return_value=True,
|
) as mock_contacts:
|
||||||
):
|
mock_contacts.return_value = MOCK_2FA_CONTACTS
|
||||||
return await hass.config_entries.flow.async_configure(
|
return await hass.config_entries.flow.async_configure(
|
||||||
user_form["flow_id"], user_input=TEST_CREDS
|
user_form["flow_id"], user_input=TEST_CREDS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def two_factor_verify_form(hass, two_factor_start_form):
|
||||||
|
"""Return two factor form for Subaru config flow."""
|
||||||
|
with patch(
|
||||||
|
MOCK_API_2FA_REQUEST,
|
||||||
|
return_value=True,
|
||||||
|
), patch(MOCK_API_2FA_CONTACTS, new_callable=PropertyMock) as mock_contacts:
|
||||||
|
mock_contacts.return_value = MOCK_2FA_CONTACTS
|
||||||
|
return await hass.config_entries.flow.async_configure(
|
||||||
|
two_factor_start_form["flow_id"],
|
||||||
|
user_input={config_flow.CONF_CONTACT_METHOD: "email@addr.com"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def pin_form(hass, two_factor_verify_form):
|
||||||
|
"""Return PIN input form for Subaru config flow."""
|
||||||
|
with patch(
|
||||||
|
MOCK_API_2FA_VERIFY,
|
||||||
|
return_value=True,
|
||||||
|
), patch(MOCK_API_IS_PIN_REQUIRED, return_value=True):
|
||||||
|
return await hass.config_entries.flow.async_configure(
|
||||||
|
two_factor_verify_form["flow_id"],
|
||||||
|
user_input={config_flow.CONF_VALIDATION_CODE: "123456"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def options_form(hass):
|
async def options_form(hass):
|
||||||
"""Return options form for Subaru config flow."""
|
"""Return options form for Subaru config flow."""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user