From 0916701a0b3107c0e436d5cd061543b38e9b49c6 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 12 Apr 2023 06:51:41 +0200 Subject: [PATCH] Catch ssl errors in rest (#91074) * catch ssl.SSLError * add test * fail setup on ssl error * adjust tests --- .../components/rest/binary_sensor.py | 12 ++++++ homeassistant/components/rest/data.py | 9 ++++ homeassistant/components/rest/sensor.py | 8 ++++ tests/components/rest/test_binary_sensor.py | 23 ++++++++++ tests/components/rest/test_init.py | 42 +++++++++++++++++++ tests/components/rest/test_sensor.py | 23 ++++++++++ 6 files changed, 117 insertions(+) diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 320413a10a0..60d9a2d8504 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -1,6 +1,9 @@ """Support for RESTful binary sensors.""" from __future__ import annotations +import logging +import ssl + import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -31,6 +34,8 @@ from .data import RestData from .entity import RestEntity from .schema import BINARY_SENSOR_SCHEMA, RESOURCE_SCHEMA +_LOGGER = logging.getLogger(__name__) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({**RESOURCE_SCHEMA, **BINARY_SENSOR_SCHEMA}) PLATFORM_SCHEMA = vol.All( @@ -59,6 +64,13 @@ async def async_setup_platform( if rest.data is None: if rest.last_exception: + if isinstance(rest.last_exception, ssl.SSLError): + _LOGGER.error( + "Error connecting %s failed with %s", + conf[CONF_RESOURCE], + rest.last_exception, + ) + return raise PlatformNotReady from rest.last_exception raise PlatformNotReady diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 7a5d62694b9..a6501c769d2 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +import ssl import httpx @@ -88,3 +89,11 @@ class RestData: self.last_exception = ex self.data = None self.headers = None + except ssl.SSLError as ex: + if log_errors: + _LOGGER.error( + "Error connecting to %s failed with %s", self._resource, ex + ) + self.last_exception = ex + self.data = None + self.headers = None diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 07fef4c4eab..ead5a5893f4 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +import ssl from xml.parsers.expat import ExpatError from jsonpath import jsonpath @@ -67,6 +68,13 @@ async def async_setup_platform( if rest.data is None: if rest.last_exception: + if isinstance(rest.last_exception, ssl.SSLError): + _LOGGER.error( + "Error connecting %s failed with %s", + conf[CONF_RESOURCE], + rest.last_exception, + ) + return raise PlatformNotReady from rest.last_exception raise PlatformNotReady diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index d6d02fec379..86bac75de91 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -2,6 +2,7 @@ import asyncio from http import HTTPStatus +import ssl from unittest.mock import MagicMock, patch import httpx @@ -81,6 +82,28 @@ async def test_setup_failed_connect( assert "server offline" in caplog.text +@respx.mock +async def test_setup_fail_on_ssl_erros( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test setup when connection error occurs.""" + respx.get("https://localhost").mock(side_effect=ssl.SSLError("ssl error")) + assert await async_setup_component( + hass, + BINARY_SENSOR_DOMAIN, + { + BINARY_SENSOR_DOMAIN: { + "platform": DOMAIN, + "resource": "https://localhost", + "method": "GET", + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all(BINARY_SENSOR_DOMAIN)) == 0 + assert "ssl error" in caplog.text + + @respx.mock async def test_setup_timeout(hass: HomeAssistant) -> None: """Test setup when connection timeout occurs.""" diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index 79d59be3f44..e19c7dc3cc7 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -3,8 +3,10 @@ import asyncio from datetime import timedelta from http import HTTPStatus +import ssl from unittest.mock import patch +import pytest import respx from homeassistant import config as hass_config @@ -133,6 +135,46 @@ async def test_setup_with_endpoint_timeout_with_recovery(hass: HomeAssistant) -> assert hass.states.get("binary_sensor.binary_sensor2").state == "off" +@respx.mock +async def test_setup_with_ssl_error( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test setup with an ssl error.""" + await async_setup_component(hass, "homeassistant", {}) + + respx.get("https://localhost").mock(side_effect=ssl.SSLError("ssl error")) + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + { + "resource": "https://localhost", + "method": "GET", + "verify_ssl": "false", + "timeout": 30, + "sensor": [ + { + "unit_of_measurement": UnitOfInformation.MEGABYTES, + "name": "sensor1", + "value_template": "{{ value_json.sensor1 }}", + }, + ], + "binary_sensor": [ + { + "name": "binary_sensor1", + "value_template": "{{ value_json.binary_sensor1 }}", + }, + ], + } + ] + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + assert "ssl error" in caplog.text + + @respx.mock async def test_setup_minimum_resource_template(hass: HomeAssistant) -> None: """Test setup with minimum configuration (resource_template).""" diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index e9dc8d2c3b3..3f7625da501 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the REST sensor platform.""" import asyncio from http import HTTPStatus +import ssl from unittest.mock import MagicMock, patch import httpx @@ -77,6 +78,28 @@ async def test_setup_failed_connect( assert "server offline" in caplog.text +@respx.mock +async def test_setup_fail_on_ssl_erros( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test setup when connection error occurs.""" + respx.get("https://localhost").mock(side_effect=ssl.SSLError("ssl error")) + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + { + SENSOR_DOMAIN: { + "platform": DOMAIN, + "resource": "https://localhost", + "method": "GET", + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all(SENSOR_DOMAIN)) == 0 + assert "ssl error" in caplog.text + + @respx.mock async def test_setup_timeout(hass: HomeAssistant) -> None: """Test setup when connection timeout occurs."""