From 35c8fefbd6885fd2061ff6554a962639a2882b77 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 4 Jul 2025 20:38:42 +0000 Subject: [PATCH] Fix AttributeError in radio_browser media source when runtime_data is missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The media_player.play_media action was failing with AttributeError when the radio_browser integration wasn't properly loaded. This happened when accessing the media source before the config entry was fully initialized. - Add proper error handling in RadioMediaSource.radios property - Raise Unresolvable exception when runtime_data is missing or None - Add comprehensive tests for the error handling scenario Fixes #141755 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../components/radio_browser/media_source.py | 2 + .../radio_browser/test_media_source.py | 139 ++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 tests/components/radio_browser/test_media_source.py diff --git a/homeassistant/components/radio_browser/media_source.py b/homeassistant/components/radio_browser/media_source.py index dc91525677b..4fe78646de1 100644 --- a/homeassistant/components/radio_browser/media_source.py +++ b/homeassistant/components/radio_browser/media_source.py @@ -50,6 +50,8 @@ class RadioMediaSource(MediaSource): @property def radios(self) -> RadioBrowser: """Return the radio browser.""" + if not hasattr(self.entry, "runtime_data") or self.entry.runtime_data is None: + raise Unresolvable("Radio Browser integration not properly loaded") return self.entry.runtime_data async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: diff --git a/tests/components/radio_browser/test_media_source.py b/tests/components/radio_browser/test_media_source.py new file mode 100644 index 00000000000..3ad9458dd2a --- /dev/null +++ b/tests/components/radio_browser/test_media_source.py @@ -0,0 +1,139 @@ +"""Test the Radio Browser media source.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +import pytest +from radios import RadioBrowser + +from homeassistant.components.media_source import MediaSourceItem, Unresolvable +from homeassistant.components.radio_browser.const import DOMAIN +from homeassistant.components.radio_browser.media_source import ( + RadioMediaSource, + async_get_media_source, +) +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_radio_browser() -> AsyncMock: + """Mock RadioBrowser.""" + radio_browser = AsyncMock(spec=RadioBrowser) + # Mock station without full Station object to avoid constructor complexity + mock_station = AsyncMock() + mock_station.uuid = "test-uuid" + mock_station.name = "Test Station" + mock_station.url = "https://example.com/stream" + mock_station.codec = "MP3" + mock_station.favicon = "https://example.com/favicon.ico" + radio_browser.station.return_value = mock_station + return radio_browser + + +async def test_media_source_without_runtime_data( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test media source raises error when runtime_data is missing.""" + mock_config_entry.add_to_hass(hass) + + # Don't set runtime_data to simulate the error condition + media_source = RadioMediaSource(hass, mock_config_entry) + + with pytest.raises( + Unresolvable, match="Radio Browser integration not properly loaded" + ): + _ = media_source.radios + + +async def test_media_source_with_none_runtime_data( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test media source raises error when runtime_data is None.""" + mock_config_entry.add_to_hass(hass) + mock_config_entry.runtime_data = None + + media_source = RadioMediaSource(hass, mock_config_entry) + + with pytest.raises( + Unresolvable, match="Radio Browser integration not properly loaded" + ): + _ = media_source.radios + + +async def test_media_source_with_runtime_data( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_radio_browser: AsyncMock, +) -> None: + """Test media source works correctly with runtime_data.""" + mock_config_entry.add_to_hass(hass) + mock_config_entry.runtime_data = mock_radio_browser + + media_source = RadioMediaSource(hass, mock_config_entry) + + # Should not raise an error + radios = media_source.radios + assert radios is mock_radio_browser + + +async def test_async_get_media_source( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_radio_browser: AsyncMock, +) -> None: + """Test async_get_media_source function.""" + mock_config_entry.add_to_hass(hass) + mock_config_entry.runtime_data = mock_radio_browser + + media_source = await async_get_media_source(hass) + + assert isinstance(media_source, RadioMediaSource) + assert media_source.entry is mock_config_entry + assert media_source.radios is mock_radio_browser + + +async def test_async_resolve_media_with_missing_runtime_data( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test async_resolve_media raises error when runtime_data is missing.""" + + mock_config_entry.add_to_hass(hass) + # Don't set runtime_data to simulate the error condition + + media_source = RadioMediaSource(hass, mock_config_entry) + item = MediaSourceItem( + hass=hass, + domain=DOMAIN, + identifier="test-uuid", + target_media_player=None, + ) + + with pytest.raises( + Unresolvable, match="Radio Browser integration not properly loaded" + ): + await media_source.async_resolve_media(item) + + +async def test_async_browse_media_with_missing_runtime_data( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test async_browse_media raises error when runtime_data is missing.""" + + mock_config_entry.add_to_hass(hass) + # Don't set runtime_data to simulate the error condition + + media_source = RadioMediaSource(hass, mock_config_entry) + item = MediaSourceItem( + hass=hass, + domain=DOMAIN, + identifier="", + target_media_player=None, + ) + + with pytest.raises( + Unresolvable, match="Radio Browser integration not properly loaded" + ): + await media_source.async_browse_media(item)