Add Pterodactyl binary sensor tests (#142401)

* Add binary sensor tests

* Wait for background tasks as well in test_binary_sensor_update_failure

* Fix module docstring

* Use snapshot_platform, move constants to const.py, do not use snapshot for testing state updates

* Use JSON fixtures

* Use helper for loading JSON fixtures, remove unneeded mock in setup_integration

* Move mocks to pytest markers where possible
This commit is contained in:
elmurato 2025-05-18 16:46:11 +02:00 committed by GitHub
parent 705a987547
commit 3eb0c8ddff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 383 additions and 109 deletions

View File

@ -1 +1,16 @@
"""Tests for the Pterodactyl integration.""" """Tests for the Pterodactyl integration."""
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> MockConfigEntry:
"""Set up Pterodactyl mock config entry."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry

View File

@ -9,108 +9,9 @@ import pytest
from homeassistant.components.pterodactyl.const import DOMAIN from homeassistant.components.pterodactyl.const import DOMAIN
from homeassistant.const import CONF_API_KEY, CONF_URL from homeassistant.const import CONF_API_KEY, CONF_URL
from tests.common import MockConfigEntry from .const import TEST_API_KEY, TEST_URL
TEST_URL = "https://192.168.0.1:8080/" from tests.common import MockConfigEntry, load_json_object_fixture
TEST_API_KEY = "TestClientApiKey"
TEST_USER_INPUT = {
CONF_URL: TEST_URL,
CONF_API_KEY: TEST_API_KEY,
}
TEST_SERVER_LIST_DATA = {
"meta": {"pagination": {"total": 2, "count": 2, "per_page": 50, "current_page": 1}},
"data": [
{
"object": "server",
"attributes": {
"server_owner": True,
"identifier": "1",
"internal_id": 1,
"uuid": "1-1-1-1-1",
"name": "Test Server 1",
"node": "default_node",
"description": "Description of Test Server 1",
"limits": {
"memory": 2048,
"swap": 1024,
"disk": 10240,
"io": 500,
"cpu": 100,
"threads": None,
"oom_disabled": True,
},
"invocation": "java -jar test_server1.jar",
"docker_image": "test_docker_image_1",
"egg_features": ["java_version"],
},
},
{
"object": "server",
"attributes": {
"server_owner": True,
"identifier": "2",
"internal_id": 2,
"uuid": "2-2-2-2-2",
"name": "Test Server 2",
"node": "default_node",
"description": "Description of Test Server 2",
"limits": {
"memory": 2048,
"swap": 1024,
"disk": 10240,
"io": 500,
"cpu": 100,
"threads": None,
"oom_disabled": True,
},
"invocation": "java -jar test_server_2.jar",
"docker_image": "test_docker_image2",
"egg_features": ["java_version"],
},
},
],
}
TEST_SERVER = {
"server_owner": True,
"identifier": "1",
"internal_id": 1,
"uuid": "1-1-1-1-1",
"name": "Test Server 1",
"node": "default_node",
"is_node_under_maintenance": False,
"sftp_details": {"ip": "192.168.0.1", "port": 2022},
"description": "",
"limits": {
"memory": 2048,
"swap": 1024,
"disk": 10240,
"io": 500,
"cpu": 100,
"threads": None,
"oom_disabled": True,
},
"invocation": "java -jar test.jar",
"docker_image": "test_docker_image",
"egg_features": ["eula", "java_version", "pid_limit"],
"feature_limits": {"databases": 0, "allocations": 0, "backups": 3},
"status": None,
"is_suspended": False,
"is_installing": False,
"is_transferring": False,
"relationships": {"allocations": {...}, "variables": {...}},
}
TEST_SERVER_UTILIZATION = {
"current_state": "running",
"is_suspended": False,
"resources": {
"memory_bytes": 1111,
"cpu_absolute": 22,
"disk_bytes": 3333,
"network_rx_bytes": 44,
"network_tx_bytes": 55,
"uptime": 6666,
},
}
@pytest.fixture @pytest.fixture
@ -139,17 +40,25 @@ def mock_config_entry() -> MockConfigEntry:
@pytest.fixture @pytest.fixture
def mock_pterodactyl(): def mock_pterodactyl() -> Generator[AsyncMock]:
"""Mock the Pterodactyl API.""" """Mock the Pterodactyl API."""
with patch( with patch(
"homeassistant.components.pterodactyl.api.PterodactylClient", autospec=True "homeassistant.components.pterodactyl.api.PterodactylClient", autospec=True
) as mock: ) as mock:
server_list_data = load_json_object_fixture("server_list_data.json", DOMAIN)
server_1_data = load_json_object_fixture("server_1_data.json", DOMAIN)
server_2_data = load_json_object_fixture("server_2_data.json", DOMAIN)
utilization_data = load_json_object_fixture("utilization_data.json", DOMAIN)
mock.return_value.client.servers.list_servers.return_value = PaginatedResponse( mock.return_value.client.servers.list_servers.return_value = PaginatedResponse(
mock.return_value, "client", TEST_SERVER_LIST_DATA mock.return_value, "client", server_list_data
) )
mock.return_value.client.servers.get_server.return_value = TEST_SERVER mock.return_value.client.servers.get_server.side_effect = [
server_1_data,
server_2_data,
]
mock.return_value.client.servers.get_server_utilization.return_value = ( mock.return_value.client.servers.get_server_utilization.return_value = (
TEST_SERVER_UTILIZATION utilization_data
) )
yield mock.return_value yield mock.return_value

View File

@ -0,0 +1,12 @@
"""Constants for Pterodactyl tests."""
from homeassistant.const import CONF_API_KEY, CONF_URL
TEST_URL = "https://192.168.0.1:8080/"
TEST_API_KEY = "TestClientApiKey"
TEST_USER_INPUT = {
CONF_URL: TEST_URL,
CONF_API_KEY: TEST_API_KEY,
}

View File

@ -0,0 +1,39 @@
{
"server_owner": true,
"identifier": "1",
"internal_id": 1,
"uuid": "1-1-1-1-1",
"name": "Test Server 1",
"node": "default_node",
"is_node_under_maintenance": false,
"sftp_details": {
"ip": "192.168.0.1",
"port": 2022
},
"description": "",
"limits": {
"memory": 2048,
"swap": 1024,
"disk": 10240,
"io": 500,
"cpu": 100,
"threads": null,
"oom_disabled": true
},
"invocation": "java -jar test1.jar",
"docker_image": "test_docker_image1",
"egg_features": ["eula", "java_version", "pid_limit"],
"feature_limits": {
"databases": 0,
"allocations": 0,
"backups": 3
},
"status": null,
"is_suspended": false,
"is_installing": false,
"is_transferring": false,
"relationships": {
"allocations": {},
"variables": {}
}
}

View File

@ -0,0 +1,39 @@
{
"server_owner": true,
"identifier": "2",
"internal_id": 2,
"uuid": "2-2-2-2-2",
"name": "Test Server 2",
"node": "default_node",
"is_node_under_maintenance": false,
"sftp_details": {
"ip": "192.168.0.2",
"port": 2022
},
"description": "",
"limits": {
"memory": 4096,
"swap": 2048,
"disk": 20480,
"io": 1000,
"cpu": 200,
"threads": null,
"oom_disabled": true
},
"invocation": "java -jar test2.jar",
"docker_image": "test_docker_image2",
"egg_features": ["eula", "java_version", "pid_limit"],
"feature_limits": {
"databases": 1,
"allocations": 1,
"backups": 5
},
"status": null,
"is_suspended": false,
"is_installing": false,
"is_transferring": false,
"relationships": {
"allocations": {},
"variables": {}
}
}

View File

@ -0,0 +1,60 @@
{
"meta": {
"pagination": {
"total": 2,
"count": 2,
"per_page": 50,
"current_page": 1
}
},
"data": [
{
"object": "server",
"attributes": {
"server_owner": true,
"identifier": "1",
"internal_id": 1,
"uuid": "1-1-1-1-1",
"name": "Test Server 1",
"node": "default_node",
"description": "Description of Test Server 1",
"limits": {
"memory": 2048,
"swap": 1024,
"disk": 10240,
"io": 500,
"cpu": 100,
"threads": null,
"oom_disabled": true
},
"invocation": "java -jar test1.jar",
"docker_image": "test_docker_image_1",
"egg_features": ["java_version"]
}
},
{
"object": "server",
"attributes": {
"server_owner": true,
"identifier": "2",
"internal_id": 2,
"uuid": "2-2-2-2-2",
"name": "Test Server 2",
"node": "default_node",
"description": "Description of Test Server 2",
"limits": {
"memory": 2048,
"swap": 1024,
"disk": 10240,
"io": 500,
"cpu": 100,
"threads": null,
"oom_disabled": true
},
"invocation": "java -jar test2.jar",
"docker_image": "test_docker_image2",
"egg_features": ["java_version"]
}
}
]
}

View File

@ -0,0 +1,12 @@
{
"current_state": "running",
"is_suspended": false,
"resources": {
"memory_bytes": 1111,
"cpu_absolute": 22,
"disk_bytes": 3333,
"network_rx_bytes": 44,
"network_tx_bytes": 55,
"uptime": 6666
}
}

View File

@ -0,0 +1,97 @@
# serializer version: 1
# name: test_binary_sensor[binary_sensor.test_server_1_status-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.test_server_1_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.RUNNING: 'running'>,
'original_icon': None,
'original_name': 'Status',
'platform': 'pterodactyl',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'status',
'unique_id': '1-1-1-1-1_status',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor[binary_sensor.test_server_1_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'running',
'friendly_name': 'Test Server 1 Status',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.test_server_1_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_binary_sensor[binary_sensor.test_server_2_status-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.test_server_2_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.RUNNING: 'running'>,
'original_icon': None,
'original_name': 'Status',
'platform': 'pterodactyl',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'status',
'unique_id': '2-2-2-2-2_status',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor[binary_sensor.test_server_2_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'running',
'friendly_name': 'Test Server 2 Status',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.test_server_2_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@ -0,0 +1,89 @@
"""Tests for the binary sensor platform of the Pterodactyl integration."""
from collections.abc import Generator
from datetime import timedelta
from unittest.mock import AsyncMock, patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from requests.exceptions import ConnectionError
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@pytest.mark.usefixtures("mock_pterodactyl")
async def test_binary_sensor(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test binary sensor."""
with patch(
"homeassistant.components.pterodactyl._PLATFORMS", [Platform.BINARY_SENSOR]
):
mock_config_entry = await setup_integration(hass, mock_config_entry)
assert len(hass.states.async_all(Platform.BINARY_SENSOR)) == 2
await snapshot_platform(
hass, entity_registry, snapshot, mock_config_entry.entry_id
)
@pytest.mark.usefixtures("mock_pterodactyl")
async def test_binary_sensor_update(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test binary sensor update."""
await setup_integration(hass, mock_config_entry)
freezer.tick(timedelta(seconds=90))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert len(hass.states.async_all(Platform.BINARY_SENSOR)) == 2
assert (
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_1_status").state
== STATE_ON
)
assert (
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_2_status").state
== STATE_ON
)
async def test_binary_sensor_update_failure(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_pterodactyl: Generator[AsyncMock],
freezer: FrozenDateTimeFactory,
) -> None:
"""Test failed binary sensor update."""
await setup_integration(hass, mock_config_entry)
mock_pterodactyl.client.servers.get_server.side_effect = ConnectionError(
"Simulated connection error"
)
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert len(hass.states.async_all(Platform.BINARY_SENSOR)) == 2
assert (
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_1_status").state
== STATE_UNAVAILABLE
)
assert (
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_2_status").state
== STATE_UNAVAILABLE
)

View File

@ -1,6 +1,8 @@
"""Test the Pterodactyl config flow.""" """Test the Pterodactyl config flow."""
from pydactyl import PterodactylClient from collections.abc import Generator
from unittest.mock import AsyncMock
from pydactyl.exceptions import BadRequestError, PterodactylApiError from pydactyl.exceptions import BadRequestError, PterodactylApiError
import pytest import pytest
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
@ -12,7 +14,7 @@ from homeassistant.const import CONF_API_KEY, CONF_URL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from .conftest import TEST_API_KEY, TEST_URL, TEST_USER_INPUT from .const import TEST_API_KEY, TEST_URL, TEST_USER_INPUT
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -59,7 +61,7 @@ async def test_recovery_after_error(
hass: HomeAssistant, hass: HomeAssistant,
exception_type: Exception, exception_type: Exception,
expected_error: str, expected_error: str,
mock_pterodactyl: PterodactylClient, mock_pterodactyl: Generator[AsyncMock],
) -> None: ) -> None:
"""Test recovery after an error.""" """Test recovery after an error."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -143,7 +145,7 @@ async def test_reauth_recovery_after_error(
exception_type: Exception, exception_type: Exception,
expected_error: str, expected_error: str,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
mock_pterodactyl: PterodactylClient, mock_pterodactyl: Generator[AsyncMock],
) -> None: ) -> None:
"""Test recovery after an error during re-authentication.""" """Test recovery after an error during re-authentication."""
mock_config_entry.add_to_hass(hass) mock_config_entry.add_to_hass(hass)