From 949b0e1b65bf4f0d06b78a99aa6ec352f2d202e1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 8 Apr 2022 00:43:09 +0200 Subject: [PATCH] Don't allow in-memory SQLite database (#69616) --- homeassistant/components/recorder/__init__.py | 15 ++++++++- tests/common.py | 20 +++++++++--- tests/components/analytics/test_analytics.py | 12 +++---- tests/components/energy/test_sensor.py | 32 ++++++++++++------- tests/components/energy/test_websocket_api.py | 6 ++-- tests/components/recorder/test_init.py | 8 +++++ tests/components/recorder/test_migrate.py | 22 +++++++------ .../components/recorder/test_websocket_api.py | 2 +- 8 files changed, 77 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 48f565ddb56..91c5fe65812 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -177,6 +177,19 @@ FILTER_SCHEMA = INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend( {vol.Optional(CONF_EXCLUDE, default=EXCLUDE_SCHEMA({})): EXCLUDE_SCHEMA} ) + +ALLOW_IN_MEMORY_DB = False + + +def validate_db_url(db_url: str) -> Any: + """Validate database URL.""" + # Don't allow on-memory sqlite databases + if (db_url == SQLITE_URL_PREFIX or ":memory:" in db_url) and not ALLOW_IN_MEMORY_DB: + raise vol.Invalid("In-memory SQLite database is not supported") + + return db_url + + CONFIG_SCHEMA = vol.Schema( { vol.Optional(DOMAIN, default=dict): vol.All( @@ -190,7 +203,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Coerce(int), vol.Range(min=1) ), vol.Optional(CONF_PURGE_INTERVAL, default=1): cv.positive_int, - vol.Optional(CONF_DB_URL): cv.string, + vol.Optional(CONF_DB_URL): vol.All(cv.string, validate_db_url), vol.Optional( CONF_COMMIT_INTERVAL, default=DEFAULT_COMMIT_INTERVAL ): cv.positive_int, diff --git a/tests/common.py b/tests/common.py index 4920d27fd1c..1ae385f3b5e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -910,10 +910,16 @@ def init_recorder_component(hass, add_config=None): if recorder.CONF_COMMIT_INTERVAL not in config: config[recorder.CONF_COMMIT_INTERVAL] = 0 - with patch("homeassistant.components.recorder.migration.migrate_schema"): + with patch( + "homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", + True, + ), patch("homeassistant.components.recorder.migration.migrate_schema"): assert setup_component(hass, recorder.DOMAIN, {recorder.DOMAIN: config}) assert recorder.DOMAIN in hass.config.components - _LOGGER.info("In-memory recorder successfully started") + _LOGGER.info( + "Test recorder successfully started, database location: %s", + config[recorder.CONF_DB_URL], + ) async def async_init_recorder_component(hass, add_config=None): @@ -924,12 +930,18 @@ async def async_init_recorder_component(hass, add_config=None): if recorder.CONF_COMMIT_INTERVAL not in config: config[recorder.CONF_COMMIT_INTERVAL] = 0 - with patch("homeassistant.components.recorder.migration.migrate_schema"): + with patch( + "homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", + True, + ), patch("homeassistant.components.recorder.migration.migrate_schema"): assert await async_setup_component( hass, recorder.DOMAIN, {recorder.DOMAIN: config} ) assert recorder.DOMAIN in hass.config.components - _LOGGER.info("In-memory recorder successfully started") + _LOGGER.info( + "Test recorder successfully started, database location: %s", + config[recorder.CONF_DB_URL], + ) def mock_restore_cache(hass, states): diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index f36fbdd9d79..a18c59f171f 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -451,15 +451,13 @@ async def test_send_with_no_energy(hass, aioclient_mock): assert "energy" not in postdata -async def test_send_with_no_energy_config(hass, aioclient_mock): +async def test_send_with_no_energy_config(hass, aioclient_mock, recorder_mock): """Test send base preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) - assert await async_setup_component( - hass, "energy", {"recorder": {"db_url": "sqlite://"}} - ) + assert await async_setup_component(hass, "energy", {}) with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex, patch( "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION @@ -475,15 +473,13 @@ async def test_send_with_no_energy_config(hass, aioclient_mock): assert not postdata["energy"]["configured"] -async def test_send_with_energy_config(hass, aioclient_mock): +async def test_send_with_energy_config(hass, aioclient_mock, recorder_mock): """Test send base preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) - assert await async_setup_component( - hass, "energy", {"recorder": {"db_url": "sqlite://"}} - ) + assert await async_setup_component(hass, "energy", {}) with patch("uuid.UUID.hex", new_callable=PropertyMock) as hex, patch( "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 3971df86862..fa350329e97 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -29,12 +29,15 @@ from tests.common import async_init_recorder_component from tests.components.recorder.common import async_wait_recording_done_without_instance -async def setup_integration(hass): +@pytest.fixture +async def setup_integration(recorder_mock): """Set up the integration.""" - assert await async_setup_component( - hass, "energy", {"recorder": {"db_url": "sqlite://"}} - ) - await hass.async_block_till_done() + + async def setup_integration(hass): + assert await async_setup_component(hass, "energy", {}) + await hass.async_block_till_done() + + return setup_integration def get_statistics_for_entity(statistics_results, entity_id): @@ -45,7 +48,7 @@ def get_statistics_for_entity(statistics_results, entity_id): return None -async def test_cost_sensor_no_states(hass, hass_storage) -> None: +async def test_cost_sensor_no_states(hass, hass_storage, setup_integration) -> None: """Test sensors are created.""" energy_data = data.EnergyManager.default_preferences() energy_data["energy_sources"].append( @@ -91,6 +94,7 @@ async def test_cost_sensor_price_entity_total_increasing( hass, hass_storage, hass_ws_client, + setup_integration, initial_energy, initial_cost, price_entity, @@ -294,6 +298,7 @@ async def test_cost_sensor_price_entity_total( hass, hass_storage, hass_ws_client, + setup_integration, initial_energy, initial_cost, price_entity, @@ -500,6 +505,7 @@ async def test_cost_sensor_price_entity_total_no_reset( hass, hass_storage, hass_ws_client, + setup_integration, initial_energy, initial_cost, price_entity, @@ -670,7 +676,7 @@ async def test_cost_sensor_price_entity_total_no_reset( ], ) async def test_cost_sensor_handle_energy_units( - hass, hass_storage, energy_unit, factor + hass, hass_storage, setup_integration, energy_unit, factor ) -> None: """Test energy cost price from sensor entity.""" energy_attributes = { @@ -736,7 +742,7 @@ async def test_cost_sensor_handle_energy_units( ], ) async def test_cost_sensor_handle_price_units( - hass, hass_storage, price_unit, factor + hass, hass_storage, setup_integration, price_unit, factor ) -> None: """Test energy cost price from sensor entity.""" energy_attributes = { @@ -798,7 +804,7 @@ async def test_cost_sensor_handle_price_units( assert state.state == "20.0" -async def test_cost_sensor_handle_gas(hass, hass_storage) -> None: +async def test_cost_sensor_handle_gas(hass, hass_storage, setup_integration) -> None: """Test gas cost price from sensor entity.""" energy_attributes = { ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, @@ -847,7 +853,9 @@ async def test_cost_sensor_handle_gas(hass, hass_storage) -> None: assert state.state == "50.0" -async def test_cost_sensor_handle_gas_kwh(hass, hass_storage) -> None: +async def test_cost_sensor_handle_gas_kwh( + hass, hass_storage, setup_integration +) -> None: """Test gas cost price from sensor entity.""" energy_attributes = { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, @@ -898,7 +906,7 @@ async def test_cost_sensor_handle_gas_kwh(hass, hass_storage) -> None: @pytest.mark.parametrize("state_class", [None]) async def test_cost_sensor_wrong_state_class( - hass, hass_storage, caplog, state_class + hass, hass_storage, setup_integration, caplog, state_class ) -> None: """Test energy sensor rejects sensor with wrong state_class.""" energy_attributes = { @@ -960,7 +968,7 @@ async def test_cost_sensor_wrong_state_class( @pytest.mark.parametrize("state_class", [SensorStateClass.MEASUREMENT]) async def test_cost_sensor_state_class_measurement_no_reset( - hass, hass_storage, caplog, state_class + hass, hass_storage, setup_integration, caplog, state_class ) -> None: """Test energy sensor rejects state_class measurement with no last_reset.""" energy_attributes = { diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index 46c6a5c0fa6..523bb7b102f 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -18,11 +18,9 @@ from tests.components.recorder.common import async_wait_recording_done_without_i @pytest.fixture(autouse=True) -async def setup_integration(hass): +async def setup_integration(hass, recorder_mock): """Set up the integration.""" - assert await async_setup_component( - hass, "energy", {"recorder": {"db_url": "sqlite://"}} - ) + assert await async_setup_component(hass, "energy", {}) @pytest.fixture diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index de1a0b4c8b4..903be6ca2cd 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1394,3 +1394,11 @@ async def test_database_lock_without_instance(hass): assert await instance.lock_database() finally: assert instance.unlock_database() + + +async def test_in_memory_database(hass, caplog): + """Test connecting to an in-memory recorder is not allowed.""" + assert not await async_setup_component( + hass, recorder.DOMAIN, {recorder.DOMAIN: {recorder.CONF_DB_URL: "sqlite://"}} + ) + assert "In-memory SQLite database is not supported" in caplog.text diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 337c0a65c2b..a169cdd8356 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -43,7 +43,7 @@ async def test_schema_update_calls(hass): """Test that schema migrations occur in correct order.""" assert recorder.util.async_migration_in_progress(hass) is False - with patch( + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.create_engine", new=create_engine_test ), patch( "homeassistant.components.recorder.migration._apply_update", @@ -68,8 +68,9 @@ async def test_migration_in_progress(hass): assert recorder.util.async_migration_in_progress(hass) is False with patch( - "homeassistant.components.recorder.create_engine", new=create_engine_test - ): + "homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", + True, + ), patch("homeassistant.components.recorder.create_engine", new=create_engine_test): await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -84,7 +85,7 @@ async def test_database_migration_failed(hass): """Test we notify if the migration fails.""" assert recorder.util.async_migration_in_progress(hass) is False - with patch( + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.create_engine", new=create_engine_test ), patch( "homeassistant.components.recorder.migration._apply_update", @@ -117,7 +118,7 @@ async def test_database_migration_encounters_corruption(hass): sqlite3_exception = DatabaseError("statement", {}, []) sqlite3_exception.__cause__ = sqlite3.DatabaseError() - with patch( + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.migration.schema_is_current", side_effect=[False, True], ), patch( @@ -141,7 +142,7 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): """Test we fail on database error when we cannot recover.""" assert recorder.util.async_migration_in_progress(hass) is False - with patch( + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.migration.schema_is_current", side_effect=[False, True], ), patch( @@ -176,8 +177,9 @@ async def test_events_during_migration_are_queued(hass): assert recorder.util.async_migration_in_progress(hass) is False with patch( - "homeassistant.components.recorder.create_engine", new=create_engine_test - ): + "homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", + True, + ), patch("homeassistant.components.recorder.create_engine", new=create_engine_test): await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -200,7 +202,7 @@ async def test_events_during_migration_queue_exhausted(hass): assert recorder.util.async_migration_in_progress(hass) is False - with patch( + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.create_engine", new=create_engine_test ), patch.object(recorder, "MAX_QUEUE_BACKLOG", 1): await async_setup_component( @@ -283,7 +285,7 @@ async def test_schema_migrate(hass, start_version): migration_version = res.schema_version migration_done.set() - with patch( + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.create_engine", new=_create_engine_test ), patch( "homeassistant.components.recorder.Recorder._setup_run", diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 3b09350417b..8385136967c 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -332,7 +332,7 @@ async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client): migration_done.wait() return real_migration(*args) - with patch( + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.Recorder.async_periodic_statistics" ), patch( "homeassistant.components.recorder.create_engine", new=create_engine_test