Validate slug in addon services (#99232)
* Validate slug in addon services * Move validator into hassio component * Fixes from mypy * Fix test for changes * Adjust fixtures to current supervisor * Fix call counts after fixture adjustment * Increase coverage
This commit is contained in:
parent
e2dd7f2069
commit
e0eb63c588
4 changed files with 95 additions and 41 deletions
|
@ -32,6 +32,7 @@ from homeassistant.core import (
|
||||||
HassJob,
|
HassJob,
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
ServiceCall,
|
ServiceCall,
|
||||||
|
async_get_hass,
|
||||||
callback,
|
callback,
|
||||||
)
|
)
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
@ -149,9 +150,22 @@ SERVICE_RESTORE_FULL = "restore_full"
|
||||||
SERVICE_RESTORE_PARTIAL = "restore_partial"
|
SERVICE_RESTORE_PARTIAL = "restore_partial"
|
||||||
|
|
||||||
|
|
||||||
|
def valid_addon(value: Any) -> str:
|
||||||
|
"""Validate value is a valid addon slug."""
|
||||||
|
value = cv.slug(value)
|
||||||
|
|
||||||
|
hass: HomeAssistant | None = None
|
||||||
|
with suppress(HomeAssistantError):
|
||||||
|
hass = async_get_hass()
|
||||||
|
|
||||||
|
if hass and (addons := get_addons_info(hass)) is not None and value not in addons:
|
||||||
|
raise vol.Invalid("Not a valid add-on slug")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
SCHEMA_NO_DATA = vol.Schema({})
|
SCHEMA_NO_DATA = vol.Schema({})
|
||||||
|
|
||||||
SCHEMA_ADDON = vol.Schema({vol.Required(ATTR_ADDON): cv.string})
|
SCHEMA_ADDON = vol.Schema({vol.Required(ATTR_ADDON): valid_addon})
|
||||||
|
|
||||||
SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend(
|
SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend(
|
||||||
{vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)}
|
{vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)}
|
||||||
|
@ -174,7 +188,7 @@ SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend(
|
||||||
{
|
{
|
||||||
vol.Optional(ATTR_HOMEASSISTANT): cv.boolean,
|
vol.Optional(ATTR_HOMEASSISTANT): cv.boolean,
|
||||||
vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]),
|
vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]),
|
||||||
vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]),
|
vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.slug]),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -189,7 +203,7 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend(
|
||||||
{
|
{
|
||||||
vol.Optional(ATTR_HOMEASSISTANT): cv.boolean,
|
vol.Optional(ATTR_HOMEASSISTANT): cv.boolean,
|
||||||
vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]),
|
vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]),
|
||||||
vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]),
|
vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.slug]),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
from yarl import URL
|
||||||
|
|
||||||
from homeassistant.components.http import (
|
from homeassistant.components.http import (
|
||||||
CONF_SERVER_HOST,
|
CONF_SERVER_HOST,
|
||||||
|
@ -530,6 +531,11 @@ class HassIO:
|
||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
"""
|
"""
|
||||||
|
url = f"http://{self._ip}{command}"
|
||||||
|
if url != str(URL(url)):
|
||||||
|
_LOGGER.error("Invalid request %s", command)
|
||||||
|
raise HassioAPIError()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
request = await self.websession.request(
|
request = await self.websession.request(
|
||||||
method,
|
method,
|
||||||
|
|
|
@ -413,3 +413,10 @@ async def test_api_reboot_host(
|
||||||
|
|
||||||
assert await handler.async_reboot_host(hass) == {}
|
assert await handler.async_reboot_host(hass) == {}
|
||||||
assert aioclient_mock.call_count == 1
|
assert aioclient_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_send_command_invalid_command(hass: HomeAssistant, hassio_stubs) -> None:
|
||||||
|
"""Test send command fails when command is invalid."""
|
||||||
|
hassio: HassIO = hass.data["hassio"]
|
||||||
|
with pytest.raises(HassioAPIError):
|
||||||
|
await hassio.send_command("/test/../bad")
|
||||||
|
|
|
@ -5,6 +5,7 @@ from typing import Any
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from voluptuous import Invalid
|
||||||
|
|
||||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||||
from homeassistant.components import frontend
|
from homeassistant.components import frontend
|
||||||
|
@ -100,30 +101,30 @@ def mock_all(aioclient_mock, request, os_info):
|
||||||
"version_latest": "1.0.0",
|
"version_latest": "1.0.0",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"auto_update": True,
|
"auto_update": True,
|
||||||
},
|
|
||||||
"addons": [
|
"addons": [
|
||||||
{
|
{
|
||||||
"name": "test",
|
"name": "test",
|
||||||
"slug": "test",
|
"slug": "test",
|
||||||
"installed": True,
|
"state": "stopped",
|
||||||
"update_available": False,
|
"update_available": False,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"version_latest": "1.0.0",
|
"version_latest": "1.0.0",
|
||||||
"repository": "core",
|
"repository": "core",
|
||||||
"url": "https://github.com/home-assistant/addons/test",
|
"icon": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "test2",
|
"name": "test2",
|
||||||
"slug": "test2",
|
"slug": "test2",
|
||||||
"installed": True,
|
"state": "stopped",
|
||||||
"update_available": False,
|
"update_available": False,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"version_latest": "1.0.0",
|
"version_latest": "1.0.0",
|
||||||
"repository": "core",
|
"repository": "core",
|
||||||
"url": "https://github.com",
|
"icon": False,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
aioclient_mock.get(
|
aioclient_mock.get(
|
||||||
"http://127.0.0.1/core/stats",
|
"http://127.0.0.1/core/stats",
|
||||||
|
@ -243,7 +244,7 @@ async def test_setup_api_ping(
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result
|
assert result
|
||||||
assert aioclient_mock.call_count == 18
|
assert aioclient_mock.call_count == 22
|
||||||
assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0"
|
assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0"
|
||||||
assert hass.components.hassio.is_hassio()
|
assert hass.components.hassio.is_hassio()
|
||||||
|
|
||||||
|
@ -288,7 +289,7 @@ async def test_setup_api_push_api_data(
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result
|
assert result
|
||||||
assert aioclient_mock.call_count == 18
|
assert aioclient_mock.call_count == 22
|
||||||
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
||||||
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
|
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
|
||||||
assert aioclient_mock.mock_calls[1][2]["watchdog"]
|
assert aioclient_mock.mock_calls[1][2]["watchdog"]
|
||||||
|
@ -307,7 +308,7 @@ async def test_setup_api_push_api_data_server_host(
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result
|
assert result
|
||||||
assert aioclient_mock.call_count == 18
|
assert aioclient_mock.call_count == 22
|
||||||
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
||||||
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
|
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
|
||||||
assert not aioclient_mock.mock_calls[1][2]["watchdog"]
|
assert not aioclient_mock.mock_calls[1][2]["watchdog"]
|
||||||
|
@ -324,7 +325,7 @@ async def test_setup_api_push_api_data_default(
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result
|
assert result
|
||||||
assert aioclient_mock.call_count == 18
|
assert aioclient_mock.call_count == 22
|
||||||
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
||||||
assert aioclient_mock.mock_calls[1][2]["port"] == 8123
|
assert aioclient_mock.mock_calls[1][2]["port"] == 8123
|
||||||
refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"]
|
refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"]
|
||||||
|
@ -404,7 +405,7 @@ async def test_setup_api_existing_hassio_user(
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result
|
assert result
|
||||||
assert aioclient_mock.call_count == 18
|
assert aioclient_mock.call_count == 22
|
||||||
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
||||||
assert aioclient_mock.mock_calls[1][2]["port"] == 8123
|
assert aioclient_mock.mock_calls[1][2]["port"] == 8123
|
||||||
assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token
|
assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token
|
||||||
|
@ -421,7 +422,7 @@ async def test_setup_core_push_timezone(
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result
|
assert result
|
||||||
assert aioclient_mock.call_count == 18
|
assert aioclient_mock.call_count == 22
|
||||||
assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone"
|
assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone"
|
||||||
|
|
||||||
with patch("homeassistant.util.dt.set_default_time_zone"):
|
with patch("homeassistant.util.dt.set_default_time_zone"):
|
||||||
|
@ -441,7 +442,7 @@ async def test_setup_hassio_no_additional_data(
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result
|
assert result
|
||||||
assert aioclient_mock.call_count == 18
|
assert aioclient_mock.call_count == 22
|
||||||
assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456"
|
assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456"
|
||||||
|
|
||||||
|
|
||||||
|
@ -486,13 +487,17 @@ async def test_service_register(hassio_env, hass: HomeAssistant) -> None:
|
||||||
|
|
||||||
@pytest.mark.freeze_time("2021-11-13 11:48:00")
|
@pytest.mark.freeze_time("2021-11-13 11:48:00")
|
||||||
async def test_service_calls(
|
async def test_service_calls(
|
||||||
hassio_env,
|
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
aioclient_mock: AiohttpClientMocker,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
caplog: pytest.LogCaptureFixture,
|
caplog: pytest.LogCaptureFixture,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Call service and check the API calls behind that."""
|
"""Call service and check the API calls behind that."""
|
||||||
|
with patch.dict(os.environ, MOCK_ENVIRON), patch(
|
||||||
|
"homeassistant.components.hassio.HassIO.is_connected",
|
||||||
|
return_value=None,
|
||||||
|
):
|
||||||
assert await async_setup_component(hass, "hassio", {})
|
assert await async_setup_component(hass, "hassio", {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
aioclient_mock.post("http://127.0.0.1/addons/test/start", json={"result": "ok"})
|
aioclient_mock.post("http://127.0.0.1/addons/test/start", json={"result": "ok"})
|
||||||
aioclient_mock.post("http://127.0.0.1/addons/test/stop", json={"result": "ok"})
|
aioclient_mock.post("http://127.0.0.1/addons/test/stop", json={"result": "ok"})
|
||||||
|
@ -519,14 +524,14 @@ async def test_service_calls(
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert aioclient_mock.call_count == 10
|
assert aioclient_mock.call_count == 26
|
||||||
assert aioclient_mock.mock_calls[-1][2] == "test"
|
assert aioclient_mock.mock_calls[-1][2] == "test"
|
||||||
|
|
||||||
await hass.services.async_call("hassio", "host_shutdown", {})
|
await hass.services.async_call("hassio", "host_shutdown", {})
|
||||||
await hass.services.async_call("hassio", "host_reboot", {})
|
await hass.services.async_call("hassio", "host_reboot", {})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert aioclient_mock.call_count == 12
|
assert aioclient_mock.call_count == 28
|
||||||
|
|
||||||
await hass.services.async_call("hassio", "backup_full", {})
|
await hass.services.async_call("hassio", "backup_full", {})
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
|
@ -541,7 +546,7 @@ async def test_service_calls(
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert aioclient_mock.call_count == 14
|
assert aioclient_mock.call_count == 30
|
||||||
assert aioclient_mock.mock_calls[-1][2] == {
|
assert aioclient_mock.mock_calls[-1][2] == {
|
||||||
"name": "2021-11-13 11:48:00",
|
"name": "2021-11-13 11:48:00",
|
||||||
"homeassistant": True,
|
"homeassistant": True,
|
||||||
|
@ -566,7 +571,7 @@ async def test_service_calls(
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert aioclient_mock.call_count == 16
|
assert aioclient_mock.call_count == 32
|
||||||
assert aioclient_mock.mock_calls[-1][2] == {
|
assert aioclient_mock.mock_calls[-1][2] == {
|
||||||
"addons": ["test"],
|
"addons": ["test"],
|
||||||
"folders": ["ssl"],
|
"folders": ["ssl"],
|
||||||
|
@ -584,7 +589,7 @@ async def test_service_calls(
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert aioclient_mock.call_count == 17
|
assert aioclient_mock.call_count == 33
|
||||||
assert aioclient_mock.mock_calls[-1][2] == {
|
assert aioclient_mock.mock_calls[-1][2] == {
|
||||||
"name": "backup_name",
|
"name": "backup_name",
|
||||||
"location": "backup_share",
|
"location": "backup_share",
|
||||||
|
@ -599,13 +604,35 @@ async def test_service_calls(
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert aioclient_mock.call_count == 18
|
assert aioclient_mock.call_count == 34
|
||||||
assert aioclient_mock.mock_calls[-1][2] == {
|
assert aioclient_mock.mock_calls[-1][2] == {
|
||||||
"name": "2021-11-13 11:48:00",
|
"name": "2021-11-13 11:48:00",
|
||||||
"location": None,
|
"location": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_invalid_service_calls(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
) -> None:
|
||||||
|
"""Call service with invalid input and check that it raises."""
|
||||||
|
with patch.dict(os.environ, MOCK_ENVIRON), patch(
|
||||||
|
"homeassistant.components.hassio.HassIO.is_connected",
|
||||||
|
return_value=None,
|
||||||
|
):
|
||||||
|
assert await async_setup_component(hass, "hassio", {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
with pytest.raises(Invalid):
|
||||||
|
await hass.services.async_call(
|
||||||
|
"hassio", "addon_start", {"addon": "does_not_exist"}
|
||||||
|
)
|
||||||
|
with pytest.raises(Invalid):
|
||||||
|
await hass.services.async_call(
|
||||||
|
"hassio", "addon_stdin", {"addon": "does_not_exist", "input": "test"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_service_calls_core(
|
async def test_service_calls_core(
|
||||||
hassio_env, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
hassio_env, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -889,7 +916,7 @@ async def test_setup_hardware_integration(
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result
|
assert result
|
||||||
assert aioclient_mock.call_count == 18
|
assert aioclient_mock.call_count == 22
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue