diff --git a/custom_components/hacs/__init__.py b/custom_components/hacs/__init__.py index 16ffa2969be..b2f247ac492 100644 --- a/custom_components/hacs/__init__.py +++ b/custom_components/hacs/__init__.py @@ -79,6 +79,7 @@ async def async_initialize_integration( hacs.integration = integration hacs.version = integration.version + hacs.configuration.dev = integration.version == "0.0.0" hacs.hass = hass hacs.queue = QueueManager(hass=hass) hacs.data = HacsData(hacs=hacs) diff --git a/custom_components/hacs/base.py b/custom_components/hacs/base.py index faaec5a8349..692ee6602d2 100644 --- a/custom_components/hacs/base.py +++ b/custom_components/hacs/base.py @@ -184,7 +184,7 @@ class HacsRepositories: """HACS Repositories.""" _default_repositories: set[str] = field(default_factory=set) - _repositories: list[str] = field(default_factory=list) + _repositories: list[HacsRepository] = field(default_factory=list) _repositories_by_full_name: dict[str, str] = field(default_factory=dict) _repositories_by_id: dict[str, str] = field(default_factory=dict) _removed_repositories: list[RemovedRepository] = field(default_factory=list) @@ -554,48 +554,16 @@ async def async_register_repository( self.repositories.register(repository, default) async def startup_tasks(self, _event=None) -> None: - """Tasks that are started after startup.""" + """Tasks that are started after setup.""" await self.async_set_stage(HacsStage.STARTUP) self.status.background_task = True - self.hass.bus.async_fire("hacs/status", {}) - - try: - await self.handle_critical_repositories_startup() - await self.async_load_default_repositories() - except HacsException as exception: - self.log.warning( - "Could not load default repositories: %s, retrying in 5 minuttes", exception - ) - if not self.system.disabled: - async_call_later(self.hass, timedelta(minutes=5), self.startup_tasks) - return - - self.recuring_tasks.append( - self.hass.helpers.event.async_track_time_interval( - self.recurring_tasks_installed, timedelta(hours=2) - ) - ) + self.status.startup = False - self.recuring_tasks.append( - self.hass.helpers.event.async_track_time_interval( - self.recurring_tasks_all, timedelta(hours=25) - ) - ) + self.hass.bus.async_fire("hacs/status", {}) - self.status.startup = False await self.async_set_stage(HacsStage.RUNNING) self.hass.bus.async_fire("hacs/reload", {"force": True}) - try: - await self.recurring_tasks_installed() - except HacsException as exception: - self.log.warning( - "Could not run initial task for downloaded repositories: %s, retrying in 5 minuttes", - exception, - ) - if not self.system.disabled: - async_call_later(self.hass, timedelta(minutes=5), self.startup_tasks) - return if queue_task := self.tasks.get("prosess_queue"): await queue_task.execute_task() @@ -603,177 +571,6 @@ async def startup_tasks(self, _event=None) -> None: self.status.background_task = False self.hass.bus.async_fire("hacs/status", {}) - async def handle_critical_repositories_startup(self) -> None: - """Handled critical repositories during startup.""" - alert = False - critical = await async_load_from_store(self.hass, "critical") - if not critical: - return - for repo in critical: - if not repo["acknowledged"]: - alert = True - if alert: - self.log.critical("URGENT!: Check the HACS panel!") - self.hass.components.persistent_notification.create( - title="URGENT!", message="**Check the HACS panel!**" - ) - - async def handle_critical_repositories(self) -> None: - """Handled critical repositories during runtime.""" - # Get critical repositories - critical_queue = QueueManager(hass=self.hass) - instored = [] - critical = [] - was_installed = False - - try: - critical = await self.async_github_get_hacs_default_file("critical") - except GitHubNotModifiedException: - return - except GitHubException: - pass - - if not critical: - self.log.debug("No critical repositories") - return - - stored_critical = await async_load_from_store(self.hass, "critical") - - for stored in stored_critical or []: - instored.append(stored["repository"]) - - stored_critical = [] - - for repository in critical: - removed_repo = self.repositories.removed_repository(repository["repository"]) - removed_repo.removal_type = "critical" - repo = self.repositories.get_by_full_name(repository["repository"]) - - stored = { - "repository": repository["repository"], - "reason": repository["reason"], - "link": repository["link"], - "acknowledged": True, - } - if repository["repository"] not in instored: - if repo is not None and repo.installed: - self.log.critical( - "Removing repository %s, it is marked as critical", - repository["repository"], - ) - was_installed = True - stored["acknowledged"] = False - # Remove from HACS - critical_queue.add(repo.uninstall()) - repo.remove() - - stored_critical.append(stored) - removed_repo.update_data(stored) - - # Uninstall - await critical_queue.execute() - - # Save to FS - await async_save_to_store(self.hass, "critical", stored_critical) - - # Restart HASS - if was_installed: - self.log.critical("Resarting Home Assistant") - self.hass.async_create_task(self.hass.async_stop(100)) - - async def recurring_tasks_installed(self, _notarealarg=None) -> None: - """Recurring tasks for installed repositories.""" - self.log.debug("Starting recurring background task for installed repositories") - self.status.background_task = True - self.hass.bus.async_fire("hacs/status", {}) - - for repository in self.repositories.list_all: - if self.status.startup and repository.data.full_name == HacsGitHubRepo.INTEGRATION: - continue - if repository.data.installed and repository.data.category in self.common.categories: - self.queue.add(repository.update_repository()) - - await self.handle_critical_repositories() - self.status.background_task = False - self.hass.bus.async_fire("hacs/status", {}) - await self.data.async_write() - self.log.debug("Recurring background task for installed repositories done") - - async def recurring_tasks_all(self, _notarealarg=None) -> None: - """Recurring tasks for all repositories.""" - self.log.debug("Starting recurring background task for all repositories") - self.status.background_task = True - self.hass.bus.async_fire("hacs/status", {}) - - for repository in self.repositories.list_all: - if repository.data.category in self.common.categories: - self.queue.add(repository.common_update()) - - await self.async_load_default_repositories() - self.status.background_task = False - await self.data.async_write() - self.hass.bus.async_fire("hacs/status", {}) - self.hass.bus.async_fire("hacs/repository", {"action": "reload"}) - self.log.debug("Recurring background task for all repositories done") - - async def async_load_default_repositories(self) -> None: - """Load known repositories.""" - need_to_save = False - self.log.info("Loading known repositories") - - for item in await self.async_github_get_hacs_default_file(HacsCategory.REMOVED): - removed = self.repositories.removed_repository(item["repository"]) - removed.update_data(item) - - for category in self.common.categories or []: - self.queue.add(self.async_get_category_repositories(HacsCategory(category))) - - if queue_task := self.tasks.get("prosess_queue"): - await queue_task.execute_task() - - for removed in self.repositories.list_removed: - if (repository := self.repositories.get_by_full_name(removed.repository)) is None: - continue - if repository.data.installed and removed.removal_type != "critical": - self.log.warning( - "You have '%s' installed with HACS " - "this repository has been removed from HACS, please consider removing it. " - "Removal reason (%s)", - repository.data.full_name, - removed.reason, - ) - else: - need_to_save = True - repository.remove() - - if need_to_save: - await self.data.async_write() - - async def async_get_category_repositories(self, category: HacsCategory) -> None: - """Get repositories from category.""" - repositories = await self.async_github_get_hacs_default_file(category) - for repo in repositories: - if self.common.renamed_repositories.get(repo): - repo = self.common.renamed_repositories[repo] - if self.repositories.is_removed(repo): - continue - if repo in self.common.archived_repositories: - continue - repository = self.repositories.get_by_full_name(repo) - if repository is not None: - self.repositories.mark_default(repository) - if self.status.new: - # Force update for new installations - self.queue.add(repository.common_update()) - continue - self.queue.add( - self.async_register_repository( - repository_full_name=repo, - category=category, - default=True, - ) - ) - async def async_download_file(self, url: str) -> bytes | None: """Download files, and return the content.""" if url is None: diff --git a/custom_components/hacs/tasks/handle_critical_notification.py b/custom_components/hacs/tasks/handle_critical_notification.py new file mode 100644 index 00000000000..497a5842cf9 --- /dev/null +++ b/custom_components/hacs/tasks/handle_critical_notification.py @@ -0,0 +1,35 @@ +""""Hacs base setup task.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant + +from ..base import HacsBase +from ..enums import HacsStage +from ..utils.store import async_load_from_store +from .base import HacsTask + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """Hacs notify critical during startup task.""" + + stages = [HacsStage.STARTUP] + + async def async_execute(self) -> None: + """Execute the task.""" + alert = False + critical = await async_load_from_store(self.hass, "critical") + if not critical: + return + for repo in critical: + if not repo["acknowledged"]: + alert = True + if alert: + self.hacs.log.critical("URGENT!: Check the HACS panel!") + self.hass.components.persistent_notification.create( + title="URGENT!", message="**Check the HACS panel!**" + ) diff --git a/custom_components/hacs/tasks/update_all_repositories.py b/custom_components/hacs/tasks/update_all_repositories.py new file mode 100644 index 00000000000..b91866cd7cf --- /dev/null +++ b/custom_components/hacs/tasks/update_all_repositories.py @@ -0,0 +1,36 @@ +""""Hacs base setup task.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.core import HomeAssistant + +from ..base import HacsBase +from .base import HacsTask + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """Hacs update all task.""" + + schedule = timedelta(hours=25) + + async def async_execute(self) -> None: + """Execute the task.""" + self.hacs.log.debug("Starting recurring background task for all repositories") + self.hacs.status.background_task = True + self.hass.bus.async_fire("hacs/status", {}) + + for repository in self.hacs.repositories.list_all: + if repository.data.category in self.hacs.common.categories: + self.hacs.queue.add(repository.common_update()) + + self.hacs.status.background_task = False + await self.hacs.data.async_write() + self.hass.bus.async_fire("hacs/status", {}) + self.hass.bus.async_fire("hacs/repository", {"action": "reload"}) + self.hacs.log.debug("Recurring background task for all repositories done") diff --git a/custom_components/hacs/tasks/update_critical_repositories.py b/custom_components/hacs/tasks/update_critical_repositories.py new file mode 100644 index 00000000000..20e6c783aa5 --- /dev/null +++ b/custom_components/hacs/tasks/update_critical_repositories.py @@ -0,0 +1,92 @@ +""""Hacs base setup task.""" +from __future__ import annotations + +from datetime import timedelta + +from aiogithubapi import GitHubNotModifiedException +from homeassistant.core import HomeAssistant + +from custom_components.hacs.utils.queue_manager import QueueManager +from custom_components.hacs.utils.store import ( + async_load_from_store, + async_save_to_store, +) + +from ..base import HacsBase +from ..enums import HacsStage +from ..exceptions import HacsException +from .base import HacsTask + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """Hacs update critical task.""" + + schedule = timedelta(minutes=30) + stages = [HacsStage.RUNNING] + + async def async_execute(self) -> None: + """Execute the task.""" + critical_queue = QueueManager(hass=self.hass) + instored = [] + critical = [] + was_installed = False + + try: + critical = await self.hacs.async_github_get_hacs_default_file("critical") + except GitHubNotModifiedException: + return + except HacsException: + pass + + if not critical: + self.hacs.log.debug("No critical repositories") + return + + stored_critical = await async_load_from_store(self.hass, "critical") + + for stored in stored_critical or []: + instored.append(stored["repository"]) + + stored_critical = [] + + for repository in critical: + removed_repo = self.hacs.repositories.removed_repository(repository["repository"]) + removed_repo.removal_type = "critical" + repo = self.hacs.repositories.get_by_full_name(repository["repository"]) + + stored = { + "repository": repository["repository"], + "reason": repository["reason"], + "link": repository["link"], + "acknowledged": True, + } + if repository["repository"] not in instored: + if repo is not None and repo.data.installed: + self.hacs.log.critical( + "Removing repository %s, it is marked as critical", + repository["repository"], + ) + was_installed = True + stored["acknowledged"] = False + # Remove from HACS + critical_queue.add(repo.uninstall()) + repo.remove() + + stored_critical.append(stored) + removed_repo.update_data(stored) + + # Uninstall + await critical_queue.execute() + + # Save to FS + await async_save_to_store(self.hass, "critical", stored_critical) + + # Restart HASS + if was_installed: + self.hacs.log.critical("Resarting Home Assistant") + self.hass.async_create_task(self.hass.async_stop(100)) diff --git a/custom_components/hacs/tasks/update_default_repositories.py b/custom_components/hacs/tasks/update_default_repositories.py new file mode 100644 index 00000000000..d1a3d6a4a58 --- /dev/null +++ b/custom_components/hacs/tasks/update_default_repositories.py @@ -0,0 +1,62 @@ +""""Hacs base setup task.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.core import HomeAssistant + +from ..base import HacsBase +from ..enums import HacsCategory, HacsStage +from ..exceptions import HacsException +from .base import HacsTask + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """Hacs update default task.""" + + schedule = timedelta(hours=3) + stages = [HacsStage.STARTUP] + + async def async_execute(self) -> None: + """Execute the task.""" + self.hacs.log.info("Loading known repositories") + + for category in self.hacs.common.categories or []: + self.hacs.queue.add(self.async_get_category_repositories(HacsCategory(category))) + + if queue_task := self.hacs.tasks.get("prosess_queue"): + await queue_task.execute_task() + + async def async_get_category_repositories(self, category: HacsCategory) -> None: + """Get repositories from category.""" + try: + repositories = await self.hacs.async_github_get_hacs_default_file(category) + except HacsException: + return + + for repo in repositories: + if self.hacs.common.renamed_repositories.get(repo): + repo = self.hacs.common.renamed_repositories[repo] + if self.hacs.repositories.is_removed(repo): + continue + if repo in self.hacs.common.archived_repositories: + continue + repository = self.hacs.repositories.get_by_full_name(repo) + if repository is not None: + self.hacs.repositories.mark_default(repository) + if self.hacs.status.new and self.hacs.configuration.dev: + # Force update for new installations + self.hacs.queue.add(repository.common_update()) + continue + self.hacs.queue.add( + self.hacs.async_register_repository( + repository_full_name=repo, + category=category, + default=True, + ) + ) diff --git a/custom_components/hacs/tasks/update_downloaded_repositories.py b/custom_components/hacs/tasks/update_downloaded_repositories.py new file mode 100644 index 00000000000..c3f73a748a8 --- /dev/null +++ b/custom_components/hacs/tasks/update_downloaded_repositories.py @@ -0,0 +1,42 @@ +""""Hacs base setup task.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.core import HomeAssistant + +from ..base import HacsBase +from ..enums import HacsGitHubRepo, HacsStage +from .base import HacsTask + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """Hacs update downloaded task.""" + + schedule = timedelta(hours=2) + stages = [HacsStage.STARTUP] + + async def async_execute(self) -> None: + """Execute the task.""" + self.hacs.log.debug("Starting recurring background task for installed repositories") + self.hacs.status.background_task = True + self.hass.bus.async_fire("hacs/status", {}) + + for repository in self.hacs.repositories.list_all: + if self.hacs.status.startup and repository.data.full_name == HacsGitHubRepo.INTEGRATION: + continue + if ( + repository.data.installed + and repository.data.category in self.hacs.common.categories + ): + self.hacs.queue.add(repository.update_repository()) + + self.hacs.status.background_task = False + self.hass.bus.async_fire("hacs/status", {}) + await self.hacs.data.async_write() + self.hacs.log.debug("Recurring background task for installed repositories done") diff --git a/custom_components/hacs/tasks/update_removed_repositories.py b/custom_components/hacs/tasks/update_removed_repositories.py new file mode 100644 index 00000000000..07432810c87 --- /dev/null +++ b/custom_components/hacs/tasks/update_removed_repositories.py @@ -0,0 +1,58 @@ +""""Hacs base setup task.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.core import HomeAssistant + +from ..base import HacsBase +from ..enums import HacsCategory, HacsStage +from ..exceptions import HacsException +from .base import HacsTask + + +async def async_setup_task(hacs: HacsBase, hass: HomeAssistant) -> Task: + """Set up this task.""" + return Task(hacs=hacs, hass=hass) + + +class Task(HacsTask): + """Hacs update removed task.""" + + schedule = timedelta(hours=1) + stages = [HacsStage.STARTUP] + + async def async_execute(self) -> None: + """Execute the task.""" + + need_to_save = False + self.hacs.log.info("Loading removed repositories") + + try: + removed_repositories = await self.hacs.async_github_get_hacs_default_file( + HacsCategory.REMOVED + ) + except HacsException: + return + + for item in removed_repositories: + removed = self.hacs.repositories.removed_repository(item["repository"]) + removed.update_data(item) + + for removed in self.hacs.repositories.list_removed: + if (repository := self.hacs.repositories.get_by_full_name(removed.repository)) is None: + continue + if repository.data.installed and removed.removal_type != "critical": + self.hacs.log.warning( + "You have '%s' installed with HACS " + "this repository has been removed from HACS, please consider removing it. " + "Removal reason (%s)", + repository.data.full_name, + removed.reason, + ) + else: + need_to_save = True + repository.remove() + + if need_to_save: + await self.hacs.data.async_write() diff --git a/manage/update_default_repositories.py b/manage/update_default_repositories.py index 5e97883227e..148a8cbc1b4 100644 --- a/manage/update_default_repositories.py +++ b/manage/update_default_repositories.py @@ -6,11 +6,17 @@ def update(): """Update the shipped default repositories data file.""" - storage, to_store = None, {} + storage, to_store, old = None, {}, {} + updated = 0 with open(f"{os.getcwd()}/config/.storage/hacs.repositories", encoding="utf-8") as storage_file: storage = json.load(storage_file) + with open( + f"{os.getcwd()}/custom_components/hacs/utils/default.repositories", encoding="utf-8" + ) as old_file: + old = json.load(old_file) + if storage is None: sys.exit("No storage file") @@ -21,6 +27,9 @@ def update(): for key in ("installed_commit", "selected_tag", "version_installed"): storage["data"][repo][key] = None + if old.get(repo, {}).get("etag_repository") != storage["data"][repo].get("etag_repository"): + updated += 1 + to_store[repo] = storage["data"][repo] with open( @@ -30,6 +39,8 @@ def update(): ) as to_store_file: to_store_file.write(json.dumps(to_store)) + print(f"{updated} was updated") + if __name__ == "__main__": update() diff --git a/tests/tasks/test_handle_critical_notification.py b/tests/tasks/test_handle_critical_notification.py new file mode 100644 index 00000000000..98c6b983483 --- /dev/null +++ b/tests/tasks/test_handle_critical_notification.py @@ -0,0 +1,51 @@ +# pylint: disable=missing-function-docstring,missing-module-docstring, protected-access +from unittest.mock import patch + +import pytest + +from custom_components.hacs.base import HacsBase +from custom_components.hacs.const import VERSION_STORAGE + + +@pytest.mark.asyncio +async def test_handle_critical_notification_nothing_to_notify( + hacs: HacsBase, + caplog: pytest.LogCaptureFixture, +): + await hacs.tasks.async_load() + task = hacs.tasks.get("handle_critical_notification") + + assert task + + with patch("custom_components.hacs.utils.store.json_util.load_json", return_value={}): + await task.execute_task() + + with patch( + "custom_components.hacs.utils.store.json_util.load_json", + return_value={"version": VERSION_STORAGE, "data": []}, + ): + await task.execute_task() + + with patch( + "custom_components.hacs.utils.store.json_util.load_json", + return_value={"version": VERSION_STORAGE, "data": [{"acknowledged": True}]}, + ): + await task.execute_task() + + assert "URGENT!: Check the HACS panel!" not in caplog.text + + +@pytest.mark.asyncio +async def test_handle_critical_notification(hacs: HacsBase, caplog: pytest.LogCaptureFixture): + await hacs.tasks.async_load() + task = hacs.tasks.get("handle_critical_notification") + + assert task + + with patch( + "custom_components.hacs.utils.store.json_util.load_json", + return_value={"version": VERSION_STORAGE, "data": [{"acknowledged": False}]}, + ): + await task.execute_task() + + assert "URGENT!: Check the HACS panel!" in caplog.text diff --git a/tests/tasks/test_update_all_repositories.py b/tests/tasks/test_update_all_repositories.py new file mode 100644 index 00000000000..59fe6e130b9 --- /dev/null +++ b/tests/tasks/test_update_all_repositories.py @@ -0,0 +1,22 @@ +# pylint: disable=missing-function-docstring,missing-module-docstring, protected-access +import pytest + +from custom_components.hacs.base import HacsBase +from custom_components.hacs.enums import HacsCategory +from custom_components.hacs.repositories.base import HacsRepository + + +@pytest.mark.asyncio +async def test_update_all_repositories(hacs: HacsBase, repository: HacsRepository): + await hacs.tasks.async_load() + task = hacs.tasks.get("update_all_repositories") + + repository.data.category = HacsCategory.INTEGRATION + hacs.repositories.register(repository) + hacs.enable_hacs_category(HacsCategory.INTEGRATION) + + assert task + + assert hacs.queue.pending_tasks == 0 + await task.execute_task() + assert hacs.queue.pending_tasks == 1 diff --git a/tests/tasks/test_update_critical_repositories.py b/tests/tasks/test_update_critical_repositories.py new file mode 100644 index 00000000000..00c9953620f --- /dev/null +++ b/tests/tasks/test_update_critical_repositories.py @@ -0,0 +1,152 @@ +# pylint: disable=missing-function-docstring,missing-module-docstring, protected-access +from unittest.mock import AsyncMock, patch + +from aiogithubapi import GitHubNotModifiedException +import pytest + +from custom_components.hacs.base import HacsBase +from custom_components.hacs.const import VERSION_STORAGE +from custom_components.hacs.exceptions import HacsException +from custom_components.hacs.repositories.base import HacsRepository + + +@pytest.mark.asyncio +async def test_update_critical_repositories_no_critical( + hacs: HacsBase, + caplog: pytest.LogCaptureFixture, +): + await hacs.tasks.async_load() + task = hacs.tasks.get("update_critical_repositories") + + assert task + + with patch( + "custom_components.hacs.base.HacsBase.async_github_get_hacs_default_file", + return_value=[], + ), patch("custom_components.hacs.utils.store.json_util.load_json") as load_json_mock: + await task.execute_task() + assert "No critical repositories" in caplog.text + load_json_mock.assert_not_called() + + +@pytest.mark.asyncio +async def test_update_critical_repositories_exception( + hacs: HacsBase, + caplog: pytest.LogCaptureFixture, +): + await hacs.tasks.async_load() + task = hacs.tasks.get("update_critical_repositories") + + assert task + + with patch( + "custom_components.hacs.base.HacsBase.async_github_get_hacs_default_file", + side_effect=GitHubNotModifiedException("err"), + ), patch("custom_components.hacs.utils.store.json_util.load_json") as load_json_mock: + await task.execute_task() + load_json_mock.assert_not_called() + assert "No critical repositories" not in caplog.text + + with patch( + "custom_components.hacs.base.HacsBase.async_github_get_hacs_default_file", + side_effect=HacsException("err"), + ), patch("custom_components.hacs.utils.store.json_util.load_json") as load_json_mock: + await task.execute_task() + load_json_mock.assert_not_called() + assert "No critical repositories" in caplog.text + + +@pytest.mark.asyncio +async def test_update_critical_repositories_update_in_stored( + hacs: HacsBase, + caplog: pytest.LogCaptureFixture, +): + hacs.data.async_write = AsyncMock() + await hacs.tasks.async_load() + task = hacs.tasks.get("update_critical_repositories") + + assert task + + with patch( + "custom_components.hacs.base.HacsBase.async_github_get_hacs_default_file", + return_value=[{"repository": "test/test", "reason": "test", "link": "test"}], + ), patch( + "custom_components.hacs.utils.store.json_util.load_json", + return_value={ + "version": VERSION_STORAGE, + "data": [{"acknowledged": True, "repository": "test/test"}], + }, + ): + await task.execute_task() + assert "Resarting Home Assistant" not in caplog.text + assert "The queue is empty" in caplog.text + + +@pytest.mark.asyncio +async def test_update_critical_repositories_update_not_in_stored_not_installed( + hacs: HacsBase, + caplog: pytest.LogCaptureFixture, + repository: HacsRepository, +): + hacs.data.async_write = AsyncMock() + await hacs.tasks.async_load() + task = hacs.tasks.get("update_critical_repositories") + + assert task + + with patch( + "custom_components.hacs.base.HacsBase.async_github_get_hacs_default_file", + return_value=[{"repository": "test/test", "reason": "test", "link": "test"}], + ), patch( + "custom_components.hacs.utils.store.json_util.load_json", + return_value={ + "version": VERSION_STORAGE, + "data": [], + }, + ), patch( + "custom_components.hacs.base.HacsRepositories.get_by_full_name", + return_value=repository, + ): + await task.execute_task() + assert "Resarting Home Assistant" not in caplog.text + assert "The queue is empty" in caplog.text + + +@pytest.mark.asyncio +async def test_update_critical_repositories_update_not_in_stored_installed( + hacs: HacsBase, + caplog: pytest.LogCaptureFixture, + repository: HacsRepository, +): + hacs.data.async_write = AsyncMock() + await hacs.tasks.async_load() + task = hacs.tasks.get("update_critical_repositories") + + assert task + + repository.data.installed = True + + with patch( + "custom_components.hacs.base.HacsBase.async_github_get_hacs_default_file", + return_value=[{"repository": "test/test", "reason": "test", "link": "test"}], + ), patch( + "custom_components.hacs.utils.store.json_util.load_json", + return_value={ + "version": VERSION_STORAGE, + "data": [], + }, + ), patch( + "custom_components.hacs.base.HacsRepositories.get_by_full_name", + return_value=repository, + ), patch( + "custom_components.hacs.repositories.base.HacsRepository.uninstall", + return_value=AsyncMock(), + ) as uninstall_mock, patch( + "homeassistant.core.HomeAssistant.async_stop", + return_value=AsyncMock(), + ) as async_stop_mock: + await task.execute_task() + uninstall_mock.assert_called() + async_stop_mock.assert_called() + assert "Resarting Home Assistant" in caplog.text + assert "Queue execution finished for 1 tasks finished" in caplog.text diff --git a/tests/tasks/test_update_downloaded_repositories.py b/tests/tasks/test_update_downloaded_repositories.py new file mode 100644 index 00000000000..bebda59f5dc --- /dev/null +++ b/tests/tasks/test_update_downloaded_repositories.py @@ -0,0 +1,48 @@ +# pylint: disable=missing-function-docstring,missing-module-docstring, protected-access +import pytest + +from custom_components.hacs.base import HacsBase +from custom_components.hacs.enums import HacsCategory, HacsStage +from custom_components.hacs.repositories.base import HacsRepository + + +@pytest.mark.asyncio +async def test_update_downloaded_repositories(hacs: HacsBase, repository: HacsRepository): + await hacs.tasks.async_load() + task = hacs.tasks.get("update_downloaded_repositories") + + repository.data.category = HacsCategory.INTEGRATION + repository.data.installed = True + hacs.repositories.register(repository) + + hacs.enable_hacs_category(HacsCategory.INTEGRATION) + + assert task + + assert hacs.queue.pending_tasks == 0 + await task.execute_task() + assert hacs.queue.pending_tasks == 1 + + +@pytest.mark.asyncio +async def test_update_downloaded_repositories_skip_hacs_on_startup( + hacs: HacsBase, + repository: HacsRepository, +): + await hacs.tasks.async_load() + task = hacs.tasks.get("update_downloaded_repositories") + + hacs.status.startup = True + + repository.data.category = HacsCategory.INTEGRATION + repository.data.installed = True + repository.data.full_name = "hacs/integration" + hacs.repositories.register(repository) + + hacs.enable_hacs_category(HacsCategory.INTEGRATION) + + assert task + + assert hacs.queue.pending_tasks == 0 + await task.execute_task() + assert hacs.queue.pending_tasks == 0 diff --git a/tests/tasks/test_update_removed_repositories.py b/tests/tasks/test_update_removed_repositories.py new file mode 100644 index 00000000000..2fe29731ae9 --- /dev/null +++ b/tests/tasks/test_update_removed_repositories.py @@ -0,0 +1,167 @@ +# pylint: disable=missing-function-docstring,missing-module-docstring, protected-access +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from custom_components.hacs.base import HacsBase, HacsRepositories +from custom_components.hacs.enums import HacsDisabledReason +from custom_components.hacs.exceptions import HacsException +from custom_components.hacs.repositories.base import HacsRepository +from custom_components.hacs.utils.data import HacsData + + +@pytest.mark.asyncio +async def test_update_removed_repositories_no_removed(hacs: HacsBase): + hacs.data.async_write = AsyncMock() + await hacs.tasks.async_load() + task = hacs.tasks.get("update_removed_repositories") + + assert task + + with patch( + "custom_components.hacs.base.HacsBase.async_github_get_hacs_default_file", + return_value=[], + ), patch( + "custom_components.hacs.repositories.base.HacsRepository.remove", + return_value=MagicMock(), + ) as remove_mock: + await task.execute_task() + hacs.data.async_write.assert_not_called() + remove_mock.assert_not_called() + + +@pytest.mark.asyncio +async def test_update_removed_repositories_exception(hacs: HacsBase): + hacs.data.async_write = AsyncMock() + await hacs.tasks.async_load() + task = hacs.tasks.get("update_removed_repositories") + + assert task + + with patch( + "custom_components.hacs.base.HacsBase.async_github_get_hacs_default_file", + side_effect=HacsException("err"), + ), patch( + "custom_components.hacs.repositories.base.HacsRepository.remove", + return_value=MagicMock(), + ) as remove_mock: + await task.execute_task() + hacs.data.async_write.assert_not_called() + remove_mock.assert_not_called() + + +@pytest.mark.asyncio +async def test_update_removed_repositories_not_tracked(hacs: HacsBase): + hacs.data.async_write = AsyncMock() + await hacs.tasks.async_load() + task = hacs.tasks.get("update_removed_repositories") + + assert task + + with patch( + "custom_components.hacs.base.HacsBase.async_github_get_hacs_default_file", + return_value=[{"repository": "test/test"}], + ), patch( + "custom_components.hacs.repositories.base.HacsRepository.remove", + return_value=MagicMock(), + ) as remove_mock: + await task.execute_task() + hacs.data.async_write.assert_not_called() + remove_mock.assert_not_called() + + +@pytest.mark.asyncio +async def test_update_removed_repositories_installed_not_critical( + hacs: HacsBase, + repository: HacsRepository, + caplog: pytest.LogCaptureFixture, +): + hacs.data.async_write = AsyncMock() + await hacs.tasks.async_load() + task = hacs.tasks.get("update_removed_repositories") + + assert task + + repository.data.installed = True + + with patch( + "custom_components.hacs.base.HacsBase.async_github_get_hacs_default_file", + return_value=[{"repository": "test/test", "removal_type": "generic"}], + ), patch( + "custom_components.hacs.base.HacsRepositories.get_by_full_name", + return_value=repository, + ), patch( + "custom_components.hacs.repositories.base.HacsRepository.remove", + return_value=MagicMock(), + ) as remove_mock: + await task.execute_task() + hacs.data.async_write.assert_not_called() + remove_mock.assert_not_called() + assert ( + "You have 'test/test' installed with HACS this repository has been removed from HACS" + in caplog.text + ) + + +@pytest.mark.asyncio +async def test_update_removed_repositories_installed_critical( + hacs: HacsBase, + repository: HacsRepository, + caplog: pytest.LogCaptureFixture, +): + hacs.data.async_write = AsyncMock() + await hacs.tasks.async_load() + task = hacs.tasks.get("update_removed_repositories") + + assert task + + repository.data.installed = True + + with patch( + "custom_components.hacs.base.HacsBase.async_github_get_hacs_default_file", + return_value=[{"repository": "test/test", "removal_type": "critical"}], + ), patch( + "custom_components.hacs.base.HacsRepositories.get_by_full_name", + return_value=repository, + ), patch( + "custom_components.hacs.repositories.base.HacsRepository.remove", + return_value=MagicMock(), + ) as remove_mock: + await task.execute_task() + hacs.data.async_write.assert_called() + remove_mock.assert_called() + assert ( + "You have 'test/test' installed with HACS this repository has been removed from HACS" + not in caplog.text + ) + + +@pytest.mark.asyncio +async def test_update_removed_repositories_not_installed_critical( + hacs: HacsBase, + repository: HacsRepository, + caplog: pytest.LogCaptureFixture, +): + hacs.data.async_write = AsyncMock() + await hacs.tasks.async_load() + task = hacs.tasks.get("update_removed_repositories") + + assert task + + with patch( + "custom_components.hacs.base.HacsBase.async_github_get_hacs_default_file", + return_value=[{"repository": "test/test", "removal_type": "critical"}], + ), patch( + "custom_components.hacs.base.HacsRepositories.get_by_full_name", + return_value=repository, + ), patch( + "custom_components.hacs.repositories.base.HacsRepository.remove", + return_value=MagicMock(), + ) as remove_mock: + await task.execute_task() + hacs.data.async_write.assert_called() + remove_mock.assert_called() + assert ( + "You have 'test/test' installed with HACS this repository has been removed from HACS" + not in caplog.text + )