Add expiration timestamp to cert_expiry sensors (#36399)

* Add expiration timestamp to cert_expiry sensors

* Clear timestamp if cert becomes invalid

* Use timezone-aware timestamps

* Use DataUpdateCoordinator, split timestamp to separate sensor

* Use UTC, simpler add/remove handling

* Review fixes

* Fix incomplete mock that fails in 3.8

* Use static timestamps, improve helper method name
This commit is contained in:
jjlawren 2020-06-18 11:29:46 -05:00 committed by GitHub
parent f69fc79fd1
commit e92e26b73a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 266 additions and 142 deletions

View File

@ -1,6 +1,20 @@
"""The cert_expiry component.""" """The cert_expiry component."""
from datetime import timedelta
import logging
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DEFAULT_PORT, DOMAIN
from .errors import TemporaryFailure, ValidationFailure
from .helper import get_cert_expiry_timestamp
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(hours=12)
async def async_setup(hass, config): async def async_setup(hass, config):
@ -10,6 +24,20 @@ async def async_setup(hass, config):
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Load the saved entities.""" """Load the saved entities."""
host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]
coordinator = CertExpiryDataUpdateCoordinator(hass, host, port)
await coordinator.async_refresh()
if not coordinator.last_update_success:
raise ConfigEntryNotReady
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
if entry.unique_id is None:
hass.config_entries.async_update_entry(entry, unique_id=f"{host}:{port}")
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "sensor") hass.config_entries.async_forward_entry_setup(entry, "sensor")
@ -20,3 +48,37 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
async def async_unload_entry(hass, entry): async def async_unload_entry(hass, entry):
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_forward_entry_unload(entry, "sensor") return await hass.config_entries.async_forward_entry_unload(entry, "sensor")
class CertExpiryDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching Cert Expiry data from single endpoint."""
def __init__(self, hass, host, port):
"""Initialize global Cert Expiry data updater."""
self.host = host
self.port = port
self.cert_error = None
self.is_cert_valid = False
display_port = f":{port}" if port != DEFAULT_PORT else ""
name = f"{self.host}{display_port}"
super().__init__(
hass, _LOGGER, name=name, update_interval=SCAN_INTERVAL,
)
async def _async_update_data(self):
"""Fetch certificate."""
try:
timestamp = await get_cert_expiry_timestamp(self.hass, self.host, self.port)
except TemporaryFailure as err:
raise UpdateFailed(err.args[0])
except ValidationFailure as err:
self.cert_error = err
self.is_cert_valid = False
_LOGGER.error("Certificate validation error: %s [%s]", self.host, err)
return None
self.cert_error = None
self.is_cert_valid = True
return timestamp

View File

@ -13,7 +13,7 @@ from .errors import (
ResolveFailed, ResolveFailed,
ValidationFailure, ValidationFailure,
) )
from .helper import get_cert_time_to_expiry from .helper import get_cert_expiry_timestamp
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -31,7 +31,7 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def _test_connection(self, user_input=None): async def _test_connection(self, user_input=None):
"""Test connection to the server and try to get the certificate.""" """Test connection to the server and try to get the certificate."""
try: try:
await get_cert_time_to_expiry( await get_cert_expiry_timestamp(
self.hass, self.hass,
user_input[CONF_HOST], user_input[CONF_HOST],
user_input.get(CONF_PORT, DEFAULT_PORT), user_input.get(CONF_PORT, DEFAULT_PORT),

View File

@ -1,8 +1,9 @@
"""Helper functions for the Cert Expiry platform.""" """Helper functions for the Cert Expiry platform."""
from datetime import datetime
import socket import socket
import ssl import ssl
from homeassistant.util import dt
from .const import TIMEOUT from .const import TIMEOUT
from .errors import ( from .errors import (
ConnectionRefused, ConnectionRefused,
@ -23,8 +24,8 @@ def get_cert(host, port):
return cert return cert
async def get_cert_time_to_expiry(hass, hostname, port): async def get_cert_expiry_timestamp(hass, hostname, port):
"""Return the certificate's time to expiry in days.""" """Return the certificate's expiration timestamp."""
try: try:
cert = await hass.async_add_executor_job(get_cert, hostname, port) cert = await hass.async_add_executor_job(get_cert, hostname, port)
except socket.gaierror: except socket.gaierror:
@ -39,6 +40,4 @@ async def get_cert_time_to_expiry(hass, hostname, port):
raise ValidationFailure(err.args[0]) raise ValidationFailure(err.args[0])
ts_seconds = ssl.cert_time_to_seconds(cert["notAfter"]) ts_seconds = ssl.cert_time_to_seconds(cert["notAfter"])
timestamp = datetime.fromtimestamp(ts_seconds) return dt.utc_from_timestamp(ts_seconds)
expiry = timestamp - datetime.today()
return expiry.days

View File

@ -9,18 +9,17 @@ from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_PORT, CONF_PORT,
DEVICE_CLASS_TIMESTAMP,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_START,
TIME_DAYS, TIME_DAYS,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from homeassistant.util import dt
from .const import DEFAULT_PORT, DOMAIN from .const import DEFAULT_PORT, DOMAIN
from .errors import TemporaryFailure, ValidationFailure
from .helper import get_cert_time_to_expiry
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -56,63 +55,37 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_entry(hass, entry, async_add_entities):
"""Add cert-expiry entry.""" """Add cert-expiry entry."""
days = 0 coordinator = hass.data[DOMAIN][entry.entry_id]
error = None
hostname = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]
if entry.unique_id is None: sensors = [
hass.config_entries.async_update_entry(entry, unique_id=f"{hostname}:{port}") SSLCertificateDays(coordinator),
SSLCertificateTimestamp(coordinator),
]
try: async_add_entities(sensors, True)
days = await get_cert_time_to_expiry(hass, hostname, port)
except TemporaryFailure as err:
_LOGGER.error(err)
raise PlatformNotReady
except ValidationFailure as err:
error = err
async_add_entities(
[SSLCertificate(hostname, port, days, error)], False,
)
return True
class SSLCertificate(Entity): class CertExpiryEntity(Entity):
"""Implementation of the certificate expiry sensor.""" """Defines a base Cert Expiry entity."""
def __init__(self, server_name, server_port, days, error): def __init__(self, coordinator):
"""Initialize the sensor.""" """Initialize the Cert Expiry entity."""
self.server_name = server_name self.coordinator = coordinator
self.server_port = server_port
display_port = f":{server_port}" if server_port != DEFAULT_PORT else "" async def async_added_to_hass(self):
self._name = f"Cert Expiry ({self.server_name}{display_port})" """Connect to dispatcher listening for entity data notifications."""
self._available = True self.async_on_remove(
self._error = error self.coordinator.async_add_listener(self.async_write_ha_state)
self._state = days )
self._valid = False
if error is None: async def async_update(self):
self._valid = True """Update Cert Expiry entity."""
await self.coordinator.async_request_refresh()
@property @property
def name(self): def available(self):
"""Return the name of the sensor.""" """Return True if entity is available."""
return self._name return self.coordinator.last_update_success
@property
def unique_id(self):
"""Return a unique id for the sensor."""
return f"{self.server_name}:{self.server_port}"
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
return TIME_DAYS
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property @property
def icon(self): def icon(self):
@ -120,42 +93,68 @@ class SSLCertificate(Entity):
return "mdi:certificate" return "mdi:certificate"
@property @property
def available(self): def should_poll(self):
"""Return the availability of the sensor.""" """Return the polling requirement of the entity."""
return self._available return False
async def async_update(self):
"""Fetch the certificate information."""
try:
days_to_expiry = await get_cert_time_to_expiry(
self.hass, self.server_name, self.server_port
)
except TemporaryFailure as err:
_LOGGER.error(err.args[0])
self._available = False
return
except ValidationFailure as err:
_LOGGER.error(
"Certificate validation error: %s [%s]", self.server_name, err
)
self._available = True
self._error = err
self._state = 0
self._valid = False
return
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
"Unknown error checking %s:%s", self.server_name, self.server_port
)
self._available = False
return
self._available = True
self._error = None
self._state = days_to_expiry
self._valid = True
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return additional sensor state attributes.""" """Return additional sensor state attributes."""
return {"is_valid": self._valid, "error": str(self._error)} return {
"is_valid": self.coordinator.is_cert_valid,
"error": str(self.coordinator.cert_error),
}
class SSLCertificateDays(CertExpiryEntity):
"""Implementation of the Cert Expiry days sensor."""
@property
def name(self):
"""Return the name of the sensor."""
return f"Cert Expiry ({self.coordinator.name})"
@property
def state(self):
"""Return the state of the sensor."""
if not self.coordinator.is_cert_valid:
return 0
expiry = self.coordinator.data - dt.utcnow()
return expiry.days
@property
def unique_id(self):
"""Return a unique id for the sensor."""
return f"{self.coordinator.host}:{self.coordinator.port}"
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
return TIME_DAYS
class SSLCertificateTimestamp(CertExpiryEntity):
"""Implementation of the Cert Expiry timestamp sensor."""
@property
def device_class(self):
"""Return the device class of the sensor."""
return DEVICE_CLASS_TIMESTAMP
@property
def name(self):
"""Return the name of the sensor."""
return f"Cert Expiry Timestamp ({self.coordinator.name})"
@property
def state(self):
"""Return the state of the sensor."""
if self.coordinator.data:
return self.coordinator.data.isoformat()
return None
@property
def unique_id(self):
"""Return a unique id for the sensor."""
return f"{self.coordinator.host}:{self.coordinator.port}-timestamp"

View File

@ -0,0 +1,15 @@
"""Helpers for Cert Expiry tests."""
from datetime import datetime, timedelta
from homeassistant.util import dt
def static_datetime():
"""Build a datetime object for testing in the correct timezone."""
return dt.as_utc(datetime(2020, 6, 12, 8, 0, 0))
def future_timestamp(days):
"""Create timestamp object for requested days in future."""
delta = timedelta(days=days, minutes=1)
return static_datetime() + delta

View File

@ -7,6 +7,7 @@ from homeassistant.components.cert_expiry.const import DEFAULT_PORT, DOMAIN
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
from .const import HOST, PORT from .const import HOST, PORT
from .helpers import future_timestamp
from tests.async_mock import patch from tests.async_mock import patch
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -21,7 +22,7 @@ async def test_user(hass):
assert result["step_id"] == "user" assert result["step_id"] == "user"
with patch( with patch(
"homeassistant.components.cert_expiry.config_flow.get_cert_time_to_expiry" "homeassistant.components.cert_expiry.config_flow.get_cert_expiry_timestamp"
): ):
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_HOST: HOST, CONF_PORT: PORT} result["flow_id"], user_input={CONF_HOST: HOST, CONF_PORT: PORT}
@ -65,12 +66,15 @@ async def test_user_with_bad_cert(hass):
async def test_import_host_only(hass): async def test_import_host_only(hass):
"""Test import with host only.""" """Test import with host only."""
with patch( with patch(
"homeassistant.components.cert_expiry.config_flow.get_cert_time_to_expiry", "homeassistant.components.cert_expiry.config_flow.get_cert_expiry_timestamp"
return_value=1, ), patch(
"homeassistant.components.cert_expiry.get_cert_expiry_timestamp",
return_value=future_timestamp(1),
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "import"}, data={CONF_HOST: HOST} DOMAIN, context={"source": "import"}, data={CONF_HOST: HOST}
) )
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == HOST assert result["title"] == HOST
@ -78,21 +82,21 @@ async def test_import_host_only(hass):
assert result["data"][CONF_PORT] == DEFAULT_PORT assert result["data"][CONF_PORT] == DEFAULT_PORT
assert result["result"].unique_id == f"{HOST}:{DEFAULT_PORT}" assert result["result"].unique_id == f"{HOST}:{DEFAULT_PORT}"
with patch("homeassistant.components.cert_expiry.sensor.async_setup_entry"):
await hass.async_block_till_done()
async def test_import_host_and_port(hass): async def test_import_host_and_port(hass):
"""Test import with host and port.""" """Test import with host and port."""
with patch( with patch(
"homeassistant.components.cert_expiry.config_flow.get_cert_time_to_expiry", "homeassistant.components.cert_expiry.config_flow.get_cert_expiry_timestamp"
return_value=1, ), patch(
"homeassistant.components.cert_expiry.get_cert_expiry_timestamp",
return_value=future_timestamp(1),
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": "import"}, context={"source": "import"},
data={CONF_HOST: HOST, CONF_PORT: PORT}, data={CONF_HOST: HOST, CONF_PORT: PORT},
) )
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == HOST assert result["title"] == HOST
@ -100,18 +104,19 @@ async def test_import_host_and_port(hass):
assert result["data"][CONF_PORT] == PORT assert result["data"][CONF_PORT] == PORT
assert result["result"].unique_id == f"{HOST}:{PORT}" assert result["result"].unique_id == f"{HOST}:{PORT}"
with patch("homeassistant.components.cert_expiry.sensor.async_setup_entry"):
await hass.async_block_till_done()
async def test_import_non_default_port(hass): async def test_import_non_default_port(hass):
"""Test import with host and non-default port.""" """Test import with host and non-default port."""
with patch( with patch(
"homeassistant.components.cert_expiry.config_flow.get_cert_time_to_expiry" "homeassistant.components.cert_expiry.config_flow.get_cert_expiry_timestamp"
), patch(
"homeassistant.components.cert_expiry.get_cert_expiry_timestamp",
return_value=future_timestamp(1),
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "import"}, data={CONF_HOST: HOST, CONF_PORT: 888} DOMAIN, context={"source": "import"}, data={CONF_HOST: HOST, CONF_PORT: 888}
) )
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == f"{HOST}:888" assert result["title"] == f"{HOST}:888"
@ -119,21 +124,21 @@ async def test_import_non_default_port(hass):
assert result["data"][CONF_PORT] == 888 assert result["data"][CONF_PORT] == 888
assert result["result"].unique_id == f"{HOST}:888" assert result["result"].unique_id == f"{HOST}:888"
with patch("homeassistant.components.cert_expiry.sensor.async_setup_entry"):
await hass.async_block_till_done()
async def test_import_with_name(hass): async def test_import_with_name(hass):
"""Test import with name (deprecated).""" """Test import with name (deprecated)."""
with patch( with patch(
"homeassistant.components.cert_expiry.config_flow.get_cert_time_to_expiry", "homeassistant.components.cert_expiry.config_flow.get_cert_expiry_timestamp"
return_value=1, ), patch(
"homeassistant.components.cert_expiry.get_cert_expiry_timestamp",
return_value=future_timestamp(1),
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": "import"}, context={"source": "import"},
data={CONF_NAME: "legacy", CONF_HOST: HOST, CONF_PORT: PORT}, data={CONF_NAME: "legacy", CONF_HOST: HOST, CONF_PORT: PORT},
) )
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == HOST assert result["title"] == HOST
@ -141,9 +146,6 @@ async def test_import_with_name(hass):
assert result["data"][CONF_PORT] == PORT assert result["data"][CONF_PORT] == PORT
assert result["result"].unique_id == f"{HOST}:{PORT}" assert result["result"].unique_id == f"{HOST}:{PORT}"
with patch("homeassistant.components.cert_expiry.sensor.async_setup_entry"):
await hass.async_block_till_done()
async def test_bad_import(hass): async def test_bad_import(hass):
"""Test import step.""" """Test import step."""

View File

@ -9,6 +9,7 @@ from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .const import HOST, PORT from .const import HOST, PORT
from .helpers import future_timestamp, static_datetime
from tests.async_mock import patch from tests.async_mock import patch
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
@ -30,11 +31,10 @@ async def test_setup_with_config(hass):
async_fire_time_changed(hass, next_update) async_fire_time_changed(hass, next_update)
with patch( with patch(
"homeassistant.components.cert_expiry.config_flow.get_cert_time_to_expiry", "homeassistant.components.cert_expiry.config_flow.get_cert_expiry_timestamp"
return_value=100,
), patch( ), patch(
"homeassistant.components.cert_expiry.sensor.get_cert_time_to_expiry", "homeassistant.components.cert_expiry.get_cert_expiry_timestamp",
return_value=100, return_value=future_timestamp(1),
): ):
await hass.async_block_till_done() await hass.async_block_till_done()
@ -52,8 +52,8 @@ async def test_update_unique_id(hass):
assert not entry.unique_id assert not entry.unique_id
with patch( with patch(
"homeassistant.components.cert_expiry.sensor.get_cert_time_to_expiry", "homeassistant.components.cert_expiry.get_cert_expiry_timestamp",
return_value=100, return_value=future_timestamp(1),
): ):
assert await async_setup_component(hass, DOMAIN, {}) is True assert await async_setup_component(hass, DOMAIN, {}) is True
await hass.async_block_till_done() await hass.async_block_till_done()
@ -62,7 +62,8 @@ async def test_update_unique_id(hass):
assert entry.unique_id == f"{HOST}:{PORT}" assert entry.unique_id == f"{HOST}:{PORT}"
async def test_unload_config_entry(hass): @patch("homeassistant.util.dt.utcnow", return_value=static_datetime())
async def test_unload_config_entry(mock_now, hass):
"""Test unloading a config entry.""" """Test unloading a config entry."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
@ -76,8 +77,8 @@ async def test_unload_config_entry(hass):
assert entry is config_entries[0] assert entry is config_entries[0]
with patch( with patch(
"homeassistant.components.cert_expiry.sensor.get_cert_time_to_expiry", "homeassistant.components.cert_expiry.get_cert_expiry_timestamp",
return_value=100, return_value=future_timestamp(100),
): ):
assert await async_setup_component(hass, DOMAIN, {}) is True assert await async_setup_component(hass, DOMAIN, {}) is True
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -3,16 +3,19 @@ from datetime import timedelta
import socket import socket
import ssl import ssl
from homeassistant.const import CONF_HOST, CONF_PORT, STATE_UNAVAILABLE from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY
from homeassistant.const import CONF_HOST, CONF_PORT, STATE_UNAVAILABLE, STATE_UNKNOWN
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .const import HOST, PORT from .const import HOST, PORT
from .helpers import future_timestamp, static_datetime
from tests.async_mock import patch from tests.async_mock import patch
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
async def test_async_setup_entry(hass): @patch("homeassistant.util.dt.utcnow", return_value=static_datetime())
async def test_async_setup_entry(mock_now, hass):
"""Test async_setup_entry.""" """Test async_setup_entry."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain="cert_expiry", domain="cert_expiry",
@ -20,9 +23,11 @@ async def test_async_setup_entry(hass):
unique_id=f"{HOST}:{PORT}", unique_id=f"{HOST}:{PORT}",
) )
timestamp = future_timestamp(100)
with patch( with patch(
"homeassistant.components.cert_expiry.sensor.get_cert_time_to_expiry", "homeassistant.components.cert_expiry.get_cert_expiry_timestamp",
return_value=100, return_value=timestamp,
): ):
entry.add_to_hass(hass) entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id) assert await hass.config_entries.async_setup(entry.entry_id)
@ -35,6 +40,13 @@ async def test_async_setup_entry(hass):
assert state.attributes.get("error") == "None" assert state.attributes.get("error") == "None"
assert state.attributes.get("is_valid") assert state.attributes.get("is_valid")
state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
assert state is not None
assert state.state != STATE_UNAVAILABLE
assert state.state == timestamp.isoformat()
assert state.attributes.get("error") == "None"
assert state.attributes.get("is_valid")
async def test_async_setup_entry_bad_cert(hass): async def test_async_setup_entry_bad_cert(hass):
"""Test async_setup_entry with a bad/expired cert.""" """Test async_setup_entry with a bad/expired cert."""
@ -73,11 +85,10 @@ async def test_async_setup_entry_host_unavailable(hass):
side_effect=socket.gaierror, side_effect=socket.gaierror,
): ):
entry.add_to_hass(hass) entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id) assert await hass.config_entries.async_setup(entry.entry_id) is False
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("sensor.cert_expiry_example_com") assert entry.state == ENTRY_STATE_SETUP_RETRY
assert state is None
next_update = dt_util.utcnow() + timedelta(seconds=45) next_update = dt_util.utcnow() + timedelta(seconds=45)
async_fire_time_changed(hass, next_update) async_fire_time_changed(hass, next_update)
@ -91,7 +102,8 @@ async def test_async_setup_entry_host_unavailable(hass):
assert state is None assert state is None
async def test_update_sensor(hass): @patch("homeassistant.util.dt.utcnow", return_value=static_datetime())
async def test_update_sensor(mock_now, hass):
"""Test async_update for sensor.""" """Test async_update for sensor."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain="cert_expiry", domain="cert_expiry",
@ -99,9 +111,11 @@ async def test_update_sensor(hass):
unique_id=f"{HOST}:{PORT}", unique_id=f"{HOST}:{PORT}",
) )
timestamp = future_timestamp(100)
with patch( with patch(
"homeassistant.components.cert_expiry.sensor.get_cert_time_to_expiry", "homeassistant.components.cert_expiry.get_cert_expiry_timestamp",
return_value=100, return_value=timestamp,
): ):
entry.add_to_hass(hass) entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id) assert await hass.config_entries.async_setup(entry.entry_id)
@ -114,12 +128,21 @@ async def test_update_sensor(hass):
assert state.attributes.get("error") == "None" assert state.attributes.get("error") == "None"
assert state.attributes.get("is_valid") assert state.attributes.get("is_valid")
state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
assert state is not None
assert state.state != STATE_UNAVAILABLE
assert state.state == timestamp.isoformat()
assert state.attributes.get("error") == "None"
assert state.attributes.get("is_valid")
timestamp2 = future_timestamp(99)
next_update = dt_util.utcnow() + timedelta(hours=12) next_update = dt_util.utcnow() + timedelta(hours=12)
async_fire_time_changed(hass, next_update) async_fire_time_changed(hass, next_update)
with patch( with patch(
"homeassistant.components.cert_expiry.sensor.get_cert_time_to_expiry", "homeassistant.components.cert_expiry.get_cert_expiry_timestamp",
return_value=99, return_value=timestamp2,
): ):
await hass.async_block_till_done() await hass.async_block_till_done()
@ -130,8 +153,16 @@ async def test_update_sensor(hass):
assert state.attributes.get("error") == "None" assert state.attributes.get("error") == "None"
assert state.attributes.get("is_valid") assert state.attributes.get("is_valid")
state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
assert state is not None
assert state.state != STATE_UNAVAILABLE
assert state.state == timestamp2.isoformat()
assert state.attributes.get("error") == "None"
assert state.attributes.get("is_valid")
async def test_update_sensor_network_errors(hass):
@patch("homeassistant.util.dt.utcnow", return_value=static_datetime())
async def test_update_sensor_network_errors(mock_now, hass):
"""Test async_update for sensor.""" """Test async_update for sensor."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain="cert_expiry", domain="cert_expiry",
@ -139,9 +170,11 @@ async def test_update_sensor_network_errors(hass):
unique_id=f"{HOST}:{PORT}", unique_id=f"{HOST}:{PORT}",
) )
timestamp = future_timestamp(100)
with patch( with patch(
"homeassistant.components.cert_expiry.sensor.get_cert_time_to_expiry", "homeassistant.components.cert_expiry.get_cert_expiry_timestamp",
return_value=100, return_value=timestamp,
): ):
entry.add_to_hass(hass) entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id) assert await hass.config_entries.async_setup(entry.entry_id)
@ -154,6 +187,13 @@ async def test_update_sensor_network_errors(hass):
assert state.attributes.get("error") == "None" assert state.attributes.get("error") == "None"
assert state.attributes.get("is_valid") assert state.attributes.get("is_valid")
state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
assert state is not None
assert state.state != STATE_UNAVAILABLE
assert state.state == timestamp.isoformat()
assert state.attributes.get("error") == "None"
assert state.attributes.get("is_valid")
next_update = dt_util.utcnow() + timedelta(hours=12) next_update = dt_util.utcnow() + timedelta(hours=12)
async_fire_time_changed(hass, next_update) async_fire_time_changed(hass, next_update)
@ -170,8 +210,8 @@ async def test_update_sensor_network_errors(hass):
async_fire_time_changed(hass, next_update) async_fire_time_changed(hass, next_update)
with patch( with patch(
"homeassistant.components.cert_expiry.sensor.get_cert_time_to_expiry", "homeassistant.components.cert_expiry.get_cert_expiry_timestamp",
return_value=99, return_value=future_timestamp(99),
): ):
await hass.async_block_till_done() await hass.async_block_till_done()
@ -198,6 +238,12 @@ async def test_update_sensor_network_errors(hass):
assert state.attributes.get("error") == "something bad" assert state.attributes.get("error") == "something bad"
assert not state.attributes.get("is_valid") assert not state.attributes.get("is_valid")
state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
assert state is not None
assert state.state == STATE_UNKNOWN
assert state.attributes.get("error") == "something bad"
assert not state.attributes.get("is_valid")
next_update = dt_util.utcnow() + timedelta(hours=12) next_update = dt_util.utcnow() + timedelta(hours=12)
async_fire_time_changed(hass, next_update) async_fire_time_changed(hass, next_update)