mirror of https://github.com/home-assistant/core
496 lines
16 KiB
Python
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,
|
|
)
|