Refactoring around add-on store Repository classes (#5990)

* Rename repository fixture to test_repository

Also don't remove the built-in repositories. The list was incomplete,
and tests don't seem to require that anymore.

* Get rid of StoreType

The type doesn't have much value, we have constant strings anyways.

* Introduce types.py

* Use slug to determine which repository urls to return

* Simplify BuiltinRepository enum

* Mock GitRepo load

* Improve URL handling and repository creation logic

* Refactor update_repositories

* Get rid of get_from_url

It is no longer used in production code.

* More refactoring

* Address pylint

* Introduce is_git_based property to Repository class

Return all git based URLs, including the Core repository.

* Revert "Introduce is_git_based property to Repository class"

This reverts commit dfd5ad79bf23e0e127fc45d97d6f8de0e796faa0.

* Fold type.py into const.py

Align more with how Supervisor code is typically structured.

* Update supervisor/store/__init__.py

Co-authored-by: Mike Degatano <michael.degatano@gmail.com>

* Apply repository remove suggestion

* Fix tests

---------

Co-authored-by: Mike Degatano <michael.degatano@gmail.com>
This commit is contained in:
Stefan Agner 2025-07-10 11:07:53 +02:00 committed by GitHub
parent 7873c457d5
commit baf9695cf7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 247 additions and 235 deletions

View File

@ -931,5 +931,5 @@ class Backup(JobGroup):
Return a coroutine. Return a coroutine.
""" """
return self.sys_store.update_repositories( return self.sys_store.update_repositories(
self.repositories, add_with_errors=True, replace=replace self.repositories, issue_on_error=True, replace=replace
) )

View File

@ -4,7 +4,7 @@ import asyncio
from collections.abc import Awaitable from collections.abc import Awaitable
import logging import logging
from ..const import ATTR_REPOSITORIES, URL_HASSIO_ADDONS from ..const import ATTR_REPOSITORIES, REPOSITORY_CORE, URL_HASSIO_ADDONS
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import ( from ..exceptions import (
StoreError, StoreError,
@ -18,14 +18,10 @@ from ..jobs.decorator import Job, JobCondition
from ..resolution.const import ContextType, IssueType, SuggestionType from ..resolution.const import ContextType, IssueType, SuggestionType
from ..utils.common import FileConfiguration from ..utils.common import FileConfiguration
from .addon import AddonStore from .addon import AddonStore
from .const import FILE_HASSIO_STORE, StoreType from .const import FILE_HASSIO_STORE, BuiltinRepository
from .data import StoreData from .data import StoreData
from .repository import Repository from .repository import Repository
from .validate import ( from .validate import SCHEMA_STORE_FILE, ensure_builtin_repositories
BUILTIN_REPOSITORIES,
SCHEMA_STORE_FILE,
ensure_builtin_repositories,
)
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -56,7 +52,8 @@ class StoreManager(CoreSysAttributes, FileConfiguration):
return [ return [
repository.source repository.source
for repository in self.all for repository in self.all
if repository.type == StoreType.GIT if repository.slug
not in {BuiltinRepository.LOCAL.value, BuiltinRepository.CORE.value}
] ]
def get(self, slug: str) -> Repository: def get(self, slug: str) -> Repository:
@ -65,19 +62,11 @@ class StoreManager(CoreSysAttributes, FileConfiguration):
raise StoreNotFound() raise StoreNotFound()
return self.repositories[slug] return self.repositories[slug]
def get_from_url(self, url: str) -> Repository:
"""Return Repository with slug."""
for repository in self.all:
if repository.source != url:
continue
return repository
raise StoreNotFound()
async def load(self) -> None: async def load(self) -> None:
"""Start up add-on management.""" """Start up add-on management."""
# Init custom repositories and load add-ons # Init custom repositories and load add-ons
await self.update_repositories( await self.update_repositories(
self._data[ATTR_REPOSITORIES], add_with_errors=True self._data[ATTR_REPOSITORIES], issue_on_error=True
) )
@Job( @Job(
@ -126,14 +115,14 @@ class StoreManager(CoreSysAttributes, FileConfiguration):
) )
async def add_repository(self, url: str, *, persist: bool = True) -> None: async def add_repository(self, url: str, *, persist: bool = True) -> None:
"""Add a repository.""" """Add a repository."""
await self._add_repository(url, persist=persist, add_with_errors=False) await self._add_repository(url, persist=persist, issue_on_error=False)
async def _add_repository( async def _add_repository(
self, url: str, *, persist: bool = True, add_with_errors: bool = False self, url: str, *, persist: bool = True, issue_on_error: bool = False
) -> None: ) -> None:
"""Add a repository.""" """Add a repository."""
if url == URL_HASSIO_ADDONS: if url == URL_HASSIO_ADDONS:
url = StoreType.CORE url = REPOSITORY_CORE
repository = Repository.create(self.coresys, url) repository = Repository.create(self.coresys, url)
@ -145,7 +134,7 @@ class StoreManager(CoreSysAttributes, FileConfiguration):
await repository.load() await repository.load()
except StoreGitCloneError as err: except StoreGitCloneError as err:
_LOGGER.error("Can't retrieve data from %s due to %s", url, err) _LOGGER.error("Can't retrieve data from %s due to %s", url, err)
if add_with_errors: if issue_on_error:
self.sys_resolution.create_issue( self.sys_resolution.create_issue(
IssueType.FATAL_ERROR, IssueType.FATAL_ERROR,
ContextType.STORE, ContextType.STORE,
@ -158,7 +147,7 @@ class StoreManager(CoreSysAttributes, FileConfiguration):
except StoreGitError as err: except StoreGitError as err:
_LOGGER.error("Can't load data from repository %s due to %s", url, err) _LOGGER.error("Can't load data from repository %s due to %s", url, err)
if add_with_errors: if issue_on_error:
self.sys_resolution.create_issue( self.sys_resolution.create_issue(
IssueType.FATAL_ERROR, IssueType.FATAL_ERROR,
ContextType.STORE, ContextType.STORE,
@ -171,7 +160,7 @@ class StoreManager(CoreSysAttributes, FileConfiguration):
except StoreJobError as err: except StoreJobError as err:
_LOGGER.error("Can't add repository %s due to %s", url, err) _LOGGER.error("Can't add repository %s due to %s", url, err)
if add_with_errors: if issue_on_error:
self.sys_resolution.create_issue( self.sys_resolution.create_issue(
IssueType.FATAL_ERROR, IssueType.FATAL_ERROR,
ContextType.STORE, ContextType.STORE,
@ -184,7 +173,7 @@ class StoreManager(CoreSysAttributes, FileConfiguration):
else: else:
if not await repository.validate(): if not await repository.validate():
if add_with_errors: if issue_on_error:
_LOGGER.error("%s is not a valid add-on repository", url) _LOGGER.error("%s is not a valid add-on repository", url)
self.sys_resolution.create_issue( self.sys_resolution.create_issue(
IssueType.CORRUPT_REPOSITORY, IssueType.CORRUPT_REPOSITORY,
@ -213,7 +202,7 @@ class StoreManager(CoreSysAttributes, FileConfiguration):
async def remove_repository(self, repository: Repository, *, persist: bool = True): async def remove_repository(self, repository: Repository, *, persist: bool = True):
"""Remove a repository.""" """Remove a repository."""
if repository.source in BUILTIN_REPOSITORIES: if repository.is_builtin:
raise StoreInvalidAddonRepo( raise StoreInvalidAddonRepo(
"Can't remove built-in repositories!", logger=_LOGGER.error "Can't remove built-in repositories!", logger=_LOGGER.error
) )
@ -236,38 +225,54 @@ class StoreManager(CoreSysAttributes, FileConfiguration):
self, self,
list_repositories: list[str], list_repositories: list[str],
*, *,
add_with_errors: bool = False, issue_on_error: bool = False,
replace: bool = True, replace: bool = True,
): ):
"""Add a new custom repository.""" """Update repositories by adding new ones and removing stale ones."""
new_rep = set( current_repositories = {repository.source for repository in self.all}
ensure_builtin_repositories(list_repositories)
if replace # Determine changes needed
else list_repositories + self.repository_urls if replace:
) target_repositories = set(ensure_builtin_repositories(list_repositories))
old_rep = {repository.source for repository in self.all} repositories_to_add = target_repositories - current_repositories
else:
# When not replacing, just add the new repositories
repositories_to_add = set(list_repositories) - current_repositories
target_repositories = current_repositories | repositories_to_add
# Add new repositories # Add new repositories
add_errors = await asyncio.gather( add_errors = await asyncio.gather(
*[ *[
self._add_repository(url, persist=False, add_with_errors=True) # Use _add_repository to avoid JobCondition.SUPERVISOR_UPDATED
if add_with_errors # to prevent proper loading of repositories on startup.
self._add_repository(url, persist=False, issue_on_error=True)
if issue_on_error
else self.add_repository(url, persist=False) else self.add_repository(url, persist=False)
for url in new_rep - old_rep for url in repositories_to_add
], ],
return_exceptions=True, return_exceptions=True,
) )
# Delete stale repositories remove_errors: list[BaseException | None] = []
remove_errors = await asyncio.gather( if replace:
*[ # Determine repositories to remove
self.remove_repository(self.get_from_url(url), persist=False) repositories_to_remove: list[Repository] = [
for url in old_rep - new_rep - BUILTIN_REPOSITORIES repository
], for repository in self.all
return_exceptions=True, if repository.source not in target_repositories
) and not repository.is_builtin
]
# Always update data, even there are errors, some changes may have succeeded # Remove repositories
remove_errors = await asyncio.gather(
*[
self.remove_repository(repository, persist=False)
for repository in repositories_to_remove
],
return_exceptions=True,
)
# Always update data, even if there are errors, some changes may have succeeded
await self.data.update() await self.data.update()
await self._read_addons() await self._read_addons()

View File

@ -3,14 +3,39 @@
from enum import StrEnum from enum import StrEnum
from pathlib import Path from pathlib import Path
from ..const import SUPERVISOR_DATA from ..const import (
REPOSITORY_CORE,
REPOSITORY_LOCAL,
SUPERVISOR_DATA,
URL_HASSIO_ADDONS,
)
FILE_HASSIO_STORE = Path(SUPERVISOR_DATA, "store.json") FILE_HASSIO_STORE = Path(SUPERVISOR_DATA, "store.json")
"""Repository type definitions for the store."""
class StoreType(StrEnum): class BuiltinRepository(StrEnum):
"""Store Types.""" """All built-in repositories that come pre-configured."""
CORE = "core" # Local repository (non-git, special handling)
LOCAL = "local" LOCAL = REPOSITORY_LOCAL
GIT = "git"
# Git-based built-in repositories
CORE = REPOSITORY_CORE
COMMUNITY_ADDONS = "https://github.com/hassio-addons/repository"
ESPHOME = "https://github.com/esphome/home-assistant-addon"
MUSIC_ASSISTANT = "https://github.com/music-assistant/home-assistant-addon"
@property
def git_url(self) -> str:
"""Return the git URL for this repository."""
if self == BuiltinRepository.LOCAL:
raise RuntimeError("Local repository does not have a git URL")
if self == BuiltinRepository.CORE:
return URL_HASSIO_ADDONS
else:
return self.value # For URL-based repos, value is the URL
# All repositories that are considered "built-in" and protected from removal
ALL_BUILTIN_REPOSITORIES = {repo.value for repo in BuiltinRepository}

View File

@ -25,7 +25,6 @@ from ..exceptions import ConfigurationFileError
from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason
from ..utils.common import find_one_filetype, read_json_or_yaml_file from ..utils.common import find_one_filetype, read_json_or_yaml_file
from ..utils.json import read_json_file from ..utils.json import read_json_file
from .const import StoreType
from .utils import extract_hash_from_path from .utils import extract_hash_from_path
from .validate import SCHEMA_REPOSITORY_CONFIG from .validate import SCHEMA_REPOSITORY_CONFIG
@ -169,7 +168,7 @@ class StoreData(CoreSysAttributes):
self.sys_resolution.add_unhealthy_reason( self.sys_resolution.add_unhealthy_reason(
UnhealthyReason.OSERROR_BAD_MESSAGE UnhealthyReason.OSERROR_BAD_MESSAGE
) )
elif path.stem != StoreType.LOCAL: elif repository != REPOSITORY_LOCAL:
suggestion = [SuggestionType.EXECUTE_RESET] suggestion = [SuggestionType.EXECUTE_RESET]
self.sys_resolution.create_issue( self.sys_resolution.create_issue(
IssueType.CORRUPT_REPOSITORY, IssueType.CORRUPT_REPOSITORY,

View File

@ -10,14 +10,21 @@ import voluptuous as vol
from supervisor.utils import get_latest_mtime from supervisor.utils import get_latest_mtime
from ..const import ATTR_MAINTAINER, ATTR_NAME, ATTR_URL, FILE_SUFFIX_CONFIGURATION from ..const import (
ATTR_MAINTAINER,
ATTR_NAME,
ATTR_URL,
FILE_SUFFIX_CONFIGURATION,
REPOSITORY_CORE,
REPOSITORY_LOCAL,
)
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import ConfigurationFileError, StoreError from ..exceptions import ConfigurationFileError, StoreError
from ..utils.common import read_json_or_yaml_file from ..utils.common import read_json_or_yaml_file
from .const import StoreType from .const import BuiltinRepository
from .git import GitRepo from .git import GitRepo
from .utils import get_hash_from_repository from .utils import get_hash_from_repository
from .validate import SCHEMA_REPOSITORY_CONFIG, BuiltinRepository from .validate import SCHEMA_REPOSITORY_CONFIG
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
UNKNOWN = "unknown" UNKNOWN = "unknown"
@ -26,21 +33,45 @@ UNKNOWN = "unknown"
class Repository(CoreSysAttributes, ABC): class Repository(CoreSysAttributes, ABC):
"""Add-on store repository in Supervisor.""" """Add-on store repository in Supervisor."""
def __init__(self, coresys: CoreSys, repository: str): def __init__(self, coresys: CoreSys, repository: str, local_path: Path, slug: str):
"""Initialize add-on store repository object.""" """Initialize add-on store repository object."""
self._slug: str self._slug: str = slug
self._type: StoreType self._local_path: Path = local_path
self.coresys: CoreSys = coresys self.coresys: CoreSys = coresys
self.source: str = repository self.source: str = repository
@staticmethod @staticmethod
def create(coresys: CoreSys, repository: str) -> Repository: def create(coresys: CoreSys, repository: str) -> Repository:
"""Create a repository instance.""" """Create a repository instance."""
if repository == StoreType.LOCAL:
return RepositoryLocal(coresys)
if repository in BuiltinRepository: if repository in BuiltinRepository:
return RepositoryGitBuiltin(coresys, BuiltinRepository(repository)) return Repository._create_builtin(coresys, BuiltinRepository(repository))
return RepositoryCustom(coresys, repository) else:
return Repository._create_custom(coresys, repository)
@staticmethod
def _create_builtin(coresys: CoreSys, builtin: BuiltinRepository) -> Repository:
"""Create builtin repository."""
if builtin == BuiltinRepository.LOCAL:
slug = REPOSITORY_LOCAL
local_path = coresys.config.path_addons_local
return RepositoryLocal(coresys, local_path, slug)
elif builtin == BuiltinRepository.CORE:
slug = REPOSITORY_CORE
local_path = coresys.config.path_addons_core
else:
# For other builtin repositories (URL-based)
slug = get_hash_from_repository(builtin.value)
local_path = coresys.config.path_addons_git / slug
return RepositoryGitBuiltin(
coresys, builtin.value, local_path, slug, builtin.git_url
)
@staticmethod
def _create_custom(coresys: CoreSys, repository: str) -> RepositoryCustom:
"""Create custom repository."""
slug = get_hash_from_repository(repository)
local_path = coresys.config.path_addons_git / slug
return RepositoryCustom(coresys, repository, local_path, slug)
def __repr__(self) -> str: def __repr__(self) -> str:
"""Return internal representation.""" """Return internal representation."""
@ -52,9 +83,9 @@ class Repository(CoreSysAttributes, ABC):
return self._slug return self._slug
@property @property
def type(self) -> StoreType: def local_path(self) -> Path:
"""Return type of the store.""" """Return local path to repository."""
return self._type return self._local_path
@property @property
def data(self) -> dict: def data(self) -> dict:
@ -76,6 +107,11 @@ class Repository(CoreSysAttributes, ABC):
"""Return url of repository.""" """Return url of repository."""
return self.data.get(ATTR_MAINTAINER, UNKNOWN) return self.data.get(ATTR_MAINTAINER, UNKNOWN)
@property
@abstractmethod
def is_builtin(self) -> bool:
"""Return True if this is a built-in repository."""
@abstractmethod @abstractmethod
async def validate(self) -> bool: async def validate(self) -> bool:
"""Check if store is valid.""" """Check if store is valid."""
@ -103,12 +139,10 @@ class Repository(CoreSysAttributes, ABC):
class RepositoryBuiltin(Repository, ABC): class RepositoryBuiltin(Repository, ABC):
"""A built-in add-on repository.""" """A built-in add-on repository."""
def __init__(self, coresys: CoreSys, builtin: BuiltinRepository) -> None: @property
"""Initialize object.""" def is_builtin(self) -> bool:
super().__init__(coresys, builtin.value) """Return True if this is a built-in repository."""
self._builtin = builtin return True
self._slug = builtin.id
self._type = builtin.type
async def validate(self) -> bool: async def validate(self) -> bool:
"""Assume built-in repositories are always valid.""" """Assume built-in repositories are always valid."""
@ -171,15 +205,15 @@ class RepositoryGit(Repository, ABC):
class RepositoryLocal(RepositoryBuiltin): class RepositoryLocal(RepositoryBuiltin):
"""A local add-on repository.""" """A local add-on repository."""
def __init__(self, coresys: CoreSys) -> None: def __init__(self, coresys: CoreSys, local_path: Path, slug: str) -> None:
"""Initialize object.""" """Initialize object."""
super().__init__(coresys, BuiltinRepository.LOCAL) super().__init__(coresys, BuiltinRepository.LOCAL.value, local_path, slug)
self._latest_mtime: float | None = None self._latest_mtime: float | None = None
async def load(self) -> None: async def load(self) -> None:
"""Load addon repository.""" """Load addon repository."""
self._latest_mtime, _ = await self.sys_run_in_executor( self._latest_mtime, _ = await self.sys_run_in_executor(
get_latest_mtime, self.sys_config.path_addons_local get_latest_mtime, self.local_path
) )
async def update(self) -> bool: async def update(self) -> bool:
@ -189,7 +223,7 @@ class RepositoryLocal(RepositoryBuiltin):
""" """
# Check local modifications # Check local modifications
latest_mtime, modified_path = await self.sys_run_in_executor( latest_mtime, modified_path = await self.sys_run_in_executor(
get_latest_mtime, self.sys_config.path_addons_local get_latest_mtime, self.local_path
) )
if self._latest_mtime != latest_mtime: if self._latest_mtime != latest_mtime:
_LOGGER.debug( _LOGGER.debug(
@ -212,21 +246,26 @@ class RepositoryLocal(RepositoryBuiltin):
class RepositoryGitBuiltin(RepositoryBuiltin, RepositoryGit): class RepositoryGitBuiltin(RepositoryBuiltin, RepositoryGit):
"""A built-in add-on repository based on git.""" """A built-in add-on repository based on git."""
def __init__(self, coresys: CoreSys, builtin: BuiltinRepository) -> None: def __init__(
self, coresys: CoreSys, repository: str, local_path: Path, slug: str, url: str
) -> None:
"""Initialize object.""" """Initialize object."""
super().__init__(coresys, builtin) super().__init__(coresys, repository, local_path, slug)
self._git = GitRepo(coresys, builtin.get_path(coresys), builtin.url) self._git = GitRepo(coresys, local_path, url)
class RepositoryCustom(RepositoryGit): class RepositoryCustom(RepositoryGit):
"""A custom add-on repository.""" """A custom add-on repository."""
def __init__(self, coresys: CoreSys, url: str) -> None: def __init__(self, coresys: CoreSys, url: str, local_path: Path, slug: str) -> None:
"""Initialize object.""" """Initialize object."""
super().__init__(coresys, url) super().__init__(coresys, url, local_path, slug)
self._slug = get_hash_from_repository(url) self._git = GitRepo(coresys, local_path, url)
self._type = StoreType.GIT
self._git = GitRepo(coresys, coresys.config.path_addons_git / self._slug, url) @property
def is_builtin(self) -> bool:
"""Return True if this is a built-in repository."""
return False
async def remove(self) -> None: async def remove(self) -> None:
"""Remove add-on repository.""" """Remove add-on repository."""

View File

@ -1,62 +1,10 @@
"""Validate add-ons options schema.""" """Validate add-ons options schema."""
from enum import StrEnum
from pathlib import Path
import voluptuous as vol import voluptuous as vol
from ..const import ( from ..const import ATTR_MAINTAINER, ATTR_NAME, ATTR_REPOSITORIES, ATTR_URL
ATTR_MAINTAINER,
ATTR_NAME,
ATTR_REPOSITORIES,
ATTR_URL,
URL_HASSIO_ADDONS,
)
from ..coresys import CoreSys
from ..validate import RE_REPOSITORY from ..validate import RE_REPOSITORY
from .const import StoreType from .const import ALL_BUILTIN_REPOSITORIES, BuiltinRepository
from .utils import get_hash_from_repository
URL_COMMUNITY_ADDONS = "https://github.com/hassio-addons/repository"
URL_ESPHOME = "https://github.com/esphome/home-assistant-addon"
URL_MUSIC_ASSISTANT = "https://github.com/music-assistant/home-assistant-addon"
class BuiltinRepository(StrEnum):
"""Built-in add-on repository."""
CORE = StoreType.CORE.value
LOCAL = StoreType.LOCAL.value
COMMUNITY_ADDONS = URL_COMMUNITY_ADDONS
ESPHOME = URL_ESPHOME
MUSIC_ASSISTANT = URL_MUSIC_ASSISTANT
def __init__(self, value: str) -> None:
"""Initialize repository item."""
if value == StoreType.LOCAL:
self.id = value
self.url = ""
self.type = StoreType.LOCAL
elif value == StoreType.CORE:
self.id = value
self.url = URL_HASSIO_ADDONS
self.type = StoreType.CORE
else:
self.id = get_hash_from_repository(value)
self.url = value
self.type = StoreType.GIT
def get_path(self, coresys: CoreSys) -> Path:
"""Get path to git repo for repository."""
if self.id == StoreType.LOCAL:
return coresys.config.path_addons_local
if self.id == StoreType.CORE:
return coresys.config.path_addons_core
return Path(coresys.config.path_addons_git, self.id)
BUILTIN_REPOSITORIES = {r.value for r in BuiltinRepository}
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
SCHEMA_REPOSITORY_CONFIG = vol.Schema( SCHEMA_REPOSITORY_CONFIG = vol.Schema(
@ -75,12 +23,12 @@ def ensure_builtin_repositories(addon_repositories: list[str]) -> list[str]:
Note: This should not be used in validation as the resulting list is not Note: This should not be used in validation as the resulting list is not
stable. This can have side effects when comparing data later on. stable. This can have side effects when comparing data later on.
""" """
return list(set(addon_repositories) | BUILTIN_REPOSITORIES) return list(set(addon_repositories) | ALL_BUILTIN_REPOSITORIES)
def validate_repository(repository: str) -> str: def validate_repository(repository: str) -> str:
"""Validate a valid repository.""" """Validate a valid repository."""
if repository in [StoreType.CORE, StoreType.LOCAL]: if repository in BuiltinRepository:
return repository return repository
data = RE_REPOSITORY.match(repository) data = RE_REPOSITORY.match(repository)
@ -99,7 +47,7 @@ repositories = vol.All([validate_repository], vol.Unique())
SCHEMA_STORE_FILE = vol.Schema( SCHEMA_STORE_FILE = vol.Schema(
{ {
vol.Optional( vol.Optional(
ATTR_REPOSITORIES, default=list(BUILTIN_REPOSITORIES) ATTR_REPOSITORIES, default=list(ALL_BUILTIN_REPOSITORIES)
): repositories, ): repositories,
}, },
extra=vol.REMOVE_EXTRA, extra=vol.REMOVE_EXTRA,

View File

@ -209,7 +209,7 @@ async def test_watchdog_on_stop(coresys: CoreSys, install_addon_ssh: Addon) -> N
async def test_listener_attached_on_install( async def test_listener_attached_on_install(
coresys: CoreSys, mock_amd64_arch_supported: None, repository coresys: CoreSys, mock_amd64_arch_supported: None, test_repository
): ):
"""Test events listener attached on addon install.""" """Test events listener attached on addon install."""
coresys.hardware.disk.get_disk_free_space = lambda x: 5000 coresys.hardware.disk.get_disk_free_space = lambda x: 5000
@ -242,7 +242,7 @@ async def test_listener_attached_on_install(
) )
async def test_watchdog_during_attach( async def test_watchdog_during_attach(
coresys: CoreSys, coresys: CoreSys,
repository: Repository, test_repository: Repository,
boot_timedelta: timedelta, boot_timedelta: timedelta,
restart_count: int, restart_count: int,
): ):
@ -710,7 +710,7 @@ async def test_local_example_install(
coresys: CoreSys, coresys: CoreSys,
container: MagicMock, container: MagicMock,
tmp_supervisor_data: Path, tmp_supervisor_data: Path,
repository, test_repository,
mock_aarch64_arch_supported: None, mock_aarch64_arch_supported: None,
): ):
"""Test install of an addon.""" """Test install of an addon."""

View File

@ -67,7 +67,7 @@ async def fixture_remove_wait_boot(coresys: CoreSys) -> AsyncGenerator[None]:
@pytest.fixture(name="install_addon_example_image") @pytest.fixture(name="install_addon_example_image")
async def fixture_install_addon_example_image( async def fixture_install_addon_example_image(
coresys: CoreSys, repository coresys: CoreSys, test_repository
) -> Generator[Addon]: ) -> Generator[Addon]:
"""Install local_example add-on with image.""" """Install local_example add-on with image."""
store = coresys.addons.store["local_example_image"] store = coresys.addons.store["local_example_image"]

View File

@ -54,7 +54,7 @@ async def test_addons_info(
# DEPRECATED - Remove with legacy routing logic on 1/2023 # DEPRECATED - Remove with legacy routing logic on 1/2023
async def test_addons_info_not_installed( async def test_addons_info_not_installed(
api_client: TestClient, coresys: CoreSys, repository: Repository api_client: TestClient, coresys: CoreSys, test_repository: Repository
): ):
"""Test getting addon info for not installed addon.""" """Test getting addon info for not installed addon."""
resp = await api_client.get(f"/addons/{TEST_ADDON_SLUG}/info") resp = await api_client.get(f"/addons/{TEST_ADDON_SLUG}/info")
@ -533,7 +533,7 @@ async def test_addon_not_found(
("get", "/addons/local_ssh/logs/boots/1/follow", False), ("get", "/addons/local_ssh/logs/boots/1/follow", False),
], ],
) )
@pytest.mark.usefixtures("repository") @pytest.mark.usefixtures("test_repository")
async def test_addon_not_installed( async def test_addon_not_installed(
api_client: TestClient, method: str, url: str, json_expected: bool api_client: TestClient, method: str, url: str, json_expected: bool
): ):

View File

@ -30,7 +30,7 @@ REPO_URL = "https://github.com/awesome-developer/awesome-repo"
async def test_api_store( async def test_api_store(
api_client: TestClient, api_client: TestClient,
store_addon: AddonStore, store_addon: AddonStore,
repository: Repository, test_repository: Repository,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
): ):
"""Test /store REST API.""" """Test /store REST API."""
@ -38,7 +38,7 @@ async def test_api_store(
result = await resp.json() result = await resp.json()
assert result["data"]["addons"][-1]["slug"] == store_addon.slug assert result["data"]["addons"][-1]["slug"] == store_addon.slug
assert result["data"]["repositories"][-1]["slug"] == repository.slug assert result["data"]["repositories"][-1]["slug"] == test_repository.slug
assert ( assert (
f"Add-on {store_addon.slug} not supported on this platform" not in caplog.text f"Add-on {store_addon.slug} not supported on this platform" not in caplog.text
@ -73,23 +73,25 @@ async def test_api_store_addons_addon_version(
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_api_store_repositories(api_client: TestClient, repository: Repository): async def test_api_store_repositories(
api_client: TestClient, test_repository: Repository
):
"""Test /store/repositories REST API.""" """Test /store/repositories REST API."""
resp = await api_client.get("/store/repositories") resp = await api_client.get("/store/repositories")
result = await resp.json() result = await resp.json()
assert result["data"][-1]["slug"] == repository.slug assert result["data"][-1]["slug"] == test_repository.slug
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_api_store_repositories_repository( async def test_api_store_repositories_repository(
api_client: TestClient, repository: Repository api_client: TestClient, test_repository: Repository
): ):
"""Test /store/repositories/{repository} REST API.""" """Test /store/repositories/{repository} REST API."""
resp = await api_client.get(f"/store/repositories/{repository.slug}") resp = await api_client.get(f"/store/repositories/{test_repository.slug}")
result = await resp.json() result = await resp.json()
assert result["data"]["slug"] == repository.slug assert result["data"]["slug"] == test_repository.slug
async def test_api_store_add_repository( async def test_api_store_add_repository(
@ -106,18 +108,17 @@ async def test_api_store_add_repository(
assert response.status == 200 assert response.status == 200
assert REPO_URL in coresys.store.repository_urls assert REPO_URL in coresys.store.repository_urls
assert isinstance(coresys.store.get_from_url(REPO_URL), Repository)
async def test_api_store_remove_repository( async def test_api_store_remove_repository(
api_client: TestClient, coresys: CoreSys, repository: Repository api_client: TestClient, coresys: CoreSys, test_repository: Repository
): ):
"""Test DELETE /store/repositories/{repository} REST API.""" """Test DELETE /store/repositories/{repository} REST API."""
response = await api_client.delete(f"/store/repositories/{repository.slug}") response = await api_client.delete(f"/store/repositories/{test_repository.slug}")
assert response.status == 200 assert response.status == 200
assert repository.source not in coresys.store.repository_urls assert test_repository.source not in coresys.store.repository_urls
assert repository.slug not in coresys.store.repositories assert test_repository.slug not in coresys.store.repositories
async def test_api_store_update_healthcheck( async def test_api_store_update_healthcheck(
@ -329,7 +330,7 @@ async def test_store_addon_not_found(
("post", "/addons/local_ssh/update"), ("post", "/addons/local_ssh/update"),
], ],
) )
@pytest.mark.usefixtures("repository") @pytest.mark.usefixtures("test_repository")
async def test_store_addon_not_installed(api_client: TestClient, method: str, url: str): async def test_store_addon_not_installed(api_client: TestClient, method: str, url: str):
"""Test store addon not installed error.""" """Test store addon not installed error."""
resp = await api_client.request(method, url) resp = await api_client.request(method, url)

View File

@ -9,12 +9,7 @@ from blockbuster import BlockingError
import pytest import pytest
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.exceptions import ( from supervisor.exceptions import HassioError, HostNotSupportedError, StoreGitError
HassioError,
HostNotSupportedError,
StoreGitError,
StoreNotFound,
)
from supervisor.store.repository import Repository from supervisor.store.repository import Repository
from tests.api import common_test_api_advanced_logs from tests.api import common_test_api_advanced_logs
@ -38,8 +33,6 @@ async def test_api_supervisor_options_add_repository(
): ):
"""Test add a repository via POST /supervisor/options REST API.""" """Test add a repository via POST /supervisor/options REST API."""
assert REPO_URL not in coresys.store.repository_urls assert REPO_URL not in coresys.store.repository_urls
with pytest.raises(StoreNotFound):
coresys.store.get_from_url(REPO_URL)
with ( with (
patch("supervisor.store.repository.RepositoryGit.load", return_value=None), patch("supervisor.store.repository.RepositoryGit.load", return_value=None),
@ -51,23 +44,22 @@ async def test_api_supervisor_options_add_repository(
assert response.status == 200 assert response.status == 200
assert REPO_URL in coresys.store.repository_urls assert REPO_URL in coresys.store.repository_urls
assert isinstance(coresys.store.get_from_url(REPO_URL), Repository)
async def test_api_supervisor_options_remove_repository( async def test_api_supervisor_options_remove_repository(
api_client: TestClient, coresys: CoreSys, repository: Repository api_client: TestClient, coresys: CoreSys, test_repository: Repository
): ):
"""Test remove a repository via POST /supervisor/options REST API.""" """Test remove a repository via POST /supervisor/options REST API."""
assert repository.source in coresys.store.repository_urls assert test_repository.source in coresys.store.repository_urls
assert repository.slug in coresys.store.repositories assert test_repository.slug in coresys.store.repositories
response = await api_client.post( response = await api_client.post(
"/supervisor/options", json={"addons_repositories": []} "/supervisor/options", json={"addons_repositories": []}
) )
assert response.status == 200 assert response.status == 200
assert repository.source not in coresys.store.repository_urls assert test_repository.source not in coresys.store.repository_urls
assert repository.slug not in coresys.store.repositories assert test_repository.slug not in coresys.store.repositories
@pytest.mark.parametrize("git_error", [None, StoreGitError()]) @pytest.mark.parametrize("git_error", [None, StoreGitError()])
@ -87,8 +79,6 @@ async def test_api_supervisor_options_repositories_skipped_on_error(
assert response.status == 400 assert response.status == 400
assert len(coresys.resolution.suggestions) == 0 assert len(coresys.resolution.suggestions) == 0
assert REPO_URL not in coresys.store.repository_urls assert REPO_URL not in coresys.store.repository_urls
with pytest.raises(StoreNotFound):
coresys.store.get_from_url(REPO_URL)
async def test_api_supervisor_options_repo_error_with_config_change( async def test_api_supervisor_options_repo_error_with_config_change(

View File

@ -591,7 +591,7 @@ def run_supervisor_state(request: pytest.FixtureRequest) -> Generator[MagicMock]
@pytest.fixture @pytest.fixture
def store_addon(coresys: CoreSys, tmp_path, repository): def store_addon(coresys: CoreSys, tmp_path, test_repository):
"""Store add-on fixture.""" """Store add-on fixture."""
addon_obj = AddonStore(coresys, "test_store_addon") addon_obj = AddonStore(coresys, "test_store_addon")
@ -604,18 +604,11 @@ def store_addon(coresys: CoreSys, tmp_path, repository):
@pytest.fixture @pytest.fixture
async def repository(coresys: CoreSys): async def test_repository(coresys: CoreSys):
"""Repository fixture.""" """Test add-on store repository fixture."""
coresys.store._data[ATTR_REPOSITORIES].remove(
"https://github.com/hassio-addons/repository"
)
coresys.store._data[ATTR_REPOSITORIES].remove(
"https://github.com/esphome/home-assistant-addon"
)
coresys.config._data[ATTR_ADDONS_CUSTOM_LIST] = [] coresys.config._data[ATTR_ADDONS_CUSTOM_LIST] = []
with ( with (
patch("supervisor.store.validate.BUILTIN_REPOSITORIES", {"local", "core"}),
patch("supervisor.store.git.GitRepo.load", return_value=None), patch("supervisor.store.git.GitRepo.load", return_value=None),
): ):
await coresys.store.load() await coresys.store.load()
@ -633,7 +626,7 @@ async def repository(coresys: CoreSys):
@pytest.fixture @pytest.fixture
async def install_addon_ssh(coresys: CoreSys, repository): async def install_addon_ssh(coresys: CoreSys, test_repository):
"""Install local_ssh add-on.""" """Install local_ssh add-on."""
store = coresys.addons.store[TEST_ADDON_SLUG] store = coresys.addons.store[TEST_ADDON_SLUG]
await coresys.addons.data.install(store) await coresys.addons.data.install(store)
@ -645,7 +638,7 @@ async def install_addon_ssh(coresys: CoreSys, repository):
@pytest.fixture @pytest.fixture
async def install_addon_example(coresys: CoreSys, repository): async def install_addon_example(coresys: CoreSys, test_repository):
"""Install local_example add-on.""" """Install local_example add-on."""
store = coresys.addons.store["local_example"] store = coresys.addons.store["local_example"]
await coresys.addons.data.install(store) await coresys.addons.data.install(store)

View File

@ -10,7 +10,7 @@ from supervisor.resolution.fixups.store_execute_remove import FixupStoreExecuteR
from supervisor.store.repository import Repository from supervisor.store.repository import Repository
async def test_fixup(coresys: CoreSys, repository: Repository): async def test_fixup(coresys: CoreSys, test_repository: Repository):
"""Test fixup.""" """Test fixup."""
store_execute_remove = FixupStoreExecuteRemove(coresys) store_execute_remove = FixupStoreExecuteRemove(coresys)
@ -18,16 +18,20 @@ async def test_fixup(coresys: CoreSys, repository: Repository):
coresys.resolution.add_suggestion( coresys.resolution.add_suggestion(
Suggestion( Suggestion(
SuggestionType.EXECUTE_REMOVE, ContextType.STORE, reference=repository.slug SuggestionType.EXECUTE_REMOVE,
ContextType.STORE,
reference=test_repository.slug,
) )
) )
coresys.resolution.add_issue( coresys.resolution.add_issue(
Issue( Issue(
IssueType.CORRUPT_REPOSITORY, ContextType.STORE, reference=repository.slug IssueType.CORRUPT_REPOSITORY,
ContextType.STORE,
reference=test_repository.slug,
) )
) )
with patch.object(type(repository), "remove") as remove_repo: with patch.object(type(test_repository), "remove") as remove_repo:
await store_execute_remove() await store_execute_remove()
assert remove_repo.called assert remove_repo.called
@ -36,4 +40,4 @@ async def test_fixup(coresys: CoreSys, repository: Repository):
assert len(coresys.resolution.suggestions) == 0 assert len(coresys.resolution.suggestions) == 0
assert len(coresys.resolution.issues) == 0 assert len(coresys.resolution.issues) == 0
assert repository.slug not in coresys.store.repositories assert test_repository.slug not in coresys.store.repositories

View File

@ -3,14 +3,14 @@
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
def test_local_store(coresys: CoreSys, repository) -> None: def test_local_store(coresys: CoreSys, test_repository) -> None:
"""Test loading from local store.""" """Test loading from local store."""
assert coresys.store.get("local") assert coresys.store.get("local")
assert "local_ssh" in coresys.addons.store assert "local_ssh" in coresys.addons.store
def test_core_store(coresys: CoreSys, repository) -> None: def test_core_store(coresys: CoreSys, test_repository) -> None:
"""Test loading from core store.""" """Test loading from core store."""
assert coresys.store.get("core") assert coresys.store.get("core")

View File

@ -15,11 +15,20 @@ from supervisor.exceptions import (
StoreNotFound, StoreNotFound,
) )
from supervisor.resolution.const import SuggestionType from supervisor.resolution.const import SuggestionType
from supervisor.store import BUILTIN_REPOSITORIES, StoreManager from supervisor.store import StoreManager
from supervisor.store.addon import AddonStore from supervisor.store.addon import AddonStore
from supervisor.store.const import ALL_BUILTIN_REPOSITORIES
from supervisor.store.repository import Repository from supervisor.store.repository import Repository
def get_repository_by_url(store_manager: StoreManager, url: str) -> Repository:
"""Test helper to get repository by URL."""
for repository in store_manager.all:
if repository.source == url:
return repository
raise StoreNotFound()
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _auto_supervisor_internet(supervisor_internet): def _auto_supervisor_internet(supervisor_internet):
# Use the supervisor_internet fixture to ensure that all tests has internet access # Use the supervisor_internet fixture to ensure that all tests has internet access
@ -33,7 +42,7 @@ async def test_add_valid_repository(
"""Test add custom repository.""" """Test add custom repository."""
current = coresys.store.repository_urls current = coresys.store.repository_urls
with ( with (
patch("supervisor.store.repository.RepositoryGit.load", return_value=None), patch("supervisor.store.git.GitRepo.load", return_value=None),
patch( patch(
"supervisor.utils.common.read_yaml_file", "supervisor.utils.common.read_yaml_file",
return_value={"name": "Awesome repository"}, return_value={"name": "Awesome repository"},
@ -45,7 +54,7 @@ async def test_add_valid_repository(
else: else:
await store_manager.add_repository("http://example.com") await store_manager.add_repository("http://example.com")
assert store_manager.get_from_url("http://example.com").validate() assert get_repository_by_url(store_manager, "http://example.com").validate()
assert "http://example.com" in coresys.store.repository_urls assert "http://example.com" in coresys.store.repository_urls
@ -54,17 +63,19 @@ async def test_add_invalid_repository(coresys: CoreSys, store_manager: StoreMana
"""Test add invalid custom repository.""" """Test add invalid custom repository."""
current = coresys.store.repository_urls current = coresys.store.repository_urls
with ( with (
patch("supervisor.store.repository.RepositoryGit.load", return_value=None), patch("supervisor.store.git.GitRepo.load", return_value=None),
patch( patch(
"pathlib.Path.read_text", "pathlib.Path.read_text",
return_value="", return_value="",
), ),
): ):
await store_manager.update_repositories( await store_manager.update_repositories(
current + ["http://example.com"], add_with_errors=True current + ["http://example.com"], issue_on_error=True
) )
assert not await store_manager.get_from_url("http://example.com").validate() assert not await get_repository_by_url(
store_manager, "http://example.com"
).validate()
assert "http://example.com" in coresys.store.repository_urls assert "http://example.com" in coresys.store.repository_urls
assert coresys.resolution.suggestions[-1].type == SuggestionType.EXECUTE_REMOVE assert coresys.resolution.suggestions[-1].type == SuggestionType.EXECUTE_REMOVE
@ -77,7 +88,7 @@ async def test_error_on_invalid_repository(
"""Test invalid repository not added.""" """Test invalid repository not added."""
current = coresys.store.repository_urls current = coresys.store.repository_urls
with ( with (
patch("supervisor.store.repository.RepositoryGit.load", return_value=None), patch("supervisor.store.git.GitRepo.load", return_value=None),
patch( patch(
"pathlib.Path.read_text", "pathlib.Path.read_text",
return_value="", return_value="",
@ -91,8 +102,6 @@ async def test_error_on_invalid_repository(
assert "http://example.com" not in coresys.store.repository_urls assert "http://example.com" not in coresys.store.repository_urls
assert len(coresys.resolution.suggestions) == 0 assert len(coresys.resolution.suggestions) == 0
with pytest.raises(StoreNotFound):
store_manager.get_from_url("http://example.com")
async def test_add_invalid_repository_file( async def test_add_invalid_repository_file(
@ -101,7 +110,7 @@ async def test_add_invalid_repository_file(
"""Test add invalid custom repository file.""" """Test add invalid custom repository file."""
current = coresys.store.repository_urls current = coresys.store.repository_urls
with ( with (
patch("supervisor.store.repository.RepositoryGit.load", return_value=None), patch("supervisor.store.git.GitRepo.load", return_value=None),
patch( patch(
"pathlib.Path.read_text", "pathlib.Path.read_text",
return_value=json.dumps({"name": "Awesome repository"}), return_value=json.dumps({"name": "Awesome repository"}),
@ -109,10 +118,12 @@ async def test_add_invalid_repository_file(
patch("pathlib.Path.exists", return_value=False), patch("pathlib.Path.exists", return_value=False),
): ):
await store_manager.update_repositories( await store_manager.update_repositories(
current + ["http://example.com"], add_with_errors=True current + ["http://example.com"], issue_on_error=True
) )
assert not await store_manager.get_from_url("http://example.com").validate() assert not await get_repository_by_url(
store_manager, "http://example.com"
).validate()
assert "http://example.com" in coresys.store.repository_urls assert "http://example.com" in coresys.store.repository_urls
assert coresys.resolution.suggestions[-1].type == SuggestionType.EXECUTE_REMOVE assert coresys.resolution.suggestions[-1].type == SuggestionType.EXECUTE_REMOVE
@ -133,14 +144,13 @@ async def test_add_repository_with_git_error(
): ):
"""Test repo added with issue on git error.""" """Test repo added with issue on git error."""
current = coresys.store.repository_urls current = coresys.store.repository_urls
with patch("supervisor.store.repository.RepositoryGit.load", side_effect=git_error): with patch("supervisor.store.git.GitRepo.load", side_effect=git_error):
await store_manager.update_repositories( await store_manager.update_repositories(
current + ["http://example.com"], add_with_errors=True current + ["http://example.com"], issue_on_error=True
) )
assert "http://example.com" in coresys.store.repository_urls assert "http://example.com" in coresys.store.repository_urls
assert coresys.resolution.suggestions[-1].type == suggestion_type assert coresys.resolution.suggestions[-1].type == suggestion_type
assert isinstance(store_manager.get_from_url("http://example.com"), Repository)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -161,7 +171,7 @@ async def test_error_on_repository_with_git_error(
"""Test repo not added on git error.""" """Test repo not added on git error."""
current = coresys.store.repository_urls current = coresys.store.repository_urls
with ( with (
patch("supervisor.store.repository.RepositoryGit.load", side_effect=git_error), patch("supervisor.store.git.GitRepo.load", side_effect=git_error),
pytest.raises(StoreError), pytest.raises(StoreError),
): ):
if use_update: if use_update:
@ -171,8 +181,6 @@ async def test_error_on_repository_with_git_error(
assert "http://example.com" not in coresys.store.repository_urls assert "http://example.com" not in coresys.store.repository_urls
assert len(coresys.resolution.suggestions) == 0 assert len(coresys.resolution.suggestions) == 0
with pytest.raises(StoreNotFound):
store_manager.get_from_url("http://example.com")
@pytest.mark.asyncio @pytest.mark.asyncio
@ -180,8 +188,8 @@ async def test_preinstall_valid_repository(
coresys: CoreSys, store_manager: StoreManager coresys: CoreSys, store_manager: StoreManager
): ):
"""Test add core repository valid.""" """Test add core repository valid."""
with patch("supervisor.store.repository.RepositoryGit.load", return_value=None): with patch("supervisor.store.git.GitRepo.load", return_value=None):
await store_manager.update_repositories(BUILTIN_REPOSITORIES) await store_manager.update_repositories(list(ALL_BUILTIN_REPOSITORIES))
def validate(): def validate():
assert store_manager.get("core").validate() assert store_manager.get("core").validate()
@ -197,21 +205,21 @@ async def test_preinstall_valid_repository(
async def test_remove_repository( async def test_remove_repository(
coresys: CoreSys, coresys: CoreSys,
store_manager: StoreManager, store_manager: StoreManager,
repository: Repository, test_repository: Repository,
use_update: bool, use_update: bool,
): ):
"""Test removing a custom repository.""" """Test removing a custom repository."""
assert repository.source in coresys.store.repository_urls assert test_repository.source in coresys.store.repository_urls
assert repository.slug in coresys.store.repositories assert test_repository.slug in coresys.store.repositories
if use_update: if use_update:
await store_manager.update_repositories([]) await store_manager.update_repositories([])
else: else:
await store_manager.remove_repository(repository) await store_manager.remove_repository(test_repository)
assert repository.source not in coresys.store.repository_urls assert test_repository.source not in coresys.store.repository_urls
assert repository.slug not in coresys.addons.store assert test_repository.slug not in coresys.addons.store
assert repository.slug not in coresys.store.repositories assert test_repository.slug not in coresys.store.repositories
@pytest.mark.parametrize("use_update", [True, False]) @pytest.mark.parametrize("use_update", [True, False])
@ -243,7 +251,7 @@ async def test_remove_used_repository(
async def test_update_partial_error(coresys: CoreSys, store_manager: StoreManager): async def test_update_partial_error(coresys: CoreSys, store_manager: StoreManager):
"""Test partial error on update does partial save and errors.""" """Test partial error on update does partial save and errors."""
with patch("supervisor.store.repository.RepositoryGit.validate", return_value=True): with patch("supervisor.store.repository.RepositoryGit.validate", return_value=True):
with patch("supervisor.store.repository.RepositoryGit.load", return_value=None): with patch("supervisor.store.git.GitRepo.load", return_value=None):
await store_manager.update_repositories([]) await store_manager.update_repositories([])
store_manager.data.update.assert_called_once() store_manager.data.update.assert_called_once()
@ -254,7 +262,7 @@ async def test_update_partial_error(coresys: CoreSys, store_manager: StoreManage
with ( with (
patch( patch(
"supervisor.store.repository.RepositoryGit.load", "supervisor.store.git.GitRepo.load",
side_effect=[None, StoreGitError()], side_effect=[None, StoreGitError()],
), ),
pytest.raises(StoreError), pytest.raises(StoreError),
@ -268,27 +276,27 @@ async def test_update_partial_error(coresys: CoreSys, store_manager: StoreManage
async def test_error_adding_duplicate( async def test_error_adding_duplicate(
coresys: CoreSys, store_manager: StoreManager, repository: Repository coresys: CoreSys, store_manager: StoreManager, test_repository: Repository
): ):
"""Test adding a duplicate repository causes an error.""" """Test adding a duplicate repository causes an error."""
assert repository.source in coresys.store.repository_urls assert test_repository.source in coresys.store.repository_urls
with ( with (
patch("supervisor.store.repository.RepositoryGit.validate", return_value=True), patch("supervisor.store.repository.RepositoryGit.validate", return_value=True),
patch("supervisor.store.repository.RepositoryGit.load", return_value=None), patch("supervisor.store.git.GitRepo.load", return_value=None),
pytest.raises(StoreError), pytest.raises(StoreError),
): ):
await store_manager.add_repository(repository.source) await store_manager.add_repository(test_repository.source)
async def test_add_with_update_repositories( async def test_add_with_update_repositories(
coresys: CoreSys, store_manager: StoreManager, repository: Repository coresys: CoreSys, store_manager: StoreManager, test_repository: Repository
): ):
"""Test adding repositories to existing ones using update.""" """Test adding repositories to existing ones using update."""
assert repository.source in coresys.store.repository_urls assert test_repository.source in coresys.store.repository_urls
assert "http://example.com" not in coresys.store.repository_urls assert "http://example.com" not in coresys.store.repository_urls
with ( with (
patch("supervisor.store.repository.RepositoryGit.load", return_value=None), patch("supervisor.store.git.GitRepo.load", return_value=None),
patch( patch(
"supervisor.utils.common.read_yaml_file", "supervisor.utils.common.read_yaml_file",
return_value={"name": "Awesome repository"}, return_value={"name": "Awesome repository"},
@ -297,7 +305,7 @@ async def test_add_with_update_repositories(
): ):
await store_manager.update_repositories(["http://example.com"], replace=False) await store_manager.update_repositories(["http://example.com"], replace=False)
assert repository.source in coresys.store.repository_urls assert test_repository.source in coresys.store.repository_urls
assert "http://example.com" in coresys.store.repository_urls assert "http://example.com" in coresys.store.repository_urls
@ -326,7 +334,7 @@ async def test_repositories_loaded_ignore_updates(
): ):
"""Test repositories loaded whether or not supervisor needs an update.""" """Test repositories loaded whether or not supervisor needs an update."""
with ( with (
patch("supervisor.store.repository.RepositoryGit.load", return_value=None), patch("supervisor.store.git.GitRepo.load", return_value=None),
patch.object( patch.object(
type(coresys.supervisor), type(coresys.supervisor),
"need_update", "need_update",

View File

@ -203,7 +203,7 @@ async def test_update_unavailable_addon(
) )
async def test_install_unavailable_addon( async def test_install_unavailable_addon(
coresys: CoreSys, coresys: CoreSys,
repository: Repository, test_repository: Repository,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
config: dict[str, Any], config: dict[str, Any],
log: str, log: str,