mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Add IMAP seen, move and delete service (#114501)
* Add seen, move and delete IMAP services * Add entry_id to the imap_content event data * Use config validation library * Add tests * Add logging * Typo in docstr * Add guard
This commit is contained in:
parent
906d3198e3
commit
83b56ab005
@ -2,16 +2,23 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aioimaplib import IMAP4_SSL, AioImapException
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aioimaplib import IMAP4_SSL, AioImapException, Response
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryError,
|
||||
ConfigEntryNotReady,
|
||||
ServiceValidationError,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import CONF_ENABLE_PUSH, DOMAIN
|
||||
from .coordinator import (
|
||||
@ -23,6 +30,70 @@ from .errors import InvalidAuth, InvalidFolder
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
CONF_ENTRY = "entry"
|
||||
CONF_SEEN = "seen"
|
||||
CONF_UID = "uid"
|
||||
CONF_TARGET_FOLDER = "target_folder"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_SERVICE_UID_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ENTRY): cv.string,
|
||||
vol.Required(CONF_UID): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_SEEN_SCHEMA = _SERVICE_UID_SCHEMA
|
||||
SERVICE_MOVE_SCHEMA = _SERVICE_UID_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_SEEN): cv.boolean,
|
||||
vol.Required(CONF_TARGET_FOLDER): cv.string,
|
||||
}
|
||||
)
|
||||
SERVICE_DELETE_SCHEMA = _SERVICE_UID_SCHEMA
|
||||
|
||||
|
||||
async def async_get_imap_client(hass: HomeAssistant, entry_id: str) -> IMAP4_SSL:
|
||||
"""Get IMAP client and connect."""
|
||||
if hass.data[DOMAIN].get(entry_id) is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_entry",
|
||||
)
|
||||
entry = hass.config_entries.async_get_entry(entry_id)
|
||||
if TYPE_CHECKING:
|
||||
assert entry is not None
|
||||
try:
|
||||
client = await connect_to_server(entry.data)
|
||||
except InvalidAuth as exc:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||
) from exc
|
||||
except InvalidFolder as exc:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_folder"
|
||||
) from exc
|
||||
except (TimeoutError, AioImapException) as exc:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="imap_server_fail",
|
||||
translation_placeholders={"error": str(exc)},
|
||||
) from exc
|
||||
return client
|
||||
|
||||
|
||||
@callback
|
||||
def raise_on_error(response: Response, translation_key: str) -> None:
|
||||
"""Get error message from response."""
|
||||
if response.result != "OK":
|
||||
error: str = response.lines[0].decode("utf-8")
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders={"error": error},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up imap from a config entry."""
|
||||
@ -49,6 +120,97 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
async def async_seen(call: ServiceCall) -> None:
|
||||
"""Process mark as seen service call."""
|
||||
entry_id: str = call.data[CONF_ENTRY]
|
||||
uid: str = call.data[CONF_UID]
|
||||
_LOGGER.debug(
|
||||
"Mark message %s as seen. Entry: %s",
|
||||
uid,
|
||||
entry_id,
|
||||
)
|
||||
client = await async_get_imap_client(hass, entry_id)
|
||||
try:
|
||||
response = await client.store(uid, "+FLAGS (\\Seen)")
|
||||
except (TimeoutError, AioImapException) as exc:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="imap_server_fail",
|
||||
translation_placeholders={"error": str(exc)},
|
||||
) from exc
|
||||
raise_on_error(response, "seen_failed")
|
||||
await client.close()
|
||||
|
||||
if not hass.services.has_service(DOMAIN, "seen"):
|
||||
hass.services.async_register(DOMAIN, "seen", async_seen, SERVICE_SEEN_SCHEMA)
|
||||
|
||||
async def async_move(call: ServiceCall) -> None:
|
||||
"""Process move email service call."""
|
||||
entry_id: str = call.data[CONF_ENTRY]
|
||||
uid: str = call.data[CONF_UID]
|
||||
seen = bool(call.data.get(CONF_SEEN))
|
||||
target_folder: str = call.data[CONF_TARGET_FOLDER]
|
||||
_LOGGER.debug(
|
||||
"Move message %s to folder %s. Mark as seen: %s. Entry: %s",
|
||||
uid,
|
||||
target_folder,
|
||||
seen,
|
||||
entry_id,
|
||||
)
|
||||
client = await async_get_imap_client(hass, entry_id)
|
||||
try:
|
||||
if seen:
|
||||
response = await client.store(uid, "+FLAGS (\\Seen)")
|
||||
raise_on_error(response, "seen_failed")
|
||||
response = await client.copy(uid, target_folder)
|
||||
raise_on_error(response, "copy_failed")
|
||||
response = await client.store(uid, "+FLAGS (\\Deleted)")
|
||||
raise_on_error(response, "delete_failed")
|
||||
response = await asyncio.wait_for(
|
||||
client.protocol.expunge(uid, by_uid=True), client.timeout
|
||||
)
|
||||
raise_on_error(response, "expunge_failed")
|
||||
except (TimeoutError, AioImapException) as exc:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="imap_server_fail",
|
||||
translation_placeholders={"error": str(exc)},
|
||||
) from exc
|
||||
await client.close()
|
||||
|
||||
if not hass.services.has_service(DOMAIN, "move"):
|
||||
hass.services.async_register(DOMAIN, "move", async_move, SERVICE_MOVE_SCHEMA)
|
||||
|
||||
async def async_delete(call: ServiceCall) -> None:
|
||||
"""Process deleting email service call."""
|
||||
entry_id: str = call.data[CONF_ENTRY]
|
||||
uid: str = call.data[CONF_UID]
|
||||
_LOGGER.debug(
|
||||
"Delete message %s. Entry: %s",
|
||||
uid,
|
||||
entry_id,
|
||||
)
|
||||
client = await async_get_imap_client(hass, entry_id)
|
||||
try:
|
||||
response = await client.store(uid, "+FLAGS (\\Deleted)")
|
||||
raise_on_error(response, "delete_failed")
|
||||
response = await asyncio.wait_for(
|
||||
client.protocol.expunge(uid, by_uid=True), client.timeout
|
||||
)
|
||||
raise_on_error(response, "expunge_failed")
|
||||
except (TimeoutError, AioImapException) as exc:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="imap_server_fail",
|
||||
translation_placeholders={"error": str(exc)},
|
||||
) from exc
|
||||
await client.close()
|
||||
|
||||
if not hass.services.has_service(DOMAIN, "delete"):
|
||||
hass.services.async_register(
|
||||
DOMAIN, "delete", async_delete, SERVICE_DELETE_SCHEMA
|
||||
)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
entry.async_on_unload(
|
||||
@ -67,4 +229,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator
|
||||
) = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
await coordinator.shutdown()
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.services.async_remove(DOMAIN, "seen")
|
||||
hass.services.async_remove(DOMAIN, "move")
|
||||
hass.services.async_remove(DOMAIN, "delete")
|
||||
return unload_ok
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Coordinator for imag integration."""
|
||||
"""Coordinator for imap integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@ -254,6 +254,7 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]):
|
||||
initial = False
|
||||
self._last_message_id = message_id
|
||||
data = {
|
||||
"entry_id": self.config_entry.entry_id,
|
||||
"server": self.config_entry.data[CONF_SERVER],
|
||||
"username": self.config_entry.data[CONF_USERNAME],
|
||||
"search": self.config_entry.data[CONF_SEARCH],
|
||||
|
@ -8,5 +8,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"seen": "mdi:email-open-outline",
|
||||
"move": "mdi:email-arrow-right-outline",
|
||||
"delete": "mdi:trash-can-outline"
|
||||
}
|
||||
}
|
||||
|
45
homeassistant/components/imap/services.yaml
Normal file
45
homeassistant/components/imap/services.yaml
Normal file
@ -0,0 +1,45 @@
|
||||
seen:
|
||||
fields:
|
||||
entry:
|
||||
required: true
|
||||
selector:
|
||||
config_entry:
|
||||
integration: "imap"
|
||||
uid:
|
||||
required: true
|
||||
example: "12"
|
||||
selector:
|
||||
text:
|
||||
move:
|
||||
fields:
|
||||
entry:
|
||||
required: true
|
||||
selector:
|
||||
config_entry:
|
||||
integration: "imap"
|
||||
uid:
|
||||
required: true
|
||||
example: "12"
|
||||
selector:
|
||||
text:
|
||||
seen:
|
||||
selector:
|
||||
boolean:
|
||||
target_folder:
|
||||
required: true
|
||||
example: "INBOX.Trash"
|
||||
selector:
|
||||
text:
|
||||
|
||||
delete:
|
||||
fields:
|
||||
entry:
|
||||
required: true
|
||||
selector:
|
||||
config_entry:
|
||||
integration: "imap"
|
||||
uid:
|
||||
example: "12"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
@ -35,6 +35,32 @@
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"copy_failed": {
|
||||
"message": "Copying the message failed with '{error}'."
|
||||
},
|
||||
"delete_failed": {
|
||||
"message": "Marking the the message for deletion failed with '{error}'."
|
||||
},
|
||||
"expunge_failed": {
|
||||
"message": "Expungling the the message failed with '{error}'."
|
||||
},
|
||||
"invalid_entry": {
|
||||
"message": "No valid IMAP entry was found."
|
||||
},
|
||||
"invalid_auth": {
|
||||
"message": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
},
|
||||
"invalid_folder": {
|
||||
"message": "[%key:component::imap::config::error::invalid_folder%]"
|
||||
},
|
||||
"imap_server_fail": {
|
||||
"message": "The IMAP server failed to connect: {error}."
|
||||
},
|
||||
"seen_failed": {
|
||||
"message": "Marking message as seen failed with '{error}'."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
@ -64,5 +90,57 @@
|
||||
"intermediate": "Intermediate ciphers"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"seen": {
|
||||
"name": "Mark message as seen",
|
||||
"description": "Mark an email as seen.",
|
||||
"fields": {
|
||||
"entry": {
|
||||
"name": "Entry",
|
||||
"description": "The IMAP config entry."
|
||||
},
|
||||
"uid": {
|
||||
"name": "UID",
|
||||
"description": "The email identifier (UID)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"move": {
|
||||
"name": "Move message",
|
||||
"description": "Move an email to a target folder.",
|
||||
"fields": {
|
||||
"entry": {
|
||||
"name": "[%key:component::imap::services::seen::fields::entry::name%]",
|
||||
"description": "[%key:component::imap::services::seen::fields::entry::description%]"
|
||||
},
|
||||
"seen": {
|
||||
"name": "Seen",
|
||||
"description": "Mark the email as seen."
|
||||
},
|
||||
"uid": {
|
||||
"name": "[%key:component::imap::services::seen::fields::uid::name%]",
|
||||
"description": "[%key:component::imap::services::seen::fields::uid::description%]"
|
||||
},
|
||||
"target_folder": {
|
||||
"name": "Target folder",
|
||||
"description": "The target folder the email should be moved to."
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"name": "Delete message",
|
||||
"description": "Delete an email.",
|
||||
"fields": {
|
||||
"entry": {
|
||||
"name": "[%key:component::imap::services::seen::fields::entry::name%]",
|
||||
"description": "[%key:component::imap::services::seen::fields::entry::description%]"
|
||||
},
|
||||
"uid": {
|
||||
"name": "[%key:component::imap::services::seen::fields::uid::name%]",
|
||||
"description": "[%key:component::imap::services::seen::fields::uid::description%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Fixtures for imap tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import AsyncGenerator, Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from aioimaplib import AUTH, LOGOUT, NONAUTH, SELECTED, STARTED, Response
|
||||
@ -62,7 +62,7 @@ async def mock_imap_protocol(
|
||||
imap_pending_idle: bool,
|
||||
imap_login_state: str,
|
||||
imap_select_state: str,
|
||||
) -> Generator[MagicMock, None]:
|
||||
) -> AsyncGenerator[MagicMock, None]:
|
||||
"""Mock the aioimaplib IMAP protocol handler."""
|
||||
|
||||
with patch(
|
||||
@ -79,7 +79,14 @@ async def mock_imap_protocol(
|
||||
|
||||
async def close() -> Response:
|
||||
"""Mock imap close the selected folder."""
|
||||
imap_mock.protocol.state = imap_login_state
|
||||
return Response("OK", [])
|
||||
|
||||
async def store(uid: str, flags: str) -> Response:
|
||||
"""Mock imap store command."""
|
||||
return Response("OK", [])
|
||||
|
||||
async def copy(uid: str, folder: str) -> Response:
|
||||
"""Mock imap store command."""
|
||||
return Response("OK", [])
|
||||
|
||||
async def logout() -> Response:
|
||||
@ -101,12 +108,17 @@ async def mock_imap_protocol(
|
||||
imap_mock.has_pending_idle.return_value = imap_pending_idle
|
||||
imap_mock.protocol = MagicMock()
|
||||
imap_mock.protocol.state = STARTED
|
||||
imap_mock.protocol.expunge = AsyncMock()
|
||||
imap_mock.protocol.expunge.return_value = Response("OK", [])
|
||||
imap_mock.has_capability.return_value = imap_has_capability
|
||||
imap_mock.login.side_effect = login
|
||||
imap_mock.close.side_effect = close
|
||||
imap_mock.copy.side_effect = copy
|
||||
imap_mock.logout.side_effect = logout
|
||||
imap_mock.select.side_effect = select
|
||||
imap_mock.search.return_value = Response(*imap_search)
|
||||
imap_mock.store.side_effect = store
|
||||
imap_mock.fetch.return_value = Response(*imap_fetch)
|
||||
imap_mock.wait_hello_from_server.side_effect = wait_hello_from_server
|
||||
imap_mock.timeout = 3
|
||||
yield imap_mock
|
||||
|
@ -14,6 +14,7 @@ from homeassistant.components.imap.errors import InvalidAuth, InvalidFolder
|
||||
from homeassistant.components.sensor.const import SensorStateClass
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .const import (
|
||||
@ -780,3 +781,139 @@ async def test_enforce_polling(
|
||||
mock_imap_protocol.wait_server_push.assert_not_called()
|
||||
else:
|
||||
mock_imap_protocol.assert_has_calls([call.wait_server_push])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("imap_search", "imap_fetch"),
|
||||
[(TEST_SEARCH_RESPONSE, TEST_FETCH_RESPONSE_TEXT_PLAIN)],
|
||||
)
|
||||
@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"])
|
||||
async def test_services(hass: HomeAssistant, mock_imap_protocol: MagicMock) -> None:
|
||||
"""Test receiving a message successfully."""
|
||||
event_called = async_capture_events(hass, "imap_content")
|
||||
|
||||
config = MOCK_CONFIG.copy()
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data=config)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
# Make sure we have had one update (when polling)
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=5))
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.imap_email_email_com")
|
||||
# we should have received one message
|
||||
assert state is not None
|
||||
assert state.state == "1"
|
||||
assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT
|
||||
|
||||
# we should have received one event
|
||||
assert len(event_called) == 1
|
||||
data: dict[str, Any] = event_called[0].data
|
||||
assert data["server"] == "imap.server.com"
|
||||
assert data["username"] == "email@email.com"
|
||||
assert data["search"] == "UnSeen UnDeleted"
|
||||
assert data["folder"] == "INBOX"
|
||||
assert data["sender"] == "john.doe@example.com"
|
||||
assert data["subject"] == "Test subject"
|
||||
assert data["uid"] == "1"
|
||||
assert data["entry_id"] == config_entry.entry_id
|
||||
|
||||
# Test seen service
|
||||
data = {"entry": config_entry.entry_id, "uid": "1"}
|
||||
await hass.services.async_call(DOMAIN, "seen", data, blocking=True)
|
||||
mock_imap_protocol.store.assert_called_with("1", "+FLAGS (\\Seen)")
|
||||
mock_imap_protocol.store.reset_mock()
|
||||
|
||||
# Test move service
|
||||
data = {
|
||||
"entry": config_entry.entry_id,
|
||||
"uid": "1",
|
||||
"seen": True,
|
||||
"target_folder": "Trash",
|
||||
}
|
||||
await hass.services.async_call(DOMAIN, "move", data, blocking=True)
|
||||
mock_imap_protocol.store.assert_has_calls(
|
||||
[call("1", "+FLAGS (\\Seen)"), call("1", "+FLAGS (\\Deleted)")]
|
||||
)
|
||||
mock_imap_protocol.copy.assert_called_with("1", "Trash")
|
||||
mock_imap_protocol.protocol.expunge.assert_called_once()
|
||||
mock_imap_protocol.store.reset_mock()
|
||||
mock_imap_protocol.copy.reset_mock()
|
||||
mock_imap_protocol.protocol.expunge.reset_mock()
|
||||
|
||||
# Test delete service
|
||||
data = {"entry": config_entry.entry_id, "uid": "1"}
|
||||
await hass.services.async_call(DOMAIN, "delete", data, blocking=True)
|
||||
mock_imap_protocol.store.assert_called_with("1", "+FLAGS (\\Deleted)")
|
||||
mock_imap_protocol.protocol.expunge.assert_called_once()
|
||||
|
||||
# Test with invalid entry_id
|
||||
data = {"entry": "invalid", "uid": "1"}
|
||||
with pytest.raises(ServiceValidationError) as exc:
|
||||
await hass.services.async_call(DOMAIN, "seen", data, blocking=True)
|
||||
assert exc.value.translation_domain == DOMAIN
|
||||
assert exc.value.translation_key == "invalid_entry"
|
||||
|
||||
# Test processing imap client failures
|
||||
exceptions = {
|
||||
"invalid_auth": {"exc": InvalidAuth(), "translation_placeholders": None},
|
||||
"invalid_folder": {"exc": InvalidFolder(), "translation_placeholders": None},
|
||||
"imap_server_fail": {
|
||||
"exc": AioImapException("Bla"),
|
||||
"translation_placeholders": {"error": "Bla"},
|
||||
},
|
||||
}
|
||||
for translation_key, attrs in exceptions.items():
|
||||
with patch(
|
||||
"homeassistant.components.imap.connect_to_server", side_effect=attrs["exc"]
|
||||
):
|
||||
data = {"entry": config_entry.entry_id, "uid": "1"}
|
||||
with pytest.raises(ServiceValidationError) as exc:
|
||||
await hass.services.async_call(DOMAIN, "seen", data, blocking=True)
|
||||
assert exc.value.translation_domain == DOMAIN
|
||||
assert exc.value.translation_key == translation_key
|
||||
assert (
|
||||
exc.value.translation_placeholders == attrs["translation_placeholders"]
|
||||
)
|
||||
|
||||
# Test unexpected errors with storing a flag during a service call
|
||||
service_calls = {
|
||||
"seen": {"entry": config_entry.entry_id, "uid": "1"},
|
||||
"move": {
|
||||
"entry": config_entry.entry_id,
|
||||
"uid": "1",
|
||||
"seen": False,
|
||||
"target_folder": "Trash",
|
||||
},
|
||||
"delete": {"entry": config_entry.entry_id, "uid": "1"},
|
||||
}
|
||||
store_error_translation_key = {
|
||||
"seen": "seen_failed",
|
||||
"move": "copy_failed",
|
||||
"delete": "delete_failed",
|
||||
}
|
||||
for service, data in service_calls.items():
|
||||
with (
|
||||
pytest.raises(ServiceValidationError) as exc,
|
||||
patch.object(
|
||||
mock_imap_protocol, "store", side_effect=AioImapException("Bla")
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(DOMAIN, service, data, blocking=True)
|
||||
assert exc.value.translation_domain == DOMAIN
|
||||
assert exc.value.translation_key == "imap_server_fail"
|
||||
assert exc.value.translation_placeholders == {"error": "Bla"}
|
||||
# Test with bad responses on store command
|
||||
with (
|
||||
pytest.raises(ServiceValidationError) as exc,
|
||||
patch.object(
|
||||
mock_imap_protocol, "store", return_value=Response("BAD", [b"Bla"])
|
||||
),
|
||||
patch.object(
|
||||
mock_imap_protocol, "copy", return_value=Response("BAD", [b"Bla"])
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(DOMAIN, service, data, blocking=True)
|
||||
assert exc.value.translation_domain == DOMAIN
|
||||
assert exc.value.translation_key == store_error_translation_key[service]
|
||||
assert exc.value.translation_placeholders == {"error": "Bla"}
|
||||
|
Loading…
x
Reference in New Issue
Block a user