diff --git a/action/action.py b/action/action.py index 00841c76560..302b8ac161f 100644 --- a/action/action.py +++ b/action/action.py @@ -4,8 +4,8 @@ import logging import os -import aiohttp from aiogithubapi import GitHub, GitHubAPI +import aiohttp from homeassistant.core import HomeAssistant from custom_components.hacs.base import HacsBase diff --git a/custom_components/hacs/repositories/base.py b/custom_components/hacs/repositories/base.py index 28919c2e630..84f7dcb1eda 100644 --- a/custom_components/hacs/repositories/base.py +++ b/custom_components/hacs/repositories/base.py @@ -621,14 +621,12 @@ async def download_content(self) -> None: async def async_get_hacs_json(self, ref: str = None) -> dict[str, Any] | None: """Get the content of the hacs.json file.""" - ref = ref or version_to_download(self) - try: response = await self.hacs.async_github_api_method( method=self.hacs.githubapi.repos.contents.get, repository=self.data.full_name, path=RepositoryFile.HACS_JSON, - **{"params": {"ref": ref}}, + **{"params": {"ref": ref or version_to_download(self)}}, ) return json.loads(decode_content(response.data.content)) except BaseException: # pylint: disable=broad-except diff --git a/custom_components/hacs/repositories/integration.py b/custom_components/hacs/repositories/integration.py index 46edf538191..46b47a951c2 100644 --- a/custom_components/hacs/repositories/integration.py +++ b/custom_components/hacs/repositories/integration.py @@ -1,14 +1,16 @@ """Class for integrations in HACS.""" from __future__ import annotations -from typing import TYPE_CHECKING +import json +from typing import TYPE_CHECKING, Any from homeassistant.loader import async_get_custom_components -from ..enums import HacsCategory, HacsGitHubRepo +from ..enums import HacsCategory, HacsGitHubRepo, RepositoryFile from ..exceptions import HacsException -from ..utils import filters -from ..utils.information import get_integration_manifest +from ..utils.decode import decode_content +from ..utils.filters import get_first_directory_in_directory +from ..utils.version import version_to_download from .base import HacsRepository if TYPE_CHECKING: @@ -51,19 +53,32 @@ async def validate_repository(self): self.content.path.remote = "" if self.content.path.remote == "custom_components": - name = filters.get_first_directory_in_directory(self.tree, "custom_components") + name = get_first_directory_in_directory(self.tree, "custom_components") if name is None: raise HacsException( f"Repository structure for {self.ref.replace('tags/','')} is not compliant" ) self.content.path.remote = f"custom_components/{name}" - try: - await get_integration_manifest(self) - except HacsException as exception: - if self.hacs.system.action: - raise HacsException(f"::error:: {exception}") from exception - self.logger.error("%s %s", self, exception) + # Get the content of manifest.json + if manifest := await self.async_get_integration_manifest(): + try: + self.integration_manifest = manifest + self.data.authors = manifest["codeowners"] + self.data.domain = manifest["domain"] + self.data.manifest_name = manifest["name"] + self.data.config_flow = manifest.get("config_flow", False) + + # Set local path + self.content.path.local = self.localpath + + except KeyError as exception: + self.validate.errors.append( + f"Missing expected key '{exception}' in { RepositoryFile.MAINIFEST_JSON}" + ) + self.hacs.log.error( + "Missing expected key '%s' in '%s'", exception, RepositoryFile.MAINIFEST_JSON + ) # Handle potential errors if self.validate.errors: @@ -81,13 +96,28 @@ async def update_repository(self, ignore_issues=False, force=False): self.content.path.remote = "" if self.content.path.remote == "custom_components": - name = filters.get_first_directory_in_directory(self.tree, "custom_components") + name = get_first_directory_in_directory(self.tree, "custom_components") self.content.path.remote = f"custom_components/{name}" - try: - await get_integration_manifest(self) - except HacsException as exception: - self.logger.error("%s %s", self, exception) + # Get the content of manifest.json + if manifest := await self.async_get_integration_manifest(): + try: + self.integration_manifest = manifest + self.data.authors = manifest["codeowners"] + self.data.domain = manifest["domain"] + self.data.manifest_name = manifest["name"] + self.data.config_flow = manifest.get("config_flow", False) + + # Set local path + self.content.path.local = self.localpath + + except KeyError as exception: + self.validate.errors.append( + f"Missing expected key '{exception}' in { RepositoryFile.MAINIFEST_JSON}" + ) + self.hacs.log.error( + "Missing expected key '%s' in '%s'", exception, RepositoryFile.MAINIFEST_JSON + ) # Set local path self.content.path.local = self.localpath @@ -98,3 +128,25 @@ async def reload_custom_components(self): del self.hacs.hass.data["custom_components"] await async_get_custom_components(self.hacs.hass) self.logger.info("Custom_component cache reloaded") + + async def async_get_integration_manifest(self, ref: str = None) -> dict[str, Any] | None: + """Get the content of the manifest.json file.""" + manifest_path = ( + "manifest.json" + if self.data.content_in_root + else f"{self.content.path.remote}/{RepositoryFile.MAINIFEST_JSON}" + ) + + if not manifest_path in (x.full_path for x in self.tree): + raise HacsException(f"No {RepositoryFile.MAINIFEST_JSON} file found '{manifest_path}'") + + try: + response = await self.hacs.async_github_api_method( + method=self.hacs.githubapi.repos.contents.get, + repository=self.data.full_name, + path=manifest_path, + **{"params": {"ref": ref or version_to_download(self)}}, + ) + return json.loads(decode_content(response.data.content)) + except BaseException as err: # pylint: disable=broad-except + pass diff --git a/custom_components/hacs/utils/information.py b/custom_components/hacs/utils/information.py index ade4e55f50f..0dbeb5350ff 100644 --- a/custom_components/hacs/utils/information.py +++ b/custom_components/hacs/utils/information.py @@ -1,6 +1,4 @@ """Return repository information if any.""" -import json - from aiogithubapi import AIOGitHubAPIException from ..exceptions import HacsException @@ -22,31 +20,3 @@ async def get_releases(repository, prerelease=False, returnlimit=5): return releases except (ValueError, AIOGitHubAPIException) as exception: raise HacsException(exception) - - -async def get_integration_manifest(repository): - """Return the integration manifest.""" - if repository.data.content_in_root: - manifest_path = "manifest.json" - else: - manifest_path = f"{repository.content.path.remote}/manifest.json" - if not manifest_path in [x.full_path for x in repository.tree]: - raise HacsException(f"No file found '{manifest_path}'") - try: - manifest = await repository.repository_object.get_contents(manifest_path, repository.ref) - manifest = json.loads(manifest.content) - except BaseException as exception: # pylint: disable=broad-except - raise HacsException(f"Could not read manifest.json [{exception}]") - - try: - repository.integration_manifest = manifest - repository.data.authors = manifest["codeowners"] - repository.data.domain = manifest["domain"] - repository.data.manifest_name = manifest["name"] - repository.data.config_flow = manifest.get("config_flow", False) - - # Set local path - repository.content.path.local = repository.localpath - - except KeyError as exception: - raise HacsException(f"Missing expected key {exception} in '{manifest_path}'") from exception diff --git a/tests/common.py b/tests/common.py index 959eade8ab4..c73fba428cf 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,4 +1,6 @@ # pylint: disable=missing-docstring,invalid-name +from __future__ import annotations + import asyncio from contextlib import contextmanager import functools as ft @@ -27,29 +29,38 @@ def fixture(filename, asjson=True): """Load a fixture.""" - path = os.path.join(os.path.dirname(__file__), "fixtures", filename) - with open(path, encoding="utf-8") as fptr: - if asjson: - return json.loads(fptr.read()) - return fptr.read() + filename = f"{filename}.json" if "." not in filename else filename + path = os.path.join( + os.path.dirname(__file__), + "fixtures", + filename.lower().replace("/", "_"), + ) + try: + with open(path, encoding="utf-8") as fptr: + _LOGGER.debug("Loading fixture from %s", path) + if asjson: + return json.loads(fptr.read()) + return fptr.read() + except OSError as err: + raise OSError(f"Missing fixture for {path.split('fixtures/')[1]}") from err def dummy_repository_base(hacs, repository=None): if repository is None: repository = HacsRepository(hacs) + repository.data.full_name = "test/test" + repository.data.full_name_lower = "test/test" repository.hacs = hacs repository.hacs.hass = hacs.hass repository.hacs.core.config_path = hacs.hass.config.path() repository.logger = get_hacs_logger() - repository.data.full_name = "test/test" - repository.data.full_name_lower = "test/test" repository.data.domain = "test" repository.data.last_version = "3" repository.data.selected_tag = "3" repository.ref = version_to_download(repository) repository.integration_manifest = {"config_flow": False, "domain": "test"} repository.data.published_tags = ["1", "2", "3"] - repository.data.update_data(fixture("repository_data.json")) + repository.data.update_data(fixture("repository_data.json", asjson=True)) async def update_repository(): pass diff --git a/tests/conftest.py b/tests/conftest.py index 77e7b53b954..f810553c552 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -125,9 +125,6 @@ def hacs(hass: HomeAssistant): hacs_obj.version = hacs_obj.integration.version hacs_obj.configuration.token = TOKEN - if not "PYTEST" in os.environ and "GITHUB_ACTION" in os.environ: - hacs_obj.system.action = True - hass.data[DOMAIN] = hacs_obj yield hacs_obj diff --git a/tests/helpers/functions/test_logger.py b/tests/helpers/functions/test_logger.py index a2407528582..3af45cf5800 100644 --- a/tests/helpers/functions/test_logger.py +++ b/tests/helpers/functions/test_logger.py @@ -1,9 +1,8 @@ -import os +from logging import Logger from custom_components.hacs.utils.logger import get_hacs_logger def test_logger(): - os.environ["GITHUB_ACTION"] = "value" - get_hacs_logger() - del os.environ["GITHUB_ACTION"] + hacs_logger = get_hacs_logger() + assert isinstance(hacs_logger, Logger) diff --git a/tests/helpers/information/test_get_integration_manifest.py b/tests/helpers/information/test_get_integration_manifest.py index 5967bf55690..edf22760704 100644 --- a/tests/helpers/information/test_get_integration_manifest.py +++ b/tests/helpers/information/test_get_integration_manifest.py @@ -4,13 +4,10 @@ import json from aiogithubapi.objects.repository.content import AIOGitHubAPIRepositoryTreeContent -import aiohttp import pytest from custom_components.hacs.exceptions import HacsException -from custom_components.hacs.utils.information import get_integration_manifest -from tests.common import TOKEN from tests.sample_data import ( integration_manifest, repository_data, @@ -61,7 +58,7 @@ async def test_get_integration_manifest(repository_integration, aresponses): "main", ) ] - await get_integration_manifest(repository_integration) + await repository_integration.async_get_integration_manifest() assert repository_integration.data.domain == integration_manifest["domain"] @@ -86,100 +83,4 @@ async def test_get_integration_manifest_no_file(repository_integration, arespons ) = await repository_integration.async_get_legacy_repository_object() repository_integration.content.path.remote = "custom_components/test" with pytest.raises(HacsException): - await get_integration_manifest(repository_integration) - - -@pytest.mark.asyncio -async def test_get_integration_manifest_format_issue(repository_integration, aresponses): - aresponses.add( - "api.github.com", - "/rate_limit", - "get", - aresponses.Response(body=b"{}", headers=response_rate_limit_header), - ) - aresponses.add( - "api.github.com", - "/repos/test/test", - "get", - aresponses.Response(body=json.dumps(repository_data), headers=response_rate_limit_header), - ) - aresponses.add( - "api.github.com", - "/rate_limit", - "get", - aresponses.Response(body=b"{}", headers=response_rate_limit_header), - ) - aresponses.add( - "api.github.com", - "/repos/test/test/contents/custom_components/test/manifest.json", - "get", - aresponses.Response( - body=json.dumps({"content": {"wrong": "format"}}), - headers=response_rate_limit_header, - ), - ) - - ( - repository_integration.repository_object, - _, - ) = await repository_integration.async_get_legacy_repository_object() - repository_integration.content.path.remote = "custom_components/test" - repository_integration.tree = [ - AIOGitHubAPIRepositoryTreeContent( - {"path": "custom_components/test/manifest.json", "type": "blob"}, - "test/test", - "main", - ) - ] - with pytest.raises(HacsException): - await get_integration_manifest(repository_integration) - - -@pytest.mark.asyncio -async def test_get_integration_manifest_missing_required_key( - repository_integration, aresponses, event_loop -): - aresponses.add( - "api.github.com", - "/rate_limit", - "get", - aresponses.Response(body=b"{}", headers=response_rate_limit_header), - ) - aresponses.add( - "api.github.com", - "/repos/test/test", - "get", - aresponses.Response(body=json.dumps(repository_data), headers=response_rate_limit_header), - ) - aresponses.add( - "api.github.com", - "/rate_limit", - "get", - aresponses.Response(body=b"{}", headers=response_rate_limit_header), - ) - del integration_manifest["domain"] - content = base64.b64encode(json.dumps(integration_manifest).encode("utf-8")) - aresponses.add( - "api.github.com", - "/repos/test/test/contents/custom_components/test/manifest.json", - "get", - aresponses.Response( - body=json.dumps({"content": content.decode("utf-8")}), - headers=response_rate_limit_header, - ), - ) - - ( - repository_integration.repository_object, - _, - ) = await repository_integration.async_get_legacy_repository_object() - repository_integration.content.path.remote = "custom_components/test" - repository_integration.tree = [ - AIOGitHubAPIRepositoryTreeContent( - {"path": "custom_components/test/manifest.json", "type": "blob"}, - "test/test", - "main", - ) - ] - with pytest.raises(HacsException): - await get_integration_manifest(repository_integration) + await repository_integration.async_get_integration_manifest()