diff --git a/custom_components/nestquest/__init__.py b/custom_components/nestquest/__init__.py index d69e2d5..91a962d 100644 --- a/custom_components/nestquest/__init__.py +++ b/custom_components/nestquest/__init__.py @@ -270,22 +270,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: db = await _async_open_database( hass, await async_get_db_path(hass) ) - stored_admin_ids = ( - entry.options.get(CONF_ADMIN_USER_IDS) - or entry.data.get(CONF_ADMIN_USER_IDS) - ) - context = getattr(entry, "context", None) - context_user_id = ( - context.get("user_id") - if isinstance(context, dict) - else None - ) - await seed_setup_admin( - db, - stored_admin_ids=stored_admin_ids, - context_user_id=context_user_id, - owner_ids=await _async_owner_user_ids(hass), - ) + try: + stored_admin_ids = ( + entry.options.get(CONF_ADMIN_USER_IDS) + or entry.data.get(CONF_ADMIN_USER_IDS) + ) + context = getattr(entry, "context", None) + context_user_id = ( + context.get("user_id") + if isinstance(context, dict) + else None + ) + await seed_setup_admin( + db, + stored_admin_ids=stored_admin_ids, + context_user_id=context_user_id, + owner_ids=await _async_owner_user_ids(hass), + ) + except BaseException: + # A seeding failure must not leak the just-opened + # connection: close it before the error propagates, so + # HA's setup retry starts from a clean handle (the + # first-setup path closes symmetrically). + await db.close() + raise existing.database = db entry.runtime_data = existing return True diff --git a/tests/test_admin_allowlist.py b/tests/test_admin_allowlist.py index 7b4def1..b5d15eb 100644 --- a/tests/test_admin_allowlist.py +++ b/tests/test_admin_allowlist.py @@ -375,3 +375,43 @@ def test_seed_owner_ids_argument(tmp_path) -> None: return seeded _with_db(tmp_path, "seed-owner-arg.db")(_body) + + +async def test_reopened_setup_failure_closes_database( + hass, make_entry, monkeypatch +) -> None: + """A seeding failure on the reopened-record path closes the just- + opened connection before re-raising (no leaked handles across HA's + setup retries).""" + import pytest as pytest_module + from unittest.mock import AsyncMock + + import custom_components.nestquest as nestquest_module + from custom_components.nestquest.db import NestQuestDatabase + + # First: a successful setup to create the runtime record and file. + entry = await _setup_entry(hass, make_entry(), hass.registry) + await async_unload_entry(hass, entry) + + # Now a re-setup whose seeding fails after the reopen. + async def _failing_seed(database, **kwargs): + raise RuntimeError("seed boom") + + monkeypatch.setattr( + nestquest_module, "seed_setup_admin", _failing_seed, raising=True + ) + + close_calls: list[bool] = [] + original_close = NestQuestDatabase.close + + async def _spy_close(self): + close_calls.append(True) + return await original_close(self) + + monkeypatch.setattr(NestQuestDatabase, "close", _spy_close) + + with pytest_module.raises(RuntimeError, match="seed boom"): + await _setup_entry(hass, entry, hass.registry) + # The reopen path opened AND closed a connection; the failure is + # the last thing observed. + assert close_calls[-1] is True