Remove strict connection (#117933)
This commit is contained in:
parent
6f81852eb4
commit
cb62f4242e
32 changed files with 39 additions and 1816 deletions
|
@ -1,28 +1,23 @@
|
|||
"""The tests for the Home Assistant HTTP component."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
from ipaddress import ip_network
|
||||
import logging
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from aiohttp import BasicAuth, ServerDisconnectedError, web
|
||||
from aiohttp.test_utils import TestClient
|
||||
from aiohttp import BasicAuth, web
|
||||
from aiohttp.web_exceptions import HTTPUnauthorized
|
||||
from aiohttp_session import get_session
|
||||
import jwt
|
||||
import pytest
|
||||
import yarl
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_READ_ONLY
|
||||
from homeassistant.auth.models import RefreshToken, User
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.auth.providers import trusted_networks
|
||||
from homeassistant.auth.providers.legacy_api_password import (
|
||||
LegacyApiPasswordAuthProvider,
|
||||
)
|
||||
from homeassistant.auth.session import SESSION_ID, TEMP_TIMEOUT
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.http import KEY_HASS
|
||||
from homeassistant.components.http.auth import (
|
||||
|
@ -30,12 +25,11 @@ from homeassistant.components.http.auth import (
|
|||
DATA_SIGN_SECRET,
|
||||
SIGN_QUERY_PARAM,
|
||||
STORAGE_KEY,
|
||||
STRICT_CONNECTION_GUARD_PAGE,
|
||||
async_setup_auth,
|
||||
async_sign_path,
|
||||
async_user_not_allowed_do_auth,
|
||||
)
|
||||
from homeassistant.components.http.const import KEY_AUTHENTICATED, StrictConnectionMode
|
||||
from homeassistant.components.http.const import KEY_AUTHENTICATED
|
||||
from homeassistant.components.http.forwarded import async_setup_forwarded
|
||||
from homeassistant.components.http.request_context import (
|
||||
current_request,
|
||||
|
@ -43,11 +37,10 @@ from homeassistant.components.http.request_context import (
|
|||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import HTTP_HEADER_HA_AUTH
|
||||
|
||||
from tests.common import MockUser, async_fire_time_changed
|
||||
from tests.common import MockUser
|
||||
from tests.test_util import mock_real_ip
|
||||
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
||||
|
||||
|
@ -137,7 +130,7 @@ async def test_cant_access_with_password_in_header(
|
|||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test access with password in header."""
|
||||
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app)
|
||||
client = await aiohttp_client(app)
|
||||
|
||||
req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
|
||||
|
@ -154,7 +147,7 @@ async def test_cant_access_with_password_in_query(
|
|||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test access with password in URL."""
|
||||
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app)
|
||||
client = await aiohttp_client(app)
|
||||
|
||||
resp = await client.get("/", params={"api_password": API_PASSWORD})
|
||||
|
@ -174,7 +167,7 @@ async def test_basic_auth_does_not_work(
|
|||
legacy_auth: LegacyApiPasswordAuthProvider,
|
||||
) -> None:
|
||||
"""Test access with basic authentication."""
|
||||
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app)
|
||||
client = await aiohttp_client(app)
|
||||
|
||||
req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD))
|
||||
|
@ -198,7 +191,7 @@ async def test_cannot_access_with_trusted_ip(
|
|||
hass_owner_user: MockUser,
|
||||
) -> None:
|
||||
"""Test access with an untrusted ip address."""
|
||||
await async_setup_auth(hass, app2, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app2)
|
||||
|
||||
set_mock_ip = mock_real_ip(app2)
|
||||
client = await aiohttp_client(app2)
|
||||
|
@ -226,7 +219,7 @@ async def test_auth_active_access_with_access_token_in_header(
|
|||
) -> None:
|
||||
"""Test access with access token in header."""
|
||||
token = hass_access_token
|
||||
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app)
|
||||
client = await aiohttp_client(app)
|
||||
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
|
||||
|
||||
|
@ -262,7 +255,7 @@ async def test_auth_active_access_with_trusted_ip(
|
|||
hass_owner_user: MockUser,
|
||||
) -> None:
|
||||
"""Test access with an untrusted ip address."""
|
||||
await async_setup_auth(hass, app2, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app2)
|
||||
|
||||
set_mock_ip = mock_real_ip(app2)
|
||||
client = await aiohttp_client(app2)
|
||||
|
@ -289,7 +282,7 @@ async def test_auth_legacy_support_api_password_cannot_access(
|
|||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test access using api_password if auth.support_legacy."""
|
||||
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app)
|
||||
client = await aiohttp_client(app)
|
||||
|
||||
req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
|
||||
|
@ -311,7 +304,7 @@ async def test_auth_access_signed_path_with_refresh_token(
|
|||
"""Test access with signed url."""
|
||||
app.router.add_post("/", mock_handler)
|
||||
app.router.add_get("/another_path", mock_handler)
|
||||
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app)
|
||||
client = await aiohttp_client(app)
|
||||
|
||||
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
|
||||
|
@ -356,7 +349,7 @@ async def test_auth_access_signed_path_with_query_param(
|
|||
"""Test access with signed url and query params."""
|
||||
app.router.add_post("/", mock_handler)
|
||||
app.router.add_get("/another_path", mock_handler)
|
||||
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app)
|
||||
client = await aiohttp_client(app)
|
||||
|
||||
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
|
||||
|
@ -386,7 +379,7 @@ async def test_auth_access_signed_path_with_query_param_order(
|
|||
"""Test access with signed url and query params different order."""
|
||||
app.router.add_post("/", mock_handler)
|
||||
app.router.add_get("/another_path", mock_handler)
|
||||
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app)
|
||||
client = await aiohttp_client(app)
|
||||
|
||||
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
|
||||
|
@ -427,7 +420,7 @@ async def test_auth_access_signed_path_with_query_param_safe_param(
|
|||
"""Test access with signed url and changing a safe param."""
|
||||
app.router.add_post("/", mock_handler)
|
||||
app.router.add_get("/another_path", mock_handler)
|
||||
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app)
|
||||
client = await aiohttp_client(app)
|
||||
|
||||
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
|
||||
|
@ -466,7 +459,7 @@ async def test_auth_access_signed_path_with_query_param_tamper(
|
|||
"""Test access with signed url and query params that have been tampered with."""
|
||||
app.router.add_post("/", mock_handler)
|
||||
app.router.add_get("/another_path", mock_handler)
|
||||
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app)
|
||||
client = await aiohttp_client(app)
|
||||
|
||||
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
|
||||
|
@ -535,7 +528,7 @@ async def test_auth_access_signed_path_with_http(
|
|||
)
|
||||
|
||||
app.router.add_get("/hello", mock_handler)
|
||||
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app)
|
||||
client = await aiohttp_client(app)
|
||||
|
||||
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
|
||||
|
@ -559,7 +552,7 @@ async def test_auth_access_signed_path_with_content_user(
|
|||
hass: HomeAssistant, app, aiohttp_client: ClientSessionGenerator
|
||||
) -> None:
|
||||
"""Test access signed url uses content user."""
|
||||
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app)
|
||||
signed_path = async_sign_path(hass, "/", timedelta(seconds=5))
|
||||
signature = yarl.URL(signed_path).query["authSig"]
|
||||
claims = jwt.decode(
|
||||
|
@ -579,7 +572,7 @@ async def test_local_only_user_rejected(
|
|||
) -> None:
|
||||
"""Test access with access token in header."""
|
||||
token = hass_access_token
|
||||
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app)
|
||||
set_mock_ip = mock_real_ip(app)
|
||||
client = await aiohttp_client(app)
|
||||
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
|
||||
|
@ -645,7 +638,7 @@ async def test_create_user_once(hass: HomeAssistant) -> None:
|
|||
"""Test that we reuse the user."""
|
||||
cur_users = len(await hass.auth.async_get_users())
|
||||
app = web.Application()
|
||||
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app)
|
||||
users = await hass.auth.async_get_users()
|
||||
assert len(users) == cur_users + 1
|
||||
|
||||
|
@ -657,287 +650,7 @@ async def test_create_user_once(hass: HomeAssistant) -> None:
|
|||
assert len(user.refresh_tokens) == 1
|
||||
assert user.system_generated
|
||||
|
||||
await async_setup_auth(hass, app, StrictConnectionMode.DISABLED)
|
||||
await async_setup_auth(hass, app)
|
||||
|
||||
# test it did not create a user
|
||||
assert len(await hass.auth.async_get_users()) == cur_users + 1
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_strict_connection(hass):
|
||||
"""Fixture to set up a web.Application."""
|
||||
|
||||
async def handler(request):
|
||||
"""Return if request was authenticated."""
|
||||
return web.json_response(data={"authenticated": request[KEY_AUTHENTICATED]})
|
||||
|
||||
app = web.Application()
|
||||
app[KEY_HASS] = hass
|
||||
app.router.add_get("/", handler)
|
||||
async_setup_forwarded(app, True, [])
|
||||
return app
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"strict_connection_mode", [e.value for e in StrictConnectionMode]
|
||||
)
|
||||
async def test_strict_connection_non_cloud_authenticated_requests(
|
||||
hass: HomeAssistant,
|
||||
app_strict_connection: web.Application,
|
||||
aiohttp_client: ClientSessionGenerator,
|
||||
hass_access_token: str,
|
||||
strict_connection_mode: StrictConnectionMode,
|
||||
) -> None:
|
||||
"""Test authenticated requests with strict connection."""
|
||||
token = hass_access_token
|
||||
await async_setup_auth(hass, app_strict_connection, strict_connection_mode)
|
||||
set_mock_ip = mock_real_ip(app_strict_connection)
|
||||
client = await aiohttp_client(app_strict_connection)
|
||||
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
|
||||
assert refresh_token
|
||||
assert hass.auth.session._strict_connection_sessions == {}
|
||||
|
||||
signed_path = async_sign_path(
|
||||
hass, "/", timedelta(seconds=5), refresh_token_id=refresh_token.id
|
||||
)
|
||||
|
||||
for remote_addr in (*LOCALHOST_ADDRESSES, *PRIVATE_ADDRESSES, *EXTERNAL_ADDRESSES):
|
||||
set_mock_ip(remote_addr)
|
||||
|
||||
# authorized requests should work normally
|
||||
req = await client.get("/", headers={"Authorization": f"Bearer {token}"})
|
||||
assert req.status == HTTPStatus.OK
|
||||
assert await req.json() == {"authenticated": True}
|
||||
req = await client.get(signed_path)
|
||||
assert req.status == HTTPStatus.OK
|
||||
assert await req.json() == {"authenticated": True}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"strict_connection_mode", [e.value for e in StrictConnectionMode]
|
||||
)
|
||||
async def test_strict_connection_non_cloud_local_unauthenticated_requests(
|
||||
hass: HomeAssistant,
|
||||
app_strict_connection: web.Application,
|
||||
aiohttp_client: ClientSessionGenerator,
|
||||
strict_connection_mode: StrictConnectionMode,
|
||||
) -> None:
|
||||
"""Test local unauthenticated requests with strict connection."""
|
||||
await async_setup_auth(hass, app_strict_connection, strict_connection_mode)
|
||||
set_mock_ip = mock_real_ip(app_strict_connection)
|
||||
client = await aiohttp_client(app_strict_connection)
|
||||
assert hass.auth.session._strict_connection_sessions == {}
|
||||
|
||||
for remote_addr in (*LOCALHOST_ADDRESSES, *PRIVATE_ADDRESSES):
|
||||
set_mock_ip(remote_addr)
|
||||
# local requests should work normally
|
||||
req = await client.get("/")
|
||||
assert req.status == HTTPStatus.OK
|
||||
assert await req.json() == {"authenticated": False}
|
||||
|
||||
|
||||
def _add_set_cookie_endpoint(app: web.Application, refresh_token: RefreshToken) -> None:
|
||||
"""Add an endpoint to set a cookie."""
|
||||
|
||||
async def set_cookie(request: web.Request) -> web.Response:
|
||||
hass = request.app[KEY_HASS]
|
||||
# Clear all sessions
|
||||
hass.auth.session._temp_sessions.clear()
|
||||
hass.auth.session._strict_connection_sessions.clear()
|
||||
|
||||
if request.query["token"] == "refresh":
|
||||
await hass.auth.session.async_create_session(request, refresh_token)
|
||||
else:
|
||||
await hass.auth.session.async_create_temp_unauthorized_session(request)
|
||||
session = await get_session(request)
|
||||
return web.Response(text=session[SESSION_ID])
|
||||
|
||||
app.router.add_get("/test/cookie", set_cookie)
|
||||
|
||||
|
||||
async def _test_strict_connection_non_cloud_enabled_setup(
|
||||
hass: HomeAssistant,
|
||||
app: web.Application,
|
||||
aiohttp_client: ClientSessionGenerator,
|
||||
hass_access_token: str,
|
||||
strict_connection_mode: StrictConnectionMode,
|
||||
) -> tuple[TestClient, Callable[[str], None], RefreshToken]:
|
||||
"""Test external unauthenticated requests with strict connection non cloud enabled."""
|
||||
refresh_token = hass.auth.async_validate_access_token(hass_access_token)
|
||||
assert refresh_token
|
||||
session = hass.auth.session
|
||||
assert session._strict_connection_sessions == {}
|
||||
assert session._temp_sessions == {}
|
||||
|
||||
_add_set_cookie_endpoint(app, refresh_token)
|
||||
await async_setup_auth(hass, app, strict_connection_mode)
|
||||
set_mock_ip = mock_real_ip(app)
|
||||
client = await aiohttp_client(app)
|
||||
return (client, set_mock_ip, refresh_token)
|
||||
|
||||
|
||||
async def _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests(
|
||||
hass: HomeAssistant,
|
||||
app: web.Application,
|
||||
aiohttp_client: ClientSessionGenerator,
|
||||
hass_access_token: str,
|
||||
perform_unauthenticated_request: Callable[
|
||||
[HomeAssistant, TestClient], Awaitable[None]
|
||||
],
|
||||
strict_connection_mode: StrictConnectionMode,
|
||||
) -> None:
|
||||
"""Test external unauthenticated requests with strict connection non cloud enabled."""
|
||||
client, set_mock_ip, _ = await _test_strict_connection_non_cloud_enabled_setup(
|
||||
hass, app, aiohttp_client, hass_access_token, strict_connection_mode
|
||||
)
|
||||
|
||||
for remote_addr in EXTERNAL_ADDRESSES:
|
||||
set_mock_ip(remote_addr)
|
||||
await perform_unauthenticated_request(hass, client)
|
||||
|
||||
|
||||
async def _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests_refresh_token(
|
||||
hass: HomeAssistant,
|
||||
app: web.Application,
|
||||
aiohttp_client: ClientSessionGenerator,
|
||||
hass_access_token: str,
|
||||
perform_unauthenticated_request: Callable[
|
||||
[HomeAssistant, TestClient], Awaitable[None]
|
||||
],
|
||||
strict_connection_mode: StrictConnectionMode,
|
||||
) -> None:
|
||||
"""Test external unauthenticated requests with strict connection non cloud enabled and refresh token cookie."""
|
||||
(
|
||||
client,
|
||||
set_mock_ip,
|
||||
refresh_token,
|
||||
) = await _test_strict_connection_non_cloud_enabled_setup(
|
||||
hass, app, aiohttp_client, hass_access_token, strict_connection_mode
|
||||
)
|
||||
session = hass.auth.session
|
||||
|
||||
# set strict connection cookie with refresh token
|
||||
set_mock_ip(LOCALHOST_ADDRESSES[0])
|
||||
session_id = await (await client.get("/test/cookie?token=refresh")).text()
|
||||
assert session._strict_connection_sessions == {session_id: refresh_token.id}
|
||||
for remote_addr in EXTERNAL_ADDRESSES:
|
||||
set_mock_ip(remote_addr)
|
||||
req = await client.get("/")
|
||||
assert req.status == HTTPStatus.OK
|
||||
assert await req.json() == {"authenticated": False}
|
||||
|
||||
# Invalidate refresh token, which should also invalidate session
|
||||
hass.auth.async_remove_refresh_token(refresh_token)
|
||||
assert session._strict_connection_sessions == {}
|
||||
for remote_addr in EXTERNAL_ADDRESSES:
|
||||
set_mock_ip(remote_addr)
|
||||
await perform_unauthenticated_request(hass, client)
|
||||
|
||||
|
||||
async def _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests_temp_session(
|
||||
hass: HomeAssistant,
|
||||
app: web.Application,
|
||||
aiohttp_client: ClientSessionGenerator,
|
||||
hass_access_token: str,
|
||||
perform_unauthenticated_request: Callable[
|
||||
[HomeAssistant, TestClient], Awaitable[None]
|
||||
],
|
||||
strict_connection_mode: StrictConnectionMode,
|
||||
) -> None:
|
||||
"""Test external unauthenticated requests with strict connection non cloud enabled and temp cookie."""
|
||||
client, set_mock_ip, _ = await _test_strict_connection_non_cloud_enabled_setup(
|
||||
hass, app, aiohttp_client, hass_access_token, strict_connection_mode
|
||||
)
|
||||
session = hass.auth.session
|
||||
|
||||
# set strict connection cookie with temp session
|
||||
assert session._temp_sessions == {}
|
||||
set_mock_ip(LOCALHOST_ADDRESSES[0])
|
||||
session_id = await (await client.get("/test/cookie?token=temp")).text()
|
||||
assert client.session.cookie_jar.filter_cookies(URL("http://127.0.0.1"))
|
||||
assert session_id in session._temp_sessions
|
||||
for remote_addr in EXTERNAL_ADDRESSES:
|
||||
set_mock_ip(remote_addr)
|
||||
resp = await client.get("/")
|
||||
assert resp.status == HTTPStatus.OK
|
||||
assert await resp.json() == {"authenticated": False}
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + TEMP_TIMEOUT + timedelta(minutes=1))
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert session._temp_sessions == {}
|
||||
for remote_addr in EXTERNAL_ADDRESSES:
|
||||
set_mock_ip(remote_addr)
|
||||
await perform_unauthenticated_request(hass, client)
|
||||
|
||||
|
||||
async def _drop_connection_unauthorized_request(
|
||||
_: HomeAssistant, client: TestClient
|
||||
) -> None:
|
||||
with pytest.raises(ServerDisconnectedError):
|
||||
# unauthorized requests should raise ServerDisconnectedError
|
||||
await client.get("/")
|
||||
|
||||
|
||||
async def _guard_page_unauthorized_request(
|
||||
hass: HomeAssistant, client: TestClient
|
||||
) -> None:
|
||||
req = await client.get("/")
|
||||
assert req.status == HTTPStatus.IM_A_TEAPOT
|
||||
|
||||
def read_guard_page() -> str:
|
||||
with open(STRICT_CONNECTION_GUARD_PAGE, encoding="utf-8") as file:
|
||||
return file.read()
|
||||
|
||||
assert await req.text() == await hass.async_add_executor_job(read_guard_page)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_func",
|
||||
[
|
||||
_test_strict_connection_non_cloud_enabled_external_unauthenticated_requests,
|
||||
_test_strict_connection_non_cloud_enabled_external_unauthenticated_requests_refresh_token,
|
||||
_test_strict_connection_non_cloud_enabled_external_unauthenticated_requests_temp_session,
|
||||
],
|
||||
ids=[
|
||||
"no cookie",
|
||||
"refresh token cookie",
|
||||
"temp session cookie",
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("strict_connection_mode", "request_func"),
|
||||
[
|
||||
(StrictConnectionMode.DROP_CONNECTION, _drop_connection_unauthorized_request),
|
||||
(StrictConnectionMode.GUARD_PAGE, _guard_page_unauthorized_request),
|
||||
],
|
||||
ids=["drop connection", "static page"],
|
||||
)
|
||||
async def test_strict_connection_non_cloud_external_unauthenticated_requests(
|
||||
hass: HomeAssistant,
|
||||
app_strict_connection: web.Application,
|
||||
aiohttp_client: ClientSessionGenerator,
|
||||
hass_access_token: str,
|
||||
test_func: Callable[
|
||||
[
|
||||
HomeAssistant,
|
||||
web.Application,
|
||||
ClientSessionGenerator,
|
||||
str,
|
||||
Callable[[HomeAssistant, TestClient], Awaitable[None]],
|
||||
StrictConnectionMode,
|
||||
],
|
||||
Awaitable[None],
|
||||
],
|
||||
strict_connection_mode: StrictConnectionMode,
|
||||
request_func: Callable[[HomeAssistant, TestClient], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Test external unauthenticated requests with strict connection non cloud."""
|
||||
await test_func(
|
||||
hass,
|
||||
app_strict_connection,
|
||||
aiohttp_client,
|
||||
hass_access_token,
|
||||
request_func,
|
||||
strict_connection_mode,
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue