diff --git a/custom_components/hacs/__init__.py b/custom_components/hacs/__init__.py index 6af3f8fc839..07622b8fdd9 100644 --- a/custom_components/hacs/__init__.py +++ b/custom_components/hacs/__init__.py @@ -214,10 +214,11 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> unload_ok = await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) - hacs.set_stage(None) - hacs.disable_hacs(HacsDisabledReason.REMOVED) - - hass.data.pop(DOMAIN, None) + # Only clean up if unload was successful + if unload_ok: + hacs.set_stage(None) + hacs.disable_hacs(HacsDisabledReason.REMOVED) + hass.data.pop(DOMAIN, None) return unload_ok diff --git a/custom_components/hacs/switch.py b/custom_components/hacs/switch.py index 4fa43d4e35f..074097267a9 100644 --- a/custom_components/hacs/switch.py +++ b/custom_components/hacs/switch.py @@ -22,7 +22,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Setup switch platform.""" - hacs: HacsBase = hass.data[DOMAIN] + if (hacs := hass.data.get(DOMAIN)) is None: + # HACS is not properly initialized + return + async_add_entities( HacsRepositoryPreReleaseSwitchEntity(hacs=hacs, repository=repository) for repository in hacs.repositories.list_downloaded diff --git a/custom_components/hacs/update.py b/custom_components/hacs/update.py index ec4014302db..4b5ef6747a3 100644 --- a/custom_components/hacs/update.py +++ b/custom_components/hacs/update.py @@ -21,7 +21,10 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Setup update platform.""" - hacs: HacsBase = hass.data[DOMAIN] + if (hacs := hass.data.get(DOMAIN)) is None: + # HACS is not properly initialized + return + async_add_entities( HacsRepositoryUpdateEntity(hacs=hacs, repository=repository) for repository in hacs.repositories.list_downloaded diff --git a/custom_components/hacs/websocket/repositories.py b/custom_components/hacs/websocket/repositories.py index 879f68af8da..654c0147cf3 100644 --- a/custom_components/hacs/websocket/repositories.py +++ b/custom_components/hacs/websocket/repositories.py @@ -34,7 +34,11 @@ async def hacs_repositories_list( msg: dict[str, Any], ) -> None: """List repositories.""" - hacs: HacsBase = hass.data.get(DOMAIN) + if (hacs := hass.data.get(DOMAIN)) is None: + # HACS is not properly initialized + connection.send_error(msg["id"], "hacs_not_initialized", "HACS is not properly initialized") + return + connection.send_message( websocket_api.result_message( msg["id"], @@ -91,7 +95,10 @@ async def hacs_repositories_clear_new( msg: dict[str, Any], ) -> None: """Clear new repositories for specific categories.""" - hacs: HacsBase = hass.data.get(DOMAIN) + if (hacs := hass.data.get(DOMAIN)) is None: + # HACS is not properly initialized + connection.send_error(msg["id"], "hacs_not_initialized", "HACS is not properly initialized") + return if repo := msg.get("repository"): repository = hacs.repositories.get_by_id(repo) @@ -123,7 +130,11 @@ async def hacs_repositories_removed( msg: dict[str, Any], ) -> None: """Get information about removed repositories.""" - hacs: HacsBase = hass.data.get(DOMAIN) + if (hacs := hass.data.get(DOMAIN)) is None: + # HACS is not properly initialized + connection.send_error(msg["id"], "hacs_not_initialized", "HACS is not properly initialized") + return + content = [] for repo in hacs.repositories.list_removed: if repo.repository not in hacs.common.ignored_repositories: @@ -146,7 +157,11 @@ async def hacs_repositories_add( msg: dict[str, Any], ) -> None: """Add custom repositoriy.""" - hacs: HacsBase = hass.data.get(DOMAIN) + if (hacs := hass.data.get(DOMAIN)) is None: + # HACS is not properly initialized + connection.send_error(msg["id"], "hacs_not_initialized", "HACS is not properly initialized") + return + repository = regex.extract_repository_from_url(msg["repository"]) category = msg["category"] @@ -207,7 +222,11 @@ async def hacs_repositories_remove( msg: dict[str, Any], ) -> None: """Remove custom repositoriy.""" - hacs: HacsBase = hass.data.get(DOMAIN) + if (hacs := hass.data.get(DOMAIN)) is None: + # HACS is not properly initialized + connection.send_error(msg["id"], "hacs_not_initialized", "HACS is not properly initialized") + return + repository = hacs.repositories.get_by_id(msg["repository"]) repository.remove() diff --git a/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-add-payload6.json b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-add-payload6.json new file mode 100644 index 00000000000..2f33378e257 --- /dev/null +++ b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-add-payload6.json @@ -0,0 +1,9 @@ +{ + "tests/websocket/test_repositories_not_initialized.py::test_websocket_repositories_commands_hacs_not_initialized[hacs/repositories/add-payload6]": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-clear-new-payload2.json b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-clear-new-payload2.json new file mode 100644 index 00000000000..3d692793a2a --- /dev/null +++ b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-clear-new-payload2.json @@ -0,0 +1,9 @@ +{ + "tests/websocket/test_repositories_not_initialized.py::test_websocket_repositories_commands_hacs_not_initialized[hacs/repositories/clear_new-payload2]": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-clear-new-payload3.json b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-clear-new-payload3.json new file mode 100644 index 00000000000..37955416cb0 --- /dev/null +++ b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-clear-new-payload3.json @@ -0,0 +1,9 @@ +{ + "tests/websocket/test_repositories_not_initialized.py::test_websocket_repositories_commands_hacs_not_initialized[hacs/repositories/clear_new-payload3]": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-clear-new-payload4.json b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-clear-new-payload4.json new file mode 100644 index 00000000000..75a371d0951 --- /dev/null +++ b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-clear-new-payload4.json @@ -0,0 +1,9 @@ +{ + "tests/websocket/test_repositories_not_initialized.py::test_websocket_repositories_commands_hacs_not_initialized[hacs/repositories/clear_new-payload4]": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-list-payload0.json b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-list-payload0.json new file mode 100644 index 00000000000..29cd6df6af0 --- /dev/null +++ b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-list-payload0.json @@ -0,0 +1,9 @@ +{ + "tests/websocket/test_repositories_not_initialized.py::test_websocket_repositories_commands_hacs_not_initialized[hacs/repositories/list-payload0]": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-list-payload1.json b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-list-payload1.json new file mode 100644 index 00000000000..51724ba2f52 --- /dev/null +++ b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-list-payload1.json @@ -0,0 +1,9 @@ +{ + "tests/websocket/test_repositories_not_initialized.py::test_websocket_repositories_commands_hacs_not_initialized[hacs/repositories/list-payload1]": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-remove-payload7.json b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-remove-payload7.json new file mode 100644 index 00000000000..8994eec4a1b --- /dev/null +++ b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-remove-payload7.json @@ -0,0 +1,9 @@ +{ + "tests/websocket/test_repositories_not_initialized.py::test_websocket_repositories_commands_hacs_not_initialized[hacs/repositories/remove-payload7]": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-removed-payload5.json b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-removed-payload5.json new file mode 100644 index 00000000000..3d3d5acc590 --- /dev/null +++ b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-commands-hacs-not-initialized-hacs-repositories-removed-payload5.json @@ -0,0 +1,9 @@ +{ + "tests/websocket/test_repositories_not_initialized.py::test_websocket_repositories_commands_hacs_not_initialized[hacs/repositories/removed-payload5]": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1 + } +} \ No newline at end of file diff --git a/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-list-with-hacs-initialized.json b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-list-with-hacs-initialized.json new file mode 100644 index 00000000000..aece5f4f773 --- /dev/null +++ b/tests/snapshots/api-usage/tests/websocket/test_repositories_not_initializedtest-websocket-repositories-list-with-hacs-initialized.json @@ -0,0 +1,9 @@ +{ + "tests/websocket/test_repositories_not_initialized.py::test_websocket_repositories_list_with_hacs_initialized": { + "https://api.github.com/repos/hacs/integration": 1, + "https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1, + "https://api.github.com/repos/hacs/integration/contents/hacs.json": 1, + "https://api.github.com/repos/hacs/integration/git/trees/main": 1, + "https://api.github.com/repos/hacs/integration/releases": 1 + } +} \ No newline at end of file diff --git a/tests/websocket/__init__.py b/tests/websocket/__init__.py new file mode 100644 index 00000000000..4a2523bdf36 --- /dev/null +++ b/tests/websocket/__init__.py @@ -0,0 +1 @@ +"""Tests for websocket when HACS is not initialized.""" diff --git a/tests/websocket/test_repositories_not_initialized.py b/tests/websocket/test_repositories_not_initialized.py new file mode 100644 index 00000000000..23262dc1528 --- /dev/null +++ b/tests/websocket/test_repositories_not_initialized.py @@ -0,0 +1,62 @@ +"""Tests for websocket repositories commands when HACS is not initialized.""" + +from collections.abc import Generator + +from homeassistant.core import HomeAssistant +import pytest + +from custom_components.hacs.const import DOMAIN + +from tests.common import WSClient, get_hacs + + +@pytest.mark.parametrize( + "command,payload", + [ + ("hacs/repositories/list", {}), + ("hacs/repositories/list", {"categories": ["integration"]}), + ("hacs/repositories/clear_new", {}), + ("hacs/repositories/clear_new", {"categories": ["plugin"]}), + ("hacs/repositories/clear_new", {"repository": "test/repo"}), + ("hacs/repositories/removed", {}), + ("hacs/repositories/add", {"repository": "test/repo", "category": "integration"}), + ("hacs/repositories/remove", {"repository": "123"}), + ], +) +async def test_websocket_repositories_commands_hacs_not_initialized( + hass: HomeAssistant, + setup_integration: Generator, # Need this to register websocket commands + ws_client: WSClient, + command: str, + payload: dict, +): + """Test that websocket repository commands return proper errors when HACS is not initialized.""" + # Ensure HACS is not in hass.data (not initialized) + hass.data.pop(DOMAIN, None) + + # Send websocket command + response = await ws_client.send_and_receive_json(command, payload) + + # Verify error response + assert response["success"] is False + assert response["error"]["code"] == "hacs_not_initialized" + assert response["error"]["message"] == "HACS is not properly initialized" + + +async def test_websocket_repositories_list_with_hacs_initialized( + hass: HomeAssistant, + setup_integration: Generator, # This sets up HACS properly + ws_client: WSClient, +): + """Test that websocket works normally when HACS is properly initialized.""" + # Verify HACS is properly initialized + hacs = get_hacs(hass) + assert hacs is not None + + # Send websocket command - should work normally + response = await ws_client.send_and_receive_json("hacs/repositories/list", {}) + + # Verify successful response (should be a list, not an error) + assert response["success"] is True + assert "result" in response + assert isinstance(response["result"], list)