mirror of
https://github.com/home-assistant/core.git
synced 2026-09-17 05:58:41 +01:00
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
443 lines
14 KiB
Python
443 lines
14 KiB
Python
"""The tests for the Collection Image image platform."""
|
|
|
|
from http import HTTPStatus
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from freezegun import freeze_time
|
|
import pytest
|
|
|
|
from homeassistant.components.collection_image import DOMAIN
|
|
from homeassistant.components.image import Image, async_get_image
|
|
from homeassistant.components.media_source import PlayMedia, Unresolvable
|
|
from homeassistant.const import (
|
|
ATTR_ENTITY_ID,
|
|
EVENT_HOMEASSISTANT_STARTED,
|
|
STATE_UNAVAILABLE,
|
|
STATE_UNKNOWN,
|
|
)
|
|
from homeassistant.core import CoreState, HomeAssistant
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
|
|
from .conftest import MediaSourceMocks, MediaSourceState
|
|
from .const import (
|
|
DEFAULT_ENTITY_ID,
|
|
MOCK_MEDIA_DIR_URI_1,
|
|
MOCK_MEDIA_DIR_URI_2,
|
|
MOCK_MEDIA_DIR_URI_BROWSE_ERROR,
|
|
MOCK_MEDIA_DIR_URI_EMPTY,
|
|
MOCK_MEDIA_IMAGE_URI_1,
|
|
MOCK_MEDIA_IMAGE_URI_2,
|
|
MOCK_MEDIA_IMAGE_URI_3,
|
|
MOCK_MEDIA_IMAGE_URI_4,
|
|
TEST_IMAGE,
|
|
TEST_IMAGE_2,
|
|
)
|
|
from .helpers import config_entry_from_uri
|
|
|
|
from tests.common import MockConfigEntry
|
|
from tests.typing import ClientSessionGenerator
|
|
|
|
TEST_TIME = "2025-11-08T12:00:00+00:00"
|
|
TEST_TIME_NEXT = "2025-11-08T13:00:00+00:00"
|
|
|
|
|
|
async def _verify_path_image(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
):
|
|
client = await hass_client()
|
|
|
|
resp = await client.get(f"/api/image_proxy/{DEFAULT_ENTITY_ID}")
|
|
assert resp.status == HTTPStatus.OK
|
|
assert resp.content_type == "image/png"
|
|
expected_data = await hass.async_add_executor_job(TEST_IMAGE.read_bytes)
|
|
body = await resp.read()
|
|
assert body == expected_data
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_media_source")
|
|
async def test_image(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test loading an image."""
|
|
with (
|
|
freeze_time(TEST_TIME),
|
|
):
|
|
config_entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(DEFAULT_ENTITY_ID)
|
|
|
|
assert state and state.state == TEST_TIME
|
|
|
|
await _verify_path_image(hass, hass_client)
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_media_source")
|
|
@pytest.mark.parametrize(
|
|
("uris", "expected_images"),
|
|
[
|
|
(
|
|
[MOCK_MEDIA_DIR_URI_1, MOCK_MEDIA_DIR_URI_2],
|
|
[
|
|
MOCK_MEDIA_IMAGE_URI_1,
|
|
MOCK_MEDIA_IMAGE_URI_2,
|
|
MOCK_MEDIA_IMAGE_URI_3,
|
|
MOCK_MEDIA_IMAGE_URI_4,
|
|
],
|
|
),
|
|
(
|
|
[MOCK_MEDIA_DIR_URI_2, MOCK_MEDIA_DIR_URI_1],
|
|
[
|
|
MOCK_MEDIA_IMAGE_URI_2,
|
|
MOCK_MEDIA_IMAGE_URI_3,
|
|
MOCK_MEDIA_IMAGE_URI_4,
|
|
MOCK_MEDIA_IMAGE_URI_1,
|
|
],
|
|
),
|
|
(
|
|
[MOCK_MEDIA_DIR_URI_1, MOCK_MEDIA_DIR_URI_BROWSE_ERROR],
|
|
[MOCK_MEDIA_IMAGE_URI_1],
|
|
),
|
|
(
|
|
[
|
|
MOCK_MEDIA_DIR_URI_BROWSE_ERROR,
|
|
MOCK_MEDIA_DIR_URI_1,
|
|
MOCK_MEDIA_DIR_URI_EMPTY,
|
|
],
|
|
[MOCK_MEDIA_IMAGE_URI_1],
|
|
),
|
|
(
|
|
[
|
|
MOCK_MEDIA_DIR_URI_EMPTY,
|
|
MOCK_MEDIA_DIR_URI_1,
|
|
],
|
|
[MOCK_MEDIA_IMAGE_URI_1],
|
|
),
|
|
],
|
|
)
|
|
async def test_image_multi(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
config_entry: MockConfigEntry,
|
|
media_source_state: MediaSourceState,
|
|
uris: list[str],
|
|
expected_images: list[str],
|
|
) -> None:
|
|
"""Test multiple media sources."""
|
|
with (
|
|
freeze_time(TEST_TIME),
|
|
patch(
|
|
"homeassistant.components.collection_image.image.random.choice",
|
|
return_value=media_source_state.browse_results[
|
|
MOCK_MEDIA_DIR_URI_2
|
|
].children[2],
|
|
) as mock_choice,
|
|
):
|
|
config_entry = config_entry_from_uri(uris)
|
|
config_entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert [
|
|
image.media_content_id for image in mock_choice.call_args.args[0]
|
|
] == expected_images
|
|
state = hass.states.get(DEFAULT_ENTITY_ID)
|
|
assert state and state.state == TEST_TIME
|
|
await _verify_path_image(hass, hass_client)
|
|
|
|
|
|
async def test_image_during_startup(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
config_entry: MockConfigEntry,
|
|
mock_media_source: MediaSourceMocks,
|
|
) -> None:
|
|
"""Test loading an image, ensuring that we don't browse until after startup is complete."""
|
|
with freeze_time(TEST_TIME):
|
|
hass.set_state(CoreState.starting)
|
|
|
|
config_entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
mock_media_source.image_browse.assert_not_called()
|
|
mock_media_source.resolve.assert_not_called()
|
|
|
|
hass.set_state(CoreState.running)
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
await hass.async_block_till_done()
|
|
|
|
mock_media_source.image_browse.assert_awaited_once()
|
|
mock_media_source.resolve.assert_awaited_once()
|
|
|
|
state = hass.states.get(DEFAULT_ENTITY_ID)
|
|
|
|
assert state and state.state == TEST_TIME
|
|
|
|
await _verify_path_image(hass, hass_client)
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_media_source")
|
|
async def test_image_url(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
config_entry: MockConfigEntry,
|
|
media_source_state: MediaSourceState,
|
|
) -> None:
|
|
"""Test loading an image, when media resolves to a URL."""
|
|
media_source_state.resolve_results[MOCK_MEDIA_IMAGE_URI_1] = PlayMedia(
|
|
url="http://example.com/test.png",
|
|
mime_type="image/png",
|
|
)
|
|
|
|
expected_data = await hass.async_add_executor_job(TEST_IMAGE.read_bytes)
|
|
with (
|
|
freeze_time(TEST_TIME),
|
|
):
|
|
config_entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(DEFAULT_ENTITY_ID)
|
|
|
|
assert state and state.state == TEST_TIME
|
|
|
|
client = await hass_client()
|
|
|
|
with patch(
|
|
"homeassistant.components.collection_image.image.CollectionImageImageEntity._async_load_image_from_url",
|
|
new_callable=AsyncMock,
|
|
) as mock_load:
|
|
mock_load.return_value = Image(
|
|
content_type="image/png",
|
|
content=expected_data,
|
|
)
|
|
resp = await client.get(f"/api/image_proxy/{DEFAULT_ENTITY_ID}")
|
|
mock_load.assert_awaited_once_with("http://example.com/test.png")
|
|
|
|
assert resp.status == HTTPStatus.OK
|
|
assert resp.content_type == "image/png"
|
|
body = await resp.read()
|
|
assert body == expected_data
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_media_source")
|
|
@pytest.mark.parametrize(
|
|
("uris", "error_messages"),
|
|
[
|
|
(
|
|
MOCK_MEDIA_DIR_URI_EMPTY,
|
|
[f"image.random_image: No valid images in {MOCK_MEDIA_DIR_URI_EMPTY}"],
|
|
),
|
|
(
|
|
[MOCK_MEDIA_DIR_URI_BROWSE_ERROR],
|
|
["image.random_image: Mock directory failed to browse"],
|
|
),
|
|
(
|
|
[MOCK_MEDIA_DIR_URI_EMPTY, MOCK_MEDIA_DIR_URI_BROWSE_ERROR],
|
|
[
|
|
f"image.random_image: No valid images in {MOCK_MEDIA_DIR_URI_EMPTY}",
|
|
"image.random_image: Mock directory failed to browse",
|
|
],
|
|
),
|
|
],
|
|
)
|
|
async def test_media_error(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
caplog: pytest.LogCaptureFixture,
|
|
uris: str | list[str],
|
|
error_messages: list[str],
|
|
) -> None:
|
|
"""Test various cases where media fails to browse images."""
|
|
|
|
config_entry = config_entry_from_uri(uris)
|
|
config_entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(DEFAULT_ENTITY_ID)
|
|
|
|
assert state and state.state == STATE_UNAVAILABLE
|
|
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
for err in error_messages:
|
|
assert err in caplog.text
|
|
|
|
client = await hass_client()
|
|
resp = await client.get(f"/api/image_proxy/{DEFAULT_ENTITY_ID}")
|
|
assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR
|
|
|
|
|
|
async def test_unresolvable(
|
|
hass: HomeAssistant,
|
|
config_entry: MockConfigEntry,
|
|
media_source_state: MediaSourceState,
|
|
mock_media_source: MediaSourceMocks,
|
|
caplog: pytest.LogCaptureFixture,
|
|
hass_client: ClientSessionGenerator,
|
|
) -> None:
|
|
"""Test when resolving an image fails."""
|
|
media_source_state.resolve_exceptions[MOCK_MEDIA_IMAGE_URI_1] = Unresolvable(
|
|
"Mock image failed to resolve"
|
|
)
|
|
|
|
config_entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert mock_media_source.image_browse.call_count == 1
|
|
assert mock_media_source.resolve.call_count == 1
|
|
|
|
state = hass.states.get(DEFAULT_ENTITY_ID)
|
|
|
|
assert state and state.state == STATE_UNKNOWN
|
|
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
assert "image.random_image: Mock image failed to resolve" in caplog.text
|
|
|
|
# Test we can recover by calling shuffle again when the image is resolvable
|
|
del media_source_state.resolve_exceptions[MOCK_MEDIA_IMAGE_URI_1]
|
|
|
|
with (
|
|
freeze_time(TEST_TIME),
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
"shuffle",
|
|
{ATTR_ENTITY_ID: DEFAULT_ENTITY_ID},
|
|
blocking=True,
|
|
)
|
|
|
|
assert mock_media_source.image_browse.call_count == 2
|
|
assert mock_media_source.resolve.call_count == 2
|
|
|
|
state = hass.states.get(DEFAULT_ENTITY_ID)
|
|
|
|
assert state and state.state == TEST_TIME
|
|
|
|
await _verify_path_image(hass, hass_client)
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_media_source")
|
|
async def test_image_file_read_error(
|
|
hass: HomeAssistant,
|
|
config_entry: MockConfigEntry,
|
|
media_source_state: MediaSourceState,
|
|
hass_client: ClientSessionGenerator,
|
|
) -> None:
|
|
"""Test that a file read error is surfaced when serving the image."""
|
|
missing_path = Path(__file__).parent / "does_not_exist.png"
|
|
media_source_state.resolve_results[MOCK_MEDIA_IMAGE_URI_1] = PlayMedia(
|
|
url="",
|
|
mime_type="image/png",
|
|
path=missing_path,
|
|
)
|
|
|
|
with (
|
|
freeze_time(TEST_TIME),
|
|
):
|
|
config_entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
# Browse and resolve succeeded, so the entity is available with an image.
|
|
state = hass.states.get(DEFAULT_ENTITY_ID)
|
|
assert state and state.state == TEST_TIME
|
|
|
|
with pytest.raises(HomeAssistantError) as exc_info:
|
|
await async_get_image(hass, DEFAULT_ENTITY_ID)
|
|
assert exc_info.value.translation_key == "image_read_error"
|
|
assert exc_info.value.translation_placeholders["path"] == str(missing_path)
|
|
|
|
client = await hass_client()
|
|
resp = await client.get(f"/api/image_proxy/{DEFAULT_ENTITY_ID}")
|
|
assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_media_source")
|
|
async def test_multi_shuffle(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
media_source_state: MediaSourceState,
|
|
) -> None:
|
|
"""Check that shuffling random image does not return the same image."""
|
|
media_source_state.resolve_results[MOCK_MEDIA_IMAGE_URI_4] = PlayMedia(
|
|
url="",
|
|
mime_type="image/png",
|
|
path=TEST_IMAGE_2,
|
|
)
|
|
|
|
config_entry = config_entry_from_uri(MOCK_MEDIA_DIR_URI_2)
|
|
with (
|
|
freeze_time(TEST_TIME),
|
|
patch(
|
|
"homeassistant.components.collection_image.image.random.choice",
|
|
return_value=media_source_state.browse_results[
|
|
MOCK_MEDIA_DIR_URI_2
|
|
].children[1],
|
|
) as mock_choice,
|
|
):
|
|
config_entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert [image.media_content_id for image in mock_choice.call_args.args[0]] == [
|
|
MOCK_MEDIA_IMAGE_URI_2,
|
|
MOCK_MEDIA_IMAGE_URI_3,
|
|
MOCK_MEDIA_IMAGE_URI_4,
|
|
]
|
|
|
|
state = hass.states.get(DEFAULT_ENTITY_ID)
|
|
|
|
assert state and state.state == TEST_TIME
|
|
|
|
client = await hass_client()
|
|
|
|
resp = await client.get(f"/api/image_proxy/{DEFAULT_ENTITY_ID}")
|
|
assert resp.status == HTTPStatus.OK
|
|
assert resp.content_type == "image/png"
|
|
expected_data = await hass.async_add_executor_job(TEST_IMAGE.read_bytes)
|
|
body = await resp.read()
|
|
assert body == expected_data
|
|
|
|
with (
|
|
freeze_time(TEST_TIME_NEXT),
|
|
patch(
|
|
"homeassistant.components.collection_image.image.random.choice",
|
|
return_value=media_source_state.browse_results[
|
|
MOCK_MEDIA_DIR_URI_2
|
|
].children[2],
|
|
) as mock_choice,
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
"shuffle",
|
|
{ATTR_ENTITY_ID: DEFAULT_ENTITY_ID},
|
|
blocking=True,
|
|
)
|
|
|
|
# On the second call, URI_3 will not be included as it is the current image.
|
|
assert [image.media_content_id for image in mock_choice.call_args.args[0]] == [
|
|
MOCK_MEDIA_IMAGE_URI_2,
|
|
MOCK_MEDIA_IMAGE_URI_4,
|
|
]
|
|
|
|
state = hass.states.get(DEFAULT_ENTITY_ID)
|
|
|
|
assert state and state.state == TEST_TIME_NEXT
|
|
|
|
resp = await client.get(f"/api/image_proxy/{DEFAULT_ENTITY_ID}")
|
|
assert resp.status == HTTPStatus.OK
|
|
assert resp.content_type == "image/png"
|
|
expected_data = await hass.async_add_executor_job(TEST_IMAGE_2.read_bytes)
|
|
body = await resp.read()
|
|
assert body == expected_data
|