Validate config entry when adding or updating entity registry entry (#135067)

This commit is contained in:
Erik Montnemery 2025-01-17 19:18:13 +01:00 committed by GitHub
parent 028a0d4eec
commit 235fda55fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 71 additions and 7 deletions

View File

@ -648,6 +648,7 @@ def _validate_item(
domain: str, domain: str,
platform: str, platform: str,
*, *,
config_entry_id: str | None | UndefinedType = None,
device_id: str | None | UndefinedType = None, device_id: str | None | UndefinedType = None,
disabled_by: RegistryEntryDisabler | None | UndefinedType = None, disabled_by: RegistryEntryDisabler | None | UndefinedType = None,
entity_category: EntityCategory | None | UndefinedType = None, entity_category: EntityCategory | None | UndefinedType = None,
@ -672,6 +673,11 @@ def _validate_item(
unique_id, unique_id,
report_issue, report_issue,
) )
if config_entry_id and config_entry_id is not UNDEFINED:
if not hass.config_entries.async_get_entry(config_entry_id):
raise ValueError(
f"Can't link entity to unknown config entry {config_entry_id}"
)
if device_id and device_id is not UNDEFINED: if device_id and device_id is not UNDEFINED:
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
if not device_registry.async_get(device_id): if not device_registry.async_get(device_id):
@ -864,6 +870,7 @@ class EntityRegistry(BaseRegistry):
self.hass, self.hass,
domain, domain,
platform, platform,
config_entry_id=config_entry_id,
device_id=device_id, device_id=device_id,
disabled_by=disabled_by, disabled_by=disabled_by,
entity_category=entity_category, entity_category=entity_category,
@ -1096,6 +1103,7 @@ class EntityRegistry(BaseRegistry):
self.hass, self.hass,
old.domain, old.domain,
old.platform, old.platform,
config_entry_id=config_entry_id,
device_id=device_id, device_id=device_id,
disabled_by=disabled_by, disabled_by=disabled_by,
entity_category=entity_category, entity_category=entity_category,

View File

@ -82,6 +82,7 @@ def _setup_entry(hass: HomeAssistant, config, sensors, unique_id=None):
options={CONF_CONSIDER_HOME: 60}, options={CONF_CONSIDER_HOME: 60},
unique_id=unique_id, unique_id=unique_id,
) )
config_entry.add_to_hass(hass)
# init variable # init variable
obj_prefix = slugify(HOST) obj_prefix = slugify(HOST)
@ -131,8 +132,6 @@ async def _test_sensors(
disabled_by=None, disabled_by=None,
) )
config_entry.add_to_hass(hass)
# initial devices setup # initial devices setup
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -751,6 +751,7 @@ async def test_unique_id_migration(
old_unique_id, old_unique_id,
) -> None: ) -> None:
"""Test that old unique id format is migrated to the new format that supports multiple accounts.""" """Test that old unique id format is migrated to the new format that supports multiple accounts."""
config_entry.add_to_hass(hass)
# Create an entity using the old unique id format # Create an entity using the old unique id format
entity_registry.async_get_or_create( entity_registry.async_get_or_create(
DOMAIN, DOMAIN,
@ -805,6 +806,7 @@ async def test_invalid_unique_id_cleanup(
mock_calendars_yaml, mock_calendars_yaml,
) -> None: ) -> None:
"""Test that old unique id format that is not actually unique is removed.""" """Test that old unique id format that is not actually unique is removed."""
config_entry.add_to_hass(hass)
# Create an entity using the old unique id format # Create an entity using the old unique id format
entity_registry.async_get_or_create( entity_registry.async_get_or_create(
DOMAIN, DOMAIN,

View File

@ -1200,6 +1200,7 @@ async def test_unique_id(
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
) -> None: ) -> None:
"""Test unique id convert to string.""" """Test unique id convert to string."""
config_entry.add_to_hass(hass)
entity_registry.async_get_or_create( entity_registry.async_get_or_create(
Platform.CLIMATE, Platform.CLIMATE,
DOMAIN, DOMAIN,

View File

@ -166,6 +166,7 @@ async def test_group_entity_migration_with_v1_id(
) -> None: ) -> None:
"""Test if entity schema for grouped_lights migrates from v1 to v2.""" """Test if entity schema for grouped_lights migrates from v1 to v2."""
config_entry = mock_bridge_v2.config_entry = mock_config_entry_v2 config_entry = mock_bridge_v2.config_entry = mock_config_entry_v2
config_entry.add_to_hass(hass)
# create (deviceless) entity with V1 schema in registry # create (deviceless) entity with V1 schema in registry
# using the legacy style group id as unique id # using the legacy style group id as unique id
@ -201,6 +202,7 @@ async def test_group_entity_migration_with_v2_group_id(
) -> None: ) -> None:
"""Test if entity schema for grouped_lights migrates from v1 to v2.""" """Test if entity schema for grouped_lights migrates from v1 to v2."""
config_entry = mock_bridge_v2.config_entry = mock_config_entry_v2 config_entry = mock_bridge_v2.config_entry = mock_config_entry_v2
config_entry.add_to_hass(hass)
# create (deviceless) entity with V1 schema in registry # create (deviceless) entity with V1 schema in registry
# using the V2 group id as unique id # using the V2 group id as unique id

View File

@ -368,6 +368,7 @@ async def test_migrate_entry(
version=1, version=1,
minor_version=1, minor_version=1,
) )
entry.add_to_hass(hass)
# Add entries with int unique_id # Add entries with int unique_id
entity_registry.async_get_or_create( entity_registry.async_get_or_create(
@ -387,7 +388,6 @@ async def test_migrate_entry(
assert entry.version == 1 assert entry.version == 1
assert entry.minor_version == 1 assert entry.minor_version == 1
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -444,6 +444,7 @@ async def test_no_listen_start(
version=1, version=1,
data={"username": "foo", "token": {}}, data={"username": "foo", "token": {}},
) )
mock_entry.add_to_hass(hass)
# Create a binary sensor entity so it is not ignored by the deprecation check # Create a binary sensor entity so it is not ignored by the deprecation check
# and the listener will start # and the listener will start
entity_registry.async_get_or_create( entity_registry.async_get_or_create(
@ -457,7 +458,6 @@ async def test_no_listen_start(
mock_ring_event_listener_class.return_value.started = False mock_ring_event_listener_class.return_value.started = False
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id) await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -46,6 +46,7 @@ async def test_entity_id_migration(
) -> None: ) -> None:
"""Test the migration of unique IDs on config entry setup.""" """Test the migration of unique IDs on config entry setup."""
config_entry = mock_config_entry(unique_id="binary_sensor_test_diffuser_v1") config_entry = mock_config_entry(unique_id="binary_sensor_test_diffuser_v1")
config_entry.add_to_hass(hass)
# Pre-create old style unique IDs # Pre-create old style unique IDs
charging = entity_registry.async_get_or_create( charging = entity_registry.async_get_or_create(

View File

@ -100,6 +100,7 @@ async def test_entity_entry_migration(
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
) -> None: ) -> None:
"""Test successful migration of entry data.""" """Test successful migration of entry data."""
mock_config_entry.add_to_hass(hass)
entity = entity_registry.async_get_or_create( entity = entity_registry.async_get_or_create(
suggested_object_id="advice", suggested_object_id="advice",
disabled_by=None, disabled_by=None,

View File

@ -374,6 +374,7 @@ async def test_change_device_source(
# Configure source entity 3 (without a device) # Configure source entity 3 (without a device)
source_config_entry_3 = MockConfigEntry() source_config_entry_3 = MockConfigEntry()
source_config_entry_3.add_to_hass(hass)
source_entity_3 = entity_registry.async_get_or_create( source_entity_3 = entity_registry.async_get_or_create(
"sensor", "sensor",
"test", "test",

View File

@ -869,6 +869,7 @@ async def test_setup_entry(
platform = MockPlatform(async_setup_entry=async_setup_entry) platform = MockPlatform(async_setup_entry=async_setup_entry)
config_entry = MockConfigEntry(entry_id="super-mock-id") config_entry = MockConfigEntry(entry_id="super-mock-id")
config_entry.add_to_hass(hass)
entity_platform = MockEntityPlatform( entity_platform = MockEntityPlatform(
hass, platform_name=config_entry.domain, platform=platform hass, platform_name=config_entry.domain, platform=platform
) )
@ -1886,6 +1887,7 @@ async def test_setup_entry_with_entities_that_block_forever(
platform = MockPlatform(async_setup_entry=async_setup_entry) platform = MockPlatform(async_setup_entry=async_setup_entry)
config_entry = MockConfigEntry(entry_id="super-mock-id") config_entry = MockConfigEntry(entry_id="super-mock-id")
config_entry.add_to_hass(hass)
platform = MockEntityPlatform( platform = MockEntityPlatform(
hass, platform_name=config_entry.domain, platform=platform hass, platform_name=config_entry.domain, platform=platform
) )
@ -1934,6 +1936,7 @@ async def test_cancellation_is_not_blocked(
platform = MockPlatform(async_setup_entry=async_setup_entry) platform = MockPlatform(async_setup_entry=async_setup_entry)
config_entry = MockConfigEntry(entry_id="super-mock-id") config_entry = MockConfigEntry(entry_id="super-mock-id")
config_entry.add_to_hass(hass)
platform = MockEntityPlatform( platform = MockEntityPlatform(
hass, platform_name=config_entry.domain, platform=platform hass, platform_name=config_entry.domain, platform=platform
) )

View File

@ -616,11 +616,13 @@ async def test_updating_config_entry_id(
"""Test that we update config entry id in registry.""" """Test that we update config entry id in registry."""
update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED)
mock_config_1 = MockConfigEntry(domain="light", entry_id="mock-id-1") mock_config_1 = MockConfigEntry(domain="light", entry_id="mock-id-1")
mock_config_1.add_to_hass(hass)
entry = entity_registry.async_get_or_create( entry = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=mock_config_1 "light", "hue", "5678", config_entry=mock_config_1
) )
mock_config_2 = MockConfigEntry(domain="light", entry_id="mock-id-2") mock_config_2 = MockConfigEntry(domain="light", entry_id="mock-id-2")
mock_config_2.add_to_hass(hass)
entry2 = entity_registry.async_get_or_create( entry2 = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=mock_config_2 "light", "hue", "5678", config_entry=mock_config_2
) )
@ -647,6 +649,7 @@ async def test_removing_config_entry_id(
"""Test that we update config entry id in registry.""" """Test that we update config entry id in registry."""
update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED)
mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1")
mock_config.add_to_hass(hass)
entry = entity_registry.async_get_or_create( entry = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=mock_config "light", "hue", "5678", config_entry=mock_config
@ -670,11 +673,14 @@ async def test_removing_config_entry_id(
async def test_deleted_entity_removing_config_entry_id( async def test_deleted_entity_removing_config_entry_id(
hass: HomeAssistant,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
) -> None: ) -> None:
"""Test that we update config entry id in registry on deleted entity.""" """Test that we update config entry id in registry on deleted entity."""
mock_config1 = MockConfigEntry(domain="light", entry_id="mock-id-1") mock_config1 = MockConfigEntry(domain="light", entry_id="mock-id-1")
mock_config2 = MockConfigEntry(domain="light", entry_id="mock-id-2") mock_config2 = MockConfigEntry(domain="light", entry_id="mock-id-2")
mock_config1.add_to_hass(hass)
mock_config2.add_to_hass(hass)
entry1 = entity_registry.async_get_or_create( entry1 = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=mock_config1 "light", "hue", "5678", config_entry=mock_config1
@ -979,9 +985,12 @@ async def test_migration_1_11(
} }
async def test_update_entity_unique_id(entity_registry: er.EntityRegistry) -> None: async def test_update_entity_unique_id(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test entity's unique_id is updated.""" """Test entity's unique_id is updated."""
mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1")
mock_config.add_to_hass(hass)
entry = entity_registry.async_get_or_create( entry = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=mock_config "light", "hue", "5678", config_entry=mock_config
@ -1007,10 +1016,12 @@ async def test_update_entity_unique_id(entity_registry: er.EntityRegistry) -> No
async def test_update_entity_unique_id_conflict( async def test_update_entity_unique_id_conflict(
hass: HomeAssistant,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
) -> None: ) -> None:
"""Test migration raises when unique_id already in use.""" """Test migration raises when unique_id already in use."""
mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1")
mock_config.add_to_hass(hass)
entry = entity_registry.async_get_or_create( entry = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=mock_config "light", "hue", "5678", config_entry=mock_config
) )
@ -1099,9 +1110,12 @@ async def test_update_entity_entity_id_entity_id(
assert entity_registry.async_get(state_entity_id) is None assert entity_registry.async_get(state_entity_id) is None
async def test_update_entity(entity_registry: er.EntityRegistry) -> None: async def test_update_entity(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test updating entity.""" """Test updating entity."""
mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1")
mock_config.add_to_hass(hass)
entry = entity_registry.async_get_or_create( entry = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=mock_config "light", "hue", "5678", config_entry=mock_config
) )
@ -1126,9 +1140,12 @@ async def test_update_entity(entity_registry: er.EntityRegistry) -> None:
entry = updated_entry entry = updated_entry
async def test_update_entity_options(entity_registry: er.EntityRegistry) -> None: async def test_update_entity_options(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test updating entity.""" """Test updating entity."""
mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1")
mock_config.add_to_hass(hass)
entry = entity_registry.async_get_or_create( entry = entity_registry.async_get_or_create(
"light", "hue", "5678", config_entry=mock_config "light", "hue", "5678", config_entry=mock_config
) )
@ -1181,6 +1198,7 @@ async def test_disabled_by(entity_registry: er.EntityRegistry) -> None:
async def test_disabled_by_config_entry_pref( async def test_disabled_by_config_entry_pref(
hass: HomeAssistant,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
) -> None: ) -> None:
"""Test config entry preference setting disabled_by.""" """Test config entry preference setting disabled_by."""
@ -1189,6 +1207,7 @@ async def test_disabled_by_config_entry_pref(
entry_id="mock-id-1", entry_id="mock-id-1",
pref_disable_new_entities=True, pref_disable_new_entities=True,
) )
mock_config.add_to_hass(hass)
entry = entity_registry.async_get_or_create( entry = entity_registry.async_get_or_create(
"light", "hue", "AAAA", config_entry=mock_config "light", "hue", "AAAA", config_entry=mock_config
) )
@ -1761,6 +1780,25 @@ def test_entity_registry_items() -> None:
assert entities.get_entry(entry2.id) is None assert entities.get_entry(entry2.id) is None
async def test_config_entry_does_not_exist(entity_registry: er.EntityRegistry) -> None:
"""Test adding an entity linked to an unknown config entry."""
mock_config = MockConfigEntry(
domain="light",
entry_id="mock-id-1",
pref_disable_new_entities=True,
)
with pytest.raises(ValueError):
entity_registry.async_get_or_create(
"light", "hue", "1234", config_entry=mock_config
)
entity_id = entity_registry.async_get_or_create("light", "hue", "1234").entity_id
with pytest.raises(ValueError):
entity_registry.async_update_entity(
entity_id, config_entry_id=mock_config.entry_id
)
async def test_device_does_not_exist(entity_registry: er.EntityRegistry) -> None: async def test_device_does_not_exist(entity_registry: er.EntityRegistry) -> None:
"""Test adding an entity linked to an unknown device.""" """Test adding an entity linked to an unknown device."""
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -1848,6 +1886,7 @@ def test_migrate_entity_to_new_platform(
) -> None: ) -> None:
"""Test migrate_entity_to_new_platform.""" """Test migrate_entity_to_new_platform."""
orig_config_entry = MockConfigEntry(domain="light") orig_config_entry = MockConfigEntry(domain="light")
orig_config_entry.add_to_hass(hass)
orig_unique_id = "5678" orig_unique_id = "5678"
orig_entry = entity_registry.async_get_or_create( orig_entry = entity_registry.async_get_or_create(
@ -1870,6 +1909,7 @@ def test_migrate_entity_to_new_platform(
) )
new_config_entry = MockConfigEntry(domain="light") new_config_entry = MockConfigEntry(domain="light")
new_config_entry.add_to_hass(hass)
new_unique_id = "1234" new_unique_id = "1234"
assert entity_registry.async_update_entity_platform( assert entity_registry.async_update_entity_platform(
@ -1924,6 +1964,7 @@ async def test_restore_entity(
"""Make sure entity registry id is stable and entity_id is reused if possible.""" """Make sure entity registry id is stable and entity_id is reused if possible."""
update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED)
config_entry = MockConfigEntry(domain="light") config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
entry1 = entity_registry.async_get_or_create( entry1 = entity_registry.async_get_or_create(
"light", "hue", "1234", config_entry=config_entry "light", "hue", "1234", config_entry=config_entry
) )
@ -2018,6 +2059,8 @@ async def test_async_migrate_entry_delete_self(
"""Test async_migrate_entry.""" """Test async_migrate_entry."""
config_entry1 = MockConfigEntry(domain="test1") config_entry1 = MockConfigEntry(domain="test1")
config_entry2 = MockConfigEntry(domain="test2") config_entry2 = MockConfigEntry(domain="test2")
config_entry1.add_to_hass(hass)
config_entry2.add_to_hass(hass)
entry1 = entity_registry.async_get_or_create( entry1 = entity_registry.async_get_or_create(
"light", "hue", "1234", config_entry=config_entry1, original_name="Entry 1" "light", "hue", "1234", config_entry=config_entry1, original_name="Entry 1"
) )
@ -2053,6 +2096,8 @@ async def test_async_migrate_entry_delete_other(
"""Test async_migrate_entry.""" """Test async_migrate_entry."""
config_entry1 = MockConfigEntry(domain="test1") config_entry1 = MockConfigEntry(domain="test1")
config_entry2 = MockConfigEntry(domain="test2") config_entry2 = MockConfigEntry(domain="test2")
config_entry1.add_to_hass(hass)
config_entry2.add_to_hass(hass)
entry1 = entity_registry.async_get_or_create( entry1 = entity_registry.async_get_or_create(
"light", "hue", "1234", config_entry=config_entry1, original_name="Entry 1" "light", "hue", "1234", config_entry=config_entry1, original_name="Entry 1"
) )

View File

@ -2126,6 +2126,7 @@ async def test_state_translated(
hass.states.async_set("domain.is_unknown", "unknown", attributes={}) hass.states.async_set("domain.is_unknown", "unknown", attributes={})
config_entry = MockConfigEntry(domain="light") config_entry = MockConfigEntry(domain="light")
config_entry.add_to_hass(hass)
entity_registry.async_get_or_create( entity_registry.async_get_or_create(
"light", "light",
"hue", "hue",