Fix blocking I/O in the event loop when registering static paths (#119629)
This commit is contained in:
parent
eb89ce47ea
commit
faa55de538
10 changed files with 171 additions and 53 deletions
|
@ -106,6 +106,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await async_register_dynalite_frontend(hass)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -131,9 +133,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
raise ConfigEntryNotReady
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
await async_register_dynalite_frontend(hass)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import panel_custom, websocket_api
|
from homeassistant.components import panel_custom, websocket_api
|
||||||
from homeassistant.components.cover import DEVICE_CLASSES
|
from homeassistant.components.cover import DEVICE_CLASSES
|
||||||
|
from homeassistant.components.http import StaticPathConfig
|
||||||
from homeassistant.const import CONF_DEFAULT, CONF_HOST, CONF_NAME, CONF_PORT
|
from homeassistant.const import CONF_DEFAULT, CONF_HOST, CONF_NAME, CONF_PORT
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
|
@ -98,19 +99,18 @@ async def async_register_dynalite_frontend(hass: HomeAssistant):
|
||||||
"""Register the Dynalite frontend configuration panel."""
|
"""Register the Dynalite frontend configuration panel."""
|
||||||
websocket_api.async_register_command(hass, get_dynalite_config)
|
websocket_api.async_register_command(hass, get_dynalite_config)
|
||||||
websocket_api.async_register_command(hass, save_dynalite_config)
|
websocket_api.async_register_command(hass, save_dynalite_config)
|
||||||
if DOMAIN not in hass.data.get("frontend_panels", {}):
|
path = locate_dir()
|
||||||
path = locate_dir()
|
build_id = get_build_id()
|
||||||
build_id = get_build_id()
|
await hass.http.async_register_static_paths(
|
||||||
hass.http.register_static_path(
|
[StaticPathConfig(URL_BASE, path, cache_headers=(build_id != "dev"))]
|
||||||
URL_BASE, path, cache_headers=(build_id != "dev")
|
)
|
||||||
)
|
|
||||||
|
|
||||||
await panel_custom.async_register_panel(
|
await panel_custom.async_register_panel(
|
||||||
hass=hass,
|
hass=hass,
|
||||||
frontend_url_path=DOMAIN,
|
frontend_url_path=DOMAIN,
|
||||||
config_panel_domain=DOMAIN,
|
config_panel_domain=DOMAIN,
|
||||||
webcomponent_name="dynalite-panel",
|
webcomponent_name="dynalite-panel",
|
||||||
module_url=f"{URL_BASE}/entrypoint-{build_id}.js",
|
module_url=f"{URL_BASE}/entrypoint-{build_id}.js",
|
||||||
embed_iframe=True,
|
embed_iframe=True,
|
||||||
require_admin=True,
|
require_admin=True,
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,7 +15,7 @@ import voluptuous as vol
|
||||||
from yarl import URL
|
from yarl import URL
|
||||||
|
|
||||||
from homeassistant.components import onboarding, websocket_api
|
from homeassistant.components import onboarding, websocket_api
|
||||||
from homeassistant.components.http import KEY_HASS, HomeAssistantView
|
from homeassistant.components.http import KEY_HASS, HomeAssistantView, StaticPathConfig
|
||||||
from homeassistant.components.websocket_api.connection import ActiveConnection
|
from homeassistant.components.websocket_api.connection import ActiveConnection
|
||||||
from homeassistant.config import async_hass_config_yaml
|
from homeassistant.config import async_hass_config_yaml
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
@ -378,6 +378,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
is_dev = repo_path is not None
|
is_dev = repo_path is not None
|
||||||
root_path = _frontend_root(repo_path)
|
root_path = _frontend_root(repo_path)
|
||||||
|
|
||||||
|
static_paths_configs: list[StaticPathConfig] = []
|
||||||
|
|
||||||
for path, should_cache in (
|
for path, should_cache in (
|
||||||
("service_worker.js", False),
|
("service_worker.js", False),
|
||||||
("robots.txt", False),
|
("robots.txt", False),
|
||||||
|
@ -386,10 +388,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
("frontend_latest", not is_dev),
|
("frontend_latest", not is_dev),
|
||||||
("frontend_es5", not is_dev),
|
("frontend_es5", not is_dev),
|
||||||
):
|
):
|
||||||
hass.http.register_static_path(f"/{path}", str(root_path / path), should_cache)
|
static_paths_configs.append(
|
||||||
|
StaticPathConfig(f"/{path}", str(root_path / path), should_cache)
|
||||||
|
)
|
||||||
|
|
||||||
hass.http.register_static_path(
|
static_paths_configs.append(
|
||||||
"/auth/authorize", str(root_path / "authorize.html"), False
|
StaticPathConfig("/auth/authorize", str(root_path / "authorize.html"), False)
|
||||||
)
|
)
|
||||||
# https://wicg.github.io/change-password-url/
|
# https://wicg.github.io/change-password-url/
|
||||||
hass.http.register_redirect(
|
hass.http.register_redirect(
|
||||||
|
@ -397,9 +401,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
)
|
)
|
||||||
|
|
||||||
local = hass.config.path("www")
|
local = hass.config.path("www")
|
||||||
if os.path.isdir(local):
|
if await hass.async_add_executor_job(os.path.isdir, local):
|
||||||
hass.http.register_static_path("/local", local, not is_dev)
|
static_paths_configs.append(StaticPathConfig("/local", local, not is_dev))
|
||||||
|
|
||||||
|
await hass.http.async_register_static_paths(static_paths_configs)
|
||||||
# Shopping list panel was replaced by todo panel in 2023.11
|
# Shopping list panel was replaced by todo panel in 2023.11
|
||||||
hass.http.register_redirect("/shopping-list", "/todo")
|
hass.http.register_redirect("/shopping-list", "/todo")
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import voluptuous as vol
|
||||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||||
from homeassistant.components import panel_custom
|
from homeassistant.components import panel_custom
|
||||||
from homeassistant.components.homeassistant import async_set_stop_handler
|
from homeassistant.components.homeassistant import async_set_stop_handler
|
||||||
|
from homeassistant.components.http import StaticPathConfig
|
||||||
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_NAME,
|
ATTR_NAME,
|
||||||
|
@ -350,8 +351,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||||
# This overrides the normal API call that would be forwarded
|
# This overrides the normal API call that would be forwarded
|
||||||
development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO)
|
development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO)
|
||||||
if development_repo is not None:
|
if development_repo is not None:
|
||||||
hass.http.register_static_path(
|
await hass.http.async_register_static_paths(
|
||||||
"/api/hassio/app", os.path.join(development_repo, "hassio/build"), False
|
[
|
||||||
|
StaticPathConfig(
|
||||||
|
"/api/hassio/app",
|
||||||
|
os.path.join(development_repo, "hassio/build"),
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.http.register_view(HassIOView(host, websession))
|
hass.http.register_view(HassIOView(host, websession))
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Collection
|
||||||
|
from dataclasses import dataclass
|
||||||
import datetime
|
import datetime
|
||||||
|
from functools import partial
|
||||||
from ipaddress import IPv4Network, IPv6Network, ip_network
|
from ipaddress import IPv4Network, IPv6Network, ip_network
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
@ -29,7 +32,7 @@ from yarl import URL
|
||||||
|
|
||||||
from homeassistant.components.network import async_get_source_ip
|
from homeassistant.components.network import async_get_source_ip
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, SERVER_PORT
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, SERVER_PORT
|
||||||
from homeassistant.core import Event, HomeAssistant
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import storage
|
from homeassistant.helpers import storage
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
@ -134,6 +137,21 @@ HTTP_SCHEMA: Final = vol.All(
|
||||||
CONFIG_SCHEMA: Final = vol.Schema({DOMAIN: HTTP_SCHEMA}, extra=vol.ALLOW_EXTRA)
|
CONFIG_SCHEMA: Final = vol.Schema({DOMAIN: HTTP_SCHEMA}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class StaticPathConfig:
|
||||||
|
"""Configuration for a static path."""
|
||||||
|
|
||||||
|
url_path: str
|
||||||
|
path: str
|
||||||
|
cache_headers: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
_STATIC_CLASSES = {
|
||||||
|
True: CachingStaticResource,
|
||||||
|
False: web.StaticResource,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConfData(TypedDict, total=False):
|
class ConfData(TypedDict, total=False):
|
||||||
"""Typed dict for config data."""
|
"""Typed dict for config data."""
|
||||||
|
|
||||||
|
@ -284,6 +302,16 @@ class HomeAssistantApplication(web.Application):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _serve_file_with_cache_headers(
|
||||||
|
path: str, request: web.Request
|
||||||
|
) -> web.FileResponse:
|
||||||
|
return web.FileResponse(path, headers=CACHE_HEADERS)
|
||||||
|
|
||||||
|
|
||||||
|
async def _serve_file(path: str, request: web.Request) -> web.FileResponse:
|
||||||
|
return web.FileResponse(path)
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistantHTTP:
|
class HomeAssistantHTTP:
|
||||||
"""HTTP server for Home Assistant."""
|
"""HTTP server for Home Assistant."""
|
||||||
|
|
||||||
|
@ -403,30 +431,58 @@ class HomeAssistantHTTP:
|
||||||
self.app.router.add_route("GET", url, redirect)
|
self.app.router.add_route("GET", url, redirect)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _make_static_resources(
|
||||||
|
self, configs: Collection[StaticPathConfig]
|
||||||
|
) -> dict[str, CachingStaticResource | web.StaticResource | None]:
|
||||||
|
"""Create a list of static resources."""
|
||||||
|
return {
|
||||||
|
config.url_path: _STATIC_CLASSES[config.cache_headers](
|
||||||
|
config.url_path, config.path
|
||||||
|
)
|
||||||
|
if os.path.isdir(config.path)
|
||||||
|
else None
|
||||||
|
for config in configs
|
||||||
|
}
|
||||||
|
|
||||||
|
async def async_register_static_paths(
|
||||||
|
self, configs: Collection[StaticPathConfig]
|
||||||
|
) -> None:
|
||||||
|
"""Register a folder or file to serve as a static path."""
|
||||||
|
resources = await self.hass.async_add_executor_job(
|
||||||
|
self._make_static_resources, configs
|
||||||
|
)
|
||||||
|
self._async_register_static_paths(configs, resources)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_register_static_paths(
|
||||||
|
self,
|
||||||
|
configs: Collection[StaticPathConfig],
|
||||||
|
resources: dict[str, CachingStaticResource | web.StaticResource | None],
|
||||||
|
) -> None:
|
||||||
|
"""Register a folders or files to serve as a static path."""
|
||||||
|
app = self.app
|
||||||
|
allow_cors = app[KEY_ALLOW_CONFIGRED_CORS]
|
||||||
|
for config in configs:
|
||||||
|
if resource := resources[config.url_path]:
|
||||||
|
app.router.register_resource(resource)
|
||||||
|
allow_cors(resource)
|
||||||
|
|
||||||
|
target = (
|
||||||
|
_serve_file_with_cache_headers if config.cache_headers else _serve_file
|
||||||
|
)
|
||||||
|
allow_cors(
|
||||||
|
self.app.router.add_route(
|
||||||
|
"GET", config.url_path, partial(target, config.path)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def register_static_path(
|
def register_static_path(
|
||||||
self, url_path: str, path: str, cache_headers: bool = True
|
self, url_path: str, path: str, cache_headers: bool = True
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register a folder or file to serve as a static path."""
|
"""Register a folder or file to serve as a static path."""
|
||||||
if os.path.isdir(path):
|
configs = [StaticPathConfig(url_path, path, cache_headers)]
|
||||||
if cache_headers:
|
resources = self._make_static_resources(configs)
|
||||||
resource: CachingStaticResource | web.StaticResource = (
|
self._async_register_static_paths(configs, resources)
|
||||||
CachingStaticResource(url_path, path)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
resource = web.StaticResource(url_path, path)
|
|
||||||
self.app.router.register_resource(resource)
|
|
||||||
self.app[KEY_ALLOW_CONFIGRED_CORS](resource)
|
|
||||||
return
|
|
||||||
|
|
||||||
async def serve_file(request: web.Request) -> web.FileResponse:
|
|
||||||
"""Serve file from disk."""
|
|
||||||
if cache_headers:
|
|
||||||
return web.FileResponse(path, headers=CACHE_HEADERS)
|
|
||||||
return web.FileResponse(path)
|
|
||||||
|
|
||||||
self.app[KEY_ALLOW_CONFIGRED_CORS](
|
|
||||||
self.app.router.add_route("GET", url_path, serve_file)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _create_ssl_context(self) -> ssl.SSLContext | None:
|
def _create_ssl_context(self) -> ssl.SSLContext | None:
|
||||||
context: ssl.SSLContext | None = None
|
context: ssl.SSLContext | None = None
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
from insteon_frontend import get_build_id, locate_dir
|
from insteon_frontend import get_build_id, locate_dir
|
||||||
|
|
||||||
from homeassistant.components import panel_custom, websocket_api
|
from homeassistant.components import panel_custom, websocket_api
|
||||||
|
from homeassistant.components.http import StaticPathConfig
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
from ..const import CONF_DEV_PATH, DOMAIN
|
from ..const import CONF_DEV_PATH, DOMAIN
|
||||||
|
@ -91,7 +92,9 @@ async def async_register_insteon_frontend(hass: HomeAssistant):
|
||||||
is_dev = dev_path is not None
|
is_dev = dev_path is not None
|
||||||
path = dev_path if dev_path else locate_dir()
|
path = dev_path if dev_path else locate_dir()
|
||||||
build_id = get_build_id(is_dev)
|
build_id = get_build_id(is_dev)
|
||||||
hass.http.register_static_path(URL_BASE, path, cache_headers=not is_dev)
|
await hass.http.async_register_static_paths(
|
||||||
|
[StaticPathConfig(URL_BASE, path, cache_headers=not is_dev)]
|
||||||
|
)
|
||||||
|
|
||||||
await panel_custom.async_register_panel(
|
await panel_custom.async_register_panel(
|
||||||
hass=hass,
|
hass=hass,
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"after_dependencies": ["panel_custom"],
|
"after_dependencies": ["panel_custom"],
|
||||||
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
|
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["file_upload", "websocket_api"],
|
"dependencies": ["file_upload", "http", "websocket_api"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/knx",
|
"documentation": "https://www.home-assistant.io/integrations/knx",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
|
|
|
@ -9,6 +9,7 @@ import voluptuous as vol
|
||||||
from xknxproject.exceptions import XknxProjectException
|
from xknxproject.exceptions import XknxProjectException
|
||||||
|
|
||||||
from homeassistant.components import panel_custom, websocket_api
|
from homeassistant.components import panel_custom, websocket_api
|
||||||
|
from homeassistant.components.http import StaticPathConfig
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
@ -31,10 +32,14 @@ async def register_panel(hass: HomeAssistant) -> None:
|
||||||
websocket_api.async_register_command(hass, ws_get_knx_project)
|
websocket_api.async_register_command(hass, ws_get_knx_project)
|
||||||
|
|
||||||
if DOMAIN not in hass.data.get("frontend_panels", {}):
|
if DOMAIN not in hass.data.get("frontend_panels", {}):
|
||||||
hass.http.register_static_path(
|
await hass.http.async_register_static_paths(
|
||||||
URL_BASE,
|
[
|
||||||
path=knx_panel.locate_dir(),
|
StaticPathConfig(
|
||||||
cache_headers=knx_panel.is_prod_build,
|
URL_BASE,
|
||||||
|
path=knx_panel.locate_dir(),
|
||||||
|
cache_headers=knx_panel.is_prod_build,
|
||||||
|
)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
await panel_custom.async_register_panel(
|
await panel_custom.async_register_panel(
|
||||||
hass=hass,
|
hass=hass,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
from asyncio import AbstractEventLoop
|
from asyncio import AbstractEventLoop
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
@ -787,3 +788,23 @@ async def test_get_icons_for_single_integration(
|
||||||
assert msg["type"] == TYPE_RESULT
|
assert msg["type"] == TYPE_RESULT
|
||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
assert msg["result"] == {"resources": {"http": {}}}
|
assert msg["result"] == {"resources": {"http": {}}}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_www_local_dir(
|
||||||
|
hass: HomeAssistant, tmp_path: Path, hass_client: ClientSessionGenerator
|
||||||
|
) -> None:
|
||||||
|
"""Test local www folder."""
|
||||||
|
hass.config.config_dir = str(tmp_path)
|
||||||
|
tmp_path_www = tmp_path / "www"
|
||||||
|
x_txt_file = tmp_path_www / "x.txt"
|
||||||
|
|
||||||
|
def _create_www_and_x_txt():
|
||||||
|
tmp_path_www.mkdir()
|
||||||
|
x_txt_file.write_text("any")
|
||||||
|
|
||||||
|
await hass.async_add_executor_job(_create_www_and_x_txt)
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, "frontend", {})
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.get("/local/x.txt")
|
||||||
|
assert resp.status == HTTPStatus.OK
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
"""The tests for http static files."""
|
"""The tests for http static files."""
|
||||||
|
|
||||||
|
from http import HTTPStatus
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from aiohttp.test_utils import TestClient
|
from aiohttp.test_utils import TestClient
|
||||||
from aiohttp.web_exceptions import HTTPForbidden
|
from aiohttp.web_exceptions import HTTPForbidden
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.http import StaticPathConfig
|
||||||
from homeassistant.components.http.static import CachingStaticResource, _get_file_path
|
from homeassistant.components.http.static import CachingStaticResource, _get_file_path
|
||||||
from homeassistant.core import EVENT_HOMEASSISTANT_START, HomeAssistant
|
from homeassistant.core import EVENT_HOMEASSISTANT_START, HomeAssistant
|
||||||
from homeassistant.helpers.http import KEY_ALLOW_CONFIGRED_CORS
|
from homeassistant.helpers.http import KEY_ALLOW_CONFIGRED_CORS
|
||||||
|
@ -59,3 +61,23 @@ async def test_static_path_blocks_anchors(
|
||||||
# changes we still block it.
|
# changes we still block it.
|
||||||
with pytest.raises(HTTPForbidden):
|
with pytest.raises(HTTPForbidden):
|
||||||
_get_file_path(canonical_url, tmp_path)
|
_get_file_path(canonical_url, tmp_path)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_register_static_paths(
|
||||||
|
hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
|
) -> None:
|
||||||
|
"""Test registering multiple static paths."""
|
||||||
|
assert await async_setup_component(hass, "frontend", {})
|
||||||
|
path = str(Path(__file__).parent)
|
||||||
|
await hass.http.async_register_static_paths(
|
||||||
|
[
|
||||||
|
StaticPathConfig("/something", path),
|
||||||
|
StaticPathConfig("/something_else", path),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.get("/something/__init__.py")
|
||||||
|
assert resp.status == HTTPStatus.OK
|
||||||
|
resp = await client.get("/something_else/__init__.py")
|
||||||
|
assert resp.status == HTTPStatus.OK
|
||||||
|
|
Loading…
Add table
Reference in a new issue