Add option flow for imap integration (#89914)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
This commit is contained in:
parent
0d58646823
commit
5b3c57ff1e
4 changed files with 197 additions and 19 deletions
|
@ -10,6 +10,7 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
|
@ -36,6 +37,13 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
|||
}
|
||||
)
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_FOLDER, default="INBOX"): str,
|
||||
vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(user_input: dict[str, Any]) -> dict[str, str]:
|
||||
"""Validate user input."""
|
||||
|
@ -80,9 +88,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_FOLDER: user_input[CONF_FOLDER],
|
||||
CONF_SEARCH: user_input[CONF_SEARCH],
|
||||
key: user_input[key]
|
||||
for key in (CONF_USERNAME, CONF_SERVER, CONF_FOLDER, CONF_SEARCH)
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -128,3 +135,64 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
) -> OptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlow(config_entry)
|
||||
|
||||
|
||||
class OptionsFlow(config_entries.OptionsFlowWithConfigEntry):
|
||||
"""Option flow handler."""
|
||||
|
||||
def _async_abort_entries_match(
|
||||
self, match_dict: dict[str, Any] | None
|
||||
) -> dict[str, str]:
|
||||
"""Validate the user input against other config entries."""
|
||||
if match_dict is None:
|
||||
return {}
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
for entry in [
|
||||
entry
|
||||
for entry in self.hass.config_entries.async_entries(DOMAIN)
|
||||
if entry is not self.config_entry
|
||||
]:
|
||||
if all(item in entry.data.items() for item in match_dict.items()):
|
||||
errors["base"] = "already_configured"
|
||||
break
|
||||
return errors
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage the options."""
|
||||
errors: dict[str, str] = self._async_abort_entries_match(
|
||||
{
|
||||
CONF_SERVER: self._config_entry.data[CONF_SERVER],
|
||||
CONF_USERNAME: self._config_entry.data[CONF_USERNAME],
|
||||
CONF_FOLDER: user_input[CONF_FOLDER],
|
||||
CONF_SEARCH: user_input[CONF_SEARCH],
|
||||
}
|
||||
if user_input
|
||||
else None
|
||||
)
|
||||
entry_data: dict[str, Any] = dict(self._config_entry.data)
|
||||
if not errors and user_input is not None:
|
||||
entry_data.update(user_input)
|
||||
errors = await validate_input(entry_data)
|
||||
if not errors:
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.config_entry, data=entry_data
|
||||
)
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
||||
)
|
||||
return self.async_create_entry(data={})
|
||||
|
||||
schema = self.add_suggested_values_to_schema(OPTIONS_SCHEMA, entry_data)
|
||||
|
||||
return self.async_show_form(step_id="init", data_schema=schema, errors=errors)
|
||||
|
|
|
@ -31,5 +31,23 @@
|
|||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"folder": "[%key:component::imap::config::step::user::data::folder%]",
|
||||
"search": "[%key:component::imap::config::step::user::data::search%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "An entry with these folder and search options already exists",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_charset": "[%key:component::imap::config::error::invalid_charset%]",
|
||||
"invalid_folder": "[%key:component::imap::config::error::invalid_folder%]",
|
||||
"invalid_search": "[%key:component::imap::config::error::invalid_search%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
14
tests/components/imap/conftest.py
Normal file
14
tests/components/imap/conftest.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
"""Test the iamp config flow."""
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.imap.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
|
@ -1,11 +1,11 @@
|
|||
"""Test the imap config flow."""
|
||||
import asyncio
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aioimaplib import AioImapException
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.imap.const import (
|
||||
CONF_CHARSET,
|
||||
CONF_FOLDER,
|
||||
|
@ -29,8 +29,15 @@ MOCK_CONFIG = {
|
|||
"search": "UnSeen UnDeleted",
|
||||
}
|
||||
|
||||
MOCK_OPTIONS = {
|
||||
"folder": "INBOX",
|
||||
"search": "UnSeen UnDeleted",
|
||||
}
|
||||
|
||||
async def test_form(hass: HomeAssistant) -> None:
|
||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
|
@ -40,10 +47,7 @@ async def test_form(hass: HomeAssistant) -> None:
|
|||
|
||||
with patch(
|
||||
"homeassistant.components.imap.config_flow.connect_to_server"
|
||||
) as mock_client, patch(
|
||||
"homeassistant.components.imap.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
) as mock_client:
|
||||
mock_client.return_value.search.return_value = (
|
||||
"OK",
|
||||
[b""],
|
||||
|
@ -184,10 +188,7 @@ async def test_form_invalid_search(hass: HomeAssistant) -> None:
|
|||
with patch(
|
||||
"homeassistant.components.imap.config_flow.connect_to_server"
|
||||
) as mock_client:
|
||||
mock_client.return_value.search.return_value = (
|
||||
"BAD",
|
||||
[b"Invalid search"],
|
||||
)
|
||||
mock_client.return_value.search.return_value = ("BAD", [b"Invalid search"])
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], MOCK_CONFIG
|
||||
)
|
||||
|
@ -196,7 +197,7 @@ async def test_form_invalid_search(hass: HomeAssistant) -> None:
|
|||
assert result2["errors"] == {CONF_SEARCH: "invalid_search"}
|
||||
|
||||
|
||||
async def test_reauth_success(hass: HomeAssistant) -> None:
|
||||
async def test_reauth_success(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
|
||||
"""Test we can reauth."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
|
@ -219,10 +220,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None:
|
|||
|
||||
with patch(
|
||||
"homeassistant.components.imap.config_flow.connect_to_server"
|
||||
) as mock_client, patch(
|
||||
"homeassistant.components.imap.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
) as mock_client:
|
||||
mock_client.return_value.search.return_value = (
|
||||
"OK",
|
||||
[b""],
|
||||
|
@ -310,3 +308,83 @@ async def test_reauth_failed_conn_error(hass: HomeAssistant) -> None:
|
|||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_options_form(hass: HomeAssistant) -> None:
|
||||
"""Test we show the options form."""
|
||||
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
new_config = MOCK_OPTIONS.copy()
|
||||
new_config["folder"] = "INBOX.Notifications"
|
||||
new_config["search"] = "UnSeen UnDeleted!!INVALID"
|
||||
|
||||
# simulate initial search setup error
|
||||
with patch(
|
||||
"homeassistant.components.imap.config_flow.connect_to_server"
|
||||
) as mock_client:
|
||||
mock_client.return_value.search.return_value = ("BAD", [b"Invalid search"])
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], new_config
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["errors"] == {CONF_SEARCH: "invalid_search"}
|
||||
|
||||
new_config["search"] = "UnSeen UnDeleted"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.imap.config_flow.connect_to_server"
|
||||
) as mock_client:
|
||||
mock_client.return_value.search.return_value = ("OK", [b""])
|
||||
result3 = await hass.config_entries.options.async_configure(
|
||||
result2["flow_id"],
|
||||
new_config,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result3["data"] == {}
|
||||
for key, value in new_config.items():
|
||||
assert entry.data[key] == value
|
||||
|
||||
|
||||
async def test_key_options_in_options_form(hass: HomeAssistant) -> None:
|
||||
"""Test we cannot change options if that would cause duplicates."""
|
||||
|
||||
entry1 = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG)
|
||||
entry1.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry1.entry_id)
|
||||
|
||||
config2 = MOCK_CONFIG.copy()
|
||||
config2["folder"] = "INBOX.Notifications"
|
||||
entry2 = MockConfigEntry(domain=DOMAIN, data=config2)
|
||||
entry2.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry2.entry_id)
|
||||
|
||||
# Now try to set back the folder option of entry2
|
||||
# so that it conflicts with that of entry1
|
||||
result = await hass.config_entries.options.async_init(entry2.entry_id)
|
||||
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
new_config = MOCK_OPTIONS.copy()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.imap.config_flow.connect_to_server"
|
||||
) as mock_client:
|
||||
mock_client.return_value.search.return_value = ("OK", [b""])
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
new_config,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result2["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "already_configured"}
|
||||
|
|
Loading…
Add table
Reference in a new issue