Add config flow to Slack integration (#69880)

This commit is contained in:
Robert Hillis 2022-05-13 20:05:06 -04:00 committed by GitHub
parent 21b1667de9
commit 4ea6e5dfc0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 537 additions and 127 deletions

View file

@ -1054,6 +1054,7 @@ omit =
homeassistant/components/sky_hub/*
homeassistant/components/skybeacon/sensor.py
homeassistant/components/skybell/*
homeassistant/components/slack/__init__.py
homeassistant/components/slack/notify.py
homeassistant/components/sia/__init__.py
homeassistant/components/sia/alarm_control_panel.py

View file

@ -919,8 +919,8 @@ build.json @home-assistant/supervisor
/tests/components/siren/ @home-assistant/core @raman325
/homeassistant/components/sisyphus/ @jkeljo
/homeassistant/components/sky_hub/ @rogerselwyn
/homeassistant/components/slack/ @bachya
/tests/components/slack/ @bachya
/homeassistant/components/slack/ @bachya @tkdrob
/tests/components/slack/ @bachya @tkdrob
/homeassistant/components/sleepiq/ @mfugate1 @kbickar
/tests/components/sleepiq/ @mfugate1 @kbickar
/homeassistant/components/slide/ @ualex73

View file

@ -1,2 +1,62 @@
"""The slack component."""
DOMAIN = "slack"
"""The slack integration."""
import logging
from aiohttp.client_exceptions import ClientError
from slack import WebClient
from slack.errors import SlackApiError
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_PLATFORM, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, discovery
from homeassistant.helpers.typing import ConfigType
from .const import DATA_CLIENT, DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.NOTIFY]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Slack component."""
# Iterate all entries for notify to only get Slack
if Platform.NOTIFY in config:
for entry in config[Platform.NOTIFY]:
if entry[CONF_PLATFORM] == DOMAIN:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=entry
)
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Slack from a config entry."""
session = aiohttp_client.async_get_clientsession(hass)
slack = WebClient(token=entry.data[CONF_API_KEY], run_async=True, session=session)
try:
await slack.auth_test()
except (SlackApiError, ClientError) as ex:
if isinstance(ex, SlackApiError) and ex.response["error"] == "invalid_auth":
_LOGGER.error("Invalid API key")
return False
raise ConfigEntryNotReady("Error while setting up integration") from ex
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data | {DATA_CLIENT: slack}
hass.async_create_task(
discovery.async_load_platform(
hass,
Platform.NOTIFY,
DOMAIN,
hass.data[DOMAIN][entry.entry_id],
hass.data[DOMAIN],
)
)
return True

View file

@ -0,0 +1,87 @@
"""Config flow for Slack integration."""
from __future__ import annotations
import logging
from slack import WebClient
from slack.errors import SlackApiError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_NAME, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client
from .const import CONF_DEFAULT_CHANNEL, DOMAIN
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_API_KEY): str,
vol.Required(CONF_DEFAULT_CHANNEL): str,
vol.Optional(CONF_ICON): str,
vol.Optional(CONF_USERNAME): str,
}
)
class SlackFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Slack."""
async def async_step_user(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle a flow initiated by the user."""
errors = {}
if user_input is not None:
error, info = await self._async_try_connect(user_input[CONF_API_KEY])
if error is not None:
errors["base"] = error
elif info is not None:
await self.async_set_unique_id(info["team_id"].lower())
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input.get(CONF_NAME, info["team"]),
data={CONF_NAME: user_input.get(CONF_NAME, info["team"])}
| user_input,
)
user_input = user_input or {}
return self.async_show_form(
step_id="user",
data_schema=CONFIG_SCHEMA,
errors=errors,
)
async def async_step_import(self, import_config: dict[str, str]) -> FlowResult:
"""Import a config entry from configuration.yaml."""
_LOGGER.warning(
"Configuration of the Slack integration in YAML is deprecated and "
"will be removed in a future release; Your existing configuration "
"has been imported into the UI automatically and can be safely removed "
"from your configuration.yaml file"
)
entries = self._async_current_entries()
if any(x.data[CONF_API_KEY] == import_config[CONF_API_KEY] for x in entries):
return self.async_abort(reason="already_configured")
return await self.async_step_user(import_config)
async def _async_try_connect(
self, token: str
) -> tuple[str, None] | tuple[None, dict[str, str]]:
"""Try connecting to Slack."""
session = aiohttp_client.async_get_clientsession(self.hass)
client = WebClient(token=token, run_async=True, session=session)
try:
info = await client.auth_test()
except SlackApiError as ex:
if ex.response["error"] == "invalid_auth":
return "invalid_auth", None
return "cannot_connect", None
except Exception as ex: # pylint:disable=broad-except
_LOGGER.exception("Unexpected exception: %s", ex)
return "unknown", None
return None, info

View file

@ -0,0 +1,16 @@
"""Constants for the Slack integration."""
from typing import Final
ATTR_BLOCKS = "blocks"
ATTR_BLOCKS_TEMPLATE = "blocks_template"
ATTR_FILE = "file"
ATTR_PASSWORD = "password"
ATTR_PATH = "path"
ATTR_URL = "url"
ATTR_USERNAME = "username"
CONF_DEFAULT_CHANNEL = "default_channel"
DATA_CLIENT = "client"
DEFAULT_TIMEOUT_SECONDS = 15
DOMAIN: Final = "slack"

View file

@ -1,9 +1,10 @@
{
"domain": "slack",
"name": "Slack",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/slack",
"requirements": ["slackclient==2.5.0"],
"codeowners": ["@bachya"],
"codeowners": ["@bachya", "@tkdrob"],
"iot_class": "cloud_push",
"loggers": ["slack"]
}

View file

@ -20,26 +20,32 @@ from homeassistant.components.notify import (
PLATFORM_SCHEMA,
BaseNotificationService,
)
from homeassistant.const import ATTR_ICON, CONF_API_KEY, CONF_ICON, CONF_USERNAME
from homeassistant.const import (
ATTR_ICON,
CONF_API_KEY,
CONF_ICON,
CONF_PATH,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import aiohttp_client, config_validation as cv, template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import (
ATTR_BLOCKS,
ATTR_BLOCKS_TEMPLATE,
ATTR_FILE,
ATTR_PASSWORD,
ATTR_PATH,
ATTR_URL,
ATTR_USERNAME,
CONF_DEFAULT_CHANNEL,
DATA_CLIENT,
)
_LOGGER = logging.getLogger(__name__)
ATTR_BLOCKS = "blocks"
ATTR_BLOCKS_TEMPLATE = "blocks_template"
ATTR_FILE = "file"
ATTR_PASSWORD = "password"
ATTR_PATH = "path"
ATTR_URL = "url"
ATTR_USERNAME = "username"
CONF_DEFAULT_CHANNEL = "default_channel"
DEFAULT_TIMEOUT_SECONDS = 15
FILE_PATH_SCHEMA = vol.Schema({vol.Required(ATTR_PATH): cv.isfile})
FILE_PATH_SCHEMA = vol.Schema({vol.Required(CONF_PATH): cv.isfile})
FILE_URL_SCHEMA = vol.Schema(
{
@ -66,6 +72,7 @@ DATA_SCHEMA = vol.All(
cv.ensure_list, [vol.Any(DATA_FILE_SCHEMA, DATA_TEXT_ONLY_SCHEMA)]
)
# Deprecated in Home Assistant 2022.5
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
@ -109,27 +116,13 @@ async def async_get_service(
discovery_info: DiscoveryInfoType | None = None,
) -> SlackNotificationService | None:
"""Set up the Slack notification service."""
session = aiohttp_client.async_get_clientsession(hass)
client = WebClient(token=config[CONF_API_KEY], run_async=True, session=session)
try:
await client.auth_test()
except SlackApiError as err:
_LOGGER.error("Error while setting up integration: %r", err)
if discovery_info is None:
return None
except ClientError as err:
_LOGGER.warning(
"Error testing connection to slack: %r "
"Continuing setup anyway, but notify service might not work",
err,
)
return SlackNotificationService(
hass,
client,
config[CONF_DEFAULT_CHANNEL],
username=config.get(CONF_USERNAME),
icon=config.get(CONF_ICON),
discovery_info.pop(DATA_CLIENT),
discovery_info,
)
@ -153,16 +146,12 @@ class SlackNotificationService(BaseNotificationService):
self,
hass: HomeAssistant,
client: WebClient,
default_channel: str,
username: str | None,
icon: str | None,
config: dict[str, str],
) -> None:
"""Initialize."""
self._client = client
self._default_channel = default_channel
self._hass = hass
self._icon = icon
self._username = username
self._client = client
self._config = config
async def _async_send_local_file_message(
self,
@ -294,7 +283,7 @@ class SlackNotificationService(BaseNotificationService):
title = kwargs.get(ATTR_TITLE)
targets = _async_sanitize_channel_names(
kwargs.get(ATTR_TARGET, [self._default_channel])
kwargs.get(ATTR_TARGET, [self._config[CONF_DEFAULT_CHANNEL]])
)
# Message Type 1: A text-only message
@ -312,8 +301,8 @@ class SlackNotificationService(BaseNotificationService):
targets,
message,
title,
username=data.get(ATTR_USERNAME, self._username),
icon=data.get(ATTR_ICON, self._icon),
username=data.get(ATTR_USERNAME, self._config.get(ATTR_USERNAME)),
icon=data.get(ATTR_ICON, self._config.get(ATTR_ICON)),
blocks=blocks,
)

View file

@ -0,0 +1,29 @@
{
"config": {
"step": {
"user": {
"description": "Refer to the documentation on getting your Slack API key.",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"default_channel": "Default Channel",
"icon": "Icon",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"api_key": "The Slack API token to use for sending Slack messages.",
"default_channel": "The channel to post to if no channel is specified when sending a message.",
"icon": "Use one of the Slack emojis as an Icon for the supplied username.",
"username": "Home Assistant will post to Slack using the username specified."
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
}
}

View file

@ -0,0 +1,29 @@
{
"config": {
"abort": {
"already_configured": "Service is already configured"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"api_key": "API Key",
"default_channel": "Default Channel",
"icon": "Icon",
"username": "Username"
},
"description": "Refer to the documentation on getting your Slack API key.",
"data_description": {
"api_key": "The Slack API token to use for sending Slack messages.",
"default_channel": "The channel to post to if no channel is specified when sending a message.",
"icon": "Use one of the Slack emojis as an Icon for the supplied username.",
"username": "Home Assistant will post to Slack using the username specified."
}
}
}
}
}

View file

@ -306,6 +306,7 @@ FLOWS = {
"shopping_list",
"sia",
"simplisafe",
"slack",
"sleepiq",
"slimproto",
"sma",

View file

@ -1 +1,72 @@
"""Slack notification tests."""
"""Tests for the Slack integration."""
from __future__ import annotations
import json
from homeassistant.components.slack.const import CONF_DEFAULT_CHANNEL, DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_NAME
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture
from tests.test_util.aiohttp import AiohttpClientMocker
AUTH_URL = "https://www.slack.com/api/auth.test"
TOKEN = "abc123"
TEAM_NAME = "Test Team"
TEAM_ID = "abc123def"
CONF_INPUT = {CONF_API_KEY: TOKEN, CONF_DEFAULT_CHANNEL: "test_channel"}
CONF_DATA = CONF_INPUT | {CONF_NAME: TEAM_NAME}
def create_entry(hass: HomeAssistant) -> ConfigEntry:
"""Add config entry in Home Assistant."""
entry = MockConfigEntry(
domain=DOMAIN,
data=CONF_DATA,
unique_id=TEAM_ID,
)
entry.add_to_hass(hass)
return entry
def mock_connection(
aioclient_mock: AiohttpClientMocker, error: str | None = None
) -> None:
"""Mock connection."""
if error is not None:
if error == "invalid_auth":
aioclient_mock.post(
AUTH_URL,
text=json.dumps({"ok": False, "error": "invalid_auth"}),
)
else:
aioclient_mock.post(
AUTH_URL,
text=json.dumps({"ok": False, "error": "cannot_connect"}),
)
else:
aioclient_mock.post(
AUTH_URL,
text=load_fixture("slack/auth_test.json"),
)
async def async_init_integration(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
skip_setup: bool = False,
error: str | None = None,
) -> ConfigEntry:
"""Set up the Slack integration in Home Assistant."""
entry = create_entry(hass)
mock_connection(aioclient_mock, error)
if not skip_setup:
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry

View file

@ -0,0 +1,10 @@
{
"ok": true,
"url": "https://newscorp-tech.slack.com/",
"team": "Test Team",
"user": "user.name",
"team_id": "ABC123DEF",
"user_id": "ABCDEF12345",
"enterprise_id": "123ABCDEF",
"is_enterprise_install": false
}

View file

@ -0,0 +1,140 @@
"""Test Slack config flow."""
from unittest.mock import patch
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.slack.const import DOMAIN
from homeassistant.core import HomeAssistant
from . import CONF_DATA, CONF_INPUT, TEAM_NAME, create_entry, mock_connection
from tests.test_util.aiohttp import AiohttpClientMocker
async def test_flow_user(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test user initialized flow."""
mock_connection(aioclient_mock)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=CONF_INPUT,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == TEAM_NAME
assert result["data"] == CONF_DATA
async def test_flow_user_already_configured(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test user initialized flow with duplicate server."""
create_entry(hass)
mock_connection(aioclient_mock)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=CONF_INPUT,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_flow_user_invalid_auth(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test user initialized flow with invalid token."""
mock_connection(aioclient_mock, "invalid_auth")
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data=CONF_DATA,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "invalid_auth"}
async def test_flow_user_cannot_connect(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test user initialized flow with unreachable server."""
mock_connection(aioclient_mock, "cannot_connect")
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data=CONF_DATA,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "cannot_connect"}
async def test_flow_user_unknown_error(hass: HomeAssistant) -> None:
"""Test user initialized flow with unreachable server."""
with patch(
"homeassistant.components.slack.config_flow.WebClient.auth_test"
) as mock:
mock.side_effect = Exception
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data=CONF_DATA,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "unknown"}
async def test_flow_import(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test an import flow."""
mock_connection(aioclient_mock)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=CONF_DATA,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == TEAM_NAME
assert result["data"] == CONF_DATA
async def test_flow_import_no_name(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test import flow with no name in config."""
mock_connection(aioclient_mock)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=CONF_INPUT,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == TEAM_NAME
assert result["data"] == CONF_DATA
async def test_flow_import_already_configured(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test an import flow already configured."""
create_entry(hass)
mock_connection(aioclient_mock)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=CONF_DATA,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"

View file

@ -0,0 +1,39 @@
"""Test Slack integration."""
from homeassistant.components.slack.const import DOMAIN
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import HomeAssistant
from . import CONF_DATA, async_init_integration
from tests.test_util.aiohttp import AiohttpClientMocker
async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None:
"""Test Slack setup."""
entry: ConfigEntry = await async_init_integration(hass, aioclient_mock)
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.LOADED
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.data == CONF_DATA
async def test_async_setup_entry_not_ready(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test that it throws ConfigEntryNotReady when exception occurs during setup."""
entry: ConfigEntry = await async_init_integration(
hass, aioclient_mock, error="cannot_connect"
)
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.SETUP_RETRY
async def test_async_setup_entry_invalid_auth(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test invalid auth during setup."""
entry: ConfigEntry = await async_init_integration(
hass, aioclient_mock, error="invalid_auth"
)
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.SETUP_ERROR

View file

@ -1,13 +1,10 @@
"""Test slack notifications."""
from __future__ import annotations
import copy
import logging
from unittest.mock import AsyncMock, Mock, patch
from unittest.mock import AsyncMock, Mock
from _pytest.logging import LogCaptureFixture
import aiohttp
from slack.errors import SlackApiError
from homeassistant.components import notify
from homeassistant.components.slack import DOMAIN
@ -15,15 +12,9 @@ from homeassistant.components.slack.notify import (
CONF_DEFAULT_CHANNEL,
SlackNotificationService,
)
from homeassistant.const import (
CONF_API_KEY,
CONF_ICON,
CONF_NAME,
CONF_PLATFORM,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.const import ATTR_ICON, CONF_API_KEY, CONF_NAME, CONF_PLATFORM
from . import CONF_DATA
MODULE_PATH = "homeassistant.components.slack.notify"
SERVICE_NAME = f"notify_{DOMAIN}"
@ -47,74 +38,14 @@ def filter_log_records(caplog: LogCaptureFixture) -> list[logging.LogRecord]:
]
async def test_setup(hass: HomeAssistant, caplog: LogCaptureFixture):
"""Test setup slack notify."""
config = DEFAULT_CONFIG
with patch(
MODULE_PATH + ".aiohttp_client",
**{"async_get_clientsession.return_value": (session := Mock())},
), patch(
MODULE_PATH + ".WebClient",
return_value=(client := AsyncMock()),
) as mock_client:
await async_setup_component(hass, notify.DOMAIN, config)
await hass.async_block_till_done()
assert hass.services.has_service(notify.DOMAIN, SERVICE_NAME)
caplog_records_slack = filter_log_records(caplog)
assert len(caplog_records_slack) == 0
mock_client.assert_called_with(token="12345", run_async=True, session=session)
client.auth_test.assert_called_once_with()
async def test_setup_clientError(hass: HomeAssistant, caplog: LogCaptureFixture):
"""Test setup slack notify with aiohttp.ClientError exception."""
config = copy.deepcopy(DEFAULT_CONFIG)
config[notify.DOMAIN][0].update({CONF_USERNAME: "user", CONF_ICON: "icon"})
with patch(
MODULE_PATH + ".aiohttp_client",
**{"async_get_clientsession.return_value": Mock()},
), patch(MODULE_PATH + ".WebClient", return_value=(client := AsyncMock())):
client.auth_test.side_effect = [aiohttp.ClientError]
await async_setup_component(hass, notify.DOMAIN, config)
await hass.async_block_till_done()
assert hass.services.has_service(notify.DOMAIN, SERVICE_NAME)
caplog_records_slack = filter_log_records(caplog)
assert len(caplog_records_slack) == 1
record = caplog_records_slack[0]
assert record.levelno == logging.WARNING
assert aiohttp.ClientError.__qualname__ in record.message
async def test_setup_slackApiError(hass: HomeAssistant, caplog: LogCaptureFixture):
"""Test setup slack notify with SlackApiError exception."""
config = DEFAULT_CONFIG
with patch(
MODULE_PATH + ".aiohttp_client",
**{"async_get_clientsession.return_value": Mock()},
), patch(MODULE_PATH + ".WebClient", return_value=(client := AsyncMock())):
client.auth_test.side_effect = [err := SlackApiError("msg", "resp")]
await async_setup_component(hass, notify.DOMAIN, config)
await hass.async_block_till_done()
assert hass.services.has_service(notify.DOMAIN, SERVICE_NAME) is False
caplog_records_slack = filter_log_records(caplog)
assert len(caplog_records_slack) == 1
record = caplog_records_slack[0]
assert record.levelno == logging.ERROR
assert err.__class__.__qualname__ in record.message
async def test_message_includes_default_emoji():
"""Tests that default icon is used when no message icon is given."""
mock_client = Mock()
mock_client.chat_postMessage = AsyncMock()
expected_icon = ":robot_face:"
service = SlackNotificationService(None, mock_client, "_", "_", expected_icon)
service = SlackNotificationService(
None, mock_client, CONF_DATA | {ATTR_ICON: expected_icon}
)
await service.async_send_message("test")
@ -128,7 +59,9 @@ async def test_message_emoji_overrides_default():
"""Tests that overriding the default icon emoji when sending a message works."""
mock_client = Mock()
mock_client.chat_postMessage = AsyncMock()
service = SlackNotificationService(None, mock_client, "_", "_", "default_icon")
service = SlackNotificationService(
None, mock_client, CONF_DATA | {ATTR_ICON: "default_icon"}
)
expected_icon = ":new:"
await service.async_send_message("test", data={"icon": expected_icon})
@ -144,7 +77,9 @@ async def test_message_includes_default_icon_url():
mock_client = Mock()
mock_client.chat_postMessage = AsyncMock()
expected_icon = "https://example.com/hass.png"
service = SlackNotificationService(None, mock_client, "_", "_", expected_icon)
service = SlackNotificationService(
None, mock_client, CONF_DATA | {ATTR_ICON: expected_icon}
)
await service.async_send_message("test")
@ -158,10 +93,12 @@ async def test_message_icon_url_overrides_default():
"""Tests that overriding the default icon url when sending a message works."""
mock_client = Mock()
mock_client.chat_postMessage = AsyncMock()
service = SlackNotificationService(None, mock_client, "_", "_", "default_icon")
service = SlackNotificationService(
None, mock_client, CONF_DATA | {ATTR_ICON: "default_icon"}
)
expected_icon = "https://example.com/hass.png"
await service.async_send_message("test", data={"icon": expected_icon})
await service.async_send_message("test", data={ATTR_ICON: expected_icon})
mock_fn = mock_client.chat_postMessage
mock_fn.assert_called_once()