From fbab6741afce0b3814f2c91c2da2418f3e153563 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Wed, 21 May 2025 21:37:07 +0100 Subject: [PATCH] Update exception handling for initialization for Squeezebox (#144674) * initial * tests * translate exceptions * updates * tests updates * remove bare exception * merge fix --- .../components/squeezebox/__init__.py | 59 ++++++++++++++++-- .../components/squeezebox/strings.json | 12 ++++ tests/components/squeezebox/test_init.py | 61 +++++++++++++++++++ 3 files changed, 128 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index 2fcb17b9781..18acd74efd7 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -3,6 +3,7 @@ from asyncio import timeout from dataclasses import dataclass from datetime import datetime +from http import HTTPStatus import logging from pysqueezebox import Player, Server @@ -16,7 +17,11 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryError, + ConfigEntryNotReady, +) from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import ( @@ -93,15 +98,61 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) - status = await lms.async_query( "serverstatus", "-", "-", "prefs:libraryname" ) - except Exception as err: + except TimeoutError as err: # Specifically catch timeout + _LOGGER.warning("Timeout connecting to LMS %s: %s", host, err) raise ConfigEntryNotReady( - f"Error communicating config not read for {host}" + translation_domain=DOMAIN, + translation_key="init_timeout", + translation_placeholders={ + "host": str(host), + }, ) from err if not status: - raise ConfigEntryNotReady(f"Error Config Not read for {host}") + # pysqueezebox's async_query returns None on various issues, + # including HTTP errors where it sets lms.http_status. + http_status = getattr(lms, "http_status", "N/A") + + if http_status == HTTPStatus.UNAUTHORIZED: + _LOGGER.warning("Authentication failed for Squeezebox server %s", host) + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="init_auth_failed", + translation_placeholders={ + "host": str(host), + }, + ) + + # For other errors where status is None (e.g., server error, connection refused by server) + _LOGGER.warning( + "LMS %s returned no status or an error (HTTP status: %s). Retrying setup", + host, + http_status, + ) + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="init_get_status_failed", + translation_placeholders={ + "host": str(host), + "http_status": str(http_status), + }, + ) + + # If we are here, status is a valid dictionary _LOGGER.debug("LMS Status for setup = %s", status) + # Check for essential keys in status before using them + if STATUS_QUERY_UUID not in status: + _LOGGER.error("LMS %s status response missing UUID", host) + # This is a non-recoverable error with the current server response + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="init_missing_uuid", + translation_placeholders={ + "host": str(host), + }, + ) + lms.uuid = status[STATUS_QUERY_UUID] _LOGGER.debug("LMS %s = '%s' with uuid = %s ", lms.name, host, lms.uuid) lms.name = ( diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json index 6a4e30119a0..593d637e0db 100644 --- a/homeassistant/components/squeezebox/strings.json +++ b/homeassistant/components/squeezebox/strings.json @@ -158,6 +158,18 @@ } }, "exceptions": { + "init_timeout": { + "message": "Timeout connecting to LMS {host}." + }, + "init_auth_failed": { + "message": "Authentication failed for {host)." + }, + "init_get_status_failed": { + "message": "Failed to get status from LMS {host} (HTTP status: {http_status}). Will retry." + }, + "init_missing_uuid": { + "message": "LMS {host} status response missing essential data (UUID)." + }, "invalid_announce_media_type": { "message": "Only type 'music' can be played as announcement (received type {media_type})." }, diff --git a/tests/components/squeezebox/test_init.py b/tests/components/squeezebox/test_init.py index 9074f57cdcb..f70782b13da 100644 --- a/tests/components/squeezebox/test_init.py +++ b/tests/components/squeezebox/test_init.py @@ -1,7 +1,9 @@ """Test squeezebox initialization.""" +from http import HTTPStatus from unittest.mock import patch +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -21,3 +23,62 @@ async def test_init_api_fail( ), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) + + +async def test_init_timeout_error( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test init fail due to TimeoutError.""" + + # Setup component to raise TimeoutError + with ( + patch( + "homeassistant.components.squeezebox.Server.async_query", + side_effect=TimeoutError, + ), + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_init_unauthorized( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test init fail due to unauthorized error.""" + + # Setup component to simulate unauthorized response + with ( + patch( + "homeassistant.components.squeezebox.Server.async_query", + return_value=False, # async_query returns False on auth failure + ), + patch( + "homeassistant.components.squeezebox.Server", # Patch the Server class itself + autospec=True, + ) as mock_server_instance, + ): + mock_server_instance.return_value.http_status = HTTPStatus.UNAUTHORIZED + assert not await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_init_missing_uuid( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test init fail due to missing UUID in server status.""" + # A response that is truthy but does not contain STATUS_QUERY_UUID + mock_status_without_uuid = {"name": "Test Server"} + + with patch( + "homeassistant.components.squeezebox.Server.async_query", + return_value=mock_status_without_uuid, + ) as mock_async_query: + # ConfigEntryError is raised, caught by setup, and returns False + assert not await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.SETUP_ERROR + mock_async_query.assert_called_once_with( + "serverstatus", "-", "-", "prefs:libraryname" + )