mirror of
https://github.com/home-assistant/core.git
synced 2025-04-26 10:17:51 +00:00

* Remove go2rtc config flow * Address review comments * Update manifest * Always validate go2rtc server URL * Remove extra client * Update homeassistant/components/go2rtc/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Improve test coverage --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
285 lines
8.6 KiB
Python
285 lines
8.6 KiB
Python
"""The tests for the go2rtc component."""
|
|
|
|
from collections.abc import Callable, Generator
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
|
|
from go2rtc_client import Stream, WebRTCSdpAnswer, WebRTCSdpOffer
|
|
from go2rtc_client.models import Producer
|
|
import pytest
|
|
|
|
from homeassistant.components.camera import (
|
|
DOMAIN as CAMERA_DOMAIN,
|
|
Camera,
|
|
CameraEntityFeature,
|
|
)
|
|
from homeassistant.components.camera.const import StreamType
|
|
from homeassistant.components.camera.helper import get_camera_from_entity_id
|
|
from homeassistant.components.go2rtc import WebRTCProvider
|
|
from homeassistant.components.go2rtc.const import DOMAIN
|
|
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
|
from homeassistant.const import CONF_URL
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers.typing import ConfigType
|
|
from homeassistant.setup import async_setup_component
|
|
|
|
from tests.common import (
|
|
MockConfigEntry,
|
|
MockModule,
|
|
mock_config_flow,
|
|
mock_integration,
|
|
mock_platform,
|
|
setup_test_component_platform,
|
|
)
|
|
|
|
TEST_DOMAIN = "test"
|
|
|
|
# The go2rtc provider does not inspect the details of the offer and answer,
|
|
# and is only a pass through.
|
|
OFFER_SDP = "v=0\r\no=carol 28908764872 28908764872 IN IP4 100.3.6.6\r\n..."
|
|
ANSWER_SDP = "v=0\r\no=bob 2890844730 2890844730 IN IP4 host.example.com\r\n..."
|
|
|
|
|
|
class MockCamera(Camera):
|
|
"""Mock Camera Entity."""
|
|
|
|
_attr_name = "Test"
|
|
_attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialize the mock entity."""
|
|
super().__init__()
|
|
self._stream_source: str | None = "rtsp://stream"
|
|
|
|
def set_stream_source(self, stream_source: str | None) -> None:
|
|
"""Set the stream source."""
|
|
self._stream_source = stream_source
|
|
|
|
async def stream_source(self) -> str | None:
|
|
"""Return the source of the stream.
|
|
|
|
This is used by cameras with CameraEntityFeature.STREAM
|
|
and StreamType.HLS.
|
|
"""
|
|
return self._stream_source
|
|
|
|
|
|
@pytest.fixture
|
|
def integration_entity() -> MockCamera:
|
|
"""Mock Camera Entity."""
|
|
return MockCamera()
|
|
|
|
|
|
@pytest.fixture
|
|
def integration_config_entry(hass: HomeAssistant) -> ConfigEntry:
|
|
"""Test mock config entry."""
|
|
entry = MockConfigEntry(domain=TEST_DOMAIN)
|
|
entry.add_to_hass(hass)
|
|
return entry
|
|
|
|
|
|
@pytest.fixture(name="go2rtc_binary")
|
|
def go2rtc_binary_fixture() -> str:
|
|
"""Fixture to provide go2rtc binary name."""
|
|
return "/usr/bin/go2rtc"
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_get_binary(go2rtc_binary) -> Generator[Mock]:
|
|
"""Mock _get_binary."""
|
|
with patch(
|
|
"homeassistant.components.go2rtc.shutil.which",
|
|
return_value=go2rtc_binary,
|
|
) as mock_which:
|
|
yield mock_which
|
|
|
|
|
|
@pytest.fixture(name="is_docker_env")
|
|
def is_docker_env_fixture() -> bool:
|
|
"""Fixture to provide is_docker_env return value."""
|
|
return True
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_is_docker_env(is_docker_env) -> Generator[Mock]:
|
|
"""Mock is_docker_env."""
|
|
with patch(
|
|
"homeassistant.components.go2rtc.is_docker_env",
|
|
return_value=is_docker_env,
|
|
) as mock_is_docker_env:
|
|
yield mock_is_docker_env
|
|
|
|
|
|
@pytest.fixture
|
|
async def init_test_integration(
|
|
hass: HomeAssistant,
|
|
integration_config_entry: ConfigEntry,
|
|
integration_entity: MockCamera,
|
|
) -> None:
|
|
"""Initialize components."""
|
|
|
|
async def async_setup_entry_init(
|
|
hass: HomeAssistant, config_entry: ConfigEntry
|
|
) -> bool:
|
|
"""Set up test config entry."""
|
|
await hass.config_entries.async_forward_entry_setups(
|
|
config_entry, [CAMERA_DOMAIN]
|
|
)
|
|
return True
|
|
|
|
async def async_unload_entry_init(
|
|
hass: HomeAssistant, config_entry: ConfigEntry
|
|
) -> bool:
|
|
"""Unload test config entry."""
|
|
await hass.config_entries.async_forward_entry_unload(
|
|
config_entry, CAMERA_DOMAIN
|
|
)
|
|
return True
|
|
|
|
mock_integration(
|
|
hass,
|
|
MockModule(
|
|
TEST_DOMAIN,
|
|
async_setup_entry=async_setup_entry_init,
|
|
async_unload_entry=async_unload_entry_init,
|
|
),
|
|
)
|
|
setup_test_component_platform(
|
|
hass, CAMERA_DOMAIN, [integration_entity], from_config_entry=True
|
|
)
|
|
mock_platform(hass, f"{TEST_DOMAIN}.config_flow", Mock())
|
|
|
|
with mock_config_flow(TEST_DOMAIN, ConfigFlow):
|
|
assert await hass.config_entries.async_setup(integration_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
return integration_config_entry
|
|
|
|
|
|
async def _test_setup(
|
|
hass: HomeAssistant,
|
|
mock_client: AsyncMock,
|
|
config: ConfigType,
|
|
after_setup_fn: Callable[[], None],
|
|
) -> None:
|
|
"""Test the go2rtc config entry."""
|
|
entity_id = "camera.test"
|
|
camera = get_camera_from_entity_id(hass, entity_id)
|
|
assert camera.frontend_stream_type == StreamType.HLS
|
|
|
|
assert await async_setup_component(hass, DOMAIN, config)
|
|
await hass.async_block_till_done()
|
|
after_setup_fn()
|
|
|
|
mock_client.webrtc.forward_whep_sdp_offer.return_value = WebRTCSdpAnswer(ANSWER_SDP)
|
|
|
|
answer = await camera.async_handle_web_rtc_offer(OFFER_SDP)
|
|
assert answer == ANSWER_SDP
|
|
|
|
mock_client.webrtc.forward_whep_sdp_offer.assert_called_once_with(
|
|
entity_id, WebRTCSdpOffer(OFFER_SDP)
|
|
)
|
|
mock_client.streams.add.assert_called_once_with(entity_id, "rtsp://stream")
|
|
|
|
# If the stream is already added, the stream should not be added again.
|
|
mock_client.streams.add.reset_mock()
|
|
mock_client.streams.list.return_value = {
|
|
entity_id: Stream([Producer("rtsp://stream")])
|
|
}
|
|
|
|
answer = await camera.async_handle_web_rtc_offer(OFFER_SDP)
|
|
assert answer == ANSWER_SDP
|
|
mock_client.streams.add.assert_not_called()
|
|
assert mock_client.webrtc.forward_whep_sdp_offer.call_count == 2
|
|
assert isinstance(camera._webrtc_providers[0], WebRTCProvider)
|
|
|
|
# Set stream source to None and provider should be skipped
|
|
mock_client.streams.list.return_value = {}
|
|
camera.set_stream_source(None)
|
|
with pytest.raises(
|
|
HomeAssistantError,
|
|
match="WebRTC offer was not accepted by the supported providers",
|
|
):
|
|
await camera.async_handle_web_rtc_offer(OFFER_SDP)
|
|
|
|
|
|
@pytest.mark.usefixtures(
|
|
"init_test_integration", "mock_get_binary", "mock_is_docker_env"
|
|
)
|
|
async def test_setup_go_binary(
|
|
hass: HomeAssistant,
|
|
mock_client: AsyncMock,
|
|
mock_server: AsyncMock,
|
|
mock_server_start: Mock,
|
|
mock_server_stop: Mock,
|
|
) -> None:
|
|
"""Test the go2rtc config entry with binary."""
|
|
|
|
def after_setup() -> None:
|
|
mock_server.assert_called_once_with(hass, "/usr/bin/go2rtc")
|
|
mock_server_start.assert_called_once()
|
|
|
|
await _test_setup(hass, mock_client, {DOMAIN: {}}, after_setup)
|
|
|
|
await hass.async_stop()
|
|
mock_server_stop.assert_called_once()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("go2rtc_binary", "is_docker_env"),
|
|
[
|
|
("/usr/bin/go2rtc", True),
|
|
(None, False),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("init_test_integration")
|
|
async def test_setup_go(
|
|
hass: HomeAssistant,
|
|
mock_client: AsyncMock,
|
|
mock_server: Mock,
|
|
mock_get_binary: Mock,
|
|
mock_is_docker_env: Mock,
|
|
) -> None:
|
|
"""Test the go2rtc config entry without binary."""
|
|
config = {DOMAIN: {CONF_URL: "http://localhost:1984/"}}
|
|
|
|
def after_setup() -> None:
|
|
mock_server.assert_not_called()
|
|
|
|
await _test_setup(hass, mock_client, config, after_setup)
|
|
|
|
mock_get_binary.assert_not_called()
|
|
mock_get_binary.assert_not_called()
|
|
mock_server.assert_not_called()
|
|
|
|
|
|
ERR_BINARY_NOT_FOUND = "Could not find go2rtc docker binary"
|
|
ERR_CONNECT = "Could not connect to go2rtc instance"
|
|
ERR_INVALID_URL = "Invalid config for 'go2rtc': invalid url"
|
|
ERR_URL_REQUIRED = "Go2rtc URL required in non-docker installs"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("config", "go2rtc_binary", "is_docker_env", "expected_log_message"),
|
|
[
|
|
({}, None, False, "KeyError: 'go2rtc'"),
|
|
({}, None, True, "KeyError: 'go2rtc'"),
|
|
({DOMAIN: {}}, None, False, ERR_URL_REQUIRED),
|
|
({DOMAIN: {}}, None, True, ERR_BINARY_NOT_FOUND),
|
|
({DOMAIN: {}}, "/usr/bin/go2rtc", True, ERR_CONNECT),
|
|
({DOMAIN: {CONF_URL: "invalid"}}, None, True, ERR_INVALID_URL),
|
|
({DOMAIN: {CONF_URL: "http://localhost:1984/"}}, None, True, ERR_CONNECT),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("mock_get_binary", "mock_is_docker_env", "mock_server")
|
|
async def test_setup_with_error(
|
|
hass: HomeAssistant,
|
|
config: ConfigType,
|
|
caplog: pytest.LogCaptureFixture,
|
|
expected_log_message: str,
|
|
) -> None:
|
|
"""Test setup integration fails."""
|
|
|
|
assert not await async_setup_component(hass, DOMAIN, config)
|
|
assert expected_log_message in caplog.text
|