mirror of
https://github.com/home-assistant/core.git
synced 2025-12-24 21:06:19 +00:00
Use basic auth in go2rtc (#157008)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -4,9 +4,10 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from secrets import token_hex
|
||||
import shutil
|
||||
|
||||
from aiohttp import ClientSession, UnixConnector
|
||||
from aiohttp import BasicAuth, ClientSession, UnixConnector
|
||||
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
|
||||
from awesomeversion import AwesomeVersion
|
||||
from go2rtc_client import Go2RtcRestClient
|
||||
@@ -36,7 +37,12 @@ from homeassistant.components.camera import (
|
||||
from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOMAIN
|
||||
from homeassistant.components.stream import Orientation
|
||||
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_URL,
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
@@ -44,7 +50,10 @@ from homeassistant.helpers import (
|
||||
discovery_flow,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_create_clientsession,
|
||||
async_get_clientsession,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
from homeassistant.util.package import is_docker_env
|
||||
@@ -62,14 +71,43 @@ from .server import Server
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_FFMPEG = "ffmpeg"
|
||||
_AUTH = "auth"
|
||||
|
||||
|
||||
def _validate_auth(config: dict) -> dict:
|
||||
"""Validate that username and password are only set when a URL is configured or when debug UI is enabled."""
|
||||
auth_exists = CONF_USERNAME in config
|
||||
debug_ui_enabled = config.get(CONF_DEBUG_UI, False)
|
||||
|
||||
if debug_ui_enabled and not auth_exists:
|
||||
raise vol.Invalid("Username and password must be set when debug_ui is true")
|
||||
|
||||
if auth_exists and CONF_URL not in config and not debug_ui_enabled:
|
||||
raise vol.Invalid(
|
||||
"Username and password can only be set when a URL is configured or debug_ui is true"
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Exclusive(CONF_URL, DOMAIN, DEBUG_UI_URL_MESSAGE): cv.url,
|
||||
vol.Exclusive(CONF_DEBUG_UI, DOMAIN, DEBUG_UI_URL_MESSAGE): cv.boolean,
|
||||
}
|
||||
DOMAIN: vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Exclusive(CONF_URL, DOMAIN, DEBUG_UI_URL_MESSAGE): cv.url,
|
||||
vol.Exclusive(
|
||||
CONF_DEBUG_UI, DOMAIN, DEBUG_UI_URL_MESSAGE
|
||||
): cv.boolean,
|
||||
vol.Inclusive(CONF_USERNAME, _AUTH): vol.All(
|
||||
cv.string, vol.Length(min=1)
|
||||
),
|
||||
vol.Inclusive(CONF_PASSWORD, _AUTH): vol.All(
|
||||
cv.string, vol.Length(min=1)
|
||||
),
|
||||
}
|
||||
),
|
||||
_validate_auth,
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
@@ -83,12 +121,19 @@ type Go2RtcConfigEntry = ConfigEntry[WebRTCProvider]
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up WebRTC."""
|
||||
url: str | None = None
|
||||
username: str | None = None
|
||||
password: str | None = None
|
||||
|
||||
if DOMAIN not in config and DEFAULT_CONFIG_DOMAIN not in config:
|
||||
await _remove_go2rtc_entries(hass)
|
||||
return True
|
||||
|
||||
domain_config = config.get(DOMAIN, {})
|
||||
username = domain_config.get(CONF_USERNAME)
|
||||
password = domain_config.get(CONF_PASSWORD)
|
||||
|
||||
if not (configured_by_user := DOMAIN in config) or not (
|
||||
url := config[DOMAIN].get(CONF_URL)
|
||||
url := domain_config.get(CONF_URL)
|
||||
):
|
||||
if not is_docker_env():
|
||||
if not configured_by_user:
|
||||
@@ -101,13 +146,26 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
_LOGGER.error("Could not find go2rtc docker binary")
|
||||
return False
|
||||
|
||||
# Generate random credentials when not provided to secure the server
|
||||
if not username or not password:
|
||||
username = token_hex()
|
||||
password = token_hex()
|
||||
_LOGGER.debug("Generated random credentials for go2rtc server")
|
||||
|
||||
auth = BasicAuth(username, password)
|
||||
# HA will manage the binary
|
||||
session = ClientSession(connector=UnixConnector(path=HA_MANAGED_UNIX_SOCKET))
|
||||
# Manually created session (not using the helper) needs to be closed manually
|
||||
# See on_stop listener below
|
||||
session = ClientSession(
|
||||
connector=UnixConnector(path=HA_MANAGED_UNIX_SOCKET), auth=auth
|
||||
)
|
||||
server = Server(
|
||||
hass,
|
||||
binary,
|
||||
session,
|
||||
enable_ui=config.get(DOMAIN, {}).get(CONF_DEBUG_UI, False),
|
||||
enable_ui=domain_config.get(CONF_DEBUG_UI, False),
|
||||
username=username,
|
||||
password=password,
|
||||
)
|
||||
try:
|
||||
await server.start()
|
||||
@@ -122,6 +180,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop)
|
||||
|
||||
url = HA_MANAGED_URL
|
||||
elif username and password:
|
||||
# Create session with BasicAuth if credentials are provided
|
||||
auth = BasicAuth(username, password)
|
||||
session = async_create_clientsession(hass, auth=auth)
|
||||
else:
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ _RESPAWN_COOLDOWN = 1
|
||||
|
||||
# Default configuration for HA
|
||||
# - Unix socket for secure local communication
|
||||
# - Basic auth enabled, including local connections
|
||||
# - HTTP API only enabled when UI is enabled
|
||||
# - Enable rtsp for localhost only as ffmpeg needs it
|
||||
# - Clear default ice servers
|
||||
@@ -37,6 +38,9 @@ api:
|
||||
listen: "{listen_config}"
|
||||
unix_listen: "{unix_socket}"
|
||||
allow_paths: {api_allow_paths}
|
||||
local_auth: true
|
||||
username: {username}
|
||||
password: {password}
|
||||
|
||||
# ffmpeg needs the exec module
|
||||
# Restrict execution to only ffmpeg binary
|
||||
@@ -118,7 +122,7 @@ def _format_list_for_yaml(items: tuple[str, ...]) -> str:
|
||||
return f"[{formatted_items}]"
|
||||
|
||||
|
||||
def _create_temp_file(enable_ui: bool) -> str:
|
||||
def _create_temp_file(enable_ui: bool, username: str, password: str) -> str:
|
||||
"""Create temporary config file."""
|
||||
app_modules: tuple[str, ...] = _APP_MODULES
|
||||
api_paths: tuple[str, ...] = _API_ALLOW_PATHS
|
||||
@@ -142,6 +146,8 @@ def _create_temp_file(enable_ui: bool) -> str:
|
||||
unix_socket=HA_MANAGED_UNIX_SOCKET,
|
||||
app_modules=_format_list_for_yaml(app_modules),
|
||||
api_allow_paths=_format_list_for_yaml(api_paths),
|
||||
username=username,
|
||||
password=password,
|
||||
).encode()
|
||||
)
|
||||
return file.name
|
||||
@@ -157,15 +163,19 @@ class Server:
|
||||
session: ClientSession,
|
||||
*,
|
||||
enable_ui: bool = False,
|
||||
username: str,
|
||||
password: str,
|
||||
) -> None:
|
||||
"""Initialize the server."""
|
||||
self._hass = hass
|
||||
self._binary = binary
|
||||
self._session = session
|
||||
self._enable_ui = enable_ui
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE)
|
||||
self._process: asyncio.subprocess.Process | None = None
|
||||
self._startup_complete = asyncio.Event()
|
||||
self._enable_ui = enable_ui
|
||||
self._watchdog_task: asyncio.Task | None = None
|
||||
self._watchdog_tasks: list[asyncio.Task] = []
|
||||
|
||||
@@ -180,7 +190,7 @@ class Server:
|
||||
"""Start the server."""
|
||||
_LOGGER.debug("Starting go2rtc server")
|
||||
config_file = await self._hass.async_add_executor_job(
|
||||
_create_temp_file, self._enable_ui
|
||||
_create_temp_file, self._enable_ui, self._username, self._password
|
||||
)
|
||||
|
||||
self._startup_complete.clear()
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
# serializer version: 1
|
||||
# name: test_server_run_success[False]
|
||||
# name: test_server_run_success[False-d2a0b844f4cdbe773702176c47c9a675eb0c56a0779b8f880cdb3b492ed3b1c1-bc495d266a32e66ba69b9c72546e00101e04fb573f1bd08863fe4ad1aac02949]
|
||||
_CallList([
|
||||
_Call(
|
||||
tuple(
|
||||
b'# This file is managed by Home Assistant\n# Do not edit it manually\n\napp:\n modules: ["api","exec","ffmpeg","http","mjpeg","onvif","rtmp","rtsp","srtp","webrtc","ws"]\n\napi:\n listen: ""\n unix_listen: "/run/go2rtc.sock"\n allow_paths: ["/","/api","/api/frame.jpeg","/api/schemes","/api/streams","/api/webrtc","/api/ws"]\n\n# ffmpeg needs the exec module\n# Restrict execution to only ffmpeg binary\nexec:\n allow_paths:\n - ffmpeg\n\nrtsp:\n listen: "127.0.0.1:18554"\n\nwebrtc:\n listen: ":18555/tcp"\n ice_servers: []\n',
|
||||
b'# This file is managed by Home Assistant\n# Do not edit it manually\n\napp:\n modules: ["api","exec","ffmpeg","http","mjpeg","onvif","rtmp","rtsp","srtp","webrtc","ws"]\n\napi:\n listen: ""\n unix_listen: "/run/go2rtc.sock"\n allow_paths: ["/","/api","/api/frame.jpeg","/api/schemes","/api/streams","/api/webrtc","/api/ws"]\n local_auth: true\n username: d2a0b844f4cdbe773702176c47c9a675eb0c56a0779b8f880cdb3b492ed3b1c1\n password: bc495d266a32e66ba69b9c72546e00101e04fb573f1bd08863fe4ad1aac02949\n\n# ffmpeg needs the exec module\n# Restrict execution to only ffmpeg binary\nexec:\n allow_paths:\n - ffmpeg\n\nrtsp:\n listen: "127.0.0.1:18554"\n\nwebrtc:\n listen: ":18555/tcp"\n ice_servers: []\n',
|
||||
),
|
||||
dict({
|
||||
}),
|
||||
),
|
||||
])
|
||||
# ---
|
||||
# name: test_server_run_success[True]
|
||||
# name: test_server_run_success[True-user-pass]
|
||||
_CallList([
|
||||
_Call(
|
||||
tuple(
|
||||
b'# This file is managed by Home Assistant\n# Do not edit it manually\n\napp:\n modules: ["api","exec","ffmpeg","http","mjpeg","onvif","rtmp","rtsp","srtp","webrtc","ws","debug"]\n\napi:\n listen: ":11984"\n unix_listen: "/run/go2rtc.sock"\n allow_paths: ["/","/api","/api/frame.jpeg","/api/schemes","/api/streams","/api/webrtc","/api/ws","/api/config","/api/log","/api/streams.dot"]\n\n# ffmpeg needs the exec module\n# Restrict execution to only ffmpeg binary\nexec:\n allow_paths:\n - ffmpeg\n\nrtsp:\n listen: "127.0.0.1:18554"\n\nwebrtc:\n listen: ":18555/tcp"\n ice_servers: []\n',
|
||||
b'# This file is managed by Home Assistant\n# Do not edit it manually\n\napp:\n modules: ["api","exec","ffmpeg","http","mjpeg","onvif","rtmp","rtsp","srtp","webrtc","ws","debug"]\n\napi:\n listen: ":11984"\n unix_listen: "/run/go2rtc.sock"\n allow_paths: ["/","/api","/api/frame.jpeg","/api/schemes","/api/streams","/api/webrtc","/api/ws","/api/config","/api/log","/api/streams.dot"]\n local_auth: true\n username: user\n password: pass\n\n# ffmpeg needs the exec module\n# Restrict execution to only ffmpeg binary\nexec:\n allow_paths:\n - ffmpeg\n\nrtsp:\n listen: "127.0.0.1:18554"\n\nwebrtc:\n listen: ":18555/tcp"\n ice_servers: []\n',
|
||||
),
|
||||
dict({
|
||||
}),
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
from typing import NamedTuple
|
||||
from unittest.mock import ANY, AsyncMock, Mock, patch
|
||||
|
||||
from aiohttp import UnixConnector
|
||||
from aiohttp import BasicAuth, UnixConnector
|
||||
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
|
||||
from awesomeversion import AwesomeVersion
|
||||
from go2rtc_client import Stream
|
||||
@@ -44,7 +44,7 @@ from homeassistant.components.go2rtc.const import (
|
||||
)
|
||||
from homeassistant.components.stream import Orientation
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
|
||||
from homeassistant.core import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
@@ -192,12 +192,40 @@ async def _test_setup_and_signaling(
|
||||
"mock_go2rtc_entry",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("config", "ui_enabled"),
|
||||
("config", "ui_enabled", "expected_username", "expected_password"),
|
||||
[
|
||||
({DOMAIN: {}}, False),
|
||||
({DOMAIN: {CONF_DEBUG_UI: True}}, True),
|
||||
({DEFAULT_CONFIG_DOMAIN: {}}, False),
|
||||
({DEFAULT_CONFIG_DOMAIN: {}, DOMAIN: {CONF_DEBUG_UI: True}}, True),
|
||||
({DOMAIN: {}}, False, "mock_username_token", "mock_password_token"),
|
||||
(
|
||||
{
|
||||
DOMAIN: {
|
||||
CONF_DEBUG_UI: True,
|
||||
CONF_USERNAME: "test_username",
|
||||
CONF_PASSWORD: "test",
|
||||
}
|
||||
},
|
||||
True,
|
||||
"test_username",
|
||||
"test",
|
||||
),
|
||||
(
|
||||
{DEFAULT_CONFIG_DOMAIN: {}},
|
||||
False,
|
||||
"mock_username_token",
|
||||
"mock_password_token",
|
||||
),
|
||||
(
|
||||
{
|
||||
DEFAULT_CONFIG_DOMAIN: {},
|
||||
DOMAIN: {
|
||||
CONF_DEBUG_UI: True,
|
||||
CONF_USERNAME: "test_username",
|
||||
CONF_PASSWORD: "test",
|
||||
},
|
||||
},
|
||||
True,
|
||||
"test_username",
|
||||
"test",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("has_go2rtc_entry", [True, False])
|
||||
@@ -213,25 +241,39 @@ async def test_setup_go_binary(
|
||||
has_go2rtc_entry: bool,
|
||||
config: ConfigType,
|
||||
ui_enabled: bool,
|
||||
expected_username: str,
|
||||
expected_password: str,
|
||||
) -> None:
|
||||
"""Test the go2rtc config entry with binary."""
|
||||
assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry
|
||||
|
||||
def after_setup() -> None:
|
||||
server.assert_called_once_with(
|
||||
hass, "/usr/bin/go2rtc", ANY, enable_ui=ui_enabled
|
||||
hass,
|
||||
"/usr/bin/go2rtc",
|
||||
ANY,
|
||||
enable_ui=ui_enabled,
|
||||
username=expected_username,
|
||||
password=expected_password,
|
||||
)
|
||||
call_kwargs = server.call_args[1]
|
||||
assert call_kwargs["username"] == expected_username
|
||||
assert call_kwargs["password"] == expected_password
|
||||
server_start.assert_called_once()
|
||||
|
||||
await _test_setup_and_signaling(
|
||||
hass,
|
||||
issue_registry,
|
||||
rest_client,
|
||||
ws_client,
|
||||
config,
|
||||
after_setup,
|
||||
init_test_integration,
|
||||
)
|
||||
with patch("homeassistant.components.go2rtc.token_hex") as mock_token_hex:
|
||||
# First call for username, second call for password
|
||||
mock_token_hex.side_effect = ["mock_username_token", "mock_password_token"]
|
||||
|
||||
await _test_setup_and_signaling(
|
||||
hass,
|
||||
issue_registry,
|
||||
rest_client,
|
||||
ws_client,
|
||||
config,
|
||||
after_setup,
|
||||
init_test_integration,
|
||||
)
|
||||
|
||||
await hass.async_stop()
|
||||
server_stop.assert_called_once()
|
||||
@@ -424,6 +466,25 @@ ERR_UNSUPPORTED_VERSION = "The go2rtc server version is not supported"
|
||||
_INVALID_CONFIG = "Invalid config for 'go2rtc': "
|
||||
ERR_INVALID_URL = _INVALID_CONFIG + "invalid url"
|
||||
ERR_EXCLUSIVE = _INVALID_CONFIG + DEBUG_UI_URL_MESSAGE
|
||||
ERR_AUTH_WITHOUT_URL_OR_UI = (
|
||||
_INVALID_CONFIG
|
||||
+ "Username and password can only be set when a URL is configured or debug_ui is true"
|
||||
)
|
||||
ERR_AUTH_INCOMPLETE = (
|
||||
_INVALID_CONFIG
|
||||
+ "some but not all values in the same group of inclusion 'auth' 'go2rtc-><auth>',"
|
||||
)
|
||||
ERR_AUTH_REQUIRED_WITH_DEBUG_UI = (
|
||||
_INVALID_CONFIG + "Username and password must be set when debug_ui is true"
|
||||
)
|
||||
ERR_USERNAME_EMPTY = (
|
||||
_INVALID_CONFIG
|
||||
+ "length of value must be at least 1 for dictionary value 'go2rtc->username'"
|
||||
)
|
||||
ERR_PASSWORD_EMPTY = (
|
||||
_INVALID_CONFIG
|
||||
+ "length of value must be at least 1 for dictionary value 'go2rtc->password'"
|
||||
)
|
||||
ERR_URL_REQUIRED = "Go2rtc URL required in non-docker installs"
|
||||
|
||||
|
||||
@@ -464,6 +525,60 @@ async def test_non_user_setup_with_error(
|
||||
True,
|
||||
ERR_EXCLUSIVE,
|
||||
),
|
||||
(
|
||||
{DOMAIN: {CONF_USERNAME: "test_user", CONF_PASSWORD: "test_pass"}},
|
||||
"/usr/bin/go2rtc",
|
||||
True,
|
||||
ERR_AUTH_WITHOUT_URL_OR_UI,
|
||||
),
|
||||
(
|
||||
{DOMAIN: {CONF_URL: "http://localhost:1984", CONF_USERNAME: "test_user"}},
|
||||
None,
|
||||
True,
|
||||
ERR_AUTH_INCOMPLETE,
|
||||
),
|
||||
(
|
||||
{DOMAIN: {CONF_URL: "http://localhost:1984", CONF_PASSWORD: "test_pass"}},
|
||||
None,
|
||||
True,
|
||||
ERR_AUTH_INCOMPLETE,
|
||||
),
|
||||
(
|
||||
{DOMAIN: {CONF_DEBUG_UI: True, CONF_USERNAME: "test_user"}},
|
||||
"/usr/bin/go2rtc",
|
||||
True,
|
||||
ERR_AUTH_INCOMPLETE,
|
||||
),
|
||||
(
|
||||
{DOMAIN: {CONF_DEBUG_UI: True, CONF_PASSWORD: "test_pass"}},
|
||||
"/usr/bin/go2rtc",
|
||||
True,
|
||||
ERR_AUTH_INCOMPLETE,
|
||||
),
|
||||
(
|
||||
{DOMAIN: {CONF_DEBUG_UI: True}},
|
||||
"/usr/bin/go2rtc",
|
||||
True,
|
||||
ERR_AUTH_REQUIRED_WITH_DEBUG_UI,
|
||||
),
|
||||
(
|
||||
{DOMAIN: {CONF_DEBUG_UI: True, CONF_USERNAME: "", CONF_PASSWORD: ""}},
|
||||
"/usr/bin/go2rtc",
|
||||
True,
|
||||
ERR_USERNAME_EMPTY,
|
||||
),
|
||||
(
|
||||
{
|
||||
DOMAIN: {
|
||||
CONF_DEBUG_UI: True,
|
||||
CONF_USERNAME: "username",
|
||||
CONF_PASSWORD: "",
|
||||
}
|
||||
},
|
||||
"/usr/bin/go2rtc",
|
||||
True,
|
||||
ERR_PASSWORD_EMPTY,
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("has_go2rtc_entry", [True, False])
|
||||
@@ -923,20 +1038,32 @@ async def test_unix_socket_connection(hass: HomeAssistant) -> None:
|
||||
"""Test Unix socket is used for HA-managed go2rtc instances."""
|
||||
config = {DOMAIN: {}}
|
||||
|
||||
with patch("homeassistant.components.go2rtc.ClientSession") as mock_session_cls:
|
||||
with (
|
||||
patch("homeassistant.components.go2rtc.ClientSession") as mock_session_cls,
|
||||
patch("homeassistant.components.go2rtc.token_hex") as mock_token_hex,
|
||||
):
|
||||
mock_session = AsyncMock()
|
||||
mock_session_cls.return_value = mock_session
|
||||
# First call for username, second call for password
|
||||
mock_token_hex.side_effect = ["mock_username_token", "mock_password_token"]
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Verify ClientSession was created with UnixConnector
|
||||
# Verify ClientSession was created with UnixConnector and auth
|
||||
mock_session_cls.assert_called_once()
|
||||
call_kwargs = mock_session_cls.call_args[1]
|
||||
assert "connector" in call_kwargs
|
||||
connector = call_kwargs["connector"]
|
||||
assert isinstance(connector, UnixConnector)
|
||||
assert connector.path == HA_MANAGED_UNIX_SOCKET
|
||||
# Auth should be auto-generated when credentials are not explicitly configured
|
||||
assert "auth" in call_kwargs
|
||||
auth = call_kwargs["auth"]
|
||||
assert isinstance(auth, BasicAuth)
|
||||
# Verify auto-generated credentials match our mocked values
|
||||
assert auth.login == "mock_username_token"
|
||||
assert auth.password == "mock_password_token"
|
||||
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||
await hass.async_block_till_done()
|
||||
@@ -960,3 +1087,86 @@ async def test_unix_socket_not_used_for_custom_server(hass: HomeAssistant) -> No
|
||||
|
||||
# Verify standard clientsession was used, not UnixConnector
|
||||
mock_get_session.assert_called_once_with(hass)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("rest_client", "server")
|
||||
async def test_basic_auth_with_custom_url(hass: HomeAssistant) -> None:
|
||||
"""Test BasicAuth session is created when username and password are provided with custom URL."""
|
||||
config = {
|
||||
DOMAIN: {
|
||||
CONF_URL: "http://localhost:1984/",
|
||||
CONF_USERNAME: "test_user",
|
||||
CONF_PASSWORD: "test_pass",
|
||||
}
|
||||
}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.go2rtc.async_create_clientsession"
|
||||
) as mock_create_session:
|
||||
mock_session = AsyncMock()
|
||||
mock_create_session.return_value = mock_session
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Verify async_create_clientsession was called with BasicAuth
|
||||
mock_create_session.assert_called_once()
|
||||
call_kwargs = mock_create_session.call_args[1]
|
||||
assert "auth" in call_kwargs
|
||||
auth = call_kwargs["auth"]
|
||||
assert isinstance(auth, BasicAuth)
|
||||
assert auth.login == "test_user"
|
||||
assert auth.password == "test_pass"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("rest_client")
|
||||
async def test_basic_auth_with_debug_ui(hass: HomeAssistant) -> None:
|
||||
"""Test BasicAuth session is created when username and password are provided with debug_ui."""
|
||||
config = {
|
||||
DOMAIN: {
|
||||
CONF_DEBUG_UI: True,
|
||||
CONF_USERNAME: "test_user",
|
||||
CONF_PASSWORD: "test_pass",
|
||||
}
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.go2rtc.Server",
|
||||
autospec=True,
|
||||
) as mock_server_cls,
|
||||
patch("homeassistant.components.go2rtc.ClientSession") as mock_session_cls,
|
||||
patch("homeassistant.components.go2rtc.is_docker_env", return_value=True),
|
||||
patch(
|
||||
"homeassistant.components.go2rtc.shutil.which",
|
||||
return_value="/usr/bin/go2rtc",
|
||||
),
|
||||
):
|
||||
mock_session = AsyncMock()
|
||||
mock_session_cls.return_value = mock_session
|
||||
|
||||
# Configure the Server mock instance
|
||||
mock_server_instance = AsyncMock()
|
||||
mock_server_cls.return_value = mock_server_instance
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Verify ClientSession was created with BasicAuth and UnixConnector
|
||||
mock_session_cls.assert_called_once()
|
||||
call_kwargs = mock_session_cls.call_args[1]
|
||||
assert "connector" in call_kwargs
|
||||
connector = call_kwargs["connector"]
|
||||
assert isinstance(connector, UnixConnector)
|
||||
assert connector.path == HA_MANAGED_UNIX_SOCKET
|
||||
assert "auth" in call_kwargs
|
||||
auth = call_kwargs["auth"]
|
||||
assert isinstance(auth, BasicAuth)
|
||||
assert auth.login == "test_user"
|
||||
assert auth.password == "test_pass"
|
||||
|
||||
# Verify Server was called with username and password
|
||||
mock_server_cls.assert_called_once()
|
||||
call_kwargs = mock_server_cls.call_args[1]
|
||||
assert call_kwargs["username"] == "test_user"
|
||||
assert call_kwargs["password"] == "test_pass"
|
||||
|
||||
@@ -22,6 +22,18 @@ def enable_ui() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def username() -> str:
|
||||
"""Fixture to provide a username."""
|
||||
return "user"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def password() -> str:
|
||||
"""Fixture to provide a password."""
|
||||
return "pass"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session() -> AsyncMock:
|
||||
"""Fixture to provide a mock ClientSession."""
|
||||
@@ -29,9 +41,22 @@ def mock_session() -> AsyncMock:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def server(hass: HomeAssistant, mock_session: AsyncMock, enable_ui: bool) -> Server:
|
||||
def server(
|
||||
hass: HomeAssistant,
|
||||
mock_session: AsyncMock,
|
||||
enable_ui: bool,
|
||||
username: str,
|
||||
password: str,
|
||||
) -> Server:
|
||||
"""Fixture to initialize the Server."""
|
||||
return Server(hass, binary=TEST_BINARY, session=mock_session, enable_ui=enable_ui)
|
||||
return Server(
|
||||
hass,
|
||||
binary=TEST_BINARY,
|
||||
session=mock_session,
|
||||
enable_ui=enable_ui,
|
||||
username=username,
|
||||
password=password,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -82,8 +107,15 @@ def assert_server_output_not_logged(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"enable_ui",
|
||||
[True, False],
|
||||
("enable_ui", "username", "password"),
|
||||
[
|
||||
(True, "user", "pass"),
|
||||
(
|
||||
False,
|
||||
"d2a0b844f4cdbe773702176c47c9a675eb0c56a0779b8f880cdb3b492ed3b1c1",
|
||||
"bc495d266a32e66ba69b9c72546e00101e04fb573f1bd08863fe4ad1aac02949",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("rest_client")
|
||||
async def test_server_run_success(
|
||||
|
||||
Reference in New Issue
Block a user