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:
Mike Degatano 2022-06-07 04:02:21 -04:00 committed by GitHub
parent deeaf2133b
commit ccd2c31390
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 369 additions and 97 deletions

View File

@ -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(
{

View File

@ -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

View File

@ -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."""

View File

@ -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,

View File

@ -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] = []

View File

@ -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,
},

View File

@ -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:

View File

@ -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

View File

@ -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):

View File

@ -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__)

View File

@ -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

View File

@ -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,
)

View File

@ -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(),

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -0,0 +1,3 @@
name: ESPHome add-ons
url: "https://home-assistant.io"
maintainer: dev@ha.com

View File

@ -0,0 +1,3 @@
name: Community add-ons
url: "https://home-assistant.io"
maintainer: dev@ha.com

View File

@ -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

View File

@ -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

View 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 == []

View 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)