mirror of
https://github.com/home-assistant/core.git
synced 2026-06-03 14:14:40 +01:00
905b868c82
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
325 lines
10 KiB
Python
325 lines
10 KiB
Python
"""Test for Portainer services."""
|
|
|
|
from datetime import timedelta
|
|
from unittest.mock import AsyncMock
|
|
|
|
from pyportainer import (
|
|
PortainerAuthenticationError,
|
|
PortainerConnectionError,
|
|
PortainerTimeoutError,
|
|
)
|
|
import pytest
|
|
from voluptuous import MultipleInvalid
|
|
|
|
from homeassistant.components.portainer.const import DOMAIN
|
|
from homeassistant.components.portainer.services import (
|
|
ATTR_CONTAINER_DEVICE_ID,
|
|
ATTR_DANGLING,
|
|
ATTR_DATE_UNTIL,
|
|
ATTR_PULL_IMAGE,
|
|
ATTR_TIMEOUT,
|
|
SERVICE_PRUNE_IMAGES,
|
|
SERVICE_RECREATE_CONTAINER,
|
|
)
|
|
from homeassistant.const import ATTR_DEVICE_ID
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
|
from homeassistant.helpers.device_registry import DeviceRegistry
|
|
|
|
from . import setup_integration
|
|
from .conftest import TEST_CONTAINER_ID, TEST_CONTAINER_NAME, TEST_ENTRY
|
|
|
|
from tests.common import MockConfigEntry
|
|
|
|
TEST_ENDPOINT_ID = 1
|
|
TEST_DEVICE_IDENTIFIER = f"{TEST_ENTRY}_{TEST_ENDPOINT_ID}"
|
|
|
|
TEST_CONTAINER_DEVICE_IDENTIFIER = (
|
|
f"{TEST_ENTRY}_{TEST_ENDPOINT_ID}_{TEST_CONTAINER_NAME}"
|
|
)
|
|
|
|
|
|
async def test_services(
|
|
hass: HomeAssistant,
|
|
device_registry: DeviceRegistry,
|
|
mock_portainer_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Tests that the services are correct."""
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
device = device_registry.async_get_device(
|
|
identifiers={(DOMAIN, TEST_DEVICE_IDENTIFIER)}
|
|
)
|
|
assert device is not None
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_PRUNE_IMAGES,
|
|
{
|
|
ATTR_DEVICE_ID: device.id,
|
|
ATTR_DATE_UNTIL: timedelta(hours=24),
|
|
},
|
|
blocking=True,
|
|
)
|
|
mock_portainer_client.images_prune.assert_called_once_with(
|
|
endpoint_id=TEST_ENDPOINT_ID,
|
|
until=timedelta(hours=24),
|
|
dangling=False,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("call_arguments", "expected_until", "expected_dangling"),
|
|
[
|
|
({}, None, False),
|
|
({ATTR_DATE_UNTIL: timedelta(hours=12)}, timedelta(hours=12), False),
|
|
(
|
|
{ATTR_DATE_UNTIL: timedelta(hours=12), ATTR_DANGLING: True},
|
|
timedelta(hours=12),
|
|
True,
|
|
),
|
|
],
|
|
ids=["no optional", "with duration", "with duration and dangling"],
|
|
)
|
|
async def test_service_prune_images(
|
|
hass: HomeAssistant,
|
|
device_registry: DeviceRegistry,
|
|
mock_portainer_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
call_arguments: dict,
|
|
expected_until: timedelta | None,
|
|
expected_dangling: bool,
|
|
) -> None:
|
|
"""Test prune images service with the variants."""
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
device = device_registry.async_get_device(
|
|
identifiers={(DOMAIN, TEST_DEVICE_IDENTIFIER)}
|
|
)
|
|
assert device is not None
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_PRUNE_IMAGES,
|
|
{ATTR_DEVICE_ID: device.id, **call_arguments},
|
|
blocking=True,
|
|
)
|
|
mock_portainer_client.images_prune.assert_called_once_with(
|
|
endpoint_id=TEST_ENDPOINT_ID,
|
|
until=expected_until,
|
|
dangling=expected_dangling,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("call_arguments", "extra_expected_kwargs"),
|
|
[
|
|
({}, {"pull_image": False}),
|
|
(
|
|
{ATTR_TIMEOUT: timedelta(minutes=10)},
|
|
{"pull_image": False, "timeout": timedelta(minutes=10)},
|
|
),
|
|
(
|
|
{ATTR_TIMEOUT: timedelta(minutes=12), ATTR_PULL_IMAGE: True},
|
|
{"pull_image": True, "timeout": timedelta(minutes=12)},
|
|
),
|
|
],
|
|
ids=["no optional", "with duration", "with duration and pull_image"],
|
|
)
|
|
async def test_service_recreate_container(
|
|
hass: HomeAssistant,
|
|
device_registry: DeviceRegistry,
|
|
mock_portainer_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
call_arguments: dict,
|
|
extra_expected_kwargs: dict,
|
|
) -> None:
|
|
"""Test recreate container service with the variants."""
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
container = device_registry.async_get_device(
|
|
identifiers={(DOMAIN, TEST_CONTAINER_DEVICE_IDENTIFIER)}
|
|
)
|
|
assert container is not None
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_RECREATE_CONTAINER,
|
|
{
|
|
ATTR_CONTAINER_DEVICE_ID: container.id,
|
|
**call_arguments,
|
|
},
|
|
blocking=True,
|
|
)
|
|
mock_portainer_client.container_recreate.assert_called_once_with(
|
|
endpoint_id=TEST_ENDPOINT_ID,
|
|
container_id=TEST_CONTAINER_ID,
|
|
**extra_expected_kwargs,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("exception", "translation_key"),
|
|
[
|
|
(
|
|
PortainerAuthenticationError("auth"),
|
|
"invalid_auth_no_details",
|
|
),
|
|
(
|
|
PortainerConnectionError("conn"),
|
|
"cannot_connect_no_details",
|
|
),
|
|
(
|
|
PortainerTimeoutError("timeout"),
|
|
"timeout_connect_no_details",
|
|
),
|
|
],
|
|
)
|
|
async def test_service_recreate_container_portainer_exceptions(
|
|
hass: HomeAssistant,
|
|
device_registry: DeviceRegistry,
|
|
mock_portainer_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
exception: PortainerAuthenticationError
|
|
| PortainerConnectionError
|
|
| PortainerTimeoutError,
|
|
translation_key: str,
|
|
) -> None:
|
|
"""Test recreate container service handles Portainer exceptions."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
container = device_registry.async_get_device(
|
|
identifiers={(DOMAIN, TEST_CONTAINER_DEVICE_IDENTIFIER)}
|
|
)
|
|
assert container is not None
|
|
|
|
mock_portainer_client.container_recreate.side_effect = exception
|
|
with pytest.raises(HomeAssistantError) as err:
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_RECREATE_CONTAINER,
|
|
{ATTR_CONTAINER_DEVICE_ID: container.id},
|
|
blocking=True,
|
|
)
|
|
|
|
assert err.value.translation_key == translation_key
|
|
mock_portainer_client.container_recreate.assert_called_once()
|
|
|
|
|
|
async def test_service_validation_errors(
|
|
hass: HomeAssistant,
|
|
device_registry: DeviceRegistry,
|
|
mock_portainer_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Tests that the Portainer services handle bad data."""
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
device = device_registry.async_get_device(
|
|
identifiers={(DOMAIN, TEST_DEVICE_IDENTIFIER)}
|
|
)
|
|
assert device is not None
|
|
container = device_registry.async_get_device(
|
|
identifiers={(DOMAIN, TEST_CONTAINER_DEVICE_IDENTIFIER)}
|
|
)
|
|
assert container is not None
|
|
|
|
with pytest.raises(MultipleInvalid, match="required key not provided"):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_PRUNE_IMAGES,
|
|
{},
|
|
blocking=True,
|
|
)
|
|
mock_portainer_client.images_prune.assert_not_called()
|
|
|
|
with pytest.raises(MultipleInvalid, match="value must be at least"):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_PRUNE_IMAGES,
|
|
{ATTR_DEVICE_ID: device.id, ATTR_DATE_UNTIL: timedelta(seconds=30)},
|
|
blocking=True,
|
|
)
|
|
mock_portainer_client.images_prune.assert_not_called()
|
|
|
|
with pytest.raises(ServiceValidationError, match="Invalid device targeted"):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_PRUNE_IMAGES,
|
|
{ATTR_DEVICE_ID: "invalid_device_id"},
|
|
blocking=True,
|
|
)
|
|
mock_portainer_client.images_prune.assert_not_called()
|
|
|
|
with pytest.raises(ServiceValidationError, match="Invalid device targeted"):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_RECREATE_CONTAINER,
|
|
{ATTR_CONTAINER_DEVICE_ID: "invalid_device_id"},
|
|
blocking=True,
|
|
)
|
|
mock_portainer_client.container_recreate.assert_not_called()
|
|
|
|
other_entry = MockConfigEntry(domain="well_no_portainer_for_sure")
|
|
other_entry.add_to_hass(hass)
|
|
non_portainer_device = device_registry.async_get_or_create(
|
|
config_entry_id=other_entry.entry_id,
|
|
identifiers={("well_no_portainer_for_sure", "some_identifier")},
|
|
)
|
|
with pytest.raises(ServiceValidationError, match="Invalid device targeted"):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_RECREATE_CONTAINER,
|
|
{ATTR_CONTAINER_DEVICE_ID: non_portainer_device.id},
|
|
blocking=True,
|
|
)
|
|
mock_portainer_client.container_recreate.assert_not_called()
|
|
|
|
with pytest.raises(ServiceValidationError, match="Invalid device targeted"):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_RECREATE_CONTAINER,
|
|
{ATTR_CONTAINER_DEVICE_ID: device.id},
|
|
blocking=True,
|
|
)
|
|
mock_portainer_client.container_recreate.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("exception", "message"),
|
|
[
|
|
(
|
|
PortainerAuthenticationError("auth"),
|
|
"An error occurred while trying to authenticate",
|
|
),
|
|
(
|
|
PortainerConnectionError("conn"),
|
|
"An error occurred while trying to connect to the Portainer instance",
|
|
),
|
|
(
|
|
PortainerTimeoutError("timeout"),
|
|
"A timeout occurred while trying to connect to the Portainer instance",
|
|
),
|
|
],
|
|
)
|
|
async def test_service_portainer_exceptions(
|
|
hass: HomeAssistant,
|
|
device_registry: DeviceRegistry,
|
|
mock_portainer_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
exception: HomeAssistantError,
|
|
message: str,
|
|
) -> None:
|
|
"""Test service handles Portainer exceptions."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
device = device_registry.async_get_device(
|
|
identifiers={(DOMAIN, TEST_DEVICE_IDENTIFIER)}
|
|
)
|
|
|
|
mock_portainer_client.images_prune.side_effect = exception
|
|
with pytest.raises(HomeAssistantError, match=message):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_PRUNE_IMAGES,
|
|
{ATTR_DEVICE_ID: device.id},
|
|
blocking=True,
|
|
)
|
|
mock_portainer_client.images_prune.assert_called_once()
|