Improve exception handling in Pterodactyl (#141955)

Improve exception handling
This commit is contained in:
elmurato 2025-04-03 08:45:06 +02:00 committed by GitHub
parent 03c70e18df
commit 4a562b5085
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 61 additions and 75 deletions

View File

@ -5,20 +5,16 @@ from enum import StrEnum
import logging
from pydactyl import PterodactylClient
from pydactyl.exceptions import (
BadRequestError,
ClientConfigError,
PterodactylApiError,
PydactylError,
)
from pydactyl.exceptions import BadRequestError, PterodactylApiError
from requests.exceptions import ConnectionError, HTTPError
from homeassistant.core import HomeAssistant
_LOGGER = logging.getLogger(__name__)
class PterodactylConfigurationError(Exception):
"""Raised when the configuration is invalid."""
class PterodactylAuthorizationError(Exception):
"""Raised when access to server is unauthorized."""
class PterodactylConnectionError(Exception):
@ -75,13 +71,12 @@ class PterodactylAPI:
paginated_response = await self.hass.async_add_executor_job(
self.pterodactyl.client.servers.list_servers
)
except ClientConfigError as error:
raise PterodactylConfigurationError(error) from error
except (
PydactylError,
BadRequestError,
PterodactylApiError,
) as error:
except (BadRequestError, PterodactylApiError, ConnectionError) as error:
raise PterodactylConnectionError(error) from error
except HTTPError as error:
if error.response.status_code == 401:
raise PterodactylAuthorizationError(error) from error
raise PterodactylConnectionError(error) from error
else:
game_servers = paginated_response.collect()
@ -108,11 +103,12 @@ class PterodactylAPI:
server, utilization = await self.hass.async_add_executor_job(
self.get_server_data, identifier
)
except (
PydactylError,
BadRequestError,
PterodactylApiError,
) as error:
except (BadRequestError, PterodactylApiError, ConnectionError) as error:
raise PterodactylConnectionError(error) from error
except HTTPError as error:
if error.response.status_code == 401:
raise PterodactylAuthorizationError(error) from error
raise PterodactylConnectionError(error) from error
else:
data[identifier] = PterodactylData(
@ -145,9 +141,10 @@ class PterodactylAPI:
identifier,
command,
)
except (
PydactylError,
BadRequestError,
PterodactylApiError,
) as error:
except (BadRequestError, PterodactylApiError, ConnectionError) as error:
raise PterodactylConnectionError(error) from error
except HTTPError as error:
if error.response.status_code == 401:
raise PterodactylAuthorizationError(error) from error
raise PterodactylConnectionError(error) from error

View File

@ -9,7 +9,11 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .api import PterodactylCommand, PterodactylConnectionError
from .api import (
PterodactylAuthorizationError,
PterodactylCommand,
PterodactylConnectionError,
)
from .coordinator import PterodactylConfigEntry, PterodactylCoordinator
from .entity import PterodactylEntity
@ -94,5 +98,9 @@ class PterodactylButtonEntity(PterodactylEntity, ButtonEntity):
)
except PterodactylConnectionError as err:
raise HomeAssistantError(
f"Failed to send action '{self.entity_description.key}'"
f"Failed to send action '{self.entity_description.key}': Connection error"
) from err
except PterodactylAuthorizationError as err:
raise HomeAssistantError(
f"Failed to send action '{self.entity_description.key}': Unauthorized"
) from err

View File

@ -13,7 +13,7 @@ from homeassistant.const import CONF_API_KEY, CONF_URL
from .api import (
PterodactylAPI,
PterodactylConfigurationError,
PterodactylAuthorizationError,
PterodactylConnectionError,
)
from .const import DOMAIN
@ -49,7 +49,9 @@ class PterodactylConfigFlow(ConfigFlow, domain=DOMAIN):
try:
await api.async_init()
except (PterodactylConfigurationError, PterodactylConnectionError):
except PterodactylAuthorizationError:
errors["base"] = "invalid_auth"
except PterodactylConnectionError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception occurred during config flow")

View File

@ -12,7 +12,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .api import (
PterodactylAPI,
PterodactylConfigurationError,
PterodactylAuthorizationError,
PterodactylConnectionError,
PterodactylData,
)
@ -55,12 +55,12 @@ class PterodactylCoordinator(DataUpdateCoordinator[dict[str, PterodactylData]]):
try:
await self.api.async_init()
except PterodactylConfigurationError as error:
except (PterodactylAuthorizationError, PterodactylConnectionError) as error:
raise UpdateFailed(error) from error
async def _async_update_data(self) -> dict[str, PterodactylData]:
"""Get updated data from the Pterodactyl server."""
try:
return await self.api.async_get_data()
except PterodactylConnectionError as error:
except (PterodactylAuthorizationError, PterodactylConnectionError) as error:
raise UpdateFailed(error) from error

View File

@ -14,6 +14,7 @@
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {

View File

@ -1,8 +1,10 @@
"""Test the Pterodactyl config flow."""
from pydactyl import PterodactylClient
from pydactyl.exceptions import ClientConfigError, PterodactylApiError
from pydactyl.exceptions import BadRequestError, PterodactylApiError
import pytest
from requests.exceptions import HTTPError
from requests.models import Response
from homeassistant.components.pterodactyl.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
@ -14,6 +16,14 @@ from .conftest import TEST_URL, TEST_USER_INPUT
from tests.common import MockConfigEntry
def mock_response():
"""Mock HTTP response."""
mock = Response()
mock.status_code = 401
return mock
@pytest.mark.usefixtures("mock_pterodactyl", "mock_setup_entry")
async def test_full_flow(hass: HomeAssistant) -> None:
"""Test full flow without errors."""
@ -36,18 +46,21 @@ async def test_full_flow(hass: HomeAssistant) -> None:
@pytest.mark.usefixtures("mock_setup_entry")
@pytest.mark.parametrize(
"exception_type",
("exception_type", "expected_error"),
[
ClientConfigError,
PterodactylApiError,
(PterodactylApiError, "cannot_connect"),
(BadRequestError, "cannot_connect"),
(Exception, "unknown"),
(HTTPError(response=mock_response()), "invalid_auth"),
],
)
async def test_recovery_after_api_error(
async def test_recovery_after_error(
hass: HomeAssistant,
exception_type,
exception_type: Exception,
expected_error: str,
mock_pterodactyl: PterodactylClient,
) -> None:
"""Test recovery after an API error."""
"""Test recovery after an error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
@ -63,42 +76,7 @@ async def test_recovery_after_api_error(
await hass.async_block_till_done()
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
mock_pterodactyl.reset_mock(side_effect=True)
result = await hass.config_entries.flow.async_configure(
flow_id=result["flow_id"], user_input=TEST_USER_INPUT
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TEST_URL
assert result["data"] == TEST_USER_INPUT
@pytest.mark.usefixtures("mock_setup_entry")
async def test_recovery_after_unknown_error(
hass: HomeAssistant,
mock_pterodactyl: PterodactylClient,
) -> None:
"""Test recovery after an API error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
mock_pterodactyl.client.servers.list_servers.side_effect = Exception
result = await hass.config_entries.flow.async_configure(
flow_id=result["flow_id"],
user_input=TEST_USER_INPUT,
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "unknown"}
assert result["errors"] == {"base": expected_error}
mock_pterodactyl.reset_mock(side_effect=True)