diff --git a/homeassistant/components/sql/__init__.py b/homeassistant/components/sql/__init__.py index 63f81c784be..bba49c415f8 100644 --- a/homeassistant/components/sql/__init__.py +++ b/homeassistant/components/sql/__init__.py @@ -1,10 +1,48 @@ """The sql component.""" from __future__ import annotations -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +import voluptuous as vol -from .const import PLATFORMS +from homeassistant.components.recorder import CONF_DB_URL +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_NAME, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, + 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_COLUMN_NAME, CONF_QUERY, DOMAIN, PLATFORMS + + +def validate_sql_select(value: str) -> str: + """Validate that value is a SQL SELECT query.""" + if not value.lstrip().lower().startswith("select"): + raise vol.Invalid("Only SELECT queries allowed") + return value + + +QUERY_SCHEMA = vol.Schema( + { + vol.Required(CONF_COLUMN_NAME): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_QUERY): vol.All(cv.string, validate_sql_select), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_DB_URL): cv.string, + } +) + +CONFIG_SCHEMA = vol.Schema( + {vol.Optional(DOMAIN): vol.All(cv.ensure_list, [QUERY_SCHEMA])}, + extra=vol.ALLOW_EXTRA, +) async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: @@ -12,6 +50,19 @@ async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None await hass.config_entries.async_reload(entry.entry_id) +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up SQL from yaml config.""" + if (conf := config.get(DOMAIN)) is None: + return True + + for sensor_conf in conf: + await discovery.async_load_platform( + hass, Platform.SENSOR, DOMAIN, sensor_conf, config + ) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SQL from a config entry.""" entry.async_on_unload(entry.add_update_listener(async_update_listener)) diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index d8f4df814fc..cc8bbe672ba 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -80,12 +80,6 @@ class SQLConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return SQLOptionsFlowHandler(config_entry) - async def async_step_import(self, config: dict[str, Any] | None) -> FlowResult: - """Import a configuration from config.yaml.""" - - self._async_abort_entries_match(config) - return await self.async_step_user(user_input=config) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/sql/const.py b/homeassistant/components/sql/const.py index 7dfcc3fba81..2443e617395 100644 --- a/homeassistant/components/sql/const.py +++ b/homeassistant/components/sql/const.py @@ -7,6 +7,5 @@ DOMAIN = "sql" PLATFORMS = [Platform.SENSOR] CONF_COLUMN_NAME = "column" -CONF_QUERIES = "queries" CONF_QUERY = "query" DB_URL_RE = re.compile("//.*:.*@") diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index dfb1e15f052..4469c4c8057 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -9,25 +9,25 @@ import sqlalchemy from sqlalchemy.engine import Result from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import scoped_session, sessionmaker -import voluptuous as vol from homeassistant.components.recorder import CONF_DB_URL, DEFAULT_DB_FILE, DEFAULT_URL -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, - SensorEntity, +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_NAME, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_COLUMN_NAME, CONF_QUERIES, CONF_QUERY, DB_URL_RE, DOMAIN +from .const import CONF_COLUMN_NAME, CONF_QUERY, DB_URL_RE, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -37,62 +37,45 @@ def redact_credentials(data: str) -> str: return DB_URL_RE.sub("//****:****@", data) -_QUERY_SCHEME = vol.Schema( - { - vol.Required(CONF_COLUMN_NAME): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_QUERY): cv.string, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.string, - } -) - -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( - {vol.Required(CONF_QUERIES): [_QUERY_SCHEME], vol.Optional(CONF_DB_URL): cv.string} -) - - async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the SQL sensor platform.""" - _LOGGER.warning( - # SQL config flow added in 2022.4 and should be removed in 2022.6 - "Configuration of the SQL sensor platform in YAML is deprecated and " - "will be removed in Home Assistant 2022.6; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) + """Set up the SQL sensor from yaml.""" + if (conf := discovery_info) is None: + return - default_db_url = DEFAULT_URL.format( - hass_config_path=hass.config.path(DEFAULT_DB_FILE) - ) + name: str = conf[CONF_NAME] + query_str: str = conf[CONF_QUERY] + unit: str | None = conf.get(CONF_UNIT_OF_MEASUREMENT) + value_template: Template | None = conf.get(CONF_VALUE_TEMPLATE) + column_name: str = conf[CONF_COLUMN_NAME] + unique_id: str | None = conf.get(CONF_UNIQUE_ID) + db_url: str | None = conf.get(CONF_DB_URL) - for query in config[CONF_QUERIES]: - new_config = { - CONF_DB_URL: config.get(CONF_DB_URL, default_db_url), - CONF_NAME: query[CONF_NAME], - CONF_QUERY: query[CONF_QUERY], - CONF_UNIT_OF_MEASUREMENT: query.get(CONF_UNIT_OF_MEASUREMENT), - CONF_VALUE_TEMPLATE: query.get(CONF_VALUE_TEMPLATE), - CONF_COLUMN_NAME: query[CONF_COLUMN_NAME], - } - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=new_config, - ) - ) + if value_template is not None: + value_template.hass = hass + + await async_setup_sensor( + hass, + name, + query_str, + column_name, + unit, + value_template, + unique_id, + db_url, + True, + async_add_entities, + ) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the SQL sensor entry.""" + """Set up the SQL sensor from config entry.""" db_url: str = entry.options[CONF_DB_URL] name: str = entry.options[CONF_NAME] @@ -111,12 +94,56 @@ async def async_setup_entry( if value_template is not None: value_template.hass = hass + await async_setup_sensor( + hass, + name, + query_str, + column_name, + unit, + value_template, + entry.entry_id, + db_url, + False, + async_add_entities, + ) + + +async def async_setup_sensor( + hass: HomeAssistant, + name: str, + query_str: str, + column_name: str, + unit: str | None, + value_template: Template | None, + unique_id: str | None, + db_url: str | None, + yaml: bool, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SQL sensor.""" + + if not db_url: + db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE)) + + sess: scoped_session | None = None try: engine = sqlalchemy.create_engine(db_url, future=True) sessmaker = scoped_session(sessionmaker(bind=engine, future=True)) + + # Run a dummy query just to test the db_url + sess = sessmaker() + sess.execute("SELECT 1;") + except SQLAlchemyError as err: - _LOGGER.error("Can not open database %s", {redact_credentials(str(err))}) + _LOGGER.error( + "Couldn't connect using %s DB_URL: %s", + redact_credentials(db_url), + redact_credentials(str(err)), + ) return + finally: + if sess: + sess.close() # MSSQL uses TOP and not LIMIT if not ("LIMIT" in query_str.upper() or "SELECT TOP" in query_str.upper()): @@ -134,7 +161,8 @@ async def async_setup_entry( column_name, unit, value_template, - entry.entry_id, + unique_id, + yaml, ) ], True, @@ -155,22 +183,25 @@ class SQLSensor(SensorEntity): column: str, unit: str | None, value_template: Template | None, - entry_id: str, + unique_id: str | None, + yaml: bool, ) -> None: """Initialize the SQL sensor.""" self._query = query + self._attr_name = name if yaml else None self._attr_native_unit_of_measurement = unit self._template = value_template self._column_name = column self.sessionmaker = sessmaker self._attr_extra_state_attributes = {} - self._attr_unique_id = entry_id - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, entry_id)}, - manufacturer="SQL", - name=name, - ) + self._attr_unique_id = unique_id + if not yaml and unique_id: + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, unique_id)}, + manufacturer="SQL", + name=name, + ) def update(self) -> None: """Retrieve sensor data from the query.""" diff --git a/tests/components/sql/__init__.py b/tests/components/sql/__init__.py index db65034bd11..1c096dfef6a 100644 --- a/tests/components/sql/__init__.py +++ b/tests/components/sql/__init__.py @@ -6,7 +6,12 @@ from typing import Any from homeassistant.components.recorder import CONF_DB_URL from homeassistant.components.sql.const import CONF_COLUMN_NAME, CONF_QUERY, DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT +from homeassistant.const import ( + CONF_NAME, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, +) from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -42,6 +47,36 @@ ENTRY_CONFIG_NO_RESULTS = { CONF_UNIT_OF_MEASUREMENT: "MiB", } +YAML_CONFIG = { + "sql": { + CONF_DB_URL: "sqlite://", + CONF_NAME: "Get Value", + CONF_QUERY: "SELECT 5 as value", + CONF_COLUMN_NAME: "value", + CONF_UNIT_OF_MEASUREMENT: "MiB", + CONF_UNIQUE_ID: "unique_id_12345", + CONF_VALUE_TEMPLATE: "{{ value }}", + } +} + +YAML_CONFIG_INVALID = { + "sql": { + CONF_DB_URL: "sqlite://", + CONF_QUERY: "SELECT 5 as value", + CONF_COLUMN_NAME: "value", + CONF_UNIT_OF_MEASUREMENT: "MiB", + CONF_UNIQUE_ID: "unique_id_12345", + } +} + +YAML_CONFIG_NO_DB = { + "sql": { + CONF_NAME: "Get Value", + CONF_QUERY: "SELECT 5 as value", + CONF_COLUMN_NAME: "value", + } +} + async def init_integration( hass: HomeAssistant, diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index e5bbc163249..d26d3f9ae52 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -1,7 +1,7 @@ """Test the SQL config flow.""" from __future__ import annotations -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from sqlalchemy.exc import SQLAlchemyError @@ -21,7 +21,7 @@ from . import ( from tests.common import MockConfigEntry -async def test_form(recorder_mock, hass: HomeAssistant) -> None: +async def test_form(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -53,57 +53,7 @@ async def test_form(recorder_mock, hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_flow_success(recorder_mock, hass: HomeAssistant) -> None: - """Test a successful import of yaml.""" - - with patch( - "homeassistant.components.sql.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=ENTRY_CONFIG, - ) - await hass.async_block_till_done() - - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Get Value" - assert result2["options"] == { - "db_url": "sqlite://", - "name": "Get Value", - "query": "SELECT 5 as value", - "column": "value", - "unit_of_measurement": "MiB", - "value_template": None, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_flow_already_exist(recorder_mock, hass: HomeAssistant) -> None: - """Test import of yaml already exist.""" - - MockConfigEntry( - domain=DOMAIN, - data=ENTRY_CONFIG, - ).add_to_hass(hass) - - with patch( - "homeassistant.components.sql.async_setup_entry", - return_value=True, - ): - result3 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=ENTRY_CONFIG, - ) - await hass.async_block_till_done() - - assert result3["type"] == FlowResultType.ABORT - assert result3["reason"] == "already_configured" - - -async def test_flow_fails_db_url(recorder_mock, hass: HomeAssistant) -> None: +async def test_flow_fails_db_url(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test config flow fails incorrect db url.""" result4 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -124,7 +74,9 @@ async def test_flow_fails_db_url(recorder_mock, hass: HomeAssistant) -> None: assert result4["errors"] == {"db_url": "db_url_invalid"} -async def test_flow_fails_invalid_query(recorder_mock, hass: HomeAssistant) -> None: +async def test_flow_fails_invalid_query( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: """Test config flow fails incorrect db url.""" result4 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -170,7 +122,7 @@ async def test_flow_fails_invalid_query(recorder_mock, hass: HomeAssistant) -> N } -async def test_options_flow(recorder_mock, hass: HomeAssistant) -> None: +async def test_options_flow(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test options config flow.""" entry = MockConfigEntry( domain=DOMAIN, @@ -219,7 +171,7 @@ async def test_options_flow(recorder_mock, hass: HomeAssistant) -> None: async def test_options_flow_name_previously_removed( - recorder_mock, hass: HomeAssistant + recorder_mock: AsyncMock, hass: HomeAssistant ) -> None: """Test options config flow where the name was missing.""" entry = MockConfigEntry( @@ -270,7 +222,9 @@ async def test_options_flow_name_previously_removed( } -async def test_options_flow_fails_db_url(recorder_mock, hass: HomeAssistant) -> None: +async def test_options_flow_fails_db_url( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: """Test options flow fails incorrect db url.""" entry = MockConfigEntry( domain=DOMAIN, @@ -313,7 +267,7 @@ async def test_options_flow_fails_db_url(recorder_mock, hass: HomeAssistant) -> async def test_options_flow_fails_invalid_query( - recorder_mock, hass: HomeAssistant + recorder_mock: AsyncMock, hass: HomeAssistant ) -> None: """Test options flow fails incorrect query and template.""" entry = MockConfigEntry( @@ -369,7 +323,9 @@ async def test_options_flow_fails_invalid_query( } -async def test_options_flow_db_url_empty(recorder_mock, hass: HomeAssistant) -> None: +async def test_options_flow_db_url_empty( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: """Test options config flow with leaving db_url empty.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/sql/test_init.py b/tests/components/sql/test_init.py index 5c3f237ac49..46693195669 100644 --- a/tests/components/sql/test_init.py +++ b/tests/components/sql/test_init.py @@ -1,17 +1,27 @@ """Test for SQL component Init.""" +from __future__ import annotations + +from unittest.mock import AsyncMock, patch + +import pytest +import voluptuous as vol + from homeassistant import config_entries +from homeassistant.components.sql import validate_sql_select +from homeassistant.components.sql.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component -from . import init_integration +from . import YAML_CONFIG_INVALID, YAML_CONFIG_NO_DB, init_integration -async def test_setup_entry(hass: HomeAssistant) -> None: +async def test_setup_entry(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test setup entry.""" config_entry = await init_integration(hass) assert config_entry.state == config_entries.ConfigEntryState.LOADED -async def test_unload_entry(hass: HomeAssistant) -> None: +async def test_unload_entry(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test unload an entry.""" config_entry = await init_integration(hass) assert config_entry.state == config_entries.ConfigEntryState.LOADED @@ -19,3 +29,29 @@ async def test_unload_entry(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED + + +async def test_setup_config(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: + """Test setup from yaml config.""" + with patch( + "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", + ): + assert await async_setup_component(hass, DOMAIN, YAML_CONFIG_NO_DB) + await hass.async_block_till_done() + + +async def test_setup_invalid_config( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: + """Test setup from yaml with invalid config.""" + with patch( + "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", + ): + assert not await async_setup_component(hass, DOMAIN, YAML_CONFIG_INVALID) + await hass.async_block_till_done() + + +async def test_invalid_query(hass: HomeAssistant) -> None: + """Test invalid query.""" + with pytest.raises(vol.Invalid): + validate_sql_select("DROP TABLE *") diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index 588e1c824b7..fdcf8fe1a5b 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -1,22 +1,25 @@ """The test for the sql sensor platform.""" -from unittest.mock import patch +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import AsyncMock, patch import pytest from sqlalchemy.exc import SQLAlchemyError from homeassistant.components.sql.const import DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_NAME, STATE_UNKNOWN +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component +from homeassistant.util import dt -from . import init_integration +from . import YAML_CONFIG, init_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed -async def test_query(hass: HomeAssistant) -> None: +async def test_query(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test the SQL sensor.""" config = { "db_url": "sqlite://", @@ -31,31 +34,9 @@ async def test_query(hass: HomeAssistant) -> None: assert state.attributes["value"] == 5 -async def test_import_query(hass: HomeAssistant) -> None: - """Test the SQL sensor.""" - config = { - "sensor": { - "platform": "sql", - "db_url": "sqlite://", - "queries": [ - { - "name": "count_tables", - "query": "SELECT 5 as value", - "column": "value", - } - ], - } - } - - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - assert hass.config_entries.async_entries(DOMAIN) - options = hass.config_entries.async_entries(DOMAIN)[0].options - assert options[CONF_NAME] == "count_tables" - - -async def test_query_value_template(hass: HomeAssistant) -> None: +async def test_query_value_template( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: """Test the SQL sensor.""" config = { "db_url": "sqlite://", @@ -70,7 +51,9 @@ async def test_query_value_template(hass: HomeAssistant) -> None: assert state.state == "5" -async def test_query_value_template_invalid(hass: HomeAssistant) -> None: +async def test_query_value_template_invalid( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: """Test the SQL sensor.""" config = { "db_url": "sqlite://", @@ -85,7 +68,7 @@ async def test_query_value_template_invalid(hass: HomeAssistant) -> None: assert state.state == "5.01" -async def test_query_limit(hass: HomeAssistant) -> None: +async def test_query_limit(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test the SQL sensor with a query containing 'LIMIT' in lowercase.""" config = { "db_url": "sqlite://", @@ -101,7 +84,7 @@ async def test_query_limit(hass: HomeAssistant) -> None: async def test_query_no_value( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + recorder_mock: AsyncMock, hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test the SQL sensor with a query that returns no value.""" config = { @@ -120,7 +103,7 @@ async def test_query_no_value( async def test_query_mssql_no_result( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + recorder_mock: AsyncMock, hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test the SQL sensor with a query that returns no value.""" config = { @@ -158,6 +141,7 @@ async def test_query_mssql_no_result( ], ) async def test_invalid_url_setup( + recorder_mock: AsyncMock, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, url: str, @@ -218,13 +202,97 @@ async def test_invalid_url_on_update( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - with patch( + with patch("homeassistant.components.recorder",), patch( "homeassistant.components.sql.sensor.sqlalchemy.engine.cursor.CursorResult", side_effect=SQLAlchemyError( "sqlite://homeassistant:hunter2@homeassistant.local" ), ): - await async_update_entity(hass, "sensor.count_tables") + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=1), + ) + await hass.async_block_till_done() assert "sqlite://homeassistant:hunter2@homeassistant.local" not in caplog.text assert "sqlite://****:****@homeassistant.local" in caplog.text + + +async def test_query_from_yaml(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: + """Test the SQL sensor from yaml config.""" + + assert await async_setup_component(hass, DOMAIN, YAML_CONFIG) + await hass.async_block_till_done() + + state = hass.states.get("sensor.get_value") + assert state.state == "5" + + +async def test_config_from_old_yaml( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: + """Test the SQL sensor from old yaml config does not create any entity.""" + config = { + "sensor": { + "platform": "sql", + "db_url": "sqlite://", + "queries": [ + { + "name": "count_tables", + "query": "SELECT 5 as value", + "column": "value", + } + ], + } + } + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.count_tables") + assert not state + + +@pytest.mark.parametrize( + "url,expected_patterns,not_expected_patterns", + [ + ( + "sqlite://homeassistant:hunter2@homeassistant.local", + ["sqlite://****:****@homeassistant.local"], + ["sqlite://homeassistant:hunter2@homeassistant.local"], + ), + ( + "sqlite://homeassistant.local", + ["sqlite://homeassistant.local"], + [], + ), + ], +) +async def test_invalid_url_setup_from_yaml( + recorder_mock: AsyncMock, + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + url: str, + expected_patterns: str, + not_expected_patterns: str, +): + """Test invalid db url with redacted credentials from yaml setup.""" + config = { + "sql": { + "db_url": url, + "query": "SELECT 5 as value", + "column": "value", + "name": "count_tables", + } + } + + with patch( + "homeassistant.components.sql.sensor.sqlalchemy.create_engine", + side_effect=SQLAlchemyError(url), + ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + for pattern in not_expected_patterns: + assert pattern not in caplog.text + for pattern in expected_patterns: + assert pattern in caplog.text