mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-08-21 21:57:26 +01:00
* Bump aiohttp from 3.13.5 to 3.14.0 --- updated-dependencies: - dependency-name: aiohttp dependency-version: 3.14.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * Adjust API layer for aiohttp 3.14 aiohttp 3.14 raises NotAppKeyWarning when a plain string is used as a request storage key. Convert REQUEST_FROM to a web.RequestKey instance so request[REQUEST_FROM] no longer triggers the warning (which the test suite escalates to an error, failing every authenticated endpoint). It is typed as RequestKey[Any] to preserve the current access semantics: handlers store different origins (App, Home Assistant, host, observer) and narrow the value to the concrete type they expect. aiohttp 3.14 also widened the request.post() return type to include bytearray. Update the _process_dict annotation accordingly to satisfy mypy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Use encode_basic_auth for registry token requests aiohttp 3.14 deprecates the BasicAuth constructor and the client auth= request parameter (both removed in aiohttp 4.0), each emitting a DeprecationWarning at runtime. The registry manifest fetcher hit both when requesting a token with stored credentials. Switch to aiohttp.encode_basic_auth() and pass the result via an Authorization header instead. Add a test covering the credentials path, which the existing tests skipped by mocking _get_auth_token. It asserts the Authorization header is sent and, since the suite escalates warnings to errors, guards against the deprecations returning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stefan Agner <stefan@agner.ch> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
214 lines
6.9 KiB
Python
214 lines
6.9 KiB
Python
"""Tests for registry manifest fetcher."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from supervisor.coresys import CoreSys
|
|
from supervisor.docker.manifest import (
|
|
DOCKER_HUB,
|
|
DOCKER_HUB_API,
|
|
ImageManifest,
|
|
RegistryManifestFetcher,
|
|
parse_image_reference,
|
|
)
|
|
|
|
|
|
def test_parse_image_reference_ghcr_io():
|
|
"""Test parsing ghcr.io image."""
|
|
registry, repo, tag = parse_image_reference(
|
|
"ghcr.io/home-assistant/home-assistant", "2025.1.0"
|
|
)
|
|
assert registry == "ghcr.io"
|
|
assert repo == "home-assistant/home-assistant"
|
|
assert tag == "2025.1.0"
|
|
|
|
|
|
def test_parse_image_reference_docker_hub_with_org():
|
|
"""Test parsing Docker Hub image with organization."""
|
|
registry, repo, tag = parse_image_reference(
|
|
"homeassistant/home-assistant", "latest"
|
|
)
|
|
assert registry == DOCKER_HUB
|
|
assert repo == "homeassistant/home-assistant"
|
|
assert tag == "latest"
|
|
|
|
|
|
def test_parse_image_reference_docker_hub_official_image():
|
|
"""Test parsing Docker Hub official image (no org)."""
|
|
registry, repo, tag = parse_image_reference("alpine", "3.18")
|
|
assert registry == DOCKER_HUB
|
|
assert repo == "library/alpine"
|
|
assert tag == "3.18"
|
|
|
|
|
|
def test_parse_image_reference_gcr_io():
|
|
"""Test parsing gcr.io image."""
|
|
registry, repo, tag = parse_image_reference("gcr.io/project/image", "v1")
|
|
assert registry == "gcr.io"
|
|
assert repo == "project/image"
|
|
assert tag == "v1"
|
|
|
|
|
|
def test_image_manifest_layer_count():
|
|
"""Test ImageManifest layer_count property."""
|
|
manifest = ImageManifest(
|
|
digest="sha256:abc",
|
|
total_size=1000,
|
|
layers={"layer1": 500, "layer2": 500},
|
|
)
|
|
assert manifest.layer_count == 2
|
|
|
|
|
|
async def test_get_manifest_success(coresys: CoreSys, websession: MagicMock):
|
|
"""Test successful manifest fetch by mocking internal methods."""
|
|
fetcher = RegistryManifestFetcher(coresys)
|
|
manifest_data = {
|
|
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
|
"config": {"digest": "sha256:abc123"},
|
|
"layers": [
|
|
{"digest": "sha256:layer1abc123def456789012", "size": 1000},
|
|
{"digest": "sha256:layer2def456abc789012345", "size": 2000},
|
|
],
|
|
}
|
|
|
|
# Mock the internal methods
|
|
with (
|
|
patch.object(
|
|
fetcher, "_get_auth_token", new=AsyncMock(return_value="test-token")
|
|
),
|
|
patch.object(
|
|
fetcher, "_fetch_manifest", new=AsyncMock(return_value=manifest_data)
|
|
),
|
|
):
|
|
result = await fetcher.get_manifest(
|
|
"test.io/org/image", "v1.0", platform="linux/amd64"
|
|
)
|
|
|
|
assert result is not None
|
|
assert result.total_size == 3000
|
|
assert result.layer_count == 2
|
|
# First 12 chars after sha256:
|
|
assert "layer1abc123" in result.layers
|
|
assert result.layers["layer1abc123"] == 1000
|
|
|
|
|
|
async def test_get_manifest_returns_none_on_failure(
|
|
coresys: CoreSys, websession: MagicMock
|
|
):
|
|
"""Test that get_manifest returns None on failure."""
|
|
fetcher = RegistryManifestFetcher(coresys)
|
|
|
|
with (
|
|
patch.object(
|
|
fetcher, "_get_auth_token", new=AsyncMock(return_value="test-token")
|
|
),
|
|
patch.object(fetcher, "_fetch_manifest", new=AsyncMock(return_value=None)),
|
|
):
|
|
result = await fetcher.get_manifest(
|
|
"test.io/org/image", "v1.0", platform="linux/amd64"
|
|
)
|
|
|
|
assert result is None
|
|
|
|
|
|
def test_get_credentials_docker_hub(coresys: CoreSys, websession: MagicMock):
|
|
"""Test getting Docker Hub credentials."""
|
|
coresys.docker.config._data["registries"] = { # pylint: disable=protected-access
|
|
"docker.io": {"username": "user", "password": "pass"}
|
|
}
|
|
fetcher = RegistryManifestFetcher(coresys)
|
|
|
|
creds = fetcher._get_credentials(DOCKER_HUB) # pylint: disable=protected-access
|
|
|
|
assert creds == ("user", "pass")
|
|
|
|
|
|
def test_get_credentials_custom_registry(coresys: CoreSys, websession: MagicMock):
|
|
"""Test getting credentials for custom registry."""
|
|
coresys.docker.config._data["registries"] = { # pylint: disable=protected-access
|
|
"ghcr.io": {"username": "user", "password": "token"}
|
|
}
|
|
fetcher = RegistryManifestFetcher(coresys)
|
|
|
|
creds = fetcher._get_credentials("ghcr.io") # pylint: disable=protected-access
|
|
|
|
assert creds == ("user", "token")
|
|
|
|
|
|
def test_get_credentials_not_found(coresys: CoreSys, websession: MagicMock):
|
|
"""Test no credentials found."""
|
|
coresys.docker.config._data["registries"] = {} # pylint: disable=protected-access
|
|
fetcher = RegistryManifestFetcher(coresys)
|
|
|
|
creds = fetcher._get_credentials("unknown.io") # pylint: disable=protected-access
|
|
|
|
assert creds is None
|
|
|
|
|
|
class _MockTokenResponse:
|
|
"""Mock aiohttp response usable as an async context manager."""
|
|
|
|
def __init__(self, *, status=200, headers=None, payload=None):
|
|
"""Initialize mock response."""
|
|
self.status = status
|
|
self.headers = headers or {}
|
|
self._payload = payload or {}
|
|
|
|
async def json(self):
|
|
"""Return the response body as JSON."""
|
|
return self._payload
|
|
|
|
async def __aenter__(self):
|
|
"""Enter the context manager."""
|
|
return self
|
|
|
|
async def __aexit__(self, exc_type, exc, tb):
|
|
"""Exit the context manager."""
|
|
|
|
|
|
async def test_get_auth_token_uses_basic_auth_header(
|
|
coresys: CoreSys, websession: MagicMock
|
|
):
|
|
"""Test stored credentials are sent as an Authorization header."""
|
|
coresys.docker.config._data["registries"] = { # pylint: disable=protected-access
|
|
"ghcr.io": {"username": "user", "password": "token"}
|
|
}
|
|
fetcher = RegistryManifestFetcher(coresys)
|
|
|
|
challenge = _MockTokenResponse(
|
|
status=401,
|
|
headers={
|
|
"WWW-Authenticate": (
|
|
'Bearer realm="https://ghcr.io/token",service="ghcr.io"'
|
|
)
|
|
},
|
|
)
|
|
token_response = _MockTokenResponse(payload={"token": "secret-token"})
|
|
websession.get = MagicMock(side_effect=[challenge, token_response])
|
|
|
|
token = await fetcher._get_auth_token( # pylint: disable=protected-access
|
|
"ghcr.io", "org/image"
|
|
)
|
|
|
|
assert token == "secret-token"
|
|
# Second call fetches the token with the stored credentials as a header.
|
|
_, token_kwargs = websession.get.call_args_list[1]
|
|
assert token_kwargs["headers"] == {"Authorization": "Basic dXNlcjp0b2tlbg=="}
|
|
|
|
|
|
def test_get_api_endpoint_docker_hub(coresys: CoreSys, websession: MagicMock):
|
|
"""Test Docker Hub registry translates to API endpoint."""
|
|
fetcher = RegistryManifestFetcher(coresys)
|
|
|
|
endpoint = fetcher._get_api_endpoint(DOCKER_HUB) # pylint: disable=protected-access
|
|
|
|
assert endpoint == DOCKER_HUB_API
|
|
|
|
|
|
def test_get_api_endpoint_other_registry(coresys: CoreSys, websession: MagicMock):
|
|
"""Test other registries pass through unchanged."""
|
|
fetcher = RegistryManifestFetcher(coresys)
|
|
|
|
endpoint = fetcher._get_api_endpoint("ghcr.io") # pylint: disable=protected-access
|
|
|
|
assert endpoint == "ghcr.io"
|