Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions custom_components/hacs/websocket/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Helpers for the HACS websocket API."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from homeassistant.components import websocket_api

from ..base import HacsBase
from ..repositories.base import HacsRepository


def resolve_repository(
hacs: HacsBase,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
repository_id: str,
) -> HacsRepository | None:
"""Resolve a repository by id, send an error when it is unknown.

A stale frontend can reference repositories that no longer exist,
the caller should return early when this returns None.
"""
if (repository := hacs.repositories.get_by_id(repository_id)) is None:
connection.send_error(
msg["id"],
"repository_not_found",
f"Repository with ID ({repository_id}) not found",
)

return repository
7 changes: 5 additions & 2 deletions custom_components/hacs/websocket/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from ..const import DOMAIN
from ..enums import HacsDispatchEvent
from .helpers import resolve_repository

if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
Expand Down Expand Up @@ -94,7 +95,8 @@ async def hacs_repositories_clear_new(
hacs: HacsBase = hass.data.get(DOMAIN)

if repo := msg.get("repository"):
repository = hacs.repositories.get_by_id(repo)
if (repository := resolve_repository(hacs, connection, msg, repo)) is None:
return
repository.data.new = False

else:
Expand Down Expand Up @@ -208,7 +210,8 @@ async def hacs_repositories_remove(
) -> None:
"""Remove custom repositoriy."""
hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository"])
if (repository := resolve_repository(hacs, connection, msg, msg["repository"])) is None:
return

repository.remove()
await hacs.data.async_write()
Expand Down
45 changes: 20 additions & 25 deletions custom_components/hacs/websocket/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from ..enums import HacsDispatchEvent
from ..exceptions import HacsException
from ..utils.version import version_left_higher_then_right
from .helpers import resolve_repository

if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
Expand All @@ -34,14 +35,7 @@ async def hacs_repository_info(
) -> None:
"""Return information about a repository."""
hacs: HacsBase = hass.data.get(DOMAIN)
repository_id = msg["repository_id"]
repository = hacs.repositories.get_by_id(repository_id)
if repository is None:
connection.send_error(
msg["id"],
"repository_not_found",
f"Repository with ID ({repository_id}) not found",
)
if (repository := resolve_repository(hacs, connection, msg, msg["repository_id"])) is None:
return

if not repository.updated_info:
Expand Down Expand Up @@ -113,15 +107,8 @@ async def hacs_repository_ignore(
) -> None:
"""Ignore a repository."""
hacs: HacsBase = hass.data.get(DOMAIN)
repository_id = msg["repository"]
hacs.log.info("Ignoring %s", repository_id)
repository = hacs.repositories.get_by_id(repository_id)
if repository is None:
connection.send_error(
msg["id"],
"repository_not_found",
f"Repository with ID ({repository_id}) not found",
)
hacs.log.info("Ignoring %s", msg["repository"])
if (repository := resolve_repository(hacs, connection, msg, msg["repository"])) is None:
return

hacs.common.ignored_repositories.add(repository.data.full_name)
Expand All @@ -146,7 +133,8 @@ async def hacs_repository_state(
) -> None:
"""Set the state of a repository"""
hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository"])
if (repository := resolve_repository(hacs, connection, msg, msg["repository"])) is None:
return

repository.state = msg["state"]

Expand All @@ -170,7 +158,8 @@ async def hacs_repository_version(
) -> None:
"""Set the version of a repository"""
hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository"])
if (repository := resolve_repository(hacs, connection, msg, msg["repository"])) is None:
return

if msg["version"] == repository.data.default_branch:
repository.data.selected_tag = None
Expand Down Expand Up @@ -200,7 +189,8 @@ async def hacs_repository_beta(
) -> None:
"""Show or hide beta versions of a repository"""
hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository"])
if (repository := resolve_repository(hacs, connection, msg, msg["repository"])) is None:
return

repository.data.show_beta = msg["show_beta"]

Expand All @@ -227,7 +217,8 @@ async def hacs_repository_download(
) -> None:
"""Set the version of a repository"""
hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository"])
if (repository := resolve_repository(hacs, connection, msg, msg["repository"])) is None:
return

try:
was_installed = repository.data.installed
Expand Down Expand Up @@ -258,7 +249,8 @@ async def hacs_repository_remove(
) -> None:
"""Remove a repository."""
hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository"])
if (repository := resolve_repository(hacs, connection, msg, msg["repository"])) is None:
return

repository.data.new = False
try:
Expand Down Expand Up @@ -286,7 +278,8 @@ async def hacs_repository_refresh(
) -> None:
"""Refresh a repository."""
hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository"])
if (repository := resolve_repository(hacs, connection, msg, msg["repository"])) is None:
return

await repository.update_repository(ignore_issues=True, force=True)
await hacs.data.async_write()
Expand All @@ -311,7 +304,8 @@ async def hacs_repository_release_notes(
) -> None:
"""Return release notes."""
hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository"])
if (repository := resolve_repository(hacs, connection, msg, msg["repository"])) is None:
return

connection.send_message(
websocket_api.result_message(
Expand Down Expand Up @@ -345,7 +339,8 @@ async def hacs_repository_releases(
) -> None:
"""Return releases."""
hacs: HacsBase = hass.data.get(DOMAIN)
repository = hacs.repositories.get_by_id(msg["repository_id"])
if (repository := resolve_repository(hacs, connection, msg, msg["repository_id"])) is None:
return
try:
releases = await repository.async_get_releases()
except Exception as exception:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"tests/test_websocket.py::test_unknown_repository_returns_not_found[repositories-clear_new-]": {
"https://api.github.com/repos/hacs/integration": 1,
"https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1,
"https://api.github.com/repos/hacs/integration/contents/hacs.json": 1,
"https://api.github.com/repos/hacs/integration/git/trees/main": 1,
"https://api.github.com/repos/hacs/integration/releases": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"tests/test_websocket.py::test_unknown_repository_returns_not_found[repositories-remove-]": {
"https://api.github.com/repos/hacs/integration": 1,
"https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1,
"https://api.github.com/repos/hacs/integration/contents/hacs.json": 1,
"https://api.github.com/repos/hacs/integration/git/trees/main": 1,
"https://api.github.com/repos/hacs/integration/releases": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"tests/test_websocket.py::test_unknown_repository_returns_not_found[repository-beta-]": {
"https://api.github.com/repos/hacs/integration": 1,
"https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1,
"https://api.github.com/repos/hacs/integration/contents/hacs.json": 1,
"https://api.github.com/repos/hacs/integration/git/trees/main": 1,
"https://api.github.com/repos/hacs/integration/releases": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"tests/test_websocket.py::test_unknown_repository_returns_not_found[repository-download-]": {
"https://api.github.com/repos/hacs/integration": 1,
"https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1,
"https://api.github.com/repos/hacs/integration/contents/hacs.json": 1,
"https://api.github.com/repos/hacs/integration/git/trees/main": 1,
"https://api.github.com/repos/hacs/integration/releases": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"tests/test_websocket.py::test_unknown_repository_returns_not_found[repository-ignore-]": {
"https://api.github.com/repos/hacs/integration": 1,
"https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1,
"https://api.github.com/repos/hacs/integration/contents/hacs.json": 1,
"https://api.github.com/repos/hacs/integration/git/trees/main": 1,
"https://api.github.com/repos/hacs/integration/releases": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"tests/test_websocket.py::test_unknown_repository_returns_not_found[repository-info-]": {
"https://api.github.com/repos/hacs/integration": 1,
"https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1,
"https://api.github.com/repos/hacs/integration/contents/hacs.json": 1,
"https://api.github.com/repos/hacs/integration/git/trees/main": 1,
"https://api.github.com/repos/hacs/integration/releases": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"tests/test_websocket.py::test_unknown_repository_returns_not_found[repository-refresh-]": {
"https://api.github.com/repos/hacs/integration": 1,
"https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1,
"https://api.github.com/repos/hacs/integration/contents/hacs.json": 1,
"https://api.github.com/repos/hacs/integration/git/trees/main": 1,
"https://api.github.com/repos/hacs/integration/releases": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"tests/test_websocket.py::test_unknown_repository_returns_not_found[repository-release_notes-]": {
"https://api.github.com/repos/hacs/integration": 1,
"https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1,
"https://api.github.com/repos/hacs/integration/contents/hacs.json": 1,
"https://api.github.com/repos/hacs/integration/git/trees/main": 1,
"https://api.github.com/repos/hacs/integration/releases": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"tests/test_websocket.py::test_unknown_repository_returns_not_found[repository-releases-]": {
"https://api.github.com/repos/hacs/integration": 1,
"https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1,
"https://api.github.com/repos/hacs/integration/contents/hacs.json": 1,
"https://api.github.com/repos/hacs/integration/git/trees/main": 1,
"https://api.github.com/repos/hacs/integration/releases": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"tests/test_websocket.py::test_unknown_repository_returns_not_found[repository-remove-]": {
"https://api.github.com/repos/hacs/integration": 1,
"https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1,
"https://api.github.com/repos/hacs/integration/contents/hacs.json": 1,
"https://api.github.com/repos/hacs/integration/git/trees/main": 1,
"https://api.github.com/repos/hacs/integration/releases": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"tests/test_websocket.py::test_unknown_repository_returns_not_found[repository-state-]": {
"https://api.github.com/repos/hacs/integration": 1,
"https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1,
"https://api.github.com/repos/hacs/integration/contents/hacs.json": 1,
"https://api.github.com/repos/hacs/integration/git/trees/main": 1,
"https://api.github.com/repos/hacs/integration/releases": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"tests/test_websocket.py::test_unknown_repository_returns_not_found[repository-version-]": {
"https://api.github.com/repos/hacs/integration": 1,
"https://api.github.com/repos/hacs/integration/contents/custom_components/hacs/manifest.json": 1,
"https://api.github.com/repos/hacs/integration/contents/hacs.json": 1,
"https://api.github.com/repos/hacs/integration/git/trees/main": 1,
"https://api.github.com/repos/hacs/integration/releases": 1
}
}
48 changes: 48 additions & 0 deletions tests/test_websocket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Tests for the HACS websocket API."""

from collections.abc import Generator

from homeassistant.core import HomeAssistant
import pytest

from tests.common import WSClient

UNKNOWN_REPOSITORY_ID = "1337404"


@pytest.mark.parametrize(
("command", "payload"),
[
("hacs/repository/info", {"repository_id": UNKNOWN_REPOSITORY_ID}),
("hacs/repository/ignore", {"repository": UNKNOWN_REPOSITORY_ID}),
("hacs/repository/state", {"repository": UNKNOWN_REPOSITORY_ID, "state": "new"}),
("hacs/repository/version", {"repository": UNKNOWN_REPOSITORY_ID, "version": "1.0.0"}),
("hacs/repository/beta", {"repository": UNKNOWN_REPOSITORY_ID, "show_beta": True}),
("hacs/repository/download", {"repository": UNKNOWN_REPOSITORY_ID}),
("hacs/repository/remove", {"repository": UNKNOWN_REPOSITORY_ID}),
("hacs/repository/refresh", {"repository": UNKNOWN_REPOSITORY_ID}),
("hacs/repository/release_notes", {"repository": UNKNOWN_REPOSITORY_ID}),
("hacs/repository/releases", {"repository_id": UNKNOWN_REPOSITORY_ID}),
("hacs/repositories/clear_new", {"repository": UNKNOWN_REPOSITORY_ID}),
("hacs/repositories/remove", {"repository": UNKNOWN_REPOSITORY_ID}),
],
ids=lambda value: value.replace("hacs/", "").replace("/", "-")
if isinstance(value, str)
else "",
)
async def test_unknown_repository_returns_not_found(
hass: HomeAssistant,
setup_integration: Generator,
ws_client: WSClient,
command: str,
payload: dict,
):
"""Ensure all repository commands handle unknown repository ids."""
response = await ws_client.send_and_receive_json(command, payload)

assert response["success"] is False
assert response["error"]["code"] == "repository_not_found"
assert (
response["error"]["message"]
== f"Repository with ID ({UNKNOWN_REPOSITORY_ID}) not found"
)
Loading