From 107e1ed16ca422f13c5512114c419b53a9478454 Mon Sep 17 00:00:00 2001 From: CharlB <41644590+CharlieBailly@users.noreply.github.com> Date: Wed, 12 Oct 2022 11:27:46 +0200 Subject: [PATCH] Fix, improve input validation and add tests to ClickSend tts (#76669) Co-authored-by: Martin Hjelmare --- .../components/clicksend_tts/notify.py | 22 ++-- tests/components/clicksend_tts/__init__.py | 1 + tests/components/clicksend_tts/test_notify.py | 122 ++++++++++++++++++ 3 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 tests/components/clicksend_tts/__init__.py create mode 100644 tests/components/clicksend_tts/test_notify.py diff --git a/homeassistant/components/clicksend_tts/notify.py b/homeassistant/components/clicksend_tts/notify.py index 8026c8e150b..5ff38c41fc9 100644 --- a/homeassistant/components/clicksend_tts/notify.py +++ b/homeassistant/components/clicksend_tts/notify.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import ( CONF_API_KEY, + CONF_NAME, CONF_RECIPIENT, CONF_USERNAME, CONTENT_TYPE_JSON, @@ -23,20 +24,27 @@ HEADERS = {"Content-Type": CONTENT_TYPE_JSON} CONF_LANGUAGE = "language" CONF_VOICE = "voice" -CONF_CALLER = "caller" +MALE_VOICE = "male" +FEMALE_VOICE = "female" + +DEFAULT_NAME = "clicksend_tts" DEFAULT_LANGUAGE = "en-us" -DEFAULT_VOICE = "female" +DEFAULT_VOICE = FEMALE_VOICE TIMEOUT = 5 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_RECIPIENT): cv.string, + vol.Required(CONF_RECIPIENT): vol.All( + cv.string, vol.Match(r"^\+?[1-9]\d{1,14}$") + ), vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): cv.string, - vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): cv.string, - vol.Optional(CONF_CALLER): cv.string, + vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In( + [MALE_VOICE, FEMALE_VOICE] + ), } ) @@ -60,9 +68,6 @@ class ClicksendNotificationService(BaseNotificationService): self.recipient = config[CONF_RECIPIENT] self.language = config[CONF_LANGUAGE] self.voice = config[CONF_VOICE] - self.caller = config.get(CONF_CALLER) - if self.caller is None: - self.caller = self.recipient def send_message(self, message="", **kwargs): """Send a voice call to a user.""" @@ -70,7 +75,6 @@ class ClicksendNotificationService(BaseNotificationService): "messages": [ { "source": "hass.notify", - "from": self.caller, "to": self.recipient, "body": message, "lang": self.language, diff --git a/tests/components/clicksend_tts/__init__.py b/tests/components/clicksend_tts/__init__.py new file mode 100644 index 00000000000..c822773ef70 --- /dev/null +++ b/tests/components/clicksend_tts/__init__.py @@ -0,0 +1 @@ +"""Tests for the ClickSend TTS component.""" diff --git a/tests/components/clicksend_tts/test_notify.py b/tests/components/clicksend_tts/test_notify.py new file mode 100644 index 00000000000..9bebb3cfbca --- /dev/null +++ b/tests/components/clicksend_tts/test_notify.py @@ -0,0 +1,122 @@ +"""The test for the Facebook notify module.""" +import base64 +from http import HTTPStatus +import logging +from unittest.mock import patch + +import pytest +import requests_mock + +from homeassistant.components import notify +import homeassistant.components.clicksend_tts.notify as cs_tts +from homeassistant.setup import async_setup_component + +from tests.common import assert_setup_component + +# Infos from https://developers.clicksend.com/docs/rest/v3/#testing +TEST_USERNAME = "nocredit" +TEST_API_KEY = "D83DED51-9E35-4D42-9BB9-0E34B7CA85AE" +TEST_VOICE_NUMBER = "+61411111111" + +TEST_VOICE = "male" +TEST_LANGUAGE = "fr-fr" +TEST_MESSAGE = "Just a test message!" + + +CONFIG = { + notify.DOMAIN: { + "platform": "clicksend_tts", + cs_tts.CONF_USERNAME: TEST_USERNAME, + cs_tts.CONF_API_KEY: TEST_API_KEY, + cs_tts.CONF_RECIPIENT: TEST_VOICE_NUMBER, + cs_tts.CONF_LANGUAGE: TEST_LANGUAGE, + cs_tts.CONF_VOICE: TEST_VOICE, + } +} + + +@pytest.fixture +def mock_clicksend_tts_notify(): + """Mock Clicksend TTS notify service.""" + with patch( + "homeassistant.components.clicksend_tts.notify.get_service", autospec=True + ) as ns: + yield ns + + +async def setup_notify(hass): + """Test setup.""" + with assert_setup_component(1, notify.DOMAIN) as config: + assert await async_setup_component(hass, notify.DOMAIN, CONFIG) + assert config[notify.DOMAIN] + await hass.async_block_till_done() + + +async def test_no_notify_service(hass, mock_clicksend_tts_notify, caplog): + """Test missing platform notify service instance.""" + caplog.set_level(logging.ERROR) + mock_clicksend_tts_notify.return_value = None + await setup_notify(hass) + await hass.async_block_till_done() + assert mock_clicksend_tts_notify.called + assert "Failed to initialize notification service clicksend_tts" in caplog.text + + +async def test_send_simple_message(hass): + """Test sending a simple message with success.""" + + with requests_mock.Mocker() as mock: + # Mocking authentication endpoint + mock.get( + f"{cs_tts.BASE_API_URL}/account", + status_code=HTTPStatus.OK, + ) + + # Mocking TTS endpoint + mock.post( + f"{cs_tts.BASE_API_URL}/voice/send", + status_code=HTTPStatus.OK, + ) + + # Setting up integration + await setup_notify(hass) + + # Sending message + data = { + notify.ATTR_MESSAGE: TEST_MESSAGE, + } + await hass.services.async_call( + notify.DOMAIN, cs_tts.DEFAULT_NAME, data, blocking=True + ) + + # Checking if everything went well + assert mock.called + assert mock.call_count == 2 + + expected_body = { + "messages": [ + { + "source": "hass.notify", + "to": TEST_VOICE_NUMBER, + "body": TEST_MESSAGE, + "lang": TEST_LANGUAGE, + "voice": TEST_VOICE, + } + ] + } + assert mock.last_request.json() == expected_body + + expected_content_type = "application/json" + assert ( + "Content-Type" in mock.last_request.headers.keys() + and mock.last_request.headers["Content-Type"] == expected_content_type + ) + + encoded_auth = base64.b64encode( + f"{TEST_USERNAME}:{TEST_API_KEY}".encode() + ).decode() + expected_auth = f"Basic {encoded_auth}" + assert ( + "Authorization" in mock.last_request.headers + and mock.last_request.headers["Authorization"] == expected_auth + )