core/tests/components/image/test_init.py

496 lines
16 KiB
Python

"""The tests for the image component."""
from datetime import datetime
from http import HTTPStatus
import ssl
from unittest.mock import MagicMock, mock_open, patch
from aiohttp import hdrs
from freezegun.api import FrozenDateTimeFactory
import httpx
import pytest
import respx
from homeassistant.components import image
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
from .conftest import (
MockImageEntity,
MockImageEntityCapitalContentType,
MockImageEntityInvalidContentType,
MockImageNoDataEntity,
MockImageNoStateEntity,
MockImagePlatform,
MockImageSyncEntity,
MockURLImageEntity,
)
from tests.common import (
MockModule,
async_fire_time_changed,
mock_integration,
mock_platform,
)
from tests.typing import ClientSessionGenerator
@pytest.mark.freeze_time("2023-04-01 00:00:00+00:00")
async def test_state(
hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_image_platform: None
) -> None:
"""Test image state."""
state = hass.states.get("image.test")
assert state.state == "2023-04-01T00:00:00+00:00"
access_token = state.attributes["access_token"]
assert state.attributes == {
"access_token": access_token,
"entity_picture": f"/api/image_proxy/image.test?token={access_token}",
"friendly_name": "Test",
}
@pytest.mark.freeze_time("2023-04-01 00:00:00+00:00")
async def test_config_entry(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_image_config_entry: ConfigEntry,
) -> None:
"""Test setting up an image platform from a config entry."""
state = hass.states.get("image.test")
assert state.state == "2023-04-01T00:00:00+00:00"
access_token = state.attributes["access_token"]
assert state.attributes == {
"access_token": access_token,
"entity_picture": f"/api/image_proxy/image.test?token={access_token}",
"friendly_name": "Test",
}
@pytest.mark.freeze_time("2023-04-01 00:00:00+00:00")
async def test_state_attr(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
"""Test image state with entity picture from attr."""
mock_integration(hass, MockModule(domain="test"))
entity = MockImageEntity(hass)
entity._attr_entity_picture = "abcd"
mock_platform(hass, "test.image", MockImagePlatform([entity]))
assert await async_setup_component(
hass, image.DOMAIN, {"image": {"platform": "test"}}
)
await hass.async_block_till_done()
state = hass.states.get("image.test")
assert state.state == "2023-04-01T00:00:00+00:00"
access_token = state.attributes["access_token"]
assert state.attributes == {
"access_token": access_token,
"entity_picture": "abcd",
"friendly_name": "Test",
}
async def test_no_state(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
"""Test image state."""
mock_integration(hass, MockModule(domain="test"))
mock_platform(hass, "test.image", MockImagePlatform([MockImageNoStateEntity(hass)]))
assert await async_setup_component(
hass, image.DOMAIN, {"image": {"platform": "test"}}
)
await hass.async_block_till_done()
state = hass.states.get("image.test")
assert state.state == "unknown"
access_token = state.attributes["access_token"]
assert state.attributes == {
"access_token": access_token,
"entity_picture": f"/api/image_proxy/image.test?token={access_token}",
"friendly_name": "Test",
}
async def test_no_valid_content_type(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
"""Test invalid content type."""
mock_integration(hass, MockModule(domain="test"))
mock_platform(
hass, "test.image", MockImagePlatform([MockImageEntityInvalidContentType(hass)])
)
assert await async_setup_component(
hass, image.DOMAIN, {"image": {"platform": "test"}}
)
await hass.async_block_till_done()
client = await hass_client()
state = hass.states.get("image.test")
# assert state.state == "unknown"
access_token = state.attributes["access_token"]
assert state.attributes == {
"access_token": access_token,
"entity_picture": f"/api/image_proxy/image.test?token={access_token}",
"friendly_name": "Test",
}
resp = await client.get(f"/api/image_proxy/image.test?token={access_token}")
assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR
async def test_valid_but_capitalized_content_type(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
"""Test invalid content type."""
mock_integration(hass, MockModule(domain="test"))
mock_platform(
hass, "test.image", MockImagePlatform([MockImageEntityCapitalContentType(hass)])
)
assert await async_setup_component(
hass, image.DOMAIN, {"image": {"platform": "test"}}
)
await hass.async_block_till_done()
client = await hass_client()
state = hass.states.get("image.test")
access_token = state.attributes["access_token"]
assert state.attributes == {
"access_token": access_token,
"entity_picture": f"/api/image_proxy/image.test?token={access_token}",
"friendly_name": "Test",
}
resp = await client.get(f"/api/image_proxy/image.test?token={access_token}")
assert resp.status == HTTPStatus.OK
async def test_fetch_image_authenticated(
hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_image_platform: None
) -> None:
"""Test fetching an image with an authenticated client."""
client = await hass_client()
resp = await client.get("/api/image_proxy/image.test")
assert resp.status == HTTPStatus.OK
body = await resp.read()
assert body == b"Test"
resp = await client.get("/api/image_proxy/image.unknown")
assert resp.status == HTTPStatus.NOT_FOUND
async def test_fetch_image_fail(
hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_image_platform: None
) -> None:
"""Test fetching an image with an authenticated client."""
client = await hass_client()
with patch.object(MockImageEntity, "async_image", side_effect=TimeoutError):
resp = await client.get("/api/image_proxy/image.test")
assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR
async def test_fetch_image_sync(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
"""Test fetching an image with an authenticated client."""
mock_integration(hass, MockModule(domain="test"))
mock_platform(hass, "test.image", MockImagePlatform([MockImageSyncEntity(hass)]))
assert await async_setup_component(
hass, image.DOMAIN, {"image": {"platform": "test"}}
)
await hass.async_block_till_done()
client = await hass_client()
resp = await client.get("/api/image_proxy/image.test")
assert resp.status == HTTPStatus.OK
body = await resp.read()
assert body == b"Test"
async def test_fetch_image_unauthenticated(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
mock_image_platform: None,
) -> None:
"""Test fetching an image with an unauthenticated client."""
client = await hass_client_no_auth()
resp = await client.get("/api/image_proxy/image.test")
assert resp.status == HTTPStatus.FORBIDDEN
resp = await client.get("/api/image_proxy/image.test")
assert resp.status == HTTPStatus.FORBIDDEN
resp = await client.get(
"/api/image_proxy/image.test", headers={hdrs.AUTHORIZATION: "blabla"}
)
assert resp.status == HTTPStatus.UNAUTHORIZED
state = hass.states.get("image.test")
resp = await client.get(state.attributes["entity_picture"])
assert resp.status == HTTPStatus.OK
body = await resp.read()
assert body == b"Test"
resp = await client.get("/api/image_proxy/image.unknown")
assert resp.status == HTTPStatus.NOT_FOUND
@respx.mock
async def test_fetch_image_url_success(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
"""Test fetching an image with an authenticated client."""
respx.get("https://example.com/myimage.jpg").respond(
status_code=HTTPStatus.OK, content_type="image/png", content=b"Test"
)
mock_integration(hass, MockModule(domain="test"))
mock_platform(hass, "test.image", MockImagePlatform([MockURLImageEntity(hass)]))
assert await async_setup_component(
hass, image.DOMAIN, {"image": {"platform": "test"}}
)
await hass.async_block_till_done()
client = await hass_client()
resp = await client.get("/api/image_proxy/image.test")
assert resp.status == HTTPStatus.OK
body = await resp.read()
assert body == b"Test"
@respx.mock
@pytest.mark.parametrize(
"side_effect",
[
httpx.RequestError("server offline", request=MagicMock()),
httpx.TimeoutException,
ssl.SSLError,
],
)
async def test_fetch_image_url_exception(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
side_effect: Exception,
) -> None:
"""Test fetching an image with an authenticated client."""
respx.get("https://example.com/myimage.jpg").mock(side_effect=side_effect)
mock_integration(hass, MockModule(domain="test"))
mock_platform(hass, "test.image", MockImagePlatform([MockURLImageEntity(hass)]))
assert await async_setup_component(
hass, image.DOMAIN, {"image": {"platform": "test"}}
)
await hass.async_block_till_done()
client = await hass_client()
resp = await client.get("/api/image_proxy/image.test")
assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR
@respx.mock
@pytest.mark.parametrize(
"content_type",
[
None,
"text/plain",
],
)
async def test_fetch_image_url_wrong_content_type(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
content_type: str | None,
) -> None:
"""Test fetching an image with an authenticated client."""
respx.get("https://example.com/myimage.jpg").respond(
status_code=HTTPStatus.OK, content_type=content_type, content=b"Test"
)
mock_integration(hass, MockModule(domain="test"))
mock_platform(hass, "test.image", MockImagePlatform([MockURLImageEntity(hass)]))
assert await async_setup_component(
hass, image.DOMAIN, {"image": {"platform": "test"}}
)
await hass.async_block_till_done()
client = await hass_client()
resp = await client.get("/api/image_proxy/image.test")
assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR
async def test_image_stream(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test image stream."""
mock_integration(hass, MockModule(domain="test"))
mock_image = MockURLImageEntity(hass)
mock_platform(hass, "test.image", MockImagePlatform([mock_image]))
assert await async_setup_component(
hass, image.DOMAIN, {"image": {"platform": "test"}}
)
await hass.async_block_till_done()
client = await hass_client()
close_future = hass.loop.create_future()
original_get_still_stream = image.async_get_still_stream
async def _wrap_async_get_still_stream(*args, **kwargs):
result = await original_get_still_stream(*args, **kwargs)
hass.loop.call_soon(close_future.set_result, None)
return result
with patch(
"homeassistant.components.image.async_get_still_stream",
_wrap_async_get_still_stream,
):
with patch.object(mock_image, "async_image", return_value=b""):
resp = await client.get("/api/image_proxy_stream/image.test")
assert not resp.closed
assert resp.status == HTTPStatus.OK
mock_image.image_last_updated = datetime.now()
mock_image.async_write_ha_state()
# Two blocks to ensure the frame is written
await hass.async_block_till_done()
await hass.async_block_till_done()
with patch.object(mock_image, "async_image", return_value=b"") as mock:
# Simulate a "keep alive" frame
freezer.tick(55)
async_fire_time_changed(hass)
# Two blocks to ensure the frame is written
await hass.async_block_till_done()
await hass.async_block_till_done()
mock.assert_called_once()
with patch.object(mock_image, "async_image", return_value=None):
freezer.tick(55)
async_fire_time_changed(hass)
# Two blocks to ensure the frame is written
await hass.async_block_till_done()
await hass.async_block_till_done()
await close_future
async def test_snapshot_service(hass: HomeAssistant) -> None:
"""Test snapshot service."""
mopen = mock_open()
mock_integration(hass, MockModule(domain="test"))
mock_platform(hass, "test.image", MockImagePlatform([MockImageSyncEntity(hass)]))
assert await async_setup_component(
hass, image.DOMAIN, {"image": {"platform": "test"}}
)
await hass.async_block_till_done()
with (
patch("homeassistant.components.image.open", mopen, create=True),
patch("homeassistant.components.image.os.makedirs"),
patch.object(hass.config, "is_allowed_path", return_value=True),
):
await hass.services.async_call(
image.DOMAIN,
image.SERVICE_SNAPSHOT,
{
ATTR_ENTITY_ID: "image.test",
image.ATTR_FILENAME: "/test/snapshot.jpg",
},
blocking=True,
)
mock_write = mopen().write
assert len(mock_write.mock_calls) == 1
assert mock_write.mock_calls[0][1][0] == b"Test"
async def test_snapshot_service_no_image(hass: HomeAssistant) -> None:
"""Test snapshot service with no image."""
mopen = mock_open()
mock_integration(hass, MockModule(domain="test"))
mock_platform(hass, "test.image", MockImagePlatform([MockImageNoDataEntity(hass)]))
assert await async_setup_component(
hass, image.DOMAIN, {"image": {"platform": "test"}}
)
await hass.async_block_till_done()
with (
patch("homeassistant.components.image.open", mopen, create=True),
patch(
"homeassistant.components.image.os.makedirs",
),
patch.object(hass.config, "is_allowed_path", return_value=True),
):
await hass.services.async_call(
image.DOMAIN,
image.SERVICE_SNAPSHOT,
{
ATTR_ENTITY_ID: "image.test",
image.ATTR_FILENAME: "/test/snapshot.jpg",
},
blocking=True,
)
mock_write = mopen().write
assert len(mock_write.mock_calls) == 0
async def test_snapshot_service_not_allowed_path(hass: HomeAssistant) -> None:
"""Test snapshot service with a not allowed path."""
mock_integration(hass, MockModule(domain="test"))
mock_platform(hass, "test.image", MockImagePlatform([MockURLImageEntity(hass)]))
assert await async_setup_component(
hass, image.DOMAIN, {"image": {"platform": "test"}}
)
await hass.async_block_till_done()
with pytest.raises(HomeAssistantError, match="/test/snapshot.jpg"):
await hass.services.async_call(
image.DOMAIN,
image.SERVICE_SNAPSHOT,
{
ATTR_ENTITY_ID: "image.test",
image.ATTR_FILENAME: "/test/snapshot.jpg",
},
blocking=True,
)
async def test_snapshot_service_os_error(hass: HomeAssistant) -> None:
"""Test snapshot service with os error."""
mock_integration(hass, MockModule(domain="test"))
mock_platform(hass, "test.image", MockImagePlatform([MockImageSyncEntity(hass)]))
assert await async_setup_component(
hass, image.DOMAIN, {"image": {"platform": "test"}}
)
await hass.async_block_till_done()
with (
patch.object(hass.config, "is_allowed_path", return_value=True),
patch("os.makedirs", side_effect=OSError),
pytest.raises(HomeAssistantError),
):
await hass.services.async_call(
image.DOMAIN,
image.SERVICE_SNAPSHOT,
{
ATTR_ENTITY_ID: "image.test",
image.ATTR_FILENAME: "/test/snapshot.jpg",
},
blocking=True,
)