mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-09 18:26:30 +00:00
Move repository urls to store settings file (#3665)
* Move repository urls to store settings file * Remove default repos from supervisor config * Fix clone at initial store load * Mock git load in repository fixture
This commit is contained in:
parent
deeaf2133b
commit
ccd2c31390
@ -35,7 +35,7 @@ from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import APIError, APIForbidden
|
||||
from ..store.addon import AddonStore
|
||||
from ..store.repository import Repository
|
||||
from ..validate import validate_repository
|
||||
from ..store.validate import validate_repository
|
||||
|
||||
SCHEMA_UPDATE = vol.Schema(
|
||||
{
|
||||
|
@ -46,8 +46,9 @@ from ..const import (
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import APIError
|
||||
from ..store.validate import repositories
|
||||
from ..utils.validate import validate_timezone
|
||||
from ..validate import repositories, version_tag, wait_boot
|
||||
from ..validate import version_tag, wait_boot
|
||||
from .utils import api_process, api_process_raw, api_validate
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -115,7 +116,7 @@ class APISupervisor(CoreSysAttributes):
|
||||
ATTR_DEBUG_BLOCK: self.sys_config.debug_block,
|
||||
ATTR_DIAGNOSTICS: self.sys_config.diagnostics,
|
||||
ATTR_ADDONS: list_addons,
|
||||
ATTR_ADDONS_REPOSITORIES: self.sys_config.addons_repositories,
|
||||
ATTR_ADDONS_REPOSITORIES: self.sys_store.repository_urls,
|
||||
}
|
||||
|
||||
@api_process
|
||||
|
@ -497,18 +497,16 @@ class Backup(CoreSysAttributes):
|
||||
|
||||
def store_repositories(self):
|
||||
"""Store repository list into backup."""
|
||||
self.repositories = self.sys_config.addons_repositories
|
||||
self.repositories = self.sys_store.repository_urls
|
||||
|
||||
async def restore_repositories(self, replace: bool = False):
|
||||
"""Restore repositories from backup.
|
||||
|
||||
Return a coroutine.
|
||||
"""
|
||||
new_list: set[str] = set(self.repositories)
|
||||
if not replace:
|
||||
new_list.update(self.sys_config.addons_repositories)
|
||||
|
||||
await self.sys_store.update_repositories(list(new_list), add_with_errors=True)
|
||||
await self.sys_store.update_repositories(
|
||||
self.repositories, add_with_errors=True, replace=replace
|
||||
)
|
||||
|
||||
def store_dockerconfig(self):
|
||||
"""Store the configuration for Docker."""
|
||||
|
@ -28,7 +28,8 @@ from ..const import (
|
||||
FOLDER_SHARE,
|
||||
FOLDER_SSL,
|
||||
)
|
||||
from ..validate import SCHEMA_DOCKER_CONFIG, repositories, version_tag
|
||||
from ..store.validate import repositories
|
||||
from ..validate import SCHEMA_DOCKER_CONFIG, version_tag
|
||||
|
||||
ALL_FOLDERS = [
|
||||
FOLDER_SHARE,
|
||||
|
@ -327,3 +327,7 @@ class CoreConfig(FileConfiguration):
|
||||
return
|
||||
|
||||
self._data[ATTR_ADDONS_CUSTOM_LIST].remove(repo)
|
||||
|
||||
def clear_addons_repositories(self) -> None:
|
||||
"""Clear custom repositories list from core config."""
|
||||
self._data[ATTR_ADDONS_CUSTOM_LIST] = []
|
||||
|
@ -52,7 +52,6 @@ def filter_data(coresys: CoreSys, event: dict, hint: dict) -> dict:
|
||||
"supervisor": {
|
||||
"channel": coresys.updater.channel,
|
||||
"installed_addons": installed_addons,
|
||||
"repositories": coresys.config.addons_repositories,
|
||||
},
|
||||
"host": {
|
||||
"arch": coresys.arch.default,
|
||||
@ -79,6 +78,9 @@ def filter_data(coresys: CoreSys, event: dict, hint: dict) -> dict:
|
||||
],
|
||||
"unhealthy": coresys.resolution.unhealthy,
|
||||
},
|
||||
"store": {
|
||||
"repositories": coresys.store.repository_urls,
|
||||
},
|
||||
"misc": {
|
||||
"fallback_dns": coresys.plugins.dns.fallback,
|
||||
},
|
||||
|
@ -29,14 +29,9 @@ class FixupStoreExecuteRemove(FixupBase):
|
||||
|
||||
# Remove repository
|
||||
try:
|
||||
await repository.remove()
|
||||
await self.sys_store.remove_repository(repository)
|
||||
except StoreError:
|
||||
raise ResolutionFixupError() from None
|
||||
else:
|
||||
self.sys_store.repositories.pop(repository.slug, None)
|
||||
|
||||
self.sys_config.drop_addon_repository(repository.source)
|
||||
self.sys_config.save_data()
|
||||
|
||||
@property
|
||||
def suggestion(self) -> SuggestionType:
|
||||
|
@ -2,7 +2,10 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from ..const import URL_HASSIO_ADDONS
|
||||
from supervisor.store.validate import SCHEMA_STORE_FILE
|
||||
from supervisor.utils.common import FileConfiguration
|
||||
|
||||
from ..const import ATTR_REPOSITORIES, URL_HASSIO_ADDONS
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..exceptions import (
|
||||
StoreError,
|
||||
@ -15,29 +18,43 @@ from ..exceptions import (
|
||||
from ..jobs.decorator import Job, JobCondition
|
||||
from ..resolution.const import ContextType, IssueType, SuggestionType
|
||||
from .addon import AddonStore
|
||||
from .const import StoreType
|
||||
from .const import FILE_HASSIO_STORE, StoreType
|
||||
from .data import StoreData
|
||||
from .repository import Repository
|
||||
from .validate import BUILTIN_REPOSITORIES, ensure_builtin_repositories
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
BUILTIN_REPOSITORIES = {StoreType.CORE.value, StoreType.LOCAL.value}
|
||||
|
||||
|
||||
class StoreManager(CoreSysAttributes):
|
||||
class StoreManager(CoreSysAttributes, FileConfiguration):
|
||||
"""Manage add-ons inside Supervisor."""
|
||||
|
||||
def __init__(self, coresys: CoreSys):
|
||||
"""Initialize Docker base wrapper."""
|
||||
super().__init__(FILE_HASSIO_STORE, SCHEMA_STORE_FILE)
|
||||
self.coresys: CoreSys = coresys
|
||||
self.data = StoreData(coresys)
|
||||
self.repositories: dict[str, Repository] = {}
|
||||
self._repositories: dict[str, Repository] = {}
|
||||
|
||||
@property
|
||||
def all(self) -> list[Repository]:
|
||||
"""Return list of add-on repositories."""
|
||||
return list(self.repositories.values())
|
||||
|
||||
@property
|
||||
def repositories(self) -> dict[str, Repository]:
|
||||
"""Return repositories dictionary."""
|
||||
return self._repositories
|
||||
|
||||
@property
|
||||
def repository_urls(self) -> list[str]:
|
||||
"""Return source URL for all git repositories."""
|
||||
return [
|
||||
repository.url
|
||||
for repository in self.all
|
||||
if repository.type == StoreType.GIT
|
||||
]
|
||||
|
||||
def get(self, slug: str) -> Repository:
|
||||
"""Return Repository with slug."""
|
||||
if slug not in self.repositories:
|
||||
@ -56,11 +73,17 @@ class StoreManager(CoreSysAttributes):
|
||||
"""Start up add-on management."""
|
||||
await self.data.update()
|
||||
|
||||
# Init Supervisor built-in repositories
|
||||
repositories = set(self.sys_config.addons_repositories) | BUILTIN_REPOSITORIES
|
||||
# Backwards compatibility - Remove after 2022.9
|
||||
if len(self.sys_config.addons_repositories) > 0:
|
||||
self._data[ATTR_REPOSITORIES] = ensure_builtin_repositories(
|
||||
self.sys_config.addons_repositories
|
||||
)
|
||||
self.sys_config.clear_addons_repositories()
|
||||
|
||||
# Init custom repositories and load add-ons
|
||||
await self.update_repositories(repositories, add_with_errors=True)
|
||||
await self.update_repositories(
|
||||
self._data[ATTR_REPOSITORIES], add_with_errors=True
|
||||
)
|
||||
|
||||
async def reload(self) -> None:
|
||||
"""Update add-ons from repository and reload list."""
|
||||
@ -144,9 +167,9 @@ class StoreManager(CoreSysAttributes):
|
||||
)
|
||||
|
||||
# Add Repository to list
|
||||
if repository.type == StoreType.GIT:
|
||||
self.sys_config.add_addon_repository(repository.source)
|
||||
self._data[ATTR_REPOSITORIES].append(url)
|
||||
self.repositories[repository.slug] = repository
|
||||
self.save_data()
|
||||
|
||||
# Persist changes
|
||||
if persist:
|
||||
@ -155,7 +178,7 @@ class StoreManager(CoreSysAttributes):
|
||||
|
||||
async def remove_repository(self, repository: Repository, *, persist: bool = True):
|
||||
"""Remove a repository."""
|
||||
if repository.type != StoreType.GIT:
|
||||
if repository.url in BUILTIN_REPOSITORIES:
|
||||
raise StoreInvalidAddonRepo(
|
||||
"Can't remove built-in repositories!", logger=_LOGGER.error
|
||||
)
|
||||
@ -166,7 +189,8 @@ class StoreManager(CoreSysAttributes):
|
||||
logger=_LOGGER.error,
|
||||
)
|
||||
await self.repositories.pop(repository.slug).remove()
|
||||
self.sys_config.drop_addon_repository(repository.url)
|
||||
self._data[ATTR_REPOSITORIES].remove(repository.url)
|
||||
self.save_data()
|
||||
|
||||
if persist:
|
||||
await self.data.update()
|
||||
@ -174,10 +198,18 @@ class StoreManager(CoreSysAttributes):
|
||||
|
||||
@Job(conditions=[JobCondition.INTERNET_SYSTEM])
|
||||
async def update_repositories(
|
||||
self, list_repositories, *, add_with_errors: bool = False
|
||||
self,
|
||||
list_repositories: list[str],
|
||||
*,
|
||||
add_with_errors: bool = False,
|
||||
replace: bool = True,
|
||||
):
|
||||
"""Add a new custom repository."""
|
||||
new_rep = set(list_repositories)
|
||||
new_rep = set(
|
||||
ensure_builtin_repositories(list_repositories)
|
||||
if replace
|
||||
else list_repositories + self.repository_urls
|
||||
)
|
||||
old_rep = {repository.source for repository in self.all}
|
||||
|
||||
# Add new repositories
|
||||
|
@ -1,5 +1,10 @@
|
||||
"""Constants for the add-on store."""
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from supervisor.const import SUPERVISOR_DATA
|
||||
|
||||
FILE_HASSIO_STORE = Path(SUPERVISOR_DATA, "store.json")
|
||||
|
||||
|
||||
class StoreType(str, Enum):
|
||||
|
@ -13,8 +13,8 @@ from ..exceptions import StoreGitCloneError, StoreGitError, StoreJobError
|
||||
from ..jobs.decorator import Job, JobCondition
|
||||
from ..resolution.const import ContextType, IssueType, SuggestionType
|
||||
from ..utils import remove_folder
|
||||
from ..validate import RE_REPOSITORY
|
||||
from .utils import get_hash_from_repository
|
||||
from .validate import RE_REPOSITORY
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -36,7 +36,6 @@ class Repository(CoreSysAttributes):
|
||||
self._type = StoreType.CORE
|
||||
else:
|
||||
self.git = GitRepoCustom(coresys, repository)
|
||||
self.source = repository
|
||||
self._slug = get_hash_from_repository(repository)
|
||||
self._type = StoreType.GIT
|
||||
|
||||
|
@ -2,7 +2,19 @@
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from ..const import ATTR_MAINTAINER, ATTR_NAME, ATTR_URL
|
||||
from supervisor.store.const import StoreType
|
||||
|
||||
from ..const import ATTR_MAINTAINER, ATTR_NAME, ATTR_REPOSITORIES, ATTR_URL
|
||||
from ..validate import RE_REPOSITORY
|
||||
|
||||
URL_COMMUNITY_ADDONS = "https://github.com/hassio-addons/repository"
|
||||
URL_ESPHOME = "https://github.com/esphome/home-assistant-addon"
|
||||
BUILTIN_REPOSITORIES = {
|
||||
StoreType.CORE.value,
|
||||
StoreType.LOCAL.value,
|
||||
URL_COMMUNITY_ADDONS,
|
||||
URL_ESPHOME,
|
||||
}
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
SCHEMA_REPOSITORY_CONFIG = vol.Schema(
|
||||
@ -13,3 +25,37 @@ SCHEMA_REPOSITORY_CONFIG = vol.Schema(
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def validate_repository(repository: str) -> str:
|
||||
"""Validate a valid repository."""
|
||||
if repository in [StoreType.CORE.value, StoreType.LOCAL.value]:
|
||||
return repository
|
||||
|
||||
data = RE_REPOSITORY.match(repository)
|
||||
if not data:
|
||||
raise vol.Invalid("No valid repository format!") from None
|
||||
|
||||
# Validate URL
|
||||
# pylint: disable=no-value-for-parameter
|
||||
vol.Url()(data.group("url"))
|
||||
|
||||
return repository
|
||||
|
||||
|
||||
def ensure_builtin_repositories(addon_repositories: list[str]) -> list[str]:
|
||||
"""Ensure builtin repositories are in list."""
|
||||
return list(set(addon_repositories) | BUILTIN_REPOSITORIES)
|
||||
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
repositories = vol.All([validate_repository], vol.Unique(), ensure_builtin_repositories)
|
||||
|
||||
SCHEMA_STORE_FILE = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
ATTR_REPOSITORIES, default=list(BUILTIN_REPOSITORIES)
|
||||
): repositories,
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
@ -41,6 +41,7 @@ from .const import (
|
||||
)
|
||||
from .utils.validate import validate_timezone
|
||||
|
||||
# Move to store.validate when addons_repository config removed
|
||||
RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$")
|
||||
RE_REGISTRY = re.compile(r"^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$")
|
||||
|
||||
@ -84,6 +85,7 @@ def dns_url(url: str) -> str:
|
||||
dns_server_list = vol.All(vol.Length(max=8), [dns_url])
|
||||
|
||||
|
||||
# Remove with addons_repositories config
|
||||
def validate_repository(repository: str) -> str:
|
||||
"""Validate a valid repository."""
|
||||
data = RE_REPOSITORY.match(repository)
|
||||
@ -146,13 +148,7 @@ SCHEMA_SUPERVISOR_CONFIG = vol.Schema(
|
||||
ATTR_VERSION, default=AwesomeVersion(SUPERVISOR_VERSION)
|
||||
): version_tag,
|
||||
vol.Optional(ATTR_IMAGE): docker_image,
|
||||
vol.Optional(
|
||||
ATTR_ADDONS_CUSTOM_LIST,
|
||||
default=[
|
||||
"https://github.com/hassio-addons/repository",
|
||||
"https://github.com/esphome/home-assistant-addon",
|
||||
],
|
||||
): repositories,
|
||||
vol.Optional(ATTR_ADDONS_CUSTOM_LIST, default=[]): repositories,
|
||||
vol.Optional(ATTR_WAIT_BOOT, default=5): wait_boot,
|
||||
vol.Optional(ATTR_LOGGING, default=LogLevel.INFO): vol.Coerce(LogLevel),
|
||||
vol.Optional(ATTR_DEBUG, default=False): vol.Boolean(),
|
||||
|
@ -82,7 +82,7 @@ async def test_api_store_add_repository(api_client: TestClient, coresys: CoreSys
|
||||
)
|
||||
|
||||
assert response.status == 200
|
||||
assert REPO_URL in coresys.config.addons_repositories
|
||||
assert REPO_URL in coresys.store.repository_urls
|
||||
assert isinstance(coresys.store.get_from_url(REPO_URL), Repository)
|
||||
|
||||
|
||||
@ -93,5 +93,5 @@ async def test_api_store_remove_repository(
|
||||
response = await api_client.delete(f"/store/repositories/{repository.slug}")
|
||||
|
||||
assert response.status == 200
|
||||
assert repository.url not in coresys.config.addons_repositories
|
||||
assert repository.url not in coresys.store.repository_urls
|
||||
assert repository.slug not in coresys.store.repositories
|
||||
|
@ -26,7 +26,7 @@ async def test_api_supervisor_options_add_repository(
|
||||
api_client: TestClient, coresys: CoreSys
|
||||
):
|
||||
"""Test add a repository via POST /supervisor/options REST API."""
|
||||
assert REPO_URL not in coresys.config.addons_repositories
|
||||
assert REPO_URL not in coresys.store.repository_urls
|
||||
with pytest.raises(StoreNotFound):
|
||||
coresys.store.get_from_url(REPO_URL)
|
||||
|
||||
@ -38,7 +38,7 @@ async def test_api_supervisor_options_add_repository(
|
||||
)
|
||||
|
||||
assert response.status == 200
|
||||
assert REPO_URL in coresys.config.addons_repositories
|
||||
assert REPO_URL in coresys.store.repository_urls
|
||||
assert isinstance(coresys.store.get_from_url(REPO_URL), Repository)
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ async def test_api_supervisor_options_remove_repository(
|
||||
api_client: TestClient, coresys: CoreSys, repository: Repository
|
||||
):
|
||||
"""Test remove a repository via POST /supervisor/options REST API."""
|
||||
assert repository.url in coresys.config.addons_repositories
|
||||
assert repository.url in coresys.store.repository_urls
|
||||
assert repository.slug in coresys.store.repositories
|
||||
|
||||
response = await api_client.post(
|
||||
@ -54,7 +54,7 @@ async def test_api_supervisor_options_remove_repository(
|
||||
)
|
||||
|
||||
assert response.status == 200
|
||||
assert repository.url not in coresys.config.addons_repositories
|
||||
assert repository.url not in coresys.store.repository_urls
|
||||
assert repository.slug not in coresys.store.repositories
|
||||
|
||||
|
||||
@ -65,14 +65,18 @@ async def test_api_supervisor_options_repositories_skipped_on_error(
|
||||
"""Test repositories skipped on error via POST /supervisor/options REST API."""
|
||||
with patch(
|
||||
"supervisor.store.repository.Repository.load", side_effect=git_error
|
||||
), patch("supervisor.store.repository.Repository.validate", return_value=False):
|
||||
), patch(
|
||||
"supervisor.store.repository.Repository.validate", return_value=False
|
||||
), patch(
|
||||
"supervisor.store.repository.Repository.remove"
|
||||
):
|
||||
response = await api_client.post(
|
||||
"/supervisor/options", json={"addons_repositories": [REPO_URL]}
|
||||
)
|
||||
|
||||
assert response.status == 400
|
||||
assert len(coresys.resolution.suggestions) == 0
|
||||
assert REPO_URL not in coresys.config.addons_repositories
|
||||
assert REPO_URL not in coresys.store.repository_urls
|
||||
with pytest.raises(StoreNotFound):
|
||||
coresys.store.get_from_url(REPO_URL)
|
||||
|
||||
@ -92,7 +96,7 @@ async def test_api_supervisor_options_repo_error_with_config_change(
|
||||
)
|
||||
|
||||
assert response.status == 400
|
||||
assert REPO_URL not in coresys.config.addons_repositories
|
||||
assert REPO_URL not in coresys.store.repository_urls
|
||||
|
||||
assert coresys.config.debug
|
||||
coresys.updater.save_data.assert_called_once()
|
||||
|
@ -16,7 +16,7 @@ from supervisor import config as su_config
|
||||
from supervisor.addons.addon import Addon
|
||||
from supervisor.api import RestAPI
|
||||
from supervisor.bootstrap import initialize_coresys
|
||||
from supervisor.const import REQUEST_FROM
|
||||
from supervisor.const import ATTR_REPOSITORIES, REQUEST_FROM
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.dbus.agent import OSAgent
|
||||
from supervisor.dbus.const import DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED
|
||||
@ -39,7 +39,6 @@ from .const import TEST_ADDON_SLUG
|
||||
|
||||
async def mock_async_return_true() -> bool:
|
||||
"""Mock methods to return True."""
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -74,7 +73,6 @@ def docker() -> DockerAPI:
|
||||
@pytest.fixture
|
||||
def dbus() -> DBus:
|
||||
"""Mock DBUS."""
|
||||
|
||||
dbus_commands = []
|
||||
|
||||
async def mock_get_properties(dbus_obj, interface):
|
||||
@ -208,6 +206,7 @@ async def coresys(loop, docker, network_manager, aiohttp_client, run_dir) -> Cor
|
||||
coresys_obj._jobs.save_data = MagicMock()
|
||||
coresys_obj._resolution.save_data = MagicMock()
|
||||
coresys_obj._addons.data.save_data = MagicMock()
|
||||
coresys_obj._store.save_data = MagicMock()
|
||||
|
||||
# Mock test client
|
||||
coresys_obj.arch._default_arch = "amd64"
|
||||
@ -246,7 +245,9 @@ async def coresys(loop, docker, network_manager, aiohttp_client, run_dir) -> Cor
|
||||
unwrap(coresys_obj.updater.fetch_data), coresys_obj.updater
|
||||
)
|
||||
|
||||
yield coresys_obj
|
||||
# Don't remove files/folders related to addons and stores
|
||||
with patch("supervisor.store.git.GitRepo._remove"):
|
||||
yield coresys_obj
|
||||
|
||||
await coresys_obj.websession.close()
|
||||
|
||||
@ -315,21 +316,29 @@ def store_addon(coresys: CoreSys, tmp_path, repository):
|
||||
@pytest.fixture
|
||||
async def repository(coresys: CoreSys):
|
||||
"""Repository fixture."""
|
||||
coresys.config.drop_addon_repository("https://github.com/hassio-addons/repository")
|
||||
coresys.config.drop_addon_repository(
|
||||
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"
|
||||
)
|
||||
await coresys.store.load()
|
||||
repository_obj = Repository(
|
||||
coresys, "https://github.com/awesome-developer/awesome-repo"
|
||||
)
|
||||
coresys.config.clear_addons_repositories()
|
||||
|
||||
coresys.store.repositories[repository_obj.slug] = repository_obj
|
||||
coresys.config.add_addon_repository(
|
||||
"https://github.com/awesome-developer/awesome-repo"
|
||||
)
|
||||
with patch(
|
||||
"supervisor.store.validate.BUILTIN_REPOSITORIES", {"local", "core"}
|
||||
), patch("supervisor.store.git.GitRepo.load", return_value=None):
|
||||
await coresys.store.load()
|
||||
|
||||
yield repository_obj
|
||||
repository_obj = Repository(
|
||||
coresys, "https://github.com/awesome-developer/awesome-repo"
|
||||
)
|
||||
|
||||
coresys.store.repositories[repository_obj.slug] = repository_obj
|
||||
coresys.store._data[ATTR_REPOSITORIES].append(
|
||||
"https://github.com/awesome-developer/awesome-repo"
|
||||
)
|
||||
|
||||
yield repository_obj
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
3
tests/fixtures/addons/git/5c53de3b/repository.yaml
vendored
Normal file
3
tests/fixtures/addons/git/5c53de3b/repository.yaml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
name: ESPHome add-ons
|
||||
url: "https://home-assistant.io"
|
||||
maintainer: dev@ha.com
|
3
tests/fixtures/addons/git/a0d7b954/repository.yaml
vendored
Normal file
3
tests/fixtures/addons/git/a0d7b954/repository.yaml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
name: Community add-ons
|
||||
url: "https://home-assistant.io"
|
||||
maintainer: dev@ha.com
|
@ -1,36 +1,34 @@
|
||||
"""Test evaluation base."""
|
||||
# pylint: disable=import-error,protected-access
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import patch
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
||||
from supervisor.resolution.data import Issue, Suggestion
|
||||
from supervisor.resolution.fixups.store_execute_remove import FixupStoreExecuteRemove
|
||||
from supervisor.store.repository import Repository
|
||||
|
||||
|
||||
async def test_fixup(coresys: CoreSys):
|
||||
async def test_fixup(coresys: CoreSys, repository: Repository):
|
||||
"""Test fixup."""
|
||||
store_execute_remove = FixupStoreExecuteRemove(coresys)
|
||||
|
||||
assert store_execute_remove.auto is False
|
||||
|
||||
coresys.resolution.suggestions = Suggestion(
|
||||
SuggestionType.EXECUTE_REMOVE, ContextType.STORE, reference="test"
|
||||
SuggestionType.EXECUTE_REMOVE, ContextType.STORE, reference=repository.slug
|
||||
)
|
||||
coresys.resolution.issues = Issue(
|
||||
IssueType.CORRUPT_REPOSITORY, ContextType.STORE, reference="test"
|
||||
IssueType.CORRUPT_REPOSITORY, ContextType.STORE, reference=repository.slug
|
||||
)
|
||||
|
||||
mock_repositorie = AsyncMock()
|
||||
mock_repositorie.slug = "test"
|
||||
with patch.object(type(repository), "remove") as remove_repo:
|
||||
await store_execute_remove()
|
||||
|
||||
coresys.store.repositories["test"] = mock_repositorie
|
||||
assert remove_repo.called
|
||||
|
||||
await store_execute_remove()
|
||||
|
||||
assert mock_repositorie.remove.called
|
||||
assert coresys.config.save_data.called
|
||||
assert coresys.store.save_data.called
|
||||
assert len(coresys.resolution.suggestions) == 0
|
||||
assert len(coresys.resolution.issues) == 0
|
||||
|
||||
assert "test" not in coresys.store.repositories
|
||||
assert repository.slug not in coresys.store.repositories
|
||||
|
@ -23,7 +23,7 @@ async def test_add_valid_repository(
|
||||
coresys: CoreSys, store_manager: StoreManager, use_update: bool
|
||||
):
|
||||
"""Test add custom repository."""
|
||||
current = coresys.config.addons_repositories
|
||||
current = coresys.store.repository_urls
|
||||
with patch("supervisor.store.repository.Repository.load", return_value=None), patch(
|
||||
"supervisor.utils.common.read_yaml_file",
|
||||
return_value={"name": "Awesome repository"},
|
||||
@ -35,7 +35,7 @@ async def test_add_valid_repository(
|
||||
|
||||
assert store_manager.get_from_url("http://example.com").validate()
|
||||
|
||||
assert "http://example.com" in coresys.config.addons_repositories
|
||||
assert "http://example.com" in coresys.store.repository_urls
|
||||
|
||||
|
||||
@pytest.mark.parametrize("use_update", [True, False])
|
||||
@ -43,7 +43,7 @@ async def test_add_invalid_repository(
|
||||
coresys: CoreSys, store_manager: StoreManager, use_update: bool
|
||||
):
|
||||
"""Test add invalid custom repository."""
|
||||
current = coresys.config.addons_repositories
|
||||
current = coresys.store.repository_urls
|
||||
with patch("supervisor.store.repository.Repository.load", return_value=None), patch(
|
||||
"pathlib.Path.read_text",
|
||||
return_value="",
|
||||
@ -59,7 +59,7 @@ async def test_add_invalid_repository(
|
||||
|
||||
assert not store_manager.get_from_url("http://example.com").validate()
|
||||
|
||||
assert "http://example.com" in coresys.config.addons_repositories
|
||||
assert "http://example.com" in coresys.store.repository_urls
|
||||
assert coresys.resolution.suggestions[-1].type == SuggestionType.EXECUTE_REMOVE
|
||||
|
||||
|
||||
@ -68,7 +68,7 @@ async def test_error_on_invalid_repository(
|
||||
coresys: CoreSys, store_manager: StoreManager, use_update
|
||||
):
|
||||
"""Test invalid repository not added."""
|
||||
current = coresys.config.addons_repositories
|
||||
current = coresys.store.repository_urls
|
||||
with patch("supervisor.store.repository.Repository.load", return_value=None), patch(
|
||||
"pathlib.Path.read_text",
|
||||
return_value="",
|
||||
@ -78,7 +78,7 @@ async def test_error_on_invalid_repository(
|
||||
else:
|
||||
await store_manager.add_repository("http://example.com")
|
||||
|
||||
assert "http://example.com" not in coresys.config.addons_repositories
|
||||
assert "http://example.com" not in coresys.store.repository_urls
|
||||
assert len(coresys.resolution.suggestions) == 0
|
||||
with pytest.raises(StoreNotFound):
|
||||
store_manager.get_from_url("http://example.com")
|
||||
@ -89,7 +89,7 @@ async def test_add_invalid_repository_file(
|
||||
coresys: CoreSys, store_manager: StoreManager, use_update: bool
|
||||
):
|
||||
"""Test add invalid custom repository file."""
|
||||
current = coresys.config.addons_repositories
|
||||
current = coresys.store.repository_urls
|
||||
with patch("supervisor.store.repository.Repository.load", return_value=None), patch(
|
||||
"pathlib.Path.read_text",
|
||||
return_value=json.dumps({"name": "Awesome repository"}),
|
||||
@ -105,7 +105,7 @@ async def test_add_invalid_repository_file(
|
||||
|
||||
assert not store_manager.get_from_url("http://example.com").validate()
|
||||
|
||||
assert "http://example.com" in coresys.config.addons_repositories
|
||||
assert "http://example.com" in coresys.store.repository_urls
|
||||
assert coresys.resolution.suggestions[-1].type == SuggestionType.EXECUTE_REMOVE
|
||||
|
||||
|
||||
@ -126,7 +126,7 @@ async def test_add_repository_with_git_error(
|
||||
suggestion_type: SuggestionType,
|
||||
):
|
||||
"""Test repo added with issue on git error."""
|
||||
current = coresys.config.addons_repositories
|
||||
current = coresys.store.repository_urls
|
||||
with patch("supervisor.store.repository.Repository.load", side_effect=git_error):
|
||||
if use_update:
|
||||
await store_manager.update_repositories(
|
||||
@ -137,7 +137,7 @@ async def test_add_repository_with_git_error(
|
||||
"http://example.com", add_with_errors=True
|
||||
)
|
||||
|
||||
assert "http://example.com" in coresys.config.addons_repositories
|
||||
assert "http://example.com" in coresys.store.repository_urls
|
||||
assert coresys.resolution.suggestions[-1].type == suggestion_type
|
||||
assert isinstance(store_manager.get_from_url("http://example.com"), Repository)
|
||||
|
||||
@ -158,7 +158,7 @@ async def test_error_on_repository_with_git_error(
|
||||
git_error: StoreGitError,
|
||||
):
|
||||
"""Test repo not added on git error."""
|
||||
current = coresys.config.addons_repositories
|
||||
current = coresys.store.repository_urls
|
||||
with patch(
|
||||
"supervisor.store.repository.Repository.load", side_effect=git_error
|
||||
), pytest.raises(StoreError):
|
||||
@ -167,7 +167,7 @@ async def test_error_on_repository_with_git_error(
|
||||
else:
|
||||
await store_manager.add_repository("http://example.com")
|
||||
|
||||
assert "http://example.com" not in coresys.config.addons_repositories
|
||||
assert "http://example.com" not in coresys.store.repository_urls
|
||||
assert len(coresys.resolution.suggestions) == 0
|
||||
with pytest.raises(StoreNotFound):
|
||||
store_manager.get_from_url("http://example.com")
|
||||
@ -182,6 +182,8 @@ async def test_preinstall_valid_repository(
|
||||
await store_manager.update_repositories(BUILTIN_REPOSITORIES)
|
||||
assert store_manager.get("core").validate()
|
||||
assert store_manager.get("local").validate()
|
||||
assert store_manager.get("a0d7b954").validate()
|
||||
assert store_manager.get("5c53de3b").validate()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("use_update", [True, False])
|
||||
@ -192,7 +194,7 @@ async def test_remove_repository(
|
||||
use_update: bool,
|
||||
):
|
||||
"""Test removing a custom repository."""
|
||||
assert repository.url in coresys.config.addons_repositories
|
||||
assert repository.url in coresys.store.repository_urls
|
||||
assert repository.slug in coresys.store.repositories
|
||||
|
||||
if use_update:
|
||||
@ -200,7 +202,7 @@ async def test_remove_repository(
|
||||
else:
|
||||
await store_manager.remove_repository(repository)
|
||||
|
||||
assert repository.url not in coresys.config.addons_repositories
|
||||
assert repository.url not in coresys.store.repository_urls
|
||||
assert repository.slug not in coresys.addons.store
|
||||
assert repository.slug not in coresys.store.repositories
|
||||
|
||||
@ -233,15 +235,16 @@ async def test_remove_used_repository(
|
||||
|
||||
async def test_update_partial_error(coresys: CoreSys, store_manager: StoreManager):
|
||||
"""Test partial error on update does partial save and errors."""
|
||||
current = coresys.config.addons_repositories
|
||||
initial = len(current)
|
||||
with patch("supervisor.store.repository.Repository.validate", return_value=True):
|
||||
with patch("supervisor.store.repository.Repository.load", return_value=None):
|
||||
await store_manager.update_repositories(current)
|
||||
await store_manager.update_repositories([])
|
||||
|
||||
store_manager.data.update.assert_called_once()
|
||||
store_manager.data.update.reset_mock()
|
||||
|
||||
current = coresys.store.repository_urls
|
||||
initial = len(current)
|
||||
|
||||
with patch(
|
||||
"supervisor.store.repository.Repository.load",
|
||||
side_effect=[None, StoreGitError()],
|
||||
@ -250,7 +253,7 @@ async def test_update_partial_error(coresys: CoreSys, store_manager: StoreManage
|
||||
current + ["http://example.com", "http://example2.com"]
|
||||
)
|
||||
|
||||
assert len(coresys.config.addons_repositories) == initial + 1
|
||||
assert len(coresys.store.repository_urls) == initial + 1
|
||||
store_manager.data.update.assert_called_once()
|
||||
|
||||
|
||||
@ -258,7 +261,7 @@ async def test_error_adding_duplicate(
|
||||
coresys: CoreSys, store_manager: StoreManager, repository: Repository
|
||||
):
|
||||
"""Test adding a duplicate repository causes an error."""
|
||||
assert repository.url in coresys.config.addons_repositories
|
||||
assert repository.url in coresys.store.repository_urls
|
||||
with patch(
|
||||
"supervisor.store.repository.Repository.validate", return_value=True
|
||||
), patch(
|
||||
@ -267,3 +270,20 @@ async def test_error_adding_duplicate(
|
||||
StoreError
|
||||
):
|
||||
await store_manager.add_repository(repository.url)
|
||||
|
||||
|
||||
async def test_add_with_update_repositories(
|
||||
coresys: CoreSys, store_manager: StoreManager, repository: Repository
|
||||
):
|
||||
"""Test adding repositories to existing ones using update."""
|
||||
assert repository.url in coresys.store.repository_urls
|
||||
assert "http://example.com" not in coresys.store.repository_urls
|
||||
|
||||
with patch("supervisor.store.repository.Repository.load", return_value=None), patch(
|
||||
"supervisor.utils.common.read_yaml_file",
|
||||
return_value={"name": "Awesome repository"},
|
||||
), patch("pathlib.Path.exists", return_value=True):
|
||||
await store_manager.update_repositories(["http://example.com"], replace=False)
|
||||
|
||||
assert repository.url in coresys.store.repository_urls
|
||||
assert "http://example.com" in coresys.store.repository_urls
|
||||
|
98
tests/store/test_store_manager.py
Normal file
98
tests/store/test_store_manager.py
Normal file
@ -0,0 +1,98 @@
|
||||
"""Test store manager."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from supervisor.const import ATTR_ADDONS_CUSTOM_LIST
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.store import StoreManager
|
||||
from supervisor.store.repository import Repository
|
||||
|
||||
|
||||
async def test_default_load(coresys: CoreSys):
|
||||
"""Test default load from config."""
|
||||
store_manager = StoreManager(coresys)
|
||||
|
||||
with patch(
|
||||
"supervisor.store.repository.Repository.load", return_value=None
|
||||
), patch.object(
|
||||
type(coresys.config), "addons_repositories", return_value=[]
|
||||
), patch(
|
||||
"pathlib.Path.exists", return_value=True
|
||||
):
|
||||
await store_manager.load()
|
||||
|
||||
assert len(store_manager.all) == 4
|
||||
assert isinstance(store_manager.get("core"), Repository)
|
||||
assert isinstance(store_manager.get("local"), Repository)
|
||||
|
||||
assert len(store_manager.repository_urls) == 2
|
||||
assert (
|
||||
"https://github.com/hassio-addons/repository" in store_manager.repository_urls
|
||||
)
|
||||
assert (
|
||||
"https://github.com/esphome/home-assistant-addon"
|
||||
in store_manager.repository_urls
|
||||
)
|
||||
|
||||
|
||||
async def test_load_with_custom_repository(coresys: CoreSys):
|
||||
"""Test load from config with custom repository."""
|
||||
with patch(
|
||||
"supervisor.utils.common.read_json_or_yaml_file",
|
||||
return_value={"repositories": ["http://example.com"]},
|
||||
), patch("pathlib.Path.is_file", return_value=True):
|
||||
store_manager = StoreManager(coresys)
|
||||
|
||||
with patch(
|
||||
"supervisor.store.repository.Repository.load", return_value=None
|
||||
), patch.object(
|
||||
type(coresys.config), "addons_repositories", return_value=[]
|
||||
), patch(
|
||||
"supervisor.store.repository.Repository.validate", return_value=True
|
||||
), patch(
|
||||
"pathlib.Path.exists", return_value=True
|
||||
):
|
||||
await store_manager.load()
|
||||
|
||||
assert len(store_manager.all) == 5
|
||||
assert isinstance(store_manager.get("core"), Repository)
|
||||
assert isinstance(store_manager.get("local"), Repository)
|
||||
|
||||
assert len(store_manager.repository_urls) == 3
|
||||
assert (
|
||||
"https://github.com/hassio-addons/repository" in store_manager.repository_urls
|
||||
)
|
||||
assert (
|
||||
"https://github.com/esphome/home-assistant-addon"
|
||||
in store_manager.repository_urls
|
||||
)
|
||||
assert "http://example.com" in store_manager.repository_urls
|
||||
|
||||
|
||||
async def test_load_from_core_config(coresys: CoreSys):
|
||||
"""Test custom repositories loaded from core config when present."""
|
||||
store_manager = StoreManager(coresys)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
coresys.config._data[ATTR_ADDONS_CUSTOM_LIST] = ["http://example.com"]
|
||||
assert coresys.config.addons_repositories == ["http://example.com"]
|
||||
|
||||
with patch("supervisor.store.repository.Repository.load", return_value=None), patch(
|
||||
"supervisor.store.repository.Repository.validate", return_value=True
|
||||
), patch("pathlib.Path.exists", return_value=True):
|
||||
await store_manager.load()
|
||||
|
||||
assert len(store_manager.all) == 5
|
||||
assert isinstance(store_manager.get("core"), Repository)
|
||||
assert isinstance(store_manager.get("local"), Repository)
|
||||
|
||||
assert len(store_manager.repository_urls) == 3
|
||||
assert (
|
||||
"https://github.com/hassio-addons/repository" in store_manager.repository_urls
|
||||
)
|
||||
assert (
|
||||
"https://github.com/esphome/home-assistant-addon"
|
||||
in store_manager.repository_urls
|
||||
)
|
||||
assert "http://example.com" in store_manager.repository_urls
|
||||
|
||||
assert coresys.config.addons_repositories == []
|
58
tests/store/test_validate.py
Normal file
58
tests/store/test_validate.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""Test schema validation."""
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from voluptuous import Invalid
|
||||
|
||||
from supervisor.const import ATTR_REPOSITORIES
|
||||
from supervisor.store.validate import SCHEMA_STORE_FILE, repositories
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
{},
|
||||
{ATTR_REPOSITORIES: []},
|
||||
{ATTR_REPOSITORIES: ["https://github.com/esphome/home-assistant-addon"]},
|
||||
],
|
||||
)
|
||||
async def test_default_config(config: dict[Any]):
|
||||
"""Test built-ins included by default."""
|
||||
conf = SCHEMA_STORE_FILE(config)
|
||||
assert ATTR_REPOSITORIES in conf
|
||||
assert "core" in conf[ATTR_REPOSITORIES]
|
||||
assert "local" in conf[ATTR_REPOSITORIES]
|
||||
assert "https://github.com/hassio-addons/repository" in conf[ATTR_REPOSITORIES]
|
||||
assert 1 == len(
|
||||
[
|
||||
repo
|
||||
for repo in conf[ATTR_REPOSITORIES]
|
||||
if repo == "https://github.com/esphome/home-assistant-addon"
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"repo_list,valid",
|
||||
[
|
||||
([], True),
|
||||
(["core", "local"], True),
|
||||
(["https://github.com/hassio-addons/repository"], True),
|
||||
(["not_a_url"], False),
|
||||
(["https://fail.com/duplicate", "https://fail.com/duplicate"], False),
|
||||
],
|
||||
)
|
||||
async def test_repository_validate(repo_list: list[str], valid: bool):
|
||||
"""Test repository list validate."""
|
||||
if valid:
|
||||
processed = repositories(repo_list)
|
||||
assert len(processed) == 4
|
||||
assert set(repositories(repo_list)) == {
|
||||
"core",
|
||||
"local",
|
||||
"https://github.com/hassio-addons/repository",
|
||||
"https://github.com/esphome/home-assistant-addon",
|
||||
}
|
||||
else:
|
||||
with pytest.raises(Invalid):
|
||||
repositories(repo_list)
|
Loading…
x
Reference in New Issue
Block a user