diff --git a/homeassistant/components/smtp/__init__.py b/homeassistant/components/smtp/__init__.py index 5dffe09d6e6..5e7fb41c212 100644 --- a/homeassistant/components/smtp/__init__.py +++ b/homeassistant/components/smtp/__init__.py @@ -1,98 +1 @@ -"""Set up the smtp component.""" - -from __future__ import annotations - -import logging - -import voluptuous as vol - -from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN, PLATFORM_SCHEMA -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_PASSWORD, - CONF_PORT, - CONF_RECIPIENT, - CONF_SENDER, - CONF_TIMEOUT, - CONF_USERNAME, - CONF_VERIFY_SSL, - Platform, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType - -from .const import ( - CONF_DEBUG, - CONF_ENCRYPTION, - CONF_SENDER_NAME, - CONF_SERVER, - DEFAULT_DEBUG, - DEFAULT_ENCRYPTION, - DEFAULT_HOST, - DEFAULT_PORT, - DEFAULT_TIMEOUT, - DOMAIN, - ENCRYPTION_OPTIONS, -) -from .notify import MailNotificationService - -_LOGGER = logging.getLogger(__name__) - - -CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [vol.Email()]), - vol.Required(CONF_SENDER): vol.Email(), - vol.Optional(CONF_SERVER, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_ENCRYPTION, default=DEFAULT_ENCRYPTION): vol.In( - ENCRYPTION_OPTIONS - ), - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SENDER_NAME): cv.string, - vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - } -) - - -PLATFORMS = [Platform.NOTIFY] - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the SMTP notify component from configuration.yaml.""" - hass.data.setdefault(DOMAIN, {})["hass_config"] = config - if NOTIFY_DOMAIN in config: - for platform_config in config[NOTIFY_DOMAIN]: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=platform_config - ) - ) - - return True - - -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: - """Set up SMTP config entry.""" - hass_config: ConfigType = hass.data[DOMAIN]["hass_config"] - hass.data[DOMAIN][config_entry.entry_id] = None - config = dict(config_entry.data) | {"entry_id": config_entry.entry_id} - discovery.load_platform(hass, Platform.NOTIFY.value, DOMAIN, config, hass_config) - return True - - -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: - """Unload SMTP config entry.""" - mail_service: MailNotificationService | None = hass.data[DOMAIN][ - config_entry.entry_id - ] - if mail_service is not None: - await mail_service.async_unregister_services() - return True +"""The smtp component.""" diff --git a/homeassistant/components/smtp/config_flow.py b/homeassistant/components/smtp/config_flow.py deleted file mode 100644 index cffb5daa977..00000000000 --- a/homeassistant/components/smtp/config_flow.py +++ /dev/null @@ -1,197 +0,0 @@ -"""Config flow for SMTP integration.""" -from __future__ import annotations - -import logging -from typing import Any - -import voluptuous as vol - -from homeassistant.config_entries import ConfigFlow -from homeassistant.const import ( - CONF_NAME, - CONF_PASSWORD, - CONF_PLATFORM, - CONF_PORT, - CONF_RECIPIENT, - CONF_SENDER, - CONF_TIMEOUT, - CONF_USERNAME, - CONF_VERIFY_SSL, -) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.data_entry_flow import FlowResult -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.selector import ( - BooleanSelector, - BooleanSelectorConfig, - NumberSelector, - NumberSelectorConfig, - NumberSelectorMode, - SelectSelector, - SelectSelectorConfig, - TextSelector, - TextSelectorConfig, - TextSelectorType, -) - -from .const import ( - CONF_DEBUG, - CONF_ENCRYPTION, - CONF_SENDER_NAME, - CONF_SERVER, - DEFAULT_DEBUG, - DEFAULT_ENCRYPTION, - DEFAULT_HOST, - DEFAULT_PORT, - DEFAULT_TIMEOUT, - DOMAIN, - ENCRYPTION_OPTIONS, -) -from .notify import MailNotificationService - -_LOGGER = logging.getLogger(__name__) - - -BASE_DATA_FIELDS = { - vol.Optional(CONF_NAME): TextSelector( - TextSelectorConfig(type=TextSelectorType.TEXT) - ), - vol.Required(CONF_RECIPIENT): SelectSelector( - SelectSelectorConfig(options=[], multiple=True, custom_value=True) - ), - vol.Required(CONF_SENDER): TextSelector( - TextSelectorConfig(type=TextSelectorType.EMAIL) - ), - vol.Required(CONF_SERVER, default=DEFAULT_HOST): TextSelector( - TextSelectorConfig(type=TextSelectorType.TEXT) - ), - vol.Required(CONF_PORT, default=DEFAULT_PORT): NumberSelector( - NumberSelectorConfig(min=1, max=65535, mode=NumberSelectorMode.BOX, step=1) - ), - vol.Optional(CONF_ENCRYPTION, default=DEFAULT_ENCRYPTION): SelectSelector( - SelectSelectorConfig(options=ENCRYPTION_OPTIONS, translation_key="encryption") - ), - vol.Optional(CONF_USERNAME): TextSelector( - TextSelectorConfig(type=TextSelectorType.TEXT) - ), - vol.Optional(CONF_PASSWORD): TextSelector( - TextSelectorConfig(type=TextSelectorType.PASSWORD) - ), - vol.Optional(CONF_SENDER_NAME): TextSelector( - TextSelectorConfig(type=TextSelectorType.TEXT) - ), -} - -ADVANCED_DATA_FIELDS = { - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): NumberSelector( - NumberSelectorConfig(min=1, max=60, mode=NumberSelectorMode.BOX) - ), - vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): BooleanSelector( - BooleanSelectorConfig() - ), - vol.Optional(CONF_VERIFY_SSL, default=True): BooleanSelector( - BooleanSelectorConfig() - ), -} - -CONFIG_SCHEMA = vol.Schema(BASE_DATA_FIELDS | ADVANCED_DATA_FIELDS) - -UNIQUE_ENTRY_KEYS = [CONF_RECIPIENT, CONF_SENDER, CONF_SERVER, CONF_NAME] - - -def validate_smtp_settings(settings: dict[str, Any], errors: dict[str, str]) -> None: - """Validate SMTP connection settings.""" - try: - for recipient in cv.ensure_list(settings[CONF_RECIPIENT]): - vol.Email()(recipient) - except vol.Invalid: - errors[CONF_RECIPIENT] = "invalid_email_address" - try: - vol.Email()(settings[CONF_SENDER]) - except vol.Invalid: - errors[CONF_SENDER] = "invalid_email_address" - if settings.get(CONF_USERNAME) and not settings.get(CONF_PASSWORD): - errors[CONF_PASSWORD] = "username_and_password" - if settings.get(CONF_PASSWORD) and not settings.get(CONF_USERNAME): - errors[CONF_USERNAME] = "username_and_password" - - if errors: - return - settings[CONF_PORT] = cv.positive_int(settings[CONF_PORT]) - settings[CONF_TIMEOUT] = cv.positive_int(settings[CONF_TIMEOUT]) - - service_class = MailNotificationService( - settings[CONF_SERVER], - settings[CONF_PORT], - settings.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), - settings[CONF_SENDER], - settings[CONF_ENCRYPTION], - settings.get(CONF_USERNAME), - settings.get(CONF_PASSWORD), - settings[CONF_RECIPIENT], - settings.get(CONF_SENDER_NAME), - settings.get(CONF_DEBUG, DEFAULT_DEBUG), - settings.get(CONF_VERIFY_SSL, True), - ) - service_class.connection_is_valid(errors=errors) - - -class SMTPConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for SMTP.""" - - VERSION = 1 - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the initial step.""" - errors: dict[str, str] = {} - if user_input is not None: - self._async_abort_entries_match( - {key: user_input[key] for key in UNIQUE_ENTRY_KEYS if key in user_input} - ) - data: dict[str, Any] = CONFIG_SCHEMA(user_input) - await self.hass.async_add_executor_job(validate_smtp_settings, data, errors) - if not errors: - name = data.get(CONF_NAME, "SMTP") - return self.async_create_entry(title=name, data=data) - - fields = BASE_DATA_FIELDS - if self.show_advanced_options: - fields |= ADVANCED_DATA_FIELDS - - schema = self.add_suggested_values_to_schema(vol.Schema(fields), user_input) - return self.async_show_form( - step_id="user", - data_schema=schema, - errors=errors, - ) - - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Import configuration from yaml.""" - async_create_issue( - self.hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2023.9.0", - is_fixable=False, - is_persistent=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "SMTP", - }, - ) - self._async_abort_entries_match( - {key: config[key] for key in UNIQUE_ENTRY_KEYS if key in config} - ) - config.pop(CONF_PLATFORM) - config[CONF_RECIPIENT] = cv.ensure_list(config[CONF_RECIPIENT]) - config = CONFIG_SCHEMA(config) - return self.async_create_entry( - title=config.get(CONF_NAME, "SMTP"), - data=config, - ) diff --git a/homeassistant/components/smtp/manifest.json b/homeassistant/components/smtp/manifest.json index 7bc0a75e9bf..0e0bba707ac 100644 --- a/homeassistant/components/smtp/manifest.json +++ b/homeassistant/components/smtp/manifest.json @@ -2,7 +2,6 @@ "domain": "smtp", "name": "SMTP", "codeowners": [], - "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smtp", "iot_class": "cloud_push" } diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index ae70eedb79f..87600650551 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -10,13 +10,15 @@ import logging import os from pathlib import Path import smtplib -import socket + +import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import ( @@ -27,9 +29,12 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_USERNAME, CONF_VERIFY_SSL, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util from homeassistant.util.ssl import client_context @@ -47,10 +52,31 @@ from .const import ( DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN, + ENCRYPTION_OPTIONS, ) +PLATFORMS = [Platform.NOTIFY] + _LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [vol.Email()]), + vol.Required(CONF_SENDER): vol.Email(), + vol.Optional(CONF_SERVER, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_ENCRYPTION, default=DEFAULT_ENCRYPTION): vol.In( + ENCRYPTION_OPTIONS + ), + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SENDER_NAME): cv.string, + vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + } +) + def get_service( hass: HomeAssistant, @@ -58,30 +84,21 @@ def get_service( discovery_info: DiscoveryInfoType | None = None, ) -> MailNotificationService | None: """Get the mail notification service.""" - if discovery_info is None: - _LOGGER.warning( - "The notify platform setup the smtp integration via configuration.yaml " - "is deprecated. Your config has been migrated to a config entry and " - "should be removed from your configuration.yaml. " - "Canceling setup via configuration.yaml" - ) - return None - entry_id = discovery_info["entry_id"] + setup_reload_service(hass, DOMAIN, PLATFORMS) mail_service = MailNotificationService( - discovery_info.get(CONF_SERVER, DEFAULT_HOST), - discovery_info.get(CONF_PORT, DEFAULT_PORT), - discovery_info.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), - discovery_info[CONF_SENDER], - discovery_info.get(CONF_ENCRYPTION, DEFAULT_ENCRYPTION), - discovery_info.get(CONF_USERNAME), - discovery_info.get(CONF_PASSWORD), - discovery_info[CONF_RECIPIENT], - discovery_info.get(CONF_SENDER_NAME), - discovery_info.get(CONF_DEBUG, DEFAULT_DEBUG), - discovery_info.get(CONF_VERIFY_SSL, True), + config[CONF_SERVER], + config[CONF_PORT], + config[CONF_TIMEOUT], + config[CONF_SENDER], + config[CONF_ENCRYPTION], + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD), + config[CONF_RECIPIENT], + config.get(CONF_SENDER_NAME), + config[CONF_DEBUG], + config[CONF_VERIFY_SSL], ) - hass.data[DOMAIN][entry_id] = mail_service if mail_service.connection_is_valid(): return mail_service @@ -104,7 +121,7 @@ class MailNotificationService(BaseNotificationService): sender_name, debug, verify_ssl, - ) -> None: + ): """Initialize the SMTP service.""" self._server = server self._port = port @@ -140,32 +157,25 @@ class MailNotificationService(BaseNotificationService): mail.login(self.username, self.password) return mail - def connection_is_valid(self, errors: dict[str, str] | None = None) -> bool: + def connection_is_valid(self): """Check for valid config, verify connectivity.""" server = None try: server = self.connect() - except smtplib.SMTPAuthenticationError: - if errors is None: - _LOGGER.exception( - "Login not possible. Please check your setting and/or your credentials" - ) - else: - errors["base"] = "authentication_failed" - return False + except (smtplib.socket.gaierror, ConnectionRefusedError): + _LOGGER.exception( + ( + "SMTP server not found or refused connection (%s:%s). Please check" + " the IP address, hostname, and availability of your SMTP server" + ), + self._server, + self._port, + ) - except (socket.gaierror, ConnectionRefusedError, OSError): - if errors is None: - _LOGGER.exception( - ( - "SMTP server not found or refused connection (%s:%s). Please check" - " the IP address, hostname, and availability of your SMTP server" - ), - self._server, - self._port, - ) - else: - errors["base"] = "connection_refused" + except smtplib.SMTPAuthenticationError: + _LOGGER.exception( + "Login not possible. Please check your setting and/or your credentials" + ) return False finally: diff --git a/homeassistant/components/smtp/services.yaml b/homeassistant/components/smtp/services.yaml new file mode 100644 index 00000000000..c983a105c93 --- /dev/null +++ b/homeassistant/components/smtp/services.yaml @@ -0,0 +1 @@ +reload: diff --git a/homeassistant/components/smtp/strings.json b/homeassistant/components/smtp/strings.json index 4f18aecdd3b..37250fa6447 100644 --- a/homeassistant/components/smtp/strings.json +++ b/homeassistant/components/smtp/strings.json @@ -1,52 +1,8 @@ { - "config": { - "step": { - "user": { - "data": { - "name": "Name", - "recipient": "Recipients", - "sender": "Sender", - "sender_name": "Sender name", - "server": "[%key:common::config_flow::data::host%]", - "port": "[%key:common::config_flow::data::port%]", - "timeout": "Request Timeout (seconds)", - "encryption": "Encryption", - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]", - "debug": "Enable SMTP debug", - "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" - }, - "data_description": { - "name": "When set it will create a specific notify service with this name, defaults to `notify.notify`", - "recipient": "The email addres(ses) of the receipient(s) that should receive an email notification", - "sender": "The email address of the sender", - "sender_name": "The name of the sender", - "server": "The SMTP server hostname or IP-adress", - "port": "The port of the SMTP server", - "encryption": "The encryption type of the email server.", - "username": "The username for authentication to the SMTP server", - "password": "The password for authentication to the SMTP server" - } - } - }, - "abort": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" - }, - "error": { - "authentication_failed": "Login not possible. Please check your setting and/or your credentials", - "connection_refused": "SMTP server not found or refused connection. Please check the IP address, hostname, and availability of your SMTP server", - "invalid_email_address": "Invalid email address specified", - "username_and_password": "Username and password should be configured together" - } - }, - "selector": { - "encryption": { - "options": { - "tls": "TLS (usually over port 465)", - "starttls": "STARTTLS (usually over port 587 or 25)", - "none": "No encryption (usually over port 25)" - } + "services": { + "reload": { + "name": "[%key:common::action::reload%]", + "description": "Reloads smtp notify services." } }, "exceptions": { diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 39c084bf052..4d909f40736 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -474,7 +474,6 @@ FLOWS = { "smarttub", "smhi", "sms", - "smtp", "snapcast", "snooz", "solaredge", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 73e09678baa..81b3b3b8192 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5461,7 +5461,7 @@ "smtp": { "name": "SMTP", "integration_type": "hub", - "config_flow": true, + "config_flow": false, "iot_class": "cloud_push" }, "snapcast": { diff --git a/tests/components/smtp/conftest.py b/tests/components/smtp/conftest.py deleted file mode 100644 index 80aee9c731d..00000000000 --- a/tests/components/smtp/conftest.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Fixtures for smtp tests.""" - -from collections.abc import Generator -from unittest.mock import AsyncMock, patch - -import pytest - - -@pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock, None, None]: - """Override async_setup_entry.""" - with patch( - "homeassistant.components.smtp.async_setup_entry", return_value=True - ) as mock_setup_entry: - yield mock_setup_entry diff --git a/tests/components/smtp/const.py b/tests/components/smtp/const.py deleted file mode 100644 index 1b3f7522f81..00000000000 --- a/tests/components/smtp/const.py +++ /dev/null @@ -1,36 +0,0 @@ -""""Shared constants for SMTP tests.""" - -from homeassistant.components.smtp.const import DOMAIN - -MOCKED_CONFIG_ENTRY_DATA = { - "name": DOMAIN, - "recipient": ["test@example.com"], - "sender": "test@example.com", - "server": "localhost", - "port": 587, - "encryption": "starttls", - "debug": False, - "verify_ssl": True, - "timeout": 5, -} - -MOCKED_USER_ADVANCED_DATA = { - "name": DOMAIN, - "recipient": ["test@example.com"], - "sender": "test@example.com", - "server": "localhost", - "port": 587, - "encryption": "starttls", - "debug": False, - "verify_ssl": True, - "timeout": 5, -} - -MOCKED_USER_BASIC_DATA = { - "name": DOMAIN, - "recipient": ["test@example.com"], - "sender": "test@example.com", - "server": "localhost", - "port": 587, - "encryption": "starttls", -} diff --git a/tests/components/smtp/test_config_flow.py b/tests/components/smtp/test_config_flow.py deleted file mode 100644 index 59a682393dd..00000000000 --- a/tests/components/smtp/test_config_flow.py +++ /dev/null @@ -1,178 +0,0 @@ -"""Tests for the SMTP config flow.""" - -from copy import deepcopy -from typing import Any -from unittest.mock import AsyncMock, patch - -import pytest - -from homeassistant import config_entries -import homeassistant.components.notify as notify -from homeassistant.components.smtp.const import DOMAIN -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType -from homeassistant.setup import async_setup_component - -from .const import ( - MOCKED_CONFIG_ENTRY_DATA, - MOCKED_USER_ADVANCED_DATA, - MOCKED_USER_BASIC_DATA, -) - -from tests.common import MockConfigEntry - - -@patch( - "homeassistant.components.smtp.notify.MailNotificationService.connection_is_valid", - lambda x: True, -) -async def test_import_entry(hass: HomeAssistant) -> None: - """Test import of a confif entry from yaml.""" - assert await async_setup_component( - hass, - notify.DOMAIN, - { - notify.DOMAIN: [ - { - "name": DOMAIN, - "platform": DOMAIN, - "recipient": "test@example.com", - "sender": "test@example.com", - }, - ] - }, - ) - # Wait for discovery to finish - await hass.async_block_till_done() - assert hass.services.has_service(notify.DOMAIN, DOMAIN) - - -async def test_no_import(hass: HomeAssistant) -> None: - """Test platform setup without config succeeds.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - -@pytest.mark.parametrize( - ("user_input", "advanced_settings"), - [(MOCKED_USER_BASIC_DATA, False), (MOCKED_USER_ADVANCED_DATA, True)], -) -async def test_form( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - user_input: dict[str, Any], - advanced_settings: bool, -) -> None: - """Test we get the form.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_USER, - "show_advanced_options": advanced_settings, - }, - ) - assert result["type"] == FlowResultType.FORM - assert result["errors"] == {} - - def _connection_is_valid(errors: dict[str, str] | None = None) -> bool: - """Check for valid config, verify connectivity.""" - return True - - with patch( - "homeassistant.components.smtp.notify.MailNotificationService.connection_is_valid", - side_effect=_connection_is_valid, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input - ) - await hass.async_block_till_done() - - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "smtp" - assert result2["data"] == MOCKED_CONFIG_ENTRY_DATA - assert len(mock_setup_entry.mock_calls) == 1 - - -@pytest.mark.parametrize( - ("mocked_user_input", "errors"), - [ - ( - {"recipient": ["test@example.com", "not_a_valid_email"]}, - {"recipient": "invalid_email_address"}, - ), - ({"sender": "not_a_valid_email"}, {"sender": "invalid_email_address"}), - ({"username": "someuser"}, {"password": "username_and_password"}), - ({"password": "somepassword"}, {"username": "username_and_password"}), - ], -) -async def test_invalid_form( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mocked_user_input: dict[str, Any], - errors: dict[str, str], -) -> None: - """Test form validation works.""" - user_input = deepcopy(MOCKED_USER_BASIC_DATA) - user_input.update(mocked_user_input) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USER}, - ) - assert result["type"] == FlowResultType.FORM - assert result["errors"] == {} - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input - ) - await hass.async_block_till_done() - - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == errors - assert len(mock_setup_entry.mock_calls) == 0 - - -async def test_entry_already_configured(hass: HomeAssistant) -> None: - """Test aborting if the entry is already configured.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCKED_CONFIG_ENTRY_DATA) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == FlowResultType.FORM - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - MOCKED_USER_BASIC_DATA, - ) - await hass.async_block_till_done() - - assert result2["type"] == FlowResultType.ABORT - assert result2["reason"] == "already_configured" - - -@pytest.mark.parametrize("error", ["authentication_failed", "connection_refused"]) -async def test_form_invalid_auth_or_connection_refused( - hass: HomeAssistant, error: str -) -> None: - """Test we handle invalid auth.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - def _connection_is_valid(errors: dict[str, str] | None = None) -> bool: - """Check for valid config, verify connectivity.""" - errors["base"] = error - return False - - with patch( - "homeassistant.components.smtp.notify.MailNotificationService.connection_is_valid", - side_effect=_connection_is_valid, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCKED_USER_BASIC_DATA - ) - - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == { - "base": error, - } diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py index 55676f55a70..182b45d9c1b 100644 --- a/tests/components/smtp/test_notify.py +++ b/tests/components/smtp/test_notify.py @@ -1,20 +1,20 @@ """The tests for the notify smtp platform.""" -from copy import deepcopy from pathlib import Path import re from unittest.mock import patch import pytest +from homeassistant import config as hass_config import homeassistant.components.notify as notify from homeassistant.components.smtp.const import DOMAIN from homeassistant.components.smtp.notify import MailNotificationService +from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError +from homeassistant.setup import async_setup_component -from .const import MOCKED_CONFIG_ENTRY_DATA - -from tests.common import MockConfigEntry +from tests.common import get_fixture_path class MockSMTP(MailNotificationService): @@ -25,6 +25,46 @@ class MockSMTP(MailNotificationService): return msg.as_string(), recipients +async def test_reload_notify(hass: HomeAssistant) -> None: + """Verify we can reload the notify service.""" + + with patch( + "homeassistant.components.smtp.notify.MailNotificationService.connection_is_valid" + ): + assert await async_setup_component( + hass, + notify.DOMAIN, + { + notify.DOMAIN: [ + { + "name": DOMAIN, + "platform": DOMAIN, + "recipient": "test@example.com", + "sender": "test@example.com", + }, + ] + }, + ) + await hass.async_block_till_done() + + assert hass.services.has_service(notify.DOMAIN, DOMAIN) + + yaml_path = get_fixture_path("configuration.yaml", "smtp") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch( + "homeassistant.components.smtp.notify.MailNotificationService.connection_is_valid" + ): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert not hass.services.has_service(notify.DOMAIN, DOMAIN) + assert hass.services.has_service(notify.DOMAIN, "smtp_reloaded") + + @pytest.fixture def message(): """Return MockSMTP object with test data.""" @@ -83,35 +123,6 @@ EMAIL_DATA = [ ] -@patch( - "homeassistant.components.smtp.notify.MailNotificationService.connection_is_valid", - lambda x: True, -) -async def test_reload_smtp(hass: HomeAssistant) -> None: - """Verify we can reload a smtp config entry.""" - data = deepcopy(MOCKED_CONFIG_ENTRY_DATA) - entry = MockConfigEntry(domain=DOMAIN, data=data) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - # Wait for discovery to finish - await hass.async_block_till_done() - - assert hass.services.has_service(notify.DOMAIN, DOMAIN) - await hass.config_entries.async_reload(entry.entry_id) - assert not hass.services.has_service(notify.DOMAIN, DOMAIN) - await hass.async_block_till_done() - # Wait for discovery to finish - assert hass.services.has_service(notify.DOMAIN, DOMAIN) - - # Unloading the entry should remove the service - await hass.config_entries.async_unload(entry.entry_id) - assert not hass.services.has_service(notify.DOMAIN, DOMAIN) - await hass.config_entries.async_setup(entry.entry_id) - # Wait for discovery to finish - await hass.async_block_till_done() - assert hass.services.has_service(notify.DOMAIN, DOMAIN) - - @pytest.mark.parametrize( ("message_data", "data", "content_type"), EMAIL_DATA,