Files
core/tests/components/subaru/test_config_flow.py
Erik Montnemery 4b34d1bbb5 Merge config subentry feature branch to dev (#136121)
* Reapply "Add support for subentries to config entries" (#133470) (#136061)

* Reapply "Add support for subentries to config entries" (#133470)

This reverts commit ecb3bf79f3.

* Update test snapshot

* Add config subentry support to device registry (#128157)

* Add config subentry support to device registry

* Apply suggestions from code review

* Update syrupy serializer

* Update snapshots

* Address review comments

* Allow a device to be connected to no or a single subentry of a config entry

* Update snapshots

* Revert "Allow a device to be connected to no or a single subentry of a config entry"

This reverts commit ec6f613151cb4a806b7961033c004b71b76510c2.

* Update test snapshots

* Bump release version in comments

* Rename config_subentries to config_entries_subentries

* Add config subentry support to entity registry (#128155)

* Add config subentry support to entity registry

* Update syrupy serializer

* Update snapshots

* Update snapshots

* Accept suggested changes

* Clean registries when removing subentry (#136671)

* Clean up registries when removing subentry

* Update tests

* Clean up subentries from deleted devices when removing config entry (#136669)

* Clean up subentries from deleted devices when removing config entry

* Move

* Add config subentry support to entity platform (#128161)

* Add config subentry support to entity platform

* Rename subentry_id to config_subentry_id

* Store subentry type in subentry (#136687)

* Add reconfigure support to config subentries (#133353)

* Add reconfigure support to config subentries

* Update test

* Minor adjustment

* Rename supported_subentry_flows to supported_subentry_types

* Address review comments

* Add subentry support to kitchen sink (#136755)

* Add subentry support to kitchen sink

* Add subentry reconfigure support to kitchen_sink

* Update kitchen_sink tests with subentry type stored in config entry

* Update kitchen_sink

* Update kitchen_sink

* Adjust kitchen sink tests

* Fix hassfest

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Improve docstrings and strings.json

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update snapshots

* Update snapshots

* Update snapshots

* Update snapshots

* Update snapshots

* Update snapshots

* Update snapshots

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-02-10 16:40:07 +01:00

461 lines
15 KiB
Python

"""Tests for the Subaru component config flow."""
from copy import deepcopy
from unittest import mock
from unittest.mock import PropertyMock, patch
import pytest
from subarulink.exceptions import InvalidCredentials, InvalidPIN, SubaruException
from homeassistant import config_entries
from homeassistant.components.subaru import config_flow
from homeassistant.components.subaru.const import CONF_UPDATE_ENABLED, DOMAIN
from homeassistant.config_entries import ConfigFlowResult
from homeassistant.const import CONF_DEVICE_ID, CONF_PIN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.setup import async_setup_component
from .conftest import (
MOCK_API_2FA_CONTACTS,
MOCK_API_2FA_REQUEST,
MOCK_API_2FA_VERIFY,
MOCK_API_CONNECT,
MOCK_API_DEVICE_REGISTERED,
MOCK_API_IS_PIN_REQUIRED,
MOCK_API_TEST_PIN,
MOCK_API_UPDATE_SAVED_PIN,
TEST_CONFIG,
TEST_CREDS,
TEST_DEVICE_ID,
TEST_PIN,
TEST_USERNAME,
)
from tests.common import MockConfigEntry
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) -> None:
"""Test the initial user form for first step of the config flow."""
assert user_form["description_placeholders"] is None
assert user_form["errors"] is None
assert user_form["handler"] == DOMAIN
assert user_form["step_id"] == "user"
assert user_form["type"] is FlowResultType.FORM
async def test_user_form_repeat_identifier(hass: HomeAssistant, user_form) -> None:
"""Test we handle repeat identifiers."""
entry = MockConfigEntry(
domain=DOMAIN, title=TEST_USERNAME, data=TEST_CREDS, options=None
)
entry.add_to_hass(hass)
with patch(
MOCK_API_CONNECT,
return_value=True,
) as mock_connect:
result = await hass.config_entries.flow.async_configure(
user_form["flow_id"],
TEST_CREDS,
)
assert len(mock_connect.mock_calls) == 0
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_user_form_cannot_connect(hass: HomeAssistant, user_form) -> None:
"""Test we handle cannot connect error."""
with patch(
MOCK_API_CONNECT,
side_effect=SubaruException(None),
) as mock_connect:
result = await hass.config_entries.flow.async_configure(
user_form["flow_id"],
TEST_CREDS,
)
assert len(mock_connect.mock_calls) == 1
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
async def test_user_form_invalid_auth(hass: HomeAssistant, user_form) -> None:
"""Test we handle invalid auth."""
with patch(
MOCK_API_CONNECT,
side_effect=InvalidCredentials("invalidAccount"),
) as mock_connect:
result = await hass.config_entries.flow.async_configure(
user_form["flow_id"],
TEST_CREDS,
)
assert len(mock_connect.mock_calls) == 1
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "invalid_auth"}
async def test_user_form_pin_not_required(
hass: HomeAssistant, two_factor_verify_form
) -> None:
"""Test successful login when no PIN is required."""
with (
patch(
MOCK_API_2FA_VERIFY,
return_value=True,
) as mock_two_factor_verify,
patch(
MOCK_API_IS_PIN_REQUIRED,
return_value=False,
) as mock_is_pin_required,
patch(ASYNC_SETUP_ENTRY, return_value=True) as mock_setup_entry,
):
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) == 1
assert len(mock_setup_entry.mock_calls) == 1
expected = {
"context": {"source": "user"},
"title": TEST_USERNAME,
"description": None,
"description_placeholders": None,
"flow_id": mock.ANY,
"result": mock.ANY,
"handler": DOMAIN,
"type": "create_entry",
"version": 1,
"data": deepcopy(TEST_CONFIG),
"options": {},
"minor_version": 1,
"subentries": (),
}
expected["data"][CONF_PIN] = None
result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID
assert result == expected
async def test_registered_pin_required(hass: HomeAssistant, user_form) -> None:
"""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: HomeAssistant, user_form) -> None:
"""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: HomeAssistant, two_factor_start_form
) -> None:
"""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: HomeAssistant, two_factor_start_form
) -> None:
"""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"] is FlowResultType.ABORT
assert result["reason"] == "two_factor_request_failed"
async def test_two_factor_verify_success(
hass: HomeAssistant, two_factor_verify_form
) -> None:
"""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: HomeAssistant, two_factor_verify_form
) -> None:
"""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: HomeAssistant, two_factor_verify_form
) -> None:
"""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) -> None:
"""Test the pin entry form for second step of the config flow."""
expected = {
"data_schema": config_flow.PIN_SCHEMA,
"description_placeholders": None,
"errors": None,
"flow_id": mock.ANY,
"handler": DOMAIN,
"step_id": "pin",
"type": "form",
"last_step": None,
"preview": None,
}
assert pin_form == expected
async def test_pin_form_bad_pin_format(hass: HomeAssistant, pin_form) -> None:
"""Test we handle invalid pin."""
with (
patch(
MOCK_API_TEST_PIN,
) as mock_test_pin,
patch(
MOCK_API_UPDATE_SAVED_PIN,
return_value=True,
) as mock_update_saved_pin,
):
result = await hass.config_entries.flow.async_configure(
pin_form["flow_id"], user_input={CONF_PIN: "abcd"}
)
assert len(mock_test_pin.mock_calls) == 0
assert len(mock_update_saved_pin.mock_calls) == 1
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "bad_pin_format"}
async def test_pin_form_success(hass: HomeAssistant, pin_form) -> None:
"""Test successful PIN entry."""
with (
patch(
MOCK_API_TEST_PIN,
return_value=True,
) as mock_test_pin,
patch(
MOCK_API_UPDATE_SAVED_PIN,
return_value=True,
) as mock_update_saved_pin,
patch(ASYNC_SETUP_ENTRY, return_value=True) as mock_setup_entry,
):
result = await hass.config_entries.flow.async_configure(
pin_form["flow_id"], user_input={CONF_PIN: TEST_PIN}
)
assert len(mock_test_pin.mock_calls) == 1
assert len(mock_update_saved_pin.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
expected = {
"context": {"source": "user"},
"title": TEST_USERNAME,
"description": None,
"description_placeholders": None,
"flow_id": mock.ANY,
"result": mock.ANY,
"handler": DOMAIN,
"type": "create_entry",
"version": 1,
"data": TEST_CONFIG,
"options": {},
"minor_version": 1,
"subentries": (),
}
result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID
assert result == expected
async def test_pin_form_incorrect_pin(hass: HomeAssistant, pin_form) -> None:
"""Test we handle invalid pin."""
with (
patch(
MOCK_API_TEST_PIN,
side_effect=InvalidPIN("invalidPin"),
) as mock_test_pin,
patch(
MOCK_API_UPDATE_SAVED_PIN,
return_value=True,
) as mock_update_saved_pin,
):
result = await hass.config_entries.flow.async_configure(
pin_form["flow_id"], user_input={CONF_PIN: TEST_PIN}
)
assert len(mock_test_pin.mock_calls) == 1
assert len(mock_update_saved_pin.mock_calls) == 1
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "incorrect_pin"}
async def test_option_flow_form(options_form) -> None:
"""Test config flow options form."""
assert options_form["description_placeholders"] is None
assert options_form["errors"] is None
assert options_form["step_id"] == "init"
assert options_form["type"] is FlowResultType.FORM
async def test_option_flow(hass: HomeAssistant, options_form) -> None:
"""Test config flow options."""
result = await hass.config_entries.options.async_configure(
options_form["flow_id"],
user_input={
CONF_UPDATE_ENABLED: False,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_UPDATE_ENABLED: False,
}
@pytest.fixture
async def user_form(hass: HomeAssistant) -> ConfigFlowResult:
"""Return initial form for Subaru config flow."""
return await hass.config_entries.flow.async_init(
config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
@pytest.fixture
async def two_factor_start_form(
hass: HomeAssistant, user_form: ConfigFlowResult
) -> ConfigFlowResult:
"""Return two factor form for Subaru config flow."""
with (
patch(MOCK_API_CONNECT, 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(
user_form["flow_id"], user_input=TEST_CREDS
)
@pytest.fixture
async def two_factor_verify_form(
hass: HomeAssistant, two_factor_start_form: ConfigFlowResult
) -> ConfigFlowResult:
"""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: HomeAssistant, two_factor_verify_form: ConfigFlowResult
) -> ConfigFlowResult:
"""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
async def options_form(hass: HomeAssistant) -> ConfigFlowResult:
"""Return options form for Subaru config flow."""
entry = MockConfigEntry(domain=DOMAIN, data={}, options=None)
entry.add_to_hass(hass)
await async_setup_component(hass, DOMAIN, {})
return await hass.config_entries.options.async_init(entry.entry_id)