diff --git a/homeassistant/components/generic/diagnostics.py b/homeassistant/components/generic/diagnostics.py new file mode 100644 index 00000000000..00be287f053 --- /dev/null +++ b/homeassistant/components/generic/diagnostics.py @@ -0,0 +1,49 @@ +"""Diagnostics support for generic (IP camera).""" +from __future__ import annotations + +from typing import Any + +import yarl + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from .const import CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE + +TO_REDACT = { + CONF_PASSWORD, + CONF_USERNAME, +} + + +# A very similar redact function is in components.sql. Possible to be made common. +def redact_url(data: str) -> str: + """Redact credentials from string url.""" + url_in = yarl.URL(data) + if url_in.user: + url = url_in.with_user("****") + if url_in.password: + url = url.with_password("****") + if url_in.path: + url = url.with_path("****") + if url_in.query_string: + url = url.with_query("****=****") + return str(url) + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + options = async_redact_data(entry.options, TO_REDACT) + for key in (CONF_STREAM_SOURCE, CONF_STILL_IMAGE_URL): + if (value := options.get(key)) is not None: + options[key] = redact_url(value) + + return { + "title": entry.title, + "data": async_redact_data(entry.data, TO_REDACT), + "options": options, + } diff --git a/tests/components/generic/conftest.py b/tests/components/generic/conftest.py index 9daa3574e6e..dc5c545869b 100644 --- a/tests/components/generic/conftest.py +++ b/tests/components/generic/conftest.py @@ -10,6 +10,8 @@ import respx from homeassistant import config_entries, setup from homeassistant.components.generic.const import DOMAIN +from tests.common import MockConfigEntry + @pytest.fixture(scope="package") def fakeimgbytes_png(): @@ -79,3 +81,36 @@ async def user_flow(hass): assert result["errors"] == {} return result + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(hass): + """Define a config entry fixture.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Test Camera", + unique_id="abc123", + data={}, + options={ + "still_image_url": "http://joebloggs:letmein1@example.com/secret1/file.jpg?pw=qwerty", + "stream_source": "http://janebloggs:letmein2@example.com/stream", + "username": "johnbloggs", + "password": "letmein123", + "limit_refetch_to_url_change": False, + "authentication": "basic", + "framerate": 2.0, + "verify_ssl": True, + "content_type": "image/jpeg", + }, + version=1, + ) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture +async def setup_entry(hass, config_entry): + """Set up a config entry ready to be used in tests.""" + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + return config_entry diff --git a/tests/components/generic/test_diagnostics.py b/tests/components/generic/test_diagnostics.py new file mode 100644 index 00000000000..2d4e4c536d8 --- /dev/null +++ b/tests/components/generic/test_diagnostics.py @@ -0,0 +1,24 @@ +"""Test generic (IP camera) diagnostics.""" +from homeassistant.components.diagnostics import REDACTED + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_entry_diagnostics(hass, hass_client, setup_entry): + """Test config entry diagnostics.""" + + assert await get_diagnostics_for_config_entry(hass, hass_client, setup_entry) == { + "title": "Test Camera", + "data": {}, + "options": { + "still_image_url": "http://****:****@example.com/****?****=****", + "stream_source": "http://****:****@example.com/****", + "username": REDACTED, + "password": REDACTED, + "limit_refetch_to_url_change": False, + "authentication": "basic", + "framerate": 2.0, + "verify_ssl": True, + "content_type": "image/jpeg", + }, + }