Add config flow to Slack integration (#69880)
This commit is contained in:
parent
21b1667de9
commit
4ea6e5dfc0
15 changed files with 537 additions and 127 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
87
homeassistant/components/slack/config_flow.py
Normal file
87
homeassistant/components/slack/config_flow.py
Normal 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
|
16
homeassistant/components/slack/const.py
Normal file
16
homeassistant/components/slack/const.py
Normal 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"
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
29
homeassistant/components/slack/strings.json
Normal file
29
homeassistant/components/slack/strings.json
Normal 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%]"
|
||||
}
|
||||
}
|
||||
}
|
29
homeassistant/components/slack/translations/en.json
Normal file
29
homeassistant/components/slack/translations/en.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -306,6 +306,7 @@ FLOWS = {
|
|||
"shopping_list",
|
||||
"sia",
|
||||
"simplisafe",
|
||||
"slack",
|
||||
"sleepiq",
|
||||
"slimproto",
|
||||
"sma",
|
||||
|
|
|
@ -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
|
||||
|
|
10
tests/components/slack/fixtures/auth_test.json
Normal file
10
tests/components/slack/fixtures/auth_test.json
Normal 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
|
||||
}
|
140
tests/components/slack/test_config_flow.py
Normal file
140
tests/components/slack/test_config_flow.py
Normal 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"
|
39
tests/components/slack/test_init.py
Normal file
39
tests/components/slack/test_init.py
Normal 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
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue