From 4ea6e5dfc0f50adff04e5676edab5c713e472808 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 13 May 2022 20:05:06 -0400 Subject: [PATCH] Add config flow to Slack integration (#69880) --- .coveragerc | 1 + CODEOWNERS | 4 +- homeassistant/components/slack/__init__.py | 64 +++++++- homeassistant/components/slack/config_flow.py | 87 +++++++++++ homeassistant/components/slack/const.py | 16 ++ homeassistant/components/slack/manifest.json | 3 +- homeassistant/components/slack/notify.py | 71 ++++----- homeassistant/components/slack/strings.json | 29 ++++ .../components/slack/translations/en.json | 29 ++++ homeassistant/generated/config_flows.py | 1 + tests/components/slack/__init__.py | 73 ++++++++- .../components/slack/fixtures/auth_test.json | 10 ++ tests/components/slack/test_config_flow.py | 140 ++++++++++++++++++ tests/components/slack/test_init.py | 39 +++++ tests/components/slack/test_notify.py | 97 +++--------- 15 files changed, 537 insertions(+), 127 deletions(-) create mode 100644 homeassistant/components/slack/config_flow.py create mode 100644 homeassistant/components/slack/const.py create mode 100644 homeassistant/components/slack/strings.json create mode 100644 homeassistant/components/slack/translations/en.json create mode 100644 tests/components/slack/fixtures/auth_test.json create mode 100644 tests/components/slack/test_config_flow.py create mode 100644 tests/components/slack/test_init.py diff --git a/.coveragerc b/.coveragerc index 7ce0e5ee299..898a14b9c86 100644 --- a/.coveragerc +++ b/.coveragerc @@ -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 diff --git a/CODEOWNERS b/CODEOWNERS index d07ce028ba9..68f9017363e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/homeassistant/components/slack/__init__.py b/homeassistant/components/slack/__init__.py index e3ae111f2ea..ae52013621f 100644 --- a/homeassistant/components/slack/__init__.py +++ b/homeassistant/components/slack/__init__.py @@ -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 diff --git a/homeassistant/components/slack/config_flow.py b/homeassistant/components/slack/config_flow.py new file mode 100644 index 00000000000..253750a2310 --- /dev/null +++ b/homeassistant/components/slack/config_flow.py @@ -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 diff --git a/homeassistant/components/slack/const.py b/homeassistant/components/slack/const.py new file mode 100644 index 00000000000..b7b5707aeeb --- /dev/null +++ b/homeassistant/components/slack/const.py @@ -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" diff --git a/homeassistant/components/slack/manifest.json b/homeassistant/components/slack/manifest.json index d54bb9e0ec6..57c1690e647 100644 --- a/homeassistant/components/slack/manifest.json +++ b/homeassistant/components/slack/manifest.json @@ -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"] } diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 4dfacda266c..bfcf79ef676 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -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, ) diff --git a/homeassistant/components/slack/strings.json b/homeassistant/components/slack/strings.json new file mode 100644 index 00000000000..f14129cf156 --- /dev/null +++ b/homeassistant/components/slack/strings.json @@ -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%]" + } + } +} diff --git a/homeassistant/components/slack/translations/en.json b/homeassistant/components/slack/translations/en.json new file mode 100644 index 00000000000..5a97fb1938e --- /dev/null +++ b/homeassistant/components/slack/translations/en.json @@ -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." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ae2ab6339aa..89a9e4e489a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -306,6 +306,7 @@ FLOWS = { "shopping_list", "sia", "simplisafe", + "slack", "sleepiq", "slimproto", "sma", diff --git a/tests/components/slack/__init__.py b/tests/components/slack/__init__.py index b32ec5ef7b1..6a258ce9027 100644 --- a/tests/components/slack/__init__.py +++ b/tests/components/slack/__init__.py @@ -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 diff --git a/tests/components/slack/fixtures/auth_test.json b/tests/components/slack/fixtures/auth_test.json new file mode 100644 index 00000000000..b7b6ee3e9bd --- /dev/null +++ b/tests/components/slack/fixtures/auth_test.json @@ -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 +} diff --git a/tests/components/slack/test_config_flow.py b/tests/components/slack/test_config_flow.py new file mode 100644 index 00000000000..850690783e8 --- /dev/null +++ b/tests/components/slack/test_config_flow.py @@ -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" diff --git a/tests/components/slack/test_init.py b/tests/components/slack/test_init.py new file mode 100644 index 00000000000..487a65b8ef0 --- /dev/null +++ b/tests/components/slack/test_init.py @@ -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 diff --git a/tests/components/slack/test_notify.py b/tests/components/slack/test_notify.py index f10673cced4..b5fa08fa54f 100644 --- a/tests/components/slack/test_notify.py +++ b/tests/components/slack/test_notify.py @@ -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()